@hominis/fireforge 0.45.0 → 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 +80 -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.d.ts +2 -0
- package/dist/src/commands/doctor.js +7 -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 -45
- 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 -34
- 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 -8
- 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 +24 -2
- package/dist/src/commands/re-export-adjacent.js +32 -16
- package/dist/src/commands/re-export-bulk-scan.js +6 -10
- package/dist/src/commands/re-export-drift.js +4 -4
- package/dist/src/commands/re-export-files.d.ts +2 -3
- package/dist/src/commands/re-export-files.js +38 -29
- 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-scan.js +5 -2
- 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 +28 -11
- 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 +31 -7
- 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 +5 -3
- package/dist/src/commands/test-run.js +25 -18
- package/dist/src/commands/test-stale-gate.js +4 -4
- 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 +42 -5
- 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 +110 -10
- 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.js +3 -2
- package/dist/src/core/build-overwrite-guard.js +1 -1
- package/dist/src/core/build-prepare.d.ts +0 -8
- package/dist/src/core/build-prepare.js +7 -12
- 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.js +1 -1
- 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 +14 -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.js +8 -7
- 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 +4 -3
- package/dist/src/core/mach-known-noise-filter.js +7 -9
- 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 +39 -53
- package/dist/src/core/mach.js +13 -81
- 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 +6 -12
- 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 +37 -24
- 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.js +3 -3
- 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.js +1 -1
- 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.d.ts +14 -9
- package/dist/src/core/patch-lint.js +4 -3
- 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 +0 -8
- package/dist/src/core/patch-transform.js +0 -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 +6 -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 +2 -2
- package/dist/src/core/test-stale-check.d.ts +0 -2
- package/dist/src/core/test-stale-check.js +2 -2
- 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.js +20 -28
- 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 +77 -123
- package/dist/src/types/config.d.ts +34 -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.js +8 -13
- 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 +89 -1
- package/docs/exit-codes.md +31 -19
- 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 +8 -1
- 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
package/dist/src/core/git.js
CHANGED
|
@@ -225,15 +225,8 @@ export async function stageAllFiles(dir, options = {}) {
|
|
|
225
225
|
// monolithic budget forward.
|
|
226
226
|
phase = 'chunked';
|
|
227
227
|
phaseStartedAt = Date.now();
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
reportProgress?.('Git phase complete: chunked source indexing finished.');
|
|
231
|
-
}
|
|
232
|
-
catch (error) {
|
|
233
|
-
if (error instanceof GitIndexingTimeoutError)
|
|
234
|
-
throw error;
|
|
235
|
-
throw error;
|
|
236
|
-
}
|
|
228
|
+
await stageAllFilesChunked(dir, scan, options);
|
|
229
|
+
reportProgress?.('Git phase complete: chunked source indexing finished.');
|
|
237
230
|
}
|
|
238
231
|
finally {
|
|
239
232
|
if (heartbeatTimer)
|
|
@@ -285,10 +278,20 @@ export async function initRepository(dir, branchName = 'main', options = {}) {
|
|
|
285
278
|
reportProgress('Configuring origin remote for build compatibility...');
|
|
286
279
|
await git(['remote', 'add', 'origin', 'https://github.com/mozilla-firefox/firefox'], dir);
|
|
287
280
|
reportProgress('Git phase complete: source git repository metadata initialized.');
|
|
281
|
+
await stageAndCommitSource(dir, reportProgress, 'Indexing Firefox source with git add -A (this can take several minutes on large trees)...');
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The tail shared by {@link initRepository} and {@link resumeRepository}:
|
|
285
|
+
* normalizes the Firefox ignore files, stages the whole tree, and creates
|
|
286
|
+
* the initial source commit. Index-lock failures on either git step are
|
|
287
|
+
* wrapped into the typed lock error so both entry points report them the
|
|
288
|
+
* same way.
|
|
289
|
+
*/
|
|
290
|
+
async function stageAndCommitSource(dir, reportProgress, stagingMessage) {
|
|
288
291
|
reportProgress('Normalizing Firefox ignore files for Git-backed mach lint compatibility...');
|
|
289
292
|
await ensureFirefoxIgnorefileCompatibility(dir);
|
|
290
293
|
// Add all files
|
|
291
|
-
reportProgress(
|
|
294
|
+
reportProgress(stagingMessage);
|
|
292
295
|
await assertNoGitIndexLock(dir);
|
|
293
296
|
try {
|
|
294
297
|
await stageAllFiles(dir, { onProgress: reportProgress });
|
|
@@ -310,7 +313,6 @@ export async function initRepository(dir, branchName = 'main', options = {}) {
|
|
|
310
313
|
* cleans up stale locks, stages all files, and creates the initial commit.
|
|
311
314
|
*/
|
|
312
315
|
export async function resumeRepository(dir, options = {}) {
|
|
313
|
-
await ensureGit();
|
|
314
316
|
const reportProgress = options.onProgress ?? (() => { });
|
|
315
317
|
if (!(await isGitRepository(dir))) {
|
|
316
318
|
throw new GitError('Not a git repository', 'resume');
|
|
@@ -324,24 +326,7 @@ export async function resumeRepository(dir, options = {}) {
|
|
|
324
326
|
await cleanupIndexLock(dir);
|
|
325
327
|
// Ensure origin remote exists (may have been added before the interrupt)
|
|
326
328
|
await ensureOriginRemote(dir);
|
|
327
|
-
reportProgress
|
|
328
|
-
await ensureFirefoxIgnorefileCompatibility(dir);
|
|
329
|
-
// Stage all files
|
|
330
|
-
reportProgress('Indexing Firefox source (resuming)...');
|
|
331
|
-
await assertNoGitIndexLock(dir);
|
|
332
|
-
try {
|
|
333
|
-
await stageAllFiles(dir, { onProgress: reportProgress });
|
|
334
|
-
}
|
|
335
|
-
catch (error) {
|
|
336
|
-
throw await maybeWrapIndexLockError(dir, error);
|
|
337
|
-
}
|
|
338
|
-
// Create initial commit
|
|
339
|
-
try {
|
|
340
|
-
await createInitialSourceCommit(dir, reportProgress);
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
throw await maybeWrapIndexLockError(dir, error);
|
|
344
|
-
}
|
|
329
|
+
await stageAndCommitSource(dir, reportProgress, 'Indexing Firefox source (resuming)...');
|
|
345
330
|
}
|
|
346
331
|
async function assertNoGitIndexLock(dir) {
|
|
347
332
|
const lockPath = join(dir, '.git', 'index.lock');
|
|
@@ -499,7 +484,6 @@ export async function reversePatch(patchPath, repoDir) {
|
|
|
499
484
|
* @returns True if there are uncommitted changes
|
|
500
485
|
*/
|
|
501
486
|
export async function hasChanges(repoDir) {
|
|
502
|
-
await ensureGit();
|
|
503
487
|
const entries = await getWorkingTreeStatus(repoDir);
|
|
504
488
|
return entries.length > 0;
|
|
505
489
|
}
|
|
@@ -518,7 +502,6 @@ export function isMissingHeadError(error) {
|
|
|
518
502
|
* @returns Commit hash
|
|
519
503
|
*/
|
|
520
504
|
export async function getHead(repoDir) {
|
|
521
|
-
await ensureGit();
|
|
522
505
|
const output = await git(['rev-parse', 'HEAD'], repoDir);
|
|
523
506
|
return output.trim();
|
|
524
507
|
}
|
|
@@ -528,7 +511,6 @@ export async function getHead(repoDir) {
|
|
|
528
511
|
* @returns Branch name
|
|
529
512
|
*/
|
|
530
513
|
export async function getCurrentBranch(repoDir) {
|
|
531
|
-
await ensureGit();
|
|
532
514
|
const output = await git(['rev-parse', '--abbrev-ref', 'HEAD'], repoDir);
|
|
533
515
|
return output.trim();
|
|
534
516
|
}
|
|
@@ -537,7 +519,6 @@ export async function getCurrentBranch(repoDir) {
|
|
|
537
519
|
* @param repoDir - Repository directory
|
|
538
520
|
*/
|
|
539
521
|
export async function resetChanges(repoDir) {
|
|
540
|
-
await ensureGit();
|
|
541
522
|
try {
|
|
542
523
|
await git(['reset', '--hard', 'HEAD'], repoDir);
|
|
543
524
|
}
|
|
@@ -546,16 +527,6 @@ export async function resetChanges(repoDir) {
|
|
|
546
527
|
}
|
|
547
528
|
await git(['clean', '-fd'], repoDir);
|
|
548
529
|
}
|
|
549
|
-
/**
|
|
550
|
-
* Creates a commit with all current changes.
|
|
551
|
-
* @param repoDir - Repository directory
|
|
552
|
-
* @param message - Commit message
|
|
553
|
-
*/
|
|
554
|
-
export async function commit(repoDir, message) {
|
|
555
|
-
await ensureGit();
|
|
556
|
-
await stageAllFiles(repoDir);
|
|
557
|
-
await git(['commit', '-m', message], repoDir);
|
|
558
|
-
}
|
|
559
530
|
/**
|
|
560
531
|
* Gets the status of files with their status codes.
|
|
561
532
|
* @param repoDir - Repository directory
|
|
@@ -31,6 +31,7 @@ import { toError } from '../utils/errors.js';
|
|
|
31
31
|
import { info, verbose, warn } from '../utils/logger.js';
|
|
32
32
|
import { exec } from '../utils/process.js';
|
|
33
33
|
import { parsePsDuration } from '../utils/ps-duration.js';
|
|
34
|
+
import { sleep } from '../utils/sleep.js';
|
|
34
35
|
/**
|
|
35
36
|
* Harness helper executables/scripts. Every one of these is started by the
|
|
36
37
|
* mochitest/xpcshell harness and is expected to die with it; none of them
|
|
@@ -93,10 +94,10 @@ const COMMAND_EXCERPT_LIMIT = 160;
|
|
|
93
94
|
*/
|
|
94
95
|
export function formatOrphanReport(orphans) {
|
|
95
96
|
const rows = orphans
|
|
96
|
-
.map((p) => ` PID ${
|
|
97
|
+
.map((p) => ` PID ${p.pid} (up ${p.elapsed}): ${p.command.slice(0, COMMAND_EXCERPT_LIMIT)}`)
|
|
97
98
|
.join('\n');
|
|
98
99
|
const pids = orphans.map((p) => String(p.pid)).join(' ');
|
|
99
|
-
return (`${
|
|
100
|
+
return (`${orphans.length} harness helper process(es) from an EARLIER run are still alive ` +
|
|
100
101
|
`(this preflight runs before the current run spawns anything, so none of these belong to ` +
|
|
101
102
|
`it):\n${rows}\n` +
|
|
102
103
|
`Survivors like these slow every later run without appearing anywhere in its output — a ` +
|
|
@@ -107,7 +108,7 @@ export function formatOrphanReport(orphans) {
|
|
|
107
108
|
async function listSystemProcesses() {
|
|
108
109
|
const result = await exec('ps', ['-axo', 'pid=,ppid=,etime=,command='], { timeout: 10000 });
|
|
109
110
|
if (result.exitCode !== 0) {
|
|
110
|
-
throw new Error(`ps exited ${
|
|
111
|
+
throw new Error(`ps exited ${result.exitCode}`);
|
|
111
112
|
}
|
|
112
113
|
return result.stdout;
|
|
113
114
|
}
|
|
@@ -159,13 +160,13 @@ async function reapOrphanedHarnessProcesses(orphans) {
|
|
|
159
160
|
process.kill(orphan.pid, 'SIGTERM');
|
|
160
161
|
}
|
|
161
162
|
catch (error) {
|
|
162
|
-
verbose(`--reap-orphans: SIGTERM to ${
|
|
163
|
+
verbose(`--reap-orphans: SIGTERM to ${orphan.pid} failed (${toError(error).message}).`);
|
|
163
164
|
continue;
|
|
164
165
|
}
|
|
165
166
|
// Grace period before escalating: the httpd shape in the field
|
|
166
167
|
// incident ignored SIGTERM while spinning, but an ordinary helper exits
|
|
167
168
|
// promptly and must not be SIGKILLed for being slow by a millisecond.
|
|
168
|
-
await
|
|
169
|
+
await sleep(REAP_GRACE_MS);
|
|
169
170
|
let alive = true;
|
|
170
171
|
try {
|
|
171
172
|
process.kill(orphan.pid, 0);
|
|
@@ -178,9 +179,9 @@ async function reapOrphanedHarnessProcesses(orphans) {
|
|
|
178
179
|
process.kill(orphan.pid, 'SIGKILL');
|
|
179
180
|
}
|
|
180
181
|
catch (error) {
|
|
181
|
-
verbose(`--reap-orphans: SIGKILL to ${
|
|
182
|
+
verbose(`--reap-orphans: SIGKILL to ${orphan.pid} failed (${toError(error).message}).`);
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
|
-
info(`--reap-orphans: terminated PID ${
|
|
185
|
+
info(`--reap-orphans: terminated PID ${orphan.pid}.`);
|
|
185
186
|
}
|
|
186
187
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HistoryEntry } from './destructive.js';
|
|
2
|
+
/**
|
|
3
|
+
* Appends a history entry for a mutation that has ALREADY committed, degrading
|
|
4
|
+
* to a warning when the log cannot be written.
|
|
5
|
+
*
|
|
6
|
+
* Eleven call sites across `export-flow` and the `patch` subcommands had each
|
|
7
|
+
* grown the same `try { await appendHistory(...) } catch { warn(...) }` block.
|
|
8
|
+
* The swallow is deliberate and load-bearing: the patch files and the manifest
|
|
9
|
+
* are already on disk by the time this runs, so letting a failed log write
|
|
10
|
+
* reject would report a completed, irreversible mutation as a failure and send
|
|
11
|
+
* the operator looking for damage that is not there. Because that reasoning is
|
|
12
|
+
* easy to lose in a copy, it now lives in one place.
|
|
13
|
+
* @param patchesDir - The `patches/` directory holding the history log.
|
|
14
|
+
* @param entry - The entry to append.
|
|
15
|
+
* @param context - What just committed, rendered into
|
|
16
|
+
* `History log append failed after <context>: <reason>` — e.g.
|
|
17
|
+
* `patch tier committed (007-foo.patch)`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function appendHistoryBestEffort(patchesDir: string, entry: HistoryEntry, context: string): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { toError } from '../utils/errors.js';
|
|
3
|
+
import { warn } from '../utils/logger.js';
|
|
4
|
+
import { appendHistory } from './destructive.js';
|
|
5
|
+
/**
|
|
6
|
+
* Appends a history entry for a mutation that has ALREADY committed, degrading
|
|
7
|
+
* to a warning when the log cannot be written.
|
|
8
|
+
*
|
|
9
|
+
* Eleven call sites across `export-flow` and the `patch` subcommands had each
|
|
10
|
+
* grown the same `try { await appendHistory(...) } catch { warn(...) }` block.
|
|
11
|
+
* The swallow is deliberate and load-bearing: the patch files and the manifest
|
|
12
|
+
* are already on disk by the time this runs, so letting a failed log write
|
|
13
|
+
* reject would report a completed, irreversible mutation as a failure and send
|
|
14
|
+
* the operator looking for damage that is not there. Because that reasoning is
|
|
15
|
+
* easy to lose in a copy, it now lives in one place.
|
|
16
|
+
* @param patchesDir - The `patches/` directory holding the history log.
|
|
17
|
+
* @param entry - The entry to append.
|
|
18
|
+
* @param context - What just committed, rendered into
|
|
19
|
+
* `History log append failed after <context>: <reason>` — e.g.
|
|
20
|
+
* `patch tier committed (007-foo.patch)`.
|
|
21
|
+
*/
|
|
22
|
+
export async function appendHistoryBestEffort(patchesDir, entry, context) {
|
|
23
|
+
try {
|
|
24
|
+
await appendHistory(patchesDir, entry);
|
|
25
|
+
}
|
|
26
|
+
catch (historyError) {
|
|
27
|
+
warn(`History log append failed after ${context}: ${toError(historyError).message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -15,11 +15,6 @@ export interface MachErrorHint {
|
|
|
15
15
|
/** Actionable, one-line hint to surface alongside the raw mach output. */
|
|
16
16
|
hint: string;
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Registered hint patterns. Order-sensitive: the first match wins per
|
|
20
|
-
* pattern, but multiple distinct patterns may fire for the same stderr.
|
|
21
|
-
*/
|
|
22
|
-
export declare const MACH_ERROR_HINTS: MachErrorHint[];
|
|
23
18
|
/**
|
|
24
19
|
* Scans captured stderr for known mach errors and returns matching hints.
|
|
25
20
|
* Pure function — safe to call on any string; never throws.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Registered hint patterns. Order-sensitive: the first match wins per
|
|
14
14
|
* pattern, but multiple distinct patterns may fire for the same stderr.
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
const MACH_ERROR_HINTS = [
|
|
17
17
|
{
|
|
18
18
|
pattern: /mozbuild\.preprocessor\.Preprocessor\.Error[\s\S]*?no preprocessor directives found/,
|
|
19
19
|
hint: 'A file registered under JS_PREFERENCE_PP_FILES contains no preprocessor directives. ' +
|
|
@@ -41,10 +41,11 @@
|
|
|
41
41
|
* `runProtectedMachBuild`.
|
|
42
42
|
*/
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
* teardown
|
|
44
|
+
* Whole-block recognition — every signal must hold (see module doc). True
|
|
45
|
+
* when CAPTURED output carries the documented mozsystemmonitor teardown
|
|
46
|
+
* traceback.
|
|
46
47
|
*
|
|
47
|
-
* The echo filter
|
|
48
|
+
* The echo filter below only affects what a human sees. The CLASSIFIER
|
|
48
49
|
* reads the raw capture, and it needs the same recognition to tell "the
|
|
49
50
|
* suite finished clean and then upstream fell over at shutdown" from "the
|
|
50
51
|
* suite did not finish". Deliberately the same two-signal test the echo
|
|
@@ -57,15 +57,12 @@ const RESOURCEMONITOR_FRAME_PATTERN = /mozsystemmonitor[/\\]resourcemonitor\.py/
|
|
|
57
57
|
* there; duplicated here with this cross-reference rather than exported).
|
|
58
58
|
*/
|
|
59
59
|
const SHUTDOWN_MARKER_PATTERN = /\bSUITE_END\b/;
|
|
60
|
-
/** Whole-block recognition — every signal must hold (see module doc). */
|
|
61
|
-
function isRecognizedTeardownNoise(block) {
|
|
62
|
-
return (KNOWN_TEARDOWN_ATTRIBUTE_ERROR_PATTERN.test(block) && RESOURCEMONITOR_FRAME_PATTERN.test(block));
|
|
63
|
-
}
|
|
64
60
|
/**
|
|
65
|
-
*
|
|
66
|
-
* teardown
|
|
61
|
+
* Whole-block recognition — every signal must hold (see module doc). True
|
|
62
|
+
* when CAPTURED output carries the documented mozsystemmonitor teardown
|
|
63
|
+
* traceback.
|
|
67
64
|
*
|
|
68
|
-
* The echo filter
|
|
65
|
+
* The echo filter below only affects what a human sees. The CLASSIFIER
|
|
69
66
|
* reads the raw capture, and it needs the same recognition to tell "the
|
|
70
67
|
* suite finished clean and then upstream fell over at shutdown" from "the
|
|
71
68
|
* suite did not finish". Deliberately the same two-signal test the echo
|
|
@@ -82,7 +79,8 @@ function isRecognizedTeardownNoise(block) {
|
|
|
82
79
|
* @returns True when the recognized teardown traceback is present
|
|
83
80
|
*/
|
|
84
81
|
export function hasKnownTeardownNoise(output) {
|
|
85
|
-
return
|
|
82
|
+
return (KNOWN_TEARDOWN_ATTRIBUTE_ERROR_PATTERN.test(output) &&
|
|
83
|
+
RESOURCEMONITOR_FRAME_PATTERN.test(output));
|
|
86
84
|
}
|
|
87
85
|
/**
|
|
88
86
|
* True when a SINGLE line is the documented teardown family's
|
|
@@ -159,7 +157,7 @@ export function createKnownTeardownNoiseFilter(context = createTeardownNoiseCont
|
|
|
159
157
|
const block = resetHold();
|
|
160
158
|
if (block.length === 0)
|
|
161
159
|
return '';
|
|
162
|
-
return context.shutdownSeen &&
|
|
160
|
+
return context.shutdownSeen && hasKnownTeardownNoise(block)
|
|
163
161
|
? KNOWN_TEARDOWN_NOISE_ANNOTATION
|
|
164
162
|
: block;
|
|
165
163
|
};
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { MozconfigError } from '../errors/build.js';
|
|
4
4
|
import { pathExists, readText, writeTextIfChanged } from '../utils/fs.js';
|
|
5
5
|
import { verbose } from '../utils/logger.js';
|
|
6
|
+
import { normalizePathSlashes } from '../utils/paths.js';
|
|
6
7
|
import { getPlatform } from '../utils/platform.js';
|
|
7
8
|
import { BrandingMozconfigMismatchError, splitAppId } from './branding.js';
|
|
8
9
|
/**
|
|
@@ -96,7 +97,7 @@ export async function assertBrandingMozconfigAgreement(engineDir, mozconfigPath,
|
|
|
96
97
|
// Normalise both sides to forward slashes before compare — Windows-edited
|
|
97
98
|
// configs can carry backslash path separators that the build would treat
|
|
98
99
|
// as literal characters in a repo-relative path.
|
|
99
|
-
const normalizedFound = found
|
|
100
|
+
const normalizedFound = normalizePathSlashes(found);
|
|
100
101
|
if (normalizedFound !== expected) {
|
|
101
102
|
throw new BrandingMozconfigMismatchError(expected, found, 'name-mismatch');
|
|
102
103
|
}
|
|
@@ -6,7 +6,12 @@ import { pathExists, readText } from '../utils/fs.js';
|
|
|
6
6
|
import { verbose } from '../utils/logger.js';
|
|
7
7
|
import { exec, executableExists } from '../utils/process.js';
|
|
8
8
|
/** Cached Python resolution state */
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The resolved interpreter and the requirement range it was resolved for.
|
|
11
|
+
* `undefined` means "not resolved yet"; the two always move together, so one
|
|
12
|
+
* nullable record beats two independently-deletable fields.
|
|
13
|
+
*/
|
|
14
|
+
let pythonCache;
|
|
10
15
|
const DEFAULT_MACH_PYTHON_REQUIREMENTS = {
|
|
11
16
|
min: { major: 3, minor: 8 },
|
|
12
17
|
max: { major: 3, minor: 12 },
|
|
@@ -73,10 +78,11 @@ function buildPythonCandidates(requirements) {
|
|
|
73
78
|
}
|
|
74
79
|
/**
|
|
75
80
|
* Resets the resolved Python executable. Primarily useful for testing.
|
|
81
|
+
*
|
|
82
|
+
* @internal Exported only so tests can reach it; not part of the public surface.
|
|
76
83
|
*/
|
|
77
84
|
export function resetResolvedPython() {
|
|
78
|
-
|
|
79
|
-
delete pythonCache.requirementsKey;
|
|
85
|
+
pythonCache = undefined;
|
|
80
86
|
}
|
|
81
87
|
/**
|
|
82
88
|
* Dynamically resolves the python executable and ensures it is available.
|
|
@@ -86,7 +92,7 @@ export function resetResolvedPython() {
|
|
|
86
92
|
export async function ensurePython(engineDir) {
|
|
87
93
|
const requirements = await readMachPythonRequirements(engineDir);
|
|
88
94
|
const requirementsKey = formatRequirementsKey(requirements);
|
|
89
|
-
if (pythonCache
|
|
95
|
+
if (pythonCache?.requirementsKey === requirementsKey) {
|
|
90
96
|
return;
|
|
91
97
|
}
|
|
92
98
|
const candidates = buildPythonCandidates(requirements);
|
|
@@ -99,8 +105,7 @@ export async function ensurePython(engineDir) {
|
|
|
99
105
|
]);
|
|
100
106
|
const version = parsePythonVersion(stdout);
|
|
101
107
|
if (version && isVersionWithinRequirements(version, requirements)) {
|
|
102
|
-
pythonCache
|
|
103
|
-
pythonCache.requirementsKey = requirementsKey;
|
|
108
|
+
pythonCache = { python: candidate, requirementsKey };
|
|
104
109
|
return;
|
|
105
110
|
}
|
|
106
111
|
}
|
|
@@ -118,7 +123,7 @@ export async function ensurePython(engineDir) {
|
|
|
118
123
|
*/
|
|
119
124
|
export async function getPython(engineDir) {
|
|
120
125
|
await ensurePython(engineDir);
|
|
121
|
-
if (
|
|
126
|
+
if (pythonCache === undefined) {
|
|
122
127
|
throw new PythonNotFoundError();
|
|
123
128
|
}
|
|
124
129
|
return pythonCache.python;
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
* regression test (mach-resource-shim.python.test.ts), which asserts the
|
|
72
72
|
* degraded readings' runtime behavior rather than string-matching the source.
|
|
73
73
|
*/
|
|
74
|
-
export declare const GUARD_PYTHON_SOURCE = "# Generated by FireForge - do not edit.\n# Degrades the broken host resource monitor family (psutil vs Darwin) from\n# fatal startup errors into non-fatal warnings, so mach builds and tests\n# proceed. Installed into the mach virtualenv site-packages via a .pth file\n# (survives mach's venv re-exec, which drops PYTHONPATH).\nimport builtins\nimport collections\nimport sys\nimport warnings\n\n# Any psutil degradation observed in this process. Once set, monitors are\n# kept inert (start never spawns the collector child) \u2014 on a flapping host a\n# healthy-looking moment must not be trusted to start a sample stream that a\n# later degraded reading could malform.\n_fireforge_host_degraded = [False]\n\n\ndef _fireforge_degraded_notice(exc):\n _fireforge_host_degraded[0] = True\n warnings.warn(\n \"FireForge: host resource monitor degraded (%s); continuing without resource monitoring.\" % exc\n )\n\n\nclass _DegradedReading(object):\n # Last-resort duck type of psutil's namedtuple results (svmem field\n # order): mozsystemmonitor subscripts, iterates, and unpacks readings,\n # so the degraded fallback must survive r[0], list(r), len(r), r._fields\n # and r._asdict(), not just attribute access. The parent also reconstructs\n # readings via type(reading)(*values), so the constructor must tolerate\n # the full positional field list. Normal\n # degraded paths use the per-function namedtuple fallbacks below; this\n # class only backstops a fallback that itself raised.\n _fields = (\"total\", \"available\", \"percent\", \"used\", \"free\", \"active\", \"inactive\", \"wired\")\n total = available = used = free = active = inactive = wired = 0\n percent = 0.0\n\n def __init__(self, *_args, **_kwargs):\n pass\n\n def __getattr__(self, _name):\n return 0\n\n def _values(self):\n return tuple(getattr(self, _field) for _field in self._fields)\n\n def __getitem__(self, index):\n return self._values()[index]\n\n def __iter__(self):\n return iter(self._values())\n\n def __len__(self):\n return len(self._fields)\n\n def _asdict(self):\n return dict(zip(self._fields, self._values()))\n\n\n# Per-function fallback result classes with the exact macOS field\n# orders/arities. Module-level on purpose: readings cross the\n# mozsystemmonitor collector pipe via pickle (by reference), and the parent\n# rebuilds each sample with type(reading)(*values) \u2014 so a swap fallback must\n# be 6-field sswap-shaped in every position. An svmem-shaped fallback there\n# makes the parent reject every sample, fill the pipe, block the collector\n# child in send(), and wedge mach's atexit join forever.\n_FallbackVmem = collections.namedtuple(\n \"_FallbackVmem\",\n (\"total\", \"available\", \"percent\", \"used\", \"free\", \"active\", \"inactive\", \"wired\"),\n)\n_FallbackSwap = collections.namedtuple(\n \"_FallbackSwap\", (\"total\", \"used\", \"free\", \"percent\", \"sin\", \"sout\")\n)\n_FallbackCpuTimes = collections.namedtuple(\"_FallbackCpuTimes\", (\"user\", \"nice\", \"system\", \"idle\"))\n_FallbackDiskIO = collections.namedtuple(\n \"_FallbackDiskIO\",\n (\"read_count\", \"write_count\", \"read_bytes\", \"write_bytes\", \"read_time\", \"write_time\"),\n)\n\n_FALLBACK_CLASSES = {\n \"virtual_memory\": _FallbackVmem,\n \"swap_memory\": _FallbackSwap,\n \"cpu_times\": _FallbackCpuTimes,\n \"disk_io_counters\": _FallbackDiskIO,\n}\n\n# psutil result namedtuple per wrapped function, resolvable via getattr from\n# the platform module / _common without invoking the failing syscall.\n_PSUTIL_RESULT_CLASSES = {\n \"virtual_memory\": \"svmem\",\n \"swap_memory\": \"sswap\",\n \"cpu_times\": \"scputimes\",\n \"disk_io_counters\": \"sdiskio\",\n}\n\n\ndef _zeroed(cls):\n return cls(*([0] * len(cls._fields)))\n\n\ndef _percpu_requested(args, kwargs, positional_index):\n if \"percpu\" in kwargs:\n return bool(kwargs[\"percpu\"])\n return len(args) > positional_index and bool(args[positional_index])\n\n\ndef _degraded_result_factory(psutil, name):\n # cpu_percent returns a plain float (or a per-CPU list) callers do\n # arithmetic on.\n if name == \"cpu_percent\":\n def _cpu_percent_fallback(*args, **kwargs):\n # cpu_percent(interval=None, percpu=False)\n if _percpu_requested(args, kwargs, 1):\n return []\n return 0.0\n\n return _cpu_percent_fallback\n # The guard-owned class is always shape-correct for this function;\n # psutil's own result namedtuple is still preferred when resolvable so\n # type() captures stay identical to real readings on a flapping host.\n _cls = _FALLBACK_CLASSES.get(name, _DegradedReading)\n _cls_name = _PSUTIL_RESULT_CLASSES.get(name)\n if _cls_name is not None:\n for _mod in (getattr(psutil, \"_psplatform\", None), getattr(psutil, \"_common\", None)):\n _real = getattr(_mod, _cls_name, None) if _mod is not None else None\n if _real is not None and getattr(_real, \"_fields\", None):\n _cls = _real\n break\n if name == \"cpu_times\":\n def _cpu_times_fallback(*args, **kwargs):\n # cpu_times(percpu=False); the collector child samples per-CPU.\n if _percpu_requested(args, kwargs, 0):\n return []\n return _zeroed(_cls)\n\n return _cpu_times_fallback\n\n def _reading_fallback(*_args, **_kwargs):\n return _zeroed(_cls)\n\n return _reading_fallback\n\n\ndef _guard_psutil():\n try:\n import psutil\n except Exception:\n return\n\n def _wrap(orig, fallback):\n def wrapper(*args, **kwargs):\n try:\n return orig(*args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n try:\n return fallback(*args, **kwargs)\n except Exception: # noqa: BLE001\n return _DegradedReading()\n\n return wrapper\n\n for _name in (\"virtual_memory\", \"swap_memory\", \"cpu_percent\", \"cpu_times\", \"disk_io_counters\"):\n _orig = getattr(psutil, _name, None)\n if _orig is not None and not getattr(_orig, \"_fireforge_guarded\", False):\n try:\n _fallback = _degraded_result_factory(psutil, _name)\n except Exception: # noqa: BLE001\n _fallback = _DegradedReading\n _wrapped = _wrap(_orig, _fallback)\n _wrapped._fireforge_guarded = True\n setattr(psutil, _name, _wrapped)\n\n\n_MONITOR_METHOD_NAMES = (\n \"start\",\n \"stop\",\n \"record_event\",\n \"record_marker\",\n \"begin_phase\",\n \"finish_phase\",\n \"aggregate_cpu_percent\",\n \"aggregate_cpu_times\",\n \"aggregate_io\",\n \"min_memory_available\",\n \"as_dict\",\n)\n\n\ndef _fireforge_stop_collector(monitor):\n # Best-effort: terminate a live collector child and drain the parent end\n # of the pipe, so a child blocked in send() on a full pipe can never\n # keep mach's atexit join (os.waitpid) waiting forever. Attribute names\n # follow upstream resourcemonitor.py (self._process / self._pipe); a\n # miss on a refactored upstream is harmless \u2014 the shape-correct\n # fallbacks above still keep the sample stream well-formed.\n try:\n proc = getattr(monitor, \"_process\", None)\n if proc is not None and getattr(proc, \"is_alive\", lambda: False)():\n proc.terminate()\n proc.join(1)\n except Exception: # noqa: BLE001\n pass\n try:\n pipe = getattr(monitor, \"_pipe\", None)\n if pipe is not None:\n while pipe.poll(0):\n pipe.recv()\n except Exception: # noqa: BLE001\n pass\n\n\ndef _monitor_per_cpu_requested(args, kwargs):\n # aggregate_cpu_percent/aggregate_cpu_times(start=None, end=None,\n # phase=None, per_cpu=True) \u2014 self already stripped from args.\n if \"per_cpu\" in kwargs:\n return bool(kwargs[\"per_cpu\"])\n if len(args) > 3:\n return bool(args[3])\n return True\n\n\ndef _monitor_degraded_result(name, args, kwargs):\n # Degraded aggregate methods return zeroed shapes, not None, where\n # callers subscript the result: mozbuild's log_resource_usage does\n # usage[\"io\"].read_bytes after a successful compile, so None there fails\n # the build with complete artifacts already on disk.\n if name == \"aggregate_io\":\n return _zeroed(_FallbackDiskIO)\n if name == \"aggregate_cpu_percent\":\n return [] if _monitor_per_cpu_requested(args, kwargs) else 0.0\n if name == \"aggregate_cpu_times\":\n return [] if _monitor_per_cpu_requested(args, kwargs) else _zeroed(_FallbackCpuTimes)\n return None\n\n\ndef _patch_monitor_class(cls):\n if cls is None or getattr(cls, \"_fireforge_guarded\", False):\n return\n orig_init = cls.__init__\n\n def guarded_init(self, *args, **kwargs):\n # Pre-populate the attributes a partially-constructed instance is\n # later polled for, so a failing __init__ can never surface\n # AttributeError: poll_interval.\n self.poll_interval = kwargs.get(\"poll_interval\") or 1.0\n self._fireforge_degraded = False\n try:\n orig_init(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n self._fireforge_degraded = True\n _fireforge_degraded_notice(exc)\n if _fireforge_host_degraded[0] and not self._fireforge_degraded:\n # Host already degraded once: keep the monitor inert so the\n # collector child never spawns on a flapping host.\n self._fireforge_degraded = True\n if self._fireforge_degraded:\n _fireforge_stop_collector(self)\n\n cls.__init__ = guarded_init\n\n def _wrap_method(name, orig):\n def wrapper(self, *args, **kwargs):\n if (\n name == \"start\"\n and _fireforge_host_degraded[0]\n and not getattr(self, \"_fireforge_degraded\", False)\n ):\n self._fireforge_degraded = True\n _fireforge_stop_collector(self)\n if getattr(self, \"_fireforge_degraded\", False):\n if name == \"stop\":\n _fireforge_stop_collector(self)\n return _monitor_degraded_result(name, args, kwargs)\n try:\n return orig(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n self._fireforge_degraded = True\n _fireforge_degraded_notice(exc)\n # A raising stop (or any degraded transition) must still\n # take the collector child down, or the parent's atexit\n # join hangs on a child blocked writing to a full pipe.\n _fireforge_stop_collector(self)\n return _monitor_degraded_result(name, args, kwargs)\n\n return wrapper\n\n for _name in _MONITOR_METHOD_NAMES:\n _orig = getattr(cls, _name, None)\n if _orig is not None:\n setattr(cls, _name, _wrap_method(_name, _orig))\n cls._fireforge_guarded = True\n\n\ndef _patch_build_monitor_class(cls):\n # mozbuild's BuildMonitor is not a SystemResourceMonitor; only\n # log_resource_usage needs guarding \u2014 end-of-build resource reporting\n # must never fail a build whose artifacts are complete. No __init__\n # replacement, no degraded-instance gating.\n if cls is None or getattr(cls, \"_fireforge_guarded\", False):\n return\n _orig = getattr(cls, \"log_resource_usage\", None)\n if _orig is not None:\n def _guarded_log_resource_usage(self, *args, **kwargs):\n try:\n return _orig(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n return None\n\n cls.log_resource_usage = _guarded_log_resource_usage\n cls._fireforge_guarded = True\n\n\n_MONITOR_CLASS_PATCHES = (\n (\"mozsystemmonitor.resourcemonitor\", \"SystemResourceMonitor\", _patch_monitor_class),\n (\"mozbuild.resources\", \"SystemResourceMonitor\", _patch_monitor_class),\n (\"mozbuild.controller.building\", \"BuildMonitor\", _patch_build_monitor_class),\n)\n\n\ndef _patch_loaded_monitor_modules():\n for _mod_name, _cls_name, _patcher in _MONITOR_CLASS_PATCHES:\n _mod = sys.modules.get(_mod_name)\n if _mod is not None:\n try:\n _patcher(getattr(_mod, _cls_name, None))\n except Exception: # noqa: BLE001\n pass\n\n\ndef _install_import_hook():\n _orig_import = builtins.__import__\n if getattr(_orig_import, \"_fireforge_guarded\", False):\n return\n\n def _guarding_import(name, *args, **kwargs):\n _module = _orig_import(name, *args, **kwargs)\n try:\n _patch_loaded_monitor_modules()\n except Exception: # noqa: BLE001\n pass\n return _module\n\n _guarding_import._fireforge_guarded = True\n builtins.__import__ = _guarding_import\n\n\ntry:\n _guard_psutil()\n _patch_loaded_monitor_modules()\n _install_import_hook()\nexcept Exception: # noqa: BLE001\n pass\n";
|
|
74
|
+
export declare const GUARD_PYTHON_SOURCE = "# Generated by FireForge - do not edit.\n# Degrades the broken host resource monitor family (psutil vs Darwin) from\n# fatal startup errors into non-fatal warnings, so mach builds and tests\n# proceed. Installed into the mach virtualenv site-packages via a .pth file\n# (survives mach's venv re-exec, which drops PYTHONPATH).\nimport builtins\nimport collections\nimport inspect\nimport sys\nimport warnings\n\n# Any psutil degradation observed in this process. Once set, monitors are\n# kept inert (start never spawns the collector child) \u2014 on a flapping host a\n# healthy-looking moment must not be trusted to start a sample stream that a\n# later degraded reading could malform.\n_fireforge_host_degraded = [False]\n\n\ndef _fireforge_degraded_notice(exc):\n _fireforge_host_degraded[0] = True\n warnings.warn(\n \"FireForge: host resource monitor degraded (%s); continuing without resource monitoring.\" % exc\n )\n\n\nclass _DegradedReading(object):\n # Last-resort duck type of psutil's namedtuple results (svmem field\n # order): mozsystemmonitor subscripts, iterates, and unpacks readings,\n # so the degraded fallback must survive r[0], list(r), len(r), r._fields\n # and r._asdict(), not just attribute access. The parent also reconstructs\n # readings via type(reading)(*values), so the constructor must tolerate\n # the full positional field list. Normal\n # degraded paths use the per-function namedtuple fallbacks below; this\n # class only backstops a fallback that itself raised.\n _fields = (\"total\", \"available\", \"percent\", \"used\", \"free\", \"active\", \"inactive\", \"wired\")\n total = available = used = free = active = inactive = wired = 0\n percent = 0.0\n\n def __init__(self, *_args, **_kwargs):\n pass\n\n def __getattr__(self, _name):\n return 0\n\n def _values(self):\n return tuple(getattr(self, _field) for _field in self._fields)\n\n def __getitem__(self, index):\n return self._values()[index]\n\n def __iter__(self):\n return iter(self._values())\n\n def __len__(self):\n return len(self._fields)\n\n def _asdict(self):\n return dict(zip(self._fields, self._values()))\n\n\n# Per-function fallback result classes with the exact macOS field\n# orders/arities. Module-level on purpose: readings cross the\n# mozsystemmonitor collector pipe via pickle (by reference), and the parent\n# rebuilds each sample with type(reading)(*values) \u2014 so a swap fallback must\n# be 6-field sswap-shaped in every position. An svmem-shaped fallback there\n# makes the parent reject every sample, fill the pipe, block the collector\n# child in send(), and wedge mach's atexit join forever.\n_FallbackVmem = collections.namedtuple(\n \"_FallbackVmem\",\n (\"total\", \"available\", \"percent\", \"used\", \"free\", \"active\", \"inactive\", \"wired\"),\n)\n_FallbackSwap = collections.namedtuple(\n \"_FallbackSwap\", (\"total\", \"used\", \"free\", \"percent\", \"sin\", \"sout\")\n)\n_FallbackCpuTimes = collections.namedtuple(\"_FallbackCpuTimes\", (\"user\", \"nice\", \"system\", \"idle\"))\n_FallbackDiskIO = collections.namedtuple(\n \"_FallbackDiskIO\",\n (\"read_count\", \"write_count\", \"read_bytes\", \"write_bytes\", \"read_time\", \"write_time\"),\n)\n\n_FALLBACK_CLASSES = {\n \"virtual_memory\": _FallbackVmem,\n \"swap_memory\": _FallbackSwap,\n \"cpu_times\": _FallbackCpuTimes,\n \"disk_io_counters\": _FallbackDiskIO,\n}\n\n# psutil result namedtuple per wrapped function, resolvable via getattr from\n# the platform module / _common without invoking the failing syscall.\n_PSUTIL_RESULT_CLASSES = {\n \"virtual_memory\": \"svmem\",\n \"swap_memory\": \"sswap\",\n \"cpu_times\": \"scputimes\",\n \"disk_io_counters\": \"sdiskio\",\n}\n\n\ndef _zeroed(cls):\n return cls(*([0] * len(cls._fields)))\n\n\ndef _percpu_requested(args, kwargs, positional_index):\n if \"percpu\" in kwargs:\n return bool(kwargs[\"percpu\"])\n return len(args) > positional_index and bool(args[positional_index])\n\n\ndef _degraded_result_factory(psutil, name):\n # cpu_percent returns a plain float (or a per-CPU list) callers do\n # arithmetic on.\n if name == \"cpu_percent\":\n def _cpu_percent_fallback(*args, **kwargs):\n # cpu_percent(interval=None, percpu=False)\n if _percpu_requested(args, kwargs, 1):\n return []\n return 0.0\n\n return _cpu_percent_fallback\n # The guard-owned class is always shape-correct for this function;\n # psutil's own result namedtuple is still preferred when resolvable so\n # type() captures stay identical to real readings on a flapping host.\n _cls = _FALLBACK_CLASSES.get(name, _DegradedReading)\n _cls_name = _PSUTIL_RESULT_CLASSES.get(name)\n if _cls_name is not None:\n for _mod in (getattr(psutil, \"_psplatform\", None), getattr(psutil, \"_common\", None)):\n _real = getattr(_mod, _cls_name, None) if _mod is not None else None\n if _real is not None and getattr(_real, \"_fields\", None):\n _cls = _real\n break\n if name == \"cpu_times\":\n def _cpu_times_fallback(*args, **kwargs):\n # cpu_times(percpu=False); the collector child samples per-CPU.\n if _percpu_requested(args, kwargs, 0):\n return []\n return _zeroed(_cls)\n\n return _cpu_times_fallback\n\n def _reading_fallback(*_args, **_kwargs):\n return _zeroed(_cls)\n\n return _reading_fallback\n\n\ndef _guard_psutil():\n try:\n import psutil\n except Exception:\n return\n\n def _wrap(orig, fallback):\n def wrapper(*args, **kwargs):\n try:\n return orig(*args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n try:\n return fallback(*args, **kwargs)\n except Exception: # noqa: BLE001\n return _DegradedReading()\n\n return wrapper\n\n for _name in (\"virtual_memory\", \"swap_memory\", \"cpu_percent\", \"cpu_times\", \"disk_io_counters\"):\n _orig = getattr(psutil, _name, None)\n if _orig is not None and not getattr(_orig, \"_fireforge_guarded\", False):\n try:\n _fallback = _degraded_result_factory(psutil, _name)\n except Exception: # noqa: BLE001\n _fallback = _DegradedReading\n _wrapped = _wrap(_orig, _fallback)\n _wrapped._fireforge_guarded = True\n setattr(psutil, _name, _wrapped)\n\n\n_MONITOR_METHOD_NAMES = (\n \"start\",\n \"stop\",\n \"record_event\",\n \"record_marker\",\n \"begin_phase\",\n \"finish_phase\",\n \"aggregate_cpu_percent\",\n \"aggregate_cpu_times\",\n \"aggregate_io\",\n \"min_memory_available\",\n \"as_dict\",\n)\n\n\ndef _fireforge_stop_collector(monitor):\n # Best-effort: terminate a live collector child and drain the parent end\n # of the pipe, so a child blocked in send() on a full pipe can never\n # keep mach's atexit join (os.waitpid) waiting forever. Attribute names\n # follow upstream resourcemonitor.py (self._process / self._pipe); a\n # miss on a refactored upstream is harmless \u2014 the shape-correct\n # fallbacks above still keep the sample stream well-formed.\n try:\n proc = getattr(monitor, \"_process\", None)\n if proc is not None and getattr(proc, \"is_alive\", lambda: False)():\n proc.terminate()\n proc.join(1)\n except Exception: # noqa: BLE001\n pass\n try:\n pipe = getattr(monitor, \"_pipe\", None)\n if pipe is not None:\n while pipe.poll(0):\n pipe.recv()\n except Exception: # noqa: BLE001\n pass\n\n\ndef _monitor_per_cpu_requested(args, kwargs):\n # aggregate_cpu_percent/aggregate_cpu_times(start=None, end=None,\n # phase=None, per_cpu=True) \u2014 self already stripped from args.\n if \"per_cpu\" in kwargs:\n return bool(kwargs[\"per_cpu\"])\n if len(args) > 3:\n return bool(args[3])\n return True\n\n\ndef _monitor_degraded_result(name, args, kwargs):\n # Degraded aggregate methods return zeroed shapes, not None, where\n # callers subscript the result: mozbuild's log_resource_usage does\n # usage[\"io\"].read_bytes after a successful compile, so None there fails\n # the build with complete artifacts already on disk.\n if name == \"aggregate_io\":\n return _zeroed(_FallbackDiskIO)\n if name == \"aggregate_cpu_percent\":\n return [] if _monitor_per_cpu_requested(args, kwargs) else 0.0\n if name == \"aggregate_cpu_times\":\n return [] if _monitor_per_cpu_requested(args, kwargs) else _zeroed(_FallbackCpuTimes)\n return None\n\n\ndef _patch_monitor_class(cls):\n if cls is None or getattr(cls, \"_fireforge_guarded\", False):\n return\n orig_init = cls.__init__\n\n def guarded_init(self, *args, **kwargs):\n # Pre-populate the attributes a partially-constructed instance is\n # later polled for, so a failing __init__ can never surface\n # AttributeError: poll_interval.\n self.poll_interval = kwargs.get(\"poll_interval\") or 1.0\n self._fireforge_degraded = False\n try:\n orig_init(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n self._fireforge_degraded = True\n _fireforge_degraded_notice(exc)\n if _fireforge_host_degraded[0] and not self._fireforge_degraded:\n # Host already degraded once: keep the monitor inert so the\n # collector child never spawns on a flapping host.\n self._fireforge_degraded = True\n if self._fireforge_degraded:\n _fireforge_stop_collector(self)\n\n cls.__init__ = guarded_init\n\n def _wrap_method(name, orig):\n def wrapper(self, *args, **kwargs):\n if (\n name == \"start\"\n and _fireforge_host_degraded[0]\n and not getattr(self, \"_fireforge_degraded\", False)\n ):\n self._fireforge_degraded = True\n _fireforge_stop_collector(self)\n if getattr(self, \"_fireforge_degraded\", False):\n if name == \"stop\":\n _fireforge_stop_collector(self)\n return _monitor_degraded_result(name, args, kwargs)\n try:\n return orig(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n self._fireforge_degraded = True\n _fireforge_degraded_notice(exc)\n # A raising stop (or any degraded transition) must still\n # take the collector child down, or the parent's atexit\n # join hangs on a child blocked writing to a full pipe.\n _fireforge_stop_collector(self)\n return _monitor_degraded_result(name, args, kwargs)\n\n return wrapper\n\n def _wrap_unbound(name, func):\n # Upstream declares record_event/record_marker as @staticmethods (and\n # may declare others as @classmethods). Those take no receiver, so\n # they cannot use the per-instance degraded flag and must not have one\n # threaded in \u2014 see the descriptor dispatch below.\n def wrapper(*args, **kwargs):\n if _fireforge_host_degraded[0]:\n return _monitor_degraded_result(name, args, kwargs)\n try:\n return func(*args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n return _monitor_degraded_result(name, args, kwargs)\n\n return wrapper\n\n for _name in _MONITOR_METHOD_NAMES:\n # getattr_static returns the raw class attribute, so a staticmethod\n # is still a staticmethod object here. Plain getattr would hand back\n # the underlying function, and re-setting THAT on the class rebinds\n # it as an instance method: the receiver is then passed through as an\n # extra leading positional and every call raises\n # \"takes N positional arguments but N+1 were given\". Re-apply the\n # descriptor the attribute actually had.\n try:\n _raw = inspect.getattr_static(cls, _name)\n except AttributeError:\n continue\n if isinstance(_raw, staticmethod):\n setattr(cls, _name, staticmethod(_wrap_unbound(_name, _raw.__func__)))\n continue\n if isinstance(_raw, classmethod):\n setattr(cls, _name, classmethod(_wrap_unbound(_name, _raw.__func__)))\n continue\n _orig = getattr(cls, _name, None)\n if _orig is not None:\n setattr(cls, _name, _wrap_method(_name, _orig))\n cls._fireforge_guarded = True\n\n\ndef _patch_build_monitor_class(cls):\n # mozbuild's BuildMonitor is not a SystemResourceMonitor; only\n # log_resource_usage needs guarding \u2014 end-of-build resource reporting\n # must never fail a build whose artifacts are complete. No __init__\n # replacement, no degraded-instance gating.\n if cls is None or getattr(cls, \"_fireforge_guarded\", False):\n return\n _orig = getattr(cls, \"log_resource_usage\", None)\n if _orig is not None:\n def _guarded_log_resource_usage(self, *args, **kwargs):\n try:\n return _orig(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n return None\n\n cls.log_resource_usage = _guarded_log_resource_usage\n cls._fireforge_guarded = True\n\n\n_MONITOR_CLASS_PATCHES = (\n (\"mozsystemmonitor.resourcemonitor\", \"SystemResourceMonitor\", _patch_monitor_class),\n (\"mozbuild.resources\", \"SystemResourceMonitor\", _patch_monitor_class),\n (\"mozbuild.controller.building\", \"BuildMonitor\", _patch_build_monitor_class),\n)\n\n\ndef _patch_loaded_monitor_modules():\n for _mod_name, _cls_name, _patcher in _MONITOR_CLASS_PATCHES:\n _mod = sys.modules.get(_mod_name)\n if _mod is not None:\n try:\n _patcher(getattr(_mod, _cls_name, None))\n except Exception: # noqa: BLE001\n pass\n\n\ndef _install_import_hook():\n _orig_import = builtins.__import__\n if getattr(_orig_import, \"_fireforge_guarded\", False):\n return\n\n def _guarding_import(name, *args, **kwargs):\n _module = _orig_import(name, *args, **kwargs)\n try:\n _patch_loaded_monitor_modules()\n except Exception: # noqa: BLE001\n pass\n return _module\n\n _guarding_import._fireforge_guarded = True\n builtins.__import__ = _guarding_import\n\n\ntry:\n _guard_psutil()\n _patch_loaded_monitor_modules()\n _install_import_hook()\nexcept Exception: # noqa: BLE001\n pass\n";
|
|
75
75
|
/** Result of installing the mach resource guard before a dispatch. */
|
|
76
76
|
export interface MachResourceGuardInstallation {
|
|
77
77
|
/**
|
|
@@ -83,6 +83,18 @@ export interface MachResourceGuardInstallation {
|
|
|
83
83
|
/** site-packages directories the in-venv guard was written into. */
|
|
84
84
|
sitePackagesDirs: string[];
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Refuses a shim directory that is not a private directory of the current
|
|
88
|
+
* user. `lstat`, not `stat`: a symlink planted at the path would otherwise
|
|
89
|
+
* be followed and the check would describe its target. On Windows only the
|
|
90
|
+
* "is a real directory" half applies — there is no POSIX owner or mode bit
|
|
91
|
+
* to read, and the per-user name is the isolation.
|
|
92
|
+
*
|
|
93
|
+
* @param dir - Shim directory that {@link ensureSitecustomizeFallback} created
|
|
94
|
+
* @throws BuildError when the directory is a symlink/file, owned by another
|
|
95
|
+
* uid, or group/world-writable
|
|
96
|
+
*/
|
|
97
|
+
export declare function assertPrivateShimDir(dir: string): Promise<void>;
|
|
86
98
|
/**
|
|
87
99
|
* Installs the resource-monitor degrade guard for a mach dispatch:
|
|
88
100
|
* `fireforge_mach_guard.pth` + `fireforge_mach_guard.py` into every
|
|
@@ -63,13 +63,20 @@
|
|
|
63
63
|
* warn-and-continue on any exception, so end-of-build resource reporting
|
|
64
64
|
* can never fail a build whose artifacts are complete.
|
|
65
65
|
*/
|
|
66
|
-
import { readdir } from 'node:fs/promises';
|
|
67
|
-
import { homedir, tmpdir } from 'node:os';
|
|
66
|
+
import { lstat, mkdir, readdir } from 'node:fs/promises';
|
|
67
|
+
import { homedir, tmpdir, userInfo } from 'node:os';
|
|
68
68
|
import { delimiter, join } from 'node:path';
|
|
69
|
+
import { BuildError } from '../errors/build.js';
|
|
69
70
|
import { toError } from '../utils/errors.js';
|
|
70
|
-
import {
|
|
71
|
+
import { pathExists, writeText } from '../utils/fs.js';
|
|
71
72
|
import { verbose } from '../utils/logger.js';
|
|
72
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Prefix of the per-user directory (under the OS temp dir) holding the
|
|
75
|
+
* PYTHONPATH fallback. The uid (or, on Windows, the username) is appended:
|
|
76
|
+
* a FIXED name under a shared `/tmp` lets any other local account pre-create
|
|
77
|
+
* the directory and own the `sitecustomize.py` that every mach dispatch
|
|
78
|
+
* then imports.
|
|
79
|
+
*/
|
|
73
80
|
const RESOURCE_SHIM_DIRNAME = 'fireforge-mach-resource-shim';
|
|
74
81
|
/** Basename (sans extension) of the in-venv guard module. */
|
|
75
82
|
const GUARD_MODULE_NAME = 'fireforge_mach_guard';
|
|
@@ -89,6 +96,7 @@ export const GUARD_PYTHON_SOURCE = `# Generated by FireForge - do not edit.
|
|
|
89
96
|
# (survives mach's venv re-exec, which drops PYTHONPATH).
|
|
90
97
|
import builtins
|
|
91
98
|
import collections
|
|
99
|
+
import inspect
|
|
92
100
|
import sys
|
|
93
101
|
import warnings
|
|
94
102
|
|
|
@@ -369,7 +377,40 @@ def _patch_monitor_class(cls):
|
|
|
369
377
|
|
|
370
378
|
return wrapper
|
|
371
379
|
|
|
380
|
+
def _wrap_unbound(name, func):
|
|
381
|
+
# Upstream declares record_event/record_marker as @staticmethods (and
|
|
382
|
+
# may declare others as @classmethods). Those take no receiver, so
|
|
383
|
+
# they cannot use the per-instance degraded flag and must not have one
|
|
384
|
+
# threaded in — see the descriptor dispatch below.
|
|
385
|
+
def wrapper(*args, **kwargs):
|
|
386
|
+
if _fireforge_host_degraded[0]:
|
|
387
|
+
return _monitor_degraded_result(name, args, kwargs)
|
|
388
|
+
try:
|
|
389
|
+
return func(*args, **kwargs)
|
|
390
|
+
except Exception as exc: # noqa: BLE001
|
|
391
|
+
_fireforge_degraded_notice(exc)
|
|
392
|
+
return _monitor_degraded_result(name, args, kwargs)
|
|
393
|
+
|
|
394
|
+
return wrapper
|
|
395
|
+
|
|
372
396
|
for _name in _MONITOR_METHOD_NAMES:
|
|
397
|
+
# getattr_static returns the raw class attribute, so a staticmethod
|
|
398
|
+
# is still a staticmethod object here. Plain getattr would hand back
|
|
399
|
+
# the underlying function, and re-setting THAT on the class rebinds
|
|
400
|
+
# it as an instance method: the receiver is then passed through as an
|
|
401
|
+
# extra leading positional and every call raises
|
|
402
|
+
# "takes N positional arguments but N+1 were given". Re-apply the
|
|
403
|
+
# descriptor the attribute actually had.
|
|
404
|
+
try:
|
|
405
|
+
_raw = inspect.getattr_static(cls, _name)
|
|
406
|
+
except AttributeError:
|
|
407
|
+
continue
|
|
408
|
+
if isinstance(_raw, staticmethod):
|
|
409
|
+
setattr(cls, _name, staticmethod(_wrap_unbound(_name, _raw.__func__)))
|
|
410
|
+
continue
|
|
411
|
+
if isinstance(_raw, classmethod):
|
|
412
|
+
setattr(cls, _name, classmethod(_wrap_unbound(_name, _raw.__func__)))
|
|
413
|
+
continue
|
|
373
414
|
_orig = getattr(cls, _name, None)
|
|
374
415
|
if _orig is not None:
|
|
375
416
|
setattr(cls, _name, _wrap_method(_name, _orig))
|
|
@@ -488,13 +529,75 @@ async function discoverMachSitePackages(engineDir) {
|
|
|
488
529
|
return found;
|
|
489
530
|
}
|
|
490
531
|
/**
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
532
|
+
* Per-user discriminator for the shim directory name. The numeric uid on
|
|
533
|
+
* POSIX, read via `process.getuid` rather than `os.userInfo()`: the latter
|
|
534
|
+
* resolves the passwd entry and THROWS (`ERR_SYSTEM_ERROR`) for a uid that
|
|
535
|
+
* has none — `docker --user 1001:1001`, OpenShift's random uids, many CI
|
|
536
|
+
* agents — which would take every build and test dispatch down with a raw
|
|
537
|
+
* system error. On Windows there is no uid; the username is the
|
|
538
|
+
* discriminator, with `userInfo()` still guarded because the same lookup
|
|
539
|
+
* can fail there for a service account, falling back to the environment.
|
|
540
|
+
*/
|
|
541
|
+
function shimDirOwner() {
|
|
542
|
+
const uid = process.getuid?.();
|
|
543
|
+
if (uid !== undefined)
|
|
544
|
+
return String(uid);
|
|
545
|
+
let username;
|
|
546
|
+
try {
|
|
547
|
+
username = userInfo().username;
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
username = process.env['USERNAME'] ?? process.env['USER'];
|
|
551
|
+
}
|
|
552
|
+
return (username ?? 'unknown-user').replace(/[^A-Za-z0-9_.-]/g, '_');
|
|
553
|
+
}
|
|
554
|
+
/** Per-user shim directory: `<tmpdir>/fireforge-mach-resource-shim-<uid>`. */
|
|
555
|
+
function resourceShimDir() {
|
|
556
|
+
return join(tmpdir(), `${RESOURCE_SHIM_DIRNAME}-${shimDirOwner()}`);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Refuses a shim directory that is not a private directory of the current
|
|
560
|
+
* user. `lstat`, not `stat`: a symlink planted at the path would otherwise
|
|
561
|
+
* be followed and the check would describe its target. On Windows only the
|
|
562
|
+
* "is a real directory" half applies — there is no POSIX owner or mode bit
|
|
563
|
+
* to read, and the per-user name is the isolation.
|
|
564
|
+
*
|
|
565
|
+
* @param dir - Shim directory that {@link ensureSitecustomizeFallback} created
|
|
566
|
+
* @throws BuildError when the directory is a symlink/file, owned by another
|
|
567
|
+
* uid, or group/world-writable
|
|
568
|
+
*/
|
|
569
|
+
export async function assertPrivateShimDir(dir) {
|
|
570
|
+
const stats = await lstat(dir);
|
|
571
|
+
const refuse = (reason) => {
|
|
572
|
+
throw new BuildError(`Refusing to use mach resource shim directory ${dir}: ${reason}. ` +
|
|
573
|
+
'Another local account may have planted it; remove or fix it and retry.');
|
|
574
|
+
};
|
|
575
|
+
if (!stats.isDirectory()) {
|
|
576
|
+
refuse('not a directory (symlink or file)');
|
|
577
|
+
}
|
|
578
|
+
if (process.platform === 'win32')
|
|
579
|
+
return;
|
|
580
|
+
const uid = process.getuid?.();
|
|
581
|
+
if (uid !== undefined && stats.uid !== uid) {
|
|
582
|
+
refuse(`owned by uid ${stats.uid}, not the current user (uid ${uid})`);
|
|
583
|
+
}
|
|
584
|
+
if ((stats.mode & 0o022) !== 0) {
|
|
585
|
+
refuse(`mode ${(stats.mode & 0o777).toString(8)} is group/world-writable`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Writes the PYTHONPATH `sitecustomize.py` fallback into a per-user,
|
|
590
|
+
* mode-0700 FireForge-owned temp directory, verified private before the
|
|
591
|
+
* write, and returns that directory. Idempotent — overwriting each call
|
|
592
|
+
* keeps it in sync with this source across upgrades.
|
|
494
593
|
*/
|
|
495
594
|
async function ensureSitecustomizeFallback() {
|
|
496
|
-
const dir =
|
|
497
|
-
|
|
595
|
+
const dir = resourceShimDir();
|
|
596
|
+
// `mode` only applies when mkdir creates the directory (and is masked by
|
|
597
|
+
// the umask); a pre-existing one keeps whatever it had, which is exactly
|
|
598
|
+
// what the assertion below refuses if it is not private.
|
|
599
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
600
|
+
await assertPrivateShimDir(dir);
|
|
498
601
|
await writeText(join(dir, 'sitecustomize.py'), GUARD_PYTHON_SOURCE);
|
|
499
602
|
return dir;
|
|
500
603
|
}
|