@@ -21,7 +21,7 @@ const {
2121 readScriptPhasesManifest,
2222} = require ( '../generate-spm-xcodeproj' ) ;
2323const { DOMParser} = require ( '@xmldom/xmldom' ) ;
24- const { execFileSync} = require ( 'node:child_process' ) ;
24+ const { execFileSync, spawnSync } = require ( 'node:child_process' ) ;
2525const fs = require ( 'node:fs' ) ;
2626const os = require ( 'node:os' ) ;
2727const path = require ( 'node:path' ) ;
@@ -333,6 +333,181 @@ describe('sync scripts', () => {
333333 } ) ;
334334 } ) ;
335335
336+ describe ( 'stale inputs and a recorded plugin framework mismatch' , ( ) => {
337+ const MISMATCH =
338+ 'error: The Xcode project links ExpoCore.framework, but no autolinking plugin provides it on this machine.' ;
339+
340+ let root ;
341+ let srcRoot ;
342+ let xcodeprojPath ;
343+ let generatedDir ;
344+ let fakeNode ;
345+ let rnDir ;
346+ let syncLog ;
347+
348+ // Does to the generated dir what a real sync does: on a mismatch it prints
349+ // the errors, records them, refreshes the stamp, and exits 2.
350+ const FAKE_NODE = `#!/bin/bash
351+ if [ "$1" = "--print" ]; then
352+ echo "$FAKE_RN_DIR"
353+ exit 0
354+ fi
355+ echo sync >> "$FAKE_SYNC_LOG"
356+ DIR="$SRCROOT/build/generated/autolinking"
357+ date > "$DIR/.spm-sync-stamp"
358+ if [ -n "$FAKE_MISMATCH" ]; then
359+ echo "$FAKE_MISMATCH" >&2
360+ echo "$FAKE_MISMATCH" > "$DIR/.spm-plugin-mismatch"
361+ exit 2
362+ fi
363+ rm -f "$DIR/.spm-plugin-mismatch"
364+ ` ;
365+
366+ const setTime = ( file , secondsFromNow ) => {
367+ const time = Date . now ( ) / 1000 + secondsFromNow ;
368+ fs . utimesSync ( file , time , time ) ;
369+ } ;
370+
371+ beforeEach ( ( ) => {
372+ root = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'spm-sync-mismatch-' ) ) ;
373+ srcRoot = path . join ( root , 'app' ) ;
374+ xcodeprojPath = path . join ( srcRoot , 'MyApp.xcodeproj' ) ;
375+ generatedDir = path . join ( srcRoot , 'build' , 'generated' , 'autolinking' ) ;
376+ rnDir = path . join ( root , 'react-native' ) ;
377+ fs . mkdirSync ( xcodeprojPath , { recursive : true } ) ;
378+ fs . mkdirSync ( generatedDir , { recursive : true } ) ;
379+ fs . mkdirSync ( path . join ( rnDir , 'scripts' ) , { recursive : true } ) ;
380+ fs . writeFileSync ( path . join ( rnDir , 'scripts' , 'setup-apple-spm.js' ) , '' ) ;
381+ fs . writeFileSync ( path . join ( srcRoot , 'package.json' ) , '{}\n' ) ;
382+ fs . writeFileSync ( path . join ( xcodeprojPath , '.spm-injected.json' ) , '{}\n' ) ;
383+ fs . writeFileSync (
384+ path . join ( generatedDir , '.spm-plugin-flavored-frameworks.json' ) ,
385+ '[]\n' ,
386+ ) ;
387+ for ( const file of [
388+ path . join ( srcRoot , 'package.json' ) ,
389+ path . join ( xcodeprojPath , '.spm-injected.json' ) ,
390+ path . join ( generatedDir , '.spm-plugin-flavored-frameworks.json' ) ,
391+ ] ) {
392+ setTime ( file , - 100 ) ;
393+ }
394+ fakeNode = path . join ( root , 'node' ) ;
395+ fs . writeFileSync ( fakeNode , FAKE_NODE , { mode : 0o755 } ) ;
396+ syncLog = path . join ( root , 'syncs.log' ) ;
397+ } ) ;
398+
399+ afterEach ( ( ) => {
400+ fs . rmSync ( root , { recursive : true , force : true } ) ;
401+ } ) ;
402+
403+ function build ( { mismatch} ) {
404+ fs . writeFileSync ( syncLog , '' ) ;
405+ const result = spawnSync ( '/bin/bash' , [ '-c' , script ] , {
406+ encoding : 'utf8' ,
407+ env : {
408+ PATH : process . env . PATH ,
409+ SRCROOT : srcRoot ,
410+ PROJECT_FILE_PATH : xcodeprojPath ,
411+ NODE_BINARY : fakeNode ,
412+ FAKE_RN_DIR : rnDir ,
413+ FAKE_SYNC_LOG : syncLog ,
414+ FAKE_MISMATCH : mismatch ? MISMATCH : '' ,
415+ } ,
416+ } ) ;
417+ const output = result . stdout + result . stderr ;
418+ return {
419+ status : result . status ,
420+ errors : output . split ( MISMATCH ) . length - 1 ,
421+ syncs : fs . readFileSync ( syncLog , 'utf8' ) . split ( 'sync' ) . length - 1 ,
422+ } ;
423+ }
424+
425+ function writeStamp ( secondsFromNow ) {
426+ const stamp = path . join ( generatedDir , '.spm-sync-stamp' ) ;
427+ fs . writeFileSync ( stamp , 'earlier sync\n' ) ;
428+ setTime ( stamp , secondsFromNow ) ;
429+ }
430+
431+ it ( 'syncs when the marker is newer than the stamp' , ( ) => {
432+ writeStamp ( - 50 ) ;
433+ setTime ( path . join ( xcodeprojPath , '.spm-injected.json' ) , - 10 ) ;
434+ expect ( build ( { mismatch : false } ) ) . toEqual ( {
435+ status : 0 ,
436+ errors : 0 ,
437+ syncs : 1 ,
438+ } ) ;
439+ } ) ;
440+
441+ it ( 'does not sync when only project.pbxproj is newer than the stamp' , ( ) => {
442+ writeStamp ( - 50 ) ;
443+ const pbxprojPath = path . join ( xcodeprojPath , 'project.pbxproj' ) ;
444+ fs . writeFileSync ( pbxprojPath , '// !$*UTF8*$!\n' ) ;
445+ setTime ( pbxprojPath , - 10 ) ;
446+ expect ( build ( { mismatch : false } ) ) . toEqual ( {
447+ status : 0 ,
448+ errors : 0 ,
449+ syncs : 0 ,
450+ } ) ;
451+ } ) ;
452+
453+ it ( 'passes the next build after the recovery sync clears the mismatch' , ( ) => {
454+ build ( { mismatch : true } ) ;
455+ // `npx react-native spm sync`, run after precompiling the framework.
456+ spawnSync ( fakeNode , [ 'setup-apple-spm.js' , 'sync' ] , {
457+ env : { SRCROOT : srcRoot , FAKE_SYNC_LOG : syncLog , FAKE_MISMATCH : '' } ,
458+ } ) ;
459+ expect ( build ( { mismatch : false } ) ) . toEqual ( {
460+ status : 0 ,
461+ errors : 0 ,
462+ syncs : 0 ,
463+ } ) ;
464+ } ) ;
465+
466+ it ( 'fails the first build and prints the errors once' , ( ) => {
467+ expect ( build ( { mismatch : true } ) ) . toEqual ( {
468+ status : 1 ,
469+ errors : 1 ,
470+ syncs : 1 ,
471+ } ) ;
472+ } ) ;
473+
474+ it ( 'fails later builds with the recorded errors without re-running sync' , ( ) => {
475+ build ( { mismatch : true } ) ;
476+ expect ( build ( { mismatch : true } ) ) . toEqual ( {
477+ status : 1 ,
478+ errors : 1 ,
479+ syncs : 0 ,
480+ } ) ;
481+ } ) ;
482+
483+ it . each ( [
484+ [
485+ 'the marker changes' ,
486+ ( ) => path . join ( xcodeprojPath , '.spm-injected.json' ) ,
487+ ] ,
488+ [
489+ 'spm update rewrites the plugin manifest' ,
490+ ( ) => path . join ( generatedDir , '.spm-plugin-flavored-frameworks.json' ) ,
491+ ] ,
492+ [ 'a dependency input changes' , ( ) => path . join ( srcRoot , 'package.json' ) ] ,
493+ ] ) ( 're-runs sync when %s' , ( _ , changedFile ) => {
494+ build ( { mismatch : true } ) ;
495+ setTime ( path . join ( generatedDir , '.spm-sync-stamp' ) , - 50 ) ;
496+ setTime ( path . join ( generatedDir , '.spm-plugin-mismatch' ) , - 50 ) ;
497+ setTime ( changedFile ( ) , - 10 ) ;
498+ expect ( build ( { mismatch : false } ) ) . toEqual ( {
499+ status : 0 ,
500+ errors : 0 ,
501+ syncs : 1 ,
502+ } ) ;
503+ expect ( build ( { mismatch : false } ) ) . toEqual ( {
504+ status : 0 ,
505+ errors : 0 ,
506+ syncs : 0 ,
507+ } ) ;
508+ } ) ;
509+ } ) ;
510+
336511 it ( 'is deterministic, shared with the pre-action, and valid POSIX shell' , ( ) => {
337512 expect ( buildSyncAutolinkingScript ( baked ) ) . toBe ( script ) ;
338513 expect ( buildSchemePreActionScript ( baked ) ) . toBe ( script ) ;
0 commit comments