diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml
new file mode 100644
index 00000000..78cbf9b7
--- /dev/null
+++ b/.github/actions/setup-build-env/action.yml
@@ -0,0 +1,29 @@
+name: Setup build environment
+description: Node + Python + native build dependencies shared by the workflow build/test jobs.
+inputs:
+ node-version:
+ description: Node.js version to install
+ default: "22"
+ registry-url:
+ description: npm registry URL to configure for publishing (optional)
+ default: ""
+ install-test-tools:
+ description: Also install xcparse and the JUnit report tools ("true"/"false")
+ default: "false"
+runs:
+ using: composite
+ steps:
+ - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ with:
+ node-version: ${{ inputs.node-version }}
+ registry-url: ${{ inputs.registry-url }}
+ # setup-node v6 enables dependency caching by default when it detects a
+ # package manager — a cache-poisoning vector for the release jobs (see
+ # npm_release.yml). Disabled everywhere; also keeps pre-v6 behavior.
+ package-manager-cache: false
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
+ with:
+ python-version: "3"
+ - name: Install dependencies
+ shell: bash
+ run: ./scripts/install-ci-deps.sh ${{ inputs.install-test-tools == 'true' && '--test-tools' || '' }}
diff --git a/.github/workflows/npm_release.yml b/.github/workflows/npm_release.yml
index 8daf97e6..350d8db2 100644
--- a/.github/workflows/npm_release.yml
+++ b/.github/workflows/npm_release.yml
@@ -64,40 +64,9 @@ jobs:
- name: Compute version and tag
id: out
env:
+ # env indirection keeps the dispatch input out of shell interpolation
INPUT_VERSION: ${{ inputs.version }}
- run: |
- if [ -n "$INPUT_VERSION" ]; then
- # manual release: cut tag v and publish (latest unless prerelease)
- NPM_VERSION="${INPUT_VERSION#v}"
- elif [ "${GITHUB_REF#refs/tags/}" != "$GITHUB_REF" ]; then
- # tag push (v*): the tag is authoritative. Strip the leading 'v' and
- # assert it matches package.json so the release can't drift from its tag.
- NPM_VERSION="${GITHUB_REF#refs/tags/}"
- NPM_VERSION="${NPM_VERSION#v}"
- PKG_VERSION=$(node -e "console.log(require('./package.json').version);")
- if [ "$NPM_VERSION" != "$PKG_VERSION" ]; then
- echo "::error::Tag v$NPM_VERSION does not match package.json version $PKG_VERSION. Bump package.json before tagging." >&2
- exit 1
- fi
- else
- # branch push (main) or manual run without a version -> "next" prerelease
- NPM_VERSION=$(node ./scripts/get-next-version.js)
- fi
- NPM_TAG=$(NPM_VERSION=$NPM_VERSION node ./scripts/get-npm-tag.js)
- echo "NPM_VERSION=$NPM_VERSION" >> $GITHUB_OUTPUT
- echo "NPM_TAG=$NPM_TAG" >> $GITHUB_OUTPUT
- # Target matrix: ios always builds/publishes. visionos is built and
- # published ONLY for real releases (a v* tag push, or a manual dispatch
- # with a version), never for the rolling "next" channel that every push
- # to main produces. The discriminator is the resolved dist-tag: pushes
- # to main (and dispatch without a version) -> "next" -> ios only.
- if [ "$NPM_TAG" = "next" ]; then
- BUILD_MATRIX='{"include":[{"target":"ios","script":"build-ios"}]}'
- else
- BUILD_MATRIX='{"include":[{"target":"ios","script":"build-ios"},{"target":"visionos","script":"build-vision"}]}'
- fi
- echo "BUILD_MATRIX=$BUILD_MATRIX" >> $GITHUB_OUTPUT
- echo "Resolved $NPM_VERSION (tag: $NPM_TAG); build targets: $BUILD_MATRIX"
+ run: node ./scripts/resolve-release.mjs --version "$INPUT_VERSION"
build:
name: Build ${{ matrix.target }}
@@ -130,32 +99,19 @@ jobs:
with:
path: ./llvm
key: ${{ hashFiles('download_llvm.sh') }}
- - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ - uses: ./.github/actions/setup-build-env
with:
- node-version: 22
- package-manager-cache: false
registry-url: "https://registry.npmjs.org"
- - name: Install Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- with:
- python-version: "3"
- - name: Install Dependencies
- run: |
- npm install
- python3 -m pip install --upgrade pip six
- # Ensure CMake is available without conflicting with pinned Homebrew formula
- if ! command -v cmake >/dev/null; then
- brew list cmake || brew install cmake
- fi
- # Some scripts expect cmake at /usr/local/bin; create a shim if needed
- if [ ! -x /usr/local/bin/cmake ]; then
- sudo mkdir -p /usr/local/bin
- sudo ln -sf "$(command -v cmake)" /usr/local/bin/cmake
- fi
- name: Set package version
run: npm version $NPM_VERSION --no-git-tag-version --allow-same-version
- name: Build (${{ matrix.target }})
- run: npm run ${{ matrix.script }}
+ # remote mode: the published package embeds no binaries and pins
+ # github.com/NativeScript/ios-spm at this version instead (the
+ # github-release + spm-update jobs make that pin resolvable).
+ env:
+ # env indirection keeps matrix values out of shell interpolation
+ BUILD_SCRIPT: ${{ matrix.script }}
+ run: npm run "$BUILD_SCRIPT" -- --spm-mode=remote
- name: Upload npm package artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
@@ -193,29 +149,9 @@ jobs:
with:
path: ./llvm
key: ${{ hashFiles('download_llvm.sh') }}
- - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- with:
- node-version: 22
- package-manager-cache: false
- - name: Install Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
+ - uses: ./.github/actions/setup-build-env
with:
- python-version: "3"
- - name: Install Dependencies
- run: |
- npm install
- python3 -m pip install --upgrade pip six
- # Ensure CMake is available without conflicting with pinned Homebrew formula
- if ! command -v cmake >/dev/null; then
- brew list cmake || brew install cmake
- fi
- # Some scripts expect cmake at /usr/local/bin; create a shim if needed
- if [ ! -x /usr/local/bin/cmake ]; then
- sudo mkdir -p /usr/local/bin
- sudo ln -sf "$(command -v cmake)" /usr/local/bin/cmake
- fi
- brew install chargepoint/xcparse/xcparse
- npm install -g @edusperoni/junit-cli-report-viewer verify-junit-xml
+ install-test-tools: "true"
- name: Prepare
run: npm run setup-ci
- name: Prepare test folder
@@ -360,26 +296,19 @@ jobs:
path: spm-artifacts
merge-multiple: true
- name: Generate Package.swift
+ # The generator emits the WHOLE manifest, shaped by the checksums it
+ # finds: "next" builds produce only checksums-ios.env, so the next
+ # manifest omits the visionOS product/targets whose Release assets
+ # don't exist (SwiftPM eagerly downloads every binaryTarget in a
+ # resolved manifest — an unshipped asset 404s for every consumer).
+ # Any non-"next" --channel requires the full checksum set, so a real
+ # release can never silently ship an iOS-only manifest.
run: |
- # The generator emits the WHOLE manifest, shaped by the checksums it
- # is given: "next" builds produce only checksums-ios.env, so the next
- # manifest omits the visionOS product/targets whose Release assets
- # don't exist (SwiftPM eagerly downloads every binaryTarget in a
- # resolved manifest — an unshipped asset 404s for every consumer).
- # Real releases pass --strict so a full release can never silently
- # ship an iOS-only manifest.
- STRICT=""
- if [ "$NPM_TAG" != "next" ]; then STRICT="--strict"; fi
- CHECKSUM_ARGS=""
- for f in spm-artifacts/checksums-*.env; do
- [ -f "$f" ] && CHECKSUM_ARGS="$CHECKSUM_ARGS --checksums $f"
- done
- echo "Using checksum files:$CHECKSUM_ARGS"
- if [ -z "$CHECKSUM_ARGS" ]; then echo "No checksum files found" >&2; exit 1; fi
node ios/scripts/generate-spm-manifest.mjs \
--package ios-spm/Package.swift \
--version "$NPM_VERSION" \
- $CHECKSUM_ARGS $STRICT
+ --checksums-dir spm-artifacts \
+ --channel "$NPM_TAG"
- name: Commit and tag ios-spm
working-directory: ios-spm
env:
@@ -493,29 +422,19 @@ jobs:
with:
node-version: 22
package-manager-cache: false
- - name: Assert ios-spm manifest pins this release
- run: |
- # Guard against the tag pointing at a STALE manifest — e.g. a
- # commit/tag step that silently failed and left a prior version's
- # nsVersion + artifact URLs/checksums in place. Those still resolve
- # and checksum-pass (they're internally consistent), so `swift
- # package resolve` below won't catch it; this explicit version
- # assertion does.
- url="https://raw.githubusercontent.com/NativeScript/ios-spm/${NPM_VERSION}/Package.swift"
- manifest="$(curl -fsSL "$url")"
- if ! printf '%s\n' "$manifest" | grep -q "let nsVersion = \"${NPM_VERSION}\""; then
- echo "::error::ios-spm@${NPM_VERSION} does not pin nsVersion=\"${NPM_VERSION}\" (stale manifest?). Found:" >&2
- printf '%s\n' "$manifest" | grep -n "nsVersion" >&2 || true
- exit 1
- fi
- echo "OK: ios-spm@${NPM_VERSION} pins nsVersion=\"${NPM_VERSION}\""
- name: Generate a probe package that depends on ios-spm
# The probe pins ios-spm at the exact released version. Resolving it
# takes the same path a consumer's xcodebuild does, and the released
# manifest only declares targets whose assets exist (channel-shaped by
# generate-spm-manifest.mjs) — so this single probe is valid for every
- # channel.
- run: node scripts/generate-spm-probe.mjs --version "$NPM_VERSION" --dir spmverify
+ # channel. --assert-release-manifest additionally guards against the
+ # tag pointing at a stale (prior version's) manifest, which would
+ # still resolve and checksum-pass below.
+ run: |
+ node scripts/generate-spm-probe.mjs \
+ --version "$NPM_VERSION" \
+ --dir spmverify \
+ --assert-release-manifest
- name: Resolve (downloads the xcframework zips and verifies their checksums)
working-directory: spmverify
run: |
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 7ccb2b55..1785a710 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -23,26 +23,7 @@ jobs:
with:
path: ./llvm
key: ${{ hashFiles('download_llvm.sh') }}
- - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- with:
- node-version: 20
- - name: Install Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- with:
- python-version: "3"
- - name: Install Dependencies
- run: |
- npm install
- python3 -m pip install --upgrade pip six
- # Ensure CMake is available without conflicting with pinned Homebrew formula
- if ! command -v cmake >/dev/null; then
- brew list cmake || brew install cmake
- fi
- # Some scripts expect cmake at /usr/local/bin; create a shim if needed
- if [ ! -x /usr/local/bin/cmake ]; then
- sudo mkdir -p /usr/local/bin
- sudo ln -sf "$(command -v cmake)" /usr/local/bin/cmake
- fi
+ - uses: ./.github/actions/setup-build-env
- name: Get Current Version
run: |
echo NPM_VERSION=$(node -e "console.log(require('./package.json').version);") >> $GITHUB_ENV
@@ -52,12 +33,20 @@ jobs:
echo NPM_VERSION=$NPM_VERSION >> $GITHUB_ENV
npm version $NPM_VERSION --no-git-tag-version
- name: Build
+ # Default (embedded) SwiftPM mode: the xcframework zips ship inside the
+ # package, so the artifact below is self-contained — download it and
+ # use it directly with `ns platform add ios --framework-path=`.
run: npm run build-ios
- name: Upload npm package artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: npm-package
path: dist/nativescript-ios-${{env.NPM_VERSION}}.tgz
+ - name: Upload SwiftPM artifacts (xcframework zips + checksums)
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: spm-artifacts
+ path: dist/artifacts/*
- name: Upload dSYMs artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
@@ -80,28 +69,9 @@ jobs:
with:
path: ./llvm
key: ${{ hashFiles('download_llvm.sh') }}
- - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- with:
- node-version: 20
- - name: Install Python
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
+ - uses: ./.github/actions/setup-build-env
with:
- python-version: "3"
- - name: Install Dependencies
- run: |
- npm install
- python3 -m pip install --upgrade pip six
- # Ensure CMake is available without conflicting with pinned Homebrew formula
- if ! command -v cmake >/dev/null; then
- brew list cmake || brew install cmake
- fi
- # Some scripts expect cmake at /usr/local/bin; create a shim if needed
- if [ ! -x /usr/local/bin/cmake ]; then
- sudo mkdir -p /usr/local/bin
- sudo ln -sf "$(command -v cmake)" /usr/local/bin/cmake
- fi
- brew install chargepoint/xcparse/xcparse
- npm install -g @edusperoni/junit-cli-report-viewer verify-junit-xml
+ install-test-tools: "true"
- name: Prepare test folder
id: prepare-test-folder
run: |
diff --git a/README.md b/README.md
index 7c396fc6..4c65f0f6 100644
--- a/README.md
+++ b/README.md
@@ -240,10 +240,42 @@ The compiled fat static libraries will be placed inside the `v8/dist` folder.
# Building a Distribution Package
-1. Bump the version in package.json
+1. (Optional) Bump the version in package.json
2. Run: `npm run update-version` (*This will update the runtime headers with version info*)
-3. Build & pack: `npm run build`
+3. Build & pack: `npm run build-ios`
-This will create: `dist/npm/nativescript-ios-{version}.tgz` NPM package ready for publishing.
+This will create `dist/nativescript-ios-{version}.tgz`.
+
+The runtime's NativeScript / TKLiveSync xcframeworks are consumed via SwiftPM, and the
+package can be built in two modes (`--spm-mode`, see `./build_npm_ios.sh --help`):
+
+- **`embedded` (default)** — a self-contained package: the freshly built xcframeworks are
+ embedded (as zips, since npm strips symlinks) at `framework/internal/local-spm` together
+ with a local SwiftPM manifest, and the app template consumes them by relative path.
+ The tgz works on any machine. This is what local builds and PR artifacts produce.
+- **`remote`** — the deploy shape published to npm by the release workflow
+ (`npm run build-ios -- --spm-mode=remote`): no binaries are embedded; the app template
+ pins https://github.com/NativeScript/ios-spm at exactly the package version. The release
+ workflow uploads the xcframework zips as GitHub Release assets and tags ios-spm to match,
+ so a remote-mode package only resolves for versions that shipped through
+ `.github/workflows/npm_release.yml` — don't use this mode for local builds.
+
+# Local Development & Linking
+
+To use a locally built runtime in an app, build the default (embedded) package and point
+the {N} CLI at it:
+
+```bash
+npm run build-ios
+ns platform add ios --framework-path=/path/to/ns-v8ios-runtime/dist/nativescript-ios-{version}.tgz
+```
+
+Because the embedded package is self-contained, the tgz can be copied to other machines,
+shared with teammates, or consumed by an app's CI. The same applies to the `npm-package`
+artifact produced by this repo's pull-request workflow — download it and use it with
+`--framework-path` directly.
+
+To rebuild just the package after runtime changes (without the full `build-ios` pipeline),
+re-run `./build_spm_artifacts.sh ios` followed by `./build_npm_ios.sh`.
diff --git a/build_all_ios.sh b/build_all_ios.sh
index 832c38bb..07a0fd2a 100755
--- a/build_all_ios.sh
+++ b/build_all_ios.sh
@@ -1,6 +1,16 @@
#!/bin/bash
set -e
+# Arguments (e.g. --spm-mode ) are forwarded to
+# build_npm_ios.sh; run it with --help for details.
+for arg in "$@"; do
+ if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
+ echo "Usage: ./build_all_ios.sh [--spm-mode ]"
+ echo "Arguments are forwarded to build_npm_ios.sh (see ./build_npm_ios.sh --help)."
+ exit 0
+ fi
+done
+
rm -rf ./dist
./update_version.sh
./build_metadata_generator.sh
@@ -8,4 +18,4 @@ rm -rf ./dist
./build_tklivesync.sh --no-vision
./prepare_dSYMs.sh
./build_spm_artifacts.sh ios
-./build_npm_ios.sh
\ No newline at end of file
+./build_npm_ios.sh "$@"
diff --git a/build_all_vision.sh b/build_all_vision.sh
index b8ef5b49..fbbfe770 100755
--- a/build_all_vision.sh
+++ b/build_all_vision.sh
@@ -1,6 +1,16 @@
#!/bin/bash
set -e
+# Arguments (e.g. --spm-mode ) are forwarded to
+# build_npm_vision.sh; run it with --help for details.
+for arg in "$@"; do
+ if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
+ echo "Usage: ./build_all_vision.sh [--spm-mode ]"
+ echo "Arguments are forwarded to build_npm_vision.sh (see ./build_npm_vision.sh --help)."
+ exit 0
+ fi
+done
+
rm -rf ./dist
./update_version.sh
./build_metadata_generator.sh
@@ -8,4 +18,4 @@ rm -rf ./dist
./build_tklivesync.sh --no-catalyst --no-iphone --no-sim
./prepare_dSYMs.sh
./build_spm_artifacts.sh visionos
-./build_npm_vision.sh
\ No newline at end of file
+./build_npm_vision.sh "$@"
diff --git a/build_npm_ios.sh b/build_npm_ios.sh
index ae62019f..51b28700 100755
--- a/build_npm_ios.sh
+++ b/build_npm_ios.sh
@@ -2,7 +2,26 @@
set -e
source "$(dirname "$0")/build_utils.sh"
-checkpoint "Preparing npm package for iOS..."
+usage() {
+ echo "Usage: ./build_npm_ios.sh [--spm-mode ]"
+ echo ""
+ echo " --spm-mode embedded (default) self-contained package: the xcframework zips"
+ echo " from dist/artifacts are embedded at"
+ echo " framework/internal/local-spm and the app template"
+ echo " references them by relative path. Portable — for local"
+ echo " testing (ns platform add ios --framework-path=...) and"
+ echo " PR artifacts."
+ echo " --spm-mode remote deploy shape used by the release workflow: no binaries"
+ echo " embedded; the app template pins"
+ echo " github.com/NativeScript/ios-spm at exactly this package"
+ echo " version. Only resolves for versions shipped by the"
+ echo " release pipeline."
+ echo " -h, --help show this help"
+}
+
+parse_spm_mode_args "$@"
+
+checkpoint "Preparing npm package for iOS ($SPM_MODE SwiftPM mode)..."
OUTPUT_DIR="dist/npm"
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
@@ -11,56 +30,36 @@ cp ./package.json "$OUTPUT_DIR"
cp -r "./project-template-ios/" "$OUTPUT_DIR/framework"
-# The NativeScript / TKLiveSync xcframeworks are NO LONGER bundled in npm. They
-# are published as GitHub Release artifacts and consumed via SwiftPM
-# (github.com/NativeScript/ios-spm). Stamp the runtime version into the packaged
-# template's SwiftPM reference so it resolves the matching release.
-NPM_VERSION=$(node -e "console.log(require('./package.json').version)")
-node ./scripts/stamp-template-version.mjs \
- "$OUTPUT_DIR/framework/__PROJECT_NAME__.xcodeproj/project.pbxproj" \
- "$NPM_VERSION"
-
-# Local builds have no ios-spm release tag to resolve, so by default (outside
-# CI) rewrite the template to a LOCAL SwiftPM package over the xcframeworks
-# just built into dist/. Force with NS_SPM_LOCAL=1, disable with NS_SPM_LOCAL=0.
-# The resulting .tgz embeds an absolute path into this checkout — local
-# `ns platform add ios --framework-path=dist/nativescript-ios-*.tgz` use only.
-if [[ "${NS_SPM_LOCAL:-}" == "1" || ( -z "${CI:-}" && "${NS_SPM_LOCAL:-}" != "0" ) ]]; then
- LOCAL_SPM_DIR="$(pwd)/dist/local-spm"
- rm -rf "$LOCAL_SPM_DIR"
+# The runtime xcframeworks are consumed via SwiftPM in both modes; what differs
+# is where the SwiftPM package lives (see usage above). The zips come from
+# build_spm_artifacts.sh — zipped because npm strips symlinks (the Catalyst
+# slices contain them) and SwiftPM extracts local zip binary targets itself.
+if [ "$SPM_MODE" = "embedded" ]; then
+ ARTIFACTS_DIR="dist/artifacts"
+ LOCAL_SPM_DIR="$OUTPUT_DIR/framework/internal/local-spm"
+ for zip in NativeScript.xcframework.zip TKLiveSync.xcframework.zip; do
+ if [ ! -f "$ARTIFACTS_DIR/$zip" ]; then
+ echo "Missing $ARTIFACTS_DIR/$zip — run ./build_spm_artifacts.sh ios first." >&2
+ exit 1
+ fi
+ done
mkdir -p "$LOCAL_SPM_DIR"
- cp -R "dist/NativeScript.xcframework" "$LOCAL_SPM_DIR/"
- cp -R "dist/TKLiveSync.xcframework" "$LOCAL_SPM_DIR/"
- cat > "$LOCAL_SPM_DIR/Package.swift" <<'EOF'
-// swift-tools-version: 5.10
-// Local dev override of github.com/NativeScript/ios-spm: same product shape,
-// but binaryTargets point at the freshly built xcframeworks in this folder.
-import PackageDescription
-
-let package = Package(
- name: "NativeScriptSDK",
- platforms: [
- .iOS(.v13),
- .macCatalyst(.v13),
- ],
- products: [
- .library(name: "NativeScript", targets: ["NativeScript", "TKLiveSync"]),
- .library(name: "NativeScriptSDK", targets: ["NativeScript", "TKLiveSync"]),
- ],
- dependencies: [],
- targets: [
- .binaryTarget(name: "NativeScript", path: "NativeScript.xcframework"),
- .binaryTarget(name: "TKLiveSync", path: "TKLiveSync.xcframework"),
- ]
-)
-EOF
+ cp "./spm-templates/local-spm-ios/Package.swift" "$LOCAL_SPM_DIR/"
+ cp "$ARTIFACTS_DIR/NativeScript.xcframework.zip" \
+ "$ARTIFACTS_DIR/TKLiveSync.xcframework.zip" \
+ "$LOCAL_SPM_DIR/"
node ./scripts/stamp-template-local-spm.mjs \
"$OUTPUT_DIR/framework/__PROJECT_NAME__.xcodeproj/project.pbxproj" \
- "$LOCAL_SPM_DIR"
+ "internal/local-spm" \
+ --package-dir "$LOCAL_SPM_DIR"
+else
+ NPM_VERSION=$(node -e "console.log(require('./package.json').version)")
+ node ./scripts/stamp-template-version.mjs \
+ "$OUTPUT_DIR/framework/__PROJECT_NAME__.xcodeproj/project.pbxproj" \
+ "$NPM_VERSION"
fi
-# Build-time metadata generator is still shipped in npm (Phase 1). See the
-# distribution plan for moving this to an on-demand artifact (Phase 2).
+# Build-time metadata generator is still shipped in npm (Phase 1).
mkdir -p "$OUTPUT_DIR/framework/internal/metadata-generator-x86_64"
cp -r "metadata-generator/dist/x86_64/." "$OUTPUT_DIR/framework/internal/metadata-generator-x86_64"
diff --git a/build_npm_vision.sh b/build_npm_vision.sh
index e8c52af7..4d8acfce 100755
--- a/build_npm_vision.sh
+++ b/build_npm_vision.sh
@@ -2,7 +2,25 @@
set -e
source "$(dirname "$0")/build_utils.sh"
-checkpoint "Preparing npm package for visionOS..."
+usage() {
+ echo "Usage: ./build_npm_vision.sh [--spm-mode ]"
+ echo ""
+ echo " --spm-mode embedded (default) self-contained package: the xcframework zips"
+ echo " from dist/artifacts are embedded at"
+ echo " framework/internal/local-spm and the app template"
+ echo " references them by relative path. Portable — for local"
+ echo " testing (ns platform add visionos --framework-path=...)."
+ echo " --spm-mode remote deploy shape used by the release workflow: no binaries"
+ echo " embedded; the app template pins"
+ echo " github.com/NativeScript/ios-spm at exactly this package"
+ echo " version. Only resolves for versions shipped by the"
+ echo " release pipeline."
+ echo " -h, --help show this help"
+}
+
+parse_spm_mode_args "$@"
+
+checkpoint "Preparing npm package for visionOS ($SPM_MODE SwiftPM mode)..."
OUTPUT_DIR="dist/npm"
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
@@ -11,17 +29,36 @@ cp ./package.json "$OUTPUT_DIR"
cp -r "./project-template-vision/" "$OUTPUT_DIR/framework"
-# The NativeScript / TKLiveSync xcframeworks are NO LONGER bundled in npm. They
-# are published as GitHub Release artifacts and consumed via SwiftPM
-# (github.com/NativeScript/ios-spm). Stamp the runtime version into the packaged
-# template's SwiftPM reference so it resolves the matching release.
-NPM_VERSION=$(node -e "console.log(require('./package.json').version)")
-node ./scripts/stamp-template-version.mjs \
- "$OUTPUT_DIR/framework/__PROJECT_NAME__.xcodeproj/project.pbxproj" \
- "$NPM_VERSION"
-
-# Build-time metadata generator is still shipped in npm (Phase 1). See the
-# distribution plan for moving this to an on-demand artifact (Phase 2).
+# The runtime xcframeworks are consumed via SwiftPM in both modes; what differs
+# is where the SwiftPM package lives (see usage above). The zips come from
+# build_spm_artifacts.sh — zipped because npm strips symlinks and SwiftPM
+# extracts local zip binary targets itself.
+if [ "$SPM_MODE" = "embedded" ]; then
+ ARTIFACTS_DIR="dist/artifacts"
+ LOCAL_SPM_DIR="$OUTPUT_DIR/framework/internal/local-spm"
+ for zip in NativeScript.visionos.xcframework.zip TKLiveSync.visionos.xcframework.zip; do
+ if [ ! -f "$ARTIFACTS_DIR/$zip" ]; then
+ echo "Missing $ARTIFACTS_DIR/$zip — run ./build_spm_artifacts.sh visionos first." >&2
+ exit 1
+ fi
+ done
+ mkdir -p "$LOCAL_SPM_DIR"
+ cp "./spm-templates/local-spm-visionos/Package.swift" "$LOCAL_SPM_DIR/"
+ cp "$ARTIFACTS_DIR/NativeScript.visionos.xcframework.zip" \
+ "$ARTIFACTS_DIR/TKLiveSync.visionos.xcframework.zip" \
+ "$LOCAL_SPM_DIR/"
+ node ./scripts/stamp-template-local-spm.mjs \
+ "$OUTPUT_DIR/framework/__PROJECT_NAME__.xcodeproj/project.pbxproj" \
+ "internal/local-spm" \
+ --package-dir "$LOCAL_SPM_DIR"
+else
+ NPM_VERSION=$(node -e "console.log(require('./package.json').version)")
+ node ./scripts/stamp-template-version.mjs \
+ "$OUTPUT_DIR/framework/__PROJECT_NAME__.xcodeproj/project.pbxproj" \
+ "$NPM_VERSION"
+fi
+
+# Build-time metadata generator is still shipped in npm (Phase 1).
mkdir -p "$OUTPUT_DIR/framework/internal/metadata-generator-x86_64"
cp -r "metadata-generator/dist/x86_64/." "$OUTPUT_DIR/framework/internal/metadata-generator-x86_64"
diff --git a/build_utils.sh b/build_utils.sh
index 5b60fcf1..88261bdd 100644
--- a/build_utils.sh
+++ b/build_utils.sh
@@ -16,4 +16,46 @@ function checkpoint {
echo "--- $(date +'%T') --- $1 "
echo "$delimiter"
echo ""
+}
+
+# Shared --spm-mode argument parsing for build_npm_ios.sh / build_npm_vision.sh.
+# Sets SPM_MODE (embedded|remote, default embedded). The calling script must
+# define usage(); accepts both "--spm-mode " and "--spm-mode=".
+function parse_spm_mode_args {
+ SPM_MODE="embedded"
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --spm-mode)
+ if [ $# -lt 2 ]; then
+ echo "--spm-mode requires a value" >&2
+ usage >&2
+ exit 1
+ fi
+ SPM_MODE="$2"
+ shift 2
+ ;;
+ --spm-mode=*)
+ SPM_MODE="${1#*=}"
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown argument: $1" >&2
+ usage >&2
+ exit 1
+ ;;
+ esac
+ done
+
+ case "$SPM_MODE" in
+ embedded|remote) ;;
+ *)
+ echo "Invalid --spm-mode '$SPM_MODE' (expected embedded or remote)" >&2
+ usage >&2
+ exit 1
+ ;;
+ esac
}
\ No newline at end of file
diff --git a/scripts/generate-spm-manifest.mjs b/scripts/generate-spm-manifest.mjs
index 106274e1..8619c4bd 100644
--- a/scripts/generate-spm-manifest.mjs
+++ b/scripts/generate-spm-manifest.mjs
@@ -1,44 +1,85 @@
#!/usr/bin/env node
// Generate ios-spm/Package.swift for a release.
//
-// ensure binaryTarget whose Release asset was never uploaded breaks
-// resolution for every consumer of that version. Here, the visionOS
-// product/targets are emitted only when the visionOS checksums are provided.
+// A binaryTarget whose Release asset was never uploaded breaks resolution for
+// every consumer of that version (SwiftPM eagerly downloads every binaryTarget
+// in a resolved manifest). Here, the visionOS product/targets are emitted only
+// when the visionOS checksums are provided.
//
// Checksums come from the KEY=sha256 env files produced by
-// build_spm_artifacts.sh (one file per platform). The iOS checksums are always
-// required. --strict additionally requires the visionOS checksums; pass it for
-// every non-"next" release so a real release can never silently ship an
+// build_spm_artifacts.sh (one file per platform), passed individually via
+// --checksums and/or collected from a directory via --checksums-dir. The iOS
+// checksums are always required. The visionOS checksums are additionally
+// required when the release channel is anything but "next" (--channel), or
+// when --strict is passed — so a real release can never silently ship an
// iOS-only manifest.
-//
-// Usage:
-// node scripts/generate-spm-manifest.mjs \
-// --package /path/to/ios-spm/Package.swift \
-// --version 9.1.0 \
-// --checksums checksums-ios.env [--checksums checksums-visionos.env] \
-// [--strict]
import fs from "node:fs";
+import path from "node:path";
+import { parseArgs } from "node:util";
+
+const USAGE = `Usage: node scripts/generate-spm-manifest.mjs --package --version
+ (--checksums ... | --checksums-dir ) [--channel ] [--strict]
+
+ --package path to the ios-spm Package.swift to (over)write
+ --version release version the manifest pins (nsVersion + asset URLs)
+ --checksums a checksums-.env file from build_spm_artifacts.sh
+ (repeatable)
+ --checksums-dir directory to scan for checksums-*.env files (e.g. the merged
+ spm-artifacts download)
+ --channel npm dist-tag of this release; any channel other than "next"
+ requires the full checksum set (implies --strict)
+ --strict require the visionOS checksums regardless of channel
+ -h, --help show this help`;
+
+let values;
+try {
+ ({ values } = parseArgs({
+ options: {
+ package: { type: "string" },
+ version: { type: "string" },
+ checksums: { type: "string", multiple: true, default: [] },
+ "checksums-dir": { type: "string" },
+ channel: { type: "string" },
+ strict: { type: "boolean", default: false },
+ help: { type: "boolean", short: "h", default: false },
+ },
+ }));
+} catch (e) {
+ console.error(e.message);
+ console.error(USAGE);
+ process.exit(1);
+}
+if (values.help) {
+ console.log(USAGE);
+ process.exit(0);
+}
+
+const opts = {
+ package: values.package,
+ version: values.version,
+ checksums: [...values.checksums],
+ strict: values.strict || (values.channel !== undefined && values.channel !== "next"),
+};
-const args = process.argv.slice(2);
-const opts = { checksums: [] };
-for (let i = 0; i < args.length; i++) {
- const a = args[i];
- if (a === "--package") opts.package = args[++i];
- else if (a === "--version") opts.version = args[++i];
- else if (a === "--checksums") opts.checksums.push(args[++i]);
- else if (a === "--strict") opts.strict = true;
- else {
- console.error(`Unknown argument: ${a}`);
+const checksumsDir = values["checksums-dir"];
+if (checksumsDir) {
+ if (!fs.existsSync(checksumsDir)) {
+ console.error(`ERROR: --checksums-dir ${checksumsDir} does not exist`);
process.exit(1);
}
+ const found = fs
+ .readdirSync(checksumsDir)
+ .filter((f) => /^checksums-.*\.env$/.test(f))
+ .sort()
+ .map((f) => path.join(checksumsDir, f));
+ opts.checksums.push(...found);
}
if (!opts.package || !opts.version || opts.checksums.length === 0) {
- console.error(
- "Usage: generate-spm-manifest.mjs --package --version --checksums [--checksums ] [--strict]"
- );
+ console.error(USAGE);
process.exit(1);
}
+console.log(`Using checksum files: ${opts.checksums.join(", ")}`);
// The version is interpolated into Swift source and into a release URL; accept
// semver (with optional prerelease) and nothing else.
@@ -101,7 +142,7 @@ if (presentVision.length !== 0 && presentVision.length !== VISION_KEYS.length) {
const includeVision = presentVision.length === VISION_KEYS.length;
if (opts.strict && !includeVision) {
console.error(
- "ERROR: --strict requires the visionOS checksums (a non-next release must ship the full manifest)"
+ "ERROR: this channel requires the visionOS checksums (a non-next release must ship the full manifest)"
);
process.exit(1);
}
diff --git a/scripts/generate-spm-probe.mjs b/scripts/generate-spm-probe.mjs
index 2b07e55d..685f6c53 100644
--- a/scripts/generate-spm-probe.mjs
+++ b/scripts/generate-spm-probe.mjs
@@ -11,35 +11,79 @@
// manifests only declare the iOS artifacts, real releases also declare (and
// therefore also verify) the visionOS ones.
//
-// Usage: node scripts/generate-spm-probe.mjs --version [--dir spmverify]
+// --assert-release-manifest additionally fetches the released manifest and
+// asserts it pins nsVersion to this exact version. This guards against the tag
+// pointing at a STALE manifest — e.g. a commit/tag step that silently failed
+// and left a prior version's nsVersion + artifact URLs/checksums in place.
+// Those still resolve and checksum-pass (they're internally consistent), so
+// `swift package resolve` won't catch it; the explicit assertion does.
import fs from "node:fs";
import path from "node:path";
+import { parseArgs } from "node:util";
-const args = process.argv.slice(2);
-const opts = { dir: "spmverify" };
-for (let i = 0; i < args.length; i++) {
- const a = args[i];
- if (a === "--version") opts.version = args[++i];
- else if (a === "--dir") opts.dir = args[++i];
- else {
- console.error(`Unknown argument: ${a}`);
- process.exit(1);
- }
+const USAGE = `Usage: node scripts/generate-spm-probe.mjs --version [--dir ] [--assert-release-manifest]
+
+ --version released ios-spm version to pin (exact)
+ --dir output directory for the probe package (default: spmverify)
+ --assert-release-manifest fetch the released ios-spm manifest and assert it
+ pins nsVersion to this exact version
+ -h, --help show this help`;
+
+let values;
+try {
+ ({ values } = parseArgs({
+ options: {
+ version: { type: "string" },
+ dir: { type: "string", default: "spmverify" },
+ "assert-release-manifest": { type: "boolean", default: false },
+ help: { type: "boolean", short: "h", default: false },
+ },
+ }));
+} catch (e) {
+ console.error(e.message);
+ console.error(USAGE);
+ process.exit(1);
+}
+if (values.help) {
+ console.log(USAGE);
+ process.exit(0);
}
-if (!opts.version) {
- console.error("Usage: generate-spm-probe.mjs --version [--dir ]");
+if (!values.version) {
+ console.error(USAGE);
process.exit(1);
}
// The version is interpolated into Swift source; accept semver (with optional
// prerelease) and nothing else.
const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
-if (!VERSION_RE.test(opts.version)) {
- console.error(`ERROR: "${opts.version}" is not a valid release version`);
+if (!VERSION_RE.test(values.version)) {
+ console.error(`ERROR: "${values.version}" is not a valid release version`);
process.exit(1);
}
+if (values["assert-release-manifest"]) {
+ const url = `https://raw.githubusercontent.com/NativeScript/ios-spm/${values.version}/Package.swift`;
+ const res = await fetch(url);
+ if (!res.ok) {
+ console.error(
+ `ERROR: could not fetch ${url} (HTTP ${res.status}) — was ios-spm tagged for this release?`
+ );
+ process.exit(1);
+ }
+ const releasedManifest = await res.text();
+ if (!releasedManifest.includes(`let nsVersion = "${values.version}"`)) {
+ console.error(
+ `ERROR: ios-spm@${values.version} does not pin nsVersion="${values.version}" (stale manifest?). Found:`
+ );
+ for (const line of releasedManifest.split("\n")) {
+ if (line.includes("nsVersion")) console.error(` ${line.trim()}`);
+ }
+ process.exit(1);
+ }
+ console.log(`OK: ios-spm@${values.version} pins nsVersion="${values.version}"`);
+}
+
const manifest = `// swift-tools-version: 5.10
import PackageDescription
@@ -47,7 +91,7 @@ let package = Package(
name: "Probe",
platforms: [.iOS(.v13)],
dependencies: [
- .package(url: "https://github.com/NativeScript/ios-spm.git", exact: "${opts.version}")
+ .package(url: "https://github.com/NativeScript/ios-spm.git", exact: "${values.version}")
],
targets: [
.target(name: "Probe", dependencies: [.product(name: "NativeScript", package: "ios-spm")])
@@ -55,11 +99,11 @@ let package = Package(
)
`;
-fs.mkdirSync(path.join(opts.dir, "Sources", "Probe"), { recursive: true });
-fs.writeFileSync(path.join(opts.dir, "Package.swift"), manifest);
+fs.mkdirSync(path.join(values.dir, "Sources", "Probe"), { recursive: true });
+fs.writeFileSync(path.join(values.dir, "Package.swift"), manifest);
fs.writeFileSync(
- path.join(opts.dir, "Sources", "Probe", "Probe.swift"),
+ path.join(values.dir, "Sources", "Probe", "Probe.swift"),
"// probe\n"
);
-console.log(`Generated probe package in ${opts.dir} (ios-spm exact: ${opts.version})`);
+console.log(`Generated probe package in ${values.dir} (ios-spm exact: ${values.version})`);
diff --git a/scripts/get-next-version.js b/scripts/get-next-version.js
index ac391288..04fdf722 100644
--- a/scripts/get-next-version.js
+++ b/scripts/get-next-version.js
@@ -2,14 +2,40 @@ const semver = require("semver");
const child_process = require("child_process");
const dayjs = require("dayjs");
const fs = require("fs");
+const { parseArgs } = require("node:util");
+
+const USAGE = `Usage: node scripts/get-next-version.js [--version ] [--tag ]
+
+ --version base version (default: $NPM_VERSION, then package.json)
+ --tag prerelease channel, e.g. next or pr (default: $NPM_TAG, then next)
+ -h, --help show this help`;
+
+let values;
+try {
+ ({ values } = parseArgs({
+ options: {
+ version: { type: "string" },
+ tag: { type: "string" },
+ help: { type: "boolean", short: "h", default: false },
+ },
+ }));
+} catch (e) {
+ console.error(e.message);
+ console.error(USAGE);
+ process.exit(1);
+}
+if (values.help) {
+ console.log(USAGE);
+ process.exit(0);
+}
const currentVersion =
- process.env.NPM_VERSION || require("../package.json").version;
+ values.version || process.env.NPM_VERSION || require("../package.json").version;
if (!currentVersion) {
throw new Error("Invalid current version");
}
-const currentTag = process.env.NPM_TAG || "next";
+const currentTag = values.tag || process.env.NPM_TAG || "next";
const runID = process.env.GITHUB_RUN_ID || 0;
let prPrerelease = "";
diff --git a/scripts/get-npm-tag.js b/scripts/get-npm-tag.js
index 68cd158c..b42c2ad6 100644
--- a/scripts/get-npm-tag.js
+++ b/scripts/get-npm-tag.js
@@ -1,7 +1,32 @@
const semver = require("semver");
+const { parseArgs } = require("node:util");
+
+const USAGE = `Usage: node scripts/get-npm-tag.js [--version ]
+
+ --version version to derive the npm dist-tag from
+ (default: $NPM_VERSION, then package.json)
+ -h, --help show this help`;
+
+let values;
+try {
+ ({ values } = parseArgs({
+ options: {
+ version: { type: "string" },
+ help: { type: "boolean", short: "h", default: false },
+ },
+ }));
+} catch (e) {
+ console.error(e.message);
+ console.error(USAGE);
+ process.exit(1);
+}
+if (values.help) {
+ console.log(USAGE);
+ process.exit(0);
+}
const currentVersion =
- process.env.NPM_VERSION || require("../package.json").version;
+ values.version || process.env.NPM_VERSION || require("../package.json").version;
function validateNpmTag(version) {
const parsed = semver.parse(version);
diff --git a/scripts/install-ci-deps.sh b/scripts/install-ci-deps.sh
new file mode 100755
index 00000000..b3821744
--- /dev/null
+++ b/scripts/install-ci-deps.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+# CI-only: install the dependencies shared by the workflow build/test jobs.
+# Invoked by the setup-build-env composite action (.github/actions/setup-build-env).
+set -e
+
+usage() {
+ echo "Usage: ./scripts/install-ci-deps.sh [--test-tools]"
+ echo ""
+ echo " --test-tools also install xcparse and the JUnit report tools used by the"
+ echo " test jobs"
+ echo " -h, --help show this help"
+}
+
+TEST_TOOLS=0
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --test-tools)
+ TEST_TOOLS=1
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown argument: $1" >&2
+ usage >&2
+ exit 1
+ ;;
+ esac
+done
+
+npm install
+python3 -m pip install --upgrade pip six
+
+# Ensure CMake is available without conflicting with pinned Homebrew formula
+if ! command -v cmake >/dev/null; then
+ brew list cmake || brew install cmake
+fi
+# Some scripts expect cmake at /usr/local/bin; create a shim if needed
+if [ ! -x /usr/local/bin/cmake ]; then
+ sudo mkdir -p /usr/local/bin
+ sudo ln -sf "$(command -v cmake)" /usr/local/bin/cmake
+fi
+
+if [ "$TEST_TOOLS" = "1" ]; then
+ brew install chargepoint/xcparse/xcparse
+ npm install -g @edusperoni/junit-cli-report-viewer verify-junit-xml
+fi
diff --git a/scripts/resolve-release.mjs b/scripts/resolve-release.mjs
new file mode 100644
index 00000000..fb7a8dc9
--- /dev/null
+++ b/scripts/resolve-release.mjs
@@ -0,0 +1,92 @@
+#!/usr/bin/env node
+// Resolve the release identity for the npm_release workflow's `setup` job and
+// write it to $GITHUB_OUTPUT: NPM_VERSION, NPM_TAG, BUILD_MATRIX.
+//
+// Trigger shapes:
+// * workflow_dispatch with a version -> that version (leading "v" allowed);
+// publishes "latest" unless the version is a prerelease (get-npm-tag.js)
+// * push of a v* tag -> version from the tag, which is
+// authoritative and must match package.json so a release can't drift
+// * push to main / dispatch w/o input -> rolling "next" prerelease
+// (get-next-version.js)
+//
+// The build matrix: ios always builds/publishes; visionos only for real
+// releases (npm tag != "next"), never for the rolling next channel. Each entry
+// carries `target` (package identity, @nativescript/) and `script`
+// (the npm build script; note the vision script is build-vision).
+//
+// GITHUB_REF and GITHUB_OUTPUT are read from the environment (GitHub's
+// contract); the workflow_dispatch version input is a real argument.
+import { execFileSync } from "node:child_process";
+import fs from "node:fs";
+import path from "node:path";
+import { parseArgs } from "node:util";
+import { fileURLToPath } from "node:url";
+
+const USAGE = `Usage: node scripts/resolve-release.mjs [--version ]
+
+ --version release version to cut (leading "v" allowed); empty or omitted
+ resolves from GITHUB_REF (v* tag) or falls back to a rolling
+ "next" prerelease
+ -h, --help show this help`;
+
+let values;
+try {
+ ({ values } = parseArgs({
+ options: {
+ version: { type: "string", default: "" },
+ help: { type: "boolean", short: "h", default: false },
+ },
+ }));
+} catch (e) {
+ console.error(e.message);
+ console.error(USAGE);
+ process.exit(1);
+}
+if (values.help) {
+ console.log(USAGE);
+ process.exit(0);
+}
+
+const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
+const runScript = (script, args = []) =>
+ execFileSync(process.execPath, [path.join(scriptsDir, script), ...args])
+ .toString()
+ .trim();
+
+const inputVersion = values.version.trim();
+const ref = process.env.GITHUB_REF || "";
+
+let version;
+if (inputVersion) {
+ version = inputVersion.replace(/^v/, "");
+} else if (ref.startsWith("refs/tags/")) {
+ version = ref.slice("refs/tags/".length).replace(/^v/, "");
+ const pkgVersion = JSON.parse(
+ fs.readFileSync(path.join(scriptsDir, "..", "package.json"), "utf8")
+ ).version;
+ if (version !== pkgVersion) {
+ console.error(
+ `::error::Tag v${version} does not match package.json version ${pkgVersion}. Bump package.json before tagging.`
+ );
+ process.exit(1);
+ }
+} else {
+ version = runScript("get-next-version.js");
+}
+
+const tag = runScript("get-npm-tag.js", ["--version", version]);
+
+const targets = [{ target: "ios", script: "build-ios" }];
+if (tag !== "next") {
+ targets.push({ target: "visionos", script: "build-vision" });
+}
+const matrix = JSON.stringify({ include: targets });
+
+if (process.env.GITHUB_OUTPUT) {
+ fs.appendFileSync(
+ process.env.GITHUB_OUTPUT,
+ `NPM_VERSION=${version}\nNPM_TAG=${tag}\nBUILD_MATRIX=${matrix}\n`
+ );
+}
+console.log(`Resolved ${version} (tag: ${tag}); build targets: ${matrix}`);
diff --git a/scripts/stamp-template-local-spm.mjs b/scripts/stamp-template-local-spm.mjs
index 1264a051..13a3beb7 100644
--- a/scripts/stamp-template-local-spm.mjs
+++ b/scripts/stamp-template-local-spm.mjs
@@ -1,37 +1,56 @@
#!/usr/bin/env node
-// Stamp a packaged project template to consume the runtime via a LOCAL SwiftPM
-// package instead of the released ios-spm tag (the D6 "dev/offline override"
-// from SPM_DISTRIBUTION_PLAN.md).
+// Stamp a packaged project template to consume the runtime from the local
+// SwiftPM package embedded in the npm package (framework/internal/local-spm)
+// instead of the released ios-spm tag.
//
-// Local `npm run build-ios` produces xcframeworks in dist/ but no ios-spm
-// release tag, so the template's XCRemoteSwiftPackageReference (exactVersion
-// pinned by stamp-template-version.mjs) can never resolve. This script rewrites
-// that reference into an XCLocalSwiftPackageReference pointing at the local-spm
-// package the build just created (dist/local-spm — Package.swift + binary
-// targets over the built xcframeworks).
-//
-// The resulting npm package is machine-specific (it embeds an absolute path
-// into this checkout) and is only meant for `ns platform add ios
-// --framework-path=dist/nativescript-ios-.tgz` style local testing.
-//
-// Usage: node scripts/stamp-template-local-spm.mjs
+// Replaces the template's XCRemoteSwiftPackageReference (which pins
+// github.com/NativeScript/ios-spm at __NS_RUNTIME_VERSION__, see
+// stamp-template-version.mjs) with an XCLocalSwiftPackageReference at the
+// given relative path. Xcode resolves that path relative to the project
+// directory, so "internal/local-spm" keeps resolving after the {N} CLI copies
+// the template into an app's platforms folder.
import fs from "node:fs";
import path from "node:path";
+import { parseArgs } from "node:util";
-const [, , pbxPath, localSpmPath] = process.argv;
+const USAGE = `Usage: node scripts/stamp-template-local-spm.mjs [--package-dir ]
-if (!pbxPath || !localSpmPath) {
- console.error("Usage: stamp-template-local-spm.mjs ");
+ packaged template project to stamp
+ path Xcode resolves relative to the project directory,
+ e.g. "internal/local-spm"
+ --package-dir on-disk location of the package being referenced; when
+ given, it must contain a Package.swift (sanity check that
+ the packaging step actually embedded the package)
+ -h, --help show this help`;
+
+let values, positionals;
+try {
+ ({ values, positionals } = parseArgs({
+ options: {
+ "package-dir": { type: "string" },
+ help: { type: "boolean", short: "h", default: false },
+ },
+ allowPositionals: true,
+ }));
+} catch (e) {
+ console.error(e.message);
+ console.error(USAGE);
process.exit(1);
}
+if (values.help) {
+ console.log(USAGE);
+ process.exit(0);
+}
-if (!path.isAbsolute(localSpmPath)) {
- console.error(`local-spm path must be absolute, got: ${localSpmPath}`);
+const [pbxPath, stampPath] = positionals;
+if (!pbxPath || !stampPath || positionals.length > 2) {
+ console.error(USAGE);
process.exit(1);
}
-if (!fs.existsSync(path.join(localSpmPath, "Package.swift"))) {
- console.error(`No Package.swift found in ${localSpmPath} — run the runtime build first.`);
+const packageDir = values["package-dir"];
+if (packageDir && !fs.existsSync(path.join(packageDir, "Package.swift"))) {
+ console.error(`No Package.swift found in ${packageDir} — was the local SwiftPM package embedded?`);
process.exit(1);
}
@@ -59,7 +78,7 @@ contents = contents.replace(
`/* Begin XCLocalSwiftPackageReference section */
\t\t${refUuid} /* XCLocalSwiftPackageReference "local-spm" */ = {
\t\t\tisa = XCLocalSwiftPackageReference;
-\t\t\trelativePath = ${JSON.stringify(localSpmPath)};
+\t\t\trelativePath = ${JSON.stringify(stampPath)};
\t\t};
/* End XCLocalSwiftPackageReference section */`
);
@@ -69,4 +88,4 @@ contents = contents.replace(
contents = contents.split(`${refUuid} /* XCRemoteSwiftPackageReference "ios-spm" */`).join(`${refUuid} /* XCLocalSwiftPackageReference "local-spm" */`);
fs.writeFileSync(pbxPath, contents);
-console.log(`Stamped ${pbxPath} → local SwiftPM package at ${localSpmPath}`);
+console.log(`Stamped ${pbxPath} → local SwiftPM package at ${stampPath}`);
diff --git a/scripts/stamp-template-version.mjs b/scripts/stamp-template-version.mjs
index bff2b32f..3422b8df 100644
--- a/scripts/stamp-template-version.mjs
+++ b/scripts/stamp-template-version.mjs
@@ -6,14 +6,34 @@
// with the concrete version being published, so `@nativescript/ios@X` always
// resolves the matching `ios-spm` tag `X` (and therefore the xcframework built
// for X). The {N} CLI copies the stamped template verbatim — no CLI change.
-//
-// Usage: node scripts/stamp-template-version.mjs
import fs from "node:fs";
+import { parseArgs } from "node:util";
+
+const USAGE = `Usage: node scripts/stamp-template-version.mjs
+
+ packaged template project to stamp
+ version to pin the ios-spm reference to (exactVersion)
+ -h, --help show this help`;
-const [, , pbxPath, version] = process.argv;
+let values, positionals;
+try {
+ ({ values, positionals } = parseArgs({
+ options: { help: { type: "boolean", short: "h", default: false } },
+ allowPositionals: true,
+ }));
+} catch (e) {
+ console.error(e.message);
+ console.error(USAGE);
+ process.exit(1);
+}
+if (values.help) {
+ console.log(USAGE);
+ process.exit(0);
+}
-if (!pbxPath || !version) {
- console.error("Usage: stamp-template-version.mjs ");
+const [pbxPath, version] = positionals;
+if (!pbxPath || !version || positionals.length > 2) {
+ console.error(USAGE);
process.exit(1);
}
diff --git a/spm-templates/local-spm-ios/Package.swift b/spm-templates/local-spm-ios/Package.swift
new file mode 100644
index 00000000..c73bd8bf
--- /dev/null
+++ b/spm-templates/local-spm-ios/Package.swift
@@ -0,0 +1,29 @@
+// swift-tools-version: 5.10
+// Local SwiftPM package embedded in @nativescript/ios packages built with
+// NS_SPM_MODE=embedded (the default outside the release pipeline). Same
+// product shape as the released github.com/NativeScript/ios-spm manifest, but
+// the binary targets point at the xcframework zips packed next to this
+// manifest (framework/internal/local-spm), so the npm package is fully
+// self-contained and portable.
+//
+// The frameworks are zipped because npm strips symlinks (the Mac Catalyst
+// slices contain them); SwiftPM extracts local zip binary targets itself.
+import PackageDescription
+
+let package = Package(
+ name: "NativeScriptSDK",
+ platforms: [
+ .iOS(.v13),
+ .macCatalyst(.v13),
+ ],
+ products: [
+ // iOS family (iphoneos + iphonesimulator + Mac Catalyst)
+ .library(name: "NativeScript", targets: ["NativeScript", "TKLiveSync"]),
+ // Backwards-compatible alias for the historical product name.
+ .library(name: "NativeScriptSDK", targets: ["NativeScript", "TKLiveSync"]),
+ ],
+ targets: [
+ .binaryTarget(name: "NativeScript", path: "NativeScript.xcframework.zip"),
+ .binaryTarget(name: "TKLiveSync", path: "TKLiveSync.xcframework.zip"),
+ ]
+)
diff --git a/spm-templates/local-spm-visionos/Package.swift b/spm-templates/local-spm-visionos/Package.swift
new file mode 100644
index 00000000..a420da83
--- /dev/null
+++ b/spm-templates/local-spm-visionos/Package.swift
@@ -0,0 +1,28 @@
+// swift-tools-version: 5.10
+// Local SwiftPM package embedded in @nativescript/visionos packages built with
+// NS_SPM_MODE=embedded (the default outside the release pipeline). Product and
+// target names mirror the visionOS half of the released
+// github.com/NativeScript/ios-spm manifest (see generate-spm-manifest.mjs),
+// but the binary targets point at the xcframework zips packed next to this
+// manifest, so the npm package is fully self-contained and portable.
+//
+// The frameworks are zipped because npm strips symlinks; SwiftPM extracts
+// local zip binary targets itself.
+import PackageDescription
+
+let package = Package(
+ name: "NativeScriptSDK",
+ platforms: [
+ .iOS(.v13),
+ .macCatalyst(.v13),
+ .visionOS(.v1),
+ ],
+ products: [
+ // visionOS family (xros + xrsimulator)
+ .library(name: "NativeScriptVisionOS", targets: ["NativeScriptVisionOS", "TKLiveSyncVisionOS"]),
+ ],
+ targets: [
+ .binaryTarget(name: "NativeScriptVisionOS", path: "NativeScript.visionos.xcframework.zip"),
+ .binaryTarget(name: "TKLiveSyncVisionOS", path: "TKLiveSync.visionos.xcframework.zip"),
+ ]
+)