Skip to content

Commit 361ddf3

Browse files
committed
Simplify plugin framework mismatch check and tests
1 parent 25524a3 commit 361ddf3

5 files changed

Lines changed: 41 additions & 115 deletions

File tree

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ The declarations are recorded to
130130
`<outputDir>/.spm-plugin-flavored-frameworks.json`, normalized into the same
131131
immutable app-local slots as React Native, and added to Xcode's exact linker and
132132
embed settings. They are not emitted as SwiftPM product dependencies. Adding or
133-
removing one requires `npx react-native spm`. The build-time `spm sync` never
134-
changes the Xcode project. When the frameworks that plugins provide and the
135-
frameworks that the project links disagree, sync fails the build. An `error:`
136-
line names each framework and the command to run.
133+
removing one requires `spm update`; the build-time `spm sync` intentionally does
134+
not mutate runtime framework settings. When the frameworks that plugins provide
135+
and the frameworks that the project links disagree, sync fails the build. An
136+
`error:` line names each framework and the command to run.
137137

138138
### `watchPaths` — plugin staleness inputs
139139

@@ -351,10 +351,9 @@ green build missing native code) is worse than a loud stop.
351351
on the app target at the requested position, recording the id→UUID map in the
352352
`.spm-injected.json` marker so a re-run refreshes each phase's content in
353353
place, re-seats it when its declared position or order changed, `update`
354-
removes phases that left the sidecar, and `deinit` reverts them. The
355-
build-time `sync` only rewrites the sidecar; it never mutates the project.
356-
(For `flavoredFrameworks`, it also fails the build when the project is out of
357-
date.)
354+
removes phases that left the sidecar, and `deinit` reverts them. Like
355+
`flavoredFrameworks`, the build-time `sync` only rewrites the sidecar; it
356+
never mutates the project.
358357
- **Co-design with Expo (not final):** codegen **provider ordering** — codegen
359358
must consume the same discovered module set the plugin contributes — is
360359
intentionally left for the first real plugin to drive to a stable shape.

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,4 @@ script branches on the exit code:
755755

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
758-
manifest or an out-of-date project should not pass silently. Sync writes its
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.
758+
manifest or an out-of-date project should not pass silently.

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

Lines changed: 15 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -119,23 +119,14 @@ function writeSidecar(appRoot, frameworks) {
119119
}
120120

121121
function projectFiles(xcodeprojPath) {
122-
const files = {};
123-
const walk = dir => {
124-
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
125-
const full = path.join(dir, entry.name);
126-
if (entry.isDirectory()) {
127-
walk(full);
128-
} else {
129-
files[path.relative(xcodeprojPath, full)] = fs.readFileSync(full);
130-
}
131-
}
132-
};
133-
walk(xcodeprojPath);
134-
return files;
122+
return ['project.pbxproj', SPM_INJECTED_MARKER].map(name => {
123+
const file = path.join(xcodeprojPath, name);
124+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
125+
});
135126
}
136127

137-
// Runs the check and proves it wrote nothing under the .xcodeproj (pbxproj,
138-
// marker, scheme). Returns the printed lines and the thrown error, if any.
128+
// Runs the check and proves it wrote neither the pbxproj nor the marker.
129+
// Returns the printed lines and the thrown error, if any.
139130
function check(app, env = {}) {
140131
const before = projectFiles(app.xcodeprojPath);
141132
let error = null;
@@ -175,24 +166,6 @@ describe('assertPluginFrameworksLinked', () => {
175166
]);
176167
});
177168

178-
it('reports every mismatch in both directions', () => {
179-
const app = scaffoldApp();
180-
const other = frameworkEntry(
181-
'other-plugin',
182-
'OtherPlugin',
183-
'plugins/other-plugin.xcframework',
184-
);
185-
inject(app, [REACT, EXPO]);
186-
writeSidecar(app.appRoot, [other]);
187-
188-
const {error, lines} = check(app);
189-
190-
expect(error).toBeInstanceOf(PluginFrameworkMismatchError);
191-
expect(lines).toHaveLength(2);
192-
expect(lines[0]).toMatch(/^error: OtherPlugin \(other-plugin\) /);
193-
expect(lines[1]).toMatch(/^error: The Xcode project links ExpoCore\./);
194-
});
195-
196169
it('fails when a plugin renames its framework but keeps its id', () => {
197170
const app = scaffoldApp();
198171
inject(app, [REACT, EXPO]);
@@ -238,25 +211,12 @@ describe('assertPluginFrameworksLinked', () => {
238211
inject(app, [REACT, HERMES, EXPO]);
239212
writeSidecar(app.appRoot, [EXPO]);
240213
fs.rmSync(
241-
path.join(
242-
app.appRoot,
243-
'build',
244-
'xcframeworks',
245-
'flavored-frameworks.json',
246-
),
214+
path.join(app.appRoot, 'build/xcframeworks/flavored-frameworks.json'),
247215
);
248216

249217
expect(check(app)).toEqual({error: null, lines: []});
250218
});
251219

252-
it('never reports built-in frameworks', () => {
253-
const app = scaffoldApp();
254-
inject(app, [REACT, HERMES]);
255-
writeSidecar(app.appRoot, []);
256-
257-
expect(check(app)).toEqual({error: null, lines: []});
258-
});
259-
260220
it('skips a project that was never injected', () => {
261221
const app = scaffoldApp();
262222
writeSidecar(app.appRoot, [EXPO]);
@@ -268,27 +228,19 @@ describe('assertPluginFrameworksLinked', () => {
268228
const app = scaffoldApp();
269229
inject(app, [REACT]);
270230
writeSidecar(app.appRoot, [EXPO]);
271-
const markerPath = path.join(app.xcodeprojPath, SPM_INJECTED_MARKER);
272-
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
273-
fs.writeFileSync(
274-
markerPath,
275-
JSON.stringify({...marker, rootUuid: 'NOT_THE_PROJECT_ROOT'}),
276-
);
231+
fs.writeFileSync(path.join(app.xcodeprojPath, 'project.pbxproj'), PLAIN);
277232

278233
expect(check(app)).toEqual({error: null, lines: []});
279234
});
280235
});
281236

282-
describe('injectSpmIntoExistingXcodeproj with a plugin framework', () => {
283-
it('is idempotent', () => {
284-
const app = scaffoldApp();
285-
inject(app, [REACT, EXPO]);
286-
const pbxprojPath = path.join(app.xcodeprojPath, 'project.pbxproj');
287-
const once = fs.readFileSync(pbxprojPath, 'utf8');
288-
expect(once).toContain('$(RN_SPM_EXPO_CORE_FRAMEWORK)');
237+
it('injects a plugin framework idempotently', () => {
238+
const app = scaffoldApp();
239+
inject(app, [REACT, EXPO]);
240+
const pbxprojPath = path.join(app.xcodeprojPath, 'project.pbxproj');
241+
const once = fs.readFileSync(pbxprojPath, 'utf8');
289242

290-
inject(app, [REACT, EXPO]);
243+
inject(app, [REACT, EXPO]);
291244

292-
expect(fs.readFileSync(pbxprojPath, 'utf8')).toBe(once);
293-
});
245+
expect(fs.readFileSync(pbxprojPath, 'utf8')).toBe(once);
294246
});

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

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,9 @@ describe('sync-spm-autolinking main', () => {
137137
expect(fs.existsSync(stampPath())).toBe(false);
138138
});
139139

140-
// No stamp on a mismatch, so the next build re-runs sync and fails again
141-
// until the project is updated.
142-
it('fails without a stamp when plugin frameworks and the project disagree', async () => {
140+
it('fails and removes the stamp when plugin frameworks and the project disagree', async () => {
141+
fs.mkdirSync(path.dirname(stampPath()), {recursive: true});
142+
fs.writeFileSync(stampPath(), 'earlier sync\n');
143143
const mismatch = new PluginFrameworkMismatchError();
144144
const deps = makeDeps({
145145
assertPluginFrameworksLinked: jest.fn(() => {
@@ -152,17 +152,4 @@ 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-
});
168155
});

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

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,7 +2231,7 @@ function readScriptPhasesManifest(
22312231
*/
22322232
function readMarker(
22332233
xcodeprojPath /*: string */,
2234-
) /*: ?{rootUuid?: ?string, targetUuid?: ?string, generatedSources?: {[string]: Array<string>}, scriptPhases?: {[string]: string}, artifactsVersionOverride?: ?string, configCommand?: ?Array<string>, buildSettingChanges?: Array<BuildSettingChange>, createdArrayFields?: Array<CreatedArrayField>, scheme?: {file?: ?string, created?: ?boolean}, ...} */ {
2234+
) /*: ?{targetUuid?: ?string, generatedSources?: {[string]: Array<string>}, scriptPhases?: {[string]: string}, artifactsVersionOverride?: ?string, configCommand?: ?Array<string>, buildSettingChanges?: Array<BuildSettingChange>, createdArrayFields?: Array<CreatedArrayField>, scheme?: {file?: ?string, created?: ?boolean}, ...} */ {
22352235
const markerPath = path.join(xcodeprojPath, SPM_INJECTED_MARKER);
22362236
try {
22372237
// $FlowFixMe[incompatible-return] JSON.parse returns any
@@ -2277,17 +2277,9 @@ class PluginFrameworkMismatchError extends Error {
22772277
}
22782278
}
22792279

2280-
function pbxListValues(list /*: string */) /*: Array<string> */ {
2281-
return (list.match(/"(?:[^"\\]|\\.)*"|[^\s,()"]+/g) ?? []).map(unquotePlist);
2282-
}
2283-
22842280
/**
2285-
* Compares the precompiled frameworks autolinking plugins provide on this
2286-
* machine with the plugin frameworks the injected project links. Only `spm
2287-
* add` / `spm update` change what the project links, so the build-time sync
2288-
* reports a mismatch instead of fixing it: one `error:` line per framework,
2289-
* then PluginFrameworkMismatchError. Never writes the project. Checks the
2290-
* project Xcode is building (`PROJECT_FILE_PATH`) when it is injected.
2281+
* Only `spm add` / `spm update` change what the project links, so the
2282+
* build-time sync reports a plugin framework mismatch instead of fixing it.
22912283
*/
22922284
function assertPluginFrameworksLinked(
22932285
appRoot /*: string */,
@@ -2300,15 +2292,17 @@ function assertPluginFrameworksLinked(
23002292
fs.existsSync(path.join(builtProjectPath, SPM_INJECTED_MARKER))
23012293
? builtProjectPath
23022294
: findInjectedXcodeproj(appRoot);
2303-
const rootUuid =
2304-
xcodeprojPath != null ? readMarker(xcodeprojPath)?.rootUuid : null;
2305-
if (xcodeprojPath == null || rootUuid == null) {
2295+
if (xcodeprojPath == null) {
23062296
return;
23072297
}
23082298
const text = fs.readFileSync(
23092299
path.join(xcodeprojPath, 'project.pbxproj'),
23102300
'utf8',
23112301
);
2302+
const rootUuid = findProjectObject(text)?.uuid;
2303+
if (rootUuid == null) {
2304+
return;
2305+
}
23122306
const embedPhase = findObjectByUuid(
23132307
text,
23142308
namespacedUUID(
@@ -2321,14 +2315,10 @@ function assertPluginFrameworksLinked(
23212315
return;
23222316
}
23232317

2324-
const inputs = pbxListValues(
2325-
findField(text, embedPhase, 'inputPaths')?.value ?? '',
2326-
);
2327-
const outputs = pbxListValues(
2328-
findField(text, embedPhase, 'outputPaths')?.value ?? '',
2329-
);
2318+
const inputs = findField(text, embedPhase, 'inputPaths')?.value ?? '';
2319+
const outputs = findField(text, embedPhase, 'outputPaths')?.value ?? '';
23302320
const linkedPrefixes = new Set(
2331-
inputs.map(input => /^\$\((RN_SPM_\w+)_FRAMEWORK\)$/.exec(input)?.[1]),
2321+
Array.from(inputs.matchAll(/\$\((RN_SPM_\w+)_FRAMEWORK\)/g), m => m[1]),
23322322
);
23332323
// Built-in framework names come from the staged manifest. Without it, only
23342324
// setting prefixes are compared: a missing manifest is not a plugin mismatch.
@@ -2345,9 +2335,10 @@ function assertPluginFrameworksLinked(
23452335
.map(framework => framework.frameworkName),
23462336
);
23472337
linkedPluginNames = new Set(
2348-
outputs
2349-
.map(output => path.basename(output, '.framework'))
2350-
.filter(name => !builtinNames.has(name)),
2338+
Array.from(
2339+
outputs.matchAll(/([^/"\s]+)\.framework\b/g),
2340+
m => m[1],
2341+
).filter(name => !builtinNames.has(name)),
23512342
);
23522343
}
23532344

0 commit comments

Comments
 (0)