Skip to content

Commit c75f551

Browse files
committed
Add support for versioned frameworks
1 parent 092f72f commit c75f551

File tree

4 files changed

+169
-42
lines changed

4 files changed

+169
-42
lines changed

.changeset/rich-weeks-cry.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"cmake-rn": minor
3+
"ferric-cli": minor
4+
"react-native-node-api": minor
5+
---
6+
7+
Add support for building versioned frameworks for Apple Darwin / macOS

packages/cmake-rn/src/platforms/apple.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export const platform: Platform<Triplet[], AppleOpts> = {
383383
const [artifact] = artifacts;
384384
await createAppleFramework({
385385
libraryPath: path.join(buildPath, artifact.path),
386-
versioned: triplet.endsWith("-darwin"),
386+
kind: triplet.endsWith("-darwin") ? "versioned" : "flat",
387387
bundleIdentifier: appleBundleIdentifier,
388388
});
389389
}

packages/ferric/src/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,8 @@ export const buildCommand = new Command("build")
309309
limit(() =>
310310
createAppleFramework({
311311
libraryPath: library.path,
312+
kind: library.os === "darwin" ? "versioned" : "flat",
312313
bundleIdentifier: appleBundleIdentifier,
313-
versioned: library.os === "darwin",
314314
}),
315315
),
316316
),

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

Lines changed: 160 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,64 +22,184 @@ export function escapeBundleIdentifier(input: string) {
2222
return input.replace(/[^A-Za-z0-9-.]/g, "-");
2323
}
2424

25+
/** Serialize a plist object and write it to the given path. */
26+
async function writeInfoPlist(
27+
infoPlistPath: string,
28+
plistDict: Record<string, unknown>,
29+
) {
30+
await fs.promises.writeFile(infoPlistPath, plist.build(plistDict), "utf8");
31+
}
32+
33+
/** Build and write the framework Info.plist to the given path. */
34+
async function writeFrameworkInfoPlist(
35+
infoPlistPath: string,
36+
libraryName: string,
37+
bundleIdentifier?: string,
38+
) {
39+
await writeInfoPlist(infoPlistPath, {
40+
CFBundleDevelopmentRegion: "en",
41+
CFBundleExecutable: libraryName,
42+
CFBundleIdentifier: escapeBundleIdentifier(
43+
bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`,
44+
),
45+
CFBundleInfoDictionaryVersion: "6.0",
46+
CFBundleName: libraryName,
47+
CFBundlePackageType: "FMWK",
48+
CFBundleShortVersionString: "1.0",
49+
CFBundleVersion: "1",
50+
NSPrincipalClass: "",
51+
});
52+
}
53+
54+
/** Update the library binary’s install name so it resolves correctly at load time. */
55+
async function updateLibraryInstallName(
56+
binaryPath: string,
57+
libraryName: string,
58+
cwd: string,
59+
) {
60+
await spawn(
61+
"install_name_tool",
62+
["-id", `@rpath/${libraryName}.framework/${libraryName}`, binaryPath],
63+
{ outputMode: "buffered", cwd },
64+
);
65+
}
66+
2567
type CreateAppleFrameworkOptions = {
2668
libraryPath: string;
27-
versioned?: boolean;
69+
kind: "flat" | "versioned";
2870
bundleIdentifier?: string;
2971
};
3072

31-
export async function createAppleFramework({
73+
/**
74+
* Creates a flat (non-versioned) .framework bundle:
75+
* MyFramework.framework/MyFramework, Info.plist, Headers/
76+
*/
77+
async function createFlatFramework({
3278
libraryPath,
33-
versioned = false,
79+
frameworkPath,
80+
libraryName,
3481
bundleIdentifier,
35-
}: CreateAppleFrameworkOptions) {
36-
if (versioned) {
37-
// TODO: Add support for generating a Versions/Current/Resources/Info.plist convention framework
38-
throw new Error("Creating versioned frameworks is not supported yet");
39-
}
40-
assert(fs.existsSync(libraryPath), `Library not found: ${libraryPath}`);
41-
// Write a info.plist file to the framework
42-
const libraryName = path.basename(libraryPath, path.extname(libraryPath));
43-
const frameworkPath = path.join(
44-
path.dirname(libraryPath),
45-
`${libraryName}.framework`,
46-
);
47-
// Create the framework from scratch
48-
await fs.promises.rm(frameworkPath, { recursive: true, force: true });
82+
}: {
83+
libraryPath: string;
84+
frameworkPath: string;
85+
libraryName: string;
86+
bundleIdentifier?: string;
87+
}): Promise<string> {
4988
await fs.promises.mkdir(frameworkPath);
5089
await fs.promises.mkdir(path.join(frameworkPath, "Headers"));
51-
// Create an empty Info.plist file
52-
await fs.promises.writeFile(
90+
await writeFrameworkInfoPlist(
5391
path.join(frameworkPath, "Info.plist"),
54-
plist.build({
55-
CFBundleDevelopmentRegion: "en",
56-
CFBundleExecutable: libraryName,
57-
CFBundleIdentifier: escapeBundleIdentifier(
58-
bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`,
59-
),
60-
CFBundleInfoDictionaryVersion: "6.0",
61-
CFBundleName: libraryName,
62-
CFBundlePackageType: "FMWK",
63-
CFBundleShortVersionString: "1.0",
64-
CFBundleVersion: "1",
65-
NSPrincipalClass: "",
66-
}),
67-
"utf8",
92+
libraryName,
93+
bundleIdentifier,
6894
);
6995
const newLibraryPath = path.join(frameworkPath, libraryName);
7096
// TODO: Consider copying the library instead of renaming it
7197
await fs.promises.rename(libraryPath, newLibraryPath);
72-
// Update the name of the library
73-
await spawn(
74-
"install_name_tool",
75-
["-id", `@rpath/${libraryName}.framework/${libraryName}`, newLibraryPath],
76-
{
77-
outputMode: "buffered",
78-
},
98+
await updateLibraryInstallName(libraryName, libraryName, frameworkPath);
99+
return frameworkPath;
100+
}
101+
102+
/**
103+
* Version identifier for the single version we create.
104+
* Apple uses A, B, ... for major versions; we only ever create one version.
105+
*/
106+
const VERSIONED_FRAMEWORK_VERSION = "A";
107+
108+
/**
109+
* Creates a versioned .framework bundle (Versions/Current convention):
110+
* MyFramework.framework/
111+
* MyFramework -> Versions/Current/MyFramework
112+
* Resources -> Versions/Current/Resources
113+
* Headers -> Versions/Current/Headers
114+
* Versions/
115+
* A/MyFramework, Resources/Info.plist, Headers/
116+
* Current -> A
117+
* See: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html
118+
*/
119+
async function createVersionedFramework({
120+
libraryPath,
121+
frameworkPath,
122+
libraryName,
123+
bundleIdentifier,
124+
}: {
125+
libraryPath: string;
126+
frameworkPath: string;
127+
libraryName: string;
128+
bundleIdentifier?: string;
129+
}): Promise<string> {
130+
const versionsDir = path.join(frameworkPath, "Versions");
131+
const versionDir = path.join(versionsDir, VERSIONED_FRAMEWORK_VERSION);
132+
const versionResourcesDir = path.join(versionDir, "Resources");
133+
const versionHeadersDir = path.join(versionDir, "Headers");
134+
135+
await fs.promises.mkdir(versionResourcesDir, { recursive: true });
136+
await fs.promises.mkdir(versionHeadersDir, { recursive: true });
137+
138+
await writeFrameworkInfoPlist(
139+
path.join(versionResourcesDir, "Info.plist"),
140+
libraryName,
141+
bundleIdentifier,
142+
);
143+
144+
const versionBinaryPath = path.join(versionDir, libraryName);
145+
await fs.promises.rename(libraryPath, versionBinaryPath);
146+
await updateLibraryInstallName(
147+
path.join("Versions", VERSIONED_FRAMEWORK_VERSION, libraryName),
148+
libraryName,
149+
frameworkPath,
150+
);
151+
152+
const currentLink = path.join(versionsDir, "Current");
153+
await fs.promises.symlink(VERSIONED_FRAMEWORK_VERSION, currentLink);
154+
155+
await fs.promises.symlink(
156+
"Versions/Current/Resources",
157+
path.join(frameworkPath, "Resources"),
158+
);
159+
await fs.promises.symlink(
160+
"Versions/Current/Headers",
161+
path.join(frameworkPath, "Headers"),
79162
);
163+
await fs.promises.symlink(
164+
path.join("Versions", "Current", libraryName),
165+
path.join(frameworkPath, libraryName),
166+
);
167+
80168
return frameworkPath;
81169
}
82170

171+
export async function createAppleFramework({
172+
libraryPath,
173+
kind,
174+
bundleIdentifier,
175+
}: CreateAppleFrameworkOptions) {
176+
assert(fs.existsSync(libraryPath), `Library not found: ${libraryPath}`);
177+
const libraryName = path.basename(libraryPath, path.extname(libraryPath));
178+
const frameworkPath = path.join(
179+
path.dirname(libraryPath),
180+
`${libraryName}.framework`,
181+
);
182+
await fs.promises.rm(frameworkPath, { recursive: true, force: true });
183+
184+
if (kind === "versioned") {
185+
return createVersionedFramework({
186+
libraryPath,
187+
frameworkPath,
188+
libraryName,
189+
bundleIdentifier,
190+
});
191+
} else if (kind === "flat") {
192+
return createFlatFramework({
193+
libraryPath,
194+
frameworkPath,
195+
libraryName,
196+
bundleIdentifier,
197+
});
198+
} else {
199+
throw new Error(`Unexpected framework kind: ${kind as string}`);
200+
}
201+
}
202+
83203
export async function createXCframework({
84204
frameworkPaths,
85205
outputPath,

0 commit comments

Comments
 (0)