@gjsify/cli 0.45.0 → 0.46.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 +142 -134
- package/lib/commands/ship.js +89 -76
- package/lib/commands/upgrade.d.ts +23 -0
- package/lib/commands/upgrade.js +95 -12
- package/lib/templates/install.mjs.tmpl +2 -2
- package/lib/types/config-data.d.ts +64 -0
- package/lib/utils/gi-search-path.js +11 -3
- package/lib/utils/ship/discover.js +85 -0
- package/lib/utils/ship/formats.d.ts +23 -0
- package/lib/utils/ship/formats.js +44 -0
- package/lib/utils/ship/launcher.js +57 -0
- package/lib/utils/ship/layout.d.ts +64 -2
- package/lib/utils/ship/layout.js +165 -12
- package/lib/utils/ship/msi.js +24 -7
- package/lib/utils/ship/payload.js +22 -3
- package/lib/utils/ship/pe-launcher.d.ts +29 -0
- package/lib/utils/ship/pe-launcher.js +663 -0
- package/lib/utils/ship/plan.js +35 -0
- package/lib/utils/ship/plist.d.ts +41 -1
- package/lib/utils/ship/plist.js +59 -1
- package/lib/utils/ship/rpm.js +7 -0
- package/lib/utils/ship/settings.d.ts +76 -5
- package/lib/utils/ship/settings.js +89 -8
- package/lib/utils/ship/share-dirs.d.ts +30 -0
- package/lib/utils/ship/share-dirs.js +30 -0
- package/lib/utils/ship/signing.d.ts +122 -6
- package/lib/utils/ship/signing.js +256 -67
- package/lib/utils/ship/stage-writer.js +20 -1
- package/lib/utils/ship/types.d.ts +34 -2
- package/lib/utils/system-gi.d.ts +20 -5
- package/lib/utils/system-gi.js +36 -13
- package/package.json +19 -19
|
@@ -8,6 +8,30 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
8
8
|
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
9
9
|
import { LICENSE_FILE_NAMES } from './settings.js';
|
|
10
10
|
const ICON_EXTENSIONS = ['.svg', '.png'];
|
|
11
|
+
/**
|
|
12
|
+
* The face formats every FreeType opens, with no build option deciding it.
|
|
13
|
+
*
|
|
14
|
+
* TrueType, OpenType and their collections. Deliberately short: the point of the
|
|
15
|
+
* set is that a file in it resolves inside the app's OWN runtime closure as well
|
|
16
|
+
* as it does on the machine that packaged it.
|
|
17
|
+
*/
|
|
18
|
+
const FONT_EXTENSIONS = ['.ttf', '.otf', '.ttc', '.otc'];
|
|
19
|
+
/**
|
|
20
|
+
* Web-font wrappers, refused BY NAME rather than filtered away.
|
|
21
|
+
*
|
|
22
|
+
* They are the mistake worth a diagnostic. An `OFL.txt` beside the faces is
|
|
23
|
+
* ignored the way a stray file in an icon directory is; a `.woff2` is a font
|
|
24
|
+
* somebody put there on purpose, and dropping it silently costs the same as
|
|
25
|
+
* staging it — the app ships without the family and Pango substitutes, at exit 0.
|
|
26
|
+
*
|
|
27
|
+
* Measured in BOTH directions, which is why this is a refusal rather than a
|
|
28
|
+
* warning: `fc-query` reads a `.woff2`'s family fine on Fedora 44 (fontconfig
|
|
29
|
+
* 2.17.0, FreeType built with brotli), and that support is
|
|
30
|
+
* `FT_CONFIG_OPTION_USE_BROTLI` — a property of whichever FreeType the shipped
|
|
31
|
+
* artifact loads, which for a `.app` or a Windows program directory is the one
|
|
32
|
+
* inside `@gjsify/gtk-runtime-<target>` and not this host's.
|
|
33
|
+
*/
|
|
34
|
+
const WEB_FONT_EXTENSIONS = ['.woff', '.woff2', '.eot'];
|
|
11
35
|
export function discoverPayload(input) {
|
|
12
36
|
const { projectDir, ship } = input;
|
|
13
37
|
const bundlePath = resolveBundle(input);
|
|
@@ -29,6 +53,7 @@ export function discoverPayload(input) {
|
|
|
29
53
|
schemaFiles: discoverSchemas(projectDir, ship.schemas),
|
|
30
54
|
typelibFiles: discoverTypelibs(projectDir, ship.bundledTypelibs),
|
|
31
55
|
localeFiles,
|
|
56
|
+
fontFiles: discoverFonts(projectDir, ship.fonts),
|
|
32
57
|
licenseFile: discoverLicense(projectDir, ship.licenseFile),
|
|
33
58
|
};
|
|
34
59
|
}
|
|
@@ -186,6 +211,66 @@ function listIcons(dir) {
|
|
|
186
211
|
.map((rel) => join(dir, rel.split('/').join(sep)))
|
|
187
212
|
.sort();
|
|
188
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Font faces the application SHIPS (`gjsify.ship.fonts`), or the GNOME-shaped
|
|
216
|
+
* default `data/fonts` when it exists.
|
|
217
|
+
*
|
|
218
|
+
* A FILE or a DIRECTORY, like `icon`, and a directory is walked rather than
|
|
219
|
+
* globbed because a family arrives as several faces and often with its licence
|
|
220
|
+
* beside them.
|
|
221
|
+
*
|
|
222
|
+
* The refusals are the interesting half, and both cover the same failure: a font
|
|
223
|
+
* that installs and is never found. Pango does not report a missing family — it
|
|
224
|
+
* substitutes, so the app renders in the wrong typeface with no error, no exit
|
|
225
|
+
* code and nothing for CI to see. That is why a configured path holding no usable
|
|
226
|
+
* face fails here rather than staging an empty directory, and why a web-font
|
|
227
|
+
* wrapper is named rather than filtered (see {@link WEB_FONT_EXTENSIONS}).
|
|
228
|
+
*/
|
|
229
|
+
function discoverFonts(projectDir, configured) {
|
|
230
|
+
const declared = configured ?? 'data/fonts';
|
|
231
|
+
const root = configured === undefined ? join(projectDir, 'data', 'fonts') : resolve(projectDir, configured);
|
|
232
|
+
if (!existsSync(root)) {
|
|
233
|
+
if (configured === undefined)
|
|
234
|
+
return [];
|
|
235
|
+
throw new Error(`gjsify ship: the configured font path ${configured} does not exist.`);
|
|
236
|
+
}
|
|
237
|
+
if (!statSync(root).isDirectory()) {
|
|
238
|
+
assertNotWebFont(root, declared);
|
|
239
|
+
if (!FONT_EXTENSIONS.some((ext) => root.toLowerCase().endsWith(ext))) {
|
|
240
|
+
throw new Error(`gjsify ship: \`gjsify.ship.fonts\` names ${configured}, which is not a font face ` +
|
|
241
|
+
`fontconfig will open (${FONT_EXTENSIONS.join(', ')}). ` +
|
|
242
|
+
'A file it cannot read installs and is never found, and Pango substitutes a fallback ' +
|
|
243
|
+
'family rather than reporting anything.');
|
|
244
|
+
}
|
|
245
|
+
return [root];
|
|
246
|
+
}
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const rel of listFilesRecursive(root)) {
|
|
249
|
+
assertNotWebFont(rel, declared);
|
|
250
|
+
if (FONT_EXTENSIONS.some((ext) => rel.toLowerCase().endsWith(ext))) {
|
|
251
|
+
out.push(join(root, rel.split('/').join(sep)));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (out.length === 0 && configured !== undefined) {
|
|
255
|
+
throw new Error(`gjsify ship: \`gjsify.ship.fonts\` (${configured}) holds no font face ` +
|
|
256
|
+
`(${FONT_EXTENSIONS.join(', ')}). Declaring fonts that ship nothing is a promise the ` +
|
|
257
|
+
'package does not keep — and its symptom is a substituted family, not an error.');
|
|
258
|
+
}
|
|
259
|
+
return out.sort();
|
|
260
|
+
}
|
|
261
|
+
/** A `.woff`/`.woff2`/`.eot` is named, never dropped — {@link WEB_FONT_EXTENSIONS} carries the reason. */
|
|
262
|
+
function assertNotWebFont(path, where) {
|
|
263
|
+
const lower = path.toLowerCase();
|
|
264
|
+
const wrapper = WEB_FONT_EXTENSIONS.find((ext) => lower.endsWith(ext));
|
|
265
|
+
if (wrapper === undefined)
|
|
266
|
+
return;
|
|
267
|
+
throw new Error(`gjsify ship: \`gjsify.ship.fonts\` (${where}) carries ${path}, and \`${wrapper}\` is a web-font ` +
|
|
268
|
+
'wrapper whose support is a FreeType BUILD option — brotli for woff2, zlib for woff. Whether it ' +
|
|
269
|
+
'opens is therefore a property of the FreeType the SHIPPED artifact loads (inside ' +
|
|
270
|
+
'`@gjsify/gtk-runtime-<target>` for a `.app` or a Windows program directory), not of the machine ' +
|
|
271
|
+
`that packaged it. Ship the desktop face instead (${FONT_EXTENSIONS.join(', ')}); a font that ` +
|
|
272
|
+
'fails to open is not an error, it is Pango quietly substituting a different typeface.');
|
|
273
|
+
}
|
|
189
274
|
function discoverSchemas(projectDir, configured) {
|
|
190
275
|
const roots = configured !== undefined ? [resolve(projectDir, configured)] : [join(projectDir, 'data')];
|
|
191
276
|
for (const root of roots) {
|
|
@@ -116,3 +116,26 @@ export declare function configuredFormats(raw: readonly string[], layout: Layout
|
|
|
116
116
|
formats: FormatDescriptor[];
|
|
117
117
|
dropped: FormatId[];
|
|
118
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* Can this format's container hold a notarisation ticket?
|
|
121
|
+
*
|
|
122
|
+
* A SWITCH WITH A `never` GUARD and not a field, deliberately — the difference
|
|
123
|
+
* from `FormatDescriptor.host` is who owns the fact. Where a format can be PACKED
|
|
124
|
+
* is a property of this project's toolchain and belongs on the descriptor; whether
|
|
125
|
+
* `stapler` will write a ticket into it is Apple's list, unchanged by anything
|
|
126
|
+
* here, and six of the nine rows are only "no" because notarisation is a macOS
|
|
127
|
+
* concept they have nothing to do with. Putting it in a `switch` keeps the answer
|
|
128
|
+
* beside its citation and still makes a tenth format a compile error.
|
|
129
|
+
*
|
|
130
|
+
* `stapler(1)`: *"stapler works only with UDIF disk images, signed \"flat\"
|
|
131
|
+
* installer packages, and certain code-signed executable bundles such as
|
|
132
|
+
* \".app\"."* And Apple's notarisation guide for the exception that decides the
|
|
133
|
+
* zip: *"While you can notarize a ZIP archive, you can't staple to it directly.
|
|
134
|
+
* Instead, run stapler against each item that you added to the archive. Then
|
|
135
|
+
* create a new ZIP file containing the stapled items for distribution."*
|
|
136
|
+
*
|
|
137
|
+
* `macos-app` is `true` here even though `notarytool` cannot SUBMIT a directory:
|
|
138
|
+
* the two questions are separate, and a `.app` is precisely the item Apple's
|
|
139
|
+
* remedy above tells you to staple after submitting the archive around it.
|
|
140
|
+
*/
|
|
141
|
+
export declare function canCarryTicket(id: FormatId): boolean;
|
|
@@ -947,3 +947,47 @@ export function configuredFormats(raw, layout) {
|
|
|
947
947
|
dropped: named.filter((name) => FORMATS[name].layoutOs !== layout.os),
|
|
948
948
|
};
|
|
949
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Can this format's container hold a notarisation ticket?
|
|
952
|
+
*
|
|
953
|
+
* A SWITCH WITH A `never` GUARD and not a field, deliberately — the difference
|
|
954
|
+
* from `FormatDescriptor.host` is who owns the fact. Where a format can be PACKED
|
|
955
|
+
* is a property of this project's toolchain and belongs on the descriptor; whether
|
|
956
|
+
* `stapler` will write a ticket into it is Apple's list, unchanged by anything
|
|
957
|
+
* here, and six of the nine rows are only "no" because notarisation is a macOS
|
|
958
|
+
* concept they have nothing to do with. Putting it in a `switch` keeps the answer
|
|
959
|
+
* beside its citation and still makes a tenth format a compile error.
|
|
960
|
+
*
|
|
961
|
+
* `stapler(1)`: *"stapler works only with UDIF disk images, signed \"flat\"
|
|
962
|
+
* installer packages, and certain code-signed executable bundles such as
|
|
963
|
+
* \".app\"."* And Apple's notarisation guide for the exception that decides the
|
|
964
|
+
* zip: *"While you can notarize a ZIP archive, you can't staple to it directly.
|
|
965
|
+
* Instead, run stapler against each item that you added to the archive. Then
|
|
966
|
+
* create a new ZIP file containing the stapled items for distribution."*
|
|
967
|
+
*
|
|
968
|
+
* `macos-app` is `true` here even though `notarytool` cannot SUBMIT a directory:
|
|
969
|
+
* the two questions are separate, and a `.app` is precisely the item Apple's
|
|
970
|
+
* remedy above tells you to staple after submitting the archive around it.
|
|
971
|
+
*/
|
|
972
|
+
export function canCarryTicket(id) {
|
|
973
|
+
switch (id) {
|
|
974
|
+
case 'macos-app':
|
|
975
|
+
case 'macos-app-dmg':
|
|
976
|
+
return true;
|
|
977
|
+
case 'macos-app-zip':
|
|
978
|
+
// Nothing outside darwin has a ticket to carry: `stapler` is Apple's tool
|
|
979
|
+
// and no other OS pre-clears an artifact at all (§ A5 — Windows SmartScreen
|
|
980
|
+
// accrues reputation per file hash instead).
|
|
981
|
+
case 'deb':
|
|
982
|
+
case 'rpm':
|
|
983
|
+
case 'flatpak':
|
|
984
|
+
case 'windows-dir':
|
|
985
|
+
case 'windows-dir-zip':
|
|
986
|
+
case 'msi':
|
|
987
|
+
return false;
|
|
988
|
+
default: {
|
|
989
|
+
const unhandled = id;
|
|
990
|
+
throw new Error(`gjsify ship: no ticket answer for format "${String(unhandled)}".`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
@@ -25,6 +25,16 @@
|
|
|
25
25
|
// the tree was installed. Getting that from the launcher rather than from the app
|
|
26
26
|
// keeps the app free of install-layout knowledge.
|
|
27
27
|
//
|
|
28
|
+
// IT ALSO CARRIES THE FONTS, and that reader is not GLib (ADR 0038). fontconfig
|
|
29
|
+
// expands the stock `fonts.conf`'s `<dir prefix="xdg">fonts</dir>` over
|
|
30
|
+
// `XDG_DATA_DIRS` as well as `XDG_DATA_HOME`, so this one export is the whole
|
|
31
|
+
// mechanism behind `gjsify.ship.fonts` outside a `/usr` prefix and is what
|
|
32
|
+
// reaches `/app/share/fonts` in a Flatpak. `fonts-conf(5)` documents only
|
|
33
|
+
// `XDG_DATA_HOME`: the behaviour is NOT re-derivable from the manual, so do not
|
|
34
|
+
// narrow this line to the GLib readers on the strength of what the man page
|
|
35
|
+
// says. Measured across fontconfig 2.14.1 → 2.18.3 — ADR 0038 § What was
|
|
36
|
+
// measured has the versions and the control.
|
|
37
|
+
//
|
|
28
38
|
// THREE FORMS, because neither other OS runs this file. macOS runs a `/bin/sh`
|
|
29
39
|
// script too but cannot use `readlink -f` and must not be handed a `DYLD_*`;
|
|
30
40
|
// Windows runs `cmd.exe`, which shares no syntax with either. What they DO share
|
|
@@ -33,6 +43,20 @@
|
|
|
33
43
|
// dispatch is a `switch` with a `never` guard, the same closed-vocabulary
|
|
34
44
|
// discipline `packOne` uses for the packers.
|
|
35
45
|
import { posix } from 'node:path';
|
|
46
|
+
import { SHARE } from './share-dirs.js';
|
|
47
|
+
/**
|
|
48
|
+
* `<the layout's data dir>/fonts/<appId>` — the staged face directory (ADR 0038).
|
|
49
|
+
*
|
|
50
|
+
* ONE spelling, and the leaf comes off `SHARE.fonts` rather than being typed out,
|
|
51
|
+
* because all three forms below want a path `planFonts` decides. A literal in each
|
|
52
|
+
* would keep naming the old directory the day that constant moves, and the suites
|
|
53
|
+
* would then pass PAST each other rather than fail: the launcher tests read this
|
|
54
|
+
* string, the payload test reads the plan, and neither can see that the two
|
|
55
|
+
* disagree.
|
|
56
|
+
*/
|
|
57
|
+
function stagedFontDir(dataDir, appId) {
|
|
58
|
+
return `${dataDir}/${posix.basename(SHARE.fonts)}/${appId}`;
|
|
59
|
+
}
|
|
36
60
|
/** POSIX-shell single-quote: the only escape inside `'…'` is `'\''`. */
|
|
37
61
|
function shellQuote(value) {
|
|
38
62
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -128,6 +152,15 @@ function renderPrefixLauncher(settings, bundleRelPath, layout) {
|
|
|
128
152
|
if (settings.localeFiles.length > 0) {
|
|
129
153
|
lines.push(`GJSIFY_LOCALE_DIR="$prefix"/${dirs.data}/locale`, 'export GJSIFY_LOCALE_DIR');
|
|
130
154
|
}
|
|
155
|
+
// Where the faces landed, handed over for the same reason and in the same
|
|
156
|
+
// shape as the catalogue directory above (ADR 0038) — and REDUNDANT on this
|
|
157
|
+
// OS, where fontconfig has already found them through XDG_DATA_DIRS. Exported
|
|
158
|
+
// anyway, because the one thing a consumer must not have to write is an OS
|
|
159
|
+
// branch around a path this command chose; `Layout.fontGap` says where the
|
|
160
|
+
// variable is the mechanism instead of a courtesy.
|
|
161
|
+
if (settings.fontFiles.length > 0) {
|
|
162
|
+
lines.push(`GJSIFY_FONT_DIR="$prefix"/${stagedFontDir(dirs.data, settings.appId)}`, 'export GJSIFY_FONT_DIR');
|
|
163
|
+
}
|
|
131
164
|
lines.push(`exec ${execLine(layout, `"$prefix"/${dirs.bundle}/${bundleRelPath}`, settings)} "$@"`, '');
|
|
132
165
|
return lines.join('\n');
|
|
133
166
|
}
|
|
@@ -241,6 +274,15 @@ function renderAppBundleLauncher(settings, bundleRelPath, layout, runtime) {
|
|
|
241
274
|
if (settings.localeFiles.length > 0) {
|
|
242
275
|
lines.push(`GJSIFY_LOCALE_DIR="$contents/${under(dirs.data)}/locale"`, 'export GJSIFY_LOCALE_DIR');
|
|
243
276
|
}
|
|
277
|
+
// The face directory, for the same reason as above — and on THIS OS it is
|
|
278
|
+
// informational rather than the mechanism: `Info.plist`'s
|
|
279
|
+
// `ATSApplicationFontsPath` has already had macOS activate the same directory
|
|
280
|
+
// before this script's interpreter started (ADR 0038). Exported all the same,
|
|
281
|
+
// so a consumer that wants to name a bundled family has one spelling on three
|
|
282
|
+
// operating systems instead of an OS branch.
|
|
283
|
+
if (settings.fontFiles.length > 0) {
|
|
284
|
+
lines.push(`GJSIFY_FONT_DIR="$contents/${stagedFontDir(under(dirs.data), settings.appId)}"`, 'export GJSIFY_FONT_DIR');
|
|
285
|
+
}
|
|
244
286
|
// `"$here/node"`, not `node`. The bare name is true of a developer's machine
|
|
245
287
|
// and false of a `.app` a stranger downloads, and the difference is not a
|
|
246
288
|
// degraded experience: macOS ships no Node at all, so the launcher's first act
|
|
@@ -324,6 +366,16 @@ function renderWindowsLauncher(settings, bundleRelPath, layout, runtime) {
|
|
|
324
366
|
if (settings.localeFiles.length > 0) {
|
|
325
367
|
lines.push(`set "GJSIFY_LOCALE_DIR=${at(dirs.data)}\\locale"`);
|
|
326
368
|
}
|
|
369
|
+
// THE ONE OS WHERE THIS VARIABLE IS THE WHOLE HANDOVER (ADR 0038), because
|
|
370
|
+
// Windows has no declarative font activation at all and the app must call
|
|
371
|
+
// `PangoCairo.FontMap.get_default().add_font_file()` itself. This line is how
|
|
372
|
+
// it is told where. Do NOT drop it as redundant with XDG_DATA_DIRS above —
|
|
373
|
+
// that one reaches no face here. Which three mechanisms look like they would
|
|
374
|
+
// and do not is `LAYOUTS.windows.fontGap` in `layout.ts`, beside the string
|
|
375
|
+
// the command prints.
|
|
376
|
+
if (settings.fontFiles.length > 0) {
|
|
377
|
+
lines.push(`set "GJSIFY_FONT_DIR=${at(stagedFontDir(dirs.data, settings.appId))}"`);
|
|
378
|
+
}
|
|
327
379
|
// `"%HERE%node.exe"`, not `node`. The bare name is true of a developer's
|
|
328
380
|
// machine and false of a program directory a stranger unzips: Windows ships no
|
|
329
381
|
// Node at all, so the launcher's first act would be `'node' is not recognized
|
|
@@ -364,6 +416,11 @@ function windowsPath(rel) {
|
|
|
364
416
|
* interpreter that can read the payload is the one the payload was BUILT for,
|
|
365
417
|
* and that is `settings.app` on all three.
|
|
366
418
|
*
|
|
419
|
+
* Since #1486 `settings.app` is itself resolved per target (`resolveShipApp`), so
|
|
420
|
+
* "NOT the layout" keeps meaning exactly what it meant: the layout does not DERIVE
|
|
421
|
+
* the runtime, an author STATES it per OS. Deriving it here would still stage
|
|
422
|
+
* `exec node` in front of a GJS bundle for everyone who said nothing.
|
|
423
|
+
*
|
|
367
424
|
* `Layout.runtimeGap` is where the honest half of that lives: it says, per OS,
|
|
368
425
|
* why the staged launcher does not yet name what § 4 derives, and `gjsify ship`
|
|
369
426
|
* prints it.
|
|
@@ -38,6 +38,17 @@ export interface LayoutMetadataInput extends LayoutIdentity {
|
|
|
38
38
|
version: string;
|
|
39
39
|
/** Packaging revision within one upstream version — the second half of `CFBundleVersion`. */
|
|
40
40
|
release: string;
|
|
41
|
+
/**
|
|
42
|
+
* `process.arch` the payload is LABELLED for — what `--arch` names.
|
|
43
|
+
*
|
|
44
|
+
* Here because a layout can own a PROGRAM and not only a manifest, and a
|
|
45
|
+
* program has an architecture: the windows row emits an x86-64 GUI launcher,
|
|
46
|
+
* and staging it into a tree labelled `arm64` would make
|
|
47
|
+
* `assertPayloadMatchesArch` refuse a stage over a file the author never
|
|
48
|
+
* named. `Layout.arches` already says the windows layout has no other
|
|
49
|
+
* architecture; this is the same fact where the file is written.
|
|
50
|
+
*/
|
|
51
|
+
arch: string;
|
|
41
52
|
}
|
|
42
53
|
/** The four places a payload file can belong, as stage-relative directories. */
|
|
43
54
|
export interface LayoutDirs {
|
|
@@ -84,7 +95,7 @@ export interface Layout {
|
|
|
84
95
|
* M1 may honestly claim. A layout with no packer produces no installable
|
|
85
96
|
* artifact, so writing `exec node` into a stage assembled from a `--app gjs`
|
|
86
97
|
* bundle would put a runtime that cannot read the payload in front of it —
|
|
87
|
-
* the exact defect `
|
|
98
|
+
* the exact defect `resolveShipApp`'s refusal exists to prevent, produced by the
|
|
88
99
|
* code meant to prevent it. Measured before this was data: `gjsify ship
|
|
89
100
|
* darwin --stage` on a project with no `gjsify.app` key exited 0 and staged
|
|
90
101
|
* `exec node "$contents/Resources/lib/gjs.js"` in front of a bundle whose
|
|
@@ -100,6 +111,28 @@ export interface Layout {
|
|
|
100
111
|
* know, and it is the same fact that retires this field when M0 lands.
|
|
101
112
|
*/
|
|
102
113
|
runtimeGap?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Why a face in the payload's `share/fonts` may not be REACHED on this OS, or
|
|
116
|
+
* absent where nothing stands in its way.
|
|
117
|
+
*
|
|
118
|
+
* Optional, mirroring {@link runtimeGap} rather than {@link metadata}'s
|
|
119
|
+
* required-and-empty shape, because it asks the same kind of question: is there
|
|
120
|
+
* something true about this OS that the staged tree cannot show? Linux answers
|
|
121
|
+
* by having none.
|
|
122
|
+
*
|
|
123
|
+
* THE GAP IS NOT THE PATH (ADR 0038). The faces are STAGED identically on all
|
|
124
|
+
* three layouts and `planStage` emits one plan for them; what differs is what
|
|
125
|
+
* READS the directory. On Linux fontconfig does, expanding
|
|
126
|
+
* `<dir prefix="xdg">fonts</dir>` over the `XDG_DATA_DIRS` every launcher
|
|
127
|
+
* already exports — measured in eight independent fontconfigs — so that row has
|
|
128
|
+
* nothing to report. On the other two the reader is a property of
|
|
129
|
+
* `@gjsify/gtk-runtime-<target>` rather than of this payload, and it is not
|
|
130
|
+
* fontconfig in either: CoreText on macOS, DirectWrite on Windows, the latter
|
|
131
|
+
* measured on hardware (`LAYOUTS.windows.fontGap` carries the numbers).
|
|
132
|
+
* `gjsify ship` prints this only when the payload actually carries a face, so a
|
|
133
|
+
* project shipping none is told nothing.
|
|
134
|
+
*/
|
|
135
|
+
fontGap?: string;
|
|
103
136
|
/** Suffix the launcher's filename carries, `''` where the OS needs none. */
|
|
104
137
|
launcherExt: string;
|
|
105
138
|
/**
|
|
@@ -136,8 +169,17 @@ export interface Layout {
|
|
|
136
169
|
* The result is placed by `placeStage`, which applies the SAME uniqueness check
|
|
137
170
|
* it applies to mapped files: an `extraFiles` destination that lands on
|
|
138
171
|
* `Contents/Info.plist` is refused rather than silently replacing it.
|
|
172
|
+
*
|
|
173
|
+
* IT TAKES THE PAYLOAD, in the prefix-relative shape, because a manifest is a
|
|
174
|
+
* statement ABOUT a tree and one key already has to be (ADR 0038):
|
|
175
|
+
* `ATSApplicationFontsPath` names the directory the bundle's faces are in, and
|
|
176
|
+
* emitting it over a bundle that carries none would point macOS at a path that
|
|
177
|
+
* is not there. That is also the seam `CFBundleDocumentTypes` will need, which
|
|
178
|
+
* is ADR 0024 stages 4 and 5 — so the parameter is the payload rather than a
|
|
179
|
+
* `hasFonts` flag: the next key to need it will need a different question
|
|
180
|
+
* answered about the same tree.
|
|
139
181
|
*/
|
|
140
|
-
metadata: (input: LayoutMetadataInput) => StagedFile[];
|
|
182
|
+
metadata: (input: LayoutMetadataInput, payload: readonly StagedFile[]) => StagedFile[];
|
|
141
183
|
/**
|
|
142
184
|
* The `process.arch` values this layout can be assembled for, or `null` when
|
|
143
185
|
* it imposes no limit of its own.
|
|
@@ -182,6 +224,26 @@ export declare const LAYOUTS: Record<LayoutName, Layout>;
|
|
|
182
224
|
export declare const LAYOUT_NAMES: LayoutName[];
|
|
183
225
|
/** Stage-relative path of the launcher this layout puts in front of the app. */
|
|
184
226
|
export declare function launcherPath(layout: Layout, identity: LayoutIdentity): string;
|
|
227
|
+
/**
|
|
228
|
+
* Stage-relative path of the windows layout's GUI launcher.
|
|
229
|
+
*
|
|
230
|
+
* IT MUST BE `<launcherPath minus `.cmd`>.exe` AND THE STUB DEPENDS ON IT:
|
|
231
|
+
* `buildGuiLauncher` finds its `.cmd` by overwriting the last three characters of
|
|
232
|
+
* its own module filename, which is a three-character store instead of a path
|
|
233
|
+
* search only because the two names differ in exactly those bytes. Deriving both
|
|
234
|
+
* from `LAYOUTS.windows.launcherExt` here is what keeps that true — renaming the
|
|
235
|
+
* batch launcher to `.bat` would otherwise leave a stub looking for a file that
|
|
236
|
+
* is not there, at exit 0, with a window that never appears.
|
|
237
|
+
*/
|
|
238
|
+
export declare function windowsGuiLauncherPath(identity: LayoutIdentity): string;
|
|
239
|
+
/**
|
|
240
|
+
* What the windows GUI launcher calls the file it writes under `%TEMP%`.
|
|
241
|
+
*
|
|
242
|
+
* Named after the binary rather than the display name: `%TEMP%` is shared by
|
|
243
|
+
* every application on the machine, and `binaryName` is the one identifier this
|
|
244
|
+
* project already refuses to let collide.
|
|
245
|
+
*/
|
|
246
|
+
export declare function windowsLaunchLogLeaf(identity: LayoutIdentity): string;
|
|
185
247
|
/**
|
|
186
248
|
* The layout for one `process.platform` token, refusing every alias.
|
|
187
249
|
*
|
package/lib/utils/ship/layout.js
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
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 { buildGuiLauncher } from './pe-launcher.js';
|
|
24
25
|
import { BUNDLE_INFO_PLIST, BUNDLE_PKGINFO, renderInfoPlist, renderPkgInfo } from './plist.js';
|
|
26
|
+
import { SHARE } from './share-dirs.js';
|
|
25
27
|
/**
|
|
26
28
|
* Characters a `<App>.app` directory name may not contain.
|
|
27
29
|
*
|
|
@@ -31,6 +33,27 @@ import { BUNDLE_INFO_PLIST, BUNDLE_PKGINFO, renderInfoPlist, renderPkgInfo } fro
|
|
|
31
33
|
* `hdiutil` time, which is three milestones away and on another host.
|
|
32
34
|
*/
|
|
33
35
|
const BUNDLE_NAME_FORBIDDEN = /[/:\\]/;
|
|
36
|
+
/**
|
|
37
|
+
* The value `ATSApplicationFontsPath` takes for this bundle, or nothing when the
|
|
38
|
+
* payload carries no face (ADR 0038).
|
|
39
|
+
*
|
|
40
|
+
* DERIVED THROUGH `place()`, over a file the payload actually holds, rather than
|
|
41
|
+
* written out as `share/fonts/<appId>`. The key is documented as a path relative
|
|
42
|
+
* to `Contents/Resources`, which is this layout's `dirs.other` — so running the
|
|
43
|
+
* real map on a real entry and taking the Resources prefix back off is the one
|
|
44
|
+
* spelling that cannot come apart from where the faces were staged. A literal
|
|
45
|
+
* would keep pointing at the old directory the day `dirs` changes, and macOS
|
|
46
|
+
* reports an empty font directory the way it reports everything else here: not at
|
|
47
|
+
* all.
|
|
48
|
+
*/
|
|
49
|
+
function darwinFontsPath(identity, payload) {
|
|
50
|
+
const dir = `${SHARE.fonts}/${identity.appId}`;
|
|
51
|
+
const face = payload.find((file) => file.path.startsWith(`${dir}/`));
|
|
52
|
+
if (face === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const dirs = LAYOUTS.darwin.dirs(identity);
|
|
55
|
+
return posix.relative(dirs.other, posix.dirname(place(LAYOUTS.darwin, identity, face.path)));
|
|
56
|
+
}
|
|
34
57
|
function appBundleDir(identity) {
|
|
35
58
|
if (BUNDLE_NAME_FORBIDDEN.test(identity.name)) {
|
|
36
59
|
throw new Error(`gjsify ship: the macOS layout would put this app in "${identity.name}.app", and a bundle ` +
|
|
@@ -79,6 +102,30 @@ export const LAYOUTS = {
|
|
|
79
102
|
'macOS ships no Node, and there is no RELOCATABLE GJS (`build-gtk-runtime-darwin.mjs`: "GJS ' +
|
|
80
103
|
'ships no relocation"), so ADR 0024 § 4 derives Node here. A self-contained bundle needs `@gjsify/node-runtime-darwin-<arch>` and a ' +
|
|
81
104
|
'`--app node` payload (#1354 M0), or `@gjsify/gjs-runtime-darwin-<arch>` (ADR 0024 stage 7).',
|
|
105
|
+
// THE ONE LAYOUT WHERE THE ENV VARIABLE IS NOT THE MECHANISM, and assuming it
|
|
106
|
+
// was is the mistake this comment exists to stop being made again. macOS Pango
|
|
107
|
+
// is CORETEXT, not fontconfig: GTK's own `meson.build` carries
|
|
108
|
+
// `fontconfig_dep = [] # only used in x11 backend` and Homebrew's gtk4 formula
|
|
109
|
+
// builds `-Dx11-backend=false -Dmacos-backend=true`, while cairo's `quartz`
|
|
110
|
+
// option defaults to `auto` and auto-enables on darwin — so a `fonts.conf`
|
|
111
|
+
// inside a `.app` would be inert twice over, wrong Pango backend AND no
|
|
112
|
+
// GTK-side fontconfig. `XDG_DATA_DIRS` still carries the icons and the schemas
|
|
113
|
+
// here; it does not carry the faces.
|
|
114
|
+
//
|
|
115
|
+
// What does is `ATSApplicationFontsPath`, emitted into `Info.plist` by
|
|
116
|
+
// `metadata` above — Apple's own per-app activation, and the ORDERING is why it
|
|
117
|
+
// beats a runtime call rather than merely being tidier:
|
|
118
|
+
// `pango_core_text_font_map_changed()` only bumps a serial, there is no
|
|
119
|
+
// `kCTFontManagerRegisteredFontsChangedNotification` observer and no re-scan
|
|
120
|
+
// path in `pangocoretext-fontmap.c`, so a face registered after the font map
|
|
121
|
+
// initialises is not recoverable by poking it. The system activates this key's
|
|
122
|
+
// directory at LAUNCH, before any of the app's code runs.
|
|
123
|
+
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 — NOT the XDG_DATA_DIRS path, because Pango on " +
|
|
125
|
+
'macOS is CoreText-backed and GTK is not built against fontconfig there. That the activation ' +
|
|
126
|
+
"reaches Pango's CoreText font map is UNVERIFIED: no leg in this repository runs a `.app`. " +
|
|
127
|
+
'Confirm with `PangoCairo.FontMap.get_default().list_families()` in the shipped bundle, and ' +
|
|
128
|
+
'`PANGOCAIRO_BACKEND=bogus` to print which backend it was built with.',
|
|
82
129
|
launcherExt: '',
|
|
83
130
|
root: appBundleDir,
|
|
84
131
|
// Apple's, all four. `Contents/MacOS` holds executables, `Contents/Resources`
|
|
@@ -99,11 +146,11 @@ export const LAYOUTS = {
|
|
|
99
146
|
// nothing tells LaunchServices which file under `Contents/MacOS` to exec,
|
|
100
147
|
// and a `*.app` with no `Info.plist` is a folder with a suffix — which is
|
|
101
148
|
// exactly what M1 staged. `plist.ts` carries the per-key citations.
|
|
102
|
-
metadata: (input) => [
|
|
149
|
+
metadata: (input, payload) => [
|
|
103
150
|
{
|
|
104
151
|
path: `${appBundleDir(input)}/${BUNDLE_INFO_PLIST}`,
|
|
105
152
|
mode: 0o644,
|
|
106
|
-
source: { kind: 'text', text: renderInfoPlist(input) },
|
|
153
|
+
source: { kind: 'text', text: renderInfoPlist(input, darwinFontsPath(input, payload)) },
|
|
107
154
|
},
|
|
108
155
|
{
|
|
109
156
|
path: `${appBundleDir(input)}/${BUNDLE_PKGINFO}`,
|
|
@@ -137,6 +184,55 @@ export const LAYOUTS = {
|
|
|
137
184
|
'cannot assume — Windows ships neither, and there is NO GJS host on Windows at all ' +
|
|
138
185
|
'(ADR 0024 § 4), so § 4 derives Node here. A self-contained directory needs ' +
|
|
139
186
|
'`@gjsify/node-runtime-win32-x64` and a `--app node` payload (#1354 M3).',
|
|
187
|
+
// THE ONE LAYOUT WITH NO DECLARATIVE ANSWER AT ALL, and the three obvious
|
|
188
|
+
// candidates are each dead for a different reason — written down because each
|
|
189
|
+
// one looks right until it is read. The first is now dead by MEASUREMENT
|
|
190
|
+
// rather than by reading (ADR 0038 § W1-W3, Windows 11 / GTK 4.22.4).
|
|
191
|
+
//
|
|
192
|
+
// * FONTCONFIG. gvsbuild does build it and pango links it
|
|
193
|
+
// (`-Dfontconfig=enabled`), but `pangocairo-fontmap.c` picks the first
|
|
194
|
+
// backend COMPILED IN, in the order coretext → win32 → fc, and cairo's
|
|
195
|
+
// meson adds `cairo-win32` unconditionally on a Windows host. So the fc
|
|
196
|
+
// font map is built and never selected, and the whole `etc/fonts` +
|
|
197
|
+
// `FONTCONFIG_PATH` arrangement `@gjsify/gtk-runtime-win32-x64` ships is
|
|
198
|
+
// inert. `APPSHAREFONTDIR` even resolves to exactly this layout
|
|
199
|
+
// (`<exe dir>\..\share\fonts`) and feeds the map nothing reads. RUN: with
|
|
200
|
+
// a `FONTCONFIG_FILE` naming the face's directory beside WINDOWSFONTDIR
|
|
201
|
+
// the default map stays at 82 families without it — and stays at 82 when
|
|
202
|
+
// that directory is the ONLY one configured, which is the row that shows
|
|
203
|
+
// the map is not reading a misconfigured fontconfig but no fontconfig.
|
|
204
|
+
// The control that makes that a finding: a `PangoFT2.FontMap` from the
|
|
205
|
+
// SAME config in the SAME process goes 76 → 77 WITH the face, and to 5
|
|
206
|
+
// families exclusively. The bundle also ships no `fc-cache.exe`.
|
|
207
|
+
// * XDG_DATA_DIRS, which carries the faces on Linux. `FcConfigXdgDataDirs()`
|
|
208
|
+
// splits on a HARDCODED colon (its own comment says the spec asks for one),
|
|
209
|
+
// while `FC_SEARCH_PATH_SEPARATOR` is `;` here — so `C:\App\share` splits
|
|
210
|
+
// into `C` and `\App\share`. The Linux measurement does not carry over.
|
|
211
|
+
// * `AddFontResourceEx(..., FR_PRIVATE)`. It registers with GDI, and
|
|
212
|
+
// `pango_win32_font_map_init()` has exactly one population call —
|
|
213
|
+
// `pango_win32_dwrite_font_map_populate()` — with no `EnumFontFamilies` and
|
|
214
|
+
// no `AddFontResource` anywhere in the backend. A GDI-private font cannot
|
|
215
|
+
// reach GTK4 text.
|
|
216
|
+
//
|
|
217
|
+
// What is left is a runtime call, which a packaging command has no business
|
|
218
|
+
// making inside somebody's application — so the launcher hands over the
|
|
219
|
+
// directory (`GJSIFY_FONT_DIR`) and this string names the call. That the call
|
|
220
|
+
// WORKS is measured too, and on GTK's own map rather than a loose Pango
|
|
221
|
+
// object: `label.get_pango_context().get_font_map()` IS the default map
|
|
222
|
+
// there, `add_font_file` on it answers true, families go 82 → 83 and the
|
|
223
|
+
// family then loads. With the discriminator that keeps "resolved" honest — an
|
|
224
|
+
// invented family renders as Tahoma at different metrics with a Pango
|
|
225
|
+
// warning, which is exactly what the probe family did BEFORE the call.
|
|
226
|
+
fontGap: 'the faces are staged in `share/fonts/<appId>` and the launcher exports GJSIFY_FONT_DIR at it, ' +
|
|
227
|
+
'but WINDOWS HAS NO DECLARATIVE FONT ACTIVATION and nothing here reaches a bundled face on its ' +
|
|
228
|
+
'own: GTK4 uses pangowin32, whose font map is populated from DirectWrite and never from ' +
|
|
229
|
+
'fontconfig or from GDI. MEASURED on Windows 11 / GTK 4.22.4 rather than inferred (ADR 0038 ' +
|
|
230
|
+
'W1-W5): a FONTCONFIG_FILE naming the staged directory moves the default font map by nothing ' +
|
|
231
|
+
'even when it is the ONLY configuration present — that map does not read fontconfig, and exposes ' +
|
|
232
|
+
'no config_changed or set_config with which to make it — while add_font_file on the same map a ' +
|
|
233
|
+
'Gtk.Label renders through does register the family. So register them at startup: ' +
|
|
234
|
+
'`PangoCairo.FontMap.get_default().add_font_file(path)` (Pango 1.56+, in the typelib), or the ' +
|
|
235
|
+
'app renders in a substituted family with no error.',
|
|
140
236
|
// `.cmd`, not `.bat`: the two differ in whether a failing built-in (`set`,
|
|
141
237
|
// `path`, `append`) sets ERRORLEVEL, and `.cmd` is the one where it does.
|
|
142
238
|
launcherExt: '.cmd',
|
|
@@ -146,15 +242,41 @@ export const LAYOUTS = {
|
|
|
146
242
|
// has no rpath — a DLL is found on `PATH`, in the directory of the image
|
|
147
243
|
// that loaded it, or not at all.
|
|
148
244
|
dirs: () => ({ launcher: '', bundle: 'app', native: 'lib', data: 'share', other: '' }),
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
245
|
+
// NO METADATA, and one PROGRAM. A Windows installer's metadata lives in the
|
|
246
|
+
// `.msi`'s own tables, not in a file inside the program directory —
|
|
247
|
+
// `msi.ts` puts every such fact in the `Product`/`Package` attributes of the
|
|
248
|
+
// document it renders — so this row still owns no manifest, and the field is
|
|
249
|
+
// not a macOS detail wearing a general name.
|
|
250
|
+
//
|
|
251
|
+
// What it DOES own is the GUI-subsystem launcher beside the `.cmd`, and it
|
|
252
|
+
// arrives through this seam rather than through a branch in the stager,
|
|
253
|
+
// which is exactly what the field's contract says a fourth row should do.
|
|
254
|
+
//
|
|
255
|
+
// A SECOND LAUNCHER, NOT A REPLACEMENT. The `.cmd` makes every environment
|
|
256
|
+
// decision, is what `assertLauncherMatchesInterpreter` and
|
|
257
|
+
// `.github/ship-oracle/verify-program-dir.py` read back, and is the right
|
|
258
|
+
// entry point in a terminal. The `.exe` is the entry point for a
|
|
259
|
+
// double-click and for the shortcut an `.msi` writes — the two launches
|
|
260
|
+
// Windows gives a console window today, because `cmd.exe` is a
|
|
261
|
+
// console-subsystem image (ADR 0024 § M3). It RUNS the `.cmd`.
|
|
262
|
+
//
|
|
263
|
+
// AND ONLY FOR THE ARCHITECTURE IT IS. The stub is x86-64 machine code, so
|
|
264
|
+
// staging it into a tree labelled `arm64` would have
|
|
265
|
+
// `assertPayloadMatchesArch` refuse the STAGE over a file the author never
|
|
266
|
+
// named — turning "this layout has no arm64 runtime" (a warning, with
|
|
267
|
+
// `Layout.arches.why` to act on) into "your payload is mislabelled" (a
|
|
268
|
+
// refusal, about our own file). `arches.only` already says there is no
|
|
269
|
+
// other Windows architecture; a stage for one stays assemblable and stays
|
|
270
|
+
// unpackable, which is the split `Layout.runtimeGap` draws everywhere else.
|
|
271
|
+
metadata: (input) => input.arch === 'x64'
|
|
272
|
+
? [
|
|
273
|
+
{
|
|
274
|
+
path: windowsGuiLauncherPath(input),
|
|
275
|
+
mode: 0o755,
|
|
276
|
+
source: { kind: 'bytes', data: buildGuiLauncher({ logLeaf: windowsLaunchLogLeaf(input) }) },
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
: [],
|
|
158
280
|
// ONE, and the blocker is a project we do not own. `wingtk/gvsbuild`
|
|
159
281
|
// hardcodes `self.platform = "x64"` in `utils/base_project.py` and its last
|
|
160
282
|
// five releases publish exactly two assets each, both x64 — so there is no
|
|
@@ -185,6 +307,36 @@ const BY_OS = new Map(LAYOUT_NAMES.map((name) => [LAYOUTS[name].os, LAYOUTS[name
|
|
|
185
307
|
export function launcherPath(layout, identity) {
|
|
186
308
|
return posix.join(layout.dirs(identity).launcher, `${identity.binaryName}${layout.launcherExt}`);
|
|
187
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Stage-relative path of the windows layout's GUI launcher.
|
|
312
|
+
*
|
|
313
|
+
* IT MUST BE `<launcherPath minus `.cmd`>.exe` AND THE STUB DEPENDS ON IT:
|
|
314
|
+
* `buildGuiLauncher` finds its `.cmd` by overwriting the last three characters of
|
|
315
|
+
* its own module filename, which is a three-character store instead of a path
|
|
316
|
+
* search only because the two names differ in exactly those bytes. Deriving both
|
|
317
|
+
* from `LAYOUTS.windows.launcherExt` here is what keeps that true — renaming the
|
|
318
|
+
* batch launcher to `.bat` would otherwise leave a stub looking for a file that
|
|
319
|
+
* is not there, at exit 0, with a window that never appears.
|
|
320
|
+
*/
|
|
321
|
+
export function windowsGuiLauncherPath(identity) {
|
|
322
|
+
const layout = LAYOUTS.windows;
|
|
323
|
+
if (layout.launcherExt.length !== 4) {
|
|
324
|
+
throw new Error(`gjsify ship: the windows launcher extension is "${layout.launcherExt}" and the GUI stub can only ` +
|
|
325
|
+
'reach a four-character one — it rewrites the last three characters of its own filename rather ' +
|
|
326
|
+
'than searching for a path (`utils/ship/pe-launcher.ts`).');
|
|
327
|
+
}
|
|
328
|
+
return posix.join(layout.dirs(identity).launcher, `${identity.binaryName}.exe`);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* What the windows GUI launcher calls the file it writes under `%TEMP%`.
|
|
332
|
+
*
|
|
333
|
+
* Named after the binary rather than the display name: `%TEMP%` is shared by
|
|
334
|
+
* every application on the machine, and `binaryName` is the one identifier this
|
|
335
|
+
* project already refuses to let collide.
|
|
336
|
+
*/
|
|
337
|
+
export function windowsLaunchLogLeaf(identity) {
|
|
338
|
+
return `${identity.binaryName}.launch.log`;
|
|
339
|
+
}
|
|
188
340
|
/**
|
|
189
341
|
* The layout for one `process.platform` token, refusing every alias.
|
|
190
342
|
*
|
|
@@ -339,8 +491,9 @@ export function placeStage(layout, identity, files, carried = []) {
|
|
|
339
491
|
// prefix-relative plan, so there is nothing to map. `<layout> metadata` rather
|
|
340
492
|
// than a path as the origin label, because a collision message naming
|
|
341
493
|
// `Contents/Info.plist` as both sides would say nothing.
|
|
342
|
-
for (const file of layout.metadata(identity))
|
|
494
|
+
for (const file of layout.metadata(identity, files)) {
|
|
343
495
|
claim(file.path, `the ${layout.name} layout's own metadata`, file);
|
|
496
|
+
}
|
|
344
497
|
// A THIRD source, and stage-relative like the second: the runtime the bundle
|
|
345
498
|
// CARRIES (`utils/ship/app-runtime.ts`). It goes through `claim` for the reason
|
|
346
499
|
// the metadata does — an `extraFiles` destination landing on a staged dylib
|