@gjsify/cli 0.47.0 → 0.49.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 +164 -153
- package/lib/actions/build.js +2 -1
- package/lib/commands/affected-classify.js +30 -1
- package/lib/commands/build.js +4 -0
- package/lib/commands/dlx.js +7 -3
- package/lib/commands/foreach.js +9 -2
- package/lib/commands/install.js +21 -1
- package/lib/commands/publish.js +1 -8
- package/lib/commands/run.js +7 -2
- package/lib/commands/ship.js +32 -4
- package/lib/commands/upgrade.js +78 -30
- package/lib/config.js +13 -0
- package/lib/types/cli-build-options.d.ts +6 -0
- package/lib/types/config-data.d.ts +51 -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/detect-native-packages.d.ts +31 -0
- package/lib/utils/detect-native-packages.js +37 -1
- 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,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 for the CLI's own Node entry, which may not reach `@gjsify/buffer`.
|
|
3
|
+
*
|
|
4
|
+
* `@gjsify/buffer` exports exactly these two functions and this file duplicates
|
|
5
|
+
* them — deliberately, and the reason is a LAYER, not taste. The CLI's Node entry
|
|
6
|
+
* is the bootstrap artifact (ADR 0002): `gjsify install` links it before anything
|
|
7
|
+
* in this tree is built. `scripts/bootstrap-native-facades.mjs` emits `lib/esm`
|
|
8
|
+
* for its link-time runtime deps with plain `tsc`, and `buildCliRuntimeDeps()`
|
|
9
|
+
* resolves each one under `packages/infra/<name>`. `@gjsify/buffer` lives under
|
|
10
|
+
* `packages/node/` and its own `build` is `gjsify run build:gjsify && …`, i.e. it
|
|
11
|
+
* needs the BUNDLER — the artifact the bootstrap exists to produce. Importing it
|
|
12
|
+
* from here inverts that order, and the failure is not a type error: the ESM link
|
|
13
|
+
* fails at `bootstrap-native-facades`, so `build:infra` dies on every host before
|
|
14
|
+
* a single test runs.
|
|
15
|
+
*
|
|
16
|
+
* Measured: with `import { base64Encode } from '@gjsify/buffer'` in the CLI's Node
|
|
17
|
+
* entry, ten CI legs failed identically — "Missing at ESM link time:
|
|
18
|
+
* packages/infra/cli/node_modules/@gjsify/buffer/lib/esm/index.js, imported from
|
|
19
|
+
* lib/utils/ship/stage-manifest.js" — while all three REQUIRED checks stayed green,
|
|
20
|
+
* because `check-build-infra-order` holds the declaration order for TYPES and this
|
|
21
|
+
* is a runtime edge.
|
|
22
|
+
*
|
|
23
|
+
* So: one copy inside the CLI, imported by both call sites, rather than the two
|
|
24
|
+
* that prompted the deduplication in the first place. If the CLI ever gains a
|
|
25
|
+
* general way to reach `packages/node/*` at bootstrap time, this file is what
|
|
26
|
+
* should go.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Chunked because `String.fromCharCode(...bytes)` is a spread: a tarball-sized
|
|
30
|
+
* array blows the argument limit and throws `RangeError: Maximum call stack size
|
|
31
|
+
* exceeded`. 0x8000 is the size the CLI's publish path has always used.
|
|
32
|
+
*/
|
|
33
|
+
export declare function base64Encode(bytes: Uint8Array): string;
|
|
34
|
+
export declare function base64Decode(text: string): Uint8Array;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 for the CLI's own Node entry, which may not reach `@gjsify/buffer`.
|
|
3
|
+
*
|
|
4
|
+
* `@gjsify/buffer` exports exactly these two functions and this file duplicates
|
|
5
|
+
* them — deliberately, and the reason is a LAYER, not taste. The CLI's Node entry
|
|
6
|
+
* is the bootstrap artifact (ADR 0002): `gjsify install` links it before anything
|
|
7
|
+
* in this tree is built. `scripts/bootstrap-native-facades.mjs` emits `lib/esm`
|
|
8
|
+
* for its link-time runtime deps with plain `tsc`, and `buildCliRuntimeDeps()`
|
|
9
|
+
* resolves each one under `packages/infra/<name>`. `@gjsify/buffer` lives under
|
|
10
|
+
* `packages/node/` and its own `build` is `gjsify run build:gjsify && …`, i.e. it
|
|
11
|
+
* needs the BUNDLER — the artifact the bootstrap exists to produce. Importing it
|
|
12
|
+
* from here inverts that order, and the failure is not a type error: the ESM link
|
|
13
|
+
* fails at `bootstrap-native-facades`, so `build:infra` dies on every host before
|
|
14
|
+
* a single test runs.
|
|
15
|
+
*
|
|
16
|
+
* Measured: with `import { base64Encode } from '@gjsify/buffer'` in the CLI's Node
|
|
17
|
+
* entry, ten CI legs failed identically — "Missing at ESM link time:
|
|
18
|
+
* packages/infra/cli/node_modules/@gjsify/buffer/lib/esm/index.js, imported from
|
|
19
|
+
* lib/utils/ship/stage-manifest.js" — while all three REQUIRED checks stayed green,
|
|
20
|
+
* because `check-build-infra-order` holds the declaration order for TYPES and this
|
|
21
|
+
* is a runtime edge.
|
|
22
|
+
*
|
|
23
|
+
* So: one copy inside the CLI, imported by both call sites, rather than the two
|
|
24
|
+
* that prompted the deduplication in the first place. If the CLI ever gains a
|
|
25
|
+
* general way to reach `packages/node/*` at bootstrap time, this file is what
|
|
26
|
+
* should go.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Chunked because `String.fromCharCode(...bytes)` is a spread: a tarball-sized
|
|
30
|
+
* array blows the argument limit and throws `RangeError: Maximum call stack size
|
|
31
|
+
* exceeded`. 0x8000 is the size the CLI's publish path has always used.
|
|
32
|
+
*/
|
|
33
|
+
export function base64Encode(bytes) {
|
|
34
|
+
let str = '';
|
|
35
|
+
const chunk = 0x8000;
|
|
36
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
37
|
+
str += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
38
|
+
}
|
|
39
|
+
return btoa(str);
|
|
40
|
+
}
|
|
41
|
+
export function base64Decode(text) {
|
|
42
|
+
const binary = atob(text);
|
|
43
|
+
const bytes = new Uint8Array(binary.length);
|
|
44
|
+
for (let i = 0; i < binary.length; i++)
|
|
45
|
+
bytes[i] = binary.charCodeAt(i);
|
|
46
|
+
return bytes;
|
|
47
|
+
}
|
|
@@ -1,4 +1,65 @@
|
|
|
1
1
|
import type { Workspace } from '@gjsify/workspace';
|
|
2
|
+
/**
|
|
3
|
+
* Literal module specifiers of `code`: static `import`/`export … from`, plus
|
|
4
|
+
* `import('<literal>')`.
|
|
5
|
+
*
|
|
6
|
+
* acorn rather than a regex — see the header. `allowHashBang` because the CLI's
|
|
7
|
+
* own entry is a bin (`#!/usr/bin/env node`), and a parse failure yields NO
|
|
8
|
+
* specifiers, which under-selects rather than mis-selects.
|
|
9
|
+
*/
|
|
10
|
+
export interface SpecifierNode {
|
|
11
|
+
type?: string;
|
|
12
|
+
value?: unknown;
|
|
13
|
+
expressions?: unknown[];
|
|
14
|
+
quasis?: Array<{
|
|
15
|
+
value?: {
|
|
16
|
+
cooked?: unknown;
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The string a node names statically, or `null` when it is computed.
|
|
22
|
+
*
|
|
23
|
+
* A substitution-free TEMPLATE LITERAL is as static as a quoted string, and
|
|
24
|
+
* skipping it is not hypothetical: the minifier rewrites `await
|
|
25
|
+
* import('gi://GLib?version=2.0')` to a backtick form, so a Literal-only reader
|
|
26
|
+
* silently loses every dynamic import in a built bundle.
|
|
27
|
+
*
|
|
28
|
+
* Not named for specifiers, because it does not only read them: the same
|
|
29
|
+
* question is asked of a CALL ARGUMENT by `utils/ship/gi-namespaces.ts`, whose
|
|
30
|
+
* `requireGi("Gtk", "4.0")` carries a namespace and a version in exactly the two
|
|
31
|
+
* spellings above.
|
|
32
|
+
*/
|
|
33
|
+
export declare function staticStringValue(node: SpecifierNode | undefined): string | null;
|
|
34
|
+
/** One node of the walk: whatever acorn produced, seen as a bag of properties. */
|
|
35
|
+
export type AstNode = {
|
|
36
|
+
type?: string;
|
|
37
|
+
} & Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Parse `code` as a module and call `visit` for every node in it.
|
|
40
|
+
*
|
|
41
|
+
* Exported because a SECOND parse is how the two readers of a built bundle
|
|
42
|
+
* would drift apart: {@link moduleSpecifiers} answers "what does this file
|
|
43
|
+
* import" and `utils/ship/gi-namespaces.ts` answers "which GI namespaces does
|
|
44
|
+
* it load", and the second question has an answer the first cannot see — a
|
|
45
|
+
* `--app node` bundle carries no `gi://` specifier at all, only the
|
|
46
|
+
* `requireGi()` calls the bundler rewrote them into. One walker, two readers.
|
|
47
|
+
*
|
|
48
|
+
* A parse failure yields NO nodes rather than throwing: every caller is
|
|
49
|
+
* summarising a file it did not write, and under-selecting is the failure mode
|
|
50
|
+
* that keeps a correct project buildable.
|
|
51
|
+
*/
|
|
52
|
+
export declare function walkModuleAst(code: string, visit: (node: AstNode) => void): void;
|
|
53
|
+
/**
|
|
54
|
+
* The module `node` imports from, or `null` when `node` is not an import at all.
|
|
55
|
+
*
|
|
56
|
+
* FOUR node types, in one place. Every reader of a built bundle in this tree asks
|
|
57
|
+
* the same question of the same four — a static `import`, both `export … from`
|
|
58
|
+
* forms and `import()` — and each copy of that list is a chance for one reader to
|
|
59
|
+
* miss a form the others catch. The bare side-effect `import "gi://Soup"` is what
|
|
60
|
+
* that costs when it happens (`utils/ship/gi-namespaces.ts`).
|
|
61
|
+
*/
|
|
62
|
+
export declare function importedSpecifier(node: AstNode): string | null;
|
|
2
63
|
export declare function moduleSpecifiers(code: string): string[];
|
|
3
64
|
export interface CliRuntimeClosureOptions {
|
|
4
65
|
/**
|
|
@@ -79,14 +79,19 @@ function splitWorkspaceSpecifier(specifier, byName) {
|
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* The string a
|
|
82
|
+
* The string a node names statically, or `null` when it is computed.
|
|
83
83
|
*
|
|
84
84
|
* A substitution-free TEMPLATE LITERAL is as static as a quoted string, and
|
|
85
85
|
* skipping it is not hypothetical: the minifier rewrites `await
|
|
86
86
|
* import('gi://GLib?version=2.0')` to a backtick form, so a Literal-only reader
|
|
87
87
|
* silently loses every dynamic import in a built bundle.
|
|
88
|
+
*
|
|
89
|
+
* Not named for specifiers, because it does not only read them: the same
|
|
90
|
+
* question is asked of a CALL ARGUMENT by `utils/ship/gi-namespaces.ts`, whose
|
|
91
|
+
* `requireGi("Gtk", "4.0")` carries a namespace and a version in exactly the two
|
|
92
|
+
* spellings above.
|
|
88
93
|
*/
|
|
89
|
-
function
|
|
94
|
+
export function staticStringValue(node) {
|
|
90
95
|
if (node?.type === 'Literal')
|
|
91
96
|
return typeof node.value === 'string' ? node.value : null;
|
|
92
97
|
if (node?.type === 'TemplateLiteral' && node.expressions?.length === 0) {
|
|
@@ -95,39 +100,71 @@ function literalSpecifier(node) {
|
|
|
95
100
|
}
|
|
96
101
|
return null;
|
|
97
102
|
}
|
|
98
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Parse `code` as a module and call `visit` for every node in it.
|
|
105
|
+
*
|
|
106
|
+
* Exported because a SECOND parse is how the two readers of a built bundle
|
|
107
|
+
* would drift apart: {@link moduleSpecifiers} answers "what does this file
|
|
108
|
+
* import" and `utils/ship/gi-namespaces.ts` answers "which GI namespaces does
|
|
109
|
+
* it load", and the second question has an answer the first cannot see — a
|
|
110
|
+
* `--app node` bundle carries no `gi://` specifier at all, only the
|
|
111
|
+
* `requireGi()` calls the bundler rewrote them into. One walker, two readers.
|
|
112
|
+
*
|
|
113
|
+
* A parse failure yields NO nodes rather than throwing: every caller is
|
|
114
|
+
* summarising a file it did not write, and under-selecting is the failure mode
|
|
115
|
+
* that keeps a correct project buildable.
|
|
116
|
+
*/
|
|
117
|
+
export function walkModuleAst(code, visit) {
|
|
99
118
|
let ast;
|
|
100
119
|
try {
|
|
101
120
|
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true });
|
|
102
121
|
}
|
|
103
122
|
catch {
|
|
104
|
-
return
|
|
123
|
+
return;
|
|
105
124
|
}
|
|
106
|
-
const
|
|
107
|
-
const visit = (node) => {
|
|
125
|
+
const descend = (node) => {
|
|
108
126
|
if (!node || typeof node !== 'object')
|
|
109
127
|
return;
|
|
110
128
|
if (Array.isArray(node)) {
|
|
111
129
|
for (const child of node)
|
|
112
|
-
|
|
130
|
+
descend(child);
|
|
113
131
|
return;
|
|
114
132
|
}
|
|
115
133
|
const n = node;
|
|
116
|
-
|
|
117
|
-
n.type === 'ExportNamedDeclaration' ||
|
|
118
|
-
n.type === 'ExportAllDeclaration' ||
|
|
119
|
-
n.type === 'ImportExpression') {
|
|
120
|
-
const specifier = literalSpecifier(n.source);
|
|
121
|
-
if (specifier !== null)
|
|
122
|
-
out.push(specifier);
|
|
123
|
-
}
|
|
134
|
+
visit(n);
|
|
124
135
|
for (const [key, value] of Object.entries(n)) {
|
|
125
136
|
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc')
|
|
126
137
|
continue;
|
|
127
|
-
|
|
138
|
+
descend(value);
|
|
128
139
|
}
|
|
129
140
|
};
|
|
130
|
-
|
|
141
|
+
descend(ast);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The module `node` imports from, or `null` when `node` is not an import at all.
|
|
145
|
+
*
|
|
146
|
+
* FOUR node types, in one place. Every reader of a built bundle in this tree asks
|
|
147
|
+
* the same question of the same four — a static `import`, both `export … from`
|
|
148
|
+
* forms and `import()` — and each copy of that list is a chance for one reader to
|
|
149
|
+
* miss a form the others catch. The bare side-effect `import "gi://Soup"` is what
|
|
150
|
+
* that costs when it happens (`utils/ship/gi-namespaces.ts`).
|
|
151
|
+
*/
|
|
152
|
+
export function importedSpecifier(node) {
|
|
153
|
+
if (node.type !== 'ImportDeclaration' &&
|
|
154
|
+
node.type !== 'ExportNamedDeclaration' &&
|
|
155
|
+
node.type !== 'ExportAllDeclaration' &&
|
|
156
|
+
node.type !== 'ImportExpression') {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return staticStringValue(node.source);
|
|
160
|
+
}
|
|
161
|
+
export function moduleSpecifiers(code) {
|
|
162
|
+
const out = [];
|
|
163
|
+
walkModuleAst(code, (node) => {
|
|
164
|
+
const specifier = importedSpecifier(node);
|
|
165
|
+
if (specifier !== null)
|
|
166
|
+
out.push(specifier);
|
|
167
|
+
});
|
|
131
168
|
return out;
|
|
132
169
|
}
|
|
133
170
|
/** The file a relative specifier names, probing the extensionless spellings Node accepts. */
|
|
@@ -233,6 +233,37 @@ export declare function resolveHostLibc(input: {
|
|
|
233
233
|
* reach it.
|
|
234
234
|
*/
|
|
235
235
|
export declare function detectHostLibc(platform: string): HostLibc | null;
|
|
236
|
+
/**
|
|
237
|
+
* The packages a musl host had to take the DEFAULT prebuild of, because they ship
|
|
238
|
+
* no `-musl` directory — the fallback {@link hostPlatformTokens} deliberately
|
|
239
|
+
* allows, named so a caller can report it. Returns names and prints nothing: this
|
|
240
|
+
* module resolves, and `libc` is a parameter for the same reason it is on every
|
|
241
|
+
* other function here (there is no musl runner in this repo's CI, so an injected
|
|
242
|
+
* `libc: 'musl'` is the only way the branch executes).
|
|
243
|
+
*
|
|
244
|
+
* Why a fallback that mostly WORKS is worth naming. musl's loader resolves
|
|
245
|
+
* `libc.so.6`/`libm.so.6` to itself, so a glibc-linked prebuild loads — and a
|
|
246
|
+
* bridge that records no libc at all then runs correctly, which is why the
|
|
247
|
+
* fallback exists rather than resolving to null. The one that is NOT agnostic dies
|
|
248
|
+
* at `dlopen` on its first glibc-only symbol, far from its cause. Measured on a
|
|
249
|
+
* OnePlus 6 (postmarketOS v26.06, musl 1.2.6, aarch64, gjs 1.88.1) against the
|
|
250
|
+
* published 0.48.0 train: nine of ten prebuilds loaded, while
|
|
251
|
+
* `@gjsify/lightningcss-native-linux-arm64` answered `gnu_get_libc_version: symbol
|
|
252
|
+
* not found`. `gjsify install` had printed "System dependencies OK", and the first
|
|
253
|
+
* symptom was rolldown reporting `Could not load src/application.css` — the CSS
|
|
254
|
+
* bridge silently absent from the bundle. Nothing in that chain names libc, which
|
|
255
|
+
* is what makes the report worth more than the failure.
|
|
256
|
+
*
|
|
257
|
+
* Membership goes through {@link parsePlatformToken}, not the suffix constant, so
|
|
258
|
+
* this does not become a second reader of the libc half of the token grammar —
|
|
259
|
+
* `darwin-arm64-musl` is a malformed token rather than a musl build, and only the
|
|
260
|
+
* grammar knows that.
|
|
261
|
+
*
|
|
262
|
+
* TODO(open-todos: `-musl` prebuild package is published): naming the fallback is
|
|
263
|
+
* not fixing it. No `-musl` target is published for any bridge, so on a musl host
|
|
264
|
+
* this reports every prebuild in the tree and there is nothing to fall back FROM.
|
|
265
|
+
*/
|
|
266
|
+
export declare function muslPrebuildFallbacks(libc: HostLibc | null, packages: readonly NativePackage[]): string[];
|
|
236
267
|
/**
|
|
237
268
|
* The environment-variable name the host's dynamic loader consults for an
|
|
238
269
|
* additional shared-library search path, plus the separator its search paths
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// win32 AND musl branches unit-testable from a glibc Linux host. The host values
|
|
23
23
|
// are read only at the outermost call site, as a default.
|
|
24
24
|
import { readdirSync, existsSync } from 'node:fs';
|
|
25
|
-
import { join, resolve } from 'node:path';
|
|
25
|
+
import { basename, join, resolve } from 'node:path';
|
|
26
26
|
import { readPackageJson } from './pkg-json.js';
|
|
27
27
|
import { composeDyldFallback, systemGiLibraryDirs } from './system-gi.js';
|
|
28
28
|
/**
|
|
@@ -500,6 +500,42 @@ export function detectHostLibc(platform) {
|
|
|
500
500
|
const muslLoaderPresent = existsSync('/lib') && readdirSync('/lib').some((f) => f.startsWith('ld-musl-'));
|
|
501
501
|
return resolveHostLibc({ platform, glibcVersionRuntime, muslLoaderPresent });
|
|
502
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* The packages a musl host had to take the DEFAULT prebuild of, because they ship
|
|
505
|
+
* no `-musl` directory — the fallback {@link hostPlatformTokens} deliberately
|
|
506
|
+
* allows, named so a caller can report it. Returns names and prints nothing: this
|
|
507
|
+
* module resolves, and `libc` is a parameter for the same reason it is on every
|
|
508
|
+
* other function here (there is no musl runner in this repo's CI, so an injected
|
|
509
|
+
* `libc: 'musl'` is the only way the branch executes).
|
|
510
|
+
*
|
|
511
|
+
* Why a fallback that mostly WORKS is worth naming. musl's loader resolves
|
|
512
|
+
* `libc.so.6`/`libm.so.6` to itself, so a glibc-linked prebuild loads — and a
|
|
513
|
+
* bridge that records no libc at all then runs correctly, which is why the
|
|
514
|
+
* fallback exists rather than resolving to null. The one that is NOT agnostic dies
|
|
515
|
+
* at `dlopen` on its first glibc-only symbol, far from its cause. Measured on a
|
|
516
|
+
* OnePlus 6 (postmarketOS v26.06, musl 1.2.6, aarch64, gjs 1.88.1) against the
|
|
517
|
+
* published 0.48.0 train: nine of ten prebuilds loaded, while
|
|
518
|
+
* `@gjsify/lightningcss-native-linux-arm64` answered `gnu_get_libc_version: symbol
|
|
519
|
+
* not found`. `gjsify install` had printed "System dependencies OK", and the first
|
|
520
|
+
* symptom was rolldown reporting `Could not load src/application.css` — the CSS
|
|
521
|
+
* bridge silently absent from the bundle. Nothing in that chain names libc, which
|
|
522
|
+
* is what makes the report worth more than the failure.
|
|
523
|
+
*
|
|
524
|
+
* Membership goes through {@link parsePlatformToken}, not the suffix constant, so
|
|
525
|
+
* this does not become a second reader of the libc half of the token grammar —
|
|
526
|
+
* `darwin-arm64-musl` is a malformed token rather than a musl build, and only the
|
|
527
|
+
* grammar knows that.
|
|
528
|
+
*
|
|
529
|
+
* TODO(open-todos: `-musl` prebuild package is published): naming the fallback is
|
|
530
|
+
* not fixing it. No `-musl` target is published for any bridge, so on a musl host
|
|
531
|
+
* this reports every prebuild in the tree and there is nothing to fall back FROM.
|
|
532
|
+
*/
|
|
533
|
+
export function muslPrebuildFallbacks(libc, packages) {
|
|
534
|
+
if (libc !== 'musl')
|
|
535
|
+
return [];
|
|
536
|
+
const fellBack = packages.filter((pkg) => parsePlatformToken(basename(pkg.prebuildsDir)).libc !== 'musl');
|
|
537
|
+
return fellBack.map((pkg) => pkg.name);
|
|
538
|
+
}
|
|
503
539
|
/**
|
|
504
540
|
* The environment-variable name the host's dynamic loader consults for an
|
|
505
541
|
* additional shared-library search path, plus the separator its search paths
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything after the `--` separator, as an argv.
|
|
3
|
+
*
|
|
4
|
+
* @param args a yargs-parsed argument object from a command whose builder sets
|
|
5
|
+
* `parserConfiguration({ 'populate--': true, 'parse-positional-numbers': false })`
|
|
6
|
+
* @throws if an entry is not a string, i.e. if that configuration is missing
|
|
7
|
+
*/
|
|
8
|
+
export declare function doubleDashArgs(args: object): string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The argv a caller wrote after `--`, read back off yargs.
|
|
2
|
+
//
|
|
3
|
+
// THE INCIDENT (#1531). `foreach.ts` and `run.ts` each read `args['--']` and
|
|
4
|
+
// narrowed it with `.filter((v): v is string => typeof v === 'string')`. That
|
|
5
|
+
// reads like a type narrowing and is a DELETION: yargs types a bare number in
|
|
6
|
+
// its `populate--` array as a `number`, so the value disappears and the option
|
|
7
|
+
// in front of it swallows the next flag. Measured against this repo's own
|
|
8
|
+
// yargs 18.0.0:
|
|
9
|
+
//
|
|
10
|
+
// in : gjsify publish --verify-timeout 5 --tag latest
|
|
11
|
+
// raw : ["gjsify","publish","--verify-timeout",5,"--tag","latest"]
|
|
12
|
+
// filter: ["gjsify","publish","--verify-timeout","--tag","latest"]
|
|
13
|
+
//
|
|
14
|
+
// That argv shipped: `npm:publish` passes `--verify-timeout 5 --tag latest`,
|
|
15
|
+
// and release run 33804937881 echoed `--verify-timeout --tag latest` 209 times
|
|
16
|
+
// — `--verify-timeout` taking `--tag` as its value, `latest` a stray
|
|
17
|
+
// positional, `--tag latest` gone. Nobody noticed because npm's default
|
|
18
|
+
// dist-tag IS `latest`, which is luck rather than design.
|
|
19
|
+
//
|
|
20
|
+
// TWO HALVES, AND THE FIRST ONE IS THE FIX. A coercion here (`String(v)`) would
|
|
21
|
+
// recover the value and still lose its SPELLING — measured against yargs 18.0.0,
|
|
22
|
+
// `1.0` comes back as 1, `0.10` as 0.1, `0x10` as 16 and `1e3` as 1000, and an
|
|
23
|
+
// argv is text, not arithmetic. (A leading zero is NOT in that set: `007` stays
|
|
24
|
+
// the string yargs was given. It reads like the obvious example and is the one
|
|
25
|
+
// shape here that never broke.) So the parse is corrected at
|
|
26
|
+
// the source: every command that sets `populate--` also sets
|
|
27
|
+
// `parse-positional-numbers: false`, which leaves `_`, the variadic positionals
|
|
28
|
+
// and the `--` tail as the strings the user typed while option values declared
|
|
29
|
+
// `type: 'number'` are still parsed (measured: `--jobs 4` stays `4` the number).
|
|
30
|
+
//
|
|
31
|
+
// This function is the second half. It does NOT coerce — with the parser
|
|
32
|
+
// configured as above a non-string cannot arrive, so one that does means the
|
|
33
|
+
// configuration was dropped, and that is the failure to name rather than to
|
|
34
|
+
// paper over. A silent `.filter()` over a parser-produced array is the wrong
|
|
35
|
+
// tool wherever the array is data rather than a union.
|
|
36
|
+
//
|
|
37
|
+
// ONE reader for all of it, because there were three and they disagreed:
|
|
38
|
+
// `foreach.ts` (twice) and `run.ts` filtered, `dlx.ts` coerced. The drifted
|
|
39
|
+
// copies are the ones that shipped the wrong argv.
|
|
40
|
+
/**
|
|
41
|
+
* Everything after the `--` separator, as an argv.
|
|
42
|
+
*
|
|
43
|
+
* @param args a yargs-parsed argument object from a command whose builder sets
|
|
44
|
+
* `parserConfiguration({ 'populate--': true, 'parse-positional-numbers': false })`
|
|
45
|
+
* @throws if an entry is not a string, i.e. if that configuration is missing
|
|
46
|
+
*/
|
|
47
|
+
export function doubleDashArgs(args) {
|
|
48
|
+
const raw = args['--'];
|
|
49
|
+
if (raw === undefined || raw === null)
|
|
50
|
+
return [];
|
|
51
|
+
if (!Array.isArray(raw)) {
|
|
52
|
+
throw new Error(`gjsify: internal error — yargs' \`--\` slot holds a ${typeof raw} rather than an array, so the ` +
|
|
53
|
+
'arguments after `--` cannot be forwarded. The command builder must set ' +
|
|
54
|
+
"`parserConfiguration({ 'populate--': true })`.");
|
|
55
|
+
}
|
|
56
|
+
return raw.map((value, index) => {
|
|
57
|
+
if (typeof value === 'string')
|
|
58
|
+
return value;
|
|
59
|
+
throw new Error(`gjsify: internal error — the argument after \`--\` at position ${index} arrived as a ` +
|
|
60
|
+
`${typeof value} (${String(value)}) instead of a string, so forwarding it would change what ` +
|
|
61
|
+
'the caller wrote. The command builder must set ' +
|
|
62
|
+
"`parserConfiguration({ 'parse-positional-numbers': false })` alongside `populate--`.");
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -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
|
+
}
|