@gjsify/cli 0.47.0 → 0.48.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 +86 -83
- package/lib/commands/affected-classify.js +30 -1
- package/lib/commands/dlx.js +7 -3
- package/lib/commands/foreach.js +9 -2
- package/lib/commands/publish.js +1 -8
- package/lib/commands/run.js +7 -2
- package/lib/commands/ship.js +32 -4
- package/lib/types/config-data.d.ts +37 -1
- package/lib/utils/base64.d.ts +34 -0
- package/lib/utils/base64.js +47 -0
- package/lib/utils/cli-runtime-closure.d.ts +61 -0
- package/lib/utils/cli-runtime-closure.js +54 -17
- package/lib/utils/double-dash-args.d.ts +8 -0
- package/lib/utils/double-dash-args.js +64 -0
- package/lib/utils/ship/changelog.d.ts +56 -0
- package/lib/utils/ship/changelog.js +166 -0
- package/lib/utils/ship/discover.d.ts +10 -1
- package/lib/utils/ship/discover.js +76 -2
- package/lib/utils/ship/entry-interpreter.d.ts +44 -0
- package/lib/utils/ship/entry-interpreter.js +109 -0
- package/lib/utils/ship/formats.js +5 -0
- package/lib/utils/ship/gi-namespaces.js +175 -12
- package/lib/utils/ship/plan.d.ts +5 -1
- package/lib/utils/ship/plan.js +21 -2
- package/lib/utils/ship/settings.d.ts +48 -2
- package/lib/utils/ship/settings.js +63 -3
- package/lib/utils/ship/stage-manifest.d.ts +22 -3
- package/lib/utils/ship/stage-manifest.js +40 -13
- package/lib/utils/ship/types.d.ts +13 -0
- package/package.json +19 -19
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PackSettings } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `Thu, 04 Sep 2026 12:34:56 +0000` — the trailer format Policy § 4.4 requires.
|
|
4
|
+
*
|
|
5
|
+
* Always UTC, and the offset is written out rather than derived: the build stamp is
|
|
6
|
+
* Unix seconds, so there is no zone to preserve, and a package built in two places
|
|
7
|
+
* must not differ by a timezone.
|
|
8
|
+
*
|
|
9
|
+
* @param unixSeconds the build stamp every header in the artifact shares
|
|
10
|
+
*/
|
|
11
|
+
export declare function rfc822(unixSeconds: number): string;
|
|
12
|
+
/**
|
|
13
|
+
* The bullet lines a project's own changelog carries for ONE version.
|
|
14
|
+
*
|
|
15
|
+
* Reads the Markdown release-note shape `release-it`'s conventional-changelog
|
|
16
|
+
* preset writes — `## [0.47.0](compare-url) (2026-09-03)`, or a bare `## 0.47.0`
|
|
17
|
+
* for a first release — and takes the `*`/`-` bullets under it up to the next
|
|
18
|
+
* version heading. Category subheadings (`### Bug Fixes`) are dropped: a Debian
|
|
19
|
+
* change block is a flat list, and a heading rendered as a bullet reads as a change
|
|
20
|
+
* that was made.
|
|
21
|
+
*
|
|
22
|
+
* Markdown links collapse to their text (`[#1510](https://…)` → `#1510`) and bold
|
|
23
|
+
* scopes lose their asterisks, because the destination is plain text that `apt
|
|
24
|
+
* changelog` prints to a terminal.
|
|
25
|
+
*
|
|
26
|
+
* @returns one string per bullet, empty when the file does not name this version
|
|
27
|
+
*/
|
|
28
|
+
export declare function changelogEntriesFor(version: string, source: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* The whole `changelog.Debian` file: one entry, for the version being packaged.
|
|
31
|
+
*
|
|
32
|
+
* `unstable` is the distribution and `medium` the urgency — the values `dh_make`
|
|
33
|
+
* seeds a package with, and the only honest ones for an artifact built outside any
|
|
34
|
+
* Debian suite: this package targets no release, and claiming one would put a
|
|
35
|
+
* specific false statement in a field `apt` reads.
|
|
36
|
+
*
|
|
37
|
+
* The trailer's spacing is load-bearing and is the usual way this file is got
|
|
38
|
+
* wrong: exactly one leading space, then `--`, one space, the maintainer, and TWO
|
|
39
|
+
* spaces before the date.
|
|
40
|
+
*
|
|
41
|
+
* TWO lintian tags this file still raises, both measured on lintian 2.117 against
|
|
42
|
+
* gjsify's own `.deb` and both left alone deliberately:
|
|
43
|
+
*
|
|
44
|
+
* - `initial-upload-closes-no-bugs`, because the Debian revision is `-1` and the
|
|
45
|
+
* entry closes no bug. It is a statement about the Debian ARCHIVE's ITP
|
|
46
|
+
* workflow — the first upload of a new source package should close its ITP bug
|
|
47
|
+
* — and a package built by `gjsify ship` is never uploaded there. There is no
|
|
48
|
+
* bug number that would be true to write, so the tag is inapplicable rather
|
|
49
|
+
* than unmet.
|
|
50
|
+
* - `changelog-not-compressed-with-max-compression`. Policy asks for `gzip -9`
|
|
51
|
+
* and neither `CompressionStream` nor `@gjsify/zlib` takes a level, so
|
|
52
|
+
* `gzipDeterministic` cannot ask for one. Ledgered in `status/open-todos.md`:
|
|
53
|
+
* the fix is a capability in the core, and stamping the header's XFL byte to
|
|
54
|
+
* claim max compression we did not use is not one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function renderDebianChangelog(settings: PackSettings, source: string | undefined, mtime: number): string;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// `/usr/share/doc/<package>/changelog.Debian.gz` — Debian Policy § 4.4.
|
|
2
|
+
//
|
|
3
|
+
// WHY IT EXISTS AT ALL. Every `.deb` this writer produced shipped without one,
|
|
4
|
+
// which the first real lintian this project ever ran named as the only
|
|
5
|
+
// error-severity tag left once the copyright landed:
|
|
6
|
+
//
|
|
7
|
+
// E: gjsify: no-changelog usr/share/doc/gjsify/changelog.Debian.gz (non-native package)
|
|
8
|
+
//
|
|
9
|
+
// "non-native" is not a coincidence: `deb.ts` writes `Version: <version>-<release>`,
|
|
10
|
+
// and a version carrying a Debian revision is by definition a non-native package,
|
|
11
|
+
// whose changelog is `changelog.Debian.gz` rather than `changelog.gz`.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THE ENTRY SAYS — the open question the ledger left, decided here.
|
|
14
|
+
//
|
|
15
|
+
// ONE ENTRY, for the version being packaged, and that is not a truncation. Policy
|
|
16
|
+
// § 4.4's file is the DEBIAN changelog: it records the packaging, not upstream's
|
|
17
|
+
// development. `gjsify ship` performs exactly one packaging event per version, so
|
|
18
|
+
// one entry is the whole history there is to tell. The upstream history has its own
|
|
19
|
+
// (optional) file, `changelog.gz`, and this writer does not claim to produce it.
|
|
20
|
+
//
|
|
21
|
+
// The BODY comes from the project's own changelog when one is found — `CHANGELOG.md`
|
|
22
|
+
// beside the project or up at its repository root, the same search the licence uses,
|
|
23
|
+
// overridable with `gjsify.ship.changelogFile`. That was the choice worth making:
|
|
24
|
+
// the GitHub release body says the same thing and is not reachable from a packing
|
|
25
|
+
// host that may be offline (ADR 0024 § A2), while `CHANGELOG.md` is in the tree the
|
|
26
|
+
// stage is assembled from. When nothing there names this version, the entry says so
|
|
27
|
+
// in one line rather than inventing content.
|
|
28
|
+
//
|
|
29
|
+
// AND ONLY THE PACKAGED VERSION GETS A DATE. A Debian entry needs a distribution, an
|
|
30
|
+
// urgency and an RFC822 timestamp; this writer has a real timestamp for exactly one
|
|
31
|
+
// of them — `mtime`, the build stamp every other header in the artifact already
|
|
32
|
+
// shares. Rendering the older versions would mean stamping each with a fabricated
|
|
33
|
+
// time, since `CHANGELOG.md` dates them to the DAY, and a Debian changelog is read
|
|
34
|
+
// by tools that compare those stamps.
|
|
35
|
+
//
|
|
36
|
+
// Deterministic by construction: the date is the build stamp, and the month and day
|
|
37
|
+
// names are spelled out here rather than taken from `toLocaleString`, which answers
|
|
38
|
+
// in the host's locale.
|
|
39
|
+
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
40
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
41
|
+
/**
|
|
42
|
+
* `Thu, 04 Sep 2026 12:34:56 +0000` — the trailer format Policy § 4.4 requires.
|
|
43
|
+
*
|
|
44
|
+
* Always UTC, and the offset is written out rather than derived: the build stamp is
|
|
45
|
+
* Unix seconds, so there is no zone to preserve, and a package built in two places
|
|
46
|
+
* must not differ by a timezone.
|
|
47
|
+
*
|
|
48
|
+
* @param unixSeconds the build stamp every header in the artifact shares
|
|
49
|
+
*/
|
|
50
|
+
export function rfc822(unixSeconds) {
|
|
51
|
+
const at = new Date(unixSeconds * 1000);
|
|
52
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
53
|
+
return (`${DAYS[at.getUTCDay()]}, ${pad(at.getUTCDate())} ${MONTHS[at.getUTCMonth()]} ${at.getUTCFullYear()} ` +
|
|
54
|
+
`${pad(at.getUTCHours())}:${pad(at.getUTCMinutes())}:${pad(at.getUTCSeconds())} +0000`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The bullet lines a project's own changelog carries for ONE version.
|
|
58
|
+
*
|
|
59
|
+
* Reads the Markdown release-note shape `release-it`'s conventional-changelog
|
|
60
|
+
* preset writes — `## [0.47.0](compare-url) (2026-09-03)`, or a bare `## 0.47.0`
|
|
61
|
+
* for a first release — and takes the `*`/`-` bullets under it up to the next
|
|
62
|
+
* version heading. Category subheadings (`### Bug Fixes`) are dropped: a Debian
|
|
63
|
+
* change block is a flat list, and a heading rendered as a bullet reads as a change
|
|
64
|
+
* that was made.
|
|
65
|
+
*
|
|
66
|
+
* Markdown links collapse to their text (`[#1510](https://…)` → `#1510`) and bold
|
|
67
|
+
* scopes lose their asterisks, because the destination is plain text that `apt
|
|
68
|
+
* changelog` prints to a terminal.
|
|
69
|
+
*
|
|
70
|
+
* @returns one string per bullet, empty when the file does not name this version
|
|
71
|
+
*/
|
|
72
|
+
export function changelogEntriesFor(version, source) {
|
|
73
|
+
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
74
|
+
const heading = new RegExp(`^##+[ \\t]+\\[?${escaped}\\]?[^\\n]*$`, 'm');
|
|
75
|
+
const start = heading.exec(source);
|
|
76
|
+
if (start === null)
|
|
77
|
+
return [];
|
|
78
|
+
const rest = source.slice(start.index + start[0].length);
|
|
79
|
+
// The next VERSION heading, not the next heading of any depth: `### Features`
|
|
80
|
+
// is inside this release, `## [0.46.0]` is the next one.
|
|
81
|
+
const end = /^##[ \t]+\[?\d/m.exec(rest);
|
|
82
|
+
const body = end === null ? rest : rest.slice(0, end.index);
|
|
83
|
+
const entries = [];
|
|
84
|
+
for (const line of body.split('\n')) {
|
|
85
|
+
const bullet = /^[*-][ \t]+(.*\S)\s*$/.exec(line);
|
|
86
|
+
if (bullet === null)
|
|
87
|
+
continue;
|
|
88
|
+
entries.push(bullet[1]
|
|
89
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
90
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
91
|
+
.replace(/\s+/g, ' ')
|
|
92
|
+
.trim());
|
|
93
|
+
}
|
|
94
|
+
return entries;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* One bullet, wrapped so no line exceeds {@link WIDTH}.
|
|
98
|
+
*
|
|
99
|
+
* MEASURED, not stylistic: lintian raises `debian-changelog-line-too-long` past 80
|
|
100
|
+
* columns, and a conventional-commit subject plus its scope, its PR link and its
|
|
101
|
+
* `closes` list goes well past it. Continuation lines are indented to sit under the
|
|
102
|
+
* bullet text — Debian's change block only requires at least two leading spaces, and
|
|
103
|
+
* anything less than four would read as a new item.
|
|
104
|
+
*
|
|
105
|
+
* A single word longer than the width is left on its own line rather than broken: a
|
|
106
|
+
* URL is the usual case, and a hyphenated URL is worse than a long one.
|
|
107
|
+
*/
|
|
108
|
+
function wrapEntry(entry) {
|
|
109
|
+
const lines = [];
|
|
110
|
+
let line = ' *';
|
|
111
|
+
for (const word of entry.split(/\s+/)) {
|
|
112
|
+
if (word === '')
|
|
113
|
+
continue;
|
|
114
|
+
if (line.length + 1 + word.length > WIDTH && line !== ' *' && line !== ' ') {
|
|
115
|
+
lines.push(line);
|
|
116
|
+
line = ' ';
|
|
117
|
+
}
|
|
118
|
+
line += ` ${word}`;
|
|
119
|
+
}
|
|
120
|
+
lines.push(line);
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
/** lintian's `debian-changelog-line-too-long` threshold. */
|
|
124
|
+
const WIDTH = 80;
|
|
125
|
+
/**
|
|
126
|
+
* The whole `changelog.Debian` file: one entry, for the version being packaged.
|
|
127
|
+
*
|
|
128
|
+
* `unstable` is the distribution and `medium` the urgency — the values `dh_make`
|
|
129
|
+
* seeds a package with, and the only honest ones for an artifact built outside any
|
|
130
|
+
* Debian suite: this package targets no release, and claiming one would put a
|
|
131
|
+
* specific false statement in a field `apt` reads.
|
|
132
|
+
*
|
|
133
|
+
* The trailer's spacing is load-bearing and is the usual way this file is got
|
|
134
|
+
* wrong: exactly one leading space, then `--`, one space, the maintainer, and TWO
|
|
135
|
+
* spaces before the date.
|
|
136
|
+
*
|
|
137
|
+
* TWO lintian tags this file still raises, both measured on lintian 2.117 against
|
|
138
|
+
* gjsify's own `.deb` and both left alone deliberately:
|
|
139
|
+
*
|
|
140
|
+
* - `initial-upload-closes-no-bugs`, because the Debian revision is `-1` and the
|
|
141
|
+
* entry closes no bug. It is a statement about the Debian ARCHIVE's ITP
|
|
142
|
+
* workflow — the first upload of a new source package should close its ITP bug
|
|
143
|
+
* — and a package built by `gjsify ship` is never uploaded there. There is no
|
|
144
|
+
* bug number that would be true to write, so the tag is inapplicable rather
|
|
145
|
+
* than unmet.
|
|
146
|
+
* - `changelog-not-compressed-with-max-compression`. Policy asks for `gzip -9`
|
|
147
|
+
* and neither `CompressionStream` nor `@gjsify/zlib` takes a level, so
|
|
148
|
+
* `gzipDeterministic` cannot ask for one. Ledgered in `status/open-todos.md`:
|
|
149
|
+
* the fix is a capability in the core, and stamping the header's XFL byte to
|
|
150
|
+
* claim max compression we did not use is not one.
|
|
151
|
+
*/
|
|
152
|
+
export function renderDebianChangelog(settings, source, mtime) {
|
|
153
|
+
const entries = source === undefined ? [] : changelogEntriesFor(settings.version, source);
|
|
154
|
+
const fallback = settings.homepage
|
|
155
|
+
? `Release ${settings.version}. See ${settings.homepage} for the upstream changes.`
|
|
156
|
+
: `Release ${settings.version}.`;
|
|
157
|
+
const body = (entries.length > 0 ? entries : [fallback]).flatMap(wrapEntry);
|
|
158
|
+
return [
|
|
159
|
+
`${settings.binaryName} (${settings.version}-${settings.release}) unstable; urgency=medium`,
|
|
160
|
+
'',
|
|
161
|
+
...body,
|
|
162
|
+
'',
|
|
163
|
+
` -- ${settings.maintainer} ${rfc822(mtime)}`,
|
|
164
|
+
'',
|
|
165
|
+
].join('\n');
|
|
166
|
+
}
|
|
@@ -6,8 +6,17 @@ export interface DiscoverInput {
|
|
|
6
6
|
ship: ConfigDataShip;
|
|
7
7
|
/** `gjsify.flatpak.icon`, the pre-existing spelling of the same thing. */
|
|
8
8
|
flatpakIcon?: string;
|
|
9
|
-
/** The bundle path the project declares. */
|
|
9
|
+
/** The bundle path the project declares FOR THIS TARGET. */
|
|
10
10
|
declaredBundle?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The entries other targets declare, which this one must not carry.
|
|
13
|
+
*
|
|
14
|
+
* See {@link withoutForeignEntries} — same class as the locale tree below,
|
|
15
|
+
* with a sharper edge: a foreign entry is not merely dead weight, it is a
|
|
16
|
+
* bundle this target's interpreter cannot load, inside an artifact that says
|
|
17
|
+
* it can.
|
|
18
|
+
*/
|
|
19
|
+
foreignBundles?: readonly string[];
|
|
11
20
|
}
|
|
12
21
|
export declare function discoverPayload(input: DiscoverInput): DiscoveredPayload;
|
|
13
22
|
/**
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// (`data/icons/hicolor/…`, `data/*.gschema.xml`) needs no configuration at all.
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
8
8
|
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
9
|
-
import { LICENSE_FILE_NAMES } from './settings.js';
|
|
9
|
+
import { CHANGELOG_FILE_NAMES, LICENSE_FILE_NAMES, } from './settings.js';
|
|
10
10
|
const ICON_EXTENSIONS = ['.svg', '.png'];
|
|
11
11
|
/**
|
|
12
12
|
* The face formats every FreeType opens, with no build option deciding it.
|
|
@@ -48,13 +48,14 @@ export function discoverPayload(input) {
|
|
|
48
48
|
// Measured on a probe package: the same `.mo` appeared at both paths. The `lib/` copy is
|
|
49
49
|
// dead weight — nothing looks there — and it is the same failure that once shipped the test
|
|
50
50
|
// suite: whatever is beside the bundle gets carried whether or not it belongs in a package.
|
|
51
|
-
bundleFiles: withoutLocaleTree(listFilesRecursive(bundleDir), bundleDir, projectDir, ship.localeDir),
|
|
51
|
+
bundleFiles: withoutForeignEntries(withoutLocaleTree(listFilesRecursive(bundleDir), bundleDir, projectDir, ship.localeDir), bundleDir, projectDir, input.foreignBundles ?? [], bundlePath),
|
|
52
52
|
iconFiles: discoverIcons(projectDir, ship.icon ?? input.flatpakIcon),
|
|
53
53
|
schemaFiles: discoverSchemas(projectDir, ship.schemas),
|
|
54
54
|
typelibFiles: discoverTypelibs(projectDir, ship.bundledTypelibs),
|
|
55
55
|
localeFiles,
|
|
56
56
|
fontFiles: discoverFonts(projectDir, ship.fonts),
|
|
57
57
|
licenseFile: discoverLicense(projectDir, ship.licenseFile),
|
|
58
|
+
changelogFile: discoverChangelog(projectDir, ship.changelogFile),
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
@@ -168,6 +169,45 @@ function discoverLocales(projectDir, dir) {
|
|
|
168
169
|
}
|
|
169
170
|
return out;
|
|
170
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Drop the entries declared for OTHER targets from this target's payload.
|
|
174
|
+
*
|
|
175
|
+
* `gjsify.ship.bundle` is per OS (#1545), so a project with two hosts builds
|
|
176
|
+
* `dist/app.gjs.mjs` and `dist/app.node.mjs` into ONE directory — and staging
|
|
177
|
+
* that directory wholesale hands each artifact the other's bundle. It is not
|
|
178
|
+
* dead weight like the locale copy: on macOS it lands inside the signed `.app`,
|
|
179
|
+
* and it is a file whose only property is that this artifact's interpreter
|
|
180
|
+
* cannot load it.
|
|
181
|
+
*
|
|
182
|
+
* Only the declared entries themselves, never a guess at what else belongs to
|
|
183
|
+
* them: a bundle's shared chunks are indistinguishable from the application's
|
|
184
|
+
* own files, and dropping one would break the artifact this list exists to keep
|
|
185
|
+
* whole.
|
|
186
|
+
*/
|
|
187
|
+
function withoutForeignEntries(bundleFiles, bundleDir, projectDir, foreign, bundlePath) {
|
|
188
|
+
if (foreign.length === 0)
|
|
189
|
+
return bundleFiles;
|
|
190
|
+
const dropped = new Set();
|
|
191
|
+
for (const entry of foreign) {
|
|
192
|
+
const abs = isAbsolute(entry) ? entry : resolve(projectDir, entry);
|
|
193
|
+
// THIS TARGET'S OWN ENTRY, under another spelling. The foreign set is
|
|
194
|
+
// compared as declared STRINGS, and `./dist/app.mjs`, `dist/../dist/app.mjs`
|
|
195
|
+
// and an absolute path are three spellings of one file — dropping it would
|
|
196
|
+
// stage a launcher whose bundle is not in the payload, which is a worse
|
|
197
|
+
// artifact than the one this exclusion exists to prevent. Paths are the
|
|
198
|
+
// only comparison that cannot be spelled around.
|
|
199
|
+
if (abs === bundlePath)
|
|
200
|
+
continue;
|
|
201
|
+
const rel = relative(bundleDir, abs);
|
|
202
|
+
// Outside this payload — another target may build somewhere else entirely,
|
|
203
|
+
// and then there is nothing here to subtract. Same shape as the locale
|
|
204
|
+
// tree above, including the POSIX normalisation Windows needs.
|
|
205
|
+
if (rel === '' || rel.startsWith('..') || isAbsolute(rel))
|
|
206
|
+
continue;
|
|
207
|
+
dropped.add(rel.split(sep).join(posix.sep));
|
|
208
|
+
}
|
|
209
|
+
return bundleFiles.filter((file) => !dropped.has(file));
|
|
210
|
+
}
|
|
171
211
|
function resolveBundle(input) {
|
|
172
212
|
const declared = input.declaredBundle;
|
|
173
213
|
if (declared === undefined) {
|
|
@@ -336,6 +376,40 @@ function discoverLicense(projectDir, configured) {
|
|
|
336
376
|
}
|
|
337
377
|
return undefined;
|
|
338
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* The project's own changelog — the prose the `.deb`'s `changelog.Debian.gz` is
|
|
381
|
+
* rendered from (Debian Policy § 4.4).
|
|
382
|
+
*
|
|
383
|
+
* SAME SEARCH AS THE LICENCE, deliberately, and for the same reason: a package
|
|
384
|
+
* inside a monorepo does not carry its own copy. `packages/infra/cli` has no
|
|
385
|
+
* CHANGELOG.md and the release train writes one at the repository root, so a
|
|
386
|
+
* `projectDir`-only search would find nothing for the one in-tree project that
|
|
387
|
+
* declares `gjsify.ship`. {@link licenseSearchDirs} stops at the first ancestor
|
|
388
|
+
* that is a project root, which is what keeps an unrelated parent directory's
|
|
389
|
+
* file out.
|
|
390
|
+
*
|
|
391
|
+
* Absence is NOT an error here, unlike the licence: Policy § 4.4 requires the
|
|
392
|
+
* FILE, not a particular history, and `renderDebianChangelog` writes a
|
|
393
|
+
* one-line entry naming the version when there is no prose to quote. Refusing
|
|
394
|
+
* would make a changelog a precondition for packaging anything.
|
|
395
|
+
*/
|
|
396
|
+
function discoverChangelog(projectDir, configured) {
|
|
397
|
+
if (configured !== undefined) {
|
|
398
|
+
const target = resolve(projectDir, configured);
|
|
399
|
+
if (!existsSync(target)) {
|
|
400
|
+
throw new Error(`gjsify ship: the configured changelog file ${configured} does not exist.`);
|
|
401
|
+
}
|
|
402
|
+
return target;
|
|
403
|
+
}
|
|
404
|
+
for (const dir of licenseSearchDirs(projectDir)) {
|
|
405
|
+
for (const name of CHANGELOG_FILE_NAMES) {
|
|
406
|
+
const target = join(dir, name);
|
|
407
|
+
if (existsSync(target))
|
|
408
|
+
return target;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
339
413
|
/** `projectDir` first, then each ancestor up to and including the project root. */
|
|
340
414
|
function licenseSearchDirs(projectDir) {
|
|
341
415
|
const dirs = [];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { HostOs } from './types.js';
|
|
2
|
+
/** What a shipped artifact's `bin/<name>` can exec. */
|
|
3
|
+
export type ShipInterpreter = 'gjs' | 'node';
|
|
4
|
+
/** The specifiers in an entry that only ONE of the two hosts can resolve. */
|
|
5
|
+
export interface EntryEvidence {
|
|
6
|
+
/** `gi://…` and the bare GJS built-ins — resolvable under GJS, refused by node. */
|
|
7
|
+
gi: string[];
|
|
8
|
+
/** `node:…` specifiers — resolvable under Node, refused by GJS's loader. */
|
|
9
|
+
node: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Host-only specifiers of `source`, deduplicated and sorted. */
|
|
12
|
+
export declare function entryEvidence(source: string): EntryEvidence;
|
|
13
|
+
/** Everything the refusal names, so it can point at the key that decides each half. */
|
|
14
|
+
export interface EntryCheckInput {
|
|
15
|
+
/** The resolved interpreter of THIS target. */
|
|
16
|
+
interpreter: ShipInterpreter;
|
|
17
|
+
/** The entry's source, as it will be installed. */
|
|
18
|
+
source: string;
|
|
19
|
+
/** How the entry is spelled in the project, for the message. */
|
|
20
|
+
entry: string;
|
|
21
|
+
/** The config key the interpreter came from — `gjsify.app` or `gjsify.ship.app.<os>`. */
|
|
22
|
+
appKey: string;
|
|
23
|
+
/** The config key the entry came from — `gjsify.main`, `gjsify.ship.bundle[.<os>]`, … */
|
|
24
|
+
bundleKey: string;
|
|
25
|
+
/**
|
|
26
|
+
* The layout being assembled, which is the key a per-target fix goes under.
|
|
27
|
+
*
|
|
28
|
+
* {@link HostOs} and not `string`: an OS spelling this command does not
|
|
29
|
+
* assemble for is the silent defect the two resolvers refuse by name, and a
|
|
30
|
+
* message that offers `gjsify.ship.bundle.<whatever-was-passed>` as the fix
|
|
31
|
+
* would be the one place that hands the reader the typo back.
|
|
32
|
+
*/
|
|
33
|
+
layoutOs: HostOs;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Refuse a payload the target's own interpreter cannot load.
|
|
37
|
+
*
|
|
38
|
+
* ASYMMETRIC LIKE `assertLauncherMatchesInterpreter`, and for the same reason:
|
|
39
|
+
* evidence FOR the wrong host is a refusal, absence of evidence is not. A bundle
|
|
40
|
+
* that imports neither scheme — a pure-JS CLI, a bundle whose GI reach is all
|
|
41
|
+
* dynamic — passes, because nothing here observed a contradiction. This function
|
|
42
|
+
* reports what it read, never what it failed to read.
|
|
43
|
+
*/
|
|
44
|
+
export declare function assertEntryRunsUnder(input: EntryCheckInput): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Can the interpreter this target execs load the entry this target ships?
|
|
2
|
+
//
|
|
3
|
+
// THE DEFECT (#1545, measured on a React-Native-on-GTK application at 0.47.0).
|
|
4
|
+
// `gjsify ship` chose the runtime and the payload independently and never
|
|
5
|
+
// compared them. `gjsify.ship.app.darwin: "node"` made the `.app`'s launcher
|
|
6
|
+
// `exec node`, while the payload came from the project's `main`, which for a
|
|
7
|
+
// project whose primary target is Linux is its `--app gjs` bundle. The run
|
|
8
|
+
// printed one line on the subject and it read as CONFIRMATION —
|
|
9
|
+
// "runtime for darwin: node — `gjsify.ship.app.darwin` overrides `gjsify.app`" —
|
|
10
|
+
// then produced an artifact that dies before a line of application code:
|
|
11
|
+
//
|
|
12
|
+
// Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file,
|
|
13
|
+
// data, and node are supported by the default ESM loader. Received protocol 'gi:'
|
|
14
|
+
//
|
|
15
|
+
// So the artifact is built, reported as built, and cannot start. That is the
|
|
16
|
+
// class this repository pays most for: a step that reports success while having
|
|
17
|
+
// measured nothing.
|
|
18
|
+
//
|
|
19
|
+
// THE DISCRIMINATOR IS THE SPECIFIER THE OTHER HOST'S LOADER REFUSES, and it is
|
|
20
|
+
// exact in both directions — measured on this machine, GJS 1.86 / Node 24:
|
|
21
|
+
//
|
|
22
|
+
// node ← `import … from 'gi://Gtk?version=4.0'`
|
|
23
|
+
// ERR_UNSUPPORTED_ESM_URL_SCHEME … Received protocol 'gi:'
|
|
24
|
+
// node ← `import system from 'system'`
|
|
25
|
+
// ERR_MODULE_NOT_FOUND — a GJS built-in is not a package
|
|
26
|
+
// gjs ← `import … from 'node:fs'`
|
|
27
|
+
// ImportError: Unsupported URI scheme for importing: node
|
|
28
|
+
//
|
|
29
|
+
// None is a heuristic about how the bundle was built: it is the specifier the
|
|
30
|
+
// running loader rejects, read out of the file that will be installed. And none
|
|
31
|
+
// can appear in a bundle the OTHER target built for itself — `--app node`
|
|
32
|
+
// rewrites every `gi://` into `requireGi()` and every bare GJS built-in into an
|
|
33
|
+
// `@gjsify/node-gi` shim (`gjs-gi-node.ts`), `--app gjs` resolves every `node:`
|
|
34
|
+
// builtin to its `@gjsify/*` implementation — so the evidence is present exactly
|
|
35
|
+
// when the pair is wrong.
|
|
36
|
+
//
|
|
37
|
+
// THE BARE BUILT-INS ARE NOT DECORATION. A GJS bundle that reaches GI through the
|
|
38
|
+
// legacy `imports.gi` object carries no `gi://` at all, and a `--app gjs` build of
|
|
39
|
+
// one imports `system` and nothing else this reader would have seen — so #1545's
|
|
40
|
+
// own pair (a GJS payload under a node launcher) passed the guard written for it
|
|
41
|
+
// until they were added. What is deliberately NOT read is the ambient `imports`
|
|
42
|
+
// object itself: node fails on it at the first USE, not at load, so a refusal
|
|
43
|
+
// citing it would be making a claim this file cannot keep.
|
|
44
|
+
//
|
|
45
|
+
// WHY A REFUSAL AND NOT A WARNING. There is no reading of this artifact under
|
|
46
|
+
// which it works. A warning at the end of a `ship` run competes with the
|
|
47
|
+
// artifact path printed beside it, and the artifact is what gets uploaded.
|
|
48
|
+
import { importedSpecifier, walkModuleAst } from '../cli-runtime-closure.js';
|
|
49
|
+
/**
|
|
50
|
+
* The GJS built-in modules, which are bare specifiers rather than a scheme.
|
|
51
|
+
*
|
|
52
|
+
* `cairo` is deliberately absent: it is also an ordinary npm package name, so a
|
|
53
|
+
* node bundle can legitimately import it and refusing that would fail a working
|
|
54
|
+
* artifact. `system` and `gettext` have no npm counterpart a real project
|
|
55
|
+
* imports, which is what makes them evidence rather than a guess.
|
|
56
|
+
*/
|
|
57
|
+
const GJS_BUILTINS = new Set(['system', 'gettext']);
|
|
58
|
+
/** Host-only specifiers of `source`, deduplicated and sorted. */
|
|
59
|
+
export function entryEvidence(source) {
|
|
60
|
+
const gi = new Set();
|
|
61
|
+
const node = new Set();
|
|
62
|
+
walkModuleAst(source, (astNode) => {
|
|
63
|
+
const specifier = importedSpecifier(astNode);
|
|
64
|
+
if (specifier === null)
|
|
65
|
+
return;
|
|
66
|
+
if (specifier.startsWith('gi://') || GJS_BUILTINS.has(specifier))
|
|
67
|
+
gi.add(specifier);
|
|
68
|
+
else if (specifier.startsWith('node:'))
|
|
69
|
+
node.add(specifier);
|
|
70
|
+
});
|
|
71
|
+
return { gi: [...gi].sort(), node: [...node].sort() };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Refuse a payload the target's own interpreter cannot load.
|
|
75
|
+
*
|
|
76
|
+
* ASYMMETRIC LIKE `assertLauncherMatchesInterpreter`, and for the same reason:
|
|
77
|
+
* evidence FOR the wrong host is a refusal, absence of evidence is not. A bundle
|
|
78
|
+
* that imports neither scheme — a pure-JS CLI, a bundle whose GI reach is all
|
|
79
|
+
* dynamic — passes, because nothing here observed a contradiction. This function
|
|
80
|
+
* reports what it read, never what it failed to read.
|
|
81
|
+
*/
|
|
82
|
+
export function assertEntryRunsUnder(input) {
|
|
83
|
+
const evidence = entryEvidence(input.source);
|
|
84
|
+
const refused = input.interpreter === 'node' ? evidence.gi : evidence.node;
|
|
85
|
+
if (refused.length === 0)
|
|
86
|
+
return;
|
|
87
|
+
const other = input.interpreter === 'node' ? 'gjs' : 'node';
|
|
88
|
+
// The error the reader will actually see, which differs WITHIN the node case:
|
|
89
|
+
// a `gi:` specifier is refused as an unknown URL scheme, a bare GJS built-in
|
|
90
|
+
// as a package that does not exist. Naming the wrong one sends them looking
|
|
91
|
+
// for the wrong thing in a log they have not opened yet.
|
|
92
|
+
const loader = input.interpreter === 'gjs'
|
|
93
|
+
? 'GJS\'s loader refuses it: ImportError, "Unsupported URI scheme for importing: node"'
|
|
94
|
+
: refused.some((specifier) => specifier.startsWith('gi://'))
|
|
95
|
+
? 'node\'s ESM loader refuses it: ERR_UNSUPPORTED_ESM_URL_SCHEME, "Only URLs with a scheme in: ' +
|
|
96
|
+
'file, data, and node are supported by the default ESM loader"'
|
|
97
|
+
: "node's ESM loader refuses it: ERR_MODULE_NOT_FOUND — a GJS built-in is not a package";
|
|
98
|
+
const shown = refused.slice(0, 3).join(', ');
|
|
99
|
+
const more = refused.length > 3 ? `, and ${refused.length - 3} more` : '';
|
|
100
|
+
throw new Error(`gjsify ship: this target execs \`${input.interpreter}\` (\`${input.appKey}\`), and the entry it ` +
|
|
101
|
+
`would ship — ${input.entry} (\`${input.bundleKey}\`) — imports ${shown}${more}.\n` +
|
|
102
|
+
` ${loader}, before a line of application code runs.\n` +
|
|
103
|
+
' The artifact would be built, reported as built, and fail at launch on every machine.\n' +
|
|
104
|
+
` Either ship this target the \`--app ${input.interpreter}\` build of the app — ` +
|
|
105
|
+
`\`gjsify.ship.bundle.${input.layoutOs}\` selects the entry per target, the way ` +
|
|
106
|
+
`\`gjsify.ship.app.${input.layoutOs}\` selects the runtime —\n` +
|
|
107
|
+
` or set \`gjsify.ship.app.${input.layoutOs}\` to "${other}" if this bundle is the one ` +
|
|
108
|
+
'that should ship.');
|
|
109
|
+
}
|
|
@@ -275,6 +275,11 @@ export const FORMATS = {
|
|
|
275
275
|
// /usr/share/doc/<package>/copyright, and lintian errors without it.
|
|
276
276
|
licenseDest: (binaryName) => `share/doc/${binaryName}/copyright`,
|
|
277
277
|
licenseKind: 'debian-copyright',
|
|
278
|
+
// Debian Policy § 4.4. `.Debian` and not plain `changelog` because
|
|
279
|
+
// `deb.ts` writes `Version: <version>-<release>`, i.e. a Debian
|
|
280
|
+
// revision, i.e. a non-native package — which is the half of lintian's
|
|
281
|
+
// `no-changelog … (non-native package)` that names the FILE.
|
|
282
|
+
changelogDest: (binaryName) => `share/doc/${binaryName}/changelog.Debian.gz`,
|
|
278
283
|
archName: debArch,
|
|
279
284
|
fileName: (s, archLabel) => `${s.binaryName}_${s.version}-${s.release}_${archLabel}.deb`,
|
|
280
285
|
artifactKind: 'file',
|