@gjsify/cli 0.53.0 → 0.54.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 +263 -248
- package/lib/bundler-pick.d.ts +35 -0
- package/lib/bundler-pick.js +74 -7
- package/lib/cli-app.js +3 -1
- package/lib/commands/blueprint/index.d.ts +4 -0
- package/lib/commands/blueprint/index.js +15 -0
- package/lib/commands/blueprint/types.d.ts +8 -0
- package/lib/commands/blueprint/types.js +124 -0
- package/lib/commands/devtools.d.ts +21 -0
- package/lib/commands/devtools.js +146 -0
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/ship.js +51 -8
- package/lib/commands/webext/index.js +1 -1
- package/lib/utils/oxc-resolve.js +9 -7
- package/lib/utils/ship/app-runtime.d.ts +7 -5
- package/lib/utils/ship/app-runtime.js +11 -8
- package/lib/utils/ship/appimage.d.ts +9 -0
- package/lib/utils/ship/appimage.js +2 -1
- package/lib/utils/ship/deb.d.ts +10 -0
- package/lib/utils/ship/deb.js +7 -0
- package/lib/utils/ship/depends.d.ts +37 -3
- package/lib/utils/ship/depends.js +21 -9
- package/lib/utils/ship/gi-namespaces.d.ts +16 -0
- package/lib/utils/ship/gi-namespaces.js +64 -1
- package/lib/utils/ship/launcher.js +2 -2
- package/lib/utils/ship/layout.js +25 -2
- package/lib/utils/ship/rpm.d.ts +5 -0
- package/lib/utils/ship/rpm.js +42 -0
- package/lib/utils/ship/settings.js +6 -0
- package/lib/utils/ship/signing.d.ts +7 -2
- package/lib/utils/ship/signing.js +59 -32
- package/lib/utils/ship/stage-manifest.d.ts +14 -0
- package/lib/utils/ship/stage-manifest.js +7 -0
- package/lib/utils/webext/targets.d.ts +1 -1
- package/lib/utils/webext/targets.js +20 -8
- package/package.json +20 -19
package/lib/commands/ship.js
CHANGED
|
@@ -45,7 +45,7 @@ import { compileSchemasForPayload, compileSchemasForStage } from '../utils/ship/
|
|
|
45
45
|
import { buildMsi, MSI_PAYLOAD_DIR } from '../utils/ship/msi.js';
|
|
46
46
|
import { buildZip, zipEntriesFromPayload } from '../utils/ship/zip.js';
|
|
47
47
|
import { assertEntryRunsUnder } from '../utils/ship/entry-interpreter.js';
|
|
48
|
-
import {
|
|
48
|
+
import { scanGiRequirements } from '../utils/ship/gi-namespaces.js';
|
|
49
49
|
import { assertLauncherMatchesInterpreter, assertPayloadMatchesArch, buildTimestamp, isArchIndependent, linuxInstallDependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
50
50
|
import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
|
|
51
51
|
import { assertHostCanSign, notarizeArtifact, stapleArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
|
|
@@ -537,9 +537,20 @@ async function assemble(args) {
|
|
|
537
537
|
// `gi://` specifiers are what the emitted `Depends:` is derived from
|
|
538
538
|
// (ADR 0024 § 6), and the packing host has the staged copy but no way to
|
|
539
539
|
// tell which staged file is the entry.
|
|
540
|
-
const
|
|
541
|
-
|
|
540
|
+
const gi = scanGiRequirements(bundleSource);
|
|
541
|
+
const namespaces = gi.namespaces;
|
|
542
|
+
const optionalNamespaces = gi.optional;
|
|
543
|
+
if (args.verbose) {
|
|
544
|
+
const hard = namespaces.filter((ns) => !optionalNamespaces.includes(ns));
|
|
542
545
|
console.log(`${LOG} gi namespaces: ${namespaces.join(', ') || '(none)'}`);
|
|
546
|
+
// The split is worth its own line whenever it is non-empty: an author who
|
|
547
|
+
// wrote `&optional` and sees the namespace under the plain list has learned
|
|
548
|
+
// nothing about whether their package will declare it.
|
|
549
|
+
if (optionalNamespaces.length > 0) {
|
|
550
|
+
console.log(`${LOG} gi optional namespaces: ${optionalNamespaces.join(', ')}`);
|
|
551
|
+
console.log(`${LOG} gi required namespaces: ${hard.join(', ') || '(none)'}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
543
554
|
// PLACED, like the payload, and through the same map. `planOverlay` answers a
|
|
544
555
|
// question about the FORMAT (`share/doc/<pkg>/copyright` for Debian policy,
|
|
545
556
|
// `share/licenses/<pkg>/LICENSE` for rpm) in the prefix-relative shape every
|
|
@@ -578,8 +589,9 @@ async function assemble(args) {
|
|
|
578
589
|
// into a Debian `Depends:`, at exit 0).
|
|
579
590
|
if (format.depends === null)
|
|
580
591
|
continue;
|
|
581
|
-
deriveDepends(format.depends, {
|
|
592
|
+
const derived = deriveDepends(format.depends, {
|
|
582
593
|
namespaces,
|
|
594
|
+
optionalNamespaces,
|
|
583
595
|
hasIcons: facts.hasIcons,
|
|
584
596
|
hasSchemas: facts.hasSchemas,
|
|
585
597
|
interpreter: settings.app,
|
|
@@ -589,6 +601,17 @@ async function assemble(args) {
|
|
|
589
601
|
minGjsVersion: settings.minGjsVersion,
|
|
590
602
|
minNodeVersion: settings.minNodeVersion,
|
|
591
603
|
});
|
|
604
|
+
// Reported here and not inside `deriveDepends`, which is also called by
|
|
605
|
+
// `packOne` on a host that has no project — and this is the only place that
|
|
606
|
+
// does. An optional namespace nobody mapped produces NO package field, and
|
|
607
|
+
// the app runs without the typelib either way, so nothing downstream will
|
|
608
|
+
// ever say so again.
|
|
609
|
+
for (const namespace of derived.unmappedOptional) {
|
|
610
|
+
console.warn(`${LOG} gi://${namespace} is imported optionally, but no ${format.depends} package is known ` +
|
|
611
|
+
'to ship its typelib, so no Recommends entry names it. The package builds and runs ' +
|
|
612
|
+
`without it. Add the row if the namespace should be recommended: \`gjsify.ship.typelibPackages.` +
|
|
613
|
+
`${namespace} = { "${format.depends}": "…" }\`.`);
|
|
614
|
+
}
|
|
592
615
|
// One warning per package, for the interpreter it actually declares. A
|
|
593
616
|
// floor warning about a dependency this package does not emit is noise,
|
|
594
617
|
// and a noisy warning is the one nobody reads when it matters.
|
|
@@ -602,7 +625,16 @@ async function assemble(args) {
|
|
|
602
625
|
// is sometimes packable elsewhere and sometimes not is a worse contract
|
|
603
626
|
// than one that always is, and it is what lets the e2e suite compare the
|
|
604
627
|
// one-process artifact with the two-phase one.
|
|
605
|
-
const manifest = writeStageManifest({
|
|
628
|
+
const manifest = writeStageManifest({
|
|
629
|
+
stageDir,
|
|
630
|
+
settings,
|
|
631
|
+
formats,
|
|
632
|
+
mtime,
|
|
633
|
+
namespaces,
|
|
634
|
+
optionalNamespaces,
|
|
635
|
+
staged,
|
|
636
|
+
overlay,
|
|
637
|
+
});
|
|
606
638
|
if (args.stage) {
|
|
607
639
|
// `(none)` spelled out, because an empty list printed as nothing after
|
|
608
640
|
// "formats " reads as a truncated line rather than as a fact. WHICH fact
|
|
@@ -638,6 +670,7 @@ async function assemble(args) {
|
|
|
638
670
|
stageDir,
|
|
639
671
|
outRoot,
|
|
640
672
|
namespaces,
|
|
673
|
+
optionalNamespaces,
|
|
641
674
|
mtime,
|
|
642
675
|
sign,
|
|
643
676
|
notarize,
|
|
@@ -799,6 +832,9 @@ async function finishFromStage(args, fromStage) {
|
|
|
799
832
|
for (const file of manifest.staged)
|
|
800
833
|
console.log(`${LOG} ${file.path}`);
|
|
801
834
|
console.log(`${LOG} gi namespaces: ${manifest.namespaces.join(', ') || '(none)'}`);
|
|
835
|
+
if ((manifest.optionalNamespaces ?? []).length > 0) {
|
|
836
|
+
console.log(`${LOG} gi optional namespaces: ${manifest.optionalNamespaces?.join(', ')}`);
|
|
837
|
+
}
|
|
802
838
|
}
|
|
803
839
|
// The stage's own directory is not written to — `writeStage` wipes what it
|
|
804
840
|
// is pointed at, and pointing it at an arriving stage would delete the
|
|
@@ -832,6 +868,7 @@ async function finishFromStage(args, fromStage) {
|
|
|
832
868
|
stageDir,
|
|
833
869
|
outRoot,
|
|
834
870
|
namespaces: manifest.namespaces,
|
|
871
|
+
optionalNamespaces: manifest.optionalNamespaces ?? [],
|
|
835
872
|
mtime,
|
|
836
873
|
sign,
|
|
837
874
|
notarize,
|
|
@@ -858,10 +895,11 @@ async function packOne(input) {
|
|
|
858
895
|
// this answers used to be answered from the project's file lists, which are
|
|
859
896
|
// absolute paths on the build host and therefore unavailable here.
|
|
860
897
|
const facts = readPayloadFacts(assembled);
|
|
861
|
-
const
|
|
862
|
-
? []
|
|
898
|
+
const derived = format.depends === null
|
|
899
|
+
? { requires: [], recommends: [], unmappedOptional: [] }
|
|
863
900
|
: deriveDepends(format.depends, {
|
|
864
901
|
namespaces: input.namespaces,
|
|
902
|
+
optionalNamespaces: input.optionalNamespaces,
|
|
865
903
|
hasIcons: facts.hasIcons,
|
|
866
904
|
hasSchemas: facts.hasSchemas,
|
|
867
905
|
interpreter: settings.app,
|
|
@@ -871,6 +909,8 @@ async function packOne(input) {
|
|
|
871
909
|
minGjsVersion: settings.minGjsVersion,
|
|
872
910
|
minNodeVersion: settings.minNodeVersion,
|
|
873
911
|
});
|
|
912
|
+
const depends = derived.requires;
|
|
913
|
+
const recommends = derived.recommends;
|
|
874
914
|
// `--arch` is a CLAIM about the payload and nothing else compared it to one:
|
|
875
915
|
// an x86-64 `.so` staged with `--arch arm64` packed at exit 0 and `rpm -qp
|
|
876
916
|
// --qf %{ARCH}` confirmed `aarch64`, because rpm reads the header and the
|
|
@@ -956,7 +996,9 @@ async function packOne(input) {
|
|
|
956
996
|
})
|
|
957
997
|
: assembled;
|
|
958
998
|
const archLabel = format.archName(input.arch, isArchIndependent(payload));
|
|
959
|
-
|
|
999
|
+
// `recommends` rides the shared input object, so a format that does not write the
|
|
1000
|
+
// field (every non-deb/rpm row) simply ignores an extra property it never reads.
|
|
1001
|
+
const common = { settings, payload, prefix: format.prefix, depends, recommends, archLabel, mtime };
|
|
960
1002
|
const outDir = join(outRoot, 'out');
|
|
961
1003
|
mkdirSync(outDir, { recursive: true });
|
|
962
1004
|
const target = join(outDir, format.fileName(settings, archLabel));
|
|
@@ -1010,6 +1052,7 @@ async function packOne(input) {
|
|
|
1010
1052
|
minGjsVersion: settings.minGjsVersion,
|
|
1011
1053
|
minNodeVersion: settings.minNodeVersion,
|
|
1012
1054
|
namespaces: input.namespaces,
|
|
1055
|
+
optionalNamespaces: input.optionalNamespaces,
|
|
1013
1056
|
bundledTypelibs: facts.bundledTypelibs,
|
|
1014
1057
|
});
|
|
1015
1058
|
const appDir = appDirFor(outRoot);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { webextBuildCommand, webextDevCommand, webextZipCommand } from './subcommands.js';
|
|
6
6
|
export const webextCommand = {
|
|
7
7
|
command: 'webext <subcommand>',
|
|
8
|
-
description: 'Browser extensions: build, zip and develop one WebExtension for Chrome, Edge, Firefox and Safari.',
|
|
8
|
+
description: 'Browser extensions: build, zip and develop one WebExtension for Chrome, Edge, Firefox, Opera and Safari.',
|
|
9
9
|
builder: (yargs) => yargs
|
|
10
10
|
.command(webextBuildCommand.command, webextBuildCommand.description, webextBuildCommand.builder, webextBuildCommand.handler)
|
|
11
11
|
.command(webextZipCommand.command, webextZipCommand.description, webextZipCommand.builder, webextZipCommand.handler)
|
package/lib/utils/oxc-resolve.js
CHANGED
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
// `GJSIFY_OXFMT=native|npm` is the explicit override (native throws when the
|
|
42
42
|
// prebuild isn't loadable instead of silently switching engines).
|
|
43
43
|
//
|
|
44
|
-
// oxlint has NO native path yet: its JS-plugin host (the
|
|
45
|
-
//
|
|
44
|
+
// oxlint has NO native path yet: its JS-plugin host (the
|
|
45
|
+
// `@gjsify/oxlint-plugin-gjsify` rules wired via `.oxlintrc.json` jsPlugins)
|
|
46
46
|
// lives in the Node launcher, so a native oxlint bridge could only run the
|
|
47
47
|
// Rust rule subset. Lint stays Node-spawned until that trade-off is decided
|
|
48
48
|
// (tracked in status/open-todos/README.md).
|
|
@@ -264,11 +264,13 @@ async function tryLoadNativeOxfmt() {
|
|
|
264
264
|
// `bundler-pick.ts`'s `tryLoadNative()` gives — so a library that will
|
|
265
265
|
// not load names itself instead of failing inside `runOxfmt()`.
|
|
266
266
|
// Resolved and imported, not statically imported — the reasoning is
|
|
267
|
-
// at the top of this file.
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
267
|
+
// at the top of this file. `./native-library` rather than `./core`:
|
|
268
|
+
// `core` re-exports `main-loop`, whose module-level singleton would
|
|
269
|
+
// then exist twice in a process that already carries it inlined in the
|
|
270
|
+
// GJS bundle. Off disk, so the walk follows the WORKSPACE and
|
|
271
|
+
// `@gjsify/utils build:esm` must precede this call — the same
|
|
272
|
+
// precondition `bundler-pick.ts` carries, held by rule 5 of
|
|
273
|
+
// `scripts/check-build-infra-order.mjs`.
|
|
272
274
|
//
|
|
273
275
|
// Its own `try` because the outer one cannot tell this apart from
|
|
274
276
|
// "there is no native backend", and `_nativeOxfmtLoadError` is the
|
|
@@ -102,8 +102,8 @@ export interface ResolvedNodeGiPackage {
|
|
|
102
102
|
* NOT the published tarball. `files` in node-gi's manifest also lists `src/`,
|
|
103
103
|
* `binding.gyp` and `scripts/install.mjs` — the inputs to `node-gyp`, which a
|
|
104
104
|
* `.app` never runs — and the `.d.ts` declarations, which nothing inside a bundle
|
|
105
|
-
* compiles against. And it lists `prebuilds/`, which this module stages
|
|
106
|
-
* `
|
|
105
|
+
* compiles against. And it lists `prebuilds/`, which this module stages under
|
|
106
|
+
* `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
|
|
107
107
|
*
|
|
108
108
|
* So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
|
|
109
109
|
* `package.json` (the `exports` map is what resolves every subpath), every `.js`
|
|
@@ -120,15 +120,17 @@ export declare function resolveNodeGiPackage(options?: ResolveRuntimeOptions): R
|
|
|
120
120
|
* literals, so the paths the launcher exports and the paths the stager writes
|
|
121
121
|
* cannot drift: they are computed here once and both callers read this result.
|
|
122
122
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
123
|
+
* `<dirs.native>/node-gi/prebuilds/<target>/` is node-gi's OWN sibling layout
|
|
124
|
+
* reproduced verbatim inside `Layout.dirs().native` (`Contents/Resources/native`
|
|
125
|
+
* on a `.app` — `layout.ts`'s darwin row carries why that is not
|
|
126
|
+
* `Contents/Frameworks`). It is not a naming preference:
|
|
125
127
|
* `resolveGtkRuntimeBundle()` probes `<dir>/lib` and `<dir>/girepository-1.0`,
|
|
126
128
|
* the addon's `@rpath` is `@loader_path/gtk/lib`, and `loaders.cache` addresses
|
|
127
129
|
* each pixbuf loader relative to the bundle toplevel. Reproducing the shape is
|
|
128
130
|
* what makes every one of those relations still true after the copy.
|
|
129
131
|
*/
|
|
130
132
|
export interface AppRuntimePaths {
|
|
131
|
-
/** `<App>.app/Contents/
|
|
133
|
+
/** `<App>.app/Contents/Resources/native/node-gi` — the package root the layout imitates. */
|
|
132
134
|
nodeGiRoot: string;
|
|
133
135
|
/**
|
|
134
136
|
* `<App>.app/Contents/Resources/lib/node_modules/@gjsify/node-gi` — where
|
|
@@ -47,11 +47,14 @@
|
|
|
47
47
|
// THAT directory — so `prebuildAddonPath()` looks for
|
|
48
48
|
// `…/node_modules/@gjsify/node-gi/prebuilds/<target>/node_gi.node` and
|
|
49
49
|
// `resolveGtkRuntimeBundle()`'s candidate 2 for `…/prebuilds/<target>/gtk`, and
|
|
50
|
-
// neither is where a `.app` may keep loadable code: `
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
50
|
+
// neither is where a `.app` may keep loadable code: `Layout.dirs().native` is, and
|
|
51
|
+
// `utils/ship/signing.ts` re-signs every Mach-O in the payload by magic number
|
|
52
|
+
// (ADR 0024 § A4 — M6 landed), which is most of this closure. What `native` may NOT
|
|
53
|
+
// be is `Contents/Frameworks`: `codesign` reads a version-named DIRECTORY there as
|
|
54
|
+
// a nested bundle and refuses the seal, which is the whole reason this file's
|
|
55
|
+
// output went into `Contents/Resources/native` instead — see `layout.ts`'s darwin
|
|
56
|
+
// row for the measurement and `gjsify ship darwin --sign` for what it used to
|
|
57
|
+
// cost.
|
|
55
58
|
//
|
|
56
59
|
// WHY NOT `gjsify.ship.bundledTypelibs`. Because it FLATTENS. `plan.ts` stages
|
|
57
60
|
// each such file as `posix.join(libDir, 'gi', basename(file))`, and
|
|
@@ -210,8 +213,8 @@ function resolveStagedPackage(specifier, options) {
|
|
|
210
213
|
* NOT the published tarball. `files` in node-gi's manifest also lists `src/`,
|
|
211
214
|
* `binding.gyp` and `scripts/install.mjs` — the inputs to `node-gyp`, which a
|
|
212
215
|
* `.app` never runs — and the `.d.ts` declarations, which nothing inside a bundle
|
|
213
|
-
* compiles against. And it lists `prebuilds/`, which this module stages
|
|
214
|
-
* `
|
|
216
|
+
* compiles against. And it lists `prebuilds/`, which this module stages under
|
|
217
|
+
* `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
|
|
215
218
|
*
|
|
216
219
|
* So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
|
|
217
220
|
* `package.json` (the `exports` map is what resolves every subpath), every `.js`
|
|
@@ -252,7 +255,7 @@ export function appRuntimePaths(layout, identity, target) {
|
|
|
252
255
|
addonPath: posix.join(prebuildDir, NODE_GI_ADDON_FILENAME),
|
|
253
256
|
// `Contents/MacOS`, because that is where a `.app` keeps executables and
|
|
254
257
|
// because `$here` — which the launcher already computes — is then the whole
|
|
255
|
-
// path expression. `
|
|
258
|
+
// path expression. `dirs.native` would work for dyld and would make
|
|
256
259
|
// the launcher walk back up for the one file it execs. On Windows
|
|
257
260
|
// `dirs.launcher` is the program directory itself, so the interpreter lands
|
|
258
261
|
// beside the `.cmd` for the same reason.
|
|
@@ -89,6 +89,15 @@ export declare function appImageHostRequirements(input: {
|
|
|
89
89
|
minGjsVersion?: string;
|
|
90
90
|
minNodeVersion?: string;
|
|
91
91
|
namespaces: readonly string[];
|
|
92
|
+
/**
|
|
93
|
+
* The subset of {@link namespaces} the bundle marks optional (ADR 0087).
|
|
94
|
+
*
|
|
95
|
+
* An AppImage has no `Depends:` field, so "optional" cannot become a package
|
|
96
|
+
* list here — it becomes WORDING instead. That is not cosmetic: an AppRun user
|
|
97
|
+
* told a typelib is required and lacking it goes looking for another distro
|
|
98
|
+
* package, while one told it is optional reads the same sentence correctly.
|
|
99
|
+
*/
|
|
100
|
+
optionalNamespaces?: readonly string[];
|
|
92
101
|
bundledTypelibs?: readonly string[];
|
|
93
102
|
}): string[];
|
|
94
103
|
/**
|
|
@@ -204,9 +204,10 @@ export function appDirFor(outRoot) {
|
|
|
204
204
|
*/
|
|
205
205
|
export function appImageHostRequirements(input) {
|
|
206
206
|
const floor = interpreterFloor(input);
|
|
207
|
+
const optional = new Set(input.optionalNamespaces ?? []);
|
|
207
208
|
return [
|
|
208
209
|
`${input.app} (>= ${floor})`,
|
|
209
|
-
...hostProvidedNamespaces(input.namespaces, input.bundledTypelibs).map((ns) => `the ${ns} typelib`),
|
|
210
|
+
...hostProvidedNamespaces(input.namespaces, input.bundledTypelibs).map((ns) => optional.has(ns) ? `the ${ns} typelib (optional)` : `the ${ns} typelib`),
|
|
210
211
|
];
|
|
211
212
|
}
|
|
212
213
|
/**
|
package/lib/utils/ship/deb.d.ts
CHANGED
|
@@ -8,6 +8,16 @@ export interface DebInputs {
|
|
|
8
8
|
prefix: string;
|
|
9
9
|
/** Already-derived dependency list, in `name`/`name >= version` form. */
|
|
10
10
|
depends: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Soft dependencies, rendered as `Recommends:`.
|
|
13
|
+
*
|
|
14
|
+
* Debian Policy § 7.2: a recommended package is installed by default and
|
|
15
|
+
* `--no-install-recommends` is the user's way out. That is what an `&optional`
|
|
16
|
+
* typelib is (ADR 0087) — the app runs without it and loses a feature — so
|
|
17
|
+
* putting it in `Depends:` would let an optional integration decide whether
|
|
18
|
+
* the package installs at all.
|
|
19
|
+
*/
|
|
20
|
+
recommends?: readonly string[];
|
|
11
21
|
/** Debian architecture (`amd64`, `all`, …). */
|
|
12
22
|
archLabel: string;
|
|
13
23
|
/** Unix seconds stamped into every header. */
|
package/lib/utils/ship/deb.js
CHANGED
|
@@ -104,6 +104,13 @@ function renderControl(inputs, installedSize) {
|
|
|
104
104
|
];
|
|
105
105
|
if (inputs.depends.length > 0)
|
|
106
106
|
fields.push(['Depends', inputs.depends.map(formatDebDepend).join(', ')]);
|
|
107
|
+
// After `Depends`, which is the field order dpkg-deb --control writes and the
|
|
108
|
+
// one `grep '^Depends:'` on a control file assumes. Omitted when empty rather
|
|
109
|
+
// than written as `Recommends:` with nothing after it — a field with an empty
|
|
110
|
+
// value is a parse error here, the same reason `formatDebDepend` exists.
|
|
111
|
+
if (inputs.recommends !== undefined && inputs.recommends.length > 0) {
|
|
112
|
+
fields.push(['Recommends', inputs.recommends.map(formatDebDepend).join(', ')]);
|
|
113
|
+
}
|
|
107
114
|
fields.push(['Section', settings.section], ['Priority', 'optional']);
|
|
108
115
|
if (settings.homepage)
|
|
109
116
|
fields.push(['Homepage', settings.homepage]);
|
|
@@ -43,6 +43,17 @@ export declare function warnAboutNodeFloor(format: DistroFormatId, floor: string
|
|
|
43
43
|
export interface DependsInputs {
|
|
44
44
|
/** GI namespaces the bundle imports, in `Ns-Version` spelling. */
|
|
45
45
|
namespaces: readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* The subset of {@link namespaces} the bundle declares OPTIONAL (ADR 0087 —
|
|
48
|
+
* `gi://Ns?version=X&optional`): a `Recommends:` entry, never a `Depends:`.
|
|
49
|
+
*
|
|
50
|
+
* Kept as its own list rather than filtered out of {@link namespaces}, because
|
|
51
|
+
* the AppImage has no package list at all and still has to SAY which of its host
|
|
52
|
+
* requirements are optional — and because a stage manifest written before the
|
|
53
|
+
* split carries no such field, where answering "everything is hard" is the safe
|
|
54
|
+
* direction.
|
|
55
|
+
*/
|
|
56
|
+
optionalNamespaces?: readonly string[];
|
|
46
57
|
/**
|
|
47
58
|
* Whether the payload installs into `share/icons/hicolor/`.
|
|
48
59
|
*
|
|
@@ -112,11 +123,34 @@ export interface DependsInputs {
|
|
|
112
123
|
*/
|
|
113
124
|
export declare function hostProvidedNamespaces(namespaces: readonly string[], bundledTypelibs?: readonly string[]): string[];
|
|
114
125
|
/**
|
|
115
|
-
*
|
|
126
|
+
* One format's dependency lists, split by how hard the requirement is.
|
|
127
|
+
*
|
|
128
|
+
* `requires` is what an absent package makes the app fail over; `recommends` is what
|
|
129
|
+
* its absence only costs. A namespace the BUNDLE marked optional (ADR 0087) is the
|
|
130
|
+
* second kind by declaration — the app's own `check()` degrades — and both `.deb`
|
|
131
|
+
* and `.rpm` have a field for it, so there is no reason to declare it hard.
|
|
132
|
+
*/
|
|
133
|
+
export interface DerivedDepends {
|
|
134
|
+
/** `Depends:` / `Requires:` — a package the artifact cannot run without. */
|
|
135
|
+
requires: string[];
|
|
136
|
+
/** `Recommends:` — installed by default where the format has such a field. */
|
|
137
|
+
recommends: string[];
|
|
138
|
+
/**
|
|
139
|
+
* Optional namespaces no table row is known for.
|
|
140
|
+
*
|
|
141
|
+
* REPORTED, never fatal: a `Recommends:` line for a namespace nobody mapped is
|
|
142
|
+
* the one case where saying nothing is correct — the app runs without the
|
|
143
|
+
* typelib either way, which is precisely what the flag declared. The HARD
|
|
144
|
+
* unmapped case still throws, below.
|
|
145
|
+
*/
|
|
146
|
+
unmappedOptional: string[];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The `Depends:` / `Requires:` list for one format, plus its `Recommends:`.
|
|
116
150
|
*
|
|
117
|
-
* @throws when a namespace has no entry in the table — see the module header.
|
|
151
|
+
* @throws when a HARD namespace has no entry in the table — see the module header.
|
|
118
152
|
*/
|
|
119
|
-
export declare function deriveDepends(format: DistroFormatId, inputs: DependsInputs):
|
|
153
|
+
export declare function deriveDepends(format: DistroFormatId, inputs: DependsInputs): DerivedDepends;
|
|
120
154
|
/** One parsed dependency. `relation`/`version` are absent for an unversioned one. */
|
|
121
155
|
export interface ParsedDepend {
|
|
122
156
|
name: string;
|
|
@@ -287,9 +287,9 @@ export function hostProvidedNamespaces(namespaces, bundledTypelibs = []) {
|
|
|
287
287
|
return [...new Set(namespaces)].sort().filter((ns) => !BUNDLED_TYPELIB.test(ns) && !shipped.has(ns));
|
|
288
288
|
}
|
|
289
289
|
/**
|
|
290
|
-
* The `Depends:` / `Requires:` list for one format
|
|
290
|
+
* The `Depends:` / `Requires:` list for one format, plus its `Recommends:`.
|
|
291
291
|
*
|
|
292
|
-
* @throws when a namespace has no entry in the table — see the module header.
|
|
292
|
+
* @throws when a HARD namespace has no entry in the table — see the module header.
|
|
293
293
|
*/
|
|
294
294
|
export function deriveDepends(format, inputs) {
|
|
295
295
|
// The interpreter the launcher execs, and nothing else: a package declares ONE.
|
|
@@ -297,17 +297,25 @@ export function deriveDepends(format, inputs) {
|
|
|
297
297
|
// no system Node, so an artifact for those CARRIES one from
|
|
298
298
|
// `@gjsify/node-runtime-<target>`, and that is the whole reason those three
|
|
299
299
|
// packages exist and this one line is their opposite.
|
|
300
|
-
const
|
|
300
|
+
const requires = inputs.interpreter === 'node'
|
|
301
301
|
? [`${NODE_PACKAGE[format]} >= ${inputs.minNodeVersion ?? DEFAULT_NODE_FLOOR}`]
|
|
302
302
|
: [`gjs >= ${inputs.minGjsVersion ?? DEFAULT_GJS_FLOOR}`];
|
|
303
|
+
const recommends = [];
|
|
304
|
+
const optional = new Set(inputs.optionalNamespaces ?? []);
|
|
303
305
|
const unmapped = [];
|
|
306
|
+
const unmappedOptional = [];
|
|
304
307
|
for (const namespace of hostProvidedNamespaces(inputs.namespaces, inputs.bundledTypelibs)) {
|
|
305
308
|
const entry = lookupTypelib(namespace, inputs.typelibPackages);
|
|
306
309
|
if (entry === undefined) {
|
|
307
|
-
unmapped.push(namespace);
|
|
310
|
+
(optional.has(namespace) ? unmappedOptional : unmapped).push(namespace);
|
|
308
311
|
continue;
|
|
309
312
|
}
|
|
310
|
-
|
|
313
|
+
// A namespace imported both ways is a HARD dependency — one importer asked
|
|
314
|
+
// for it unguarded — and a package manager field may not name it twice.
|
|
315
|
+
if (optional.has(namespace) && !requires.includes(entry[format]))
|
|
316
|
+
recommends.push(entry[format]);
|
|
317
|
+
else
|
|
318
|
+
requires.push(entry[format]);
|
|
311
319
|
}
|
|
312
320
|
if (unmapped.length > 0) {
|
|
313
321
|
throw new Error(`gjsify ship: the bundle imports ${unmapped.map((n) => `gi://${n.split('-')[0]}`).join(', ')}, ` +
|
|
@@ -323,7 +331,7 @@ export function deriveDepends(format, inputs) {
|
|
|
323
331
|
// The package that owns `/usr/share/icons/hicolor` — so this follows the icons, not the
|
|
324
332
|
// app/cli distinction that used to stand in for them.
|
|
325
333
|
if (inputs.hasIcons)
|
|
326
|
-
|
|
334
|
+
requires.push('hicolor-icon-theme');
|
|
327
335
|
// The package that ships `glib-compile-schemas`, NOT `gsettings-desktop-schemas`
|
|
328
336
|
// (which ships GNOME's own `org.gnome.desktop.*` schemas and has nothing to
|
|
329
337
|
// do with compiling ours). Measured: `rpm -qf /usr/bin/glib-compile-schemas`
|
|
@@ -333,11 +341,15 @@ export function deriveDepends(format, inputs) {
|
|
|
333
341
|
// `Gio.Settings.new()` aborts the app — an install that succeeds and an app
|
|
334
342
|
// that does not start.
|
|
335
343
|
if (inputs.hasSchemas)
|
|
336
|
-
|
|
337
|
-
|
|
344
|
+
requires.push(SCHEMA_COMPILER_PACKAGE[format]);
|
|
345
|
+
requires.push(...inputs.extra);
|
|
338
346
|
// Set-dedupe keeps first-insertion order, so the interpreter stays first and
|
|
339
347
|
// the list is stable across runs.
|
|
340
|
-
return
|
|
348
|
+
return {
|
|
349
|
+
requires: [...new Set(requires)],
|
|
350
|
+
recommends: [...new Set(recommends)].filter((name) => !requires.includes(name)),
|
|
351
|
+
unmappedOptional,
|
|
352
|
+
};
|
|
341
353
|
}
|
|
342
354
|
/**
|
|
343
355
|
* Resolve a scanned specifier to a table row.
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a bundle loads, split by whether the host must provide it.
|
|
3
|
+
*
|
|
4
|
+
* `namespaces` is the union — the same answer {@link scanGiNamespaces} has always
|
|
5
|
+
* given — and `optional` is the subset the artifact itself marks optional. A namespace
|
|
6
|
+
* in both is still a hard dependency as far as anything else is concerned: the
|
|
7
|
+
* importer asked for it both ways, and only one of those edges degrades.
|
|
8
|
+
*/
|
|
9
|
+
export interface GiRequirements {
|
|
10
|
+
/** Every GI namespace the bundle loads, `Ns-Version` where a specifier pins one. */
|
|
11
|
+
namespaces: string[];
|
|
12
|
+
/** The subset carrying the bundler's optional marker — absent is not fatal. */
|
|
13
|
+
optional: string[];
|
|
14
|
+
}
|
|
1
15
|
/**
|
|
2
16
|
* Extract the GI namespaces a bundle imports, as `Ns-Version` when the
|
|
3
17
|
* specifier pins one and bare `Ns` when it does not.
|
|
4
18
|
*/
|
|
5
19
|
export declare function scanGiNamespaces(source: string): string[];
|
|
20
|
+
/** {@link scanGiNamespaces}, plus which of them the artifact marks optional (ADR 0087). */
|
|
21
|
+
export declare function scanGiRequirements(source: string): GiRequirements;
|
|
6
22
|
/** `gi://Gtk?version=4.0` → `Gtk-4.0`; `gi://Gtk` → `Gtk`; anything else → null. */
|
|
7
23
|
export declare function parseGiSpecifier(specifier: string): string | null;
|
|
@@ -34,6 +34,18 @@
|
|
|
34
34
|
// `walkModuleAst` answers both questions in ONE pass, and it is the same acorn
|
|
35
35
|
// pass the CLI already uses to compute its own runtime closure — so there is one
|
|
36
36
|
// definition of "what does this file import" rather than two.
|
|
37
|
+
//
|
|
38
|
+
// HARD OR OPTIONAL, since ADR 0087: a `gi://Ns?version=X&optional` import still
|
|
39
|
+
// appears in the emitted file, so this scanner still reports its namespace — and
|
|
40
|
+
// `depends.ts` has to know it may be absent. The discriminator is the marker
|
|
41
|
+
// statement the bundler's own optional shim emits (`GI_OPTIONAL_MARKER`, imported
|
|
42
|
+
// from the plugin that writes it rather than restated here). NOT the `import()`
|
|
43
|
+
// SHAPE, which was the tempting reading of the same file: an app that writes its own
|
|
44
|
+
// `await import('gi://Goa')` without the flag must stay a HARD dependency, and
|
|
45
|
+
// classifying it as optional would move a package the host must have out of its
|
|
46
|
+
// `Depends:` — the failure this whole chain exists to prevent, reached from the other
|
|
47
|
+
// direction.
|
|
48
|
+
import { GI_OPTIONAL_MARKER } from '@gjsify/rolldown-plugin-gjsify';
|
|
37
49
|
import { importedSpecifier, staticStringValue, walkModuleAst, } from '../cli-runtime-closure.js';
|
|
38
50
|
/** The module a `--app node` bundle reaches GI through. */
|
|
39
51
|
const NODE_GI_MODULE = '@gjsify/node-gi/gi';
|
|
@@ -44,7 +56,12 @@ const NAMESPACE = /^[A-Za-z][A-Za-z\d_]*$/;
|
|
|
44
56
|
* specifier pins one and bare `Ns` when it does not.
|
|
45
57
|
*/
|
|
46
58
|
export function scanGiNamespaces(source) {
|
|
59
|
+
return scanGiRequirements(source).namespaces;
|
|
60
|
+
}
|
|
61
|
+
/** {@link scanGiNamespaces}, plus which of them the artifact marks optional (ADR 0087). */
|
|
62
|
+
export function scanGiRequirements(source) {
|
|
47
63
|
const found = new Set();
|
|
64
|
+
const optional = new Set();
|
|
48
65
|
// TWO SETS, because the two kinds of binding are not interchangeable and
|
|
49
66
|
// treating them as one over-approximated on a real minified bundle. A
|
|
50
67
|
// namespace object (`import * as gi`) and a required module object are only
|
|
@@ -89,11 +106,57 @@ export function scanGiNamespaces(source) {
|
|
|
89
106
|
calls.push(node);
|
|
90
107
|
});
|
|
91
108
|
for (const call of calls) {
|
|
109
|
+
const marker = optionalMarkerNamespace(call);
|
|
110
|
+
if (marker !== null) {
|
|
111
|
+
optional.add(marker);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
92
114
|
const key = requireGiNamespace(call, callable, objects);
|
|
93
115
|
if (key !== null)
|
|
94
116
|
found.add(key);
|
|
95
117
|
}
|
|
96
|
-
return [...found].sort();
|
|
118
|
+
return { namespaces: [...found].sort(), optional: [...optional].filter((ns) => found.has(ns)).sort() };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The namespace an optional-marker call names, or `null` when this is not one.
|
|
122
|
+
*
|
|
123
|
+
* The shape is the bundler's: `globalThis[Symbol.for(GI_OPTIONAL_MARKER)]?.("Ns", "X")`,
|
|
124
|
+
* matched on BOTH halves rather than on the whole statement — the minifier rewrites
|
|
125
|
+
* the string literals to backticks and the optional call to a `ChainExpression`
|
|
126
|
+
* around the `CallExpression` this walk still reaches, and a plain call (a host that
|
|
127
|
+
* has a handler the minifier could prove is non-null) reads the same way.
|
|
128
|
+
*/
|
|
129
|
+
function optionalMarkerNamespace(call) {
|
|
130
|
+
const callee = call.callee;
|
|
131
|
+
if (callee?.type !== 'MemberExpression' || callee.computed !== true)
|
|
132
|
+
return null;
|
|
133
|
+
// `globalThis[Symbol.for(KEY)]` — a computed member whose PROPERTY is the
|
|
134
|
+
// `Symbol.for` call, so the key is an argument of that inner call rather than
|
|
135
|
+
// of the outer one. Verified against acorn's own shape: `optional` chaining
|
|
136
|
+
// wraps the whole thing in a `ChainExpression`, which the walk descends into,
|
|
137
|
+
// so the `CallExpression` reaches this function either way.
|
|
138
|
+
const keyCall = callee.property;
|
|
139
|
+
if (keyCall?.type !== 'CallExpression')
|
|
140
|
+
return null;
|
|
141
|
+
const keyCallee = keyCall.callee;
|
|
142
|
+
const keyObject = keyCallee?.type === 'MemberExpression' ? keyCallee.object : undefined;
|
|
143
|
+
const keyProperty = keyCallee?.type === 'MemberExpression' ? keyCallee.property : undefined;
|
|
144
|
+
if (keyObject?.type !== 'Identifier' || keyObject.name !== 'Symbol')
|
|
145
|
+
return null;
|
|
146
|
+
if (keyProperty?.type !== 'Identifier' || keyProperty.name !== 'for')
|
|
147
|
+
return null;
|
|
148
|
+
const keyArgs = keyCall.arguments ?? [];
|
|
149
|
+
if (staticStringValue(keyArgs[0]) !== GI_OPTIONAL_MARKER)
|
|
150
|
+
return null;
|
|
151
|
+
const host = callee.object;
|
|
152
|
+
if (host?.type !== 'Identifier' || host.name !== 'globalThis')
|
|
153
|
+
return null;
|
|
154
|
+
const args = call.arguments ?? [];
|
|
155
|
+
const namespace = staticStringValue(args[0]);
|
|
156
|
+
if (namespace === null || !NAMESPACE.test(namespace))
|
|
157
|
+
return null;
|
|
158
|
+
const version = staticStringValue(args[1]);
|
|
159
|
+
return version === null || version === '' ? namespace : `${namespace}-${version}`;
|
|
97
160
|
}
|
|
98
161
|
/** `gi://Gtk?version=4.0` → `Gtk-4.0`; `gi://Gtk` → `Gtk`; anything else → null. */
|
|
99
162
|
export function parseGiSpecifier(specifier) {
|
|
@@ -171,7 +171,7 @@ function renderPrefixLauncher(settings, bundleRelPath, layout) {
|
|
|
171
171
|
* IT EXECS WHAT THE BUNDLE CARRIES. `runtime` comes from
|
|
172
172
|
* `utils/ship/app-runtime.ts` and names the interpreter, the relocated GTK
|
|
173
173
|
* closure and the node-gi addon staged inside `Contents/` — so the exec line is
|
|
174
|
-
* `"$here/node"` and the two locators below point into `
|
|
174
|
+
* `"$here/node"` and the two locators below point into `dirs.native`.
|
|
175
175
|
* When the stage carries none of them every line here is byte-identical to what
|
|
176
176
|
* M2a wrote, which is what keeps a bundle assembled without the runtime packages
|
|
177
177
|
* exactly as (un)usable as it was rather than differently broken.
|
|
@@ -261,7 +261,7 @@ function renderAppBundleLauncher(settings, bundleRelPath, layout, runtime) {
|
|
|
261
261
|
// `node_modules`, and a shipped `.app` has neither: its JavaScript is one
|
|
262
262
|
// bundled file under `Contents/Resources/lib`, so `import.meta.url` there
|
|
263
263
|
// resolves node-gi's "package root" to the bundle's directory and every probed
|
|
264
|
-
// path lands beside the bundle instead of in `
|
|
264
|
+
// path lands beside the bundle instead of in `dirs.native`.
|
|
265
265
|
//
|
|
266
266
|
// `NODE_GI_NATIVE` is the same problem for the addon: `prebuildAddonPath()`
|
|
267
267
|
// joins `prebuilds/<target>/node_gi.node` onto that same wrong root
|
package/lib/utils/ship/layout.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// is the first layout that is NOT a prefix substitution: the launcher, the bundle,
|
|
8
8
|
// the shared libraries and the data tree each move somewhere different, and on
|
|
9
9
|
// macOS the native files leave the bundle directory altogether for
|
|
10
|
-
// `Contents/
|
|
10
|
+
// `Contents/Resources/native` — a path Apple's own layout does not have, for the
|
|
11
|
+
// measured reason {@link layoutForOs}'s darwin row carries.
|
|
11
12
|
//
|
|
12
13
|
// So the layout is a MAP from the prefix-relative plan to a stage-relative path,
|
|
13
14
|
// and `planStage` keeps producing the prefix-relative plan for all three. That
|
|
@@ -168,12 +169,34 @@ export const LAYOUTS = {
|
|
|
168
169
|
// everything that is not, and `Contents/Frameworks` the dylibs — which is
|
|
169
170
|
// the split that makes this more than a prefix: the `gi/` directory that
|
|
170
171
|
// sits INSIDE `lib/<name>/` on Linux leaves the bundle directory entirely.
|
|
172
|
+
//
|
|
173
|
+
// …AND THE NATIVE DIRECTORY IS NOT APPLE'S, because `Contents/Frameworks`
|
|
174
|
+
// is RESERVED FOR NESTED CODE and one directory of it is loadable code
|
|
175
|
+
// that cannot be there. Measured on darwin-arm64 / macOS 27 with the
|
|
176
|
+
// published `@gjsify/gtk-runtime-darwin-arm64` staged by M2b: `codesign`
|
|
177
|
+
// refuses the BUNDLE SEAL with *"bundle format unrecognized, invalid, or
|
|
178
|
+
// unsuitable / In subcomponent: …/gtk/lib/gstreamer-1.0"*, so
|
|
179
|
+
// `gjsify ship darwin --sign` cannot sign a real GTK application at all —
|
|
180
|
+
// and it does so at exit 1 with the payload half-signed. The trigger is not
|
|
181
|
+
// gstreamer: it is any directory under `Contents/Frameworks` whose last
|
|
182
|
+
// dot-separated component parses as a version, because `codesign` reads
|
|
183
|
+
// `foo-1.0`, `foo1.0` and `Versions/1.0` as a VERSIONED NESTED BUNDLE
|
|
184
|
+
// (`<name>-<version>` is a framework's shape) and then finds no
|
|
185
|
+
// `Contents/Info.plist` inside. The relocated closure carries three:
|
|
186
|
+
// `girepository-1.0`, `gdk-pixbuf-2.0/2.10.0` and `lib/gstreamer-1.0`.
|
|
187
|
+
//
|
|
188
|
+
// The same three under `Contents/Resources` seal and verify at exit 0, which
|
|
189
|
+
// is the whole measurement. So `native` is the ONE seam that moves: the
|
|
190
|
+
// relocated closure, the addon, the app's own `gi/` dylibs and both launchers'
|
|
191
|
+
// `GI_TYPELIB_PATH`/`GJSIFY_GI_LIBRARY_PATH` all read it, so the tree and
|
|
192
|
+
// every path that names it move together. Library validation is unaffected —
|
|
193
|
+
// it follows the SIGNATURE (§ `signing.ts`'s darwin row), not the directory.
|
|
171
194
|
dirs: (identity) => {
|
|
172
195
|
const app = appBundleDir(identity);
|
|
173
196
|
return {
|
|
174
197
|
launcher: `${app}/Contents/MacOS`,
|
|
175
198
|
bundle: `${app}/Contents/Resources/lib`,
|
|
176
|
-
native: `${app}/Contents/
|
|
199
|
+
native: `${app}/Contents/Resources/native`,
|
|
177
200
|
data: `${app}/Contents/Resources/share`,
|
|
178
201
|
other: `${app}/Contents/Resources`,
|
|
179
202
|
};
|