From d70e7309e24cb95fc7d831780434c3b57d37bd4a Mon Sep 17 00:00:00 2001 From: Jamie Birch <14055146+shirakaba@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:47:20 +0900 Subject: [PATCH 1/5] handle Info.plist lookup in versioned frameworks --- packages/host/src/node/cli/apple.ts | 30 ++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index 92fe6468..e5b220da 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -13,7 +13,7 @@ import { } from "./link-modules.js"; type UpdateInfoPlistOptions = { - filePath: string; + frameworkPath: string; oldLibraryName: string; newLibraryName: string; }; @@ -22,17 +22,37 @@ type UpdateInfoPlistOptions = { * Update the Info.plist file of an xcframework to use the new library name. */ export async function updateInfoPlist({ - filePath, + frameworkPath, oldLibraryName, newLibraryName, }: UpdateInfoPlistOptions) { - const infoPlistContents = await fs.promises.readFile(filePath, "utf-8"); + // First, assume it is an "unversioned" framework that keeps its Info.plist in + // the root. This is the convention for iOS, tvOS, and friends. + let infoPlistPath = path.join(frameworkPath, "Info.plist"); + + let infoPlistContents: string; + try { + infoPlistContents = await fs.promises.readFile(infoPlistPath, "utf-8"); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") { + // Next, assume it is a "versioned" framework that keeps its Info.plist + // under a subdirectory. This is the convention for macOS. + infoPlistPath = path.join( + frameworkPath, + "Versions/Current/Resources/Info.plist", + ); + infoPlistContents = await fs.promises.readFile(infoPlistPath, "utf-8"); + } + + throw error; + } + // TODO: Use a proper plist parser const updatedContents = infoPlistContents.replaceAll( oldLibraryName, newLibraryName, ); - await fs.promises.writeFile(filePath, updatedContents, "utf-8"); + await fs.promises.writeFile(infoPlistPath, updatedContents, "utf-8"); } export async function linkXcframework({ @@ -126,7 +146,7 @@ export async function linkXcframework({ ); // Update the Info.plist file for the framework await updateInfoPlist({ - filePath: path.join(newFrameworkPath, "Info.plist"), + frameworkPath: newFrameworkPath, oldLibraryName, newLibraryName, }); From 08389fe1218eff74c0bd04791920a551da18b470 Mon Sep 17 00:00:00 2001 From: Jamie Birch <14055146+shirakaba@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:55:59 +0900 Subject: [PATCH 2/5] whoops --- packages/host/src/node/cli/apple.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index e5b220da..0607b311 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -42,9 +42,9 @@ export async function updateInfoPlist({ "Versions/Current/Resources/Info.plist", ); infoPlistContents = await fs.promises.readFile(infoPlistPath, "utf-8"); + } else { + throw error; } - - throw error; } // TODO: Use a proper plist parser From 6442d54ed5b8e99a6a9bfdd8afa70b852cc64f82 Mon Sep 17 00:00:00 2001 From: Jamie Birch <14055146+shirakaba@users.noreply.github.com> Date: Sun, 12 Oct 2025 12:06:11 +0900 Subject: [PATCH 3/5] extract out readInfoPlist() helper --- packages/host/src/node/cli/apple.ts | 47 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index 0607b311..caacceec 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -12,6 +12,27 @@ import { LinkModuleResult, } from "./link-modules.js"; +/** + * Resolves the Info.plist file within a framework and reads its contents. + */ +export async function readInfoPlist(frameworkPath: string) { + // First, assume it is an "unversioned" framework that keeps its Info.plist in + // the root. This is the convention for iOS, tvOS, and friends. + let infoPlistPath = path.join(frameworkPath, "Info.plist"); + + if (!fs.existsSync(infoPlistPath)) { + // Next, assume it is a "versioned" framework that keeps its Info.plist + // under a subdirectory. This is the convention for macOS. + infoPlistPath = path.join( + frameworkPath, + "Versions/Current/Resources/Info.plist", + ); + } + + const contents = await fs.promises.readFile(infoPlistPath, "utf-8"); + return { infoPlistPath, contents }; +} + type UpdateInfoPlistOptions = { frameworkPath: string; oldLibraryName: string; @@ -26,32 +47,10 @@ export async function updateInfoPlist({ oldLibraryName, newLibraryName, }: UpdateInfoPlistOptions) { - // First, assume it is an "unversioned" framework that keeps its Info.plist in - // the root. This is the convention for iOS, tvOS, and friends. - let infoPlistPath = path.join(frameworkPath, "Info.plist"); - - let infoPlistContents: string; - try { - infoPlistContents = await fs.promises.readFile(infoPlistPath, "utf-8"); - } catch (error) { - if (error instanceof Error && "code" in error && error.code === "ENOENT") { - // Next, assume it is a "versioned" framework that keeps its Info.plist - // under a subdirectory. This is the convention for macOS. - infoPlistPath = path.join( - frameworkPath, - "Versions/Current/Resources/Info.plist", - ); - infoPlistContents = await fs.promises.readFile(infoPlistPath, "utf-8"); - } else { - throw error; - } - } + const { infoPlistPath, contents } = await readInfoPlist(frameworkPath); // TODO: Use a proper plist parser - const updatedContents = infoPlistContents.replaceAll( - oldLibraryName, - newLibraryName, - ); + const updatedContents = contents.replaceAll(oldLibraryName, newLibraryName); await fs.promises.writeFile(infoPlistPath, updatedContents, "utf-8"); } From f9671b022f67286198f4b86186cc70c94c2fb488 Mon Sep 17 00:00:00 2001 From: Jamie Birch <14055146+shirakaba@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:02:54 +0900 Subject: [PATCH 4/5] rethrow with context --- packages/host/src/node/cli/apple.ts | 58 ++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index caacceec..20a9065c 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -12,24 +12,62 @@ import { LinkModuleResult, } from "./link-modules.js"; +function determineInfoPlistPath(frameworkPath: string) { + const checkedPaths = new Array(); + + // First, assume it is an "unversioned" framework that keeps its Info.plist in + // the root. This is the convention for iOS, tvOS, and friends. + let infoPlistPath = path.join(frameworkPath, "Info.plist"); + + if (fs.existsSync(infoPlistPath)) { + return infoPlistPath; + } + checkedPaths.push(infoPlistPath); + + // Next, assume it is a "versioned" framework that keeps its Info.plist + // under a subdirectory. This is the convention for macOS. + infoPlistPath = path.join( + frameworkPath, + "Versions/Current/Resources/Info.plist", + ); + + if (fs.existsSync(infoPlistPath)) { + return infoPlistPath; + } + checkedPaths.push(infoPlistPath); + + throw new Error( + [ + `Unable to locate an Info.plist file within framework. Checked the following paths:`, + ...checkedPaths.map((checkedPath) => `- ${checkedPath}`), + ].join("\n"), + ); +} + /** * Resolves the Info.plist file within a framework and reads its contents. */ export async function readInfoPlist(frameworkPath: string) { - // First, assume it is an "unversioned" framework that keeps its Info.plist in - // the root. This is the convention for iOS, tvOS, and friends. - let infoPlistPath = path.join(frameworkPath, "Info.plist"); + let infoPlistPath: string; + try { + infoPlistPath = determineInfoPlistPath(frameworkPath); + } catch (cause) { + throw new Error( + `Unable to read Info.plist for framework at path "${frameworkPath}", as an Info.plist file couldn't be found.`, + { cause }, + ); + } - if (!fs.existsSync(infoPlistPath)) { - // Next, assume it is a "versioned" framework that keeps its Info.plist - // under a subdirectory. This is the convention for macOS. - infoPlistPath = path.join( - frameworkPath, - "Versions/Current/Resources/Info.plist", + let contents: string; + try { + contents = await fs.promises.readFile(infoPlistPath, "utf-8"); + } catch (cause) { + throw new Error( + `Unable to read Info.plist for framework at path "${frameworkPath}", due to a file system error.`, + { cause }, ); } - const contents = await fs.promises.readFile(infoPlistPath, "utf-8"); return { infoPlistPath, contents }; } From 06e0b91baeb006181c36ebca3991b98de7700068 Mon Sep 17 00:00:00 2001 From: Jamie Birch <14055146+shirakaba@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:09:57 +0900 Subject: [PATCH 5/5] add tests --- packages/host/src/node/cli/apple.test.ts | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/host/src/node/cli/apple.test.ts diff --git a/packages/host/src/node/cli/apple.test.ts b/packages/host/src/node/cli/apple.test.ts new file mode 100644 index 00000000..191baafe --- /dev/null +++ b/packages/host/src/node/cli/apple.test.ts @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import path from "node:path"; +import { readInfoPlist } from "./apple"; +import { setupTempDirectory } from "../test-utils"; + +describe("apple", () => { + describe("Info.plist lookup", () => { + it("should find Info.plist files in unversioned frameworks", async (context) => { + const infoPlistContents = `...`; + const infoPlistSubPath = "Info.plist"; + const tempDirectoryPath = setupTempDirectory(context, { + [infoPlistSubPath]: infoPlistContents, + }); + + const result = await readInfoPlist(tempDirectoryPath); + + assert.strictEqual(result.contents, infoPlistContents); + assert.strictEqual( + result.infoPlistPath, + path.join(tempDirectoryPath, infoPlistSubPath), + ); + }); + + it("should find Info.plist files in versioned frameworks", async (context) => { + const infoPlistContents = `...`; + const infoPlistSubPath = "Versions/Current/Resources/Info.plist"; + const tempDirectoryPath = setupTempDirectory(context, { + [infoPlistSubPath]: infoPlistContents, + }); + + const result = await readInfoPlist(tempDirectoryPath); + + assert.strictEqual(result.contents, infoPlistContents); + assert.strictEqual( + result.infoPlistPath, + path.join(tempDirectoryPath, infoPlistSubPath), + ); + }); + + it("should throw if Info.plist is missing from framework", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, {}); + + await assert.rejects( + async () => readInfoPlist(tempDirectoryPath), + /Unable to read Info.plist for framework at path ".*?", as an Info.plist file couldn't be found./, + ); + }); + }); +});