@hominis/fireforge 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/src/commands/patch/split-plan.d.ts +18 -2
- package/dist/src/commands/patch/split-plan.js +90 -16
- package/dist/src/commands/patch/split.js +12 -3
- package/dist/src/commands/token.js +12 -1
- package/dist/src/commands/typecheck.js +35 -0
- package/dist/src/core/build-prepare.js +23 -3
- package/dist/src/core/config-validate.js +26 -0
- package/dist/src/core/furnace-apply-dry-run.d.ts +17 -0
- package/dist/src/core/furnace-apply-dry-run.js +105 -0
- package/dist/src/core/furnace-apply-ftl.d.ts +12 -0
- package/dist/src/core/furnace-apply-ftl.js +97 -1
- package/dist/src/core/furnace-apply-helpers.js +10 -80
- package/dist/src/core/mach-resource-shim.d.ts +21 -0
- package/dist/src/core/mach-resource-shim.js +92 -0
- package/dist/src/core/mach.js +9 -2
- package/dist/src/core/manifest-helpers.js +29 -4
- package/dist/src/core/patch-lint-cross.d.ts +31 -0
- package/dist/src/core/patch-lint-cross.js +83 -63
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint-reexports.js +1 -1
- package/dist/src/core/test-harness-crash.d.ts +6 -3
- package/dist/src/core/test-harness-crash.js +32 -4
- package/dist/src/core/token-dark-mode.d.ts +9 -0
- package/dist/src/core/token-dark-mode.js +1 -1
- package/dist/src/core/token-docs.d.ts +32 -0
- package/dist/src/core/token-docs.js +101 -0
- package/dist/src/core/token-manager.d.ts +8 -0
- package/dist/src/core/token-manager.js +77 -95
- package/dist/src/core/token-variant.d.ts +39 -0
- package/dist/src/core/token-variant.js +141 -0
- package/dist/src/core/typecheck.js +56 -28
- package/dist/src/types/commands/options.d.ts +5 -0
- package/dist/src/types/config.d.ts +13 -0
- package/package.json +3 -3
|
@@ -11,10 +11,106 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { join, relative } from 'node:path';
|
|
13
13
|
import { toError } from '../utils/errors.js';
|
|
14
|
-
import { copyFile, pathExists } from '../utils/fs.js';
|
|
14
|
+
import { copyFile, pathExists, readText } from '../utils/fs.js';
|
|
15
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
15
16
|
import { resolveFtlChromeSubPath, resolveFtlLocaleJarMnPath } from './furnace-constants.js';
|
|
16
17
|
import { addLocaleFtlJarMnEntry, removeLocaleFtlJarMnEntry } from './furnace-registration.js';
|
|
17
18
|
import { snapshotFile } from './furnace-rollback.js';
|
|
19
|
+
/**
|
|
20
|
+
* Builds the presence regex for a per-widget locale jar.mn line
|
|
21
|
+
* (`locale/@AB_CD@/<chromeSubPath>/<tagName>.ftl`). Shared by the prune
|
|
22
|
+
* helper and its dry-run describer so both agree on what "dangling" means.
|
|
23
|
+
*/
|
|
24
|
+
function perWidgetLocaleEntryPattern(chromeSubPath, tagName) {
|
|
25
|
+
return new RegExp(`locale\\/(?:@AB_CD@|[a-zA-Z-]+)\\/${escapeRegex(chromeSubPath)}\\/${escapeRegex(tagName)}\\.ftl`, 'm');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the engine-relative locale jar.mn and the per-widget entry regex
|
|
29
|
+
* for a `sharedFtl` widget, or `undefined` when the FTL tree exposes no
|
|
30
|
+
* locale jar.mn we can confidently name.
|
|
31
|
+
*/
|
|
32
|
+
function resolveSharedFtlPruneTarget(name, ftlDir) {
|
|
33
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
34
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
35
|
+
if (chromeSubPath === undefined || localeJarRel === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
return { localeJarRel, pattern: perWidgetLocaleEntryPattern(chromeSubPath, name) };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Removes a dangling per-widget locale jar.mn entry for a `sharedFtl` widget.
|
|
41
|
+
*
|
|
42
|
+
* A `localized: true` widget that opts into a feature-scoped `sharedFtl`
|
|
43
|
+
* bundle (its strings live under `browser/...` and load via
|
|
44
|
+
* `insertFTLIfNeeded`) must NOT carry a per-widget
|
|
45
|
+
* `locale/@AB_CD@/<chromeSubPath>/<name>.ftl` line. Such a line — written by
|
|
46
|
+
* an older FireForge before the sharedFtl apply guard, or by a layout
|
|
47
|
+
* migration — points at a `.ftl` that does not exist, so `mach build` fails
|
|
48
|
+
* hard (`Cannot find <chromeSubPath>/<name>.ftl`) and blocks every build.
|
|
49
|
+
*
|
|
50
|
+
* The pruned line is the per-widget toolkit entry only; the shared bundle's
|
|
51
|
+
* own line (a different chrome sub-path / base name) is never matched, so
|
|
52
|
+
* pruning one widget cannot orphan the shared bundle. Idempotent: when no
|
|
53
|
+
* dangling entry exists the file is left untouched (no journal churn).
|
|
54
|
+
* Returns the engine-relative jar.mn path when a line was removed, else
|
|
55
|
+
* `undefined`.
|
|
56
|
+
*/
|
|
57
|
+
async function pruneSharedFtlPerWidgetLocaleEntry(engineDir, name, ftlDir, config, rollbackJournal) {
|
|
58
|
+
if (!config.sharedFtl)
|
|
59
|
+
return undefined;
|
|
60
|
+
const target = resolveSharedFtlPruneTarget(name, ftlDir);
|
|
61
|
+
if (!target)
|
|
62
|
+
return undefined;
|
|
63
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
64
|
+
if (chromeSubPath === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const localeJarAbs = join(engineDir, target.localeJarRel);
|
|
67
|
+
if (!(await pathExists(localeJarAbs)))
|
|
68
|
+
return undefined;
|
|
69
|
+
if (!target.pattern.test(await readText(localeJarAbs)))
|
|
70
|
+
return undefined;
|
|
71
|
+
if (rollbackJournal) {
|
|
72
|
+
await snapshotFile(rollbackJournal, localeJarAbs);
|
|
73
|
+
}
|
|
74
|
+
await removeLocaleFtlJarMnEntry(engineDir, target.localeJarRel, name, chromeSubPath);
|
|
75
|
+
return target.localeJarRel;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Apply-path wrapper around {@link pruneSharedFtlPerWidgetLocaleEntry} that
|
|
79
|
+
* records the affected path / step error in the caller's collectors, mirroring
|
|
80
|
+
* {@link applyCustomFtlFile}'s contract so the main apply helper stays terse.
|
|
81
|
+
*/
|
|
82
|
+
export async function applySharedFtlPrune(engineDir, name, ftlDir, config, affectedPaths, stepErrors, rollbackJournal) {
|
|
83
|
+
try {
|
|
84
|
+
const prunedPath = await pruneSharedFtlPerWidgetLocaleEntry(engineDir, name, ftlDir, config, rollbackJournal);
|
|
85
|
+
if (prunedPath)
|
|
86
|
+
affectedPaths.push(prunedPath);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
stepErrors.push({ step: 'locale jar.mn prune', error: toError(error).message });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read-only dry-run describer for {@link pruneSharedFtlPerWidgetLocaleEntry}:
|
|
94
|
+
* returns an action when a dangling per-widget locale entry exists for a
|
|
95
|
+
* `sharedFtl` widget, else `undefined`.
|
|
96
|
+
*/
|
|
97
|
+
export async function describeSharedFtlPrune(engineDir, name, ftlDir, config) {
|
|
98
|
+
if (!config.sharedFtl)
|
|
99
|
+
return undefined;
|
|
100
|
+
const target = resolveSharedFtlPruneTarget(name, ftlDir);
|
|
101
|
+
if (!target)
|
|
102
|
+
return undefined;
|
|
103
|
+
const localeJarAbs = join(engineDir, target.localeJarRel);
|
|
104
|
+
if (!(await pathExists(localeJarAbs)))
|
|
105
|
+
return undefined;
|
|
106
|
+
if (!target.pattern.test(await readText(localeJarAbs)))
|
|
107
|
+
return undefined;
|
|
108
|
+
return {
|
|
109
|
+
component: name,
|
|
110
|
+
action: 'register-jar',
|
|
111
|
+
description: `Remove dangling per-widget locale entry for ${name} from ${target.localeJarRel} (sharedFtl bundle owns its strings)`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
18
114
|
/**
|
|
19
115
|
* Copies a component's `.ftl` into the FTL tree and registers the chrome URI
|
|
20
116
|
* in the locale jar.mn.
|
|
@@ -6,10 +6,11 @@ import { FurnaceError } from '../errors/furnace.js';
|
|
|
6
6
|
import { toError } from '../utils/errors.js';
|
|
7
7
|
import { copyFile, ensureDir, pathExists, readText, removeFile } from '../utils/fs.js';
|
|
8
8
|
import { verbose } from '../utils/logger.js';
|
|
9
|
-
import {
|
|
9
|
+
import { buildCustomDryRunActions } from './furnace-apply-dry-run.js';
|
|
10
|
+
import { applyCustomFtlFile, applySharedFtlPrune, removeCustomFtlJarMnEntry, } from './furnace-apply-ftl.js';
|
|
10
11
|
import { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
11
|
-
import { deployFileWithFragments,
|
|
12
|
-
import { addCustomElementRegistration, addJarMnEntries
|
|
12
|
+
import { deployFileWithFragments, SHARED_FRAGMENTS_DIR } from './furnace-css-fragments.js';
|
|
13
|
+
import { addCustomElementRegistration, addJarMnEntries } from './furnace-registration.js';
|
|
13
14
|
import { recordCreatedDir, snapshotFile } from './furnace-rollback.js';
|
|
14
15
|
import { checkRegistrationConsistency } from './furnace-validate-registration.js';
|
|
15
16
|
import { isGitRepository } from './git.js';
|
|
@@ -278,83 +279,6 @@ export async function hasCustomEngineDrift(root, name, componentDir, config, ftl
|
|
|
278
279
|
}
|
|
279
280
|
return false;
|
|
280
281
|
}
|
|
281
|
-
async function buildCustomDryRunActions(name, componentDir, engineDir, config, targetDir, entries, ftlDir) {
|
|
282
|
-
const actions = [];
|
|
283
|
-
const stepErrors = [];
|
|
284
|
-
for (const entry of entries) {
|
|
285
|
-
if (!isRegularFile(entry))
|
|
286
|
-
continue;
|
|
287
|
-
if (!entry.name.endsWith('.mjs') && !entry.name.endsWith('.css'))
|
|
288
|
-
continue;
|
|
289
|
-
const fragmentNote = await describeFragmentExpansion(join(componentDir, entry.name));
|
|
290
|
-
actions.push({
|
|
291
|
-
component: name,
|
|
292
|
-
action: fragmentNote ? 'expand-fragments' : 'copy',
|
|
293
|
-
source: join(componentDir, entry.name),
|
|
294
|
-
target: join(targetDir, entry.name),
|
|
295
|
-
description: `Copy ${entry.name} to ${config.targetPath}${fragmentNote}`,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
// Per-component .ftl handling is skipped when the component opts into a
|
|
299
|
-
// shared feature-scoped bundle via `sharedFtl`. The shared file is
|
|
300
|
-
// registered (and copied) by whoever owns the feature bundle, so
|
|
301
|
-
// emitting a copy-ftl / register-jar action here would duplicate (or
|
|
302
|
-
// later orphan) the entry.
|
|
303
|
-
if (config.localized && !config.sharedFtl) {
|
|
304
|
-
const ftlFile = `${name}.ftl`;
|
|
305
|
-
const ftlSrc = join(componentDir, ftlFile);
|
|
306
|
-
if (await pathExists(ftlSrc)) {
|
|
307
|
-
actions.push({
|
|
308
|
-
component: name,
|
|
309
|
-
action: 'copy-ftl',
|
|
310
|
-
source: ftlSrc,
|
|
311
|
-
target: join(engineDir, ftlDir, ftlFile),
|
|
312
|
-
description: `Copy ${ftlFile} to ${ftlDir}`,
|
|
313
|
-
});
|
|
314
|
-
const localeAction = describeLocaleFtlJarMnRegistration(name, ftlDir, ftlFile);
|
|
315
|
-
if (localeAction) {
|
|
316
|
-
actions.push(localeAction);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
if (config.register) {
|
|
321
|
-
try {
|
|
322
|
-
const modulePath = `chrome://global/content/elements/${name}.mjs`;
|
|
323
|
-
await validateCustomElementRegistration(engineDir, name, modulePath);
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
stepErrors.push({
|
|
327
|
-
step: 'customElements.js registration',
|
|
328
|
-
error: toError(error).message,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
actions.push({
|
|
332
|
-
component: name,
|
|
333
|
-
action: 'register-ce',
|
|
334
|
-
description: `Register ${name} in customElements.js (DOMContentLoaded block)`,
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
const copiedFileNames = entries
|
|
338
|
-
.filter((entry) => isRegularFile(entry) && (entry.name.endsWith('.mjs') || entry.name.endsWith('.css')))
|
|
339
|
-
.map((entry) => entry.name);
|
|
340
|
-
if (copiedFileNames.length > 0) {
|
|
341
|
-
try {
|
|
342
|
-
await validateJarMnInsertionForFiles(engineDir, name, copiedFileNames);
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
stepErrors.push({
|
|
346
|
-
step: 'jar.mn registration',
|
|
347
|
-
error: toError(error).message,
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
actions.push({
|
|
351
|
-
component: name,
|
|
352
|
-
action: 'register-jar',
|
|
353
|
-
description: `Add ${copiedFileNames.join(', ')} to jar.mn`,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
return { actions, stepErrors };
|
|
357
|
-
}
|
|
358
282
|
/** Applies a custom component into the engine tree and captures registration step errors. */
|
|
359
283
|
export async function applyCustomComponent(engineDir, name, componentDir, config, ftlDir, dryRun = false, rollbackJournal, applyOptions = {}) {
|
|
360
284
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
@@ -406,6 +330,12 @@ export async function applyCustomComponent(engineDir, name, componentDir, config
|
|
|
406
330
|
if (config.localized && !config.sharedFtl) {
|
|
407
331
|
await applyCustomFtlFile(engineDir, name, componentDir, ftlDir, affectedPaths, stepErrors, rollbackJournal);
|
|
408
332
|
}
|
|
333
|
+
else if (config.localized && config.sharedFtl) {
|
|
334
|
+
// Drop any dangling per-widget locale jar.mn entry that would point at a
|
|
335
|
+
// non-existent `<chromeSubPath>/<name>.ftl` and fail `mach build`. The
|
|
336
|
+
// shared bundle (a different chrome path/base name) is never touched.
|
|
337
|
+
await applySharedFtlPrune(engineDir, name, ftlDir, config, affectedPaths, stepErrors, rollbackJournal);
|
|
338
|
+
}
|
|
409
339
|
if (config.register) {
|
|
410
340
|
try {
|
|
411
341
|
const modulePath = `chrome://global/content/elements/${name}.mjs`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource-monitor degrade shim for `mach build`.
|
|
3
|
+
*
|
|
4
|
+
* mozbuild's build resource monitor calls `psutil.virtual_memory()` at
|
|
5
|
+
* startup (`start_resource_recording`). On some hosts (psutil vs Darwin 27)
|
|
6
|
+
* that raises `RuntimeError: host_statistics64(HOST_VM_INFO64) syscall
|
|
7
|
+
* failed`, which aborts `mach build` / `mach build faster` before any work
|
|
8
|
+
* happens — so a raw `./mach build faster` dies while the build never
|
|
9
|
+
* reaches the compiler. The suite-specific test commands sidestep the
|
|
10
|
+
* monitor entirely, but the build commands have no such escape.
|
|
11
|
+
*
|
|
12
|
+
* FireForge writes a tiny `sitecustomize.py` and points the mach
|
|
13
|
+
* subprocess's `PYTHONPATH` at it. Python imports `sitecustomize` at
|
|
14
|
+
* interpreter startup (before mach runs), so the shim wraps
|
|
15
|
+
* `psutil.virtual_memory` / `swap_memory` to downgrade the host syscall
|
|
16
|
+
* failure into a non-fatal `UserWarning` and return a zeroed reading. The
|
|
17
|
+
* build then proceeds regardless of which mach entry point spawned it. When
|
|
18
|
+
* psutil works normally the wrapper is a transparent pass-through.
|
|
19
|
+
*/
|
|
20
|
+
/** Ensures the shim exists and returns the mach `env` overlay that enables it. */
|
|
21
|
+
export declare function resolveMachBuildEnv(): Promise<Record<string, string>>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Resource-monitor degrade shim for `mach build`.
|
|
4
|
+
*
|
|
5
|
+
* mozbuild's build resource monitor calls `psutil.virtual_memory()` at
|
|
6
|
+
* startup (`start_resource_recording`). On some hosts (psutil vs Darwin 27)
|
|
7
|
+
* that raises `RuntimeError: host_statistics64(HOST_VM_INFO64) syscall
|
|
8
|
+
* failed`, which aborts `mach build` / `mach build faster` before any work
|
|
9
|
+
* happens — so a raw `./mach build faster` dies while the build never
|
|
10
|
+
* reaches the compiler. The suite-specific test commands sidestep the
|
|
11
|
+
* monitor entirely, but the build commands have no such escape.
|
|
12
|
+
*
|
|
13
|
+
* FireForge writes a tiny `sitecustomize.py` and points the mach
|
|
14
|
+
* subprocess's `PYTHONPATH` at it. Python imports `sitecustomize` at
|
|
15
|
+
* interpreter startup (before mach runs), so the shim wraps
|
|
16
|
+
* `psutil.virtual_memory` / `swap_memory` to downgrade the host syscall
|
|
17
|
+
* failure into a non-fatal `UserWarning` and return a zeroed reading. The
|
|
18
|
+
* build then proceeds regardless of which mach entry point spawned it. When
|
|
19
|
+
* psutil works normally the wrapper is a transparent pass-through.
|
|
20
|
+
*/
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
22
|
+
import { delimiter, join } from 'node:path';
|
|
23
|
+
import { ensureDir, writeText } from '../utils/fs.js';
|
|
24
|
+
/** Directory name (under the OS temp dir) holding the generated shim. */
|
|
25
|
+
const RESOURCE_SHIM_DIRNAME = 'fireforge-mach-resource-shim';
|
|
26
|
+
/**
|
|
27
|
+
* `sitecustomize.py` body. Defensive: a missing/broken psutil leaves mach
|
|
28
|
+
* untouched, and the wrapper only intercepts the failure path.
|
|
29
|
+
*/
|
|
30
|
+
const SITECUSTOMIZE_SOURCE = `# Generated by FireForge — do not edit.
|
|
31
|
+
# Degrades a broken host resource monitor (psutil vs Darwin) from a fatal
|
|
32
|
+
# RuntimeError in start_resource_recording into a non-fatal warning, so
|
|
33
|
+
# \`mach build\` does not abort before the build runs. Loaded via PYTHONPATH
|
|
34
|
+
# so it patches psutil before mach's resource monitor starts.
|
|
35
|
+
import warnings
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
import psutil
|
|
39
|
+
except Exception:
|
|
40
|
+
psutil = None
|
|
41
|
+
|
|
42
|
+
if psutil is not None:
|
|
43
|
+
class _DegradedReading(object):
|
|
44
|
+
total = available = used = free = active = inactive = wired = 0
|
|
45
|
+
percent = 0.0
|
|
46
|
+
|
|
47
|
+
def __getattr__(self, _name):
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
def _guard(orig):
|
|
51
|
+
def wrapper(*args, **kwargs):
|
|
52
|
+
try:
|
|
53
|
+
return orig(*args, **kwargs)
|
|
54
|
+
except Exception as exc: # noqa: BLE001
|
|
55
|
+
warnings.warn(
|
|
56
|
+
"FireForge: host resource monitor degraded (%s); build continues." % exc
|
|
57
|
+
)
|
|
58
|
+
return _DegradedReading()
|
|
59
|
+
|
|
60
|
+
return wrapper
|
|
61
|
+
|
|
62
|
+
for _name in ("virtual_memory", "swap_memory"):
|
|
63
|
+
_orig = getattr(psutil, _name, None)
|
|
64
|
+
if _orig is not None:
|
|
65
|
+
setattr(psutil, _name, _guard(_orig))
|
|
66
|
+
`;
|
|
67
|
+
/**
|
|
68
|
+
* Writes the `sitecustomize.py` degrade shim into a stable FireForge-owned
|
|
69
|
+
* temp directory and returns that directory. Idempotent — overwriting the
|
|
70
|
+
* file each call keeps it in sync with this source if FireForge upgrades.
|
|
71
|
+
*/
|
|
72
|
+
async function ensureMachResourceShim() {
|
|
73
|
+
const dir = join(tmpdir(), RESOURCE_SHIM_DIRNAME);
|
|
74
|
+
await ensureDir(dir);
|
|
75
|
+
await writeText(join(dir, 'sitecustomize.py'), SITECUSTOMIZE_SOURCE);
|
|
76
|
+
return dir;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds the `env` overlay (merged over `process.env` by the exec layer)
|
|
80
|
+
* that prepends the shim directory to `PYTHONPATH`, so the mach subprocess
|
|
81
|
+
* imports the degrade `sitecustomize` without clobbering an existing
|
|
82
|
+
* `PYTHONPATH`.
|
|
83
|
+
*/
|
|
84
|
+
function machResourceShimEnv(shimDir, baseEnv = process.env) {
|
|
85
|
+
const existing = baseEnv['PYTHONPATH'];
|
|
86
|
+
return { PYTHONPATH: existing ? `${shimDir}${delimiter}${existing}` : shimDir };
|
|
87
|
+
}
|
|
88
|
+
/** Ensures the shim exists and returns the mach `env` overlay that enables it. */
|
|
89
|
+
export async function resolveMachBuildEnv() {
|
|
90
|
+
return machResourceShimEnv(await ensureMachResourceShim());
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=mach-resource-shim.js.map
|
package/dist/src/core/mach.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
|
8
8
|
import { ensureFirefoxIgnorefileCompatibility } from './firefox-ignorefile.js';
|
|
9
9
|
import { explainMachError } from './mach-error-hints.js';
|
|
10
10
|
import { getPython } from './mach-python.js';
|
|
11
|
+
import { resolveMachBuildEnv } from './mach-resource-shim.js';
|
|
11
12
|
// Re-export sub-modules so existing `from './mach.js'` imports keep working.
|
|
12
13
|
export { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, } from './mach-build-artifacts.js';
|
|
13
14
|
export { generateMozconfig } from './mach-mozconfig.js';
|
|
@@ -156,7 +157,10 @@ export async function build(engineDir, jobs) {
|
|
|
156
157
|
if (jobs !== undefined) {
|
|
157
158
|
args.push('-j', String(jobs));
|
|
158
159
|
}
|
|
159
|
-
|
|
160
|
+
// Inject the resource-monitor degrade shim so a host psutil failure does
|
|
161
|
+
// not abort the build regardless of which mach entry FireForge spawns.
|
|
162
|
+
const env = await resolveMachBuildEnv();
|
|
163
|
+
const result = await runMachInheritCapture(args, engineDir, { env });
|
|
160
164
|
if (result.exitCode !== 0) {
|
|
161
165
|
surfaceMachErrorHints(result);
|
|
162
166
|
}
|
|
@@ -170,7 +174,10 @@ export async function build(engineDir, jobs) {
|
|
|
170
174
|
* @returns Captured mach result
|
|
171
175
|
*/
|
|
172
176
|
export async function buildUI(engineDir) {
|
|
173
|
-
|
|
177
|
+
// Same resource-monitor degrade shim as the full build: raw
|
|
178
|
+
// `./mach build faster` aborts on the host psutil monitor without it.
|
|
179
|
+
const env = await resolveMachBuildEnv();
|
|
180
|
+
const result = await runMachInheritCapture(['build', 'faster'], engineDir, { env });
|
|
174
181
|
if (result.exitCode !== 0) {
|
|
175
182
|
surfaceMachErrorHints(result);
|
|
176
183
|
}
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordering comparator matching mozbuild's `StrictOrderingOnAppendList`
|
|
3
|
+
* (`mozbuild.util.UnsortedError`): entries are compared **case-insensitively**,
|
|
4
|
+
* so `HominisAppearanceController` (`appe`) sorts before
|
|
5
|
+
* `HominisAppMenuIntegration` (`appm`) even though a raw byte comparison
|
|
6
|
+
* places the uppercase `M` (0x4D) before the lowercase `e` (0x65).
|
|
7
|
+
*
|
|
8
|
+
* A naive `a > b` insertion landed the new entry in byte order, which
|
|
9
|
+
* `mach configure` then rejected with `UnsortedError`, aborting the build.
|
|
10
|
+
* Ties on the lower-cased key fall back to byte order so the comparison is
|
|
11
|
+
* total and stable.
|
|
12
|
+
*/
|
|
13
|
+
function mozbuildSortCompare(a, b) {
|
|
14
|
+
const la = a.toLowerCase();
|
|
15
|
+
const lb = b.toLowerCase();
|
|
16
|
+
if (la < lb)
|
|
17
|
+
return -1;
|
|
18
|
+
if (la > lb)
|
|
19
|
+
return 1;
|
|
20
|
+
if (a < b)
|
|
21
|
+
return -1;
|
|
22
|
+
if (a > b)
|
|
23
|
+
return 1;
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
1
26
|
/**
|
|
2
27
|
* Inserts a line into an array of lines in alphabetical order within a
|
|
3
28
|
* specified range. The comparison key is extracted from each line.
|
|
@@ -16,7 +41,7 @@ export function findAlphabeticalPosition(lines, startLine, endLine, newKey, extr
|
|
|
16
41
|
existingKeys.push(key);
|
|
17
42
|
}
|
|
18
43
|
}
|
|
19
|
-
const isSorted = existingKeys.every((k, idx) => idx === 0 || k
|
|
44
|
+
const isSorted = existingKeys.every((k, idx) => idx === 0 || mozbuildSortCompare(k, existingKeys[idx - 1] ?? '') >= 0);
|
|
20
45
|
if (!isSorted) {
|
|
21
46
|
return { insertIndex: endLine, previousEntry: undefined };
|
|
22
47
|
}
|
|
@@ -29,7 +54,7 @@ export function findAlphabeticalPosition(lines, startLine, endLine, newKey, extr
|
|
|
29
54
|
const key = extractKey(line);
|
|
30
55
|
if (key === undefined)
|
|
31
56
|
continue;
|
|
32
|
-
if (key >
|
|
57
|
+
if (mozbuildSortCompare(key, newKey) > 0) {
|
|
33
58
|
insertIndex = i;
|
|
34
59
|
break;
|
|
35
60
|
}
|
|
@@ -61,7 +86,7 @@ export function findAlphabeticalTokenPosition(tokens, sectionTargetPattern, newK
|
|
|
61
86
|
continue;
|
|
62
87
|
const match = sectionTargetPattern.exec(entry.parsed.target);
|
|
63
88
|
const key = match?.[1] ?? entry.parsed.target;
|
|
64
|
-
if (key >
|
|
89
|
+
if (mozbuildSortCompare(key, newKey) > 0) {
|
|
65
90
|
insertLineIndex = entry.lineIndex;
|
|
66
91
|
break;
|
|
67
92
|
}
|
|
@@ -84,7 +109,7 @@ export function findAlphabeticalMozBuildPosition(tokens, newKey) {
|
|
|
84
109
|
let previousEntry;
|
|
85
110
|
for (const item of items) {
|
|
86
111
|
const key = item.parsed?.value ?? '';
|
|
87
|
-
if (key >
|
|
112
|
+
if (mozbuildSortCompare(key, newKey) > 0) {
|
|
88
113
|
insertLineIndex = item.lineIndex;
|
|
89
114
|
break;
|
|
90
115
|
}
|
|
@@ -180,6 +180,37 @@ export declare function findForwardImportIgnoreLines(source: string): Set<number
|
|
|
180
180
|
* engine file — not our concern).
|
|
181
181
|
* - Imports on a line suppressed by the ignore marker.
|
|
182
182
|
*/
|
|
183
|
+
/** One discovered forward-import edge: a site importing a later-created file. */
|
|
184
|
+
export interface ForwardImportEdge {
|
|
185
|
+
/** Importing patch filename. */
|
|
186
|
+
entry: string;
|
|
187
|
+
/** Importing file path relative to engine/. */
|
|
188
|
+
sitePath: string;
|
|
189
|
+
/** Exact import specifier as it appears in source. */
|
|
190
|
+
specifier: string;
|
|
191
|
+
/** Specifier with any query/hash stripped (used for fingerprints). */
|
|
192
|
+
cleaned: string;
|
|
193
|
+
/** Later-ordered patch creating the imported file. */
|
|
194
|
+
owner: string;
|
|
195
|
+
/** Path of the later-created file (relative to engine/). */
|
|
196
|
+
creates: string;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Returns every forward-import edge in the queue, one per (site, later
|
|
200
|
+
* creator) pair, regardless of any staged-dependency declarations. Used by
|
|
201
|
+
* `patch split` to discover the forward edges it introduces into the new
|
|
202
|
+
* patch so it can auto-declare them (and so its projected lint matches the
|
|
203
|
+
* real per-patch gate).
|
|
204
|
+
*/
|
|
205
|
+
export declare function collectForwardImportEdges(ctx: PatchQueueContext): ForwardImportEdge[];
|
|
206
|
+
/**
|
|
207
|
+
* Cross-patch lint rule: flag imports of a module a later-ordered patch is
|
|
208
|
+
* responsible for creating, unless the patch declares an exact staged
|
|
209
|
+
* forward-import for it. Also warns on staged-dependency declarations that
|
|
210
|
+
* never matched (stale). Matching is conservative — by basename, not by
|
|
211
|
+
* resolving `resource://`/`chrome://` URLs — with an inline ignore marker as
|
|
212
|
+
* an escape hatch for unrelated basename collisions.
|
|
213
|
+
*/
|
|
183
214
|
export declare function lintPatchQueueForwardImports(ctx: PatchQueueContext): PatchLintIssue[];
|
|
184
215
|
/**
|
|
185
216
|
* Cross-patch lint orchestrator. Runs every cross-patch rule against the
|
|
@@ -343,27 +343,8 @@ function findMatchingStagedDependency(entry, sitePath, specifier, laterOwners) {
|
|
|
343
343
|
laterOwners.some((owner) => owner.fullPath === dependency.creates &&
|
|
344
344
|
(dependency.owner === undefined || dependency.owner === owner.filename)));
|
|
345
345
|
}
|
|
346
|
-
/**
|
|
347
|
-
|
|
348
|
-
* responsible for creating.
|
|
349
|
-
*
|
|
350
|
-
* Approach is deliberately conservative — we do not resolve `resource://`
|
|
351
|
-
* URLs to engine file paths. Instead we build a cross-queue index of
|
|
352
|
-
* newly-created files keyed by their basename, and flag imports whose leaf
|
|
353
|
-
* matches an entry owned by a later-ordered patch. False positives from
|
|
354
|
-
* unrelated basename collisions (two different directories happening to
|
|
355
|
-
* create files named `Helper.sys.mjs`) are possible; the README documents
|
|
356
|
-
* the limitation and the inline ignore marker above provides an escape
|
|
357
|
-
* hatch.
|
|
358
|
-
*
|
|
359
|
-
* Rules out:
|
|
360
|
-
* - Imports whose leaf matches a newly-created file in the *same* or an
|
|
361
|
-
* *earlier* patch (legitimate use).
|
|
362
|
-
* - Imports whose leaf is not in the new-file index at all (pre-existing
|
|
363
|
-
* engine file — not our concern).
|
|
364
|
-
* - Imports on a line suppressed by the ignore marker.
|
|
365
|
-
*/
|
|
366
|
-
export function lintPatchQueueForwardImports(ctx) {
|
|
346
|
+
/** Builds the basename → later-creators index used by the forward-import scan. */
|
|
347
|
+
function buildNewFileIndex(ctx) {
|
|
367
348
|
const newFileIndex = new Map();
|
|
368
349
|
for (const entry of ctx.entries) {
|
|
369
350
|
for (const fullPath of entry.newFiles.keys()) {
|
|
@@ -376,23 +357,24 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
376
357
|
owners.push({ filename: entry.filename, order: entry.order, fullPath });
|
|
377
358
|
}
|
|
378
359
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
360
|
+
return newFileIndex;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Visits every forward-import site (an import whose leaf is created by a
|
|
364
|
+
* later-ordered patch). Shared by {@link lintPatchQueueForwardImports} (which
|
|
365
|
+
* resolves/reports each) and {@link collectForwardImportEdges} (which returns
|
|
366
|
+
* them structurally). We scan a created file's full body and a modified
|
|
367
|
+
* file's added lines only — flagging imports *this patch introduces*, not
|
|
368
|
+
* pre-existing HEAD imports that happen to collide with a later-created file.
|
|
369
|
+
*/
|
|
370
|
+
function eachForwardImportSite(ctx, newFileIndex, visit) {
|
|
387
371
|
const checkSite = (entry, sitePath, content) => {
|
|
388
372
|
if (!isForwardImportableFile(sitePath))
|
|
389
373
|
return;
|
|
390
374
|
const ignoreLines = findForwardImportIgnoreLines(content);
|
|
391
|
-
const
|
|
392
|
-
for (const { specifier, line } of extracted) {
|
|
375
|
+
for (const { specifier, line } of extractImportSpecifiersWithLines(content)) {
|
|
393
376
|
if (ignoreLines.has(line))
|
|
394
377
|
continue;
|
|
395
|
-
// Take the leaf and strip query/hash if any.
|
|
396
378
|
const cleaned = specifier.split(/[?#]/)[0] ?? specifier;
|
|
397
379
|
const leaf = basename(cleaned);
|
|
398
380
|
if (!leaf || !isForwardImportableFile(leaf))
|
|
@@ -400,41 +382,11 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
400
382
|
const owners = newFileIndex.get(leaf);
|
|
401
383
|
if (!owners)
|
|
402
384
|
continue;
|
|
403
|
-
// Is the owner a later-ordered patch (or one ordered equal but
|
|
404
|
-
// lexicographically later as a tiebreaker)?
|
|
405
385
|
const laterOwners = owners.filter((owner) => owner.order > entry.order ||
|
|
406
386
|
(owner.order === entry.order && owner.filename > entry.filename));
|
|
407
387
|
if (laterOwners.length === 0)
|
|
408
388
|
continue;
|
|
409
|
-
|
|
410
|
-
if (stagedDependency) {
|
|
411
|
-
usedStagedDeclarations.add(stagedDependencyKey(entry, stagedDependency));
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
const ownersSummary = laterOwners
|
|
415
|
-
.map((o) => `${o.filename} (creates ${o.fullPath})`)
|
|
416
|
-
.join(', ');
|
|
417
|
-
const fingerprintOwners = [...laterOwners]
|
|
418
|
-
.map((o) => `${o.filename}:${o.fullPath}`)
|
|
419
|
-
.sort((a, b) => a.localeCompare(b))
|
|
420
|
-
.join(',');
|
|
421
|
-
// Lowest ordinal that lands AFTER every later-ordered creator —
|
|
422
|
-
// turns the operator's "guess and re-run" loop into a single shot
|
|
423
|
-
// when the only fix is reordering.
|
|
424
|
-
const suggestedOrder = Math.max(...laterOwners.map((o) => o.order)) + 1;
|
|
425
|
-
issues.push({
|
|
426
|
-
file: sitePath,
|
|
427
|
-
check: 'forward-import',
|
|
428
|
-
fingerprint: `forward-import|${sitePath}|${cleaned}|${fingerprintOwners}`,
|
|
429
|
-
message: `${sitePath} in ${entry.filename} imports "${specifier}", ` +
|
|
430
|
-
`but the matching new file is created by a later patch: ${ownersSummary}. ` +
|
|
431
|
-
'Reorder the patches so the dependency is created first, move the import ' +
|
|
432
|
-
'into the later patch, declare the intentional staged dependency with ' +
|
|
433
|
-
'"fireforge patch staged-dependency --add", or mark the import with ' +
|
|
434
|
-
`"// ${FORWARD_IMPORT_IGNORE_MARKER}" if the basename collision is a false positive. ` +
|
|
435
|
-
`Closest legal ordinal that satisfies this dependency: ${suggestedOrder}.`,
|
|
436
|
-
severity: 'error',
|
|
437
|
-
});
|
|
389
|
+
visit(entry, sitePath, specifier, cleaned, laterOwners);
|
|
438
390
|
}
|
|
439
391
|
};
|
|
440
392
|
for (const entry of ctx.entries) {
|
|
@@ -443,6 +395,74 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
443
395
|
for (const [path, added] of entry.modifiedFileAdditions)
|
|
444
396
|
checkSite(entry, path, added);
|
|
445
397
|
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Returns every forward-import edge in the queue, one per (site, later
|
|
401
|
+
* creator) pair, regardless of any staged-dependency declarations. Used by
|
|
402
|
+
* `patch split` to discover the forward edges it introduces into the new
|
|
403
|
+
* patch so it can auto-declare them (and so its projected lint matches the
|
|
404
|
+
* real per-patch gate).
|
|
405
|
+
*/
|
|
406
|
+
export function collectForwardImportEdges(ctx) {
|
|
407
|
+
const newFileIndex = buildNewFileIndex(ctx);
|
|
408
|
+
const edges = [];
|
|
409
|
+
eachForwardImportSite(ctx, newFileIndex, (entry, sitePath, specifier, cleaned, laterOwners) => {
|
|
410
|
+
for (const owner of laterOwners) {
|
|
411
|
+
edges.push({
|
|
412
|
+
entry: entry.filename,
|
|
413
|
+
sitePath,
|
|
414
|
+
specifier,
|
|
415
|
+
cleaned,
|
|
416
|
+
owner: owner.filename,
|
|
417
|
+
creates: owner.fullPath,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
return edges;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Cross-patch lint rule: flag imports of a module a later-ordered patch is
|
|
425
|
+
* responsible for creating, unless the patch declares an exact staged
|
|
426
|
+
* forward-import for it. Also warns on staged-dependency declarations that
|
|
427
|
+
* never matched (stale). Matching is conservative — by basename, not by
|
|
428
|
+
* resolving `resource://`/`chrome://` URLs — with an inline ignore marker as
|
|
429
|
+
* an escape hatch for unrelated basename collisions.
|
|
430
|
+
*/
|
|
431
|
+
export function lintPatchQueueForwardImports(ctx) {
|
|
432
|
+
const newFileIndex = buildNewFileIndex(ctx);
|
|
433
|
+
const issues = [];
|
|
434
|
+
const usedStagedDeclarations = new Set();
|
|
435
|
+
eachForwardImportSite(ctx, newFileIndex, (entry, sitePath, specifier, cleaned, laterOwners) => {
|
|
436
|
+
const stagedDependency = findMatchingStagedDependency(entry, sitePath, specifier, laterOwners);
|
|
437
|
+
if (stagedDependency) {
|
|
438
|
+
usedStagedDeclarations.add(stagedDependencyKey(entry, stagedDependency));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const ownersSummary = laterOwners
|
|
442
|
+
.map((o) => `${o.filename} (creates ${o.fullPath})`)
|
|
443
|
+
.join(', ');
|
|
444
|
+
const fingerprintOwners = [...laterOwners]
|
|
445
|
+
.map((o) => `${o.filename}:${o.fullPath}`)
|
|
446
|
+
.sort((a, b) => a.localeCompare(b))
|
|
447
|
+
.join(',');
|
|
448
|
+
// Lowest ordinal that lands AFTER every later-ordered creator —
|
|
449
|
+
// turns the operator's "guess and re-run" loop into a single shot
|
|
450
|
+
// when the only fix is reordering.
|
|
451
|
+
const suggestedOrder = Math.max(...laterOwners.map((o) => o.order)) + 1;
|
|
452
|
+
issues.push({
|
|
453
|
+
file: sitePath,
|
|
454
|
+
check: 'forward-import',
|
|
455
|
+
fingerprint: `forward-import|${sitePath}|${cleaned}|${fingerprintOwners}`,
|
|
456
|
+
message: `${sitePath} in ${entry.filename} imports "${specifier}", ` +
|
|
457
|
+
`but the matching new file is created by a later patch: ${ownersSummary}. ` +
|
|
458
|
+
'Reorder the patches so the dependency is created first, move the import ' +
|
|
459
|
+
'into the later patch, declare the intentional staged dependency with ' +
|
|
460
|
+
'"fireforge patch staged-dependency --add", or mark the import with ' +
|
|
461
|
+
`"// ${FORWARD_IMPORT_IGNORE_MARKER}" if the basename collision is a false positive. ` +
|
|
462
|
+
`Closest legal ordinal that satisfies this dependency: ${suggestedOrder}.`,
|
|
463
|
+
severity: 'error',
|
|
464
|
+
});
|
|
465
|
+
});
|
|
446
466
|
for (const entry of ctx.entries) {
|
|
447
467
|
for (const dependency of entry.metadata?.stagedDependencies?.forwardImports ?? []) {
|
|
448
468
|
if (usedStagedDeclarations.has(stagedDependencyKey(entry, dependency)))
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Public re-exports for {@link ./patch-lint.ts}. Split out so the
|
|
3
3
|
* orchestrator stays within the ESLint `max-lines` budget.
|
|
4
4
|
*/
|
|
5
|
-
export { buildPatchQueueContext, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, type PatchQueueContext, type PatchQueueEntry, } from './patch-lint-cross.js';
|
|
5
|
+
export { buildPatchQueueContext, collectForwardImportEdges, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, type ForwardImportEdge, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, type PatchQueueContext, type PatchQueueEntry, } from './patch-lint-cross.js';
|
|
6
6
|
export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
|