@gjsify/cli 0.64.1-next.249 → 0.64.2-next.253
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 +128 -128
- package/lib/actions/build.d.ts +16 -0
- package/lib/actions/build.js +54 -0
- package/lib/commands/ship.js +15 -7
- package/lib/config.js +19 -0
- package/lib/types/config-data.d.ts +31 -0
- package/lib/utils/ship/flatpak-config.d.ts +33 -0
- package/lib/utils/ship/flatpak-config.js +40 -1
- package/lib/utils/ship/gi-selection.d.ts +18 -0
- package/lib/utils/ship/gi-selection.js +73 -0
- package/lib/utils/ship/layout.d.ts +40 -2
- package/lib/utils/ship/layout.js +93 -4
- package/lib/utils/ship/settings.js +4 -3
- package/package.json +21 -21
package/lib/actions/build.d.ts
CHANGED
|
@@ -94,6 +94,22 @@ export declare class BuildAction {
|
|
|
94
94
|
* on the prediction, not a second opinion.
|
|
95
95
|
*/
|
|
96
96
|
private assertNodeOutputGlobals;
|
|
97
|
+
/**
|
|
98
|
+
* Post-bundle gate for `--app nativescript`: every GI namespace the emitted
|
|
99
|
+
* bundle loads has to be one the APK was told to carry (ADR 0117 § 2).
|
|
100
|
+
*
|
|
101
|
+
* THE ORACLE IS THE EMITTED FILE, like the two gates above and for the same
|
|
102
|
+
* reason — that is the artifact that gets installed. What it reads there is a
|
|
103
|
+
* LOWER BOUND, which is why only one direction is fatal: a namespace the scan
|
|
104
|
+
* found and the config omits WILL be missing from the APK, while a namespace the
|
|
105
|
+
* config names and the scan missed may simply be reached in a way no static read
|
|
106
|
+
* can see (`planGiSelection` carries the full argument).
|
|
107
|
+
*
|
|
108
|
+
* Android is the one target where this is not advice. A `.deb` that under-declares
|
|
109
|
+
* its typelibs can still be fixed by installing a package; an APK carries its
|
|
110
|
+
* whole GI stack or does without it.
|
|
111
|
+
*/
|
|
112
|
+
private assertNativescriptGiSelected;
|
|
97
113
|
/**
|
|
98
114
|
* Post-bundle step for a `--app browser` HTML entry: write the processed
|
|
99
115
|
* `index.html` beside the JS bundle, with the entry `<script>`'s `src`
|
package/lib/actions/build.js
CHANGED
|
@@ -15,6 +15,8 @@ import { collectEntryPaths, describeJsxConfig, findJsxEntryPoint, jsxConfigMissi
|
|
|
15
15
|
import { giSystemProbes } from '../utils/gi-runtime-paths.js';
|
|
16
16
|
import { escapeRawNulForGjs } from '../utils/gjs-source-escape.js';
|
|
17
17
|
import { assertNodeBundleGlobalsShimmed } from '../utils/node-bundle-guard.js';
|
|
18
|
+
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
19
|
+
import { planGiSelection } from '../utils/ship/gi-selection.js';
|
|
18
20
|
import { DEP_CHANGE_LOCKFILES } from '../utils/package-inputs.js';
|
|
19
21
|
const DEFAULT_GJS_SHEBANG = '#!/usr/bin/env -S gjs -m';
|
|
20
22
|
/**
|
|
@@ -439,6 +441,51 @@ export class BuildAction {
|
|
|
439
441
|
assertNodeBundleGlobalsShimmed(code, item.fileName ? `${label} (${item.fileName})` : label);
|
|
440
442
|
}
|
|
441
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Post-bundle gate for `--app nativescript`: every GI namespace the emitted
|
|
446
|
+
* bundle loads has to be one the APK was told to carry (ADR 0117 § 2).
|
|
447
|
+
*
|
|
448
|
+
* THE ORACLE IS THE EMITTED FILE, like the two gates above and for the same
|
|
449
|
+
* reason — that is the artifact that gets installed. What it reads there is a
|
|
450
|
+
* LOWER BOUND, which is why only one direction is fatal: a namespace the scan
|
|
451
|
+
* found and the config omits WILL be missing from the APK, while a namespace the
|
|
452
|
+
* config names and the scan missed may simply be reached in a way no static read
|
|
453
|
+
* can see (`planGiSelection` carries the full argument).
|
|
454
|
+
*
|
|
455
|
+
* Android is the one target where this is not advice. A `.deb` that under-declares
|
|
456
|
+
* its typelibs can still be fixed by installing a package; an APK carries its
|
|
457
|
+
* whole GI stack or does without it.
|
|
458
|
+
*/
|
|
459
|
+
assertNativescriptGiSelected(result, outfile, outdir) {
|
|
460
|
+
const label = outfile ?? outdir ?? 'the nativescript bundle';
|
|
461
|
+
const scanned = new Set();
|
|
462
|
+
for (const item of result.output ?? []) {
|
|
463
|
+
if (item.type !== 'chunk')
|
|
464
|
+
continue;
|
|
465
|
+
const code = item.code;
|
|
466
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
467
|
+
continue;
|
|
468
|
+
for (const namespace of scanGiNamespaces(code))
|
|
469
|
+
scanned.add(namespace);
|
|
470
|
+
}
|
|
471
|
+
let selection;
|
|
472
|
+
try {
|
|
473
|
+
selection = planGiSelection({
|
|
474
|
+
declared: this.configData.nativescript?.giNamespaces ?? [],
|
|
475
|
+
scanned: [...scanned],
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
// Re-thrown with the artifact in it: `planGiSelection` is pure and knows
|
|
480
|
+
// the namespaces but not which file they were read from.
|
|
481
|
+
throw new Error(`gjsify build: ${label}: ${error.message}`);
|
|
482
|
+
}
|
|
483
|
+
for (const namespace of selection.unscanned) {
|
|
484
|
+
console.warn(`[gjsify] --app nativescript: nativescript.giNamespaces declares ${namespace}, which ` +
|
|
485
|
+
`${label} does not appear to import. The APK carries it anyway — drop it if nothing ` +
|
|
486
|
+
'needs it, since every selected namespace costs APK size.');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
442
489
|
/**
|
|
443
490
|
* Post-bundle step for a `--app browser` HTML entry: write the processed
|
|
444
491
|
* `index.html` beside the JS bundle, with the entry `<script>`'s `src`
|
|
@@ -849,6 +896,13 @@ export class BuildAction {
|
|
|
849
896
|
if (app === 'node' && autoMode && !pluginOpts.nodeGiGlobalsInject) {
|
|
850
897
|
this.assertNodeOutputGlobals(writeResult, outfile, outdir);
|
|
851
898
|
}
|
|
899
|
+
// Refuse to hand back a `--app nativescript` bundle that imports a GI namespace
|
|
900
|
+
// the APK will not carry. The payload is selected from a pinned sysroot at
|
|
901
|
+
// package-build time (ADR 0117), so there is no host to fall back on: the
|
|
902
|
+
// alternative to this gate is an APK that installs and throws at the import.
|
|
903
|
+
if (app === 'nativescript') {
|
|
904
|
+
this.assertNativescriptGiSelected(writeResult, outfile, outdir);
|
|
905
|
+
}
|
|
852
906
|
// Browser HTML entry: emit the processed `index.html` beside the bundle
|
|
853
907
|
// (engine-portable post-step — the native rolldown engine has no
|
|
854
908
|
// `emitFile`, so this can't live in a plugin). `outfile` is guaranteed
|
package/lib/commands/ship.js
CHANGED
|
@@ -454,13 +454,21 @@ async function assemble(args) {
|
|
|
454
454
|
})),
|
|
455
455
|
];
|
|
456
456
|
// THE APPLICATION ICON, rendered once for the sizes this layout's writers
|
|
457
|
-
// embed — six for the `.exe`'s resource directory, seven for the `.icns
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
457
|
+
// embed — six for the `.exe`'s resource directory, seven for the `.icns`,
|
|
458
|
+
// three for the hicolor theme — and nothing for a CLI. `resolveAppIcon`
|
|
459
|
+
// REFUSES an app that has no source for a size rather than staging without
|
|
460
|
+
// it: the released artifacts this closes shipped for a year with the generic
|
|
461
|
+
// icon on two platforms, and the only reason was that nothing said so
|
|
462
|
+
// (`utils/ship/icons.ts`).
|
|
463
|
+
//
|
|
464
|
+
// …EXCEPT WHERE THE ROW SAYS THE ICON IS NOT REQUIRED, which is Linux and is
|
|
465
|
+
// not a softening of that rule. An icon-less app there is reported by
|
|
466
|
+
// `resolveShipSettings`'s warning and then by its own menu entry, so refusing
|
|
467
|
+
// would stop a project that has not drawn its icon yet from building a `.deb`
|
|
468
|
+
// over a defect it can already see (`LayoutIcon.required`). The guard belongs
|
|
469
|
+
// HERE as well as in `iconFor`, because `resolveAppIcon` is the half that
|
|
470
|
+
// refuses an empty `iconFiles` — reaching it at all is what has to not happen.
|
|
471
|
+
const icon = settings.kind === 'app' && layout.icon !== undefined && (layout.icon.required || settings.iconFiles.length > 0)
|
|
464
472
|
? await resolveAppIcon({
|
|
465
473
|
iconFiles: settings.iconFiles,
|
|
466
474
|
appId: settings.appId,
|
package/lib/config.js
CHANGED
|
@@ -317,6 +317,25 @@ export class Config {
|
|
|
317
317
|
'introspection to answer `gi://` with. `--app gjs` resolves it natively and `--app node` ' +
|
|
318
318
|
'through @gjsify/node-gi, so neither needs a widget renderer standing in for it.');
|
|
319
319
|
}
|
|
320
|
+
// And the same rule once more for the namespace selection, which only an
|
|
321
|
+
// Android payload can act on: on every other target `gi://` is answered by a
|
|
322
|
+
// system typelib or by `requireGi`, so a list here would be read, validated
|
|
323
|
+
// and then change nothing about the artifact.
|
|
324
|
+
if (configData.nativescript?.giNamespaces !== undefined && configData.app !== 'nativescript') {
|
|
325
|
+
throw new Error(`gjsify build: nativescript.giNamespaces has no effect on --app ${configData.app}. ` +
|
|
326
|
+
'It selects what an Android APK carries (ADR 0117); every other target resolves ' +
|
|
327
|
+
'`gi://` against a GI stack the host already has.');
|
|
328
|
+
}
|
|
329
|
+
for (const entry of configData.nativescript?.giNamespaces ?? []) {
|
|
330
|
+
// `Ns` or `Ns-Version`, because that is what the AAR builder resolves
|
|
331
|
+
// against the typelib names in the sysroot. A free-form string here
|
|
332
|
+
// surfaces as "the prefix has no typelib for …" hours later, naming the
|
|
333
|
+
// typo rather than the shape.
|
|
334
|
+
if (!/^[A-Za-z][A-Za-z\d_]*(-\d+(\.\d+)*)?$/.test(entry)) {
|
|
335
|
+
throw new Error(`gjsify build: nativescript.giNamespaces entry "${entry}" is not a GI namespace. ` +
|
|
336
|
+
'Write it as `Gtk` or `Gtk-4.0`.');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
320
339
|
if (cliArgs.globals !== undefined)
|
|
321
340
|
configData.globals = cliArgs.globals;
|
|
322
341
|
// Fallback applied post-merge (not as a yargs `default:`) so a
|
|
@@ -213,6 +213,37 @@ export interface ConfigData {
|
|
|
213
213
|
format?: ConfigDataFormat;
|
|
214
214
|
/** Config for `gjsify test`. `--entry`, `--outdir`, `--runtime` override these. */
|
|
215
215
|
test?: ConfigDataTest;
|
|
216
|
+
/** Config for the `nativescript` target — what the APK carries (ADR 0117). */
|
|
217
|
+
nativescript?: ConfigDataNativescript;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* What a NativeScript build decides that no other target has to.
|
|
221
|
+
*
|
|
222
|
+
* Its own namespace rather than top-level keys, for the reason `flatpak` and `ship`
|
|
223
|
+
* have one: these settings mean nothing on `gjs`, `node` or `browser`, and a
|
|
224
|
+
* top-level `giNamespaces` would read like a bundler option on every target.
|
|
225
|
+
*/
|
|
226
|
+
export interface ConfigDataNativescript {
|
|
227
|
+
/**
|
|
228
|
+
* The GI namespaces this APK carries, as `Ns` or `Ns-Version` (ADR 0117 § 2).
|
|
229
|
+
*
|
|
230
|
+
* ANDROID HAS NO SYSTEM GI STACK, so unlike a `.deb` there is no `Depends:` that
|
|
231
|
+
* a host can satisfy: every typelib and every backing `.so` is in the APK or is
|
|
232
|
+
* not reachable at all. The payload is cross-compiled against a sha256-pinned
|
|
233
|
+
* sysroot long before an app build, so this list cannot conjure a namespace the
|
|
234
|
+
* sysroot lacks — it SELECTS from what the sysroot has.
|
|
235
|
+
*
|
|
236
|
+
* Declared rather than inferred, and the asymmetry is the point: the scan of the
|
|
237
|
+
* emitted bundle is a LOWER BOUND (a namespace reached through a computed
|
|
238
|
+
* specifier is invisible to it), so an imported namespace missing from this list
|
|
239
|
+
* fails the build by name, while a listed one the scan did not see is only a
|
|
240
|
+
* warning about a larger-than-necessary APK.
|
|
241
|
+
*
|
|
242
|
+
* Unset is not the same as `[]` in intent but is in effect: both mean the
|
|
243
|
+
* measured core set (GLib, GObject, Gio and friends), which backs the runtime
|
|
244
|
+
* itself and ships whether or not anything declares it.
|
|
245
|
+
*/
|
|
246
|
+
giNamespaces?: string[];
|
|
216
247
|
}
|
|
217
248
|
/** Optional pointer to a non-default oxc config file. */
|
|
218
249
|
export interface ConfigDataFormat {
|
|
@@ -43,6 +43,39 @@ export interface PickedFlatpakBuildKeys {
|
|
|
43
43
|
export declare function pickFlatpakBuildKeys(preferred: ShipFlatpakOptions | undefined, legacy: ConfigDataFlatpak): PickedFlatpakBuildKeys;
|
|
44
44
|
/** The branch a `ship` Flatpak is exported under when the project names none. */
|
|
45
45
|
export declare const DEFAULT_SHIP_FLATPAK_BRANCH = "stable";
|
|
46
|
+
/**
|
|
47
|
+
* The scalable icon directory a `kind: 'app'` Flatpak drops before
|
|
48
|
+
* `appstreamcli compose` reads the tree.
|
|
49
|
+
*
|
|
50
|
+
* THE OTHER HALF OF `LAYOUTS.linux.icon`, and the half that cannot live in the
|
|
51
|
+
* layout: the stage is one payload for four Linux formats (ADR 0024 § 2) and the
|
|
52
|
+
* `.deb`, the `.rpm` and the AppImage all WANT the SVG — it is the icon GTK
|
|
53
|
+
* prefers, and `selectAppDirIcon` ranks it above every raster. Only the Flatpak
|
|
54
|
+
* has a build step that reads the theme, so only the Flatpak removes it, and it
|
|
55
|
+
* does so inside the sandbox rather than in the tree `ship` staged.
|
|
56
|
+
*
|
|
57
|
+
* WHY IT IS REMOVED AT ALL. `flatpak-builder` runs `appstreamcli compose` on the
|
|
58
|
+
* HOST to produce `/app/share/app-info`; AppStream 1.1.3 decodes icons through
|
|
59
|
+
* gdk-pixbuf, which reads SVG only through a loader module Fedora 44 no longer
|
|
60
|
+
* ships (`LAYOUTS.linux.icon` carries the measurement). Three probes: with the
|
|
61
|
+
* SVG present compose fails whether or not rasters are there, because it PREFERS
|
|
62
|
+
* the scalable icon and does not fall back; with the SVG gone and the rasters
|
|
63
|
+
* present it succeeds and emits 64×64 and 128×128 into the catalogue. So the
|
|
64
|
+
* rasters are necessary and this is what makes them sufficient.
|
|
65
|
+
*
|
|
66
|
+
* UNCONDITIONAL, not host-detected, and that is the point. `org.flatpak.Builder`
|
|
67
|
+
* on Flathub bundles a gdk-pixbuf that CAN read SVG, so probing the host would
|
|
68
|
+
* make the same source produce a different `.flatpak` depending on where it was
|
|
69
|
+
* built — the one property ADR 0024 § A2 asks a pack to keep. One tree, every
|
|
70
|
+
* host, at the cost of a scalable icon inside a sandbox whose only reader
|
|
71
|
+
* (`Icon=<appId>` in the exported desktop entry) resolves the 256 px raster.
|
|
72
|
+
*
|
|
73
|
+
* THE UPSTREAM FIX IS NOT OURS: `libappstream-compose.so.0` already links
|
|
74
|
+
* `librsvg-2.so.2` and uses it for fonts alone, so AppStream could render the
|
|
75
|
+
* icon without gdk-pixbuf at all. Reported as an `Upstream (AppStream, file by
|
|
76
|
+
* hand)` issue rather than filed there (root AGENTS.md).
|
|
77
|
+
*/
|
|
78
|
+
export declare const SCALABLE_ICONS_CLEANUP = "/share/icons/hicolor/scalable";
|
|
46
79
|
export interface ShipFlatpakInput {
|
|
47
80
|
ship: ConfigDataShip;
|
|
48
81
|
flatpak: ConfigDataFlatpak;
|
|
@@ -88,6 +88,39 @@ export function pickFlatpakBuildKeys(preferred, legacy) {
|
|
|
88
88
|
}
|
|
89
89
|
/** The branch a `ship` Flatpak is exported under when the project names none. */
|
|
90
90
|
export const DEFAULT_SHIP_FLATPAK_BRANCH = 'stable';
|
|
91
|
+
/**
|
|
92
|
+
* The scalable icon directory a `kind: 'app'` Flatpak drops before
|
|
93
|
+
* `appstreamcli compose` reads the tree.
|
|
94
|
+
*
|
|
95
|
+
* THE OTHER HALF OF `LAYOUTS.linux.icon`, and the half that cannot live in the
|
|
96
|
+
* layout: the stage is one payload for four Linux formats (ADR 0024 § 2) and the
|
|
97
|
+
* `.deb`, the `.rpm` and the AppImage all WANT the SVG — it is the icon GTK
|
|
98
|
+
* prefers, and `selectAppDirIcon` ranks it above every raster. Only the Flatpak
|
|
99
|
+
* has a build step that reads the theme, so only the Flatpak removes it, and it
|
|
100
|
+
* does so inside the sandbox rather than in the tree `ship` staged.
|
|
101
|
+
*
|
|
102
|
+
* WHY IT IS REMOVED AT ALL. `flatpak-builder` runs `appstreamcli compose` on the
|
|
103
|
+
* HOST to produce `/app/share/app-info`; AppStream 1.1.3 decodes icons through
|
|
104
|
+
* gdk-pixbuf, which reads SVG only through a loader module Fedora 44 no longer
|
|
105
|
+
* ships (`LAYOUTS.linux.icon` carries the measurement). Three probes: with the
|
|
106
|
+
* SVG present compose fails whether or not rasters are there, because it PREFERS
|
|
107
|
+
* the scalable icon and does not fall back; with the SVG gone and the rasters
|
|
108
|
+
* present it succeeds and emits 64×64 and 128×128 into the catalogue. So the
|
|
109
|
+
* rasters are necessary and this is what makes them sufficient.
|
|
110
|
+
*
|
|
111
|
+
* UNCONDITIONAL, not host-detected, and that is the point. `org.flatpak.Builder`
|
|
112
|
+
* on Flathub bundles a gdk-pixbuf that CAN read SVG, so probing the host would
|
|
113
|
+
* make the same source produce a different `.flatpak` depending on where it was
|
|
114
|
+
* built — the one property ADR 0024 § A2 asks a pack to keep. One tree, every
|
|
115
|
+
* host, at the cost of a scalable icon inside a sandbox whose only reader
|
|
116
|
+
* (`Icon=<appId>` in the exported desktop entry) resolves the 256 px raster.
|
|
117
|
+
*
|
|
118
|
+
* THE UPSTREAM FIX IS NOT OURS: `libappstream-compose.so.0` already links
|
|
119
|
+
* `librsvg-2.so.2` and uses it for fonts alone, so AppStream could render the
|
|
120
|
+
* icon without gdk-pixbuf at all. Reported as an `Upstream (AppStream, file by
|
|
121
|
+
* hand)` issue rather than filed there (root AGENTS.md).
|
|
122
|
+
*/
|
|
123
|
+
export const SCALABLE_ICONS_CLEANUP = '/share/icons/hicolor/scalable';
|
|
91
124
|
/**
|
|
92
125
|
* Resolve the Flatpak half of `gjsify.ship`, reading the legacy block where the
|
|
93
126
|
* new one is silent, and applying `ship`'s own defaults.
|
|
@@ -116,7 +149,13 @@ export function resolveShipFlatpakSettings(input) {
|
|
|
116
149
|
// extension not being installed.
|
|
117
150
|
appendPath: appendPath ?? (extensions.length > 0 ? deriveAppendPath(extensions) : []),
|
|
118
151
|
finishArgs: finishArgs ?? (input.kind === 'cli' ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS),
|
|
119
|
-
|
|
152
|
+
// APPENDED to what the project declared, not defaulted to — the one entry
|
|
153
|
+
// here that is not the author's choice. A project with its own `cleanup`
|
|
154
|
+
// list would otherwise lose the entry that keeps its own Flatpak buildable
|
|
155
|
+
// (see {@link SCALABLE_ICONS_CLEANUP}), and `??` cannot express "ours as
|
|
156
|
+
// well as theirs". Deduplicated so a project that already wrote the glob —
|
|
157
|
+
// the workaround this replaces — gets one, and in ITS position.
|
|
158
|
+
cleanup: [...new Set([...(cleanup ?? []), ...(input.kind === 'app' ? [SCALABLE_ICONS_CLEANUP] : [])])],
|
|
120
159
|
};
|
|
121
160
|
return { settings, warnings: fromLegacy.length === 0 ? [] : [deprecationWarning(fromLegacy)] };
|
|
122
161
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** What {@link planGiSelection} decided, and what it wants the build to say out loud. */
|
|
2
|
+
export interface GiSelection {
|
|
3
|
+
/** The namespaces to hand the AAR builder, as the config spells them, deduped and sorted. */
|
|
4
|
+
selected: string[];
|
|
5
|
+
/** Declared but not found in the bundle — worth a warning, never a refusal. */
|
|
6
|
+
unscanned: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reconcile the configured namespace set with the one the emitted bundle imports.
|
|
10
|
+
*
|
|
11
|
+
* @param spec.declared `nativescript.giNamespaces`, as `Ns` or `Ns-Version`
|
|
12
|
+
* @param spec.scanned what {@link scanGiNamespaces} read off the emitted bundle
|
|
13
|
+
* @throws when the bundle imports a namespace the config does not cover
|
|
14
|
+
*/
|
|
15
|
+
export declare function planGiSelection({ declared, scanned }: {
|
|
16
|
+
declared: string[];
|
|
17
|
+
scanned: string[];
|
|
18
|
+
}): GiSelection;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Which GI namespaces does this Android APK carry? — ADR 0117 § 2, the CLI half.
|
|
2
|
+
//
|
|
3
|
+
// THE CONFIG DECIDES AND THE BUNDLE IS CHECKED AGAINST IT, which is the asymmetry
|
|
4
|
+
// the ADR turns on. The payload is cross-compiled hours earlier by
|
|
5
|
+
// `build-gi-runtime-android.mjs` against a sha256-pinned sysroot; nothing at app-build
|
|
6
|
+
// time can conjure a typelib that is not in it. So the config is the input, and the
|
|
7
|
+
// scan of the emitted bundle is the EVIDENCE that the input is complete.
|
|
8
|
+
//
|
|
9
|
+
// The scan is a LOWER BOUND, and the two directions of disagreement are therefore not
|
|
10
|
+
// symmetric:
|
|
11
|
+
//
|
|
12
|
+
// * IMPORTED, NOT CONFIGURED → build error naming the namespace. The scanner found a
|
|
13
|
+
// real import; the APK will not have the typelib; the device throws at that import,
|
|
14
|
+
// after the download, reading like an application bug. This is the same failure
|
|
15
|
+
// `deriveDepends` refuses for distro packages (ADR 0024 § 6), reached from Android.
|
|
16
|
+
// * CONFIGURED, NOT IMPORTED → warning. `scanGiNamespaces` reads the emitted file and
|
|
17
|
+
// cannot see a namespace reached through a computed specifier, nor one loaded by a
|
|
18
|
+
// native dependency. Refusing here would make a correct project unbuildable on the
|
|
19
|
+
// scanner's blind spot — so it reports a probably-larger-than-necessary APK and
|
|
20
|
+
// builds it.
|
|
21
|
+
//
|
|
22
|
+
// COMPARED BY NAMESPACE, NOT BY STRING. A config may pin an ABI (`Gtk-4.0`) where the
|
|
23
|
+
// bundle's specifier carries no version (`import 'gi://Gtk'`), and vice versa. Both
|
|
24
|
+
// sides mean the same namespace, and string equality would fail a correct project on
|
|
25
|
+
// the spelling.
|
|
26
|
+
/** `Gtk-4.0` → `Gtk`; `Gtk` → `Gtk`. */
|
|
27
|
+
function namespaceOf(entry) {
|
|
28
|
+
const dash = entry.indexOf('-');
|
|
29
|
+
return dash === -1 ? entry : entry.slice(0, dash);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The namespaces every AAR carries whether or not anything declares them.
|
|
33
|
+
*
|
|
34
|
+
* These back `libnode_gi.so` itself, so they are in the payload's DT_NEEDED closure
|
|
35
|
+
* before any selection happens (ADR 0110's measured core set). Demanding them in the
|
|
36
|
+
* config would make the key boilerplate and fail every project that exists today, for
|
|
37
|
+
* a selection that cannot change the bytes.
|
|
38
|
+
*/
|
|
39
|
+
const ALWAYS_PRESENT = new Set(['GLib', 'GObject', 'Gio', 'GioUnix', 'GLibUnix', 'GModule', 'GIRepository']);
|
|
40
|
+
/**
|
|
41
|
+
* Reconcile the configured namespace set with the one the emitted bundle imports.
|
|
42
|
+
*
|
|
43
|
+
* @param spec.declared `nativescript.giNamespaces`, as `Ns` or `Ns-Version`
|
|
44
|
+
* @param spec.scanned what {@link scanGiNamespaces} read off the emitted bundle
|
|
45
|
+
* @throws when the bundle imports a namespace the config does not cover
|
|
46
|
+
*/
|
|
47
|
+
export function planGiSelection({ declared, scanned }) {
|
|
48
|
+
// A bare and a pinned declaration of one namespace are one selection, and the
|
|
49
|
+
// pinned spelling wins: it says something the bare one does not.
|
|
50
|
+
const byNamespace = new Map();
|
|
51
|
+
for (const entry of declared) {
|
|
52
|
+
const ns = namespaceOf(entry);
|
|
53
|
+
const held = byNamespace.get(ns);
|
|
54
|
+
if (held === undefined || (held === ns && entry !== ns))
|
|
55
|
+
byNamespace.set(ns, entry);
|
|
56
|
+
}
|
|
57
|
+
const missing = [
|
|
58
|
+
...new Set(scanned.map(namespaceOf).filter((ns) => !byNamespace.has(ns) && !ALWAYS_PRESENT.has(ns))),
|
|
59
|
+
].sort();
|
|
60
|
+
if (missing.length > 0) {
|
|
61
|
+
throw new Error(`The bundle imports ${missing.join(', ')}, which nativescript.giNamespaces does not declare. ` +
|
|
62
|
+
'An Android APK carries only the typelibs the config selects (ADR 0117), so this would ' +
|
|
63
|
+
`install and then throw at the import. Add ${missing.length === 1 ? 'it' : 'them'} to ` +
|
|
64
|
+
'nativescript.giNamespaces, or stop importing ' +
|
|
65
|
+
`${missing.length === 1 ? 'it' : 'them'}.`);
|
|
66
|
+
}
|
|
67
|
+
const seen = new Set(scanned.map(namespaceOf));
|
|
68
|
+
const unscanned = [...byNamespace.entries()]
|
|
69
|
+
.filter(([ns]) => !seen.has(ns))
|
|
70
|
+
.map(([, entry]) => entry)
|
|
71
|
+
.sort();
|
|
72
|
+
return { selected: [...byNamespace.values()].sort(), unscanned };
|
|
73
|
+
}
|
|
@@ -70,15 +70,44 @@ export interface LayoutMetadataInput extends LayoutIdentity {
|
|
|
70
70
|
* Declared on the row rather than computed from it because the orchestrator
|
|
71
71
|
* renders the rasters BEFORE the row runs — `resolveAppIcon` is asynchronous (a
|
|
72
72
|
* child GJS) and `Layout.metadata` is not — so the row states its needs up front
|
|
73
|
-
* and receives exactly those sizes.
|
|
73
|
+
* and receives exactly those sizes.
|
|
74
|
+
*
|
|
75
|
+
* ON ALL THREE ROWS, and it was absent from the Linux one until a Flatpak build
|
|
76
|
+
* measured why that was wrong. The reasoning it was absent for — "the hicolor
|
|
74
77
|
* theme carries the SVG the project declared and GTK renders it itself, so there
|
|
75
|
-
* is nothing to convert and
|
|
78
|
+
* is nothing to convert" — is true of GTK and NOT of the tooling that reads the
|
|
79
|
+
* same theme at BUILD time: `appstreamcli compose` resolves an SVG through
|
|
80
|
+
* gdk-pixbuf, which needs an SVG LOADER MODULE, and a host without one fails the
|
|
81
|
+
* whole compose run rather than falling back to a raster. The same host-packaging
|
|
82
|
+
* fact `icons.ts` records for the rasterizer, one phase to the right — see
|
|
83
|
+
* {@link LAYOUTS.linux.icon}.
|
|
76
84
|
*/
|
|
77
85
|
export interface LayoutIcon {
|
|
78
86
|
/** Square edge lengths the writer embeds. */
|
|
79
87
|
sizes: readonly number[];
|
|
80
88
|
/** What is written, for the refusal: "the Windows launcher's icon". */
|
|
81
89
|
writes: string;
|
|
90
|
+
/**
|
|
91
|
+
* Whether a `kind: 'app'` stage is REFUSED when the project declares no icon
|
|
92
|
+
* source, or assembled without one.
|
|
93
|
+
*
|
|
94
|
+
* REQUIRED on every row, like {@link Layout.metadata} and
|
|
95
|
+
* {@link Layout.arches}: `false` is an answer, and an optional field is one a
|
|
96
|
+
* fourth OS forgets and then silently inherits whichever default was typed
|
|
97
|
+
* first.
|
|
98
|
+
*
|
|
99
|
+
* The two answers are different FAILURE MODES, not different strictnesses.
|
|
100
|
+
* On macOS and Windows the icon lives inside a container — an `.icns` named by
|
|
101
|
+
* `CFBundleIconFile`, a `.rsrc` in the launcher — and an application that
|
|
102
|
+
* ships without it shows the generic icon with nothing anywhere saying so:
|
|
103
|
+
* measured on released artifacts, for a year (`icons.ts`). On Linux the icon
|
|
104
|
+
* is a FILE in a theme, the desktop entry's `Icon=` names it, and an
|
|
105
|
+
* icon-less app is visibly icon-less in the menu the first time it is
|
|
106
|
+
* installed — which `resolveShipSettings` already warns about. Turning that
|
|
107
|
+
* warning into a refusal would stop a project that has not drawn its icon yet
|
|
108
|
+
* from building a `.deb` at all, for a defect its own menu entry reports.
|
|
109
|
+
*/
|
|
110
|
+
required: boolean;
|
|
82
111
|
}
|
|
83
112
|
/** The four places a payload file can belong, as stage-relative directories. */
|
|
84
113
|
export interface LayoutDirs {
|
|
@@ -252,6 +281,15 @@ export interface LayoutArches {
|
|
|
252
281
|
/** What a reader must know to act on the refusal: who blocks it, and where it is tracked. */
|
|
253
282
|
why: string;
|
|
254
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* The square sizes the Linux row writes into the hicolor theme.
|
|
286
|
+
*
|
|
287
|
+
* Here and not in a writer module like {@link ICO_SIZES} and {@link ICNS_SIZES},
|
|
288
|
+
* because there is no writer: the rasters ARE the artifact, one file per size,
|
|
289
|
+
* and the row below is the only thing that names them. See
|
|
290
|
+
* {@link LAYOUTS.linux.icon} for what asked for each of the three.
|
|
291
|
+
*/
|
|
292
|
+
export declare const HICOLOR_RASTER_SIZES: readonly [64, 128, 256];
|
|
255
293
|
export declare const LAYOUTS: Record<LayoutName, Layout>;
|
|
256
294
|
/** Every layout name, derived from the table so a new row cannot be missed. */
|
|
257
295
|
export declare const LAYOUT_NAMES: LayoutName[];
|
package/lib/utils/ship/layout.js
CHANGED
|
@@ -36,6 +36,15 @@ import { SHARE } from './share-dirs.js';
|
|
|
36
36
|
* `hdiutil` time, which is three milestones away and on another host.
|
|
37
37
|
*/
|
|
38
38
|
const BUNDLE_NAME_FORBIDDEN = /[/:\\]/;
|
|
39
|
+
/**
|
|
40
|
+
* The square sizes the Linux row writes into the hicolor theme.
|
|
41
|
+
*
|
|
42
|
+
* Here and not in a writer module like {@link ICO_SIZES} and {@link ICNS_SIZES},
|
|
43
|
+
* because there is no writer: the rasters ARE the artifact, one file per size,
|
|
44
|
+
* and the row below is the only thing that names them. See
|
|
45
|
+
* {@link LAYOUTS.linux.icon} for what asked for each of the three.
|
|
46
|
+
*/
|
|
47
|
+
export const HICOLOR_RASTER_SIZES = [64, 128, 256];
|
|
39
48
|
/**
|
|
40
49
|
* The value `ATSApplicationFontsPath` takes for this bundle, or nothing when the
|
|
41
50
|
* payload carries no face (ADR 0038).
|
|
@@ -81,6 +90,10 @@ function iconFor(input, layout) {
|
|
|
81
90
|
if (input.kind !== 'app')
|
|
82
91
|
return undefined;
|
|
83
92
|
if (input.icon === undefined) {
|
|
93
|
+
// The row's own answer, not a uniform one — {@link LayoutIcon.required}
|
|
94
|
+
// carries which failure mode this layout has.
|
|
95
|
+
if (LAYOUTS[layout].icon?.required !== true)
|
|
96
|
+
return undefined;
|
|
84
97
|
throw new Error(`gjsify ship: the ${layout} layout writes the application's icon and was given none. ` +
|
|
85
98
|
'A `kind: "app"` stage is rendered its icon by `resolveAppIcon` before the layout runs; ' +
|
|
86
99
|
'reaching this row without one is a caller that skipped that step.');
|
|
@@ -105,10 +118,78 @@ export const LAYOUTS = {
|
|
|
105
118
|
data: 'share',
|
|
106
119
|
other: '',
|
|
107
120
|
}),
|
|
121
|
+
// THE RASTERS THE BUILD-TIME READERS OF THE THEME NEED, and the one row
|
|
122
|
+
// whose icon is written as ordinary payload files rather than into a
|
|
123
|
+
// container. A GNOME app declares one scalable SVG, `planIcons` stages it
|
|
124
|
+
// at `share/icons/hicolor/scalable/apps/<appId>.svg`, and GTK renders it —
|
|
125
|
+
// so for three milestones this row wrote nothing.
|
|
126
|
+
//
|
|
127
|
+
// WHAT READS THE THEME BEFORE GTK DOES is `appstreamcli compose`, which
|
|
128
|
+
// `flatpak-builder` runs to produce `/app/share/app-info` — the catalogue
|
|
129
|
+
// every app store shows the app out of. AppStream 1.1.3 loads an icon
|
|
130
|
+
// through gdk-pixbuf, gdk-pixbuf reads SVG only through a LOADER MODULE,
|
|
131
|
+
// and that module is a property of the host's packaging rather than of
|
|
132
|
+
// this payload: on Fedora 44, workstation and `ghcr.io/gjsify/ci-fedora:44`
|
|
133
|
+
// alike, `/usr/lib64/gdk-pixbuf-2.0/2.10.0/loaders/` holds the xpm loader
|
|
134
|
+
// and nothing else, and `dnf provides '*/libpixbufloader-svg.so'` answers
|
|
135
|
+
// with no package at all — librsvg dropped it. So the compose run dies
|
|
136
|
+
// `file-read-error` ("image file format unknown") on the SVG and takes the
|
|
137
|
+
// whole `gjsify ship linux --target flatpak` with it. Exactly the fact
|
|
138
|
+
// `icons.ts` records for the rasterizer, one phase to the right, and the
|
|
139
|
+
// reason this is the SAME list of sizes rather than a flatpak-only patch:
|
|
140
|
+
// a `.deb` installing rasters beside its SVG is what every other GNOME app
|
|
141
|
+
// does, and the artifact stays one payload.
|
|
142
|
+
//
|
|
143
|
+
// IT IS NOT ENOUGH TO ADD THEM, measured in three probes: with the SVG
|
|
144
|
+
// present appstreamcli PREFERS it and does not fall back, so SVG + 128 and
|
|
145
|
+
// SVG + 64/128/256 both still fail and PNG-only succeeds. The SVG has to
|
|
146
|
+
// leave the tree the compose run sees — which is a Flatpak-only move,
|
|
147
|
+
// because only there is the tree built, and it is the generated manifest's
|
|
148
|
+
// `cleanup` that makes it (`flatpak-config.ts`).
|
|
149
|
+
//
|
|
150
|
+
// 64/128/256 is what a store needs and what the theme spec's own
|
|
151
|
+
// `index.theme` sizes cover: AppStream asks for 64 and 128 (it emitted
|
|
152
|
+
// exactly those two from the PNG-only probe), and 256 is the HiDPI entry
|
|
153
|
+
// Flathub's quality guidelines expect.
|
|
154
|
+
icon: {
|
|
155
|
+
sizes: HICOLOR_RASTER_SIZES,
|
|
156
|
+
writes: "the hicolor theme's raster icons (`share/icons/hicolor/<n>x<n>/apps/<appId>.png`)",
|
|
157
|
+
// The ONE row that assembles without an icon rather than refusing —
|
|
158
|
+
// `LayoutIcon.required` carries why.
|
|
159
|
+
required: false,
|
|
160
|
+
},
|
|
108
161
|
// A `/usr` prefix has no manifest of its own — everything a Linux package
|
|
109
162
|
// says about itself is in the `.deb`/`.rpm` header or in the freedesktop
|
|
110
|
-
// files the payload already carries.
|
|
111
|
-
|
|
163
|
+
// files the payload already carries. What it owns is the rasters above.
|
|
164
|
+
//
|
|
165
|
+
// SKIPPING A SIZE THE PAYLOAD ALREADY STAGES is not an optimisation: a
|
|
166
|
+
// project shipping its own hand-tuned `data/icons/hicolor/128x128/apps/…`
|
|
167
|
+
// has it planned by `planIcons` at the identical path, and `placeStage`
|
|
168
|
+
// REFUSES two sources for one destination (`claim`). The project's file is
|
|
169
|
+
// the one to keep — it is drawn for that size, which is the whole point of
|
|
170
|
+
// the hicolor convention (`icons.ts`) — so the row yields rather than
|
|
171
|
+
// colliding. `resolveAppIcon` has already read that same PNG into the
|
|
172
|
+
// raster map, so the bytes agree either way; what differs is only which
|
|
173
|
+
// file the stage records as the source.
|
|
174
|
+
metadata: (input, payload) => {
|
|
175
|
+
const icon = iconFor(input, 'linux');
|
|
176
|
+
if (icon === undefined)
|
|
177
|
+
return [];
|
|
178
|
+
const planned = new Set(payload.map((file) => file.path));
|
|
179
|
+
const out = [];
|
|
180
|
+
for (const size of [...icon.png.keys()].sort((a, b) => a - b)) {
|
|
181
|
+
const rel = `${SHARE.icons}/${size}x${size}/apps/${input.appId}.png`;
|
|
182
|
+
const data = icon.png.get(size);
|
|
183
|
+
if (data === undefined || planned.has(rel))
|
|
184
|
+
continue;
|
|
185
|
+
out.push({
|
|
186
|
+
path: place(LAYOUTS.linux, input, rel),
|
|
187
|
+
mode: 0o644,
|
|
188
|
+
source: { kind: 'bytes', data },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
},
|
|
112
193
|
// Every architecture Debian and rpm have a name for, which is what
|
|
113
194
|
// `DEBIAN_ARCH`/`RPM_ARCH` already enumerate — the layout adds nothing.
|
|
114
195
|
arches: null,
|
|
@@ -213,7 +294,11 @@ export const LAYOUTS = {
|
|
|
213
294
|
// released 0.8.0 bundle of one app (macOS 15.7, 2026-09-13): no key, no
|
|
214
295
|
// `.icns`, generic icon — the twin of the Windows defect `pe-launcher.ts`
|
|
215
296
|
// records.
|
|
216
|
-
icon: {
|
|
297
|
+
icon: {
|
|
298
|
+
sizes: ICNS_SIZES,
|
|
299
|
+
writes: 'the macOS bundle icon (`Contents/Resources/<binaryName>.icns`)',
|
|
300
|
+
required: true,
|
|
301
|
+
},
|
|
217
302
|
metadata: (input, payload) => {
|
|
218
303
|
const icon = iconFor(input, 'darwin');
|
|
219
304
|
const iconFile = icon === undefined ? undefined : `${input.binaryName}.icns`;
|
|
@@ -359,7 +444,11 @@ export const LAYOUTS = {
|
|
|
359
444
|
// the target's out of its resource directory — so the `.exe` carries the
|
|
360
445
|
// six sizes as `RT_ICON`/`RT_GROUP_ICON` (`ico.ts`, `pe-launcher.ts`) and
|
|
361
446
|
// there is no `.ico` file in the program directory to fall out of sync.
|
|
362
|
-
icon: {
|
|
447
|
+
icon: {
|
|
448
|
+
sizes: ICO_SIZES,
|
|
449
|
+
writes: 'the Windows launcher icon (`.rsrc` of `<binaryName>.exe`)',
|
|
450
|
+
required: true,
|
|
451
|
+
},
|
|
363
452
|
metadata: (input) => {
|
|
364
453
|
const icon = iconFor(input, 'windows');
|
|
365
454
|
return input.arch === 'x64'
|
|
@@ -181,9 +181,10 @@ export function resolveShipSettings(input) {
|
|
|
181
181
|
const description = descriptionParagraphs(metadata.description) ?? [summary];
|
|
182
182
|
const sections = sectionsFor(metadata.categories);
|
|
183
183
|
if (kind === 'app' && discovered.iconFiles.length === 0) {
|
|
184
|
-
warnings.push(`gjsify ship: no icon found — the installed app will show a placeholder in menus
|
|
185
|
-
'
|
|
186
|
-
'`gjsify.ship.icon` at an SVG (or a directory of
|
|
184
|
+
warnings.push(`gjsify ship: no icon found — the installed app will show a placeholder in menus on Linux, the ` +
|
|
185
|
+
'Flatpak will carry no catalogue icon for an app store to show, and the darwin and windows ' +
|
|
186
|
+
'layouts refuse to stage without one. Point `gjsify.ship.icon` at an SVG (or a directory of ' +
|
|
187
|
+
'sized PNGs).');
|
|
187
188
|
}
|
|
188
189
|
// Resolved here rather than in the packer, because the packer may run on a
|
|
189
190
|
// host with no `package.json` at all (ADR 0024 § A2) — and because the
|