11import assert from "node:assert/strict" ;
22import path from "node:path" ;
33import fs from "node:fs" ;
4- import os from "node:os" ;
54
65import plist from "@expo/plist" ;
6+ import * as zod from "zod" ;
7+
78import { spawn } from "@react-native-node-api/cli-utils" ;
89
910import { getLatestMtime , getLibraryName } from "../path-utils.js" ;
@@ -13,6 +14,86 @@ import {
1314 LinkModuleResult ,
1415} from "./link-modules.js" ;
1516
17+ /**
18+ * Reads and parses a plist file, converting it to XML format if needed.
19+ */
20+ async function readAndParsePlist ( plistPath : string ) : Promise < unknown > {
21+ try {
22+ // Convert to XML format if needed
23+ assert (
24+ process . platform === "darwin" ,
25+ "Updating Info.plist files are not supported on this platform" ,
26+ ) ;
27+ // Try reading the file to see if it is already in XML format
28+ const contents = await fs . promises . readFile ( plistPath , "utf-8" ) ;
29+ if ( contents . startsWith ( "<?xml" ) ) {
30+ return plist . parse ( contents ) as unknown ;
31+ } else {
32+ await spawn ( "plutil" , [ "-convert" , "xml1" , plistPath ] , {
33+ outputMode : "inherit" ,
34+ } ) ;
35+ // Read it again
36+ return plist . parse (
37+ await fs . promises . readFile ( plistPath , "utf-8" ) ,
38+ ) as unknown ;
39+ }
40+ } catch ( error ) {
41+ throw new Error (
42+ `Failed to convert plist at path "${ plistPath } " to XML format` ,
43+ { cause : error } ,
44+ ) ;
45+ }
46+ }
47+
48+ // Using a looseObject to allow additional fields that we don't know about
49+ const XcframeworkInfoSchema = zod . looseObject ( {
50+ AvailableLibraries : zod . array (
51+ zod . object ( {
52+ BinaryPath : zod . string ( ) ,
53+ LibraryIdentifier : zod . string ( ) ,
54+ LibraryPath : zod . string ( ) ,
55+ } ) ,
56+ ) ,
57+ CFBundlePackageType : zod . literal ( "XFWK" ) ,
58+ XCFrameworkFormatVersion : zod . literal ( "1.0" ) ,
59+ } ) ;
60+
61+ export async function readXcframeworkInfo ( xcframeworkPath : string ) {
62+ const infoPlistPath = path . join ( xcframeworkPath , "Info.plist" ) ;
63+ const infoPlist = await readAndParsePlist ( infoPlistPath ) ;
64+ return XcframeworkInfoSchema . parse ( infoPlist ) ;
65+ }
66+
67+ export async function writeXcframeworkInfo (
68+ xcframeworkPath : string ,
69+ info : zod . infer < typeof XcframeworkInfoSchema > ,
70+ ) {
71+ const infoPlistPath = path . join ( xcframeworkPath , "Info.plist" ) ;
72+ const infoPlistXml = plist . build ( info ) ;
73+ await fs . promises . writeFile ( infoPlistPath , infoPlistXml , "utf-8" ) ;
74+ }
75+
76+ const FrameworkInfoSchema = zod . looseObject ( {
77+ CFBundlePackageType : zod . literal ( "FMWK" ) ,
78+ CFBundleInfoDictionaryVersion : zod . literal ( "6.0" ) ,
79+ CFBundleExecutable : zod . string ( ) ,
80+ } ) ;
81+
82+ export async function readFrameworkInfo ( frameworkPath : string ) {
83+ const infoPlistPath = path . join ( frameworkPath , "Info.plist" ) ;
84+ const infoPlist = await readAndParsePlist ( infoPlistPath ) ;
85+ return FrameworkInfoSchema . parse ( infoPlist ) ;
86+ }
87+
88+ export async function writeFrameworkInfo (
89+ frameworkPath : string ,
90+ info : zod . infer < typeof FrameworkInfoSchema > ,
91+ ) {
92+ const infoPlistPath = path . join ( frameworkPath , "Info.plist" ) ;
93+ const infoPlistXml = plist . build ( info ) ;
94+ await fs . promises . writeFile ( infoPlistPath , infoPlistXml , "utf-8" ) ;
95+ }
96+
1697export function determineInfoPlistPath ( frameworkPath : string ) {
1798 const checkedPaths = new Array < string > ( ) ;
1899
@@ -109,121 +190,86 @@ export async function linkXcframework({
109190} : LinkModuleOptions ) : Promise < LinkModuleResult > {
110191 // Copy the xcframework to the output directory and rename the framework and binary
111192 const newLibraryName = getLibraryName ( modulePath , naming ) ;
193+ const newFrameworkRelativePath = `${ newLibraryName } .framework` ;
194+ const newBinaryRelativePath = `${ newFrameworkRelativePath } /${ newLibraryName } ` ;
112195 const outputPath = getLinkedModuleOutputPath ( platform , modulePath , naming ) ;
113- const tempPath = await fs . promises . mkdtemp (
114- path . join ( os . tmpdir ( ) , `react-native-node-api-${ newLibraryName } -` ) ,
115- ) ;
116- try {
117- if ( incremental && fs . existsSync ( outputPath ) ) {
118- const moduleModified = getLatestMtime ( modulePath ) ;
119- const outputModified = getLatestMtime ( outputPath ) ;
120- if ( moduleModified < outputModified ) {
121- return {
122- originalPath : modulePath ,
123- libraryName : newLibraryName ,
124- outputPath,
125- skipped : true ,
126- } ;
127- }
128- }
129- // Delete any existing xcframework (or xcodebuild will try to amend it)
130- await fs . promises . rm ( outputPath , { recursive : true , force : true } ) ;
131- await fs . promises . cp ( modulePath , tempPath , { recursive : true } ) ;
132-
133- // Following extracted function mimics `glob("*/*.framework/")`
134- function globFrameworkDirs < T > (
135- startPath : string ,
136- fn : ( parentPath : string , name : string ) => Promise < T > ,
137- ) {
138- return fs
139- . readdirSync ( startPath , { withFileTypes : true } )
140- . filter ( ( tripletEntry ) => tripletEntry . isDirectory ( ) )
141- . flatMap ( ( tripletEntry ) => {
142- const tripletPath = path . join ( startPath , tripletEntry . name ) ;
143- return fs
144- . readdirSync ( tripletPath , { withFileTypes : true } )
145- . filter (
146- ( frameworkEntry ) =>
147- frameworkEntry . isDirectory ( ) &&
148- path . extname ( frameworkEntry . name ) === ".framework" ,
149- )
150- . flatMap (
151- async ( frameworkEntry ) =>
152- await fn ( tripletPath , frameworkEntry . name ) ,
153- ) ;
154- } ) ;
196+
197+ if ( incremental && fs . existsSync ( outputPath ) ) {
198+ const moduleModified = getLatestMtime ( modulePath ) ;
199+ const outputModified = getLatestMtime ( outputPath ) ;
200+ if ( moduleModified < outputModified ) {
201+ return {
202+ originalPath : modulePath ,
203+ libraryName : newLibraryName ,
204+ outputPath,
205+ skipped : true ,
206+ } ;
155207 }
208+ }
209+ // Delete any existing xcframework (or xcodebuild will try to amend it)
210+ await fs . promises . rm ( outputPath , { recursive : true , force : true } ) ;
211+ // Copy the existing xcframework to the output path
212+ await fs . promises . cp ( modulePath , outputPath , { recursive : true } ) ;
156213
157- const frameworkPaths = await Promise . all (
158- globFrameworkDirs ( tempPath , async ( tripletPath , frameworkEntryName ) => {
159- const frameworkPath = path . join ( tripletPath , frameworkEntryName ) ;
160- const oldLibraryName = path . basename ( frameworkEntryName , ".framework" ) ;
161- const oldLibraryPath = path . join ( frameworkPath , oldLibraryName ) ;
162- const newFrameworkPath = path . join (
163- tripletPath ,
164- `${ newLibraryName } .framework` ,
165- ) ;
166- const newLibraryPath = path . join ( newFrameworkPath , newLibraryName ) ;
167- assert (
168- fs . existsSync ( oldLibraryPath ) ,
169- `Expected a library at '${ oldLibraryPath } '` ,
170- ) ;
171- // Rename the library
172- await fs . promises . rename (
173- oldLibraryPath ,
174- // Cannot use newLibraryPath here, because the framework isn't renamed yet
175- path . join ( frameworkPath , newLibraryName ) ,
176- ) ;
177- // Rename the framework
178- await fs . promises . rename ( frameworkPath , newFrameworkPath ) ;
179- // Expect the library in the new location
180- assert ( fs . existsSync ( newLibraryPath ) ) ;
181- // Update the binary
182- await spawn (
183- "install_name_tool" ,
184- [
185- "-id" ,
186- `@rpath/${ newLibraryName } .framework/${ newLibraryName } ` ,
187- newLibraryPath ,
188- ] ,
189- {
190- outputMode : "buffered" ,
191- } ,
192- ) ;
193- // Update the Info.plist file for the framework
194- await updateInfoPlist ( {
195- frameworkPath : newFrameworkPath ,
196- oldLibraryName,
197- newLibraryName,
198- } ) ;
199- return newFrameworkPath ;
200- } ) ,
201- ) ;
214+ const info = await readXcframeworkInfo ( outputPath ) ;
202215
203- // Create a new xcframework from the renamed frameworks
204- await spawn (
205- "xcodebuild" ,
206- [
207- "-create-xcframework" ,
208- ...frameworkPaths . flatMap ( ( frameworkPath ) => [
209- "-framework" ,
210- frameworkPath ,
211- ] ) ,
212- "-output" ,
216+ await Promise . all (
217+ info . AvailableLibraries . map ( async ( framework ) => {
218+ const frameworkPath = path . join (
213219 outputPath ,
214- ] ,
215- {
216- outputMode : "buffered" ,
217- } ,
218- ) ;
220+ framework . LibraryIdentifier ,
221+ framework . LibraryPath ,
222+ ) ;
223+ assert (
224+ fs . existsSync ( frameworkPath ) ,
225+ `Expected framework at '${ frameworkPath } '` ,
226+ ) ;
227+ const frameworkInfo = await readFrameworkInfo ( frameworkPath ) ;
228+ // Update install name
229+ await spawn (
230+ "install_name_tool" ,
231+ [
232+ "-id" ,
233+ `@rpath/${ newBinaryRelativePath } ` ,
234+ frameworkInfo . CFBundleExecutable ,
235+ ] ,
236+ {
237+ outputMode : "buffered" ,
238+ cwd : frameworkPath ,
239+ } ,
240+ ) ;
241+ await writeFrameworkInfo ( frameworkPath , {
242+ ...frameworkInfo ,
243+ CFBundleExecutable : newLibraryName ,
244+ } ) ;
245+ // Rename the actual binary
246+ await fs . promises . rename (
247+ path . join ( frameworkPath , frameworkInfo . CFBundleExecutable ) ,
248+ path . join ( frameworkPath , newLibraryName ) ,
249+ ) ;
250+ // Rename the framework directory
251+ await fs . promises . rename (
252+ frameworkPath ,
253+ path . join ( path . dirname ( frameworkPath ) , newFrameworkRelativePath ) ,
254+ ) ;
255+ } ) ,
256+ ) ;
219257
220- return {
221- originalPath : modulePath ,
222- libraryName : newLibraryName ,
223- outputPath,
224- skipped : false ,
225- } ;
226- } finally {
227- await fs . promises . rm ( tempPath , { recursive : true , force : true } ) ;
228- }
258+ await writeXcframeworkInfo ( outputPath , {
259+ ...info ,
260+ AvailableLibraries : info . AvailableLibraries . map ( ( library ) => {
261+ return {
262+ ...library ,
263+ BinaryPath : newBinaryRelativePath ,
264+ LibraryPath : newFrameworkRelativePath ,
265+ } ;
266+ } ) ,
267+ } ) ;
268+
269+ return {
270+ originalPath : modulePath ,
271+ libraryName : newLibraryName ,
272+ outputPath,
273+ skipped : false ,
274+ } ;
229275}
0 commit comments