Skip to content
Draft
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
16 changes: 16 additions & 0 deletions .github/actions/collect-test-diagnostics/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ inputs:
Name of the uploaded artifact. Artifact names are unique per workflow
run, so jobs that can both fail in one run need distinct names.
default: test-diagnostics
host-process-log:
description: >
"true" when the app ran on the host (Mac Catalyst): also save the host's
unified log for TestRunner processes.
default: "false"
runs:
using: composite
steps:
Expand Down Expand Up @@ -45,6 +50,17 @@ runs:
else
echo "No booted simulator; relying on the xcresult's captured app output."
fi
if [ "${{ inputs.host-process-log }}" = "true" ]; then
echo "Collecting host unified log slice"
log show --style compact --last 45m \
--predicate 'process CONTAINS "TestRunner"' \
> "$DIAG/host-unified.log" 2>/dev/null &
LOG_PID=$!
( sleep 120 && kill -9 "$LOG_PID" ) 2>/dev/null &
KILLER_PID=$!
wait "$LOG_PID" || echo "log show failed or was killed after 120s"
kill "$KILLER_PID" 2>/dev/null || true
fi
echo "Collected diagnostics:"
du -sh "$DIAG"/* 2>/dev/null || true
- name: Upload test diagnostics
Expand Down
57 changes: 57 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,60 @@ jobs:
path: |
${{env.TEST_FOLDER}}/test_results.xcresult
${{env.TEST_FOLDER}}/test_results_attempt1.xcresult
test-catalyst:
name: Test (Mac Catalyst)
runs-on: macos-15
needs: build
steps:
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: ${{env.XCODE_VERSION}}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: true
persist-credentials: false # disable to prevent leaking credentials to build scripts
- name: LLVM cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ./llvm
key: ${{ hashFiles('download_llvm.sh') }}
- uses: ./.github/actions/setup-build-env
with:
install-test-tools: "true"
- name: Prepare test folder
run: echo TEST_FOLDER=$(pwd)/build/tests/catalyst >> $GITHUB_ENV
- name: Xcode Tests
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 40
max_attempts: 2
command: ./run_tests.sh --catalyst
# run_tests.sh empties its results folder, so move the failed
# attempt's bundle out of it first.
on_retry_command: rm -rf $TEST_FOLDER-attempt1; mv $TEST_FOLDER $TEST_FOLDER-attempt1 2>/dev/null || true
- name: Test report summary
if: ${{ !cancelled() }}
uses: mikepenz/action-junit-report@a9170d5795813c01ab4901ffb045b52bab4ab09d # v6.5.0
with:
report_paths: build/tests/catalyst/test-out/*junit*.xml
annotate_only: true
detailed_summary: true
- name: Validate Test Results
run: |
find $TEST_FOLDER/test-out -name "*junit*.xml" -maxdepth 1 -print0 | xargs -n 1 -0 npx junit-cli-report-viewer
find $TEST_FOLDER/test-out -name "*junit*.xml" -maxdepth 1 -print0 | xargs -n 1 -0 npx verify-junit-xml
- name: Collect test diagnostics
if: failure()
uses: ./.github/actions/collect-test-diagnostics
with:
test-folder: ${{env.TEST_FOLDER}}
artifact-name: test-diagnostics-catalyst
host-process-log: "true"
- name: Archive Test Result Data
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-results-catalyst
path: |
${{env.TEST_FOLDER}}/test_results.xcresult
${{env.TEST_FOLDER}}-attempt1/test_results.xcresult
Binary file added NativeScript/lib/arm64-maccatalyst/libffi.a
Binary file not shown.
51 changes: 48 additions & 3 deletions NativeScript/runtime/Runtime.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@

#include "IsolateWrapper.h"

#include <TargetConditionals.h>
#include <mutex>
#include <unordered_map>
#include <vector>
#if TARGET_OS_MACCATALYST
#include <sys/mman.h>
#include <unistd.h>
#endif
#include "HttpLoader.h"
#include "ModuleBinding.hpp"
#include "ModuleInternalCallbacks.h"
Expand Down Expand Up @@ -328,17 +333,57 @@ void DisposeIsolateWhenPossible(Isolate* isolate, int gateId) {
return static_cast<Runtime*>(isolate->GetData(Constants::RUNTIME_SLOT));
}

#if TARGET_OS_MACCATALYST
// Mac Catalyst ships V8 with its JIT tiers, but a hardened-runtime process may
// only map JIT memory with com.apple.security.cs.allow-jit (or
// allow-unsigned-executable-memory). V8 does not fall back on its own: without
// --jitless it dies at its first code-space allocation, so ask the kernel the
// same question V8 will.
static bool CanMapJitMemory() {
const size_t size = static_cast<size_t>(getpagesize());
void* memory = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON | MAP_JIT, -1, 0);
if (memory == MAP_FAILED) {
return false;
}
munmap(memory, size);
return true;
}

// `"ios": { "jit": false }` in the app's package.json forces the interpreter;
// otherwise the JIT runs whenever the process may map JIT memory.
static bool ShouldEnableJit() {
id iosConfig = Runtime::GetAppConfigValue("ios");
id jitValue = [iosConfig isKindOfClass:[NSDictionary class]] ? iosConfig[@"jit"] : nil;
if ([jitValue isKindOfClass:[NSNumber class]] && ![jitValue boolValue]) {
return false;
}
if (CanMapJitMemory()) {
return true;
}
if (jitValue != nil) {
Log(@"NativeScript: \"ios.jit\" is set, but this process may not map JIT memory (missing "
@"the com.apple.security.cs.allow-jit entitlement?). Running without the JIT.");
}
return false;
}
#endif

Isolate* Runtime::CreateIsolate(const IsolateLimits& limits) {
if (!v8Initialized_) {
// Runtime::platform_ = RuntimeConfig.IsDebug
// ? v8_inspector::V8InspectorPlatform::CreateDefaultPlatform()
// : platform::NewDefaultPlatform();

// Flags must be set before V8::Initialize(), which freezes them; changing a
// flag afterwards aborts the process. Don't pass --jitless either:
// v8_enable_lite_mode already implies it and makes the flag read-only,
// which is likewise fatal to set.
// flag afterwards aborts the process. iOS V8 is built in lite mode, which
// already implies --jitless; only Mac Catalyst decides at launch.
std::string flags = RuntimeConfig.IsDebug ? "--expose_gc" : "--expose_gc --no-lazy";
#if TARGET_OS_MACCATALYST
if (!ShouldEnableJit()) {
flags += " --jitless";
}
#endif
V8::SetFlagsFromString(flags.c_str(), flags.size());

// wrap the default platform so foreground tasks ride each runtime
Expand Down
4 changes: 4 additions & 0 deletions TestFixtures/Functions/TNSFunctions.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ CFArrayRef CFArrayCreateWithString(CFStringRef string);
// Declared for the metadata generator but intentionally left undefined, so the
// symbol is listed in metadata yet absent at runtime.
void TNSFunctionMissingAtRuntime(void);

// Whether the process holds executable memory carrying V8's VM tag (255).
// V8 maps executable memory only when its JIT is on.
bool TNSHasExecutableV8Memory(void);
19 changes: 19 additions & 0 deletions TestFixtures/Functions/TNSFunctions.m
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
#import "TNSFunctions.h"
#include <mach/mach.h>

bool TNSHasExecutableV8Memory(void) {
vm_address_t address = 0;
for (;;) {
vm_size_t size = 0;
natural_t depth = 0;
struct vm_region_submap_info_64 info;
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
if (vm_region_recurse_64(mach_task_self(), &address, &size, &depth,
(vm_region_recurse_info_t)&info, &count) != KERN_SUCCESS) {
return false;
}
if (info.user_tag == 255 && (info.protection & VM_PROT_EXECUTE)) {
return true;
}
address += size;
}
}

CFArrayRef CFArrayCreateWithString(CFStringRef string) {
const void* values[] = { string };
Expand Down
4 changes: 2 additions & 2 deletions TestFixtures/NapiCoverageModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ static napi_value StringStatus(napi_env env, napi_callback_info info) {

//=== Numbers ==============================================================

static napi_value NumberParts(napi_env env, napi_callback_info info) {
static napi_value NumberPartsMethod(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
Expand Down Expand Up @@ -1345,7 +1345,7 @@ static napi_value InitNapiCoverageModule(napi_env env, napi_value exports) {
NAPI_METHOD("copyString", CopyString),
NAPI_METHOD("createStrings", CreateStrings),
NAPI_METHOD("stringStatus", StringStatus),
NAPI_METHOD("numberParts", NumberParts),
NAPI_METHOD("numberParts", NumberPartsMethod),
NAPI_METHOD("createNumbers", CreateNumbers),
NAPI_METHOD("numberStatus", NumberStatus),
NAPI_METHOD("createSymbol", CreateSymbol),
Expand Down
1 change: 1 addition & 0 deletions TestFixtures/exported-symbols.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
_CFArrayCreateWithString
_TNSHasExecutableV8Memory
_functionExplicitCreateNSObject
_functionImplicitCreate
_functionReturningFunctionPtrAsVoidPtr
Expand Down
12 changes: 12 additions & 0 deletions TestRunner/TestRunner-Catalyst.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Ad-hoc signatures carry no Team ID, so hardened-runtime library
validation would refuse the embedded frameworks. -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
9 changes: 9 additions & 0 deletions TestRunner/app/tests/CatalystJitTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// TestRunner's Catalyst build is signed with com.apple.security.cs.allow-jit,
// so the runtime must leave V8's JIT on there.
if (NSProcessInfo.processInfo.macCatalystApp) {
describe("Mac Catalyst JIT", function () {
it("runs V8 with its JIT when the process may map JIT memory", function () {
expect(TNSHasExecutableV8Memory()).toBe(true);
});
});
}
3 changes: 3 additions & 0 deletions TestRunner/app/tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ require("./WorkerTerminateTests");
// interop.setExternalSize and the runtime's own external memory charges
require("./ExternalMemoryTests");

// The runtime's launch-time JIT decision on Mac Catalyst
require("./CatalystJitTests");

// Tests common for all runtimes (git submodule of NativeScript/common-runtime-tests-app).
require("../shared/index").runAllTests();

Expand Down
6 changes: 6 additions & 0 deletions build_libffi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ ALL_SLICES=(
arm64-xros
arm64-xrsimulator
x86_64-iphonesimulator
arm64-maccatalyst
x86_64-maccatalyst
)

IOS_MIN=13.0
XROS_MIN=2.0
CATALYST_MIN=13.3
# The arm64 macabi triple has no deployment targets below 14.0.
CATALYST_ARM64_MIN=14.0

usage() {
cat <<EOF
Expand Down Expand Up @@ -107,6 +110,9 @@ slice_config() {
x86_64-iphonesimulator)
echo "iphonesimulator|x86_64-apple-darwin|-arch x86_64 -mios-simulator-version-min=$IOS_MIN"
;;
arm64-maccatalyst)
echo "macosx|aarch64-apple-darwin|-target arm64-apple-ios$CATALYST_ARM64_MIN-macabi"
;;
x86_64-maccatalyst)
echo "macosx|x86_64-apple-darwin|-target x86_64-apple-ios$CATALYST_MIN-macabi"
;;
Expand Down
6 changes: 4 additions & 2 deletions metadata-generator/build-step-metadata-generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ def map_and_list(func, iterable):
src_root = env("SRCROOT")
deployment_target_flag_name = env_or_empty("DEPLOYMENT_TARGET_CLANG_FLAG_NAME") or default_deployment_target_flag_name
# Xcode does not export MACCATALYST_DEPLOYMENT_TARGET unless the project sets it,
# so fall back to the iOS deployment target rather than dying with a KeyError.
# and may not export IPHONEOS_DEPLOYMENT_TARGET to Catalyst script phases either;
# LLVM_TARGET_TRIPLE_OS_VERSION (e.g. "ios13.1") always carries the iOS version.
deployment_target = env_or_none(env_or_empty("DEPLOYMENT_TARGET_CLANG_ENV_NAME") or default_deployment_target_clang_env_name) \
or env("IPHONEOS_DEPLOYMENT_TARGET")
or env_or_none("IPHONEOS_DEPLOYMENT_TARGET") \
or env_or_empty("LLVM_TARGET_TRIPLE_OS_VERSION").lstrip("abcdefghijklmnopqrstuvwxyz")
std = env("GCC_C_LANGUAGE_STANDARD")
header_search_paths = env_or_empty("HEADER_SEARCH_PATHS")
header_search_paths_parsed = map_and_list((lambda s: "-I" + s), shlex.split(header_search_paths))
Expand Down
Loading
Loading