@gjsify/cli 0.49.0 → 0.51.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 +98 -97
- package/lib/actions/barrels-generate.d.ts +22 -0
- package/lib/actions/barrels-generate.js +36 -1
- package/lib/actions/build.js +8 -5
- package/lib/commands/barrels.js +19 -2
- package/lib/commands/foreach.js +11 -19
- package/lib/commands/ship.js +77 -2
- package/lib/commands/test.d.ts +16 -0
- package/lib/commands/test.js +30 -29
- package/lib/commands/workspace.js +47 -44
- package/lib/index.js +10 -0
- package/lib/utils/build-cache.d.ts +15 -4
- package/lib/utils/build-cache.js +62 -67
- package/lib/utils/cli-exit.d.ts +23 -0
- package/lib/utils/cli-exit.js +78 -0
- package/lib/utils/package-inputs.d.ts +81 -0
- package/lib/utils/package-inputs.js +233 -0
- package/lib/utils/ship/appimage.d.ts +352 -0
- package/lib/utils/ship/appimage.js +671 -0
- package/lib/utils/ship/changelog.d.ts +15 -14
- package/lib/utils/ship/changelog.js +14 -13
- package/lib/utils/ship/depends.d.ts +20 -0
- package/lib/utils/ship/depends.js +31 -16
- package/lib/utils/ship/formats.d.ts +9 -0
- package/lib/utils/ship/formats.js +173 -2
- package/lib/utils/ship/gzip.d.ts +31 -2
- package/lib/utils/ship/gzip.js +55 -3
- package/lib/utils/ship/plan.d.ts +32 -4
- package/lib/utils/ship/plan.js +61 -10
- package/lib/utils/ship/settings.d.ts +14 -0
- package/lib/utils/ship/settings.js +19 -1
- package/lib/utils/ship/types.d.ts +17 -1
- package/lib/utils/simple-command.d.ts +17 -2
- package/lib/utils/simple-command.js +9 -4
- package/package.json +19 -19
|
@@ -38,19 +38,20 @@ export declare function changelogEntriesFor(version: string, source: string): st
|
|
|
38
38
|
* wrong: exactly one leading space, then `--`, one space, the maintainer, and TWO
|
|
39
39
|
* spaces before the date.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
* gjsify's own `.deb` and
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
41
|
+
* ONE lintian tag this file still raises, measured on lintian 2.117 against
|
|
42
|
+
* gjsify's own `.deb` and left alone deliberately: `initial-upload-closes-no-bugs`,
|
|
43
|
+
* because the Debian revision is `-1` and the entry closes no bug. It is a
|
|
44
|
+
* statement about the Debian ARCHIVE's ITP workflow — the first upload of a new
|
|
45
|
+
* source package should close its ITP bug — and a package built by `gjsify ship`
|
|
46
|
+
* is never uploaded there. There is no bug number that would be true to write, so
|
|
47
|
+
* the tag is inapplicable rather than unmet.
|
|
48
|
+
*
|
|
49
|
+
* `changelog-not-compressed-with-max-compression` was a second one, and closing it
|
|
50
|
+
* was a capability gap in the CORE rather than anything wrong in this file:
|
|
51
|
+
* neither `CompressionStream` nor `@gjsify/zlib` would take a compression level,
|
|
52
|
+
* so `gzipDeterministic` had none to pass on. `@gjsify/zlib` honours
|
|
53
|
+
* `options.level` now and `plan.ts` compresses this file at
|
|
54
|
+
* `POLICY_MAX_COMPRESSION`. `verify-deb.sh` gates the tag by name, so it cannot
|
|
55
|
+
* come back quietly.
|
|
55
56
|
*/
|
|
56
57
|
export declare function renderDebianChangelog(settings: PackSettings, source: string | undefined, mtime: number): string;
|
|
@@ -134,20 +134,21 @@ const WIDTH = 80;
|
|
|
134
134
|
* wrong: exactly one leading space, then `--`, one space, the maintainer, and TWO
|
|
135
135
|
* spaces before the date.
|
|
136
136
|
*
|
|
137
|
-
*
|
|
138
|
-
* gjsify's own `.deb` and
|
|
137
|
+
* ONE lintian tag this file still raises, measured on lintian 2.117 against
|
|
138
|
+
* gjsify's own `.deb` and left alone deliberately: `initial-upload-closes-no-bugs`,
|
|
139
|
+
* because the Debian revision is `-1` and the entry closes no bug. It is a
|
|
140
|
+
* statement about the Debian ARCHIVE's ITP workflow — the first upload of a new
|
|
141
|
+
* source package should close its ITP bug — and a package built by `gjsify ship`
|
|
142
|
+
* is never uploaded there. There is no bug number that would be true to write, so
|
|
143
|
+
* the tag is inapplicable rather than unmet.
|
|
139
144
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* and neither `CompressionStream` nor `@gjsify/zlib` takes a level, so
|
|
148
|
-
* `gzipDeterministic` cannot ask for one. Ledgered in `status/open-todos.md`:
|
|
149
|
-
* the fix is a capability in the core, and stamping the header's XFL byte to
|
|
150
|
-
* claim max compression we did not use is not one.
|
|
145
|
+
* `changelog-not-compressed-with-max-compression` was a second one, and closing it
|
|
146
|
+
* was a capability gap in the CORE rather than anything wrong in this file:
|
|
147
|
+
* neither `CompressionStream` nor `@gjsify/zlib` would take a compression level,
|
|
148
|
+
* so `gzipDeterministic` had none to pass on. `@gjsify/zlib` honours
|
|
149
|
+
* `options.level` now and `plan.ts` compresses this file at
|
|
150
|
+
* `POLICY_MAX_COMPRESSION`. `verify-deb.sh` gates the tag by name, so it cannot
|
|
151
|
+
* come back quietly.
|
|
151
152
|
*/
|
|
152
153
|
export function renderDebianChangelog(settings, source, mtime) {
|
|
153
154
|
const entries = source === undefined ? [] : changelogEntriesFor(settings.version, source);
|
|
@@ -91,6 +91,26 @@ export interface DependsInputs {
|
|
|
91
91
|
/** Minimum Node major, when one is depended on. Default {@link DEFAULT_NODE_FLOOR}. */
|
|
92
92
|
minNodeVersion?: string;
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* The namespaces the bundle imports that the HOST has to provide — scanned set
|
|
96
|
+
* minus everything the payload carries itself.
|
|
97
|
+
*
|
|
98
|
+
* LIFTED OUT OF {@link deriveDepends} because a second caller appeared and the
|
|
99
|
+
* question is not a `.deb`'s. `appimage.ts` asks it to write the one sentence an
|
|
100
|
+
* AppImage owes its user — *this file carries the application, and takes these
|
|
101
|
+
* from your system* — and an AppImage has no `Depends:` field, so it cannot go
|
|
102
|
+
* through the package-name table below. A copied filter would drift in the
|
|
103
|
+
* direction that is invisible: the drifted copy would still produce a plausible
|
|
104
|
+
* list, just one that no longer matches the dependencies the `.deb` beside it
|
|
105
|
+
* declares from the same payload.
|
|
106
|
+
*
|
|
107
|
+
* Deriving the carried set from the staged FILENAMES is the point, and it is
|
|
108
|
+
* `deriveDepends`' reasoning unchanged: `Gwebgl-0.1.typelib` answers for both
|
|
109
|
+
* `Gwebgl-0.1` and `Gwebgl`, so a project that declared a namespace bundled
|
|
110
|
+
* without the file being there gets an artifact that names its real requirement
|
|
111
|
+
* instead of one that installs and dies at the first import.
|
|
112
|
+
*/
|
|
113
|
+
export declare function hostProvidedNamespaces(namespaces: readonly string[], bundledTypelibs?: readonly string[]): string[];
|
|
94
114
|
/**
|
|
95
115
|
* The `Depends:` / `Requires:` list for one format.
|
|
96
116
|
*
|
|
@@ -256,6 +256,36 @@ const TYPELIB_PACKAGES = {
|
|
|
256
256
|
* whose bundle reaches one, which includes every `@gjsify/http` server.
|
|
257
257
|
*/
|
|
258
258
|
const BUNDLED_TYPELIB = /^Gjsify[A-Z]/;
|
|
259
|
+
/**
|
|
260
|
+
* The namespaces the bundle imports that the HOST has to provide — scanned set
|
|
261
|
+
* minus everything the payload carries itself.
|
|
262
|
+
*
|
|
263
|
+
* LIFTED OUT OF {@link deriveDepends} because a second caller appeared and the
|
|
264
|
+
* question is not a `.deb`'s. `appimage.ts` asks it to write the one sentence an
|
|
265
|
+
* AppImage owes its user — *this file carries the application, and takes these
|
|
266
|
+
* from your system* — and an AppImage has no `Depends:` field, so it cannot go
|
|
267
|
+
* through the package-name table below. A copied filter would drift in the
|
|
268
|
+
* direction that is invisible: the drifted copy would still produce a plausible
|
|
269
|
+
* list, just one that no longer matches the dependencies the `.deb` beside it
|
|
270
|
+
* declares from the same payload.
|
|
271
|
+
*
|
|
272
|
+
* Deriving the carried set from the staged FILENAMES is the point, and it is
|
|
273
|
+
* `deriveDepends`' reasoning unchanged: `Gwebgl-0.1.typelib` answers for both
|
|
274
|
+
* `Gwebgl-0.1` and `Gwebgl`, so a project that declared a namespace bundled
|
|
275
|
+
* without the file being there gets an artifact that names its real requirement
|
|
276
|
+
* instead of one that installs and dies at the first import.
|
|
277
|
+
*/
|
|
278
|
+
export function hostProvidedNamespaces(namespaces, bundledTypelibs = []) {
|
|
279
|
+
const shipped = new Set();
|
|
280
|
+
for (const file of bundledTypelibs) {
|
|
281
|
+
const match = /([A-Za-z0-9]+)-([\d.]+)\.typelib$/.exec(file);
|
|
282
|
+
if (!match)
|
|
283
|
+
continue;
|
|
284
|
+
shipped.add(`${match[1]}-${match[2]}`);
|
|
285
|
+
shipped.add(match[1]);
|
|
286
|
+
}
|
|
287
|
+
return [...new Set(namespaces)].sort().filter((ns) => !BUNDLED_TYPELIB.test(ns) && !shipped.has(ns));
|
|
288
|
+
}
|
|
259
289
|
/**
|
|
260
290
|
* The `Depends:` / `Requires:` list for one format.
|
|
261
291
|
*
|
|
@@ -271,22 +301,7 @@ export function deriveDepends(format, inputs) {
|
|
|
271
301
|
? [`${NODE_PACKAGE[format]} >= ${inputs.minNodeVersion ?? DEFAULT_NODE_FLOOR}`]
|
|
272
302
|
: [`gjs >= ${inputs.minGjsVersion ?? DEFAULT_GJS_FLOOR}`];
|
|
273
303
|
const unmapped = [];
|
|
274
|
-
|
|
275
|
-
// separate declaration: `Gwebgl-0.1.typelib` → `Gwebgl-0.1` and `Gwebgl`. Deriving it is the
|
|
276
|
-
// point — a project that declared a namespace bundled without the file being there would get
|
|
277
|
-
// a package that installs and dies at the first import, which is exactly what this check
|
|
278
|
-
// exists to prevent.
|
|
279
|
-
const shipped = new Set();
|
|
280
|
-
for (const file of inputs.bundledTypelibs ?? []) {
|
|
281
|
-
const match = /([A-Za-z0-9]+)-([\d.]+)\.typelib$/.exec(file);
|
|
282
|
-
if (!match)
|
|
283
|
-
continue;
|
|
284
|
-
shipped.add(`${match[1]}-${match[2]}`);
|
|
285
|
-
shipped.add(match[1]);
|
|
286
|
-
}
|
|
287
|
-
for (const namespace of [...new Set(inputs.namespaces)].sort()) {
|
|
288
|
-
if (BUNDLED_TYPELIB.test(namespace) || shipped.has(namespace))
|
|
289
|
-
continue;
|
|
304
|
+
for (const namespace of hostProvidedNamespaces(inputs.namespaces, inputs.bundledTypelibs)) {
|
|
290
305
|
const entry = lookupTypelib(namespace, inputs.typelibPackages);
|
|
291
306
|
if (entry === undefined) {
|
|
292
307
|
unmapped.push(namespace);
|
|
@@ -138,4 +138,13 @@ export declare function configuredFormats(raw: readonly string[], layout: Layout
|
|
|
138
138
|
* the two questions are separate, and a `.app` is precisely the item Apple's
|
|
139
139
|
* remedy above tells you to staple after submitting the archive around it.
|
|
140
140
|
*/
|
|
141
|
+
/**
|
|
142
|
+
* Refuse a project whose `kind` no chosen container can hold.
|
|
143
|
+
*
|
|
144
|
+
* DERIVED FROM THE TABLE (`FormatDescriptor.requiresDesktopEntry`), never from a
|
|
145
|
+
* list of format ids here — a fourth format that also needs an entry would
|
|
146
|
+
* otherwise be refused by appimagetool's own "Desktop file not found, aborting"
|
|
147
|
+
* after a full build, which is the message this replaced.
|
|
148
|
+
*/
|
|
149
|
+
export declare function assertKindCanPack(formats: readonly FormatDescriptor[], kind: 'app' | 'cli'): void;
|
|
141
150
|
export declare function canCarryTicket(id: FormatId): boolean;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// and one filename. What it also proved is what the design did NOT predict —
|
|
9
9
|
// a format can be host-bound (§ A3), so `host` is a descriptor field now.
|
|
10
10
|
import { isOnPath } from '../check-system-deps.js';
|
|
11
|
+
import { APPIMAGE_TOOL } from './appimage.js';
|
|
11
12
|
import { DMG_TOOL } from './dmg.js';
|
|
12
13
|
import { LAYOUTS, LAYOUT_NAMES } from './layout.js';
|
|
13
14
|
import { SCHEMA_COMPILER, SCHEMA_COMPILER_HINT } from './schemas.js';
|
|
@@ -72,6 +73,33 @@ function rpmArch(arch, archIndependent) {
|
|
|
72
73
|
function flatpakArch(arch, _archIndependent) {
|
|
73
74
|
return lookupArch(FLATPAK_ARCH, arch, 'Flatpak');
|
|
74
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* `process.arch` → the name appimagetool uses, which is `uname -m`'s.
|
|
78
|
+
*
|
|
79
|
+
* A TABLE OF ITS OWN even though it currently equals `RPM_ARCH` on every row it
|
|
80
|
+
* has, and the reason is that the two answer different questions: rpm's is a
|
|
81
|
+
* package-manager namespace with `noarch`, `i686` and `ppc64le` in it, while this
|
|
82
|
+
* one is the string appimagetool puts in `$ARCH` and matches against the runtimes
|
|
83
|
+
* it can embed. Sharing the table would make an rpm row addition silently claim
|
|
84
|
+
* an AppImage architecture that has no runtime — `ppc64le` is exactly that case
|
|
85
|
+
* today. So the short list is the honest one, and an arch not in it is REFUSED
|
|
86
|
+
* rather than guessed, which is `FLATPAK_ARCH`'s rule for `FLATPAK_ARCH`'s reason:
|
|
87
|
+
* the value ends up in a filename a user is asked to download.
|
|
88
|
+
*
|
|
89
|
+
* `archIndependent` is IGNORED, like Flatpak's and unlike deb's and rpm's. There
|
|
90
|
+
* is no `noarch` AppImage: the file BEGINS with an ELF runtime, so a payload of
|
|
91
|
+
* pure JavaScript still ships in an x86-64 executable and labelling it otherwise
|
|
92
|
+
* would name a file nothing can run.
|
|
93
|
+
*/
|
|
94
|
+
const APPIMAGE_ARCH = {
|
|
95
|
+
x64: 'x86_64',
|
|
96
|
+
arm64: 'aarch64',
|
|
97
|
+
ia32: 'i686',
|
|
98
|
+
arm: 'armhf',
|
|
99
|
+
};
|
|
100
|
+
function appImageArch(arch, _archIndependent) {
|
|
101
|
+
return lookupArch(APPIMAGE_ARCH, arch, 'AppImage');
|
|
102
|
+
}
|
|
75
103
|
/**
|
|
76
104
|
* `process.arch` → the name Apple's tools use.
|
|
77
105
|
*
|
|
@@ -344,6 +372,108 @@ export const FORMATS = {
|
|
|
344
372
|
fileName: (s, archLabel) => `${s.appId}-${s.version}-${s.release}.${archLabel}.flatpak`,
|
|
345
373
|
artifactKind: 'file',
|
|
346
374
|
},
|
|
375
|
+
// THE FOURTH CONTAINER OVER THE LINUX TREE, and the first row added to this
|
|
376
|
+
// table that needed NOTHING staged: `deb`, `rpm` and `flatpak` already wrap
|
|
377
|
+
// this exact payload, so ADR 0024 § 2's "one payload, a handful of layouts"
|
|
378
|
+
// is here a row plus a packer and no change to `plan.ts` at all. ADR 0024
|
|
379
|
+
// § 9 deferred it and § A24 lifts the deferral; `appimage.ts`'s header
|
|
380
|
+
// carries the argument, and the short form is that the two promises an
|
|
381
|
+
// AppImage can keep with no relocatable Linux closure — no install, no root
|
|
382
|
+
// — are the two nothing else in this table offers, and the third is
|
|
383
|
+
// DECLARED rather than implied.
|
|
384
|
+
appimage: {
|
|
385
|
+
id: 'appimage',
|
|
386
|
+
layoutOs: 'linux',
|
|
387
|
+
// `/usr`, exactly as the `.deb` — and that is the whole layout difference
|
|
388
|
+
// between the two formats. An AppImage mounts its squashfs and the AppDir's
|
|
389
|
+
// `usr/` becomes the prefix, which the staged launcher computes for itself
|
|
390
|
+
// (ADR 0024 § 3): `readlink -f "$0"` then two `dirname`s, measured inside a
|
|
391
|
+
// mount as `/tmp/.mount_xxxx/usr`. So the same `bin/<name>` works under
|
|
392
|
+
// `/usr`, under `/app` and under a mountpoint nobody chose.
|
|
393
|
+
prefix: '/usr',
|
|
394
|
+
host: {
|
|
395
|
+
// Linux-bound the way the Flatpak row is, and NOT the way the `.dmg`
|
|
396
|
+
// is: the container is an ELF runtime for Linux with a Linux filesystem
|
|
397
|
+
// behind it, so the format is bound the way the application is.
|
|
398
|
+
finishOn: ['linux'],
|
|
399
|
+
// The SCHEMA COMPILER IS ABSENT here, unlike the two windows rows and
|
|
400
|
+
// for the `.dmg`'s reason inverted: this layout HAS an install step in
|
|
401
|
+
// every other format, and inside an AppImage it does not — there is no
|
|
402
|
+
// `postinst` to run `glib-compile-schemas` in a read-only squashfs. But
|
|
403
|
+
// `compileSchemasForStage` has already run at ASSEMBLY time and
|
|
404
|
+
// `gschemas.compiled` is in the payload, so declaring the compiler on
|
|
405
|
+
// the PACK path would refuse a `--from-stage` pack that works.
|
|
406
|
+
requiredTools: [APPIMAGE_TOOL],
|
|
407
|
+
// A URL AND NOT A PACKAGE NAME, and it is the first hint in this table
|
|
408
|
+
// that has to be. `flatpak-builder` and `msitools` are in Fedora and in
|
|
409
|
+
// Debian; `appimagetool` is in neither, so pointing at a distro would
|
|
410
|
+
// send the reader to an `E: Unable to locate package`. The `--stage`
|
|
411
|
+
// half is repeated here because it is the answer for a CI image that is
|
|
412
|
+
// not going to grow a hand-installed binary.
|
|
413
|
+
installHint: 'no distribution packages it — take the release from ' +
|
|
414
|
+
'https://github.com/AppImage/appimagetool/releases, `chmod +x` it and put it on PATH as ' +
|
|
415
|
+
'`appimagetool`; or assemble here with `gjsify ship linux --stage` and pack where it is',
|
|
416
|
+
oracle: {
|
|
417
|
+
// TWO READERS, NEITHER OF THEM appimagetool AND NEITHER OF THEM THE
|
|
418
|
+
// ARTIFACT'S OWN RUNTIME — which is what `--appimage-offset` and
|
|
419
|
+
// `--appimage-extract` would be, the format reading what the format
|
|
420
|
+
// wrote (ADR 0024 § A3).
|
|
421
|
+
//
|
|
422
|
+
// * CPython reads the ELF SECTION-HEADER TABLE for the offset the
|
|
423
|
+
// filesystem starts at — `e_shoff + e_shnum * e_shentsize`,
|
|
424
|
+
// twelve bytes of `struct.unpack_from`, straight out of the ELF
|
|
425
|
+
// specification. Measured against `--appimage-offset` on a real
|
|
426
|
+
// artifact: 944632 both ways. That agreement is the
|
|
427
|
+
// discriminator for the whole container, because a truncated or
|
|
428
|
+
// empty image has no squashfs superblock there. `python3` and
|
|
429
|
+
// NOT `readelf`, which the first draft of this row declared and
|
|
430
|
+
// `verify-appimage.py` never execs — the field names the tools
|
|
431
|
+
// the oracle RUNS, and binutils is not one of them here. Same
|
|
432
|
+
// reader family the two `macos-app` rows already declare.
|
|
433
|
+
// * `unsquashfs` (squashfs-tools) then lists the filesystem at
|
|
434
|
+
// that offset with names, modes and sizes, which
|
|
435
|
+
// `.github/ship-oracle/verify-appimage.py` compares against the
|
|
436
|
+
// stage manifest. A different PROGRAM from the bundled
|
|
437
|
+
// mksquashfs appimagetool packs with, and the leg that runs it
|
|
438
|
+
// is the Linux one that already reads the `.deb`.
|
|
439
|
+
readWith: ['python3', 'unsquashfs'],
|
|
440
|
+
readOn: ['linux'],
|
|
441
|
+
selfReading: false,
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
// No `Depends:` FIELD — which is not the same as no dependencies, and this
|
|
445
|
+
// row is the one place in the table where that distinction has teeth. A
|
|
446
|
+
// `.deb` declares `gir1.2-gtk-4.0` and apt refuses to install without it;
|
|
447
|
+
// an AppImage has nowhere to write that down, so `appImageHostRequirements`
|
|
448
|
+
// derives the same set from the same scan and `ship` PRINTS it while
|
|
449
|
+
// `AppRun` carries the interpreter half as a refusal. See `depends.ts`'s
|
|
450
|
+
// `DistroFormatId`: this is `null` because there is no field, never because
|
|
451
|
+
// there is nothing to say.
|
|
452
|
+
depends: null,
|
|
453
|
+
// BOTH, like `deb` and `rpm` and for the same reason: the interpreter comes
|
|
454
|
+
// from the machine. The difference from those two is that nothing declares
|
|
455
|
+
// it — see the `depends` note above and `interpreterGap` below.
|
|
456
|
+
interpreters: ['gjs', 'node'],
|
|
457
|
+
interpreterGap: 'an AppImage carries the application and takes its interpreter from the host, exactly as a distro ' +
|
|
458
|
+
'package does — but with no dependency field to declare it in, so the requirement is printed at ' +
|
|
459
|
+
'pack time and checked by `AppRun` at launch instead',
|
|
460
|
+
licenseDest: SHARE_LICENSE_DEST,
|
|
461
|
+
licenseKind: 'plain',
|
|
462
|
+
archName: appImageArch,
|
|
463
|
+
// The BINARY name, like the `.dmg` and the two zips and for their reason:
|
|
464
|
+
// this artifact is a DOWNLOAD that lands in a browser's folder beside other
|
|
465
|
+
// files, so it carries the version and the architecture and avoids the
|
|
466
|
+
// spaces a display name may contain. Not the app id, which is the Flatpak's
|
|
467
|
+
// convention because a Flatpak ref IS the id; an AppImage installs as
|
|
468
|
+
// nothing and is addressed by its path.
|
|
469
|
+
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.${archLabel}.AppImage`,
|
|
470
|
+
artifactKind: 'file',
|
|
471
|
+
// THE ONLY ROW THAT SETS IT. appimagetool refuses an AppDir with no
|
|
472
|
+
// `.desktop` at its root, and `kind: 'cli'` stages none by design — a
|
|
473
|
+
// refusal `gjsify.ship.kind` decides on its own, so it belongs before the
|
|
474
|
+
// build beside the tool gate rather than after it inside the packer.
|
|
475
|
+
requiresDesktopEntry: true,
|
|
476
|
+
},
|
|
347
477
|
// ── macOS (#1354 M2a) ────────────────────────────────────────────────
|
|
348
478
|
//
|
|
349
479
|
// TWO ROWS FOR ONE TREE, and the pair is what `layoutOs` was added for. The
|
|
@@ -450,7 +580,21 @@ export const FORMATS = {
|
|
|
450
580
|
// is a download: it lands in a browser's downloads folder beside other
|
|
451
581
|
// files, so it carries the version and the arch, and it avoids the spaces
|
|
452
582
|
// a display name may contain.
|
|
453
|
-
|
|
583
|
+
//
|
|
584
|
+
// AND THE OPERATING SYSTEM, which the extension does not name. Every other
|
|
585
|
+
// row's suffix names exactly one platform — `.deb`, `.rpm`, `.flatpak`,
|
|
586
|
+
// `.dmg`, `.msi` — and `.zip` names none, so this row and `windows-dir-zip`
|
|
587
|
+
// are the only two artifacts in the table a user cannot tell apart by
|
|
588
|
+
// looking at them. Before this token they were separated by nothing but a
|
|
589
|
+
// coincidence between two unrelated arch tables (`MACOS_ARCH` maps x64 to
|
|
590
|
+
// `x86_64`, `WINDOWS_ARCH` to `x64`), which put a bare `…-1.arm64.zip` on a
|
|
591
|
+
// release page beside a Windows zip with no way to choose — and would have
|
|
592
|
+
// had the two formats silently overwrite each other in `ship/out/` the day
|
|
593
|
+
// a Windows/arm64 row became possible, an upstream blocker (#1117) being
|
|
594
|
+
// all that stood in the way. `macos`, not `darwin`, for the same reason
|
|
595
|
+
// `MACOS_ARCH` writes `x86_64` rather than `x64`: this label is read by
|
|
596
|
+
// whoever downloads the file, never by `process.platform`.
|
|
597
|
+
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.macos.${archLabel}.zip`,
|
|
454
598
|
artifactKind: 'file',
|
|
455
599
|
},
|
|
456
600
|
// ── macOS (#1354 M4) ─────────────────────────────────────────────────
|
|
@@ -661,7 +805,14 @@ export const FORMATS = {
|
|
|
661
805
|
// pair splits them: this one is a download that lands in a browser's
|
|
662
806
|
// folder beside other files, so it carries the version and the arch and
|
|
663
807
|
// avoids the spaces a display name may contain.
|
|
664
|
-
|
|
808
|
+
//
|
|
809
|
+
// …and the OS, for the reason spelled out on the `macos-app-zip` row it was
|
|
810
|
+
// copied from: `.zip` is the one container this table puts on more than one
|
|
811
|
+
// operating system, so it is the one name that has to say which. `windows`
|
|
812
|
+
// rather than `win32` — the spelling is the user's, like the `x64` this row
|
|
813
|
+
// keeps because Node's own archives, gvsbuild's assets and the runner label
|
|
814
|
+
// all use it.
|
|
815
|
+
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.windows.${archLabel}.zip`,
|
|
665
816
|
artifactKind: 'file',
|
|
666
817
|
},
|
|
667
818
|
// ── The Windows installer (#1354 M5) ─────────────────────────────────
|
|
@@ -974,6 +1125,25 @@ export function configuredFormats(raw, layout) {
|
|
|
974
1125
|
* the two questions are separate, and a `.app` is precisely the item Apple's
|
|
975
1126
|
* remedy above tells you to staple after submitting the archive around it.
|
|
976
1127
|
*/
|
|
1128
|
+
/**
|
|
1129
|
+
* Refuse a project whose `kind` no chosen container can hold.
|
|
1130
|
+
*
|
|
1131
|
+
* DERIVED FROM THE TABLE (`FormatDescriptor.requiresDesktopEntry`), never from a
|
|
1132
|
+
* list of format ids here — a fourth format that also needs an entry would
|
|
1133
|
+
* otherwise be refused by appimagetool's own "Desktop file not found, aborting"
|
|
1134
|
+
* after a full build, which is the message this replaced.
|
|
1135
|
+
*/
|
|
1136
|
+
export function assertKindCanPack(formats, kind) {
|
|
1137
|
+
if (kind === 'app')
|
|
1138
|
+
return;
|
|
1139
|
+
const needsEntry = formats.filter((format) => format.requiresDesktopEntry);
|
|
1140
|
+
if (needsEntry.length === 0)
|
|
1141
|
+
return;
|
|
1142
|
+
throw new Error(`gjsify ship: ${needsEntry.map((format) => format.id).join(', ')} cannot package a ` +
|
|
1143
|
+
'`kind: "cli"` project — the container needs a desktop entry at its root and a CLI stages none ' +
|
|
1144
|
+
'by design. Set `gjsify.ship.kind` to "app" if this is a GUI application, or ship it as a `.deb`, ' +
|
|
1145
|
+
'an `.rpm` or a Flatpak, none of which need one.');
|
|
1146
|
+
}
|
|
977
1147
|
export function canCarryTicket(id) {
|
|
978
1148
|
switch (id) {
|
|
979
1149
|
case 'macos-app':
|
|
@@ -986,6 +1156,7 @@ export function canCarryTicket(id) {
|
|
|
986
1156
|
case 'deb':
|
|
987
1157
|
case 'rpm':
|
|
988
1158
|
case 'flatpak':
|
|
1159
|
+
case 'appimage':
|
|
989
1160
|
case 'windows-dir':
|
|
990
1161
|
case 'windows-dir-zip':
|
|
991
1162
|
case 'msi':
|
package/lib/utils/ship/gzip.d.ts
CHANGED
|
@@ -1,2 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* gzip with the header's timestamp zeroed and the OS byte pinned to Unix.
|
|
3
|
+
*
|
|
4
|
+
* @param level 0-9, or omitted for zlib's default. THE ONLY HONEST WAY TO SET XFL:
|
|
5
|
+
* byte 8 of the header records how hard the compressor tried (2 = maximum,
|
|
6
|
+
* 4 = fastest, 0 = neither), `lintian` reads it to raise
|
|
7
|
+
* `changelog-not-compressed-with-max-compression`, and `file` prints it as
|
|
8
|
+
* "max compression". Stamping that byte here beside the mtime and OS bytes would
|
|
9
|
+
* be a different thing entirely and was rejected for it: mtime and OS are facts
|
|
10
|
+
* about the build ENVIRONMENT, which a reproducible build is entitled to
|
|
11
|
+
* normalise, while XFL is a statement about the compression actually performed.
|
|
12
|
+
* Writing a 2 we had not earned would make the artifact lie to the tool reading
|
|
13
|
+
* it. So the level travels down to the compressor instead.
|
|
14
|
+
*
|
|
15
|
+
* AND THE TEMPTATION IS REAL, which is why this is worth writing down: measured
|
|
16
|
+
* on gjsify's own `.deb`, the changelog member compressed at the default level
|
|
17
|
+
* and at 9 differs in EXACTLY ONE BYTE — position 9, XFL, 0 against 2 — and is
|
|
18
|
+
* 1152 bytes either way. For this input, stamping the byte would have produced
|
|
19
|
+
* the identical artifact. It is identical by coincidence of a small input: over
|
|
20
|
+
* this repo's full `CHANGELOG.md` the same two levels differ by thousands of
|
|
21
|
+
* bytes. A rule that happens to hold on today's input is not the rule.
|
|
22
|
+
*/
|
|
23
|
+
export declare function gzipDeterministic(input: Uint8Array, level?: number): Promise<Uint8Array>;
|
|
24
|
+
/**
|
|
25
|
+
* `gzip -9`, the level Debian Policy § 4.4 asks of a packaging changelog.
|
|
26
|
+
*
|
|
27
|
+
* Named rather than spelled `9` at the call site so the reason travels with the
|
|
28
|
+
* number: the `-n` half of Policy's `gzip -9 -n` is what {@link gzipDeterministic}
|
|
29
|
+
* already does to bytes 4-7.
|
|
30
|
+
*/
|
|
31
|
+
export declare const POLICY_MAX_COMPRESSION = 9;
|
package/lib/utils/ship/gzip.js
CHANGED
|
@@ -5,10 +5,54 @@
|
|
|
5
5
|
// input produce different bytes — which no test notices, because every test
|
|
6
6
|
// compares the DECOMPRESSED content. It is the single most-missed
|
|
7
7
|
// reproducibility bug in package writers, and it costs nothing to close.
|
|
8
|
+
//
|
|
9
|
+
// WHAT "DETERMINISTIC" DOES NOT COVER, measured 2026-09-11 and named here because
|
|
10
|
+
// the function's own name overstates it: the bytes are reproducible for a given
|
|
11
|
+
// HOST, not for a given artifact. `@gjsify/tar`'s gzip runs on the platform's
|
|
12
|
+
// zlib, and the platforms do not ship the same one — Fedora's libz is
|
|
13
|
+
// `zlib-ng-compat` (2.3.3) while Node bundles its own zlib (1.3.2.1-motley). Over
|
|
14
|
+
// this repo's `CHANGELOG.md` (876 192 bytes) at the DEFAULT level they produce
|
|
15
|
+
// 272 003 and 272 000 bytes respectively, so a `.deb` packed under GJS and one
|
|
16
|
+
// packed under Node already differ, today, before any level argument exists.
|
|
17
|
+
// Levels 1-4 differ too, in the other direction (zlib-ng is smaller). Two
|
|
18
|
+
// consequences worth keeping:
|
|
19
|
+
//
|
|
20
|
+
// * `tests/e2e/ship-from-stage` asserts byte-equality between a direct pack and
|
|
21
|
+
// a `--from-stage` pack, and it holds because both run on ONE host. It is
|
|
22
|
+
// structurally blind to this, and a cross-host pack is exactly what
|
|
23
|
+
// `--from-stage` exists to enable.
|
|
24
|
+
// * zlib-ng's level 9 is WORSE than its level 8 on large inputs — 277 974 vs
|
|
25
|
+
// 270 255 bytes on the file above, ~2.9 % bigger — which is why `level: 9` is
|
|
26
|
+
// asked for only where a reader demands it (see below) and not blanket-applied
|
|
27
|
+
// to the payload tarballs.
|
|
28
|
+
//
|
|
29
|
+
// Ledgered in `status/open-todos.md`; closing it means pinning ONE deflate
|
|
30
|
+
// implementation, which is a larger decision than this file.
|
|
8
31
|
import { gzip } from '@gjsify/tar';
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
32
|
+
/**
|
|
33
|
+
* gzip with the header's timestamp zeroed and the OS byte pinned to Unix.
|
|
34
|
+
*
|
|
35
|
+
* @param level 0-9, or omitted for zlib's default. THE ONLY HONEST WAY TO SET XFL:
|
|
36
|
+
* byte 8 of the header records how hard the compressor tried (2 = maximum,
|
|
37
|
+
* 4 = fastest, 0 = neither), `lintian` reads it to raise
|
|
38
|
+
* `changelog-not-compressed-with-max-compression`, and `file` prints it as
|
|
39
|
+
* "max compression". Stamping that byte here beside the mtime and OS bytes would
|
|
40
|
+
* be a different thing entirely and was rejected for it: mtime and OS are facts
|
|
41
|
+
* about the build ENVIRONMENT, which a reproducible build is entitled to
|
|
42
|
+
* normalise, while XFL is a statement about the compression actually performed.
|
|
43
|
+
* Writing a 2 we had not earned would make the artifact lie to the tool reading
|
|
44
|
+
* it. So the level travels down to the compressor instead.
|
|
45
|
+
*
|
|
46
|
+
* AND THE TEMPTATION IS REAL, which is why this is worth writing down: measured
|
|
47
|
+
* on gjsify's own `.deb`, the changelog member compressed at the default level
|
|
48
|
+
* and at 9 differs in EXACTLY ONE BYTE — position 9, XFL, 0 against 2 — and is
|
|
49
|
+
* 1152 bytes either way. For this input, stamping the byte would have produced
|
|
50
|
+
* the identical artifact. It is identical by coincidence of a small input: over
|
|
51
|
+
* this repo's full `CHANGELOG.md` the same two levels differ by thousands of
|
|
52
|
+
* bytes. A rule that happens to hold on today's input is not the rule.
|
|
53
|
+
*/
|
|
54
|
+
export async function gzipDeterministic(input, level) {
|
|
55
|
+
const out = await gzip(input, level === undefined ? undefined : { level });
|
|
12
56
|
if (out.byteLength < 10 || out[0] !== 0x1f || out[1] !== 0x8b) {
|
|
13
57
|
throw new Error('gjsify ship: internal error — gzip did not produce a gzip stream.');
|
|
14
58
|
}
|
|
@@ -19,3 +63,11 @@ export async function gzipDeterministic(input) {
|
|
|
19
63
|
out[9] = 3;
|
|
20
64
|
return out;
|
|
21
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* `gzip -9`, the level Debian Policy § 4.4 asks of a packaging changelog.
|
|
68
|
+
*
|
|
69
|
+
* Named rather than spelled `9` at the call site so the reason travels with the
|
|
70
|
+
* number: the `-n` half of Policy's `gzip -9 -n` is what {@link gzipDeterministic}
|
|
71
|
+
* already does to bytes 4-7.
|
|
72
|
+
*/
|
|
73
|
+
export const POLICY_MAX_COMPRESSION = 9;
|
package/lib/utils/ship/plan.d.ts
CHANGED
|
@@ -67,11 +67,39 @@ export declare function planOverlay(settings: ShipSettings, format: FormatDescri
|
|
|
67
67
|
*/
|
|
68
68
|
export declare function assertOverlayIsLicensed(formatId: FormatId, binaryName: string, overlay: readonly StagedFile[]): void;
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* Whether an icon belongs in the theme's SYMBOLIC context rather than at a size.
|
|
71
|
+
*
|
|
72
|
+
* `symbolic` is a directory of its own in a hicolor theme and NOT a size value.
|
|
73
|
+
* Measured in `refs/adwaita-icon-theme/index.theme`, which lists `symbolic/apps`
|
|
74
|
+
* in `Directories=` beside `scalable/apps` and `16x16/apps` and gives it its own
|
|
75
|
+
* section — `Context=Applications`, `Size=16`, `MinSize=8`, `MaxSize=512`,
|
|
76
|
+
* `Type=Scalable` — against the scalable row's `Size=128`. The freedesktop icon
|
|
77
|
+
* theme specification has no notion of `symbolic` at all: a theme expresses it by
|
|
78
|
+
* giving the directory a section, which is exactly why reading the EXTENSION
|
|
79
|
+
* cannot see it, and why this question has to be asked before the size one.
|
|
80
|
+
*
|
|
81
|
+
* TWO SIGNALS, the same pair `iconThemeDir` already reads for a size: the
|
|
82
|
+
* directory an author put the file in, and the name they gave it. GTK resolves a
|
|
83
|
+
* symbolic icon by the `-symbolic` name suffix, so an author who wrote the name
|
|
84
|
+
* has said as much as one who wrote the path.
|
|
85
|
+
*
|
|
86
|
+
* SVG ONLY, because the symbolic directory is declared `Type=Scalable`: the
|
|
87
|
+
* raster form GTK's own `gtk-encode-symbolic-svg` produces
|
|
88
|
+
* (`<name>-symbolic.symbolic.png`) is installed at a SIZE instead. So a PNG keeps
|
|
89
|
+
* answering the size question, and one that cannot answer it stays refused rather
|
|
90
|
+
* than quietly becoming a scalable icon that does not scale.
|
|
91
|
+
*/
|
|
92
|
+
export declare function isSymbolicIcon(iconPath: string): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The hicolor subdirectory for an icon: the `symbolic` CONTEXT, else `scalable`
|
|
95
|
+
* for an SVG, else the pixel size read from a `<n>x<n>` path component or a
|
|
96
|
+
* trailing number in the filename.
|
|
97
|
+
*
|
|
98
|
+
* Named for the theme DIRECTORY rather than a size from the moment the first of
|
|
99
|
+
* those answers stopped being one: `iconSizeDir` returning `symbolic` would be a
|
|
100
|
+
* name every caller has to read past.
|
|
73
101
|
*/
|
|
74
|
-
export declare function
|
|
102
|
+
export declare function iconThemeDir(iconPath: string): string;
|
|
75
103
|
/**
|
|
76
104
|
* A native module inside the payload has to keep its executable bit.
|
|
77
105
|
*
|