@hominis/fireforge 0.15.6 → 0.15.8
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 +78 -0
- package/README.md +158 -15
- 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 +38 -0
- package/dist/src/commands/furnace/create-dry-run.js +100 -0
- package/dist/src/commands/furnace/create-features.d.ts +24 -0
- package/dist/src/commands/furnace/create-features.js +56 -0
- package/dist/src/commands/furnace/create-templates.d.ts +9 -5
- package/dist/src/commands/furnace/create-templates.js +28 -6
- package/dist/src/commands/furnace/create.js +62 -63
- package/dist/src/commands/furnace/index.js +4 -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/run.d.ts +15 -1
- package/dist/src/commands/run.js +202 -7
- package/dist/src/commands/test.js +113 -3
- 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-apply-ftl.d.ts +5 -3
- package/dist/src/core/furnace-apply-ftl.js +6 -2
- package/dist/src/core/furnace-apply-helpers.js +14 -4
- package/dist/src/core/furnace-config-custom.d.ts +14 -0
- package/dist/src/core/furnace-config-custom.js +64 -0
- package/dist/src/core/furnace-config.js +2 -39
- package/dist/src/core/furnace-validate-accessibility.d.ts +9 -2
- package/dist/src/core/furnace-validate-accessibility.js +17 -3
- package/dist/src/core/furnace-validate-helpers.d.ts +13 -1
- package/dist/src/core/furnace-validate-helpers.js +19 -0
- 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/furnace-validate-structure.js +6 -2
- package/dist/src/core/furnace-validate.js +6 -3
- 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 +27 -1
- package/dist/src/core/mach.js +26 -2
- package/dist/src/core/shared-ftl.d.ts +28 -0
- package/dist/src/core/shared-ftl.js +42 -0
- package/dist/src/core/smoke-patterns.d.ts +45 -0
- package/dist/src/core/smoke-patterns.js +100 -0
- package/dist/src/core/test-stale-check.d.ts +42 -0
- package/dist/src/core/test-stale-check.js +114 -0
- package/dist/src/core/xpcshell-appdir.d.ts +143 -0
- package/dist/src/core/xpcshell-appdir.js +273 -0
- package/dist/src/errors/codes.d.ts +13 -0
- package/dist/src/errors/codes.js +13 -0
- package/dist/src/errors/run.d.ts +16 -0
- package/dist/src/errors/run.js +22 -0
- package/dist/src/types/commands/options.d.ts +64 -0
- package/dist/src/types/furnace.d.ts +39 -0
- package/dist/src/utils/process.d.ts +63 -0
- package/dist/src/utils/process.js +122 -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
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* aborting the whole command. Missing jar.mn on a fork without a locale
|
|
9
9
|
* package should not block a working `.mjs`/`.css` from shipping.
|
|
10
10
|
*/
|
|
11
|
-
import type { DryRunAction, StepError } from '../types/furnace.js';
|
|
11
|
+
import type { CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
|
|
12
12
|
import { type RollbackJournal } from './furnace-rollback.js';
|
|
13
13
|
/**
|
|
14
14
|
* Copies a component's `.ftl` into the FTL tree and registers the chrome URI
|
|
@@ -28,6 +28,8 @@ export declare function describeLocaleFtlJarMnRegistration(name: string, ftlDir:
|
|
|
28
28
|
/**
|
|
29
29
|
* Drops the locale jar.mn entry for `fileName` when it's a `.ftl` whose
|
|
30
30
|
* source workspace file has been deleted. Idempotent — absent entries are a
|
|
31
|
-
* no-op.
|
|
31
|
+
* no-op. Early-returns for `sharedFtl` components: the shared bundle is
|
|
32
|
+
* owned elsewhere, and dropping its jar.mn line on our component's delete
|
|
33
|
+
* would orphan every other participant.
|
|
32
34
|
*/
|
|
33
|
-
export declare function removeCustomFtlJarMnEntry(engineDir: string, fileName: string, ftlDir: string, rollbackJournal?: RollbackJournal): Promise<void>;
|
|
35
|
+
export declare function removeCustomFtlJarMnEntry(engineDir: string, fileName: string, ftlDir: string, config: CustomComponentConfig, rollbackJournal?: RollbackJournal): Promise<void>;
|
|
@@ -81,9 +81,13 @@ export function describeLocaleFtlJarMnRegistration(name, ftlDir, ftlFile) {
|
|
|
81
81
|
/**
|
|
82
82
|
* Drops the locale jar.mn entry for `fileName` when it's a `.ftl` whose
|
|
83
83
|
* source workspace file has been deleted. Idempotent — absent entries are a
|
|
84
|
-
* no-op.
|
|
84
|
+
* no-op. Early-returns for `sharedFtl` components: the shared bundle is
|
|
85
|
+
* owned elsewhere, and dropping its jar.mn line on our component's delete
|
|
86
|
+
* would orphan every other participant.
|
|
85
87
|
*/
|
|
86
|
-
export async function removeCustomFtlJarMnEntry(engineDir, fileName, ftlDir, rollbackJournal) {
|
|
88
|
+
export async function removeCustomFtlJarMnEntry(engineDir, fileName, ftlDir, config, rollbackJournal) {
|
|
89
|
+
if (config.sharedFtl)
|
|
90
|
+
return;
|
|
87
91
|
if (!fileName.endsWith('.ftl'))
|
|
88
92
|
return;
|
|
89
93
|
const tagName = fileName.slice(0, -'.ftl'.length);
|
|
@@ -139,10 +139,12 @@ export async function undeployCustomFiles(engineDir, config, deletedFiles, ftlDi
|
|
|
139
139
|
await removeFile(enginePath);
|
|
140
140
|
removed.push(relative(engineDir, enginePath));
|
|
141
141
|
}
|
|
142
|
-
// When an `.ftl` is deleted from the workspace
|
|
142
|
+
// When an `.ftl` is deleted from the workspace the corresponding locale
|
|
143
143
|
// jar.mn entry must also be dropped — otherwise the chrome URI points at
|
|
144
144
|
// a missing file and runtime Fluent resolution breaks silently.
|
|
145
|
-
|
|
145
|
+
// `removeCustomFtlJarMnEntry` early-returns for `sharedFtl` components
|
|
146
|
+
// (the shared bundle is owned elsewhere).
|
|
147
|
+
await removeCustomFtlJarMnEntry(engineDir, fileName, ftlDir, config, rollbackJournal);
|
|
146
148
|
}
|
|
147
149
|
return removed;
|
|
148
150
|
}
|
|
@@ -306,7 +308,12 @@ async function buildCustomDryRunActions(name, componentDir, engineDir, config, t
|
|
|
306
308
|
description: `Copy ${entry.name} to ${config.targetPath}`,
|
|
307
309
|
});
|
|
308
310
|
}
|
|
309
|
-
|
|
311
|
+
// Per-component .ftl handling is skipped when the component opts into a
|
|
312
|
+
// shared feature-scoped bundle via `sharedFtl`. The shared file is
|
|
313
|
+
// registered (and copied) by whoever owns the feature bundle, so
|
|
314
|
+
// emitting a copy-ftl / register-jar action here would duplicate (or
|
|
315
|
+
// later orphan) the entry.
|
|
316
|
+
if (config.localized && !config.sharedFtl) {
|
|
310
317
|
const ftlFile = `${name}.ftl`;
|
|
311
318
|
const ftlSrc = join(componentDir, ftlFile);
|
|
312
319
|
if (await pathExists(ftlSrc)) {
|
|
@@ -402,7 +409,10 @@ export async function applyCustomComponent(engineDir, name, componentDir, config
|
|
|
402
409
|
affectedPaths.push(relative(engineDir, dest));
|
|
403
410
|
copiedFileNames.push(entry.name);
|
|
404
411
|
}));
|
|
405
|
-
|
|
412
|
+
// See buildCustomDryRunActions for the rationale: when `sharedFtl` is set
|
|
413
|
+
// the shared bundle is owned elsewhere and FireForge must not copy or
|
|
414
|
+
// register a per-component `.ftl` on its behalf.
|
|
415
|
+
if (config.localized && !config.sharedFtl) {
|
|
406
416
|
await applyCustomFtlFile(engineDir, name, componentDir, ftlDir, affectedPaths, stepErrors, rollbackJournal);
|
|
407
417
|
}
|
|
408
418
|
if (config.register) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for the `custom` entries in furnace.json. Extracted from
|
|
3
|
+
* `furnace-config.ts` so the main config module stays under the
|
|
4
|
+
* per-file LOC budget — the custom-component schema has grown to
|
|
5
|
+
* carry opt-in fields (`composes`, `keyboardCovered`, `sharedFtl`) that
|
|
6
|
+
* each add their own validation branch.
|
|
7
|
+
*/
|
|
8
|
+
import type { CustomComponentConfig } from '../types/furnace.js';
|
|
9
|
+
/**
|
|
10
|
+
* Validates a custom component config object.
|
|
11
|
+
* @param data - Raw data to validate
|
|
12
|
+
* @param name - Component name for error messages
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseCustomConfig(data: Record<string, unknown>, name: string): CustomComponentConfig;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Parser for the `custom` entries in furnace.json. Extracted from
|
|
4
|
+
* `furnace-config.ts` so the main config module stays under the
|
|
5
|
+
* per-file LOC budget — the custom-component schema has grown to
|
|
6
|
+
* carry opt-in fields (`composes`, `keyboardCovered`, `sharedFtl`) that
|
|
7
|
+
* each add their own validation branch.
|
|
8
|
+
*/
|
|
9
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
10
|
+
import { isExplicitAbsolutePath } from '../utils/paths.js';
|
|
11
|
+
import { isBoolean, isString } from '../utils/validation.js';
|
|
12
|
+
import { parseStringArray } from './furnace-config.js';
|
|
13
|
+
import { validateSharedFtl } from './shared-ftl.js';
|
|
14
|
+
/**
|
|
15
|
+
* Validates a custom component config object.
|
|
16
|
+
* @param data - Raw data to validate
|
|
17
|
+
* @param name - Component name for error messages
|
|
18
|
+
*/
|
|
19
|
+
export function parseCustomConfig(data, name) {
|
|
20
|
+
if (!isString(data['description'])) {
|
|
21
|
+
throw new FurnaceError(`Furnace config: custom "${name}.description" must be a string`);
|
|
22
|
+
}
|
|
23
|
+
if (!isString(data['targetPath'])) {
|
|
24
|
+
throw new FurnaceError(`Furnace config: custom "${name}.targetPath" must be a string`);
|
|
25
|
+
}
|
|
26
|
+
if (data['targetPath'].includes('..') || data['targetPath'].includes('\0')) {
|
|
27
|
+
throw new FurnaceError(`Furnace config: custom "${name}.targetPath" must not contain ".." or null bytes (path traversal)`);
|
|
28
|
+
}
|
|
29
|
+
if (isExplicitAbsolutePath(data['targetPath'])) {
|
|
30
|
+
throw new FurnaceError(`Furnace config: custom "${name}.targetPath" must not be an absolute path`);
|
|
31
|
+
}
|
|
32
|
+
if (!isBoolean(data['register'])) {
|
|
33
|
+
throw new FurnaceError(`Furnace config: custom "${name}.register" must be a boolean`);
|
|
34
|
+
}
|
|
35
|
+
if (!isBoolean(data['localized'])) {
|
|
36
|
+
throw new FurnaceError(`Furnace config: custom "${name}.localized" must be a boolean`);
|
|
37
|
+
}
|
|
38
|
+
if (data['composes'] !== undefined) {
|
|
39
|
+
parseStringArray(data['composes'], `${name}.composes`);
|
|
40
|
+
}
|
|
41
|
+
if (data['keyboardCovered'] !== undefined && !isBoolean(data['keyboardCovered'])) {
|
|
42
|
+
throw new FurnaceError(`Furnace config: custom "${name}.keyboardCovered" must be a boolean when set`);
|
|
43
|
+
}
|
|
44
|
+
let sharedFtl;
|
|
45
|
+
if (data['sharedFtl'] !== undefined) {
|
|
46
|
+
const result = validateSharedFtl(data['sharedFtl'], { localized: data['localized'] });
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
throw new FurnaceError(`Furnace config: custom "${name}.sharedFtl" ${result.reason}`);
|
|
49
|
+
}
|
|
50
|
+
sharedFtl = result.value;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
description: data['description'],
|
|
54
|
+
targetPath: data['targetPath'],
|
|
55
|
+
register: data['register'],
|
|
56
|
+
localized: data['localized'],
|
|
57
|
+
...(data['composes'] !== undefined
|
|
58
|
+
? { composes: parseStringArray(data['composes'], `${name}.composes`) }
|
|
59
|
+
: {}),
|
|
60
|
+
...(data['keyboardCovered'] === true ? { keyboardCovered: true } : {}),
|
|
61
|
+
...(sharedFtl !== undefined ? { sharedFtl } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=furnace-config-custom.js.map
|