@hominis/fireforge 0.44.4 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +120 -0
- package/dist/bin/fireforge.js +6 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli-usage-error.d.ts +46 -0
- package/dist/src/cli-usage-error.js +81 -0
- package/dist/src/cli.d.ts +5 -1
- package/dist/src/cli.js +20 -21
- package/dist/src/commands/bootstrap.js +1 -2
- package/dist/src/commands/build.js +15 -6
- package/dist/src/commands/destructive-decision.d.ts +19 -0
- package/dist/src/commands/destructive-decision.js +31 -0
- package/dist/src/commands/doctor-check-core.d.ts +0 -6
- package/dist/src/commands/doctor-check-core.js +0 -8
- package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
- package/dist/src/commands/doctor-orphaned-harness.js +4 -12
- package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
- package/dist/src/commands/doctor-source-pin.d.ts +29 -0
- package/dist/src/commands/doctor-source-pin.js +81 -0
- package/dist/src/commands/doctor.d.ts +2 -0
- package/dist/src/commands/doctor.js +9 -8
- package/dist/src/commands/download.js +35 -24
- package/dist/src/commands/export-all.js +21 -4
- package/dist/src/commands/export-flow.js +15 -60
- package/dist/src/commands/export-placement-conflicts.js +3 -3
- package/dist/src/commands/export-placement-gate.js +2 -8
- package/dist/src/commands/export-placement-policy.js +3 -5
- package/dist/src/commands/export-shared.d.ts +42 -23
- package/dist/src/commands/export-shared.js +4 -19
- package/dist/src/commands/export.js +10 -1
- package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
- package/dist/src/commands/furnace/authoring-config.js +19 -0
- package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
- package/dist/src/commands/furnace/create-browser-test.js +3 -8
- package/dist/src/commands/furnace/create-dry-run.js +2 -5
- package/dist/src/commands/furnace/create-templates.d.ts +17 -1
- package/dist/src/commands/furnace/create-templates.js +2 -1
- package/dist/src/commands/furnace/create.js +25 -19
- package/dist/src/commands/furnace/deploy.js +11 -2
- package/dist/src/commands/furnace/init.js +2 -1
- package/dist/src/commands/furnace/override.js +28 -19
- package/dist/src/commands/furnace/refresh.js +9 -1
- package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
- package/dist/src/commands/furnace/remove-mutations.js +0 -1
- package/dist/src/commands/furnace/remove.js +2 -6
- package/dist/src/commands/furnace/rename-browser-test.js +1 -7
- package/dist/src/commands/furnace/rename.js +29 -29
- package/dist/src/commands/furnace/status.js +12 -9
- package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
- package/dist/src/commands/furnace/test-file-name.js +39 -0
- package/dist/src/commands/furnace/validation-output.d.ts +21 -9
- package/dist/src/commands/furnace/validation-output.js +3 -8
- package/dist/src/commands/import.js +2 -9
- package/dist/src/commands/lint-per-patch.d.ts +2 -2
- package/dist/src/commands/lint-per-patch.js +19 -4
- package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
- package/dist/src/commands/lint.d.ts +2 -1
- package/dist/src/commands/lint.js +1 -1
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +2 -4
- package/dist/src/commands/patch/compact.js +14 -15
- package/dist/src/commands/patch/delete.js +15 -25
- package/dist/src/commands/patch/entry-projection.d.ts +13 -0
- package/dist/src/commands/patch/entry-projection.js +24 -0
- package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
- package/dist/src/commands/patch/lint-ignore.js +19 -29
- package/dist/src/commands/patch/move-files-create.js +24 -10
- package/dist/src/commands/patch/move-files-into.js +38 -37
- package/dist/src/commands/patch/move-files.js +4 -13
- package/dist/src/commands/patch/rename.js +27 -41
- package/dist/src/commands/patch/reorder.js +25 -38
- package/dist/src/commands/patch/split-plan.d.ts +4 -5
- package/dist/src/commands/patch/split-plan.js +4 -11
- package/dist/src/commands/patch/split.js +45 -43
- package/dist/src/commands/patch/staged-dependency.js +16 -22
- package/dist/src/commands/patch/tier.js +15 -25
- package/dist/src/commands/patch-rename-map.d.ts +19 -0
- package/dist/src/commands/patch-rename-map.js +36 -0
- package/dist/src/commands/pending-resolution.d.ts +15 -0
- package/dist/src/commands/pending-resolution.js +25 -0
- package/dist/src/commands/re-export-adjacent.d.ts +32 -2
- package/dist/src/commands/re-export-adjacent.js +51 -10
- package/dist/src/commands/re-export-bulk-scan.js +6 -10
- package/dist/src/commands/re-export-drift.d.ts +8 -0
- package/dist/src/commands/re-export-drift.js +65 -4
- package/dist/src/commands/re-export-files.d.ts +2 -3
- package/dist/src/commands/re-export-files.js +38 -32
- package/dist/src/commands/re-export-lint.d.ts +3 -4
- package/dist/src/commands/re-export-lint.js +9 -2
- package/dist/src/commands/re-export-options.js +19 -5
- package/dist/src/commands/re-export-register.js +5 -1
- package/dist/src/commands/re-export-scan.js +8 -8
- package/dist/src/commands/re-export-single.d.ts +22 -3
- package/dist/src/commands/re-export-single.js +19 -11
- package/dist/src/commands/re-export.js +44 -27
- package/dist/src/commands/rebase/abort.js +3 -8
- package/dist/src/commands/rebase/continue.js +12 -12
- package/dist/src/commands/rebase/index.js +2 -2
- package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
- package/dist/src/commands/rebase/patch-loop.js +2 -7
- package/dist/src/commands/resolve.js +18 -10
- package/dist/src/commands/run.d.ts +0 -13
- package/dist/src/commands/run.js +12 -25
- package/dist/src/commands/setup-support.d.ts +16 -4
- package/dist/src/commands/setup-support.js +16 -16
- package/dist/src/commands/setup.js +3 -9
- package/dist/src/commands/source.js +8 -1
- package/dist/src/commands/status-check.js +2 -2
- package/dist/src/commands/status-output.js +2 -2
- package/dist/src/commands/status.js +3 -4
- package/dist/src/commands/test-build-phase.d.ts +2 -2
- package/dist/src/commands/test-build-phase.js +33 -8
- package/dist/src/commands/test-diagnose.js +14 -5
- package/dist/src/commands/test-modes.d.ts +2 -2
- package/dist/src/commands/test-modes.js +0 -1
- package/dist/src/commands/test-register.js +6 -3
- package/dist/src/commands/test-run.js +29 -21
- package/dist/src/commands/test-stale-gate.js +8 -5
- package/dist/src/commands/test-verdict.d.ts +16 -2
- package/dist/src/commands/test-verdict.js +18 -3
- package/dist/src/commands/test.js +51 -7
- package/dist/src/commands/token-list.d.ts +26 -0
- package/dist/src/commands/token-list.js +153 -0
- package/dist/src/commands/token.js +17 -1
- package/dist/src/commands/tree.js +7 -2
- package/dist/src/commands/typecheck.js +9 -9
- package/dist/src/commands/wire.js +4 -3
- package/dist/src/core/branding.d.ts +0 -7
- package/dist/src/core/branding.js +3 -2
- package/dist/src/core/browser-wire.js +6 -3
- package/dist/src/core/build-audit-platform.d.ts +29 -0
- package/dist/src/core/build-audit-platform.js +140 -0
- package/dist/src/core/build-audit-registration.js +3 -2
- package/dist/src/core/build-audit-skip.d.ts +57 -0
- package/dist/src/core/build-audit-skip.js +112 -0
- package/dist/src/core/build-audit.d.ts +15 -2
- package/dist/src/core/build-audit.js +154 -12
- package/dist/src/core/build-baseline.d.ts +45 -28
- package/dist/src/core/build-baseline.js +6 -29
- package/dist/src/core/build-jar-escalation.d.ts +64 -0
- package/dist/src/core/build-jar-escalation.js +128 -0
- package/dist/src/core/build-overwrite-guard.js +1 -1
- package/dist/src/core/build-prepare.d.ts +5 -2
- package/dist/src/core/build-prepare.js +34 -6
- package/dist/src/core/config-paths.d.ts +2 -6
- package/dist/src/core/config-paths.js +4 -2
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -19
- package/dist/src/core/config-validate-build-audit.d.ts +8 -0
- package/dist/src/core/config-validate-build-audit.js +80 -0
- package/dist/src/core/config-validate-file-size.d.ts +12 -0
- package/dist/src/core/config-validate-file-size.js +65 -0
- package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
- package/dist/src/core/config-validate-patch-policy.js +3 -3
- package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
- package/dist/src/core/config-validate-test-toolchains.js +4 -5
- package/dist/src/core/config-validate.js +20 -3
- package/dist/src/core/config.d.ts +2 -2
- package/dist/src/core/config.js +11 -15
- package/dist/src/core/coverage-extend.js +4 -4
- package/dist/src/core/discard-baseline.js +7 -7
- package/dist/src/core/engine-precondition.d.ts +1 -9
- package/dist/src/core/engine-precondition.js +3 -8
- package/dist/src/core/engine-session-lock.js +13 -17
- package/dist/src/core/export-moved-code-shape.js +2 -2
- package/dist/src/core/file-lock-owner.d.ts +103 -0
- package/dist/src/core/file-lock-owner.js +259 -0
- package/dist/src/core/file-lock.d.ts +2 -7
- package/dist/src/core/file-lock.js +40 -91
- package/dist/src/core/firefox-cache.d.ts +13 -1
- package/dist/src/core/firefox-cache.js +59 -34
- package/dist/src/core/firefox-download.d.ts +15 -0
- package/dist/src/core/firefox-download.js +37 -21
- package/dist/src/core/firefox-extract.d.ts +6 -5
- package/dist/src/core/firefox-extract.js +36 -18
- package/dist/src/core/firefox.d.ts +30 -32
- package/dist/src/core/firefox.js +5 -36
- package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
- package/dist/src/core/furnace-apply-dry-run.js +1 -7
- package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
- package/dist/src/core/furnace-apply-helpers.js +8 -19
- package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +30 -10
- package/dist/src/core/furnace-config.d.ts +1 -1
- package/dist/src/core/furnace-config.js +23 -36
- package/dist/src/core/furnace-constants.js +3 -2
- package/dist/src/core/furnace-dir-entry.d.ts +20 -0
- package/dist/src/core/furnace-dir-entry.js +21 -0
- package/dist/src/core/furnace-operation.d.ts +2 -0
- package/dist/src/core/furnace-operation.js +3 -1
- package/dist/src/core/furnace-refresh.d.ts +18 -6
- package/dist/src/core/furnace-refresh.js +3 -6
- package/dist/src/core/furnace-registration-ast.d.ts +0 -1
- package/dist/src/core/furnace-registration-ast.js +0 -3
- package/dist/src/core/furnace-registration.d.ts +2 -1
- package/dist/src/core/furnace-registration.js +22 -29
- package/dist/src/core/furnace-scanner.js +3 -6
- package/dist/src/core/furnace-state-persist.d.ts +2 -2
- package/dist/src/core/furnace-validate-registration.js +5 -7
- package/dist/src/core/git-base.d.ts +3 -1
- package/dist/src/core/git-base.js +25 -3
- package/dist/src/core/git-diff-new-file.d.ts +40 -0
- package/dist/src/core/git-diff-new-file.js +119 -0
- package/dist/src/core/git-diff.d.ts +9 -9
- package/dist/src/core/git-diff.js +88 -157
- package/dist/src/core/git-file-ops.d.ts +0 -7
- package/dist/src/core/git-file-ops.js +4 -28
- package/dist/src/core/git-readonly-index.d.ts +0 -2
- package/dist/src/core/git-readonly-index.js +0 -4
- package/dist/src/core/git-status.d.ts +7 -0
- package/dist/src/core/git-status.js +21 -9
- package/dist/src/core/git.d.ts +0 -6
- package/dist/src/core/git.js +14 -43
- package/dist/src/core/harness-orphans.d.ts +73 -0
- package/dist/src/core/harness-orphans.js +187 -0
- package/dist/src/core/history-log.d.ts +19 -0
- package/dist/src/core/history-log.js +29 -0
- package/dist/src/core/mach-error-hints.d.ts +0 -5
- package/dist/src/core/mach-error-hints.js +1 -1
- package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
- package/dist/src/core/mach-known-noise-filter.js +31 -10
- package/dist/src/core/mach-mozconfig.js +2 -1
- package/dist/src/core/mach-python.d.ts +2 -0
- package/dist/src/core/mach-python.js +12 -7
- package/dist/src/core/mach-resource-shim.d.ts +13 -1
- package/dist/src/core/mach-resource-shim.js +112 -9
- package/dist/src/core/mach.d.ts +46 -53
- package/dist/src/core/mach.js +31 -83
- package/dist/src/core/marionette-port.d.ts +0 -17
- package/dist/src/core/marionette-port.js +12 -58
- package/dist/src/core/marionette-preflight.js +9 -27
- package/dist/src/core/mochitest-server-port.js +3 -3
- package/dist/src/core/moz-manifest-rules.js +37 -33
- package/dist/src/core/patch-apply-fuzz.js +4 -4
- package/dist/src/core/patch-apply.d.ts +4 -5
- package/dist/src/core/patch-apply.js +55 -32
- package/dist/src/core/patch-export-coverage.d.ts +0 -9
- package/dist/src/core/patch-export-coverage.js +1 -30
- package/dist/src/core/patch-export-update.d.ts +18 -1
- package/dist/src/core/patch-export-update.js +4 -3
- package/dist/src/core/patch-export.d.ts +11 -21
- package/dist/src/core/patch-export.js +44 -66
- package/dist/src/core/patch-files.d.ts +0 -4
- package/dist/src/core/patch-files.js +0 -11
- package/dist/src/core/patch-lint-binary.d.ts +6 -12
- package/dist/src/core/patch-lint-cache.d.ts +17 -1
- package/dist/src/core/patch-lint-cache.js +3 -6
- package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
- package/dist/src/core/patch-lint-checkjs.js +43 -25
- package/dist/src/core/patch-lint-creators.d.ts +2 -6
- package/dist/src/core/patch-lint-cross.d.ts +16 -8
- package/dist/src/core/patch-lint-cross.js +49 -92
- package/dist/src/core/patch-lint-css.js +10 -6
- package/dist/src/core/patch-lint-file-size.d.ts +52 -0
- package/dist/src/core/patch-lint-file-size.js +53 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
- package/dist/src/core/patch-lint-forward-registration.js +210 -0
- package/dist/src/core/patch-lint-jsdoc.js +36 -36
- package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
- package/dist/src/core/patch-lint-prettier.d.ts +36 -0
- package/dist/src/core/patch-lint-prettier.js +126 -0
- package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
- package/dist/src/core/patch-lint-queue-types.js +12 -0
- package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
- package/dist/src/core/patch-lint-staged-registration.js +90 -0
- package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
- package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
- package/dist/src/core/patch-lint.d.ts +14 -9
- package/dist/src/core/patch-lint.js +27 -27
- package/dist/src/core/patch-lock.js +3 -3
- package/dist/src/core/patch-manifest-io.js +8 -7
- package/dist/src/core/patch-parse.d.ts +19 -8
- package/dist/src/core/patch-parse.js +25 -0
- package/dist/src/core/patch-policy.js +15 -27
- package/dist/src/core/patch-source-metadata.d.ts +1 -3
- package/dist/src/core/patch-source-metadata.js +0 -4
- package/dist/src/core/patch-transform.d.ts +21 -8
- package/dist/src/core/patch-transform.js +38 -10
- package/dist/src/core/process-cpu-time.js +1 -1
- package/dist/src/core/rebase-session.d.ts +2 -8
- package/dist/src/core/rebase-session.js +5 -17
- package/dist/src/core/register-browser-content.js +3 -2
- package/dist/src/core/register-browser-test.js +2 -1
- package/dist/src/core/register-module.js +2 -1
- package/dist/src/core/register-scaffold.js +5 -4
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/run-log-redact.d.ts +38 -0
- package/dist/src/core/run-log-redact.js +69 -0
- package/dist/src/core/run-log.d.ts +13 -2
- package/dist/src/core/run-log.js +67 -18
- package/dist/src/core/signal-critical.js +2 -4
- package/dist/src/core/smoke-patterns.d.ts +0 -1
- package/dist/src/core/smoke-patterns.js +18 -11
- package/dist/src/core/state-file.d.ts +12 -1
- package/dist/src/core/state-file.js +14 -1
- package/dist/src/core/status-classify.js +2 -1
- package/dist/src/core/test-changed-prefs.js +26 -11
- package/dist/src/core/test-harness-crash.js +61 -48
- package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
- package/dist/src/core/test-harness-verdict-notes.js +48 -0
- package/dist/src/core/test-stale-check.d.ts +23 -8
- package/dist/src/core/test-stale-check.js +77 -4
- package/dist/src/core/test-stall-triage.js +37 -19
- package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
- package/dist/src/core/test-xpcshell-retry.js +14 -11
- package/dist/src/core/token-category.d.ts +33 -0
- package/dist/src/core/token-category.js +56 -48
- package/dist/src/core/token-dark-mode.js +2 -14
- package/dist/src/core/token-inventory.d.ts +56 -0
- package/dist/src/core/token-inventory.js +109 -0
- package/dist/src/core/token-manager.d.ts +1 -1
- package/dist/src/core/token-manager.js +1 -1
- package/dist/src/core/token-variant.js +21 -22
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/tree-cow.d.ts +0 -2
- package/dist/src/core/tree-cow.js +2 -2
- package/dist/src/core/tree-store.js +4 -5
- package/dist/src/core/typecheck.js +2 -2
- package/dist/src/core/wire-dom-fragment.js +6 -17
- package/dist/src/core/wire-utils.js +5 -17
- package/dist/src/core/xpcshell-appdir.js +2 -4
- package/dist/src/errors/base.d.ts +53 -1
- package/dist/src/errors/base.js +79 -11
- package/dist/src/errors/build.d.ts +28 -0
- package/dist/src/errors/build.js +51 -10
- package/dist/src/errors/config.d.ts +0 -14
- package/dist/src/errors/config.js +13 -34
- package/dist/src/errors/download.d.ts +13 -0
- package/dist/src/errors/download.js +53 -24
- package/dist/src/errors/furnace.js +8 -9
- package/dist/src/errors/git.d.ts +8 -8
- package/dist/src/errors/git.js +20 -30
- package/dist/src/errors/patch.js +8 -10
- package/dist/src/errors/rebase.js +7 -7
- package/dist/src/types/commands/options.d.ts +96 -123
- package/dist/src/types/commands/patches.d.ts +17 -6
- package/dist/src/types/config.d.ts +75 -2
- package/dist/src/types/index.d.ts +0 -1
- package/dist/src/utils/assert.d.ts +3 -15
- package/dist/src/utils/assert.js +1 -18
- package/dist/src/utils/build-info.d.ts +1 -1
- package/dist/src/utils/child-env.d.ts +2 -1
- package/dist/src/utils/elapsed.js +10 -1
- package/dist/src/utils/fs.d.ts +4 -2
- package/dist/src/utils/fs.js +11 -14
- package/dist/src/utils/hash.d.ts +12 -0
- package/dist/src/utils/hash.js +16 -0
- package/dist/src/utils/line-dispatch.d.ts +15 -0
- package/dist/src/utils/line-dispatch.js +48 -0
- package/dist/src/utils/options.d.ts +1 -7
- package/dist/src/utils/options.js +6 -7
- package/dist/src/utils/package-root.js +3 -12
- package/dist/src/utils/platform.d.ts +0 -4
- package/dist/src/utils/process-group.d.ts +2 -1
- package/dist/src/utils/process-group.js +20 -19
- package/dist/src/utils/process.d.ts +40 -26
- package/dist/src/utils/process.js +115 -166
- package/dist/src/utils/ps-duration.js +3 -3
- package/dist/src/utils/sleep.d.ts +26 -0
- package/dist/src/utils/sleep.js +31 -0
- package/dist/src/utils/validation.d.ts +2 -38
- package/dist/src/utils/validation.js +7 -16
- package/docs/configuration.md +136 -1
- package/docs/exit-codes.md +34 -20
- package/docs/lifecycle-invariants.md +18 -12
- package/docs/machine-output.md +31 -8
- package/docs/patch-workflow.md +2 -1
- package/docs/run-logs.md +44 -4
- package/docs/testing.md +70 -11
- package/package.json +8 -7
- package/dist/src/commands/rebase.d.ts +0 -4
- package/dist/src/commands/rebase.js +0 -5
- package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
- package/dist/src/core/patch-artifact-normalize.js +0 -24
- package/dist/src/core/patch-lint-reexports.d.ts +0 -7
- package/dist/src/core/patch-lint-reexports.js +0 -8
- package/dist/src/core/wire-targets.d.ts +0 -7
- package/dist/src/core/wire-targets.js +0 -8
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* installed the probe returns `{ inUse: false }` rather than failing the
|
|
25
25
|
* test run — it is a best-effort friendliness check, not a prerequisite.
|
|
26
26
|
*/
|
|
27
|
-
import {
|
|
27
|
+
import { PreflightRefusalError } from '../errors/base.js';
|
|
28
28
|
import { toError } from '../utils/errors.js';
|
|
29
29
|
import { getPlatform } from '../utils/platform.js';
|
|
30
30
|
import { exec } from '../utils/process.js';
|
|
@@ -133,10 +133,10 @@ export async function ensureLaunchableBrowserNotRunning(launchableBinary, option
|
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
catch (error) {
|
|
136
|
-
throw new
|
|
136
|
+
throw new PreflightRefusalError(`A browser from this objdir is still running (PID ${holder.pid}), but FireForge could not terminate it: ${toError(error).message}`, 'stale-browser-kill-failed');
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
throw new
|
|
139
|
+
throw new PreflightRefusalError(describeRunningBundleRefusal(holder), 'stale-browser');
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
142
142
|
* True when the process was launched BY a test harness — its command line
|
|
@@ -349,18 +349,18 @@ export async function assertMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
|
|
|
349
349
|
const killHint = process.platform === 'win32'
|
|
350
350
|
? `Stop-Process -Id ${holder.pid} -Force`
|
|
351
351
|
: `kill ${holder.pid} # or "kill -9 ${holder.pid}" if it doesn't exit`;
|
|
352
|
-
throw new
|
|
352
|
+
throw new PreflightRefusalError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
|
|
353
353
|
`This is usually a browser left running by a previously interrupted "fireforge test" run. ` +
|
|
354
354
|
`Kill it with "${killHint}", then retry. ` +
|
|
355
355
|
`(If you expected ${holder.command} to be running on ${port}, stop it manually or pass ` +
|
|
356
|
-
`"--marionette-port <port>" to launch mach test on a different port.)
|
|
356
|
+
`"--marionette-port <port>" to launch mach test on a different port.)`, 'marionette-port-busy');
|
|
357
357
|
}
|
|
358
358
|
// Non-browser holder: mach test will still fail to bind, but the
|
|
359
359
|
// cause is not a stale FireForge-launched browser. Flag it
|
|
360
360
|
// explicitly so the operator can decide what to do instead of
|
|
361
361
|
// getting mach's bind error with no FireForge context.
|
|
362
|
-
throw new
|
|
363
|
-
`This is not a FireForge-launched browser; stop the holder process or free the port before rerunning
|
|
362
|
+
throw new PreflightRefusalError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
|
|
363
|
+
`This is not a FireForge-launched browser; stop the holder process or free the port before rerunning.`, 'marionette-port-busy');
|
|
364
364
|
}
|
|
365
365
|
/**
|
|
366
366
|
* Ensures the Marionette port is usable, optionally terminating a stale
|
|
@@ -390,8 +390,8 @@ export async function ensureMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
catch (error) {
|
|
393
|
-
throw new
|
|
394
|
-
`but FireForge could not terminate it: ${toError(error).message}
|
|
393
|
+
throw new PreflightRefusalError(`Marionette port ${port} is held by stale browser ${holder.command} (PID ${holder.pid}), ` +
|
|
394
|
+
`but FireForge could not terminate it: ${toError(error).message}`, 'stale-browser-kill-failed');
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
397
|
/**
|
|
@@ -410,10 +410,7 @@ export async function ensureMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
|
|
|
410
410
|
* `undefined`.
|
|
411
411
|
*/
|
|
412
412
|
export function extractForwardedMarionettePort(machArgs) {
|
|
413
|
-
for (
|
|
414
|
-
const arg = machArgs[i];
|
|
415
|
-
if (arg === undefined)
|
|
416
|
-
continue;
|
|
413
|
+
for (const [i, arg] of machArgs.entries()) {
|
|
417
414
|
// `--marionette-port=NNNN`
|
|
418
415
|
let match = /^--marionette-port=(\d+)$/.exec(arg);
|
|
419
416
|
if (match?.[1]) {
|
|
@@ -452,10 +449,7 @@ export function extractForwardedMarionettePort(machArgs) {
|
|
|
452
449
|
*/
|
|
453
450
|
export function forwardedMachArgsIncludeMarionetteClient(machArgs) {
|
|
454
451
|
const valueLooksLikeHostPort = (token) => /:[0-9]+$/.test(token) || /^\[[^]]+\]:[0-9]+$/.test(token);
|
|
455
|
-
for (
|
|
456
|
-
const arg = machArgs[i];
|
|
457
|
-
if (arg === undefined)
|
|
458
|
-
continue;
|
|
452
|
+
for (const [i, arg] of machArgs.entries()) {
|
|
459
453
|
if (arg.startsWith('--marionette=') && !arg.startsWith('--marionette-port=')) {
|
|
460
454
|
return true;
|
|
461
455
|
}
|
|
@@ -473,8 +467,7 @@ export function forwardedMachArgsIncludeMarionetteClient(machArgs) {
|
|
|
473
467
|
* for runs where the pref is ignored anyway.
|
|
474
468
|
*/
|
|
475
469
|
export function hasExplicitXpcshellFlavor(machArgs) {
|
|
476
|
-
for (
|
|
477
|
-
const arg = machArgs[i] ?? '';
|
|
470
|
+
for (const [i, arg] of machArgs.entries()) {
|
|
478
471
|
if (/^--flavor=xpcshell\b/.test(arg) || arg === '--flavor=xpcshell-tests')
|
|
479
472
|
return true;
|
|
480
473
|
if (arg === '--flavor' && /^xpcshell(?:-tests)?$/.test(machArgs[i + 1] ?? ''))
|
|
@@ -493,42 +486,3 @@ export function hasExplicitXpcshellFlavor(machArgs) {
|
|
|
493
486
|
export function shouldAutoForwardMarionettePortToMach(machArgs) {
|
|
494
487
|
return !hasExplicitXpcshellFlavor(machArgs);
|
|
495
488
|
}
|
|
496
|
-
/**
|
|
497
|
-
* Heuristic: do the test paths or forwarded mach args indicate a flavour
|
|
498
|
-
* that actually launches a Marionette-driven browser? Browser-chrome and
|
|
499
|
-
* mochitest do; xpcshell does not. A no-paths invocation (the default "run
|
|
500
|
-
* all tests" shape) is treated as marionette-relevant since it includes
|
|
501
|
-
* browser-chrome.
|
|
502
|
-
*
|
|
503
|
-
* Note: `fireforge test` auto-forward of `--marionette-port` to mach uses
|
|
504
|
-
* {@link shouldAutoForwardMarionettePortToMach} (mach-arg flavor gate) rather
|
|
505
|
-
* than this function alone, so toolkit paths without `/mochitest/` still get
|
|
506
|
-
* the listener pref and harness `--marionette=127.0.0.1:<n>` when appropriate.
|
|
507
|
-
*
|
|
508
|
-
* @param testPaths - Engine-relative paths after `stripEnginePrefix`.
|
|
509
|
-
* @param machArgs - Forwarded mach args (post-`--mach-arg`).
|
|
510
|
-
* @returns `true` when the run is likely to bind a Marionette listener.
|
|
511
|
-
*/
|
|
512
|
-
export function isMarionetteFlavor(testPaths, machArgs) {
|
|
513
|
-
if (hasExplicitXpcshellFlavor(machArgs))
|
|
514
|
-
return false;
|
|
515
|
-
for (const arg of machArgs) {
|
|
516
|
-
if (/^--flavor=(browser-chrome|mochitest|chrome|a11y)\b/.test(arg))
|
|
517
|
-
return true;
|
|
518
|
-
}
|
|
519
|
-
if (testPaths.length === 0)
|
|
520
|
-
return true;
|
|
521
|
-
for (const path of testPaths) {
|
|
522
|
-
const base = path.split('/').pop() ?? path;
|
|
523
|
-
if (/^browser_.+\.(js|ini|toml)$/.test(base))
|
|
524
|
-
return true;
|
|
525
|
-
if (path.includes('/mochitest/') || path.startsWith('mochitest/'))
|
|
526
|
-
return true;
|
|
527
|
-
if (path.includes('/browser-chrome/') || path.startsWith('browser-chrome/'))
|
|
528
|
-
return true;
|
|
529
|
-
if (path.includes('toolkit/content/tests/') && !path.includes('/tests/xpcshell/')) {
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return false;
|
|
534
|
-
}
|
|
@@ -22,6 +22,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
22
22
|
import { info, verbose, warn } from '../utils/logger.js';
|
|
23
23
|
import { installGracefulShutdownForwarder, trackChildClosure } from '../utils/process.js';
|
|
24
24
|
import { killProcessTree, sweepProcessGroup } from '../utils/process-group.js';
|
|
25
|
+
import { sleep } from '../utils/sleep.js';
|
|
25
26
|
import { ensureMach } from './mach.js';
|
|
26
27
|
import { getPython } from './mach-python.js';
|
|
27
28
|
/** Marionette's default TCP port when the browser is launched with `--marionette`. */
|
|
@@ -143,7 +144,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
|
|
|
143
144
|
const spawnedChild = child;
|
|
144
145
|
closure = trackChildClosure();
|
|
145
146
|
forwarder = installGracefulShutdownForwarder(spawnedChild, 500, (signal) => {
|
|
146
|
-
|
|
147
|
+
killProcessTree(spawnedChild, signal, process.platform !== 'win32');
|
|
147
148
|
});
|
|
148
149
|
child.stderr?.on('data', (data) => {
|
|
149
150
|
const chunk = data.toString();
|
|
@@ -155,10 +156,10 @@ export async function runMarionettePreflight(engineDir, options = {}) {
|
|
|
155
156
|
// never come.
|
|
156
157
|
const settleDeadline = Math.min(spawnSettleMs, Math.max(0, timeoutMs - elapsed()));
|
|
157
158
|
if (settleDeadline > 0) {
|
|
158
|
-
await
|
|
159
|
+
await sleep(settleDeadline, { unref: true });
|
|
159
160
|
}
|
|
160
161
|
if (hasChildExited(spawnedChild)) {
|
|
161
|
-
return fail('browser-spawns', `Browser process exited during spawn (exit code ${
|
|
162
|
+
return fail('browser-spawns', `Browser process exited during spawn (exit code ${spawnedChild.exitCode}, signal ${spawnedChild.signalCode ?? 'none'}). ` +
|
|
162
163
|
`stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`, elapsed());
|
|
163
164
|
}
|
|
164
165
|
// Layer 6: marionette handshake within the remaining budget.
|
|
@@ -175,7 +176,7 @@ export async function runMarionettePreflight(engineDir, options = {}) {
|
|
|
175
176
|
// Child may have exited before the socket was ever ready — surface that
|
|
176
177
|
// distinctly from "socket never answered" so the operator has a lead.
|
|
177
178
|
if (hasChildExited(spawnedChild)) {
|
|
178
|
-
return fail('marionette-handshake', `Browser process exited before marionette handshake (exit code ${
|
|
179
|
+
return fail('marionette-handshake', `Browser process exited before marionette handshake (exit code ${spawnedChild.exitCode}, signal ${spawnedChild.signalCode ?? 'none'}). ` +
|
|
179
180
|
`stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`, elapsed());
|
|
180
181
|
}
|
|
181
182
|
return fail('marionette-handshake', `Marionette socket on 127.0.0.1:${port} did not respond within ${timeoutMs}ms. ` +
|
|
@@ -203,12 +204,12 @@ async function teardownPreflightChild(child, forwarder, closure, profileDir) {
|
|
|
203
204
|
forwarder?.dispose();
|
|
204
205
|
const groupPid = child?.pid;
|
|
205
206
|
if (child && !hasChildExited(child)) {
|
|
206
|
-
|
|
207
|
+
killProcessTree(child, 'SIGTERM', process.platform !== 'win32');
|
|
207
208
|
// Small escalation: if the child doesn't honour SIGTERM quickly, SIGKILL
|
|
208
209
|
// so we don't leave a ghost mach process around after a failed probe.
|
|
209
|
-
await
|
|
210
|
+
await sleep(500, { unref: true });
|
|
210
211
|
if (!hasChildExited(child)) {
|
|
211
|
-
|
|
212
|
+
killProcessTree(child, 'SIGKILL', process.platform !== 'win32');
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
// Reap anything that outlived the group signal. The file's own comments
|
|
@@ -237,19 +238,6 @@ async function teardownPreflightChild(child, forwarder, closure, profileDir) {
|
|
|
237
238
|
warn(`Could not clean up marionette preflight profile: ${toError(error).message}`);
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
|
-
/**
|
|
241
|
-
* Sends `signal` to the child's whole process group when possible, falling
|
|
242
|
-
* back to a direct `child.kill` for environments that don't support
|
|
243
|
-
* `detached: true` (Windows in particular: Node still returns a pid, but
|
|
244
|
-
* `kill(-pid, …)` is not a supported kernel primitive).
|
|
245
|
-
*/
|
|
246
|
-
function killProcessGroup(child, signal) {
|
|
247
|
-
// Delegates to the shared implementation, which is a strict superset of
|
|
248
|
-
// the hand-rolled one this replaced: same POSIX `kill(-pid)` with the same
|
|
249
|
-
// fall-through to a direct `child.kill`, plus a Windows `taskkill /T /F`
|
|
250
|
-
// branch and an already-exited early return.
|
|
251
|
-
killProcessTree(child, signal, process.platform !== 'win32');
|
|
252
|
-
}
|
|
253
241
|
function fail(layer, message, durationMs) {
|
|
254
242
|
return {
|
|
255
243
|
ok: false,
|
|
@@ -267,7 +255,7 @@ async function waitForMarionetteSocket(port, connectFn, keepTrying) {
|
|
|
267
255
|
if (result.ok) {
|
|
268
256
|
return { ok: true };
|
|
269
257
|
}
|
|
270
|
-
await
|
|
258
|
+
await sleep(400, { unref: true });
|
|
271
259
|
}
|
|
272
260
|
return { ok: false };
|
|
273
261
|
}
|
|
@@ -312,12 +300,6 @@ function attemptMarionetteConnect(port, connectFn) {
|
|
|
312
300
|
});
|
|
313
301
|
});
|
|
314
302
|
}
|
|
315
|
-
function delay(ms) {
|
|
316
|
-
return new Promise((resolve) => {
|
|
317
|
-
const timer = setTimeout(resolve, ms);
|
|
318
|
-
timer.unref();
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
303
|
/** Renders a PASS/FAIL banner to the CLI using the shared logger helpers. */
|
|
322
304
|
export function reportMarionettePreflight(result) {
|
|
323
305
|
if (result.ok) {
|
|
@@ -100,8 +100,8 @@ export function describeMochitestServerRefusal(port, holder, recognized) {
|
|
|
100
100
|
const killHint = process.platform === 'win32'
|
|
101
101
|
? `Stop-Process -Id ${holder.pid} -Force`
|
|
102
102
|
: `kill -9 ${holder.pid}`;
|
|
103
|
-
const evidence = `The mochitest server port ${
|
|
104
|
-
`(PID ${
|
|
103
|
+
const evidence = `The mochitest server port ${port} is already held by ${holder.command} ` +
|
|
104
|
+
`(PID ${holder.pid}).\n` +
|
|
105
105
|
` command: ${holder.commandLine}\n` +
|
|
106
106
|
" A browser launched now would connect to THAT server, which cannot serve this run's " +
|
|
107
107
|
'manifest — the run would stall before TEST_START and die on the no-output timeout with ' +
|
|
@@ -115,5 +115,5 @@ export function describeMochitestServerRefusal(port, holder, recognized) {
|
|
|
115
115
|
return (evidence +
|
|
116
116
|
" This is NOT the mochitest harness's server.js, so FireForge will not offer to kill it. " +
|
|
117
117
|
`Stop the holder yourself with "${killHint}" if it is yours, or free the port, then retry. ` +
|
|
118
|
-
`Diagnose with "lsof -nP -iTCP:${
|
|
118
|
+
`Diagnose with "lsof -nP -iTCP:${port}".`);
|
|
119
119
|
}
|
|
@@ -3,6 +3,7 @@ import { basename, join } from 'node:path';
|
|
|
3
3
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
4
4
|
import { pathExists, readText } from '../utils/fs.js';
|
|
5
5
|
import { warn } from '../utils/logger.js';
|
|
6
|
+
import { normalizePathSlashes } from '../utils/paths.js';
|
|
6
7
|
import { escapeRegex } from '../utils/regex.js';
|
|
7
8
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
8
9
|
import { registerBrowserContent, registerFireForgeModule, registerSharedCSS, registerTestManifest, registerToolkitWidget, } from './moz-manifest-register.js';
|
|
@@ -84,13 +85,30 @@ export function getRules(binaryName, createManifest = false) {
|
|
|
84
85
|
},
|
|
85
86
|
];
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Reads an engine manifest and reports whether it contains any of `needles`.
|
|
90
|
+
*
|
|
91
|
+
* The five registration probes below share this shape: resolve the manifest
|
|
92
|
+
* relative to the engine, refuse with the manifest's own path when it is
|
|
93
|
+
* missing, then substring-match the entry each registrar writes.
|
|
94
|
+
*
|
|
95
|
+
* @param engineDir - Path to the engine directory
|
|
96
|
+
* @param manifestRel - Engine-relative manifest path (forward slashes; also
|
|
97
|
+
* the path named in the refusal message)
|
|
98
|
+
* @param needles - Entry forms that count as registered
|
|
99
|
+
* @returns True when the manifest contains at least one needle
|
|
100
|
+
* @throws GeneralError when the manifest does not exist
|
|
101
|
+
*/
|
|
102
|
+
async function manifestIncludes(engineDir, manifestRel, needles) {
|
|
103
|
+
const manifestPath = join(engineDir, manifestRel);
|
|
89
104
|
if (!(await pathExists(manifestPath))) {
|
|
90
|
-
throw new GeneralError(
|
|
105
|
+
throw new GeneralError(`Manifest not found: ${manifestRel}`);
|
|
91
106
|
}
|
|
92
|
-
const name = basename(fileName, '.css');
|
|
93
107
|
const content = await readText(manifestPath);
|
|
108
|
+
return needles.some((needle) => content.includes(needle));
|
|
109
|
+
}
|
|
110
|
+
async function isSharedCSSRegistered(engineDir, fileName) {
|
|
111
|
+
const name = basename(fileName, '.css');
|
|
94
112
|
// `register` writes the canonical `skin/classic/browser/<name>.css` form;
|
|
95
113
|
// `furnace chrome-doc create` writes a `content/browser/<name>.css` entry
|
|
96
114
|
// because the CSS is loaded by a chrome document via a `chrome://browser/
|
|
@@ -98,40 +116,26 @@ async function isSharedCSSRegistered(engineDir, fileName) {
|
|
|
98
116
|
// chrome-doc scaffolder are not flagged as "potentially unregistered" by
|
|
99
117
|
// `status` and so a re-run of `register` against the same file recognises
|
|
100
118
|
// the existing entry instead of proposing a duplicate.
|
|
101
|
-
return (
|
|
102
|
-
|
|
119
|
+
return manifestIncludes(engineDir, 'browser/themes/shared/jar.inc.mn', [
|
|
120
|
+
`skin/classic/browser/${name}.css`,
|
|
121
|
+
`content/browser/${name}.css`,
|
|
122
|
+
]);
|
|
103
123
|
}
|
|
104
124
|
async function isBrowserContentRegistered(engineDir, fileName) {
|
|
105
|
-
|
|
106
|
-
if (!(await pathExists(manifestPath))) {
|
|
107
|
-
throw new GeneralError('Manifest not found: browser/base/jar.mn');
|
|
108
|
-
}
|
|
109
|
-
const content = await readText(manifestPath);
|
|
110
|
-
return content.includes(`content/browser/${fileName}`);
|
|
125
|
+
return manifestIncludes(engineDir, 'browser/base/jar.mn', [`content/browser/${fileName}`]);
|
|
111
126
|
}
|
|
112
127
|
async function isTestManifestRegistered(engineDir, testDir) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
const content = await readText(manifestPath);
|
|
118
|
-
return content.includes(`content/test/${testDir}/browser.toml`);
|
|
128
|
+
return manifestIncludes(engineDir, 'browser/base/moz.build', [
|
|
129
|
+
`content/test/${testDir}/browser.toml`,
|
|
130
|
+
]);
|
|
119
131
|
}
|
|
120
132
|
async function isFireForgeModuleRegistered(engineDir, fileName, moduleDir) {
|
|
121
|
-
|
|
122
|
-
if (!(await pathExists(manifestPath))) {
|
|
123
|
-
throw new GeneralError(`Manifest not found: ${moduleDir}/moz.build`);
|
|
124
|
-
}
|
|
125
|
-
const content = await readText(manifestPath);
|
|
126
|
-
return content.includes(`"${fileName}"`);
|
|
133
|
+
return manifestIncludes(engineDir, `${moduleDir}/moz.build`, [`"${fileName}"`]);
|
|
127
134
|
}
|
|
128
135
|
async function isToolkitWidgetRegistered(engineDir, _tagName, fileName) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
const content = await readText(manifestPath);
|
|
134
|
-
return content.includes(`content/global/elements/${fileName}`);
|
|
136
|
+
return manifestIncludes(engineDir, 'toolkit/content/jar.mn', [
|
|
137
|
+
`content/global/elements/${fileName}`,
|
|
138
|
+
]);
|
|
135
139
|
}
|
|
136
140
|
/**
|
|
137
141
|
* Checks if a file path matches any known registrable pattern.
|
|
@@ -140,7 +144,7 @@ async function isToolkitWidgetRegistered(engineDir, _tagName, fileName) {
|
|
|
140
144
|
* @returns True if the file matches a known registration pattern
|
|
141
145
|
*/
|
|
142
146
|
export function matchesRegistrablePattern(filePath, binaryName) {
|
|
143
|
-
const normalized = filePath
|
|
147
|
+
const normalized = normalizePathSlashes(filePath);
|
|
144
148
|
const rules = getRules(binaryName);
|
|
145
149
|
return rules.some((rule) => rule.pattern.test(normalized));
|
|
146
150
|
}
|
|
@@ -197,7 +201,7 @@ export async function isFileRegistered(root, filePath) {
|
|
|
197
201
|
const { engine: engineDir } = getProjectPaths(root);
|
|
198
202
|
const config = await loadConfig(root);
|
|
199
203
|
const rules = getRules(config.binaryName);
|
|
200
|
-
const normalizedPath = filePath
|
|
204
|
+
const normalizedPath = normalizePathSlashes(filePath);
|
|
201
205
|
for (const rule of rules) {
|
|
202
206
|
const match = normalizedPath.match(rule.pattern);
|
|
203
207
|
if (match) {
|
|
@@ -237,7 +241,7 @@ export async function registerFile(root, filePath, dryRun = false, after, option
|
|
|
237
241
|
const config = await loadConfig(root);
|
|
238
242
|
const rules = getRules(config.binaryName, options.createManifest === true);
|
|
239
243
|
// Normalize path separators
|
|
240
|
-
const normalizedPath = filePath
|
|
244
|
+
const normalizedPath = normalizePathSlashes(filePath);
|
|
241
245
|
for (const rule of rules) {
|
|
242
246
|
const match = normalizedPath.match(rule.pattern);
|
|
243
247
|
if (match) {
|
|
@@ -42,13 +42,13 @@ const GIT_DEFAULT_CONTEXT = 3;
|
|
|
42
42
|
*/
|
|
43
43
|
export async function applyPatchWithFuzz(patchPath, engineDir, maxFuzz = 3) {
|
|
44
44
|
if (!Number.isInteger(maxFuzz) || maxFuzz < 0) {
|
|
45
|
-
throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${
|
|
45
|
+
throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${maxFuzz}.`, 'maxFuzz');
|
|
46
46
|
}
|
|
47
47
|
await ensureGit();
|
|
48
48
|
// Try exact match first, then escalate context reduction.
|
|
49
49
|
const maxSteps = Math.min(maxFuzz, GIT_DEFAULT_CONTEXT);
|
|
50
50
|
for (let step = 0; step <= maxSteps; step++) {
|
|
51
|
-
const contextArgs = step > 0 ? [`-C${
|
|
51
|
+
const contextArgs = step > 0 ? [`-C${GIT_DEFAULT_CONTEXT - step}`] : [];
|
|
52
52
|
const check = await exec('git', ['apply', '--check', ...contextArgs, '--', patchPath], {
|
|
53
53
|
cwd: engineDir,
|
|
54
54
|
});
|
|
@@ -59,12 +59,12 @@ export async function applyPatchWithFuzz(patchPath, engineDir, maxFuzz = 3) {
|
|
|
59
59
|
});
|
|
60
60
|
if (apply.exitCode === 0) {
|
|
61
61
|
if (step > 0) {
|
|
62
|
-
verbose(`Patch applied with reduced context (-C${
|
|
62
|
+
verbose(`Patch applied with reduced context (-C${GIT_DEFAULT_CONTEXT - step}): ${patchPath}`);
|
|
63
63
|
}
|
|
64
64
|
return { success: true, fuzzFactor: step };
|
|
65
65
|
}
|
|
66
66
|
// Unlikely: --check passed but apply failed; fall through to next step
|
|
67
|
-
verbose(`git apply -C${
|
|
67
|
+
verbose(`git apply -C${GIT_DEFAULT_CONTEXT - step} --check passed but apply failed: ${apply.stderr.trim()}`);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
// All context-reduction levels failed → generate .rej files for manual resolution
|
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ImportSummary, PatchInfo, PatchMetadata } from '../types/commands/index.js';
|
|
6
6
|
export { PatchError } from '../errors/patch.js';
|
|
7
|
-
export { countPatches, discoverPatches
|
|
7
|
+
export { countPatches, discoverPatches } from './patch-files.js';
|
|
8
8
|
export { withPatchDirectoryLock } from './patch-lock.js';
|
|
9
|
-
export { extractAffectedFiles
|
|
10
|
-
export { applyPatchToContent, extractNewFileContent } from './patch-transform.js';
|
|
9
|
+
export { extractAffectedFiles } from './patch-parse.js';
|
|
11
10
|
/**
|
|
12
11
|
* Options for {@link applyPatchesWithContinue}.
|
|
13
12
|
*/
|
|
@@ -47,10 +46,10 @@ export declare function matchesUntilFilename(patchFilename: string, needle: stri
|
|
|
47
46
|
* on the first failure to keep the engine directory in a clean state.
|
|
48
47
|
* @param patchesDir - Path to the patches directory
|
|
49
48
|
* @param engineDir - Path to the engine directory
|
|
50
|
-
* @param
|
|
49
|
+
* @param options - Application options
|
|
51
50
|
* @returns Import summary with all results
|
|
52
51
|
*/
|
|
53
|
-
export declare function applyPatchesWithContinue(patchesDir: string, engineDir: string,
|
|
52
|
+
export declare function applyPatchesWithContinue(patchesDir: string, engineDir: string, options?: ApplyPatchesOptions): Promise<ImportSummary>;
|
|
54
53
|
/**
|
|
55
54
|
* Batched patched-content computation shared by status classification,
|
|
56
55
|
* import's unmanaged-dirty guard, and discard baseline planning.
|
|
@@ -3,25 +3,24 @@
|
|
|
3
3
|
* Patch orchestration — coordinates patch discovery, application, and validation.
|
|
4
4
|
* Pure parsing, content transformation, and lock management are in separate modules.
|
|
5
5
|
*/
|
|
6
|
-
import { lstat, readlink, realpath } from 'node:fs/promises';
|
|
6
|
+
import { lstat, readFile, readlink, realpath, rm } from 'node:fs/promises';
|
|
7
7
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
8
8
|
import { PatchError } from '../errors/patch.js';
|
|
9
9
|
import { toError } from '../utils/errors.js';
|
|
10
|
-
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
10
|
+
import { pathExists, readText, writeFileAtomic, writeText } from '../utils/fs.js';
|
|
11
11
|
import { verbose } from '../utils/logger.js';
|
|
12
12
|
import { isContainedRelativePath } from '../utils/paths.js';
|
|
13
13
|
import { applyPatchIdempotent, reversePatch } from './git.js';
|
|
14
|
-
import {
|
|
14
|
+
import { getFileContentAtRef } from './git-file-ops.js';
|
|
15
15
|
import { discoverPatches } from './patch-files.js';
|
|
16
16
|
import { loadPatchesManifest } from './patch-manifest.js';
|
|
17
|
-
import { extractAffectedFiles, extractConflictingFiles, isNewFileInPatch } from './patch-parse.js';
|
|
18
|
-
import { applyPatchTextToContent,
|
|
17
|
+
import { extractAffectedFiles, extractConflictingFiles, isNewFileInPatch, parseDiffSections, } from './patch-parse.js';
|
|
18
|
+
import { applyPatchTextToContent, extractNewFileContentFromDiff } from './patch-transform.js';
|
|
19
19
|
// Re-export from split modules so existing import sites continue working
|
|
20
20
|
export { PatchError } from '../errors/patch.js';
|
|
21
|
-
export { countPatches, discoverPatches
|
|
21
|
+
export { countPatches, discoverPatches } from './patch-files.js';
|
|
22
22
|
export { withPatchDirectoryLock } from './patch-lock.js';
|
|
23
|
-
export { extractAffectedFiles
|
|
24
|
-
export { applyPatchToContent, extractNewFileContent } from './patch-transform.js';
|
|
23
|
+
export { extractAffectedFiles } from './patch-parse.js';
|
|
25
24
|
/**
|
|
26
25
|
* Applies a single patch.
|
|
27
26
|
*
|
|
@@ -53,28 +52,57 @@ async function applySinglePatch(patch, engineDir, protectedFiles) {
|
|
|
53
52
|
const applyError = toError(error);
|
|
54
53
|
// Check if this is a resolvable "new file" conflict
|
|
55
54
|
let resolvedNewFiles = false;
|
|
56
|
-
// Save original
|
|
55
|
+
// Save original BYTES of files we might overwrite, so we can restore on
|
|
56
|
+
// failure. Raw buffers rather than decoded text: the same snapshot has to
|
|
57
|
+
// restore a binary target byte-for-byte, and a Buffer round-trips text
|
|
58
|
+
// losslessly where a utf-8 decode of a binary file does not.
|
|
57
59
|
const savedContents = new Map();
|
|
60
|
+
// New-file sections keyed by target, so the binary discrimination below
|
|
61
|
+
// reads the ALREADY-parsed section instead of re-scanning the body per
|
|
62
|
+
// file.
|
|
63
|
+
const newFileSections = new Map(parseDiffSections(patchContent)
|
|
64
|
+
.filter((section) => section.isNewFile)
|
|
65
|
+
.map((section) => [section.targetPath, section]));
|
|
58
66
|
try {
|
|
59
67
|
for (const file of affectedFiles) {
|
|
60
|
-
if (isNewFileInPatch(patchContent, file))
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
if (!isNewFileInPatch(patchContent, file))
|
|
69
|
+
continue;
|
|
70
|
+
const targetPath = join(engineDir, file);
|
|
71
|
+
if (!(await pathExists(targetPath)))
|
|
72
|
+
continue;
|
|
73
|
+
savedContents.set(file, await readFile(targetPath));
|
|
74
|
+
const section = newFileSections.get(file);
|
|
75
|
+
if (section?.isBinary === true) {
|
|
76
|
+
// A binary new file cannot be resolved by writing extracted text —
|
|
77
|
+
// the payload is base85, whose alphabet includes '+'. It does not
|
|
78
|
+
// need to be: `git apply` decodes a `GIT binary patch` itself. So
|
|
79
|
+
// the resolution is to REMOVE the blocking file and let the retry
|
|
80
|
+
// below create it from the payload, which is exactly what the
|
|
81
|
+
// text branch achieves by overwriting.
|
|
82
|
+
if (!section.hasBinaryDelta) {
|
|
83
|
+
// `Binary files … differ` records THAT the bytes changed and
|
|
84
|
+
// carries none of them, so nothing can recreate the file. Fail
|
|
85
|
+
// honestly rather than deleting a file we cannot put back.
|
|
86
|
+
throw new PatchError(`Cannot resolve the new-file conflict for ${file}: the patch's binary section ` +
|
|
87
|
+
'carries no replayable payload (`Binary files … differ` rather than `GIT binary ' +
|
|
88
|
+
'patch`), so the file cannot be recreated. Re-export the patch with a binary-aware ' +
|
|
89
|
+
'diff.', file);
|
|
67
90
|
}
|
|
91
|
+
await rm(targetPath, { force: true });
|
|
68
92
|
}
|
|
93
|
+
else {
|
|
94
|
+
await writeText(targetPath, extractNewFileContentFromDiff(patchContent, file));
|
|
95
|
+
}
|
|
96
|
+
resolvedNewFiles = true;
|
|
69
97
|
}
|
|
70
98
|
}
|
|
71
99
|
catch (extractError) {
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
100
|
+
// Resolution threw for a LATER target after earlier targets were
|
|
101
|
+
// already overwritten or removed — restore them before reporting
|
|
102
|
+
// failure, instead of letting the exception skip the restore loop
|
|
103
|
+
// entirely and crash the whole import.
|
|
76
104
|
for (const [file, originalContent] of savedContents) {
|
|
77
|
-
await
|
|
105
|
+
await writeFileAtomic(join(engineDir, file), originalContent);
|
|
78
106
|
}
|
|
79
107
|
return { patch, success: false, error: toError(extractError).message };
|
|
80
108
|
}
|
|
@@ -91,9 +119,9 @@ async function applySinglePatch(patch, engineDir, protectedFiles) {
|
|
|
91
119
|
}
|
|
92
120
|
catch (retryError) {
|
|
93
121
|
verbose(`Auto-resolved new-file retry failed for ${patch.filename}: ${toError(retryError).message}`);
|
|
94
|
-
// Restore original file
|
|
122
|
+
// Restore original file bytes before falling through to --reject
|
|
95
123
|
for (const [file, originalContent] of savedContents) {
|
|
96
|
-
await
|
|
124
|
+
await writeFileAtomic(join(engineDir, file), originalContent);
|
|
97
125
|
}
|
|
98
126
|
}
|
|
99
127
|
}
|
|
@@ -138,7 +166,7 @@ async function rollbackPatches(results, engineDir) {
|
|
|
138
166
|
if (result.autoResolvedOriginals) {
|
|
139
167
|
for (const [file, originalContent] of result.autoResolvedOriginals) {
|
|
140
168
|
try {
|
|
141
|
-
await
|
|
169
|
+
await writeFileAtomic(join(engineDir, file), originalContent);
|
|
142
170
|
verbose(`Restored pre-existing content of ${file} after rollback`);
|
|
143
171
|
}
|
|
144
172
|
catch (restoreError) {
|
|
@@ -243,15 +271,10 @@ export function matchesUntilFilename(patchFilename, needle) {
|
|
|
243
271
|
* on the first failure to keep the engine directory in a clean state.
|
|
244
272
|
* @param patchesDir - Path to the patches directory
|
|
245
273
|
* @param engineDir - Path to the engine directory
|
|
246
|
-
* @param
|
|
274
|
+
* @param options - Application options
|
|
247
275
|
* @returns Import summary with all results
|
|
248
276
|
*/
|
|
249
|
-
export async function applyPatchesWithContinue(patchesDir, engineDir,
|
|
250
|
-
// Accept both the legacy boolean positional and the new options object so
|
|
251
|
-
// existing call sites (tests and rebase) keep working without a rewrite.
|
|
252
|
-
const options = typeof optionsOrContinue === 'boolean'
|
|
253
|
-
? { continueOnFailure: optionsOrContinue }
|
|
254
|
-
: optionsOrContinue;
|
|
277
|
+
export async function applyPatchesWithContinue(patchesDir, engineDir, options = {}) {
|
|
255
278
|
const continueOnFailure = options.continueOnFailure ?? false;
|
|
256
279
|
const untilFilename = options.untilFilename;
|
|
257
280
|
const patches = await discoverPatches(patchesDir);
|
|
@@ -377,7 +400,7 @@ export async function createPatchedContentContext(patchesDir, engineDir) {
|
|
|
377
400
|
getAffectingPatches,
|
|
378
401
|
readPatchBody,
|
|
379
402
|
computePatched: async (filePath) => {
|
|
380
|
-
let content = await
|
|
403
|
+
let content = await getFileContentAtRef(engineDir, filePath);
|
|
381
404
|
for (const patch of getAffectingPatches(filePath)) {
|
|
382
405
|
content = applyPatchTextToContent(content, await readPatchBody(patch), filePath);
|
|
383
406
|
}
|
|
@@ -2,15 +2,6 @@
|
|
|
2
2
|
* Patch coverage and supersession helpers used by export planning.
|
|
3
3
|
*/
|
|
4
4
|
import type { PatchInfo, PatchMetadata } from '../types/commands/index.js';
|
|
5
|
-
/**
|
|
6
|
-
* Finds patches that are completely superseded by newer patches.
|
|
7
|
-
* A patch is superseded if all its affected files are covered by newer patches.
|
|
8
|
-
* @param patchesDir - Path to the patches directory
|
|
9
|
-
* @param newPatchFiles - Files affected by the new patch
|
|
10
|
-
* @param excludeFilename - Filename to exclude from results (the new patch itself)
|
|
11
|
-
* @returns Superseded patches
|
|
12
|
-
*/
|
|
13
|
-
export declare function findSupersededPatches(patchesDir: string, newPatchFiles: string[], excludeFilename?: string): Promise<PatchInfo[]>;
|
|
14
5
|
/**
|
|
15
6
|
* Report whether a patch is fully covered by a new export, and which of its
|
|
16
7
|
* files caused the coverage.
|
|
@@ -2,37 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Patch coverage and supersession helpers used by export planning.
|
|
4
4
|
*/
|
|
5
|
-
import { discoverPatches
|
|
5
|
+
import { discoverPatches } from './patch-apply.js';
|
|
6
6
|
import { loadPatchesManifest } from './patch-manifest.js';
|
|
7
|
-
/**
|
|
8
|
-
* Finds patches that are completely superseded by newer patches.
|
|
9
|
-
* A patch is superseded if all its affected files are covered by newer patches.
|
|
10
|
-
* @param patchesDir - Path to the patches directory
|
|
11
|
-
* @param newPatchFiles - Files affected by the new patch
|
|
12
|
-
* @param excludeFilename - Filename to exclude from results (the new patch itself)
|
|
13
|
-
* @returns Superseded patches
|
|
14
|
-
*/
|
|
15
|
-
export async function findSupersededPatches(patchesDir, newPatchFiles, excludeFilename) {
|
|
16
|
-
const manifest = await loadPatchesManifest(patchesDir);
|
|
17
|
-
if (!manifest)
|
|
18
|
-
return [];
|
|
19
|
-
const patches = await discoverPatches(patchesDir);
|
|
20
|
-
const superseded = [];
|
|
21
|
-
for (const metadata of manifest.patches) {
|
|
22
|
-
if (excludeFilename && metadata.filename === excludeFilename)
|
|
23
|
-
continue;
|
|
24
|
-
if (metadata.filesAffected.length === 1) {
|
|
25
|
-
const affectedFile = metadata.filesAffected[0];
|
|
26
|
-
if (affectedFile && newPatchFiles.includes(affectedFile)) {
|
|
27
|
-
const patch = patches.find((p) => p.filename === metadata.filename);
|
|
28
|
-
if (patch && (await isNewFilePatch(patch.path))) {
|
|
29
|
-
superseded.push(patch);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return superseded;
|
|
35
|
-
}
|
|
36
7
|
/**
|
|
37
8
|
* Checks whether a patch is fully covered by a new export.
|
|
38
9
|
* A patch is fully covered when every file it affects is present in the new export.
|