@@ -22,64 +22,184 @@ export function escapeBundleIdentifier(input: string) {
2222 return input . replace ( / [ ^ A - Z a - z 0 - 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+
2567type 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+
83203export async function createXCframework ( {
84204 frameworkPaths,
85205 outputPath,
0 commit comments