Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ thumbs.db

.classpath
android-runtime.iml

# Emitted by tools/js2c.mjs from test-app/runtime/src/main/cpp/js during the build.
test-app/runtime/src/main/cpp/generated/

test-app/build-tools/*.log
test-app/analytics/build-statistics.json
package-lock.json
Expand Down
38 changes: 38 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Lint setup for the runtime's builtin JavaScript
// (test-app/runtime/src/main/cpp/js). Each file is compiled by BuiltinLoader
// as a FUNCTION BODY with the fixed parameters `exports`, `module` and
// `binding` (see that directory's README.md), which are declared as globals
// here. no-undef is the typo net for binding-bag destructures and
// native-global usage alike.
import globals from 'globals';

export default [
{
files: ['test-app/runtime/src/main/cpp/js/**/*.js'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'script',
globals: {
...globals.es2021,
exports: 'readonly',
module: 'readonly',
binding: 'readonly',
global: 'readonly',
console: 'readonly',
URL: 'readonly',
URLSearchParams: 'readonly',
Blob: 'readonly',
File: 'readonly',
WebAssembly: 'readonly',
// Java package roots resolved through the metadata interceptor at
// runtime:
java: 'readonly',
org: 'readonly',
},
},
rules: {
'no-undef': 'error',
'no-unused-vars': ['error', { args: 'none', caughtErrors: 'none' }],
},
},
];
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@
},
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"lint": "eslint test-app/runtime/src/main/cpp/js",
"version": "npm run changelog && git add CHANGELOG.md"
},
"devDependencies": {
"conventional-changelog-cli": "^2.1.1",
"dayjs": "^1.11.7",
"eslint": "^9.15.0",
"globals": "^15.12.0",
"semver": "^7.5.0"
}
}
38 changes: 38 additions & 0 deletions test-app/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,41 @@ include_directories(
src/main/cpp/ada
)

# The runtime's builtin JavaScript (src/main/cpp/js) embedded into a generated
# C++ table by tools/js2c.mjs. The list is explicit rather than globbed so that
# adding a file is a visible build change; --check-dir fails the build when it
# drifts from the directory contents.
set(RUNTIME_BUILTIN_JS_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp/js)
set(RUNTIME_BUILTIN_JS
${RUNTIME_BUILTIN_JS_DIR}/blob-url.js
${RUNTIME_BUILTIN_JS_DIR}/error-events.js
${RUNTIME_BUILTIN_JS_DIR}/events.js
${RUNTIME_BUILTIN_JS_DIR}/json-helper.js
${RUNTIME_BUILTIN_JS_DIR}/message-loop-timer.js
${RUNTIME_BUILTIN_JS_DIR}/require-factory.js
${RUNTIME_BUILTIN_JS_DIR}/smart-stringify.js
${RUNTIME_BUILTIN_JS_DIR}/weak-ref.js
)
set(RUNTIME_BUILTINS_GENERATED_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp/generated)
get_filename_component(RUNTIME_BUILTINS_JS2C ${PROJECT_SOURCE_DIR}/../../tools/js2c.mjs ABSOLUTE)

find_program(NODE_EXECUTABLE NAMES node nodejs)
if (NOT NODE_EXECUTABLE)
message(FATAL_ERROR "node was not found on PATH; it is required to generate RuntimeBuiltins")
endif ()

add_custom_command(
OUTPUT ${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.h
${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.cpp
COMMAND ${NODE_EXECUTABLE} ${RUNTIME_BUILTINS_JS2C}
--out-dir ${RUNTIME_BUILTINS_GENERATED_DIR}
--check-dir ${RUNTIME_BUILTIN_JS_DIR}
${RUNTIME_BUILTIN_JS}
DEPENDS ${RUNTIME_BUILTIN_JS} ${RUNTIME_BUILTINS_JS2C}
Comment on lines +84 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Detect directory drift before incremental builds.

--check-dir runs only after CMake schedules this custom command. Adding an unlisted .js file does not change an OUTPUT or a listed DEPENDS entry, so an incremental build can skip the command and silently omit the builtin.

Use a configure-time file(GLOB ... CONFIGURE_DEPENDS) check against RUNTIME_BUILTIN_JS, or add an always-run validation target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-app/runtime/CMakeLists.txt` around lines 84 - 91, Update the CMake
configuration around the custom command using RUNTIME_BUILTIN_JS so additions or
removals of builtin JavaScript files trigger reconfiguration or validation
before incremental builds. Prefer a configure-time file(GLOB ...
CONFIGURE_DEPENDS) check that compares the discovered files with
RUNTIME_BUILTIN_JS, while preserving the existing generation command and
dependency behavior.

COMMENT "Generating RuntimeBuiltins from src/main/cpp/js"
VERBATIM
)
Comment on lines +84 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 'RuntimeBuiltins\.h|GetBuiltinSource' \
  test-app/runtime/src/main/cpp/BuiltinLoader.cpp

rg -n -C 8 'add_custom_command|add_custom_target|add_dependencies|RuntimeBuiltins' \
  test-app/runtime/CMakeLists.txt

Repository: NativeScript/android

Length of output: 2688


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CMakeLists relevant sections =="
sed -n '76,110p;200,212p' test-app/runtime/CMakeLists.txt

echo
echo "== BuiltinLoader includes and GetBuiltinSource body =="
sed -n '1,70p;90,130p' test-app/runtime/src/main/cpp/BuiltinLoader.cpp

echo
echo "== Library directive with source order context =="
sed -n '170,208p' test-app/runtime/CMakeLists.txt

Repository: NativeScript/android

Length of output: 7860


🌐 Web query:

CMake add_custom_command OUTPUT multiple files attaches rule only to first output

💡 Result:

In CMake, the add_custom_command function technically supports multiple files listed under the OUTPUT keyword. However, historical and practical usage patterns indicate that relying on multiple outputs within a single add_custom_command can lead to inconsistent behavior in how build tools (like Make or Ninja) track dependencies [1]. While the documentation notes that you can list multiple outputs to indicate a command generates several files [2], build systems often struggle to track which specific output was generated or whether the rule needs to run if only one of the outputs is missing. In older versions of CMake, this caused specific issues where the build system treated the first output differently or failed to correctly track dependencies for subsequent outputs [1]. To ensure robust and reliable build behavior, the recommended approach is as follows: 1. Use add_custom_command for each unique output file individually, if the commands to generate them are different [2]. 2. If a single command generates multiple files simultaneously, define the primary output in the add_custom_command and use the BYPRODUCTS option (available in modern CMake) to list the additional files produced by that command. This informs the build system that these files are generated by the same rule [3]. 3. If you need to ensure that multiple outputs are generated together and are required by different, independent targets, wrap the generation command in an add_custom_target. This forces the generation to happen as a single atomic unit before any dependent targets attempt to use the files, preventing race conditions or incomplete builds [4][5][6]. By using add_custom_target to drive the generation, you decouple the execution of the command from the individual file dependencies, ensuring that all necessary outputs are created before any part of the build process relies on them [4][7].

Citations:


🌐 Web query:

CMake add_custom_command add_library generated header multiple source files dependency header included in other source

💡 Result:

To manage a generated header file that is used by multiple source files or targets in CMake, you must ensure that the generation command runs before any consuming files are compiled [1][2]. The recommended approach depends on whether the generated header is internal to a single target or shared across multiple independent targets. ### Scenario 1: Internal to a Single Target If the generated header is used only by source files within the same library or executable, simply adding the generated file to the target's source list is often sufficient [3][2]. CMake's dependency scanner will typically recognize that the source files include the generated header and trigger the custom command automatically [4][2]. 1. Use add_custom_command with an OUTPUT parameter pointing to the full path of the generated header (usually in CMAKE_CURRENT_BINARY_DIR) [1][5]. 2. Add the generated file directly to the add_library or add_executable call (or use target_sources) [3][6]. 3. Ensure the directory containing the header is added to your target's include directories using target_include_directories [4][7]. ### Scenario 2: Shared Across Multiple Independent Targets If multiple independent targets need the same generated header, you must avoid race conditions where multiple targets attempt to run the same command in parallel [1]. 1. Wrap the generation command in an add_custom_target to act as a central "build step" [1][3]. cmake add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.h COMMAND my_generator -o ${CMAKE_CURRENT_BINARY_DIR}/generated.h DEPENDS input.in ) add_custom_target(generate_headers DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated.h) 2. Create an INTERFACE library to propagate the dependency and include directories to other targets [3][7]. cmake add_library(generated_header_lib INTERFACE) target_include_directories(generated_header_lib INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) add_dependencies(generated_header_lib generate_headers) 3. Link this interface library to any target that needs the header [8][7]: cmake target_link_libraries(my_app PRIVATE generated_header_lib) By linking to the interface library, you transitively force the dependent targets to wait for the generation target (generate_headers) to complete before they begin their own build [3][9]. Using FILE_SET HEADERS (introduced in CMake 3.23) is the modern, preferred way to manage generated headers within these interface libraries [3][6].

Citations:


Make RuntimeBuiltins.h a target-level build dependency.

The current command attaches only to RuntimeBuiltins.cpp, while BuiltinLoader.cpp includes the generated header directly. A parallel build can compile that source before RuntimeBuiltins.h exists, causing NativeScript to fail on clean builds. Add a custom target for both generated files and attach it to NativeScript with add_dependencies().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-app/runtime/CMakeLists.txt` around lines 84 - 94, Update the generated
RuntimeBuiltins outputs around add_custom_command so both RuntimeBuiltins.h and
RuntimeBuiltins.cpp are produced by a named custom target, then attach that
target to NativeScript with add_dependencies(). Preserve the existing generation
command and ensure BuiltinLoader.cpp cannot compile until the generated header
target completes.


# This branch also produces runtime-regular-release.aar, shipped as
# nativescript-regular.aar and selected for apps that set useV8Symbols, so it
# must carry the release flags. Only a local Debug build keeps plain -g.
Expand Down Expand Up @@ -106,6 +141,7 @@ add_library(
src/main/cpp/ArrayElementAccessor.cpp
src/main/cpp/ArrayHelper.cpp
src/main/cpp/AssetExtractor.cpp
src/main/cpp/BuiltinLoader.cpp
src/main/cpp/CallbackHandlers.cpp
src/main/cpp/ConcurrentQueue.cpp
src/main/cpp/Constants.cpp
Expand Down Expand Up @@ -165,6 +201,8 @@ add_library(
src/main/cpp/HMRSupport.cpp
src/main/cpp/DevFlags.cpp

${RUNTIME_BUILTINS_GENERATED_DIR}/RuntimeBuiltins.cpp

# V8 inspector source files will be included only in Release mode
${INSPECTOR_SOURCES}
)
Expand Down
116 changes: 116 additions & 0 deletions test-app/runtime/src/main/cpp/BuiltinLoader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#include "BuiltinLoader.h"

#include <mutex>
#include <vector>

#include "ArgConverter.h"

using namespace v8;

namespace tns {

namespace {

/*
* Process-wide bytecode cache shared across isolates. Worker runtimes
* initialize on their own threads, so every access is under the mutex.
*/
std::mutex builtinCacheMutex;
std::vector<uint8_t> builtinCache[static_cast<unsigned>(BuiltinId::kCount)];

/*
* Every builtin is compiled as a function body receiving these fixed
* parameters, mirroring Node's module wrapper: a file exports through
* `module.exports`/`exports`, and natives arrive as properties of the
* `binding` bag (Node's internalBinding idiom) for each file to destructure.
*/
constexpr const char* kExportsParamName = "exports";
constexpr const char* kModuleParamName = "module";
constexpr const char* kBindingParamName = "binding";
constexpr size_t kParamCount = 3;

MaybeLocal<v8::Function> CompileBuiltin(Local<Context> context, BuiltinId id) {
Isolate* isolate = v8::Isolate::GetCurrent();
const BuiltinSource& builtin = GetBuiltinSource(id);
const unsigned index = static_cast<unsigned>(id);

// Copy the blob out so the shared slot can be refreshed concurrently while
// this compile still reads from the copy.
std::vector<uint8_t> blob;
{
std::lock_guard<std::mutex> lock(builtinCacheMutex);
blob = builtinCache[index];
}

ScriptOrigin origin(ArgConverter::ConvertToV8String(isolate, builtin.name));
Local<v8::String> sourceText = ArgConverter::ConvertToV8String(
isolate, builtin.source, static_cast<int>(builtin.length));
Local<v8::String> params[] = {
ArgConverter::ConvertToV8String(isolate, kExportsParamName),
ArgConverter::ConvertToV8String(isolate, kModuleParamName),
ArgConverter::ConvertToV8String(isolate, kBindingParamName)};

Local<v8::Function> fn;
if (!blob.empty()) {
// The Source owns and deletes the CachedData object; BufferNotOwned
// keeps the underlying bytes (our copy) out of its hands.
auto* cachedData = new ScriptCompiler::CachedData(
blob.data(), static_cast<int>(blob.size()),
ScriptCompiler::CachedData::BufferNotOwned);
ScriptCompiler::Source source(sourceText, origin, cachedData);
if (ScriptCompiler::CompileFunction(context, &source, kParamCount, params, 0, nullptr,
ScriptCompiler::kConsumeCodeCache)
.ToLocal(&fn) &&
!cachedData->rejected) {
return fn;
}
// Rejected cache (e.g. produced under different flags): fall through
// and recompile eagerly so the refreshed blob covers inner functions
// again.
}

ScriptCompiler::Source source(sourceText, origin);
if (!ScriptCompiler::CompileFunction(context, &source, kParamCount, params, 0, nullptr,
ScriptCompiler::kEagerCompile)
.ToLocal(&fn)) {
return MaybeLocal<v8::Function>();
}

std::unique_ptr<ScriptCompiler::CachedData> produced(
ScriptCompiler::CreateCodeCacheForFunction(fn));
if (produced != nullptr && produced->data != nullptr && produced->length > 0) {
std::lock_guard<std::mutex> lock(builtinCacheMutex);
builtinCache[index].assign(produced->data, produced->data + produced->length);
}

return fn;
}

} // namespace

MaybeLocal<Value> BuiltinLoader::RunBuiltin(Local<Context> context, BuiltinId id,
Local<Value> binding) {
Isolate* isolate = v8::Isolate::GetCurrent();

Local<v8::Function> fn;
if (!CompileBuiltin(context, id).ToLocal(&fn)) {
return MaybeLocal<Value>();
}

Local<Object> exportsObj = Object::New(isolate);
Local<Object> moduleObj = Object::New(isolate);
Local<v8::String> exportsKey = ArgConverter::ConvertToV8String(isolate, kExportsParamName);
if (!moduleObj->Set(context, exportsKey, exportsObj).FromMaybe(false)) {
return MaybeLocal<Value>();
}

Local<Value> args[] = {exportsObj, moduleObj,
binding.IsEmpty() ? Undefined(isolate).As<Value>() : binding};
if (fn->Call(context, Undefined(isolate), static_cast<int>(kParamCount), args).IsEmpty()) {
return MaybeLocal<Value>();
}

return moduleObj->Get(context, exportsKey);
}

} // namespace tns
29 changes: 29 additions & 0 deletions test-app/runtime/src/main/cpp/BuiltinLoader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#ifndef BUILTINLOADER_H_
#define BUILTINLOADER_H_

#include "generated/RuntimeBuiltins.h"
#include "v8.h"

namespace tns {

class BuiltinLoader {
public:
/*
* Compiles the builtin identified by id as a function body with the fixed
* parameters `exports`, `module` and `binding` (Node's module wrapper plus
* its internalBinding idiom), calls it with the given bag of natives (or
* undefined when omitted), and returns the resulting `module.exports`.
* Scripts carry an "internal/<name>.js" origin so runtime frames are
* identifiable in stack traces. Compilation goes through a process-wide
* bytecode cache: the first run in the process compiles eagerly and
* populates the cache, later isolates (workers, which run on their own
* threads) consume it instead of re-parsing the source.
*/
static v8::MaybeLocal<v8::Value> RunBuiltin(
v8::Local<v8::Context> context, BuiltinId id,
v8::Local<v8::Value> binding = v8::Local<v8::Value>());
};

} // namespace tns

#endif /* BUILTINLOADER_H_ */
Loading