@hominis/fireforge 0.15.6 → 0.15.7
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 +46 -0
- package/README.md +68 -5
- package/dist/src/commands/build.js +60 -3
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +17 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +18 -0
- package/dist/src/commands/furnace/chrome-doc-tests.d.ts +23 -0
- package/dist/src/commands/furnace/chrome-doc-tests.js +120 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +11 -0
- package/dist/src/commands/furnace/chrome-doc.js +37 -4
- package/dist/src/commands/furnace/create-dry-run.d.ts +31 -0
- package/dist/src/commands/furnace/create-dry-run.js +95 -0
- package/dist/src/commands/furnace/create-templates.js +14 -0
- package/dist/src/commands/furnace/create.js +28 -24
- package/dist/src/commands/furnace/index.js +3 -1
- package/dist/src/commands/lint.d.ts +17 -2
- package/dist/src/commands/lint.js +25 -2
- package/dist/src/commands/register.d.ts +1 -1
- package/dist/src/commands/register.js +30 -7
- package/dist/src/commands/test.js +16 -1
- package/dist/src/core/build-audit-registration.d.ts +80 -0
- package/dist/src/core/build-audit-registration.js +187 -0
- package/dist/src/core/build-audit-transforms.d.ts +23 -0
- package/dist/src/core/build-audit-transforms.js +94 -0
- package/dist/src/core/build-audit.js +107 -7
- package/dist/src/core/furnace-validate-registration.d.ts +6 -4
- package/dist/src/core/furnace-validate-registration.js +66 -6
- package/dist/src/core/mach-build-artifacts.d.ts +44 -0
- package/dist/src/core/mach-build-artifacts.js +104 -3
- package/dist/src/core/mach.d.ts +1 -1
- package/dist/src/core/mach.js +1 -1
- package/dist/src/core/test-stale-check.d.ts +42 -0
- package/dist/src/core/test-stale-check.js +114 -0
- package/dist/src/types/commands/options.d.ts +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/*
|
|
3
|
+
* Registration-aware artifact resolution for the post-build dist-tree audit.
|
|
4
|
+
*
|
|
5
|
+
* The basename-plus-similarity heuristic in `build-audit-resolve.ts` cannot
|
|
6
|
+
* distinguish two unrelated files that share a source basename. Motivating
|
|
7
|
+
* case: one fork registers `content/mybrowser.js` in `browser/base/jar.mn`
|
|
8
|
+
* (packaged under `chrome/browser/content/browser/mybrowser.js`) while an
|
|
9
|
+
* unrelated patch registers a `mybrowser.js` pref file under
|
|
10
|
+
* `browser/defaults/preferences/`. The basename walker surfaces both
|
|
11
|
+
* candidates, `scoreCandidate` awards them an equal trailing-overlap score
|
|
12
|
+
* (basename only, no meaningful mid-path match), and whichever the
|
|
13
|
+
* directory walk hits first wins — frequently the wrong one.
|
|
14
|
+
*
|
|
15
|
+
* This module anchors resolution to the `(source)` reference inside
|
|
16
|
+
* `jar.mn`. For a source under audit we walk its ancestor directories for
|
|
17
|
+
* an owning `jar.mn`, find the entry whose `(source)` resolves to our
|
|
18
|
+
* path, and expose both the target path recorded in the entry and the
|
|
19
|
+
* manifest that owns it. Callers in `build-audit.ts` prefer candidates
|
|
20
|
+
* whose absolute dist-tree path ends with the registered target, and
|
|
21
|
+
* report an unambiguous "registered but not packaged" miss when no such
|
|
22
|
+
* candidate exists — rather than falling through to the heuristic which
|
|
23
|
+
* would pick an unrelated same-basename file.
|
|
24
|
+
*
|
|
25
|
+
* Fallback semantics: when no jar.mn registration is found, the caller
|
|
26
|
+
* is expected to use the similarity heuristic. Registration wins when it
|
|
27
|
+
* exists; the heuristic fills the gap for sources registered through
|
|
28
|
+
* moz.build (`FINAL_TARGET_FILES`, `JS_PREFERENCE_FILES`, etc.) or
|
|
29
|
+
* `package-manifest.in` entries, which this module intentionally does
|
|
30
|
+
* not parse — supporting every Firefox registration surface would bloat
|
|
31
|
+
* the audit, and the heuristic's remaining weak case (unrelated same-
|
|
32
|
+
* basename hits) is surfaced to the operator in the warning copy.
|
|
33
|
+
*/
|
|
34
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
35
|
+
import { pathExists, readText } from '../utils/fs.js';
|
|
36
|
+
import { findAllByBasename } from './build-audit-resolve.js';
|
|
37
|
+
/** Ceiling on ancestor-directory hops when searching for an owning jar.mn. */
|
|
38
|
+
const MAX_JAR_MN_SCAN_DEPTH = 8;
|
|
39
|
+
/**
|
|
40
|
+
* Parses a single jar.mn line into `{ target, source }` when the line is a
|
|
41
|
+
* content entry with an explicit `(source)` reference. Returns undefined
|
|
42
|
+
* for comments, headers (`browser.jar:`), `%` manifest directives, blank
|
|
43
|
+
* lines, and entries without a source reference.
|
|
44
|
+
*
|
|
45
|
+
* Accepted entry shapes:
|
|
46
|
+
* ` content/browser/foo.js (content/foo.js)` bare
|
|
47
|
+
* `* content/browser/foo.js (content/foo.js)` `*` = preprocessed
|
|
48
|
+
* `en-US.jar: content/foo.js (content/foo.js)` locale-prefixed
|
|
49
|
+
*/
|
|
50
|
+
export function parseJarMnEntry(line) {
|
|
51
|
+
if (!line)
|
|
52
|
+
return undefined;
|
|
53
|
+
const trimmed = line.trim();
|
|
54
|
+
if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('%'))
|
|
55
|
+
return undefined;
|
|
56
|
+
// Leading `*` (preprocessed) and optional `locale.jar:` prefix are dropped
|
|
57
|
+
// before matching the target/(source) pair. Both are whitespace-separated
|
|
58
|
+
// from the target entry.
|
|
59
|
+
const stripped = trimmed.replace(/^\*\s+/, '').replace(/^[A-Za-z0-9.\-_]+\.jar:\s+/, '');
|
|
60
|
+
const match = /^(\S+)\s+\(([^)]+)\)\s*$/.exec(stripped);
|
|
61
|
+
if (!match)
|
|
62
|
+
return undefined;
|
|
63
|
+
const target = (match[1] ?? '').trim();
|
|
64
|
+
const source = (match[2] ?? '').trim();
|
|
65
|
+
if (!target || !source)
|
|
66
|
+
return undefined;
|
|
67
|
+
return { target, source };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Scans a jar.mn file's contents for an entry whose source reference
|
|
71
|
+
* matches `relativeSource` (POSIX, relative to the jar.mn directory).
|
|
72
|
+
* Returns the first match; jar.mn enforces uniqueness of `(source)` in
|
|
73
|
+
* practice, so a first-match wins behaviour is adequate.
|
|
74
|
+
*/
|
|
75
|
+
export function findJarMnEntryForSource(content, relativeSource) {
|
|
76
|
+
const normalized = relativeSource.replace(/\\/g, '/');
|
|
77
|
+
for (const line of content.split('\n')) {
|
|
78
|
+
const parsed = parseJarMnEntry(line);
|
|
79
|
+
if (!parsed)
|
|
80
|
+
continue;
|
|
81
|
+
if (parsed.source === normalized)
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Walks from the source's directory upward to the engine root, returning
|
|
88
|
+
* the first jar.mn entry that registers the given source. Returns undefined
|
|
89
|
+
* when no ancestor jar.mn claims the source.
|
|
90
|
+
*
|
|
91
|
+
* @param engineDir Absolute engine root; walk halts here.
|
|
92
|
+
* @param source Engine-relative POSIX source path.
|
|
93
|
+
*/
|
|
94
|
+
export async function findRegisteredTarget(engineDir, source) {
|
|
95
|
+
const sourceAbs = join(engineDir, source);
|
|
96
|
+
const root = engineDir.replace(/[/\\]+$/, '');
|
|
97
|
+
let current = dirname(sourceAbs);
|
|
98
|
+
let depth = 0;
|
|
99
|
+
while (depth <= MAX_JAR_MN_SCAN_DEPTH &&
|
|
100
|
+
(current === root || current.startsWith(`${root}/`) || current.startsWith(`${root}\\`))) {
|
|
101
|
+
const jarMn = join(current, 'jar.mn');
|
|
102
|
+
if (await pathExists(jarMn)) {
|
|
103
|
+
let content;
|
|
104
|
+
try {
|
|
105
|
+
content = await readText(jarMn);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
content = '';
|
|
109
|
+
}
|
|
110
|
+
const rel = relative(current, sourceAbs).split(sep).join('/');
|
|
111
|
+
const entry = findJarMnEntryForSource(content, rel);
|
|
112
|
+
if (entry) {
|
|
113
|
+
return { target: entry.target, source: entry.source, jarManifest: jarMn };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const parent = dirname(current);
|
|
117
|
+
if (parent === current)
|
|
118
|
+
break;
|
|
119
|
+
current = parent;
|
|
120
|
+
depth += 1;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Normalises an absolute filesystem path to POSIX separators so the
|
|
126
|
+
* suffix comparison against a jar.mn target (always POSIX) is platform-
|
|
127
|
+
* independent.
|
|
128
|
+
*/
|
|
129
|
+
function toPosix(path) {
|
|
130
|
+
return path.split(sep).join('/');
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Probes the dist tree for the artifact registered against the given
|
|
134
|
+
* source. Returns the matched candidate and the registration hit that
|
|
135
|
+
* anchored it; undefined when the source has no owning jar.mn or when
|
|
136
|
+
* no same-basename candidate under the search roots ends with the
|
|
137
|
+
* registered target path.
|
|
138
|
+
*
|
|
139
|
+
* Suffix-matching against the target path is intentional: jar.mn targets
|
|
140
|
+
* are relative to the jar root (`browser.jar:`, `toolkit.jar:`), but the
|
|
141
|
+
* dist tree prefixes every entry with a jar-specific directory
|
|
142
|
+
* (`.../chrome/browser/content/browser/…`). The source basename plus the
|
|
143
|
+
* target suffix are unambiguous across every packaging convention we
|
|
144
|
+
* care about.
|
|
145
|
+
*
|
|
146
|
+
* @param engineDir Absolute engine root.
|
|
147
|
+
* @param source Engine-relative POSIX source path.
|
|
148
|
+
* @param searchRoots Absolute roots to probe (dist/, _tests/).
|
|
149
|
+
*/
|
|
150
|
+
export async function resolveArtifactByRegistration(engineDir, source, searchRoots) {
|
|
151
|
+
const hit = await findRegisteredTarget(engineDir, source);
|
|
152
|
+
if (!hit)
|
|
153
|
+
return undefined;
|
|
154
|
+
const name = basename(source);
|
|
155
|
+
const targetSuffix = `/${hit.target.replace(/^\/+/, '')}`;
|
|
156
|
+
const candidates = [];
|
|
157
|
+
for (const root of searchRoots) {
|
|
158
|
+
const found = await findAllByBasename(root, name);
|
|
159
|
+
candidates.push(...found);
|
|
160
|
+
}
|
|
161
|
+
for (const candidate of candidates) {
|
|
162
|
+
if (toPosix(candidate).endsWith(targetSuffix)) {
|
|
163
|
+
return { artifact: candidate, hit };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Returns the absolute paths of every same-basename candidate under the
|
|
170
|
+
* given search roots. Used by the audit to enumerate ALL false-match
|
|
171
|
+
* candidates when the heuristic fallback downgrades to "missing" — the
|
|
172
|
+
* operator needs to see the full set, not just the scorer's pick, to
|
|
173
|
+
* distinguish a registration bug from a genuine packaging drop.
|
|
174
|
+
*
|
|
175
|
+
* @param source Engine-relative POSIX source path.
|
|
176
|
+
* @param searchRoots Absolute roots to scan.
|
|
177
|
+
*/
|
|
178
|
+
export async function collectSameBasenameCandidates(source, searchRoots) {
|
|
179
|
+
const name = basename(source);
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const root of searchRoots) {
|
|
182
|
+
const found = await findAllByBasename(root, name);
|
|
183
|
+
out.push(...found);
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=build-audit-registration.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the expected chrome-tree suffix for an engine-relative POSIX
|
|
3
|
+
* source path when the path falls under a known transform prefix;
|
|
4
|
+
* undefined otherwise.
|
|
5
|
+
*
|
|
6
|
+
* @param source Engine-relative POSIX source path.
|
|
7
|
+
*/
|
|
8
|
+
export declare function expectedChromeSuffix(source: string): string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Probes the dist tree for the artifact implied by a known
|
|
11
|
+
* source→chrome transform. Returns the first absolute candidate whose
|
|
12
|
+
* POSIX path ends with the expected chrome suffix, or undefined when
|
|
13
|
+
* no transform applies or no candidate matches.
|
|
14
|
+
*
|
|
15
|
+
* The transform check is treated as high-confidence by `build-audit.ts`
|
|
16
|
+
* (callers pass `{ registered: true }` to `evaluateArtifactMtime`), so
|
|
17
|
+
* a match bypasses the structural-relation check that rejects generic
|
|
18
|
+
* basename collisions.
|
|
19
|
+
*
|
|
20
|
+
* @param source Engine-relative POSIX source path.
|
|
21
|
+
* @param searchRoots Absolute roots to probe (dist/, _tests/).
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveArtifactByKnownTransform(source: string, searchRoots: readonly string[]): Promise<string | undefined>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/*
|
|
3
|
+
* Known source→packaging path transforms for the post-build dist-tree audit.
|
|
4
|
+
*
|
|
5
|
+
* Motivating case: a source at `engine/browser/base/content/foo.js` ships
|
|
6
|
+
* under `chrome/browser/content/browser/foo.js`. If an unrelated patch
|
|
7
|
+
* registers a different `foo.js` elsewhere in the tree (e.g. a pref file
|
|
8
|
+
* under `browser/defaults/preferences/foo.js`), the basename walker
|
|
9
|
+
* surfaces both candidates, `scoreCandidate` awards them an identical
|
|
10
|
+
* trailing-overlap score (basename only; every intermediate segment is
|
|
11
|
+
* in the generic list), and whichever the directory walk hits first wins.
|
|
12
|
+
* The heuristic then declares the chosen candidate "not structurally
|
|
13
|
+
* related" and reports the correctly-packaged chrome resource as missing.
|
|
14
|
+
*
|
|
15
|
+
* The transforms below anchor resolution to the well-known subtree→chrome
|
|
16
|
+
* conventions used by upstream mozilla-central jar.mn. When a source
|
|
17
|
+
* matches one of these prefixes, a candidate whose absolute path ends
|
|
18
|
+
* with the implied chrome suffix is treated as a confident match — the
|
|
19
|
+
* scorer never runs and the structural-relation check is bypassed.
|
|
20
|
+
*
|
|
21
|
+
* Scope is intentionally narrow: only subtrees whose packaging target is
|
|
22
|
+
* stable across every fork we know about. A fork that reroutes a known
|
|
23
|
+
* subtree can still win by adding `(source)` annotations in its own
|
|
24
|
+
* `jar.mn`, which `resolveArtifactByRegistration` consults first.
|
|
25
|
+
*/
|
|
26
|
+
import { basename, sep } from 'node:path';
|
|
27
|
+
import { findAllByBasename } from './build-audit-resolve.js';
|
|
28
|
+
/**
|
|
29
|
+
* Table of `prefix → chrome-suffix` transforms. Each rule names an
|
|
30
|
+
* engine-relative subtree prefix and produces the expected dist-tree
|
|
31
|
+
* suffix for a file under it. Rules are evaluated in array order and
|
|
32
|
+
* the first-matching prefix wins, so `toolkit/content/widgets/` must
|
|
33
|
+
* precede the looser `toolkit/content/`.
|
|
34
|
+
*/
|
|
35
|
+
const KNOWN_TRANSFORMS = [
|
|
36
|
+
{
|
|
37
|
+
prefix: 'browser/base/content/',
|
|
38
|
+
build: (rest) => `chrome/browser/content/browser/${rest}`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
prefix: 'toolkit/content/widgets/',
|
|
42
|
+
build: (rest) => `chrome/toolkit/content/global/elements/${rest}`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
prefix: 'toolkit/content/',
|
|
46
|
+
build: (rest) => `chrome/toolkit/content/global/${rest}`,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Returns the expected chrome-tree suffix for an engine-relative POSIX
|
|
51
|
+
* source path when the path falls under a known transform prefix;
|
|
52
|
+
* undefined otherwise.
|
|
53
|
+
*
|
|
54
|
+
* @param source Engine-relative POSIX source path.
|
|
55
|
+
*/
|
|
56
|
+
export function expectedChromeSuffix(source) {
|
|
57
|
+
for (const rule of KNOWN_TRANSFORMS) {
|
|
58
|
+
if (source.startsWith(rule.prefix)) {
|
|
59
|
+
return rule.build(source.slice(rule.prefix.length));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Probes the dist tree for the artifact implied by a known
|
|
66
|
+
* source→chrome transform. Returns the first absolute candidate whose
|
|
67
|
+
* POSIX path ends with the expected chrome suffix, or undefined when
|
|
68
|
+
* no transform applies or no candidate matches.
|
|
69
|
+
*
|
|
70
|
+
* The transform check is treated as high-confidence by `build-audit.ts`
|
|
71
|
+
* (callers pass `{ registered: true }` to `evaluateArtifactMtime`), so
|
|
72
|
+
* a match bypasses the structural-relation check that rejects generic
|
|
73
|
+
* basename collisions.
|
|
74
|
+
*
|
|
75
|
+
* @param source Engine-relative POSIX source path.
|
|
76
|
+
* @param searchRoots Absolute roots to probe (dist/, _tests/).
|
|
77
|
+
*/
|
|
78
|
+
export async function resolveArtifactByKnownTransform(source, searchRoots) {
|
|
79
|
+
const suffix = expectedChromeSuffix(source);
|
|
80
|
+
if (!suffix)
|
|
81
|
+
return undefined;
|
|
82
|
+
const name = basename(source);
|
|
83
|
+
const suffixWithSlash = `/${suffix}`;
|
|
84
|
+
for (const root of searchRoots) {
|
|
85
|
+
const candidates = await findAllByBasename(root, name);
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
if (candidate.split(sep).join('/').endsWith(suffixWithSlash)) {
|
|
88
|
+
return candidate;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=build-audit-transforms.js.map
|
|
@@ -39,7 +39,9 @@ import { toError } from '../utils/errors.js';
|
|
|
39
39
|
import { pathExists } from '../utils/fs.js';
|
|
40
40
|
import { info, verbose, warn } from '../utils/logger.js';
|
|
41
41
|
import { detectPlatformGate } from './build-audit-platform.js';
|
|
42
|
+
import { collectSameBasenameCandidates, findRegisteredTarget, resolveArtifactByRegistration, } from './build-audit-registration.js';
|
|
42
43
|
import { countTrailingSegmentMatches, isTestPath, resolveBestArtifact, } from './build-audit-resolve.js';
|
|
44
|
+
import { resolveArtifactByKnownTransform } from './build-audit-transforms.js';
|
|
43
45
|
import { hasChanges, isMissingHeadError } from './git.js';
|
|
44
46
|
import { git } from './git-base.js';
|
|
45
47
|
import { getUntrackedFiles } from './git-status.js';
|
|
@@ -317,6 +319,40 @@ async function auditSinglePath(source, ctx) {
|
|
|
317
319
|
return { source, artifact: undefined, status: 'skipped' };
|
|
318
320
|
}
|
|
319
321
|
const roots = searchRootsFor(source, ctx.distRoot, ctx.testsRoot);
|
|
322
|
+
// Registration-aware resolution first: a `jar.mn` entry whose `(source)`
|
|
323
|
+
// references this file is authoritative over the basename-similarity
|
|
324
|
+
// heuristic. The motivating case is a fork that adds `content/foo.js`
|
|
325
|
+
// in `browser/base/jar.mn` while an unrelated patch registers a pref
|
|
326
|
+
// file of the same basename elsewhere — the heuristic cannot
|
|
327
|
+
// distinguish them, so the audit falsely reports "missing" against the
|
|
328
|
+
// correctly-packaged file.
|
|
329
|
+
const registered = await resolveArtifactByRegistration(ctx.engineDir, source, roots);
|
|
330
|
+
if (registered) {
|
|
331
|
+
return evaluateArtifactMtime(source, registered.artifact, sourceMtime, { registered: true });
|
|
332
|
+
}
|
|
333
|
+
// Registration exists but no matching dist entry: explicit miss,
|
|
334
|
+
// distinct from an unregistered source. This surfaces in the warning so
|
|
335
|
+
// the operator knows the jar.mn entry is intact and packaging is the
|
|
336
|
+
// bug, not the source registration.
|
|
337
|
+
const registrationMissed = await reportRegistrationMiss(ctx.engineDir, source, roots);
|
|
338
|
+
if (registrationMissed)
|
|
339
|
+
return registrationMissed;
|
|
340
|
+
// Known-transform resolution comes before the similarity heuristic so a
|
|
341
|
+
// source under `browser/base/content/` (or another prefix whose chrome
|
|
342
|
+
// target is stable across forks) is matched against its expected
|
|
343
|
+
// `chrome/...` suffix rather than whichever same-basename candidate the
|
|
344
|
+
// directory walk happened to hit first. Motivating case: a source at
|
|
345
|
+
// `engine/browser/base/content/foo.js` whose correctly-packaged artifact
|
|
346
|
+
// lives at `chrome/browser/content/browser/foo.js` but which an unrelated
|
|
347
|
+
// patch also placed under `browser/defaults/preferences/foo.js`; every
|
|
348
|
+
// intermediate segment of the source is in the scorer's generic list,
|
|
349
|
+
// so `resolveBestArtifact` picks whichever hit first and `isConfidentMatch`
|
|
350
|
+
// rejects every candidate, classifying the correctly-packaged file as
|
|
351
|
+
// "missing" even though packaging had landed it.
|
|
352
|
+
const byTransform = await resolveArtifactByKnownTransform(source, roots);
|
|
353
|
+
if (byTransform) {
|
|
354
|
+
return evaluateArtifactMtime(source, byTransform, sourceMtime, { registered: true });
|
|
355
|
+
}
|
|
320
356
|
const artifact = await resolveBestArtifact(source, roots);
|
|
321
357
|
if (!artifact) {
|
|
322
358
|
const where = isTestPath(source) ? '_tests/' : 'dist/';
|
|
@@ -327,6 +363,42 @@ async function auditSinglePath(source, ctx) {
|
|
|
327
363
|
warning: `Audit: engine/${source} was touched but no packaged artifact with basename "${basename(source)}" was found under ${where}. Missing moz.build / jar.mn / package-manifest.in registration?`,
|
|
328
364
|
};
|
|
329
365
|
}
|
|
366
|
+
return evaluateArtifactMtime(source, artifact, sourceMtime, { registered: false, roots });
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Short-circuits the audit for sources that are registered in a jar.mn
|
|
370
|
+
* but whose target path is absent from every search root. Returns the
|
|
371
|
+
* miss entry when the registration lookup saw a `(source)` claim but
|
|
372
|
+
* no dist candidate endswith the target; undefined otherwise.
|
|
373
|
+
*/
|
|
374
|
+
async function reportRegistrationMiss(engineDir, source, roots) {
|
|
375
|
+
const hit = await findRegisteredTarget(engineDir, source);
|
|
376
|
+
if (!hit)
|
|
377
|
+
return undefined;
|
|
378
|
+
const where = isTestPath(source) ? '_tests/' : 'dist/';
|
|
379
|
+
// Name every same-basename hit so the operator sees what did land in
|
|
380
|
+
// dist, rather than guessing from a single "nearest" pick.
|
|
381
|
+
const candidates = await collectSameBasenameCandidates(source, roots);
|
|
382
|
+
const nearHits = describeCandidates(candidates);
|
|
383
|
+
const manifest = relativeManifestPath(engineDir, hit.jarManifest);
|
|
384
|
+
return {
|
|
385
|
+
source,
|
|
386
|
+
artifact: undefined,
|
|
387
|
+
status: 'missing',
|
|
388
|
+
warning: `Audit: engine/${source} is registered in ${manifest} as ` +
|
|
389
|
+
`"${hit.target} (${hit.source})" but no packaged artifact ending in "/${hit.target}" ` +
|
|
390
|
+
`was found under ${where}. Build reported success but the file's path did not ` +
|
|
391
|
+
`flow through packaging${nearHits ? ` — same-basename hits: ${nearHits}` : ''}.`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Renders packaged-artifact mtime classification (updated / stale / missing-
|
|
396
|
+
* via-disappearance) for a resolved candidate. Shared by the registration-
|
|
397
|
+
* anchored path (confident by construction) and the heuristic fallback
|
|
398
|
+
* (which still applies the structural-relatedness check before claiming
|
|
399
|
+
* `stale`).
|
|
400
|
+
*/
|
|
401
|
+
async function evaluateArtifactMtime(source, artifact, sourceMtime, mode) {
|
|
330
402
|
let artifactMtime;
|
|
331
403
|
try {
|
|
332
404
|
const artifactStat = await stat(artifact);
|
|
@@ -341,18 +413,18 @@ async function auditSinglePath(source, ctx) {
|
|
|
341
413
|
};
|
|
342
414
|
}
|
|
343
415
|
if (artifactMtime + 1 < sourceMtime) {
|
|
344
|
-
|
|
345
|
-
// A same-basename hit in an unrelated subtree (e.g. `head.js` in a
|
|
346
|
-
// completely different upstream test helper) should be reported as
|
|
347
|
-
// missing — the operator needs to check registration, not puzzle
|
|
348
|
-
// over why an unrelated file appears "older than the source".
|
|
349
|
-
if (!isConfidentMatch(source, artifact)) {
|
|
416
|
+
if (!mode.registered && !isConfidentMatch(source, artifact)) {
|
|
350
417
|
const where = isTestPath(source) ? '_tests/' : 'dist/';
|
|
418
|
+
const candidates = await collectSameBasenameCandidates(source, mode.roots);
|
|
419
|
+
const nearHits = describeCandidates(candidates);
|
|
351
420
|
return {
|
|
352
421
|
source,
|
|
353
422
|
artifact: undefined,
|
|
354
423
|
status: 'missing',
|
|
355
|
-
warning: `Audit: engine/${source} was touched but no related packaged artifact with
|
|
424
|
+
warning: `Audit: engine/${source} was touched but no related packaged artifact with ` +
|
|
425
|
+
`basename "${basename(source)}" was found under ${where}` +
|
|
426
|
+
(nearHits ? `. Same-basename hits in unrelated subtrees: ${nearHits}` : '') +
|
|
427
|
+
`. Missing moz.build / jar.mn / package-manifest.in registration?`,
|
|
356
428
|
};
|
|
357
429
|
}
|
|
358
430
|
return {
|
|
@@ -364,6 +436,34 @@ async function auditSinglePath(source, ctx) {
|
|
|
364
436
|
}
|
|
365
437
|
return { source, artifact, status: 'updated' };
|
|
366
438
|
}
|
|
439
|
+
/** Cap on candidate list rendering before truncating with `(+N more)`. */
|
|
440
|
+
const CANDIDATE_LIST_LIMIT = 5;
|
|
441
|
+
/**
|
|
442
|
+
* Renders a comma-separated list of same-basename hits for inclusion in a
|
|
443
|
+
* warning, truncated at {@link CANDIDATE_LIST_LIMIT} with a `(+N more)`
|
|
444
|
+
* tail. Returns the empty string when no candidates are supplied so
|
|
445
|
+
* callers can omit the parenthetical entirely rather than render a stub.
|
|
446
|
+
*/
|
|
447
|
+
function describeCandidates(candidates) {
|
|
448
|
+
if (candidates.length === 0)
|
|
449
|
+
return '';
|
|
450
|
+
const head = candidates.slice(0, CANDIDATE_LIST_LIMIT).join(', ');
|
|
451
|
+
if (candidates.length <= CANDIDATE_LIST_LIMIT)
|
|
452
|
+
return head;
|
|
453
|
+
return `${head}, … (+${candidates.length - CANDIDATE_LIST_LIMIT} more)`;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Formats a manifest path relative to the engine root when it lives
|
|
457
|
+
* underneath, falling back to the absolute path otherwise. Keeps warning
|
|
458
|
+
* text short and anchored to `engine/…` when the manifest is in-tree.
|
|
459
|
+
*/
|
|
460
|
+
function relativeManifestPath(engineDir, manifest) {
|
|
461
|
+
const root = engineDir.replace(/[/\\]+$/, '');
|
|
462
|
+
if (manifest.startsWith(`${root}/`)) {
|
|
463
|
+
return `engine/${manifest.slice(root.length + 1)}`;
|
|
464
|
+
}
|
|
465
|
+
return manifest;
|
|
466
|
+
}
|
|
367
467
|
/**
|
|
368
468
|
* Runs the post-build audit. Emits per-file warnings for missing or
|
|
369
469
|
* stale artifacts and a summary info line at the end. Always returns
|
|
@@ -35,10 +35,12 @@ export declare function validateJarMnEntries(root: string, config: FurnaceConfig
|
|
|
35
35
|
* linked in at least one chrome host document. Without the link, tokens
|
|
36
36
|
* silently resolve to nothing at runtime.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* Scan set is the union of (a) the configured `tokenHostDocuments` (or
|
|
39
|
+
* the upstream default when unset) and (b) any `browser/base/content/*.xhtml`
|
|
40
|
+
* document that references `tagName` — the auto-detection path catches
|
|
41
|
+
* forks that mount components from a replacement chrome document without
|
|
42
|
+
* having configured `tokenHostDocuments`. The warning fires only when
|
|
43
|
+
* NONE of the documents in the final scan set link the tokens CSS.
|
|
42
44
|
*/
|
|
43
45
|
export declare function validateTokenLink(componentDir: string, tagName: string, root: string, tokenPrefix?: string, tokenHostDocuments?: string[]): Promise<ValidationIssue[]>;
|
|
44
46
|
/**
|
|
@@ -211,15 +211,73 @@ export async function validateJarMnEntries(root, config) {
|
|
|
211
211
|
* `tokenHostDocuments` is not configured in furnace.json.
|
|
212
212
|
*/
|
|
213
213
|
const DEFAULT_TOKEN_HOST_DOCUMENTS = ['browser/base/content/browser.xhtml'];
|
|
214
|
+
/**
|
|
215
|
+
* Directory scanned for additional chrome host documents that mount the
|
|
216
|
+
* component under audit. Kept narrow (top-level `browser/base/content/`)
|
|
217
|
+
* so the auto-detection stays cheap and only triggers on the well-known
|
|
218
|
+
* location forks use for replacement chrome documents.
|
|
219
|
+
*/
|
|
220
|
+
const AUTO_DETECT_HOST_DIR = 'browser/base/content';
|
|
221
|
+
/**
|
|
222
|
+
* Scans `browser/base/content/*.xhtml` for chrome documents that reference
|
|
223
|
+
* `tagName`. Returned paths are engine-relative and deduplicated against
|
|
224
|
+
* `already`, so callers can merge them with the caller-configured set
|
|
225
|
+
* without producing double entries in warning output.
|
|
226
|
+
*
|
|
227
|
+
* Motivating case: a fork that mounts a custom element from its own
|
|
228
|
+
* top-level chrome document (e.g. `mybrowser.xhtml`) without setting
|
|
229
|
+
* `tokenHostDocuments`. The stock `browser.xhtml` was the only thing
|
|
230
|
+
* scanned, so the tokens CSS link in the ACTUAL host document went
|
|
231
|
+
* unnoticed and the warning false-fired.
|
|
232
|
+
*
|
|
233
|
+
* @param engineDir Absolute engine root.
|
|
234
|
+
* @param tagName Custom element tag the CSS belongs to.
|
|
235
|
+
* @param already Paths already in the scan set (POSIX, engine-relative).
|
|
236
|
+
*/
|
|
237
|
+
async function autoDetectTokenHostDocuments(engineDir, tagName, already) {
|
|
238
|
+
const contentDir = join(engineDir, AUTO_DETECT_HOST_DIR);
|
|
239
|
+
if (!(await pathExists(contentDir)))
|
|
240
|
+
return [];
|
|
241
|
+
let entries;
|
|
242
|
+
try {
|
|
243
|
+
entries = await readdir(contentDir);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
const alreadySet = new Set(already);
|
|
249
|
+
const detected = [];
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
if (!entry.endsWith('.xhtml'))
|
|
252
|
+
continue;
|
|
253
|
+
const relPath = `${AUTO_DETECT_HOST_DIR}/${entry}`;
|
|
254
|
+
if (alreadySet.has(relPath))
|
|
255
|
+
continue;
|
|
256
|
+
const absPath = join(contentDir, entry);
|
|
257
|
+
let content;
|
|
258
|
+
try {
|
|
259
|
+
content = await readText(absPath);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (content.includes(tagName)) {
|
|
265
|
+
detected.push(relPath);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return detected;
|
|
269
|
+
}
|
|
214
270
|
/**
|
|
215
271
|
* Validates that components using design tokens have the tokens CSS
|
|
216
272
|
* linked in at least one chrome host document. Without the link, tokens
|
|
217
273
|
* silently resolve to nothing at runtime.
|
|
218
274
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
275
|
+
* Scan set is the union of (a) the configured `tokenHostDocuments` (or
|
|
276
|
+
* the upstream default when unset) and (b) any `browser/base/content/*.xhtml`
|
|
277
|
+
* document that references `tagName` — the auto-detection path catches
|
|
278
|
+
* forks that mount components from a replacement chrome document without
|
|
279
|
+
* having configured `tokenHostDocuments`. The warning fires only when
|
|
280
|
+
* NONE of the documents in the final scan set link the tokens CSS.
|
|
223
281
|
*/
|
|
224
282
|
export async function validateTokenLink(componentDir, tagName, root, tokenPrefix, tokenHostDocuments) {
|
|
225
283
|
const issues = [];
|
|
@@ -233,7 +291,7 @@ export async function validateTokenLink(componentDir, tagName, root, tokenPrefix
|
|
|
233
291
|
if (!cssContent.includes(tokenPrefix))
|
|
234
292
|
return issues;
|
|
235
293
|
const { engine: engineDir } = getProjectPaths(root);
|
|
236
|
-
const
|
|
294
|
+
const configuredHosts = tokenHostDocuments && tokenHostDocuments.length > 0
|
|
237
295
|
? tokenHostDocuments
|
|
238
296
|
: DEFAULT_TOKEN_HOST_DOCUMENTS;
|
|
239
297
|
let tokensCssFile;
|
|
@@ -247,6 +305,8 @@ export async function validateTokenLink(componentDir, tagName, root, tokenPrefix
|
|
|
247
305
|
warn(`Could not resolve token CSS link target for ${tagName} during validation: ${reason}`);
|
|
248
306
|
return issues;
|
|
249
307
|
}
|
|
308
|
+
const autoDetected = await autoDetectTokenHostDocuments(engineDir, tagName, configuredHosts);
|
|
309
|
+
const hostDocuments = [...configuredHosts, ...autoDetected];
|
|
250
310
|
const checkedDocuments = [];
|
|
251
311
|
let anyLinks = false;
|
|
252
312
|
for (const relDocPath of hostDocuments) {
|
|
@@ -268,7 +328,7 @@ export async function validateTokenLink(componentDir, tagName, root, tokenPrefix
|
|
|
268
328
|
component: tagName,
|
|
269
329
|
severity: 'warning',
|
|
270
330
|
check: 'missing-token-link',
|
|
271
|
-
message: `Component uses ${tokenPrefix}* tokens but none of the
|
|
331
|
+
message: `Component uses ${tokenPrefix}* tokens but none of the scanned chrome host documents (${docsList}) link ${tokensCssFile}. Tokens will silently resolve to nothing. Configure additional hosts via furnace.json "tokenHostDocuments" if needed.`,
|
|
272
332
|
});
|
|
273
333
|
}
|
|
274
334
|
return issues;
|
|
@@ -27,3 +27,47 @@ export interface BuildArtifactCheck {
|
|
|
27
27
|
export declare function hasBuildArtifacts(engineDir: string): Promise<BuildArtifactCheck>;
|
|
28
28
|
/** Builds a user-facing explanation when detected build artifacts belong to another workspace. */
|
|
29
29
|
export declare function buildArtifactMismatchMessage(engineDir: string, buildCheck: BuildArtifactCheck, commandName: string): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Outcome of an in-place mozinfo.json rewrite attempt. A successful rewrite
|
|
32
|
+
* returns the paths written; a refused rewrite returns a human-readable
|
|
33
|
+
* reason so the build flow can surface it alongside the original mismatch
|
|
34
|
+
* message before falling back to the clean-rebuild instruction.
|
|
35
|
+
*/
|
|
36
|
+
export interface MozinfoRewriteResult {
|
|
37
|
+
/** Whether mozinfo.json was patched in place. */
|
|
38
|
+
rewritten: boolean;
|
|
39
|
+
/** Reason the rewrite was refused (populated when `rewritten === false`). */
|
|
40
|
+
reason?: string;
|
|
41
|
+
/** New `topsrcdir` value written to disk (populated on success). */
|
|
42
|
+
newTopsrcdir?: string;
|
|
43
|
+
/** New `topobjdir` value written to disk (populated on success). */
|
|
44
|
+
newTopobjdir?: string;
|
|
45
|
+
/** New `mozconfig` value written to disk (populated on success when it lived inside topsrcdir). */
|
|
46
|
+
newMozconfig?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Safe-relocation rewriter for mozinfo.json under the active obj-* tree.
|
|
50
|
+
*
|
|
51
|
+
* Firefox build artefacts bake the topsrcdir into many generated files
|
|
52
|
+
* (Makefiles, config.status, backend.mk, .deps dependency files — anything
|
|
53
|
+
* produced by `mach configure`). A fresh `mach configure` rebuilds those
|
|
54
|
+
* from the top, so the rewriter only needs to patch the one file `mach`
|
|
55
|
+
* reads to learn where its checkout actually lives. Once mozinfo.json
|
|
56
|
+
* agrees with the on-disk layout, `mach configure` regenerates the rest.
|
|
57
|
+
*
|
|
58
|
+
* Safety rules — the rewrite is refused when any of them are violated:
|
|
59
|
+
* - `topsrcdir` and `topobjdir` must both be present and non-empty.
|
|
60
|
+
* - `topobjdir` must resolve to `<topsrcdir>/<objDir>`; a non-in-tree
|
|
61
|
+
* objdir means the previous workspace was configured differently,
|
|
62
|
+
* so a blind prefix-rewrite could point mach at the wrong tree.
|
|
63
|
+
* - The computed new `topobjdir` must be `<engineDir>/<objDir>`; if it
|
|
64
|
+
* is not, the objDir name itself changed and we cannot prove safety.
|
|
65
|
+
*
|
|
66
|
+
* When any rule trips, the caller should fall back to the clean-rebuild
|
|
67
|
+
* instruction — that's always a correct (if expensive) recovery path.
|
|
68
|
+
*
|
|
69
|
+
* @param engineDir Absolute path to the current engine checkout.
|
|
70
|
+
* @param objDir Name of the obj-* directory to rewrite against.
|
|
71
|
+
* @returns Result object; callers inspect `rewritten` and surface `reason`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function attemptMozinfoRewrite(engineDir: string, objDir: string): Promise<MozinfoRewriteResult>;
|