@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,34 @@
|
|
|
1
|
+
import type { FormatDescriptor, ShipSettings, StagedFile } from './types.js';
|
|
2
|
+
/** Everything the planner needs that it cannot derive from the settings alone. */
|
|
3
|
+
export interface StageInputs {
|
|
4
|
+
/** Paths inside `settings.bundleDir`, relative and POSIX-separated. */
|
|
5
|
+
bundleFiles: readonly string[];
|
|
6
|
+
/** The rendered launcher script. */
|
|
7
|
+
launcher: string;
|
|
8
|
+
/** The rendered AppStream MetaInfo XML. */
|
|
9
|
+
metainfo: string;
|
|
10
|
+
/** The rendered desktop entry — required for `kind: 'app'`, unused for `'cli'`. */
|
|
11
|
+
desktopEntry?: string;
|
|
12
|
+
/** Contents of the project's licence file, when one was found. */
|
|
13
|
+
licenseText?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build the prefix-relative payload.
|
|
17
|
+
*
|
|
18
|
+
* The result is deduplicated by path (last write wins, so `extraFiles` can
|
|
19
|
+
* override anything the defaults staged) and sorted, which makes the tree —
|
|
20
|
+
* and therefore every artifact built from it — deterministic.
|
|
21
|
+
*/
|
|
22
|
+
export declare function planStage(settings: ShipSettings, inputs: StageInputs): StagedFile[];
|
|
23
|
+
/**
|
|
24
|
+
* The per-format additions. Only the licence differs today, and it differs in
|
|
25
|
+
* BOTH place and shape: Debian policy wants a machine-readable `copyright` in
|
|
26
|
+
* `share/doc/<pkg>/`, RPM wants the licence text under `share/licenses/<pkg>/`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function planOverlay(settings: ShipSettings, format: FormatDescriptor, inputs: StageInputs): StagedFile[];
|
|
29
|
+
/**
|
|
30
|
+
* The hicolor subdirectory for an icon: `scalable` for SVG, otherwise the
|
|
31
|
+
* pixel size read from a `<n>x<n>` path component or a trailing number in the
|
|
32
|
+
* filename.
|
|
33
|
+
*/
|
|
34
|
+
export declare function iconSizeDir(iconPath: string): string;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Which file lands where — decided without touching the filesystem, so every
|
|
2
|
+
// case (including the ones that must be REFUSED) is unit-testable. Same split
|
|
3
|
+
// as `utils/copy-targets.ts`, and for the same reason: a stager that discovers
|
|
4
|
+
// its own inputs can only be tested by building a real project.
|
|
5
|
+
import { basename, extname, posix } from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* Build the prefix-relative payload.
|
|
8
|
+
*
|
|
9
|
+
* The result is deduplicated by path (last write wins, so `extraFiles` can
|
|
10
|
+
* override anything the defaults staged) and sorted, which makes the tree —
|
|
11
|
+
* and therefore every artifact built from it — deterministic.
|
|
12
|
+
*/
|
|
13
|
+
export function planStage(settings, inputs) {
|
|
14
|
+
const files = [];
|
|
15
|
+
const libDir = `lib/${settings.binaryName}`;
|
|
16
|
+
files.push({ path: `bin/${settings.binaryName}`, mode: 0o755, source: { kind: 'text', text: inputs.launcher } });
|
|
17
|
+
for (const rel of inputs.bundleFiles) {
|
|
18
|
+
files.push({
|
|
19
|
+
path: posix.join(libDir, rel),
|
|
20
|
+
mode: isExecutableAsset(rel) ? 0o755 : 0o644,
|
|
21
|
+
source: { kind: 'file', path: posix.join(settings.bundleDir, rel) },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
files.push({
|
|
25
|
+
path: `share/metainfo/${settings.appId}.metainfo.xml`,
|
|
26
|
+
mode: 0o644,
|
|
27
|
+
source: { kind: 'text', text: inputs.metainfo },
|
|
28
|
+
});
|
|
29
|
+
if (settings.kind === 'app') {
|
|
30
|
+
if (inputs.desktopEntry === undefined) {
|
|
31
|
+
throw new Error('gjsify ship: internal error — a desktop entry is required for `kind: "app"`.');
|
|
32
|
+
}
|
|
33
|
+
files.push({
|
|
34
|
+
path: `share/applications/${settings.appId}.desktop`,
|
|
35
|
+
mode: 0o644,
|
|
36
|
+
source: { kind: 'text', text: inputs.desktopEntry },
|
|
37
|
+
});
|
|
38
|
+
for (const icon of planIcons(settings))
|
|
39
|
+
files.push(icon);
|
|
40
|
+
}
|
|
41
|
+
for (const schema of settings.schemaFiles) {
|
|
42
|
+
const name = basename(schema);
|
|
43
|
+
// Every installed schema shares ONE system directory
|
|
44
|
+
// (`/usr/share/glib-2.0/schemas`), so a generic name is a collision
|
|
45
|
+
// with whatever else the distro installed there — and the loser is
|
|
46
|
+
// decided by install order.
|
|
47
|
+
if (!name.startsWith(settings.appId)) {
|
|
48
|
+
throw new Error(`gjsify ship: the GSettings schema ${name} must be named \`${settings.appId}[.…].gschema.xml\`. ` +
|
|
49
|
+
'Every installed schema shares one system directory, so the app id is what keeps it unique.');
|
|
50
|
+
}
|
|
51
|
+
files.push({
|
|
52
|
+
path: `share/glib-2.0/schemas/${name}`,
|
|
53
|
+
mode: 0o644,
|
|
54
|
+
source: { kind: 'file', path: schema },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const [dest, source] of Object.entries(settings.extraFiles)) {
|
|
58
|
+
files.push({ path: assertInsidePrefix(dest), mode: 0o644, source: { kind: 'file', path: source } });
|
|
59
|
+
}
|
|
60
|
+
return normalise(files);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The per-format additions. Only the licence differs today, and it differs in
|
|
64
|
+
* BOTH place and shape: Debian policy wants a machine-readable `copyright` in
|
|
65
|
+
* `share/doc/<pkg>/`, RPM wants the licence text under `share/licenses/<pkg>/`.
|
|
66
|
+
*/
|
|
67
|
+
export function planOverlay(settings, format, inputs) {
|
|
68
|
+
if (inputs.licenseText === undefined)
|
|
69
|
+
return [];
|
|
70
|
+
const text = format.licenseKind === 'debian-copyright'
|
|
71
|
+
? renderDebianCopyright(settings, inputs.licenseText)
|
|
72
|
+
: inputs.licenseText;
|
|
73
|
+
return [{ path: format.licenseDest(settings.binaryName), mode: 0o644, source: { kind: 'text', text } }];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Debian machine-readable copyright, format 1.0. The licence text is indented
|
|
77
|
+
* by one space and its blank lines become ` .` — a bare blank line ends the
|
|
78
|
+
* field, which silently truncates the licence to its first paragraph.
|
|
79
|
+
*/
|
|
80
|
+
function renderDebianCopyright(settings, licenseText) {
|
|
81
|
+
const body = licenseText
|
|
82
|
+
.trimEnd()
|
|
83
|
+
.split('\n')
|
|
84
|
+
.map((line) => (line.trim() === '' ? ' .' : ` ${line}`))
|
|
85
|
+
.join('\n');
|
|
86
|
+
return [
|
|
87
|
+
'Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/',
|
|
88
|
+
`Upstream-Name: ${settings.name}`,
|
|
89
|
+
...(settings.homepage ? [`Source: ${settings.homepage}`] : []),
|
|
90
|
+
'',
|
|
91
|
+
'Files: *',
|
|
92
|
+
`Copyright: ${settings.maintainer}`,
|
|
93
|
+
`License: ${settings.license}`,
|
|
94
|
+
body,
|
|
95
|
+
'',
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Icons land in the hicolor theme, named after the app id — that name is what
|
|
100
|
+
* the desktop entry's `Icon=` and the MetaInfo `<id>` both point at, so a file
|
|
101
|
+
* keeping its source basename installs an icon nothing ever looks up.
|
|
102
|
+
*/
|
|
103
|
+
function planIcons(settings) {
|
|
104
|
+
const out = [];
|
|
105
|
+
const seen = new Map();
|
|
106
|
+
for (const icon of settings.iconFiles) {
|
|
107
|
+
const ext = extname(icon);
|
|
108
|
+
const dir = iconSizeDir(icon);
|
|
109
|
+
const path = `share/icons/hicolor/${dir}/apps/${settings.appId}${ext}`;
|
|
110
|
+
const previous = seen.get(path);
|
|
111
|
+
if (previous !== undefined) {
|
|
112
|
+
throw new Error(`gjsify ship: ${icon} and ${previous} both install as ${path}. ` +
|
|
113
|
+
'Icon size is read from the path (`.../128x128/...`) or the filename (`icon-128.png`); ' +
|
|
114
|
+
'give them distinguishable sizes or point `gjsify.ship.icon` at a single file.');
|
|
115
|
+
}
|
|
116
|
+
seen.set(path, icon);
|
|
117
|
+
out.push({ path, mode: 0o644, source: { kind: 'file', path: icon } });
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The hicolor subdirectory for an icon: `scalable` for SVG, otherwise the
|
|
123
|
+
* pixel size read from a `<n>x<n>` path component or a trailing number in the
|
|
124
|
+
* filename.
|
|
125
|
+
*/
|
|
126
|
+
export function iconSizeDir(iconPath) {
|
|
127
|
+
if (extname(iconPath).toLowerCase() === '.svg')
|
|
128
|
+
return 'scalable';
|
|
129
|
+
const square = /(?:^|[\\/])(\d{1,4})x\1(?:[\\/]|$)/.exec(iconPath);
|
|
130
|
+
if (square)
|
|
131
|
+
return `${square[1]}x${square[1]}`;
|
|
132
|
+
const tokens = basename(iconPath, extname(iconPath)).split(/[-_.]/);
|
|
133
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
134
|
+
const token = tokens[i];
|
|
135
|
+
if (token !== undefined && /^\d{1,4}$/.test(token))
|
|
136
|
+
return `${token}x${token}`;
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`gjsify ship: cannot tell what size ${iconPath} is. ` +
|
|
139
|
+
'Put it in a `<size>x<size>/` directory, end its name with the size (`icon-128.png`), or ship an SVG.');
|
|
140
|
+
}
|
|
141
|
+
/** A staged path must stay under the prefix — no absolute paths, no `..`. */
|
|
142
|
+
function assertInsidePrefix(dest) {
|
|
143
|
+
const normalised = posix.normalize(dest);
|
|
144
|
+
if (posix.isAbsolute(normalised) || normalised === '..' || normalised.startsWith('../')) {
|
|
145
|
+
throw new Error(`gjsify ship: \`gjsify.ship.extraFiles\` destination "${dest}" escapes the install prefix. ` +
|
|
146
|
+
'Destinations are relative to the prefix, e.g. `share/applications/extra.desktop`.');
|
|
147
|
+
}
|
|
148
|
+
return normalised;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A native module inside the payload has to keep its executable bit.
|
|
152
|
+
*
|
|
153
|
+
* By NAME here, unlike `isArchIndependent`, which reads the file's magic: the
|
|
154
|
+
* planner is pure and has no bytes to look at. The two answer different
|
|
155
|
+
* questions — "should this be 0755" versus "is this package portable" — and
|
|
156
|
+
* getting this one wrong costs a mode bit, not an unusable package.
|
|
157
|
+
*/
|
|
158
|
+
function isExecutableAsset(rel) {
|
|
159
|
+
return /\.(so|node|dylib)(\.\d+)*$/.test(rel);
|
|
160
|
+
}
|
|
161
|
+
/** Last write wins, then sort — deterministic output for a deterministic artifact. */
|
|
162
|
+
function normalise(files) {
|
|
163
|
+
const byPath = new Map();
|
|
164
|
+
for (const file of files)
|
|
165
|
+
byPath.set(file.path, file);
|
|
166
|
+
return [...byPath.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
167
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const RPM_TAG_HEADERSIGNATURES = 62;
|
|
2
|
+
export declare const RPM_TAG_HEADERIMMUTABLE = 63;
|
|
3
|
+
export declare enum RpmType {
|
|
4
|
+
INT16 = 3,
|
|
5
|
+
INT32 = 4,
|
|
6
|
+
STRING = 6,
|
|
7
|
+
BIN = 7,
|
|
8
|
+
STRING_ARRAY = 8,
|
|
9
|
+
I18NSTRING = 9
|
|
10
|
+
}
|
|
11
|
+
export type RpmValue = string | string[] | number | number[] | Uint8Array;
|
|
12
|
+
export interface RpmEntry {
|
|
13
|
+
tag: number;
|
|
14
|
+
type: RpmType;
|
|
15
|
+
value: RpmValue;
|
|
16
|
+
}
|
|
17
|
+
/** Build a complete header blob, magic included. */
|
|
18
|
+
export declare function buildRpmHeader(entries: readonly RpmEntry[], regionTag: number): Uint8Array;
|
|
19
|
+
/**
|
|
20
|
+
* The 96-byte lead.
|
|
21
|
+
*
|
|
22
|
+
* Legacy: modern rpm validates the magic and the major version and takes
|
|
23
|
+
* everything else from the headers, which is why the name field is allowed to
|
|
24
|
+
* be truncated. It is written because the magic is how `file(1)`, and rpm's
|
|
25
|
+
* own first read, recognise the format at all.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildRpmLead(nevr: string): Uint8Array;
|
|
28
|
+
/** Pad a signature header to the 8-byte boundary the main header starts on. */
|
|
29
|
+
export declare function padToEight(bytes: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// The RPM header structure — an index of typed entries over a shared store.
|
|
2
|
+
//
|
|
3
|
+
// Both headers in an RPM (the signature and the main one) are this same
|
|
4
|
+
// structure, which is why it is its own module: `rpm.ts` decides WHICH tags to
|
|
5
|
+
// write, this file decides how a header is spelled.
|
|
6
|
+
//
|
|
7
|
+
// The one genuinely surprising part is the REGION. `rpm` does not read the
|
|
8
|
+
// index as a flat list: the first index entry is a region trailer pointer with
|
|
9
|
+
// tag 62 (signatures) or 63 (immutable), whose store payload is a second,
|
|
10
|
+
// 16-byte index entry carrying a NEGATIVE offset — the byte distance back to
|
|
11
|
+
// the start of the index. Getting that offset wrong does not corrupt anything
|
|
12
|
+
// visibly; `rpm -qp` simply reports "region trailer" errors or silently sees a
|
|
13
|
+
// header with no tags in it.
|
|
14
|
+
const HEADER_MAGIC = Uint8Array.from([0x8e, 0xad, 0xe8, 0x01, 0x00, 0x00, 0x00, 0x00]);
|
|
15
|
+
const INDEX_ENTRY_SIZE = 16;
|
|
16
|
+
/** The region trailer is one index entry, and rpm checks the count literally. */
|
|
17
|
+
const REGION_COUNT = 16;
|
|
18
|
+
export const RPM_TAG_HEADERSIGNATURES = 62;
|
|
19
|
+
export const RPM_TAG_HEADERIMMUTABLE = 63;
|
|
20
|
+
export var RpmType;
|
|
21
|
+
(function (RpmType) {
|
|
22
|
+
RpmType[RpmType["INT16"] = 3] = "INT16";
|
|
23
|
+
RpmType[RpmType["INT32"] = 4] = "INT32";
|
|
24
|
+
RpmType[RpmType["STRING"] = 6] = "STRING";
|
|
25
|
+
RpmType[RpmType["BIN"] = 7] = "BIN";
|
|
26
|
+
RpmType[RpmType["STRING_ARRAY"] = 8] = "STRING_ARRAY";
|
|
27
|
+
RpmType[RpmType["I18NSTRING"] = 9] = "I18NSTRING";
|
|
28
|
+
})(RpmType || (RpmType = {}));
|
|
29
|
+
/** Store alignment per type — rpm reads INT32s with an aligned load. */
|
|
30
|
+
const ALIGNMENT = {
|
|
31
|
+
[RpmType.INT16]: 2,
|
|
32
|
+
[RpmType.INT32]: 4,
|
|
33
|
+
[RpmType.STRING]: 1,
|
|
34
|
+
[RpmType.BIN]: 1,
|
|
35
|
+
[RpmType.STRING_ARRAY]: 1,
|
|
36
|
+
[RpmType.I18NSTRING]: 1,
|
|
37
|
+
};
|
|
38
|
+
/** Build a complete header blob, magic included. */
|
|
39
|
+
export function buildRpmHeader(entries, regionTag) {
|
|
40
|
+
// rpm expects the index sorted by tag; the store is laid out in the same
|
|
41
|
+
// order, which is not required but makes a hexdump readable next to a real
|
|
42
|
+
// package's.
|
|
43
|
+
const sorted = [...entries].sort((a, b) => a.tag - b.tag);
|
|
44
|
+
const index = [];
|
|
45
|
+
const chunks = [];
|
|
46
|
+
let storeSize = 0;
|
|
47
|
+
for (const entry of sorted) {
|
|
48
|
+
if (entry.tag < 100) {
|
|
49
|
+
throw new Error(`gjsify ship: rpm tag ${entry.tag} is below 100 and would be rejected by hdrchkTag.`);
|
|
50
|
+
}
|
|
51
|
+
const { data, count } = encodeValue(entry.type, entry.value, entry.tag);
|
|
52
|
+
const alignment = ALIGNMENT[entry.type];
|
|
53
|
+
const padding = (alignment - (storeSize % alignment)) % alignment;
|
|
54
|
+
if (padding > 0) {
|
|
55
|
+
chunks.push(new Uint8Array(padding));
|
|
56
|
+
storeSize += padding;
|
|
57
|
+
}
|
|
58
|
+
index.push({ tag: entry.tag, type: entry.type, offset: storeSize, count });
|
|
59
|
+
chunks.push(data);
|
|
60
|
+
storeSize += data.byteLength;
|
|
61
|
+
}
|
|
62
|
+
const trailerOffset = storeSize;
|
|
63
|
+
const indexCount = index.length + 1; // the region entry counts itself
|
|
64
|
+
chunks.push(indexEntry(regionTag, RpmType.BIN, -(INDEX_ENTRY_SIZE * indexCount), REGION_COUNT));
|
|
65
|
+
storeSize += INDEX_ENTRY_SIZE;
|
|
66
|
+
const header = new Uint8Array(HEADER_MAGIC.byteLength + 8 + indexCount * INDEX_ENTRY_SIZE + storeSize);
|
|
67
|
+
const view = new DataView(header.buffer);
|
|
68
|
+
header.set(HEADER_MAGIC, 0);
|
|
69
|
+
view.setUint32(8, indexCount);
|
|
70
|
+
view.setUint32(12, storeSize);
|
|
71
|
+
let offset = 16;
|
|
72
|
+
header.set(indexEntry(regionTag, RpmType.BIN, trailerOffset, REGION_COUNT), offset);
|
|
73
|
+
offset += INDEX_ENTRY_SIZE;
|
|
74
|
+
for (const item of index) {
|
|
75
|
+
header.set(indexEntry(item.tag, item.type, item.offset, item.count), offset);
|
|
76
|
+
offset += INDEX_ENTRY_SIZE;
|
|
77
|
+
}
|
|
78
|
+
for (const chunk of chunks) {
|
|
79
|
+
header.set(chunk, offset);
|
|
80
|
+
offset += chunk.byteLength;
|
|
81
|
+
}
|
|
82
|
+
return header;
|
|
83
|
+
}
|
|
84
|
+
function indexEntry(tag, type, offset, count) {
|
|
85
|
+
const bytes = new Uint8Array(INDEX_ENTRY_SIZE);
|
|
86
|
+
const view = new DataView(bytes.buffer);
|
|
87
|
+
view.setInt32(0, tag);
|
|
88
|
+
view.setInt32(4, type);
|
|
89
|
+
view.setInt32(8, offset);
|
|
90
|
+
view.setInt32(12, count);
|
|
91
|
+
return bytes;
|
|
92
|
+
}
|
|
93
|
+
function encodeValue(type, value, tag) {
|
|
94
|
+
const encoder = new TextEncoder();
|
|
95
|
+
switch (type) {
|
|
96
|
+
case RpmType.STRING: {
|
|
97
|
+
if (typeof value !== 'string')
|
|
98
|
+
throw typeMismatch(tag, 'a string');
|
|
99
|
+
return { data: encoder.encode(`${value}\0`), count: 1 };
|
|
100
|
+
}
|
|
101
|
+
case RpmType.STRING_ARRAY:
|
|
102
|
+
case RpmType.I18NSTRING: {
|
|
103
|
+
const list = typeof value === 'string' ? [value] : value;
|
|
104
|
+
if (!Array.isArray(list))
|
|
105
|
+
throw typeMismatch(tag, 'a string array');
|
|
106
|
+
// rpm rejects a zero count, so an empty array is a bug rather than
|
|
107
|
+
// "no value" — the caller must omit the tag instead.
|
|
108
|
+
if (list.length === 0)
|
|
109
|
+
throw new Error(`gjsify ship: rpm tag ${tag} has an empty array; omit it instead.`);
|
|
110
|
+
return { data: encoder.encode(list.map((item) => `${item}\0`).join('')), count: list.length };
|
|
111
|
+
}
|
|
112
|
+
case RpmType.BIN: {
|
|
113
|
+
if (!(value instanceof Uint8Array))
|
|
114
|
+
throw typeMismatch(tag, 'binary data');
|
|
115
|
+
return { data: value, count: value.byteLength };
|
|
116
|
+
}
|
|
117
|
+
case RpmType.INT16:
|
|
118
|
+
case RpmType.INT32: {
|
|
119
|
+
const list = typeof value === 'number' ? [value] : value;
|
|
120
|
+
if (!Array.isArray(list))
|
|
121
|
+
throw typeMismatch(tag, 'a number array');
|
|
122
|
+
if (list.length === 0)
|
|
123
|
+
throw new Error(`gjsify ship: rpm tag ${tag} has an empty array; omit it instead.`);
|
|
124
|
+
const width = type === RpmType.INT16 ? 2 : 4;
|
|
125
|
+
const data = new Uint8Array(list.length * width);
|
|
126
|
+
const view = new DataView(data.buffer);
|
|
127
|
+
list.forEach((item, i) => {
|
|
128
|
+
if (type === RpmType.INT16)
|
|
129
|
+
view.setUint16(i * width, item & 0xffff);
|
|
130
|
+
else
|
|
131
|
+
view.setUint32(i * width, item >>> 0);
|
|
132
|
+
});
|
|
133
|
+
return { data, count: list.length };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function typeMismatch(tag, expected) {
|
|
138
|
+
return new Error(`gjsify ship: internal error — rpm tag ${tag} expects ${expected}.`);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The 96-byte lead.
|
|
142
|
+
*
|
|
143
|
+
* Legacy: modern rpm validates the magic and the major version and takes
|
|
144
|
+
* everything else from the headers, which is why the name field is allowed to
|
|
145
|
+
* be truncated. It is written because the magic is how `file(1)`, and rpm's
|
|
146
|
+
* own first read, recognise the format at all.
|
|
147
|
+
*/
|
|
148
|
+
export function buildRpmLead(nevr) {
|
|
149
|
+
const lead = new Uint8Array(96);
|
|
150
|
+
const view = new DataView(lead.buffer);
|
|
151
|
+
lead.set([0xed, 0xab, 0xee, 0xdb], 0);
|
|
152
|
+
lead[4] = 3; // major
|
|
153
|
+
lead[5] = 0; // minor
|
|
154
|
+
view.setInt16(6, 0); // type: binary package
|
|
155
|
+
view.setInt16(8, 0); // archnum — informational; the header's ARCH is authoritative
|
|
156
|
+
const name = new TextEncoder().encode(nevr).slice(0, 65);
|
|
157
|
+
lead.set(name, 10);
|
|
158
|
+
view.setInt16(76, 0); // osnum — same story as archnum
|
|
159
|
+
view.setInt16(78, 5); // signature type: header-style signatures
|
|
160
|
+
return lead;
|
|
161
|
+
}
|
|
162
|
+
/** Pad a signature header to the 8-byte boundary the main header starts on. */
|
|
163
|
+
export function padToEight(bytes) {
|
|
164
|
+
const padding = (8 - (bytes.byteLength % 8)) % 8;
|
|
165
|
+
if (padding === 0)
|
|
166
|
+
return bytes;
|
|
167
|
+
const out = new Uint8Array(bytes.byteLength + padding);
|
|
168
|
+
out.set(bytes, 0);
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PayloadEntry } from './payload.js';
|
|
2
|
+
import type { ShipSettings } from './types.js';
|
|
3
|
+
export interface RpmInputs {
|
|
4
|
+
settings: ShipSettings;
|
|
5
|
+
payload: readonly PayloadEntry[];
|
|
6
|
+
prefix: string;
|
|
7
|
+
depends: readonly string[];
|
|
8
|
+
/** RPM architecture (`x86_64`, `noarch`, …). */
|
|
9
|
+
archLabel: string;
|
|
10
|
+
mtime: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildRpm(inputs: RpmInputs): Promise<Uint8Array>;
|