|
12 | 12 |
|
13 | 13 | const { |
14 | 14 | addPreActionToScheme, |
15 | | - buildEmbeddedFixScript, |
| 15 | + buildSchemePreActionScript, |
16 | 16 | buildSyncAutolinkingScript, |
17 | 17 | generateXcscheme, |
18 | 18 | } = require('../generate-spm-xcodeproj'); |
@@ -303,14 +303,20 @@ describe('buildSyncAutolinkingScript', () => { |
303 | 303 | expect(script).not.toContain('[['); |
304 | 304 | }); |
305 | 305 |
|
306 | | - it('is deterministic — the build phase and scheme pre-action get the same single script', () => { |
307 | | - // injectSpmPackages builds the phase with buildSyncAutolinkingScript(rnPath) |
308 | | - // and the scheme pre-action with the same call; a pure, deterministic result |
309 | | - // guarantees both embed byte-identical text. |
| 306 | + it('is deterministic (pure) — repeated calls are byte-identical', () => { |
310 | 307 | expect(buildSyncAutolinkingScript(BAKED)).toBe( |
311 | 308 | buildSyncAutolinkingScript(BAKED), |
312 | 309 | ); |
313 | 310 | }); |
| 311 | + |
| 312 | + it('the in-target phase and the scheme pre-action are now DIFFERENT scripts', () => { |
| 313 | + // The phase carries the swap sandwich; the pre-action is sync-only. A |
| 314 | + // pre-action swap could win its race and mask a mismatch from the in-target |
| 315 | + // detector (a false green), so they intentionally diverge. |
| 316 | + expect(buildSyncAutolinkingScript(BAKED)).not.toBe( |
| 317 | + buildSchemePreActionScript(BAKED), |
| 318 | + ); |
| 319 | + }); |
314 | 320 | }); |
315 | 321 |
|
316 | 322 | // --------------------------------------------------------------------------- |
@@ -412,38 +418,186 @@ describe('buildSyncAutolinkingScript watch-paths stale loop (behavioral)', () => |
412 | 418 | }); |
413 | 419 |
|
414 | 420 | // --------------------------------------------------------------------------- |
415 | | -// buildEmbeddedFixScript — the appended in-target phase that runs AFTER Xcode's |
416 | | -// implicit SPM Embed to deterministically correct the embedded framework flavor. |
| 421 | +// The obsolete appended "Fix SPM Embedded Flavor" phase is gone entirely — RN |
| 422 | +// never writes the .app bundle (Xcode is its single writer). |
417 | 423 | // --------------------------------------------------------------------------- |
418 | | -describe('buildEmbeddedFixScript', () => { |
419 | | - const BAKED = '../node_modules/react-native'; |
420 | | - const script = buildEmbeddedFixScript(BAKED); |
| 424 | +describe('embedded-fix phase removal', () => { |
| 425 | + it('no longer exports buildEmbeddedFixScript', () => { |
| 426 | + expect(require('../generate-spm-xcodeproj').buildEmbeddedFixScript).toBe( |
| 427 | + undefined, |
| 428 | + ); |
| 429 | + }); |
421 | 430 |
|
422 | | - it('shares the node/RN_DIR resolution preamble with the sync script', () => { |
423 | | - expect(script).toContain('NODE_BINARY="${NODE_BINARY:-}"'); |
| 431 | + it('no script references the embedded-fix phase', () => { |
| 432 | + const BAKED = '../node_modules/react-native'; |
| 433 | + for (const s of [ |
| 434 | + buildSyncAutolinkingScript(BAKED), |
| 435 | + buildSchemePreActionScript(BAKED), |
| 436 | + ]) { |
| 437 | + expect(s).not.toContain('Fix SPM Embedded Flavor'); |
| 438 | + expect(s).not.toContain('SPM embedded-flavor fix could not run'); |
| 439 | + } |
| 440 | + }); |
| 441 | +}); |
| 442 | + |
| 443 | +// --------------------------------------------------------------------------- |
| 444 | +// HARD-FAIL + CONVERGE — the in-target phase runs the swap sandwich and fails |
| 445 | +// the build ONCE at the end after correcting a mismatched start; the scheme |
| 446 | +// PRE-ACTION is sync-only (no swap → cannot mask a mismatch = false green). |
| 447 | +// --------------------------------------------------------------------------- |
| 448 | +describe('buildSchemePreActionScript (sync-only)', () => { |
| 449 | + const script = buildSchemePreActionScript('../node_modules/react-native'); |
| 450 | + |
| 451 | + it('does NOT dispatch or define a flavor swap', () => { |
| 452 | + expect(script).not.toContain('swap-flavor'); |
| 453 | + expect(script).not.toContain('run_swap_flavor'); |
| 454 | + expect(script).not.toContain('MISMATCH'); |
| 455 | + }); |
| 456 | + |
| 457 | + it('still runs the sync dispatch (its auto-heal purpose)', () => { |
424 | 458 | expect(script).toContain( |
425 | | - "require('path').dirname(require.resolve('react-native/package.json'))", |
| 459 | + '"$NODE_BINARY" "$RN_DIR/scripts/setup-apple-spm.js" sync', |
426 | 460 | ); |
427 | | - expect(script).toContain(`RN_DIR="${BAKED}"`); |
| 461 | + expect(script).toContain('if [ "$STALE" -eq 1 ]; then'); |
428 | 462 | }); |
429 | 463 |
|
430 | | - it('dispatches swap-flavor directly (no npx) and is a single dispatch (no sync, no sandwich)', () => { |
431 | | - expect(script).toContain( |
432 | | - '"$NODE_BINARY" "$RN_DIR/scripts/setup-apple-spm.js" swap-flavor', |
| 464 | + it('parses under `sh -n`', () => { |
| 465 | + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'preaction-')); |
| 466 | + const file = path.join(dir, 'pre.sh'); |
| 467 | + fs.writeFileSync(file, script); |
| 468 | + try { |
| 469 | + expect(() => execFileSync('/bin/sh', ['-n', file])).not.toThrow(); |
| 470 | + } finally { |
| 471 | + fs.rmSync(dir, {recursive: true, force: true}); |
| 472 | + } |
| 473 | + }); |
| 474 | +}); |
| 475 | + |
| 476 | +describe('buildSyncAutolinkingScript — swap sandwich + hard-fail', () => { |
| 477 | + const script = buildSyncAutolinkingScript('../node_modules/react-native'); |
| 478 | + |
| 479 | + it('runs the swap sandwich (def + leading + trailing = ≥3 run_swap_flavor uses)', () => { |
| 480 | + expect( |
| 481 | + (script.match(/run_swap_flavor/g) || []).length, |
| 482 | + ).toBeGreaterThanOrEqual(3); |
| 483 | + }); |
| 484 | + |
| 485 | + it('the LEADING swap runs BEFORE the sync dispatch (state corrected up front)', () => { |
| 486 | + const leading = script.indexOf('run_swap_flavor\n'); |
| 487 | + const sync = script.indexOf('setup-apple-spm.js" sync'); |
| 488 | + expect(leading).toBeGreaterThan(-1); |
| 489 | + expect(leading).toBeLessThan(sync); |
| 490 | + }); |
| 491 | + |
| 492 | + it('captures the corrected-mismatch signal and defers the failure to the END', () => { |
| 493 | + expect(script).toContain('MISMATCH_PENDING'); |
| 494 | + const gate = script.indexOf('if [ "$MISMATCH_PENDING" -eq 1 ]'); |
| 495 | + expect(gate).toBeGreaterThan(-1); |
| 496 | + // The failure gate is AFTER the last swap (trailing) and after sync. |
| 497 | + expect(gate).toBeGreaterThan(script.lastIndexOf('run_swap_flavor')); |
| 498 | + expect(script.indexOf('setup-apple-spm.js" sync')).toBeLessThan(gate); |
| 499 | + expect(script.indexOf('exit 1', gate)).toBeGreaterThan(gate); |
| 500 | + }); |
| 501 | + |
| 502 | + it('parses under `sh -n`', () => { |
| 503 | + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-')); |
| 504 | + const file = path.join(dir, 'phase.sh'); |
| 505 | + fs.writeFileSync(file, script); |
| 506 | + try { |
| 507 | + expect(() => execFileSync('/bin/sh', ['-n', file])).not.toThrow(); |
| 508 | + } finally { |
| 509 | + fs.rmSync(dir, {recursive: true, force: true}); |
| 510 | + } |
| 511 | + }); |
| 512 | +}); |
| 513 | + |
| 514 | +// A real-shell behavioral check of HARD-FAIL + CONVERGE. The stub setup-apple-spm |
| 515 | +// is STATEFUL: swap-flavor reads a fake pin file, and only repoints + exits 1 on |
| 516 | +// a genuine pin-vs-CONFIGURATION mismatch; sync re-pins the fake pin to debug |
| 517 | +// (modelling generate-spm-package's default-debug links). This exercises the |
| 518 | +// leading-vs-trailing distinction the constant-exit-code stub could not. |
| 519 | +describe('sync script — shell behavior of HARD-FAIL + CONVERGE', () => { |
| 520 | + let dir, rnDir, srcroot, pinFile; |
| 521 | + |
| 522 | + function setup(startFlavor) { |
| 523 | + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shell-hardfail-')); |
| 524 | + srcroot = path.join(dir, 'app'); |
| 525 | + fs.mkdirSync(srcroot, {recursive: true}); |
| 526 | + fs.writeFileSync( |
| 527 | + path.join(srcroot, 'package.json'), |
| 528 | + JSON.stringify({name: 'x'}), |
433 | 529 | ); |
434 | | - // Exactly one swap dispatch; NOT the sync-phase machinery. |
435 | | - expect(script.match(/setup-apple-spm\.js" swap-flavor/g)).toHaveLength(1); |
436 | | - expect(script).not.toContain('swap-flavor" sync'); |
437 | | - expect(script).not.toContain('run_swap_flavor'); |
438 | | - expect(script).not.toContain('"$STALE"'); |
| 530 | + pinFile = path.join(dir, 'pin'); |
| 531 | + fs.writeFileSync(pinFile, startFlavor); // the flavor the build STARTS on |
| 532 | + rnDir = path.join(dir, 'rn'); |
| 533 | + fs.mkdirSync(path.join(rnDir, 'scripts'), {recursive: true}); |
| 534 | + fs.writeFileSync( |
| 535 | + path.join(rnDir, 'scripts', 'setup-apple-spm.js'), |
| 536 | + [ |
| 537 | + "const fs = require('fs');", |
| 538 | + 'const pinFile = process.env.FAKE_PIN_FILE;', |
| 539 | + 'const a = process.argv[2];', |
| 540 | + // sync re-pins to the add-time (debug) flavor, like linkOne does. |
| 541 | + "if (a === 'sync') { fs.writeFileSync(pinFile, 'debug'); process.exit(0); }", |
| 542 | + "if (a === 'swap-flavor') {", |
| 543 | + " const desired = process.env.CONFIGURATION === 'Release' ? 'release' : 'debug';", |
| 544 | + " const pin = fs.existsSync(pinFile) ? fs.readFileSync(pinFile, 'utf8').trim() : 'debug';", |
| 545 | + ' if (pin !== desired) {', |
| 546 | + ' fs.writeFileSync(pinFile, desired);', |
| 547 | + " console.log('error: corrected ' + pin + ' -> ' + desired);", |
| 548 | + ' process.exit(1);', |
| 549 | + ' }', |
| 550 | + ' process.exit(0);', |
| 551 | + '}', |
| 552 | + 'process.exit(0);', |
| 553 | + ].join('\n'), |
| 554 | + ); |
| 555 | + const scriptFile = path.join(dir, 'phase.sh'); |
| 556 | + fs.writeFileSync(scriptFile, buildSyncAutolinkingScript(rnDir)); |
| 557 | + return scriptFile; |
| 558 | + } |
| 559 | + |
| 560 | + afterEach(() => fs.rmSync(dir, {recursive: true, force: true})); |
| 561 | + |
| 562 | + function run(scriptFile, configuration) { |
| 563 | + try { |
| 564 | + execFileSync('/bin/sh', [scriptFile], { |
| 565 | + env: { |
| 566 | + ...process.env, |
| 567 | + SRCROOT: srcroot, |
| 568 | + BUILT_PRODUCTS_DIR: path.join(dir, 'products'), |
| 569 | + NODE_BINARY: process.execPath, |
| 570 | + CONFIGURATION: configuration, |
| 571 | + FAKE_PIN_FILE: pinFile, |
| 572 | + }, |
| 573 | + stdio: ['ignore', 'pipe', 'pipe'], |
| 574 | + encoding: 'utf8', |
| 575 | + }); |
| 576 | + return {code: 0}; |
| 577 | + } catch (e) { |
| 578 | + return {code: e.status, out: (e.stdout || '') + (e.stderr || '')}; |
| 579 | + } |
| 580 | + } |
| 581 | + |
| 582 | + it('SUCCEEDS (exit 0) when only the TRAILING swap repoints (sync re-pinned mid-build) — not a mismatched start', () => { |
| 583 | + // Start on release, build Release: the LEADING swap is matched (no |
| 584 | + // correction). The (stale) sync re-pins to debug, so the TRAILING swap |
| 585 | + // repoints back to release. Only the leading swap may flag a mismatch, so |
| 586 | + // this must NOT fail the build. |
| 587 | + const {code} = run(setup('release'), 'Release'); |
| 588 | + expect(code).toBe(0); |
439 | 589 | }); |
440 | 590 |
|
441 | | - it('soft-fails (warns, no exit 1) so it never hard-breaks the build alone', () => { |
442 | | - expect(script).toContain('warning: SPM embedded-flavor fix could not run'); |
443 | | - expect(script).not.toContain('exit 1'); |
| 591 | + it('FAILS the build (exit 1) when the build STARTED on the wrong flavor (leading swap corrects)', () => { |
| 592 | + // Start on debug, build Release: the LEADING swap corrects debug->release and |
| 593 | + // flags it → fail once, converge, rebuild green. |
| 594 | + const {code, out} = run(setup('debug'), 'Release'); |
| 595 | + expect(code).toBe(1); |
| 596 | + expect(out).toMatch(/error:/); |
444 | 597 | }); |
445 | 598 |
|
446 | | - it('is POSIX-sh clean (no bashisms)', () => { |
447 | | - expect(script).not.toContain('[['); |
| 599 | + it('SUCCEEDS (exit 0) when the build started matched (Debug on debug)', () => { |
| 600 | + const {code} = run(setup('debug'), 'Debug'); |
| 601 | + expect(code).toBe(0); |
448 | 602 | }); |
449 | 603 | }); |
0 commit comments