@gjsify/cli 0.43.0 → 0.45.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 +453 -425
- package/lib/actions/build.js +2 -1
- package/lib/commands/affected-classify.js +7 -5
- package/lib/commands/build.js +6 -0
- package/lib/commands/flatpak/ci.js +10 -2
- package/lib/commands/flatpak/init.js +15 -16
- package/lib/commands/flatpak/utils.d.ts +0 -26
- package/lib/commands/flatpak/utils.js +4 -29
- package/lib/commands/gettext.d.ts +2 -1
- package/lib/commands/gettext.js +105 -47
- package/lib/commands/onboard.d.ts +7 -0
- package/lib/commands/onboard.js +349 -96
- package/lib/commands/ship.d.ts +3 -0
- package/lib/commands/ship.js +911 -70
- package/lib/commands/trust.d.ts +6 -0
- package/lib/commands/trust.js +1 -1
- package/lib/config.js +25 -0
- package/lib/types/cli-build-options.d.ts +11 -0
- package/lib/types/config-data.d.ts +137 -3
- package/lib/utils/flatpak-runtime.d.ts +41 -0
- package/lib/utils/flatpak-runtime.js +54 -0
- package/lib/utils/msgfmt-merge.d.ts +35 -0
- package/lib/utils/msgfmt-merge.js +102 -0
- package/lib/utils/npm-otp.d.ts +27 -2
- package/lib/utils/npm-otp.js +82 -7
- package/lib/utils/onboard-discovery.d.ts +68 -0
- package/lib/utils/onboard-discovery.js +178 -0
- package/lib/utils/onboard-probe.d.ts +118 -1
- package/lib/utils/onboard-probe.js +153 -4
- package/lib/utils/prompt-output.d.ts +12 -0
- package/lib/utils/prompt-output.js +54 -0
- package/lib/utils/prompt.js +22 -2
- package/lib/utils/publishable-packages.d.ts +1 -1
- package/lib/utils/publishable-packages.js +1 -0
- package/lib/utils/resolve-npm-package.d.ts +56 -0
- package/lib/utils/resolve-npm-package.js +69 -0
- package/lib/utils/ship/app-runtime.d.ts +214 -0
- package/lib/utils/ship/app-runtime.js +429 -0
- package/lib/utils/ship/depends.d.ts +54 -7
- package/lib/utils/ship/depends.js +120 -6
- package/lib/utils/ship/discover.js +18 -1
- package/lib/utils/ship/dmg.d.ts +71 -0
- package/lib/utils/ship/dmg.js +181 -0
- package/lib/utils/ship/flatpak-config.d.ts +66 -0
- package/lib/utils/ship/flatpak-config.js +132 -0
- package/lib/utils/ship/flatpak.d.ts +45 -0
- package/lib/utils/ship/flatpak.js +148 -0
- package/lib/utils/ship/formats.d.ts +116 -3
- package/lib/utils/ship/formats.js +869 -11
- package/lib/utils/ship/launcher.d.ts +13 -3
- package/lib/utils/ship/launcher.js +362 -17
- package/lib/utils/ship/layout.d.ts +268 -0
- package/lib/utils/ship/layout.js +351 -0
- package/lib/utils/ship/localize-metadata.d.ts +22 -0
- package/lib/utils/ship/localize-metadata.js +126 -0
- package/lib/utils/ship/mime.js +1 -9
- package/lib/utils/ship/msi.d.ts +126 -0
- package/lib/utils/ship/msi.js +417 -0
- package/lib/utils/ship/node-runtime.d.ts +71 -0
- package/lib/utils/ship/node-runtime.js +122 -0
- package/lib/utils/ship/payload.d.ts +156 -5
- package/lib/utils/ship/payload.js +580 -14
- package/lib/utils/ship/plan.d.ts +21 -0
- package/lib/utils/ship/plan.js +20 -7
- package/lib/utils/ship/plist.d.ts +58 -0
- package/lib/utils/ship/plist.js +198 -0
- package/lib/utils/ship/rpm.js +9 -3
- package/lib/utils/ship/schemas.d.ts +61 -0
- package/lib/utils/ship/schemas.js +118 -0
- package/lib/utils/ship/scripts.js +7 -3
- package/lib/utils/ship/settings.d.ts +19 -1
- package/lib/utils/ship/settings.js +19 -1
- package/lib/utils/ship/share-dirs.d.ts +23 -0
- package/lib/utils/ship/share-dirs.js +46 -0
- package/lib/utils/ship/signing.d.ts +195 -0
- package/lib/utils/ship/signing.js +317 -0
- package/lib/utils/ship/stage-manifest.d.ts +50 -13
- package/lib/utils/ship/stage-manifest.js +137 -14
- package/lib/utils/ship/stage-writer.d.ts +48 -0
- package/lib/utils/ship/stage-writer.js +71 -1
- package/lib/utils/ship/types.d.ts +264 -16
- package/lib/utils/ship/xml.d.ts +7 -0
- package/lib/utils/ship/xml.js +25 -0
- package/lib/utils/ship/zip.d.ts +69 -0
- package/lib/utils/ship/zip.js +245 -0
- package/lib/utils/trust-registry.js +5 -0
- package/package.json +19 -19
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Folding the staged gettext catalogues into the generated freedesktop metadata.
|
|
2
|
+
//
|
|
3
|
+
// `gjsify ship` already discovered, validated and staged the compiled `.mo`
|
|
4
|
+
// catalogues, and `app-metadata.ts` already rendered a `.desktop` entry and an
|
|
5
|
+
// AppStream component. The two never met: no `Name[xx]=` and no `xml:lang=` was
|
|
6
|
+
// emitted anywhere, so a fully translated app still showed an English name in
|
|
7
|
+
// the GNOME app menu and in Software. Nobody reported it because both files
|
|
8
|
+
// stay VALID without the translations — `desktop-file-validate` and
|
|
9
|
+
// `appstreamcli validate` pass an untranslated file at exit 0, which is why the
|
|
10
|
+
// tests for this assert the localised keys are PRESENT rather than trusting a
|
|
11
|
+
// validator to notice they are gone.
|
|
12
|
+
//
|
|
13
|
+
// The msgfmt constraints this rests on live in `../msgfmt-merge.ts`, which runs
|
|
14
|
+
// the chain for `gjsify gettext` too. Two more are local to here, both from what
|
|
15
|
+
// a STAGED locale tree may hold that a `po/` directory cannot:
|
|
16
|
+
//
|
|
17
|
+
// * The catalogues reach us as `.mo` and msgfmt reads `.po` — handing it a
|
|
18
|
+
// `.mo` gives `de.mo:1:2: syntax error`, exit 1. `msgunfmt` converts back,
|
|
19
|
+
// and the round trip is exact for the msgid/msgstr pairs these two formats
|
|
20
|
+
// consume.
|
|
21
|
+
// * One language may arrive as SEVERAL catalogues, because the tree is keyed by
|
|
22
|
+
// `<lang>/LC_MESSAGES/<domain>.mo` and a package may ship more than one text
|
|
23
|
+
// domain. `msgcat --use-first` folds those into one before the chain sees
|
|
24
|
+
// them — the alternative is `msgfmt-merge.ts` constraint 5, an invalid file
|
|
25
|
+
// out of nothing but exit-0 calls.
|
|
26
|
+
//
|
|
27
|
+
// Both tools are in the same `gettext` package as `msgfmt`, so neither adds a
|
|
28
|
+
// dependency the fold did not already have.
|
|
29
|
+
import { execFileSync } from 'node:child_process';
|
|
30
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { tmpdir } from 'node:os';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import { mergeCatalogues } from '../msgfmt-merge.js';
|
|
34
|
+
/**
|
|
35
|
+
* The locale a staged catalogue path declares.
|
|
36
|
+
*
|
|
37
|
+
* Read rather than re-validated: `discoverLocales` has already refused anything
|
|
38
|
+
* that is not `<lang>/LC_MESSAGES/<domain>.mo`, and a second validator here would
|
|
39
|
+
* be a second definition of the layout.
|
|
40
|
+
*/
|
|
41
|
+
function localeOf(rel) {
|
|
42
|
+
return rel.split('/')[0] ?? '';
|
|
43
|
+
}
|
|
44
|
+
/** Chain one metadata file through every catalogue, and read the result back. */
|
|
45
|
+
function fold(mode, source, extension, catalogues, workDir) {
|
|
46
|
+
// The source is a STRING, so it has to become a file before msgfmt can be its
|
|
47
|
+
// template — and that file needs `extension` for the same reason the
|
|
48
|
+
// intermediates do (`msgfmt-merge.ts`, constraint 4).
|
|
49
|
+
const template = join(workDir, `source${extension}`);
|
|
50
|
+
writeFileSync(template, source);
|
|
51
|
+
return readFileSync(mergeCatalogues({ mode, template, extension, catalogues, workDir }), 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Translate the generated metadata with the catalogues the package ships.
|
|
55
|
+
*
|
|
56
|
+
* Returns the input unchanged when there is nothing to fold in, so a project
|
|
57
|
+
* without a `localeDir` never needs the gettext tools. A project WITH catalogues
|
|
58
|
+
* that lacks them is REFUSED rather than shipped in English: an untranslated
|
|
59
|
+
* `.desktop` is indistinguishable from a correct one at install time, and the
|
|
60
|
+
* package would have promised translations it does not deliver.
|
|
61
|
+
*/
|
|
62
|
+
export function localizeMetadata(metadata, localeFiles) {
|
|
63
|
+
if (localeFiles.length === 0)
|
|
64
|
+
return metadata;
|
|
65
|
+
const workDir = mkdtempSync(join(tmpdir(), 'gjsify-ship-l10n-'));
|
|
66
|
+
try {
|
|
67
|
+
// Sorted so the emitted `Name[xx]=` order — and therefore the artifact's
|
|
68
|
+
// bytes — does not depend on directory-read order. `listFilesRecursive`
|
|
69
|
+
// already sorts; relying on that from here would put the determinism of this
|
|
70
|
+
// file's output in another module's keeping. The sort ALSO fixes which
|
|
71
|
+
// domain wins below, so `--use-first` is a choice and not a coin toss.
|
|
72
|
+
const sorted = [...localeFiles].sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
|
|
73
|
+
// Grouped by LOCALE, not one entry per file. `discoverLocales` accepts every
|
|
74
|
+
// `<lang>/LC_MESSAGES/<domain>.mo`, and a package that ships two text domains
|
|
75
|
+
// (its own plus a bundled library's) therefore hands us two catalogues for one
|
|
76
|
+
// language. Chained as-is that is `msgfmt-merge.ts` constraint 5: two exit-0
|
|
77
|
+
// calls and a file both validators reject — `multiple keys named "Name[de]"`
|
|
78
|
+
// (exit 1) and `tag-duplicated name (lang=de)` (exit 3). Measured.
|
|
79
|
+
const byLocale = new Map();
|
|
80
|
+
for (const [index, file] of sorted.entries()) {
|
|
81
|
+
const po = join(workDir, `${index}.po`);
|
|
82
|
+
execFileSync('msgunfmt', [file.abs, '--output-file', po], { stdio: 'pipe' });
|
|
83
|
+
const locale = localeOf(file.rel);
|
|
84
|
+
byLocale.set(locale, [...(byLocale.get(locale) ?? []), po]);
|
|
85
|
+
}
|
|
86
|
+
const catalogues = [...byLocale].map(([locale, files]) => ({
|
|
87
|
+
locale,
|
|
88
|
+
// `--use-first` rather than a plain concatenation: two domains translating
|
|
89
|
+
// the same source string is not an error a packager can act on, and the
|
|
90
|
+
// alternatives are both worse. msgfmt given both files at once refuses
|
|
91
|
+
// outright (`duplicate message definition`, 2 fatal errors, exit 1), and a
|
|
92
|
+
// plain `msgcat` writes `#-#-#-#-#` conflict markers straight into the
|
|
93
|
+
// `Name=` a user reads. First in the sort above wins, deterministically.
|
|
94
|
+
po: files.length === 1 ? files[0] : msgcat(files, join(workDir, `${locale}.merged.po`)),
|
|
95
|
+
}));
|
|
96
|
+
return {
|
|
97
|
+
metainfo: fold('--xml', metadata.metainfo, '.metainfo.xml', catalogues, workDir),
|
|
98
|
+
desktopEntry: metadata.desktopEntry === undefined
|
|
99
|
+
? undefined
|
|
100
|
+
: fold('--desktop', metadata.desktopEntry, '.desktop', catalogues, workDir),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw new Error(describeFailure(error, localeFiles.length));
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Fold several `.po` for ONE language into one, first translation wins. Returns its path. */
|
|
111
|
+
function msgcat(files, out) {
|
|
112
|
+
execFileSync('msgcat', ['--use-first', '--output-file', out, ...files], { stdio: 'pipe' });
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** One actionable line for a failed merge, naming the tool the host is missing. */
|
|
116
|
+
function describeFailure(error, count) {
|
|
117
|
+
const e = error;
|
|
118
|
+
if (e?.code === 'ENOENT') {
|
|
119
|
+
return ('gjsify ship: `msgfmt`/`msgunfmt`/`msgcat` are not on PATH, but the package stages ' +
|
|
120
|
+
`${count} gettext catalogue(s). Install them (package: gettext) — shipping the metadata ` +
|
|
121
|
+
'untranslated would install an English app menu entry for a translated app, which nothing ' +
|
|
122
|
+
'downstream can detect.');
|
|
123
|
+
}
|
|
124
|
+
const stderr = e?.stderr === undefined ? '' : `\n${String(e.stderr).trim()}`;
|
|
125
|
+
return `gjsify ship: could not fold the gettext catalogues into the freedesktop metadata.${stderr}`;
|
|
126
|
+
}
|
package/lib/utils/ship/mime.js
CHANGED
|
@@ -9,15 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// So a project that invents a type also ships a shared-mime-info document, and the package refreshes
|
|
11
11
|
// the MIME cache on install (see `scripts.ts`) — an unrefreshed cache is the same silence again.
|
|
12
|
-
|
|
13
|
-
function esc(value) {
|
|
14
|
-
return value
|
|
15
|
-
.replace(/&/g, '&')
|
|
16
|
-
.replace(/</g, '<')
|
|
17
|
-
.replace(/>/g, '>')
|
|
18
|
-
.replace(/"/g, '"')
|
|
19
|
-
.replace(/'/g, ''');
|
|
20
|
-
}
|
|
12
|
+
import { xmlEscape as esc } from './xml.js';
|
|
21
13
|
const TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/;
|
|
22
14
|
/**
|
|
23
15
|
* Validate the declared types, throwing on anything that would install and then not work.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { HostOs, PackSettings } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The UUID namespace every GUID in a generated `.msi` is derived under.
|
|
4
|
+
*
|
|
5
|
+
* A CONSTANT, and changing it is a breaking change rather than a cosmetic one:
|
|
6
|
+
* the `UpgradeCode` derived from it is how Windows Installer recognises an
|
|
7
|
+
* already-installed copy of the same product, so a new namespace makes every
|
|
8
|
+
* future release a SECOND product that installs beside the old one instead of
|
|
9
|
+
* over it. Written down here rather than passed in for that reason — a knob would
|
|
10
|
+
* be a knob that silently forks a product line.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MSI_UUID_NAMESPACE = "7b3a1f6e-0c4d-4a2b-9e51-3d8c6f2a0b14";
|
|
13
|
+
/** RFC 4122 § 4.3 name-based UUID, SHA-1 flavour. */
|
|
14
|
+
export declare function uuid5(namespace: string, name: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* The code that identifies the PRODUCT LINE, stable across every version.
|
|
17
|
+
*
|
|
18
|
+
* Derived from the app id alone. That is what `MajorUpgrade` compares, so it must
|
|
19
|
+
* NOT move when the version does — an `UpgradeCode` that changed per release would
|
|
20
|
+
* leave every old version installed beside the new one, silently, with two entries
|
|
21
|
+
* in Add/Remove Programs and two copies on disk.
|
|
22
|
+
*/
|
|
23
|
+
export declare function msiUpgradeCode(settings: PackSettings): string;
|
|
24
|
+
/**
|
|
25
|
+
* The code that identifies THIS build, and it has to move when the version does.
|
|
26
|
+
*
|
|
27
|
+
* The exact inverse of {@link msiUpgradeCode}: Windows Installer refuses to
|
|
28
|
+
* install a second package carrying a `ProductCode` it already has, so a code that
|
|
29
|
+
* did not change per version would make an upgrade a no-op. The arch is in the
|
|
30
|
+
* material because an x64 and a (future) arm64 build of one version are two
|
|
31
|
+
* products, not one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function msiProductCode(settings: PackSettings, archLabel: string): string;
|
|
34
|
+
/** A component's GUID, stable for as long as its path is. */
|
|
35
|
+
export declare function msiComponentGuid(settings: PackSettings, path: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* MSI's `ProductVersion`, which is a much smaller vocabulary than npm's.
|
|
38
|
+
*
|
|
39
|
+
* `major.minor.build`, with `major`/`minor` at most 255 and `build` at most 65535 —
|
|
40
|
+
* anything outside that is not a version Windows Installer will compare, and the
|
|
41
|
+
* fourth field it accepts is IGNORED by upgrade detection, which is why nothing is
|
|
42
|
+
* ever put there.
|
|
43
|
+
*
|
|
44
|
+
* A PRERELEASE IS REFUSED rather than truncated, and that is the interesting case.
|
|
45
|
+
* `normaliseVersion` turns `1.2.0-rc.1` into `1.2.0~rc.1` because dpkg and rpm sort
|
|
46
|
+
* a `~` before the release; MSI has no such spelling, so the only two options were
|
|
47
|
+
* to drop the suffix or to say no. Dropping it makes `1.2.0~rc.1` and `1.2.0` the
|
|
48
|
+
* same `ProductVersion` — two products `MajorUpgrade` cannot tell apart, so
|
|
49
|
+
* installing the release over the candidate leaves BOTH on the machine. That is
|
|
50
|
+
* the "installs cleanly, does the wrong thing" class, produced by the field meant
|
|
51
|
+
* to prevent it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function msiProductVersion(version: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* An MSI `Identifier`: `[A-Za-z_][A-Za-z0-9_.]{0,71}`.
|
|
56
|
+
*
|
|
57
|
+
* The readable tail is not decoration — `msiinfo export <msi> File` is the oracle,
|
|
58
|
+
* and an id of pure hash makes a mismatch unreadable. The digest is what makes it
|
|
59
|
+
* UNIQUE: it is taken over the full path, so two files whose last 57 characters
|
|
60
|
+
* agree still differ. `renderWxs` asserts uniqueness anyway rather than trusting
|
|
61
|
+
* 48 bits, because the failure of a collision is two files fighting over one row.
|
|
62
|
+
*/
|
|
63
|
+
export declare function msiIdentifier(prefix: string, path: string): string;
|
|
64
|
+
/** One payload file, as the installer will address it. */
|
|
65
|
+
export interface MsiFile {
|
|
66
|
+
/** Program-directory-relative path, POSIX-separated. */
|
|
67
|
+
path: string;
|
|
68
|
+
/**
|
|
69
|
+
* Where the bytes are, RELATIVE to the directory holding the `.wxs` and
|
|
70
|
+
* POSIX-separated — see measurement 2 in this file's header. An absolute path
|
|
71
|
+
* would make the document unusable on the second machine that compiles it.
|
|
72
|
+
*/
|
|
73
|
+
source: string;
|
|
74
|
+
}
|
|
75
|
+
/** Where {@link buildMsi} writes the payload, and therefore what `Source=` is relative to. */
|
|
76
|
+
export declare const MSI_PAYLOAD_DIR = "root";
|
|
77
|
+
export interface WxsInput {
|
|
78
|
+
settings: PackSettings;
|
|
79
|
+
/** Everything the program directory carries, in any order. */
|
|
80
|
+
files: readonly MsiFile[];
|
|
81
|
+
/** The directory the installer lays down — `windowsProgramDirName(settings)`. */
|
|
82
|
+
programDirName: string;
|
|
83
|
+
/** The format's spelling of the architecture, e.g. `x64`. */
|
|
84
|
+
archLabel: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Render the installer's source document.
|
|
88
|
+
*
|
|
89
|
+
* PURE — a string in, a string out, no filesystem and no compiler — which is what
|
|
90
|
+
* makes every decision above unit-testable on a machine with neither backend
|
|
91
|
+
* installed. The one thing it cannot answer is whether a compiler accepts what it
|
|
92
|
+
* wrote, and that is what the two legs are for.
|
|
93
|
+
*/
|
|
94
|
+
export declare function renderWxs(input: WxsInput): string;
|
|
95
|
+
/** Which compiler a host has. */
|
|
96
|
+
export type MsiBackend = 'wixl' | 'wix3';
|
|
97
|
+
/**
|
|
98
|
+
* The backend for one host.
|
|
99
|
+
*
|
|
100
|
+
* A FUNCTION of the host and nothing else, so the choice is testable from either
|
|
101
|
+
* OS. `assertHostCanFinish` has already refused anything but these two by the time
|
|
102
|
+
* this runs — the throw is for the caller that forgets to.
|
|
103
|
+
*/
|
|
104
|
+
export declare function msiBackendFor(host: string): MsiBackend;
|
|
105
|
+
export interface MsiPackInput {
|
|
106
|
+
settings: PackSettings;
|
|
107
|
+
files: readonly MsiFile[];
|
|
108
|
+
programDirName: string;
|
|
109
|
+
archLabel: string;
|
|
110
|
+
/** Working root for the rendered document and the compiler's intermediates. */
|
|
111
|
+
workDir: string;
|
|
112
|
+
/** Absolute path the finished `.msi` is written to. */
|
|
113
|
+
target: string;
|
|
114
|
+
verbose: boolean;
|
|
115
|
+
/** Injected so the dispatch is testable off the host it selects for. */
|
|
116
|
+
host?: HostOs;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Render the document and compile it into `input.target`.
|
|
120
|
+
*
|
|
121
|
+
* The `.wxs` is KEPT in `workDir` rather than written to a temporary file, and
|
|
122
|
+
* that is deliberate: it is the one artifact of this milestone a human can read,
|
|
123
|
+
* it is what a bug report should carry, and it is what the Windows leg compiles a
|
|
124
|
+
* SECOND time with WiX to produce the file the Linux oracle reads back.
|
|
125
|
+
*/
|
|
126
|
+
export declare function buildMsi(input: MsiPackInput): Promise<string>;
|