@gjsify/cli 0.51.0 → 0.52.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.
Files changed (66) hide show
  1. package/dist/cli.gjs.mjs +505 -490
  2. package/lib/cli-app.js +3 -1
  3. package/lib/commands/affected-classify.d.ts +7 -1
  4. package/lib/commands/affected-classify.js +66 -9
  5. package/lib/commands/affected-script-refs.d.ts +20 -0
  6. package/lib/commands/affected-script-refs.js +218 -0
  7. package/lib/commands/affected.js +24 -2
  8. package/lib/commands/flatpak/diff.js +8 -2
  9. package/lib/commands/flatpak/init.js +4 -1
  10. package/lib/commands/flatpak/sources.js +4 -3
  11. package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
  12. package/lib/commands/flatpak/sync-flathub.js +149 -7
  13. package/lib/commands/flatpak/utils.d.ts +10 -0
  14. package/lib/commands/flatpak/utils.js +10 -0
  15. package/lib/commands/index.d.ts +1 -0
  16. package/lib/commands/index.js +1 -0
  17. package/lib/commands/install.js +76 -0
  18. package/lib/commands/link.d.ts +13 -0
  19. package/lib/commands/link.js +243 -0
  20. package/lib/commands/onboard.js +18 -2
  21. package/lib/commands/publish.d.ts +34 -0
  22. package/lib/commands/publish.js +153 -8
  23. package/lib/commands/ship.js +46 -4
  24. package/lib/types/config-data.d.ts +1 -1
  25. package/lib/utils/check-system-deps.d.ts +1 -17
  26. package/lib/utils/check-system-deps.js +10 -68
  27. package/lib/utils/dev-link.d.ts +222 -0
  28. package/lib/utils/dev-link.js +632 -0
  29. package/lib/utils/hang-watchdog.d.ts +95 -0
  30. package/lib/utils/hang-watchdog.js +194 -0
  31. package/lib/utils/install-backend-native.d.ts +14 -0
  32. package/lib/utils/install-backend-native.js +91 -13
  33. package/lib/utils/install-extraneous.d.ts +90 -0
  34. package/lib/utils/install-extraneous.js +120 -0
  35. package/lib/utils/install-tarball-cache.d.ts +37 -9
  36. package/lib/utils/install-tarball-cache.js +59 -14
  37. package/lib/utils/normalize-bundler-options.js +11 -0
  38. package/lib/utils/publish-pins.d.ts +38 -0
  39. package/lib/utils/publish-pins.js +47 -0
  40. package/lib/utils/publish-readback.d.ts +103 -17
  41. package/lib/utils/publish-readback.js +326 -47
  42. package/lib/utils/run-gjs.js +82 -6
  43. package/lib/utils/ship/appimage.d.ts +24 -4
  44. package/lib/utils/ship/appimage.js +51 -6
  45. package/lib/utils/ship/formats.js +44 -11
  46. package/lib/utils/ship/icns.d.ts +14 -0
  47. package/lib/utils/ship/icns.js +83 -0
  48. package/lib/utils/ship/ico.d.ts +25 -0
  49. package/lib/utils/ship/ico.js +120 -0
  50. package/lib/utils/ship/icons.d.ts +191 -0
  51. package/lib/utils/ship/icons.js +398 -0
  52. package/lib/utils/ship/launcher.js +16 -1
  53. package/lib/utils/ship/layout.d.ts +32 -0
  54. package/lib/utils/ship/layout.js +112 -38
  55. package/lib/utils/ship/msi.d.ts +1 -1
  56. package/lib/utils/ship/msi.js +30 -5
  57. package/lib/utils/ship/pe-launcher.d.ts +18 -0
  58. package/lib/utils/ship/pe-launcher.js +125 -4
  59. package/lib/utils/ship/plan.d.ts +1 -34
  60. package/lib/utils/ship/plan.js +7 -56
  61. package/lib/utils/ship/plist.d.ts +20 -10
  62. package/lib/utils/ship/plist.js +29 -12
  63. package/lib/utils/ship/schemas.d.ts +46 -0
  64. package/lib/utils/ship/schemas.js +92 -11
  65. package/lib/utils/ship/settings.js +3 -2
  66. package/package.json +20 -20
@@ -99,6 +99,7 @@ import { describeExit, spawnToCompletion } from '../spawn.js';
99
99
  import { encodeUtf8 } from './bytes.js';
100
100
  import { DEFAULT_GJS_FLOOR, DEFAULT_NODE_FLOOR, hostProvidedNamespaces } from './depends.js';
101
101
  import { LAYOUTS } from './layout.js';
102
+ import { isSchemaSource, SCHEMA_CACHE } from './schemas.js';
102
103
  import { SHARE } from './share-dirs.js';
103
104
  /** The tool this packer execs. One spelling, read by this module AND by the format row. */
104
105
  export const APPIMAGE_TOOL = 'appimagetool';
@@ -234,6 +235,17 @@ export function interpreterFloor(input) {
234
235
  * drifts is the one nothing tests: `tests/e2e/ship-layout` compares the staged
235
236
  * trees, not this script.
236
237
  *
238
+ * THAT CLAIM WAS RE-MEASURED AGAINST THE ARTIFACT and it holds — which is worth
239
+ * writing down, because when Learn6502 0.8.0's AppImage died on `GSettings schema
240
+ * … not found` this script was the obvious suspect and adding
241
+ * `GSETTINGS_SCHEMA_DIR` here was the obvious fix. It would have changed nothing.
242
+ * Measured on the real 0.8.0 image, by compiling `gschemas.compiled` into the
243
+ * extracted AppDir and touching NO environment variable anywhere: the application
244
+ * started. The launcher's `XDG_DATA_DIRS` was already pointing GSettings at the
245
+ * right directory; the directory had no cache in it. An extra export would have
246
+ * been a second name for a path that was already correct, and it would have
247
+ * "fixed" the bug by sitting next to the change that did.
248
+ *
237
249
  * WHAT IT DOES ADD is the refusal ADR 0024 § 9 asks for. An AppImage whose
238
250
  * interpreter is missing exits with the loader's own message — `gjs: not found`
239
251
  * from a shell, or nothing at all when a desktop launches it — and the user
@@ -435,7 +447,7 @@ function rootIconRank(path, appId) {
435
447
  * those two are separate from each other (ADR 0024 § A3): three failures, three
436
448
  * different fixes — a different machine, a package, or a line in package.json.
437
449
  */
438
- export function assertAppImageIsPackable(payload, settings) {
450
+ export function assertAppImageIsPackable(payload, settings, schemaCache) {
439
451
  const desktopEntry = `${SHARE.applications}/${settings.appId}.desktop`;
440
452
  if (!payload.some((entry) => entry.path === desktopEntry)) {
441
453
  throw new Error(`gjsify ship: an AppImage needs a desktop entry and this payload has no ${desktopEntry}. ` +
@@ -450,10 +462,40 @@ export function assertAppImageIsPackable(payload, settings) {
450
462
  'file keeping its own basename is an icon nothing looks up — point `gjsify.ship.icon` at a PNG ' +
451
463
  'or an SVG and it is renamed into the theme for you.');
452
464
  }
465
+ // THE THIRD REFUSAL, and the only one whose failure is invisible until a user
466
+ // double-clicks the file. The two above are decided by `package.json` and
467
+ // appimagetool would have caught both eventually; this one is decided by the
468
+ // PACK PATH and nothing catches it — the image builds, the oracle reads it
469
+ // back, every listing agrees, and the app dies at its first
470
+ // `Gio.Settings.new()` because `share/glib-2.0/schemas` holds a source with no
471
+ // compiled cache beside it. That is exactly what Learn6502 0.8.0 shipped.
472
+ //
473
+ // A GUARD ON THE PURE FUNCTION rather than a step in `packOne`, because this
474
+ // is the INVARIANT and not the procedure: whoever assembles an AppDir from a
475
+ // payload carrying schema sources owes it a cache, and an assembler that
476
+ // forgets must not be able to produce a tree. It is also what keeps the e2e
477
+ // honest — the suite calls this function to build its own AppDir, so it
478
+ // inherits the refusal instead of quietly modelling the defect.
479
+ if (schemaCache === undefined && payload.some((entry) => isSchemaSource(entry.path))) {
480
+ throw new Error(`gjsify ship: this payload stages GSettings schema sources under ${SHARE.schemas}/ and no ` +
481
+ `${SCHEMA_CACHE} was compiled for it. An AppImage has NO INSTALL STEP — that is the whole ` +
482
+ 'point of the format — so the `.deb`/`.rpm` postinst that compiles the system schema ' +
483
+ 'directory never runs, and GSettings aborts on a directory holding only sources. The AppDir ' +
484
+ `must carry ${SCHEMA_CACHE}; \`compileSchemasForPayload\` produces it from this payload.`);
485
+ }
453
486
  }
454
487
  /**
455
- * The whole AppDir as one payload: the prefix under `usr/`, plus the three files
456
- * at its root.
488
+ * The whole AppDir as one payload: the prefix under `usr/`, plus the compiled
489
+ * schema cache, plus the three files at its root.
490
+ *
491
+ * THE SCHEMA CACHE IS AN ARGUMENT AND NOT A COMPILE, so this function stays pure
492
+ * and so the one impure step in the AppDir has exactly one home (`packOne`). It
493
+ * is prefix-relative on the way in — `share/glib-2.0/schemas/gschemas.compiled`,
494
+ * the same shape every other entry has — and gets the `usr/` prefix from the same
495
+ * map below, because a second spelling of that prefix is the thing this packer
496
+ * spent a comment explaining it must not have. `undefined` is legal and means the
497
+ * payload has no schemas at all; a payload that HAS them and no cache is refused
498
+ * by {@link assertAppImageIsPackable}, not silently packed.
457
499
  *
458
500
  * PURE, AND ONE LIST RATHER THAN A WRITER, which is the shape `msi.ts` and
459
501
  * `dmg.ts` already have — `packOne` owns the single `writePayload` call for every
@@ -471,15 +513,18 @@ export function assertAppImageIsPackable(payload, settings) {
471
513
  * its schemas one directory too high — at exit 0, until the first
472
514
  * `Gio.Settings.new()`.
473
515
  */
474
- export function appDirPayload(settings, payload, hostRequirements) {
475
- assertAppImageIsPackable(payload, settings);
516
+ export function appDirPayload(settings, payload, hostRequirements, schemaCache) {
517
+ assertAppImageIsPackable(payload, settings, schemaCache);
476
518
  // Non-null by construction: the assertion above refuses a payload with
477
519
  // neither, and both reads are the SAME selection it made.
478
520
  const icon = selectAppDirIcon(payload, settings.appId);
479
521
  const desktopPath = `${SHARE.applications}/${settings.appId}.desktop`;
480
522
  const desktop = payload.find((entry) => entry.path === desktopPath);
481
523
  return [
482
- ...payload.map((entry) => ({ ...entry, path: `${APPDIR_PREFIX_DIR}/${entry.path}` })),
524
+ ...[...payload, ...(schemaCache === undefined ? [] : [schemaCache])].map((entry) => ({
525
+ ...entry,
526
+ path: `${APPDIR_PREFIX_DIR}/${entry.path}`,
527
+ })),
483
528
  { path: APPRUN_NAME, mode: 0o755, data: encodeUtf8(renderAppRun(settings, hostRequirements)) },
484
529
  // THE SAME BYTES, TWICE, and that is the AppImage specification rather
485
530
  // than a duplication to lift: the entry under `usr/share/applications` is
@@ -396,23 +396,56 @@ export const FORMATS = {
396
396
  // is: the container is an ELF runtime for Linux with a Linux filesystem
397
397
  // behind it, so the format is bound the way the application is.
398
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],
399
+ // THE SCHEMA COMPILER IS DECLARED HERE, and this row is the one place
400
+ // in the table where it sits on a PACK path. It used to be absent, on
401
+ // an argument that read well and was false: "this layout has an install
402
+ // step in every other format, and `compileSchemasForStage` has already
403
+ // run at assembly time, so declaring the compiler here would refuse a
404
+ // `--from-stage` pack that works." The second clause never held.
405
+ // `compileSchemasForStage` is skipped for `layout.os === 'linux'` —
406
+ // deliberately, because the Linux stage is SHARED with the `.deb` and
407
+ // the `.rpm`, whose `share/glib-2.0/schemas` is the SYSTEM directory
408
+ // that a postinst compiles and that our cache must never be installed
409
+ // over. So no Linux stage carries `gschemas.compiled`, a `--from-stage`
410
+ // AppImage pack has to produce it, and this row needs the tool.
411
+ //
412
+ // MEASURED, not reasoned: Learn6502 0.8.0's first CI-built AppImage
413
+ // shipped `eu.jumplink.Learn6502.gschema.xml` with nothing beside it
414
+ // and died at `Gio.Settings.new()` on the first line it ran.
415
+ //
416
+ // AND ONLY THIS ONE OF THE FOUR, which is the question the fix had to
417
+ // answer next and which is measured rather than argued.
418
+ // `cacheRefreshCommands` (`scripts.ts`) names four install-time steps,
419
+ // and an AppImage runs none of them. Three DEGRADE and one ABORTS:
420
+ //
421
+ // * `glib-compile-schemas` — ABORTS. GSettings refuses a schema
422
+ // directory holding a source with no cache; there is no fallback.
423
+ // * `gtk-update-icon-cache` — does not. Measured against the rebuilt
424
+ // 0.8.0 image on GTK 4 / glib 2.88.3: with `XDG_DATA_DIRS` pointing
425
+ // at the mounted `usr/share` and no `icon-theme.cache` anywhere,
426
+ // `Gtk.IconTheme.has_icon('eu.jumplink.Learn6502')` is `true` — and
427
+ // `false` for a name that is not there, so the probe discriminates.
428
+ // GTK scans the directory; the cache is a lookup optimisation.
429
+ // * `update-desktop-database`, `update-mime-database` — about a
430
+ // SYSTEM database an install writes into. An AppImage is not
431
+ // installed, so there is nothing for them to update: what a desktop
432
+ // reads is the entry at the AppDir root, via the runtime's own
433
+ // integration.
434
+ //
435
+ // `share/locale` needs no step on any layout — the launcher exports
436
+ // `GJSIFY_LOCALE_DIR` and gettext reads the `.mo` directly.
437
+ requiredTools: [APPIMAGE_TOOL, SCHEMA_COMPILER],
407
438
  // A URL AND NOT A PACKAGE NAME, and it is the first hint in this table
408
439
  // that has to be. `flatpak-builder` and `msitools` are in Fedora and in
409
440
  // Debian; `appimagetool` is in neither, so pointing at a distro would
410
441
  // send the reader to an `E: Unable to locate package`. The `--stage`
411
442
  // 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 ' +
443
+ // not going to grow a hand-installed binary. The compiler IS packaged,
444
+ // so its half of the hint is the ordinary one.
445
+ installHint: 'no distribution packages appimagetool — take the release from ' +
414
446
  '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',
447
+ '`appimagetool`; or assemble here with `gjsify ship linux --stage` and pack where it is. ' +
448
+ `The schema compiler is packaged: ${SCHEMA_COMPILER_HINT}`,
416
449
  oracle: {
417
450
  // TWO READERS, NEITHER OF THEM appimagetool AND NEITHER OF THEM THE
418
451
  // ARTIFACT'S OWN RUNTIME — which is what `--appimage-offset` and
@@ -0,0 +1,14 @@
1
+ import type { AppIconRasters } from './icons.js';
2
+ /** One element of the container: Apple's type code and the pixel size of the PNG it holds. */
3
+ export interface IcnsElement {
4
+ readonly type: string;
5
+ readonly size: number;
6
+ /** The `.iconset` filename `iconutil` gives this element — the reader's spelling, for messages. */
7
+ readonly iconset: string;
8
+ }
9
+ /** The ten PNG-backed element types of a modern application icon, in `iconutil`'s order. */
10
+ export declare const ICNS_ELEMENTS: readonly IcnsElement[];
11
+ /** The distinct pixel sizes the ten elements need — what the layout asks the raster step for. */
12
+ export declare const ICNS_SIZES: readonly number[];
13
+ /** The `.icns` file, `TOC ` first, then the ten elements in {@link ICNS_ELEMENTS} order. */
14
+ export declare function buildIcns(icon: AppIconRasters): Uint8Array;
@@ -0,0 +1,83 @@
1
+ // The macOS icon container — what `iconutil -c icns` produces from an
2
+ // `.iconset`, written here from the same rasters the Windows icon uses.
3
+ //
4
+ // THE FORMAT is a big-endian tagged container: an 8-byte header (`icns`, total
5
+ // length), then elements, each an 8-byte head (4-char type, length INCLUDING the
6
+ // head) and its data. Since Mac OS X 10.7 an element's data may be a whole PNG
7
+ // file, one element type per (size, scale) pair — which is what lets this file
8
+ // be written with no image encoder of its own.
9
+ //
10
+ // THE ELEMENT TABLE is Apple's iconset naming turned into type codes, and it is
11
+ // cited to two readers rather than typed from memory: Pillow's
12
+ // `IcnsImagePlugin.SIZES` (12.3) maps exactly these ten types to these (size,
13
+ // scale) pairs, and `iconutil` on macOS 15 — see the report that lands this file
14
+ // — accepts the result. `icp4`/`icp5` (16 and 32 at 1x) are the two Pillow's
15
+ // WRITER omits and its READER accepts; they are included because Apple's own
16
+ // `iconutil` writes them for `icon_16x16.png` and `icon_32x32.png`, and the Finder
17
+ // list view and the Dock's smallest sizes are drawn from exactly those two.
18
+ //
19
+ // TEN ELEMENTS OVER SEVEN SIZES: a `@2x` element and a 1x element of twice the
20
+ // point size hold the SAME pixels (`ic11` = 16@2x and `icp5` = 32@1x are both 32 px
21
+ // PNGs), so the raster map is asked for seven sizes and three of them are written
22
+ // twice. Writing all ten rather than the seven distinct sizes is what makes the
23
+ // file complete in the Finder's terms: it looks up by (size, scale), not by pixel
24
+ // count.
25
+ //
26
+ // `TOC ` FIRST, as `iconutil` and Pillow both write it: an optional index of the
27
+ // elements that follow, which lets a reader seek without walking. Optional to
28
+ // read, cheap to write, and its absence is the first thing a strict reader
29
+ // notices.
30
+ //
31
+ // WHAT NO LINUX READER HERE CAN SAY. `icns2png` (libicns 0.8.1) parses `ic08`,
32
+ // `ic09` and `ic10` and refuses the other seven types by name — measured on this
33
+ // workstation, so it reads three of ten and is not an oracle for the file. Pillow
34
+ // reads all ten and is NOT in the CI image. `verify-app-plist.py` therefore reads
35
+ // the container with CPython `struct` and checks each PNG's IHDR against the type's
36
+ // size, which is one independent family; the `iconutil` run on macOS is the
37
+ // reader with authority, and it is a VM measurement, not a CI leg.
38
+ import { concatBytes } from './bytes.js';
39
+ import { readPngSize } from './icons.js';
40
+ /** The ten PNG-backed element types of a modern application icon, in `iconutil`'s order. */
41
+ export const ICNS_ELEMENTS = [
42
+ { type: 'icp4', size: 16, iconset: 'icon_16x16.png' },
43
+ { type: 'ic11', size: 32, iconset: 'icon_16x16@2x.png' },
44
+ { type: 'icp5', size: 32, iconset: 'icon_32x32.png' },
45
+ { type: 'ic12', size: 64, iconset: 'icon_32x32@2x.png' },
46
+ { type: 'ic07', size: 128, iconset: 'icon_128x128.png' },
47
+ { type: 'ic13', size: 256, iconset: 'icon_128x128@2x.png' },
48
+ { type: 'ic08', size: 256, iconset: 'icon_256x256.png' },
49
+ { type: 'ic14', size: 512, iconset: 'icon_256x256@2x.png' },
50
+ { type: 'ic09', size: 512, iconset: 'icon_512x512.png' },
51
+ { type: 'ic10', size: 1024, iconset: 'icon_512x512@2x.png' },
52
+ ];
53
+ /** The distinct pixel sizes the ten elements need — what the layout asks the raster step for. */
54
+ export const ICNS_SIZES = [...new Set(ICNS_ELEMENTS.map((element) => element.size))].sort((a, b) => a - b);
55
+ /** An element head: the four-character type and the big-endian length including these 8 bytes. */
56
+ function head(type, length) {
57
+ const out = Buffer.alloc(8);
58
+ out.write(type, 0, 'latin1');
59
+ out.writeUInt32BE(length, 4);
60
+ return new Uint8Array(out);
61
+ }
62
+ /** The `.icns` file, `TOC ` first, then the ten elements in {@link ICNS_ELEMENTS} order. */
63
+ export function buildIcns(icon) {
64
+ const elements = ICNS_ELEMENTS.map((element) => {
65
+ const png = icon.png.get(element.size);
66
+ if (png === undefined) {
67
+ throw new Error(`gjsify ship: the macOS icon needs a ${element.size} px image (${element.iconset}) and the raster ` +
68
+ `set has none — \`resolveAppIcon\` was asked for the wrong sizes.`);
69
+ }
70
+ const { width, height } = readPngSize(png, `the ${element.size} px icon image`);
71
+ if (width !== element.size || height !== element.size) {
72
+ throw new Error(`gjsify ship: the image keyed ${element.size} in the icon raster set is ${width}×${height}.`);
73
+ }
74
+ return { ...element, png };
75
+ });
76
+ const toc = concatBytes([
77
+ head('TOC ', 8 + 8 * elements.length),
78
+ ...elements.map((element) => head(element.type, 8 + element.png.length)),
79
+ ]);
80
+ const body = elements.map((element) => concatBytes([head(element.type, 8 + element.png.length), element.png]));
81
+ const total = 8 + toc.length + body.reduce((sum, element) => sum + element.length, 0);
82
+ return concatBytes([head('icns', total), toc, ...body]);
83
+ }
@@ -0,0 +1,25 @@
1
+ import type { AppIconRasters } from './icons.js';
2
+ /** The edge lengths a Windows application icon carries. Ascending, so the entry order is too. */
3
+ export declare const ICO_SIZES: readonly number[];
4
+ /** `RT_ICON` and `RT_GROUP_ICON`: the two resource type ids. */
5
+ export declare const RT_ICON = 3;
6
+ export declare const RT_GROUP_ICON = 14;
7
+ /**
8
+ * The `.ico` file: `ICONDIR`, one `ICONDIRENTRY` per image, then the PNGs.
9
+ */
10
+ export declare function buildIco(icon: AppIconRasters): Uint8Array;
11
+ /** What the PE resource directory carries: the group, and the images the group names by id. */
12
+ export interface IconResources {
13
+ /** The `RT_GROUP_ICON` payload: `GRPICONDIR` with 14-byte entries naming `icons` by 1-based id. */
14
+ group: Uint8Array;
15
+ /** The `RT_ICON` payloads, in id order: `icons[i]` is resource id `i + 1`. */
16
+ icons: readonly Uint8Array[];
17
+ }
18
+ /**
19
+ * The same images as resources: `RT_ICON` id `n` is the n-th image, and the
20
+ * `RT_GROUP_ICON` names them by that id.
21
+ *
22
+ * Ids start at 1 because resource id 0 is never used by convention and some
23
+ * readers treat it as "no icon".
24
+ */
25
+ export declare function iconResources(icon: AppIconRasters): IconResources;
@@ -0,0 +1,120 @@
1
+ // The Windows icon container, in its two spellings: the `.ico` FILE, and the
2
+ // pair of resource types (`RT_GROUP_ICON`, `RT_ICON`) the same bytes take inside
3
+ // a PE image.
4
+ //
5
+ // WHY BOTH. Explorer, the Start menu, the taskbar and the Alt-Tab list all ask
6
+ // the EXECUTABLE for its icon, so the launcher `pe-launcher.ts` emits carries the
7
+ // resource form. The `.ico` file form is what an independent reader opens —
8
+ // Pillow, `icotool`, ImageMagick — and it is how `verify-program-dir.py`
9
+ // reassembles what it extracted from the `.rsrc` section to hand it to one.
10
+ //
11
+ // THE RESOURCE FORM IS NOT THE FILE FORM, and the difference is the one thing
12
+ // worth knowing about this format. An `.ico` is an `ICONDIR` whose entries end in
13
+ // a 4-byte `dwImageOffset` into the file. Inside a PE the images are separate
14
+ // `RT_ICON` resources, so the group directory's entries end in a 2-byte `nId`
15
+ // naming the resource instead — `GRPICONDIRENTRY` is 14 bytes where
16
+ // `ICONDIRENTRY` is 16. Writing the file form into `RT_GROUP_ICON` produces a
17
+ // resource Windows reads as a group with garbage ids, and the shell shows the
18
+ // generic icon with no error anywhere.
19
+ //
20
+ // PNG PAYLOADS AT EVERY SIZE. Since Windows Vista an image entry may be a whole
21
+ // PNG file rather than a BMP `BITMAPINFOHEADER` + pixel rows + AND mask, and that
22
+ // is what removes the need for a BMP encoder here. Confirmed against two readers
23
+ // on this workstation rather than trusted: Pillow 12.3 and `icotool -l`
24
+ // (icoutils 0.32) both list a six-entry PNG-only `.ico` at 16/24/32/48/64/256
25
+ // as six 32-bit images. The 256 entry is the one every guide says MUST be PNG
26
+ // (a BMP at that size is 256 KiB); the smaller ones are PNG for the same reason
27
+ // and because one encoding is one code path. Whether Windows 11's shell draws the
28
+ // small PNG entries at the same quality as BMP ones is a VM measurement this
29
+ // tree has not made — see the report that lands this file.
30
+ //
31
+ // THE SIZES are Microsoft's list for a classic desktop application icon — 16,
32
+ // 24, 32, 48 and 256, "the full set" in the *Icons (Design basics)* guidance —
33
+ // plus 64, which the same page names for the 200 % scaling of the 32 slot. The
34
+ // Start menu and the taskbar pick from these by DPI; a set missing 16 or 32
35
+ // renders the entry as a scaled 256, which is the blur every reader has seen on a
36
+ // half-finished port.
37
+ import { concatBytes } from './bytes.js';
38
+ import { readPngSize } from './icons.js';
39
+ /** The edge lengths a Windows application icon carries. Ascending, so the entry order is too. */
40
+ export const ICO_SIZES = [16, 24, 32, 48, 64, 256];
41
+ /** `RT_ICON` and `RT_GROUP_ICON`: the two resource type ids. */
42
+ export const RT_ICON = 3;
43
+ export const RT_GROUP_ICON = 14;
44
+ /** The ICONDIRENTRY / GRPICONDIRENTRY head shared by both forms: 8 bytes. */
45
+ function entryHead(size) {
46
+ const head = Buffer.alloc(8);
47
+ // 0 means 256 in the one-byte width/height fields — the format predates 256
48
+ // px icons and a byte cannot hold the number.
49
+ head.writeUInt8(size >= 256 ? 0 : size, 0); // bWidth
50
+ head.writeUInt8(size >= 256 ? 0 : size, 1); // bHeight
51
+ head.writeUInt8(0, 2); // bColorCount — 0 for anything past 8 bpp
52
+ head.writeUInt8(0, 3); // bReserved
53
+ head.writeUInt16LE(1, 4); // wPlanes
54
+ head.writeUInt16LE(32, 6); // wBitCount — what a PNG-backed entry declares
55
+ return new Uint8Array(head);
56
+ }
57
+ /** The sizes in ascending order, each one asserted present and square at that size. */
58
+ function entries(icon) {
59
+ const out = [];
60
+ for (const size of ICO_SIZES) {
61
+ const png = icon.png.get(size);
62
+ if (png === undefined) {
63
+ throw new Error(`gjsify ship: the Windows icon needs a ${size} px image and the raster set has none — ` +
64
+ `\`resolveAppIcon\` was asked for the wrong sizes (it has ${[...icon.png.keys()].join(', ')}).`);
65
+ }
66
+ const { width, height } = readPngSize(png, `the ${size} px icon image`);
67
+ if (width !== size || height !== size) {
68
+ throw new Error(`gjsify ship: the image keyed ${size} in the icon raster set is ${width}×${height}.`);
69
+ }
70
+ if (size > 256) {
71
+ // The one-byte size fields cannot say more than 256, and Windows reads
72
+ // nothing larger from an icon.
73
+ throw new Error(`gjsify ship: a Windows icon image cannot be ${size} px — 256 is the format's ceiling.`);
74
+ }
75
+ out.push({ size, png });
76
+ }
77
+ return out;
78
+ }
79
+ /**
80
+ * The `.ico` file: `ICONDIR`, one `ICONDIRENTRY` per image, then the PNGs.
81
+ */
82
+ export function buildIco(icon) {
83
+ const images = entries(icon);
84
+ const header = Buffer.alloc(6);
85
+ header.writeUInt16LE(0, 0); // idReserved
86
+ header.writeUInt16LE(1, 2); // idType — 1 is an icon, 2 a cursor
87
+ header.writeUInt16LE(images.length, 4);
88
+ const directory = [];
89
+ let offset = 6 + 16 * images.length;
90
+ for (const { size, png } of images) {
91
+ const tail = Buffer.alloc(8);
92
+ tail.writeUInt32LE(png.length, 0); // dwBytesInRes
93
+ tail.writeUInt32LE(offset, 4); // dwImageOffset — a FILE offset
94
+ directory.push(entryHead(size), new Uint8Array(tail));
95
+ offset += png.length;
96
+ }
97
+ return concatBytes([new Uint8Array(header), ...directory, ...images.map((image) => image.png)]);
98
+ }
99
+ /**
100
+ * The same images as resources: `RT_ICON` id `n` is the n-th image, and the
101
+ * `RT_GROUP_ICON` names them by that id.
102
+ *
103
+ * Ids start at 1 because resource id 0 is never used by convention and some
104
+ * readers treat it as "no icon".
105
+ */
106
+ export function iconResources(icon) {
107
+ const images = entries(icon);
108
+ const header = Buffer.alloc(6);
109
+ header.writeUInt16LE(0, 0);
110
+ header.writeUInt16LE(1, 2);
111
+ header.writeUInt16LE(images.length, 4);
112
+ const directory = [];
113
+ images.forEach(({ size, png }, index) => {
114
+ const tail = Buffer.alloc(6);
115
+ tail.writeUInt32LE(png.length, 0); // dwBytesInRes
116
+ tail.writeUInt16LE(index + 1, 4); // nId — the RT_ICON resource, NOT an offset
117
+ directory.push(entryHead(size), new Uint8Array(tail));
118
+ });
119
+ return { group: concatBytes([new Uint8Array(header), ...directory]), icons: images.map((image) => image.png) };
120
+ }
@@ -0,0 +1,191 @@
1
+ import { spawnToCompletion } from '../spawn.js';
2
+ import type { SystemGiOptions } from '../system-gi.js';
3
+ /** The runtime that renders. On `PATH`, like `glib-compile-schemas` — a GJS host has it by definition. */
4
+ export declare const RASTERIZER = "gjs";
5
+ /**
6
+ * How to get the typelib the script imports, in the two distributions' words.
7
+ *
8
+ * `librsvg2` on Fedora carries the typelib itself (measured: `rpm -qf
9
+ * /usr/lib64/girepository-1.0/Rsvg-2.0.typelib` → `librsvg2-2.62.3`); Debian
10
+ * splits it into `gir1.2-rsvg-2.0`.
11
+ *
12
+ * The macOS row IS measured, on the `macos-suites` darwin leg, and its first
13
+ * spelling was WRONG: `brew install gjs librsvg` alone still failed, with the
14
+ * error moving from `namespace 'Rsvg' not found` to `namespace 'cairo', version
15
+ * '1.0' not found`. The script imports both, and `Cairo-1.0.typelib` belongs to
16
+ * `gobject-introspection` rather than to the `cairo` formula (`rpm -qf` says so
17
+ * for the Fedora twin). A hint that names two thirds of what is needed sends a
18
+ * stranger round the same loop this one went round.
19
+ *
20
+ * INSTALLING ALL THREE IS STILL NOT ENOUGH on macOS, and this sentence cost the
21
+ * third red run: the dylib then EXISTS and dyld does not look where it is. That
22
+ * half is not a hint, it is {@link rasterizerEnv} — and the message deliberately
23
+ * stays quiet about it, because a host that reaches this line after the repair
24
+ * really is missing a package.
25
+ *
26
+ * Assembly is a Linux step by ADR 0024 § A2, so a darwin host reaching this line
27
+ * is running `ship` outside the documented arrangement; the message should still
28
+ * tell it what to install rather than name two Linuxes at it.
29
+ */
30
+ export declare const RASTERIZER_HINT: string;
31
+ /**
32
+ * The pixels of the app icon, one PNG per square edge length.
33
+ *
34
+ * A MAP keyed by size and not an array, because every consumer asks "the PNG for
35
+ * N" — the `.ico` writer for its six entries, the `.icns` writer for its ten
36
+ * elements over seven sizes — and an array would make each of them search.
37
+ */
38
+ export interface AppIconRasters {
39
+ /** Where the pixels came from, for the log line: the SVG's path, or how many sized PNGs. */
40
+ readonly source: string;
41
+ readonly png: ReadonlyMap<number, Uint8Array>;
42
+ }
43
+ /**
44
+ * Whether an icon belongs in the theme's SYMBOLIC context rather than at a size.
45
+ *
46
+ * `symbolic` is a directory of its own in a hicolor theme and NOT a size value.
47
+ * Measured in `refs/adwaita-icon-theme/index.theme`, which lists `symbolic/apps`
48
+ * in `Directories=` beside `scalable/apps` and `16x16/apps` and gives it its own
49
+ * section — `Context=Applications`, `Size=16`, `MinSize=8`, `MaxSize=512`,
50
+ * `Type=Scalable` — against the scalable row's `Size=128`. The freedesktop icon
51
+ * theme specification has no notion of `symbolic` at all: a theme expresses it by
52
+ * giving the directory a section, which is exactly why reading the EXTENSION
53
+ * cannot see it, and why this question has to be asked before the size one.
54
+ *
55
+ * TWO SIGNALS, the same pair `iconThemeDir` already reads for a size: the
56
+ * directory an author put the file in, and the name they gave it. GTK resolves a
57
+ * symbolic icon by the `-symbolic` name suffix, so an author who wrote the name
58
+ * has said as much as one who wrote the path.
59
+ *
60
+ * SVG ONLY, because the symbolic directory is declared `Type=Scalable`: the
61
+ * raster form GTK's own `gtk-encode-symbolic-svg` produces
62
+ * (`<name>-symbolic.symbolic.png`) is installed at a SIZE instead. So a PNG keeps
63
+ * answering the size question, and one that cannot answer it stays refused rather
64
+ * than quietly becoming a scalable icon that does not scale.
65
+ */
66
+ export declare function isSymbolicIcon(iconPath: string): boolean;
67
+ /**
68
+ * The hicolor subdirectory for an icon: the `symbolic` CONTEXT, else `scalable`
69
+ * for an SVG, else the pixel size read from a `<n>x<n>` path component or a
70
+ * trailing number in the filename.
71
+ *
72
+ * Named for the theme DIRECTORY rather than a size from the moment the first of
73
+ * those answers stopped being one: `iconSizeDir` returning `symbolic` would be a
74
+ * name every caller has to read past.
75
+ */
76
+ export declare function iconThemeDir(iconPath: string): string;
77
+ /**
78
+ * A PNG's pixel size, read from its IHDR — with the file's two ends checked.
79
+ *
80
+ * The signature and the IHDR say what the file CLAIMS to be; the `IEND` trailer
81
+ * says it is all there. A PNG truncated by a half-written copy still has a valid
82
+ * header, and an icon container built around one embeds a picture that decodes
83
+ * to nothing on the target OS with no error at pack time. Twelve bytes at the end
84
+ * is the cheapest check that catches that whole class.
85
+ */
86
+ export declare function readPngSize(bytes: Uint8Array, what: string): {
87
+ width: number;
88
+ height: number;
89
+ };
90
+ /**
91
+ * The program the child GJS runs. `ARGV` is the SVG path followed by the sizes;
92
+ * stdout is one JSON object mapping each size to the PNG, base64.
93
+ *
94
+ * Legacy `imports`, deliberately: `gjs -c` takes a script, not a module, and the
95
+ * legacy namespace is the one a script can reach synchronously. `imports.cairo`
96
+ * is GJS's own cairo binding — `ImageSurface` and `writeToPNG` are its, not the
97
+ * typelib's, which has no constructors.
98
+ *
99
+ * `render_document` scales the document to the viewport, so each size is a
100
+ * fresh vector render and never a resample of a larger one — the difference
101
+ * between a crisp 16 px icon and a blurred one, which is the whole reason to
102
+ * render per size rather than once.
103
+ *
104
+ * Through a file rather than a memory surface's bytes: `writeToPNG` is the PNG
105
+ * ENCODER, and cairo is the one this process has. Base64 on stdout because it is
106
+ * the transport both runtimes' `child_process` read the same way.
107
+ */
108
+ export declare const RASTERIZE_SCRIPT: string;
109
+ /**
110
+ * The launch env for the rasterizing child — the HOST's GI libdirs put back.
111
+ *
112
+ * THE INCIDENT, measured on both `macos-suites` legs (2026-09-13). `brew install
113
+ * gjs librsvg gobject-introspection` installs everything the script imports, and
114
+ * the render still died:
115
+ *
116
+ * Failed to load shared library 'librsvg-2.2.dylib' referenced by the typelib:
117
+ * dlopen(librsvg-2.2.dylib): tried: 'librsvg-2.2.dylib' (no such file),
118
+ * '/usr/local/Cellar/gjs/1.88.1/bin/../../../../opt/glib/lib/librsvg-2.2.dylib',
119
+ * '/usr/lib/librsvg-2.2.dylib' (not in dyld cache)
120
+ * JS ERROR: Error: Unsupported type void, deriving from fundamental void
121
+ *
122
+ * `/usr/local/lib` (x64) and `/opt/homebrew/lib` (arm64) — where Homebrew puts
123
+ * the dylib — are in NEITHER list, because a typelib names its library by bare
124
+ * leaf and Homebrew's `gjs` carries one rpath, into GLIB's keg alone. The second
125
+ * line is the symptom, not the cause: the typelib loads, its library does not,
126
+ * and the first call then marshals a type that is no longer resolvable. It reads
127
+ * like GJS was built without cairo, and this comment exists because that is what
128
+ * it was first diagnosed as.
129
+ *
130
+ * {@link buildNativeEnv} already repairs exactly this, and its own header calls
131
+ * itself "the ONE place `gjsify run`, `showcase`, `storybook`, `tsc` and `info`
132
+ * all pass through". `ship` opened a SECOND gjs child that went around it — which
133
+ * is the class, not the bug: the repair is per-call-site, so every new child
134
+ * inherits the gap until someone reads that header. `rasterizerEnv` is what the
135
+ * spec beside it pins, so the wiring is asserted and not just the composition.
136
+ *
137
+ * No packages are passed: the gap is in the host's loader, not in a prebuild we
138
+ * ship, and the child is a bare `gjs -c` that loads no `.node` at all.
139
+ *
140
+ * `env` FIRST, so the composed values win over the inherited ones rather than
141
+ * being shadowed by them.
142
+ */
143
+ export declare function rasterizerEnv(opts?: {
144
+ platform?: string;
145
+ env?: Record<string, string | undefined>;
146
+ /** Injectable for tests — the darwin branch is asserted from Linux. */
147
+ systemGiDirs?: (opts: SystemGiOptions) => string[];
148
+ }): NodeJS.ProcessEnv;
149
+ export interface RasterizeInput {
150
+ /** Absolute path of the SVG. */
151
+ svg: string;
152
+ /** Square edge lengths to render, each one a separate vector render. */
153
+ sizes: readonly number[];
154
+ /**
155
+ * Injectable for tests. NOT a convenience: the {@link rasterizerEnv} repair is
156
+ * only worth anything if it reaches the child, and a spec over the composition
157
+ * alone passes just as well when nobody wires it in.
158
+ */
159
+ spawn?: typeof spawnToCompletion;
160
+ }
161
+ /**
162
+ * Render an SVG at every size through librsvg, in a child GJS.
163
+ *
164
+ * Every PNG that comes back is read for its IHDR before it is returned: the size
165
+ * the child rendered is the size the caller asked for or the whole batch is
166
+ * refused, because the writer that would embed it trusts the map's key.
167
+ */
168
+ export declare function rasterizeSvg(input: RasterizeInput): Promise<Map<number, Uint8Array>>;
169
+ export interface ResolveAppIconInput {
170
+ /** Absolute paths of the project's icons — `ShipSettings.iconFiles`. */
171
+ iconFiles: readonly string[];
172
+ appId: string;
173
+ /** The square sizes the layout's writer needs, from {@link Layout.icon}. */
174
+ sizes: readonly number[];
175
+ /** What the sizes are FOR, in the refusal: "the Windows launcher's icon". */
176
+ target: string;
177
+ /** The renderer, injectable so the selection is testable without a GJS. */
178
+ rasterize?: (input: RasterizeInput) => Promise<Map<number, Uint8Array>>;
179
+ }
180
+ /**
181
+ * The raster set a layout needs, from whatever the project declares.
182
+ *
183
+ * Sized PNGs the project ships are used at their (IHDR-read) size; every size
184
+ * they leave uncovered is rendered from the scalable SVG; a size neither can
185
+ * answer is a refusal that names the file to add. The `-symbolic` icon is never
186
+ * a source: it is the monochrome status-bar glyph, and an app icon drawn from it
187
+ * would be a grey silhouette on both platforms.
188
+ */
189
+ export declare function resolveAppIcon(input: ResolveAppIconInput): Promise<AppIconRasters>;
190
+ /** The sizes in a raster set, ascending — for log lines and error messages. */
191
+ export declare function iconSizes(icon: AppIconRasters): number[];