@hominis/fireforge 0.35.0 → 0.36.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 +29 -0
- package/README.md +34 -9
- package/dist/bin/fireforge.js +21 -6
- package/dist/src/cli.js +22 -5
- package/dist/src/commands/bootstrap-checks.js +0 -1
- package/dist/src/commands/bootstrap.d.ts +2 -1
- package/dist/src/commands/bootstrap.js +16 -7
- package/dist/src/commands/build.js +4 -3
- package/dist/src/commands/config.js +30 -10
- package/dist/src/commands/discard.js +18 -3
- package/dist/src/commands/doctor/post-rebase-audit.js +0 -1
- package/dist/src/commands/doctor-check-core.js +0 -1
- package/dist/src/commands/doctor-external-toolchains.d.ts +3 -0
- package/dist/src/commands/doctor-external-toolchains.js +54 -0
- package/dist/src/commands/doctor-furnace-jar.js +0 -1
- package/dist/src/commands/doctor-furnace-manifest-sync.js +0 -1
- package/dist/src/commands/doctor-furnace.js +2 -2
- package/dist/src/commands/doctor-working-tree.js +0 -1
- package/dist/src/commands/doctor.js +4 -1
- package/dist/src/commands/download.js +28 -13
- package/dist/src/commands/export-all.js +5 -3
- package/dist/src/commands/export-flow.js +12 -7
- package/dist/src/commands/export-placement-gate.js +0 -1
- package/dist/src/commands/export-placement-policy.js +0 -1
- package/dist/src/commands/export-shared.d.ts +5 -1
- package/dist/src/commands/export-shared.js +18 -6
- package/dist/src/commands/export.js +15 -10
- package/dist/src/commands/furnace/apply.js +22 -6
- package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
- package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
- package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
- package/dist/src/commands/furnace/chrome-doc.js +0 -1
- package/dist/src/commands/furnace/create-browser-test.js +0 -1
- package/dist/src/commands/furnace/create-dry-run.js +0 -1
- package/dist/src/commands/furnace/create-features.js +0 -1
- package/dist/src/commands/furnace/create-mochikit.js +0 -1
- package/dist/src/commands/furnace/create-readback.js +0 -1
- package/dist/src/commands/furnace/create-templates.js +0 -1
- package/dist/src/commands/furnace/create-validation.js +0 -1
- package/dist/src/commands/furnace/create-xpcshell.js +0 -1
- package/dist/src/commands/furnace/create.js +0 -1
- package/dist/src/commands/furnace/deploy.js +7 -83
- package/dist/src/commands/furnace/diff.js +0 -1
- package/dist/src/commands/furnace/index.js +59 -25
- package/dist/src/commands/furnace/init.js +0 -1
- package/dist/src/commands/furnace/list.js +0 -1
- package/dist/src/commands/furnace/override.js +0 -1
- package/dist/src/commands/furnace/preview.js +3 -3
- package/dist/src/commands/furnace/refresh.js +0 -1
- package/dist/src/commands/furnace/remove-state.d.ts +15 -0
- package/dist/src/commands/furnace/remove-state.js +47 -1
- package/dist/src/commands/furnace/remove.js +3 -9
- package/dist/src/commands/furnace/rename-browser-test.js +1 -5
- package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
- package/dist/src/commands/furnace/rename-helpers.js +30 -1
- package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
- package/dist/src/commands/furnace/rename.js +15 -36
- package/dist/src/commands/furnace/scan.js +0 -1
- package/dist/src/commands/furnace/status.js +0 -1
- package/dist/src/commands/furnace/sync.js +76 -4
- package/dist/src/commands/furnace/validate.js +0 -1
- package/dist/src/commands/furnace/validation-output.js +0 -1
- package/dist/src/commands/import.js +35 -9
- package/dist/src/commands/lint-per-patch.js +19 -7
- package/dist/src/commands/lint.js +13 -3
- package/dist/src/commands/manifest.js +1 -2
- package/dist/src/commands/package.js +2 -1
- package/dist/src/commands/patch/compact.js +1 -1
- package/dist/src/commands/patch/delete.js +1 -1
- package/dist/src/commands/patch/index.js +1 -1
- package/dist/src/commands/patch/lint-ignore.js +21 -6
- package/dist/src/commands/patch/move-files.js +1 -1
- package/dist/src/commands/patch/patch-context.js +0 -1
- package/dist/src/commands/patch/rename.js +4 -4
- package/dist/src/commands/patch/reorder.js +3 -4
- package/dist/src/commands/patch/split-plan.js +2 -1
- package/dist/src/commands/patch/split.js +4 -4
- package/dist/src/commands/patch/staged-dependency.js +1 -1
- package/dist/src/commands/patch/tier.js +18 -5
- package/dist/src/commands/re-export-bulk-scan.js +0 -1
- package/dist/src/commands/re-export-files.js +1 -1
- package/dist/src/commands/re-export-options.js +0 -1
- package/dist/src/commands/re-export-scan.js +0 -1
- package/dist/src/commands/re-export.js +6 -4
- package/dist/src/commands/rebase/abort.js +2 -9
- package/dist/src/commands/rebase/confirm.js +0 -1
- package/dist/src/commands/rebase/conflict-summary.js +0 -1
- package/dist/src/commands/rebase/continue.js +0 -1
- package/dist/src/commands/rebase/index.d.ts +1 -1
- package/dist/src/commands/rebase/index.js +10 -15
- package/dist/src/commands/rebase/patch-loop.js +38 -7
- package/dist/src/commands/rebase/summary.js +2 -3
- package/dist/src/commands/rebase.js +0 -1
- package/dist/src/commands/register.js +1 -1
- package/dist/src/commands/reset.js +16 -9
- package/dist/src/commands/resolve.js +2 -2
- package/dist/src/commands/run.js +4 -4
- package/dist/src/commands/setup-support.js +0 -1
- package/dist/src/commands/setup.js +1 -2
- package/dist/src/commands/source.js +1 -2
- package/dist/src/commands/status-output.js +1 -12
- package/dist/src/commands/status.js +147 -133
- package/dist/src/commands/test-appdir.js +0 -1
- package/dist/src/commands/test-diagnose.js +18 -2
- package/dist/src/commands/test-modes.d.ts +15 -0
- package/dist/src/commands/test-modes.js +61 -0
- package/dist/src/commands/test-register.d.ts +4 -0
- package/dist/src/commands/test-register.js +64 -0
- package/dist/src/commands/test-run.d.ts +23 -7
- package/dist/src/commands/test-run.js +15 -14
- package/dist/src/commands/test.d.ts +0 -4
- package/dist/src/commands/test.js +85 -86
- package/dist/src/commands/token-coverage.js +0 -1
- package/dist/src/commands/token.js +1 -2
- package/dist/src/commands/typecheck.js +1 -1
- package/dist/src/commands/verify.js +3 -9
- package/dist/src/commands/watch.js +5 -1
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.js +0 -1
- package/dist/src/core/brand-validation.js +0 -1
- package/dist/src/core/branding.js +0 -1
- package/dist/src/core/browser-wire.js +0 -1
- package/dist/src/core/build-audit-platform.js +0 -1
- package/dist/src/core/build-audit-registration.js +0 -1
- package/dist/src/core/build-audit-resolve.js +0 -1
- package/dist/src/core/build-audit-transforms.js +0 -1
- package/dist/src/core/build-audit.js +0 -1
- package/dist/src/core/build-baseline-types.js +0 -1
- package/dist/src/core/build-baseline.js +0 -1
- package/dist/src/core/build-prepare.js +2 -2
- package/dist/src/core/config-mutate.js +0 -1
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +6 -1
- package/dist/src/core/config-state.js +0 -1
- package/dist/src/core/config-validate-patch-policy.js +0 -1
- package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
- package/dist/src/core/config-validate-test-toolchains.js +105 -0
- package/dist/src/core/config-validate.js +3 -1
- package/dist/src/core/config.js +0 -1
- package/dist/src/core/destructive.js +0 -1
- package/dist/src/core/diff-hunks.js +13 -7
- package/dist/src/core/engine-changes.js +0 -1
- package/dist/src/core/engine-session-lock.d.ts +12 -0
- package/dist/src/core/engine-session-lock.js +45 -0
- package/dist/src/core/file-lock.d.ts +8 -0
- package/dist/src/core/file-lock.js +157 -48
- package/dist/src/core/firefox-archive.d.ts +4 -0
- package/dist/src/core/firefox-archive.js +6 -1
- package/dist/src/core/firefox-cache.js +60 -4
- package/dist/src/core/firefox-download.js +0 -1
- package/dist/src/core/firefox-extract.js +75 -16
- package/dist/src/core/firefox-ignorefile.js +0 -1
- package/dist/src/core/firefox.d.ts +12 -0
- package/dist/src/core/firefox.js +64 -2
- package/dist/src/core/furnace-apply-dry-run.js +0 -1
- package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
- package/dist/src/core/furnace-apply-ftl.js +10 -5
- package/dist/src/core/furnace-apply-helpers.js +0 -1
- package/dist/src/core/furnace-apply-output.js +2 -2
- package/dist/src/core/furnace-apply.d.ts +7 -1
- package/dist/src/core/furnace-apply.js +47 -13
- package/dist/src/core/furnace-checksum-utils.js +0 -1
- package/dist/src/core/furnace-config-array-utils.js +0 -1
- package/dist/src/core/furnace-config-custom.js +0 -1
- package/dist/src/core/furnace-config-order.js +0 -1
- package/dist/src/core/furnace-config-tokens.js +0 -1
- package/dist/src/core/furnace-config-validate.js +0 -1
- package/dist/src/core/furnace-config.d.ts +11 -0
- package/dist/src/core/furnace-config.js +37 -1
- package/dist/src/core/furnace-constants.js +0 -1
- package/dist/src/core/furnace-css-fragments.js +0 -1
- package/dist/src/core/furnace-graph-utils.js +0 -1
- package/dist/src/core/furnace-jsconfig.d.ts +2 -3
- package/dist/src/core/furnace-jsconfig.js +25 -18
- package/dist/src/core/furnace-marker.js +0 -1
- package/dist/src/core/furnace-operation.js +0 -1
- package/dist/src/core/furnace-refresh.js +0 -1
- package/dist/src/core/furnace-registration-ast.d.ts +14 -0
- package/dist/src/core/furnace-registration-ast.js +1 -2
- package/dist/src/core/furnace-registration-remove.js +0 -1
- package/dist/src/core/furnace-registration-validate.js +1 -5
- package/dist/src/core/furnace-registration.js +0 -1
- package/dist/src/core/furnace-rollback.js +0 -1
- package/dist/src/core/furnace-scanner.js +0 -1
- package/dist/src/core/furnace-staleness.js +0 -1
- package/dist/src/core/furnace-state-persist.d.ts +72 -0
- package/dist/src/core/furnace-state-persist.js +118 -0
- package/dist/src/core/furnace-step-errors.d.ts +23 -0
- package/dist/src/core/furnace-step-errors.js +25 -0
- package/dist/src/core/furnace-stories.js +0 -1
- package/dist/src/core/furnace-validate-accessibility.js +0 -1
- package/dist/src/core/furnace-validate-checks.js +0 -1
- package/dist/src/core/furnace-validate-compatibility.js +0 -1
- package/dist/src/core/furnace-validate-helpers.js +0 -1
- package/dist/src/core/furnace-validate-registration.js +7 -7
- package/dist/src/core/furnace-validate-structure.d.ts +8 -0
- package/dist/src/core/furnace-validate-structure.js +11 -2
- package/dist/src/core/furnace-validate.js +0 -1
- package/dist/src/core/furnace-version-drift.d.ts +6 -3
- package/dist/src/core/furnace-version-drift.js +6 -4
- package/dist/src/core/git-base.js +0 -1
- package/dist/src/core/git-diff.js +0 -1
- package/dist/src/core/git-file-ops.js +0 -1
- package/dist/src/core/git-status.d.ts +13 -0
- package/dist/src/core/git-status.js +32 -2
- package/dist/src/core/git.d.ts +10 -1
- package/dist/src/core/git.js +63 -6
- package/dist/src/core/license-headers.js +0 -1
- package/dist/src/core/lint-cache.js +0 -1
- package/dist/src/core/lint-projection.js +0 -1
- package/dist/src/core/mach-build-artifacts.js +0 -1
- package/dist/src/core/mach-error-hints.js +0 -1
- package/dist/src/core/mach-mozconfig.js +0 -1
- package/dist/src/core/mach-python.js +0 -1
- package/dist/src/core/mach-resource-shim.js +0 -1
- package/dist/src/core/mach.js +0 -1
- package/dist/src/core/manifest-helpers.js +0 -1
- package/dist/src/core/manifest-register.js +0 -1
- package/dist/src/core/manifest-rules.js +0 -1
- package/dist/src/core/manifest-tokenizers.js +0 -1
- package/dist/src/core/marionette-port.d.ts +8 -0
- package/dist/src/core/marionette-port.js +34 -3
- package/dist/src/core/marionette-preflight.js +0 -1
- package/dist/src/core/markdown-table.js +0 -1
- package/dist/src/core/ownership-table.js +2 -9
- package/dist/src/core/parser-fallback.js +0 -1
- package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
- package/dist/src/core/patch-apply-fuzz.js +71 -34
- package/dist/src/core/patch-apply.d.ts +30 -0
- package/dist/src/core/patch-apply.js +101 -18
- package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
- package/dist/src/core/patch-artifact-normalize.js +19 -15
- package/dist/src/core/patch-export-coverage.js +0 -1
- package/dist/src/core/patch-export-metadata.js +0 -1
- package/dist/src/core/patch-export-update.js +2 -3
- package/dist/src/core/patch-export.d.ts +2 -6
- package/dist/src/core/patch-export.js +24 -48
- package/dist/src/core/patch-files.js +0 -1
- package/dist/src/core/patch-identifier-suggest.js +0 -1
- package/dist/src/core/patch-lint-checkjs.js +0 -1
- package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
- package/dist/src/core/patch-lint-cross.js +0 -1
- package/dist/src/core/patch-lint-css.js +0 -1
- package/dist/src/core/patch-lint-diff-tag.js +0 -1
- package/dist/src/core/patch-lint-diff.d.ts +2 -1
- package/dist/src/core/patch-lint-diff.js +17 -32
- package/dist/src/core/patch-lint-imports.js +0 -1
- package/dist/src/core/patch-lint-jsdoc.js +0 -1
- package/dist/src/core/patch-lint-observer.js +0 -1
- package/dist/src/core/patch-lint-ownership.js +0 -1
- package/dist/src/core/patch-lint-reexports.js +0 -1
- package/dist/src/core/patch-lint.d.ts +37 -2
- package/dist/src/core/patch-lint.js +47 -34
- package/dist/src/core/patch-lock.js +0 -1
- package/dist/src/core/patch-manifest-consistency.js +9 -1
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +30 -5
- package/dist/src/core/patch-manifest-query.js +18 -10
- package/dist/src/core/patch-manifest-resolve.js +0 -1
- package/dist/src/core/patch-manifest-validate.js +0 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -2
- package/dist/src/core/patch-parse.d.ts +59 -0
- package/dist/src/core/patch-parse.js +209 -92
- package/dist/src/core/patch-policy.js +0 -1
- package/dist/src/core/patch-registration-refs.js +16 -26
- package/dist/src/core/patch-source-metadata.js +0 -1
- package/dist/src/core/patch-transform.d.ts +4 -0
- package/dist/src/core/patch-transform.js +30 -38
- package/dist/src/core/rebase-session.js +0 -1
- package/dist/src/core/register-browser-content.js +0 -1
- package/dist/src/core/register-module.js +0 -1
- package/dist/src/core/register-result.js +0 -1
- package/dist/src/core/register-scaffold.js +0 -1
- package/dist/src/core/register-shared-css.js +0 -1
- package/dist/src/core/register-test-manifest.js +0 -1
- package/dist/src/core/register-xpcshell-test.js +0 -1
- package/dist/src/core/shared-ftl.js +0 -1
- package/dist/src/core/signal-critical.js +0 -1
- package/dist/src/core/smoke-patterns.js +0 -1
- package/dist/src/core/state-file.js +0 -1
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +24 -16
- package/dist/src/core/test-harness-crash.d.ts +64 -7
- package/dist/src/core/test-harness-crash.js +196 -15
- package/dist/src/core/test-harness-output.js +0 -1
- package/dist/src/core/test-path-scope.d.ts +21 -13
- package/dist/src/core/test-path-scope.js +43 -28
- package/dist/src/core/test-stale-check.js +0 -1
- package/dist/src/core/test-stale-symlink.js +0 -1
- package/dist/src/core/test-xpcshell-retry.js +0 -1
- package/dist/src/core/token-coverage.js +0 -1
- package/dist/src/core/token-dark-mode.js +0 -1
- package/dist/src/core/token-docs.js +0 -1
- package/dist/src/core/token-manager.js +0 -1
- package/dist/src/core/token-scaffold.js +0 -1
- package/dist/src/core/token-variant.js +0 -1
- package/dist/src/core/toolchain-preflight.d.ts +31 -10
- package/dist/src/core/toolchain-preflight.js +97 -36
- package/dist/src/core/typecheck-shim.js +13 -1
- package/dist/src/core/typecheck.js +0 -1
- package/dist/src/core/wire-destroy.js +0 -1
- package/dist/src/core/wire-dom-fragment.js +0 -1
- package/dist/src/core/wire-init.js +0 -1
- package/dist/src/core/wire-subscript.js +0 -1
- package/dist/src/core/wire-targets.js +0 -1
- package/dist/src/core/wire-utils.js +0 -1
- package/dist/src/core/xpcshell-appdir.js +1 -4
- package/dist/src/errors/base.d.ts +29 -0
- package/dist/src/errors/base.js +35 -1
- package/dist/src/errors/build.js +0 -1
- package/dist/src/errors/codes.d.ts +6 -0
- package/dist/src/errors/codes.js +6 -1
- package/dist/src/errors/config.js +0 -1
- package/dist/src/errors/download.js +0 -1
- package/dist/src/errors/furnace.d.ts +11 -1
- package/dist/src/errors/furnace.js +6 -5
- package/dist/src/errors/git.js +0 -1
- package/dist/src/errors/patch.d.ts +17 -0
- package/dist/src/errors/patch.js +29 -1
- package/dist/src/errors/rebase.js +0 -1
- package/dist/src/errors/run.js +1 -1
- package/dist/src/index.js +0 -1
- package/dist/src/types/cli.js +0 -1
- package/dist/src/types/commands/index.js +0 -1
- package/dist/src/types/commands/options.d.ts +13 -5
- package/dist/src/types/commands/options.js +0 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/commands/patches.js +0 -1
- package/dist/src/types/commands/project.d.ts +11 -2
- package/dist/src/types/commands/project.js +0 -1
- package/dist/src/types/config.d.ts +32 -0
- package/dist/src/types/config.js +0 -1
- package/dist/src/types/furnace.d.ts +10 -0
- package/dist/src/types/furnace.js +0 -1
- package/dist/src/types/index.js +0 -1
- package/dist/src/types/typecheck.js +0 -1
- package/dist/src/utils/elapsed.js +0 -1
- package/dist/src/utils/errors.js +0 -1
- package/dist/src/utils/fs.js +37 -5
- package/dist/src/utils/logger.d.ts +7 -0
- package/dist/src/utils/logger.js +85 -3
- package/dist/src/utils/options.d.ts +15 -0
- package/dist/src/utils/options.js +25 -1
- package/dist/src/utils/package-root.js +11 -4
- package/dist/src/utils/parse.js +0 -1
- package/dist/src/utils/paths.js +0 -1
- package/dist/src/utils/platform.js +0 -1
- package/dist/src/utils/process.d.ts +33 -4
- package/dist/src/utils/process.js +183 -28
- package/dist/src/utils/regex.d.ts +13 -3
- package/dist/src/utils/regex.js +53 -4
- package/dist/src/utils/validation.d.ts +0 -12
- package/dist/src/utils/validation.js +13 -26
- package/package.json +8 -7
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { ConfigError } from '../errors/config.js';
|
|
3
|
+
import { parseObject } from '../utils/parse.js';
|
|
4
|
+
import { isContainedRelativePath } from '../utils/paths.js';
|
|
5
|
+
function optionalConfigString(rec, key, label) {
|
|
6
|
+
const value = rec.raw(key);
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (typeof value !== 'string') {
|
|
10
|
+
throw new ConfigError(`Config field "${label}" must be a string`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function optionalConfigObject(rec, key) {
|
|
15
|
+
const value = rec.raw(key);
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
try {
|
|
19
|
+
return rec.object(key);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new ConfigError(`Config field "${key}" must be an object`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Parses the optional `test` block. */
|
|
26
|
+
export function parseTestBlock(rec, config) {
|
|
27
|
+
const testRec = optionalConfigObject(rec, 'test');
|
|
28
|
+
if (!testRec)
|
|
29
|
+
return;
|
|
30
|
+
const out = {};
|
|
31
|
+
const canaryPath = optionalConfigString(testRec, 'canaryPath', 'test.canaryPath');
|
|
32
|
+
if (canaryPath !== undefined) {
|
|
33
|
+
if (canaryPath.trim() === '' || !isContainedRelativePath(canaryPath)) {
|
|
34
|
+
throw new ConfigError('Config field "test.canaryPath" must be a project-relative path');
|
|
35
|
+
}
|
|
36
|
+
out.canaryPath = canaryPath;
|
|
37
|
+
}
|
|
38
|
+
const canaryTimeout = testRec.raw('canaryTimeoutSeconds');
|
|
39
|
+
if (canaryTimeout !== undefined) {
|
|
40
|
+
if (typeof canaryTimeout !== 'number' ||
|
|
41
|
+
!Number.isInteger(canaryTimeout) ||
|
|
42
|
+
canaryTimeout < 1 ||
|
|
43
|
+
canaryTimeout > 600) {
|
|
44
|
+
throw new ConfigError('Config field "test.canaryTimeoutSeconds" must be an integer in 1..600');
|
|
45
|
+
}
|
|
46
|
+
out.canaryTimeoutSeconds = canaryTimeout;
|
|
47
|
+
}
|
|
48
|
+
config.test = out;
|
|
49
|
+
}
|
|
50
|
+
/** Parses optional project-specific external toolchain doctor probes. */
|
|
51
|
+
export function parseExternalToolchainsBlock(rec, config) {
|
|
52
|
+
const raw = rec.raw('externalToolchains');
|
|
53
|
+
if (raw === undefined)
|
|
54
|
+
return;
|
|
55
|
+
if (!Array.isArray(raw)) {
|
|
56
|
+
throw new ConfigError('Config field "externalToolchains" must be an array');
|
|
57
|
+
}
|
|
58
|
+
config.externalToolchains = raw.map((entry, index) => {
|
|
59
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
60
|
+
throw new ConfigError(`Config field "externalToolchains[${String(index)}]" must be an object`);
|
|
61
|
+
}
|
|
62
|
+
const item = entry;
|
|
63
|
+
const name = item['name'];
|
|
64
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
65
|
+
throw new ConfigError(`Config field "externalToolchains[${String(index)}].name" must be a non-empty string`);
|
|
66
|
+
}
|
|
67
|
+
const tools = item['tools'];
|
|
68
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
69
|
+
throw new ConfigError(`Config field "externalToolchains[${String(index)}].tools" must be a non-empty array`);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
tools: tools.map((tool, toolIndex) => parseExternalToolRequirement(tool, index, toolIndex)),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function parseExternalToolRequirement(tool, index, toolIndex) {
|
|
78
|
+
const label = `externalToolchains[${String(index)}].tools[${String(toolIndex)}]`;
|
|
79
|
+
if (typeof tool !== 'object' || tool === null || Array.isArray(tool)) {
|
|
80
|
+
throw new ConfigError(`Config field "${label}" must be an object`);
|
|
81
|
+
}
|
|
82
|
+
const toolRec = tool;
|
|
83
|
+
const toolName = toolRec['name'];
|
|
84
|
+
if (typeof toolName !== 'string' || toolName.trim() === '') {
|
|
85
|
+
throw new ConfigError(`Config field "${label}.name" must be a non-empty string`);
|
|
86
|
+
}
|
|
87
|
+
const toolPath = toolRec['path'];
|
|
88
|
+
if (toolPath !== undefined && (typeof toolPath !== 'string' || toolPath.trim() === '')) {
|
|
89
|
+
throw new ConfigError(`Config field "${label}.path" must be a non-empty string`);
|
|
90
|
+
}
|
|
91
|
+
const xcrun = toolRec['xcrun'];
|
|
92
|
+
if (xcrun !== undefined && typeof xcrun !== 'boolean') {
|
|
93
|
+
throw new ConfigError(`Config field "${label}.xcrun" must be a boolean`);
|
|
94
|
+
}
|
|
95
|
+
const required = toolRec['required'];
|
|
96
|
+
if (required !== undefined && typeof required !== 'boolean') {
|
|
97
|
+
throw new ConfigError(`Config field "${label}.required" must be a boolean`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
name: toolName,
|
|
101
|
+
...(toolPath !== undefined ? { path: toolPath } : {}),
|
|
102
|
+
...(xcrun !== undefined ? { xcrun } : {}),
|
|
103
|
+
...(required !== undefined ? { required } : {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -9,6 +9,7 @@ import { isContainedRelativePath, isExplicitAbsolutePath } from '../utils/paths.
|
|
|
9
9
|
import { isValidAppId, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, validateFirefoxProductVersionCompatibility, } from '../utils/validation.js';
|
|
10
10
|
import { SUPPORTED_CONFIG_ROOT_KEYS } from './config-paths.js';
|
|
11
11
|
import { parsePatchPolicyBlock } from './config-validate-patch-policy.js';
|
|
12
|
+
import { parseExternalToolchainsBlock, parseTestBlock } from './config-validate-test-toolchains.js';
|
|
12
13
|
/**
|
|
13
14
|
* Parses and validates the four required identity fields (`name`,
|
|
14
15
|
* `vendor`, `appId`, `binaryName`): all non-empty strings, with
|
|
@@ -145,6 +146,8 @@ export function validateConfig(data) {
|
|
|
145
146
|
const firefox = parseFirefoxBlock(rec);
|
|
146
147
|
const config = { ...identity, firefox };
|
|
147
148
|
parseBuildBlock(rec, config);
|
|
149
|
+
parseTestBlock(rec, config);
|
|
150
|
+
parseExternalToolchainsBlock(rec, config);
|
|
148
151
|
parseWireBlock(rec, config);
|
|
149
152
|
parseLicenseField(rec, config);
|
|
150
153
|
// Marker comment — appended to lines FireForge writes into upstream files.
|
|
@@ -429,4 +432,3 @@ function parseTypecheckProjectOverrides(raw, projects) {
|
|
|
429
432
|
}
|
|
430
433
|
return out;
|
|
431
434
|
}
|
|
432
|
-
//# sourceMappingURL=config-validate.js.map
|
package/dist/src/core/config.js
CHANGED
|
@@ -194,7 +194,6 @@ function coalescedHunk(oldLines, newLines, context) {
|
|
|
194
194
|
return [];
|
|
195
195
|
}
|
|
196
196
|
const contextStart = Math.max(0, firstDiff - context);
|
|
197
|
-
const contextEndOld = Math.min(oldLines.length - 1, lastOldDiff + context);
|
|
198
197
|
const contextEndNew = Math.min(newLines.length - 1, lastNewDiff + context);
|
|
199
198
|
const hunkLines = [];
|
|
200
199
|
for (let k = contextStart; k < firstDiff; k++) {
|
|
@@ -206,13 +205,21 @@ function coalescedHunk(oldLines, newLines, context) {
|
|
|
206
205
|
for (let k = firstDiff; k <= lastNewDiff; k++) {
|
|
207
206
|
hunkLines.push({ marker: '+', content: newLines[k] });
|
|
208
207
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
// Trailing context comes from the common suffix, which lives at
|
|
209
|
+
// DIFFERENT indices on each side (lastOldDiff+1… vs lastNewDiff+1…).
|
|
210
|
+
// The previous mixed-coordinate loop (max() of both sides indexing
|
|
211
|
+
// newLines) emitted wrong context on asymmetric edits, and the trailing
|
|
212
|
+
// lines were excluded from the @@ header lengths entirely — so on
|
|
213
|
+
// >LCS_LINE_LIMIT files the rendered header disagreed with the body.
|
|
214
|
+
for (let k = lastNewDiff + 1; k <= contextEndNew; k++) {
|
|
212
215
|
hunkLines.push({ marker: ' ', content: newLines[k] });
|
|
213
216
|
}
|
|
214
|
-
const
|
|
215
|
-
|
|
217
|
+
const leadingContext = firstDiff - contextStart;
|
|
218
|
+
// Common-suffix symmetry: oldLen-1-lastOldDiff === newLen-1-lastNewDiff,
|
|
219
|
+
// so both sides clamp to the same trailing context count.
|
|
220
|
+
const trailingContext = contextEndNew - lastNewDiff;
|
|
221
|
+
const oldLength = lastOldDiff - firstDiff + 1 + leadingContext + trailingContext;
|
|
222
|
+
const newLength = lastNewDiff - firstDiff + 1 + leadingContext + trailingContext;
|
|
216
223
|
return [
|
|
217
224
|
{
|
|
218
225
|
oldStart: contextStart + 1,
|
|
@@ -265,4 +272,3 @@ export function renderHunks(hunks) {
|
|
|
265
272
|
}
|
|
266
273
|
return out;
|
|
267
274
|
}
|
|
268
|
-
//# sourceMappingURL=diff-hunks.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
*/
|
|
4
|
+
export declare function withEngineSessionLock<T>(projectRoot: string, command: string, operation: () => Promise<T>): Promise<T>;
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
export declare function snapshotEngineGeneration(engineDir: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertEngineGenerationUnchanged(engineDir: string, before: string): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { GeneralError } from '../errors/base.js';
|
|
4
|
+
import { withFileLock } from './file-lock.js';
|
|
5
|
+
import { git } from './git-base.js';
|
|
6
|
+
const ENGINE_SESSION_LOCK_PATH = join('.fireforge', 'engine-session.lock');
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
export async function withEngineSessionLock(projectRoot, command, operation) {
|
|
11
|
+
if (process.env['FIREFORGE_ENABLE_ENGINE_SESSION_LOCK_IN_TEST'] !== '1' &&
|
|
12
|
+
(process.env['NODE_ENV'] === 'test' || process.env['VITEST'] !== undefined)) {
|
|
13
|
+
return operation();
|
|
14
|
+
}
|
|
15
|
+
return withFileLock(join(projectRoot, ENGINE_SESSION_LOCK_PATH), operation, {
|
|
16
|
+
timeoutMs: 1000,
|
|
17
|
+
ownerMetadata: [`command=${command}`, `started=${new Date().toISOString()}`],
|
|
18
|
+
onTimeoutMessage: `Another FireForge engine-mutating command is already running. ` +
|
|
19
|
+
`Wait for it to finish, then retry \`${command}\`.`,
|
|
20
|
+
onStaleLockMessage: (ageMs) => `Removed stale FireForge engine session lock (${Math.round(ageMs / 1000)}s old).`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
*/
|
|
26
|
+
export async function snapshotEngineGeneration(engineDir) {
|
|
27
|
+
try {
|
|
28
|
+
const head = (await git(['rev-parse', 'HEAD'], engineDir)).trim();
|
|
29
|
+
const status = await git(['status', '--porcelain=v1', '-z'], engineDir);
|
|
30
|
+
return `${head}\0${status}`;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
return `unavailable:${error instanceof Error ? error.message : String(error)}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
*/
|
|
39
|
+
export async function assertEngineGenerationUnchanged(engineDir, before) {
|
|
40
|
+
const after = await snapshotEngineGeneration(engineDir);
|
|
41
|
+
if (after === before)
|
|
42
|
+
return;
|
|
43
|
+
throw new GeneralError('engine/ changed while `fireforge test` was running, so this test verdict is invalid/inconclusive.\n\n' +
|
|
44
|
+
'Use one writer per engine checkout, then rebuild and rerun the affected tests.');
|
|
45
|
+
}
|
|
@@ -2,8 +2,16 @@ export interface FileLockOptions {
|
|
|
2
2
|
timeoutMs?: number;
|
|
3
3
|
pollMs?: number;
|
|
4
4
|
staleMs?: number;
|
|
5
|
+
/**
|
|
6
|
+
* Interval between stale-lock probes while waiting (defaults to
|
|
7
|
+
* {@link STALE_REPROBE_INTERVAL_MS}). Exists mainly so tests can exercise
|
|
8
|
+
* the mid-wait reaping path without multi-second sleeps.
|
|
9
|
+
*/
|
|
10
|
+
staleReprobeMs?: number;
|
|
5
11
|
onTimeoutMessage?: string;
|
|
6
12
|
onStaleLockMessage?: (ageMs: number) => string | undefined;
|
|
13
|
+
/** Extra owner-file lines for human diagnostics; ignored by lock mechanics. */
|
|
14
|
+
ownerMetadata?: readonly string[];
|
|
7
15
|
}
|
|
8
16
|
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
9
17
|
export declare function createSiblingLockPath(filePath: string, suffix?: string): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
4
5
|
import { toError } from '../utils/errors.js';
|
|
5
6
|
import { ensureDir } from '../utils/fs.js';
|
|
@@ -7,6 +8,15 @@ import { verbose, warn } from '../utils/logger.js';
|
|
|
7
8
|
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
8
9
|
const DEFAULT_LOCK_POLL_MS = 50;
|
|
9
10
|
const DEFAULT_STALE_LOCK_MS = 5 * 60_000;
|
|
11
|
+
/**
|
|
12
|
+
* Minimum interval between stale-lock probes while a waiter polls. The probe
|
|
13
|
+
* used to run exactly once per waiter: if the holder died *after* that single
|
|
14
|
+
* probe, the waiter polled a permanently dead lock until `timeoutMs` — for
|
|
15
|
+
* the build lock that meant up to 24 hours (see `withBuildLock` in mach.ts).
|
|
16
|
+
* Re-probing on a small interval bounds that hang to seconds while keeping
|
|
17
|
+
* the per-poll cost negligible (one stat + one small read every ~5 s).
|
|
18
|
+
*/
|
|
19
|
+
const STALE_REPROBE_INTERVAL_MS = 5_000;
|
|
10
20
|
function sleep(ms) {
|
|
11
21
|
return new Promise((resolve) => {
|
|
12
22
|
setTimeout(resolve, ms);
|
|
@@ -39,6 +49,100 @@ function isProcessAlive(pid) {
|
|
|
39
49
|
return getNodeErrorCode(error) !== 'ESRCH';
|
|
40
50
|
}
|
|
41
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Reads the owner PID (line 1) and acquisition token (line 2) from a lock
|
|
54
|
+
* directory's PID file. Returns `{ present: false }` when the PID file is
|
|
55
|
+
* missing or the PID does not parse as a finite integer (caller falls back
|
|
56
|
+
* to the age-only staleness heuristic).
|
|
57
|
+
*
|
|
58
|
+
* File format note: external readers (`doctor-furnace.ts`, older FireForge
|
|
59
|
+
* releases) do `parseInt(content.trim(), 10)`, which parses the leading
|
|
60
|
+
* digits of a multi-line file — so adding the token line stays compatible.
|
|
61
|
+
*/
|
|
62
|
+
async function readLockOwner(lockPath) {
|
|
63
|
+
try {
|
|
64
|
+
const pidContent = await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8');
|
|
65
|
+
const [pidLine, tokenLine] = pidContent.split('\n');
|
|
66
|
+
const pid = parseInt((pidLine ?? '').trim(), 10);
|
|
67
|
+
if (Number.isFinite(pid)) {
|
|
68
|
+
const token = tokenLine?.trim();
|
|
69
|
+
if (token !== undefined && token.length > 0) {
|
|
70
|
+
return { present: true, pid, token };
|
|
71
|
+
}
|
|
72
|
+
return { present: true, pid };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// PID file missing or unreadable — treat as absent.
|
|
77
|
+
}
|
|
78
|
+
return { present: false };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Reaps a lock directory believed to be stale, safely against concurrent
|
|
82
|
+
* reapers and a concurrent legitimate re-acquisition.
|
|
83
|
+
*
|
|
84
|
+
* The naive stat → read-PID → `rm` sequence had a TOCTOU hole: two waiters
|
|
85
|
+
* could both observe the dead owner, one would remove the stale dir and
|
|
86
|
+
* `mkdir` its own lock, and the other's already-decided `rm` then deleted
|
|
87
|
+
* the *fresh* lock — two processes inside the critical section at once.
|
|
88
|
+
*
|
|
89
|
+
* Rename-aside closes it: `rename()` is atomic, so exactly one reaper wins
|
|
90
|
+
* ownership of the directory (the loser gets ENOENT and simply re-polls),
|
|
91
|
+
* and nothing ever deletes the live path directly. After winning the rename
|
|
92
|
+
* we re-read the owner from the renamed dir — if it is now a *live* process
|
|
93
|
+
* (the lock was released and re-acquired between our staleness read and the
|
|
94
|
+
* rename), we rename it back instead of reaping.
|
|
95
|
+
*
|
|
96
|
+
* @returns true when the caller should immediately retry `mkdir` (the lock
|
|
97
|
+
* path is now free or was freed by someone else), false when the lock is
|
|
98
|
+
* legitimately held and the caller should keep waiting.
|
|
99
|
+
*/
|
|
100
|
+
async function reapStaleLock(lockPath, ageMs, onStaleLockMessage) {
|
|
101
|
+
const reapPath = `${lockPath}.reaping-${String(process.pid)}-${randomUUID()}`;
|
|
102
|
+
try {
|
|
103
|
+
await rename(lockPath, reapPath);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (getNodeErrorCode(error) === 'ENOENT') {
|
|
107
|
+
// Another reaper won, or the owner released — either way, retry mkdir.
|
|
108
|
+
verbose(`Stale lock disappeared before reaping completed: ${lockPath}`);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
verbose(`Could not rename stale lock aside (${lockPath}): ${toError(error).message}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// We exclusively own reapPath now. Re-verify: the owner may have changed
|
|
115
|
+
// between the staleness probe and the rename (release + fresh acquire).
|
|
116
|
+
const owner = await readLockOwner(reapPath);
|
|
117
|
+
if (owner.present && isProcessAlive(owner.pid)) {
|
|
118
|
+
try {
|
|
119
|
+
await rename(reapPath, lockPath);
|
|
120
|
+
verbose(`Aborted stale-lock reap of ${lockPath}: owner PID ${String(owner.pid)} is alive (lock was re-acquired mid-probe)`);
|
|
121
|
+
}
|
|
122
|
+
catch (restoreError) {
|
|
123
|
+
// Rename-back can only fail if a third process mkdir'd the lock path
|
|
124
|
+
// in this instant. The displaced live owner keeps running without its
|
|
125
|
+
// lock — surface it loudly instead of hiding the lost exclusion.
|
|
126
|
+
warn(`Could not restore live lock for PID ${String(owner.pid)} at ${lockPath} ` +
|
|
127
|
+
`(${toError(restoreError).message}). Displaced lock kept at ${reapPath} for inspection.`);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const staleMessage = onStaleLockMessage?.(ageMs);
|
|
132
|
+
if (staleMessage) {
|
|
133
|
+
warn(staleMessage);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
verbose(`Removing stale lock ${lockPath} (age: ${Math.round(ageMs / 1000)}s)`);
|
|
137
|
+
}
|
|
138
|
+
await rm(reapPath, { recursive: true, force: true });
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Probes a contended lock for staleness and reaps it when the owner is gone.
|
|
143
|
+
*
|
|
144
|
+
* @returns true when the caller should immediately retry `mkdir`.
|
|
145
|
+
*/
|
|
42
146
|
async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
43
147
|
try {
|
|
44
148
|
const lockStat = await stat(lockPath);
|
|
@@ -48,27 +152,19 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
48
152
|
// 5 minutes) was never removed even when its PID file pointed at a
|
|
49
153
|
// process that had already exited. That's the exact situation an
|
|
50
154
|
// operator lands in after SIGINT'ing `furnace preview` — the signal
|
|
51
|
-
// handler calls `process.exit`, `withFileLock`'s
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
if (!isProcessAlive(
|
|
58
|
-
|
|
59
|
-
if (staleMessage) {
|
|
60
|
-
warn(staleMessage);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
verbose(`Lock at ${lockPath} owner PID ${pidCheck.pid} is no longer running — removing (age: ${Math.round(ageMs / 1000)}s)`);
|
|
64
|
-
}
|
|
65
|
-
await rm(lockPath, { recursive: true, force: true });
|
|
66
|
-
return true;
|
|
155
|
+
// handler calls `process.exit`, `withFileLock`'s release never runs,
|
|
156
|
+
// and the next command has to either wait 5 minutes for the staleness
|
|
157
|
+
// gate or manually remove the lock directory. With the PID-first check,
|
|
158
|
+
// an explicitly-dead owner unblocks immediately.
|
|
159
|
+
const owner = await readLockOwner(lockPath);
|
|
160
|
+
if (owner.present) {
|
|
161
|
+
if (!isProcessAlive(owner.pid)) {
|
|
162
|
+
return await reapStaleLock(lockPath, ageMs, onStaleLockMessage);
|
|
67
163
|
}
|
|
68
164
|
// PID is alive — respect it regardless of age. A slow `mach build`
|
|
69
165
|
// legitimately holds the lock past the stale threshold and we don't
|
|
70
166
|
// want to race-remove it.
|
|
71
|
-
verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${
|
|
167
|
+
verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${String(owner.pid)} is still running — not removing`);
|
|
72
168
|
return false;
|
|
73
169
|
}
|
|
74
170
|
// No readable PID file. Fall back to the age-only heuristic so locks
|
|
@@ -77,12 +173,7 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
77
173
|
if (ageMs <= staleMs) {
|
|
78
174
|
return false;
|
|
79
175
|
}
|
|
80
|
-
|
|
81
|
-
if (staleMessage) {
|
|
82
|
-
warn(staleMessage);
|
|
83
|
-
}
|
|
84
|
-
await rm(lockPath, { recursive: true, force: true });
|
|
85
|
-
return true;
|
|
176
|
+
return await reapStaleLock(lockPath, ageMs, onStaleLockMessage);
|
|
86
177
|
}
|
|
87
178
|
catch (error) {
|
|
88
179
|
const code = getNodeErrorCode(error);
|
|
@@ -95,30 +186,38 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
95
186
|
}
|
|
96
187
|
}
|
|
97
188
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
189
|
+
* Releases a lock previously acquired by this process, verifying ownership
|
|
190
|
+
* first. The unconditional `finally { rm }` this replaces would remove
|
|
191
|
+
* *whichever* lock directory was present — if a stale-lock reaper had (in a
|
|
192
|
+
* pathological race) replaced our lock, we deleted the new owner's lock and
|
|
193
|
+
* compounded the double-acquisition. When the PID file no longer names us,
|
|
194
|
+
* warn and leave the directory alone.
|
|
195
|
+
*
|
|
196
|
+
* `ownerFileWritten` is false when our own owner-file write failed at
|
|
197
|
+
* acquisition (non-fatal there); ownership then cannot be verified and we
|
|
198
|
+
* fall back to unconditional removal — identical to the historical behavior
|
|
199
|
+
* and strictly better than leaking the lock.
|
|
101
200
|
*/
|
|
102
|
-
async function
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
201
|
+
async function releaseLock(lockPath, ownerToken, ownerFileWritten) {
|
|
202
|
+
if (ownerFileWritten) {
|
|
203
|
+
const owner = await readLockOwner(lockPath);
|
|
204
|
+
if (owner.present &&
|
|
205
|
+
(owner.pid !== process.pid || (owner.token !== undefined && owner.token !== ownerToken))) {
|
|
206
|
+
warn(`Not removing lock ${lockPath}: it is now owned by PID ${String(owner.pid)} — ` +
|
|
207
|
+
'this process no longer holds it.');
|
|
208
|
+
return;
|
|
108
209
|
}
|
|
109
210
|
}
|
|
110
|
-
|
|
111
|
-
// PID file missing or unreadable — treat as absent.
|
|
112
|
-
}
|
|
113
|
-
return { present: false };
|
|
211
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
114
212
|
}
|
|
115
213
|
/** Runs an async operation while holding a directory lock, with stale-lock recovery. */
|
|
116
214
|
export async function withFileLock(lockPath, operation, options = {}) {
|
|
117
215
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
118
216
|
const pollMs = options.pollMs ?? DEFAULT_LOCK_POLL_MS;
|
|
119
217
|
const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
|
|
218
|
+
const staleReprobeMs = options.staleReprobeMs ?? STALE_REPROBE_INTERVAL_MS;
|
|
120
219
|
const deadline = Date.now() + timeoutMs;
|
|
121
|
-
let
|
|
220
|
+
let lastStaleProbeAt;
|
|
122
221
|
await ensureDir(dirname(lockPath));
|
|
123
222
|
for (;;) {
|
|
124
223
|
try {
|
|
@@ -134,32 +233,42 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
134
233
|
if (!isAlreadyLocked) {
|
|
135
234
|
throw error;
|
|
136
235
|
}
|
|
137
|
-
if (
|
|
138
|
-
|
|
236
|
+
if (lastStaleProbeAt === undefined || Date.now() - lastStaleProbeAt >= staleReprobeMs) {
|
|
237
|
+
lastStaleProbeAt = Date.now();
|
|
139
238
|
if (await removeIfStaleLock(lockPath, staleMs, options.onStaleLockMessage)) {
|
|
140
239
|
continue;
|
|
141
240
|
}
|
|
142
241
|
}
|
|
143
242
|
if (Date.now() >= deadline) {
|
|
144
|
-
|
|
145
|
-
|
|
243
|
+
const owner = await readLockOwner(lockPath);
|
|
244
|
+
const holderHint = owner.present && isProcessAlive(owner.pid)
|
|
245
|
+
? ` It is held by running process ${String(owner.pid)} — wait for it to finish, or stop it and retry.`
|
|
246
|
+
: ' If no other FireForge process is running, remove the lock directory and retry.';
|
|
247
|
+
throw new Error(options.onTimeoutMessage ?? `Timed out waiting for file lock ${lockPath}.${holderHint}`, { cause: error });
|
|
146
248
|
}
|
|
147
249
|
await sleep(pollMs);
|
|
148
250
|
}
|
|
149
251
|
}
|
|
150
|
-
//
|
|
151
|
-
//
|
|
252
|
+
// Stamp ownership (PID + per-acquisition token) into the lock directory so
|
|
253
|
+
// stale-lock recovery can check liveness and release can verify the lock
|
|
254
|
+
// is still ours before removing it.
|
|
255
|
+
const ownerToken = randomUUID();
|
|
256
|
+
let ownerFileWritten = false;
|
|
152
257
|
try {
|
|
153
|
-
|
|
258
|
+
const metadata = options.ownerMetadata
|
|
259
|
+
?.map((line) => line.replace(/\r?\n/g, ' ').trim())
|
|
260
|
+
.filter((line) => line.length > 0) ?? [];
|
|
261
|
+
await writeFile(join(lockPath, LOCK_PID_FILE), `${String(process.pid)}\n${ownerToken}\n${metadata.length > 0 ? `${metadata.join('\n')}\n` : ''}`, 'utf-8');
|
|
262
|
+
ownerFileWritten = true;
|
|
154
263
|
}
|
|
155
264
|
catch {
|
|
156
|
-
// Non-fatal: stale recovery falls back to age-only heuristic
|
|
265
|
+
// Non-fatal: stale recovery falls back to age-only heuristic and release
|
|
266
|
+
// falls back to unconditional removal.
|
|
157
267
|
}
|
|
158
268
|
try {
|
|
159
269
|
return await operation();
|
|
160
270
|
}
|
|
161
271
|
finally {
|
|
162
|
-
await
|
|
272
|
+
await releaseLock(lockPath, ownerToken, ownerFileWritten);
|
|
163
273
|
}
|
|
164
274
|
}
|
|
165
|
-
//# sourceMappingURL=file-lock.js.map
|
|
@@ -12,6 +12,10 @@ export interface ResolvedArchive {
|
|
|
12
12
|
url: string;
|
|
13
13
|
filename: string;
|
|
14
14
|
metadataFilename: string;
|
|
15
|
+
/** URL of Mozilla's published SHA256SUMS file for the release. */
|
|
16
|
+
checksumsUrl: string;
|
|
17
|
+
/** The archive's path as listed inside SHA256SUMS (relative to the release dir). */
|
|
18
|
+
pathInChecksums: string;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* Sidecar metadata stored alongside a cached archive.
|
|
@@ -62,6 +62,11 @@ export function resolveArchive(version, product = 'firefox') {
|
|
|
62
62
|
url: `${getArchiveBaseUrl(product)}/${archiveVersion}/source/firefox-${archiveVersion}.source.tar.xz`,
|
|
63
63
|
filename: `firefox-${safeProduct}-${archiveVersion}.source.tar.xz`,
|
|
64
64
|
metadataFilename: `firefox-${safeProduct}-${archiveVersion}.source.tar.xz.json`,
|
|
65
|
+
// Mozilla publishes one SHA256SUMS per release directory listing every
|
|
66
|
+
// artifact by its release-relative path. Downloads verify against it by
|
|
67
|
+
// default (fail closed on mismatch), so a compromised CDN response
|
|
68
|
+
// cannot silently become the trusted git baseline.
|
|
69
|
+
checksumsUrl: `${getArchiveBaseUrl(product)}/${archiveVersion}/SHA256SUMS`,
|
|
70
|
+
pathInChecksums: `source/firefox-${archiveVersion}.source.tar.xz`,
|
|
65
71
|
};
|
|
66
72
|
}
|
|
67
|
-
//# sourceMappingURL=firefox-archive.js.map
|
|
@@ -10,7 +10,7 @@ import { pipeline } from 'node:stream/promises';
|
|
|
10
10
|
import { ChecksumMismatchError } from '../errors/download.js';
|
|
11
11
|
import { toError } from '../utils/errors.js';
|
|
12
12
|
import { pathExistsStrict, readJson, removeFile, writeJson } from '../utils/fs.js';
|
|
13
|
-
import { verbose } from '../utils/logger.js';
|
|
13
|
+
import { verbose, warn } from '../utils/logger.js';
|
|
14
14
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
15
15
|
import { validateArchiveMetadata } from './firefox-archive.js';
|
|
16
16
|
import { downloadFile } from './firefox-download.js';
|
|
@@ -24,6 +24,39 @@ async function sha256File(filePath) {
|
|
|
24
24
|
await pipeline(stream, hash);
|
|
25
25
|
return hash.digest('hex');
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Fetches Mozilla's published SHA256SUMS for the release and extracts the
|
|
29
|
+
* digest for this archive. Returns null when the checksum file cannot be
|
|
30
|
+
* fetched or does not list the archive — the caller warns loudly and
|
|
31
|
+
* continues (offline mirrors and staging hosts legitimately lack it), while
|
|
32
|
+
* a FETCHED-but-mismatching digest always fails closed.
|
|
33
|
+
*
|
|
34
|
+
* Line format (both variants appear in the wild):
|
|
35
|
+
* `<64-hex-digest> <release-relative-path>`
|
|
36
|
+
* `<64-hex-digest> *<release-relative-path>`
|
|
37
|
+
*/
|
|
38
|
+
async function fetchPublishedSha256(archive) {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(archive.checksumsUrl);
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
verbose(`SHA256SUMS fetch returned HTTP ${String(response.status)} for ${archive.checksumsUrl}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const body = await response.text();
|
|
46
|
+
for (const line of body.split('\n')) {
|
|
47
|
+
const match = /^([0-9a-f]{64})\s+\*?(.+)$/.exec(line.trim());
|
|
48
|
+
if (match?.[1] && match[2] === archive.pathInChecksums) {
|
|
49
|
+
return match[1];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
verbose(`SHA256SUMS at ${archive.checksumsUrl} does not list ${archive.pathInChecksums}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
verbose(`SHA256SUMS fetch failed for ${archive.checksumsUrl}: ${toError(error).message}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
27
60
|
/**
|
|
28
61
|
* Ensures a valid cached archive exists, downloading it if needed.
|
|
29
62
|
* @param archive - Resolved archive descriptor
|
|
@@ -117,8 +150,32 @@ async function downloadToCache(archive, cacheDir, onProgress, expectedSha256, on
|
|
|
117
150
|
promotedTarball = true;
|
|
118
151
|
onCacheProgress?.(`Calculating source archive SHA-256 for ${archive.filename}...`);
|
|
119
152
|
const sha256 = await sha256File(tarballPath);
|
|
120
|
-
if (expectedSha256
|
|
121
|
-
|
|
153
|
+
if (expectedSha256) {
|
|
154
|
+
// Operator-pinned digest (firefox.sha256 config): takes precedence
|
|
155
|
+
// and always fails closed.
|
|
156
|
+
if (sha256 !== expectedSha256) {
|
|
157
|
+
throw new ChecksumMismatchError(archive.product, expectedSha256, sha256, archive.url);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Default integrity check: verify against Mozilla's published
|
|
162
|
+
// SHA256SUMS. TLS alone is thin trust for the artifact that becomes
|
|
163
|
+
// the git baseline every patch is built on — a compromised CDN
|
|
164
|
+
// response would otherwise be trusted with no signal. Mismatch fails
|
|
165
|
+
// closed (the catch below deletes the artifact); an unfetchable
|
|
166
|
+
// SHA256SUMS degrades to a loud warning so offline/mirror workflows
|
|
167
|
+
// keep working (pin firefox.sha256 in fireforge.json to fail closed
|
|
168
|
+
// even then).
|
|
169
|
+
onCacheProgress?.(`Verifying archive against published SHA256SUMS...`);
|
|
170
|
+
const publishedSha256 = await fetchPublishedSha256(archive);
|
|
171
|
+
if (publishedSha256 === null) {
|
|
172
|
+
warn(`Could not verify ${archive.filename} against Mozilla's published SHA256SUMS ` +
|
|
173
|
+
`(${archive.checksumsUrl} unavailable). The download is trusted on TLS alone — ` +
|
|
174
|
+
'set firefox.sha256 in fireforge.json to require checksum verification.');
|
|
175
|
+
}
|
|
176
|
+
else if (sha256 !== publishedSha256) {
|
|
177
|
+
throw new ChecksumMismatchError(archive.product, publishedSha256, sha256, archive.url);
|
|
178
|
+
}
|
|
122
179
|
}
|
|
123
180
|
onCacheProgress?.(`Writing source archive cache metadata for ${archive.metadataFilename}...`);
|
|
124
181
|
await writeJson(metadataPath, {
|
|
@@ -167,4 +224,3 @@ async function removeArchivePartFiles(archive, cacheDir) {
|
|
|
167
224
|
// Cache dir may not exist yet — that's fine.
|
|
168
225
|
}
|
|
169
226
|
}
|
|
170
|
-
//# sourceMappingURL=firefox-cache.js.map
|