@gjsify/cli 0.41.0 → 0.42.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/README.md +79 -33
- package/dist/cli.gjs.mjs +1040 -405
- package/lib/actions/build.d.ts +14 -0
- package/lib/actions/build.js +54 -7
- package/lib/bundler-pick.js +10 -2
- package/lib/cli-app.js +2 -1
- package/lib/commands/browse.js +1 -1
- package/lib/commands/debug.js +1 -1
- package/lib/commands/dev.d.ts +13 -0
- package/lib/commands/dev.js +132 -0
- package/lib/commands/dlx.js +1 -1
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/run.js +38 -85
- package/lib/commands/ship.js +6 -1
- package/lib/commands/showcase.js +1 -1
- package/lib/commands/storybook.js +17 -74
- package/lib/commands/test.js +12 -16
- package/lib/commands/tsc.js +96 -45
- package/lib/config.js +4 -0
- package/lib/types/config-data.d.ts +27 -0
- package/lib/utils/app-metadata.js +27 -6
- package/lib/utils/check-system-deps.js +49 -7
- package/lib/utils/daemon-command.d.ts +7 -0
- package/lib/utils/daemon-command.js +30 -0
- package/lib/utils/dev-plan.d.ts +52 -0
- package/lib/utils/dev-plan.js +117 -0
- package/lib/utils/gi-runtime-paths.d.ts +27 -0
- package/lib/utils/gi-runtime-paths.js +84 -0
- package/lib/utils/gi-typelib.d.ts +49 -0
- package/lib/utils/gi-typelib.js +160 -0
- package/lib/utils/node-script.d.ts +4 -1
- package/lib/utils/node-script.js +27 -3
- package/lib/utils/npm-oidc.js +8 -5
- package/lib/utils/run-gjs.d.ts +12 -1
- package/lib/utils/run-gjs.js +24 -31
- package/lib/utils/run-node.d.ts +9 -4
- package/lib/utils/run-node.js +34 -41
- package/lib/utils/ship/depends.d.ts +5 -0
- package/lib/utils/ship/depends.js +20 -1
- package/lib/utils/ship/discover.js +106 -1
- package/lib/utils/ship/launcher.js +27 -4
- package/lib/utils/ship/payload.d.ts +35 -0
- package/lib/utils/ship/payload.js +84 -0
- package/lib/utils/ship/plan.js +20 -0
- package/lib/utils/ship/settings.d.ts +8 -1
- package/lib/utils/ship/settings.js +13 -1
- package/lib/utils/ship/types.d.ts +19 -0
- package/lib/utils/simple-command.d.ts +16 -0
- package/lib/utils/simple-command.js +77 -0
- package/lib/utils/spawn.d.ts +7 -1
- package/lib/utils/spawn.js +34 -7
- package/lib/utils/system-gi.d.ts +24 -0
- package/lib/utils/system-gi.js +14 -3
- package/lib/utils/watch-loop.d.ts +91 -0
- package/lib/utils/watch-loop.js +207 -0
- package/package.json +19 -18
- package/lib/templates/app/desktop.tmpl +0 -10
package/lib/utils/run-node.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ExampleRuntime } from './runtimes.js';
|
|
2
|
+
import { type SpawnCompletionContract } from './spawn.js';
|
|
2
3
|
/**
|
|
3
4
|
* Walk up from `startDir` for `<node_modules>/@gjsify/node-gi`, returning its package directory
|
|
4
5
|
* or null. A plain fs walk because Node's own resolver is not available under the GJS-bundled CLI.
|
|
@@ -19,6 +20,8 @@ export declare function resolveNodeGiForBundle(bundlePath: string, cwd: string):
|
|
|
19
20
|
/** Pick the node executable: the current one when on Node, else PATH `node`. */
|
|
20
21
|
export declare function nodeBinary(): string;
|
|
21
22
|
export interface RunNodeBundleOptions {
|
|
23
|
+
/** The caller's teardown contract — see `RunGjsBundleOptions.completion`. */
|
|
24
|
+
completion: SpawnCompletionContract;
|
|
22
25
|
/** Exit this process with code 0 once the child succeeds (terminal callers). */
|
|
23
26
|
exitOnSuccess?: boolean;
|
|
24
27
|
/** Suppress the `$ <env> node …` echo — see `RunGjsBundleOptions.quiet`. */
|
|
@@ -28,12 +31,14 @@ export interface RunNodeBundleOptions {
|
|
|
28
31
|
* Run a `--app node` bundle on Node.js with the native typelib env set (same detection as
|
|
29
32
|
* {@link runGjsBundle}). Throws when `@gjsify/node-gi` is not resolvable.
|
|
30
33
|
*/
|
|
31
|
-
export declare function runNodeBundle(bundlePath: string, extraArgs
|
|
34
|
+
export declare function runNodeBundle(bundlePath: string, extraArgs: string[] | undefined, options: RunNodeBundleOptions): Promise<void>;
|
|
32
35
|
/**
|
|
33
36
|
* Whether a `--app node` bundle actually consumes `@gjsify/node-gi` — kept EXTERNAL, so a bundle
|
|
34
37
|
* using gi:// imports the shim by name. A plain Node app does NOT, and must run on node/bun/deno
|
|
35
|
-
* WITHOUT node-gi installed, which is why the check is conditional
|
|
36
|
-
*
|
|
38
|
+
* WITHOUT node-gi installed, which is why the check is conditional rather than a flat "this is a
|
|
39
|
+
* node bundle, demand node-gi". The supervised launcher (`utils/watch-loop.ts`) asks the same
|
|
40
|
+
* question for the same reason: `gjsify dev` on a web-server template builds a node bundle that
|
|
41
|
+
* never reaches a typelib.
|
|
37
42
|
*/
|
|
38
43
|
export declare function bundleRequiresNodeGi(bundlePath: string): boolean;
|
|
39
44
|
/**
|
|
@@ -44,4 +49,4 @@ export declare function bundleRequiresNodeGi(bundlePath: string): boolean;
|
|
|
44
49
|
*
|
|
45
50
|
* `gjs` is intentionally NOT handled here — the caller dispatches it to {@link runGjsBundle}.
|
|
46
51
|
*/
|
|
47
|
-
export declare function runRuntimeBundle(runtime: Exclude<ExampleRuntime, 'gjs'>, bundlePath: string, extraArgs
|
|
52
|
+
export declare function runRuntimeBundle(runtime: Exclude<ExampleRuntime, 'gjs'>, bundlePath: string, extraArgs: string[] | undefined, options: RunNodeBundleOptions): Promise<void>;
|
package/lib/utils/run-node.js
CHANGED
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
//
|
|
16
16
|
// The node binary is `process.execPath` when the CLI runs under Node, else PATH `node` — under
|
|
17
17
|
// the GJS bundle `process.execPath` is `gjs`, which cannot run a `--app node` bundle.
|
|
18
|
-
import { spawn } from 'node:child_process';
|
|
19
18
|
import { existsSync, readFileSync } from 'node:fs';
|
|
20
19
|
import { dirname, join, resolve } from 'node:path';
|
|
21
20
|
import { isNode } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
22
21
|
import { computeNativeEnvForBundle } from './run-gjs.js';
|
|
23
22
|
import { RUNTIMES } from './runtimes.js';
|
|
23
|
+
import { describeExit, spawnToCompletion } from './spawn.js';
|
|
24
24
|
/**
|
|
25
25
|
* Walk up from `startDir` for `<node_modules>/@gjsify/node-gi`, returning its package directory
|
|
26
26
|
* or null. A plain fs walk because Node's own resolver is not available under the GJS-bundled CLI.
|
|
@@ -60,7 +60,7 @@ export function nodeBinary() {
|
|
|
60
60
|
* Run a `--app node` bundle on Node.js with the native typelib env set (same detection as
|
|
61
61
|
* {@link runGjsBundle}). Throws when `@gjsify/node-gi` is not resolvable.
|
|
62
62
|
*/
|
|
63
|
-
export async function runNodeBundle(bundlePath, extraArgs = [], options
|
|
63
|
+
export async function runNodeBundle(bundlePath, extraArgs = [], options) {
|
|
64
64
|
const cwd = process.cwd();
|
|
65
65
|
if (!resolveNodeGiForBundle(bundlePath, cwd)) {
|
|
66
66
|
throw new Error('Cannot run the storybook on Node: `@gjsify/node-gi` is not installed.\n' +
|
|
@@ -74,27 +74,28 @@ export async function runNodeBundle(bundlePath, extraArgs = [], options = {}) {
|
|
|
74
74
|
// Echo on stderr, so a child speaking a protocol on stdout stays uncontaminated.
|
|
75
75
|
const cmd = [nodeBin, ...nodeArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
|
|
76
76
|
console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${cmd}`);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
else
|
|
83
|
-
resolvePromise();
|
|
84
|
-
});
|
|
85
|
-
child.on('error', reject);
|
|
86
|
-
}).catch((err) => {
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = await spawnToCompletion(nodeBin, nodeArgs, { completion: options.completion, env });
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
87
82
|
console.error(err.message);
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
83
|
+
return process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (result.code !== 0) {
|
|
86
|
+
console.error(`node exited with ${describeExit(result)}`);
|
|
87
|
+
return process.exit(result.code ?? 1);
|
|
88
|
+
}
|
|
90
89
|
if (options.exitOnSuccess)
|
|
91
|
-
process.exit(0);
|
|
90
|
+
return process.exit(0);
|
|
92
91
|
}
|
|
93
92
|
/**
|
|
94
93
|
* Whether a `--app node` bundle actually consumes `@gjsify/node-gi` — kept EXTERNAL, so a bundle
|
|
95
94
|
* using gi:// imports the shim by name. A plain Node app does NOT, and must run on node/bun/deno
|
|
96
|
-
* WITHOUT node-gi installed, which is why the check is conditional
|
|
97
|
-
*
|
|
95
|
+
* WITHOUT node-gi installed, which is why the check is conditional rather than a flat "this is a
|
|
96
|
+
* node bundle, demand node-gi". The supervised launcher (`utils/watch-loop.ts`) asks the same
|
|
97
|
+
* question for the same reason: `gjsify dev` on a web-server template builds a node bundle that
|
|
98
|
+
* never reaches a typelib.
|
|
98
99
|
*/
|
|
99
100
|
export function bundleRequiresNodeGi(bundlePath) {
|
|
100
101
|
try {
|
|
@@ -112,7 +113,7 @@ export function bundleRequiresNodeGi(bundlePath) {
|
|
|
112
113
|
*
|
|
113
114
|
* `gjs` is intentionally NOT handled here — the caller dispatches it to {@link runGjsBundle}.
|
|
114
115
|
*/
|
|
115
|
-
export async function runRuntimeBundle(runtime, bundlePath, extraArgs = [], options
|
|
116
|
+
export async function runRuntimeBundle(runtime, bundlePath, extraArgs = [], options) {
|
|
116
117
|
const cwd = process.cwd();
|
|
117
118
|
const resolvedBundle = resolve(bundlePath);
|
|
118
119
|
if (bundleRequiresNodeGi(resolvedBundle) && !resolveNodeGiForBundle(resolvedBundle, cwd)) {
|
|
@@ -131,32 +132,24 @@ export async function runRuntimeBundle(runtime, bundlePath, extraArgs = [], opti
|
|
|
131
132
|
const shown = [cmd, ...launchArgs.map((a) => (a.includes(' ') ? `"${a}"` : a))].join(' ');
|
|
132
133
|
if (!options.quiet)
|
|
133
134
|
console.error(`$ ${envPrefix ? `${envPrefix} ` : ''}${shown}`);
|
|
134
|
-
const child = spawn(cmd, launchArgs, { env, stdio: 'inherit' });
|
|
135
135
|
// Re-raise the CHILD's own code rather than a blanket 1 — a script run through
|
|
136
136
|
// `--node-script` reports its status through this path, and a caller reading `$?` needs
|
|
137
|
-
// the real one.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
resolvePromise();
|
|
148
|
-
});
|
|
149
|
-
child.on('error', reject);
|
|
150
|
-
}).catch((err) => {
|
|
137
|
+
// the real one. A spawn failure has no code of its own and falls back to 1.
|
|
138
|
+
const failed = (code) => {
|
|
139
|
+
process.exitCode = code;
|
|
140
|
+
return process.exit(code);
|
|
141
|
+
};
|
|
142
|
+
let result;
|
|
143
|
+
try {
|
|
144
|
+
result = await spawnToCompletion(cmd, launchArgs, { completion: options.completion, env });
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
151
147
|
console.error(err.message);
|
|
152
|
-
failed
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (failed) {
|
|
158
|
-
process.exitCode = childCode;
|
|
159
|
-
return process.exit(childCode);
|
|
148
|
+
return failed(1);
|
|
149
|
+
}
|
|
150
|
+
if (result.code !== 0) {
|
|
151
|
+
console.error(`${runtime} exited with ${describeExit(result)}`);
|
|
152
|
+
return failed(result.code ?? 1);
|
|
160
153
|
}
|
|
161
154
|
if (options.exitOnSuccess)
|
|
162
155
|
return process.exit(0);
|
|
@@ -33,6 +33,11 @@ export interface DependsInputs {
|
|
|
33
33
|
deb: string;
|
|
34
34
|
rpm: string;
|
|
35
35
|
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Paths of typelib files the payload carries itself. The namespaces they cover need no distro
|
|
38
|
+
* dependency — and are derived from these filenames, never declared separately.
|
|
39
|
+
*/
|
|
40
|
+
bundledTypelibs?: readonly string[];
|
|
36
41
|
/** Minimum GJS. Default {@link DEFAULT_GJS_FLOOR}. */
|
|
37
42
|
minGjsVersion?: string;
|
|
38
43
|
}
|
|
@@ -105,6 +105,12 @@ const TYPELIB_PACKAGES = {
|
|
|
105
105
|
'Notify-0.7': { deb: 'gir1.2-notify-0.7', rpm: 'libnotify' },
|
|
106
106
|
'Vte-3.91': { deb: 'gir1.2-vte-3.91', rpm: 'vte291-gtk4' },
|
|
107
107
|
'Manette-0.2': { deb: 'gir1.2-manette-0.2', rpm: 'libmanette' },
|
|
108
|
+
// libgda — what `@gjsify/sqlite` binds, so every project with a local database lands here.
|
|
109
|
+
// The rpm name is measured on this machine (`rpm -qf …/Gda-6.0.typelib` → libgda); the deb name
|
|
110
|
+
// follows the `gir1.2-<name>-<version>` convention every row above uses and is NOT verified
|
|
111
|
+
// against a Debian system. Said plainly because a wrong name fails at `apt install` with a
|
|
112
|
+
// clear error, while no row at all fails the BUILD of every such project.
|
|
113
|
+
'Gda-6.0': { deb: 'gir1.2-gda-6.0', rpm: 'libgda' },
|
|
108
114
|
};
|
|
109
115
|
/**
|
|
110
116
|
* A typelib the payload CARRIES, rather than one the system provides.
|
|
@@ -131,8 +137,21 @@ const BUNDLED_TYPELIB = /^Gjsify[A-Z]/;
|
|
|
131
137
|
export function deriveDepends(format, inputs) {
|
|
132
138
|
const out = [`gjs >= ${inputs.minGjsVersion ?? DEFAULT_GJS_FLOOR}`];
|
|
133
139
|
const unmapped = [];
|
|
140
|
+
// Namespaces the package SHIPS ITSELF, read off the staged filenames rather than from a
|
|
141
|
+
// separate declaration: `Gwebgl-0.1.typelib` → `Gwebgl-0.1` and `Gwebgl`. Deriving it is the
|
|
142
|
+
// point — a project that declared a namespace bundled without the file being there would get
|
|
143
|
+
// a package that installs and dies at the first import, which is exactly what this check
|
|
144
|
+
// exists to prevent.
|
|
145
|
+
const shipped = new Set();
|
|
146
|
+
for (const file of inputs.bundledTypelibs ?? []) {
|
|
147
|
+
const match = /([A-Za-z0-9]+)-([\d.]+)\.typelib$/.exec(file);
|
|
148
|
+
if (!match)
|
|
149
|
+
continue;
|
|
150
|
+
shipped.add(`${match[1]}-${match[2]}`);
|
|
151
|
+
shipped.add(match[1]);
|
|
152
|
+
}
|
|
134
153
|
for (const namespace of [...new Set(inputs.namespaces)].sort()) {
|
|
135
|
-
if (BUNDLED_TYPELIB.test(namespace))
|
|
154
|
+
if (BUNDLED_TYPELIB.test(namespace) || shipped.has(namespace))
|
|
136
155
|
continue;
|
|
137
156
|
const entry = lookupTypelib(namespace, inputs.typelibPackages);
|
|
138
157
|
if (entry === undefined) {
|
|
@@ -12,15 +12,120 @@ export function discoverPayload(input) {
|
|
|
12
12
|
const { projectDir, ship } = input;
|
|
13
13
|
const bundlePath = resolveBundle(input);
|
|
14
14
|
const bundleDir = resolve(bundlePath, '..');
|
|
15
|
+
const localeFiles = discoverLocales(projectDir, ship.localeDir);
|
|
15
16
|
return {
|
|
16
17
|
bundlePath,
|
|
17
18
|
bundleDir,
|
|
18
|
-
|
|
19
|
+
// Everything beside the bundle, MINUS the locale tree when it lives there.
|
|
20
|
+
//
|
|
21
|
+
// `dist/locale/` sits next to `dist/app.gjs.mjs` in the layout
|
|
22
|
+
// `@gjsify/vite-plugin-gettext` writes by default, and the wholesale bundle staging would
|
|
23
|
+
// then put every catalogue in `lib/<binary>/locale/` as well as in `share/locale/`.
|
|
24
|
+
// Measured on a probe package: the same `.mo` appeared at both paths. The `lib/` copy is
|
|
25
|
+
// dead weight — nothing looks there — and it is the same failure that once shipped the test
|
|
26
|
+
// suite: whatever is beside the bundle gets carried whether or not it belongs in a package.
|
|
27
|
+
bundleFiles: withoutLocaleTree(listFilesRecursive(bundleDir), bundleDir, projectDir, ship.localeDir),
|
|
19
28
|
iconFiles: discoverIcons(projectDir, ship.icon ?? input.flatpakIcon),
|
|
20
29
|
schemaFiles: discoverSchemas(projectDir, ship.schemas),
|
|
30
|
+
typelibFiles: discoverTypelibs(projectDir, ship.bundledTypelibs),
|
|
31
|
+
localeFiles,
|
|
21
32
|
licenseFile: discoverLicense(projectDir, ship.licenseFile),
|
|
22
33
|
};
|
|
23
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Typelibs (and their shared libraries) the PROJECT ships itself.
|
|
37
|
+
*
|
|
38
|
+
* Necessary because gjsify's own GI libraries — `Gwebgl` for the WebGL bridge, the GTK runtime
|
|
39
|
+
* bundles, the napi host — arrive as npm prebuilds, not as distro packages. An app using one has
|
|
40
|
+
* no `gir1.2-…` to depend on: it must carry the files, and something must point GI at them.
|
|
41
|
+
*
|
|
42
|
+
* A directory rather than a list of files, because a typelib is never alone: `Gwebgl-0.1.typelib`
|
|
43
|
+
* is useless without `libgwebgl.so`, and staging one without the other produces a package that
|
|
44
|
+
* installs and then fails at the first import.
|
|
45
|
+
*/
|
|
46
|
+
function discoverTypelibs(projectDir, dirs) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const dir of dirs ?? []) {
|
|
49
|
+
const root = resolve(projectDir, dir);
|
|
50
|
+
if (!existsSync(root)) {
|
|
51
|
+
throw new Error(`gjsify ship: \`gjsify.ship.bundledTypelibs\` names ${dir}, which does not exist. ` +
|
|
52
|
+
'A missing directory here means the package would install without the typelib it promises.');
|
|
53
|
+
}
|
|
54
|
+
for (const file of listFilesRecursive(root)) {
|
|
55
|
+
if (/\.typelib$|\.so(\.\d+)*$/.test(file))
|
|
56
|
+
out.push(join(root, file));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Drop the declared locale tree from the wholesale bundle staging.
|
|
63
|
+
*
|
|
64
|
+
* Only when it actually lies inside the bundle directory — a `localeDir` elsewhere in the project
|
|
65
|
+
* was never part of `bundleFiles` and there is nothing to subtract.
|
|
66
|
+
*/
|
|
67
|
+
function withoutLocaleTree(bundleFiles, bundleDir, projectDir, localeDir) {
|
|
68
|
+
if (localeDir === undefined)
|
|
69
|
+
return bundleFiles;
|
|
70
|
+
const localeRoot = resolve(projectDir, localeDir);
|
|
71
|
+
const rel = relative(bundleDir, localeRoot);
|
|
72
|
+
// Outside the bundle dir: `relative` climbs out (`..`) or returns an absolute path.
|
|
73
|
+
if (rel === '' || rel.startsWith('..') || isAbsolute(rel))
|
|
74
|
+
return bundleFiles;
|
|
75
|
+
// Only `rel` needs normalising: it comes from `relative`, so it is host-shaped, while
|
|
76
|
+
// `bundleFiles` is POSIX by `listFilesRecursive`'s contract. Comparing a host-shaped prefix
|
|
77
|
+
// against POSIX entries matches nothing on Windows — and matching nothing here means the
|
|
78
|
+
// catalogues quietly ship twice again, on that platform only.
|
|
79
|
+
const prefix = `${rel.split(sep).join(posix.sep)}${posix.sep}`;
|
|
80
|
+
return bundleFiles.filter((file) => !`${file}${posix.sep}`.startsWith(prefix));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Compiled gettext catalogues the project ships (`gjsify.ship.localeDir`).
|
|
84
|
+
*
|
|
85
|
+
* Only `.mo` files, and only in the layout `bindtextdomain` actually reads —
|
|
86
|
+
* `<lang>/LC_MESSAGES/<domain>.mo`. Both refusals cover the same failure, which is the quiet kind:
|
|
87
|
+
* a catalogue in the wrong place, or left as `.po`, installs perfectly and the app then shows the
|
|
88
|
+
* untranslated msgid forever. That is indistinguishable from "no translation exists", so it is not
|
|
89
|
+
* something a user or a maintainer would ever report as a packaging bug.
|
|
90
|
+
*/
|
|
91
|
+
function discoverLocales(projectDir, dir) {
|
|
92
|
+
if (dir === undefined)
|
|
93
|
+
return [];
|
|
94
|
+
const root = resolve(projectDir, dir);
|
|
95
|
+
if (!existsSync(root)) {
|
|
96
|
+
throw new Error(`gjsify ship: \`gjsify.ship.localeDir\` names ${dir}, which does not exist. ` +
|
|
97
|
+
'Compile the catalogues first (msgfmt, or @gjsify/vite-plugin-gettext) — a missing ' +
|
|
98
|
+
'directory here means the package would install without the translations it promises.');
|
|
99
|
+
}
|
|
100
|
+
const out = [];
|
|
101
|
+
const strays = [];
|
|
102
|
+
for (const rel of listFilesRecursive(root)) {
|
|
103
|
+
if (rel.endsWith('.po') || rel.endsWith('.pot')) {
|
|
104
|
+
strays.push(`${rel} (a .po source; bindtextdomain reads .mo only)`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!rel.endsWith('.mo'))
|
|
108
|
+
continue;
|
|
109
|
+
// `listFilesRecursive` normalises to POSIX, so one separator is all there is to split on.
|
|
110
|
+
const parts = rel.split(posix.sep);
|
|
111
|
+
if (parts.length !== 3 || parts[1] !== 'LC_MESSAGES') {
|
|
112
|
+
strays.push(`${rel} (expected <lang>/LC_MESSAGES/<domain>.mo)`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
out.push({ rel: parts.join('/'), abs: join(root, rel) });
|
|
116
|
+
}
|
|
117
|
+
if (strays.length > 0) {
|
|
118
|
+
throw new Error(`gjsify ship: \`gjsify.ship.localeDir\` (${dir}) holds files no catalogue lookup will find:\n` +
|
|
119
|
+
strays.map((s) => ` ${s}`).join('\n') +
|
|
120
|
+
'\nA misplaced catalogue installs and then shows nothing, which reads exactly like having ' +
|
|
121
|
+
'no translation at all. Lay them out as <lang>/LC_MESSAGES/<domain>.mo.');
|
|
122
|
+
}
|
|
123
|
+
if (out.length === 0) {
|
|
124
|
+
throw new Error(`gjsify ship: \`gjsify.ship.localeDir\` (${dir}) contains no \`.mo\` catalogue. ` +
|
|
125
|
+
'Declaring a locale directory that ships nothing is a promise the package does not keep.');
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
24
129
|
function resolveBundle(input) {
|
|
25
130
|
const declared = input.declaredBundle;
|
|
26
131
|
if (declared === undefined) {
|
|
@@ -23,7 +23,7 @@ function shellQuote(value) {
|
|
|
23
23
|
export function renderLauncher(settings, bundleRelPath) {
|
|
24
24
|
const lib = `"$prefix"/lib/${settings.binaryName}`;
|
|
25
25
|
const args = settings.execArgs.map(shellQuote).join(' ');
|
|
26
|
-
|
|
26
|
+
const lines = [
|
|
27
27
|
'#!/bin/sh',
|
|
28
28
|
'# Generated by `gjsify ship` — do not edit.',
|
|
29
29
|
'set -e',
|
|
@@ -31,7 +31,30 @@ export function renderLauncher(settings, bundleRelPath) {
|
|
|
31
31
|
'prefix=$(dirname "$(dirname "$self")")',
|
|
32
32
|
'XDG_DATA_DIRS="$prefix/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"',
|
|
33
33
|
'export XDG_DATA_DIRS',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
];
|
|
35
|
+
// Typelibs the package carries itself (see `gjsify.ship.bundledTypelibs`). PREPENDED, not
|
|
36
|
+
// replacing: an app that ships `Gwebgl` still needs the system `Gtk`, and clobbering the path
|
|
37
|
+
// would trade one missing namespace for all the others. Only emitted when something was
|
|
38
|
+
// actually staged — an empty `GI_TYPELIB_PATH` entry makes GI search a directory that is not
|
|
39
|
+
// there, on every import, forever.
|
|
40
|
+
if (settings.typelibFiles.length > 0) {
|
|
41
|
+
const giDir = '"$prefix"/lib/' + settings.binaryName + '/gi';
|
|
42
|
+
for (const variable of ['GI_TYPELIB_PATH', 'LD_LIBRARY_PATH']) {
|
|
43
|
+
// `${VAR:+:$VAR}` appends the existing value with a separator ONLY when it is set —
|
|
44
|
+
// a plain `:$VAR` leaves a trailing colon, which both loaders read as "and the current
|
|
45
|
+
// directory", and a package that silently adds `.` to a library path is a bad package.
|
|
46
|
+
lines.push(`${variable}=${giDir}\${${variable}:+:$${variable}}`, `export ${variable}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Where the app's gettext catalogues landed. `bindtextdomain` takes a DIRECTORY and there is no
|
|
50
|
+
// standard environment variable it reads on its own (`TEXTDOMAINDIR` is honoured by the gettext
|
|
51
|
+
// command-line tools, not by the library), so the prefix has to reach the app somehow. Passing
|
|
52
|
+
// it from the launcher keeps the app free of install-layout knowledge, exactly as XDG_DATA_DIRS
|
|
53
|
+
// above does for icons and schemas — a baked `/usr/share/locale` would be wrong in a Flatpak
|
|
54
|
+
// and in any `--prefix` tree.
|
|
55
|
+
if (settings.localeFiles.length > 0) {
|
|
56
|
+
lines.push('GJSIFY_LOCALE_DIR="$prefix"/share/locale', 'export GJSIFY_LOCALE_DIR');
|
|
57
|
+
}
|
|
58
|
+
lines.push(`exec gjs -m ${lib}/${bundleRelPath}${args ? ` ${args}` : ''} "$@"`, '');
|
|
59
|
+
return lines.join('\n');
|
|
37
60
|
}
|
|
@@ -28,3 +28,38 @@ export declare function buildTimestamp(bundlePath: string, env?: Record<string,
|
|
|
28
28
|
* does not run.
|
|
29
29
|
*/
|
|
30
30
|
export declare function isArchIndependent(payload: readonly PayloadEntry[]): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The `process.arch` token an image says it is built for, or `null` when the
|
|
33
|
+
* question cannot be answered from the bytes.
|
|
34
|
+
*
|
|
35
|
+
* `null` covers three different things on purpose, and all three must stay
|
|
36
|
+
* silent: a file that is not a native binary at all (most of a payload), a PE
|
|
37
|
+
* (whose COFF machine field this tree has never parsed — `isNativeBinary` reads
|
|
38
|
+
* `MZ` and stops), and a Mach-O fat archive, which carries several
|
|
39
|
+
* architectures and therefore matches any label a caller could pass.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readBinaryArch(data: Uint8Array): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Refuse a payload whose binaries disagree with the label the package will carry.
|
|
44
|
+
*
|
|
45
|
+
* THE INCIDENT, measured on 0.41.0 before this existed. A project whose payload
|
|
46
|
+
* carries one x86-64 `.so`, packed on this x86-64 host:
|
|
47
|
+
*
|
|
48
|
+
* gjsify ship --skip-build --arch arm64
|
|
49
|
+
* → xarch-demo_1.2.3-1_arm64.deb, xarch-demo-1.2.3-1.aarch64.rpm
|
|
50
|
+
* rpm -qp --qf '%{ARCH}' → aarch64
|
|
51
|
+
* the .so inside it → ELF e_machine 0x3e (x86-64)
|
|
52
|
+
*
|
|
53
|
+
* `--arch` LABELS the payload; it does not cross-compile it, and nothing
|
|
54
|
+
* compared the two. The result installs on an arm64 machine — apt and dnf both
|
|
55
|
+
* believe the header — and then fails to load, which is this tree's most
|
|
56
|
+
* expensive failure class with an independent oracle actively confirming the
|
|
57
|
+
* lie: `rpm` reads the header, and the header was written from the caller's
|
|
58
|
+
* claim.
|
|
59
|
+
*
|
|
60
|
+
* Payload against LABEL, never payload against HOST. Assembling an arm64
|
|
61
|
+
* artifact on an x64 machine is a supported path — the packers are pure
|
|
62
|
+
* JavaScript and ADR 0024 § A1 turns it into a design commitment — so a host
|
|
63
|
+
* comparison would refuse the very case the split exists to allow.
|
|
64
|
+
*/
|
|
65
|
+
export declare function assertPayloadMatchesArch(payload: readonly PayloadEntry[], arch: string): void;
|
|
@@ -40,6 +40,90 @@ export function buildTimestamp(bundlePath, env = process.env) {
|
|
|
40
40
|
export function isArchIndependent(payload) {
|
|
41
41
|
return !payload.some((entry) => isNativeBinary(entry.data));
|
|
42
42
|
}
|
|
43
|
+
// `process.arch` tokens, keyed by what the image records about itself. Only the
|
|
44
|
+
// values this repository can actually produce are listed; an unknown one reads
|
|
45
|
+
// as "cannot tell" rather than as a mismatch, because refusing an artifact over
|
|
46
|
+
// a machine constant nobody here emits would be a guess wearing a gate's clothes.
|
|
47
|
+
const ELF_MACHINE_TO_ARCH = {
|
|
48
|
+
0x03: 'ia32',
|
|
49
|
+
0x15: 'ppc64',
|
|
50
|
+
0x16: 's390x',
|
|
51
|
+
0x28: 'arm',
|
|
52
|
+
0x3e: 'x64',
|
|
53
|
+
0xb7: 'arm64',
|
|
54
|
+
0xf3: 'riscv64',
|
|
55
|
+
};
|
|
56
|
+
const MACHO_CPUTYPE_TO_ARCH = {
|
|
57
|
+
0x00000007: 'ia32',
|
|
58
|
+
0x0000000c: 'arm',
|
|
59
|
+
0x01000007: 'x64',
|
|
60
|
+
0x0100000c: 'arm64',
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The `process.arch` token an image says it is built for, or `null` when the
|
|
64
|
+
* question cannot be answered from the bytes.
|
|
65
|
+
*
|
|
66
|
+
* `null` covers three different things on purpose, and all three must stay
|
|
67
|
+
* silent: a file that is not a native binary at all (most of a payload), a PE
|
|
68
|
+
* (whose COFF machine field this tree has never parsed — `isNativeBinary` reads
|
|
69
|
+
* `MZ` and stops), and a Mach-O fat archive, which carries several
|
|
70
|
+
* architectures and therefore matches any label a caller could pass.
|
|
71
|
+
*/
|
|
72
|
+
export function readBinaryArch(data) {
|
|
73
|
+
if (data.byteLength < 20)
|
|
74
|
+
return null;
|
|
75
|
+
const magic = ((data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]) >>> 0;
|
|
76
|
+
if (magic === 0x7f454c46) {
|
|
77
|
+
// ELF: EI_DATA at offset 5 says which end e_machine (offset 18) is written from.
|
|
78
|
+
const littleEndian = data[5] === 1;
|
|
79
|
+
const machine = littleEndian ? data[18] | (data[19] << 8) : (data[18] << 8) | data[19];
|
|
80
|
+
return ELF_MACHINE_TO_ARCH[machine] ?? null;
|
|
81
|
+
}
|
|
82
|
+
if (magic === 0xfeedface || magic === 0xfeedfacf) {
|
|
83
|
+
return MACHO_CPUTYPE_TO_ARCH[((data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7]) >>> 0] ?? null;
|
|
84
|
+
}
|
|
85
|
+
if (magic === 0xcefaedfe || magic === 0xcffaedfe) {
|
|
86
|
+
return MACHO_CPUTYPE_TO_ARCH[((data[7] << 24) | (data[6] << 16) | (data[5] << 8) | data[4]) >>> 0] ?? null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Refuse a payload whose binaries disagree with the label the package will carry.
|
|
92
|
+
*
|
|
93
|
+
* THE INCIDENT, measured on 0.41.0 before this existed. A project whose payload
|
|
94
|
+
* carries one x86-64 `.so`, packed on this x86-64 host:
|
|
95
|
+
*
|
|
96
|
+
* gjsify ship --skip-build --arch arm64
|
|
97
|
+
* → xarch-demo_1.2.3-1_arm64.deb, xarch-demo-1.2.3-1.aarch64.rpm
|
|
98
|
+
* rpm -qp --qf '%{ARCH}' → aarch64
|
|
99
|
+
* the .so inside it → ELF e_machine 0x3e (x86-64)
|
|
100
|
+
*
|
|
101
|
+
* `--arch` LABELS the payload; it does not cross-compile it, and nothing
|
|
102
|
+
* compared the two. The result installs on an arm64 machine — apt and dnf both
|
|
103
|
+
* believe the header — and then fails to load, which is this tree's most
|
|
104
|
+
* expensive failure class with an independent oracle actively confirming the
|
|
105
|
+
* lie: `rpm` reads the header, and the header was written from the caller's
|
|
106
|
+
* claim.
|
|
107
|
+
*
|
|
108
|
+
* Payload against LABEL, never payload against HOST. Assembling an arm64
|
|
109
|
+
* artifact on an x64 machine is a supported path — the packers are pure
|
|
110
|
+
* JavaScript and ADR 0024 § A1 turns it into a design commitment — so a host
|
|
111
|
+
* comparison would refuse the very case the split exists to allow.
|
|
112
|
+
*/
|
|
113
|
+
export function assertPayloadMatchesArch(payload, arch) {
|
|
114
|
+
for (const entry of payload) {
|
|
115
|
+
const found = readBinaryArch(entry.data);
|
|
116
|
+
if (found === null || found === arch)
|
|
117
|
+
continue;
|
|
118
|
+
throw new Error(`gjsify ship: the payload is ${found}, but the package would be labelled ${arch} — ` +
|
|
119
|
+
`${entry.path} is built for ${found}.\n` +
|
|
120
|
+
' `--arch` names the architecture the PAYLOAD was built for; it does not cross-compile ' +
|
|
121
|
+
'anything.\n' +
|
|
122
|
+
` A package labelled ${arch} installs on ${arch} and then fails to load. Build the payload ` +
|
|
123
|
+
`for ${arch}\n` +
|
|
124
|
+
' (its own prebuild), or drop `--arch` and label the payload you actually have.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
43
127
|
/** ELF, Mach-O (both endiannesses, both widths, and a fat archive) or PE. */
|
|
44
128
|
function isNativeBinary(data) {
|
|
45
129
|
if (data.byteLength < 4)
|
package/lib/utils/ship/plan.js
CHANGED
|
@@ -54,6 +54,26 @@ export function planStage(settings, inputs) {
|
|
|
54
54
|
source: { kind: 'file', path: schema },
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
// The typelibs the package carries itself, beside the bundle in `gi/`. The launcher points
|
|
58
|
+
// GI_TYPELIB_PATH and LD_LIBRARY_PATH here — see renderLauncher.
|
|
59
|
+
for (const file of settings.typelibFiles) {
|
|
60
|
+
files.push({
|
|
61
|
+
path: posix.join(libDir, 'gi', basename(file)),
|
|
62
|
+
// A shared library must be executable; a typelib need not be, but one mode for both
|
|
63
|
+
// keeps the staged tree free of a distinction nothing downstream reads.
|
|
64
|
+
mode: 0o755,
|
|
65
|
+
source: { kind: 'file', path: file },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// Compiled gettext catalogues, layout preserved: `share/locale/<lang>/LC_MESSAGES/<domain>.mo`
|
|
69
|
+
// is where `bindtextdomain` looks, and nowhere else.
|
|
70
|
+
for (const locale of settings.localeFiles) {
|
|
71
|
+
files.push({
|
|
72
|
+
path: posix.join('share/locale', locale.rel),
|
|
73
|
+
mode: 0o644,
|
|
74
|
+
source: { kind: 'file', path: locale.abs },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
57
77
|
for (const [dest, source] of Object.entries(settings.extraFiles)) {
|
|
58
78
|
files.push({ path: assertInsidePrefix(dest), mode: 0o644, source: { kind: 'file', path: source } });
|
|
59
79
|
}
|
|
@@ -27,6 +27,13 @@ export interface DiscoveredPayload {
|
|
|
27
27
|
bundleFiles: string[];
|
|
28
28
|
iconFiles: string[];
|
|
29
29
|
schemaFiles: string[];
|
|
30
|
+
/** Absolute paths of the typelibs + shared libraries the project ships itself. */
|
|
31
|
+
typelibFiles: string[];
|
|
32
|
+
/** Compiled gettext catalogues, each with its `<lang>/LC_MESSAGES/<domain>.mo` path. */
|
|
33
|
+
localeFiles: {
|
|
34
|
+
rel: string;
|
|
35
|
+
abs: string;
|
|
36
|
+
}[];
|
|
30
37
|
licenseFile?: string;
|
|
31
38
|
}
|
|
32
39
|
export interface SettingsInput {
|
|
@@ -53,7 +60,7 @@ export declare function declaredBundlePath(pkg: ShipPackageManifest, ship: Confi
|
|
|
53
60
|
/** npm package name → a package/binary name both dpkg and rpm accept. */
|
|
54
61
|
export declare function deriveBinaryName(pkgName: string | undefined): string;
|
|
55
62
|
/** AppStream description (string or blocks) → plain paragraphs for deb/rpm. */
|
|
56
|
-
export declare function descriptionParagraphs(description: string | DescriptionBlock
|
|
63
|
+
export declare function descriptionParagraphs(description: string | Array<DescriptionBlock | string> | undefined): string[] | undefined;
|
|
57
64
|
/** `LICENSE`, `LICENSE.md`, … — the names a licence file actually has. */
|
|
58
65
|
export declare const LICENSE_FILE_NAMES: readonly ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "COPYING.md"];
|
|
59
66
|
/** The bundle's own basename, used as the launcher's exec target. */
|
|
@@ -81,6 +81,8 @@ export function resolveShipSettings(input) {
|
|
|
81
81
|
bundleDir: discovered.bundleDir,
|
|
82
82
|
iconFiles: discovered.iconFiles,
|
|
83
83
|
schemaFiles: discovered.schemaFiles,
|
|
84
|
+
typelibFiles: discovered.typelibFiles,
|
|
85
|
+
localeFiles: discovered.localeFiles,
|
|
84
86
|
extraFiles: ship.extraFiles ?? {},
|
|
85
87
|
execArgs: ship.execArgs ?? [],
|
|
86
88
|
outDir: input.cli.outDir ?? ship.outDir ?? 'ship',
|
|
@@ -165,7 +167,17 @@ export function descriptionParagraphs(description) {
|
|
|
165
167
|
}
|
|
166
168
|
const out = [];
|
|
167
169
|
for (const block of description) {
|
|
168
|
-
|
|
170
|
+
// A plain string in the array is what a person writes when they mean one paragraph, and it
|
|
171
|
+
// used to reach `'p' in block` and throw a bare TypeError — "cannot use 'in' operator to
|
|
172
|
+
// search for \"p\" in \"Bauplaner rechnet…\"", which names neither the field nor the fix.
|
|
173
|
+
// Accepting it is unambiguous: the AppStream blocks are objects, so a string can only mean
|
|
174
|
+
// a paragraph.
|
|
175
|
+
if (typeof block === 'string') {
|
|
176
|
+
const text = block.trim().replace(/\s+/g, ' ');
|
|
177
|
+
if (text)
|
|
178
|
+
out.push(text);
|
|
179
|
+
}
|
|
180
|
+
else if ('p' in block) {
|
|
169
181
|
out.push(block.p.trim().replace(/\s+/g, ' '));
|
|
170
182
|
}
|
|
171
183
|
else {
|
|
@@ -101,6 +101,25 @@ export interface ShipSettings {
|
|
|
101
101
|
iconFiles: string[];
|
|
102
102
|
/** Absolute paths of `*.gschema.xml` files to install. */
|
|
103
103
|
schemaFiles: string[];
|
|
104
|
+
/**
|
|
105
|
+
* Typelibs + shared libraries the package carries itself (`gjsify.ship.bundledTypelibs`).
|
|
106
|
+
*
|
|
107
|
+
* gjsify's own GI libraries arrive as npm prebuilds, not distro packages, so an app using one
|
|
108
|
+
* has nothing to depend on and must ship the files — with the launcher pointing GI at them.
|
|
109
|
+
*/
|
|
110
|
+
typelibFiles: string[];
|
|
111
|
+
/**
|
|
112
|
+
* Compiled gettext catalogues (`gjsify.ship.localeDir`), each keeping its
|
|
113
|
+
* `<lang>/LC_MESSAGES/<domain>.mo` path so `share/locale/` comes out in the layout
|
|
114
|
+
* `bindtextdomain` reads.
|
|
115
|
+
*
|
|
116
|
+
* `rel` rather than a bare list of absolute paths, because the LAYOUT is the whole point here:
|
|
117
|
+
* a `.mo` staged by basename alone lands where no catalogue lookup will ever look at it.
|
|
118
|
+
*/
|
|
119
|
+
localeFiles: {
|
|
120
|
+
rel: string;
|
|
121
|
+
abs: string;
|
|
122
|
+
}[];
|
|
104
123
|
/** `<dest relative to prefix>` → absolute source path. */
|
|
105
124
|
extraFiles: Record<string, string>;
|
|
106
125
|
/** Arguments appended to the launcher's `exec` line. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenize `cmd`, or return `null` if the string contains anything the shell
|
|
3
|
+
* would treat specially — operators (`&& | ; < > &`), substitutions (`$(...)` /
|
|
4
|
+
* backticks / `$VAR`), unquoted globs/expansions (`* ? { } [ ] ~`), comments
|
|
5
|
+
* (`#`), or an unterminated quote — in which case the caller MUST use the real
|
|
6
|
+
* shell instead. Quotes are honoured so a quoted glob (`'src/**'`) survives as a
|
|
7
|
+
* single literal token, exactly as the shell would hand it to `gjsify` (which
|
|
8
|
+
* does its own glob expansion).
|
|
9
|
+
*/
|
|
10
|
+
export declare function tokenizeSimpleCommand(cmd: string): string[] | null;
|
|
11
|
+
/**
|
|
12
|
+
* The argv of a script that is a single `gjsify <subcommand> …` command — i.e.
|
|
13
|
+
* the tokens after `gjsify` — or `null` when it is anything else. `null` is the
|
|
14
|
+
* signal to hand the literal to a shell.
|
|
15
|
+
*/
|
|
16
|
+
export declare function gjsifyCommandArgv(literal: string): string[] | null;
|