Skip to content

Commit 25524a3

Browse files
committed
Address review: reset stamp on mismatch, compare framework names, use the built project
1 parent 0183eb7 commit 25524a3

5 files changed

Lines changed: 125 additions & 35 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -756,5 +756,6 @@ script branches on the exit code:
756756
That split is the whole reason these cases have their own exit code: a transient
757757
sync hiccup should not break a build that could still succeed, while a missing
758758
manifest or an out-of-date project should not pass silently. Sync writes its
759-
stamp only after these checks pass, so every following build fails the same way
760-
until the problem is fixed.
759+
stamp only after these checks pass. When the project is out of date, sync also
760+
removes the earlier stamp, so every following build fails the same way until the
761+
project is updated.

‎packages/react-native/scripts/spm/__tests__/plugin-framework-mismatch-test.js‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ function projectFiles(xcodeprojPath) {
136136

137137
// Runs the check and proves it wrote nothing under the .xcodeproj (pbxproj,
138138
// marker, scheme). Returns the printed lines and the thrown error, if any.
139-
function check(app) {
139+
function check(app, env = {}) {
140140
const before = projectFiles(app.xcodeprojPath);
141141
let error = null;
142142
try {
143-
assertPluginFrameworksLinked(app.appRoot);
143+
assertPluginFrameworksLinked(app.appRoot, env);
144144
} catch (e) {
145145
error = e;
146146
}
@@ -193,6 +193,38 @@ describe('assertPluginFrameworksLinked', () => {
193193
expect(lines[1]).toMatch(/^error: The Xcode project links ExpoCore\./);
194194
});
195195

196+
it('fails when a plugin renames its framework but keeps its id', () => {
197+
const app = scaffoldApp();
198+
inject(app, [REACT, EXPO]);
199+
writeSidecar(app.appRoot, [{id: 'expo-core', frameworkName: 'NewCore'}]);
200+
201+
const {error, lines} = check(app);
202+
203+
expect(error).toBeInstanceOf(PluginFrameworkMismatchError);
204+
expect(lines).toEqual([
205+
'error: NewCore (expo-core) is a precompiled framework from an autolinking plugin, but the Xcode project does not link it. Run `npx react-native spm` to update the project.',
206+
'error: The Xcode project links ExpoCore.framework, but no autolinking plugin provides it on this machine. Precompile ExpoCore, or run `npx react-native spm` to update the project.',
207+
]);
208+
});
209+
210+
it('checks the project Xcode is building, not the first injected one', () => {
211+
const app = scaffoldApp();
212+
const stale = {
213+
...app,
214+
xcodeprojPath: path.join(app.appRoot, 'A.xcodeproj'),
215+
};
216+
fs.mkdirSync(stale.xcodeprojPath);
217+
fs.writeFileSync(path.join(stale.xcodeprojPath, 'project.pbxproj'), PLAIN);
218+
inject(stale, [REACT]);
219+
inject(app, [REACT, EXPO]);
220+
writeSidecar(app.appRoot, [EXPO]);
221+
222+
expect(check(app, {PROJECT_FILE_PATH: app.xcodeprojPath})).toEqual({
223+
error: null,
224+
lines: [],
225+
});
226+
});
227+
196228
it('passes when the project links exactly the paired plugin frameworks', () => {
197229
const app = scaffoldApp();
198230
inject(app, [REACT, HERMES, EXPO]);
@@ -201,6 +233,22 @@ describe('assertPluginFrameworksLinked', () => {
201233
expect(check(app)).toEqual({error: null, lines: []});
202234
});
203235

236+
it('compares only setting prefixes when the frameworks manifest is missing', () => {
237+
const app = scaffoldApp();
238+
inject(app, [REACT, HERMES, EXPO]);
239+
writeSidecar(app.appRoot, [EXPO]);
240+
fs.rmSync(
241+
path.join(
242+
app.appRoot,
243+
'build',
244+
'xcframeworks',
245+
'flavored-frameworks.json',
246+
),
247+
);
248+
249+
expect(check(app)).toEqual({error: null, lines: []});
250+
});
251+
204252
it('never reports built-in frameworks', () => {
205253
const app = scaffoldApp();
206254
inject(app, [REACT, HERMES]);

‎packages/react-native/scripts/spm/__tests__/sync-spm-autolinking-test.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,17 @@ describe('sync-spm-autolinking main', () => {
152152
);
153153
expect(fs.existsSync(stampPath())).toBe(false);
154154
});
155+
156+
it('removes an earlier stamp when plugin frameworks and the project disagree', async () => {
157+
fs.mkdirSync(path.dirname(stampPath()), {recursive: true});
158+
fs.writeFileSync(stampPath(), 'earlier sync\n');
159+
const mismatch = new PluginFrameworkMismatchError();
160+
const deps = makeDeps({
161+
assertPluginFrameworksLinked: jest.fn(() => {
162+
throw mismatch;
163+
}),
164+
});
165+
await expect(run(deps)).rejects.toBe(mismatch);
166+
expect(fs.existsSync(stampPath())).toBe(false);
167+
});
155168
});

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

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
const {
2525
BUILTIN_FRAMEWORKS,
26+
FLAVORED_FRAMEWORKS_MANIFEST,
2627
readFlavoredFrameworksManifest,
2728
readPluginFrameworks,
2829
} = require('./flavored-frameworks');
@@ -2285,10 +2286,20 @@ function pbxListValues(list /*: string */) /*: Array<string> */ {
22852286
* machine with the plugin frameworks the injected project links. Only `spm
22862287
* add` / `spm update` change what the project links, so the build-time sync
22872288
* reports a mismatch instead of fixing it: one `error:` line per framework,
2288-
* then PluginFrameworkMismatchError. Never writes the project.
2289+
* then PluginFrameworkMismatchError. Never writes the project. Checks the
2290+
* project Xcode is building (`PROJECT_FILE_PATH`) when it is injected.
22892291
*/
2290-
function assertPluginFrameworksLinked(appRoot /*: string */) /*: void */ {
2291-
const xcodeprojPath = findInjectedXcodeproj(appRoot);
2292+
function assertPluginFrameworksLinked(
2293+
appRoot /*: string */,
2294+
env /*: {+[string]: ?string} */ = process.env,
2295+
) /*: void */ {
2296+
const builtProjectPath = env.PROJECT_FILE_PATH;
2297+
const xcodeprojPath =
2298+
builtProjectPath != null &&
2299+
builtProjectPath.endsWith('.xcodeproj') &&
2300+
fs.existsSync(path.join(builtProjectPath, SPM_INJECTED_MARKER))
2301+
? builtProjectPath
2302+
: findInjectedXcodeproj(appRoot);
22922303
const rootUuid =
22932304
xcodeprojPath != null ? readMarker(xcodeprojPath)?.rootUuid : null;
22942305
if (xcodeprojPath == null || rootUuid == null) {
@@ -2316,36 +2327,47 @@ function assertPluginFrameworksLinked(appRoot /*: string */) /*: void */ {
23162327
const outputs = pbxListValues(
23172328
findField(text, embedPhase, 'outputPaths')?.value ?? '',
23182329
);
2319-
const builtinPrefixes = new Set(
2320-
BUILTIN_FRAMEWORKS.map(framework => frameworkSettingPrefix(framework.id)),
2330+
const linkedPrefixes = new Set(
2331+
inputs.map(input => /^\$\((RN_SPM_\w+)_FRAMEWORK\)$/.exec(input)?.[1]),
23212332
);
2322-
const linkedPluginNames /*: Map<string, string> */ = new Map();
2323-
inputs
2324-
.map(input => /^\$\((RN_SPM_\w+)_FRAMEWORK\)$/.exec(input)?.[1])
2325-
.filter(Boolean)
2326-
.forEach((prefix, index) => {
2327-
if (!builtinPrefixes.has(prefix)) {
2328-
linkedPluginNames.set(
2329-
prefix,
2330-
path.basename(outputs[index] ?? prefix, '.framework'),
2331-
);
2332-
}
2333-
});
2333+
// Built-in framework names come from the staged manifest. Without it, only
2334+
// setting prefixes are compared: a missing manifest is not a plugin mismatch.
2335+
let linkedPluginNames /*: ?Set<string> */ = null;
2336+
if (
2337+
fs.existsSync(
2338+
path.join(appRoot, 'build', 'xcframeworks', FLAVORED_FRAMEWORKS_MANIFEST),
2339+
)
2340+
) {
2341+
const builtinIds = new Set(BUILTIN_FRAMEWORKS.map(({id}) => id));
2342+
const builtinNames = new Set(
2343+
readFlavoredFrameworksManifest(appRoot)
2344+
.frameworks.filter(framework => builtinIds.has(framework.id))
2345+
.map(framework => framework.frameworkName),
2346+
);
2347+
linkedPluginNames = new Set(
2348+
outputs
2349+
.map(output => path.basename(output, '.framework'))
2350+
.filter(name => !builtinNames.has(name)),
2351+
);
2352+
}
23342353

23352354
const pluginFrameworks = readPluginFrameworks(appRoot);
2336-
const pairedPrefixes = new Set(
2337-
pluginFrameworks.map(framework => frameworkSettingPrefix(framework.id)),
2355+
const pairedNames = new Set(
2356+
pluginFrameworks.map(framework => framework.frameworkName),
23382357
);
23392358
const problems /*: Array<string> */ = [];
23402359
for (const {id, frameworkName} of pluginFrameworks) {
2341-
if (!linkedPluginNames.has(frameworkSettingPrefix(id))) {
2360+
if (
2361+
!linkedPrefixes.has(frameworkSettingPrefix(id)) ||
2362+
linkedPluginNames?.has(frameworkName) === false
2363+
) {
23422364
problems.push(
23432365
`error: ${frameworkName} (${id}) is a precompiled framework from an autolinking plugin, but the Xcode project does not link it. Run \`npx react-native spm\` to update the project.`,
23442366
);
23452367
}
23462368
}
2347-
for (const [prefix, frameworkName] of linkedPluginNames) {
2348-
if (!pairedPrefixes.has(prefix)) {
2369+
for (const frameworkName of linkedPluginNames ?? []) {
2370+
if (!pairedNames.has(frameworkName)) {
23492371
problems.push(
23502372
`error: The Xcode project links ${frameworkName}.framework, but no autolinking plugin provides it on this machine. Precompile ${frameworkName}, or run \`npx react-native spm\` to update the project.`,
23512373
);

‎packages/react-native/scripts/spm/sync-spm-autolinking.js‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,21 @@ async function main(
134134
autolinkingArgv.push('--ios-deployment-target', iosDeploymentTarget);
135135
}
136136
deps.generateAutolinking(autolinkingArgv);
137-
// Before the stamp, so every build fails until the project is updated.
138-
deps.assertPluginFrameworksLinked(appRoot);
137+
const stampPath = path.join(
138+
appRoot,
139+
'build',
140+
'generated',
141+
'autolinking',
142+
'.spm-sync-stamp',
143+
);
144+
// Without a stamp every build re-runs sync and fails until the project is
145+
// updated; an earlier stamp would let the next build skip it.
146+
try {
147+
deps.assertPluginFrameworksLinked(appRoot);
148+
} catch (e) {
149+
fs.rmSync(stampPath, {force: true});
150+
throw e;
151+
}
139152

140153
// Rebuild the per-app generated-headers farm (vended as the ReactAppHeaders
141154
// SPM target inside the codegen package). React core headers need no trees
@@ -145,13 +158,6 @@ async function main(
145158
// so no path-locator JSON is written.
146159
deps.buildPerAppHeaderTree(appRoot, {log});
147160

148-
const stampPath = path.join(
149-
appRoot,
150-
'build',
151-
'generated',
152-
'autolinking',
153-
'.spm-sync-stamp',
154-
);
155161
fs.mkdirSync(path.dirname(stampPath), {recursive: true});
156162
fs.writeFileSync(stampPath, new Date().toISOString() + '\n', 'utf8');
157163
log('SPM autolinking sync complete.');

0 commit comments

Comments
 (0)