diff --git a/.changeset/quiet-links-stay.md b/.changeset/quiet-links-stay.md new file mode 100644 index 000000000..e70d36d81 --- /dev/null +++ b/.changeset/quiet-links-stay.md @@ -0,0 +1,6 @@ +--- +'@rock-js/plugin-brownfield-ios': patch +'@rock-js/platform-apple-helpers': patch +--- + +Preserve relative symlinks when copying XCFrameworks during `package:ios`. `fs.cpSync` rewrote them into absolute paths on the packaging machine, so versioned framework slices (such as the Mac Catalyst slice of `hermesvm.xcframework`) shipped symlinks pointing outside the bundle, which SwiftPM refuses to extract from a binary target archive. diff --git a/packages/platform-apple-helpers/src/lib/utils/mergeFrameworks.ts b/packages/platform-apple-helpers/src/lib/utils/mergeFrameworks.ts index b7555e7c1..964b436e9 100644 --- a/packages/platform-apple-helpers/src/lib/utils/mergeFrameworks.ts +++ b/packages/platform-apple-helpers/src/lib/utils/mergeFrameworks.ts @@ -24,7 +24,11 @@ function copyFile(sourcePath: string, destinationPath: string) { // Copy an entire directory tree, overwriting existing content. function copyDirectory(sourcePath: string, destinationPath: string) { ensureDirectory(path.dirname(destinationPath)); - fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true }); + fs.cpSync(sourcePath, destinationPath, { + recursive: true, + force: true, + verbatimSymlinks: true, + }); } // Generate a minimal Info.plist for a synthesized framework wrapper. diff --git a/packages/plugin-brownfield-ios/src/lib/__tests__/copyHermesXcframework.test.ts b/packages/plugin-brownfield-ios/src/lib/__tests__/copyHermesXcframework.test.ts new file mode 100644 index 000000000..d68a3e645 --- /dev/null +++ b/packages/plugin-brownfield-ios/src/lib/__tests__/copyHermesXcframework.test.ts @@ -0,0 +1,71 @@ +import type * as Fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import type * as Tools from '@rock-js/tools'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.unmock('node:fs'); + +const fs = await vi.importActual('node:fs'); + +vi.mock('@rock-js/tools', async (importOriginal) => ({ + ...(await importOriginal()), + spinner: () => ({ start: vi.fn(), stop: vi.fn() }), +})); + +const { copyHermesXcframework } = await import('../copyHermesXcframework.js'); + +const CATALYST_FRAMEWORK = + 'hermesvm.xcframework/ios-arm64_x86_64-maccatalyst/hermesvm.framework'; + +describe('copyHermesXcframework', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'copy-hermes-test-')); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it('keeps relative symlinks of versioned framework slices relative', () => { + const sourceDir = path.join(tempDir, 'ios'); + const destinationDir = path.join(tempDir, 'package'); + const sourceFramework = path.join( + sourceDir, + 'Pods/hermes-engine/destroot/Library/Frameworks/universal', + CATALYST_FRAMEWORK, + ); + fs.mkdirSync(path.join(sourceFramework, 'Versions/1/Resources'), { + recursive: true, + }); + fs.writeFileSync(path.join(sourceFramework, 'Versions/1/hermesvm'), ''); + fs.symlinkSync('1', path.join(sourceFramework, 'Versions/Current')); + fs.symlinkSync( + 'Versions/Current/hermesvm', + path.join(sourceFramework, 'hermesvm'), + ); + fs.symlinkSync( + 'Versions/Current/Resources', + path.join(sourceFramework, 'Resources'), + ); + + copyHermesXcframework({ + sourceDir, + destinationDir, + reactNativeVersion: '0.82.0', + }); + + const copiedFramework = path.join(destinationDir, CATALYST_FRAMEWORK); + expect( + fs.readlinkSync(path.join(copiedFramework, 'Versions/Current')), + ).toBe('1'); + expect(fs.readlinkSync(path.join(copiedFramework, 'hermesvm'))).toBe( + 'Versions/Current/hermesvm', + ); + expect(fs.readlinkSync(path.join(copiedFramework, 'Resources'))).toBe( + 'Versions/Current/Resources', + ); + }); +}); diff --git a/packages/plugin-brownfield-ios/src/lib/copyHermesXcframework.ts b/packages/plugin-brownfield-ios/src/lib/copyHermesXcframework.ts index f15993466..dfa90cd95 100644 --- a/packages/plugin-brownfield-ios/src/lib/copyHermesXcframework.ts +++ b/packages/plugin-brownfield-ios/src/lib/copyHermesXcframework.ts @@ -31,7 +31,7 @@ export function copyHermesXcframework({ `Pods/hermes-engine/destroot/Library/Frameworks/universal/${hermesFrameworkName}`, ), hermesDestination, - { recursive: true, force: true }, + { recursive: true, force: true, verbatimSymlinks: true }, ); loader.stop(`Copied ${color.bold(hermesFrameworkName)}`); diff --git a/packages/plugin-brownfield-ios/src/lib/copyReactXcframeworks.ts b/packages/plugin-brownfield-ios/src/lib/copyReactXcframeworks.ts index 181b98e02..46be52143 100644 --- a/packages/plugin-brownfield-ios/src/lib/copyReactXcframeworks.ts +++ b/packages/plugin-brownfield-ios/src/lib/copyReactXcframeworks.ts @@ -35,6 +35,7 @@ export function copyReactXcframeworks({ fs.cpSync(reactFrameworkSource, reactDestination, { recursive: true, force: true, + verbatimSymlinks: true, }); loader.stop(`Copied ${color.bold(react)}`); @@ -56,6 +57,7 @@ export function copyReactXcframeworks({ fs.cpSync(reactNativeDepsSource, reactNativeDepsDestination, { recursive: true, force: true, + verbatimSymlinks: true, }); loader.stop(`Copied ${color.bold(reactNativeDependencies)}`);