@gjsify/cli 0.39.0 → 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 +197 -180
- package/lib/cli-app.js +11 -25
- package/lib/commands/flatpak/check.js +1 -1
- package/lib/commands/flatpak/ci.js +1 -1
- package/lib/commands/flatpak/diff.js +1 -1
- package/lib/commands/flatpak/init.js +1 -1
- package/lib/commands/flatpak/scaffold.d.ts +2 -4
- package/lib/commands/flatpak/scaffold.js +25 -298
- package/lib/commands/flatpak/sync-flathub.js +1 -1
- package/lib/commands/foreach.js +34 -20
- 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/package.json +19 -18
- /package/lib/templates/{flatpak → app}/desktop.tmpl +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// The `.rpm` packer.
|
|
2
|
+
//
|
|
3
|
+
// Hand-written for the same reason as the `.deb` one, and with a better
|
|
4
|
+
// oracle: `rpm` itself is on every Fedora image this project's CI runs on, and
|
|
5
|
+
// it is a strict, independent parser. `rpm -qp --info`, `rpm -qpl`,
|
|
6
|
+
// `rpm -qp --requires` and `rpm -K` between them read the lead, both headers,
|
|
7
|
+
// every tag this file writes and the payload digest — so the e2e suite checks
|
|
8
|
+
// the artifact with code that shares nothing with the writer.
|
|
9
|
+
//
|
|
10
|
+
// `rpmbuild` was the alternative and is the wrong dependency: it is not in the
|
|
11
|
+
// CI image, adding it there would gate this PR on an image rebuild, and it
|
|
12
|
+
// cannot run under GJS at all. What it would have bought is the header
|
|
13
|
+
// encoding in `rpm-header.ts`, which is 180 lines.
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { concatBytes } from './bytes.js';
|
|
16
|
+
import { createCpioArchive, S_IFDIR, S_IFREG } from './cpio.js';
|
|
17
|
+
import { parseDepend } from './depends.js';
|
|
18
|
+
import { buildRpmHeader, buildRpmLead, padToEight, RpmType, RPM_TAG_HEADERIMMUTABLE, RPM_TAG_HEADERSIGNATURES, } from './rpm-header.js';
|
|
19
|
+
import { gzipDeterministic } from './gzip.js';
|
|
20
|
+
import { renderRpmScriptlets } from './scripts.js';
|
|
21
|
+
// Header tags. Named rather than inlined because a wrong NUMBER produces a
|
|
22
|
+
// package that parses and is quietly wrong, which is the hardest kind to spot.
|
|
23
|
+
const TAG = {
|
|
24
|
+
HEADERI18NTABLE: 100,
|
|
25
|
+
NAME: 1000,
|
|
26
|
+
VERSION: 1001,
|
|
27
|
+
RELEASE: 1002,
|
|
28
|
+
SUMMARY: 1004,
|
|
29
|
+
DESCRIPTION: 1005,
|
|
30
|
+
BUILDTIME: 1006,
|
|
31
|
+
BUILDHOST: 1007,
|
|
32
|
+
SIZE: 1009,
|
|
33
|
+
LICENSE: 1014,
|
|
34
|
+
GROUP: 1016,
|
|
35
|
+
URL: 1020,
|
|
36
|
+
OS: 1021,
|
|
37
|
+
ARCH: 1022,
|
|
38
|
+
POSTIN: 1024,
|
|
39
|
+
POSTUN: 1026,
|
|
40
|
+
FILESIZES: 1028,
|
|
41
|
+
FILEMODES: 1030,
|
|
42
|
+
FILERDEVS: 1033,
|
|
43
|
+
FILEMTIMES: 1034,
|
|
44
|
+
FILEDIGESTS: 1035,
|
|
45
|
+
FILELINKTOS: 1036,
|
|
46
|
+
FILEFLAGS: 1037,
|
|
47
|
+
FILEUSERNAME: 1039,
|
|
48
|
+
FILEGROUPNAME: 1040,
|
|
49
|
+
SOURCERPM: 1044,
|
|
50
|
+
FILEVERIFYFLAGS: 1045,
|
|
51
|
+
PROVIDENAME: 1047,
|
|
52
|
+
REQUIREFLAGS: 1048,
|
|
53
|
+
REQUIRENAME: 1049,
|
|
54
|
+
REQUIREVERSION: 1050,
|
|
55
|
+
RPMVERSION: 1064,
|
|
56
|
+
POSTINPROG: 1086,
|
|
57
|
+
POSTUNPROG: 1088,
|
|
58
|
+
FILEDEVICES: 1095,
|
|
59
|
+
FILEINODES: 1096,
|
|
60
|
+
FILELANGS: 1097,
|
|
61
|
+
PROVIDEFLAGS: 1112,
|
|
62
|
+
PROVIDEVERSION: 1113,
|
|
63
|
+
DIRINDEXES: 1116,
|
|
64
|
+
BASENAMES: 1117,
|
|
65
|
+
DIRNAMES: 1118,
|
|
66
|
+
PAYLOADFORMAT: 1124,
|
|
67
|
+
PAYLOADCOMPRESSOR: 1125,
|
|
68
|
+
PAYLOADFLAGS: 1126,
|
|
69
|
+
FILEDIGESTALGO: 5011,
|
|
70
|
+
ENCODING: 5062,
|
|
71
|
+
PAYLOADDIGEST: 5092,
|
|
72
|
+
PAYLOADDIGESTALGO: 5093,
|
|
73
|
+
PAYLOADDIGESTALT: 5097,
|
|
74
|
+
};
|
|
75
|
+
const SIGTAG = { SIZE: 1000, MD5: 1004, PAYLOADSIZE: 1007, SHA1HEADER: 269, SHA256HEADER: 273 };
|
|
76
|
+
const SENSE = {
|
|
77
|
+
LESS: 1 << 1,
|
|
78
|
+
GREATER: 1 << 2,
|
|
79
|
+
EQUAL: 1 << 3,
|
|
80
|
+
INTERP: 1 << 8,
|
|
81
|
+
SCRIPT_POST: 1 << 10,
|
|
82
|
+
SCRIPT_POSTUN: 1 << 12,
|
|
83
|
+
RPMLIB: 1 << 24,
|
|
84
|
+
};
|
|
85
|
+
/** SHA-256 in rpm's digest-algorithm numbering (PGP hash algorithm ids). */
|
|
86
|
+
const DIGEST_ALGO_SHA256 = 8;
|
|
87
|
+
/**
|
|
88
|
+
* The rpmlib features this writer's output depends on.
|
|
89
|
+
*
|
|
90
|
+
* These are not optional decoration: `CompressedFileNames` is what declares
|
|
91
|
+
* the DIRNAMES/BASENAMES/DIRINDEXES split, `FileDigests` that FILEDIGESTS is
|
|
92
|
+
* a digest rather than an MD5, and `PayloadFilesHavePrefix` that the cpio
|
|
93
|
+
* names carry a `./`. An rpm too old for one of them must refuse the package
|
|
94
|
+
* rather than misread it.
|
|
95
|
+
*/
|
|
96
|
+
const RPMLIB_REQUIRES = [
|
|
97
|
+
['rpmlib(CompressedFileNames)', '3.0.4-1'],
|
|
98
|
+
['rpmlib(FileDigests)', '4.6.0-1'],
|
|
99
|
+
['rpmlib(PayloadFilesHavePrefix)', '4.0-1'],
|
|
100
|
+
];
|
|
101
|
+
/**
|
|
102
|
+
* Directories a base system already owns.
|
|
103
|
+
*
|
|
104
|
+
* An rpm that claims nothing leaves its own `/usr/lib/<app>` unowned, which
|
|
105
|
+
* `rpm -e` then leaves behind. Listing the base-system directories it must not
|
|
106
|
+
* claim is the middle ground.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately not exhaustive, and that is affordable: rpm treats directory
|
|
109
|
+
* CO-ownership as normal (`rpm -qf /usr/share/icons/hicolor` names three
|
|
110
|
+
* packages on this machine), so a directory missing from this list costs
|
|
111
|
+
* rpmlint noise, not a broken install. The entries that matter are the ones a
|
|
112
|
+
* package would otherwise claim on every install.
|
|
113
|
+
*/
|
|
114
|
+
const SYSTEM_OWNED_DIRECTORIES = new Set([
|
|
115
|
+
'/usr',
|
|
116
|
+
'/usr/bin',
|
|
117
|
+
'/usr/lib',
|
|
118
|
+
'/usr/lib64',
|
|
119
|
+
'/usr/libexec',
|
|
120
|
+
'/usr/sbin',
|
|
121
|
+
'/usr/share',
|
|
122
|
+
'/usr/share/applications',
|
|
123
|
+
'/usr/share/doc',
|
|
124
|
+
'/usr/share/icons',
|
|
125
|
+
'/usr/share/licenses',
|
|
126
|
+
'/usr/share/man',
|
|
127
|
+
'/usr/share/metainfo',
|
|
128
|
+
// Owned by glib2 — an app that installs a schema must not claim the
|
|
129
|
+
// directory the whole system's schemas live in. Verified with `rpm -qf`.
|
|
130
|
+
'/usr/share/glib-2.0',
|
|
131
|
+
'/usr/share/glib-2.0/schemas',
|
|
132
|
+
'/etc',
|
|
133
|
+
'/opt',
|
|
134
|
+
'/var',
|
|
135
|
+
]);
|
|
136
|
+
export async function buildRpm(inputs) {
|
|
137
|
+
const { settings, mtime } = inputs;
|
|
138
|
+
const prefix = `/${inputs.prefix.replace(/^\/+|\/+$/g, '')}`;
|
|
139
|
+
const files = collectFiles(inputs, prefix);
|
|
140
|
+
const cpio = createCpioArchive(files.map((file, index) => ({
|
|
141
|
+
name: `.${file.path}`,
|
|
142
|
+
mode: file.mode,
|
|
143
|
+
data: file.data,
|
|
144
|
+
mtime,
|
|
145
|
+
ino: index + 1,
|
|
146
|
+
})));
|
|
147
|
+
const compressed = await gzipDeterministic(cpio);
|
|
148
|
+
const header = buildRpmHeader(mainHeaderEntries(inputs, files, cpio, compressed), RPM_TAG_HEADERIMMUTABLE);
|
|
149
|
+
const body = concatBytes([header, compressed]);
|
|
150
|
+
const signature = padToEight(buildRpmHeader([
|
|
151
|
+
{ tag: SIGTAG.SHA1HEADER, type: RpmType.STRING, value: hashHex('sha1', header) },
|
|
152
|
+
{ tag: SIGTAG.SHA256HEADER, type: RpmType.STRING, value: hashHex('sha256', header) },
|
|
153
|
+
{ tag: SIGTAG.SIZE, type: RpmType.INT32, value: [body.byteLength] },
|
|
154
|
+
{ tag: SIGTAG.MD5, type: RpmType.BIN, value: hashBytes('md5', body) },
|
|
155
|
+
{ tag: SIGTAG.PAYLOADSIZE, type: RpmType.INT32, value: [cpio.byteLength] },
|
|
156
|
+
], RPM_TAG_HEADERSIGNATURES));
|
|
157
|
+
const lead = buildRpmLead(`${settings.binaryName}-${settings.version}-${settings.release}`);
|
|
158
|
+
return concatBytes([lead, signature, body]);
|
|
159
|
+
}
|
|
160
|
+
/** The payload plus the directories this package should own, in install order. */
|
|
161
|
+
function collectFiles(inputs, prefix) {
|
|
162
|
+
const files = inputs.payload.map((entry) => {
|
|
163
|
+
if (entry.data.byteLength > 0xffffffff) {
|
|
164
|
+
throw new Error(`gjsify ship: ${entry.path} is larger than 4 GiB, which needs rpm's LONGFILESIZES tag. ` +
|
|
165
|
+
'That is not implemented — split the payload.');
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
path: `${prefix}/${entry.path}`,
|
|
169
|
+
mode: S_IFREG | (entry.mode & 0o7777),
|
|
170
|
+
data: entry.data,
|
|
171
|
+
isDirectory: false,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
const owned = new Set();
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
const parts = file.path.split('/').slice(1);
|
|
177
|
+
parts.pop();
|
|
178
|
+
let current = '';
|
|
179
|
+
for (const part of parts) {
|
|
180
|
+
current = `${current}/${part}`;
|
|
181
|
+
if (!SYSTEM_OWNED_DIRECTORIES.has(current))
|
|
182
|
+
owned.add(current);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const directory of owned) {
|
|
186
|
+
files.push({ path: directory, mode: S_IFDIR | 0o755, data: new Uint8Array(0), isDirectory: true });
|
|
187
|
+
}
|
|
188
|
+
// rpm reads the file list positionally across a dozen parallel arrays, so
|
|
189
|
+
// ONE order has to serve all of them; sorting here is what guarantees a
|
|
190
|
+
// directory is listed before what lives in it.
|
|
191
|
+
return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
192
|
+
}
|
|
193
|
+
function mainHeaderEntries(inputs, files, cpio, compressed) {
|
|
194
|
+
const { settings } = inputs;
|
|
195
|
+
const evr = `${settings.version}-${settings.release}`;
|
|
196
|
+
const dirNames = [];
|
|
197
|
+
const dirIndexes = [];
|
|
198
|
+
const baseNames = [];
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
const slash = file.path.lastIndexOf('/');
|
|
201
|
+
const dir = `${file.path.slice(0, slash)}/`;
|
|
202
|
+
let index = dirNames.indexOf(dir);
|
|
203
|
+
if (index === -1)
|
|
204
|
+
index = dirNames.push(dir) - 1;
|
|
205
|
+
dirIndexes.push(index);
|
|
206
|
+
baseNames.push(file.path.slice(slash + 1));
|
|
207
|
+
}
|
|
208
|
+
const scripts = renderRpmScriptlets(settings, inputs.prefix);
|
|
209
|
+
const requires = buildRequires(inputs.depends, scripts);
|
|
210
|
+
const entries = [
|
|
211
|
+
{ tag: TAG.HEADERI18NTABLE, type: RpmType.STRING_ARRAY, value: ['C'] },
|
|
212
|
+
{ tag: TAG.NAME, type: RpmType.STRING, value: settings.binaryName },
|
|
213
|
+
{ tag: TAG.VERSION, type: RpmType.STRING, value: settings.version },
|
|
214
|
+
{ tag: TAG.RELEASE, type: RpmType.STRING, value: settings.release },
|
|
215
|
+
{ tag: TAG.SUMMARY, type: RpmType.I18NSTRING, value: [settings.summary] },
|
|
216
|
+
{ tag: TAG.DESCRIPTION, type: RpmType.I18NSTRING, value: [settings.description.join('\n\n')] },
|
|
217
|
+
{ tag: TAG.BUILDTIME, type: RpmType.INT32, value: [inputs.mtime] },
|
|
218
|
+
// Not the real hostname: it would put the build machine's name in
|
|
219
|
+
// every artifact and make two builds of the same source differ.
|
|
220
|
+
{ tag: TAG.BUILDHOST, type: RpmType.STRING, value: 'localhost' },
|
|
221
|
+
{
|
|
222
|
+
tag: TAG.SIZE,
|
|
223
|
+
type: RpmType.INT32,
|
|
224
|
+
value: [files.reduce((total, file) => total + file.data.byteLength, 0)],
|
|
225
|
+
},
|
|
226
|
+
{ tag: TAG.LICENSE, type: RpmType.STRING, value: settings.license },
|
|
227
|
+
{ tag: TAG.GROUP, type: RpmType.I18NSTRING, value: [settings.group] },
|
|
228
|
+
{ tag: TAG.OS, type: RpmType.STRING, value: 'linux' },
|
|
229
|
+
{ tag: TAG.ARCH, type: RpmType.STRING, value: inputs.archLabel },
|
|
230
|
+
{ tag: TAG.SOURCERPM, type: RpmType.STRING, value: `${settings.binaryName}-${evr}.src.rpm` },
|
|
231
|
+
{ tag: TAG.RPMVERSION, type: RpmType.STRING, value: '4.20.0' },
|
|
232
|
+
{ tag: TAG.ENCODING, type: RpmType.STRING, value: 'utf-8' },
|
|
233
|
+
{ tag: TAG.FILESIZES, type: RpmType.INT32, value: files.map((file) => file.data.byteLength) },
|
|
234
|
+
{ tag: TAG.FILEMODES, type: RpmType.INT16, value: files.map((file) => file.mode) },
|
|
235
|
+
{ tag: TAG.FILERDEVS, type: RpmType.INT16, value: files.map(() => 0) },
|
|
236
|
+
{ tag: TAG.FILEMTIMES, type: RpmType.INT32, value: files.map(() => inputs.mtime) },
|
|
237
|
+
{
|
|
238
|
+
tag: TAG.FILEDIGESTS,
|
|
239
|
+
type: RpmType.STRING_ARRAY,
|
|
240
|
+
// A directory has no contents to digest, and rpm expects the empty
|
|
241
|
+
// string there rather than the digest of zero bytes.
|
|
242
|
+
value: files.map((file) => (file.isDirectory ? '' : hashHex('sha256', file.data))),
|
|
243
|
+
},
|
|
244
|
+
{ tag: TAG.FILELINKTOS, type: RpmType.STRING_ARRAY, value: files.map(() => '') },
|
|
245
|
+
{ tag: TAG.FILEFLAGS, type: RpmType.INT32, value: files.map(() => 0) },
|
|
246
|
+
{ tag: TAG.FILEUSERNAME, type: RpmType.STRING_ARRAY, value: files.map(() => 'root') },
|
|
247
|
+
{ tag: TAG.FILEGROUPNAME, type: RpmType.STRING_ARRAY, value: files.map(() => 'root') },
|
|
248
|
+
{ tag: TAG.FILEVERIFYFLAGS, type: RpmType.INT32, value: files.map(() => 0xffffffff) },
|
|
249
|
+
{ tag: TAG.FILEDEVICES, type: RpmType.INT32, value: files.map(() => 1) },
|
|
250
|
+
{ tag: TAG.FILEINODES, type: RpmType.INT32, value: files.map((_, index) => index + 1) },
|
|
251
|
+
{ tag: TAG.FILELANGS, type: RpmType.STRING_ARRAY, value: files.map(() => '') },
|
|
252
|
+
{ tag: TAG.FILEDIGESTALGO, type: RpmType.INT32, value: [DIGEST_ALGO_SHA256] },
|
|
253
|
+
{ tag: TAG.DIRINDEXES, type: RpmType.INT32, value: dirIndexes },
|
|
254
|
+
{ tag: TAG.BASENAMES, type: RpmType.STRING_ARRAY, value: baseNames },
|
|
255
|
+
{ tag: TAG.DIRNAMES, type: RpmType.STRING_ARRAY, value: dirNames },
|
|
256
|
+
{ tag: TAG.PROVIDENAME, type: RpmType.STRING_ARRAY, value: [settings.binaryName] },
|
|
257
|
+
{ tag: TAG.PROVIDEFLAGS, type: RpmType.INT32, value: [SENSE.EQUAL] },
|
|
258
|
+
{ tag: TAG.PROVIDEVERSION, type: RpmType.STRING_ARRAY, value: [evr] },
|
|
259
|
+
{ tag: TAG.REQUIRENAME, type: RpmType.STRING_ARRAY, value: requires.names },
|
|
260
|
+
{ tag: TAG.REQUIREFLAGS, type: RpmType.INT32, value: requires.flags },
|
|
261
|
+
{ tag: TAG.REQUIREVERSION, type: RpmType.STRING_ARRAY, value: requires.versions },
|
|
262
|
+
{ tag: TAG.PAYLOADFORMAT, type: RpmType.STRING, value: 'cpio' },
|
|
263
|
+
{ tag: TAG.PAYLOADCOMPRESSOR, type: RpmType.STRING, value: 'gzip' },
|
|
264
|
+
// Write-side metadata only — no reader acts on it (rpm-format.md § 6.5).
|
|
265
|
+
// `9` is what rpmbuild writes; the Web `CompressionStream` this packer
|
|
266
|
+
// uses exposes no level to report, so the honest reading of this field
|
|
267
|
+
// is "gzip, default settings" rather than a measured level.
|
|
268
|
+
{ tag: TAG.PAYLOADFLAGS, type: RpmType.STRING, value: '9' },
|
|
269
|
+
{ tag: TAG.PAYLOADDIGEST, type: RpmType.STRING_ARRAY, value: [hashHex('sha256', compressed)] },
|
|
270
|
+
{ tag: TAG.PAYLOADDIGESTALGO, type: RpmType.INT32, value: [DIGEST_ALGO_SHA256] },
|
|
271
|
+
{ tag: TAG.PAYLOADDIGESTALT, type: RpmType.STRING_ARRAY, value: [hashHex('sha256', cpio)] },
|
|
272
|
+
];
|
|
273
|
+
assertParallelFileArrays(entries, files.length);
|
|
274
|
+
if (settings.homepage)
|
|
275
|
+
entries.push({ tag: TAG.URL, type: RpmType.STRING, value: settings.homepage });
|
|
276
|
+
if (scripts.post !== undefined) {
|
|
277
|
+
entries.push({ tag: TAG.POSTIN, type: RpmType.STRING, value: scripts.post });
|
|
278
|
+
entries.push({ tag: TAG.POSTINPROG, type: RpmType.STRING, value: '/bin/sh' });
|
|
279
|
+
}
|
|
280
|
+
if (scripts.postun !== undefined) {
|
|
281
|
+
entries.push({ tag: TAG.POSTUN, type: RpmType.STRING, value: scripts.postun });
|
|
282
|
+
entries.push({ tag: TAG.POSTUNPROG, type: RpmType.STRING, value: '/bin/sh' });
|
|
283
|
+
}
|
|
284
|
+
return entries;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Every per-file tag is one column of the same table.
|
|
288
|
+
*
|
|
289
|
+
* rpm reads them POSITIONALLY and never checks that they agree: a short
|
|
290
|
+
* FILEMODES array is not an error, it is a package whose later files install
|
|
291
|
+
* with whatever value happened to sit at that index. The lengths are trivially
|
|
292
|
+
* checkable here and effectively uncheckable afterwards, so they are checked
|
|
293
|
+
* here.
|
|
294
|
+
*/
|
|
295
|
+
function assertParallelFileArrays(entries, fileCount) {
|
|
296
|
+
const perFileTags = new Set([
|
|
297
|
+
TAG.FILESIZES,
|
|
298
|
+
TAG.FILEMODES,
|
|
299
|
+
TAG.FILERDEVS,
|
|
300
|
+
TAG.FILEMTIMES,
|
|
301
|
+
TAG.FILEDIGESTS,
|
|
302
|
+
TAG.FILELINKTOS,
|
|
303
|
+
TAG.FILEFLAGS,
|
|
304
|
+
TAG.FILEUSERNAME,
|
|
305
|
+
TAG.FILEGROUPNAME,
|
|
306
|
+
TAG.FILEVERIFYFLAGS,
|
|
307
|
+
TAG.FILEDEVICES,
|
|
308
|
+
TAG.FILEINODES,
|
|
309
|
+
TAG.FILELANGS,
|
|
310
|
+
TAG.DIRINDEXES,
|
|
311
|
+
TAG.BASENAMES,
|
|
312
|
+
]);
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
if (!perFileTags.has(entry.tag))
|
|
315
|
+
continue;
|
|
316
|
+
const length = Array.isArray(entry.value) ? entry.value.length : 1;
|
|
317
|
+
if (length !== fileCount) {
|
|
318
|
+
throw new Error(`gjsify ship: internal error — rpm tag ${entry.tag} has ${length} values for ${fileCount} files.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The three parallel `Requires` arrays.
|
|
324
|
+
*
|
|
325
|
+
* A scriptlet's interpreter is a dependency like any other and rpm expects it
|
|
326
|
+
* declared with the matching `SCRIPT_*` sense bit — without it, a system
|
|
327
|
+
* without `/bin/sh` installs the package and then fails inside `%post`.
|
|
328
|
+
*/
|
|
329
|
+
function buildRequires(depends, scripts) {
|
|
330
|
+
const names = [];
|
|
331
|
+
const flags = [];
|
|
332
|
+
const versions = [];
|
|
333
|
+
for (const depend of depends) {
|
|
334
|
+
const parsed = parseDepend(depend);
|
|
335
|
+
names.push(parsed.name);
|
|
336
|
+
flags.push(senseFor(parsed.relation));
|
|
337
|
+
versions.push(parsed.version ?? '');
|
|
338
|
+
}
|
|
339
|
+
if (scripts.post !== undefined) {
|
|
340
|
+
names.push('/bin/sh');
|
|
341
|
+
flags.push(SENSE.INTERP | SENSE.SCRIPT_POST);
|
|
342
|
+
versions.push('');
|
|
343
|
+
}
|
|
344
|
+
if (scripts.postun !== undefined) {
|
|
345
|
+
names.push('/bin/sh');
|
|
346
|
+
flags.push(SENSE.INTERP | SENSE.SCRIPT_POSTUN);
|
|
347
|
+
versions.push('');
|
|
348
|
+
}
|
|
349
|
+
for (const [name, version] of RPMLIB_REQUIRES) {
|
|
350
|
+
names.push(name);
|
|
351
|
+
flags.push(SENSE.RPMLIB | SENSE.LESS | SENSE.EQUAL);
|
|
352
|
+
versions.push(version);
|
|
353
|
+
}
|
|
354
|
+
return { names, flags, versions };
|
|
355
|
+
}
|
|
356
|
+
function senseFor(relation) {
|
|
357
|
+
switch (relation) {
|
|
358
|
+
case '>=':
|
|
359
|
+
return SENSE.GREATER | SENSE.EQUAL;
|
|
360
|
+
case '>':
|
|
361
|
+
return SENSE.GREATER;
|
|
362
|
+
case '<=':
|
|
363
|
+
return SENSE.LESS | SENSE.EQUAL;
|
|
364
|
+
case '<':
|
|
365
|
+
return SENSE.LESS;
|
|
366
|
+
case '=':
|
|
367
|
+
return SENSE.EQUAL;
|
|
368
|
+
default:
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function hashHex(algorithm, data) {
|
|
373
|
+
return createHash(algorithm).update(data).digest('hex');
|
|
374
|
+
}
|
|
375
|
+
function hashBytes(algorithm, data) {
|
|
376
|
+
return new Uint8Array(createHash(algorithm).update(data).digest());
|
|
377
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ShipSettings } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The cache refreshes this payload makes necessary, each guarded by the tool's
|
|
4
|
+
* presence and `|| true`.
|
|
5
|
+
*
|
|
6
|
+
* The caches belong to the distribution, not to us: on removal the helper may
|
|
7
|
+
* already be gone, and a maintainer script that fails during a purge leaves a
|
|
8
|
+
* package the user cannot uninstall.
|
|
9
|
+
*/
|
|
10
|
+
export declare function cacheRefreshCommands(settings: ShipSettings, prefix: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* dpkg maintainer scripts, keyed by their `control.tar` filename.
|
|
13
|
+
*
|
|
14
|
+
* `postrm` guards on `remove` alone: dpkg also calls it for `purge`, `upgrade`
|
|
15
|
+
* and several abort paths, and on an upgrade the incoming package's `postinst`
|
|
16
|
+
* refreshes the caches anyway.
|
|
17
|
+
*/
|
|
18
|
+
export declare function renderDebScripts(settings: ShipSettings, prefix: string): Record<string, string>;
|
|
19
|
+
/**
|
|
20
|
+
* rpm scriptlets, for the `POSTIN` / `POSTUN` header tags.
|
|
21
|
+
*
|
|
22
|
+
* No action guard: rpm's `$1` is a COUNT, and both the install and the upgrade
|
|
23
|
+
* case want the refresh. `%postun` runs it unconditionally too — on an upgrade
|
|
24
|
+
* the incoming package's `%post` has already run, so a second refresh is wasted
|
|
25
|
+
* work rather than a wrong result, where guarding on `[ $1 -eq 0 ]` would be
|
|
26
|
+
* one more place to get the convention backwards.
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderRpmScriptlets(settings: ShipSettings, prefix: string): {
|
|
29
|
+
post?: string;
|
|
30
|
+
postun?: string;
|
|
31
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The post-install / post-remove work, and the two very different envelopes it
|
|
2
|
+
// travels in.
|
|
3
|
+
//
|
|
4
|
+
// The BODY is shared — refresh the desktop database, the icon cache and the
|
|
5
|
+
// compiled GSettings schemas, because those are system-wide caches our files
|
|
6
|
+
// changed. The GUARD is not, and assuming it was is the bug this file exists to
|
|
7
|
+
// prevent: dpkg passes `configure` / `remove` as `$1`, rpm passes the NUMBER of
|
|
8
|
+
// instances that will remain (1 on install, 0 on final erase). A dpkg-shaped
|
|
9
|
+
// `[ "$1" = "configure" ]` inside an rpm scriptlet is never true, so the
|
|
10
|
+
// package installs, the scriptlet runs, and nothing happens. Measured by
|
|
11
|
+
// reading `rpm -qp --scripts` on the first artifact this packer produced —
|
|
12
|
+
// which is exactly the class of defect a rendered-output assertion would have
|
|
13
|
+
// waved through.
|
|
14
|
+
/**
|
|
15
|
+
* The cache refreshes this payload makes necessary, each guarded by the tool's
|
|
16
|
+
* presence and `|| true`.
|
|
17
|
+
*
|
|
18
|
+
* The caches belong to the distribution, not to us: on removal the helper may
|
|
19
|
+
* already be gone, and a maintainer script that fails during a purge leaves a
|
|
20
|
+
* package the user cannot uninstall.
|
|
21
|
+
*/
|
|
22
|
+
export function cacheRefreshCommands(settings, prefix) {
|
|
23
|
+
const refreshes = [];
|
|
24
|
+
if (settings.kind === 'app') {
|
|
25
|
+
refreshes.push({
|
|
26
|
+
probe: 'update-desktop-database',
|
|
27
|
+
run: `update-desktop-database -q ${prefix}/share/applications`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (settings.iconFiles.length > 0) {
|
|
31
|
+
refreshes.push({
|
|
32
|
+
probe: 'gtk-update-icon-cache',
|
|
33
|
+
run: `gtk-update-icon-cache -q -t -f ${prefix}/share/icons/hicolor`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (settings.schemaFiles.length > 0) {
|
|
37
|
+
refreshes.push({
|
|
38
|
+
probe: 'glib-compile-schemas',
|
|
39
|
+
run: `glib-compile-schemas ${prefix}/share/glib-2.0/schemas`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return refreshes.map(({ probe, run }) => `if command -v ${probe} >/dev/null 2>&1; then ${run} || true; fi`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* dpkg maintainer scripts, keyed by their `control.tar` filename.
|
|
46
|
+
*
|
|
47
|
+
* `postrm` guards on `remove` alone: dpkg also calls it for `purge`, `upgrade`
|
|
48
|
+
* and several abort paths, and on an upgrade the incoming package's `postinst`
|
|
49
|
+
* refreshes the caches anyway.
|
|
50
|
+
*/
|
|
51
|
+
export function renderDebScripts(settings, prefix) {
|
|
52
|
+
const commands = cacheRefreshCommands(settings, prefix);
|
|
53
|
+
if (commands.length === 0)
|
|
54
|
+
return {};
|
|
55
|
+
const body = commands.map((line) => ` ${line}`).join('\n');
|
|
56
|
+
return {
|
|
57
|
+
postinst: ['#!/bin/sh', 'set -e', 'if [ "$1" = "configure" ]; then', body, 'fi', ''].join('\n'),
|
|
58
|
+
postrm: ['#!/bin/sh', 'set -e', 'if [ "$1" = "remove" ]; then', body, 'fi', ''].join('\n'),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* rpm scriptlets, for the `POSTIN` / `POSTUN` header tags.
|
|
63
|
+
*
|
|
64
|
+
* No action guard: rpm's `$1` is a COUNT, and both the install and the upgrade
|
|
65
|
+
* case want the refresh. `%postun` runs it unconditionally too — on an upgrade
|
|
66
|
+
* the incoming package's `%post` has already run, so a second refresh is wasted
|
|
67
|
+
* work rather than a wrong result, where guarding on `[ $1 -eq 0 ]` would be
|
|
68
|
+
* one more place to get the convention backwards.
|
|
69
|
+
*/
|
|
70
|
+
export function renderRpmScriptlets(settings, prefix) {
|
|
71
|
+
const commands = cacheRefreshCommands(settings, prefix);
|
|
72
|
+
if (commands.length === 0)
|
|
73
|
+
return {};
|
|
74
|
+
const body = `${commands.join('\n')}\n`;
|
|
75
|
+
return { post: body, postun: body };
|
|
76
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AppMetadata, ConfigDataFlatpak, ConfigDataShip, DescriptionBlock } from '../../types/config-data.js';
|
|
2
|
+
import type { ShipSettings } from './types.js';
|
|
3
|
+
/** The subset of `package.json` the resolver reads. */
|
|
4
|
+
export interface ShipPackageManifest {
|
|
5
|
+
name?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
license?: string;
|
|
9
|
+
homepage?: string;
|
|
10
|
+
author?: string | {
|
|
11
|
+
name?: string;
|
|
12
|
+
email?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
};
|
|
15
|
+
main?: string;
|
|
16
|
+
gjsify?: {
|
|
17
|
+
main?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Paths the caller has already found on disk. */
|
|
21
|
+
export interface DiscoveredPayload {
|
|
22
|
+
/** Absolute path to the built bundle. */
|
|
23
|
+
bundlePath: string;
|
|
24
|
+
/** Absolute path to the directory staged into `lib/<binaryName>/`. */
|
|
25
|
+
bundleDir: string;
|
|
26
|
+
/** Bundle-dir-relative paths, POSIX-separated. */
|
|
27
|
+
bundleFiles: string[];
|
|
28
|
+
iconFiles: string[];
|
|
29
|
+
schemaFiles: string[];
|
|
30
|
+
licenseFile?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface SettingsInput {
|
|
33
|
+
projectDir: string;
|
|
34
|
+
pkg: ShipPackageManifest;
|
|
35
|
+
ship: ConfigDataShip;
|
|
36
|
+
/** Metadata fallback — `gjsify.flatpak` already carries these fields for many projects. */
|
|
37
|
+
flatpak: ConfigDataFlatpak;
|
|
38
|
+
cli: {
|
|
39
|
+
outDir?: string;
|
|
40
|
+
arch?: string;
|
|
41
|
+
};
|
|
42
|
+
discovered: DiscoveredPayload;
|
|
43
|
+
}
|
|
44
|
+
export interface ResolvedSettings {
|
|
45
|
+
settings: ShipSettings;
|
|
46
|
+
/** The merged metadata the renderers read. */
|
|
47
|
+
metadata: AppMetadata;
|
|
48
|
+
warnings: string[];
|
|
49
|
+
}
|
|
50
|
+
export declare function resolveShipSettings(input: SettingsInput): ResolvedSettings;
|
|
51
|
+
/** The bundle path a project declares, in the order the rest of the CLI reads them. */
|
|
52
|
+
export declare function declaredBundlePath(pkg: ShipPackageManifest, ship: ConfigDataShip): string | undefined;
|
|
53
|
+
/** npm package name → a package/binary name both dpkg and rpm accept. */
|
|
54
|
+
export declare function deriveBinaryName(pkgName: string | undefined): string;
|
|
55
|
+
/** AppStream description (string or blocks) → plain paragraphs for deb/rpm. */
|
|
56
|
+
export declare function descriptionParagraphs(description: string | DescriptionBlock[] | undefined): string[] | undefined;
|
|
57
|
+
/** `LICENSE`, `LICENSE.md`, … — the names a licence file actually has. */
|
|
58
|
+
export declare const LICENSE_FILE_NAMES: readonly ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "COPYING.md"];
|
|
59
|
+
/** The bundle's own basename, used as the launcher's exec target. */
|
|
60
|
+
export declare function bundleEntryName(bundlePath: string): string;
|