@gjsify/cli 0.42.0 → 0.44.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 +444 -415
- package/lib/actions/build.d.ts +6 -0
- package/lib/actions/build.js +66 -9
- package/lib/commands/affected-classify.js +7 -5
- package/lib/commands/build.js +6 -0
- package/lib/commands/flatpak/ci.js +10 -2
- package/lib/commands/flatpak/init.js +15 -16
- package/lib/commands/flatpak/utils.d.ts +0 -26
- package/lib/commands/flatpak/utils.js +4 -29
- package/lib/commands/ship.d.ts +2 -0
- package/lib/commands/ship.js +358 -104
- package/lib/config.js +25 -0
- package/lib/types/cli-build-options.d.ts +11 -0
- package/lib/types/config-data.d.ts +106 -2
- package/lib/utils/flatpak-runtime.d.ts +41 -0
- package/lib/utils/flatpak-runtime.js +54 -0
- package/lib/utils/gjs-bundle-guard.d.ts +57 -3
- package/lib/utils/gjs-bundle-guard.js +122 -13
- package/lib/utils/jsx-config.d.ts +45 -0
- package/lib/utils/jsx-config.js +98 -0
- package/lib/utils/onboard-probe.d.ts +23 -0
- package/lib/utils/onboard-probe.js +65 -0
- package/lib/utils/ship/deb.d.ts +3 -3
- package/lib/utils/ship/deb.js +6 -2
- package/lib/utils/ship/depends.d.ts +13 -13
- package/lib/utils/ship/depends.js +25 -3
- package/lib/utils/ship/discover.js +72 -6
- package/lib/utils/ship/flatpak-config.d.ts +66 -0
- package/lib/utils/ship/flatpak-config.js +132 -0
- package/lib/utils/ship/flatpak.d.ts +45 -0
- package/lib/utils/ship/flatpak.js +148 -0
- package/lib/utils/ship/formats.d.ts +35 -0
- package/lib/utils/ship/formats.js +152 -5
- package/lib/utils/ship/mime.d.ts +19 -0
- package/lib/utils/ship/mime.js +89 -0
- package/lib/utils/ship/payload.d.ts +76 -0
- package/lib/utils/ship/payload.js +91 -1
- package/lib/utils/ship/plan.d.ts +30 -1
- package/lib/utils/ship/plan.js +54 -2
- package/lib/utils/ship/rpm.d.ts +3 -3
- package/lib/utils/ship/rpm.js +20 -3
- package/lib/utils/ship/scripts.d.ts +14 -4
- package/lib/utils/ship/scripts.js +26 -8
- package/lib/utils/ship/settings.js +20 -0
- package/lib/utils/ship/stage-manifest.d.ts +163 -0
- package/lib/utils/ship/stage-manifest.js +445 -0
- package/lib/utils/ship/stage-writer.d.ts +30 -3
- package/lib/utils/ship/stage-writer.js +76 -9
- package/lib/utils/ship/types.d.ts +203 -27
- package/lib/utils/ship/types.js +5 -0
- package/package.json +19 -19
|
@@ -11,6 +11,43 @@
|
|
|
11
11
|
import { DEFAULT_REGISTRY, registryFor } from '@gjsify/npm-registry';
|
|
12
12
|
import { classifyTrustList, trustUrl } from './trust-registry.js';
|
|
13
13
|
export const PROBE_RETRY_DELAY_MS = 400;
|
|
14
|
+
/**
|
|
15
|
+
* Does the registry serve a packument for this name?
|
|
16
|
+
*
|
|
17
|
+
* THE ORACLE, and the reason it exists: npm's trust endpoint answers `2xx` with an
|
|
18
|
+
* EMPTY LIST for a name that was never published. `classifyTrustList` reads that as
|
|
19
|
+
* `untrusted`, so `gjsify onboard` planned "already on npm, just needs trust" for a
|
|
20
|
+
* package that did not exist — it trusted the name, reported `0 to publish+trust`
|
|
21
|
+
* and published NOTHING. A bootstrap command reporting its own gap as closed, and
|
|
22
|
+
* it is why the v0.36.0 repair failed twice before anyone looked at the registry.
|
|
23
|
+
*
|
|
24
|
+
* `GET <registry>/<name>` is unauthenticated and needs no OTP, so it answers in the
|
|
25
|
+
* one situation that matters: a `--dry-run` before the maintainer has an OTP to
|
|
26
|
+
* hand, where every trust read is `401` and the plan is otherwise empty.
|
|
27
|
+
*
|
|
28
|
+
* `null` means UNDECIDED (network or an unexpected status) — never "absent". A
|
|
29
|
+
* bootstrap that publishes on a failed probe is worse than one that reports blocked.
|
|
30
|
+
*/
|
|
31
|
+
export async function packumentExists(registry, name) {
|
|
32
|
+
const base = registry.endsWith('/') ? registry.slice(0, -1) : registry;
|
|
33
|
+
try {
|
|
34
|
+
// `Accept` narrows the response to the abbreviated packument: the full one
|
|
35
|
+
// for a package like `@gjsify/cli` is megabytes, and existence is all we ask.
|
|
36
|
+
const res = await fetch(`${base}/${name.replace('/', '%2f')}`, {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
39
|
+
});
|
|
40
|
+
if (res.status === 404)
|
|
41
|
+
return false;
|
|
42
|
+
if (res.ok)
|
|
43
|
+
return true;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// A DNS or TLS failure is not evidence of absence.
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
14
51
|
/** Default probe concurrency — kept SMALL so a single token never bursts npm. */
|
|
15
52
|
export const DEFAULT_PROBE_CONCURRENCY = 4;
|
|
16
53
|
/** Map a classified trust state to the action the onboard sweep should take. */
|
|
@@ -48,6 +85,34 @@ export async function probeTrustState(request, ws, ctx, opts = {}) {
|
|
|
48
85
|
res = await request('GET', url);
|
|
49
86
|
state = classifyTrustList(res.status, res.json, classifyOpts);
|
|
50
87
|
}
|
|
88
|
+
// `untrusted` is the ONE ambiguous answer: the trust endpoint returns `2xx` with
|
|
89
|
+
// an empty list both for "published, nobody trusted it" and for "no such name".
|
|
90
|
+
// Only that case pays for the extra read — `trusted` cannot be a missing package
|
|
91
|
+
// and `unpublished` already has its answer.
|
|
92
|
+
// Two questions, two endpoints. The trust list answers "who may publish this",
|
|
93
|
+
// and onboard also needs "does this name exist at all" — which it can only
|
|
94
|
+
// INFER from the trust list, and infers wrongly in both ambiguous states:
|
|
95
|
+
//
|
|
96
|
+
// untrusted npm serves 2xx and an EMPTY list for a name that was never
|
|
97
|
+
// published — byte-identical to a real package that simply has
|
|
98
|
+
// no trusted publisher configured.
|
|
99
|
+
// auth-required a 401 says nothing whatsoever about existence. It is also
|
|
100
|
+
// where EVERY package lands in a `--dry-run` run before the
|
|
101
|
+
// maintainer has an OTP to hand, which is exactly the moment
|
|
102
|
+
// the plan is being read to decide whether to proceed.
|
|
103
|
+
//
|
|
104
|
+
// The packument answers the question directly and unauthenticated, and a 404
|
|
105
|
+
// there is proof no OTP could change. Only these two states pay for the extra
|
|
106
|
+
// read: `trusted` cannot be a missing package, and `unpublished` already has
|
|
107
|
+
// its answer from the endpoint it just asked.
|
|
108
|
+
if (state === 'untrusted' || state === 'auth-required') {
|
|
109
|
+
const exists = await (opts.exists ?? packumentExists)(registry, ws.name);
|
|
110
|
+
// ONLY a definitive 404 reclassifies. `null` — network failure, unexpected
|
|
111
|
+
// status — leaves the state exactly as it was: a bootstrap that publishes
|
|
112
|
+
// on a failed read is worse than one that reports itself blocked.
|
|
113
|
+
if (exists === false)
|
|
114
|
+
state = 'unpublished';
|
|
115
|
+
}
|
|
51
116
|
return { ws, registry, url, state, httpStatus: res.status, action: actionForState(state) };
|
|
52
117
|
}
|
|
53
118
|
/**
|
package/lib/utils/ship/deb.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type {
|
|
1
|
+
import { type PayloadEntry } from './payload.js';
|
|
2
|
+
import type { PackSettings } from './types.js';
|
|
3
3
|
export interface DebInputs {
|
|
4
|
-
settings:
|
|
4
|
+
settings: PackSettings;
|
|
5
5
|
/** The staged payload, prefix-relative. */
|
|
6
6
|
payload: readonly PayloadEntry[];
|
|
7
7
|
/** Install prefix, e.g. `/usr`. */
|
package/lib/utils/ship/deb.js
CHANGED
|
@@ -20,8 +20,9 @@ import { createArArchive } from './ar.js';
|
|
|
20
20
|
import { formatDebDepend } from './depends.js';
|
|
21
21
|
import { gzipDeterministic } from './gzip.js';
|
|
22
22
|
import { renderDebScripts } from './scripts.js';
|
|
23
|
+
import { readPayloadFacts } from './payload.js';
|
|
23
24
|
export async function buildDeb(inputs) {
|
|
24
|
-
const {
|
|
25
|
+
const { payload, mtime } = inputs;
|
|
25
26
|
const prefix = inputs.prefix.replace(/^\/+/, '');
|
|
26
27
|
const named = payload.map((entry) => ({ ...entry, tarPath: `./${posix.join(prefix, entry.path)}` }));
|
|
27
28
|
const directories = ancestorDirectories(named.map((entry) => entry.tarPath));
|
|
@@ -44,7 +45,10 @@ export async function buildDeb(inputs) {
|
|
|
44
45
|
{ name: './control', body: control, mode: 0o644, mtime },
|
|
45
46
|
{ name: './md5sums', body: md5sums, mode: 0o644, mtime },
|
|
46
47
|
];
|
|
47
|
-
|
|
48
|
+
// From the PAYLOAD, not the settings: the maintainer scripts refresh exactly the
|
|
49
|
+
// directories this package wrote into, and since ADR 0024 § A2 this packer also runs on a
|
|
50
|
+
// host that has the staged tree and no project to ask.
|
|
51
|
+
const maintainerScripts = renderDebScripts(readPayloadFacts(payload), inputs.prefix);
|
|
48
52
|
for (const [name, body] of Object.entries(maintainerScripts)) {
|
|
49
53
|
// dpkg refuses a maintainer script outside 0555–0775 with the mode in
|
|
50
54
|
// the error, so this is 0755 rather than "whatever the source had".
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DistroFormatId } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* The GJS the bundles target (root AGENTS.md § Constraints: GJS 1.86 /
|
|
4
4
|
* SpiderMonkey 140, Rolldown `firefox140`). A `--app gjs` bundle can carry
|
|
@@ -14,25 +14,25 @@ import type { FormatId } from './types.js';
|
|
|
14
14
|
* is how a project whose bundle really does run on older GJS lowers it.
|
|
15
15
|
*/
|
|
16
16
|
export declare const DEFAULT_GJS_FLOOR = "1.86";
|
|
17
|
-
|
|
18
|
-
* Warn when the derived floor cannot be satisfied by a released Debian.
|
|
19
|
-
* Returns the warning lines; empty when the floor is fine.
|
|
20
|
-
*/
|
|
21
|
-
export declare function warnAboutGjsFloor(format: FormatId, floor: string): string[];
|
|
17
|
+
export declare function warnAboutGjsFloor(format: DistroFormatId, floor: string): string[];
|
|
22
18
|
export interface DependsInputs {
|
|
23
19
|
/** GI namespaces the bundle imports, in `Ns-Version` spelling. */
|
|
24
20
|
namespaces: readonly string[];
|
|
25
|
-
/**
|
|
26
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Whether the payload installs into `share/icons/hicolor/`.
|
|
23
|
+
*
|
|
24
|
+
* Was `kind: 'app' | 'cli'`, which is a proxy for the question and not the question:
|
|
25
|
+
* `hicolor-icon-theme` is depended on because it OWNS that directory, so an app that
|
|
26
|
+
* installs no icon used to declare a dependency it never touched. Derived from the payload
|
|
27
|
+
* (`readPayloadFacts`), which is also the only thing the packing host has (ADR 0024 § A2).
|
|
28
|
+
*/
|
|
29
|
+
hasIcons: boolean;
|
|
27
30
|
/** Whether the payload installs GSettings schemas. */
|
|
28
31
|
hasSchemas: boolean;
|
|
29
32
|
/** User-supplied additions for this format. */
|
|
30
33
|
extra: readonly string[];
|
|
31
34
|
/** Project-supplied table rows, filling gaps in {@link TYPELIB_PACKAGES}. */
|
|
32
|
-
typelibPackages?: Record<string,
|
|
33
|
-
deb: string;
|
|
34
|
-
rpm: string;
|
|
35
|
-
}>;
|
|
35
|
+
typelibPackages?: Record<string, Record<DistroFormatId, string>>;
|
|
36
36
|
/**
|
|
37
37
|
* Paths of typelib files the payload carries itself. The namespaces they cover need no distro
|
|
38
38
|
* dependency — and are derived from these filenames, never declared separately.
|
|
@@ -46,7 +46,7 @@ export interface DependsInputs {
|
|
|
46
46
|
*
|
|
47
47
|
* @throws when a namespace has no entry in the table — see the module header.
|
|
48
48
|
*/
|
|
49
|
-
export declare function deriveDepends(format:
|
|
49
|
+
export declare function deriveDepends(format: DistroFormatId, inputs: DependsInputs): string[];
|
|
50
50
|
/** One parsed dependency. `relation`/`version` are absent for an unversioned one. */
|
|
51
51
|
export interface ParsedDepend {
|
|
52
52
|
name: string;
|
|
@@ -35,14 +35,34 @@
|
|
|
35
35
|
* is how a project whose bundle really does run on older GJS lowers it.
|
|
36
36
|
*/
|
|
37
37
|
export const DEFAULT_GJS_FLOOR = '1.86';
|
|
38
|
+
/**
|
|
39
|
+
* The package providing `glib-compile-schemas`, per format.
|
|
40
|
+
*
|
|
41
|
+
* A record, not `format === 'deb' ? … : …`. The ternary reads as a two-way
|
|
42
|
+
* choice and is really "deb, or ELSE rpm's package name" — measured on a third
|
|
43
|
+
* format, `dmg` got `glib2`, at exit 0, in the `Depends:` of a package that has
|
|
44
|
+
* no such thing. Adding a `DistroFormatId` now fails to build here instead.
|
|
45
|
+
*/
|
|
46
|
+
const SCHEMA_COMPILER_PACKAGE = { deb: 'libglib2.0-bin', rpm: 'glib2' };
|
|
38
47
|
/** Debian's `gjs` versions per suite, as measured 2026-08-15 — see {@link warnAboutGjsFloor}. */
|
|
39
48
|
const DEBIAN_GJS = 'trixie ships 1.82.3 and forky 1.88.1; 1.84 and 1.86 were skipped';
|
|
40
49
|
/**
|
|
41
50
|
* Warn when the derived floor cannot be satisfied by a released Debian.
|
|
42
51
|
* Returns the warning lines; empty when the floor is fine.
|
|
43
52
|
*/
|
|
53
|
+
/**
|
|
54
|
+
* Which formats this Debian-suite warning is ABOUT.
|
|
55
|
+
*
|
|
56
|
+
* A `Record<DistroFormatId, …>` and not `format !== 'deb'`: the negative form answers
|
|
57
|
+
* for every format that will ever exist, and answers "stay quiet" — so a third
|
|
58
|
+
* format would inherit silence about a floor nobody has checked for its distro,
|
|
59
|
+
* with no compile error. As a record, adding a `DistroFormatId` fails to build until
|
|
60
|
+
* someone decides. `rpm` is false because Fedora ships a current GJS, which is a
|
|
61
|
+
* measured fact about Fedora, not a default.
|
|
62
|
+
*/
|
|
63
|
+
const GJS_FLOOR_IS_DEBIAN_NEWS = { deb: true, rpm: false };
|
|
44
64
|
export function warnAboutGjsFloor(format, floor) {
|
|
45
|
-
if (format
|
|
65
|
+
if (!GJS_FLOOR_IS_DEBIAN_NEWS[format])
|
|
46
66
|
return [];
|
|
47
67
|
// Only a floor forky ACTUALLY satisfies is quiet. The first cut tested
|
|
48
68
|
// `>= 1.88.1` and therefore went silent for 1.90 and 2.0 as well — the
|
|
@@ -171,7 +191,9 @@ export function deriveDepends(format, inputs) {
|
|
|
171
191
|
'have to. (`gjsify.ship.depends.<format>` is for dependencies that are not typelibs at all — it ' +
|
|
172
192
|
'appends, and deliberately does not silence this.)');
|
|
173
193
|
}
|
|
174
|
-
|
|
194
|
+
// The package that owns `/usr/share/icons/hicolor` — so this follows the icons, not the
|
|
195
|
+
// app/cli distinction that used to stand in for them.
|
|
196
|
+
if (inputs.hasIcons)
|
|
175
197
|
out.push('hicolor-icon-theme');
|
|
176
198
|
// The package that ships `glib-compile-schemas`, NOT `gsettings-desktop-schemas`
|
|
177
199
|
// (which ships GNOME's own `org.gnome.desktop.*` schemas and has nothing to
|
|
@@ -182,7 +204,7 @@ export function deriveDepends(format, inputs) {
|
|
|
182
204
|
// `Gio.Settings.new()` aborts the app — an install that succeeds and an app
|
|
183
205
|
// that does not start.
|
|
184
206
|
if (inputs.hasSchemas)
|
|
185
|
-
out.push(format
|
|
207
|
+
out.push(SCHEMA_COMPILER_PACKAGE[format]);
|
|
186
208
|
out.push(...inputs.extra);
|
|
187
209
|
// Set-dedupe keeps first-insertion order, so `gjs` stays first and the
|
|
188
210
|
// list is stable across runs.
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// so `settings.ts` and `plan.ts` can stay pure. Every probe has a configured
|
|
5
5
|
// override; the probes exist so a project that follows the GNOME convention
|
|
6
6
|
// (`data/icons/hicolor/…`, `data/*.gschema.xml`) needs no configuration at all.
|
|
7
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
8
|
-
import { isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
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
11
|
export function discoverPayload(input) {
|
|
@@ -189,6 +189,34 @@ function discoverSchemas(projectDir, configured) {
|
|
|
189
189
|
}
|
|
190
190
|
return [];
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* The licence text this package ships — searched UP the tree, not just beside
|
|
194
|
+
* `package.json`.
|
|
195
|
+
*
|
|
196
|
+
* A monorepo package does not carry its own copy. `packages/infra/cli`
|
|
197
|
+
* declares `"license": "MIT"` and the text lives one `LICENSE` up at the
|
|
198
|
+
* repository root, shared by sixty-odd packages; searching `projectDir` alone
|
|
199
|
+
* found nothing, `planOverlay` turned "nothing" into an empty overlay, and the
|
|
200
|
+
* `.deb` shipped without the `/usr/share/doc/<pkg>/copyright` that Debian
|
|
201
|
+
* Policy § 12.5 requires. Nothing downstream noticed: `dpkg-deb --info` prints
|
|
202
|
+
* a clean control file and `dpkg -i` installs it. It took the first leg that
|
|
203
|
+
* ran a real `lintian` to say so.
|
|
204
|
+
*
|
|
205
|
+
* WHERE THE SEARCH STOPS is the whole design. It climbs to the first ancestor
|
|
206
|
+
* that is a project root — one holding `.git`, or a `package.json` declaring
|
|
207
|
+
* `workspaces` — and searches that directory too, then stops. Above that line
|
|
208
|
+
* a `LICENSE` belongs to some unrelated tree that happens to be a parent
|
|
209
|
+
* directory (`~/Projekte/LICENSE` is not yours), and packaging its text would
|
|
210
|
+
* make a specific false legal claim. When NO marker is found the search is the
|
|
211
|
+
* project directory alone: without a marker there is no way to tell where the
|
|
212
|
+
* project ends, and guessing wrong is the same false claim.
|
|
213
|
+
*
|
|
214
|
+
* What this cannot check is whether the inherited text is the RIGHT licence
|
|
215
|
+
* for this package — an Apache-2.0 package inside an MIT monorepo would
|
|
216
|
+
* inherit the wrong one. Reading an SPDX identifier out of licence prose is
|
|
217
|
+
* not something to guess at, so the caller PRINTS the inherited path instead
|
|
218
|
+
* (see `commands/ship.ts`), and `gjsify.ship.licenseFile` overrides it.
|
|
219
|
+
*/
|
|
192
220
|
function discoverLicense(projectDir, configured) {
|
|
193
221
|
if (configured !== undefined) {
|
|
194
222
|
const target = resolve(projectDir, configured);
|
|
@@ -197,13 +225,51 @@ function discoverLicense(projectDir, configured) {
|
|
|
197
225
|
}
|
|
198
226
|
return target;
|
|
199
227
|
}
|
|
200
|
-
for (const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
228
|
+
for (const dir of licenseSearchDirs(projectDir)) {
|
|
229
|
+
for (const name of LICENSE_FILE_NAMES) {
|
|
230
|
+
const target = join(dir, name);
|
|
231
|
+
if (existsSync(target))
|
|
232
|
+
return target;
|
|
233
|
+
}
|
|
204
234
|
}
|
|
205
235
|
return undefined;
|
|
206
236
|
}
|
|
237
|
+
/** `projectDir` first, then each ancestor up to and including the project root. */
|
|
238
|
+
function licenseSearchDirs(projectDir) {
|
|
239
|
+
const dirs = [];
|
|
240
|
+
for (let current = projectDir;;) {
|
|
241
|
+
dirs.push(current);
|
|
242
|
+
if (isProjectRoot(current))
|
|
243
|
+
return dirs;
|
|
244
|
+
const parent = dirname(current);
|
|
245
|
+
if (parent === current)
|
|
246
|
+
return [projectDir];
|
|
247
|
+
current = parent;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Is this the top of the tree the project belongs to?
|
|
252
|
+
*
|
|
253
|
+
* `.git` is tested with `existsSync` rather than `statSync().isDirectory()`
|
|
254
|
+
* because in a git WORKTREE it is a file, and this repository is developed in
|
|
255
|
+
* worktrees.
|
|
256
|
+
*/
|
|
257
|
+
function isProjectRoot(dir) {
|
|
258
|
+
if (existsSync(join(dir, '.git')))
|
|
259
|
+
return true;
|
|
260
|
+
const manifest = join(dir, 'package.json');
|
|
261
|
+
if (!existsSync(manifest))
|
|
262
|
+
return false;
|
|
263
|
+
try {
|
|
264
|
+
return JSON.parse(readFileSync(manifest, 'utf-8')).workspaces !== undefined;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// A malformed `package.json` in some ancestor is not this command's
|
|
268
|
+
// business, and it must not turn a licence search into a ship failure.
|
|
269
|
+
// Not a root, then: the climb continues and `.git` still stops it.
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
207
273
|
/**
|
|
208
274
|
* Every regular file under `dir`, as POSIX-separated relative paths.
|
|
209
275
|
*
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ConfigDataFlatpak, ConfigDataShip, ShipFlatpakOptions } from '../../types/config-data.js';
|
|
2
|
+
import type { ShipFlatpakSettings } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The release that stops reading `gjsify.flatpak` for the keys below.
|
|
5
|
+
*
|
|
6
|
+
* A major, not the next minor: the block is in published releases and in
|
|
7
|
+
* downstream `package.json`s that this repository does not control, and a
|
|
8
|
+
* window measured in weeks is a window nobody notices before it closes.
|
|
9
|
+
*/
|
|
10
|
+
export declare const LEGACY_FLATPAK_KEYS_REMOVED_IN = "1.0.0";
|
|
11
|
+
/** The keys whose new home is `gjsify.ship.flatpak`, in the order the warning lists them. */
|
|
12
|
+
export declare const MIGRATED_FLATPAK_KEYS: readonly ["runtime", "runtimeVersion", "sdkExtensions", "appendPath", "finishArgs", "cleanup"];
|
|
13
|
+
export type MigratedFlatpakKey = (typeof MIGRATED_FLATPAK_KEYS)[number];
|
|
14
|
+
/** The six keys as a reader sees them, before any default is applied. */
|
|
15
|
+
export type MigratedFlatpakValues = {
|
|
16
|
+
[K in MigratedFlatpakKey]: ConfigDataFlatpak[K];
|
|
17
|
+
};
|
|
18
|
+
export interface PickedFlatpakBuildKeys {
|
|
19
|
+
values: MigratedFlatpakValues;
|
|
20
|
+
/** Keys that came from `gjsify.flatpak`, in {@link MIGRATED_FLATPAK_KEYS} order. */
|
|
21
|
+
fromLegacy: MigratedFlatpakKey[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `gjsify.ship.flatpak.<key>` when set, else `gjsify.flatpak.<key>` — per KEY,
|
|
25
|
+
* noting which side answered.
|
|
26
|
+
*
|
|
27
|
+
* Per-key and not per-block: a project migrating one key at a time must not
|
|
28
|
+
* lose the five it has not moved yet. That is also what makes the warning
|
|
29
|
+
* actionable — it names the keys still coming from the old block, so the reader
|
|
30
|
+
* knows what to edit rather than being told a block is deprecated.
|
|
31
|
+
*
|
|
32
|
+
* A LOOP over {@link MIGRATED_FLATPAK_KEYS}, not six hand-written picks. Written
|
|
33
|
+
* out, the list was the one unbound copy of this vocabulary: a key added to it
|
|
34
|
+
* with no `pick` call behind it compiled fine, went missing from the window, and
|
|
35
|
+
* the only test that mentioned the constant compared it to a literal copy of
|
|
36
|
+
* itself. Reading the keys off the constant is what makes "this list IS the
|
|
37
|
+
* window" true rather than asserted.
|
|
38
|
+
*
|
|
39
|
+
* NO default is applied here. The defaults differ per reader — `ship` derives
|
|
40
|
+
* `finishArgs` from `kind`, `flatpak init` also merges `--sdk-extension` flags —
|
|
41
|
+
* and a default applied here would be a third answer neither of them chose.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pickFlatpakBuildKeys(preferred: ShipFlatpakOptions | undefined, legacy: ConfigDataFlatpak): PickedFlatpakBuildKeys;
|
|
44
|
+
/** The branch a `ship` Flatpak is exported under when the project names none. */
|
|
45
|
+
export declare const DEFAULT_SHIP_FLATPAK_BRANCH = "stable";
|
|
46
|
+
export interface ShipFlatpakInput {
|
|
47
|
+
ship: ConfigDataShip;
|
|
48
|
+
flatpak: ConfigDataFlatpak;
|
|
49
|
+
/** Decides the finish-args default: a GUI app needs a display, a CLI does not. */
|
|
50
|
+
kind: 'app' | 'cli';
|
|
51
|
+
}
|
|
52
|
+
export interface ResolvedShipFlatpak {
|
|
53
|
+
settings: ShipFlatpakSettings;
|
|
54
|
+
warnings: string[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the Flatpak half of `gjsify.ship`, reading the legacy block where the
|
|
58
|
+
* new one is silent, and applying `ship`'s own defaults.
|
|
59
|
+
*
|
|
60
|
+
* The per-key fallback itself is {@link pickFlatpakBuildKeys}, shared with
|
|
61
|
+
* `gjsify flatpak init`/`ci`; what is `ship`-specific and stays here is the
|
|
62
|
+
* defaulting — `branch`, the `kind`-dependent finish-args, the derived
|
|
63
|
+
* `appendPath` — plus the warning, which only `ship` prints because only `ship`
|
|
64
|
+
* has somewhere else to point at.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveShipFlatpakSettings(input: ShipFlatpakInput): ResolvedShipFlatpak;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// The deprecation window on the `gjsify.flatpak` build keys (ADR 0024 § 8).
|
|
2
|
+
//
|
|
3
|
+
// § 8 names the cost of the migration and does not let it be skipped:
|
|
4
|
+
// "`gjsify.flatpak` is a published config contract, so the keys move with a
|
|
5
|
+
// deprecation window in which both spellings resolve and the old one warns."
|
|
6
|
+
// This file is that window.
|
|
7
|
+
//
|
|
8
|
+
// WHAT MOVED and what did not — the table is `docs/ship-formats.md`, and the
|
|
9
|
+
// short form is: the six BUILD keys moved and warn from the old spelling; the
|
|
10
|
+
// `AppMetadata` half is a designed ALIAS and is NOT deprecated (both blocks
|
|
11
|
+
// describe the same app, and warning on it would print for every project that
|
|
12
|
+
// has a `gjsify.flatpak` block at all); and the TOOLCHAIN keys are untouched
|
|
13
|
+
// because `gjsify flatpak <sub>` has not moved, so deprecating them would warn
|
|
14
|
+
// on commands with nowhere else to read from.
|
|
15
|
+
//
|
|
16
|
+
// A WINDOW HAS TWO SIDES, and the first cut only built one. The six build keys
|
|
17
|
+
// are read by `gjsify flatpak init` and `flatpak ci` as well, and those
|
|
18
|
+
// commands have NOT moved — so a project that did what this file's warning told
|
|
19
|
+
// it to do, and moved them, silently lost them there: `flatpak init` fell back
|
|
20
|
+
// to its own defaults and wrote a manifest against a different
|
|
21
|
+
// `org.gnome.Platform` version with different finish-args, committed, at exit 0.
|
|
22
|
+
// That is why {@link pickFlatpakBuildKeys} is exported and BOTH command groups
|
|
23
|
+
// go through it. "Both spellings resolve" is now true of every reader of these
|
|
24
|
+
// keys, which is what makes the advice safe to follow.
|
|
25
|
+
//
|
|
26
|
+
// WHY A NAMED VERSION, IN ONE PLACE. The precedent is
|
|
27
|
+
// `utils/normalize-bundler-options.ts`, the `esbuild` → `bundler` shim, and it
|
|
28
|
+
// also carries the precedent's DEFECT: its warning text names 0.5.0, the tree is
|
|
29
|
+
// far past it, and its header now has to say "removing the shim means fixing
|
|
30
|
+
// that string too". So the version here is a constant the message reads, and the
|
|
31
|
+
// spec asserts the message names it.
|
|
32
|
+
import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, deriveAppendPath, resolveRuntime, } from '../flatpak-runtime.js';
|
|
33
|
+
/**
|
|
34
|
+
* The release that stops reading `gjsify.flatpak` for the keys below.
|
|
35
|
+
*
|
|
36
|
+
* A major, not the next minor: the block is in published releases and in
|
|
37
|
+
* downstream `package.json`s that this repository does not control, and a
|
|
38
|
+
* window measured in weeks is a window nobody notices before it closes.
|
|
39
|
+
*/
|
|
40
|
+
export const LEGACY_FLATPAK_KEYS_REMOVED_IN = '1.0.0';
|
|
41
|
+
/** The keys whose new home is `gjsify.ship.flatpak`, in the order the warning lists them. */
|
|
42
|
+
export const MIGRATED_FLATPAK_KEYS = [
|
|
43
|
+
'runtime',
|
|
44
|
+
'runtimeVersion',
|
|
45
|
+
'sdkExtensions',
|
|
46
|
+
'appendPath',
|
|
47
|
+
'finishArgs',
|
|
48
|
+
'cleanup',
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* `gjsify.ship.flatpak.<key>` when set, else `gjsify.flatpak.<key>` — per KEY,
|
|
52
|
+
* noting which side answered.
|
|
53
|
+
*
|
|
54
|
+
* Per-key and not per-block: a project migrating one key at a time must not
|
|
55
|
+
* lose the five it has not moved yet. That is also what makes the warning
|
|
56
|
+
* actionable — it names the keys still coming from the old block, so the reader
|
|
57
|
+
* knows what to edit rather than being told a block is deprecated.
|
|
58
|
+
*
|
|
59
|
+
* A LOOP over {@link MIGRATED_FLATPAK_KEYS}, not six hand-written picks. Written
|
|
60
|
+
* out, the list was the one unbound copy of this vocabulary: a key added to it
|
|
61
|
+
* with no `pick` call behind it compiled fine, went missing from the window, and
|
|
62
|
+
* the only test that mentioned the constant compared it to a literal copy of
|
|
63
|
+
* itself. Reading the keys off the constant is what makes "this list IS the
|
|
64
|
+
* window" true rather than asserted.
|
|
65
|
+
*
|
|
66
|
+
* NO default is applied here. The defaults differ per reader — `ship` derives
|
|
67
|
+
* `finishArgs` from `kind`, `flatpak init` also merges `--sdk-extension` flags —
|
|
68
|
+
* and a default applied here would be a third answer neither of them chose.
|
|
69
|
+
*/
|
|
70
|
+
export function pickFlatpakBuildKeys(preferred, legacy) {
|
|
71
|
+
const ship = preferred ?? {};
|
|
72
|
+
// Assembled through an index signature because the value type varies per
|
|
73
|
+
// key; the cast is on the finished object, where every key is present.
|
|
74
|
+
const values = {};
|
|
75
|
+
const fromLegacy = [];
|
|
76
|
+
for (const key of MIGRATED_FLATPAK_KEYS) {
|
|
77
|
+
const own = ship[key];
|
|
78
|
+
if (own !== undefined) {
|
|
79
|
+
values[key] = own;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const inherited = legacy[key];
|
|
83
|
+
if (inherited !== undefined)
|
|
84
|
+
fromLegacy.push(key);
|
|
85
|
+
values[key] = inherited;
|
|
86
|
+
}
|
|
87
|
+
return { values: values, fromLegacy };
|
|
88
|
+
}
|
|
89
|
+
/** The branch a `ship` Flatpak is exported under when the project names none. */
|
|
90
|
+
export const DEFAULT_SHIP_FLATPAK_BRANCH = 'stable';
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the Flatpak half of `gjsify.ship`, reading the legacy block where the
|
|
93
|
+
* new one is silent, and applying `ship`'s own defaults.
|
|
94
|
+
*
|
|
95
|
+
* The per-key fallback itself is {@link pickFlatpakBuildKeys}, shared with
|
|
96
|
+
* `gjsify flatpak init`/`ci`; what is `ship`-specific and stays here is the
|
|
97
|
+
* defaulting — `branch`, the `kind`-dependent finish-args, the derived
|
|
98
|
+
* `appendPath` — plus the warning, which only `ship` prints because only `ship`
|
|
99
|
+
* has somewhere else to point at.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveShipFlatpakSettings(input) {
|
|
102
|
+
const shipFlatpak = input.ship.flatpak ?? {};
|
|
103
|
+
const { values, fromLegacy } = pickFlatpakBuildKeys(shipFlatpak, input.flatpak);
|
|
104
|
+
const resolved = resolveRuntime(values, {});
|
|
105
|
+
const extensions = values.sdkExtensions ?? [];
|
|
106
|
+
const { appendPath, finishArgs, cleanup } = values;
|
|
107
|
+
const settings = {
|
|
108
|
+
runtime: resolved.runtimeId,
|
|
109
|
+
runtimeVersion: resolved.runtimeVersion,
|
|
110
|
+
sdk: resolved.sdk,
|
|
111
|
+
branch: shipFlatpak.branch ?? DEFAULT_SHIP_FLATPAK_BRANCH,
|
|
112
|
+
sdkExtensions: [...extensions],
|
|
113
|
+
// Derived from the extensions when nothing was declared, and only then:
|
|
114
|
+
// an extension whose `/usr/lib/sdk/<x>/bin` is not on PATH inside the
|
|
115
|
+
// sandbox is an extension the build cannot use, which looks like the
|
|
116
|
+
// extension not being installed.
|
|
117
|
+
appendPath: appendPath ?? (extensions.length > 0 ? deriveAppendPath(extensions) : []),
|
|
118
|
+
finishArgs: finishArgs ?? (input.kind === 'cli' ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS),
|
|
119
|
+
cleanup: cleanup ?? [],
|
|
120
|
+
};
|
|
121
|
+
return { settings, warnings: fromLegacy.length === 0 ? [] : [deprecationWarning(fromLegacy)] };
|
|
122
|
+
}
|
|
123
|
+
/** One line, naming every key still read from the old block and where each goes. */
|
|
124
|
+
function deprecationWarning(keys) {
|
|
125
|
+
const list = keys.map((key) => `gjsify.flatpak.${key}`).join(', ');
|
|
126
|
+
const moved = keys.map((key) => `gjsify.ship.flatpak.${key}`).join(', ');
|
|
127
|
+
return (`DEPRECATION: ${list} ${keys.length > 1 ? 'are' : 'is'} read by \`gjsify ship\` for compatibility and ` +
|
|
128
|
+
`will be removed in ${LEGACY_FLATPAK_KEYS_REMOVED_IN}. Move ${keys.length > 1 ? 'them' : 'it'} to ` +
|
|
129
|
+
`${moved} — both spellings resolve until then, and \`gjsify.ship.flatpak\` wins where both are set. ` +
|
|
130
|
+
'The app METADATA in `gjsify.flatpak` (name, summary, developer, categories, licence) is NOT ' +
|
|
131
|
+
'deprecated: both blocks describe the same application and either may carry it (ADR 0024 § 8).');
|
|
132
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PackSettings } from './types.js';
|
|
2
|
+
export interface FlatpakManifestInput {
|
|
3
|
+
settings: PackSettings;
|
|
4
|
+
/** Absolute path of the staged payload — the tree `readStage` validated. */
|
|
5
|
+
stageDir: string;
|
|
6
|
+
/** Absolute path of this format's rendered overlay (the licence file). */
|
|
7
|
+
overlayDir: string;
|
|
8
|
+
/** The stage's own closure file, which is not payload and must not be copied into `/app`. */
|
|
9
|
+
stageManifestFile: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The generated Flatpak manifest, as a plain object.
|
|
13
|
+
*
|
|
14
|
+
* Pure, so every field is unit-testable without flatpak tooling — and the
|
|
15
|
+
* suite that needs that is the one running in CI, where there is none.
|
|
16
|
+
*
|
|
17
|
+
* The app id is what makes this legal Flatpak: every file flatpak EXPORTS out
|
|
18
|
+
* of `/app/share` has to be named after it, and `planStage` already enforces
|
|
19
|
+
* exactly that for its own reasons — the desktop entry, the metainfo, the icon,
|
|
20
|
+
* the GSettings schema and the shared-mime-info document are all
|
|
21
|
+
* `<appId>[.…].<ext>`, because `share/glib-2.0/schemas/` and
|
|
22
|
+
* `share/mime/packages/` are directories shared between packages. Two
|
|
23
|
+
* independent constraints, one naming rule.
|
|
24
|
+
*/
|
|
25
|
+
export declare function renderShipFlatpakManifest(input: FlatpakManifestInput): Record<string, unknown>;
|
|
26
|
+
export interface FlatpakPackInput extends FlatpakManifestInput {
|
|
27
|
+
/** Working root for the manifest, the build dir, the repo and the builder's state. */
|
|
28
|
+
workDir: string;
|
|
29
|
+
/** Absolute path the finished `.flatpak` is written to. */
|
|
30
|
+
target: string;
|
|
31
|
+
/** Flatpak's spelling of the architecture, e.g. `x86_64`. */
|
|
32
|
+
archLabel: string;
|
|
33
|
+
verbose: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the app and export a single-file bundle at `input.target`.
|
|
37
|
+
*
|
|
38
|
+
* The only packer in this tree that execs anything, and the descriptor says so
|
|
39
|
+
* (`FORMATS.flatpak.host.requiredTools`). ADR 0024 § A3 is explicit that this
|
|
40
|
+
* costs the independent oracle the hand-written deb/rpm writers bought: what
|
|
41
|
+
* pays it back here is that a `.flatpak` is an OSTree static delta, so
|
|
42
|
+
* `flatpak build-import-bundle` + `ostree ls -R` read the artifact back with
|
|
43
|
+
* code this tree did not write.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildFlatpakBundle(input: FlatpakPackInput): Promise<void>;
|