Skip to content

Commit 58996f0

Browse files
committed
Copy and sign framework from xcode build phase
1 parent c2ed982 commit 58996f0

File tree

3 files changed

+191
-78
lines changed

3 files changed

+191
-78
lines changed

packages/host/src/node/cli/apple.ts

Lines changed: 172 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import * as xcode from "@bacons/xcode";
77
import * as xcodeJson from "@bacons/xcode/json";
88
import * as zod from "zod";
99

10-
import { chalk, spawn } from "@react-native-node-api/cli-utils";
10+
import { assertFixable, chalk, spawn } from "@react-native-node-api/cli-utils";
1111

1212
import { getLatestMtime, getLibraryName } from "../path-utils.js";
1313
import {
1414
getLinkedModuleOutputPath,
1515
LinkModuleOptions,
1616
LinkModuleResult,
17+
ModuleLinker,
1718
} from "./link-modules.js";
18-
import { findXcodeProject } from "./xcode-helpers.js";
19+
import { findXcodeProject, getBuildDirPath } from "./xcode-helpers.js";
1920

2021
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..", "..");
2122
const CLI_PATH = path.resolve(PACKAGE_ROOT, "bin", "react-native-node-api.mjs");
@@ -47,6 +48,8 @@ export async function ensureXcodeBuildPhase(fromPath: string) {
4748
existingBuildPhase.removeFromProject();
4849
}
4950

51+
// TODO: Declare input and output files to prevent unnecessary runs
52+
5053
mainTarget.createBuildPhase(xcode.PBXShellScriptBuildPhase, {
5154
name: BUILD_PHASE_NAME,
5255
shellScript: [
@@ -108,6 +111,9 @@ const XcframeworkInfoSchema = zod.looseObject({
108111
LibraryIdentifier: zod.string(),
109112
LibraryPath: zod.string(),
110113
DebugSymbolsPath: zod.string().optional(),
114+
SupportedArchitectures: zod.array(zod.string()),
115+
SupportedPlatform: zod.string(),
116+
SupportedPlatformVariant: zod.string().optional(),
111117
}),
112118
),
113119
CFBundlePackageType: zod.literal("XFWK"),
@@ -376,84 +382,190 @@ export async function linkVersionedFramework({
376382
);
377383
}
378384

385+
export async function createAppleLinker(): Promise<ModuleLinker> {
386+
assert.equal(
387+
process.platform,
388+
"darwin",
389+
"Linking Apple addons are only supported on macOS",
390+
);
391+
392+
const {
393+
TARGET_BUILD_DIR: targetBuildDir,
394+
FRAMEWORKS_FOLDER_PATH: frameworksFolderPath,
395+
} = process.env;
396+
assert(targetBuildDir, "Expected TARGET_BUILD_DIR to be set by Xcodebuild");
397+
assert(
398+
frameworksFolderPath,
399+
"Expected FRAMEWORKS_FOLDER_PATH to be set by Xcodebuild",
400+
);
401+
402+
const outputPath = path.join(targetBuildDir, frameworksFolderPath);
403+
await fs.promises.mkdir(outputPath, { recursive: true });
404+
405+
const {
406+
EXPANDED_CODE_SIGN_IDENTITY: signingIdentity = "-",
407+
CODE_SIGNING_REQUIRED: signingRequired,
408+
CODE_SIGNING_ALLOWED: signingAllowed,
409+
} = process.env;
410+
411+
return (options: LinkModuleOptions) => {
412+
return linkXcframework({
413+
...options,
414+
outputPath,
415+
signingIdentity:
416+
signingRequired !== "NO" && signingAllowed !== "NO"
417+
? signingIdentity
418+
: undefined,
419+
});
420+
};
421+
}
422+
423+
export function determineFrameworkSlice(): {
424+
platform: string;
425+
platformVariant?: string;
426+
architectures: string[];
427+
} {
428+
const {
429+
PLATFORM_NAME: platformName,
430+
EFFECTIVE_PLATFORM_NAME: effectivePlatformName,
431+
ARCHS: architecturesJoined,
432+
} = process.env;
433+
434+
assert(platformName, "Expected PLATFORM_NAME to be set by Xcodebuild");
435+
assert(architecturesJoined, "Expected ARCHS to be set by Xcodebuild");
436+
const architectures = architecturesJoined.split(" ");
437+
438+
const simulator = platformName.endsWith("simulator");
439+
440+
if (platformName === "iphonesimulator") {
441+
return {
442+
platform: "ios",
443+
platformVariant: simulator ? "simulator" : undefined,
444+
architectures,
445+
};
446+
} else if (platformName === "macosx") {
447+
return {
448+
platform: "macos",
449+
architectures,
450+
platformVariant: effectivePlatformName?.endsWith("maccatalyst")
451+
? "maccatalyst"
452+
: undefined,
453+
};
454+
}
455+
456+
throw new Error(
457+
`Unsupported platform: ${effectivePlatformName ?? platformName}`,
458+
);
459+
}
460+
379461
export async function linkXcframework({
380462
platform,
381463
modulePath,
382464
incremental,
383465
naming,
384-
}: LinkModuleOptions): Promise<LinkModuleResult> {
385-
assert.equal(
386-
process.platform,
387-
"darwin",
388-
"Linking Apple addons are only supported on macOS",
466+
outputPath: outputParentPath,
467+
signingIdentity,
468+
}: LinkModuleOptions & {
469+
outputPath: string;
470+
signingIdentity?: string;
471+
}): Promise<LinkModuleResult> {
472+
assertFixable(
473+
!incremental,
474+
"Incremental linking is not supported for Apple frameworks",
475+
{
476+
instructions: "Run the command with the --force flag",
477+
},
389478
);
390479
// Copy the xcframework to the output directory and rename the framework and binary
391480
const newLibraryName = getLibraryName(modulePath, naming);
392-
const outputPath = getLinkedModuleOutputPath(platform, modulePath, naming);
393-
394-
if (incremental && fs.existsSync(outputPath)) {
395-
const moduleModified = getLatestMtime(modulePath);
396-
const outputModified = getLatestMtime(outputPath);
397-
if (moduleModified < outputModified) {
398-
return {
399-
originalPath: modulePath,
400-
libraryName: newLibraryName,
401-
outputPath,
402-
skipped: true,
403-
};
404-
}
405-
}
481+
const frameworkOutputPath = path.join(
482+
outputParentPath,
483+
`${newLibraryName}.framework`,
484+
);
406485
// Delete any existing xcframework (or xcodebuild will try to amend it)
407-
await fs.promises.rm(outputPath, { recursive: true, force: true });
408-
// Copy the existing xcframework to the output path
409-
await fs.promises.cp(modulePath, outputPath, {
410-
recursive: true,
411-
verbatimSymlinks: true,
412-
});
486+
await fs.promises.rm(frameworkOutputPath, { recursive: true, force: true });
413487

414-
const info = await readXcframeworkInfo(path.join(outputPath, "Info.plist"));
488+
const info = await readXcframeworkInfo(path.join(modulePath, "Info.plist"));
415489

416-
await Promise.all(
417-
info.AvailableLibraries.map(async (framework) => {
418-
const frameworkPath = path.join(
419-
outputPath,
420-
framework.LibraryIdentifier,
421-
framework.LibraryPath,
422-
);
423-
await linkFramework({
424-
frameworkPath,
425-
newLibraryName,
426-
debugSymbolsPath: framework.DebugSymbolsPath
427-
? path.join(
428-
outputPath,
429-
framework.LibraryIdentifier,
430-
framework.DebugSymbolsPath,
431-
)
432-
: undefined,
433-
});
434-
}),
490+
// TODO: Assert the existence of environment variables injected by Xcodebuild
491+
// TODO: Pick and assert the existence of the right framework slice based on the environment variables
492+
// TODO: Link the framework into the output path
493+
494+
const expectedSlice = determineFrameworkSlice();
495+
496+
const framework = info.AvailableLibraries.find((framework) => {
497+
return (
498+
expectedSlice.platform === framework.SupportedPlatform &&
499+
expectedSlice.platformVariant === framework.SupportedPlatformVariant &&
500+
expectedSlice.architectures.every((architecture) =>
501+
framework.SupportedArchitectures.includes(architecture),
502+
)
503+
);
504+
});
505+
assert(
506+
framework,
507+
`Failed to find a framework slice matching: ${JSON.stringify(expectedSlice)}`,
435508
);
436509

437-
await writeXcframeworkInfo(outputPath, {
438-
...info,
439-
AvailableLibraries: info.AvailableLibraries.map((library) => {
440-
return {
441-
...library,
442-
LibraryPath: `${newLibraryName}.framework`,
443-
BinaryPath: `${newLibraryName}.framework/${newLibraryName}`,
444-
};
445-
}),
510+
const originalFrameworkPath = path.join(
511+
modulePath,
512+
framework.LibraryIdentifier,
513+
framework.LibraryPath,
514+
);
515+
516+
// Copy the existing framework to the output path
517+
await fs.promises.cp(originalFrameworkPath, frameworkOutputPath, {
518+
recursive: true,
519+
verbatimSymlinks: true,
446520
});
447521

448-
// Delete any leftover "magic file"
449-
await fs.promises.rm(path.join(outputPath, "react-native-node-api-module"), {
450-
force: true,
522+
await linkFramework({
523+
frameworkPath: frameworkOutputPath,
524+
newLibraryName,
525+
debugSymbolsPath: framework.DebugSymbolsPath
526+
? path.join(
527+
modulePath,
528+
framework.LibraryIdentifier,
529+
framework.DebugSymbolsPath,
530+
)
531+
: undefined,
451532
});
452533

534+
if (signingIdentity) {
535+
await signFramework({
536+
frameworkPath: frameworkOutputPath,
537+
identity: signingIdentity,
538+
});
539+
}
540+
453541
return {
454542
originalPath: modulePath,
455543
libraryName: newLibraryName,
456-
outputPath,
544+
outputPath: frameworkOutputPath,
457545
skipped: false,
546+
signed: signingIdentity ? true : false,
458547
};
459548
}
549+
550+
export async function signFramework({
551+
frameworkPath,
552+
identity,
553+
}: {
554+
frameworkPath: string;
555+
identity: string;
556+
}) {
557+
await spawn(
558+
"codesign",
559+
[
560+
"--force",
561+
"--sign",
562+
identity,
563+
"--timestamp=none",
564+
"--preserve-metadata=identifier,entitlements,flags",
565+
frameworkPath,
566+
],
567+
{
568+
outputMode: "buffered",
569+
},
570+
);
571+
}

packages/host/src/node/cli/link-modules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ export type ModuleDetails = {
4444

4545
export type LinkModuleResult = ModuleDetails & {
4646
skipped: boolean;
47+
signed?: boolean;
4748
};
4849

4950
export type ModuleOutputBase = {
5051
originalPath: string;
5152
skipped: boolean;
53+
signed?: boolean;
5254
};
5355

5456
type ModuleOutput = ModuleOutputBase &

packages/host/src/node/cli/program.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
import { command as vendorHermes } from "./hermes";
2727
import { packageNameOption, pathSuffixOption } from "./options";
2828
import { linkModules, pruneLinkedModules, ModuleLinker } from "./link-modules";
29-
import { ensureXcodeBuildPhase, linkXcframework } from "./apple";
29+
import { ensureXcodeBuildPhase, createAppleLinker } from "./apple";
3030
import { linkAndroidDir } from "./android";
3131

3232
// We're attaching a lot of listeners when spawning in parallel
@@ -36,11 +36,14 @@ export const program = new Command("react-native-node-api").addCommand(
3636
vendorHermes,
3737
);
3838

39-
function getLinker(platform: PlatformName): ModuleLinker {
39+
async function createLinker(
40+
platform: PlatformName,
41+
fromPath: string,
42+
): Promise<ModuleLinker> {
4043
if (platform === "android") {
4144
return linkAndroidDir;
4245
} else if (platform === "apple") {
43-
return linkXcframework;
46+
return createAppleLinker();
4447
} else {
4548
throw new Error(`Unknown platform: ${platform as string}`);
4649
}
@@ -99,27 +102,20 @@ program
99102

100103
for (const platform of platforms) {
101104
const platformDisplayName = getPlatformDisplayName(platform);
102-
const platformOutputPath = getAutolinkPath(platform);
103105
const modules = await oraPromise(
104-
() =>
105-
linkModules({
106+
async () =>
107+
await linkModules({
106108
platform,
107109
fromPath: path.resolve(pathArg),
108110
incremental: !force,
109111
naming: { packageName, pathSuffix },
110-
linker: getLinker(platform),
112+
linker: await createLinker(platform, path.resolve(pathArg)),
111113
}),
112114
{
113-
text: `Linking ${platformDisplayName} Node-API modules into ${prettyPath(
114-
platformOutputPath,
115-
)}`,
116-
successText: `Linked ${platformDisplayName} Node-API modules into ${prettyPath(
117-
platformOutputPath,
118-
)}`,
115+
text: `Linking ${platformDisplayName} Node-API modules`,
116+
successText: `Linked ${platformDisplayName} Node-API modules`,
119117
failText: () =>
120-
`Failed to link ${platformDisplayName} Node-API modules into ${prettyPath(
121-
platformOutputPath,
122-
)}`,
118+
`Failed to link ${platformDisplayName} Node-API modules`,
123119
},
124120
);
125121

@@ -130,16 +126,18 @@ program
130126
const failures = modules.filter((result) => "failure" in result);
131127
const linked = modules.filter((result) => "outputPath" in result);
132128

133-
for (const { originalPath, outputPath, skipped } of linked) {
129+
for (const { originalPath, outputPath, skipped, signed } of linked) {
134130
const prettyOutputPath = outputPath
135-
? "→ " + prettyPath(path.basename(outputPath))
131+
? "→ " + prettyPath(outputPath)
136132
: "";
133+
const signedSuffix = signed ? "🔏" : "";
137134
if (skipped) {
138135
console.log(
139136
chalk.greenBright("-"),
140137
"Skipped",
141138
prettyPath(originalPath),
142139
prettyOutputPath,
140+
signedSuffix,
143141
"(up to date)",
144142
);
145143
} else {
@@ -148,6 +146,7 @@ program
148146
"Linked",
149147
prettyPath(originalPath),
150148
prettyOutputPath,
149+
signedSuffix,
151150
);
152151
}
153152
}

0 commit comments

Comments
 (0)