@gjsify/cli 0.38.1 → 0.40.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 +212 -195
- package/lib/cli-app.js +11 -25
- package/lib/commands/affected-classify.d.ts +16 -0
- package/lib/commands/affected-classify.js +304 -0
- package/lib/commands/affected.js +2 -291
- package/lib/commands/flatpak/check.js +34 -43
- package/lib/commands/flatpak/ci.js +1 -1
- package/lib/commands/flatpak/deps.js +10 -19
- package/lib/commands/flatpak/diff.js +1 -1
- package/lib/commands/flatpak/init.js +1 -1
- package/lib/commands/flatpak/release.js +7 -11
- package/lib/commands/flatpak/scaffold.d.ts +2 -4
- package/lib/commands/flatpak/scaffold.js +25 -298
- package/lib/commands/flatpak/sync-flathub.js +18 -36
- package/lib/commands/foreach.js +34 -20
- package/lib/commands/gettext.js +7 -4
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/ship.d.ts +11 -0
- package/lib/commands/ship.js +217 -0
- package/lib/config.d.ts +12 -0
- package/lib/config.js +15 -0
- package/lib/types/config-data.d.ts +136 -46
- package/lib/utils/app-metadata.d.ts +51 -0
- package/lib/utils/app-metadata.js +338 -0
- package/lib/utils/cli-runtime-closure.d.ts +23 -0
- package/lib/utils/cli-runtime-closure.js +274 -0
- package/lib/utils/publish-headers.d.ts +13 -0
- package/lib/utils/publish-headers.js +30 -4
- package/lib/utils/ship/ar.d.ts +21 -0
- package/lib/utils/ship/ar.js +57 -0
- package/lib/utils/ship/bytes.d.ts +4 -0
- package/lib/utils/ship/bytes.js +23 -0
- package/lib/utils/ship/cpio.d.ts +22 -0
- package/lib/utils/ship/cpio.js +93 -0
- package/lib/utils/ship/deb.d.ts +24 -0
- package/lib/utils/ship/deb.js +118 -0
- package/lib/utils/ship/depends.d.ts +62 -0
- package/lib/utils/ship/depends.js +202 -0
- package/lib/utils/ship/discover.d.ts +21 -0
- package/lib/utils/ship/discover.js +129 -0
- package/lib/utils/ship/formats.d.ts +5 -0
- package/lib/utils/ship/formats.js +83 -0
- package/lib/utils/ship/gi-namespaces.d.ts +7 -0
- package/lib/utils/ship/gi-namespaces.js +49 -0
- package/lib/utils/ship/gzip.d.ts +2 -0
- package/lib/utils/ship/gzip.js +21 -0
- package/lib/utils/ship/launcher.d.ts +7 -0
- package/lib/utils/ship/launcher.js +37 -0
- package/lib/utils/ship/payload.d.ts +30 -0
- package/lib/utils/ship/payload.js +59 -0
- package/lib/utils/ship/plan.d.ts +34 -0
- package/lib/utils/ship/plan.js +167 -0
- package/lib/utils/ship/rpm-header.d.ts +29 -0
- package/lib/utils/ship/rpm-header.js +170 -0
- package/lib/utils/ship/rpm.d.ts +12 -0
- package/lib/utils/ship/rpm.js +377 -0
- package/lib/utils/ship/scripts.d.ts +31 -0
- package/lib/utils/ship/scripts.js +76 -0
- package/lib/utils/ship/settings.d.ts +60 -0
- package/lib/utils/ship/settings.js +184 -0
- package/lib/utils/ship/stage-writer.d.ts +17 -0
- package/lib/utils/ship/stage-writer.js +49 -0
- package/lib/utils/ship/types.d.ts +114 -0
- package/lib/utils/ship/types.js +10 -0
- package/lib/utils/ship/version.d.ts +17 -0
- package/lib/utils/ship/version.js +55 -0
- package/lib/utils/spawn.d.ts +11 -1
- package/lib/utils/spawn.js +28 -4
- package/package.json +19 -18
- /package/lib/templates/{flatpak → app}/desktop.tmpl +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Turning a project into a fully-defaulted {@link ShipSettings}.
|
|
2
|
+
//
|
|
3
|
+
// Kept free of the filesystem: the caller discovers the payload (bundle files,
|
|
4
|
+
// icons, schemas, licence) and hands the paths in, so every defaulting and
|
|
5
|
+
// every refusal is unit-testable without building an app first. The precedent
|
|
6
|
+
// is `utils/copy-targets.ts`, and the reason is the same — the cases worth
|
|
7
|
+
// testing are the ones that must FAIL.
|
|
8
|
+
//
|
|
9
|
+
// Precedence is uniform: CLI flag > `gjsify.ship` > `gjsify.flatpak` (metadata
|
|
10
|
+
// only) > derived from package.json. Never a yargs `default:`, which is
|
|
11
|
+
// indistinguishable from a value the user typed and would clobber the config.
|
|
12
|
+
import { basename } from 'node:path';
|
|
13
|
+
import { DEFAULT_GJS_FLOOR } from './depends.js';
|
|
14
|
+
import { assertRelease, normaliseVersion } from './version.js';
|
|
15
|
+
/** Freedesktop main category → deb `Section:` / rpm `Group:`. First match wins. */
|
|
16
|
+
const CATEGORY_SECTIONS = [
|
|
17
|
+
['Development', { section: 'devel', group: 'Development/Tools' }],
|
|
18
|
+
['Education', { section: 'education', group: 'Applications/Education' }],
|
|
19
|
+
['Game', { section: 'games', group: 'Amusements/Games' }],
|
|
20
|
+
['Graphics', { section: 'graphics', group: 'Applications/Multimedia' }],
|
|
21
|
+
['AudioVideo', { section: 'sound', group: 'Applications/Multimedia' }],
|
|
22
|
+
['Network', { section: 'net', group: 'Applications/Internet' }],
|
|
23
|
+
['Office', { section: 'text', group: 'Applications/Productivity' }],
|
|
24
|
+
['Science', { section: 'science', group: 'Applications/Engineering' }],
|
|
25
|
+
['System', { section: 'admin', group: 'Applications/System' }],
|
|
26
|
+
['Utility', { section: 'utils', group: 'Applications/System' }],
|
|
27
|
+
];
|
|
28
|
+
export function resolveShipSettings(input) {
|
|
29
|
+
const warnings = [];
|
|
30
|
+
const { pkg, ship, flatpak, discovered } = input;
|
|
31
|
+
// Both blocks extend AppMetadata, so `gjsify.ship` overriding
|
|
32
|
+
// `gjsify.flatpak` key-by-key is exactly the fallback ADR 0024 § 8 asks
|
|
33
|
+
// for. The non-metadata keys each block carries ride along unread.
|
|
34
|
+
const metadata = { ...flatpak, ...definedOnly(ship) };
|
|
35
|
+
const binaryName = ship.binaryName ?? deriveBinaryName(pkg.name);
|
|
36
|
+
const appId = ship.appId ?? flatpak.appId ?? reverseDnsOrThrow(pkg.name, binaryName);
|
|
37
|
+
const name = metadata.name ?? titleCase(binaryName);
|
|
38
|
+
const kind = metadata.kind ?? 'app';
|
|
39
|
+
const rawVersion = ship.version ?? pkg.version;
|
|
40
|
+
if (!rawVersion) {
|
|
41
|
+
throw new Error('gjsify ship: no version — set `version` in package.json or `gjsify.ship.version`.');
|
|
42
|
+
}
|
|
43
|
+
const normalised = normaliseVersion(rawVersion);
|
|
44
|
+
warnings.push(...normalised.warnings);
|
|
45
|
+
const license = metadata.license?.project ?? pkg.license;
|
|
46
|
+
if (!license) {
|
|
47
|
+
throw new Error('gjsify ship: no licence — set `license` in package.json (an SPDX id, e.g. "MIT") ' +
|
|
48
|
+
'or `gjsify.ship.license.project`. Both deb and rpm carry it as a required field.');
|
|
49
|
+
}
|
|
50
|
+
const maintainer = ship.maintainer ?? formatAuthor(pkg.author) ?? formatDeveloper(metadata);
|
|
51
|
+
if (!maintainer) {
|
|
52
|
+
throw new Error('gjsify ship: no maintainer — set `author` in package.json ("Name <you@example.com>"), ' +
|
|
53
|
+
'`gjsify.ship.maintainer`, or `gjsify.ship.developer`. dpkg refuses a package without one.');
|
|
54
|
+
}
|
|
55
|
+
const summary = metadata.summary ?? pkg.description ?? name;
|
|
56
|
+
const description = descriptionParagraphs(metadata.description) ?? [summary];
|
|
57
|
+
const sections = sectionsFor(metadata.categories);
|
|
58
|
+
if (kind === 'app' && discovered.iconFiles.length === 0) {
|
|
59
|
+
warnings.push(`gjsify ship: no icon found — the installed app will show a placeholder in menus and app stores. ` +
|
|
60
|
+
'Point `gjsify.ship.icon` at an SVG (or a directory of sized PNGs).');
|
|
61
|
+
}
|
|
62
|
+
const settings = {
|
|
63
|
+
projectDir: input.projectDir,
|
|
64
|
+
appId,
|
|
65
|
+
name,
|
|
66
|
+
binaryName,
|
|
67
|
+
version: normalised.version,
|
|
68
|
+
release: assertRelease(ship.release ?? '1'),
|
|
69
|
+
maintainer,
|
|
70
|
+
summary,
|
|
71
|
+
description,
|
|
72
|
+
license,
|
|
73
|
+
licenseFile: discovered.licenseFile,
|
|
74
|
+
homepage: metadata.homepageUrl ?? pkg.homepage,
|
|
75
|
+
section: ship.section ?? sections.section,
|
|
76
|
+
group: ship.group ?? sections.group,
|
|
77
|
+
kind,
|
|
78
|
+
extraDepends: { deb: ship.depends?.deb ?? [], rpm: ship.depends?.rpm ?? [] },
|
|
79
|
+
typelibPackages: ship.typelibPackages ?? {},
|
|
80
|
+
bundlePath: discovered.bundlePath,
|
|
81
|
+
bundleDir: discovered.bundleDir,
|
|
82
|
+
iconFiles: discovered.iconFiles,
|
|
83
|
+
schemaFiles: discovered.schemaFiles,
|
|
84
|
+
extraFiles: ship.extraFiles ?? {},
|
|
85
|
+
execArgs: ship.execArgs ?? [],
|
|
86
|
+
outDir: input.cli.outDir ?? ship.outDir ?? 'ship',
|
|
87
|
+
arch: input.cli.arch ?? process.arch,
|
|
88
|
+
minGjsVersion: ship.minGjsVersion ?? DEFAULT_GJS_FLOOR,
|
|
89
|
+
};
|
|
90
|
+
return { settings, metadata, warnings };
|
|
91
|
+
}
|
|
92
|
+
/** The bundle path a project declares, in the order the rest of the CLI reads them. */
|
|
93
|
+
export function declaredBundlePath(pkg, ship) {
|
|
94
|
+
return ship.bundle ?? pkg.gjsify?.main ?? pkg.main;
|
|
95
|
+
}
|
|
96
|
+
/** npm package name → a package/binary name both dpkg and rpm accept. */
|
|
97
|
+
export function deriveBinaryName(pkgName) {
|
|
98
|
+
if (!pkgName) {
|
|
99
|
+
throw new Error('gjsify ship: no package name — set `name` in package.json or `gjsify.ship.binaryName`.');
|
|
100
|
+
}
|
|
101
|
+
const unscoped = pkgName.includes('/') ? pkgName.split('/').pop() : pkgName;
|
|
102
|
+
const candidate = unscoped
|
|
103
|
+
.toLowerCase()
|
|
104
|
+
.replace(/[^a-z\d+.-]+/g, '-')
|
|
105
|
+
.replace(/^-+|-+$/g, '');
|
|
106
|
+
// dpkg: at least two characters, starts alphanumeric, then [a-z0-9+.-].
|
|
107
|
+
// rpm is looser, so satisfying dpkg satisfies both.
|
|
108
|
+
if (!/^[a-z\d][a-z\d+.-]+$/.test(candidate)) {
|
|
109
|
+
throw new Error(`gjsify ship: cannot derive a package name from "${pkgName}" (got "${candidate}"). ` +
|
|
110
|
+
'Set `gjsify.ship.binaryName` to something matching `[a-z0-9][a-z0-9+.-]+`.');
|
|
111
|
+
}
|
|
112
|
+
return candidate;
|
|
113
|
+
}
|
|
114
|
+
function reverseDnsOrThrow(pkgName, binaryName) {
|
|
115
|
+
if (pkgName && /^[A-Za-z][\w-]*(\.[A-Za-z][\w-]*){2,}$/.test(pkgName))
|
|
116
|
+
return pkgName;
|
|
117
|
+
throw new Error(`gjsify ship: no application id. Set \`gjsify.ship.appId\` (or \`gjsify.flatpak.appId\`) to a ` +
|
|
118
|
+
`reverse-DNS id such as "io.github.you.${titleCase(binaryName).replace(/\s+/g, '')}". ` +
|
|
119
|
+
'It names the desktop entry, the AppStream component and the installed icon, so it cannot be guessed.');
|
|
120
|
+
}
|
|
121
|
+
function definedOnly(value) {
|
|
122
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
123
|
+
}
|
|
124
|
+
function titleCase(value) {
|
|
125
|
+
return value
|
|
126
|
+
.split(/[-_.]+/)
|
|
127
|
+
.filter((part) => part.length > 0)
|
|
128
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
129
|
+
.join(' ');
|
|
130
|
+
}
|
|
131
|
+
function sectionsFor(categories) {
|
|
132
|
+
for (const [category, mapped] of CATEGORY_SECTIONS) {
|
|
133
|
+
if (categories?.includes(category))
|
|
134
|
+
return mapped;
|
|
135
|
+
}
|
|
136
|
+
return { section: 'misc', group: 'Applications/System' };
|
|
137
|
+
}
|
|
138
|
+
function formatAuthor(author) {
|
|
139
|
+
if (typeof author === 'string') {
|
|
140
|
+
// npm's shorthand is `Name <email> (url)`; the url has no place in
|
|
141
|
+
// either package format's maintainer field.
|
|
142
|
+
return author.replace(/\s*\([^)]*\)\s*$/, '').trim() || undefined;
|
|
143
|
+
}
|
|
144
|
+
if (author?.name)
|
|
145
|
+
return author.email ? `${author.name} <${author.email}>` : author.name;
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
function formatDeveloper(metadata) {
|
|
149
|
+
const developer = metadata.developer;
|
|
150
|
+
if (!developer?.name)
|
|
151
|
+
return undefined;
|
|
152
|
+
return developer.email ? `${developer.name} <${developer.email}>` : developer.name;
|
|
153
|
+
}
|
|
154
|
+
/** AppStream description (string or blocks) → plain paragraphs for deb/rpm. */
|
|
155
|
+
export function descriptionParagraphs(description) {
|
|
156
|
+
if (description === undefined)
|
|
157
|
+
return undefined;
|
|
158
|
+
if (typeof description === 'string') {
|
|
159
|
+
const paragraphs = description
|
|
160
|
+
.trim()
|
|
161
|
+
.split(/\n\n+/)
|
|
162
|
+
.map((p) => p.trim().replace(/\s+/g, ' '))
|
|
163
|
+
.filter((p) => p.length > 0);
|
|
164
|
+
return paragraphs.length > 0 ? paragraphs : undefined;
|
|
165
|
+
}
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const block of description) {
|
|
168
|
+
if ('p' in block) {
|
|
169
|
+
out.push(block.p.trim().replace(/\s+/g, ' '));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
for (const item of block.ul) {
|
|
173
|
+
out.push(`* ${(typeof item === 'string' ? item : item.item).trim().replace(/\s+/g, ' ')}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out.length > 0 ? out : undefined;
|
|
178
|
+
}
|
|
179
|
+
/** `LICENSE`, `LICENSE.md`, … — the names a licence file actually has. */
|
|
180
|
+
export const LICENSE_FILE_NAMES = ['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING', 'COPYING.md'];
|
|
181
|
+
/** The bundle's own basename, used as the launcher's exec target. */
|
|
182
|
+
export function bundleEntryName(bundlePath) {
|
|
183
|
+
return basename(bundlePath);
|
|
184
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PayloadEntry } from './payload.js';
|
|
2
|
+
import type { StagedFile } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Materialise a planned tree under `root`, replacing whatever was there.
|
|
5
|
+
*
|
|
6
|
+
* The directory is wiped first: a stale file from a previous run is otherwise
|
|
7
|
+
* indistinguishable from a staged one, and would be packed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeStage(root: string, files: readonly StagedFile[]): void;
|
|
10
|
+
/**
|
|
11
|
+
* Read a staged tree back as a payload.
|
|
12
|
+
*
|
|
13
|
+
* Modes come from the PLAN, not from `stat()`: a checked-out tree can carry
|
|
14
|
+
* whatever umask the build host had, and an executable bit that survives on
|
|
15
|
+
* one machine and not another is exactly the kind of difference that ships.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readStage(roots: readonly string[], files: readonly StagedFile[]): PayloadEntry[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Writing the planned payload to disk, and reading it back for the packers.
|
|
2
|
+
//
|
|
3
|
+
// The round trip is the point. `gjsify ship --stage` writes `stage/`; a packer
|
|
4
|
+
// then reads `stage/` (plus the format's `overlay/`) rather than the planner's
|
|
5
|
+
// in-memory list, so "what the user inspected" and "what got packed" cannot
|
|
6
|
+
// diverge. It also makes the staged tree a legitimate deliverable on its own —
|
|
7
|
+
// which is what a Flatpak module's `cp -a stage/.` will consume once Flatpak
|
|
8
|
+
// moves under `ship` (ADR 0024 § 8).
|
|
9
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join, sep } from 'node:path';
|
|
11
|
+
import { listFilesRecursive } from './discover.js';
|
|
12
|
+
/**
|
|
13
|
+
* Materialise a planned tree under `root`, replacing whatever was there.
|
|
14
|
+
*
|
|
15
|
+
* The directory is wiped first: a stale file from a previous run is otherwise
|
|
16
|
+
* indistinguishable from a staged one, and would be packed.
|
|
17
|
+
*/
|
|
18
|
+
export function writeStage(root, files) {
|
|
19
|
+
rmSync(root, { recursive: true, force: true, maxRetries: 5 });
|
|
20
|
+
mkdirSync(root, { recursive: true });
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const target = join(root, file.path.split('/').join(sep));
|
|
23
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
24
|
+
const data = file.source.kind === 'text' ? new TextEncoder().encode(file.source.text) : readFileSync(file.source.path);
|
|
25
|
+
writeFileSync(target, data);
|
|
26
|
+
chmodSync(target, file.mode);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Read a staged tree back as a payload.
|
|
31
|
+
*
|
|
32
|
+
* Modes come from the PLAN, not from `stat()`: a checked-out tree can carry
|
|
33
|
+
* whatever umask the build host had, and an executable bit that survives on
|
|
34
|
+
* one machine and not another is exactly the kind of difference that ships.
|
|
35
|
+
*/
|
|
36
|
+
export function readStage(roots, files) {
|
|
37
|
+
const modes = new Map(files.map((file) => [file.path, file.mode]));
|
|
38
|
+
const byPath = new Map();
|
|
39
|
+
for (const root of roots) {
|
|
40
|
+
for (const rel of listFilesRecursive(root)) {
|
|
41
|
+
byPath.set(rel, {
|
|
42
|
+
path: rel,
|
|
43
|
+
mode: modes.get(rel) ?? 0o644,
|
|
44
|
+
data: new Uint8Array(readFileSync(join(root, rel.split('/').join(sep)))),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...byPath.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
49
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** One file in the payload, addressed relative to the install prefix. */
|
|
2
|
+
export interface StagedFile {
|
|
3
|
+
/** Prefix-relative path, e.g. `bin/learn6502`, `share/applications/x.desktop`. */
|
|
4
|
+
path: string;
|
|
5
|
+
/** POSIX mode bits. */
|
|
6
|
+
mode: number;
|
|
7
|
+
/** Where the bytes come from. */
|
|
8
|
+
source: {
|
|
9
|
+
kind: 'text';
|
|
10
|
+
text: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'file';
|
|
13
|
+
path: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** A packed artifact on disk. */
|
|
17
|
+
export interface ShipArtifact {
|
|
18
|
+
format: FormatId;
|
|
19
|
+
/** Absolute path to the written file. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Size in bytes. */
|
|
22
|
+
size: number;
|
|
23
|
+
}
|
|
24
|
+
export type FormatId = 'deb' | 'rpm';
|
|
25
|
+
/**
|
|
26
|
+
* Everything that is per-FORMAT, as data.
|
|
27
|
+
*
|
|
28
|
+
* `prefix` is the one value the payload itself cannot be blind to — a launcher
|
|
29
|
+
* derives it at runtime (ADR 0024 § 3), but a D-Bus service file and an RPM's
|
|
30
|
+
* directory ownership need it spelled out.
|
|
31
|
+
*/
|
|
32
|
+
export interface FormatDescriptor {
|
|
33
|
+
id: FormatId;
|
|
34
|
+
/** Install prefix the format's contents hang under. */
|
|
35
|
+
prefix: string;
|
|
36
|
+
/** Where the project's licence text goes in this format's overlay. */
|
|
37
|
+
licenseDest: (binaryName: string) => string;
|
|
38
|
+
/**
|
|
39
|
+
* `debian-copyright` renders the machine-readable copyright 1.0 format
|
|
40
|
+
* Debian policy asks for; `plain` copies the licence file verbatim.
|
|
41
|
+
*/
|
|
42
|
+
licenseKind: 'debian-copyright' | 'plain';
|
|
43
|
+
/**
|
|
44
|
+
* This format's spelling of the architecture.
|
|
45
|
+
*
|
|
46
|
+
* `archIndependent` is DERIVED from the payload, not configured: a
|
|
47
|
+
* `--app gjs` bundle is JavaScript and a shell launcher, so it really does
|
|
48
|
+
* install everywhere, and saying `amd64` would make apt refuse it on an
|
|
49
|
+
* arm64 machine it runs on perfectly. One `.so` in the payload flips it.
|
|
50
|
+
*/
|
|
51
|
+
archName: (arch: string, archIndependent: boolean) => string;
|
|
52
|
+
/** Artifact filename, given the resolved settings and the resolved arch label. */
|
|
53
|
+
fileName: (settings: ShipSettings, archLabel: string) => string;
|
|
54
|
+
}
|
|
55
|
+
/** The resolved, fully defaulted configuration for one `gjsify ship` run. */
|
|
56
|
+
export interface ShipSettings {
|
|
57
|
+
/** Absolute path to the project being shipped. */
|
|
58
|
+
projectDir: string;
|
|
59
|
+
/** Reverse-DNS application id — desktop entry, metainfo and icon basename. */
|
|
60
|
+
appId: string;
|
|
61
|
+
/** Human-readable display name. */
|
|
62
|
+
name: string;
|
|
63
|
+
/** Package name: the `bin/` entry, the deb `Package:`, the rpm `Name:`. */
|
|
64
|
+
binaryName: string;
|
|
65
|
+
/** Upstream version, normalised for package managers. */
|
|
66
|
+
version: string;
|
|
67
|
+
/** Package revision within one upstream version (deb revision / rpm release). */
|
|
68
|
+
release: string;
|
|
69
|
+
/** `Maintainer:` / `Packager:`, `Name <email>`. */
|
|
70
|
+
maintainer: string;
|
|
71
|
+
/** One-line summary. */
|
|
72
|
+
summary: string;
|
|
73
|
+
/** Multi-paragraph description, already split. */
|
|
74
|
+
description: string[];
|
|
75
|
+
/** SPDX identifier of the project licence. */
|
|
76
|
+
license: string;
|
|
77
|
+
/** Absolute path to the licence file, when one was found. */
|
|
78
|
+
licenseFile?: string;
|
|
79
|
+
homepage?: string;
|
|
80
|
+
/** deb `Section:`. */
|
|
81
|
+
section: string;
|
|
82
|
+
/** rpm `Group:`. */
|
|
83
|
+
group: string;
|
|
84
|
+
/** `'app'` stages a desktop entry and an icon; `'cli'` stages neither. */
|
|
85
|
+
kind: 'app' | 'cli';
|
|
86
|
+
/** Extra runtime dependencies per format, appended to the derived set. */
|
|
87
|
+
extraDepends: {
|
|
88
|
+
deb: string[];
|
|
89
|
+
rpm: string[];
|
|
90
|
+
};
|
|
91
|
+
/** Project-supplied GI-namespace → package rows, filling gaps in the built-in table. */
|
|
92
|
+
typelibPackages: Record<string, {
|
|
93
|
+
deb: string;
|
|
94
|
+
rpm: string;
|
|
95
|
+
}>;
|
|
96
|
+
/** Absolute path to the built bundle that `bin/<binaryName>` executes. */
|
|
97
|
+
bundlePath: string;
|
|
98
|
+
/** Absolute path of the directory staged wholesale into `lib/<binaryName>/`. */
|
|
99
|
+
bundleDir: string;
|
|
100
|
+
/** Absolute paths of the icon files to install, largest-first is not required. */
|
|
101
|
+
iconFiles: string[];
|
|
102
|
+
/** Absolute paths of `*.gschema.xml` files to install. */
|
|
103
|
+
schemaFiles: string[];
|
|
104
|
+
/** `<dest relative to prefix>` → absolute source path. */
|
|
105
|
+
extraFiles: Record<string, string>;
|
|
106
|
+
/** Arguments appended to the launcher's `exec` line. */
|
|
107
|
+
execArgs: string[];
|
|
108
|
+
/** Output root; `stage/`, `overlay/` and `out/` hang under it. */
|
|
109
|
+
outDir: string;
|
|
110
|
+
/** Target architecture in `process.arch` spelling. */
|
|
111
|
+
arch: string;
|
|
112
|
+
/** Minimum GJS the emitted dependency asks for. */
|
|
113
|
+
minGjsVersion: string;
|
|
114
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The vocabulary `gjsify ship` is written in (ADR 0024).
|
|
2
|
+
//
|
|
3
|
+
// ONE payload, one layout per OS, one packer per format. Everything that
|
|
4
|
+
// differs between a `.deb` and an `.rpm` is a {@link FormatDescriptor} field or
|
|
5
|
+
// a {@link StagedFile} in that format's overlay — never a branch in the
|
|
6
|
+
// staging code. The reference implementation this design is taken from
|
|
7
|
+
// (`refs/gtkx`, MPL-2.0, reimplemented not copied) expresses the same
|
|
8
|
+
// distinction as `if (target === 'deb')` inside its stager, and pays for it:
|
|
9
|
+
// adding a fifth target there means editing six files.
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface NormalisedVersion {
|
|
2
|
+
/** The version string to write into `Version:`. */
|
|
3
|
+
version: string;
|
|
4
|
+
/** Non-fatal transformations worth telling the user about. */
|
|
5
|
+
warnings: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Convert an npm/semver version into one both dpkg and rpm order correctly.
|
|
9
|
+
*
|
|
10
|
+
* - a leading `v` is dropped (`v1.2.3` is a tag, not a version)
|
|
11
|
+
* - `+buildmetadata` is dropped — neither format has a place for it, and both
|
|
12
|
+
* would sort it as part of the version
|
|
13
|
+
* - `-prerelease` becomes `~prerelease`, with inner `-` flattened to `.`
|
|
14
|
+
*/
|
|
15
|
+
export declare function normaliseVersion(raw: string): NormalisedVersion;
|
|
16
|
+
/** Validate a Debian revision / RPM release. */
|
|
17
|
+
export declare function assertRelease(release: string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// npm version → package-manager version.
|
|
2
|
+
//
|
|
3
|
+
// The three vocabularies disagree in ways that are silent rather than loud:
|
|
4
|
+
// npm's `1.2.0-rc.1` sorts BEFORE `1.2.0`, and so does Debian's and RPM's
|
|
5
|
+
// `1.2.0~rc.1` — but `1.2.0-rc.1` fed to dpkg is read as upstream `1.2.0` with
|
|
6
|
+
// revision `rc.1`, which sorts AFTER `1.2.0`. A prerelease that upgrades to
|
|
7
|
+
// nothing is the kind of defect that only shows up once a release is out, so
|
|
8
|
+
// the conversion happens here and is asserted rather than hoped for.
|
|
9
|
+
// Spelled out rather than `\w`, which admits `_` — a character deb-version(7)
|
|
10
|
+
// does not allow. `1.0.0_beta` would pass here and dpkg would refuse the
|
|
11
|
+
// finished package at install time, one step too late to be useful.
|
|
12
|
+
/** `dpkg` upstream-version grammar, minus the `-` that separates the revision. */
|
|
13
|
+
const PACKAGE_VERSION = /^\d[A-Za-z0-9.+~]*$/;
|
|
14
|
+
/** Debian revision / RPM release. */
|
|
15
|
+
const RELEASE = /^[A-Za-z0-9][A-Za-z0-9.+~]*$/;
|
|
16
|
+
/**
|
|
17
|
+
* Convert an npm/semver version into one both dpkg and rpm order correctly.
|
|
18
|
+
*
|
|
19
|
+
* - a leading `v` is dropped (`v1.2.3` is a tag, not a version)
|
|
20
|
+
* - `+buildmetadata` is dropped — neither format has a place for it, and both
|
|
21
|
+
* would sort it as part of the version
|
|
22
|
+
* - `-prerelease` becomes `~prerelease`, with inner `-` flattened to `.`
|
|
23
|
+
*/
|
|
24
|
+
export function normaliseVersion(raw) {
|
|
25
|
+
const warnings = [];
|
|
26
|
+
let value = raw.trim();
|
|
27
|
+
if (value.startsWith('v'))
|
|
28
|
+
value = value.slice(1);
|
|
29
|
+
const plus = value.indexOf('+');
|
|
30
|
+
if (plus !== -1) {
|
|
31
|
+
warnings.push(`gjsify ship: dropped the build metadata "${value.slice(plus)}" from version ${raw} — ` +
|
|
32
|
+
'neither dpkg nor rpm has a field for it. Set `gjsify.ship.version` to pin an exact package version.');
|
|
33
|
+
value = value.slice(0, plus);
|
|
34
|
+
}
|
|
35
|
+
const dash = value.indexOf('-');
|
|
36
|
+
if (dash !== -1) {
|
|
37
|
+
const prerelease = value.slice(dash + 1).replace(/-/g, '.');
|
|
38
|
+
value = `${value.slice(0, dash)}~${prerelease}`;
|
|
39
|
+
}
|
|
40
|
+
if (!PACKAGE_VERSION.test(value)) {
|
|
41
|
+
throw new Error(`gjsify ship: "${raw}" is not a usable package version (normalised to "${value}"). ` +
|
|
42
|
+
'It must start with a digit and contain only letters, digits, `.`, `+` and `~`. ' +
|
|
43
|
+
'Set `gjsify.ship.version` in package.json to override it.');
|
|
44
|
+
}
|
|
45
|
+
return { version: value, warnings };
|
|
46
|
+
}
|
|
47
|
+
/** Validate a Debian revision / RPM release. */
|
|
48
|
+
export function assertRelease(release) {
|
|
49
|
+
if (!RELEASE.test(release)) {
|
|
50
|
+
throw new Error(`gjsify ship: "${release}" is not a usable package release. ` +
|
|
51
|
+
'It must start with a letter or digit and contain only letters, digits, `.`, `+` and `~`. ' +
|
|
52
|
+
'Set `gjsify.ship.release` in package.json.');
|
|
53
|
+
}
|
|
54
|
+
return release;
|
|
55
|
+
}
|
package/lib/utils/spawn.d.ts
CHANGED
|
@@ -32,8 +32,14 @@ export interface SpawnToCompletionOptions {
|
|
|
32
32
|
* stderr**, for a parent whose own stdout is machine-readable: `gjsify pack
|
|
33
33
|
* --json` emits JSON there, and interleaved lifecycle-script log lines make it
|
|
34
34
|
* unparseable for anything piping into `JSON.parse`.
|
|
35
|
+
*
|
|
36
|
+
* `'capture'` collects both streams into {@link SpawnCompletion} and emits
|
|
37
|
+
* NEITHER, for a caller that reports the child's output only on failure. Unlike
|
|
38
|
+
* `'pipe'` it works on both paths, so it composes with `completion: 'return'` —
|
|
39
|
+
* `'pipe'` hands back a live `ChildProcess` the blocking path cannot produce,
|
|
40
|
+
* which is why that one is `'exit'`-only.
|
|
35
41
|
*/
|
|
36
|
-
stdio?: 'inherit' | 'pipe' | 'inherit-stderr';
|
|
42
|
+
stdio?: 'inherit' | 'pipe' | 'inherit-stderr' | 'capture';
|
|
37
43
|
/**
|
|
38
44
|
* Run `cmd` through the platform shell so `&&`, `|` and env-var references work.
|
|
39
45
|
* `cmd` is then a whole command LINE and `args` must be empty — npm/yarn script
|
|
@@ -65,6 +71,10 @@ export interface SpawnCompletion {
|
|
|
65
71
|
code: number | null;
|
|
66
72
|
/** The terminating signal, or `null` on a normal exit. */
|
|
67
73
|
signal: NodeJS.Signals | null;
|
|
74
|
+
/** The child's stdout — only under `stdio: 'capture'`, `''` when it wrote none. */
|
|
75
|
+
stdout?: string;
|
|
76
|
+
/** The child's stderr — only under `stdio: 'capture'`, `''` when it wrote none. */
|
|
77
|
+
stderr?: string;
|
|
68
78
|
}
|
|
69
79
|
/**
|
|
70
80
|
* Run `cmd args…` to completion and resolve with its exit code and signal.
|
package/lib/utils/spawn.js
CHANGED
|
@@ -122,6 +122,16 @@ export function spawnToCompletion(cmd, args, opts) {
|
|
|
122
122
|
});
|
|
123
123
|
if (r.error)
|
|
124
124
|
return Promise.reject(mapSpawnError(r.error, opts));
|
|
125
|
+
// `'capture'` hands both streams back instead of emitting them — this path
|
|
126
|
+
// already pipes, so it is the same read, routed to the caller.
|
|
127
|
+
if (opts.stdio === 'capture') {
|
|
128
|
+
return Promise.resolve({
|
|
129
|
+
code: r.status,
|
|
130
|
+
signal: r.signal ?? null,
|
|
131
|
+
stdout: String(r.stdout ?? ''),
|
|
132
|
+
stderr: String(r.stderr ?? ''),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
125
135
|
// Captured stdout goes to the parent's STDERR under `'inherit-stderr'`, the
|
|
126
136
|
// destination the streaming path routes it to, so the two agree on where a
|
|
127
137
|
// lifecycle script's chatter lands.
|
|
@@ -140,9 +150,11 @@ export function spawnToCompletion(cmd, args, opts) {
|
|
|
140
150
|
// `['inherit', 2, 2]` is stdin inherited, stdout AND stderr → fd 2.
|
|
141
151
|
stdio: opts.stdio === 'pipe'
|
|
142
152
|
? ['ignore', 'pipe', 'pipe']
|
|
143
|
-
: opts.stdio === '
|
|
144
|
-
? ['inherit',
|
|
145
|
-
: 'inherit'
|
|
153
|
+
: opts.stdio === 'capture'
|
|
154
|
+
? ['inherit', 'pipe', 'pipe']
|
|
155
|
+
: opts.stdio === 'inherit-stderr'
|
|
156
|
+
? ['inherit', 2, 2]
|
|
157
|
+
: 'inherit',
|
|
146
158
|
};
|
|
147
159
|
if (opts.shell)
|
|
148
160
|
spawnOpts.shell = true;
|
|
@@ -152,10 +164,22 @@ export function spawnToCompletion(cmd, args, opts) {
|
|
|
152
164
|
spawnOpts.windowsVerbatimArguments = true;
|
|
153
165
|
const child = spawn(win.cmd, win.args, spawnOpts);
|
|
154
166
|
opts.onSpawn?.(child);
|
|
167
|
+
// Collected only under `'capture'`; the other modes never attach a reader,
|
|
168
|
+
// so an inherited stream is untouched.
|
|
169
|
+
let captured;
|
|
170
|
+
if (opts.stdio === 'capture') {
|
|
171
|
+
// Bound to a const the listeners close over: narrowing `captured` does
|
|
172
|
+
// not survive into a callback, and `captured!` would be a lie the day
|
|
173
|
+
// someone reorders these lines.
|
|
174
|
+
const buf = { stdout: '', stderr: '' };
|
|
175
|
+
captured = buf;
|
|
176
|
+
child.stdout?.on('data', (d) => (buf.stdout += String(d)));
|
|
177
|
+
child.stderr?.on('data', (d) => (buf.stderr += String(d)));
|
|
178
|
+
}
|
|
155
179
|
// `close` (not `exit`) so piped stdio is fully drained before the caller's
|
|
156
180
|
// flushers run; with `stdio: 'inherit'` the two are emitted back to back.
|
|
157
181
|
child.on('close', (code, signal) => {
|
|
158
|
-
resolvePromise({ code, signal });
|
|
182
|
+
resolvePromise(captured ? { code, signal, ...captured } : { code, signal });
|
|
159
183
|
});
|
|
160
184
|
child.on('error', (err) => {
|
|
161
185
|
reject(mapSpawnError(err, opts));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -134,20 +134,21 @@
|
|
|
134
134
|
"cli"
|
|
135
135
|
],
|
|
136
136
|
"dependencies": {
|
|
137
|
-
"@gjsify/buffer": "^0.
|
|
138
|
-
"@gjsify/create-app": "^0.
|
|
139
|
-
"@gjsify/node-globals": "^0.
|
|
140
|
-
"@gjsify/node-polyfills": "^0.
|
|
141
|
-
"@gjsify/npm-registry": "^0.
|
|
142
|
-
"@gjsify/resolve-npm": "^0.
|
|
143
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.
|
|
144
|
-
"@gjsify/vite-plugin-blueprint": "^0.
|
|
145
|
-
"@gjsify/rolldown-plugin-pnp": "^0.
|
|
146
|
-
"@gjsify/semver": "^0.
|
|
147
|
-
"@gjsify/tar": "^0.
|
|
148
|
-
"@gjsify/tsc": "^0.
|
|
149
|
-
"@gjsify/web-polyfills": "^0.
|
|
150
|
-
"@gjsify/workspace": "^0.
|
|
137
|
+
"@gjsify/buffer": "^0.40.0",
|
|
138
|
+
"@gjsify/create-app": "^0.40.0",
|
|
139
|
+
"@gjsify/node-globals": "^0.40.0",
|
|
140
|
+
"@gjsify/node-polyfills": "^0.40.0",
|
|
141
|
+
"@gjsify/npm-registry": "^0.40.0",
|
|
142
|
+
"@gjsify/resolve-npm": "^0.40.0",
|
|
143
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.40.0",
|
|
144
|
+
"@gjsify/vite-plugin-blueprint": "^0.40.0",
|
|
145
|
+
"@gjsify/rolldown-plugin-pnp": "^0.40.0",
|
|
146
|
+
"@gjsify/semver": "^0.40.0",
|
|
147
|
+
"@gjsify/tar": "^0.40.0",
|
|
148
|
+
"@gjsify/tsc": "^0.40.0",
|
|
149
|
+
"@gjsify/web-polyfills": "^0.40.0",
|
|
150
|
+
"@gjsify/workspace": "^0.40.0",
|
|
151
|
+
"acorn": "^8.17.0",
|
|
151
152
|
"cosmiconfig": "^9.0.2",
|
|
152
153
|
"get-tsconfig": "^4.14.0",
|
|
153
154
|
"pkg-types": "^2.3.1",
|
|
@@ -155,13 +156,13 @@
|
|
|
155
156
|
"yargs": "^18.0.0"
|
|
156
157
|
},
|
|
157
158
|
"devDependencies": {
|
|
158
|
-
"@gjsify/unit": "^0.
|
|
159
|
+
"@gjsify/unit": "^0.40.0",
|
|
159
160
|
"@types/yargs": "^17.0.35",
|
|
160
161
|
"typescript": "^6.0.3"
|
|
161
162
|
},
|
|
162
163
|
"peerDependencies": {
|
|
163
|
-
"@gjsify/oxfmt-native": "^0.
|
|
164
|
-
"@gjsify/rolldown-native": "^0.
|
|
164
|
+
"@gjsify/oxfmt-native": "^0.40.0",
|
|
165
|
+
"@gjsify/rolldown-native": "^0.40.0"
|
|
165
166
|
},
|
|
166
167
|
"peerDependenciesMeta": {
|
|
167
168
|
"@gjsify/oxfmt-native": {
|
|
File without changes
|