@gjsify/cli 0.53.0 → 0.55.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 +266 -249
- package/lib/bundler-pick.d.ts +35 -0
- package/lib/bundler-pick.js +74 -7
- package/lib/cli-app.js +3 -1
- 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 +148 -0
- package/lib/commands/devtools.d.ts +21 -0
- package/lib/commands/devtools.js +146 -0
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/ship.js +56 -8
- package/lib/commands/webext/index.js +1 -1
- package/lib/types/config-data.d.ts +5 -0
- package/lib/utils/oxc-resolve.js +9 -7
- 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/flatpak-config.d.ts +16 -0
- package/lib/utils/ship/flatpak-config.js +26 -0
- package/lib/utils/ship/formats.js +10 -7
- package/lib/utils/ship/gi-namespaces.d.ts +16 -0
- package/lib/utils/ship/gi-namespaces.js +64 -1
- package/lib/utils/ship/launcher.js +2 -2
- package/lib/utils/ship/layout.d.ts +7 -6
- package/lib/utils/ship/layout.js +42 -16
- 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/ship/types.d.ts +2 -0
- package/lib/utils/webext/targets.d.ts +1 -1
- package/lib/utils/webext/targets.js +20 -8
- package/package.json +20 -19
|
@@ -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) {
|
|
@@ -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
|
|
@@ -230,9 +230,10 @@ export interface Layout {
|
|
|
230
230
|
*
|
|
231
231
|
* * a REASON. `archName` answers "no Windows architecture is known for
|
|
232
232
|
* `process.arch` \"arm64\"", which reads as a gap in our table. The truth is
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* "here is what would have to change" is the whole
|
|
233
|
+
* a package this project has not built yet — ordered in
|
|
234
|
+
* `status/open-todos/windows.md` and tracked in #1117 — and the difference
|
|
235
|
+
* between "unsupported" and "here is what would have to change" is the whole
|
|
236
|
+
* value of the message.
|
|
236
237
|
* * a STAGE-TIME warning at the flag that caused it (`commands/ship.ts`), for
|
|
237
238
|
* a phase that deliberately does not refuse — `tests/e2e/ship-layout`
|
|
238
239
|
* assembles all three layouts from ONE payload on purpose, and that
|
|
@@ -314,9 +315,9 @@ export declare function hostLayout(platform?: string): Layout;
|
|
|
314
315
|
* AT STAGE TIME, which is the whole point — see {@link Layout.arches}. The
|
|
315
316
|
* message carries the row's own `why` rather than a generic "unsupported", because
|
|
316
317
|
* the two refusals a reader can act on are different jobs: `x64` is what a Windows
|
|
317
|
-
* artifact is built for TODAY, and `win32-arm64`
|
|
318
|
-
* not
|
|
319
|
-
* them to look for a flag.
|
|
318
|
+
* artifact is built for TODAY, and `win32-arm64` waits on a package of ours that
|
|
319
|
+
* does not exist yet. Telling an author "unknown architecture" for the second one
|
|
320
|
+
* would send them to look for a flag.
|
|
320
321
|
*
|
|
321
322
|
* `process.arch` spelling on both sides, so the value quoted back is the one the
|
|
322
323
|
* user typed after `--arch`.
|
package/lib/utils/ship/layout.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// is the first layout that is NOT a prefix substitution: the launcher, the bundle,
|
|
8
8
|
// the shared libraries and the data tree each move somewhere different, and on
|
|
9
9
|
// macOS the native files leave the bundle directory altogether for
|
|
10
|
-
// `Contents/
|
|
10
|
+
// `Contents/Resources/native` — a path Apple's own layout does not have, for the
|
|
11
|
+
// measured reason {@link layoutForOs}'s darwin row carries.
|
|
11
12
|
//
|
|
12
13
|
// So the layout is a MAP from the prefix-relative plan to a stage-relative path,
|
|
13
14
|
// and `planStage` keeps producing the prefix-relative plan for all three. That
|
|
@@ -168,12 +169,34 @@ export const LAYOUTS = {
|
|
|
168
169
|
// everything that is not, and `Contents/Frameworks` the dylibs — which is
|
|
169
170
|
// the split that makes this more than a prefix: the `gi/` directory that
|
|
170
171
|
// sits INSIDE `lib/<name>/` on Linux leaves the bundle directory entirely.
|
|
172
|
+
//
|
|
173
|
+
// …AND THE NATIVE DIRECTORY IS NOT APPLE'S, because `Contents/Frameworks`
|
|
174
|
+
// is RESERVED FOR NESTED CODE and one directory of it is loadable code
|
|
175
|
+
// that cannot be there. Measured on darwin-arm64 / macOS 27 with the
|
|
176
|
+
// published `@gjsify/gtk-runtime-darwin-arm64` staged by M2b: `codesign`
|
|
177
|
+
// refuses the BUNDLE SEAL with *"bundle format unrecognized, invalid, or
|
|
178
|
+
// unsuitable / In subcomponent: …/gtk/lib/gstreamer-1.0"*, so
|
|
179
|
+
// `gjsify ship darwin --sign` cannot sign a real GTK application at all —
|
|
180
|
+
// and it does so at exit 1 with the payload half-signed. The trigger is not
|
|
181
|
+
// gstreamer: it is any directory under `Contents/Frameworks` whose last
|
|
182
|
+
// dot-separated component parses as a version, because `codesign` reads
|
|
183
|
+
// `foo-1.0`, `foo1.0` and `Versions/1.0` as a VERSIONED NESTED BUNDLE
|
|
184
|
+
// (`<name>-<version>` is a framework's shape) and then finds no
|
|
185
|
+
// `Contents/Info.plist` inside. The relocated closure carries three:
|
|
186
|
+
// `girepository-1.0`, `gdk-pixbuf-2.0/2.10.0` and `lib/gstreamer-1.0`.
|
|
187
|
+
//
|
|
188
|
+
// The same three under `Contents/Resources` seal and verify at exit 0, which
|
|
189
|
+
// is the whole measurement. So `native` is the ONE seam that moves: the
|
|
190
|
+
// relocated closure, the addon, the app's own `gi/` dylibs and both launchers'
|
|
191
|
+
// `GI_TYPELIB_PATH`/`GJSIFY_GI_LIBRARY_PATH` all read it, so the tree and
|
|
192
|
+
// every path that names it move together. Library validation is unaffected —
|
|
193
|
+
// it follows the SIGNATURE (§ `signing.ts`'s darwin row), not the directory.
|
|
171
194
|
dirs: (identity) => {
|
|
172
195
|
const app = appBundleDir(identity);
|
|
173
196
|
return {
|
|
174
197
|
launcher: `${app}/Contents/MacOS`,
|
|
175
198
|
bundle: `${app}/Contents/Resources/lib`,
|
|
176
|
-
native: `${app}/Contents/
|
|
199
|
+
native: `${app}/Contents/Resources/native`,
|
|
177
200
|
data: `${app}/Contents/Resources/share`,
|
|
178
201
|
other: `${app}/Contents/Resources`,
|
|
179
202
|
};
|
|
@@ -355,19 +378,22 @@ export const LAYOUTS = {
|
|
|
355
378
|
]
|
|
356
379
|
: [];
|
|
357
380
|
},
|
|
358
|
-
// ONE, and the blocker is
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
381
|
+
// ONE, and the blocker is OURS now. `wingtk/gvsbuild` hardcodes
|
|
382
|
+
// `self.platform = "x64"` and publishes x64 assets only — but we stopped
|
|
383
|
+
// waiting for it: ADR 0089 builds the arm64 GTK prefix from
|
|
384
|
+
// `gjsify/gvsbuild` in our own CI and `@gjsify/node-gi` declares
|
|
385
|
+
// `win32-arm64` against it (#2003). What is missing is
|
|
386
|
+
// `@gjsify/gtk-runtime-win32-arm64`, the closure this layout stages and on
|
|
387
|
+
// Windows the only GTK there is, so a Windows/ARM artifact still has
|
|
388
|
+
// nothing to load. Adding arm64 to `only` is the PR that builds that
|
|
389
|
+
// package, not this one.
|
|
366
390
|
arches: {
|
|
367
391
|
only: ['x64'],
|
|
368
|
-
why: '
|
|
369
|
-
'
|
|
370
|
-
'
|
|
392
|
+
why: 'the arm64 GTK closure `@gjsify/gtk-runtime-win32-arm64` does not exist yet — the prefix it ' +
|
|
393
|
+
'would be built from is no longer missing, because ADR 0089 builds it from our `gjsify/gvsbuild` ' +
|
|
394
|
+
'fork in CI and `@gjsify/node-gi` ships a win32-arm64 prebuild against it — so what is left is ' +
|
|
395
|
+
'the runtime package this layout would stage, ordered in `status/open-todos/windows.md` and ' +
|
|
396
|
+
'tracked in gjsify/gjsify#1117',
|
|
371
397
|
},
|
|
372
398
|
},
|
|
373
399
|
};
|
|
@@ -479,9 +505,9 @@ export function hostLayout(platform = process.platform) {
|
|
|
479
505
|
* AT STAGE TIME, which is the whole point — see {@link Layout.arches}. The
|
|
480
506
|
* message carries the row's own `why` rather than a generic "unsupported", because
|
|
481
507
|
* the two refusals a reader can act on are different jobs: `x64` is what a Windows
|
|
482
|
-
* artifact is built for TODAY, and `win32-arm64`
|
|
483
|
-
* not
|
|
484
|
-
* them to look for a flag.
|
|
508
|
+
* artifact is built for TODAY, and `win32-arm64` waits on a package of ours that
|
|
509
|
+
* does not exist yet. Telling an author "unknown architecture" for the second one
|
|
510
|
+
* would send them to look for a flag.
|
|
485
511
|
*
|
|
486
512
|
* `process.arch` spelling on both sides, so the value quoted back is the one the
|
|
487
513
|
* user typed after `--arch`.
|
package/lib/utils/ship/rpm.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export interface RpmInputs {
|
|
|
5
5
|
payload: readonly PayloadEntry[];
|
|
6
6
|
prefix: string;
|
|
7
7
|
depends: readonly string[];
|
|
8
|
+
/**
|
|
9
|
+
* Soft dependencies, written as the `RECOMMEND*` tags — an `&optional` typelib
|
|
10
|
+
* (ADR 0087), which the package installs with and runs without.
|
|
11
|
+
*/
|
|
12
|
+
recommends?: readonly string[];
|
|
8
13
|
/** RPM architecture (`x86_64`, `noarch`, …). */
|
|
9
14
|
archLabel: string;
|
|
10
15
|
mtime: number;
|
package/lib/utils/ship/rpm.js
CHANGED
|
@@ -54,6 +54,17 @@ const TAG = {
|
|
|
54
54
|
REQUIREFLAGS: 1048,
|
|
55
55
|
REQUIRENAME: 1049,
|
|
56
56
|
REQUIREVERSION: 1050,
|
|
57
|
+
// The recommend trio. NOT the 1099/1046/1047 of rpm ≤ 4.18, which shared ONE
|
|
58
|
+
// index space with `REQUIRENAME`; since 4.19 each dependency tag carries its
|
|
59
|
+
// own arrays. MEASURED against a package `rpmbuild` produced and read back with
|
|
60
|
+
// `rpm` 6.0.2: RECOMMENDNAME is a STRING_ARRAY of its own right after
|
|
61
|
+
// PROVIDEVERSION, not an extension of the four REQUIRE entries. The numbers are
|
|
62
|
+
// in the 5000s for that reason and a wrong one is invisible here — rpm either
|
|
63
|
+
// ignores the tag or refuses the header, and `rpm -qp --recommends` is what
|
|
64
|
+
// says which (e2e `ship`).
|
|
65
|
+
RECOMMENDNAME: 5046,
|
|
66
|
+
RECOMMENDVERSION: 5047,
|
|
67
|
+
RECOMMENDFLAGS: 5048,
|
|
57
68
|
RPMVERSION: 1064,
|
|
58
69
|
POSTINPROG: 1086,
|
|
59
70
|
POSTUNPROG: 1088,
|
|
@@ -225,6 +236,10 @@ function mainHeaderEntries(inputs, files, cpio, compressed) {
|
|
|
225
236
|
// From the PAYLOAD, not the settings — see the same call in `deb.ts`.
|
|
226
237
|
const scripts = renderRpmScriptlets(readPayloadFacts(inputs.payload), inputs.prefix);
|
|
227
238
|
const requires = buildRequires(inputs.depends, scripts, readShebangInterpreters(inputs.payload));
|
|
239
|
+
// OMITTED, not written empty: rpm rejects a zero-count array (the same rule
|
|
240
|
+
// `encodeValue` enforces), so a package with no `&optional` namespace carries
|
|
241
|
+
// no RECOMMEND tag at all.
|
|
242
|
+
const recommends = buildRecommends(inputs.recommends ?? []);
|
|
228
243
|
const entries = [
|
|
229
244
|
{ tag: TAG.HEADERI18NTABLE, type: RpmType.STRING_ARRAY, value: ['C'] },
|
|
230
245
|
{ tag: TAG.NAME, type: RpmType.STRING, value: settings.binaryName },
|
|
@@ -277,6 +292,13 @@ function mainHeaderEntries(inputs, files, cpio, compressed) {
|
|
|
277
292
|
{ tag: TAG.REQUIRENAME, type: RpmType.STRING_ARRAY, value: requires.names },
|
|
278
293
|
{ tag: TAG.REQUIREFLAGS, type: RpmType.INT32, value: requires.flags },
|
|
279
294
|
{ tag: TAG.REQUIREVERSION, type: RpmType.STRING_ARRAY, value: requires.versions },
|
|
295
|
+
...(recommends.names.length > 0
|
|
296
|
+
? [
|
|
297
|
+
{ tag: TAG.RECOMMENDNAME, type: RpmType.STRING_ARRAY, value: recommends.names },
|
|
298
|
+
{ tag: TAG.RECOMMENDFLAGS, type: RpmType.INT32, value: recommends.flags },
|
|
299
|
+
{ tag: TAG.RECOMMENDVERSION, type: RpmType.STRING_ARRAY, value: recommends.versions },
|
|
300
|
+
]
|
|
301
|
+
: []),
|
|
280
302
|
{ tag: TAG.PAYLOADFORMAT, type: RpmType.STRING, value: 'cpio' },
|
|
281
303
|
{ tag: TAG.PAYLOADCOMPRESSOR, type: RpmType.STRING, value: 'gzip' },
|
|
282
304
|
// Write-side metadata only — no reader acts on it (rpm-format.md § 6.5).
|
|
@@ -383,6 +405,26 @@ function buildRequires(depends, scripts, interpreters) {
|
|
|
383
405
|
}
|
|
384
406
|
return { names, flags, versions };
|
|
385
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* The three parallel `Recommends` arrays — the soft half of
|
|
410
|
+
* {@link buildRequires}, same relation spelling, no `RPMLIB` and no sense bits.
|
|
411
|
+
*
|
|
412
|
+
* `0` is the right flags value and not a missing one: rpm reads an unversioned
|
|
413
|
+
* recommendation as "any version", which is what an `&optional` typelib package
|
|
414
|
+
* means (the app asks for the NAMESPACE, never for a version of the package).
|
|
415
|
+
*/
|
|
416
|
+
function buildRecommends(recommends) {
|
|
417
|
+
const names = [];
|
|
418
|
+
const flags = [];
|
|
419
|
+
const versions = [];
|
|
420
|
+
for (const depend of recommends) {
|
|
421
|
+
const parsed = parseDepend(depend);
|
|
422
|
+
names.push(parsed.name);
|
|
423
|
+
flags.push(senseFor(parsed.relation));
|
|
424
|
+
versions.push(parsed.version ?? '');
|
|
425
|
+
}
|
|
426
|
+
return { names, flags, versions };
|
|
427
|
+
}
|
|
386
428
|
function senseFor(relation) {
|
|
387
429
|
switch (relation) {
|
|
388
430
|
case '>=':
|
|
@@ -166,6 +166,12 @@ export function resolveShipSettings(input) {
|
|
|
166
166
|
throw new Error('gjsify ship: no licence — set `license` in package.json (an SPDX id, e.g. "MIT") ' +
|
|
167
167
|
'or `gjsify.ship.license.project`. Both deb and rpm carry it as a required field.');
|
|
168
168
|
}
|
|
169
|
+
// Written BACK, not read twice: the packers read `settings.license` and the AppStream
|
|
170
|
+
// renderers read `metadata.license.project`, so resolving the `package.json#license`
|
|
171
|
+
// fallback into the first alone left `<project_license>` empty in the staged metainfo
|
|
172
|
+
// and warned about a field the project had answered. The returned metadata is what every
|
|
173
|
+
// renderer reads, so this is where the one answer has to land.
|
|
174
|
+
metadata.license = { ...metadata.license, project: license };
|
|
169
175
|
const maintainer = ship.maintainer ?? formatAuthor(pkg.author) ?? formatDeveloper(metadata);
|
|
170
176
|
if (!maintainer) {
|
|
171
177
|
throw new Error('gjsify ship: no maintainer — set `author` in package.json ("Name <you@example.com>"), ' +
|
|
@@ -30,14 +30,19 @@ export interface SignerDescriptor {
|
|
|
30
30
|
/** argv after {@link tool}, for ONE file. */
|
|
31
31
|
args: (input: SignArgs) => readonly string[];
|
|
32
32
|
/**
|
|
33
|
-
* The entitlements this signer asks for,
|
|
33
|
+
* The entitlements this signer asks for, given the identity in hand.
|
|
34
|
+
*
|
|
35
|
+
* A FUNCTION, not a list, and the darwin row's second argument is what makes
|
|
36
|
+
* that necessary: the set depends on whether the identity is a real one, and
|
|
37
|
+
* one entitlement is exactly wrong in one of the two directions. It is a
|
|
38
|
+
* measured difference, not a preference — see {@link SIGNERS}.darwin.
|
|
34
39
|
*
|
|
35
40
|
* `[]` is an ANSWER and not an omission — the same rule `Layout.metadata`
|
|
36
41
|
* states. Windows has no equivalent concept at all, and a signer that granted
|
|
37
42
|
* nothing on macOS would be a different decision from one that has nothing to
|
|
38
43
|
* grant.
|
|
39
44
|
*/
|
|
40
|
-
entitlements: readonly string[];
|
|
45
|
+
entitlements: (adhoc: boolean) => readonly string[];
|
|
41
46
|
/**
|
|
42
47
|
* Does this signer seal the ARTIFACT ROOT after signing the images inside it?
|
|
43
48
|
*
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
// an ad-hoc `LC_CODE_SIGNATURE` already (they must — `install_name_tool`
|
|
19
19
|
// invalidates the original during relocation). So the darwin leg re-signs
|
|
20
20
|
// every image, and the result is new BYTES for the packer rather than a
|
|
21
|
-
// container around the old ones.
|
|
21
|
+
// container around the old ones. § A16's corollary of this — "the same
|
|
22
|
+
// identity everywhere, so library validation has nothing to object to" — is
|
|
23
|
+
// measured FALSE for the ad-hoc identity, and the darwin row's
|
|
24
|
+
// `entitlements` is where that correction lives.
|
|
22
25
|
// 4. THE ORDER IS STRUCTURAL, not conventional (§ A17). `readStage` compares
|
|
23
26
|
// each file's SIZE against `.gjsify-ship-stage.json`, and a size is no more
|
|
24
27
|
// re-sign-proof than a digest. BOTH halves are measured and they agree:
|
|
@@ -66,10 +69,10 @@
|
|
|
66
69
|
// `.dmg` is stapled and the `.zip` is told what a user has to do instead.
|
|
67
70
|
//
|
|
68
71
|
// WHAT IS STILL NOT DONE, deliberately: `signtool` is never given a timestamp URL
|
|
69
|
-
// (see the win32 row), and the
|
|
70
|
-
// `
|
|
71
|
-
// § A4's re-sign of every image
|
|
72
|
-
//
|
|
72
|
+
// (see the win32 row), and the named-identity half of § A16 is untested because it
|
|
73
|
+
// needs a certificate — the GRANT of `disable-library-validation` is the measured
|
|
74
|
+
// ad-hoc answer, and § A4's re-sign of every image stays the design of record for
|
|
75
|
+
// the Developer ID that notarisation will actually require.
|
|
73
76
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
74
77
|
import { dirname, join, sep } from 'node:path';
|
|
75
78
|
import { isOnPath } from '../check-system-deps.js';
|
|
@@ -123,32 +126,52 @@ export const SIGNERS = {
|
|
|
123
126
|
...(adhoc ? [] : ['--timestamp']),
|
|
124
127
|
file,
|
|
125
128
|
],
|
|
126
|
-
//
|
|
129
|
+
// FIVE OF THE REFERENCE'S SIX for an ad-hoc identity, FOUR for a real one,
|
|
130
|
+
// and the difference is the one § A16 left open — now MEASURED, on
|
|
131
|
+
// darwin-arm64 with the published `@gjsify/gtk-runtime-darwin-arm64` staged
|
|
132
|
+
// into the bundle (`utils/ship/layout.ts` carries the rest of that run).
|
|
127
133
|
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// cannot be distributed. The reference signs its own build; we sign what
|
|
134
|
-
// a stranger downloads.
|
|
135
|
-
// * `com.apple.security.cs.disable-library-validation` is § A16's open
|
|
136
|
-
// question and stays open. § A4's design of record is that every Mach-O
|
|
137
|
-
// in the closure is re-signed with the SAME identity as the launcher, so
|
|
138
|
-
// library validation has nothing to object to — and granting the
|
|
139
|
-
// entitlement anyway would make that re-sign look optional the first
|
|
140
|
-
// time somebody reads this list.
|
|
134
|
+
// § A16's premise was that library validation has nothing to object to
|
|
135
|
+
// because every Mach-O carries the SAME identity as the launcher. That is
|
|
136
|
+
// true for a Developer ID and FALSE for `--sign -`: an ad-hoc signature has
|
|
137
|
+
// no team at all, so the running process and the dylib it dlopens report
|
|
138
|
+
// different team IDs and the load is refused —
|
|
141
139
|
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
// `
|
|
147
|
-
|
|
140
|
+
// dlopen(…/prebuilds/darwin-arm64/node_gi.node): code signature in
|
|
141
|
+
// <378B2FDA-…> '…node_gi.node' not valid for use in process: mapping
|
|
142
|
+
// process and mapped file (non-platform) have different Team IDs
|
|
143
|
+
//
|
|
144
|
+
// — at `require('@gjsify/node-gi')`, the FIRST thing a shipped `.app` does,
|
|
145
|
+
// with every image correctly signed. Same bundle, same signer, same argv,
|
|
146
|
+
// one more entitlement: `interpreter:`/`gtk-runtime:`/`chrome: ok`/
|
|
147
|
+
// `render: 480x320`/`done: 0`.
|
|
148
|
+
//
|
|
149
|
+
// So it is granted WHERE IT IS NEEDED, which is ad-hoc only. A Developer ID
|
|
150
|
+
// signs the whole closure with one team and needs nothing, and Apple's
|
|
151
|
+
// notarisation rules read a `disable-library-validation` entitlement on a
|
|
152
|
+
// Developer-ID artifact as shipping unsigned code — so granting it there
|
|
153
|
+
// would trade a distributable artifact for a locally runnable one. The
|
|
154
|
+
// GRANT therefore stays what § A16 asked to know: § A4's re-sign of every
|
|
155
|
+
// image is the design of record, and the entitlement is what makes the
|
|
156
|
+
// ad-hoc proof of it RUNNABLE rather than a signature `codesign` accepts and
|
|
157
|
+
// dyld refuses.
|
|
158
|
+
//
|
|
159
|
+
// The four both share are what a shipped V8 needs under a hardened runtime,
|
|
160
|
+
// plus the one our own loader story needs: `@gjsify/node-gi`'s
|
|
161
|
+
// `maybeReexecForGtkRuntime` sets `DYLD_*` for its child on darwin, and a
|
|
162
|
+
// hardened process may not do that without `allow-dyld-environment-variables`.
|
|
163
|
+
//
|
|
164
|
+
// `get-task-allow` is absent from both, and that is a decision:
|
|
165
|
+
// `refs/node/tools/osx-entitlements.plist:15` grants it, it is a DEBUGGING
|
|
166
|
+
// entitlement — it lets another process attach to this one — and Apple's
|
|
167
|
+
// notarisation rules refuse a Developer-ID artifact carrying it. The
|
|
168
|
+
// reference signs its own build; we sign what a stranger downloads.
|
|
169
|
+
entitlements: (adhoc) => [
|
|
148
170
|
'com.apple.security.cs.allow-jit',
|
|
149
171
|
'com.apple.security.cs.allow-unsigned-executable-memory',
|
|
150
172
|
'com.apple.security.cs.disable-executable-page-protection',
|
|
151
173
|
'com.apple.security.cs.allow-dyld-environment-variables',
|
|
174
|
+
...(adhoc ? ['com.apple.security.cs.disable-library-validation'] : []),
|
|
152
175
|
],
|
|
153
176
|
// The `.app` is a bundle, and a bundle's signature is the seal over its
|
|
154
177
|
// whole tree — the images alone leave `codesign --verify` with nothing to
|
|
@@ -156,9 +179,10 @@ export const SIGNERS = {
|
|
|
156
179
|
sealsBundle: true,
|
|
157
180
|
sealAddPrefix: (bundleRoot) => `${bundleRoot}/Contents/_CodeSignature/`,
|
|
158
181
|
configKey: 'gjsify.ship.sign.darwin.identity',
|
|
159
|
-
gap: 'the images are signed and the bundle is sealed
|
|
160
|
-
'
|
|
161
|
-
'validation
|
|
182
|
+
gap: 'the images are signed and the bundle is sealed, and the seal survives both the `.zip` and the ' +
|
|
183
|
+
'`.dmg` round trip; § A16 stands measured in ONE direction only — library validation refuses an ' +
|
|
184
|
+
'ad-hoc-signed dylib, so the ad-hoc identity grants `disable-library-validation` while a named ' +
|
|
185
|
+
'one does not, and no run in this repository has ever held the named one',
|
|
162
186
|
},
|
|
163
187
|
win32: {
|
|
164
188
|
layoutOs: 'win32',
|
|
@@ -180,8 +204,10 @@ export const SIGNERS = {
|
|
|
180
204
|
// Windows has no equivalent concept. An Authenticode signature carries no
|
|
181
205
|
// capability claims — what a hardened runtime and an entitlement do on
|
|
182
206
|
// macOS is done on Windows by the manifest and by AppContainer, neither of
|
|
183
|
-
// which this signer touches.
|
|
184
|
-
|
|
207
|
+
// which this signer touches. `() => []` rather than `[]` because the field is
|
|
208
|
+
// a function of the identity (see the darwin row), and this one ignores the
|
|
209
|
+
// argument instead of reading `adhoc` out of it.
|
|
210
|
+
entitlements: () => [],
|
|
185
211
|
// A Windows program directory is a DIRECTORY and nothing more: there is no
|
|
186
212
|
// manifest to seal and no per-directory signature format. `signtool` signs
|
|
187
213
|
// images, one at a time, which is what the loop above already does.
|
|
@@ -303,9 +329,10 @@ export async function signPayload(input) {
|
|
|
303
329
|
writePayload(workDir, payload, '');
|
|
304
330
|
const adhoc = input.identity === ADHOC_IDENTITY;
|
|
305
331
|
let entitlements;
|
|
306
|
-
|
|
332
|
+
const granted = signer.entitlements(adhoc);
|
|
333
|
+
if (granted.length > 0) {
|
|
307
334
|
mkdirSync(dirname(input.entitlementsPath), { recursive: true });
|
|
308
|
-
writeFileSync(input.entitlementsPath, renderEntitlements(
|
|
335
|
+
writeFileSync(input.entitlementsPath, renderEntitlements(granted));
|
|
309
336
|
entitlements = input.entitlementsPath;
|
|
310
337
|
}
|
|
311
338
|
const run = async (file, what) => {
|
|
@@ -102,6 +102,18 @@ export interface StageManifest {
|
|
|
102
102
|
mtime: number;
|
|
103
103
|
/** GI namespaces the bundle imports, `Ns-Version` where the specifier pins one. */
|
|
104
104
|
namespaces: string[];
|
|
105
|
+
/**
|
|
106
|
+
* The subset of {@link namespaces} the BUNDLE marks optional (ADR 0087).
|
|
107
|
+
*
|
|
108
|
+
* OPTIONAL on read, deliberately, and that is the {STAGE_SCHEMA_VERSION} header's
|
|
109
|
+
* "a field an older reader would simply ignore" case: a stage written before the
|
|
110
|
+
* split has no such key, and answering "nothing is optional" makes every
|
|
111
|
+
* namespace a `Depends:` again — today's behaviour, and the SAFE direction. A
|
|
112
|
+
* newer reader packing an OLDER stage therefore over-declares instead of dropping
|
|
113
|
+
* a requirement the artifact cannot run without, which is the one error here that
|
|
114
|
+
* ships a package that dies at first launch.
|
|
115
|
+
*/
|
|
116
|
+
optionalNamespaces?: string[];
|
|
105
117
|
settings: PackSettings;
|
|
106
118
|
staged: StageFileRecord[];
|
|
107
119
|
overlay: Partial<Record<FormatId, StageOverlayFile[]>>;
|
|
@@ -113,6 +125,8 @@ export interface StageManifestInput {
|
|
|
113
125
|
formats: readonly FormatDescriptor[];
|
|
114
126
|
mtime: number;
|
|
115
127
|
namespaces: readonly string[];
|
|
128
|
+
/** The optional subset of {@link namespaces} — see {@link StageManifest}. */
|
|
129
|
+
optionalNamespaces?: readonly string[];
|
|
116
130
|
staged: readonly StagedFile[];
|
|
117
131
|
/** Per-format overlay, as `planOverlay` produced it. */
|
|
118
132
|
overlay: ReadonlyMap<FormatId, readonly StagedFile[]>;
|
|
@@ -187,6 +187,7 @@ export function writeStageManifest(input) {
|
|
|
187
187
|
formats: input.formats.map((format) => format.id),
|
|
188
188
|
mtime: input.mtime,
|
|
189
189
|
namespaces: [...input.namespaces],
|
|
190
|
+
optionalNamespaces: [...(input.optionalNamespaces ?? [])],
|
|
190
191
|
settings: toPackSettings(input.settings),
|
|
191
192
|
staged: input.staged.map((file) => ({
|
|
192
193
|
path: file.path,
|
|
@@ -276,6 +277,12 @@ export function readStageManifest(stageDir) {
|
|
|
276
277
|
}),
|
|
277
278
|
mtime: expectInteger(data.mtime, at('mtime')),
|
|
278
279
|
namespaces: expectArray(data.namespaces, at('namespaces')).map((entry, index) => expectString(entry, at(`namespaces[${index}]`))),
|
|
280
|
+
// Absent is the empty set, not an error — see the field's doc comment.
|
|
281
|
+
...(data.optionalNamespaces === undefined
|
|
282
|
+
? {}
|
|
283
|
+
: {
|
|
284
|
+
optionalNamespaces: expectArray(data.optionalNamespaces, at('optionalNamespaces')).map((entry, index) => expectString(entry, at(`optionalNamespaces[${index}]`))),
|
|
285
|
+
}),
|
|
279
286
|
settings: readPackSettings(record(data.settings, at('settings')), at('settings')),
|
|
280
287
|
staged: expectArray(data.staged, at('staged')).map((entry, index) => {
|
|
281
288
|
const file = record(entry, at(`staged[${index}]`));
|
|
@@ -423,6 +423,8 @@ export interface PackSettings {
|
|
|
423
423
|
* model back in the tree — which is the one thing § 8 gates the whole migration
|
|
424
424
|
* on. A project that genuinely needs to BUILD something inside the sandbox
|
|
425
425
|
* still has `gjsify flatpak init` + `gjsify flatpak build`, unchanged.
|
|
426
|
+
* A project that sets those keys and ships `flatpak` anyway is refused up front
|
|
427
|
+
* (`assertNoDroppedFlatpakModules`), never silently stripped of them.
|
|
426
428
|
*/
|
|
427
429
|
export interface ShipFlatpakSettings {
|
|
428
430
|
/** Runtime id, e.g. `org.gnome.Platform`. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WebextBrowser = 'chrome' | 'edge' | 'firefox' | 'safari';
|
|
1
|
+
export type WebextBrowser = 'chrome' | 'edge' | 'firefox' | 'opera' | 'safari';
|
|
2
2
|
export interface WebextTarget {
|
|
3
3
|
/** As spelled in `gjsify.webext.targets` and the output directory name. */
|
|
4
4
|
readonly id: string;
|
|
@@ -4,14 +4,22 @@
|
|
|
4
4
|
// `@gjsify/manifest-conformance` (plain `.mjs`, no import from here), so a
|
|
5
5
|
// target added on one side and not the other is refused by the rule before a
|
|
6
6
|
// build ever sees it.
|
|
7
|
-
export const WEBEXT_BROWSERS = ['chrome', 'edge', 'firefox', 'safari'];
|
|
7
|
+
export const WEBEXT_BROWSERS = ['chrome', 'edge', 'firefox', 'opera', 'safari'];
|
|
8
8
|
/** What a project gets when it declares no `targets`. */
|
|
9
9
|
export const DEFAULT_WEBEXT_TARGETS = ['chrome-mv3', 'firefox-mv3'];
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* folder would build, zip, and then
|
|
11
|
+
* Why a browser takes no Manifest V2 folder, absent where it still does. Chrome
|
|
12
|
+
* and Edge removed MV2 in the browser, so the folder would build, zip, and then
|
|
13
|
+
* be refused on load. Opera still LOADS an MV2 extension but its store is
|
|
14
|
+
* MV3-only (Opera, 2025-09), so the folder is refused a step later instead. The
|
|
15
|
+
* outcome is the same either way, which is why the sentence is per browser and
|
|
16
|
+
* not one claim the three do not all support.
|
|
13
17
|
*/
|
|
14
|
-
const
|
|
18
|
+
const MV2_UNSUPPORTED = new Map([
|
|
19
|
+
['chrome', 'chrome has removed Manifest V2'],
|
|
20
|
+
['edge', 'edge has removed Manifest V2'],
|
|
21
|
+
['opera', "opera's extension store accepts Manifest V3 only"],
|
|
22
|
+
]);
|
|
15
23
|
export function parseWebextTarget(id) {
|
|
16
24
|
const match = /^([a-z]+)-mv([23])$/.exec(id);
|
|
17
25
|
const browser = match?.[1];
|
|
@@ -20,9 +28,9 @@ export function parseWebextTarget(id) {
|
|
|
20
28
|
`${WEBEXT_BROWSERS.join(', ')} (e.g. chrome-mv3, firefox-mv2).`);
|
|
21
29
|
}
|
|
22
30
|
const manifestVersion = match[2] === '2' ? 2 : 3;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
const why = manifestVersion === 2 ? MV2_UNSUPPORTED.get(browser) : undefined;
|
|
32
|
+
if (why !== undefined) {
|
|
33
|
+
throw new Error(`gjsify webext: target "${id}" does not exist any more: ${why}. Use ${browser}-mv3.`);
|
|
26
34
|
}
|
|
27
35
|
return { id, browser, manifestVersion };
|
|
28
36
|
}
|
|
@@ -30,7 +38,11 @@ export function parseWebextTarget(id) {
|
|
|
30
38
|
export function webExtRunTarget(target) {
|
|
31
39
|
if (target.browser === 'firefox')
|
|
32
40
|
return 'firefox-desktop';
|
|
33
|
-
|
|
41
|
+
// By ENGINE, not by brand: the chromium driver's only browser-specific input is the
|
|
42
|
+
// binary, and `--browser-binary` already carries it — the claim branded Chrome and
|
|
43
|
+
// Edge already make. Refusing opera here would be a claim about the operator's
|
|
44
|
+
// binary, which is not what this switch is about.
|
|
45
|
+
if (target.browser === 'chrome' || target.browser === 'edge' || target.browser === 'opera')
|
|
34
46
|
return 'chromium';
|
|
35
47
|
return null;
|
|
36
48
|
}
|