@hominis/fireforge 0.47.1 → 0.49.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 +83 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +16 -28
- package/dist/src/commands/build.js +30 -4
- package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
- package/dist/src/commands/doctor-engine-eslint.js +12 -0
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +158 -0
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +4 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +14 -0
- package/dist/src/commands/manifest.d.ts +1 -2
- package/dist/src/commands/patch/delete.js +18 -4
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export-options.js +7 -0
- package/dist/src/commands/re-export-register.js +1 -0
- package/dist/src/commands/re-export-removed.d.ts +4 -0
- package/dist/src/commands/re-export-removed.js +49 -0
- package/dist/src/commands/re-export-single.js +4 -1
- package/dist/src/commands/re-export.js +8 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-browser-preflight.d.ts +28 -0
- package/dist/src/commands/test-browser-preflight.js +181 -0
- package/dist/src/commands/test-build-phase.js +30 -7
- package/dist/src/commands/test-checkjs.d.ts +3 -0
- package/dist/src/commands/test-checkjs.js +27 -0
- package/dist/src/commands/test-diagnose.js +3 -0
- package/dist/src/commands/test-harness-env.d.ts +5 -6
- package/dist/src/commands/test-harness-env.js +27 -7
- package/dist/src/commands/test-modes.d.ts +2 -4
- package/dist/src/commands/test-register.js +11 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +21 -1
- package/dist/src/commands/test-verdict.d.ts +2 -2
- package/dist/src/commands/test.js +83 -211
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-baseline-types.d.ts +12 -14
- package/dist/src/core/build-baseline-validate.d.ts +3 -0
- package/dist/src/core/build-baseline-validate.js +53 -0
- package/dist/src/core/build-baseline.d.ts +4 -3
- package/dist/src/core/build-baseline.js +23 -11
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -0
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/engine-write-boundary.d.ts +5 -0
- package/dist/src/core/engine-write-boundary.js +41 -0
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/furnace-apply-helpers.js +8 -2
- package/dist/src/core/furnace-apply.js +7 -0
- package/dist/src/core/furnace-validate-accessibility.js +9 -12
- package/dist/src/core/furnace-validate.js +0 -43
- package/dist/src/core/gecko-dom-types.d.ts +2 -0
- package/dist/src/core/gecko-dom-types.js +17 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/lit-template-regions.d.ts +4 -0
- package/dist/src/core/lit-template-regions.js +50 -0
- package/dist/src/core/mach-build-artifacts.d.ts +6 -6
- package/dist/src/core/mach-build-artifacts.js +13 -7
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach-resource-shim.d.ts +1 -1
- package/dist/src/core/mach-resource-shim.js +2 -0
- package/dist/src/core/marionette-port.d.ts +2 -0
- package/dist/src/core/marionette-port.js +38 -53
- package/dist/src/core/mochitest-server-port.d.ts +1 -1
- package/dist/src/core/mochitest-server-port.js +20 -17
- package/dist/src/core/objdir-maintenance.d.ts +10 -0
- package/dist/src/core/objdir-maintenance.js +151 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +10 -2
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/preflight-wait.d.ts +2 -0
- package/dist/src/core/preflight-wait.js +24 -0
- package/dist/src/core/process-owner.d.ts +9 -0
- package/dist/src/core/process-owner.js +43 -0
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/test-harness-crash.d.ts +1 -1
- package/dist/src/core/test-harness-crash.js +5 -5
- package/dist/src/core/test-host-state.d.ts +14 -0
- package/dist/src/core/test-host-state.js +80 -0
- package/dist/src/core/test-profile-files.d.ts +17 -0
- package/dist/src/core/test-profile-files.js +75 -0
- package/dist/src/core/test-stale-check.d.ts +6 -7
- package/dist/src/core/test-stale-check.js +9 -32
- package/dist/src/core/token-category.d.ts +7 -26
- package/dist/src/core/token-category.js +68 -121
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +86 -12
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +29 -3
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/assert.js +2 -7
- package/dist/src/utils/child-env.d.ts +0 -3
- package/dist/src/utils/child-env.js +0 -3
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +15 -4
- package/dist/src/utils/fs.js +11 -1
- package/dist/src/utils/options.d.ts +16 -1
- package/dist/src/utils/options.js +5 -3
- package/dist/src/utils/paths.js +3 -2
- package/dist/src/utils/process-group.d.ts +1 -2
- package/dist/src/utils/process-group.js +1 -2
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +46 -0
- package/dist/src/utils/process.d.ts +5 -2
- package/dist/src/utils/process.js +21 -8
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +82 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +13 -8
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +50 -1
- package/docs/testing.md +77 -12
- package/package.json +10 -5
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { exec } from '../utils/process.js';
|
|
3
|
+
/** Parent evidence; an unreadable parent is never proof of an orphan. */
|
|
4
|
+
export async function readProcessOwner(pid) {
|
|
5
|
+
if (process.platform === 'win32')
|
|
6
|
+
return undefined;
|
|
7
|
+
try {
|
|
8
|
+
const result = await exec('ps', ['-p', String(pid), '-o', 'ppid='], { timeout: 5000 });
|
|
9
|
+
const parentPid = Number(result.stdout.trim());
|
|
10
|
+
if (result.exitCode !== 0 || !Number.isSafeInteger(parentPid) || parentPid < 1)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (parentPid === 1)
|
|
13
|
+
return { parentPid, owner: 'init/launchd (parent exited)' };
|
|
14
|
+
const parent = await exec('ps', ['-p', String(parentPid), '-o', 'args='], { timeout: 5000 });
|
|
15
|
+
if (parent.exitCode !== 0 || !parent.stdout.trim())
|
|
16
|
+
return undefined;
|
|
17
|
+
return { parentPid, owner: parent.stdout.trim() };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Harness arguments alone never prove abandonment: require a dead parent too. */
|
|
24
|
+
export function isOrphanedHarness(commandLine, parentPid) {
|
|
25
|
+
return (parentPid === 1 &&
|
|
26
|
+
(/\s-marionette(?:\s|$)/.test(commandLine) ||
|
|
27
|
+
/\s-profile(?:\s|=)[^\n]*(?:fireforge-|mochitest|rust_mozprofile)/.test(commandLine)));
|
|
28
|
+
}
|
|
29
|
+
/** Refreshes the exact command and orphan evidence immediately before signaling. */
|
|
30
|
+
export async function isStillOrphanedProcess(pid, commandLine) {
|
|
31
|
+
// Windows ownership has no verified implementation; never infer it from
|
|
32
|
+
// command-line appearance or enable a destructive PowerShell fallback.
|
|
33
|
+
if (process.platform === 'win32')
|
|
34
|
+
return false;
|
|
35
|
+
try {
|
|
36
|
+
const result = await exec('ps', ['-p', String(pid), '-o', 'ppid=,args='], { timeout: 5000 });
|
|
37
|
+
const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(result.stdout);
|
|
38
|
+
return result.exitCode === 0 && match?.[1] === '1' && match[2] === commandLine;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** A patch to replay, in queue order. */
|
|
2
|
+
export interface DryRunPatch {
|
|
3
|
+
filename: string;
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
/** What the real rebase loop would do with one patch. */
|
|
7
|
+
export type DryRunVerdict = {
|
|
8
|
+
filename: string;
|
|
9
|
+
outcome: 'clean';
|
|
10
|
+
} | {
|
|
11
|
+
filename: string;
|
|
12
|
+
outcome: 'reduced-context';
|
|
13
|
+
step: number;
|
|
14
|
+
contextArg: string;
|
|
15
|
+
} | {
|
|
16
|
+
filename: string;
|
|
17
|
+
outcome: 'reject';
|
|
18
|
+
/** Files git named as not applying. Empty when stderr named none. */
|
|
19
|
+
files: string[];
|
|
20
|
+
/** git's own explanation, from the exact-context `--check`. */
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
|
23
|
+
/** Result of a full queue replay. */
|
|
24
|
+
export interface DryRunReplay {
|
|
25
|
+
verdicts: DryRunVerdict[];
|
|
26
|
+
/**
|
|
27
|
+
* Index of the first reject, or `undefined`. Verdicts after it were
|
|
28
|
+
* computed without that patch applied, so a reject there may cascade from
|
|
29
|
+
* the earlier one rather than be a conflict of its own.
|
|
30
|
+
*/
|
|
31
|
+
firstRejectIndex: number | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parses the file names out of `git apply --check` failure output. Git
|
|
35
|
+
* reports a context mismatch as `error: patch failed: <file>:<line>` and
|
|
36
|
+
* `error: <file>: patch does not apply`, and a missing or pre-existing
|
|
37
|
+
* target as `error: <file>: does not exist in index` /
|
|
38
|
+
* `error: <file>: already exists in index`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseApplyCheckFailures(stderr: string): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Replays `patches` onto the engine's HEAD tree in a private index.
|
|
43
|
+
* @param engineDir - Engine repository root
|
|
44
|
+
* @param patches - Patches in queue order
|
|
45
|
+
* @param maxFuzz - Maximum context-reduction steps, as for the real rebase
|
|
46
|
+
* @returns One verdict per patch
|
|
47
|
+
*/
|
|
48
|
+
export declare function replayQueueIndexOnly(engineDir: string, patches: readonly DryRunPatch[], maxFuzz: number): Promise<DryRunReplay>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `rebase --dry-run`: replays the patch queue against the engine's base
|
|
4
|
+
* commit without touching `engine/`.
|
|
5
|
+
*
|
|
6
|
+
* The replay runs on a private index seeded from HEAD (`GIT_INDEX_FILE`
|
|
7
|
+
* plus `git read-tree HEAD`) and applies each patch there with
|
|
8
|
+
* `git apply --cached`, in queue order, so a later patch sees what the
|
|
9
|
+
* earlier ones did, exactly as the real loop's worktree does. Each patch
|
|
10
|
+
* walks the same context-reduction ladder as {@link applyPatchWithFuzz}.
|
|
11
|
+
* The worktree and the real index are never written. The private index is
|
|
12
|
+
* deleted when the replay ends.
|
|
13
|
+
*/
|
|
14
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { GitError } from '../errors/git.js';
|
|
18
|
+
import { exec } from '../utils/process.js';
|
|
19
|
+
import { ensureGit } from './git-base.js';
|
|
20
|
+
import { assertValidMaxFuzz, contextReductionSteps } from './patch-apply-fuzz.js';
|
|
21
|
+
/**
|
|
22
|
+
* Parses the file names out of `git apply --check` failure output. Git
|
|
23
|
+
* reports a context mismatch as `error: patch failed: <file>:<line>` and
|
|
24
|
+
* `error: <file>: patch does not apply`, and a missing or pre-existing
|
|
25
|
+
* target as `error: <file>: does not exist in index` /
|
|
26
|
+
* `error: <file>: already exists in index`.
|
|
27
|
+
*/
|
|
28
|
+
export function parseApplyCheckFailures(stderr) {
|
|
29
|
+
const files = new Set();
|
|
30
|
+
for (const line of stderr.split('\n')) {
|
|
31
|
+
const failed = /^error: patch failed: (.+):\d+$/.exec(line);
|
|
32
|
+
if (failed?.[1]) {
|
|
33
|
+
files.add(failed[1]);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const named = /^error: (.+?): (?:patch does not apply|does not exist in index|already exists in index)$/.exec(line);
|
|
37
|
+
if (named?.[1])
|
|
38
|
+
files.add(named[1]);
|
|
39
|
+
}
|
|
40
|
+
return [...files];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Replays `patches` onto the engine's HEAD tree in a private index.
|
|
44
|
+
* @param engineDir - Engine repository root
|
|
45
|
+
* @param patches - Patches in queue order
|
|
46
|
+
* @param maxFuzz - Maximum context-reduction steps, as for the real rebase
|
|
47
|
+
* @returns One verdict per patch
|
|
48
|
+
*/
|
|
49
|
+
export async function replayQueueIndexOnly(engineDir, patches, maxFuzz) {
|
|
50
|
+
assertValidMaxFuzz(maxFuzz);
|
|
51
|
+
await ensureGit();
|
|
52
|
+
const scratch = await mkdtemp(join(tmpdir(), 'fireforge-rebase-dry-run-'));
|
|
53
|
+
const env = { GIT_INDEX_FILE: join(scratch, 'index') };
|
|
54
|
+
try {
|
|
55
|
+
const seeded = await exec('git', ['read-tree', 'HEAD'], { cwd: engineDir, env });
|
|
56
|
+
if (seeded.exitCode !== 0) {
|
|
57
|
+
throw new GitError(seeded.stderr.trim() || 'Could not seed a private index from HEAD', 'read-tree HEAD');
|
|
58
|
+
}
|
|
59
|
+
const ladder = contextReductionSteps(maxFuzz);
|
|
60
|
+
const verdicts = [];
|
|
61
|
+
let firstRejectIndex;
|
|
62
|
+
for (const patch of patches) {
|
|
63
|
+
let verdict;
|
|
64
|
+
let exactFailure = '';
|
|
65
|
+
for (const [step, contextArgs] of ladder.entries()) {
|
|
66
|
+
const check = await exec('git', ['apply', '--cached', '--check', ...contextArgs, '--', patch.path], { cwd: engineDir, env });
|
|
67
|
+
if (check.exitCode !== 0) {
|
|
68
|
+
if (step === 0)
|
|
69
|
+
exactFailure = check.stderr;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const apply = await exec('git', ['apply', '--cached', ...contextArgs, '--', patch.path], {
|
|
73
|
+
cwd: engineDir,
|
|
74
|
+
env,
|
|
75
|
+
});
|
|
76
|
+
if (apply.exitCode !== 0)
|
|
77
|
+
continue;
|
|
78
|
+
verdict =
|
|
79
|
+
step === 0
|
|
80
|
+
? { filename: patch.filename, outcome: 'clean' }
|
|
81
|
+
: {
|
|
82
|
+
filename: patch.filename,
|
|
83
|
+
outcome: 'reduced-context',
|
|
84
|
+
step,
|
|
85
|
+
contextArg: contextArgs[0] ?? '',
|
|
86
|
+
};
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (verdict === undefined) {
|
|
90
|
+
verdict = {
|
|
91
|
+
filename: patch.filename,
|
|
92
|
+
outcome: 'reject',
|
|
93
|
+
files: parseApplyCheckFailures(exactFailure),
|
|
94
|
+
detail: exactFailure.trim(),
|
|
95
|
+
};
|
|
96
|
+
firstRejectIndex ??= verdicts.length;
|
|
97
|
+
}
|
|
98
|
+
verdicts.push(verdict);
|
|
99
|
+
}
|
|
100
|
+
return { verdicts, firstRejectIndex };
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await rm(scratch, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Directory-name prefix of every FireForge-owned temporary run profile. */
|
|
2
|
+
export declare const RUN_PROFILE_PREFIX = "fireforge-run-profile-";
|
|
3
|
+
/** The profile a run launches with, and how to release it afterwards. */
|
|
4
|
+
export interface RunProfile {
|
|
5
|
+
/** Program parameters to append to `mach run` (empty for mach's default). */
|
|
6
|
+
args: string[];
|
|
7
|
+
/** Absolute profile directory, when one was chosen. */
|
|
8
|
+
dir?: string;
|
|
9
|
+
/** Whether FireForge created the directory and will delete it. */
|
|
10
|
+
temporary: boolean;
|
|
11
|
+
/** Removes a temporary profile. A no-op otherwise. Never throws. */
|
|
12
|
+
dispose: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** Inputs to {@link resolveRunProfile}. */
|
|
15
|
+
export interface RunProfileRequest {
|
|
16
|
+
/** `--profile <path>`: launch with this directory, left in place. */
|
|
17
|
+
profile?: string | undefined;
|
|
18
|
+
/** `--temp-profile`: launch with a fresh directory removed afterwards. */
|
|
19
|
+
tempProfile?: boolean | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Whether this is a `--smoke-exit` run. A smoke run with neither flag gets
|
|
22
|
+
* a temporary profile: the step exists to prove what the build does on a
|
|
23
|
+
* clean start, and the developer's profile-default carries whatever state
|
|
24
|
+
* the last session left.
|
|
25
|
+
*/
|
|
26
|
+
smoke: boolean;
|
|
27
|
+
/** Base directory for temporary profiles (defaults to the OS tmpdir). */
|
|
28
|
+
tempRoot?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Removes every temporary run profile this process still holds. Called by
|
|
32
|
+
* the CLI entry point's signal handler after child shutdown. Never throws.
|
|
33
|
+
*/
|
|
34
|
+
export declare function removeActiveRunProfiles(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Chooses the profile for a run.
|
|
37
|
+
* @param request - The run's profile flags
|
|
38
|
+
* @returns The profile arguments and their release handle
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveRunProfile(request: RunProfileRequest): Promise<RunProfile>;
|
|
41
|
+
/**
|
|
42
|
+
* Removes temporary run profiles whose owning FireForge process is gone.
|
|
43
|
+
* The `finally` that releases a profile covers a clean exit, a failed run
|
|
44
|
+
* and a forwarded SIGINT/SIGTERM, but not a SIGKILL of FireForge itself;
|
|
45
|
+
* this sweep is the backstop for that case. A directory whose marker is
|
|
46
|
+
* missing or unreadable is left alone: nothing proves it is abandoned.
|
|
47
|
+
* @param tempRoot - Base directory to sweep (defaults to the OS tmpdir)
|
|
48
|
+
* @returns The number of directories removed
|
|
49
|
+
*/
|
|
50
|
+
export declare function sweepAbandonedRunProfiles(tempRoot?: string): Promise<number>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Profile selection for `fireforge run`.
|
|
4
|
+
*
|
|
5
|
+
* `mach run` on desktop has no `--profile` option of its own: its parser's
|
|
6
|
+
* `--profile/-P` belongs to the Android run parser. On desktop a profile
|
|
7
|
+
* reaches Firefox as a program parameter (`-profile <dir>`), and mach skips
|
|
8
|
+
* creating the objdir's `tmp/profile-default` when it sees one among the
|
|
9
|
+
* params.
|
|
10
|
+
* Mach's `--temp-profile` does mint a fresh directory, but inside the objdir
|
|
11
|
+
* and never removes it, so FireForge owns the temporary profile instead:
|
|
12
|
+
* it creates the directory, seeds the two prefs mach would have written,
|
|
13
|
+
* passes it as `-profile`, and deletes it when the run ends.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join, resolve } from 'node:path';
|
|
18
|
+
import { InvalidArgumentError } from '../errors/base.js';
|
|
19
|
+
import { isProcessAlive, toError } from '../utils/errors.js';
|
|
20
|
+
import { verbose, warn } from '../utils/logger.js';
|
|
21
|
+
/** Directory-name prefix of every FireForge-owned temporary run profile. */
|
|
22
|
+
export const RUN_PROFILE_PREFIX = 'fireforge-run-profile-';
|
|
23
|
+
/** Marker file naming the pid of the FireForge process that owns the profile. */
|
|
24
|
+
const OWNER_MARKER = '.fireforge-run-owner';
|
|
25
|
+
/**
|
|
26
|
+
* The prefs `mach run` writes into the profiles it creates itself. A profile
|
|
27
|
+
* passed with `-profile` gets none of them, and a first-run default-browser
|
|
28
|
+
* prompt would sit in a headed smoke window.
|
|
29
|
+
*/
|
|
30
|
+
const SEEDED_USER_JS = 'user_pref("browser.shell.checkDefaultBrowser", false);\n' +
|
|
31
|
+
'user_pref("browser.aboutConfig.showWarning", false);\n';
|
|
32
|
+
/**
|
|
33
|
+
* Temporary profiles this process created and has not yet removed. The
|
|
34
|
+
* command's own `finally` loses the race to the entry point's signal
|
|
35
|
+
* handler, whose `process.exit` never lets it run, so the handler removes
|
|
36
|
+
* these itself once the browser tree has shut down.
|
|
37
|
+
*/
|
|
38
|
+
const activeTemporaryProfiles = new Set();
|
|
39
|
+
async function removeTemporaryProfile(dir) {
|
|
40
|
+
try {
|
|
41
|
+
await rm(dir, { recursive: true, force: true });
|
|
42
|
+
activeTemporaryProfiles.delete(dir);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
warn(`Could not remove temporary run profile ${dir}: ${toError(error).message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Removes every temporary run profile this process still holds. Called by
|
|
50
|
+
* the CLI entry point's signal handler after child shutdown. Never throws.
|
|
51
|
+
*/
|
|
52
|
+
export async function removeActiveRunProfiles() {
|
|
53
|
+
await Promise.all([...activeTemporaryProfiles].map((dir) => removeTemporaryProfile(dir)));
|
|
54
|
+
}
|
|
55
|
+
const NO_PROFILE = {
|
|
56
|
+
args: [],
|
|
57
|
+
temporary: false,
|
|
58
|
+
dispose: () => Promise.resolve(),
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Chooses the profile for a run.
|
|
62
|
+
* @param request - The run's profile flags
|
|
63
|
+
* @returns The profile arguments and their release handle
|
|
64
|
+
*/
|
|
65
|
+
export async function resolveRunProfile(request) {
|
|
66
|
+
if (request.profile !== undefined && request.tempProfile === true) {
|
|
67
|
+
throw new InvalidArgumentError('--profile and --temp-profile are mutually exclusive: name a profile or ask for a fresh one.', 'profile');
|
|
68
|
+
}
|
|
69
|
+
if (request.profile !== undefined) {
|
|
70
|
+
const dir = resolve(request.profile);
|
|
71
|
+
return { args: ['-profile', dir], dir, temporary: false, dispose: NO_PROFILE.dispose };
|
|
72
|
+
}
|
|
73
|
+
if (request.tempProfile !== true && !request.smoke) {
|
|
74
|
+
return NO_PROFILE;
|
|
75
|
+
}
|
|
76
|
+
const dir = await mkdtemp(join(request.tempRoot ?? tmpdir(), RUN_PROFILE_PREFIX));
|
|
77
|
+
activeTemporaryProfiles.add(dir);
|
|
78
|
+
// The owner marker goes in first, so a FireForge killed past this point
|
|
79
|
+
// leaves a directory the next run can prove abandoned.
|
|
80
|
+
await writeFile(join(dir, OWNER_MARKER), `${process.pid}\n`);
|
|
81
|
+
await writeFile(join(dir, 'user.js'), SEEDED_USER_JS);
|
|
82
|
+
verbose(`Temporary run profile: ${dir}`);
|
|
83
|
+
return {
|
|
84
|
+
args: ['-profile', dir],
|
|
85
|
+
dir,
|
|
86
|
+
temporary: true,
|
|
87
|
+
dispose: () => removeTemporaryProfile(dir),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Removes temporary run profiles whose owning FireForge process is gone.
|
|
92
|
+
* The `finally` that releases a profile covers a clean exit, a failed run
|
|
93
|
+
* and a forwarded SIGINT/SIGTERM, but not a SIGKILL of FireForge itself;
|
|
94
|
+
* this sweep is the backstop for that case. A directory whose marker is
|
|
95
|
+
* missing or unreadable is left alone: nothing proves it is abandoned.
|
|
96
|
+
* @param tempRoot - Base directory to sweep (defaults to the OS tmpdir)
|
|
97
|
+
* @returns The number of directories removed
|
|
98
|
+
*/
|
|
99
|
+
export async function sweepAbandonedRunProfiles(tempRoot = tmpdir()) {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = await readdir(tempRoot);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
let removed = 0;
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!entry.startsWith(RUN_PROFILE_PREFIX))
|
|
110
|
+
continue;
|
|
111
|
+
const dir = join(tempRoot, entry);
|
|
112
|
+
let pid;
|
|
113
|
+
try {
|
|
114
|
+
pid = Number.parseInt((await readFile(join(dir, OWNER_MARKER), 'utf8')).trim(), 10);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid || isProcessAlive(pid)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await rm(dir, { recursive: true, force: true });
|
|
124
|
+
removed += 1;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
verbose(`Could not remove abandoned run profile ${dir}: ${toError(error).message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return removed;
|
|
131
|
+
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { PatchedContentContext } from './patch-apply.js';
|
|
28
28
|
/** Batched patch lookup, injected by callers that classify many files. */
|
|
29
|
-
export type AffectingPatchLookup = Pick<PatchedContentContext, 'getAffectingPatches' | 'readPatchBody'
|
|
29
|
+
export type AffectingPatchLookup = Pick<PatchedContentContext, 'getAffectingPatches' | 'readPatchBody'> & Partial<Pick<PatchedContentContext, 'liveBlobHash'>>;
|
|
30
30
|
/**
|
|
31
31
|
* Classification result for a binary comparison. Structurally assignable
|
|
32
32
|
* to `ClassifiedFile` without importing it (which would create a cycle
|
|
@@ -111,7 +111,9 @@ export async function classifyBinaryOwnedFile(args) {
|
|
|
111
111
|
}
|
|
112
112
|
if (section.hasBinaryDelta && section.indexNewHash !== undefined) {
|
|
113
113
|
const fullPath = join(engineDir, entry.file);
|
|
114
|
-
const live =
|
|
114
|
+
const live = args.lookup?.liveBlobHash !== undefined
|
|
115
|
+
? await args.lookup.liveBlobHash(fullPath)
|
|
116
|
+
: (await hashObjectBatch(engineDir, [fullPath])).get(fullPath);
|
|
115
117
|
if (live !== undefined) {
|
|
116
118
|
// Prefix-compare: recorded hashes may be abbreviated (≥7 chars).
|
|
117
119
|
const matches = live.startsWith(section.indexNewHash) || section.indexNewHash.startsWith(live);
|
|
@@ -13,6 +13,7 @@ import { readText } from '../utils/fs.js';
|
|
|
13
13
|
import { verbose } from '../utils/logger.js';
|
|
14
14
|
import { normalizePathSlashes } from '../utils/paths.js';
|
|
15
15
|
import { isBrandingManagedPath } from './branding.js';
|
|
16
|
+
import { isBinaryFile } from './git-file-ops.js';
|
|
16
17
|
import { createPatchedContentContext } from './patch-apply.js';
|
|
17
18
|
import { classifyBinaryOwnedFile } from './status-binary.js';
|
|
18
19
|
/**
|
|
@@ -141,6 +142,7 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
|
|
|
141
142
|
// expected-vs-actual content comparison then routes them into `unmanaged`
|
|
142
143
|
// when the content matches neither owner's expectation.
|
|
143
144
|
const patchClaims = buildPatchClaims(ctx.manifestPatches);
|
|
145
|
+
await prefetchSingleOwnerContent(files, engineDir, patchClaims, ctx);
|
|
144
146
|
const deps = {
|
|
145
147
|
engineDir,
|
|
146
148
|
patchesDir,
|
|
@@ -149,12 +151,37 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
|
|
|
149
151
|
patchClaims,
|
|
150
152
|
ctx,
|
|
151
153
|
};
|
|
152
|
-
// Bounded pool over per-file classification
|
|
153
|
-
// spawns git). Order preserved by the mapper. Per-file failures settle
|
|
154
|
+
// Bounded pool over per-file classification. Order preserved by the mapper. Per-file failures settle
|
|
154
155
|
// inside classifySingleOwnerFile's catch, so one bad file never rejects
|
|
155
156
|
// the batch.
|
|
156
157
|
return mapWithConcurrency(files, CLASSIFY_CONCURRENCY, (entry) => classifyEntry(entry, deps));
|
|
157
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Fetches, in two git processes, what the per-file comparison would
|
|
161
|
+
* otherwise spawn git for file by file: the HEAD content of every
|
|
162
|
+
* single-owner path (one `cat-file --batch`) and the live blob hash of every
|
|
163
|
+
* one that is binary on disk (one `hash-object` per ARG_MAX chunk). On a
|
|
164
|
+
* 1,600-file Firefox scan that was ~1,550 `git show` and `hash-object`
|
|
165
|
+
* spawns. Fetching for a path the classifier ends up routing elsewhere
|
|
166
|
+
* (a branding path, say) costs a few bytes, never a wrong answer.
|
|
167
|
+
*/
|
|
168
|
+
async function prefetchSingleOwnerContent(files, engineDir, patchClaims, ctx) {
|
|
169
|
+
const singleOwner = files.filter((entry) => patchClaims.get(entry.file)?.length === 1);
|
|
170
|
+
if (singleOwner.length === 0)
|
|
171
|
+
return;
|
|
172
|
+
await ctx.prefetchBase(singleOwner.map((entry) => entry.file));
|
|
173
|
+
const present = singleOwner.filter((entry) => getPrimaryStatusCode(entry.status) !== 'D');
|
|
174
|
+
const binary = await mapWithConcurrency(present, CLASSIFY_CONCURRENCY, async (entry) => {
|
|
175
|
+
try {
|
|
176
|
+
return (await isBinaryFile(engineDir, entry.file)) ? join(engineDir, entry.file) : undefined;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// The per-file path probes again and settles the failure itself.
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
await ctx.prefetchLiveBlobHashes(binary.filter((path) => path !== undefined));
|
|
184
|
+
}
|
|
158
185
|
/** Concurrency bound for per-file classification (matches import's guard). */
|
|
159
186
|
const CLASSIFY_CONCURRENCY = 8;
|
|
160
187
|
/** Classifies one status entry. Extracted so the pool worker stays flat. */
|
|
@@ -25,7 +25,7 @@ export type { HarnessCrashSignature } from './test-harness-signature.js';
|
|
|
25
25
|
import type { HarnessCrashSignature } from './test-harness-signature.js';
|
|
26
26
|
export { headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint } from './test-stall-triage.js';
|
|
27
27
|
/** How a completed harness run should be interpreted. */
|
|
28
|
-
export type HarnessRunClassification = 'tests-ran-ok' | 'test-failures' | 'harness-crash' | 'no-tests';
|
|
28
|
+
export type HarnessRunClassification = 'tests-ran-ok' | 'test-failures' | 'harness-crash' | 'harness-arguments' | 'no-tests';
|
|
29
29
|
/** Numeric counts parsed from the harness's embedded result summary. */
|
|
30
30
|
export interface HarnessSummaryCounts {
|
|
31
31
|
/** Total checks the suite ran (`Ran N checks`), when the summary prints one. */
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { hasKnownTeardownNoise } from './mach-known-noise-filter.js';
|
|
26
26
|
import { truncateEvidence, unmarkedFailureEvidenceNote, } from './test-harness-verdict-notes.js';
|
|
27
|
+
import { classifyProfileArgumentFailure } from './test-profile-files.js';
|
|
27
28
|
// The no-output-stall triage text lives in `test-stall-triage.ts` (it is
|
|
28
29
|
// operator advice, not classification) and is re-exported here so the
|
|
29
30
|
// command layer keeps importing its harness diagnostics from one module.
|
|
@@ -472,6 +473,9 @@ export function detectHarnessCrashSignature(output) {
|
|
|
472
473
|
*/
|
|
473
474
|
export function classifyHarnessRun(exitCode, output, requestedPaths) {
|
|
474
475
|
const counts = extractSummaryCounts(output);
|
|
476
|
+
const profileFailure = classifyProfileArgumentFailure(output);
|
|
477
|
+
if (profileFailure)
|
|
478
|
+
return { ...profileFailure, ...counts };
|
|
475
479
|
const realFailures = realUnexpectedFailureLines(output);
|
|
476
480
|
const firstRealFailure = realFailures[0];
|
|
477
481
|
const failureBlocks = collectUnexpectedFailureBlocks(output);
|
|
@@ -660,11 +664,7 @@ export function formatFireforgeVerdictLine(verdict, shards) {
|
|
|
660
664
|
if (verdict.kind === 'tests-ran-ok') {
|
|
661
665
|
return `FIREFORGE-VERDICT: PASS${counts}${shardSuffix}${note}`;
|
|
662
666
|
}
|
|
663
|
-
const reason = verdict.kind === 'harness-crash'
|
|
664
|
-
? 'crash'
|
|
665
|
-
: verdict.kind === 'no-tests'
|
|
666
|
-
? 'no-tests'
|
|
667
|
-
: 'test-failures';
|
|
667
|
+
const reason = verdict.kind === 'harness-crash' ? 'crash' : verdict.kind;
|
|
668
668
|
return `FIREFORGE-VERDICT: FAIL reason=${reason}${counts}${shardSuffix}${note}`;
|
|
669
669
|
}
|
|
670
670
|
/**
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TestHostState {
|
|
2
|
+
load: number;
|
|
3
|
+
topProcess?: {
|
|
4
|
+
cpu: number;
|
|
5
|
+
command: string;
|
|
6
|
+
};
|
|
7
|
+
power: 'ac' | 'battery' | 'unknown';
|
|
8
|
+
}
|
|
9
|
+
/** Samples load/top process and the macOS power SOURCE (including charging on AC). */
|
|
10
|
+
export declare function sampleTestHost(): Promise<TestHostState>;
|
|
11
|
+
/** Reports host contention alongside a suite's result, without inventing a test failure. */
|
|
12
|
+
export declare function reportTestHost(state: TestHostState, phase: string): boolean;
|
|
13
|
+
/** Stamps each perf artifact before the verdict, preserving the producer's schema and data. */
|
|
14
|
+
export declare function stampPerfHost(env: Record<string, string> | undefined, start: TestHostState, end: TestHostState): Promise<boolean>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { loadavg } from 'node:os';
|
|
3
|
+
import { toError } from '../utils/errors.js';
|
|
4
|
+
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
5
|
+
import { notice } from '../utils/logger.js';
|
|
6
|
+
import { exec } from '../utils/process.js';
|
|
7
|
+
import { isObject } from '../utils/validation.js';
|
|
8
|
+
/** Samples load/top process and the macOS power SOURCE (including charging on AC). */
|
|
9
|
+
export async function sampleTestHost() {
|
|
10
|
+
const state = { load: loadavg()[0] ?? 0, power: 'unknown' };
|
|
11
|
+
try {
|
|
12
|
+
const result = await exec('ps', ['-axo', 'pcpu=,comm='], { timeout: 5000 });
|
|
13
|
+
const processes = result.stdout
|
|
14
|
+
.split('\n')
|
|
15
|
+
.flatMap((line) => {
|
|
16
|
+
const match = /^\s*([\d.]+)\s+(.+)$/.exec(line);
|
|
17
|
+
return match ? [{ cpu: Number(match[1]), command: match[2] ?? '' }] : [];
|
|
18
|
+
})
|
|
19
|
+
.sort((a, b) => b.cpu - a.cpu);
|
|
20
|
+
if (processes[0])
|
|
21
|
+
state.topProcess = processes[0];
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* Host diagnostics remain best effort. */
|
|
25
|
+
}
|
|
26
|
+
if (process.platform === 'darwin') {
|
|
27
|
+
try {
|
|
28
|
+
const result = await exec('pmset', ['-g', 'ps'], { timeout: 5000 });
|
|
29
|
+
state.power = /'AC Power'/.test(result.stdout)
|
|
30
|
+
? 'ac'
|
|
31
|
+
: /'Battery Power'/.test(result.stdout)
|
|
32
|
+
? 'battery'
|
|
33
|
+
: 'unknown';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* Keep unknown when power tooling is unavailable. */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
/** Reports host contention alongside a suite's result, without inventing a test failure. */
|
|
42
|
+
export function reportTestHost(state, phase) {
|
|
43
|
+
const pegged = (state.topProcess?.cpu ?? 0) >= 90;
|
|
44
|
+
if (state.load < 4 && !pegged)
|
|
45
|
+
return false;
|
|
46
|
+
notice(`Host load at ${phase}: ${state.load.toFixed(2)}; top process ${state.topProcess?.command ?? '(unknown)'} ${state.topProcess?.cpu ?? 0}% CPU. Timing failures may reflect host contention.`);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** Stamps each perf artifact before the verdict, preserving the producer's schema and data. */
|
|
50
|
+
export async function stampPerfHost(env, start, end) {
|
|
51
|
+
if (!env)
|
|
52
|
+
return false;
|
|
53
|
+
const artifacts = [
|
|
54
|
+
...new Set(Object.entries(env)
|
|
55
|
+
.filter(([key]) => key.endsWith('_PERF_SAMPLE_JSON'))
|
|
56
|
+
.map(([, path]) => path)),
|
|
57
|
+
];
|
|
58
|
+
if (artifacts.length === 0)
|
|
59
|
+
return false;
|
|
60
|
+
const changed = start.power !== 'unknown' && end.power !== 'unknown' && start.power !== end.power;
|
|
61
|
+
if (changed)
|
|
62
|
+
notice(`Perf power source changed during the run (${start.power} → ${end.power}); this sample is inconclusive.`);
|
|
63
|
+
for (const artifact of artifacts) {
|
|
64
|
+
try {
|
|
65
|
+
if (!(await pathExists(artifact)))
|
|
66
|
+
continue;
|
|
67
|
+
const sample = await readJson(artifact);
|
|
68
|
+
if (!isObject(sample))
|
|
69
|
+
throw new Error('sample JSON is not an object');
|
|
70
|
+
await writeJson(artifact, {
|
|
71
|
+
...sample,
|
|
72
|
+
fireforgeHost: { start, end, powerSourceChanged: changed },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
notice(`Could not annotate perf artifact ${artifact}: ${toError(error).message}. The harness outcome is preserved; inspect or regenerate this artifact before using it as perf evidence.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return changed;
|
|
80
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Stages explicit profile overlays with relative destinations; cleanup belongs to the caller. */
|
|
2
|
+
export interface StagedProfileFiles {
|
|
3
|
+
root: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
env: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/** Prepares profile overlays and browser/port ownership preconditions. */
|
|
8
|
+
export declare function stageProfileFiles(projectRoot: string, files: readonly string[]): Promise<StagedProfileFiles>;
|
|
9
|
+
/** Python startup overlay: allow merging ONLY the explicitly staged profile directories. */
|
|
10
|
+
export declare const PROFILE_MERGE_PYTHON = "\nimport os\nimport shutil\n_fireforge_profile_root = os.environ.get(\"FIREFORGE_PROFILE_MERGE_ROOT\")\nif _fireforge_profile_root and not getattr(shutil.copytree, \"_fireforge_profile_merge\", False):\n _fireforge_copytree = shutil.copytree\n def _fireforge_merge_copytree(src, dst, *args, **kwargs):\n if os.path.realpath(src).startswith(os.path.realpath(_fireforge_profile_root) + os.sep):\n # Python's recursive calls may supply dirs_exist_ok positionally.\n if len(args) >= 5:\n args = args[:4] + (True,) + args[5:]\n else:\n kwargs[\"dirs_exist_ok\"] = True\n return _fireforge_copytree(src, dst, *args, **kwargs)\n _fireforge_merge_copytree._fireforge_profile_merge = True\n shutil.copytree = _fireforge_merge_copytree\n";
|
|
11
|
+
/** Classifies the profile setup traceback separately from stale packaging. */
|
|
12
|
+
export declare function classifyProfileArgumentFailure(output: string): {
|
|
13
|
+
kind: 'harness-arguments';
|
|
14
|
+
note: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
/** Cleanup is independent of the harness verdict and cannot hide its primary error. */
|
|
17
|
+
export declare function cleanupProfileFiles(root: string): Promise<void>;
|