Skip to content

Commit ee77abb

Browse files
authored
perf(ios): ship default metadata as a separate framework for embedding hosts only (#488)
* build(ios): stop copying unused default metadata into NativeScript.framework The framework already embeds metadata-arm64.bin as its __DATA,__TNSMetadata section, which is the only copy the runtime reads. The loose 8.8 MB resource copy is never read, and the opt-in "Add default metadata" phase copied from a NativeScript/metadata/ directory that does not exist, gated on an INCLUDE_DEFAULT_METADATA setting nothing sets. * feat(runtime): resolve metadata from the host executable when no pointer is given When Config.MetadataPtr is nil, look for a non-empty __DATA,__TNSMetadata section in the host executable (or its <executable>.debug.dylib in Xcode debug-dylib builds) before falling back to the metadata embedded in NativeScript.framework. Hosts set up by 'ns embed ios' link their own metadata this way but never pass the pointer, so they were silently running on the framework's 2024 snapshot. * feat(ios): move default metadata into a NativeScriptDefaultMetadata framework NativeScript.framework no longer embeds the 8.8 MB default metadata snapshot. It now lives in a separate NativeScriptDefaultMetadata framework (iOS, simulator and Mac Catalyst) that only the NativeScriptSDK SwiftPM product links, so apps built by the NativeScript CLI, which use the NativeScript product and link their own metadata, stop shipping it. When neither Config.MetadataPtr nor a host executable section is available, the runtime loads the accessor exported by NativeScriptDefaultMetadata.framework (already loaded, or found next to NativeScript.framework), and otherwise fails to start with an actionable message instead of running on metadata the host never asked for. build_default_metadata.sh builds the xcframework, build_spm_artifacts.sh packages it with an NS_CHECKSUM_DEFAULTMETADATA_IOS checksum, and generate-spm-manifest.mjs declares it as a binary target of the NativeScriptSDK product. visionOS has no default metadata build. * fix(build): accept numeric boolean flags and use $PWD in the framework build scripts to_bool matched numbers with the case pattern [0-9]+, which is a glob for one digit followed by a literal plus, so BUILD_CATALYST=1 and friends were treated as invalid and turned the slice off. DIST was computed with $(PWD), which only resolves to /bin/pwd on a case-insensitive filesystem.
1 parent c5eb963 commit ee77abb

14 files changed

Lines changed: 479 additions & 63 deletions

File tree

‎NativeScript/NativeScript.mm‎

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#include "NativeScript.h"
22
#include <Foundation/Foundation.h>
3+
#include <dlfcn.h>
4+
#include <mach-o/dyld.h>
5+
#include <mach-o/getsect.h>
36
#include "inspector/JsV8InspectorClient.h"
47
#include "runtime/Console.h"
58
#include "runtime/Helpers.h"
@@ -26,7 +29,76 @@ @implementation Config
2629

2730
@implementation NativeScript
2831

29-
extern char defaultStartOfMetadataSection __asm("section$start$__DATA$__TNSMetadata");
32+
static void* HostExecutableMetadata() {
33+
// Debug builds with ENABLE_DEBUG_DYLIB link the app's code and linker flags
34+
// into <executable>.debug.dylib and leave the executable a stub.
35+
NSString* debugDylibName = [[[[NSBundle mainBundle] executablePath] lastPathComponent]
36+
stringByAppendingString:@".debug.dylib"];
37+
for (uint32_t i = 0, count = _dyld_image_count(); i < count; i++) {
38+
const struct mach_header* header = _dyld_get_image_header(i);
39+
if (header == nullptr) {
40+
continue;
41+
}
42+
if (header->filetype != MH_EXECUTE) {
43+
const char* path = _dyld_get_image_name(i);
44+
const char* name = path != nullptr ? strrchr(path, '/') : nullptr;
45+
if (debugDylibName == nil || name == nullptr ||
46+
strcmp(name + 1, debugDylibName.UTF8String) != 0) {
47+
continue;
48+
}
49+
}
50+
unsigned long size = 0;
51+
uint8_t* data = getsectiondata(reinterpret_cast<const struct mach_header_64*>(header), "__DATA",
52+
"__TNSMetadata", &size);
53+
// The app template creates an empty metadata file before the generator
54+
// runs, so an empty section means the host carries no metadata.
55+
if (data != nullptr && size > 0) {
56+
return data;
57+
}
58+
}
59+
return nullptr;
60+
}
61+
62+
// NativeScriptDefaultMetadata.framework ships only with the NativeScriptSDK
63+
// package product, embedded next to NativeScript.framework.
64+
static void* DefaultFrameworkMetadata() {
65+
const char* accessorName = "NativeScriptDefaultMetadata";
66+
void* accessor = dlsym(RTLD_DEFAULT, accessorName);
67+
if (accessor == nullptr) {
68+
NSString* frameworksDir = [[[NSBundle bundleForClass:[NativeScript class]] bundlePath]
69+
stringByDeletingLastPathComponent];
70+
NSBundle* bundle =
71+
[NSBundle bundleWithPath:[frameworksDir stringByAppendingPathComponent:
72+
@"NativeScriptDefaultMetadata.framework"]];
73+
// executablePath, unlike a hand-built path, follows Mac Catalyst's versioned layout.
74+
NSString* executable = [bundle executablePath];
75+
void* handle =
76+
executable != nil ? dlopen(executable.fileSystemRepresentation, RTLD_NOW) : nullptr;
77+
accessor = handle != nullptr ? dlsym(handle, accessorName) : nullptr;
78+
}
79+
if (accessor == nullptr) {
80+
return nullptr;
81+
}
82+
return const_cast<void*>(reinterpret_cast<const void* (*)(void)>(accessor)());
83+
}
84+
85+
static void* ResolveMetadataPtr(Config* config) {
86+
if (config.MetadataPtr != nil) {
87+
return config.MetadataPtr;
88+
}
89+
if (void* hostMetadata = HostExecutableMetadata()) {
90+
return hostMetadata;
91+
}
92+
if (void* defaultMetadata = DefaultFrameworkMetadata()) {
93+
return defaultMetadata;
94+
}
95+
std::string fatal =
96+
"no NativeScript metadata found. Pass Config.MetadataPtr, link a __DATA,__TNSMetadata "
97+
"section into the app executable (-sectcreate __DATA __TNSMetadata <metadata.bin>), or "
98+
"depend on the NativeScriptSDK package product, which bundles default metadata.";
99+
Log(@"Fatal: %s", fatal.c_str());
100+
throw tns::NativeScriptException("Fatal: " + fatal);
101+
}
30102

31103
- (void)runScriptString:(NSString*)script runLoop:(BOOL)runLoop {
32104
std::string cppString = std::string([script UTF8String]);
@@ -165,11 +237,7 @@ - (instancetype)initializeWithConfig:(Config*)config {
165237
RuntimeConfig.ApplicationPath =
166238
[[config.BaseDir stringByAppendingPathComponent:@"app"] UTF8String];
167239
}
168-
if (config.MetadataPtr != nil) {
169-
RuntimeConfig.MetadataPtr = [config MetadataPtr];
170-
} else {
171-
RuntimeConfig.MetadataPtr = &defaultStartOfMetadataSection;
172-
}
240+
RuntimeConfig.MetadataPtr = ResolveMetadataPtr(config);
173241
RuntimeConfig.IsDebug = [config IsDebug];
174242
RuntimeConfig.LogToSystemConsole = [config LogToSystemConsole];
175243

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleExecutable</key>
8+
<string>$(EXECUTABLE_NAME)</string>
9+
<key>CFBundleIdentifier</key>
10+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11+
<key>CFBundleInfoDictionaryVersion</key>
12+
<string>6.0</string>
13+
<key>CFBundleName</key>
14+
<string>$(PRODUCT_NAME)</string>
15+
<key>CFBundlePackageType</key>
16+
<string>FMWK</string>
17+
<key>CFBundleShortVersionString</key>
18+
<string>1.0</string>
19+
<key>CFBundleVersion</key>
20+
<string>$(CURRENT_PROJECT_VERSION)</string>
21+
</dict>
22+
</plist>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Linked with -sectcreate __DATA __TNSMetadata metadata-arm64.bin.
2+
extern char startOfMetadataSection __asm("section$start$__DATA$__TNSMetadata");
3+
4+
// Looked up by name with dlsym from NativeScript.framework; keep the symbol
5+
// name and signature stable.
6+
__attribute__((visibility("default"), used)) const void* NativeScriptDefaultMetadata(void) {
7+
return &startOfMetadataSection;
8+
}
File renamed without changes.

‎README.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,20 @@ package can be built in two modes (`--spm-mode`, see `./build_npm_ios.sh --help`
165165
so a remote-mode package only resolves for versions that shipped through
166166
`.github/workflows/npm_release.yml` — don't use this mode for local builds.
167167

168+
ios-spm exposes two iOS products:
169+
170+
- **`NativeScript`** — the runtime and TKLiveSync. {N} CLI apps use this product and link
171+
the metadata generated for them into their own executable.
172+
- **`NativeScriptSDK`** — additionally links `NativeScriptDefaultMetadata`, a separate
173+
framework carrying a default snapshot of iOS SDK metadata, for apps that embed NativeScript
174+
without generating metadata of their own. Built by `./build_default_metadata.sh` (iOS
175+
family only; there is no visionOS build).
176+
177+
At startup the runtime uses `Config.MetadataPtr` if set, otherwise a `__DATA,__TNSMetadata`
178+
section linked into the host executable, otherwise `NativeScriptDefaultMetadata.framework` if
179+
it is embedded next to `NativeScript.framework`, and fails to start if none is found. The
180+
embedded local package only serves CLI apps, so it does not include the default metadata.
181+
168182
# Local Development & Linking
169183

170184
To use a locally built runtime in an app, build the default (embedded) package and point

‎build_all_ios.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ rm -rf ./dist
1616
./build_metadata_generator.sh
1717
./build_nativescript.sh --no-vision
1818
./build_tklivesync.sh --no-vision
19+
./build_default_metadata.sh
1920
./prepare_dSYMs.sh
2021
./build_spm_artifacts.sh ios
2122
./build_npm_ios.sh "$@"

‎build_default_metadata.sh‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/bin/bash
2+
set -e
3+
source "$(dirname "$0")/build_utils.sh"
4+
5+
function to_bool() {
6+
local arg="$1"
7+
case "$(echo "$arg" | tr '[:upper:]' '[:lower:]')" in
8+
n|no|f|false) echo false ;;
9+
y|yes|t|true) echo true ;;
10+
# case patterns are globs, not regexes: whatever gets past this arm is all digits
11+
''|*[!0-9]*)
12+
if [ -n "$arg" ]; then
13+
echo "warning: invalid boolean argument ('$arg'). Expected true or false" >&2
14+
fi
15+
echo false
16+
;;
17+
* )
18+
if [ "$arg" -eq 0 ]; then
19+
echo false
20+
else
21+
echo true
22+
fi
23+
;;
24+
esac;
25+
}
26+
27+
BUILD_CATALYST=$(to_bool ${BUILD_CATALYST:=true})
28+
BUILD_IPHONE=$(to_bool ${BUILD_IPHONE:=true})
29+
BUILD_SIMULATOR=$(to_bool ${BUILD_SIMULATOR:=true})
30+
31+
for arg in $@; do
32+
case $arg in
33+
--catalyst|--maccatalyst) BUILD_CATALYST=true ;;
34+
--no-catalyst|--no-maccatalyst) BUILD_CATALYST=false ;;
35+
--sim|--simulator) BUILD_SIMULATOR=true ;;
36+
--no-sim|--no-simulator) BUILD_SIMULATOR=false ;;
37+
--iphone|--device) BUILD_IPHONE=true ;;
38+
--no-iphone|--no-device) BUILD_IPHONE=false ;;
39+
*) ;;
40+
esac
41+
done
42+
43+
DIST="$PWD/dist"
44+
mkdir -p $DIST
45+
46+
mkdir -p $DIST/intermediates
47+
48+
checkpoint "Cleanup NativeScriptDefaultMetadata"
49+
xcodebuild -project v8ios.xcodeproj \
50+
-target NativeScriptDefaultMetadata \
51+
-configuration Release clean \
52+
-quiet
53+
54+
if $BUILD_SIMULATOR; then
55+
checkpoint "Building NativeScriptDefaultMetadata for iphone simulators (multi-arch)"
56+
xcodebuild archive -project v8ios.xcodeproj \
57+
-scheme NativeScriptDefaultMetadata \
58+
-configuration Release \
59+
-destination "generic/platform=iOS Simulator" \
60+
-quiet \
61+
SKIP_INSTALL=NO \
62+
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
63+
-archivePath $DIST/intermediates/NativeScriptDefaultMetadata.iphonesimulator.xcarchive
64+
fi
65+
66+
if $BUILD_IPHONE; then
67+
checkpoint "Building NativeScriptDefaultMetadata for ARM64 device"
68+
xcodebuild archive -project v8ios.xcodeproj \
69+
-scheme NativeScriptDefaultMetadata \
70+
-configuration Release \
71+
-destination "generic/platform=iOS" \
72+
-quiet \
73+
SKIP_INSTALL=NO \
74+
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
75+
-archivePath $DIST/intermediates/NativeScriptDefaultMetadata.iphoneos.xcarchive
76+
fi
77+
78+
if $BUILD_CATALYST; then
79+
checkpoint "Building NativeScriptDefaultMetadata for Mac Catalyst"
80+
xcodebuild archive -project v8ios.xcodeproj \
81+
-scheme NativeScriptDefaultMetadata \
82+
-configuration Release \
83+
-destination "generic/platform=macOS,variant=Mac Catalyst" \
84+
-quiet \
85+
EXCLUDED_ARCHS="x86_64" \
86+
SKIP_INSTALL=NO \
87+
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
88+
-archivePath $DIST/intermediates/NativeScriptDefaultMetadata.maccatalyst.xcarchive
89+
fi
90+
91+
XCFRAMEWORKS=()
92+
if $BUILD_CATALYST; then
93+
XCFRAMEWORKS+=( -framework "$DIST/intermediates/NativeScriptDefaultMetadata.maccatalyst.xcarchive/Products/Library/Frameworks/NativeScriptDefaultMetadata.framework" \
94+
-debug-symbols "$DIST/intermediates/NativeScriptDefaultMetadata.maccatalyst.xcarchive/dSYMs/NativeScriptDefaultMetadata.framework.dSYM" )
95+
fi
96+
97+
if $BUILD_SIMULATOR; then
98+
XCFRAMEWORKS+=( -framework "$DIST/intermediates/NativeScriptDefaultMetadata.iphonesimulator.xcarchive/Products/Library/Frameworks/NativeScriptDefaultMetadata.framework" \
99+
-debug-symbols "$DIST/intermediates/NativeScriptDefaultMetadata.iphonesimulator.xcarchive/dSYMs/NativeScriptDefaultMetadata.framework.dSYM" )
100+
fi
101+
102+
if $BUILD_IPHONE; then
103+
XCFRAMEWORKS+=( -framework "$DIST/intermediates/NativeScriptDefaultMetadata.iphoneos.xcarchive/Products/Library/Frameworks/NativeScriptDefaultMetadata.framework" \
104+
-debug-symbols "$DIST/intermediates/NativeScriptDefaultMetadata.iphoneos.xcarchive/dSYMs/NativeScriptDefaultMetadata.framework.dSYM" )
105+
fi
106+
107+
checkpoint "Creating NativeScriptDefaultMetadata.xcframework"
108+
OUTPUT_DIR="$DIST/NativeScriptDefaultMetadata.xcframework"
109+
rm -rf $OUTPUT_DIR
110+
xcodebuild -create-xcframework ${XCFRAMEWORKS[@]} -output "$OUTPUT_DIR"
111+
112+
rm -rf "$DIST/intermediates"

‎build_nativescript.sh‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,22 @@ source "$(dirname "$0")/build_utils.sh"
99
function to_bool() {
1010
local arg="$1"
1111
case "$(echo "$arg" | tr '[:upper:]' '[:lower:]')" in
12-
[0-9]+)
13-
if [ $arg -eq 0 ]; then
14-
echo false
15-
else
16-
echo true
17-
fi
18-
;;
1912
n|no|f|false) echo false ;;
2013
y|yes|t|true) echo true ;;
21-
* )
14+
# case patterns are globs, not regexes: whatever gets past this arm is all digits
15+
''|*[!0-9]*)
2216
if [ -n "$arg" ]; then
2317
echo "warning: invalid boolean argument ('$arg'). Expected true or false" >&2
2418
fi
2519
echo false
2620
;;
21+
* )
22+
if [ "$arg" -eq 0 ]; then
23+
echo false
24+
else
25+
echo true
26+
fi
27+
;;
2728
esac;
2829
}
2930

@@ -54,7 +55,7 @@ if ! $VERBOSE; then
5455
fi
5556

5657
DEV_TEAM=${DEVELOPMENT_TEAM:-}
57-
DIST=$(PWD)/dist
58+
DIST="$PWD/dist"
5859
mkdir -p $DIST
5960

6061
mkdir -p $DIST/intermediates
@@ -75,7 +76,6 @@ xcodebuild archive -project v8ios.xcodeproj \
7576
EXCLUDED_ARCHS="x86_64" \
7677
SKIP_INSTALL=NO \
7778
BUILD_LIBRARIES_FOR_DISTRIBUTION=YES \
78-
INCLUDE_DEFAULT_METADATA=$INCLUDE_DEFAULT_METADATA\
7979
-archivePath $DIST/intermediates/NativeScript.maccatalyst.xcarchive
8080
fi
8181

@@ -90,7 +90,6 @@ xcodebuild archive -project v8ios.xcodeproj \
9090
DEVELOPMENT_TEAM=$DEV_TEAM \
9191
SKIP_INSTALL=NO \
9292
BUILD_LIBRARIES_FOR_DISTRIBUTION=YES \
93-
INCLUDE_DEFAULT_METADATA=$INCLUDE_DEFAULT_METADATA\
9493
-archivePath $DIST/intermediates/NativeScript.iphonesimulator.xcarchive
9594
fi
9695

@@ -105,7 +104,6 @@ xcodebuild archive -project v8ios.xcodeproj \
105104
DEVELOPMENT_TEAM=$DEV_TEAM \
106105
SKIP_INSTALL=NO \
107106
BUILD_LIBRARIES_FOR_DISTRIBUTION=YES \
108-
INCLUDE_DEFAULT_METADATA=$INCLUDE_DEFAULT_METADATA\
109107
-archivePath $DIST/intermediates/NativeScript.iphoneos.xcarchive
110108
fi
111109

‎build_spm_artifacts.sh‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,22 @@ zip_xcframework "TKLiveSync.xcframework" "$TK_ZIP"
6969
NS_SUM="$(compute_checksum "$OUT/$NS_ZIP")"
7070
TK_SUM="$(compute_checksum "$OUT/$TK_ZIP")"
7171

72+
# Default metadata ships for the iOS family only.
73+
if [ "$TARGET" = "ios" ]; then
74+
DM_ZIP="NativeScriptDefaultMetadata.xcframework.zip"
75+
zip_xcframework "NativeScriptDefaultMetadata.xcframework" "$DM_ZIP"
76+
DM_SUM="$(compute_checksum "$OUT/$DM_ZIP")"
77+
fi
78+
7279
# Per-target filename so the iOS and visionOS env files don't collide when the
7380
# release/stamp jobs merge both platforms' artifacts into one directory.
7481
CHECKSUMS="$OUT/checksums-$TARGET.env"
7582
{
7683
echo "${NS_KEY}=${NS_SUM}"
7784
echo "${TK_KEY}=${TK_SUM}"
85+
if [ -n "${DM_SUM:-}" ]; then
86+
echo "NS_CHECKSUM_DEFAULTMETADATA_IOS=${DM_SUM}"
87+
fi
7888
} > "$CHECKSUMS"
7989

8090
checkpoint "SwiftPM artifacts ready in $OUT:"

0 commit comments

Comments
 (0)