@gjsify/cli 0.51.1 → 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 +19 -1
- 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/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/settings.js +3 -2
- package/package.json +20 -20
package/lib/utils/ship/layout.js
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
// the data tree is addressed by `appId`. Nothing here is derived from gjsify's own
|
|
22
22
|
// tree, and nothing here has a gjsify-specific default.
|
|
23
23
|
import { posix } from 'node:path';
|
|
24
|
+
import { buildIcns, ICNS_SIZES } from './icns.js';
|
|
25
|
+
import { ICO_SIZES, iconResources } from './ico.js';
|
|
24
26
|
import { buildGuiLauncher } from './pe-launcher.js';
|
|
25
|
-
import { BUNDLE_INFO_PLIST, BUNDLE_PKGINFO, renderInfoPlist, renderPkgInfo } from './plist.js';
|
|
27
|
+
import { BUNDLE_INFO_PLIST, BUNDLE_PKGINFO, BUNDLE_RESOURCES, renderInfoPlist, renderPkgInfo } from './plist.js';
|
|
26
28
|
import { SHARE } from './share-dirs.js';
|
|
27
29
|
/**
|
|
28
30
|
* Characters a `<App>.app` directory name may not contain.
|
|
@@ -63,6 +65,27 @@ function appBundleDir(identity) {
|
|
|
63
65
|
}
|
|
64
66
|
return `${identity.name}.app`;
|
|
65
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The rasters a row writes its icon from, or nothing for a CLI — and a refusal
|
|
70
|
+
* for an application that arrives without them.
|
|
71
|
+
*
|
|
72
|
+
* The second of two guards. `commands/ship.ts` renders the icon for every row
|
|
73
|
+
* that declares {@link Layout.icon}; this is the row itself checking it was
|
|
74
|
+
* given one, so a stage assembled through any other path (a test, a future
|
|
75
|
+
* caller) cannot produce an application whose Start-menu entry or Finder icon is
|
|
76
|
+
* the generic one without saying so. That silence is the measured defect the
|
|
77
|
+
* whole seam exists against (`icons.ts`).
|
|
78
|
+
*/
|
|
79
|
+
function iconFor(input, layout) {
|
|
80
|
+
if (input.kind !== 'app')
|
|
81
|
+
return undefined;
|
|
82
|
+
if (input.icon === undefined) {
|
|
83
|
+
throw new Error(`gjsify ship: the ${layout} layout writes the application's icon and was given none. ` +
|
|
84
|
+
'A `kind: "app"` stage is rendered its icon by `resolveAppIcon` before the layout runs; ' +
|
|
85
|
+
'reaching this row without one is a caller that skipped that step.');
|
|
86
|
+
}
|
|
87
|
+
return input.icon;
|
|
88
|
+
}
|
|
66
89
|
export const LAYOUTS = {
|
|
67
90
|
linux: {
|
|
68
91
|
name: 'linux',
|
|
@@ -102,15 +125,15 @@ export const LAYOUTS = {
|
|
|
102
125
|
'macOS ships no Node, and there is no RELOCATABLE GJS (`build-gtk-runtime-darwin.mjs`: "GJS ' +
|
|
103
126
|
'ships no relocation"), so ADR 0024 § 4 derives Node here. A self-contained bundle needs `@gjsify/node-runtime-darwin-<arch>` and a ' +
|
|
104
127
|
'`--app node` payload (#1354 M0), or `@gjsify/gjs-runtime-darwin-<arch>` (ADR 0024 stage 7).',
|
|
105
|
-
// THE
|
|
106
|
-
// was is the mistake this comment exists to stop being made again. macOS Pango
|
|
107
|
-
//
|
|
108
|
-
// `fontconfig_dep = [] # only used in x11 backend` and Homebrew's gtk4
|
|
109
|
-
// builds `-Dx11-backend=false -Dmacos-backend=true`, while cairo's `quartz`
|
|
128
|
+
// THE LAYOUT WHERE THE ENV VARIABLE IS NOT THE MECHANISM BY DEFAULT, and assuming
|
|
129
|
+
// it was is the mistake this comment exists to stop being made again. macOS Pango
|
|
130
|
+
// resolves CORETEXT unless something chooses otherwise: GTK's own `meson.build`
|
|
131
|
+
// carries `fontconfig_dep = [] # only used in x11 backend` and Homebrew's gtk4
|
|
132
|
+
// formula builds `-Dx11-backend=false -Dmacos-backend=true`, while cairo's `quartz`
|
|
110
133
|
// option defaults to `auto` and auto-enables on darwin — so a `fonts.conf`
|
|
111
|
-
// inside a `.app`
|
|
134
|
+
// inside a `.app` is inert twice over there, wrong Pango backend AND no
|
|
112
135
|
// GTK-side fontconfig. `XDG_DATA_DIRS` still carries the icons and the schemas
|
|
113
|
-
//
|
|
136
|
+
// in that case; it does not carry the faces.
|
|
114
137
|
//
|
|
115
138
|
// What does is `ATSApplicationFontsPath`, emitted into `Info.plist` by
|
|
116
139
|
// `metadata` above — Apple's own per-app activation, and the ORDERING is why it
|
|
@@ -120,14 +143,25 @@ export const LAYOUTS = {
|
|
|
120
143
|
// path in `pangocoretext-fontmap.c`, so a face registered after the font map
|
|
121
144
|
// initialises is not recoverable by poking it. The system activates this key's
|
|
122
145
|
// directory at LAUNCH, before any of the app's code runs.
|
|
146
|
+
//
|
|
147
|
+
// WHAT CHOOSES OTHERWISE is `@gjsify/node-gi`'s loader, which sets
|
|
148
|
+
// `PANGOCAIRO_BACKEND=fc` for the bundled windowing runtime (ADR 0038 § Amendment
|
|
149
|
+
// 3) — so a `--app node` `.app` carrying that bundle reads the faces through
|
|
150
|
+
// fontconfig and registers them like the other two layouts. The declarative route
|
|
151
|
+
// stays emitted all the same: it is what a CoreText map still needs, and nothing
|
|
152
|
+
// here can know which runtime a given `.app` ends up carrying. Both routes named,
|
|
153
|
+
// neither assumed — which is also why `fontGap` prints the probe that answers it
|
|
154
|
+
// for a particular build rather than a claim about the platform.
|
|
123
155
|
fontGap: 'the faces are staged in `share/fonts/<appId>` and `Info.plist` carries `ATSApplicationFontsPath` ' +
|
|
124
|
-
"at it, which is macOS's own per-app activation
|
|
125
|
-
'macOS
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'
|
|
129
|
-
'
|
|
130
|
-
'
|
|
156
|
+
"at it, which is macOS's own per-app activation and the route that matters whenever Pango on " +
|
|
157
|
+
'macOS resolves CoreText by default — which it does on a Homebrew GTK, because that one is not ' +
|
|
158
|
+
'built against fontconfig. A `.app` carrying the bundled GTK runtime reads the same faces through ' +
|
|
159
|
+
"fontconfig instead: `@gjsify/node-gi`'s loader selects it with `PANGOCAIRO_BACKEND=fc` " +
|
|
160
|
+
'(ADR 0038 § Amendment 3), and `initFonts()` from `@gjsify/gtk-host/fonts` registers them as it ' +
|
|
161
|
+
'does on Windows. Either way no branch is needed in your app: a CoreText map answers ' +
|
|
162
|
+
'G_IO_ERROR_NOT_SUPPORTED and the call reports that as declined. Confirm in the shipped bundle ' +
|
|
163
|
+
'with `PangoCairo.FontMap.get_default().list_families()`, and `PANGOCAIRO_BACKEND=bogus` to ' +
|
|
164
|
+
'print which backends it was built with.',
|
|
131
165
|
launcherExt: '',
|
|
132
166
|
root: appBundleDir,
|
|
133
167
|
// Apple's, all four. `Contents/MacOS` holds executables, `Contents/Resources`
|
|
@@ -148,18 +182,40 @@ export const LAYOUTS = {
|
|
|
148
182
|
// nothing tells LaunchServices which file under `Contents/MacOS` to exec,
|
|
149
183
|
// and a `*.app` with no `Info.plist` is a folder with a suffix — which is
|
|
150
184
|
// exactly what M1 staged. `plist.ts` carries the per-key citations.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
//
|
|
186
|
+
// AND THE ICON, the third file — `Contents/Resources/<binaryName>.icns`,
|
|
187
|
+
// named by `CFBundleIconFile`. Both halves or neither: the key without the
|
|
188
|
+
// file is a dangling reference the Finder resolves to the generic icon, and
|
|
189
|
+
// the file without the key is bytes nothing reads. Measured absent on the
|
|
190
|
+
// released 0.8.0 bundle of one app (macOS 15.7, 2026-09-13): no key, no
|
|
191
|
+
// `.icns`, generic icon — the twin of the Windows defect `pe-launcher.ts`
|
|
192
|
+
// records.
|
|
193
|
+
icon: { sizes: ICNS_SIZES, writes: 'the macOS bundle icon (`Contents/Resources/<binaryName>.icns`)' },
|
|
194
|
+
metadata: (input, payload) => {
|
|
195
|
+
const icon = iconFor(input, 'darwin');
|
|
196
|
+
const iconFile = icon === undefined ? undefined : `${input.binaryName}.icns`;
|
|
197
|
+
return [
|
|
198
|
+
{
|
|
199
|
+
path: `${appBundleDir(input)}/${BUNDLE_INFO_PLIST}`,
|
|
200
|
+
mode: 0o644,
|
|
201
|
+
source: { kind: 'text', text: renderInfoPlist(input, darwinFontsPath(input, payload), iconFile) },
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
path: `${appBundleDir(input)}/${BUNDLE_PKGINFO}`,
|
|
205
|
+
mode: 0o644,
|
|
206
|
+
source: { kind: 'text', text: renderPkgInfo() },
|
|
207
|
+
},
|
|
208
|
+
...(icon === undefined || iconFile === undefined
|
|
209
|
+
? []
|
|
210
|
+
: [
|
|
211
|
+
{
|
|
212
|
+
path: `${appBundleDir(input)}/${BUNDLE_RESOURCES}/${iconFile}`,
|
|
213
|
+
mode: 0o644,
|
|
214
|
+
source: { kind: 'bytes', data: buildIcns(icon) },
|
|
215
|
+
},
|
|
216
|
+
]),
|
|
217
|
+
];
|
|
218
|
+
},
|
|
163
219
|
// The two architectures macOS runs on, which is also exactly what
|
|
164
220
|
// `@gjsify/gtk-runtime-darwin-*` and `@gjsify/node-runtime-darwin-*` are
|
|
165
221
|
// published for. Nothing upstream blocks a third; there is no third.
|
|
@@ -227,8 +283,10 @@ export const LAYOUTS = {
|
|
|
227
283
|
// warning, which is exactly what the probe family did BEFORE the call.
|
|
228
284
|
fontGap: 'the faces are staged in `share/fonts/<appId>` and the launcher exports GJSIFY_FONT_DIR at it, ' +
|
|
229
285
|
'but WINDOWS HAS NO DECLARATIVE FONT ACTIVATION and nothing here reaches a bundled face on its ' +
|
|
230
|
-
'own: GTK4
|
|
231
|
-
'fontconfig or from GDI
|
|
286
|
+
'own: GTK4 resolves pangowin32, whose font map is populated from DirectWrite and never from ' +
|
|
287
|
+
'fontconfig or from GDI, unless something selects otherwise — which the bundled GTK runtime does ' +
|
|
288
|
+
'(`PANGOCAIRO_BACKEND=fc`, ADR 0038 § Amendment 3), and an app may. MEASURED on the DirectWrite ' +
|
|
289
|
+
'map, Windows 11 / GTK 4.22.4, rather than inferred (ADR 0038 ' +
|
|
232
290
|
'W1-W5): a FONTCONFIG_FILE naming the staged directory moves the default font map by nothing ' +
|
|
233
291
|
'even when it is the ONLY configuration present — that map does not read fontconfig, and exposes ' +
|
|
234
292
|
'no config_changed or set_config with which to make it — while add_font_file on the same map a ' +
|
|
@@ -272,15 +330,31 @@ export const LAYOUTS = {
|
|
|
272
330
|
// refusal, about our own file). `arches.only` already says there is no
|
|
273
331
|
// other Windows architecture; a stage for one stays assemblable and stays
|
|
274
332
|
// unpackable, which is the split `Layout.runtimeGap` draws everywhere else.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
333
|
+
//
|
|
334
|
+
// THE ICON RIDES INSIDE THAT PROGRAM. A shortcut with no icon of its own
|
|
335
|
+
// shows its target's, and Explorer, the Start menu and the taskbar all read
|
|
336
|
+
// the target's out of its resource directory — so the `.exe` carries the
|
|
337
|
+
// six sizes as `RT_ICON`/`RT_GROUP_ICON` (`ico.ts`, `pe-launcher.ts`) and
|
|
338
|
+
// there is no `.ico` file in the program directory to fall out of sync.
|
|
339
|
+
icon: { sizes: ICO_SIZES, writes: 'the Windows launcher icon (`.rsrc` of `<binaryName>.exe`)' },
|
|
340
|
+
metadata: (input) => {
|
|
341
|
+
const icon = iconFor(input, 'windows');
|
|
342
|
+
return input.arch === 'x64'
|
|
343
|
+
? [
|
|
344
|
+
{
|
|
345
|
+
path: windowsGuiLauncherPath(input),
|
|
346
|
+
mode: 0o755,
|
|
347
|
+
source: {
|
|
348
|
+
kind: 'bytes',
|
|
349
|
+
data: buildGuiLauncher({
|
|
350
|
+
logLeaf: windowsLaunchLogLeaf(input),
|
|
351
|
+
...(icon === undefined ? {} : { icon: iconResources(icon) }),
|
|
352
|
+
}),
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
]
|
|
356
|
+
: [];
|
|
357
|
+
},
|
|
284
358
|
// ONE, and the blocker is a project we do not own. `wingtk/gvsbuild`
|
|
285
359
|
// hardcodes `self.platform = "x64"` in `utils/base_project.py` and its last
|
|
286
360
|
// five releases publish exactly two assets each, both x64 — so there is no
|
package/lib/utils/ship/msi.d.ts
CHANGED
|
@@ -60,7 +60,7 @@ export declare function msiProductVersion(version: string): string;
|
|
|
60
60
|
* agree still differ. `renderWxs` asserts uniqueness anyway rather than trusting
|
|
61
61
|
* 48 bits, because the failure of a collision is two files fighting over one row.
|
|
62
62
|
*/
|
|
63
|
-
export declare function msiIdentifier(prefix: string, path: string): string;
|
|
63
|
+
export declare function msiIdentifier(prefix: string, path: string, suffix?: string): string;
|
|
64
64
|
/** One payload file, as the installer will address it. */
|
|
65
65
|
export interface MsiFile {
|
|
66
66
|
/** Program-directory-relative path, POSIX-separated. */
|
package/lib/utils/ship/msi.js
CHANGED
|
@@ -175,11 +175,13 @@ export function msiProductVersion(version) {
|
|
|
175
175
|
* agree still differ. `renderWxs` asserts uniqueness anyway rather than trusting
|
|
176
176
|
* 48 bits, because the failure of a collision is two files fighting over one row.
|
|
177
177
|
*/
|
|
178
|
-
export function msiIdentifier(prefix, path) {
|
|
178
|
+
export function msiIdentifier(prefix, path, suffix = '') {
|
|
179
179
|
const digest = createHash('sha1').update(new TextEncoder().encode(path)).digest('hex').slice(0, 12);
|
|
180
180
|
const safe = path.replace(/[^A-Za-z0-9_.]/g, '_');
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
// The suffix is part of the 72, or an `Icon` id (which has to END in `.exe`)
|
|
182
|
+
// built from a long path would be the one identifier over the limit.
|
|
183
|
+
const room = 72 - prefix.length - 2 - digest.length - suffix.length;
|
|
184
|
+
return `${prefix}_${safe.length > room ? safe.slice(safe.length - room) : safe}_${digest}${suffix}`;
|
|
183
185
|
}
|
|
184
186
|
/**
|
|
185
187
|
* What Windows Installer will not hold in a `FileName`/`DefaultDir` cell.
|
|
@@ -264,9 +266,27 @@ export function renderWxs(input) {
|
|
|
264
266
|
identifiers.set(id, what);
|
|
265
267
|
return id;
|
|
266
268
|
};
|
|
269
|
+
// THE ICON IS THE LAUNCHER ITSELF. The Icon table holds a file — an `.ico`,
|
|
270
|
+
// or an `.exe`/`.dll` the installer reads an icon OUT OF by `IconIndex` — and
|
|
271
|
+
// the stub already carries the app's icon in its resource directory
|
|
272
|
+
// (`pe-launcher.ts`). Naming the launcher as the icon source means one file
|
|
273
|
+
// holds the pixels the shortcut, Add/Remove Programs and Explorer all show,
|
|
274
|
+
// and nothing in `ship/msi/` can drift from what is installed.
|
|
275
|
+
//
|
|
276
|
+
// WHY THE MSI NEEDS ITS OWN ROW when the `.exe` already has the icon. A
|
|
277
|
+
// shortcut with no `Icon_` shows its target's icon once the target exists;
|
|
278
|
+
// this one is ADVERTISED, and Windows Installer's Shortcut table documents the
|
|
279
|
+
// `Icon_` column as the icon shown for an advertised shortcut — whose target
|
|
280
|
+
// is a descriptor the shell resolves through the installer, not the file. And
|
|
281
|
+
// `ARPPRODUCTICON` is the only route to an icon in Add/Remove Programs at all;
|
|
282
|
+
// the `.exe` is never consulted there. Two consumers the embedded icon cannot
|
|
283
|
+
// reach, one row. Windows Installer requires the Icon NAME to carry the
|
|
284
|
+
// extension of the file it holds, so the id ends in `.exe`.
|
|
285
|
+
const launcherIconId = claim(msiIdentifier('i', launcher, '.exe'), 'the launcher icon');
|
|
267
286
|
const lines = [];
|
|
268
287
|
const componentIds = [];
|
|
269
288
|
let launcherFound = false;
|
|
289
|
+
let launcherSource;
|
|
270
290
|
const emitDir = (node, indent) => {
|
|
271
291
|
for (const file of node.files) {
|
|
272
292
|
const componentId = claim(msiIdentifier('c', file.path), file.path);
|
|
@@ -289,12 +309,13 @@ export function renderWxs(input) {
|
|
|
289
309
|
// with it.
|
|
290
310
|
if (file.path === launcher) {
|
|
291
311
|
launcherFound = true;
|
|
312
|
+
launcherSource = file.source;
|
|
292
313
|
lines.push(`${indent} <File Id="${fileId}" Name="${name}" ` +
|
|
293
314
|
`Source="${xmlEscape(file.source)}" KeyPath="yes">`);
|
|
294
315
|
lines.push(`${indent} <Shortcut Id="${claim('s_startmenu', 'the Start-Menu shortcut')}" ` +
|
|
295
316
|
`Directory="ProgramMenuFolder" Name="${xmlEscape(settings.name)}" ` +
|
|
296
317
|
`WorkingDirectory="INSTALLDIR" Description="${xmlEscape(settings.summary)}" ` +
|
|
297
|
-
`Advertise="yes" />`);
|
|
318
|
+
`Icon="${launcherIconId}" IconIndex="0" Advertise="yes" />`);
|
|
298
319
|
lines.push(`${indent} </File>`);
|
|
299
320
|
}
|
|
300
321
|
else {
|
|
@@ -317,7 +338,7 @@ export function renderWxs(input) {
|
|
|
317
338
|
if (componentIds.length === 0) {
|
|
318
339
|
throw new Error('gjsify ship: the windows payload is empty, so there is nothing for an `.msi` to install.');
|
|
319
340
|
}
|
|
320
|
-
if (!launcherFound) {
|
|
341
|
+
if (!launcherFound || launcherSource === undefined) {
|
|
321
342
|
throw new Error(`gjsify ship: the windows payload carries no "${launcher}" at the root of the program directory, ` +
|
|
322
343
|
'so the installer would have nothing to point a Start-Menu shortcut at — an application a user ' +
|
|
323
344
|
'installs and\n cannot start. `windows-dir` puts the launcher there; a payload without one ' +
|
|
@@ -343,6 +364,10 @@ export function renderWxs(input) {
|
|
|
343
364
|
// downloads rather than an `.msi` plus loose `.cab` siblings that a user
|
|
344
365
|
// will separate from it.
|
|
345
366
|
' <Media Id="1" Cabinet="app.cab" EmbedCab="yes" />',
|
|
367
|
+
// The launcher, a second time, as the Icon table's binary — see
|
|
368
|
+
// `launcherIconId` above for why the same file is both.
|
|
369
|
+
` <Icon Id="${launcherIconId}" SourceFile="${xmlEscape(launcherSource)}" />`,
|
|
370
|
+
` <Property Id="ARPPRODUCTICON" Value="${launcherIconId}" />`,
|
|
346
371
|
...about,
|
|
347
372
|
' <Directory Id="TARGETDIR" Name="SourceDir">',
|
|
348
373
|
' <Directory Id="ProgramFiles64Folder">',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type IconResources } from './ico.js';
|
|
1
2
|
/**
|
|
2
3
|
* `IMAGE_SUBSYSTEM_WINDOWS_GUI` — the one field this whole file exists to set.
|
|
3
4
|
*
|
|
@@ -17,7 +18,24 @@ export interface GuiLauncherInput {
|
|
|
17
18
|
* Files\<App>` is read-only for the user who runs the app.
|
|
18
19
|
*/
|
|
19
20
|
logLeaf: string;
|
|
21
|
+
/**
|
|
22
|
+
* The application icon as PE resources, or absent for a launcher with none.
|
|
23
|
+
*
|
|
24
|
+
* Absent is what a `kind: 'cli'` project gets; an `app` always has one, and
|
|
25
|
+
* the layout refuses to stage otherwise (`layout.ts`, `LAYOUTS.windows`), so
|
|
26
|
+
* "no `.rsrc`" is never how an application quietly loses its icon again.
|
|
27
|
+
*/
|
|
28
|
+
icon?: IconResources;
|
|
20
29
|
}
|
|
30
|
+
/** `IMAGE_DIRECTORY_ENTRY_RESOURCE` — the data directory slot the `.rsrc` section is named in. */
|
|
31
|
+
export declare const PE_DIRECTORY_RESOURCE = 2;
|
|
32
|
+
/**
|
|
33
|
+
* The language every resource is filed under: `en-US`, 0x0409, which is what
|
|
34
|
+
* `rc.exe` and `windres` emit when a script names none. Icon lookup does not
|
|
35
|
+
* depend on it — `FindResource` falls back through neutral to the first language
|
|
36
|
+
* present — so one language is enough and it is the conventional one.
|
|
37
|
+
*/
|
|
38
|
+
export declare const RESOURCE_LANGUAGE = 1033;
|
|
21
39
|
/**
|
|
22
40
|
* The GUI-subsystem launcher, as a complete PE32+ image.
|
|
23
41
|
*
|
|
@@ -51,6 +51,22 @@
|
|
|
51
51
|
// `objdump -x` (binutils' `pei-x86-64`) and `.github/ship-oracle/verify-program-dir.py`
|
|
52
52
|
// both parse the emitted file, and `tests/e2e/ship-windows` drives the emitted
|
|
53
53
|
// image through the same `readBinaryArch` that reads a real `node.exe`.
|
|
54
|
+
//
|
|
55
|
+
// IT CARRIES THE APP'S ICON, AND THAT IS WHY IT IS THE FILE THE SHORTCUT NAMES.
|
|
56
|
+
// Explorer, the Start menu, the taskbar, Alt-Tab and a pinned entry all read an
|
|
57
|
+
// application's icon out of the EXECUTABLE's resource directory; the running
|
|
58
|
+
// window's own icon comes from GTK at runtime and is the one place the icon
|
|
59
|
+
// showed before this section existed. Measured on Windows 11 against Learn6502
|
|
60
|
+
// 0.8.0: taskbar button iconed, Start-menu entry the generic blank document —
|
|
61
|
+
// the shortcut asked this file, and this file had nothing. So the image gets a
|
|
62
|
+
// third section, `.rsrc`, holding the three-level resource tree Windows expects
|
|
63
|
+
// (type → id → language) with one `RT_GROUP_ICON` naming `n` `RT_ICON` entries
|
|
64
|
+
// (`ico.ts` builds both payloads). Three things in it are silent mistakes and
|
|
65
|
+
// are therefore written down: a data entry's `OffsetToData` is an RVA and not a
|
|
66
|
+
// file offset; directory entries must be SORTED ascending by id, because the
|
|
67
|
+
// loader binary-searches them; and the group's entries name resources by id, not
|
|
68
|
+
// by the file offset an `.ico` carries.
|
|
69
|
+
import { RT_GROUP_ICON, RT_ICON } from './ico.js';
|
|
54
70
|
/**
|
|
55
71
|
* `IMAGE_SUBSYSTEM_WINDOWS_GUI` — the one field this whole file exists to set.
|
|
56
72
|
*
|
|
@@ -228,6 +244,88 @@ const CREATE_ALWAYS = 2;
|
|
|
228
244
|
const FILE_ATTRIBUTE_NORMAL = 0x80;
|
|
229
245
|
/** Capacities in WCHARs. `cmdBuf` holds the whole assembled command line. */
|
|
230
246
|
const BUFFERS = { sysBuf: 264, selfBuf: 520, tmpBuf: 264, logBuf: 600, cmdBuf: 4096 };
|
|
247
|
+
/** `IMAGE_DIRECTORY_ENTRY_RESOURCE` — the data directory slot the `.rsrc` section is named in. */
|
|
248
|
+
export const PE_DIRECTORY_RESOURCE = 2;
|
|
249
|
+
/**
|
|
250
|
+
* The language every resource is filed under: `en-US`, 0x0409, which is what
|
|
251
|
+
* `rc.exe` and `windres` emit when a script names none. Icon lookup does not
|
|
252
|
+
* depend on it — `FindResource` falls back through neutral to the first language
|
|
253
|
+
* present — so one language is enough and it is the conventional one.
|
|
254
|
+
*/
|
|
255
|
+
export const RESOURCE_LANGUAGE = 0x0409;
|
|
256
|
+
/**
|
|
257
|
+
* The `.rsrc` section: the resource tree, its data entries, and the payloads.
|
|
258
|
+
*
|
|
259
|
+
* ONE FIXED SHAPE, laid out top to bottom so every offset is arithmetic:
|
|
260
|
+
*
|
|
261
|
+
* root directory 2 entries: RT_ICON (3), RT_GROUP_ICON (14) — ascending
|
|
262
|
+
* RT_ICON directory n entries: ids 1..n
|
|
263
|
+
* n language dirs 1 entry each: 0x0409 → a data entry
|
|
264
|
+
* RT_GROUP_ICON dir 1 entry: id 1
|
|
265
|
+
* 1 language dir 1 entry: 0x0409 → a data entry
|
|
266
|
+
* n + 1 data entries OffsetToData is an RVA into this section's payloads
|
|
267
|
+
* payloads the n PNGs, then the group, each 8-aligned
|
|
268
|
+
*
|
|
269
|
+
* `sectionRva` is what turns a payload's position into the RVA the data entry
|
|
270
|
+
* needs; every other offset in the tree is relative to the section start, with
|
|
271
|
+
* the high bit set on an entry that points at a subdirectory. Both conventions
|
|
272
|
+
* are the format's and both are exercised by `verify-program-dir.py`, which walks
|
|
273
|
+
* this tree with CPython and reassembles the `.ico`.
|
|
274
|
+
*/
|
|
275
|
+
function buildResourceSection(icon, sectionRva) {
|
|
276
|
+
const n = icon.icons.length;
|
|
277
|
+
const DIRECTORY = 16;
|
|
278
|
+
const ENTRY = 8;
|
|
279
|
+
const DATA_ENTRY = 16;
|
|
280
|
+
const SUBDIRECTORY = 0x80000000;
|
|
281
|
+
const rootAt = 0;
|
|
282
|
+
const iconDirAt = rootAt + DIRECTORY + 2 * ENTRY;
|
|
283
|
+
const iconLangAt = (index) => iconDirAt + DIRECTORY + n * ENTRY + index * (DIRECTORY + ENTRY);
|
|
284
|
+
const groupDirAt = iconLangAt(n);
|
|
285
|
+
const groupLangAt = groupDirAt + DIRECTORY + ENTRY;
|
|
286
|
+
const dataEntriesAt = groupLangAt + DIRECTORY + ENTRY;
|
|
287
|
+
const dataEntryAt = (index) => dataEntriesAt + index * DATA_ENTRY;
|
|
288
|
+
let payloadAt = align(dataEntryAt(n + 1), 8);
|
|
289
|
+
const payloads = [];
|
|
290
|
+
for (const bytes of [...icon.icons, icon.group]) {
|
|
291
|
+
payloads.push({ at: payloadAt, bytes });
|
|
292
|
+
payloadAt = align(payloadAt + bytes.length, 8);
|
|
293
|
+
}
|
|
294
|
+
const section = Buffer.alloc(payloadAt);
|
|
295
|
+
const directory = (at, count) => {
|
|
296
|
+
// Characteristics, TimeDateStamp, Major/MinorVersion: all zero; then the
|
|
297
|
+
// named-entry count (none here) and the id-entry count.
|
|
298
|
+
section.writeUInt16LE(0, at + 12);
|
|
299
|
+
section.writeUInt16LE(count, at + 14);
|
|
300
|
+
};
|
|
301
|
+
const entry = (at, id, target, subdirectory) => {
|
|
302
|
+
section.writeUInt32LE(id, at);
|
|
303
|
+
section.writeUInt32LE(subdirectory ? (SUBDIRECTORY | target) >>> 0 : target, at + 4);
|
|
304
|
+
};
|
|
305
|
+
const dataEntry = (index, payload) => {
|
|
306
|
+
section.writeUInt32LE(sectionRva + payload.at, dataEntryAt(index)); // an RVA, not a file offset
|
|
307
|
+
section.writeUInt32LE(payload.bytes.length, dataEntryAt(index) + 4);
|
|
308
|
+
// CodePage and Reserved stay zero.
|
|
309
|
+
};
|
|
310
|
+
directory(rootAt, 2);
|
|
311
|
+
entry(rootAt + DIRECTORY, RT_ICON, iconDirAt, true);
|
|
312
|
+
entry(rootAt + DIRECTORY + ENTRY, RT_GROUP_ICON, groupDirAt, true);
|
|
313
|
+
directory(iconDirAt, n);
|
|
314
|
+
for (let index = 0; index < n; index++) {
|
|
315
|
+
entry(iconDirAt + DIRECTORY + index * ENTRY, index + 1, iconLangAt(index), true);
|
|
316
|
+
directory(iconLangAt(index), 1);
|
|
317
|
+
entry(iconLangAt(index) + DIRECTORY, RESOURCE_LANGUAGE, dataEntryAt(index), false);
|
|
318
|
+
dataEntry(index, payloads[index]);
|
|
319
|
+
}
|
|
320
|
+
directory(groupDirAt, 1);
|
|
321
|
+
entry(groupDirAt + DIRECTORY, 1, groupLangAt, true);
|
|
322
|
+
directory(groupLangAt, 1);
|
|
323
|
+
entry(groupLangAt + DIRECTORY, RESOURCE_LANGUAGE, dataEntryAt(n), false);
|
|
324
|
+
dataEntry(n, payloads[n]);
|
|
325
|
+
for (const payload of payloads)
|
|
326
|
+
section.set(payload.bytes, payload.at);
|
|
327
|
+
return new Uint8Array(section);
|
|
328
|
+
}
|
|
231
329
|
/**
|
|
232
330
|
* The stub's instruction stream.
|
|
233
331
|
*
|
|
@@ -601,7 +699,18 @@ export function buildGuiLauncher(input) {
|
|
|
601
699
|
const headerSize = FILE_ALIGNMENT;
|
|
602
700
|
const textOffset = headerSize;
|
|
603
701
|
const dataOffset = textOffset + textRaw;
|
|
604
|
-
|
|
702
|
+
// The icon's section, APPENDED after `.data` so no existing RVA or file
|
|
703
|
+
// offset moves — inserting it would mean re-resolving every RIP-relative
|
|
704
|
+
// fixup for a file the code never touches.
|
|
705
|
+
const rsrcRva = dataRva + align(dataSize, SECTION_ALIGNMENT);
|
|
706
|
+
const rsrc = input.icon === undefined ? undefined : buildResourceSection(input.icon, rsrcRva);
|
|
707
|
+
const rsrcRaw = rsrc === undefined ? 0 : align(rsrc.length, FILE_ALIGNMENT);
|
|
708
|
+
const rsrcOffset = dataOffset + dataRaw;
|
|
709
|
+
const sectionCount = rsrc === undefined ? 2 : 3;
|
|
710
|
+
const lastRva = rsrc === undefined
|
|
711
|
+
? dataRva + align(dataSize, SECTION_ALIGNMENT)
|
|
712
|
+
: rsrcRva + align(rsrc.length, SECTION_ALIGNMENT);
|
|
713
|
+
const image = Buffer.alloc(rsrcOffset + rsrcRaw);
|
|
605
714
|
// ── DOS header ───────────────────────────────────────────────────────────
|
|
606
715
|
image.write('MZ', 0, 'ascii');
|
|
607
716
|
image.write('This program cannot be run in DOS mode.\r\r\n$', 0x40, 'ascii');
|
|
@@ -610,7 +719,7 @@ export function buildGuiLauncher(input) {
|
|
|
610
719
|
image.write('PE\0\0', PE_OFFSET, 'ascii');
|
|
611
720
|
const coff = PE_OFFSET + 4;
|
|
612
721
|
image.writeUInt16LE(MACHINE_AMD64, coff);
|
|
613
|
-
image.writeUInt16LE(
|
|
722
|
+
image.writeUInt16LE(sectionCount, coff + 2); // NumberOfSections
|
|
614
723
|
image.writeUInt32LE(0, coff + 4); // TimeDateStamp — zero, so the image is reproducible
|
|
615
724
|
image.writeUInt16LE(240, coff + 16); // SizeOfOptionalHeader (112 + 16 * 8)
|
|
616
725
|
// EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE | RELOCS_STRIPPED. The stub is
|
|
@@ -622,7 +731,7 @@ export function buildGuiLauncher(input) {
|
|
|
622
731
|
image.writeUInt16LE(0x20b, opt); // PE32+
|
|
623
732
|
image.writeUInt8(14, opt + 2); // MajorLinkerVersion — cosmetic
|
|
624
733
|
image.writeUInt32LE(textRaw, opt + 4); // SizeOfCode
|
|
625
|
-
image.writeUInt32LE(dataRaw, opt + 8); // SizeOfInitializedData
|
|
734
|
+
image.writeUInt32LE(dataRaw + rsrcRaw, opt + 8); // SizeOfInitializedData — `.data` and `.rsrc` are both
|
|
626
735
|
image.writeUInt32LE(textRva, opt + 16); // AddressOfEntryPoint
|
|
627
736
|
image.writeUInt32LE(textRva, opt + 20); // BaseOfCode
|
|
628
737
|
image.writeBigUInt64LE(IMAGE_BASE, opt + 24);
|
|
@@ -630,7 +739,7 @@ export function buildGuiLauncher(input) {
|
|
|
630
739
|
image.writeUInt32LE(FILE_ALIGNMENT, opt + 36);
|
|
631
740
|
image.writeUInt16LE(6, opt + 40); // MajorOperatingSystemVersion
|
|
632
741
|
image.writeUInt16LE(6, opt + 48); // MajorSubsystemVersion
|
|
633
|
-
image.writeUInt32LE(
|
|
742
|
+
image.writeUInt32LE(lastRva, opt + 56); // SizeOfImage — through the last section, whichever that is
|
|
634
743
|
image.writeUInt32LE(headerSize, opt + 60); // SizeOfHeaders
|
|
635
744
|
image.writeUInt16LE(PE_SUBSYSTEM_GUI, opt + 68);
|
|
636
745
|
image.writeUInt16LE(0x0100 | 0x8000, opt + 70); // NX_COMPAT | TERMINAL_SERVER_AWARE
|
|
@@ -644,6 +753,12 @@ export function buildGuiLauncher(input) {
|
|
|
644
753
|
image.writeUInt32LE(40, directories + 1 * 8 + 4);
|
|
645
754
|
image.writeUInt32LE(rvaOf('iat.GetModuleFileNameW'), directories + 12 * 8); // IAT
|
|
646
755
|
image.writeUInt32LE((IMPORTS.length + 1) * 8, directories + 12 * 8 + 4);
|
|
756
|
+
if (rsrc !== undefined) {
|
|
757
|
+
// RESOURCE: the whole section, RVA and byte size. Without this slot the
|
|
758
|
+
// section is bytes the loader maps and the shell never looks at.
|
|
759
|
+
image.writeUInt32LE(rsrcRva, directories + PE_DIRECTORY_RESOURCE * 8);
|
|
760
|
+
image.writeUInt32LE(rsrc.length, directories + PE_DIRECTORY_RESOURCE * 8 + 4);
|
|
761
|
+
}
|
|
647
762
|
// ── section table ────────────────────────────────────────────────────────
|
|
648
763
|
const sections = directories + 16 * 8;
|
|
649
764
|
const writeSection = (index, name, rva, virtualSize, raw, rawSize, characteristics) => {
|
|
@@ -657,7 +772,13 @@ export function buildGuiLauncher(input) {
|
|
|
657
772
|
};
|
|
658
773
|
writeSection(0, '.text', textRva, codeSize, textOffset, textRaw, 0x60000020);
|
|
659
774
|
writeSection(1, '.data', dataRva, dataSize, dataOffset, dataRaw, 0xc0000040);
|
|
775
|
+
// INITIALIZED_DATA | MEM_READ and nothing else: resources are read by the
|
|
776
|
+
// shell and the loader, never written and never executed.
|
|
777
|
+
if (rsrc !== undefined)
|
|
778
|
+
writeSection(2, '.rsrc', rsrcRva, rsrc.length, rsrcOffset, rsrcRaw, 0x40000040);
|
|
660
779
|
image.set(text, textOffset);
|
|
661
780
|
image.set(data, dataOffset);
|
|
781
|
+
if (rsrc !== undefined)
|
|
782
|
+
image.set(rsrc, rsrcOffset);
|
|
662
783
|
return new Uint8Array(image);
|
|
663
784
|
}
|
package/lib/utils/ship/plan.d.ts
CHANGED
|
@@ -66,40 +66,7 @@ export declare function planOverlay(settings: ShipSettings, format: FormatDescri
|
|
|
66
66
|
* version can no longer reach it.
|
|
67
67
|
*/
|
|
68
68
|
export declare function assertOverlayIsLicensed(formatId: FormatId, binaryName: string, overlay: readonly StagedFile[]): void;
|
|
69
|
-
|
|
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.
|
|
101
|
-
*/
|
|
102
|
-
export declare function iconThemeDir(iconPath: string): string;
|
|
69
|
+
export { iconThemeDir, isSymbolicIcon } from './icons.js';
|
|
103
70
|
/**
|
|
104
71
|
* A native module inside the payload has to keep its executable bit.
|
|
105
72
|
*
|
package/lib/utils/ship/plan.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// its own inputs can only be tested by building a real project.
|
|
5
5
|
import { renderDebianChangelog } from './changelog.js';
|
|
6
6
|
import { gzipDeterministic, POLICY_MAX_COMPRESSION } from './gzip.js';
|
|
7
|
+
import { iconThemeDir, isSymbolicIcon } from './icons.js';
|
|
7
8
|
import { renderMimePackage } from './mime.js';
|
|
8
9
|
import { SHARE } from './share-dirs.js';
|
|
9
10
|
import { basename, extname, posix } from 'node:path';
|
|
@@ -267,62 +268,12 @@ function planIcons(settings) {
|
|
|
267
268
|
}
|
|
268
269
|
return out;
|
|
269
270
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
* section — `Context=Applications`, `Size=16`, `MinSize=8`, `MaxSize=512`,
|
|
277
|
-
* `Type=Scalable` — against the scalable row's `Size=128`. The freedesktop icon
|
|
278
|
-
* theme specification has no notion of `symbolic` at all: a theme expresses it by
|
|
279
|
-
* giving the directory a section, which is exactly why reading the EXTENSION
|
|
280
|
-
* cannot see it, and why this question has to be asked before the size one.
|
|
281
|
-
*
|
|
282
|
-
* TWO SIGNALS, the same pair `iconThemeDir` already reads for a size: the
|
|
283
|
-
* directory an author put the file in, and the name they gave it. GTK resolves a
|
|
284
|
-
* symbolic icon by the `-symbolic` name suffix, so an author who wrote the name
|
|
285
|
-
* has said as much as one who wrote the path.
|
|
286
|
-
*
|
|
287
|
-
* SVG ONLY, because the symbolic directory is declared `Type=Scalable`: the
|
|
288
|
-
* raster form GTK's own `gtk-encode-symbolic-svg` produces
|
|
289
|
-
* (`<name>-symbolic.symbolic.png`) is installed at a SIZE instead. So a PNG keeps
|
|
290
|
-
* answering the size question, and one that cannot answer it stays refused rather
|
|
291
|
-
* than quietly becoming a scalable icon that does not scale.
|
|
292
|
-
*/
|
|
293
|
-
export function isSymbolicIcon(iconPath) {
|
|
294
|
-
if (extname(iconPath).toLowerCase() !== '.svg')
|
|
295
|
-
return false;
|
|
296
|
-
if (/(?:^|[\\/])symbolic[\\/]/.test(iconPath))
|
|
297
|
-
return true;
|
|
298
|
-
return basename(iconPath, extname(iconPath)).endsWith('-symbolic');
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* The hicolor subdirectory for an icon: the `symbolic` CONTEXT, else `scalable`
|
|
302
|
-
* for an SVG, else the pixel size read from a `<n>x<n>` path component or a
|
|
303
|
-
* trailing number in the filename.
|
|
304
|
-
*
|
|
305
|
-
* Named for the theme DIRECTORY rather than a size from the moment the first of
|
|
306
|
-
* those answers stopped being one: `iconSizeDir` returning `symbolic` would be a
|
|
307
|
-
* name every caller has to read past.
|
|
308
|
-
*/
|
|
309
|
-
export function iconThemeDir(iconPath) {
|
|
310
|
-
if (isSymbolicIcon(iconPath))
|
|
311
|
-
return 'symbolic';
|
|
312
|
-
if (extname(iconPath).toLowerCase() === '.svg')
|
|
313
|
-
return 'scalable';
|
|
314
|
-
const square = /(?:^|[\\/])(\d{1,4})x\1(?:[\\/]|$)/.exec(iconPath);
|
|
315
|
-
if (square)
|
|
316
|
-
return `${square[1]}x${square[1]}`;
|
|
317
|
-
const tokens = basename(iconPath, extname(iconPath)).split(/[-_.]/);
|
|
318
|
-
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
319
|
-
const token = tokens[i];
|
|
320
|
-
if (token !== undefined && /^\d{1,4}$/.test(token))
|
|
321
|
-
return `${token}x${token}`;
|
|
322
|
-
}
|
|
323
|
-
throw new Error(`gjsify ship: cannot tell what size ${iconPath} is. ` +
|
|
324
|
-
'Put it in a `<size>x<size>/` directory, end its name with the size (`icon-128.png`), or ship an SVG.');
|
|
325
|
-
}
|
|
271
|
+
// The two icon questions the plan asks — symbolic or sized, and which theme
|
|
272
|
+
// directory — are answered by `icons.ts` since the icon became a CONVERTED
|
|
273
|
+
// artifact on two of the three layouts, and re-exported here so the plan's
|
|
274
|
+
// readers keep one import. One definition: a copy here would be the one that
|
|
275
|
+
// stages `symbolic/` under a size the day the other changes.
|
|
276
|
+
export { iconThemeDir, isSymbolicIcon } from './icons.js';
|
|
326
277
|
/** A staged path must stay under the prefix — no absolute paths, no `..`. */
|
|
327
278
|
function assertInsidePrefix(dest) {
|
|
328
279
|
const normalised = posix.normalize(dest);
|