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"), + ] +)