@hominis/fireforge 0.15.2 → 0.15.4
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 +30 -1
- package/README.md +55 -1
- package/dist/src/commands/build.js +29 -2
- package/dist/src/commands/doctor-furnace.js +8 -1
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +49 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +151 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +34 -0
- package/dist/src/commands/furnace/chrome-doc.js +168 -0
- package/dist/src/commands/furnace/create-mochikit.d.ts +30 -0
- package/dist/src/commands/furnace/create-mochikit.js +70 -0
- package/dist/src/commands/furnace/create-templates.d.ts +32 -0
- package/dist/src/commands/furnace/create-templates.js +69 -0
- package/dist/src/commands/furnace/create.d.ts +17 -0
- package/dist/src/commands/furnace/create.js +54 -16
- package/dist/src/commands/furnace/index.d.ts +2 -1
- package/dist/src/commands/furnace/index.js +20 -3
- package/dist/src/commands/lint.d.ts +13 -1
- package/dist/src/commands/lint.js +33 -7
- package/dist/src/commands/wire.js +59 -6
- package/dist/src/core/browser-wire.d.ts +8 -0
- package/dist/src/core/browser-wire.js +2 -2
- package/dist/src/core/build-audit.d.ts +46 -0
- package/dist/src/core/build-audit.js +251 -0
- package/dist/src/core/build-baseline.d.ts +59 -0
- package/dist/src/core/build-baseline.js +83 -0
- package/dist/src/core/build-prepare.d.ts +20 -1
- package/dist/src/core/build-prepare.js +89 -4
- package/dist/src/core/furnace-operation.d.ts +2 -1
- package/dist/src/core/furnace-operation.js +13 -7
- package/dist/src/core/mach-error-hints.d.ts +29 -0
- package/dist/src/core/mach-error-hints.js +43 -0
- package/dist/src/core/mach.d.ts +5 -2
- package/dist/src/core/mach.js +31 -4
- package/dist/src/core/patch-lint-diff-tag.d.ts +33 -0
- package/dist/src/core/patch-lint-diff-tag.js +83 -0
- package/dist/src/core/wire-dom-fragment.d.ts +16 -4
- package/dist/src/core/wire-dom-fragment.js +32 -17
- package/dist/src/types/commands/options.d.ts +22 -0
- package/dist/src/types/commands/patches.d.ts +9 -0
- package/dist/src/types/furnace.d.ts +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { join, relative } from 'node:path';
|
|
3
3
|
import { DEFAULT_BROWSER_SUBSCRIPT_DIR, wireSubscript } from '../core/browser-wire.js';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
|
+
import { furnaceConfigExists as checkFurnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
|
|
5
6
|
import { consumeParserFallbackEvents } from '../core/parser-fallback.js';
|
|
7
|
+
import { DEFAULT_DOM_TARGET } from '../core/wire-dom-fragment.js';
|
|
6
8
|
import { InvalidArgumentError } from '../errors/base.js';
|
|
7
9
|
import { toError } from '../utils/errors.js';
|
|
8
10
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -10,7 +12,7 @@ import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
|
10
12
|
import { pickDefined } from '../utils/options.js';
|
|
11
13
|
import { isContainedRelativePath, isPathInsideRoot, toRootRelativePath } from '../utils/paths.js';
|
|
12
14
|
const BROWSER_BASE_DIR = 'browser/base';
|
|
13
|
-
function printWireDryRun(engineDir, name, subscriptDir, domFilePath, options) {
|
|
15
|
+
function printWireDryRun(engineDir, name, subscriptDir, domFilePath, domTargetPath, options) {
|
|
14
16
|
info('[dry-run] Would wire subscript:');
|
|
15
17
|
info(` source: ${subscriptDir}/${name}.js`);
|
|
16
18
|
info(` browser-main.js: loadSubScript("chrome://browser/content/${name}.js")`);
|
|
@@ -22,12 +24,45 @@ function printWireDryRun(engineDir, name, subscriptDir, domFilePath, options) {
|
|
|
22
24
|
}
|
|
23
25
|
if (domFilePath) {
|
|
24
26
|
const includePath = relative(join(engineDir, subscriptDir), join(engineDir, domFilePath)).replace(/\\/g, '/');
|
|
25
|
-
info(`
|
|
27
|
+
info(` ${domTargetPath}: #include ${includePath}`);
|
|
26
28
|
}
|
|
27
29
|
const relPath = relative(join(engineDir, BROWSER_BASE_DIR), join(engineDir, subscriptDir)).replace(/\\/g, '/');
|
|
28
30
|
info(` jar.mn: content/browser/${name}.js (${relPath}/${name}.js)`);
|
|
29
31
|
outro('Dry run complete');
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the chrome document the `#include` directive is inserted into.
|
|
35
|
+
*
|
|
36
|
+
* Preference order:
|
|
37
|
+
* 1. `--target <path>` CLI flag (explicit caller intent)
|
|
38
|
+
* 2. First entry of `furnace.json.tokenHostDocuments` (fork-configured
|
|
39
|
+
* chrome doc list; already consumed by the missing-token-link
|
|
40
|
+
* validator and the doctor check)
|
|
41
|
+
* 3. `browser/base/content/browser.xhtml` (upstream default)
|
|
42
|
+
*
|
|
43
|
+
* Step 2 is silent — a missing / invalid furnace.json falls through to the
|
|
44
|
+
* upstream default rather than surfacing a warning, because forks that don't
|
|
45
|
+
* use furnace shouldn't have to configure anything.
|
|
46
|
+
*/
|
|
47
|
+
async function resolveDomTargetPath(projectRoot, explicit) {
|
|
48
|
+
if (explicit !== undefined) {
|
|
49
|
+
return explicit;
|
|
50
|
+
}
|
|
51
|
+
if (await checkFurnaceConfigExists(projectRoot)) {
|
|
52
|
+
try {
|
|
53
|
+
const furnaceConfig = await loadFurnaceConfig(projectRoot);
|
|
54
|
+
const first = furnaceConfig.tokenHostDocuments?.[0];
|
|
55
|
+
if (first !== undefined && first.length > 0) {
|
|
56
|
+
return first;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Fall through to default — a broken furnace.json should not block
|
|
61
|
+
// the wire command. The doctor surfaces that issue separately.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return DEFAULT_DOM_TARGET;
|
|
65
|
+
}
|
|
31
66
|
/**
|
|
32
67
|
* Validates a subscript name supplied on the command line. Subscripts are
|
|
33
68
|
* resolved into filenames under the subscript directory and registered in
|
|
@@ -90,6 +125,21 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
90
125
|
}
|
|
91
126
|
domFilePath = toRootRelativePath(paths.engine, options.dom);
|
|
92
127
|
}
|
|
128
|
+
// Resolve the chrome document the `#include` directive will land in.
|
|
129
|
+
// Only consulted when `--dom` is supplied — we still resolve it here so
|
|
130
|
+
// the dry-run plan can print the target accurately.
|
|
131
|
+
if (options.target !== undefined && !isContainedRelativePath(options.target)) {
|
|
132
|
+
throw new InvalidArgumentError(`Target chrome document must stay within engine/: ${options.target}`, 'target');
|
|
133
|
+
}
|
|
134
|
+
const domTargetPath = await resolveDomTargetPath(projectRoot, options.target);
|
|
135
|
+
if (domFilePath) {
|
|
136
|
+
const paths = getProjectPaths(projectRoot);
|
|
137
|
+
if (!options.dryRun && !(await pathExists(join(paths.engine, domTargetPath)))) {
|
|
138
|
+
throw new InvalidArgumentError(`Chrome document not found in engine: ${domTargetPath}\n` +
|
|
139
|
+
'Set "tokenHostDocuments" in furnace.json (first entry is used by wire) ' +
|
|
140
|
+
'or pass --target <path>.', 'target');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
93
143
|
// Verify the subscript file exists in engine/ (skip for dry-run)
|
|
94
144
|
if (!options.dryRun) {
|
|
95
145
|
const paths = getProjectPaths(projectRoot);
|
|
@@ -100,13 +150,14 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
100
150
|
}
|
|
101
151
|
}
|
|
102
152
|
if (options.dryRun) {
|
|
103
|
-
printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, options);
|
|
153
|
+
printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, domTargetPath, options);
|
|
104
154
|
return;
|
|
105
155
|
}
|
|
106
156
|
const result = await wireSubscript(projectRoot, name, {
|
|
107
157
|
...(options.init !== undefined ? { init: options.init } : {}),
|
|
108
158
|
...(options.destroy !== undefined ? { destroy: options.destroy } : {}),
|
|
109
159
|
...(domFilePath !== undefined ? { domFilePath } : {}),
|
|
160
|
+
...(domFilePath !== undefined && domTargetPath !== DEFAULT_DOM_TARGET ? { domTargetPath } : {}),
|
|
110
161
|
...(options.after !== undefined ? { after: options.after } : {}),
|
|
111
162
|
...(subscriptDir !== DEFAULT_BROWSER_SUBSCRIPT_DIR ? { subscriptDir } : {}),
|
|
112
163
|
dryRun: false,
|
|
@@ -140,10 +191,10 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
140
191
|
}
|
|
141
192
|
if (domFilePath) {
|
|
142
193
|
if (result.domInserted) {
|
|
143
|
-
success(`Inserted #include directive into
|
|
194
|
+
success(`Inserted #include directive into ${domTargetPath}`);
|
|
144
195
|
}
|
|
145
196
|
else {
|
|
146
|
-
info(`#include directive already present in
|
|
197
|
+
info(`#include directive already present in ${domTargetPath} (skipped)`);
|
|
147
198
|
}
|
|
148
199
|
}
|
|
149
200
|
if (result.jarMnResult.skipped) {
|
|
@@ -161,10 +212,12 @@ export function registerWire(program, { getProjectRoot, withErrorHandling }) {
|
|
|
161
212
|
.description('Wire a chrome subscript into the browser')
|
|
162
213
|
.option('--init <expression>', 'Init expression for browser-init.js onLoad()')
|
|
163
214
|
.option('--destroy <expression>', 'Destroy expression for browser-init.js onUnload()')
|
|
164
|
-
.option('--dom <file>', 'XHTML fragment file to insert into
|
|
215
|
+
.option('--dom <file>', 'XHTML fragment file to insert into the chrome document')
|
|
165
216
|
.option('--dry-run', 'Show what would be changed without writing')
|
|
166
217
|
.option('--after <name>', 'Insert init block after the block for this name')
|
|
167
218
|
.option('--subscript-dir <dir>', 'Subscript directory relative to engine/ (default: browser/base/content)')
|
|
219
|
+
.option('--target <path>', 'Chrome document to insert --dom into, relative to engine/ ' +
|
|
220
|
+
'(default: first entry of furnace.json tokenHostDocuments, else browser/base/content/browser.xhtml)')
|
|
168
221
|
.action(withErrorHandling(async (name, options) => {
|
|
169
222
|
await wireCommand(getProjectRoot(), name, pickDefined(options));
|
|
170
223
|
}));
|
|
@@ -22,6 +22,14 @@ export interface WireOptions {
|
|
|
22
22
|
destroy?: string | undefined;
|
|
23
23
|
/** Path to `.inc.xhtml` file relative to engine root */
|
|
24
24
|
domFilePath?: string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Top-level chrome document the DOM fragment's `#include` directive is
|
|
27
|
+
* inserted into, relative to engine/. Defaults to
|
|
28
|
+
* `browser/base/content/browser.xhtml`. Forks that replace browser.xhtml
|
|
29
|
+
* with a custom chrome document (e.g. `mybrowser.xhtml`) pass the
|
|
30
|
+
* replacement path here.
|
|
31
|
+
*/
|
|
32
|
+
domTargetPath?: string | undefined;
|
|
25
33
|
/** Dry run — don't write any files */
|
|
26
34
|
dryRun?: boolean | undefined;
|
|
27
35
|
/** Insert init block after the block containing this name */
|
|
@@ -48,10 +48,10 @@ export async function wireSubscript(root, name, options = {}) {
|
|
|
48
48
|
if (options.destroy) {
|
|
49
49
|
destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy);
|
|
50
50
|
}
|
|
51
|
-
// 4. Add #include directive to
|
|
51
|
+
// 4. Add #include directive to the top-level chrome document (if provided)
|
|
52
52
|
let domInserted = false;
|
|
53
53
|
if (options.domFilePath) {
|
|
54
|
-
domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath));
|
|
54
|
+
domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath), options.domTargetPath);
|
|
55
55
|
}
|
|
56
56
|
// 5. Register in jar.mn
|
|
57
57
|
const jarMnResult = await registerBrowserContent(engineDir, `${name}.js`, undefined, jarMnSourcePath);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BuildBaseline } from './build-baseline.js';
|
|
2
|
+
/** Result of a single artifact lookup. */
|
|
3
|
+
export interface AuditEntry {
|
|
4
|
+
/** Engine-relative source file path (POSIX separators). */
|
|
5
|
+
source: string;
|
|
6
|
+
/**
|
|
7
|
+
* Resolved artifact path inside the dist tree, or undefined when no
|
|
8
|
+
* candidate bundle location was found. An entry with an undefined path
|
|
9
|
+
* and status "missing" means the source was packageable but nothing
|
|
10
|
+
* that looked like its artifact showed up in the bundle.
|
|
11
|
+
*/
|
|
12
|
+
artifact: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* updated: an artifact exists and is at least as new as the source.
|
|
15
|
+
* stale: artifact exists but is older than the source (probable packaging drop).
|
|
16
|
+
* missing: no artifact with a matching basename was found anywhere under dist/.
|
|
17
|
+
* skipped: the file extension / path does not imply packaging; not counted.
|
|
18
|
+
*/
|
|
19
|
+
status: 'updated' | 'stale' | 'missing' | 'skipped';
|
|
20
|
+
}
|
|
21
|
+
/** Summary counts for the "Packaged:" end-of-build line. */
|
|
22
|
+
export interface AuditSummary {
|
|
23
|
+
updated: number;
|
|
24
|
+
stale: number;
|
|
25
|
+
missing: number;
|
|
26
|
+
skipped: number;
|
|
27
|
+
entries: AuditEntry[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Decides whether a source path should be packaged. Returns true for paths
|
|
31
|
+
* whose extension or directory fragment matches a known-packaged pattern.
|
|
32
|
+
* @param sourcePath Engine-relative POSIX path (for example browser/app/profile/pref.js).
|
|
33
|
+
* @returns True when the path implies packaging.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isPackageablePath(sourcePath: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Runs the post-build audit. Emits per-file warnings for missing or
|
|
38
|
+
* stale artifacts and a summary info line at the end. Always returns
|
|
39
|
+
* the summary; never throws on audit failure (the audit itself must
|
|
40
|
+
* never fail a successful build).
|
|
41
|
+
* @param projectRoot Root of the project (reserved for future fork-specific rules).
|
|
42
|
+
* @param engineDir Path to the engine directory.
|
|
43
|
+
* @param baseline Optional previous-build baseline marker.
|
|
44
|
+
* @returns Summary of artifact status counts.
|
|
45
|
+
*/
|
|
46
|
+
export declare function auditBuildArtifacts(projectRoot: string, engineDir: string, baseline: BuildBaseline | undefined): Promise<AuditSummary>;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/*
|
|
3
|
+
* Post-build dist-tree audit.
|
|
4
|
+
*
|
|
5
|
+
* Purpose: catch the class of bug where a file under engine/ was edited
|
|
6
|
+
* but never registered in moz.build, jar.mn, or package-manifest.in, so
|
|
7
|
+
* the mach build reports success but the packaged bundle carries stale
|
|
8
|
+
* or missing content. A fork-specific pref file that was never registered
|
|
9
|
+
* for packaging is the motivating case.
|
|
10
|
+
*
|
|
11
|
+
* The audit is best-effort and warn-only:
|
|
12
|
+
* - It enumerates engine files changed since the previous build baseline
|
|
13
|
+
* (git-tracked diff + workdir modifications).
|
|
14
|
+
* - For each file whose path pattern implies packaging, it resolves
|
|
15
|
+
* the expected dist artifact under obj-star/dist/binary-name-star.
|
|
16
|
+
* - A warning fires when the expected artifact is missing OR when its
|
|
17
|
+
* mtime is older than the engine source (the build was reported
|
|
18
|
+
* successful but that file's path never flowed through packaging).
|
|
19
|
+
* - False positives are acceptable at this stage: fork-specific packaging
|
|
20
|
+
* tricks FireForge doesn't know about will surface as warnings an
|
|
21
|
+
* operator can investigate. The audit never fails the build.
|
|
22
|
+
*/
|
|
23
|
+
import { stat } from 'node:fs/promises';
|
|
24
|
+
import { basename, join } from 'node:path';
|
|
25
|
+
import { toError } from '../utils/errors.js';
|
|
26
|
+
import { pathExists } from '../utils/fs.js';
|
|
27
|
+
import { info, verbose, warn } from '../utils/logger.js';
|
|
28
|
+
import { hasChanges, isMissingHeadError } from './git.js';
|
|
29
|
+
import { git } from './git-base.js';
|
|
30
|
+
import { getUntrackedFiles } from './git-status.js';
|
|
31
|
+
/** Path extensions that are conventionally packaged into the Firefox bundle. */
|
|
32
|
+
const PACKAGEABLE_EXTENSIONS = [
|
|
33
|
+
'.js',
|
|
34
|
+
'.mjs',
|
|
35
|
+
'.jsm',
|
|
36
|
+
'.css',
|
|
37
|
+
'.ftl',
|
|
38
|
+
'.xhtml',
|
|
39
|
+
'.xul',
|
|
40
|
+
'.html',
|
|
41
|
+
'.properties',
|
|
42
|
+
];
|
|
43
|
+
/** Path fragments whose contents are packaged regardless of extension. */
|
|
44
|
+
const PACKAGEABLE_PATH_FRAGMENTS = ['/app/profile/', '/chrome/', '/locales/'];
|
|
45
|
+
/** Directories that are build artifacts, not source — never audited. */
|
|
46
|
+
const IGNORE_PATH_FRAGMENTS = ['obj-', 'node_modules/', '.git/', '.cargo/', '.mozbuild/'];
|
|
47
|
+
/*
|
|
48
|
+
* Finds the first file with the given basename anywhere under the dist
|
|
49
|
+
* bundle. Scans the darwin Contents/Resources layout and the linux/win
|
|
50
|
+
* top-level layout with a depth-limited traversal so deeply-nested
|
|
51
|
+
* node_modules in the dist copy do not dominate the audit wall clock.
|
|
52
|
+
*/
|
|
53
|
+
async function findArtifactByBasename(distRoot, name, maxDepth = 10) {
|
|
54
|
+
const { readdir } = await import('node:fs/promises');
|
|
55
|
+
const stack = [{ dir: distRoot, depth: 0 }];
|
|
56
|
+
while (stack.length > 0) {
|
|
57
|
+
const entry = stack.pop();
|
|
58
|
+
if (!entry)
|
|
59
|
+
break;
|
|
60
|
+
if (entry.depth > maxDepth)
|
|
61
|
+
continue;
|
|
62
|
+
let children;
|
|
63
|
+
try {
|
|
64
|
+
children = await readdir(entry.dir, { withFileTypes: true });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
const fullPath = join(entry.dir, child.name);
|
|
71
|
+
if (child.isDirectory()) {
|
|
72
|
+
// Skip the symlinked mozbuild cache tree which contains full copies
|
|
73
|
+
// and would dominate the scan on macOS.
|
|
74
|
+
if (child.name.startsWith('.'))
|
|
75
|
+
continue;
|
|
76
|
+
stack.push({ dir: fullPath, depth: entry.depth + 1 });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (child.name === name) {
|
|
80
|
+
return fullPath;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decides whether a source path should be packaged. Returns true for paths
|
|
88
|
+
* whose extension or directory fragment matches a known-packaged pattern.
|
|
89
|
+
* @param sourcePath Engine-relative POSIX path (for example browser/app/profile/pref.js).
|
|
90
|
+
* @returns True when the path implies packaging.
|
|
91
|
+
*/
|
|
92
|
+
export function isPackageablePath(sourcePath) {
|
|
93
|
+
for (const fragment of IGNORE_PATH_FRAGMENTS) {
|
|
94
|
+
if (sourcePath.includes(fragment))
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
for (const ext of PACKAGEABLE_EXTENSIONS) {
|
|
98
|
+
if (sourcePath.endsWith(ext))
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
for (const fragment of PACKAGEABLE_PATH_FRAGMENTS) {
|
|
102
|
+
if (sourcePath.includes(fragment))
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Collects engine-relative paths changed since the baseline's HEAD SHA.
|
|
109
|
+
* Always includes modified + untracked workdir paths. When the baseline is
|
|
110
|
+
* missing or the engine has no HEAD yet, falls back to workdir-only diffs.
|
|
111
|
+
*/
|
|
112
|
+
async function collectChangedFiles(engineDir, baseline) {
|
|
113
|
+
const collected = new Set();
|
|
114
|
+
if (baseline?.engineHeadSha) {
|
|
115
|
+
try {
|
|
116
|
+
const output = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
|
|
117
|
+
for (const line of output.split('\n')) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (trimmed)
|
|
120
|
+
collected.add(trimmed);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (!isMissingHeadError(error)) {
|
|
125
|
+
verbose(`Audit: could not diff against baseline SHA — ${toError(error).message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
if (await hasChanges(engineDir)) {
|
|
131
|
+
const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
|
|
132
|
+
for (const line of worktreeDiff.split('\n')) {
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
if (trimmed)
|
|
135
|
+
collected.add(trimmed);
|
|
136
|
+
}
|
|
137
|
+
const untracked = await getUntrackedFiles(engineDir);
|
|
138
|
+
for (const file of untracked) {
|
|
139
|
+
collected.add(file);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
verbose(`Audit: could not enumerate workdir changes — ${toError(error).message}`);
|
|
145
|
+
}
|
|
146
|
+
return [...collected].sort();
|
|
147
|
+
}
|
|
148
|
+
/*
|
|
149
|
+
* Finds the unique obj-star directory with a dist subtree, or undefined
|
|
150
|
+
* when zero or multiple match. The ambiguous case is already rejected
|
|
151
|
+
* by pre-flight in build.ts, so the auditor only has to handle
|
|
152
|
+
* one-or-none.
|
|
153
|
+
*/
|
|
154
|
+
async function resolveDistRoot(engineDir) {
|
|
155
|
+
const { readdir } = await import('node:fs/promises');
|
|
156
|
+
let entries;
|
|
157
|
+
try {
|
|
158
|
+
entries = await readdir(engineDir);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
const objDirs = entries.filter((e) => e.startsWith('obj-'));
|
|
164
|
+
for (const objDir of objDirs) {
|
|
165
|
+
const distPath = join(engineDir, objDir, 'dist');
|
|
166
|
+
if (await pathExists(distPath)) {
|
|
167
|
+
return distPath;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Runs the post-build audit. Emits per-file warnings for missing or
|
|
174
|
+
* stale artifacts and a summary info line at the end. Always returns
|
|
175
|
+
* the summary; never throws on audit failure (the audit itself must
|
|
176
|
+
* never fail a successful build).
|
|
177
|
+
* @param projectRoot Root of the project (reserved for future fork-specific rules).
|
|
178
|
+
* @param engineDir Path to the engine directory.
|
|
179
|
+
* @param baseline Optional previous-build baseline marker.
|
|
180
|
+
* @returns Summary of artifact status counts.
|
|
181
|
+
*/
|
|
182
|
+
export async function auditBuildArtifacts(projectRoot, engineDir, baseline) {
|
|
183
|
+
void projectRoot;
|
|
184
|
+
const summary = {
|
|
185
|
+
updated: 0,
|
|
186
|
+
stale: 0,
|
|
187
|
+
missing: 0,
|
|
188
|
+
skipped: 0,
|
|
189
|
+
entries: [],
|
|
190
|
+
};
|
|
191
|
+
const distRoot = await resolveDistRoot(engineDir);
|
|
192
|
+
if (!distRoot) {
|
|
193
|
+
verbose('Audit skipped: no dist tree found under obj-*/dist/.');
|
|
194
|
+
return summary;
|
|
195
|
+
}
|
|
196
|
+
const changed = await collectChangedFiles(engineDir, baseline);
|
|
197
|
+
if (changed.length === 0) {
|
|
198
|
+
return summary;
|
|
199
|
+
}
|
|
200
|
+
for (const source of changed) {
|
|
201
|
+
if (!isPackageablePath(source)) {
|
|
202
|
+
summary.skipped += 1;
|
|
203
|
+
summary.entries.push({ source, artifact: undefined, status: 'skipped' });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const sourcePath = join(engineDir, source);
|
|
207
|
+
let sourceMtime;
|
|
208
|
+
try {
|
|
209
|
+
const sourceStat = await stat(sourcePath);
|
|
210
|
+
sourceMtime = sourceStat.mtimeMs;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// File was deleted since the diff was computed. Skip — a deletion
|
|
214
|
+
// that didn't propagate to the dist tree is a distinct class of bug
|
|
215
|
+
// we don't audit yet.
|
|
216
|
+
summary.skipped += 1;
|
|
217
|
+
summary.entries.push({ source, artifact: undefined, status: 'skipped' });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const artifact = await findArtifactByBasename(distRoot, basename(source));
|
|
221
|
+
if (!artifact) {
|
|
222
|
+
summary.missing += 1;
|
|
223
|
+
summary.entries.push({ source, artifact: undefined, status: 'missing' });
|
|
224
|
+
warn(`Audit: engine/${source} was touched but no packaged artifact with basename "${basename(source)}" was found under ${distRoot}. Missing moz.build / jar.mn / package-manifest.in registration?`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
let artifactMtime;
|
|
228
|
+
try {
|
|
229
|
+
const artifactStat = await stat(artifact);
|
|
230
|
+
artifactMtime = artifactStat.mtimeMs;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Disappeared after the directory scan; treat as missing.
|
|
234
|
+
summary.missing += 1;
|
|
235
|
+
summary.entries.push({ source, artifact, status: 'missing' });
|
|
236
|
+
warn(`Audit: engine/${source} has no readable packaged artifact at ${artifact} (disappeared during audit).`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (artifactMtime + 1 < sourceMtime) {
|
|
240
|
+
summary.stale += 1;
|
|
241
|
+
summary.entries.push({ source, artifact, status: 'stale' });
|
|
242
|
+
warn(`Audit: engine/${source} is newer than its packaged artifact ${artifact}. Build reported success but the file's path may not flow through packaging — check moz.build / jar.mn entries.`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
summary.updated += 1;
|
|
246
|
+
summary.entries.push({ source, artifact, status: 'updated' });
|
|
247
|
+
}
|
|
248
|
+
info(`Packaged: ${summary.updated} updated, ${summary.stale} stale, ${summary.missing} missing, ${summary.skipped} skipped`);
|
|
249
|
+
return summary;
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=build-audit.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persists a marker describing the state of the engine tree at the time of
|
|
3
|
+
* the last successful `fireforge build`. Two downstream consumers share this
|
|
4
|
+
* marker:
|
|
5
|
+
*
|
|
6
|
+
* - `build-audit`: after a build succeeds, compare engine files touched
|
|
7
|
+
* since the baseline against the dist bundle to flag silent
|
|
8
|
+
* packaging drops (e.g. a pref file never registered in moz.build).
|
|
9
|
+
* - `build-prepare`: before a build starts, detect whether any
|
|
10
|
+
* `moz.build` / `moz.configure` / `Makefile.in` changed since the
|
|
11
|
+
* baseline and run `mach configure` before the build step so the
|
|
12
|
+
* recursive-make backend isn't stale.
|
|
13
|
+
*
|
|
14
|
+
* The marker lives under `.fireforge/last-build.json`. It is written only
|
|
15
|
+
* on successful build completion; a failed build does not update it, so a
|
|
16
|
+
* subsequent run still audits against the last known-good tree.
|
|
17
|
+
*/
|
|
18
|
+
/** Shape of the on-disk baseline marker. */
|
|
19
|
+
export interface BuildBaseline {
|
|
20
|
+
/** SHA of engine HEAD at the time the build succeeded. */
|
|
21
|
+
engineHeadSha: string;
|
|
22
|
+
/**
|
|
23
|
+
* ISO-8601 timestamp of when the baseline was recorded. Informational —
|
|
24
|
+
* downstream code keys off `engineHeadSha` for diffs, but the timestamp
|
|
25
|
+
* helps operators reason about stale markers.
|
|
26
|
+
*/
|
|
27
|
+
builtAt: string;
|
|
28
|
+
/**
|
|
29
|
+
* The binaryName used at build time. Captured so the dist-tree audit
|
|
30
|
+
* can resolve the expected bundle root under obj-star/dist/ even when
|
|
31
|
+
* the project has since been renamed.
|
|
32
|
+
*/
|
|
33
|
+
binaryName: string;
|
|
34
|
+
}
|
|
35
|
+
/** Name of the last-build marker file under `.fireforge/`. */
|
|
36
|
+
export declare const BUILD_BASELINE_FILENAME = "last-build.json";
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the on-disk path of the build baseline marker.
|
|
39
|
+
* @param projectRoot - Root directory of the project
|
|
40
|
+
* @returns Absolute path of the marker file
|
|
41
|
+
*/
|
|
42
|
+
export declare function getBuildBaselinePath(projectRoot: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Reads the last-build baseline if present. Returns undefined when no
|
|
45
|
+
* previous successful build has been recorded — callers must tolerate that
|
|
46
|
+
* path (first build, cleaned workspace).
|
|
47
|
+
* @param projectRoot - Root directory of the project
|
|
48
|
+
*/
|
|
49
|
+
export declare function readBuildBaseline(projectRoot: string): Promise<BuildBaseline | undefined>;
|
|
50
|
+
/**
|
|
51
|
+
* Records a successful build by writing a fresh baseline marker. Captures
|
|
52
|
+
* engine HEAD SHA (or an empty string when the engine has no HEAD yet) and
|
|
53
|
+
* the current binaryName. Caller is responsible for only invoking this
|
|
54
|
+
* after the build exit code was zero.
|
|
55
|
+
* @param projectRoot - Root directory of the project
|
|
56
|
+
* @param engineDir - Path to the engine directory
|
|
57
|
+
* @param binaryName - Current `binaryName` from fireforge.json
|
|
58
|
+
*/
|
|
59
|
+
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string): Promise<void>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Persists a marker describing the state of the engine tree at the time of
|
|
4
|
+
* the last successful `fireforge build`. Two downstream consumers share this
|
|
5
|
+
* marker:
|
|
6
|
+
*
|
|
7
|
+
* - `build-audit`: after a build succeeds, compare engine files touched
|
|
8
|
+
* since the baseline against the dist bundle to flag silent
|
|
9
|
+
* packaging drops (e.g. a pref file never registered in moz.build).
|
|
10
|
+
* - `build-prepare`: before a build starts, detect whether any
|
|
11
|
+
* `moz.build` / `moz.configure` / `Makefile.in` changed since the
|
|
12
|
+
* baseline and run `mach configure` before the build step so the
|
|
13
|
+
* recursive-make backend isn't stale.
|
|
14
|
+
*
|
|
15
|
+
* The marker lives under `.fireforge/last-build.json`. It is written only
|
|
16
|
+
* on successful build completion; a failed build does not update it, so a
|
|
17
|
+
* subsequent run still audits against the last known-good tree.
|
|
18
|
+
*/
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
21
|
+
import { FIREFORGE_DIR } from './config-paths.js';
|
|
22
|
+
import { getHead, isMissingHeadError } from './git.js';
|
|
23
|
+
/** Name of the last-build marker file under `.fireforge/`. */
|
|
24
|
+
export const BUILD_BASELINE_FILENAME = 'last-build.json';
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the on-disk path of the build baseline marker.
|
|
27
|
+
* @param projectRoot - Root directory of the project
|
|
28
|
+
* @returns Absolute path of the marker file
|
|
29
|
+
*/
|
|
30
|
+
export function getBuildBaselinePath(projectRoot) {
|
|
31
|
+
return join(projectRoot, FIREFORGE_DIR, BUILD_BASELINE_FILENAME);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads the last-build baseline if present. Returns undefined when no
|
|
35
|
+
* previous successful build has been recorded — callers must tolerate that
|
|
36
|
+
* path (first build, cleaned workspace).
|
|
37
|
+
* @param projectRoot - Root directory of the project
|
|
38
|
+
*/
|
|
39
|
+
export async function readBuildBaseline(projectRoot) {
|
|
40
|
+
const path = getBuildBaselinePath(projectRoot);
|
|
41
|
+
if (!(await pathExists(path))) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return await readJson(path);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A corrupt marker is equivalent to no marker — the audit/auto-configure
|
|
49
|
+
// will treat it as "first build" rather than block on the inconsistency.
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Records a successful build by writing a fresh baseline marker. Captures
|
|
55
|
+
* engine HEAD SHA (or an empty string when the engine has no HEAD yet) and
|
|
56
|
+
* the current binaryName. Caller is responsible for only invoking this
|
|
57
|
+
* after the build exit code was zero.
|
|
58
|
+
* @param projectRoot - Root directory of the project
|
|
59
|
+
* @param engineDir - Path to the engine directory
|
|
60
|
+
* @param binaryName - Current `binaryName` from fireforge.json
|
|
61
|
+
*/
|
|
62
|
+
export async function writeBuildBaseline(projectRoot, engineDir, binaryName) {
|
|
63
|
+
let engineHeadSha = '';
|
|
64
|
+
try {
|
|
65
|
+
engineHeadSha = await getHead(engineDir);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// Engine may be an unborn branch (freshly cloned + reset, or mid-import)
|
|
69
|
+
// — record an empty SHA and let downstream fall back to "no prior state"
|
|
70
|
+
// behavior. Any other git failure is bubbled up; we don't want to
|
|
71
|
+
// silently write a garbage marker.
|
|
72
|
+
if (!isMissingHeadError(error)) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const baseline = {
|
|
77
|
+
engineHeadSha,
|
|
78
|
+
builtAt: new Date().toISOString(),
|
|
79
|
+
binaryName,
|
|
80
|
+
};
|
|
81
|
+
await writeJson(getBuildBaselinePath(projectRoot), baseline);
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=build-baseline.js.map
|
|
@@ -3,13 +3,32 @@
|
|
|
3
3
|
* story cleanup, branding setup, Furnace component application, and mozconfig generation.
|
|
4
4
|
*/
|
|
5
5
|
import type { FireForgeConfig, ProjectPaths } from '../types/config.js';
|
|
6
|
+
import type { BuildBaseline } from './build-baseline.js';
|
|
6
7
|
/**
|
|
7
8
|
* Result of the build preparation phase.
|
|
8
9
|
*/
|
|
9
10
|
export interface BuildPreparation {
|
|
10
11
|
/** Number of Furnace components applied (0 if none or no furnace.json) */
|
|
11
12
|
furnaceApplied: number;
|
|
13
|
+
/** True when `mach configure` was auto-run to refresh a stale backend. */
|
|
14
|
+
reconfigured: boolean;
|
|
12
15
|
}
|
|
16
|
+
/** Options for {@link prepareBuildEnvironment}. */
|
|
17
|
+
export interface PrepareBuildOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Previous successful-build baseline, used to detect `moz.build` /
|
|
20
|
+
* `moz.configure` / `Makefile.in` changes that require a fresh
|
|
21
|
+
* `mach configure` before the build. When undefined, the auto-configure
|
|
22
|
+
* step is skipped — there's no reference point for what "changed since"
|
|
23
|
+
* means.
|
|
24
|
+
*/
|
|
25
|
+
previousBaseline?: BuildBaseline | undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when the file path matches a pattern that forces
|
|
29
|
+
* `mach configure` to regenerate the backend. Exported for testing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isBackendInvalidatingFile(path: string): boolean;
|
|
13
32
|
/**
|
|
14
33
|
* Runs the shared pre-flight steps for build and package commands:
|
|
15
34
|
* 1. Cleans Furnace stories from engine (prevents leaking into production)
|
|
@@ -22,4 +41,4 @@ export interface BuildPreparation {
|
|
|
22
41
|
* @param config - Loaded FireForge configuration
|
|
23
42
|
* @returns Preparation results
|
|
24
43
|
*/
|
|
25
|
-
export declare function prepareBuildEnvironment(projectRoot: string, paths: ProjectPaths, config: FireForgeConfig): Promise<BuildPreparation>;
|
|
44
|
+
export declare function prepareBuildEnvironment(projectRoot: string, paths: ProjectPaths, config: FireForgeConfig, options?: PrepareBuildOptions): Promise<BuildPreparation>;
|