@gjsify/cli 0.52.0 → 0.54.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/dist/cli.gjs.mjs +351 -305
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +72 -1
- package/lib/bundler-pick.js +175 -17
- package/lib/cli-app.js +7 -2
- package/lib/commands/blueprint/index.d.ts +4 -0
- package/lib/commands/blueprint/index.js +15 -0
- package/lib/commands/blueprint/types.d.ts +8 -0
- package/lib/commands/blueprint/types.js +124 -0
- package/lib/commands/devtools.d.ts +21 -0
- package/lib/commands/devtools.js +146 -0
- package/lib/commands/env.d.ts +31 -0
- package/lib/commands/env.js +114 -0
- package/lib/commands/exec.d.ts +10 -0
- package/lib/commands/exec.js +208 -0
- package/lib/commands/foreach.js +5 -97
- package/lib/commands/format.js +26 -6
- package/lib/commands/index.d.ts +5 -0
- package/lib/commands/index.js +5 -0
- package/lib/commands/install.d.ts +2 -2
- package/lib/commands/install.js +5 -5
- package/lib/commands/self-update.js +2 -1
- package/lib/commands/ship.js +51 -8
- package/lib/commands/test.d.ts +18 -9
- package/lib/commands/test.js +56 -11
- package/lib/commands/tsc.js +4 -4
- package/lib/commands/upgrade.d.ts +15 -0
- package/lib/commands/upgrade.js +76 -2
- package/lib/commands/webext/index.d.ts +2 -0
- package/lib/commands/webext/index.js +15 -0
- package/lib/commands/webext/subcommands.d.ts +16 -0
- package/lib/commands/webext/subcommands.js +204 -0
- package/lib/types/config-data.d.ts +1 -1
- package/lib/utils/bin-shim.d.ts +8 -2
- package/lib/utils/bin-shim.js +6 -5
- package/lib/utils/bundle-inputs.d.ts +84 -0
- package/lib/utils/bundle-inputs.js +268 -0
- package/lib/utils/check-system-deps.d.ts +4 -2
- package/lib/utils/check-system-deps.js +21 -26
- package/lib/utils/dev-link.d.ts +9 -6
- package/lib/utils/dev-link.js +71 -19
- package/lib/utils/exec-bin.d.ts +137 -0
- package/lib/utils/exec-bin.js +321 -0
- package/lib/utils/gi-runtime-paths.js +3 -2
- package/lib/utils/gitignore.d.ts +15 -0
- package/lib/utils/gitignore.js +178 -0
- package/lib/utils/gjsify-shim.d.ts +47 -0
- package/lib/utils/gjsify-shim.js +97 -8
- package/lib/utils/install-backend-native.d.ts +19 -1
- package/lib/utils/install-backend-native.js +93 -7
- package/lib/utils/install-lock.js +4 -25
- package/lib/utils/native-skip-scan.d.ts +20 -0
- package/lib/utils/native-skip-scan.js +343 -0
- package/lib/utils/node-script.js +1 -1
- package/lib/utils/oxc-resolve.js +46 -5
- package/lib/utils/process-table.d.ts +22 -0
- package/lib/utils/process-table.js +188 -0
- package/lib/utils/prune-prefix.js +1 -1
- package/lib/utils/run-gjs.d.ts +15 -1
- package/lib/utils/run-gjs.js +16 -7
- package/lib/utils/run-node.d.ts +2 -0
- package/lib/utils/run-node.js +2 -1
- package/lib/utils/runtimes.js +14 -13
- package/lib/utils/ship/app-runtime.d.ts +7 -5
- package/lib/utils/ship/app-runtime.js +11 -8
- package/lib/utils/ship/appimage.d.ts +9 -0
- package/lib/utils/ship/appimage.js +2 -1
- package/lib/utils/ship/deb.d.ts +10 -0
- package/lib/utils/ship/deb.js +7 -0
- package/lib/utils/ship/depends.d.ts +37 -3
- package/lib/utils/ship/depends.js +21 -9
- package/lib/utils/ship/gi-namespaces.d.ts +16 -0
- package/lib/utils/ship/gi-namespaces.js +64 -1
- package/lib/utils/ship/gzip.js +1 -1
- package/lib/utils/ship/icons.d.ts +6 -0
- package/lib/utils/ship/icons.js +6 -5
- package/lib/utils/ship/launcher.js +2 -2
- package/lib/utils/ship/layout.js +25 -2
- package/lib/utils/ship/rpm.d.ts +5 -0
- package/lib/utils/ship/rpm.js +42 -0
- package/lib/utils/ship/settings.js +6 -0
- package/lib/utils/ship/signing.d.ts +7 -2
- package/lib/utils/ship/signing.js +59 -32
- package/lib/utils/ship/stage-manifest.d.ts +14 -0
- package/lib/utils/ship/stage-manifest.js +7 -0
- package/lib/utils/spawn.d.ts +13 -0
- package/lib/utils/spawn.js +10 -5
- package/lib/utils/watch-loop.d.ts +15 -1
- package/lib/utils/watch-loop.js +9 -5
- package/lib/utils/webext/build.d.ts +42 -0
- package/lib/utils/webext/build.js +120 -0
- package/lib/utils/webext/config.d.ts +72 -0
- package/lib/utils/webext/config.js +234 -0
- package/lib/utils/webext/icons.d.ts +24 -0
- package/lib/utils/webext/icons.js +83 -0
- package/lib/utils/webext/launch.d.ts +32 -0
- package/lib/utils/webext/launch.js +77 -0
- package/lib/utils/webext/manifest.d.ts +53 -0
- package/lib/utils/webext/manifest.js +219 -0
- package/lib/utils/webext/pages.d.ts +26 -0
- package/lib/utils/webext/pages.js +61 -0
- package/lib/utils/webext/targets.d.ts +13 -0
- package/lib/utils/webext/targets.js +48 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +20 -19
package/lib/utils/run-gjs.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type SpawnCompletionContract } from './spawn.js';
|
|
|
6
6
|
* variable is set is host-dependent (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see
|
|
7
7
|
* `buildNativeEnv`.
|
|
8
8
|
*/
|
|
9
|
-
export declare function computeNativeEnvForBundle(bundlePath: string, cwd?: string, inherited?: Record<string, string | undefined
|
|
9
|
+
export declare function computeNativeEnvForBundle(bundlePath: string, cwd?: string, inherited?: Record<string, string | undefined>, extraRoots?: readonly string[]): {
|
|
10
10
|
env: NativeEnv;
|
|
11
11
|
envPrefix: string;
|
|
12
12
|
};
|
|
@@ -41,5 +41,19 @@ export interface RunGjsBundleOptions {
|
|
|
41
41
|
* (`utils/node-script.ts`).
|
|
42
42
|
*/
|
|
43
43
|
quiet?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Do not add `gjs exited with code N` to a failing child's own output. For a
|
|
46
|
+
* transparent runner (`gjsify exec`) a non-zero exit is often an ANSWER — `semver
|
|
47
|
+
* -r '>2' 1.0.0` exits 1 to say "no match" — and the child's code is re-raised
|
|
48
|
+
* either way.
|
|
49
|
+
*/
|
|
50
|
+
quietExit?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* More directories to look for native `@gjsify/*` prebuilds in, after the cwd and the
|
|
53
|
+
* bundle's own tree. `gjsify exec` passes the running CLI's: a rebuilt bin takes its
|
|
54
|
+
* polyfills from beside the CLI, so their typelibs live there too — web-ext's rebuild
|
|
55
|
+
* died at load with `Typelib file for namespace 'GjsifyHttpSoupBridge' … not found`.
|
|
56
|
+
*/
|
|
57
|
+
nativeRoots?: readonly string[];
|
|
44
58
|
}
|
|
45
59
|
export declare function runGjsBundle(bundlePath: string, extraArgs: string[] | undefined, options: RunGjsBundleOptions): Promise<void>;
|
package/lib/utils/run-gjs.js
CHANGED
|
@@ -21,12 +21,20 @@ import { captureNativeBacktrace, createHangWatchdog, formatHangReport, hangGrace
|
|
|
21
21
|
* variable is set is host-dependent (`LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` / `PATH`) — see
|
|
22
22
|
* `buildNativeEnv`.
|
|
23
23
|
*/
|
|
24
|
-
export function computeNativeEnvForBundle(bundlePath, cwd = process.cwd(), inherited = process.env) {
|
|
24
|
+
export function computeNativeEnvForBundle(bundlePath, cwd = process.cwd(), inherited = process.env, extraRoots = []) {
|
|
25
25
|
const resolvedBundle = resolve(bundlePath);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
26
|
+
// First root wins per package name: the project, then the bundle's own tree, then
|
|
27
|
+
// whatever the caller adds (see `RunGjsBundleOptions.nativeRoots`).
|
|
28
|
+
const nativePackages = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const root of [cwd, dirname(resolvedBundle), ...extraRoots]) {
|
|
31
|
+
for (const pkg of detectNativePackages(root)) {
|
|
32
|
+
if (seen.has(pkg.name))
|
|
33
|
+
continue;
|
|
34
|
+
seen.add(pkg.name);
|
|
35
|
+
nativePackages.push(pkg);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
30
38
|
// `inherited` feeds BOTH halves — composition and comparison. Split them and the win32
|
|
31
39
|
// branch disagrees with itself: `buildNativeEnv` writes the library variable back under
|
|
32
40
|
// the host's own spelling (`Path` in a stock env block), so a comparison keyed on the
|
|
@@ -58,7 +66,7 @@ function safeMkdtemp() {
|
|
|
58
66
|
}
|
|
59
67
|
}
|
|
60
68
|
export async function runGjsBundle(bundlePath, extraArgs = [], options) {
|
|
61
|
-
const { env: nativeEnv, envPrefix } = computeNativeEnvForBundle(bundlePath);
|
|
69
|
+
const { env: nativeEnv, envPrefix } = computeNativeEnvForBundle(bundlePath, process.cwd(), process.env, options.nativeRoots);
|
|
62
70
|
// The breadcrumb `@gjsify/unit` writes what it is running into, and this process polls —
|
|
63
71
|
// the only way a bundle that has stopped turning its main loop can still say which test it
|
|
64
72
|
// died in. Anything that is not a unit run never writes the file, so the watchdog stays
|
|
@@ -149,7 +157,8 @@ export async function runGjsBundle(bundlePath, extraArgs = [], options) {
|
|
|
149
157
|
if (hung)
|
|
150
158
|
return failed(result.code ?? 1);
|
|
151
159
|
if (result.code !== 0) {
|
|
152
|
-
|
|
160
|
+
if (!options.quietExit)
|
|
161
|
+
console.error(`gjs exited with ${describeExit(result)}`);
|
|
153
162
|
return failed(result.code ?? 1);
|
|
154
163
|
}
|
|
155
164
|
// See RunGjsBundleOptions.exitOnSuccess — terminal callers opt in,
|
package/lib/utils/run-node.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface RunNodeBundleOptions {
|
|
|
26
26
|
exitOnSuccess?: boolean;
|
|
27
27
|
/** Suppress the `$ <env> node …` echo — see `RunGjsBundleOptions.quiet`. */
|
|
28
28
|
quiet?: boolean;
|
|
29
|
+
/** Leave a failing child's output alone — see `RunGjsBundleOptions.quietExit`. */
|
|
30
|
+
quietExit?: boolean;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* Run a `--app node` bundle on Node.js with the native typelib env set (same detection as
|
package/lib/utils/run-node.js
CHANGED
|
@@ -148,7 +148,8 @@ export async function runRuntimeBundle(runtime, bundlePath, extraArgs = [], opti
|
|
|
148
148
|
return failed(1);
|
|
149
149
|
}
|
|
150
150
|
if (result.code !== 0) {
|
|
151
|
-
|
|
151
|
+
if (!options.quietExit)
|
|
152
|
+
console.error(`${runtime} exited with ${describeExit(result)}`);
|
|
152
153
|
return failed(result.code ?? 1);
|
|
153
154
|
}
|
|
154
155
|
if (options.exitOnSuccess)
|
package/lib/utils/runtimes.js
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
// runtime→build-target mapping and `availableRuntimes()` live in ONE place instead
|
|
7
7
|
// of a per-example copy.
|
|
8
8
|
//
|
|
9
|
-
// Intentionally PURE (only `node:child_process`
|
|
10
|
-
// `@gjsify/rolldown-plugin-gjsify/runtime` host
|
|
11
|
-
// imports) so an external example can deep-import
|
|
12
|
-
// (`@gjsify/cli/lib/utils/runtimes.js`) without pulling
|
|
13
|
-
// same deep-import precedent as `@gjsify/cli/lib/utils/run-gjs.js`.
|
|
14
|
-
import {
|
|
9
|
+
// Intentionally PURE (only `node:child_process`, `./spawn.js`'s Windows command
|
|
10
|
+
// rewrite and the side-effect-free `@gjsify/rolldown-plugin-gjsify/runtime` host
|
|
11
|
+
// detector, no `gi://` or native imports) so an external example can deep-import
|
|
12
|
+
// it from the installed CLI (`@gjsify/cli/lib/utils/runtimes.js`) without pulling
|
|
13
|
+
// in the whole bundler — the same deep-import precedent as `@gjsify/cli/lib/utils/run-gjs.js`.
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
15
|
import { hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
16
|
+
import { toSpawnable } from './spawn.js';
|
|
16
17
|
/** All runtimes, in canonical order (gjs first — it is the reference). */
|
|
17
18
|
export const EXAMPLE_RUNTIMES = ['gjs', 'node', 'bun', 'deno'];
|
|
18
19
|
/**
|
|
@@ -57,13 +58,13 @@ export function requiresGjsSystemDeps(runtime) {
|
|
|
57
58
|
}
|
|
58
59
|
/** Whether `<probe> --version` succeeds (i.e. the runtime binary is on PATH). */
|
|
59
60
|
export function isRuntimeOnPath(probe) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
const inv = toSpawnable(probe, ['--version']);
|
|
62
|
+
const r = spawnSync(inv.cmd, inv.args, {
|
|
63
|
+
stdio: 'ignore',
|
|
64
|
+
timeout: 15000,
|
|
65
|
+
windowsVerbatimArguments: inv.windowsVerbatimArguments,
|
|
66
|
+
});
|
|
67
|
+
return !r.error && r.status === 0;
|
|
67
68
|
}
|
|
68
69
|
/**
|
|
69
70
|
* Whether a specific runtime is runnable here. The HOST runtime needs no PATH probe
|
|
@@ -102,8 +102,8 @@ export interface ResolvedNodeGiPackage {
|
|
|
102
102
|
* NOT the published tarball. `files` in node-gi's manifest also lists `src/`,
|
|
103
103
|
* `binding.gyp` and `scripts/install.mjs` — the inputs to `node-gyp`, which a
|
|
104
104
|
* `.app` never runs — and the `.d.ts` declarations, which nothing inside a bundle
|
|
105
|
-
* compiles against. And it lists `prebuilds/`, which this module stages
|
|
106
|
-
* `
|
|
105
|
+
* compiles against. And it lists `prebuilds/`, which this module stages under
|
|
106
|
+
* `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
|
|
107
107
|
*
|
|
108
108
|
* So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
|
|
109
109
|
* `package.json` (the `exports` map is what resolves every subpath), every `.js`
|
|
@@ -120,15 +120,17 @@ export declare function resolveNodeGiPackage(options?: ResolveRuntimeOptions): R
|
|
|
120
120
|
* literals, so the paths the launcher exports and the paths the stager writes
|
|
121
121
|
* cannot drift: they are computed here once and both callers read this result.
|
|
122
122
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
123
|
+
* `<dirs.native>/node-gi/prebuilds/<target>/` is node-gi's OWN sibling layout
|
|
124
|
+
* reproduced verbatim inside `Layout.dirs().native` (`Contents/Resources/native`
|
|
125
|
+
* on a `.app` — `layout.ts`'s darwin row carries why that is not
|
|
126
|
+
* `Contents/Frameworks`). It is not a naming preference:
|
|
125
127
|
* `resolveGtkRuntimeBundle()` probes `<dir>/lib` and `<dir>/girepository-1.0`,
|
|
126
128
|
* the addon's `@rpath` is `@loader_path/gtk/lib`, and `loaders.cache` addresses
|
|
127
129
|
* each pixbuf loader relative to the bundle toplevel. Reproducing the shape is
|
|
128
130
|
* what makes every one of those relations still true after the copy.
|
|
129
131
|
*/
|
|
130
132
|
export interface AppRuntimePaths {
|
|
131
|
-
/** `<App>.app/Contents/
|
|
133
|
+
/** `<App>.app/Contents/Resources/native/node-gi` — the package root the layout imitates. */
|
|
132
134
|
nodeGiRoot: string;
|
|
133
135
|
/**
|
|
134
136
|
* `<App>.app/Contents/Resources/lib/node_modules/@gjsify/node-gi` — where
|
|
@@ -47,11 +47,14 @@
|
|
|
47
47
|
// THAT directory — so `prebuildAddonPath()` looks for
|
|
48
48
|
// `…/node_modules/@gjsify/node-gi/prebuilds/<target>/node_gi.node` and
|
|
49
49
|
// `resolveGtkRuntimeBundle()`'s candidate 2 for `…/prebuilds/<target>/gtk`, and
|
|
50
|
-
// neither is where a `.app` may keep loadable code: `
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
50
|
+
// neither is where a `.app` may keep loadable code: `Layout.dirs().native` is, and
|
|
51
|
+
// `utils/ship/signing.ts` re-signs every Mach-O in the payload by magic number
|
|
52
|
+
// (ADR 0024 § A4 — M6 landed), which is most of this closure. What `native` may NOT
|
|
53
|
+
// be is `Contents/Frameworks`: `codesign` reads a version-named DIRECTORY there as
|
|
54
|
+
// a nested bundle and refuses the seal, which is the whole reason this file's
|
|
55
|
+
// output went into `Contents/Resources/native` instead — see `layout.ts`'s darwin
|
|
56
|
+
// row for the measurement and `gjsify ship darwin --sign` for what it used to
|
|
57
|
+
// cost.
|
|
55
58
|
//
|
|
56
59
|
// WHY NOT `gjsify.ship.bundledTypelibs`. Because it FLATTENS. `plan.ts` stages
|
|
57
60
|
// each such file as `posix.join(libDir, 'gi', basename(file))`, and
|
|
@@ -210,8 +213,8 @@ function resolveStagedPackage(specifier, options) {
|
|
|
210
213
|
* NOT the published tarball. `files` in node-gi's manifest also lists `src/`,
|
|
211
214
|
* `binding.gyp` and `scripts/install.mjs` — the inputs to `node-gyp`, which a
|
|
212
215
|
* `.app` never runs — and the `.d.ts` declarations, which nothing inside a bundle
|
|
213
|
-
* compiles against. And it lists `prebuilds/`, which this module stages
|
|
214
|
-
* `
|
|
216
|
+
* compiles against. And it lists `prebuilds/`, which this module stages under
|
|
217
|
+
* `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
|
|
215
218
|
*
|
|
216
219
|
* So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
|
|
217
220
|
* `package.json` (the `exports` map is what resolves every subpath), every `.js`
|
|
@@ -252,7 +255,7 @@ export function appRuntimePaths(layout, identity, target) {
|
|
|
252
255
|
addonPath: posix.join(prebuildDir, NODE_GI_ADDON_FILENAME),
|
|
253
256
|
// `Contents/MacOS`, because that is where a `.app` keeps executables and
|
|
254
257
|
// because `$here` — which the launcher already computes — is then the whole
|
|
255
|
-
// path expression. `
|
|
258
|
+
// path expression. `dirs.native` would work for dyld and would make
|
|
256
259
|
// the launcher walk back up for the one file it execs. On Windows
|
|
257
260
|
// `dirs.launcher` is the program directory itself, so the interpreter lands
|
|
258
261
|
// beside the `.cmd` for the same reason.
|
|
@@ -89,6 +89,15 @@ export declare function appImageHostRequirements(input: {
|
|
|
89
89
|
minGjsVersion?: string;
|
|
90
90
|
minNodeVersion?: string;
|
|
91
91
|
namespaces: readonly string[];
|
|
92
|
+
/**
|
|
93
|
+
* The subset of {@link namespaces} the bundle marks optional (ADR 0087).
|
|
94
|
+
*
|
|
95
|
+
* An AppImage has no `Depends:` field, so "optional" cannot become a package
|
|
96
|
+
* list here — it becomes WORDING instead. That is not cosmetic: an AppRun user
|
|
97
|
+
* told a typelib is required and lacking it goes looking for another distro
|
|
98
|
+
* package, while one told it is optional reads the same sentence correctly.
|
|
99
|
+
*/
|
|
100
|
+
optionalNamespaces?: readonly string[];
|
|
92
101
|
bundledTypelibs?: readonly string[];
|
|
93
102
|
}): string[];
|
|
94
103
|
/**
|
|
@@ -204,9 +204,10 @@ export function appDirFor(outRoot) {
|
|
|
204
204
|
*/
|
|
205
205
|
export function appImageHostRequirements(input) {
|
|
206
206
|
const floor = interpreterFloor(input);
|
|
207
|
+
const optional = new Set(input.optionalNamespaces ?? []);
|
|
207
208
|
return [
|
|
208
209
|
`${input.app} (>= ${floor})`,
|
|
209
|
-
...hostProvidedNamespaces(input.namespaces, input.bundledTypelibs).map((ns) => `the ${ns} typelib`),
|
|
210
|
+
...hostProvidedNamespaces(input.namespaces, input.bundledTypelibs).map((ns) => optional.has(ns) ? `the ${ns} typelib (optional)` : `the ${ns} typelib`),
|
|
210
211
|
];
|
|
211
212
|
}
|
|
212
213
|
/**
|
package/lib/utils/ship/deb.d.ts
CHANGED
|
@@ -8,6 +8,16 @@ export interface DebInputs {
|
|
|
8
8
|
prefix: string;
|
|
9
9
|
/** Already-derived dependency list, in `name`/`name >= version` form. */
|
|
10
10
|
depends: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Soft dependencies, rendered as `Recommends:`.
|
|
13
|
+
*
|
|
14
|
+
* Debian Policy § 7.2: a recommended package is installed by default and
|
|
15
|
+
* `--no-install-recommends` is the user's way out. That is what an `&optional`
|
|
16
|
+
* typelib is (ADR 0087) — the app runs without it and loses a feature — so
|
|
17
|
+
* putting it in `Depends:` would let an optional integration decide whether
|
|
18
|
+
* the package installs at all.
|
|
19
|
+
*/
|
|
20
|
+
recommends?: readonly string[];
|
|
11
21
|
/** Debian architecture (`amd64`, `all`, …). */
|
|
12
22
|
archLabel: string;
|
|
13
23
|
/** Unix seconds stamped into every header. */
|
package/lib/utils/ship/deb.js
CHANGED
|
@@ -104,6 +104,13 @@ function renderControl(inputs, installedSize) {
|
|
|
104
104
|
];
|
|
105
105
|
if (inputs.depends.length > 0)
|
|
106
106
|
fields.push(['Depends', inputs.depends.map(formatDebDepend).join(', ')]);
|
|
107
|
+
// After `Depends`, which is the field order dpkg-deb --control writes and the
|
|
108
|
+
// one `grep '^Depends:'` on a control file assumes. Omitted when empty rather
|
|
109
|
+
// than written as `Recommends:` with nothing after it — a field with an empty
|
|
110
|
+
// value is a parse error here, the same reason `formatDebDepend` exists.
|
|
111
|
+
if (inputs.recommends !== undefined && inputs.recommends.length > 0) {
|
|
112
|
+
fields.push(['Recommends', inputs.recommends.map(formatDebDepend).join(', ')]);
|
|
113
|
+
}
|
|
107
114
|
fields.push(['Section', settings.section], ['Priority', 'optional']);
|
|
108
115
|
if (settings.homepage)
|
|
109
116
|
fields.push(['Homepage', settings.homepage]);
|
|
@@ -43,6 +43,17 @@ export declare function warnAboutNodeFloor(format: DistroFormatId, floor: string
|
|
|
43
43
|
export interface DependsInputs {
|
|
44
44
|
/** GI namespaces the bundle imports, in `Ns-Version` spelling. */
|
|
45
45
|
namespaces: readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* The subset of {@link namespaces} the bundle declares OPTIONAL (ADR 0087 —
|
|
48
|
+
* `gi://Ns?version=X&optional`): a `Recommends:` entry, never a `Depends:`.
|
|
49
|
+
*
|
|
50
|
+
* Kept as its own list rather than filtered out of {@link namespaces}, because
|
|
51
|
+
* the AppImage has no package list at all and still has to SAY which of its host
|
|
52
|
+
* requirements are optional — and because a stage manifest written before the
|
|
53
|
+
* split carries no such field, where answering "everything is hard" is the safe
|
|
54
|
+
* direction.
|
|
55
|
+
*/
|
|
56
|
+
optionalNamespaces?: readonly string[];
|
|
46
57
|
/**
|
|
47
58
|
* Whether the payload installs into `share/icons/hicolor/`.
|
|
48
59
|
*
|
|
@@ -112,11 +123,34 @@ export interface DependsInputs {
|
|
|
112
123
|
*/
|
|
113
124
|
export declare function hostProvidedNamespaces(namespaces: readonly string[], bundledTypelibs?: readonly string[]): string[];
|
|
114
125
|
/**
|
|
115
|
-
*
|
|
126
|
+
* One format's dependency lists, split by how hard the requirement is.
|
|
127
|
+
*
|
|
128
|
+
* `requires` is what an absent package makes the app fail over; `recommends` is what
|
|
129
|
+
* its absence only costs. A namespace the BUNDLE marked optional (ADR 0087) is the
|
|
130
|
+
* second kind by declaration — the app's own `check()` degrades — and both `.deb`
|
|
131
|
+
* and `.rpm` have a field for it, so there is no reason to declare it hard.
|
|
132
|
+
*/
|
|
133
|
+
export interface DerivedDepends {
|
|
134
|
+
/** `Depends:` / `Requires:` — a package the artifact cannot run without. */
|
|
135
|
+
requires: string[];
|
|
136
|
+
/** `Recommends:` — installed by default where the format has such a field. */
|
|
137
|
+
recommends: string[];
|
|
138
|
+
/**
|
|
139
|
+
* Optional namespaces no table row is known for.
|
|
140
|
+
*
|
|
141
|
+
* REPORTED, never fatal: a `Recommends:` line for a namespace nobody mapped is
|
|
142
|
+
* the one case where saying nothing is correct — the app runs without the
|
|
143
|
+
* typelib either way, which is precisely what the flag declared. The HARD
|
|
144
|
+
* unmapped case still throws, below.
|
|
145
|
+
*/
|
|
146
|
+
unmappedOptional: string[];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The `Depends:` / `Requires:` list for one format, plus its `Recommends:`.
|
|
116
150
|
*
|
|
117
|
-
* @throws when a namespace has no entry in the table — see the module header.
|
|
151
|
+
* @throws when a HARD namespace has no entry in the table — see the module header.
|
|
118
152
|
*/
|
|
119
|
-
export declare function deriveDepends(format: DistroFormatId, inputs: DependsInputs):
|
|
153
|
+
export declare function deriveDepends(format: DistroFormatId, inputs: DependsInputs): DerivedDepends;
|
|
120
154
|
/** One parsed dependency. `relation`/`version` are absent for an unversioned one. */
|
|
121
155
|
export interface ParsedDepend {
|
|
122
156
|
name: string;
|
|
@@ -287,9 +287,9 @@ export function hostProvidedNamespaces(namespaces, bundledTypelibs = []) {
|
|
|
287
287
|
return [...new Set(namespaces)].sort().filter((ns) => !BUNDLED_TYPELIB.test(ns) && !shipped.has(ns));
|
|
288
288
|
}
|
|
289
289
|
/**
|
|
290
|
-
* The `Depends:` / `Requires:` list for one format
|
|
290
|
+
* The `Depends:` / `Requires:` list for one format, plus its `Recommends:`.
|
|
291
291
|
*
|
|
292
|
-
* @throws when a namespace has no entry in the table — see the module header.
|
|
292
|
+
* @throws when a HARD namespace has no entry in the table — see the module header.
|
|
293
293
|
*/
|
|
294
294
|
export function deriveDepends(format, inputs) {
|
|
295
295
|
// The interpreter the launcher execs, and nothing else: a package declares ONE.
|
|
@@ -297,17 +297,25 @@ export function deriveDepends(format, inputs) {
|
|
|
297
297
|
// no system Node, so an artifact for those CARRIES one from
|
|
298
298
|
// `@gjsify/node-runtime-<target>`, and that is the whole reason those three
|
|
299
299
|
// packages exist and this one line is their opposite.
|
|
300
|
-
const
|
|
300
|
+
const requires = inputs.interpreter === 'node'
|
|
301
301
|
? [`${NODE_PACKAGE[format]} >= ${inputs.minNodeVersion ?? DEFAULT_NODE_FLOOR}`]
|
|
302
302
|
: [`gjs >= ${inputs.minGjsVersion ?? DEFAULT_GJS_FLOOR}`];
|
|
303
|
+
const recommends = [];
|
|
304
|
+
const optional = new Set(inputs.optionalNamespaces ?? []);
|
|
303
305
|
const unmapped = [];
|
|
306
|
+
const unmappedOptional = [];
|
|
304
307
|
for (const namespace of hostProvidedNamespaces(inputs.namespaces, inputs.bundledTypelibs)) {
|
|
305
308
|
const entry = lookupTypelib(namespace, inputs.typelibPackages);
|
|
306
309
|
if (entry === undefined) {
|
|
307
|
-
unmapped.push(namespace);
|
|
310
|
+
(optional.has(namespace) ? unmappedOptional : unmapped).push(namespace);
|
|
308
311
|
continue;
|
|
309
312
|
}
|
|
310
|
-
|
|
313
|
+
// A namespace imported both ways is a HARD dependency — one importer asked
|
|
314
|
+
// for it unguarded — and a package manager field may not name it twice.
|
|
315
|
+
if (optional.has(namespace) && !requires.includes(entry[format]))
|
|
316
|
+
recommends.push(entry[format]);
|
|
317
|
+
else
|
|
318
|
+
requires.push(entry[format]);
|
|
311
319
|
}
|
|
312
320
|
if (unmapped.length > 0) {
|
|
313
321
|
throw new Error(`gjsify ship: the bundle imports ${unmapped.map((n) => `gi://${n.split('-')[0]}`).join(', ')}, ` +
|
|
@@ -323,7 +331,7 @@ export function deriveDepends(format, inputs) {
|
|
|
323
331
|
// The package that owns `/usr/share/icons/hicolor` — so this follows the icons, not the
|
|
324
332
|
// app/cli distinction that used to stand in for them.
|
|
325
333
|
if (inputs.hasIcons)
|
|
326
|
-
|
|
334
|
+
requires.push('hicolor-icon-theme');
|
|
327
335
|
// The package that ships `glib-compile-schemas`, NOT `gsettings-desktop-schemas`
|
|
328
336
|
// (which ships GNOME's own `org.gnome.desktop.*` schemas and has nothing to
|
|
329
337
|
// do with compiling ours). Measured: `rpm -qf /usr/bin/glib-compile-schemas`
|
|
@@ -333,11 +341,15 @@ export function deriveDepends(format, inputs) {
|
|
|
333
341
|
// `Gio.Settings.new()` aborts the app — an install that succeeds and an app
|
|
334
342
|
// that does not start.
|
|
335
343
|
if (inputs.hasSchemas)
|
|
336
|
-
|
|
337
|
-
|
|
344
|
+
requires.push(SCHEMA_COMPILER_PACKAGE[format]);
|
|
345
|
+
requires.push(...inputs.extra);
|
|
338
346
|
// Set-dedupe keeps first-insertion order, so the interpreter stays first and
|
|
339
347
|
// the list is stable across runs.
|
|
340
|
-
return
|
|
348
|
+
return {
|
|
349
|
+
requires: [...new Set(requires)],
|
|
350
|
+
recommends: [...new Set(recommends)].filter((name) => !requires.includes(name)),
|
|
351
|
+
unmappedOptional,
|
|
352
|
+
};
|
|
341
353
|
}
|
|
342
354
|
/**
|
|
343
355
|
* Resolve a scanned specifier to a table row.
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a bundle loads, split by whether the host must provide it.
|
|
3
|
+
*
|
|
4
|
+
* `namespaces` is the union — the same answer {@link scanGiNamespaces} has always
|
|
5
|
+
* given — and `optional` is the subset the artifact itself marks optional. A namespace
|
|
6
|
+
* in both is still a hard dependency as far as anything else is concerned: the
|
|
7
|
+
* importer asked for it both ways, and only one of those edges degrades.
|
|
8
|
+
*/
|
|
9
|
+
export interface GiRequirements {
|
|
10
|
+
/** Every GI namespace the bundle loads, `Ns-Version` where a specifier pins one. */
|
|
11
|
+
namespaces: string[];
|
|
12
|
+
/** The subset carrying the bundler's optional marker — absent is not fatal. */
|
|
13
|
+
optional: string[];
|
|
14
|
+
}
|
|
1
15
|
/**
|
|
2
16
|
* Extract the GI namespaces a bundle imports, as `Ns-Version` when the
|
|
3
17
|
* specifier pins one and bare `Ns` when it does not.
|
|
4
18
|
*/
|
|
5
19
|
export declare function scanGiNamespaces(source: string): string[];
|
|
20
|
+
/** {@link scanGiNamespaces}, plus which of them the artifact marks optional (ADR 0087). */
|
|
21
|
+
export declare function scanGiRequirements(source: string): GiRequirements;
|
|
6
22
|
/** `gi://Gtk?version=4.0` → `Gtk-4.0`; `gi://Gtk` → `Gtk`; anything else → null. */
|
|
7
23
|
export declare function parseGiSpecifier(specifier: string): string | null;
|
|
@@ -34,6 +34,18 @@
|
|
|
34
34
|
// `walkModuleAst` answers both questions in ONE pass, and it is the same acorn
|
|
35
35
|
// pass the CLI already uses to compute its own runtime closure — so there is one
|
|
36
36
|
// definition of "what does this file import" rather than two.
|
|
37
|
+
//
|
|
38
|
+
// HARD OR OPTIONAL, since ADR 0087: a `gi://Ns?version=X&optional` import still
|
|
39
|
+
// appears in the emitted file, so this scanner still reports its namespace — and
|
|
40
|
+
// `depends.ts` has to know it may be absent. The discriminator is the marker
|
|
41
|
+
// statement the bundler's own optional shim emits (`GI_OPTIONAL_MARKER`, imported
|
|
42
|
+
// from the plugin that writes it rather than restated here). NOT the `import()`
|
|
43
|
+
// SHAPE, which was the tempting reading of the same file: an app that writes its own
|
|
44
|
+
// `await import('gi://Goa')` without the flag must stay a HARD dependency, and
|
|
45
|
+
// classifying it as optional would move a package the host must have out of its
|
|
46
|
+
// `Depends:` — the failure this whole chain exists to prevent, reached from the other
|
|
47
|
+
// direction.
|
|
48
|
+
import { GI_OPTIONAL_MARKER } from '@gjsify/rolldown-plugin-gjsify';
|
|
37
49
|
import { importedSpecifier, staticStringValue, walkModuleAst, } from '../cli-runtime-closure.js';
|
|
38
50
|
/** The module a `--app node` bundle reaches GI through. */
|
|
39
51
|
const NODE_GI_MODULE = '@gjsify/node-gi/gi';
|
|
@@ -44,7 +56,12 @@ const NAMESPACE = /^[A-Za-z][A-Za-z\d_]*$/;
|
|
|
44
56
|
* specifier pins one and bare `Ns` when it does not.
|
|
45
57
|
*/
|
|
46
58
|
export function scanGiNamespaces(source) {
|
|
59
|
+
return scanGiRequirements(source).namespaces;
|
|
60
|
+
}
|
|
61
|
+
/** {@link scanGiNamespaces}, plus which of them the artifact marks optional (ADR 0087). */
|
|
62
|
+
export function scanGiRequirements(source) {
|
|
47
63
|
const found = new Set();
|
|
64
|
+
const optional = new Set();
|
|
48
65
|
// TWO SETS, because the two kinds of binding are not interchangeable and
|
|
49
66
|
// treating them as one over-approximated on a real minified bundle. A
|
|
50
67
|
// namespace object (`import * as gi`) and a required module object are only
|
|
@@ -89,11 +106,57 @@ export function scanGiNamespaces(source) {
|
|
|
89
106
|
calls.push(node);
|
|
90
107
|
});
|
|
91
108
|
for (const call of calls) {
|
|
109
|
+
const marker = optionalMarkerNamespace(call);
|
|
110
|
+
if (marker !== null) {
|
|
111
|
+
optional.add(marker);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
92
114
|
const key = requireGiNamespace(call, callable, objects);
|
|
93
115
|
if (key !== null)
|
|
94
116
|
found.add(key);
|
|
95
117
|
}
|
|
96
|
-
return [...found].sort();
|
|
118
|
+
return { namespaces: [...found].sort(), optional: [...optional].filter((ns) => found.has(ns)).sort() };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The namespace an optional-marker call names, or `null` when this is not one.
|
|
122
|
+
*
|
|
123
|
+
* The shape is the bundler's: `globalThis[Symbol.for(GI_OPTIONAL_MARKER)]?.("Ns", "X")`,
|
|
124
|
+
* matched on BOTH halves rather than on the whole statement — the minifier rewrites
|
|
125
|
+
* the string literals to backticks and the optional call to a `ChainExpression`
|
|
126
|
+
* around the `CallExpression` this walk still reaches, and a plain call (a host that
|
|
127
|
+
* has a handler the minifier could prove is non-null) reads the same way.
|
|
128
|
+
*/
|
|
129
|
+
function optionalMarkerNamespace(call) {
|
|
130
|
+
const callee = call.callee;
|
|
131
|
+
if (callee?.type !== 'MemberExpression' || callee.computed !== true)
|
|
132
|
+
return null;
|
|
133
|
+
// `globalThis[Symbol.for(KEY)]` — a computed member whose PROPERTY is the
|
|
134
|
+
// `Symbol.for` call, so the key is an argument of that inner call rather than
|
|
135
|
+
// of the outer one. Verified against acorn's own shape: `optional` chaining
|
|
136
|
+
// wraps the whole thing in a `ChainExpression`, which the walk descends into,
|
|
137
|
+
// so the `CallExpression` reaches this function either way.
|
|
138
|
+
const keyCall = callee.property;
|
|
139
|
+
if (keyCall?.type !== 'CallExpression')
|
|
140
|
+
return null;
|
|
141
|
+
const keyCallee = keyCall.callee;
|
|
142
|
+
const keyObject = keyCallee?.type === 'MemberExpression' ? keyCallee.object : undefined;
|
|
143
|
+
const keyProperty = keyCallee?.type === 'MemberExpression' ? keyCallee.property : undefined;
|
|
144
|
+
if (keyObject?.type !== 'Identifier' || keyObject.name !== 'Symbol')
|
|
145
|
+
return null;
|
|
146
|
+
if (keyProperty?.type !== 'Identifier' || keyProperty.name !== 'for')
|
|
147
|
+
return null;
|
|
148
|
+
const keyArgs = keyCall.arguments ?? [];
|
|
149
|
+
if (staticStringValue(keyArgs[0]) !== GI_OPTIONAL_MARKER)
|
|
150
|
+
return null;
|
|
151
|
+
const host = callee.object;
|
|
152
|
+
if (host?.type !== 'Identifier' || host.name !== 'globalThis')
|
|
153
|
+
return null;
|
|
154
|
+
const args = call.arguments ?? [];
|
|
155
|
+
const namespace = staticStringValue(args[0]);
|
|
156
|
+
if (namespace === null || !NAMESPACE.test(namespace))
|
|
157
|
+
return null;
|
|
158
|
+
const version = staticStringValue(args[1]);
|
|
159
|
+
return version === null || version === '' ? namespace : `${namespace}-${version}`;
|
|
97
160
|
}
|
|
98
161
|
/** `gi://Gtk?version=4.0` → `Gtk-4.0`; `gi://Gtk` → `Gtk`; anything else → null. */
|
|
99
162
|
export function parseGiSpecifier(specifier) {
|
package/lib/utils/ship/gzip.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// asked for only where a reader demands it (see below) and not blanket-applied
|
|
27
27
|
// to the payload tarballs.
|
|
28
28
|
//
|
|
29
|
-
// Ledgered in `status/open-todos.md`; closing it means pinning ONE deflate
|
|
29
|
+
// Ledgered in `status/open-todos/README.md`; closing it means pinning ONE deflate
|
|
30
30
|
// implementation, which is a larger decision than this file.
|
|
31
31
|
import { gzip } from '@gjsify/tar';
|
|
32
32
|
/**
|
|
@@ -157,6 +157,12 @@ export interface RasterizeInput {
|
|
|
157
157
|
* alone passes just as well when nobody wires it in.
|
|
158
158
|
*/
|
|
159
159
|
spawn?: typeof spawnToCompletion;
|
|
160
|
+
/**
|
|
161
|
+
* The command a refusal names (default `gjsify ship`). `gjsify webext` renders
|
|
162
|
+
* its toolbar icons through the same child, and a message blaming `ship`
|
|
163
|
+
* would send its reader to the wrong command's documentation.
|
|
164
|
+
*/
|
|
165
|
+
command?: string;
|
|
160
166
|
}
|
|
161
167
|
/**
|
|
162
168
|
* Render an SVG at every size through librsvg, in a child GJS.
|
package/lib/utils/ship/icons.js
CHANGED
|
@@ -267,6 +267,7 @@ export function rasterizerEnv(opts = {}) {
|
|
|
267
267
|
export async function rasterizeSvg(input) {
|
|
268
268
|
if (input.sizes.length === 0)
|
|
269
269
|
return new Map();
|
|
270
|
+
const command = input.command ?? 'gjsify ship';
|
|
270
271
|
// THE CLI'S OWN SPAWN WRAPPER, not `promisify(execFile)`. Measured under the
|
|
271
272
|
// GJS-built CLI (`dist/cli.gjs.mjs`, gjs 1.88): `node:util`'s `promisify` over
|
|
272
273
|
// `@gjsify/child_process`'s `execFile` resolves with the bare stdout string —
|
|
@@ -285,7 +286,7 @@ export async function rasterizeSvg(input) {
|
|
|
285
286
|
// A 1024 px PNG of a detailed icon is a few hundred kilobytes; base64 adds
|
|
286
287
|
// a third. Node's default 1 MiB is within reach of one large element.
|
|
287
288
|
maxBuffer: 64 * 1024 * 1024,
|
|
288
|
-
notFound: () => new Error(
|
|
289
|
+
notFound: () => new Error(`${command}: \`${RASTERIZER}\` is not on PATH, and it is what renders ${input.svg} into the ` +
|
|
289
290
|
`sizes an icon needs on this OS. ${RASTERIZER_HINT}.`),
|
|
290
291
|
});
|
|
291
292
|
const stdout = result.stdout ?? '';
|
|
@@ -296,7 +297,7 @@ export async function rasterizeSvg(input) {
|
|
|
296
297
|
const hint = /Rsvg|Typelib|cairo/i.test(stderr)
|
|
297
298
|
? ` The Rsvg typelib or GJS's cairo binding is missing. ${RASTERIZER_HINT}.`
|
|
298
299
|
: '';
|
|
299
|
-
throw new Error(
|
|
300
|
+
throw new Error(`${command}: rendering ${input.svg} through ${RASTERIZER} failed (exit ${result.code ?? result.signal}).` +
|
|
300
301
|
`${hint}${stderr === '' ? '' : `\n ${stderr.split('\n').join('\n ')}`}`);
|
|
301
302
|
}
|
|
302
303
|
let parsed;
|
|
@@ -304,19 +305,19 @@ export async function rasterizeSvg(input) {
|
|
|
304
305
|
parsed = JSON.parse(stdout);
|
|
305
306
|
}
|
|
306
307
|
catch {
|
|
307
|
-
throw new Error(
|
|
308
|
+
throw new Error(`${command}: ${RASTERIZER} printed something other than the rendered icon while rendering ` +
|
|
308
309
|
`${input.svg}: ${stdout.slice(0, 200)}`);
|
|
309
310
|
}
|
|
310
311
|
const out = new Map();
|
|
311
312
|
for (const size of input.sizes) {
|
|
312
313
|
const encoded = parsed[String(size)];
|
|
313
314
|
if (encoded === undefined) {
|
|
314
|
-
throw new Error(
|
|
315
|
+
throw new Error(`${command}: ${RASTERIZER} rendered no ${size} px image of ${input.svg}.`);
|
|
315
316
|
}
|
|
316
317
|
const png = new Uint8Array(Buffer.from(encoded, 'base64'));
|
|
317
318
|
const { width, height } = readPngSize(png, `the ${size} px render of ${input.svg}`);
|
|
318
319
|
if (width !== size || height !== size) {
|
|
319
|
-
throw new Error(
|
|
320
|
+
throw new Error(`${command}: asked ${RASTERIZER} for a ${size} px render of ${input.svg} and got ${width}×${height}.`);
|
|
320
321
|
}
|
|
321
322
|
out.set(size, png);
|
|
322
323
|
}
|
|
@@ -171,7 +171,7 @@ function renderPrefixLauncher(settings, bundleRelPath, layout) {
|
|
|
171
171
|
* IT EXECS WHAT THE BUNDLE CARRIES. `runtime` comes from
|
|
172
172
|
* `utils/ship/app-runtime.ts` and names the interpreter, the relocated GTK
|
|
173
173
|
* closure and the node-gi addon staged inside `Contents/` — so the exec line is
|
|
174
|
-
* `"$here/node"` and the two locators below point into `
|
|
174
|
+
* `"$here/node"` and the two locators below point into `dirs.native`.
|
|
175
175
|
* When the stage carries none of them every line here is byte-identical to what
|
|
176
176
|
* M2a wrote, which is what keeps a bundle assembled without the runtime packages
|
|
177
177
|
* exactly as (un)usable as it was rather than differently broken.
|
|
@@ -261,7 +261,7 @@ function renderAppBundleLauncher(settings, bundleRelPath, layout, runtime) {
|
|
|
261
261
|
// `node_modules`, and a shipped `.app` has neither: its JavaScript is one
|
|
262
262
|
// bundled file under `Contents/Resources/lib`, so `import.meta.url` there
|
|
263
263
|
// resolves node-gi's "package root" to the bundle's directory and every probed
|
|
264
|
-
// path lands beside the bundle instead of in `
|
|
264
|
+
// path lands beside the bundle instead of in `dirs.native`.
|
|
265
265
|
//
|
|
266
266
|
// `NODE_GI_NATIVE` is the same problem for the addon: `prebuildAddonPath()`
|
|
267
267
|
// joins `prebuilds/<target>/node_gi.node` onto that same wrong root
|