@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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// The Flatpak packer — ADR 0024 § 8, stage 6.
|
|
2
|
+
//
|
|
3
|
+
// This is a MOVE, not a second implementation. `gjsify flatpak` has emitted
|
|
4
|
+
// Flatpak manifests since long before `ship` existed; what it emitted was a
|
|
5
|
+
// `buildsystem: meson` module, which made every app carry meson glue whose only
|
|
6
|
+
// job was to call `gjsify build` and copy the result into a prefix — 158 lines
|
|
7
|
+
// of it in Learn6502, 66 of them `GI_TYPELIB_PATH`/`LD_LIBRARY_PATH` plumbing.
|
|
8
|
+
// `ship` already produces that prefix. So the module becomes:
|
|
9
|
+
//
|
|
10
|
+
// buildsystem: simple
|
|
11
|
+
// build-commands: mkdir -p /app; cp -a stage/. /app/; cp -a overlay/. /app/
|
|
12
|
+
//
|
|
13
|
+
// and meson leaves the sandbox. Nothing else about the payload changes — the
|
|
14
|
+
// launcher derives its own prefix at runtime (§ 3), which is exactly why one
|
|
15
|
+
// staged tree can be `/usr` in a `.deb` and `/app` here.
|
|
16
|
+
//
|
|
17
|
+
// FOUR THINGS ABOUT flatpak-builder 1.4.10 WERE MEASURED before this file was
|
|
18
|
+
// written, because each had a plausible wrong answer and flatpak-builder reports
|
|
19
|
+
// none of them: an absolute `dir` source `path` works, `skip` works and is what
|
|
20
|
+
// keeps the stage's own sidecar out of `/app`, `cp -a` preserves the mode, and
|
|
21
|
+
// `--show-manifest` is NOT a validator (it accepted an unknown source property
|
|
22
|
+
// and `buildsystem: "nonsense"` at exit 0, so it must never be used as a gate).
|
|
23
|
+
// The numbers, the commands and the oracle that replaces `--show-manifest`:
|
|
24
|
+
// `docs/ship-formats.md`.
|
|
25
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { describeExit, spawnToCompletion } from '../spawn.js';
|
|
28
|
+
/**
|
|
29
|
+
* The generated Flatpak manifest, as a plain object.
|
|
30
|
+
*
|
|
31
|
+
* Pure, so every field is unit-testable without flatpak tooling — and the
|
|
32
|
+
* suite that needs that is the one running in CI, where there is none.
|
|
33
|
+
*
|
|
34
|
+
* The app id is what makes this legal Flatpak: every file flatpak EXPORTS out
|
|
35
|
+
* of `/app/share` has to be named after it, and `planStage` already enforces
|
|
36
|
+
* exactly that for its own reasons — the desktop entry, the metainfo, the icon,
|
|
37
|
+
* the GSettings schema and the shared-mime-info document are all
|
|
38
|
+
* `<appId>[.…].<ext>`, because `share/glib-2.0/schemas/` and
|
|
39
|
+
* `share/mime/packages/` are directories shared between packages. Two
|
|
40
|
+
* independent constraints, one naming rule.
|
|
41
|
+
*/
|
|
42
|
+
export function renderShipFlatpakManifest(input) {
|
|
43
|
+
const { settings, stageDir, overlayDir } = input;
|
|
44
|
+
const flatpak = settings.flatpak;
|
|
45
|
+
const manifest = {
|
|
46
|
+
id: settings.appId,
|
|
47
|
+
runtime: flatpak.runtime,
|
|
48
|
+
'runtime-version': flatpak.runtimeVersion,
|
|
49
|
+
sdk: flatpak.sdk,
|
|
50
|
+
branch: flatpak.branch,
|
|
51
|
+
// `bin/<binaryName>`, which is the one executable the stage plans. NOT
|
|
52
|
+
// the app id, which is what `gjsify flatpak init` defaults to: under
|
|
53
|
+
// `ship` the launcher's name is decided by `gjsify.ship.binaryName`, and
|
|
54
|
+
// a `command` naming a file that is not in `/app/bin` produces an app
|
|
55
|
+
// that installs and cannot be launched.
|
|
56
|
+
command: settings.binaryName,
|
|
57
|
+
'finish-args': flatpak.finishArgs,
|
|
58
|
+
};
|
|
59
|
+
if (flatpak.sdkExtensions.length > 0)
|
|
60
|
+
manifest['sdk-extensions'] = flatpak.sdkExtensions;
|
|
61
|
+
if (flatpak.appendPath.length > 0) {
|
|
62
|
+
manifest['build-options'] = { 'append-path': flatpak.appendPath.join(':') };
|
|
63
|
+
}
|
|
64
|
+
if (flatpak.cleanup.length > 0)
|
|
65
|
+
manifest.cleanup = flatpak.cleanup;
|
|
66
|
+
manifest.modules = [
|
|
67
|
+
{
|
|
68
|
+
name: settings.binaryName,
|
|
69
|
+
buildsystem: 'simple',
|
|
70
|
+
// `mkdir -p /app` first: flatpak-builder creates the app directory,
|
|
71
|
+
// but a module whose only command is a `cp` into a path that does
|
|
72
|
+
// not exist yet is one refactor away from failing, and the mkdir is
|
|
73
|
+
// free. `stage/.` and not `stage` — `cp -a stage /app/` would
|
|
74
|
+
// create `/app/stage`, which installs cleanly and puts the whole
|
|
75
|
+
// payload one directory too deep.
|
|
76
|
+
'build-commands': ['mkdir -p /app', 'cp -a stage/. /app/', 'cp -a overlay/. /app/'],
|
|
77
|
+
sources: [
|
|
78
|
+
{ type: 'dir', path: stageDir, dest: 'stage', skip: [input.stageManifestFile] },
|
|
79
|
+
{ type: 'dir', path: overlayDir, dest: 'overlay' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
return manifest;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build the app and export a single-file bundle at `input.target`.
|
|
87
|
+
*
|
|
88
|
+
* The only packer in this tree that execs anything, and the descriptor says so
|
|
89
|
+
* (`FORMATS.flatpak.host.requiredTools`). ADR 0024 § A3 is explicit that this
|
|
90
|
+
* costs the independent oracle the hand-written deb/rpm writers bought: what
|
|
91
|
+
* pays it back here is that a `.flatpak` is an OSTree static delta, so
|
|
92
|
+
* `flatpak build-import-bundle` + `ostree ls -R` read the artifact back with
|
|
93
|
+
* code this tree did not write.
|
|
94
|
+
*/
|
|
95
|
+
export async function buildFlatpakBundle(input) {
|
|
96
|
+
const { settings, workDir, archLabel } = input;
|
|
97
|
+
const manifestPath = join(workDir, `${settings.appId}.json`);
|
|
98
|
+
const buildDir = join(workDir, 'build');
|
|
99
|
+
const repoDir = join(workDir, 'repo');
|
|
100
|
+
// Kept between runs: it is flatpak-builder's own ostree cache, and dropping
|
|
101
|
+
// it turns every re-pack into a full rebuild of every module.
|
|
102
|
+
const stateDir = join(workDir, 'state');
|
|
103
|
+
mkdirSync(workDir, { recursive: true });
|
|
104
|
+
// The repo is wiped, the state dir is not. A repo that survives holds the
|
|
105
|
+
// PREVIOUS commit on the same ref, and `build-bundle` would happily export
|
|
106
|
+
// it if this run's export failed for any reason that did not fail the
|
|
107
|
+
// build — a stale artifact wearing the new version's filename.
|
|
108
|
+
rmSync(repoDir, { recursive: true, force: true, maxRetries: 5 });
|
|
109
|
+
writeFileSync(manifestPath, `${JSON.stringify(renderShipFlatpakManifest(input), null, 2)}\n`);
|
|
110
|
+
await run('flatpak-builder', [
|
|
111
|
+
'--force-clean',
|
|
112
|
+
// Needed wherever FUSE is unavailable — containers, most CI runners.
|
|
113
|
+
// It only disables a read-only-bind optimisation, and a build that
|
|
114
|
+
// cannot start is worse than one that copies a few more bytes.
|
|
115
|
+
'--disable-rofiles-fuse',
|
|
116
|
+
'--delete-build-dirs',
|
|
117
|
+
`--state-dir=${stateDir}`,
|
|
118
|
+
`--arch=${archLabel}`,
|
|
119
|
+
`--repo=${repoDir}`,
|
|
120
|
+
buildDir,
|
|
121
|
+
manifestPath,
|
|
122
|
+
], input, 'flatpak-builder not found. Install it (Fedora: `sudo dnf install flatpak-builder`).');
|
|
123
|
+
// The branch is passed explicitly even though the manifest declares it: the
|
|
124
|
+
// two have to name the same ref, and `build-bundle`'s default is `master`
|
|
125
|
+
// while a manifest that sets `branch` exports somewhere else. Leaving it
|
|
126
|
+
// implicit is how this exports nothing and says "ref not found".
|
|
127
|
+
await run('flatpak', ['build-bundle', `--arch=${archLabel}`, repoDir, input.target, settings.appId, settings.flatpak.branch], input, 'flatpak not found. Install it via your distro: see https://flathub.org/setup.');
|
|
128
|
+
}
|
|
129
|
+
async function run(cmd, args, input, notFoundHint) {
|
|
130
|
+
if (input.verbose)
|
|
131
|
+
console.log(`[gjsify ship] ${cmd} ${args.join(' ')}`);
|
|
132
|
+
// `completion: 'return'` because `ship` packs the remaining formats and
|
|
133
|
+
// prints the artifact list afterwards, so it cannot end in `process.exit()`
|
|
134
|
+
// the way `gjsify flatpak build` does. Under GJS that selects the blocking
|
|
135
|
+
// path, which captures the child's output and re-emits it at the end
|
|
136
|
+
// instead of streaming it — the documented cost of a spawn a caller has to
|
|
137
|
+
// return from (see utils/spawn.ts), and the reason `--verbose` prints the
|
|
138
|
+
// invocation before the silence starts.
|
|
139
|
+
const result = await spawnToCompletion(cmd, args, {
|
|
140
|
+
completion: 'return',
|
|
141
|
+
cwd: input.workDir,
|
|
142
|
+
stdio: 'inherit',
|
|
143
|
+
notFound: () => new Error(`gjsify ship: ${notFoundHint}`),
|
|
144
|
+
});
|
|
145
|
+
if (result.code !== 0) {
|
|
146
|
+
throw new Error(`gjsify ship: ${cmd} failed${describeExit(result)}.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
import type { FormatDescriptor, FormatId } from './types.js';
|
|
2
2
|
export declare const FORMATS: Record<FormatId, FormatDescriptor>;
|
|
3
3
|
export declare const FORMAT_IDS: FormatId[];
|
|
4
|
+
/**
|
|
5
|
+
* What `gjsify ship` builds when nothing said otherwise — every format that
|
|
6
|
+
* needs no tool and no particular host.
|
|
7
|
+
*
|
|
8
|
+
* A SECOND derivation from the same table, and it has to be one: `FORMAT_IDS`
|
|
9
|
+
* used to be both "every format that exists" and "every format to build by
|
|
10
|
+
* default", which was the same list only while every format was `finishOn:
|
|
11
|
+
* 'any'`. Adding Flatpak to `FORMAT_IDS` alone would have made a bare
|
|
12
|
+
* `gjsify ship` demand `flatpak-builder` of every project that ever packaged a
|
|
13
|
+
* `.deb` — and on a host without it, of every `release-cut.yml` run, which
|
|
14
|
+
* packs `@gjsify/cli` itself on a bare ubuntu runner. A host-bound format is
|
|
15
|
+
* opt-in through `--target` or `gjsify.ship.targets`; `--help` still lists all
|
|
16
|
+
* of them.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEFAULT_FORMAT_IDS: FormatId[];
|
|
19
|
+
/**
|
|
20
|
+
* Refuse a format this host cannot finish, BEFORE anything is built or written.
|
|
21
|
+
*
|
|
22
|
+
* `host` is injected so both branches are unit-testable from any machine — the
|
|
23
|
+
* same reason `resolvePrebuildDirName` and `checkTypeSkew`'s readers are pure.
|
|
24
|
+
* The message names the two-phase split rather than just saying no: the answer
|
|
25
|
+
* to "I am on macOS and want a Flatpak" is `--stage` here and `--from-stage` on
|
|
26
|
+
* a Linux runner, and a refusal that does not say so reads as "unsupported".
|
|
27
|
+
*/
|
|
28
|
+
export declare function assertHostCanFinish(format: FormatDescriptor, host?: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Refuse a format whose tools are not installed, with the tool named.
|
|
31
|
+
*
|
|
32
|
+
* Separate from {@link assertHostCanFinish} because the two failures have
|
|
33
|
+
* different fixes — one needs a different machine, the other needs a package —
|
|
34
|
+
* and separate from the packer's own `notFound` handler because it fires BEFORE
|
|
35
|
+
* a build: `gjsify ship` runs the project's `build` script first, and finding
|
|
36
|
+
* out afterwards that `flatpak-builder` is absent costs the whole build.
|
|
37
|
+
*/
|
|
38
|
+
export declare function assertToolsInstalled(format: FormatDescriptor, present?: (cmd: string) => boolean): void;
|
|
4
39
|
/** Parse `--target deb,rpm` into a sorted, deduplicated descriptor list. */
|
|
5
40
|
export declare function resolveFormats(raw: readonly string[]): FormatDescriptor[];
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
// The per-format table. Everything that differs between
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The per-format table. Everything that differs between the formats outside the
|
|
2
|
+
// archive container itself lives here as DATA, so a new format is a row rather
|
|
3
|
+
// than an edit across the stager, the overlay builder and the artifact namer.
|
|
4
|
+
//
|
|
5
|
+
// Flatpak is the row that proves the claim: ADR 0024 § 2 predicted "the whole
|
|
6
|
+
// difference between a Flatpak and an `.rpm` is a four-line prefix map", and
|
|
7
|
+
// this file is where that turned out to be one `prefix: '/app'`, one arch table
|
|
8
|
+
// and one filename. What it also proved is what the design did NOT predict —
|
|
9
|
+
// a format can be host-bound (§ A3), so `host` is a descriptor field now.
|
|
10
|
+
import { isOnPath } from '../check-system-deps.js';
|
|
5
11
|
// `process.arch` → the format's architecture name. Taken from dpkg's own
|
|
6
12
|
// `data/cputable` and rpm's arch table. `arm` cannot be told apart from
|
|
7
13
|
// `armel` by a running process, and Debian's hard-float port is what anything
|
|
@@ -32,16 +38,49 @@ function lookupArch(table, arch, label) {
|
|
|
32
38
|
}
|
|
33
39
|
return mapped;
|
|
34
40
|
}
|
|
41
|
+
// Flatpak's own vocabulary, which is ostree's: `flatpak --supported-arches` on
|
|
42
|
+
// x86_64 answers `x86_64` then `i386`, and a ref is `app/<id>/<arch>/<branch>`.
|
|
43
|
+
// Deliberately short — a name this table does not know is refused rather than
|
|
44
|
+
// guessed, because the arch is not a label here, it is part of the ref the
|
|
45
|
+
// bundle is exported under and installed from.
|
|
46
|
+
const FLATPAK_ARCH = {
|
|
47
|
+
x64: 'x86_64',
|
|
48
|
+
arm64: 'aarch64',
|
|
49
|
+
ia32: 'i386',
|
|
50
|
+
arm: 'arm',
|
|
51
|
+
};
|
|
35
52
|
function debArch(arch, archIndependent) {
|
|
36
53
|
return archIndependent ? 'all' : lookupArch(DEBIAN_ARCH, arch, 'Debian');
|
|
37
54
|
}
|
|
38
55
|
function rpmArch(arch, archIndependent) {
|
|
39
56
|
return archIndependent ? 'noarch' : lookupArch(RPM_ARCH, arch, 'RPM');
|
|
40
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Flatpak has no `noarch`, so `archIndependent` is ignored — and that is a
|
|
60
|
+
* measured difference, not an oversight.
|
|
61
|
+
*
|
|
62
|
+
* `all`/`noarch` exist because apt and dnf REFUSE a package whose architecture
|
|
63
|
+
* is not the machine's. Flatpak asks the opposite question: every ref names an
|
|
64
|
+
* arch, `flatpak install` resolves the one matching the host, and an app with no
|
|
65
|
+
* arch is not installable at all. So a payload of pure JavaScript still ships as
|
|
66
|
+
* `app/<id>/x86_64/stable` — which is also what Flathub does for every
|
|
67
|
+
* interpreted app it hosts.
|
|
68
|
+
*/
|
|
69
|
+
function flatpakArch(arch, _archIndependent) {
|
|
70
|
+
return lookupArch(FLATPAK_ARCH, arch, 'Flatpak');
|
|
71
|
+
}
|
|
72
|
+
/** `.deb` and `.rpm` are written by this tree, so they exec nothing and read back with GNU tools. */
|
|
73
|
+
const WRITTEN_HERE = (readWith) => ({
|
|
74
|
+
finishOn: 'any',
|
|
75
|
+
requiredTools: [],
|
|
76
|
+
oracle: { readWith, readOn: ['linux'], selfReading: false },
|
|
77
|
+
});
|
|
41
78
|
export const FORMATS = {
|
|
42
79
|
deb: {
|
|
43
80
|
id: 'deb',
|
|
44
81
|
prefix: '/usr',
|
|
82
|
+
host: WRITTEN_HERE(['ar', 'tar', 'dpkg-deb', 'lintian']),
|
|
83
|
+
depends: 'deb',
|
|
45
84
|
// Debian policy § 12.5: every package ships its copyright in
|
|
46
85
|
// /usr/share/doc/<package>/copyright, and lintian errors without it.
|
|
47
86
|
licenseDest: (binaryName) => `share/doc/${binaryName}/copyright`,
|
|
@@ -52,13 +91,121 @@ export const FORMATS = {
|
|
|
52
91
|
rpm: {
|
|
53
92
|
id: 'rpm',
|
|
54
93
|
prefix: '/usr',
|
|
94
|
+
host: WRITTEN_HERE(['rpm', 'rpm2cpio', 'cpio']),
|
|
95
|
+
depends: 'rpm',
|
|
55
96
|
licenseDest: (binaryName) => `share/licenses/${binaryName}/LICENSE`,
|
|
56
97
|
licenseKind: 'plain',
|
|
57
98
|
archName: rpmArch,
|
|
58
99
|
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.${archLabel}.rpm`,
|
|
59
100
|
},
|
|
101
|
+
flatpak: {
|
|
102
|
+
id: 'flatpak',
|
|
103
|
+
// The one-line difference ADR 0024 § 2 promised. Nothing in the payload
|
|
104
|
+
// changes: the launcher derives its prefix at runtime (§ 3), so the same
|
|
105
|
+
// staged `bin/<name>` works under /usr and under /app.
|
|
106
|
+
prefix: '/app',
|
|
107
|
+
host: {
|
|
108
|
+
// A single-file bundle is an OSTree static delta, and the only writer
|
|
109
|
+
// of one is `flatpak build-bundle`. Unlike the `.dmg` (§ A6) that is
|
|
110
|
+
// not an OS restriction: flatpak runs on Linux, so the format is
|
|
111
|
+
// Linux-bound the way the app itself is.
|
|
112
|
+
finishOn: ['linux'],
|
|
113
|
+
requiredTools: ['flatpak-builder', 'flatpak'],
|
|
114
|
+
installHint: 'Fedora: `sudo dnf install flatpak flatpak-builder`, Debian/Ubuntu: ' +
|
|
115
|
+
'`sudo apt install flatpak flatpak-builder`',
|
|
116
|
+
oracle: {
|
|
117
|
+
// `flatpak build-import-bundle` into a FRESH repo, then
|
|
118
|
+
// `ostree ls -R`: ostree parses the delta this tree never wrote,
|
|
119
|
+
// and prints a path + mode + size per file. Measured on this
|
|
120
|
+
// workstation — that listing is what the e2e suite compares
|
|
121
|
+
// against the staged payload.
|
|
122
|
+
readWith: ['flatpak', 'ostree'],
|
|
123
|
+
readOn: ['linux'],
|
|
124
|
+
selfReading: false,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
// A Flatpak's one dependency is its runtime, named in the manifest. See
|
|
128
|
+
// `DistroFormatId`: there is no `Depends:` field here to under-declare,
|
|
129
|
+
// which also means the unmapped-namespace refusal ADR 0024 § 6 built for
|
|
130
|
+
// deb and rpm has nothing to say about this format.
|
|
131
|
+
depends: null,
|
|
132
|
+
// No policy demands a location, so this follows rpm's — one fewer shape
|
|
133
|
+
// for a reader to learn, and `/app/share/licenses/<name>/LICENSE` is
|
|
134
|
+
// where the equivalent file sits in the `.rpm` built from the same stage.
|
|
135
|
+
licenseDest: (binaryName) => `share/licenses/${binaryName}/LICENSE`,
|
|
136
|
+
licenseKind: 'plain',
|
|
137
|
+
archName: flatpakArch,
|
|
138
|
+
// Named after the APP ID, not the binary: the id is the ref the file
|
|
139
|
+
// installs as, and it is what `flatpak install ./file.flatpak` prints.
|
|
140
|
+
fileName: (s, archLabel) => `${s.appId}-${s.version}-${s.release}.${archLabel}.flatpak`,
|
|
141
|
+
},
|
|
60
142
|
};
|
|
61
|
-
|
|
143
|
+
// DERIVED, never a second list. `FORMATS` is `Record<FormatId, …>`, so the
|
|
144
|
+
// compiler already refuses a `FormatId` with no descriptor — reading the keys
|
|
145
|
+
// back inherits that guarantee for free. Written out by hand this was the one
|
|
146
|
+
// unbound copy of the vocabulary: adding a format to `FormatId` and `FORMATS`
|
|
147
|
+
// compiled fine and left this list short, which would have made the new format
|
|
148
|
+
// missing from `--help` and REFUSED by `readStage` as an unknown id.
|
|
149
|
+
// Insertion order is stable for string keys, and `resolveFormats` sorts anyway.
|
|
150
|
+
export const FORMAT_IDS = Object.keys(FORMATS);
|
|
151
|
+
/**
|
|
152
|
+
* What `gjsify ship` builds when nothing said otherwise — every format that
|
|
153
|
+
* needs no tool and no particular host.
|
|
154
|
+
*
|
|
155
|
+
* A SECOND derivation from the same table, and it has to be one: `FORMAT_IDS`
|
|
156
|
+
* used to be both "every format that exists" and "every format to build by
|
|
157
|
+
* default", which was the same list only while every format was `finishOn:
|
|
158
|
+
* 'any'`. Adding Flatpak to `FORMAT_IDS` alone would have made a bare
|
|
159
|
+
* `gjsify ship` demand `flatpak-builder` of every project that ever packaged a
|
|
160
|
+
* `.deb` — and on a host without it, of every `release-cut.yml` run, which
|
|
161
|
+
* packs `@gjsify/cli` itself on a bare ubuntu runner. A host-bound format is
|
|
162
|
+
* opt-in through `--target` or `gjsify.ship.targets`; `--help` still lists all
|
|
163
|
+
* of them.
|
|
164
|
+
*/
|
|
165
|
+
export const DEFAULT_FORMAT_IDS = FORMAT_IDS.filter((id) => FORMATS[id].host.finishOn === 'any');
|
|
166
|
+
/**
|
|
167
|
+
* Refuse a format this host cannot finish, BEFORE anything is built or written.
|
|
168
|
+
*
|
|
169
|
+
* `host` is injected so both branches are unit-testable from any machine — the
|
|
170
|
+
* same reason `resolvePrebuildDirName` and `checkTypeSkew`'s readers are pure.
|
|
171
|
+
* The message names the two-phase split rather than just saying no: the answer
|
|
172
|
+
* to "I am on macOS and want a Flatpak" is `--stage` here and `--from-stage` on
|
|
173
|
+
* a Linux runner, and a refusal that does not say so reads as "unsupported".
|
|
174
|
+
*/
|
|
175
|
+
export function assertHostCanFinish(format, host = process.platform) {
|
|
176
|
+
const { finishOn } = format.host;
|
|
177
|
+
if (finishOn === 'any' || finishOn.includes(host))
|
|
178
|
+
return;
|
|
179
|
+
throw new Error(`gjsify ship: a ${format.id} artifact is packed on ${finishOn.join(' or ')} and ` +
|
|
180
|
+
`this host is ${host} (ADR 0024 § A1: a container is produced where its format's tool lives). ` +
|
|
181
|
+
'Assembly is not host-bound, so the way across is the two-phase split: run ' +
|
|
182
|
+
'`gjsify ship --stage` here, move `ship/stage/` to a ' +
|
|
183
|
+
`${finishOn.join('/')} runner, and finish with ` +
|
|
184
|
+
`\`gjsify ship --from-stage <dir> --target ${format.id}\`.`);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Refuse a format whose tools are not installed, with the tool named.
|
|
188
|
+
*
|
|
189
|
+
* Separate from {@link assertHostCanFinish} because the two failures have
|
|
190
|
+
* different fixes — one needs a different machine, the other needs a package —
|
|
191
|
+
* and separate from the packer's own `notFound` handler because it fires BEFORE
|
|
192
|
+
* a build: `gjsify ship` runs the project's `build` script first, and finding
|
|
193
|
+
* out afterwards that `flatpak-builder` is absent costs the whole build.
|
|
194
|
+
*/
|
|
195
|
+
export function assertToolsInstalled(format, present = isOnPath) {
|
|
196
|
+
const missing = format.host.requiredTools.filter((tool) => !present(tool));
|
|
197
|
+
if (missing.length === 0)
|
|
198
|
+
return;
|
|
199
|
+
// The install instruction is the DESCRIPTOR's, not this function's: hardcoded
|
|
200
|
+
// here it was `dnf install flatpak flatpak-builder` for every format that
|
|
201
|
+
// will ever need a tool, which is the branch this table exists to avoid.
|
|
202
|
+
const hint = format.host.installHint;
|
|
203
|
+
throw new Error(`gjsify ship: packing a ${format.id} needs ${format.host.requiredTools.join(' and ')}, and ` +
|
|
204
|
+
`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} not on PATH. ` +
|
|
205
|
+
`${hint === undefined ? 'Install it' : `Install it (${hint})`}, or drop this target — the other ` +
|
|
206
|
+
`formats need no tools at all. \`gjsify ship --stage\` also works without ${missing.join('/')}: ` +
|
|
207
|
+
'the payload is assembled by this CLI and only the container needs them.');
|
|
208
|
+
}
|
|
62
209
|
/** Parse `--target deb,rpm` into a sorted, deduplicated descriptor list. */
|
|
63
210
|
export function resolveFormats(raw) {
|
|
64
211
|
const names = raw
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ShipMimeType } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate the declared types, throwing on anything that would install and then not work.
|
|
4
|
+
*
|
|
5
|
+
* Each refusal is a silent failure made loud:
|
|
6
|
+
* - a malformed type name is written into the XML and ignored by `update-mime-database`;
|
|
7
|
+
* - a glob without `*.` never matches (`bauplan` matches only a file called exactly that);
|
|
8
|
+
* - a type with no glob and no parent type can never be DETECTED, so it is registered and unused;
|
|
9
|
+
* - a duplicate definition makes which comment wins depend on document order.
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateMimeTypes(types: readonly ShipMimeType[]): void;
|
|
12
|
+
/**
|
|
13
|
+
* The shared-mime-info document for `types`.
|
|
14
|
+
*
|
|
15
|
+
* One file per package, named after the app id, because `share/mime/packages/` is shared: a generic
|
|
16
|
+
* name is a collision with whatever another package put there — the same reason the schema staging
|
|
17
|
+
* uses the app id.
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderMimePackage(types: readonly ShipMimeType[]): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Defining a file type for the system — the half `MimeType=` cannot do on its own.
|
|
2
|
+
//
|
|
3
|
+
// A desktop entry's `MimeType=` says "I open this type". It does NOT say the type EXISTS. For a
|
|
4
|
+
// standard type that is fine, because the distribution's shared-mime-info already defines it; for a
|
|
5
|
+
// type of the project's own it is not, and the failure is the quietest kind there is: nothing knows
|
|
6
|
+
// what a `.bauplan` file is, so the file manager never assigns the type, `MimeType=` matches
|
|
7
|
+
// nothing, and a double-click does nothing at all. No error is printed and no log line is written —
|
|
8
|
+
// the association simply never fires, which is indistinguishable from "the app is not installed".
|
|
9
|
+
//
|
|
10
|
+
// So a project that invents a type also ships a shared-mime-info document, and the package refreshes
|
|
11
|
+
// the MIME cache on install (see `scripts.ts`) — an unrefreshed cache is the same silence again.
|
|
12
|
+
/** XML text escaping. Only the five that matter in element content and attributes. */
|
|
13
|
+
function esc(value) {
|
|
14
|
+
return value
|
|
15
|
+
.replace(/&/g, '&')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>')
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
.replace(/'/g, ''');
|
|
20
|
+
}
|
|
21
|
+
const TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/;
|
|
22
|
+
/**
|
|
23
|
+
* Validate the declared types, throwing on anything that would install and then not work.
|
|
24
|
+
*
|
|
25
|
+
* Each refusal is a silent failure made loud:
|
|
26
|
+
* - a malformed type name is written into the XML and ignored by `update-mime-database`;
|
|
27
|
+
* - a glob without `*.` never matches (`bauplan` matches only a file called exactly that);
|
|
28
|
+
* - a type with no glob and no parent type can never be DETECTED, so it is registered and unused;
|
|
29
|
+
* - a duplicate definition makes which comment wins depend on document order.
|
|
30
|
+
*/
|
|
31
|
+
export function validateMimeTypes(types) {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
for (const entry of types) {
|
|
34
|
+
if (!TYPE_RE.test(entry.type)) {
|
|
35
|
+
throw new Error(`gjsify ship: \`gjsify.ship.mimeTypes\` has an invalid type "${entry.type}". ` +
|
|
36
|
+
'Expected `<media>/<subtype>`, e.g. `application/x-bauplan` — update-mime-database ' +
|
|
37
|
+
'ignores a malformed name, so the type would install and never resolve.');
|
|
38
|
+
}
|
|
39
|
+
if (seen.has(entry.type)) {
|
|
40
|
+
throw new Error(`gjsify ship: \`gjsify.ship.mimeTypes\` defines ${entry.type} twice. ` +
|
|
41
|
+
'Which comment and glob set win would depend on document order.');
|
|
42
|
+
}
|
|
43
|
+
seen.add(entry.type);
|
|
44
|
+
if (entry.comment.trim() === '') {
|
|
45
|
+
throw new Error(`gjsify ship: ${entry.type} has an empty \`comment\`. A file manager then shows the ` +
|
|
46
|
+
'user the raw type string instead of a name.');
|
|
47
|
+
}
|
|
48
|
+
for (const glob of entry.globs ?? []) {
|
|
49
|
+
if (!glob.includes('*') && !glob.includes('?') && !glob.includes('[')) {
|
|
50
|
+
throw new Error(`gjsify ship: ${entry.type} has the glob "${glob}", which matches only a file ` +
|
|
51
|
+
`named exactly that. A suffix pattern is written \`*.${glob.replace(/^\.+/, '')}\`.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if ((entry.globs ?? []).length === 0 && entry.subClassOf === undefined) {
|
|
55
|
+
throw new Error(`gjsify ship: ${entry.type} declares neither \`globs\` nor \`subClassOf\`, so nothing ` +
|
|
56
|
+
'can ever match it. The type would be registered and then never assigned to a file.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The shared-mime-info document for `types`.
|
|
62
|
+
*
|
|
63
|
+
* One file per package, named after the app id, because `share/mime/packages/` is shared: a generic
|
|
64
|
+
* name is a collision with whatever another package put there — the same reason the schema staging
|
|
65
|
+
* uses the app id.
|
|
66
|
+
*/
|
|
67
|
+
export function renderMimePackage(types) {
|
|
68
|
+
const body = types
|
|
69
|
+
.map((entry) => {
|
|
70
|
+
const lines = [` <mime-type type="${esc(entry.type)}">`, ` <comment>${esc(entry.comment)}</comment>`];
|
|
71
|
+
if (entry.subClassOf !== undefined) {
|
|
72
|
+
lines.push(` <sub-class-of type="${esc(entry.subClassOf)}"/>`);
|
|
73
|
+
}
|
|
74
|
+
for (const glob of entry.globs ?? []) {
|
|
75
|
+
lines.push(` <glob pattern="${esc(glob)}"/>`);
|
|
76
|
+
}
|
|
77
|
+
if (entry.genericIcon !== undefined) {
|
|
78
|
+
lines.push(` <generic-icon name="${esc(entry.genericIcon)}"/>`);
|
|
79
|
+
}
|
|
80
|
+
lines.push(' </mime-type>');
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
})
|
|
83
|
+
.join('\n');
|
|
84
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
85
|
+
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
|
86
|
+
${body}
|
|
87
|
+
</mime-info>
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
@@ -16,8 +16,84 @@ export interface PayloadEntry {
|
|
|
16
16
|
* what `rpm -qi` then shows a user, and an artifact that looks broken is a
|
|
17
17
|
* support question. The mtime keeps the property that matters (pack the same
|
|
18
18
|
* build twice, get the same bytes) while saying something true.
|
|
19
|
+
*
|
|
20
|
+
* Only the ASSEMBLING host runs this. A stage records the answer it got
|
|
21
|
+
* (`.gjsify-ship-stage.json` → `mtime`) and the packing host reuses it: an
|
|
22
|
+
* artifact upload does not carry mtimes, so re-stat'ing the stage there would
|
|
23
|
+
* stamp every header with "whenever the download finished" and quietly destroy
|
|
24
|
+
* the reproducibility this function exists to protect.
|
|
19
25
|
*/
|
|
20
26
|
export declare function buildTimestamp(bundlePath: string, env?: Record<string, string | undefined>): number;
|
|
27
|
+
/**
|
|
28
|
+
* What the payload itself says about what it installs.
|
|
29
|
+
*
|
|
30
|
+
* Every field here used to be answered from the SETTINGS — `settings.iconFiles.length > 0`,
|
|
31
|
+
* `settings.schemaFiles.length > 0`, `settings.typelibFiles` — i.e. from lists of absolute paths
|
|
32
|
+
* on the BUILD host. Two things were wrong with that, and the second is why this function exists
|
|
33
|
+
* at all:
|
|
34
|
+
*
|
|
35
|
+
* 1. It answered a different question than the one being asked. `cacheRefreshCommands` emits
|
|
36
|
+
* `gtk-update-icon-cache <prefix>/share/icons/hicolor` — the honest precondition is "did this
|
|
37
|
+
* package install anything into that directory", not "did the project have icon files lying
|
|
38
|
+
* around". They come apart for a `kind: 'cli'` project with a `data/icons/` folder: the
|
|
39
|
+
* planner stages no icon (icons are an `'app'` thing), the settings still listed them, and the
|
|
40
|
+
* postinst refreshed a cache for files that were never installed.
|
|
41
|
+
* 2. An absolute build-host path cannot cross to the host that packs the artifact
|
|
42
|
+
* (ADR 0024 § A2). Carrying `iconFiles: ["/home/…/icon.svg"]` in a stage manifest so that
|
|
43
|
+
* `.length > 0` can be read on another machine is authoring a value that is measurable right
|
|
44
|
+
* there in the tree.
|
|
45
|
+
*
|
|
46
|
+
* Path-only, on purpose — `isArchIndependent` is the sibling that reads BYTES, and the two answer
|
|
47
|
+
* different questions from the same payload. The same split as `plan.ts`'s `isExecutableAsset`
|
|
48
|
+
* (by name) versus this module's magic sniffing (by content).
|
|
49
|
+
*/
|
|
50
|
+
export interface PayloadFacts {
|
|
51
|
+
/** The payload installs a `share/applications/*.desktop`. */
|
|
52
|
+
hasDesktopEntry: boolean;
|
|
53
|
+
/** The payload installs into `share/icons/hicolor/`. */
|
|
54
|
+
hasIcons: boolean;
|
|
55
|
+
/** The payload installs a compiled-on-install `share/glib-2.0/schemas/*.gschema.xml`. */
|
|
56
|
+
hasSchemas: boolean;
|
|
57
|
+
/** The payload installs a `share/mime/packages/*.xml`, so the mime cache needs rebuilding. */
|
|
58
|
+
hasMimeTypes: boolean;
|
|
59
|
+
/** Prefix-relative paths of the typelibs the payload carries itself. */
|
|
60
|
+
bundledTypelibs: string[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read {@link PayloadFacts} off a payload or off a plan.
|
|
64
|
+
*
|
|
65
|
+
* Takes anything with a `path`, so the assembling phase can ask the same
|
|
66
|
+
* question of the PLAN (before the tree is read back) that the packing phase
|
|
67
|
+
* asks of the payload. One function, so the two phases cannot disagree about
|
|
68
|
+
* whether a package installs a schema.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readPayloadFacts(entries: readonly {
|
|
71
|
+
path: string;
|
|
72
|
+
}[]): PayloadFacts;
|
|
73
|
+
/**
|
|
74
|
+
* The interpreters the payload's own executables need, read off their shebangs.
|
|
75
|
+
*
|
|
76
|
+
* An interpreter is a dependency like any other, and rpm expects it declared:
|
|
77
|
+
* `rpmbuild`'s file-based generator emits one `Requires` per executable
|
|
78
|
+
* shebang, with the `RPMSENSE_FIND_REQUIRES` sense that says "derived, not
|
|
79
|
+
* declared". Measured on Fedora 44 against a package whose only file is a
|
|
80
|
+
* `#!/bin/sh` script: `rpm -qp --requires` → `/usr/bin/sh 16384`.
|
|
81
|
+
*
|
|
82
|
+
* The LITERAL path, not a resolved one. `rpmbuild` prints `/usr/bin/sh` there
|
|
83
|
+
* because it resolved `/bin` through the symlink of the usrmerged host it ran
|
|
84
|
+
* on; this writer has no target host to resolve against (ADR 0024 § A1 — the
|
|
85
|
+
* packers are pure JavaScript and run anywhere), and `/bin/sh` is satisfied on
|
|
86
|
+
* both layouts: measured on Fedora 44, `rpm -q --whatprovides /bin/sh` and
|
|
87
|
+
* `/usr/bin/sh` both answer `bash`. It is also the spelling the scriptlet
|
|
88
|
+
* requirements already use.
|
|
89
|
+
*
|
|
90
|
+
* EXECUTABLE files only, which is the same rule `rpmbuild` applies. A GJS
|
|
91
|
+
* bundle staged 0644 carries `#!/usr/bin/env -S gjs -m` for the days it is run
|
|
92
|
+
* directly, but nothing in the installed package executes it as a program — the
|
|
93
|
+
* launcher `exec`s `gjs` with it as an argument — so declaring `/usr/bin/env`
|
|
94
|
+
* for it would be a dependency on a path this package never uses.
|
|
95
|
+
*/
|
|
96
|
+
export declare function readShebangInterpreters(payload: readonly PayloadEntry[]): string[];
|
|
21
97
|
/**
|
|
22
98
|
* Does the payload contain anything architecture-specific?
|
|
23
99
|
*
|