@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.
- package/dist/cli.gjs.mjs +505 -490
- package/lib/cli-app.js +3 -1
- package/lib/commands/affected-classify.d.ts +7 -1
- package/lib/commands/affected-classify.js +66 -9
- package/lib/commands/affected-script-refs.d.ts +20 -0
- package/lib/commands/affected-script-refs.js +218 -0
- package/lib/commands/affected.js +24 -2
- package/lib/commands/flatpak/diff.js +8 -2
- package/lib/commands/flatpak/init.js +4 -1
- package/lib/commands/flatpak/sources.js +4 -3
- package/lib/commands/flatpak/sync-flathub.d.ts +2 -0
- package/lib/commands/flatpak/sync-flathub.js +149 -7
- package/lib/commands/flatpak/utils.d.ts +10 -0
- package/lib/commands/flatpak/utils.js +10 -0
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.js +76 -0
- package/lib/commands/link.d.ts +13 -0
- package/lib/commands/link.js +243 -0
- package/lib/commands/onboard.js +18 -2
- package/lib/commands/publish.d.ts +34 -0
- package/lib/commands/publish.js +153 -8
- package/lib/commands/ship.js +46 -4
- package/lib/types/config-data.d.ts +1 -1
- package/lib/utils/check-system-deps.d.ts +1 -17
- package/lib/utils/check-system-deps.js +10 -68
- package/lib/utils/dev-link.d.ts +222 -0
- package/lib/utils/dev-link.js +632 -0
- package/lib/utils/hang-watchdog.d.ts +95 -0
- package/lib/utils/hang-watchdog.js +194 -0
- package/lib/utils/install-backend-native.d.ts +14 -0
- package/lib/utils/install-backend-native.js +91 -13
- package/lib/utils/install-extraneous.d.ts +90 -0
- package/lib/utils/install-extraneous.js +120 -0
- package/lib/utils/install-tarball-cache.d.ts +37 -9
- package/lib/utils/install-tarball-cache.js +59 -14
- package/lib/utils/normalize-bundler-options.js +11 -0
- package/lib/utils/publish-pins.d.ts +38 -0
- package/lib/utils/publish-pins.js +47 -0
- package/lib/utils/publish-readback.d.ts +103 -17
- package/lib/utils/publish-readback.js +326 -47
- package/lib/utils/run-gjs.js +82 -6
- package/lib/utils/ship/appimage.d.ts +24 -4
- package/lib/utils/ship/appimage.js +51 -6
- package/lib/utils/ship/formats.js +44 -11
- package/lib/utils/ship/icns.d.ts +14 -0
- package/lib/utils/ship/icns.js +83 -0
- package/lib/utils/ship/ico.d.ts +25 -0
- package/lib/utils/ship/ico.js +120 -0
- package/lib/utils/ship/icons.d.ts +191 -0
- package/lib/utils/ship/icons.js +398 -0
- package/lib/utils/ship/launcher.js +16 -1
- package/lib/utils/ship/layout.d.ts +32 -0
- package/lib/utils/ship/layout.js +112 -38
- package/lib/utils/ship/msi.d.ts +1 -1
- package/lib/utils/ship/msi.js +30 -5
- package/lib/utils/ship/pe-launcher.d.ts +18 -0
- package/lib/utils/ship/pe-launcher.js +125 -4
- package/lib/utils/ship/plan.d.ts +1 -34
- package/lib/utils/ship/plan.js +7 -56
- package/lib/utils/ship/plist.d.ts +20 -10
- package/lib/utils/ship/plist.js +29 -12
- package/lib/utils/ship/schemas.d.ts +46 -0
- package/lib/utils/ship/schemas.js +92 -11
- package/lib/utils/ship/settings.js +3 -2
- package/package.json +20 -20
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// The application icon: one source, one raster step, and the sizes each OS
|
|
2
|
+
// wants — so the `.ico`, the `.icns` and the hicolor theme all come from the
|
|
3
|
+
// same pixels.
|
|
4
|
+
//
|
|
5
|
+
// THE DEFECT THIS CLOSES, measured on the released artifacts of one app
|
|
6
|
+
// (Learn6502 0.8.0, 2026-09-13). On Windows 11 the Start-menu entry showed the
|
|
7
|
+
// generic blank-document icon: the shortcut asks the `.exe`, and the emitted
|
|
8
|
+
// launcher carried no resource directory at all. On macOS 15 the bundle had no
|
|
9
|
+
// `CFBundleIconFile`, no `CFBundleIconName` and no `.icns` anywhere under
|
|
10
|
+
// `Contents/`, so the Finder drew the generic application icon. The app declares
|
|
11
|
+
// ONE icon, a scalable SVG under `hicolor/scalable/apps/`, which is exactly what a
|
|
12
|
+
// GNOME app ships — and neither OS reads an SVG. So every SVG-only app shipped to
|
|
13
|
+
// those two platforms with no icon, and nothing said so.
|
|
14
|
+
//
|
|
15
|
+
// WHY `gjsify ship` CONVERTS rather than asking the project for rasters. The
|
|
16
|
+
// alternative — refuse the pack until the author adds `16x16/… 256x256/…` PNGs —
|
|
17
|
+
// costs every GNOME app a project-side change to ship the icon it already has,
|
|
18
|
+
// and it costs it per platform: Windows wants six sizes, macOS ten elements over
|
|
19
|
+
// seven. A build system owns that translation the way it owns the launcher and
|
|
20
|
+
// the desktop entry. The author declares the icon once; each layout says which
|
|
21
|
+
// sizes it writes ({@link Layout.icon}); this module renders them.
|
|
22
|
+
//
|
|
23
|
+
// THE RASTERIZER IS librsvg THROUGH GJS, IN A CHILD PROCESS, and each half of
|
|
24
|
+
// that was measured rather than chosen:
|
|
25
|
+
//
|
|
26
|
+
// * librsvg, not ImageMagick or `rsvg-convert`: neither binary is in the CI
|
|
27
|
+
// image (`.docker/ci-fedora.Dockerfile`), and `rsvg-convert` is not on the
|
|
28
|
+
// workstation either. `Rsvg-2.0.typelib` IS in the image, because GTK needs
|
|
29
|
+
// it, and so is `cairo-1.0.typelib`.
|
|
30
|
+
// * `Rsvg.Handle` rendered onto a cairo surface, not `GdkPixbuf.new_from_file_at_size`
|
|
31
|
+
// on the SVG. The pixbuf route depends on a loader MODULE: on the workstation
|
|
32
|
+
// `/usr/lib64/gdk-pixbuf-2.0/2.10.0/loaders/` holds only the xpm loader and the
|
|
33
|
+
// CI image has no loaders directory at all, so whether an SVG decodes that way
|
|
34
|
+
// is a property of the host's gdk-pixbuf packaging. `rsvg_handle_render_document`
|
|
35
|
+
// depends on librsvg alone.
|
|
36
|
+
// * a `gjs -c` child, not an in-process `gi://` import. `gjsify ship` runs under
|
|
37
|
+
// GJS when installed the Node-free way and under Node (`lib/index.js`) in the
|
|
38
|
+
// CI leg that assembles the Windows artifact — and the CLI's own source has no
|
|
39
|
+
// `gi://` import anywhere, because Node reaches GI only through a node-gi
|
|
40
|
+
// prebuild that leg does not carry. GJS is on every host that assembles (ADR
|
|
41
|
+
// 0024 § A2: "assemble anywhere, under GJS"), so a child process is the one
|
|
42
|
+
// path that is the same on both runtimes. The script is a string constant
|
|
43
|
+
// here, not a file in `lib/templates`: `dist/cli.gjs.mjs` is a single bundled
|
|
44
|
+
// file and a template it would have to locate on disk is a second thing to
|
|
45
|
+
// ship.
|
|
46
|
+
//
|
|
47
|
+
// DETERMINISTIC ACROSS HOSTS, as far as measured: the same SVG rendered at 16
|
|
48
|
+
// and 256 px on the workstation and inside `ghcr.io/gjsify/ci-fedora:44` gave
|
|
49
|
+
// byte-identical PNGs (one sha256, librsvg 2.62.3 on both). A different librsvg
|
|
50
|
+
// may antialias differently, which makes the raster a function of the assembling
|
|
51
|
+
// host's library like every compiled artifact — the STAGE stays reproducible, and
|
|
52
|
+
// repacking it (ADR 0024 § A2) renders nothing.
|
|
53
|
+
//
|
|
54
|
+
// A PROJECT'S OWN PNG WINS at its size. A hand-tuned 16 px icon is drawn for that
|
|
55
|
+
// size and an SVG rendered at 16 px is not, and the hicolor convention exists to
|
|
56
|
+
// carry exactly that distinction. The PNG's IHDR is what decides its size, not its
|
|
57
|
+
// path: a `32x32/apps/x.png` that is 48 px wide is refused by name rather than
|
|
58
|
+
// embedded under the wrong header.
|
|
59
|
+
//
|
|
60
|
+
// WHAT IS REFUSED, and why refusal is the mechanism that matters more than the
|
|
61
|
+
// conversion. An app with no icon source at all, or with PNGs that leave a
|
|
62
|
+
// required size uncovered and no SVG to render it from, fails the STAGE naming
|
|
63
|
+
// the file to add. The previous behaviour was a warning on Linux and silence on
|
|
64
|
+
// the other two, and silence is how two platforms shipped without an icon for
|
|
65
|
+
// every release until someone opened the Start menu.
|
|
66
|
+
import { readFileSync } from 'node:fs';
|
|
67
|
+
import { basename, extname } from 'node:path';
|
|
68
|
+
import { buildNativeEnv } from '../detect-native-packages.js';
|
|
69
|
+
import { spawnToCompletion } from '../spawn.js';
|
|
70
|
+
/** The runtime that renders. On `PATH`, like `glib-compile-schemas` — a GJS host has it by definition. */
|
|
71
|
+
export const RASTERIZER = 'gjs';
|
|
72
|
+
/**
|
|
73
|
+
* How to get the typelib the script imports, in the two distributions' words.
|
|
74
|
+
*
|
|
75
|
+
* `librsvg2` on Fedora carries the typelib itself (measured: `rpm -qf
|
|
76
|
+
* /usr/lib64/girepository-1.0/Rsvg-2.0.typelib` → `librsvg2-2.62.3`); Debian
|
|
77
|
+
* splits it into `gir1.2-rsvg-2.0`.
|
|
78
|
+
*
|
|
79
|
+
* The macOS row IS measured, on the `macos-suites` darwin leg, and its first
|
|
80
|
+
* spelling was WRONG: `brew install gjs librsvg` alone still failed, with the
|
|
81
|
+
* error moving from `namespace 'Rsvg' not found` to `namespace 'cairo', version
|
|
82
|
+
* '1.0' not found`. The script imports both, and `Cairo-1.0.typelib` belongs to
|
|
83
|
+
* `gobject-introspection` rather than to the `cairo` formula (`rpm -qf` says so
|
|
84
|
+
* for the Fedora twin). A hint that names two thirds of what is needed sends a
|
|
85
|
+
* stranger round the same loop this one went round.
|
|
86
|
+
*
|
|
87
|
+
* INSTALLING ALL THREE IS STILL NOT ENOUGH on macOS, and this sentence cost the
|
|
88
|
+
* third red run: the dylib then EXISTS and dyld does not look where it is. That
|
|
89
|
+
* half is not a hint, it is {@link rasterizerEnv} — and the message deliberately
|
|
90
|
+
* stays quiet about it, because a host that reaches this line after the repair
|
|
91
|
+
* really is missing a package.
|
|
92
|
+
*
|
|
93
|
+
* Assembly is a Linux step by ADR 0024 § A2, so a darwin host reaching this line
|
|
94
|
+
* is running `ship` outside the documented arrangement; the message should still
|
|
95
|
+
* tell it what to install rather than name two Linuxes at it.
|
|
96
|
+
*/
|
|
97
|
+
export const RASTERIZER_HINT = 'Fedora: `sudo dnf install gjs librsvg2`, Debian/Ubuntu: `sudo apt install gjs gir1.2-rsvg-2.0`, ' +
|
|
98
|
+
'macOS: `brew install gjs librsvg gobject-introspection`';
|
|
99
|
+
/**
|
|
100
|
+
* Whether an icon belongs in the theme's SYMBOLIC context rather than at a size.
|
|
101
|
+
*
|
|
102
|
+
* `symbolic` is a directory of its own in a hicolor theme and NOT a size value.
|
|
103
|
+
* Measured in `refs/adwaita-icon-theme/index.theme`, which lists `symbolic/apps`
|
|
104
|
+
* in `Directories=` beside `scalable/apps` and `16x16/apps` and gives it its own
|
|
105
|
+
* section — `Context=Applications`, `Size=16`, `MinSize=8`, `MaxSize=512`,
|
|
106
|
+
* `Type=Scalable` — against the scalable row's `Size=128`. The freedesktop icon
|
|
107
|
+
* theme specification has no notion of `symbolic` at all: a theme expresses it by
|
|
108
|
+
* giving the directory a section, which is exactly why reading the EXTENSION
|
|
109
|
+
* cannot see it, and why this question has to be asked before the size one.
|
|
110
|
+
*
|
|
111
|
+
* TWO SIGNALS, the same pair `iconThemeDir` already reads for a size: the
|
|
112
|
+
* directory an author put the file in, and the name they gave it. GTK resolves a
|
|
113
|
+
* symbolic icon by the `-symbolic` name suffix, so an author who wrote the name
|
|
114
|
+
* has said as much as one who wrote the path.
|
|
115
|
+
*
|
|
116
|
+
* SVG ONLY, because the symbolic directory is declared `Type=Scalable`: the
|
|
117
|
+
* raster form GTK's own `gtk-encode-symbolic-svg` produces
|
|
118
|
+
* (`<name>-symbolic.symbolic.png`) is installed at a SIZE instead. So a PNG keeps
|
|
119
|
+
* answering the size question, and one that cannot answer it stays refused rather
|
|
120
|
+
* than quietly becoming a scalable icon that does not scale.
|
|
121
|
+
*/
|
|
122
|
+
export function isSymbolicIcon(iconPath) {
|
|
123
|
+
if (extname(iconPath).toLowerCase() !== '.svg')
|
|
124
|
+
return false;
|
|
125
|
+
if (/(?:^|[\\/])symbolic[\\/]/.test(iconPath))
|
|
126
|
+
return true;
|
|
127
|
+
return basename(iconPath, extname(iconPath)).endsWith('-symbolic');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The hicolor subdirectory for an icon: the `symbolic` CONTEXT, else `scalable`
|
|
131
|
+
* for an SVG, else the pixel size read from a `<n>x<n>` path component or a
|
|
132
|
+
* trailing number in the filename.
|
|
133
|
+
*
|
|
134
|
+
* Named for the theme DIRECTORY rather than a size from the moment the first of
|
|
135
|
+
* those answers stopped being one: `iconSizeDir` returning `symbolic` would be a
|
|
136
|
+
* name every caller has to read past.
|
|
137
|
+
*/
|
|
138
|
+
export function iconThemeDir(iconPath) {
|
|
139
|
+
if (isSymbolicIcon(iconPath))
|
|
140
|
+
return 'symbolic';
|
|
141
|
+
if (extname(iconPath).toLowerCase() === '.svg')
|
|
142
|
+
return 'scalable';
|
|
143
|
+
const square = /(?:^|[\\/])(\d{1,4})x\1(?:[\\/]|$)/.exec(iconPath);
|
|
144
|
+
if (square)
|
|
145
|
+
return `${square[1]}x${square[1]}`;
|
|
146
|
+
const tokens = basename(iconPath, extname(iconPath)).split(/[-_.]/);
|
|
147
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
148
|
+
const token = tokens[i];
|
|
149
|
+
if (token !== undefined && /^\d{1,4}$/.test(token))
|
|
150
|
+
return `${token}x${token}`;
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`gjsify ship: cannot tell what size ${iconPath} is. ` +
|
|
153
|
+
'Put it in a `<size>x<size>/` directory, end its name with the size (`icon-128.png`), or ship an SVG.');
|
|
154
|
+
}
|
|
155
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
156
|
+
/** `IEND` with its zero length and its fixed CRC — the last twelve bytes of every whole PNG. */
|
|
157
|
+
const PNG_IEND = [0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82];
|
|
158
|
+
/**
|
|
159
|
+
* A PNG's pixel size, read from its IHDR — with the file's two ends checked.
|
|
160
|
+
*
|
|
161
|
+
* The signature and the IHDR say what the file CLAIMS to be; the `IEND` trailer
|
|
162
|
+
* says it is all there. A PNG truncated by a half-written copy still has a valid
|
|
163
|
+
* header, and an icon container built around one embeds a picture that decodes
|
|
164
|
+
* to nothing on the target OS with no error at pack time. Twelve bytes at the end
|
|
165
|
+
* is the cheapest check that catches that whole class.
|
|
166
|
+
*/
|
|
167
|
+
export function readPngSize(bytes, what) {
|
|
168
|
+
if (bytes.length < 33 || PNG_SIGNATURE.some((byte, index) => bytes[index] !== byte)) {
|
|
169
|
+
throw new Error(`gjsify ship: ${what} is not a PNG — it does not start with the PNG signature.`);
|
|
170
|
+
}
|
|
171
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
172
|
+
// The first chunk must be IHDR: 13 bytes, type `IHDR`, then width and height big-endian.
|
|
173
|
+
if (view.getUint32(8) !== 13 || Buffer.from(bytes.subarray(12, 16)).toString('latin1') !== 'IHDR') {
|
|
174
|
+
throw new Error(`gjsify ship: ${what} is not a PNG this command can read — its first chunk is not IHDR.`);
|
|
175
|
+
}
|
|
176
|
+
const tail = bytes.length - PNG_IEND.length;
|
|
177
|
+
if (PNG_IEND.some((byte, index) => bytes[tail + index] !== byte)) {
|
|
178
|
+
throw new Error(`gjsify ship: ${what} is truncated — it does not end with an IEND chunk, so it would embed as a ` +
|
|
179
|
+
'picture that decodes to nothing.');
|
|
180
|
+
}
|
|
181
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The program the child GJS runs. `ARGV` is the SVG path followed by the sizes;
|
|
185
|
+
* stdout is one JSON object mapping each size to the PNG, base64.
|
|
186
|
+
*
|
|
187
|
+
* Legacy `imports`, deliberately: `gjs -c` takes a script, not a module, and the
|
|
188
|
+
* legacy namespace is the one a script can reach synchronously. `imports.cairo`
|
|
189
|
+
* is GJS's own cairo binding — `ImageSurface` and `writeToPNG` are its, not the
|
|
190
|
+
* typelib's, which has no constructors.
|
|
191
|
+
*
|
|
192
|
+
* `render_document` scales the document to the viewport, so each size is a
|
|
193
|
+
* fresh vector render and never a resample of a larger one — the difference
|
|
194
|
+
* between a crisp 16 px icon and a blurred one, which is the whole reason to
|
|
195
|
+
* render per size rather than once.
|
|
196
|
+
*
|
|
197
|
+
* Through a file rather than a memory surface's bytes: `writeToPNG` is the PNG
|
|
198
|
+
* ENCODER, and cairo is the one this process has. Base64 on stdout because it is
|
|
199
|
+
* the transport both runtimes' `child_process` read the same way.
|
|
200
|
+
*/
|
|
201
|
+
export const RASTERIZE_SCRIPT = `
|
|
202
|
+
imports.gi.versions.Rsvg = '2.0';
|
|
203
|
+
const { Rsvg, GLib } = imports.gi;
|
|
204
|
+
const Cairo = imports.cairo;
|
|
205
|
+
const [svg, ...sizes] = ARGV;
|
|
206
|
+
const handle = Rsvg.Handle.new_from_file(svg);
|
|
207
|
+
const out = {};
|
|
208
|
+
for (const text of sizes) {
|
|
209
|
+
const size = Number(text);
|
|
210
|
+
const surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, size, size);
|
|
211
|
+
const cr = new Cairo.Context(surface);
|
|
212
|
+
handle.render_document(cr, new Rsvg.Rectangle({ x: 0, y: 0, width: size, height: size }));
|
|
213
|
+
cr.$dispose();
|
|
214
|
+
surface.flush();
|
|
215
|
+
const path = GLib.build_filenamev([GLib.get_tmp_dir(), \`gjsify-ship-icon-\${GLib.get_monotonic_time()}-\${size}.png\`]);
|
|
216
|
+
surface.writeToPNG(path);
|
|
217
|
+
const [, bytes] = GLib.file_get_contents(path);
|
|
218
|
+
GLib.unlink(path);
|
|
219
|
+
out[size] = GLib.base64_encode(bytes);
|
|
220
|
+
}
|
|
221
|
+
print(JSON.stringify(out));
|
|
222
|
+
`.trim();
|
|
223
|
+
/**
|
|
224
|
+
* The launch env for the rasterizing child — the HOST's GI libdirs put back.
|
|
225
|
+
*
|
|
226
|
+
* THE INCIDENT, measured on both `macos-suites` legs (2026-09-13). `brew install
|
|
227
|
+
* gjs librsvg gobject-introspection` installs everything the script imports, and
|
|
228
|
+
* the render still died:
|
|
229
|
+
*
|
|
230
|
+
* Failed to load shared library 'librsvg-2.2.dylib' referenced by the typelib:
|
|
231
|
+
* dlopen(librsvg-2.2.dylib): tried: 'librsvg-2.2.dylib' (no such file),
|
|
232
|
+
* '/usr/local/Cellar/gjs/1.88.1/bin/../../../../opt/glib/lib/librsvg-2.2.dylib',
|
|
233
|
+
* '/usr/lib/librsvg-2.2.dylib' (not in dyld cache)
|
|
234
|
+
* JS ERROR: Error: Unsupported type void, deriving from fundamental void
|
|
235
|
+
*
|
|
236
|
+
* `/usr/local/lib` (x64) and `/opt/homebrew/lib` (arm64) — where Homebrew puts
|
|
237
|
+
* the dylib — are in NEITHER list, because a typelib names its library by bare
|
|
238
|
+
* leaf and Homebrew's `gjs` carries one rpath, into GLIB's keg alone. The second
|
|
239
|
+
* line is the symptom, not the cause: the typelib loads, its library does not,
|
|
240
|
+
* and the first call then marshals a type that is no longer resolvable. It reads
|
|
241
|
+
* like GJS was built without cairo, and this comment exists because that is what
|
|
242
|
+
* it was first diagnosed as.
|
|
243
|
+
*
|
|
244
|
+
* {@link buildNativeEnv} already repairs exactly this, and its own header calls
|
|
245
|
+
* itself "the ONE place `gjsify run`, `showcase`, `storybook`, `tsc` and `info`
|
|
246
|
+
* all pass through". `ship` opened a SECOND gjs child that went around it — which
|
|
247
|
+
* is the class, not the bug: the repair is per-call-site, so every new child
|
|
248
|
+
* inherits the gap until someone reads that header. `rasterizerEnv` is what the
|
|
249
|
+
* spec beside it pins, so the wiring is asserted and not just the composition.
|
|
250
|
+
*
|
|
251
|
+
* No packages are passed: the gap is in the host's loader, not in a prebuild we
|
|
252
|
+
* ship, and the child is a bare `gjs -c` that loads no `.node` at all.
|
|
253
|
+
*
|
|
254
|
+
* `env` FIRST, so the composed values win over the inherited ones rather than
|
|
255
|
+
* being shadowed by them.
|
|
256
|
+
*/
|
|
257
|
+
export function rasterizerEnv(opts = {}) {
|
|
258
|
+
return { ...(opts.env ?? process.env), ...buildNativeEnv([], opts) };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Render an SVG at every size through librsvg, in a child GJS.
|
|
262
|
+
*
|
|
263
|
+
* Every PNG that comes back is read for its IHDR before it is returned: the size
|
|
264
|
+
* the child rendered is the size the caller asked for or the whole batch is
|
|
265
|
+
* refused, because the writer that would embed it trusts the map's key.
|
|
266
|
+
*/
|
|
267
|
+
export async function rasterizeSvg(input) {
|
|
268
|
+
if (input.sizes.length === 0)
|
|
269
|
+
return new Map();
|
|
270
|
+
// THE CLI'S OWN SPAWN WRAPPER, not `promisify(execFile)`. Measured under the
|
|
271
|
+
// GJS-built CLI (`dist/cli.gjs.mjs`, gjs 1.88): `node:util`'s `promisify` over
|
|
272
|
+
// `@gjsify/child_process`'s `execFile` resolves with the bare stdout string —
|
|
273
|
+
// the polyfill has no `promisify.custom` producing Node's `{ stdout, stderr }`
|
|
274
|
+
// — so a destructured `stdout` was `undefined` on that runtime and only that
|
|
275
|
+
// one. `schemas.ts` uses the same pattern and never noticed, because it ignores
|
|
276
|
+
// the result. `spawnToCompletion` captures on both paths and picks the blocking
|
|
277
|
+
// one under GJS per its teardown contract; `'return'` is what every packer here
|
|
278
|
+
// declares, for the reason `msi.ts` gives at its own call.
|
|
279
|
+
const result = await (input.spawn ?? spawnToCompletion)(RASTERIZER, ['-c', RASTERIZE_SCRIPT, input.svg, ...input.sizes.map(String)], {
|
|
280
|
+
completion: 'return',
|
|
281
|
+
stdio: 'capture',
|
|
282
|
+
// The host's own GI libdirs — see {@link rasterizerEnv}. Without it a
|
|
283
|
+
// Mac with librsvg installed still cannot render.
|
|
284
|
+
env: rasterizerEnv(),
|
|
285
|
+
// A 1024 px PNG of a detailed icon is a few hundred kilobytes; base64 adds
|
|
286
|
+
// a third. Node's default 1 MiB is within reach of one large element.
|
|
287
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
288
|
+
notFound: () => new Error(`gjsify ship: \`${RASTERIZER}\` is not on PATH, and it is what renders ${input.svg} into the ` +
|
|
289
|
+
`sizes an icon needs on this OS. ${RASTERIZER_HINT}.`),
|
|
290
|
+
});
|
|
291
|
+
const stdout = result.stdout ?? '';
|
|
292
|
+
if (result.code !== 0) {
|
|
293
|
+
const stderr = (result.stderr ?? '').trim();
|
|
294
|
+
// The two typelib-shaped failures GJS reports, spelled by their symptom
|
|
295
|
+
// rather than by GJS's wording, which is translated and changes.
|
|
296
|
+
const hint = /Rsvg|Typelib|cairo/i.test(stderr)
|
|
297
|
+
? ` The Rsvg typelib or GJS's cairo binding is missing. ${RASTERIZER_HINT}.`
|
|
298
|
+
: '';
|
|
299
|
+
throw new Error(`gjsify ship: rendering ${input.svg} through ${RASTERIZER} failed (exit ${result.code ?? result.signal}).` +
|
|
300
|
+
`${hint}${stderr === '' ? '' : `\n ${stderr.split('\n').join('\n ')}`}`);
|
|
301
|
+
}
|
|
302
|
+
let parsed;
|
|
303
|
+
try {
|
|
304
|
+
parsed = JSON.parse(stdout);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
throw new Error(`gjsify ship: ${RASTERIZER} printed something other than the rendered icon while rendering ` +
|
|
308
|
+
`${input.svg}: ${stdout.slice(0, 200)}`);
|
|
309
|
+
}
|
|
310
|
+
const out = new Map();
|
|
311
|
+
for (const size of input.sizes) {
|
|
312
|
+
const encoded = parsed[String(size)];
|
|
313
|
+
if (encoded === undefined) {
|
|
314
|
+
throw new Error(`gjsify ship: ${RASTERIZER} rendered no ${size} px image of ${input.svg}.`);
|
|
315
|
+
}
|
|
316
|
+
const png = new Uint8Array(Buffer.from(encoded, 'base64'));
|
|
317
|
+
const { width, height } = readPngSize(png, `the ${size} px render of ${input.svg}`);
|
|
318
|
+
if (width !== size || height !== size) {
|
|
319
|
+
throw new Error(`gjsify ship: asked ${RASTERIZER} for a ${size} px render of ${input.svg} and got ${width}×${height}.`);
|
|
320
|
+
}
|
|
321
|
+
out.set(size, png);
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The raster set a layout needs, from whatever the project declares.
|
|
327
|
+
*
|
|
328
|
+
* Sized PNGs the project ships are used at their (IHDR-read) size; every size
|
|
329
|
+
* they leave uncovered is rendered from the scalable SVG; a size neither can
|
|
330
|
+
* answer is a refusal that names the file to add. The `-symbolic` icon is never
|
|
331
|
+
* a source: it is the monochrome status-bar glyph, and an app icon drawn from it
|
|
332
|
+
* would be a grey silhouette on both platforms.
|
|
333
|
+
*/
|
|
334
|
+
export async function resolveAppIcon(input) {
|
|
335
|
+
const scalable = input.iconFiles.filter((file) => extname(file).toLowerCase() === '.svg' && !isSymbolicIcon(file));
|
|
336
|
+
const rasters = input.iconFiles.filter((file) => extname(file).toLowerCase() === '.png' && !isSymbolicIcon(file));
|
|
337
|
+
const suggestedSvg = `data/icons/hicolor/scalable/apps/${input.appId}.svg`;
|
|
338
|
+
if (scalable.length === 0 && rasters.length === 0) {
|
|
339
|
+
throw new Error(`gjsify ship: ${input.target} needs the app's icon and the project declares none. Add a scalable SVG ` +
|
|
340
|
+
`at ${suggestedSvg} (or point \`gjsify.ship.icon\` at one) and \`gjsify ship\` renders every ` +
|
|
341
|
+
'size from it. An app with no icon here ships with the generic one, and nothing on the target ' +
|
|
342
|
+
'OS says so.');
|
|
343
|
+
}
|
|
344
|
+
// The SVG the sizes are rendered from. More than one non-symbolic SVG is
|
|
345
|
+
// possible (a `scalable/` and a stray `data/icons/logo.svg`); the one under a
|
|
346
|
+
// `scalable/` directory is the hicolor convention's answer, else the first by
|
|
347
|
+
// path order — and the choice is printed by `source` either way.
|
|
348
|
+
const svg = scalable.find((file) => /(?:^|[\\/])scalable[\\/]/.test(file)) ?? scalable[0];
|
|
349
|
+
const png = new Map();
|
|
350
|
+
const providedBy = new Map();
|
|
351
|
+
for (const file of rasters) {
|
|
352
|
+
const bytes = new Uint8Array(readFileSync(file));
|
|
353
|
+
const { width, height } = readPngSize(bytes, file);
|
|
354
|
+
if (width !== height) {
|
|
355
|
+
throw new Error(`gjsify ship: ${file} is ${width}×${height}, and an app icon is square on every platform that ` +
|
|
356
|
+
'reads one — Windows and macOS both index icon images by one edge length.');
|
|
357
|
+
}
|
|
358
|
+
const claimed = iconThemeDir(file);
|
|
359
|
+
if (claimed !== `${width}x${width}`) {
|
|
360
|
+
throw new Error(`gjsify ship: ${file} is ${width}×${height} but its path says ${claimed}. The theme looks the file ` +
|
|
361
|
+
'up by the path, so this icon would be served at the wrong size — move it or resize it.');
|
|
362
|
+
}
|
|
363
|
+
if (!input.sizes.includes(width))
|
|
364
|
+
continue;
|
|
365
|
+
const previous = providedBy.get(width);
|
|
366
|
+
if (previous !== undefined) {
|
|
367
|
+
throw new Error(`gjsify ship: ${file} and ${previous} are both ${width} px icons. Keep one.`);
|
|
368
|
+
}
|
|
369
|
+
providedBy.set(width, file);
|
|
370
|
+
png.set(width, bytes);
|
|
371
|
+
}
|
|
372
|
+
const missing = input.sizes.filter((size) => !png.has(size));
|
|
373
|
+
if (missing.length > 0) {
|
|
374
|
+
if (svg === undefined) {
|
|
375
|
+
throw new Error(`gjsify ship: ${input.target} needs the app's icon at ${missing.join(', ')} px and the project ` +
|
|
376
|
+
`ships PNGs only for ${[...png.keys()].sort((a, b) => a - b).join(', ') || 'no size'}. Add a ` +
|
|
377
|
+
`scalable SVG at ${suggestedSvg} and the rest is rendered from it, or add ` +
|
|
378
|
+
`${missing.map((size) => `data/icons/hicolor/${size}x${size}/apps/${input.appId}.png`).join(', ')}.`);
|
|
379
|
+
}
|
|
380
|
+
const rendered = await (input.rasterize ?? rasterizeSvg)({ svg, sizes: missing });
|
|
381
|
+
for (const size of missing) {
|
|
382
|
+
const bytes = rendered.get(size);
|
|
383
|
+
if (bytes === undefined)
|
|
384
|
+
throw new Error(`gjsify ship: ${svg} rendered no ${size} px image.`);
|
|
385
|
+
png.set(size, bytes);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const source = providedBy.size === 0
|
|
389
|
+
? `rendered from ${svg}`
|
|
390
|
+
: missing.length === 0
|
|
391
|
+
? `${providedBy.size} sized PNG(s) the project ships`
|
|
392
|
+
: `${providedBy.size} sized PNG(s) the project ships, the rest rendered from ${svg}`;
|
|
393
|
+
return { source, png };
|
|
394
|
+
}
|
|
395
|
+
/** The sizes in a raster set, ascending — for log lines and error messages. */
|
|
396
|
+
export function iconSizes(icon) {
|
|
397
|
+
return [...icon.png.keys()].sort((a, b) => a - b);
|
|
398
|
+
}
|
|
@@ -234,7 +234,22 @@ function renderAppBundleLauncher(settings, bundleRelPath, layout, runtime) {
|
|
|
234
234
|
'set -e',
|
|
235
235
|
'here=$(cd -- "$(dirname -- "$0")" && pwd)',
|
|
236
236
|
'contents=$(dirname -- "$here")',
|
|
237
|
-
|
|
237
|
+
// NO SYSTEM DEFAULT, which is where this line differs from the prefix form's
|
|
238
|
+
// and is the one place the two must NOT be copies. `/usr/local/share:/usr/share`
|
|
239
|
+
// is the XDG base-directory spec's default for a LINUX system, and macOS has
|
|
240
|
+
// neither: `/usr/local/share` is Intel Homebrew's (Apple Silicon puts it under
|
|
241
|
+
// `/opt/homebrew`) and `/usr/share` holds Apple's own data, no `glib-2.0/schemas`
|
|
242
|
+
// and no `icons/hicolor`. So the baked pair either named nothing or named a
|
|
243
|
+
// package manager the user never opted into — and a `.app` carries its own
|
|
244
|
+
// closure precisely so it depends on neither.
|
|
245
|
+
//
|
|
246
|
+
// `${VAR:+:$VAR}` rather than `${VAR:-<default>}`: a developer who HAS exported
|
|
247
|
+
// `XDG_DATA_DIRS` still keeps it, appended after the bundle's own, and an unset
|
|
248
|
+
// one contributes nothing rather than an empty entry (which GLib reads as the
|
|
249
|
+
// current directory). Same semantics the `.cmd` form gets from `prependVar`,
|
|
250
|
+
// which is why the two now agree — before this they did not, and neither form
|
|
251
|
+
// said so.
|
|
252
|
+
`XDG_DATA_DIRS="$contents/${under(dirs.data)}"\${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}`,
|
|
238
253
|
'export XDG_DATA_DIRS',
|
|
239
254
|
];
|
|
240
255
|
// The two locators a carried runtime needs, and both are read by node-gi ITSELF
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AppIconRasters } from './icons.js';
|
|
1
2
|
import type { HostOs, StagedFile } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* The positional `gjsify ship <os>` takes, in ADR 0024 § A2's spelling.
|
|
@@ -49,6 +50,35 @@ export interface LayoutMetadataInput extends LayoutIdentity {
|
|
|
49
50
|
* architecture; this is the same fact where the file is written.
|
|
50
51
|
*/
|
|
51
52
|
arch: string;
|
|
53
|
+
/** `'app'` or `'cli'`: whether the layout is owed an icon at all. */
|
|
54
|
+
kind: 'app' | 'cli';
|
|
55
|
+
/**
|
|
56
|
+
* The app icon's rasters, or `undefined` for a `kind: 'cli'` project.
|
|
57
|
+
*
|
|
58
|
+
* A REQUIRED KEY that may hold `undefined`, not an optional one, for the
|
|
59
|
+
* reason this interface's own header gives: a call site has to SAY "no icon",
|
|
60
|
+
* and a row that writes one refuses an `app` that says it (`iconFor`). The
|
|
61
|
+
* orchestrator renders these for exactly the rows that declare
|
|
62
|
+
* {@link Layout.icon}, so the two guards meet in the middle and neither can
|
|
63
|
+
* quietly stage an iconless application again.
|
|
64
|
+
*/
|
|
65
|
+
icon: AppIconRasters | undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* What a layout WRITES the app icon as, and the sizes it needs to do it.
|
|
69
|
+
*
|
|
70
|
+
* Declared on the row rather than computed from it because the orchestrator
|
|
71
|
+
* renders the rasters BEFORE the row runs — `resolveAppIcon` is asynchronous (a
|
|
72
|
+
* child GJS) and `Layout.metadata` is not — so the row states its needs up front
|
|
73
|
+
* and receives exactly those sizes. Absent on Linux, deliberately: the hicolor
|
|
74
|
+
* theme carries the SVG the project declared and GTK renders it itself, so there
|
|
75
|
+
* is nothing to convert and nothing to refuse.
|
|
76
|
+
*/
|
|
77
|
+
export interface LayoutIcon {
|
|
78
|
+
/** Square edge lengths the writer embeds. */
|
|
79
|
+
sizes: readonly number[];
|
|
80
|
+
/** What is written, for the refusal: "the Windows launcher's icon". */
|
|
81
|
+
writes: string;
|
|
52
82
|
}
|
|
53
83
|
/** The four places a payload file can belong, as stage-relative directories. */
|
|
54
84
|
export interface LayoutDirs {
|
|
@@ -146,6 +176,8 @@ export interface Layout {
|
|
|
146
176
|
* so a wrapper directory here would become a doubled path there.
|
|
147
177
|
*/
|
|
148
178
|
root: (identity: LayoutIdentity) => string;
|
|
179
|
+
/** The icon this layout writes from the shared rasters, or absent where the theme carries the source. */
|
|
180
|
+
icon?: LayoutIcon;
|
|
149
181
|
/** The four destinations, resolved against one app's names. */
|
|
150
182
|
dirs: (identity: LayoutIdentity) => LayoutDirs;
|
|
151
183
|
/**
|