Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
injectSpmIntoPbxproj,
planInjection,
} = require('../generate-spm-xcodeproj');
const {findField, findObjectByUuid, quoteIfNeeded} = require('../spm-pbxproj');
const {
findField,
findObjectByUuid,
quoteIfNeeded,
removeField,
setScalarField,
} = require('../spm-pbxproj');
const {isBalanced} = require('./pbxproj-oracles');
const fs = require('node:fs');
const os = require('node:os');
Expand Down Expand Up @@ -85,12 +91,16 @@
const SYNC_SCRIPT_PATH = 'ReactNativeSPM/Scripts/sync-autolinking.sh';
const EMBED_SCRIPT_PATH = 'ReactNativeSPM/Scripts/embed-flavored-frameworks.sh';

// The raw (still plist-quoted) shellScript value of the phase labelled `label`.
function shellScriptOf(text, label) {
function scriptPhaseOf(text, label) {
const uuid = new RegExp(`([0-9A-F]{24}) /\\* ${label} \\*/ = \\{`).exec(
text,
)[1];
return findField(text, findObjectByUuid(text, uuid), 'shellScript').value;
return findObjectByUuid(text, uuid);
}

// The raw (still plist-quoted) shellScript value of the phase labelled `label`.
function shellScriptOf(text, label) {
return findField(text, scriptPhaseOf(text, label), 'shellScript').value;
}

const RN_PATH = '../node_modules/react-native';
Expand Down Expand Up @@ -658,7 +668,35 @@
const sourcesIdx = text.indexOf('Sources */,');
expect(syncIdx).toBeGreaterThan(-1);
expect(syncIdx).toBeLessThan(sourcesIdx);
});
expect(
findField(
text,
scriptPhaseOf(text, 'Sync SPM Autolinking'),
'alwaysOutOfDate',
)?.value,
).toBe('1');
});

it.each([null, '0'])(
'marks an existing sync phase with alwaysOutOfDate=%s as always running',
previousValue => {
const {text} = inject(PLAIN);
const phase = scriptPhaseOf(text, 'Sync SPM Autolinking');
const before =
previousValue == null
? removeField(text, phase, 'alwaysOutOfDate')
: setScalarField(text, phase, 'alwaysOutOfDate', previousValue);
const {text: updated} = inject(before);
expect(
findField(
updated,
scriptPhaseOf(updated, 'Sync SPM Autolinking'),
'alwaysOutOfDate',
)?.value,
).toBe('1');
expect(inject(updated).text).toBe(updated);
},
);

// The script bodies change whenever React Native's do, so they live in
// generated files; the phase only carries a wrapper that never changes.
Expand Down Expand Up @@ -994,26 +1032,29 @@
});

it('emits an unquoted alwaysOutOfDate = 1 only when the phase asks for it', () => {
const withFlag = inject(
const {text: withFlag, scriptPhaseUuids} = inject(
PLAIN,
null,
null,
[],
[{...APP_CONFIG_PHASE, alwaysOutOfDate: true}],
).text;
expect(withFlag).toContain('alwaysOutOfDate = 1;');
);
const uuid = scriptPhaseUuids[APP_CONFIG_PHASE.id];
const phase = findObjectByUuid(withFlag, uuid);
expect(findField(withFlag, phase, 'alwaysOutOfDate')?.value).toBe('1');
// Xcode writes it immediately after `isa` — match that to avoid churn.
expect(withFlag).toContain(
expect(withFlag.slice(phase.bodyOpen, phase.bodyClose)).toContain(
'isa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;',
);

for (const phase of [

Check warning on line 1050 in packages/react-native/scripts/spm/__tests__/inject-spm-xcodeproj-test.js

View workflow job for this annotation

GitHub Actions / test_js (24)

'phase' is already declared in the upper scope on line 1043 column 11

Check warning on line 1050 in packages/react-native/scripts/spm/__tests__/inject-spm-xcodeproj-test.js

View workflow job for this annotation

GitHub Actions / test_js (22.13.0)

'phase' is already declared in the upper scope on line 1043 column 11
APP_CONFIG_PHASE,
{...APP_CONFIG_PHASE, alwaysOutOfDate: false},
]) {
expect(inject(PLAIN, null, null, [], [phase]).text).not.toContain(
'alwaysOutOfDate',
);
const {text} = inject(PLAIN, null, null, [], [phase]);
expect(
findField(text, findObjectByUuid(text, uuid), 'alwaysOutOfDate'),
).toBeNull();
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {
readPinnedConfigCommand,
removeSpmInjection,
} = require('../generate-spm-xcodeproj');
const {findField, findObjectByUuid} = require('../spm-pbxproj');
const {isBalanced} = require('./pbxproj-oracles');
const fs = require('node:fs');
const os = require('node:os');
Expand Down Expand Up @@ -1380,7 +1381,10 @@ describe('script-phases reconciliation on update', () => {
reactNativeRoot: rnRoot,
xcodeprojPath,
});
expect(pbxprojOf(xcodeprojPath)).toContain('alwaysOutOfDate = 1;');
const uuid = readMarker(xcodeprojPath).scriptPhases[APP_CONFIG_PHASE.id];
const fieldOf = text =>
findField(text, findObjectByUuid(text, uuid), 'alwaysOutOfDate');
expect(fieldOf(pbxprojOf(xcodeprojPath))?.value).toBe('1');

writeScriptPhases(appRoot, [{...APP_CONFIG_PHASE, alwaysOutOfDate: false}]);
injectSpmIntoExistingXcodeproj({
Expand All @@ -1390,7 +1394,7 @@ describe('script-phases reconciliation on update', () => {
});

const after = pbxprojOf(xcodeprojPath);
expect(after).not.toContain('alwaysOutOfDate');
expect(fieldOf(after)).toBeNull();
expect(after).toContain('Bundle Expo app.config');

// …and back on again, stably (the field is re-added to an existing object,
Expand All @@ -1403,7 +1407,7 @@ describe('script-phases reconciliation on update', () => {
xcodeprojPath,
});
const back = pbxprojOf(xcodeprojPath);
expect(back).toContain('alwaysOutOfDate = 1;');
expect(fieldOf(back)?.value).toBe('1');
injectSpmIntoExistingXcodeproj({
appRoot,
reactNativeRoot: rnRoot,
Expand Down
12 changes: 5 additions & 7 deletions packages/react-native/scripts/spm/generate-spm-xcodeproj.js
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,8 @@ function injectSpmIntoPbxproj(
syncPhaseUuid,
'Sync SPM Autolinking',
generatedScriptWrapper(SYNC_AUTOLINKING_SCRIPT_PATH),
// The script checks its own stamp and discovered inputs on every build.
{alwaysOutOfDate: true},
);
if (!text.includes(syncPhaseUuid)) {
text = insertObjectsIntoSection(
Expand All @@ -1771,13 +1773,9 @@ function injectSpmIntoPbxproj(
serializeEntry(syncEntry),
);
} else {
// Already injected on a prior run — the phase object owns these fields,
// so refresh them in place (same quoting used at creation) in case a
// project injected before the script moved to its own file still carries
// it inline, or one injected before shellPath moved to bash (see
// shellScriptPhase) still has the stale value. Byte-identical when neither
// did; field order and every other byte of the phase are untouched.
for (const key of ['shellScript', 'shellPath']) {
// Refresh generated fields in existing projects, including older phases
// with inline scripts, /bin/sh or dependency analysis enabled.
for (const key of ['shellScript', 'shellPath', 'alwaysOutOfDate']) {
const current = findObjectByUuid(text, syncPhaseUuid);
if (current != null) {
text = setScalarField(text, current, key, syncEntry.fields[key]);
Expand Down
Loading