@gjsify/cli 0.46.0 → 0.48.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 +87 -83
- package/lib/commands/affected-classify.js +30 -1
- package/lib/commands/dlx.js +7 -3
- package/lib/commands/foreach.js +9 -2
- package/lib/commands/onboard.js +11 -0
- package/lib/commands/publish.d.ts +41 -0
- package/lib/commands/publish.js +223 -10
- package/lib/commands/run.js +7 -2
- package/lib/commands/ship.js +32 -4
- package/lib/types/config-data.d.ts +37 -1
- package/lib/utils/base64.d.ts +34 -0
- package/lib/utils/base64.js +47 -0
- package/lib/utils/cli-runtime-closure.d.ts +61 -0
- package/lib/utils/cli-runtime-closure.js +54 -17
- package/lib/utils/double-dash-args.d.ts +8 -0
- package/lib/utils/double-dash-args.js +64 -0
- package/lib/utils/publish-readback.d.ts +132 -0
- package/lib/utils/publish-readback.js +273 -0
- package/lib/utils/ship/changelog.d.ts +56 -0
- package/lib/utils/ship/changelog.js +166 -0
- package/lib/utils/ship/discover.d.ts +10 -1
- package/lib/utils/ship/discover.js +76 -2
- package/lib/utils/ship/entry-interpreter.d.ts +44 -0
- package/lib/utils/ship/entry-interpreter.js +109 -0
- package/lib/utils/ship/formats.js +5 -0
- package/lib/utils/ship/gi-namespaces.js +175 -12
- package/lib/utils/ship/layout.js +8 -4
- package/lib/utils/ship/plan.d.ts +5 -1
- package/lib/utils/ship/plan.js +21 -2
- package/lib/utils/ship/settings.d.ts +48 -2
- package/lib/utils/ship/settings.js +63 -3
- package/lib/utils/ship/stage-manifest.d.ts +22 -3
- package/lib/utils/ship/stage-manifest.js +40 -13
- package/lib/utils/ship/types.d.ts +13 -0
- package/package.json +19 -19
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { HostOs } from './types.js';
|
|
2
|
+
/** What a shipped artifact's `bin/<name>` can exec. */
|
|
3
|
+
export type ShipInterpreter = 'gjs' | 'node';
|
|
4
|
+
/** The specifiers in an entry that only ONE of the two hosts can resolve. */
|
|
5
|
+
export interface EntryEvidence {
|
|
6
|
+
/** `gi://…` and the bare GJS built-ins — resolvable under GJS, refused by node. */
|
|
7
|
+
gi: string[];
|
|
8
|
+
/** `node:…` specifiers — resolvable under Node, refused by GJS's loader. */
|
|
9
|
+
node: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Host-only specifiers of `source`, deduplicated and sorted. */
|
|
12
|
+
export declare function entryEvidence(source: string): EntryEvidence;
|
|
13
|
+
/** Everything the refusal names, so it can point at the key that decides each half. */
|
|
14
|
+
export interface EntryCheckInput {
|
|
15
|
+
/** The resolved interpreter of THIS target. */
|
|
16
|
+
interpreter: ShipInterpreter;
|
|
17
|
+
/** The entry's source, as it will be installed. */
|
|
18
|
+
source: string;
|
|
19
|
+
/** How the entry is spelled in the project, for the message. */
|
|
20
|
+
entry: string;
|
|
21
|
+
/** The config key the interpreter came from — `gjsify.app` or `gjsify.ship.app.<os>`. */
|
|
22
|
+
appKey: string;
|
|
23
|
+
/** The config key the entry came from — `gjsify.main`, `gjsify.ship.bundle[.<os>]`, … */
|
|
24
|
+
bundleKey: string;
|
|
25
|
+
/**
|
|
26
|
+
* The layout being assembled, which is the key a per-target fix goes under.
|
|
27
|
+
*
|
|
28
|
+
* {@link HostOs} and not `string`: an OS spelling this command does not
|
|
29
|
+
* assemble for is the silent defect the two resolvers refuse by name, and a
|
|
30
|
+
* message that offers `gjsify.ship.bundle.<whatever-was-passed>` as the fix
|
|
31
|
+
* would be the one place that hands the reader the typo back.
|
|
32
|
+
*/
|
|
33
|
+
layoutOs: HostOs;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Refuse a payload the target's own interpreter cannot load.
|
|
37
|
+
*
|
|
38
|
+
* ASYMMETRIC LIKE `assertLauncherMatchesInterpreter`, and for the same reason:
|
|
39
|
+
* evidence FOR the wrong host is a refusal, absence of evidence is not. A bundle
|
|
40
|
+
* that imports neither scheme — a pure-JS CLI, a bundle whose GI reach is all
|
|
41
|
+
* dynamic — passes, because nothing here observed a contradiction. This function
|
|
42
|
+
* reports what it read, never what it failed to read.
|
|
43
|
+
*/
|
|
44
|
+
export declare function assertEntryRunsUnder(input: EntryCheckInput): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Can the interpreter this target execs load the entry this target ships?
|
|
2
|
+
//
|
|
3
|
+
// THE DEFECT (#1545, measured on a React-Native-on-GTK application at 0.47.0).
|
|
4
|
+
// `gjsify ship` chose the runtime and the payload independently and never
|
|
5
|
+
// compared them. `gjsify.ship.app.darwin: "node"` made the `.app`'s launcher
|
|
6
|
+
// `exec node`, while the payload came from the project's `main`, which for a
|
|
7
|
+
// project whose primary target is Linux is its `--app gjs` bundle. The run
|
|
8
|
+
// printed one line on the subject and it read as CONFIRMATION —
|
|
9
|
+
// "runtime for darwin: node — `gjsify.ship.app.darwin` overrides `gjsify.app`" —
|
|
10
|
+
// then produced an artifact that dies before a line of application code:
|
|
11
|
+
//
|
|
12
|
+
// Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file,
|
|
13
|
+
// data, and node are supported by the default ESM loader. Received protocol 'gi:'
|
|
14
|
+
//
|
|
15
|
+
// So the artifact is built, reported as built, and cannot start. That is the
|
|
16
|
+
// class this repository pays most for: a step that reports success while having
|
|
17
|
+
// measured nothing.
|
|
18
|
+
//
|
|
19
|
+
// THE DISCRIMINATOR IS THE SPECIFIER THE OTHER HOST'S LOADER REFUSES, and it is
|
|
20
|
+
// exact in both directions — measured on this machine, GJS 1.86 / Node 24:
|
|
21
|
+
//
|
|
22
|
+
// node ← `import … from 'gi://Gtk?version=4.0'`
|
|
23
|
+
// ERR_UNSUPPORTED_ESM_URL_SCHEME … Received protocol 'gi:'
|
|
24
|
+
// node ← `import system from 'system'`
|
|
25
|
+
// ERR_MODULE_NOT_FOUND — a GJS built-in is not a package
|
|
26
|
+
// gjs ← `import … from 'node:fs'`
|
|
27
|
+
// ImportError: Unsupported URI scheme for importing: node
|
|
28
|
+
//
|
|
29
|
+
// None is a heuristic about how the bundle was built: it is the specifier the
|
|
30
|
+
// running loader rejects, read out of the file that will be installed. And none
|
|
31
|
+
// can appear in a bundle the OTHER target built for itself — `--app node`
|
|
32
|
+
// rewrites every `gi://` into `requireGi()` and every bare GJS built-in into an
|
|
33
|
+
// `@gjsify/node-gi` shim (`gjs-gi-node.ts`), `--app gjs` resolves every `node:`
|
|
34
|
+
// builtin to its `@gjsify/*` implementation — so the evidence is present exactly
|
|
35
|
+
// when the pair is wrong.
|
|
36
|
+
//
|
|
37
|
+
// THE BARE BUILT-INS ARE NOT DECORATION. A GJS bundle that reaches GI through the
|
|
38
|
+
// legacy `imports.gi` object carries no `gi://` at all, and a `--app gjs` build of
|
|
39
|
+
// one imports `system` and nothing else this reader would have seen — so #1545's
|
|
40
|
+
// own pair (a GJS payload under a node launcher) passed the guard written for it
|
|
41
|
+
// until they were added. What is deliberately NOT read is the ambient `imports`
|
|
42
|
+
// object itself: node fails on it at the first USE, not at load, so a refusal
|
|
43
|
+
// citing it would be making a claim this file cannot keep.
|
|
44
|
+
//
|
|
45
|
+
// WHY A REFUSAL AND NOT A WARNING. There is no reading of this artifact under
|
|
46
|
+
// which it works. A warning at the end of a `ship` run competes with the
|
|
47
|
+
// artifact path printed beside it, and the artifact is what gets uploaded.
|
|
48
|
+
import { importedSpecifier, walkModuleAst } from '../cli-runtime-closure.js';
|
|
49
|
+
/**
|
|
50
|
+
* The GJS built-in modules, which are bare specifiers rather than a scheme.
|
|
51
|
+
*
|
|
52
|
+
* `cairo` is deliberately absent: it is also an ordinary npm package name, so a
|
|
53
|
+
* node bundle can legitimately import it and refusing that would fail a working
|
|
54
|
+
* artifact. `system` and `gettext` have no npm counterpart a real project
|
|
55
|
+
* imports, which is what makes them evidence rather than a guess.
|
|
56
|
+
*/
|
|
57
|
+
const GJS_BUILTINS = new Set(['system', 'gettext']);
|
|
58
|
+
/** Host-only specifiers of `source`, deduplicated and sorted. */
|
|
59
|
+
export function entryEvidence(source) {
|
|
60
|
+
const gi = new Set();
|
|
61
|
+
const node = new Set();
|
|
62
|
+
walkModuleAst(source, (astNode) => {
|
|
63
|
+
const specifier = importedSpecifier(astNode);
|
|
64
|
+
if (specifier === null)
|
|
65
|
+
return;
|
|
66
|
+
if (specifier.startsWith('gi://') || GJS_BUILTINS.has(specifier))
|
|
67
|
+
gi.add(specifier);
|
|
68
|
+
else if (specifier.startsWith('node:'))
|
|
69
|
+
node.add(specifier);
|
|
70
|
+
});
|
|
71
|
+
return { gi: [...gi].sort(), node: [...node].sort() };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Refuse a payload the target's own interpreter cannot load.
|
|
75
|
+
*
|
|
76
|
+
* ASYMMETRIC LIKE `assertLauncherMatchesInterpreter`, and for the same reason:
|
|
77
|
+
* evidence FOR the wrong host is a refusal, absence of evidence is not. A bundle
|
|
78
|
+
* that imports neither scheme — a pure-JS CLI, a bundle whose GI reach is all
|
|
79
|
+
* dynamic — passes, because nothing here observed a contradiction. This function
|
|
80
|
+
* reports what it read, never what it failed to read.
|
|
81
|
+
*/
|
|
82
|
+
export function assertEntryRunsUnder(input) {
|
|
83
|
+
const evidence = entryEvidence(input.source);
|
|
84
|
+
const refused = input.interpreter === 'node' ? evidence.gi : evidence.node;
|
|
85
|
+
if (refused.length === 0)
|
|
86
|
+
return;
|
|
87
|
+
const other = input.interpreter === 'node' ? 'gjs' : 'node';
|
|
88
|
+
// The error the reader will actually see, which differs WITHIN the node case:
|
|
89
|
+
// a `gi:` specifier is refused as an unknown URL scheme, a bare GJS built-in
|
|
90
|
+
// as a package that does not exist. Naming the wrong one sends them looking
|
|
91
|
+
// for the wrong thing in a log they have not opened yet.
|
|
92
|
+
const loader = input.interpreter === 'gjs'
|
|
93
|
+
? 'GJS\'s loader refuses it: ImportError, "Unsupported URI scheme for importing: node"'
|
|
94
|
+
: refused.some((specifier) => specifier.startsWith('gi://'))
|
|
95
|
+
? 'node\'s ESM loader refuses it: ERR_UNSUPPORTED_ESM_URL_SCHEME, "Only URLs with a scheme in: ' +
|
|
96
|
+
'file, data, and node are supported by the default ESM loader"'
|
|
97
|
+
: "node's ESM loader refuses it: ERR_MODULE_NOT_FOUND — a GJS built-in is not a package";
|
|
98
|
+
const shown = refused.slice(0, 3).join(', ');
|
|
99
|
+
const more = refused.length > 3 ? `, and ${refused.length - 3} more` : '';
|
|
100
|
+
throw new Error(`gjsify ship: this target execs \`${input.interpreter}\` (\`${input.appKey}\`), and the entry it ` +
|
|
101
|
+
`would ship — ${input.entry} (\`${input.bundleKey}\`) — imports ${shown}${more}.\n` +
|
|
102
|
+
` ${loader}, before a line of application code runs.\n` +
|
|
103
|
+
' The artifact would be built, reported as built, and fail at launch on every machine.\n' +
|
|
104
|
+
` Either ship this target the \`--app ${input.interpreter}\` build of the app — ` +
|
|
105
|
+
`\`gjsify.ship.bundle.${input.layoutOs}\` selects the entry per target, the way ` +
|
|
106
|
+
`\`gjsify.ship.app.${input.layoutOs}\` selects the runtime —\n` +
|
|
107
|
+
` or set \`gjsify.ship.app.${input.layoutOs}\` to "${other}" if this bundle is the one ` +
|
|
108
|
+
'that should ship.');
|
|
109
|
+
}
|
|
@@ -275,6 +275,11 @@ export const FORMATS = {
|
|
|
275
275
|
// /usr/share/doc/<package>/copyright, and lintian errors without it.
|
|
276
276
|
licenseDest: (binaryName) => `share/doc/${binaryName}/copyright`,
|
|
277
277
|
licenseKind: 'debian-copyright',
|
|
278
|
+
// Debian Policy § 4.4. `.Debian` and not plain `changelog` because
|
|
279
|
+
// `deb.ts` writes `Version: <version>-<release>`, i.e. a Debian
|
|
280
|
+
// revision, i.e. a non-native package — which is the half of lintian's
|
|
281
|
+
// `no-changelog … (non-native package)` that names the FILE.
|
|
282
|
+
changelogDest: (binaryName) => `share/doc/${binaryName}/changelog.Debian.gz`,
|
|
278
283
|
archName: debArch,
|
|
279
284
|
fileName: (s, archLabel) => `${s.binaryName}_${s.version}-${s.release}_${archLabel}.deb`,
|
|
280
285
|
artifactKind: 'file',
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
// Which GI namespaces does this bundle actually load?
|
|
2
2
|
//
|
|
3
|
-
// Read off the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Read off the EMITTED bundle, because that is the file that gets installed —
|
|
4
|
+
// and the emitted form is not one form but two, which is the correction this
|
|
5
|
+
// header carries. Under `--app gjs`, `gi://` is a real module protocol, so the
|
|
6
|
+
// bundler keeps those specifiers in the output verbatim
|
|
7
|
+
// (`rolldown-plugin-gjsify`'s externals plugin) and the artifact carries its own
|
|
8
|
+
// dependency list. Under `--app node` there is no such protocol:
|
|
9
|
+
// `gjsGiNodePlugin` rewrites every `gi://Ns?version=X` into a shim whose body is
|
|
10
|
+
// `require('@gjsify/node-gi/gi').requireGi("Ns", "X")`, so the specifier this
|
|
11
|
+
// scanner was written to find is gone from the file by construction.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THAT COST, and why reading one form is not "mostly right": these
|
|
14
|
+
// namespaces are what `depends.ts` maps to `gir1.2-gtk-4.0` and friends
|
|
15
|
+
// (ADR 0024 § 6). A `--app node` project therefore derived an EMPTY typelib
|
|
16
|
+
// dependency set — the package installed cleanly and died at its first GI call,
|
|
17
|
+
// which is the same failure the bare-side-effect-import fix below was written to
|
|
18
|
+
// prevent, arriving again through the other build target. Nothing caught it
|
|
19
|
+
// because every ship fixture's "node bundle" was hand-written with `gi://`
|
|
20
|
+
// imports in it: a shape `gjsify build --app node` cannot emit and node cannot
|
|
21
|
+
// run.
|
|
8
22
|
//
|
|
9
23
|
// PARSED, not pattern-matched, and the first version of this file is why. A
|
|
10
24
|
// regex over the bundle text got it wrong in both directions at once:
|
|
@@ -17,18 +31,65 @@
|
|
|
17
31
|
// `from`, and since an unmapped namespace fails the build, that made a
|
|
18
32
|
// correct project unbuildable.
|
|
19
33
|
//
|
|
20
|
-
// `
|
|
21
|
-
// pass the CLI already uses to compute its own runtime closure — so there is
|
|
22
|
-
//
|
|
23
|
-
import {
|
|
34
|
+
// `walkModuleAst` answers both questions in ONE pass, and it is the same acorn
|
|
35
|
+
// pass the CLI already uses to compute its own runtime closure — so there is one
|
|
36
|
+
// definition of "what does this file import" rather than two.
|
|
37
|
+
import { importedSpecifier, staticStringValue, walkModuleAst, } from '../cli-runtime-closure.js';
|
|
38
|
+
/** The module a `--app node` bundle reaches GI through. */
|
|
39
|
+
const NODE_GI_MODULE = '@gjsify/node-gi/gi';
|
|
40
|
+
/** Namespaces are GI identifiers; anything else is a call this reader misread. */
|
|
41
|
+
const NAMESPACE = /^[A-Za-z][A-Za-z\d_]*$/;
|
|
24
42
|
/**
|
|
25
43
|
* Extract the GI namespaces a bundle imports, as `Ns-Version` when the
|
|
26
44
|
* specifier pins one and bare `Ns` when it does not.
|
|
27
45
|
*/
|
|
28
46
|
export function scanGiNamespaces(source) {
|
|
29
47
|
const found = new Set();
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
// TWO SETS, because the two kinds of binding are not interchangeable and
|
|
49
|
+
// treating them as one over-approximated on a real minified bundle. A
|
|
50
|
+
// namespace object (`import * as gi`) and a required module object are only
|
|
51
|
+
// ever the OBJECT of `.requireGi(…)`: calling a module namespace is a
|
|
52
|
+
// TypeError, so a bare `gi(…)` is never one of ours. Only `requireGi` itself
|
|
53
|
+
// — the named import, or the default export, which IS `requireGi` — is
|
|
54
|
+
// callable on its own.
|
|
55
|
+
//
|
|
56
|
+
// Measured on `gjsify build --app node` (minify is the DEFAULT): a file with
|
|
57
|
+
// `import * as gi from '@gjsify/node-gi/gi'` and a callback parameter emitted
|
|
58
|
+
// as `function render(e,t){return e(t)+e(\`Zzqfoo\`)}` — where the minifier
|
|
59
|
+
// gave the import and the parameter the SAME short name in different scopes.
|
|
60
|
+
// Read as one set, `e(\`Zzqfoo\`)` became the namespace `Zzqfoo`, and
|
|
61
|
+
// `deriveDepends` then refused to package a correct project.
|
|
62
|
+
const callable = new Set();
|
|
63
|
+
const objects = new Set();
|
|
64
|
+
const calls = [];
|
|
65
|
+
walkModuleAst(source, (node) => {
|
|
66
|
+
const specifier = importedSpecifier(node);
|
|
67
|
+
if (specifier !== null) {
|
|
68
|
+
const key = parseGiSpecifier(specifier);
|
|
69
|
+
if (key !== null)
|
|
70
|
+
found.add(key);
|
|
71
|
+
if (node.type === 'ImportDeclaration' && specifier === NODE_GI_MODULE) {
|
|
72
|
+
collectNodeGiBindings(node, callable, objects);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (node.type === 'VariableDeclarator' && loadsNodeGiModule(node.init)) {
|
|
77
|
+
const id = node.id;
|
|
78
|
+
// An OBJECT: `const gi = require('@gjsify/node-gi/gi')` is the module,
|
|
79
|
+
// reached as `gi.requireGi(…)` and never called directly.
|
|
80
|
+
if (id?.type === 'Identifier' && typeof id.name === 'string')
|
|
81
|
+
objects.add(id.name);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Collected rather than resolved in place: a call can precede the
|
|
85
|
+
// `require` that binds its callee (the shim's `load()` is hoisted above
|
|
86
|
+
// nothing, but a minifier is free to reorder declarations), so the
|
|
87
|
+
// binding set has to be complete before any call is judged.
|
|
88
|
+
if (node.type === 'CallExpression')
|
|
89
|
+
calls.push(node);
|
|
90
|
+
});
|
|
91
|
+
for (const call of calls) {
|
|
92
|
+
const key = requireGiNamespace(call, callable, objects);
|
|
32
93
|
if (key !== null)
|
|
33
94
|
found.add(key);
|
|
34
95
|
}
|
|
@@ -40,10 +101,112 @@ export function parseGiSpecifier(specifier) {
|
|
|
40
101
|
return null;
|
|
41
102
|
const rest = specifier.slice('gi://'.length);
|
|
42
103
|
const [namespace, query] = rest.split('?', 2);
|
|
43
|
-
if (namespace === undefined ||
|
|
104
|
+
if (namespace === undefined || !NAMESPACE.test(namespace))
|
|
44
105
|
return null;
|
|
45
106
|
if (query === undefined)
|
|
46
107
|
return namespace;
|
|
47
108
|
const version = /(?:^|&)version=([^&]+)/.exec(query)?.[1];
|
|
48
109
|
return version === undefined ? namespace : `${namespace}-${version}`;
|
|
49
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* What an `import … from '@gjsify/node-gi/gi'` binds, split by what it can BE.
|
|
113
|
+
*
|
|
114
|
+
* All three spellings appear in real code and the default export IS `requireGi`
|
|
115
|
+
* (`packages/node-gi/node-gi/gi.d.ts`), so the named import (under any local
|
|
116
|
+
* name — `import { requireGi as gi }` renames it) and the default import are
|
|
117
|
+
* CALLABLE. A namespace import is not: calling a module namespace object throws,
|
|
118
|
+
* so it can only ever be the object of `.requireGi(…)`, and putting it in the
|
|
119
|
+
* callable set is what let a minified callback parameter of the same name be
|
|
120
|
+
* read as a `requireGi` call.
|
|
121
|
+
*/
|
|
122
|
+
function collectNodeGiBindings(node, callable, objects) {
|
|
123
|
+
const specifiers = node.specifiers ?? [];
|
|
124
|
+
for (const specifier of specifiers) {
|
|
125
|
+
const local = specifier.local;
|
|
126
|
+
if (local?.type !== 'Identifier' || typeof local.name !== 'string')
|
|
127
|
+
continue;
|
|
128
|
+
const imported = specifier.imported;
|
|
129
|
+
if (specifier.type === 'ImportNamespaceSpecifier')
|
|
130
|
+
objects.add(local.name);
|
|
131
|
+
else if (specifier.type === 'ImportDefaultSpecifier')
|
|
132
|
+
callable.add(local.name);
|
|
133
|
+
else if (imported?.type === 'Identifier' && imported.name === 'requireGi')
|
|
134
|
+
callable.add(local.name);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A call that LOADS `@gjsify/node-gi/gi` — recognised by its argument, never by
|
|
139
|
+
* the name of the callee.
|
|
140
|
+
*
|
|
141
|
+
* THE CALLEE NAME DOES NOT SURVIVE BUNDLING, and reading it is how the first cut
|
|
142
|
+
* of this function passed its own tests while answering `[]` for every real
|
|
143
|
+
* artifact. Measured on `gjsify build --app node` output for a two-import app,
|
|
144
|
+
* which is the exact input this module exists to read:
|
|
145
|
+
*
|
|
146
|
+
* unminified const require$1 = createRequire(import.meta.url);
|
|
147
|
+
* … require$1("@gjsify/node-gi/gi").requireGi("Gtk", "4.0")
|
|
148
|
+
* minified const n = e(import.meta.url);
|
|
149
|
+
* … n(`@gjsify/node-gi/gi`).requireGi(`Gtk`, `4.0`)
|
|
150
|
+
*
|
|
151
|
+
* Two shims in one bundle means two `require` bindings, so the second is renamed
|
|
152
|
+
* even without `--minify` (which is the DEFAULT). A reader keyed on the
|
|
153
|
+
* identifier `require` therefore loses one namespace unminified and both
|
|
154
|
+
* minified — silently, since an empty namespace list is a legal answer.
|
|
155
|
+
*
|
|
156
|
+
* The module STRING is what survives every rename, and it is a precise
|
|
157
|
+
* discriminator: a call taking `'@gjsify/node-gi/gi'` as its first argument is a
|
|
158
|
+
* load of that module under any name a bundler gives the loader.
|
|
159
|
+
*/
|
|
160
|
+
function loadsNodeGiModule(node) {
|
|
161
|
+
if (!node || typeof node !== 'object')
|
|
162
|
+
return false;
|
|
163
|
+
const call = node;
|
|
164
|
+
if (call.type !== 'CallExpression')
|
|
165
|
+
return false;
|
|
166
|
+
const args = call.arguments ?? [];
|
|
167
|
+
return staticStringValue(args[0]) === NODE_GI_MODULE;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The namespace a `requireGi(…)` call names, or `null` when this is not one.
|
|
171
|
+
*
|
|
172
|
+
* BINDING-TRACED rather than name-matched, and the asymmetry is deliberate: a
|
|
173
|
+
* bare `.requireGi(…)` on any object would over-approximate, and
|
|
174
|
+
* over-approximating is NOT the harmless direction here — an unmapped namespace
|
|
175
|
+
* fails the build, so a foreign method sharing the name would make a correct
|
|
176
|
+
* project unpackageable. Every accepted shape traces back to an import or
|
|
177
|
+
* require of `@gjsify/node-gi/gi` in this same file, which is what a bundle
|
|
178
|
+
* always has: the shim carries its own `require`, and a hand-written node-gi app
|
|
179
|
+
* carries its own import.
|
|
180
|
+
*
|
|
181
|
+
* Shadowing is the one gap left, and the split above is what makes it small.
|
|
182
|
+
* This reader has no scope analysis, so a local binding that reuses the name of
|
|
183
|
+
* the CALLABLE import — `requireGi` itself, or the default import — and is called
|
|
184
|
+
* with a GI-shaped string literal would still be read as one. A minifier can
|
|
185
|
+
* manufacture exactly that collision, which is why the two shapes it produces
|
|
186
|
+
* most (the namespace object and the required module object) are the two that no
|
|
187
|
+
* longer count as callees.
|
|
188
|
+
*/
|
|
189
|
+
function requireGiNamespace(call, callable, objects) {
|
|
190
|
+
const callee = call.callee;
|
|
191
|
+
let reached = false;
|
|
192
|
+
if (callee?.type === 'Identifier') {
|
|
193
|
+
reached = typeof callee.name === 'string' && callable.has(callee.name);
|
|
194
|
+
}
|
|
195
|
+
else if (callee?.type === 'MemberExpression' && callee.computed !== true) {
|
|
196
|
+
const property = callee.property;
|
|
197
|
+
if (property?.type !== 'Identifier' || property.name !== 'requireGi')
|
|
198
|
+
return null;
|
|
199
|
+
const object = callee.object;
|
|
200
|
+
reached =
|
|
201
|
+
loadsNodeGiModule(object) ||
|
|
202
|
+
(object?.type === 'Identifier' && typeof object.name === 'string' && objects.has(object.name));
|
|
203
|
+
}
|
|
204
|
+
if (!reached)
|
|
205
|
+
return null;
|
|
206
|
+
const args = call.arguments ?? [];
|
|
207
|
+
const namespace = staticStringValue(args[0]);
|
|
208
|
+
if (namespace === null || !NAMESPACE.test(namespace))
|
|
209
|
+
return null;
|
|
210
|
+
const version = staticStringValue(args[1]);
|
|
211
|
+
return version === null ? namespace : `${namespace}-${version}`;
|
|
212
|
+
}
|
package/lib/utils/ship/layout.js
CHANGED
|
@@ -125,7 +125,9 @@ export const LAYOUTS = {
|
|
|
125
125
|
'macOS is CoreText-backed and GTK is not built against fontconfig there. That the activation ' +
|
|
126
126
|
"reaches Pango's CoreText font map is UNVERIFIED: no leg in this repository runs a `.app`. " +
|
|
127
127
|
'Confirm with `PangoCairo.FontMap.get_default().list_families()` in the shipped bundle, and ' +
|
|
128
|
-
'`PANGOCAIRO_BACKEND=bogus` to print which backend it was built with.'
|
|
128
|
+
'`PANGOCAIRO_BACKEND=bogus` to print which backend it was built with. An app that calls ' +
|
|
129
|
+
'`initFonts()` from `@gjsify/gtk-host/fonts` for the Windows row needs no branch around it: the ' +
|
|
130
|
+
'CoreText map answers G_IO_ERROR_NOT_SUPPORTED and that call deliberately does nothing here.',
|
|
129
131
|
launcherExt: '',
|
|
130
132
|
root: appBundleDir,
|
|
131
133
|
// Apple's, all four. `Contents/MacOS` holds executables, `Contents/Resources`
|
|
@@ -230,9 +232,11 @@ export const LAYOUTS = {
|
|
|
230
232
|
'W1-W5): a FONTCONFIG_FILE naming the staged directory moves the default font map by nothing ' +
|
|
231
233
|
'even when it is the ONLY configuration present — that map does not read fontconfig, and exposes ' +
|
|
232
234
|
'no config_changed or set_config with which to make it — while add_font_file on the same map a ' +
|
|
233
|
-
'Gtk.Label renders through does register the family. So register them at startup
|
|
234
|
-
|
|
235
|
-
'
|
|
235
|
+
'Gtk.Label renders through does register the family. So register them at startup, with one call ' +
|
|
236
|
+
"from the GTK host layer: `import { initFonts } from '@gjsify/gtk-host/fonts'`, then `initFonts()` " +
|
|
237
|
+
'before the first window — it reads GJSIFY_FONT_DIR itself, registers every face it holds, and ' +
|
|
238
|
+
'no-ops on the operating systems that already activated them. Without it the app renders in a ' +
|
|
239
|
+
'substituted family with no error.',
|
|
236
240
|
// `.cmd`, not `.bat`: the two differ in whether a failing built-in (`set`,
|
|
237
241
|
// `path`, `append`) sets ERRORLEVEL, and `.cmd` is the one where it does.
|
|
238
242
|
launcherExt: '.cmd',
|
package/lib/utils/ship/plan.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface StageInputs {
|
|
|
11
11
|
desktopEntry?: string;
|
|
12
12
|
/** Contents of the project's licence file, when one was found. */
|
|
13
13
|
licenseText?: string;
|
|
14
|
+
/** Contents of the project's own changelog, when one was found. */
|
|
15
|
+
changelogText?: string;
|
|
16
|
+
/** The build stamp every header and every rendered file shares — the changelog's date. */
|
|
17
|
+
mtime: number;
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
20
|
* Build the prefix-relative payload.
|
|
@@ -46,7 +50,7 @@ export declare function planStage(settings: ShipSettings, inputs: StageInputs):
|
|
|
46
50
|
* missing `LICENSE` — actually is; {@link assertOverlayIsLicensed} repeats it at
|
|
47
51
|
* pack time for a stage that arrived from somewhere else.
|
|
48
52
|
*/
|
|
49
|
-
export declare function planOverlay(settings: ShipSettings, format: FormatDescriptor, inputs: StageInputs): StagedFile[]
|
|
53
|
+
export declare function planOverlay(settings: ShipSettings, format: FormatDescriptor, inputs: StageInputs): Promise<StagedFile[]>;
|
|
50
54
|
/**
|
|
51
55
|
* The same refusal, at pack time.
|
|
52
56
|
*
|
package/lib/utils/ship/plan.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// case (including the ones that must be REFUSED) is unit-testable. Same split
|
|
3
3
|
// as `utils/copy-targets.ts`, and for the same reason: a stager that discovers
|
|
4
4
|
// its own inputs can only be tested by building a real project.
|
|
5
|
+
import { renderDebianChangelog } from './changelog.js';
|
|
6
|
+
import { gzipDeterministic } from './gzip.js';
|
|
5
7
|
import { renderMimePackage } from './mime.js';
|
|
6
8
|
import { SHARE } from './share-dirs.js';
|
|
7
9
|
import { basename, extname, posix } from 'node:path';
|
|
@@ -152,7 +154,7 @@ function planFonts(settings) {
|
|
|
152
154
|
* missing `LICENSE` — actually is; {@link assertOverlayIsLicensed} repeats it at
|
|
153
155
|
* pack time for a stage that arrived from somewhere else.
|
|
154
156
|
*/
|
|
155
|
-
export function planOverlay(settings, format, inputs) {
|
|
157
|
+
export async function planOverlay(settings, format, inputs) {
|
|
156
158
|
if (inputs.licenseText === undefined) {
|
|
157
159
|
throw new Error(`gjsify ship: no licence file found, so the ${format.id} package would ship no ` +
|
|
158
160
|
`${format.licenseDest(settings.binaryName)}. Debian Policy § 12.5 requires it and lintian ` +
|
|
@@ -163,7 +165,24 @@ export function planOverlay(settings, format, inputs) {
|
|
|
163
165
|
const text = format.licenseKind === 'debian-copyright'
|
|
164
166
|
? renderDebianCopyright(settings, inputs.licenseText)
|
|
165
167
|
: inputs.licenseText;
|
|
166
|
-
|
|
168
|
+
const files = [
|
|
169
|
+
{ path: format.licenseDest(settings.binaryName), mode: 0o644, source: { kind: 'text', text } },
|
|
170
|
+
];
|
|
171
|
+
// COMPRESSED HERE, not at pack time, and that is what makes the two-phase
|
|
172
|
+
// handoff sound: `--from-stage` rehydrates the overlay out of the sidecar and
|
|
173
|
+
// packs it, so the assembling host's bytes have to BE the shipped bytes. A
|
|
174
|
+
// gzip performed on the packing host would put a second compressor's output in
|
|
175
|
+
// an artifact whose every other byte came from the first. `gzipDeterministic`
|
|
176
|
+
// zeroes the header stamp for the usual reason (utils/ship/gzip.ts).
|
|
177
|
+
if (format.changelogDest !== undefined) {
|
|
178
|
+
const changelog = renderDebianChangelog(settings, inputs.changelogText, inputs.mtime);
|
|
179
|
+
files.push({
|
|
180
|
+
path: format.changelogDest(settings.binaryName),
|
|
181
|
+
mode: 0o644,
|
|
182
|
+
source: { kind: 'bytes', data: await gzipDeterministic(new TextEncoder().encode(changelog)) },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return files;
|
|
167
186
|
}
|
|
168
187
|
/**
|
|
169
188
|
* The same refusal, at pack time.
|
|
@@ -37,6 +37,7 @@ export interface DiscoveredPayload {
|
|
|
37
37
|
/** Absolute paths of the font faces the application ships itself. */
|
|
38
38
|
fontFiles: string[];
|
|
39
39
|
licenseFile?: string;
|
|
40
|
+
changelogFile?: string;
|
|
40
41
|
}
|
|
41
42
|
export interface SettingsInput {
|
|
42
43
|
projectDir: string;
|
|
@@ -144,13 +145,58 @@ export interface ResolvedSettings {
|
|
|
144
145
|
warnings: string[];
|
|
145
146
|
}
|
|
146
147
|
export declare function resolveShipSettings(input: SettingsInput): ResolvedSettings;
|
|
147
|
-
/** The
|
|
148
|
-
export
|
|
148
|
+
/** The entry one ship target installs, which key named it, and what it must NOT carry. */
|
|
149
|
+
export interface ResolvedShipBundle {
|
|
150
|
+
/** The declared path as written — project-relative or absolute. */
|
|
151
|
+
declared?: string;
|
|
152
|
+
/** The config key the answer came from — named in the refusal and in the notice. */
|
|
153
|
+
key: string;
|
|
154
|
+
/** Whether a per-target key overrode the project default. */
|
|
155
|
+
overridden: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* The entries OTHER targets declared, as written.
|
|
158
|
+
*
|
|
159
|
+
* Not decoration: `discoverPayload` stages the whole directory the entry
|
|
160
|
+
* lives in, so a project that keeps `dist/app.gjs.mjs` beside
|
|
161
|
+
* `dist/app.node.mjs` ships both to both targets. One of them is a bundle
|
|
162
|
+
* that target's interpreter cannot load, in an artifact advertising that it
|
|
163
|
+
* can — and on macOS it is inside the signed bundle.
|
|
164
|
+
*/
|
|
165
|
+
foreign: string[];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The ONE place a ship target's ENTRY is decided — {@link resolveShipApp}'s twin.
|
|
169
|
+
*
|
|
170
|
+
* The two are twins because they answer one question between them, and #1545 is
|
|
171
|
+
* what answering only half of it costs: `gjsify.ship.app.darwin: "node"` moved
|
|
172
|
+
* the launcher to `node` while the payload stayed `gjsify.main`, which for a
|
|
173
|
+
* project whose primary target is Linux is its `--app gjs` bundle. Both keys
|
|
174
|
+
* were honoured, they contradicted each other, and the `.app` died on its first
|
|
175
|
+
* import with the only line of output on the subject reading as confirmation.
|
|
176
|
+
*
|
|
177
|
+
* A DECLARATION rather than a derivation, and both alternatives were considered
|
|
178
|
+
* where the issue raised them. Deriving the entry from the runtime by filename
|
|
179
|
+
* convention (`app.node.mjs` beside `app.gjs.mjs`) is a heuristic on an output
|
|
180
|
+
* name the project owns — the same guess `tests/e2e/ship`'s fixture exists to
|
|
181
|
+
* refuse. Having ship invoke `gjsify build --app <runtime>` itself cannot work
|
|
182
|
+
* for the phase that needs it most: `--from-stage` packs on a host with no
|
|
183
|
+
* project, no bundler and no sources (ADR 0024 § A2).
|
|
184
|
+
*
|
|
185
|
+
* Fall-through, not requirement: a target with no key keeps `gjsify.main`, so
|
|
186
|
+
* every single-host project is unaffected and nothing new is mandatory.
|
|
187
|
+
*/
|
|
188
|
+
export declare function resolveShipBundle(input: {
|
|
189
|
+
pkg: ShipPackageManifest;
|
|
190
|
+
ship: ConfigDataShip;
|
|
191
|
+
layoutOs: HostOs;
|
|
192
|
+
}): ResolvedShipBundle;
|
|
149
193
|
/** npm package name → a package/binary name both dpkg and rpm accept. */
|
|
150
194
|
export declare function deriveBinaryName(pkgName: string | undefined): string;
|
|
151
195
|
/** AppStream description (string or blocks) → plain paragraphs for deb/rpm. */
|
|
152
196
|
export declare function descriptionParagraphs(description: string | Array<DescriptionBlock | string> | undefined): string[] | undefined;
|
|
153
197
|
/** `LICENSE`, `LICENSE.md`, … — the names a licence file actually has. */
|
|
154
198
|
export declare const LICENSE_FILE_NAMES: readonly ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "COPYING.md"];
|
|
199
|
+
/** `CHANGELOG.md`, … — the names a project's own changelog actually has. */
|
|
200
|
+
export declare const CHANGELOG_FILE_NAMES: readonly ["CHANGELOG.md", "CHANGELOG", "CHANGELOG.txt", "NEWS.md", "NEWS"];
|
|
155
201
|
/** The bundle's own basename, used as the launcher's exec target. */
|
|
156
202
|
export declare function bundleEntryName(bundlePath: string): string;
|
|
@@ -177,6 +177,7 @@ export function resolveShipSettings(input) {
|
|
|
177
177
|
description,
|
|
178
178
|
license,
|
|
179
179
|
licenseFile: discovered.licenseFile,
|
|
180
|
+
changelogFile: discovered.changelogFile,
|
|
180
181
|
homepage: metadata.homepageUrl ?? pkg.homepage,
|
|
181
182
|
section: ship.section ?? sections.section,
|
|
182
183
|
group: ship.group ?? sections.group,
|
|
@@ -203,9 +204,66 @@ export function resolveShipSettings(input) {
|
|
|
203
204
|
};
|
|
204
205
|
return { settings, metadata, warnings };
|
|
205
206
|
}
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
/**
|
|
208
|
+
* The ONE place a ship target's ENTRY is decided — {@link resolveShipApp}'s twin.
|
|
209
|
+
*
|
|
210
|
+
* The two are twins because they answer one question between them, and #1545 is
|
|
211
|
+
* what answering only half of it costs: `gjsify.ship.app.darwin: "node"` moved
|
|
212
|
+
* the launcher to `node` while the payload stayed `gjsify.main`, which for a
|
|
213
|
+
* project whose primary target is Linux is its `--app gjs` bundle. Both keys
|
|
214
|
+
* were honoured, they contradicted each other, and the `.app` died on its first
|
|
215
|
+
* import with the only line of output on the subject reading as confirmation.
|
|
216
|
+
*
|
|
217
|
+
* A DECLARATION rather than a derivation, and both alternatives were considered
|
|
218
|
+
* where the issue raised them. Deriving the entry from the runtime by filename
|
|
219
|
+
* convention (`app.node.mjs` beside `app.gjs.mjs`) is a heuristic on an output
|
|
220
|
+
* name the project owns — the same guess `tests/e2e/ship`'s fixture exists to
|
|
221
|
+
* refuse. Having ship invoke `gjsify build --app <runtime>` itself cannot work
|
|
222
|
+
* for the phase that needs it most: `--from-stage` packs on a host with no
|
|
223
|
+
* project, no bundler and no sources (ADR 0024 § A2).
|
|
224
|
+
*
|
|
225
|
+
* Fall-through, not requirement: a target with no key keeps `gjsify.main`, so
|
|
226
|
+
* every single-host project is unaffected and nothing new is mandatory.
|
|
227
|
+
*/
|
|
228
|
+
export function resolveShipBundle(input) {
|
|
229
|
+
const declaredTable = input.ship.bundle;
|
|
230
|
+
const perTarget = typeof declaredTable === 'object' && declaredTable !== null ? declaredTable : {};
|
|
231
|
+
// The WHOLE table, for the reason `resolveShipApp` gives: a mis-keyed
|
|
232
|
+
// override is silent by construction, so the run that must report `windows`
|
|
233
|
+
// is the one assembling some OTHER layout.
|
|
234
|
+
for (const os of Object.keys(perTarget)) {
|
|
235
|
+
if (Object.hasOwn(SHIP_APP_OSES, os))
|
|
236
|
+
continue;
|
|
237
|
+
throw new Error(`gjsify ship: \`gjsify.ship.bundle.${os}\` is not an OS this command assembles for. Known: ` +
|
|
238
|
+
`${Object.keys(SHIP_APP_OSES).join(', ')} — the \`process.platform\` spelling, not the ` +
|
|
239
|
+
"`gjsify ship <os>` positional's. An entry under a key nothing reads leaves that target on " +
|
|
240
|
+
'`gjsify.main` with nothing to say so.');
|
|
241
|
+
}
|
|
242
|
+
const projectWide = typeof declaredTable === 'string' ? declaredTable : undefined;
|
|
243
|
+
const fallback = input.pkg.gjsify?.main ?? input.pkg.main;
|
|
244
|
+
const entryFor = (os) => perTarget[os] ?? projectWide ?? fallback;
|
|
245
|
+
const override = perTarget[input.layoutOs];
|
|
246
|
+
const declared = entryFor(input.layoutOs);
|
|
247
|
+
const key = override !== undefined
|
|
248
|
+
? `gjsify.ship.bundle.${input.layoutOs}`
|
|
249
|
+
: projectWide !== undefined
|
|
250
|
+
? 'gjsify.ship.bundle'
|
|
251
|
+
: input.pkg.gjsify?.main !== undefined
|
|
252
|
+
? 'gjsify.main'
|
|
253
|
+
: 'main';
|
|
254
|
+
// RESOLVED per OS, not read off the table — because the entry another target
|
|
255
|
+
// ships is as often the FALL-THROUGH as it is a declared one, and that is
|
|
256
|
+
// #1545's own project shape: `main` is the GJS bundle, only darwin declares
|
|
257
|
+
// its own, and reading the table alone leaves the GJS bundle sitting inside
|
|
258
|
+
// the signed `.app` with nothing to name it. When nothing is keyed per OS
|
|
259
|
+
// every target resolves the same entry and this set is empty by construction.
|
|
260
|
+
const foreign = [
|
|
261
|
+
...new Set(Object.keys(SHIP_APP_OSES)
|
|
262
|
+
.filter((os) => os !== input.layoutOs)
|
|
263
|
+
.map(entryFor)
|
|
264
|
+
.filter((entry) => entry !== undefined && entry !== declared)),
|
|
265
|
+
];
|
|
266
|
+
return { declared, key, overridden: override !== undefined, foreign };
|
|
209
267
|
}
|
|
210
268
|
/** npm package name → a package/binary name both dpkg and rpm accept. */
|
|
211
269
|
export function deriveBinaryName(pkgName) {
|
|
@@ -302,6 +360,8 @@ export function descriptionParagraphs(description) {
|
|
|
302
360
|
}
|
|
303
361
|
/** `LICENSE`, `LICENSE.md`, … — the names a licence file actually has. */
|
|
304
362
|
export const LICENSE_FILE_NAMES = ['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING', 'COPYING.md'];
|
|
363
|
+
/** `CHANGELOG.md`, … — the names a project's own changelog actually has. */
|
|
364
|
+
export const CHANGELOG_FILE_NAMES = ['CHANGELOG.md', 'CHANGELOG', 'CHANGELOG.txt', 'NEWS.md', 'NEWS'];
|
|
305
365
|
/** The bundle's own basename, used as the launcher's exec target. */
|
|
306
366
|
export function bundleEntryName(bundlePath) {
|
|
307
367
|
return basename(bundlePath);
|