@gjsify/cli 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.gjs.mjs +351 -305
- package/lib/actions/build.d.ts +22 -0
- package/lib/actions/build.js +37 -9
- package/lib/bundler-pick.d.ts +72 -1
- package/lib/bundler-pick.js +175 -17
- package/lib/cli-app.js +7 -2
- package/lib/commands/blueprint/index.d.ts +4 -0
- package/lib/commands/blueprint/index.js +15 -0
- package/lib/commands/blueprint/types.d.ts +8 -0
- package/lib/commands/blueprint/types.js +124 -0
- package/lib/commands/devtools.d.ts +21 -0
- package/lib/commands/devtools.js +146 -0
- package/lib/commands/env.d.ts +31 -0
- package/lib/commands/env.js +114 -0
- package/lib/commands/exec.d.ts +10 -0
- package/lib/commands/exec.js +208 -0
- package/lib/commands/foreach.js +5 -97
- package/lib/commands/format.js +26 -6
- package/lib/commands/index.d.ts +5 -0
- package/lib/commands/index.js +5 -0
- package/lib/commands/install.d.ts +2 -2
- package/lib/commands/install.js +5 -5
- package/lib/commands/self-update.js +2 -1
- package/lib/commands/ship.js +51 -8
- package/lib/commands/test.d.ts +18 -9
- package/lib/commands/test.js +56 -11
- package/lib/commands/tsc.js +4 -4
- package/lib/commands/upgrade.d.ts +15 -0
- package/lib/commands/upgrade.js +76 -2
- package/lib/commands/webext/index.d.ts +2 -0
- package/lib/commands/webext/index.js +15 -0
- package/lib/commands/webext/subcommands.d.ts +16 -0
- package/lib/commands/webext/subcommands.js +204 -0
- package/lib/types/config-data.d.ts +1 -1
- package/lib/utils/bin-shim.d.ts +8 -2
- package/lib/utils/bin-shim.js +6 -5
- package/lib/utils/bundle-inputs.d.ts +84 -0
- package/lib/utils/bundle-inputs.js +268 -0
- package/lib/utils/check-system-deps.d.ts +4 -2
- package/lib/utils/check-system-deps.js +21 -26
- package/lib/utils/dev-link.d.ts +9 -6
- package/lib/utils/dev-link.js +71 -19
- package/lib/utils/exec-bin.d.ts +137 -0
- package/lib/utils/exec-bin.js +321 -0
- package/lib/utils/gi-runtime-paths.js +3 -2
- package/lib/utils/gitignore.d.ts +15 -0
- package/lib/utils/gitignore.js +178 -0
- package/lib/utils/gjsify-shim.d.ts +47 -0
- package/lib/utils/gjsify-shim.js +97 -8
- package/lib/utils/install-backend-native.d.ts +19 -1
- package/lib/utils/install-backend-native.js +93 -7
- package/lib/utils/install-lock.js +4 -25
- package/lib/utils/native-skip-scan.d.ts +20 -0
- package/lib/utils/native-skip-scan.js +343 -0
- package/lib/utils/node-script.js +1 -1
- package/lib/utils/oxc-resolve.js +46 -5
- package/lib/utils/process-table.d.ts +22 -0
- package/lib/utils/process-table.js +188 -0
- package/lib/utils/prune-prefix.js +1 -1
- package/lib/utils/run-gjs.d.ts +15 -1
- package/lib/utils/run-gjs.js +16 -7
- package/lib/utils/run-node.d.ts +2 -0
- package/lib/utils/run-node.js +2 -1
- package/lib/utils/runtimes.js +14 -13
- package/lib/utils/ship/app-runtime.d.ts +7 -5
- package/lib/utils/ship/app-runtime.js +11 -8
- package/lib/utils/ship/appimage.d.ts +9 -0
- package/lib/utils/ship/appimage.js +2 -1
- package/lib/utils/ship/deb.d.ts +10 -0
- package/lib/utils/ship/deb.js +7 -0
- package/lib/utils/ship/depends.d.ts +37 -3
- package/lib/utils/ship/depends.js +21 -9
- package/lib/utils/ship/gi-namespaces.d.ts +16 -0
- package/lib/utils/ship/gi-namespaces.js +64 -1
- package/lib/utils/ship/gzip.js +1 -1
- package/lib/utils/ship/icons.d.ts +6 -0
- package/lib/utils/ship/icons.js +6 -5
- package/lib/utils/ship/launcher.js +2 -2
- package/lib/utils/ship/layout.js +25 -2
- package/lib/utils/ship/rpm.d.ts +5 -0
- package/lib/utils/ship/rpm.js +42 -0
- package/lib/utils/ship/settings.js +6 -0
- package/lib/utils/ship/signing.d.ts +7 -2
- package/lib/utils/ship/signing.js +59 -32
- package/lib/utils/ship/stage-manifest.d.ts +14 -0
- package/lib/utils/ship/stage-manifest.js +7 -0
- package/lib/utils/spawn.d.ts +13 -0
- package/lib/utils/spawn.js +10 -5
- package/lib/utils/watch-loop.d.ts +15 -1
- package/lib/utils/watch-loop.js +9 -5
- package/lib/utils/webext/build.d.ts +42 -0
- package/lib/utils/webext/build.js +120 -0
- package/lib/utils/webext/config.d.ts +72 -0
- package/lib/utils/webext/config.js +234 -0
- package/lib/utils/webext/icons.d.ts +24 -0
- package/lib/utils/webext/icons.js +83 -0
- package/lib/utils/webext/launch.d.ts +32 -0
- package/lib/utils/webext/launch.js +77 -0
- package/lib/utils/webext/manifest.d.ts +53 -0
- package/lib/utils/webext/manifest.js +219 -0
- package/lib/utils/webext/pages.d.ts +26 -0
- package/lib/utils/webext/pages.js +61 -0
- package/lib/utils/webext/targets.d.ts +13 -0
- package/lib/utils/webext/targets.js +48 -0
- package/lib/utils/win32-command.d.ts +17 -0
- package/lib/utils/win32-command.js +21 -6
- package/package.json +20 -19
package/lib/utils/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
|
};
|
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}]`));
|
package/lib/utils/spawn.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { type Win32Invocation } from './win32-command.js';
|
|
2
3
|
/**
|
|
3
4
|
* What the calling command does once this spawn settles.
|
|
4
5
|
*
|
|
@@ -82,6 +83,18 @@ export interface SpawnCompletion {
|
|
|
82
83
|
/** The child's stderr — only under `stdio: 'capture'`, `''` when it wrote none. */
|
|
83
84
|
stderr?: string;
|
|
84
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Rewrite a bare command for Windows (see `utils/win32-command.ts`). A no-op on
|
|
88
|
+
* every other platform, and on win32 for anything already carrying a path or
|
|
89
|
+
* extension. `env` is the CHILD's, because its `PATH` is what must be searched.
|
|
90
|
+
*
|
|
91
|
+
* Exported for the synchronous PROBES that cannot go through
|
|
92
|
+
* {@link spawnToCompletion} (`<tool> --version` in `check-system-deps.ts`, the
|
|
93
|
+
* runtime probe in `runtimes.ts`): a raw `execFileSync('deno', …)` is ENOENT on
|
|
94
|
+
* Windows when deno came from `npm i -g deno`, which installs a `.cmd` shim, so
|
|
95
|
+
* `--runtime deno` was refused as "not installed" on a host where `deno` runs.
|
|
96
|
+
*/
|
|
97
|
+
export declare function toSpawnable(cmd: string, args: readonly string[], env?: NodeJS.ProcessEnv): Win32Invocation;
|
|
85
98
|
/**
|
|
86
99
|
* Run `cmd args…` to completion and resolve with its exit code and signal.
|
|
87
100
|
*
|
package/lib/utils/spawn.js
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
//
|
|
61
61
|
// ## What is spawned still matters — the Windows rewrite
|
|
62
62
|
//
|
|
63
|
-
// This helper also carries the Windows command rewrite (`
|
|
63
|
+
// This helper also carries the Windows command rewrite (`toSpawnable`), which is a
|
|
64
64
|
// second reason to come through it, and the one that applies OUTSIDE the CLI too:
|
|
65
65
|
//
|
|
66
66
|
// • a real executable (`git`, `gh`, `node`, `gjs`, `flatpak`, `process.execPath`)
|
|
@@ -93,10 +93,15 @@ function resolveEnv(opts) {
|
|
|
93
93
|
/**
|
|
94
94
|
* Rewrite a bare command for Windows (see `utils/win32-command.ts`). A no-op on
|
|
95
95
|
* every other platform, and on win32 for anything already carrying a path or
|
|
96
|
-
* extension.
|
|
97
|
-
*
|
|
96
|
+
* extension. `env` is the CHILD's, because its `PATH` is what must be searched.
|
|
97
|
+
*
|
|
98
|
+
* Exported for the synchronous PROBES that cannot go through
|
|
99
|
+
* {@link spawnToCompletion} (`<tool> --version` in `check-system-deps.ts`, the
|
|
100
|
+
* runtime probe in `runtimes.ts`): a raw `execFileSync('deno', …)` is ENOENT on
|
|
101
|
+
* Windows when deno came from `npm i -g deno`, which installs a `.cmd` shim, so
|
|
102
|
+
* `--runtime deno` was refused as "not installed" on a host where `deno` runs.
|
|
98
103
|
*/
|
|
99
|
-
function
|
|
104
|
+
export function toSpawnable(cmd, args, env) {
|
|
100
105
|
const rewritten = resolveWin32Command(cmd, args, {
|
|
101
106
|
platform: process.platform,
|
|
102
107
|
env: env ?? process.env,
|
|
@@ -132,7 +137,7 @@ export function spawnToCompletion(cmd, args, opts) {
|
|
|
132
137
|
// must not touch it — see `SpawnToCompletionOptions.shell`.
|
|
133
138
|
const invocation = opts.shell
|
|
134
139
|
? { cmd, args: [...args], windowsVerbatimArguments: false }
|
|
135
|
-
:
|
|
140
|
+
: toSpawnable(cmd, args, env);
|
|
136
141
|
if (mustBlock(opts)) {
|
|
137
142
|
// `'inherit'` is NOT forwarded through `@gjsify/child_process`'s GJS
|
|
138
143
|
// spawnSync path (it drives a private main context via
|
|
@@ -23,9 +23,23 @@ export declare function spawnSupervised(cmd: string, cmdArgs: string[], env?: No
|
|
|
23
23
|
* "this project").
|
|
24
24
|
*/
|
|
25
25
|
export declare function spawnBundleSupervised(runtime: ExampleRuntime, bundlePath: string, cwd: string, subject: string): Promise<ChildProcess>;
|
|
26
|
+
/** One path the loop watches — a directory recursively, or the directory's own entries only. */
|
|
27
|
+
export interface WatchTarget {
|
|
28
|
+
path: string;
|
|
29
|
+
recursive: boolean;
|
|
30
|
+
}
|
|
26
31
|
export interface WatchLoopOptions {
|
|
27
|
-
/** Directory watched recursively. */
|
|
32
|
+
/** Directory watched recursively — the label and the default when `watch` is not given. */
|
|
28
33
|
dir: string;
|
|
34
|
+
/**
|
|
35
|
+
* Several paths instead of `dir` alone. `gjsify webext dev` needs it: its
|
|
36
|
+
* sources sit in sibling top-level directories next to `node_modules`, and a
|
|
37
|
+
* recursive watch on the project root costs one `Gio.FileMonitor` per
|
|
38
|
+
* directory under GJS, `node_modules` included.
|
|
39
|
+
*/
|
|
40
|
+
watch?: readonly WatchTarget[];
|
|
41
|
+
/** An absolute changed path the loop must not rebuild for (build output, caches). */
|
|
42
|
+
ignore?: (changed: string) => boolean;
|
|
29
43
|
/** How `dir` is spelled in the loop's own log lines. Defaults to `dir`. */
|
|
30
44
|
dirLabel?: string;
|
|
31
45
|
/** Prefix for the loop's log lines: `[<label>] …`. */
|
package/lib/utils/watch-loop.js
CHANGED
|
@@ -125,7 +125,7 @@ export function isSelfWrite(dir, output, filename) {
|
|
|
125
125
|
* fix, and a dev loop that dies on a syntax error is a dev loop nobody uses.
|
|
126
126
|
*/
|
|
127
127
|
export async function runWatchLoop(opts) {
|
|
128
|
-
const { dir, label, prepare, build, spawnChild, output, debounceMs = 200 } = opts;
|
|
128
|
+
const { dir, label, prepare, build, spawnChild, output, ignore, debounceMs = 200 } = opts;
|
|
129
129
|
// Say so BEFORE the first build: `gjsify run`'s in-process dispatch acts on
|
|
130
130
|
// this the moment the handler resolves, and a first build that throws still
|
|
131
131
|
// leaves the loop watching.
|
|
@@ -164,13 +164,16 @@ export async function runWatchLoop(opts) {
|
|
|
164
164
|
};
|
|
165
165
|
await rebuild();
|
|
166
166
|
let debounce = null;
|
|
167
|
-
const
|
|
168
|
-
|
|
167
|
+
const watchers = (opts.watch ?? [{ path: dir, recursive: true }]).map((target) => watch(target.path, { recursive: target.recursive }, (_event, filename) => {
|
|
168
|
+
const name = typeof filename === 'string' ? filename : null;
|
|
169
|
+
if (isSelfWrite(target.path, output, name))
|
|
170
|
+
return;
|
|
171
|
+
if (ignore && name !== null && ignore(resolve(target.path, name)))
|
|
169
172
|
return;
|
|
170
173
|
if (debounce)
|
|
171
174
|
clearTimeout(debounce);
|
|
172
175
|
debounce = setTimeout(() => void rebuild(), debounceMs);
|
|
173
|
-
});
|
|
176
|
+
}));
|
|
174
177
|
// Under GJS nothing else keeps us here. An `fs.watch` handle is a Gio file
|
|
175
178
|
// monitor, not a Node handle count, and the supervised spawn's own
|
|
176
179
|
// `ensureMainLoop()` declines to arm anything: it is reached from a
|
|
@@ -198,7 +201,8 @@ export async function runWatchLoop(opts) {
|
|
|
198
201
|
if (debounce)
|
|
199
202
|
clearTimeout(debounce);
|
|
200
203
|
child?.kill();
|
|
201
|
-
watcher
|
|
204
|
+
for (const watcher of watchers)
|
|
205
|
+
watcher.close();
|
|
202
206
|
process.exit(0);
|
|
203
207
|
};
|
|
204
208
|
process.on('SIGINT', shutdown);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ZipEntry } from '../ship/zip.js';
|
|
2
|
+
import type { RasterizeInput } from '../ship/icons.js';
|
|
3
|
+
import type { ResolvedWebext } from './config.js';
|
|
4
|
+
import type { WebextTarget } from './targets.js';
|
|
5
|
+
export interface BundleRequest {
|
|
6
|
+
entry: string;
|
|
7
|
+
outfile: string;
|
|
8
|
+
format: 'esm' | 'iife';
|
|
9
|
+
define: Record<string, string>;
|
|
10
|
+
minify: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface WebextBuildOptions {
|
|
13
|
+
config: ResolvedWebext;
|
|
14
|
+
targets: readonly WebextTarget[];
|
|
15
|
+
/** Absolute; the target folders are `<outDir>/<target>`. */
|
|
16
|
+
outDir: string;
|
|
17
|
+
mode: 'production' | 'development';
|
|
18
|
+
/** Effective defines: the config's, then the command line's. */
|
|
19
|
+
define: Record<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Overwrite files instead of starting from an empty folder. `dev` needs it:
|
|
22
|
+
* deleting the folder `web-ext run` watches makes Firefox unload the
|
|
23
|
+
* extension mid-rebuild (measured in beifahrer).
|
|
24
|
+
*/
|
|
25
|
+
inPlace: boolean;
|
|
26
|
+
zip: boolean;
|
|
27
|
+
/** Bundle one entry. Injectable so the assembly is testable without a bundler. */
|
|
28
|
+
bundle: (request: BundleRequest) => Promise<void>;
|
|
29
|
+
rasterize?: (input: RasterizeInput) => Promise<Map<number, Uint8Array>>;
|
|
30
|
+
log?: (line: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface WebextBuildResult {
|
|
33
|
+
/** target id → absolute folder. */
|
|
34
|
+
folders: Map<string, string>;
|
|
35
|
+
/** target id → absolute zip path (only with `zip`). */
|
|
36
|
+
zips: Map<string, string>;
|
|
37
|
+
}
|
|
38
|
+
/** One `SOURCE_DATE_EPOCH`-aware timestamp for every entry, so a rebuild zips to the same bytes. */
|
|
39
|
+
export declare function zipTimestamp(env?: NodeJS.ProcessEnv): number;
|
|
40
|
+
/** Every file under `dir`, as archive entries relative to it. */
|
|
41
|
+
export declare function zipEntriesOf(dir: string): ZipEntry[];
|
|
42
|
+
export declare function buildWebext(opts: WebextBuildOptions): Promise<WebextBuildResult>;
|