Skip to content

Commit ef71d3d

Browse files
committed
Address review: record linked frameworks in the marker, cache mismatch errors, recovery step, exit codes
1 parent 34497ba commit ef71d3d

8 files changed

Lines changed: 582 additions & 112 deletions

File tree

‎packages/community-cli-plugin/src/commands/spm.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ const spmCommand: Command = {
2525
name: 'spm [action]',
2626
description:
2727
'Set up or maintain Swift Package Manager support for the iOS/macOS app. ' +
28-
'Actions: add, update, deinit, scaffold. With no action: add (or update ' +
29-
'if SPM is already set up).',
28+
'Actions: add, update, deinit, scaffold, sync. With no action: add (or ' +
29+
'update if SPM is already set up).',
3030
options: [
3131
{
3232
name: '--version <string>',

‎packages/react-native/scripts/setup-apple-spm.js‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@
3434
* .spm-injected.json) and drop the marker.
3535
* scaffold Generate Package.swift for community deps that
3636
* lack SPM support.
37-
* sync / codegen / download Advanced/internal: `sync` is invoked by the
38-
* generated Xcode build phase; `codegen` and
39-
* `download` run a single pipeline step.
37+
* sync Re-run the build-time autolinking sync and its
38+
* checks. The generated Xcode build phase runs
39+
* it; run it by hand after precompiling a module.
40+
* codegen / download Advanced/internal: run a single pipeline step.
4041
*
4142
* Zero-arg `npx react-native spm` auto-detects: a freshly-scaffolded CocoaPods
4243
* project (clean tree, stock Podfile) → `add --deintegrate`; an injected

‎packages/react-native/scripts/spm/__docs__/spm-scripts.md‎

Lines changed: 31 additions & 19 deletions
Large diffs are not rendered by default.

‎packages/react-native/scripts/spm/__tests__/generate-spm-xcodeproj-test.js‎

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const {
2121
readScriptPhasesManifest,
2222
} = require('../generate-spm-xcodeproj');
2323
const {DOMParser} = require('@xmldom/xmldom');
24-
const {execFileSync} = require('node:child_process');
24+
const {execFileSync, spawnSync} = require('node:child_process');
2525
const fs = require('node:fs');
2626
const os = require('node:os');
2727
const 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

Comments
 (0)