@gjsify/cli 0.44.0 → 0.46.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 +202 -167
- 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 +850 -58
- package/lib/commands/trust.d.ts +6 -0
- package/lib/commands/trust.js +1 -1
- package/lib/commands/upgrade.d.ts +23 -0
- package/lib/commands/upgrade.js +95 -12
- package/lib/templates/install.mjs.tmpl +2 -2
- package/lib/types/config-data.d.ts +127 -1
- package/lib/utils/gi-search-path.js +11 -3
- 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 +50 -0
- package/lib/utils/ship/depends.js +117 -3
- package/lib/utils/ship/discover.js +103 -1
- package/lib/utils/ship/dmg.d.ts +71 -0
- package/lib/utils/ship/dmg.js +181 -0
- package/lib/utils/ship/flatpak.js +1 -1
- package/lib/utils/ship/formats.d.ts +108 -7
- package/lib/utils/ship/formats.js +778 -15
- package/lib/utils/ship/launcher.d.ts +13 -3
- package/lib/utils/ship/launcher.js +419 -17
- package/lib/utils/ship/layout.d.ts +330 -0
- package/lib/utils/ship/layout.js +504 -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 +434 -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 +599 -14
- package/lib/utils/ship/pe-launcher.d.ts +29 -0
- package/lib/utils/ship/pe-launcher.js +663 -0
- package/lib/utils/ship/plan.d.ts +21 -0
- package/lib/utils/ship/plan.js +55 -7
- package/lib/utils/ship/plist.d.ts +98 -0
- package/lib/utils/ship/plist.js +256 -0
- package/lib/utils/ship/rpm.js +16 -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 +91 -2
- package/lib/utils/ship/settings.js +93 -1
- package/lib/utils/ship/share-dirs.d.ts +53 -0
- package/lib/utils/ship/share-dirs.js +76 -0
- package/lib/utils/ship/signing.d.ts +311 -0
- package/lib/utils/ship/signing.js +506 -0
- package/lib/utils/ship/stage-manifest.d.ts +40 -12
- package/lib/utils/ship/stage-manifest.js +99 -13
- package/lib/utils/ship/stage-writer.d.ts +48 -0
- package/lib/utils/ship/stage-writer.js +91 -2
- package/lib/utils/ship/types.d.ts +191 -14
- 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/system-gi.d.ts +20 -5
- package/lib/utils/system-gi.js +36 -13
- package/lib/utils/trust-registry.js +5 -0
- package/package.json +19 -19
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>;
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// The Windows installer — ADR 0024 stage 5, issue #1354 M5.
|
|
2
|
+
//
|
|
3
|
+
// ONE AUTHORED DOCUMENT, TWO COMPILERS. Everything below produces a `.wxs` in
|
|
4
|
+
// WiX v3's schema (`http://schemas.microsoft.com/wix/2006/wi`) and hands it to
|
|
5
|
+
// whichever compiler the host has: `wixl` from `msitools` on Linux, `candle` +
|
|
6
|
+
// `light` from WiX Toolset v3.14 on Windows. That is not two implementations of
|
|
7
|
+
// the same thing — the DOCUMENT is the implementation and it is written once;
|
|
8
|
+
// the backends are two readers of it, which is the entire point (§ A6). A
|
|
9
|
+
// wixl-only path would be this tree's writer read back by `msiinfo`, i.e. by
|
|
10
|
+
// wixl's own package.
|
|
11
|
+
//
|
|
12
|
+
// WIX v3 AND NOT v4/v5, and that is forced rather than nostalgic: `wixl`
|
|
13
|
+
// implements a subset of the 2006 schema, while WiX v4 moved to
|
|
14
|
+
// `http://wixtoolset.org/schemas/v4/wxs` with renamed elements. One document
|
|
15
|
+
// cannot be in two namespaces, so the shared one is v3 — which is also what
|
|
16
|
+
// `windows-latest` ships (measured: WiX Toolset 3.14.1.8722 in the
|
|
17
|
+
// `actions/runner-images` software list for Windows Server 2025).
|
|
18
|
+
//
|
|
19
|
+
// WHAT THE INSTALLER ACTUALLY DOES, because "an `.msi` exists" is not the claim:
|
|
20
|
+
//
|
|
21
|
+
// * lays the program directory `windows-dir` produces under
|
|
22
|
+
// `%ProgramFiles%\<App>`, overridable with `msiexec INSTALLDIR=…`;
|
|
23
|
+
// * gives the user something to start — one Start-Menu shortcut at the top
|
|
24
|
+
// level, aimed at the GUI-subsystem `.exe` launcher the zip's user
|
|
25
|
+
// double-clicks (see the last paragraph of this header);
|
|
26
|
+
// * comes off again: Add/Remove Programs gets its entry from the MSI's own
|
|
27
|
+
// `ProductName`/`ProductVersion`/`Manufacturer`, and `msiexec /x` removes
|
|
28
|
+
// every file the package installed. The `windows-dir-selfcontained` leg's
|
|
29
|
+
// sibling asserts the install root is GONE afterwards, because an installer
|
|
30
|
+
// that cannot be removed is worse than none.
|
|
31
|
+
//
|
|
32
|
+
// FIVE THINGS WERE MEASURED against `msitools 0.106.58-1.fc44` (`wixl
|
|
33
|
+
// 0.106.58-a155`) before this file was written, each with a plausible wrong
|
|
34
|
+
// answer:
|
|
35
|
+
//
|
|
36
|
+
// 1. `wixl -a x64` sets Component `Attributes` to 256 — `msidbComponent-
|
|
37
|
+
// Attributes64bit` — on its own, so `Win64="yes"` is neither written here nor
|
|
38
|
+
// needed on either backend (`candle -arch x64` does the same).
|
|
39
|
+
// 2. An ABSOLUTE `Source=` resolves — and this file writes RELATIVE ones anyway,
|
|
40
|
+
// which is the measurement changing a decision rather than confirming it. The
|
|
41
|
+
// SAME document is compiled twice, on two machines: `wixl` here and WiX v3 on
|
|
42
|
+
// the `windows-latest` leg, out of a `.wxs` that travelled as an artifact. An
|
|
43
|
+
// absolute path is the build host's, so the second compile would fail on every
|
|
44
|
+
// file. `root/<path>`, POSIX-separated, resolves for both — Win32 accepts `/`
|
|
45
|
+
// — and it also keeps the document itself byte-identical between two builds of
|
|
46
|
+
// one project in different directories.
|
|
47
|
+
// 3. An explicit `Product/@Id` GUID is honoured (`msiinfo export … Property`
|
|
48
|
+
// prints it back verbatim), so the artifact can be DETERMINISTIC. `Id="*"`
|
|
49
|
+
// would give every rebuild a different `ProductCode`.
|
|
50
|
+
// 4. A `<Shortcut Advertise="yes">` nested inside `<File>` compiles, which is
|
|
51
|
+
// what keeps the shortcut in the launcher's own per-machine component instead
|
|
52
|
+
// of a second component with an HKCU key path — the shape ICE43/ICE57 argue
|
|
53
|
+
// about on `light`.
|
|
54
|
+
// 5. `Property/@Secure` is ACCEPTED and IGNORED by wixl (`SecureCustom-
|
|
55
|
+
// Properties` came back holding only the two `MajorUpgrade` values), so this
|
|
56
|
+
// file does not emit one: a `<Property Id="INSTALLDIR">` beside
|
|
57
|
+
// `<Directory Id="INSTALLDIR">` would be a divergence between the backends
|
|
58
|
+
// bought for nothing. A directory whose id is a public property is settable
|
|
59
|
+
// from a command line either way.
|
|
60
|
+
//
|
|
61
|
+
// AND ONE THING IT USED NOT TO FIX. `node.exe` is a CONSOLE-subsystem image and
|
|
62
|
+
// the Node release ships no `nodew.exe`, so a Start-Menu shortcut to the `.cmd`
|
|
63
|
+
// popped a console window behind the GUI exactly as a double-click did. The
|
|
64
|
+
// shortcut now points at the GUI-subsystem launcher the windows LAYOUT stages
|
|
65
|
+
// (`utils/ship/pe-launcher.ts`), which runs the same `.cmd` with no window —
|
|
66
|
+
// measured on `win11-gjsify` in session 1, where the `.cmd` produces two new
|
|
67
|
+
// visible console-host windows and the `.exe` produces none. Still no CI leg can
|
|
68
|
+
// observe it: every Windows job starts the app from a shell and already has a
|
|
69
|
+
// console.
|
|
70
|
+
import { createHash } from 'node:crypto';
|
|
71
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
72
|
+
import { join, posix } from 'node:path';
|
|
73
|
+
import { describeExit, spawnToCompletion } from '../spawn.js';
|
|
74
|
+
import { windowsGuiLauncherPath } from './layout.js';
|
|
75
|
+
import { xmlEscape } from './xml.js';
|
|
76
|
+
/**
|
|
77
|
+
* The UUID namespace every GUID in a generated `.msi` is derived under.
|
|
78
|
+
*
|
|
79
|
+
* A CONSTANT, and changing it is a breaking change rather than a cosmetic one:
|
|
80
|
+
* the `UpgradeCode` derived from it is how Windows Installer recognises an
|
|
81
|
+
* already-installed copy of the same product, so a new namespace makes every
|
|
82
|
+
* future release a SECOND product that installs beside the old one instead of
|
|
83
|
+
* over it. Written down here rather than passed in for that reason — a knob would
|
|
84
|
+
* be a knob that silently forks a product line.
|
|
85
|
+
*/
|
|
86
|
+
export const MSI_UUID_NAMESPACE = '7b3a1f6e-0c4d-4a2b-9e51-3d8c6f2a0b14';
|
|
87
|
+
/** RFC 4122 § 4.3 name-based UUID, SHA-1 flavour. */
|
|
88
|
+
export function uuid5(namespace, name) {
|
|
89
|
+
const ns = namespace.replace(/-/g, '');
|
|
90
|
+
const nsBytes = new Uint8Array(16);
|
|
91
|
+
for (let i = 0; i < 16; i += 1)
|
|
92
|
+
nsBytes[i] = Number.parseInt(ns.slice(i * 2, i * 2 + 2), 16);
|
|
93
|
+
const digest = createHash('sha1').update(nsBytes).update(new TextEncoder().encode(name)).digest();
|
|
94
|
+
const bytes = Uint8Array.from(digest.subarray(0, 16));
|
|
95
|
+
// Version 5 in the high nibble of byte 6, RFC 4122 variant in byte 8.
|
|
96
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
|
|
97
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
98
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
99
|
+
return [hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16), hex.slice(16, 20), hex.slice(20)].join('-');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The code that identifies the PRODUCT LINE, stable across every version.
|
|
103
|
+
*
|
|
104
|
+
* Derived from the app id alone. That is what `MajorUpgrade` compares, so it must
|
|
105
|
+
* NOT move when the version does — an `UpgradeCode` that changed per release would
|
|
106
|
+
* leave every old version installed beside the new one, silently, with two entries
|
|
107
|
+
* in Add/Remove Programs and two copies on disk.
|
|
108
|
+
*/
|
|
109
|
+
export function msiUpgradeCode(settings) {
|
|
110
|
+
return uuid5(MSI_UUID_NAMESPACE, `upgrade\n${settings.appId}`).toUpperCase();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The code that identifies THIS build, and it has to move when the version does.
|
|
114
|
+
*
|
|
115
|
+
* The exact inverse of {@link msiUpgradeCode}: Windows Installer refuses to
|
|
116
|
+
* install a second package carrying a `ProductCode` it already has, so a code that
|
|
117
|
+
* did not change per version would make an upgrade a no-op. The arch is in the
|
|
118
|
+
* material because an x64 and a (future) arm64 build of one version are two
|
|
119
|
+
* products, not one.
|
|
120
|
+
*/
|
|
121
|
+
export function msiProductCode(settings, archLabel) {
|
|
122
|
+
const material = `product\n${settings.appId}\n${settings.version}\n${settings.release}\n${archLabel}`;
|
|
123
|
+
return uuid5(MSI_UUID_NAMESPACE, material).toUpperCase();
|
|
124
|
+
}
|
|
125
|
+
/** A component's GUID, stable for as long as its path is. */
|
|
126
|
+
export function msiComponentGuid(settings, path) {
|
|
127
|
+
return uuid5(MSI_UUID_NAMESPACE, `component\n${settings.appId}\n${path}`).toUpperCase();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* MSI's `ProductVersion`, which is a much smaller vocabulary than npm's.
|
|
131
|
+
*
|
|
132
|
+
* `major.minor.build`, with `major`/`minor` at most 255 and `build` at most 65535 —
|
|
133
|
+
* anything outside that is not a version Windows Installer will compare, and the
|
|
134
|
+
* fourth field it accepts is IGNORED by upgrade detection, which is why nothing is
|
|
135
|
+
* ever put there.
|
|
136
|
+
*
|
|
137
|
+
* A PRERELEASE IS REFUSED rather than truncated, and that is the interesting case.
|
|
138
|
+
* `normaliseVersion` turns `1.2.0-rc.1` into `1.2.0~rc.1` because dpkg and rpm sort
|
|
139
|
+
* a `~` before the release; MSI has no such spelling, so the only two options were
|
|
140
|
+
* to drop the suffix or to say no. Dropping it makes `1.2.0~rc.1` and `1.2.0` the
|
|
141
|
+
* same `ProductVersion` — two products `MajorUpgrade` cannot tell apart, so
|
|
142
|
+
* installing the release over the candidate leaves BOTH on the machine. That is
|
|
143
|
+
* the "installs cleanly, does the wrong thing" class, produced by the field meant
|
|
144
|
+
* to prevent it.
|
|
145
|
+
*/
|
|
146
|
+
export function msiProductVersion(version) {
|
|
147
|
+
const match = /^(\d+)\.(\d+)(?:\.(\d+))?$/.exec(version);
|
|
148
|
+
if (match === null) {
|
|
149
|
+
throw new Error(`gjsify ship: "${version}" is not a version an \`.msi\` can carry. Windows Installer's ` +
|
|
150
|
+
'`ProductVersion` is `major.minor.build` and nothing else — no prerelease suffix, no build ' +
|
|
151
|
+
'metadata.\n' +
|
|
152
|
+
' Dropping the suffix is not an option this command will take silently: `1.2.0~rc.1` and ' +
|
|
153
|
+
'`1.2.0` would\n' +
|
|
154
|
+
' become the same version, `MajorUpgrade` could not tell them apart, and installing one over ' +
|
|
155
|
+
'the other\n' +
|
|
156
|
+
' would leave both on the machine. Set `gjsify.ship.version` to a plain `x.y.z`, or drop ' +
|
|
157
|
+
'`msi` from the targets.');
|
|
158
|
+
}
|
|
159
|
+
const [major, minor, build] = [Number(match[1]), Number(match[2]), Number(match[3] ?? '0')];
|
|
160
|
+
if (major > 255 || minor > 255 || build > 65535) {
|
|
161
|
+
throw new Error(`gjsify ship: version "${version}" is outside what Windows Installer compares — \`major\` and ` +
|
|
162
|
+
'`minor` are at most 255 and `build` at most 65535. A field over the limit is not rejected by ' +
|
|
163
|
+
'the installer, it is\n' +
|
|
164
|
+
' TRUNCATED, so the artifact would carry a version that is not the one it was built from. ' +
|
|
165
|
+
'Set `gjsify.ship.version`.');
|
|
166
|
+
}
|
|
167
|
+
return `${major}.${minor}.${build}`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* An MSI `Identifier`: `[A-Za-z_][A-Za-z0-9_.]{0,71}`.
|
|
171
|
+
*
|
|
172
|
+
* The readable tail is not decoration — `msiinfo export <msi> File` is the oracle,
|
|
173
|
+
* and an id of pure hash makes a mismatch unreadable. The digest is what makes it
|
|
174
|
+
* UNIQUE: it is taken over the full path, so two files whose last 57 characters
|
|
175
|
+
* agree still differ. `renderWxs` asserts uniqueness anyway rather than trusting
|
|
176
|
+
* 48 bits, because the failure of a collision is two files fighting over one row.
|
|
177
|
+
*/
|
|
178
|
+
export function msiIdentifier(prefix, path) {
|
|
179
|
+
const digest = createHash('sha1').update(new TextEncoder().encode(path)).digest('hex').slice(0, 12);
|
|
180
|
+
const safe = path.replace(/[^A-Za-z0-9_.]/g, '_');
|
|
181
|
+
const room = 72 - prefix.length - 2 - digest.length;
|
|
182
|
+
return `${prefix}_${safe.length > room ? safe.slice(safe.length - room) : safe}_${digest}`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* What Windows Installer will not hold in a `FileName`/`DefaultDir` cell.
|
|
186
|
+
*
|
|
187
|
+
* A SUPERSET of `windowsProgramDirName`'s rules and a different question: that one
|
|
188
|
+
* asks what Win32 can create, this asks what the installer DATABASE can address.
|
|
189
|
+
* MSI adds `;` (its list separator inside `DefaultDir`) and a 255-byte ceiling per
|
|
190
|
+
* segment to the reserved characters, and refuses `.`/`..` outright.
|
|
191
|
+
*/
|
|
192
|
+
const MSI_NAME_FORBIDDEN = /[<>:"/\\|?*;]/;
|
|
193
|
+
function assertNameIsAddressable(segment, path) {
|
|
194
|
+
if (segment === '.' || segment === '..' || segment.length === 0 || MSI_NAME_FORBIDDEN.test(segment)) {
|
|
195
|
+
throw new Error(`gjsify ship: the payload holds "${path}", and Windows Installer cannot address the path segment ` +
|
|
196
|
+
`"${segment}". A \`FileName\` or \`DefaultDir\` cell reserves < > : " / \\ | ? * and ; (its own ` +
|
|
197
|
+
'list separator), and \n `.` and `..` are not names. The `.msi` would build and install the ' +
|
|
198
|
+
'file somewhere else, or not at all.');
|
|
199
|
+
}
|
|
200
|
+
if (segment.length > 255) {
|
|
201
|
+
throw new Error(`gjsify ship: the payload holds "${path}", whose segment "${segment.slice(0, 40)}…" is ` +
|
|
202
|
+
`${segment.length} characters. Windows Installer's \`FileName\` column is 255.`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** Where {@link buildMsi} writes the payload, and therefore what `Source=` is relative to. */
|
|
206
|
+
export const MSI_PAYLOAD_DIR = 'root';
|
|
207
|
+
function directoryTree(files) {
|
|
208
|
+
const root = { path: '', name: '', children: new Map(), files: [] };
|
|
209
|
+
for (const file of [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
|
|
210
|
+
const segments = file.path.split('/');
|
|
211
|
+
const leaf = segments.pop();
|
|
212
|
+
if (leaf === undefined)
|
|
213
|
+
throw new Error(`gjsify ship: the payload holds an empty path.`);
|
|
214
|
+
let node = root;
|
|
215
|
+
for (const segment of segments) {
|
|
216
|
+
assertNameIsAddressable(segment, file.path);
|
|
217
|
+
const next = node.children.get(segment) ?? {
|
|
218
|
+
path: node.path === '' ? segment : `${node.path}/${segment}`,
|
|
219
|
+
name: segment,
|
|
220
|
+
children: new Map(),
|
|
221
|
+
files: [],
|
|
222
|
+
};
|
|
223
|
+
node.children.set(segment, next);
|
|
224
|
+
node = next;
|
|
225
|
+
}
|
|
226
|
+
assertNameIsAddressable(leaf, file.path);
|
|
227
|
+
node.files.push(file);
|
|
228
|
+
}
|
|
229
|
+
return root;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Render the installer's source document.
|
|
233
|
+
*
|
|
234
|
+
* PURE — a string in, a string out, no filesystem and no compiler — which is what
|
|
235
|
+
* makes every decision above unit-testable on a machine with neither backend
|
|
236
|
+
* installed. The one thing it cannot answer is whether a compiler accepts what it
|
|
237
|
+
* wrote, and that is what the two legs are for.
|
|
238
|
+
*/
|
|
239
|
+
export function renderWxs(input) {
|
|
240
|
+
const { settings, programDirName, archLabel } = input;
|
|
241
|
+
const productVersion = msiProductVersion(settings.version);
|
|
242
|
+
// `Name <email>` is `PackSettings.maintainer`'s shape; ARP shows `Publisher`
|
|
243
|
+
// to a human, and an address in that field reads as a mistake.
|
|
244
|
+
const manufacturer = settings.maintainer.replace(/\s*<[^>]*>\s*$/, '').trim() || settings.maintainer;
|
|
245
|
+
// THE SHORTCUT POINTS AT THE `.exe`, NOT THE `.cmd` — this installer's half of
|
|
246
|
+
// the console-window fix. A Start-Menu shortcut to a batch file starts
|
|
247
|
+
// `cmd.exe`, a console-subsystem image, so Windows allocates a console for it
|
|
248
|
+
// and a black window sits behind the app (ADR 0024 § M3). The GUI-subsystem
|
|
249
|
+
// launcher beside it — `utils/ship/pe-launcher.ts`, staged by the windows
|
|
250
|
+
// LAYOUT — runs the same `.cmd` and has no window of its own.
|
|
251
|
+
//
|
|
252
|
+
// Derived from `windowsGuiLauncherPath` and never spelled again here: the stub
|
|
253
|
+
// finds its `.cmd` by rewriting the last three characters of its own filename,
|
|
254
|
+
// so the two names are ONE decision, and a second literal is where they would
|
|
255
|
+
// come apart.
|
|
256
|
+
const launcher = windowsGuiLauncherPath(settings);
|
|
257
|
+
const identifiers = new Map();
|
|
258
|
+
const claim = (id, what) => {
|
|
259
|
+
const previous = identifiers.get(id);
|
|
260
|
+
if (previous !== undefined) {
|
|
261
|
+
throw new Error(`gjsify ship: the MSI identifier "${id}" would be shared by ${previous} and ${what}. ` +
|
|
262
|
+
'Two rows cannot carry one id — one would silently replace the other.');
|
|
263
|
+
}
|
|
264
|
+
identifiers.set(id, what);
|
|
265
|
+
return id;
|
|
266
|
+
};
|
|
267
|
+
const lines = [];
|
|
268
|
+
const componentIds = [];
|
|
269
|
+
let launcherFound = false;
|
|
270
|
+
const emitDir = (node, indent) => {
|
|
271
|
+
for (const file of node.files) {
|
|
272
|
+
const componentId = claim(msiIdentifier('c', file.path), file.path);
|
|
273
|
+
const fileId = claim(msiIdentifier('f', file.path), file.path);
|
|
274
|
+
componentIds.push(componentId);
|
|
275
|
+
const guid = msiComponentGuid(settings, file.path);
|
|
276
|
+
// `posix.basename`, not `basename`: `MsiFile.path` is POSIX-separated
|
|
277
|
+
// by construction (it is the STAGED path), and the host's separator is
|
|
278
|
+
// not the authority for it — a Windows packing host would otherwise
|
|
279
|
+
// split a name containing a backslash.
|
|
280
|
+
const name = xmlEscape(posix.basename(file.path));
|
|
281
|
+
lines.push(`${indent}<Component Id="${componentId}" Guid="${guid}">`);
|
|
282
|
+
// THE SHORTCUT LIVES IN THE LAUNCHER'S OWN COMPONENT, nested in its
|
|
283
|
+
// `<File>` and advertised. The obvious alternative — a second component
|
|
284
|
+
// under `ProgramMenuFolder` whose key path is an HKCU registry value —
|
|
285
|
+
// is the shape `light`'s ICE43 asks for and ICE57 then objects to,
|
|
286
|
+
// because a per-machine install with a per-user key path is a component
|
|
287
|
+
// that is half in each world. Nesting it makes the key path the file, so
|
|
288
|
+
// the component is per-machine throughout and the shortcut is removed
|
|
289
|
+
// with it.
|
|
290
|
+
if (file.path === launcher) {
|
|
291
|
+
launcherFound = true;
|
|
292
|
+
lines.push(`${indent} <File Id="${fileId}" Name="${name}" ` +
|
|
293
|
+
`Source="${xmlEscape(file.source)}" KeyPath="yes">`);
|
|
294
|
+
lines.push(`${indent} <Shortcut Id="${claim('s_startmenu', 'the Start-Menu shortcut')}" ` +
|
|
295
|
+
`Directory="ProgramMenuFolder" Name="${xmlEscape(settings.name)}" ` +
|
|
296
|
+
`WorkingDirectory="INSTALLDIR" Description="${xmlEscape(settings.summary)}" ` +
|
|
297
|
+
`Advertise="yes" />`);
|
|
298
|
+
lines.push(`${indent} </File>`);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
lines.push(`${indent} <File Id="${fileId}" Name="${name}" ` +
|
|
302
|
+
`Source="${xmlEscape(file.source)}" KeyPath="yes" />`);
|
|
303
|
+
}
|
|
304
|
+
lines.push(`${indent}</Component>`);
|
|
305
|
+
}
|
|
306
|
+
for (const child of [...node.children.values()].sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
307
|
+
const dirId = claim(msiIdentifier('d', child.path), `the directory ${child.path}`);
|
|
308
|
+
lines.push(`${indent}<Directory Id="${dirId}" Name="${xmlEscape(child.name)}">`);
|
|
309
|
+
emitDir(child, `${indent} `);
|
|
310
|
+
lines.push(`${indent}</Directory>`);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
emitDir(directoryTree(input.files), ' ');
|
|
314
|
+
// EMPTINESS FIRST, and the order is load-bearing rather than tidy: an empty
|
|
315
|
+
// payload has no launcher either, so the launcher refusal below fires on it and
|
|
316
|
+
// sends the reader to look for a `.cmd` in a tree that has no files at all.
|
|
317
|
+
if (componentIds.length === 0) {
|
|
318
|
+
throw new Error('gjsify ship: the windows payload is empty, so there is nothing for an `.msi` to install.');
|
|
319
|
+
}
|
|
320
|
+
if (!launcherFound) {
|
|
321
|
+
throw new Error(`gjsify ship: the windows payload carries no "${launcher}" at the root of the program directory, ` +
|
|
322
|
+
'so the installer would have nothing to point a Start-Menu shortcut at — an application a user ' +
|
|
323
|
+
'installs and\n cannot start. `windows-dir` puts the launcher there; a payload without one ' +
|
|
324
|
+
'is a stage assembled for another layout.');
|
|
325
|
+
}
|
|
326
|
+
const about = settings.homepage === undefined
|
|
327
|
+
? []
|
|
328
|
+
: [` <Property Id="ARPURLINFOABOUT" Value="${xmlEscape(settings.homepage)}" />`];
|
|
329
|
+
return [
|
|
330
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
331
|
+
'<!-- Generated by `gjsify ship` (ADR 0024 stage 5). Do not edit; edit utils/ship/msi.ts. -->',
|
|
332
|
+
'<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">',
|
|
333
|
+
` <Product Id="${msiProductCode(settings, archLabel)}" Name="${xmlEscape(settings.name)}" ` +
|
|
334
|
+
`Language="1033" Version="${productVersion}" Manufacturer="${xmlEscape(manufacturer)}" ` +
|
|
335
|
+
`UpgradeCode="${msiUpgradeCode(settings)}">`,
|
|
336
|
+
` <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" ` +
|
|
337
|
+
`Manufacturer="${xmlEscape(manufacturer)}" Description="${xmlEscape(settings.summary)}" />`,
|
|
338
|
+
// The whole reason `UpgradeCode` is derived from the app id: this is what
|
|
339
|
+
// takes the previous version off before laying the new one down.
|
|
340
|
+
` <MajorUpgrade DowngradeErrorMessage="A newer version of ${xmlEscape(settings.name)} is ` +
|
|
341
|
+
`already installed." />`,
|
|
342
|
+
// ONE embedded cabinet, so the artifact is the single file a browser
|
|
343
|
+
// downloads rather than an `.msi` plus loose `.cab` siblings that a user
|
|
344
|
+
// will separate from it.
|
|
345
|
+
' <Media Id="1" Cabinet="app.cab" EmbedCab="yes" />',
|
|
346
|
+
...about,
|
|
347
|
+
' <Directory Id="TARGETDIR" Name="SourceDir">',
|
|
348
|
+
' <Directory Id="ProgramFiles64Folder">',
|
|
349
|
+
// ONE LEVEL, not `%ProgramFiles%\<Publisher>\<App>`, and the reason is an
|
|
350
|
+
// assertion rather than taste: the tree this lays down is then byte-for-byte
|
|
351
|
+
// the tree `windows-dir-zip` expands to, so the two artifacts can be
|
|
352
|
+
// compared against each other instead of each against its own idea of the
|
|
353
|
+
// layout. A publisher level would also be a directory nothing owns, left
|
|
354
|
+
// behind empty by an uninstall.
|
|
355
|
+
` <Directory Id="INSTALLDIR" Name="${xmlEscape(programDirName)}">`,
|
|
356
|
+
...lines,
|
|
357
|
+
' </Directory>',
|
|
358
|
+
' </Directory>',
|
|
359
|
+
// Declared and left empty: the shortcut reaches it by `Directory=`, so this
|
|
360
|
+
// exists only to give that reference a row. No `<RemoveFolder>` on it — that
|
|
361
|
+
// would ask the installer to delete the Start Menu's Programs folder itself.
|
|
362
|
+
' <Directory Id="ProgramMenuFolder" />',
|
|
363
|
+
' </Directory>',
|
|
364
|
+
` <Feature Id="Main" Title="${xmlEscape(settings.name)}" Level="1">`,
|
|
365
|
+
...componentIds.map((id) => ` <ComponentRef Id="${id}" />`),
|
|
366
|
+
' </Feature>',
|
|
367
|
+
' </Product>',
|
|
368
|
+
'</Wix>',
|
|
369
|
+
'',
|
|
370
|
+
].join('\n');
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The backend for one host.
|
|
374
|
+
*
|
|
375
|
+
* A FUNCTION of the host and nothing else, so the choice is testable from either
|
|
376
|
+
* OS. `assertHostCanFinish` has already refused anything but these two by the time
|
|
377
|
+
* this runs — the throw is for the caller that forgets to.
|
|
378
|
+
*/
|
|
379
|
+
export function msiBackendFor(host) {
|
|
380
|
+
if (host === 'linux')
|
|
381
|
+
return 'wixl';
|
|
382
|
+
if (host === 'win32')
|
|
383
|
+
return 'wix3';
|
|
384
|
+
throw new Error(`gjsify ship: no MSI compiler is wired for ${host}. \`wixl\` (msitools) is used on linux and WiX v3 ` +
|
|
385
|
+
'on win32; assembly is not host-bound, so `gjsify ship windows --stage` here and ' +
|
|
386
|
+
'`gjsify ship --from-stage <dir> --target msi` there is the way across.');
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Render the document and compile it into `input.target`.
|
|
390
|
+
*
|
|
391
|
+
* The `.wxs` is KEPT in `workDir` rather than written to a temporary file, and
|
|
392
|
+
* that is deliberate: it is the one artifact of this milestone a human can read,
|
|
393
|
+
* it is what a bug report should carry, and it is what the Windows leg compiles a
|
|
394
|
+
* SECOND time with WiX to produce the file the Linux oracle reads back.
|
|
395
|
+
*/
|
|
396
|
+
export async function buildMsi(input) {
|
|
397
|
+
const host = input.host ?? process.platform;
|
|
398
|
+
const backend = msiBackendFor(host);
|
|
399
|
+
mkdirSync(input.workDir, { recursive: true });
|
|
400
|
+
const wxsPath = join(input.workDir, `${input.settings.binaryName}.wxs`);
|
|
401
|
+
writeFileSync(wxsPath, renderWxs(input));
|
|
402
|
+
if (backend === 'wixl') {
|
|
403
|
+
await run('wixl', ['-a', input.archLabel, '-o', input.target, wxsPath], input);
|
|
404
|
+
return wxsPath;
|
|
405
|
+
}
|
|
406
|
+
// WiX v3 is two passes, and the intermediate directory is wiped between runs
|
|
407
|
+
// for the reason the Flatpak packer wipes its repo: a `.wixobj` that survives a
|
|
408
|
+
// failed compile is one `light` will happily link into an artifact wearing the
|
|
409
|
+
// new version's filename.
|
|
410
|
+
const objDir = join(input.workDir, 'obj');
|
|
411
|
+
rmSync(objDir, { recursive: true, force: true, maxRetries: 5 });
|
|
412
|
+
mkdirSync(objDir, { recursive: true });
|
|
413
|
+
await run('candle.exe', ['-nologo', '-arch', input.archLabel, '-out', `${objDir}\\`, wxsPath], input);
|
|
414
|
+
await run('light.exe', ['-nologo', '-out', input.target, join(objDir, `${input.settings.binaryName}.wixobj`)], input);
|
|
415
|
+
return wxsPath;
|
|
416
|
+
}
|
|
417
|
+
async function run(cmd, args, input) {
|
|
418
|
+
if (input.verbose)
|
|
419
|
+
console.log(`[gjsify ship] ${cmd} ${args.join(' ')}`);
|
|
420
|
+
// `completion: 'return'`, as the Flatpak packer does and for the same reason:
|
|
421
|
+
// `ship` packs the remaining formats and prints the artifact list afterwards,
|
|
422
|
+
// so it cannot end in `process.exit()`.
|
|
423
|
+
const result = await spawnToCompletion(cmd, [...args], {
|
|
424
|
+
completion: 'return',
|
|
425
|
+
cwd: input.workDir,
|
|
426
|
+
stdio: 'inherit',
|
|
427
|
+
notFound: () => new Error(`gjsify ship: ${cmd} not found. It is the MSI compiler for this host — ` +
|
|
428
|
+
'Fedora: `sudo dnf install msitools`, Debian/Ubuntu: `sudo apt install msitools`, ' +
|
|
429
|
+
'Windows: WiX Toolset v3.14 with its `bin` directory on PATH.'),
|
|
430
|
+
});
|
|
431
|
+
if (result.code !== 0) {
|
|
432
|
+
throw new Error(`gjsify ship: ${cmd} failed with ${describeExit(result)}.`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** The targets a bundled interpreter is published for. */
|
|
2
|
+
export declare const NODE_RUNTIME_TARGETS: readonly ["darwin-arm64", "darwin-x64", "win32-x64"];
|
|
3
|
+
export type NodeRuntimeTarget = (typeof NODE_RUNTIME_TARGETS)[number];
|
|
4
|
+
/** Is this `<platform>-<arch>` one of the published targets? */
|
|
5
|
+
export declare function isNodeRuntimeTarget(target: string): target is NodeRuntimeTarget;
|
|
6
|
+
/** The npm name carrying the interpreter for a target. One spelling, derived. */
|
|
7
|
+
export declare function nodeRuntimePackageName(target: NodeRuntimeTarget): string;
|
|
8
|
+
/**
|
|
9
|
+
* The interpreter's filename inside the package, from the TARGET.
|
|
10
|
+
*
|
|
11
|
+
* Never from `process.platform`. Assembling a Windows artifact on Linux or macOS
|
|
12
|
+
* is a supported path (ADR 0024 § A1 — the packers are pure JavaScript and run
|
|
13
|
+
* anywhere), and a host-derived name would look for `node` inside the win32
|
|
14
|
+
* package, find nothing, and report it as a missing payload.
|
|
15
|
+
*
|
|
16
|
+
* `string`, not {@link NodeRuntimeTarget}, and the widening is what keeps this
|
|
17
|
+
* ONE rule: `utils/ship/app-runtime.ts` has to name the interpreter's file inside
|
|
18
|
+
* the ARTIFACT and holds a `GtkRuntimeTarget` when it does. Those two unions are
|
|
19
|
+
* deliberately separate tables (see `GTK_RUNTIME_TARGETS`), so a narrow parameter
|
|
20
|
+
* here would have forced a second `startsWith('win32-')` over there — and a stage
|
|
21
|
+
* whose source is `node.exe` and whose destination is `node` is a program
|
|
22
|
+
* directory whose launcher execs a file nothing staged.
|
|
23
|
+
*/
|
|
24
|
+
export declare function nodeRuntimeBinaryName(target: string): string;
|
|
25
|
+
/** Where the interpreter and its licence are, once found. */
|
|
26
|
+
export interface ResolvedNodeRuntime {
|
|
27
|
+
/** The npm package it came from, or `'GJSIFY_NODE_RUNTIME'` for the override. */
|
|
28
|
+
source: string;
|
|
29
|
+
/** The payload directory holding both files. */
|
|
30
|
+
binDir: string;
|
|
31
|
+
/** Absolute path to the interpreter. */
|
|
32
|
+
nodePath: string;
|
|
33
|
+
/**
|
|
34
|
+
* Absolute path to Node's own LICENSE.
|
|
35
|
+
*
|
|
36
|
+
* Part of the RESULT rather than something a caller looks up afterwards: an
|
|
37
|
+
* interpreter copied into an artifact without its licence is redistribution
|
|
38
|
+
* with no terms attached, and a shape that hands out the binary alone makes
|
|
39
|
+
* forgetting the easier path.
|
|
40
|
+
*/
|
|
41
|
+
licensePath: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ResolveNodeRuntimeOptions {
|
|
44
|
+
/** Project directory to resolve from — the consumer's, not gjsify's. */
|
|
45
|
+
cwd?: string;
|
|
46
|
+
/** Environment to read `GJSIFY_NODE_RUNTIME` from. Injected so it is testable. */
|
|
47
|
+
env?: Record<string, string | undefined>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Locate the bundled Node interpreter for `target`, or `null`.
|
|
51
|
+
*
|
|
52
|
+
* Search order, mirroring `resolveGtkRuntimeBundle()` in
|
|
53
|
+
* `packages/node-gi/node-gi/gtk-runtime.js` rather than inventing a second shape:
|
|
54
|
+
*
|
|
55
|
+
* 1. `GJSIFY_NODE_RUNTIME` — a `bin/` directory, for a maintainer holding an
|
|
56
|
+
* unpublished or patched interpreter.
|
|
57
|
+
* 2. `@gjsify/node-runtime-<target>` resolved by name through
|
|
58
|
+
* {@link resolveNpmPackage}, which walks the CONSUMER's `node_modules` chain
|
|
59
|
+
* as well as the CLI bundle's own. That multi-anchor walk is what makes the
|
|
60
|
+
* "only `@gjsify/cli` installed, no gjsify checkout" case work, and it is
|
|
61
|
+
* also why this does not use a bare `createRequire`: GJS's ESM loader has no
|
|
62
|
+
* node_modules walker at all.
|
|
63
|
+
*
|
|
64
|
+
* `null`, never a throw. Whether a missing interpreter is fatal depends on what
|
|
65
|
+
* the caller is building — and the caller is the only one that can say so
|
|
66
|
+
* usefully. A package resolved but not POPULATED (the payload is gitignored, so
|
|
67
|
+
* this is exactly what an in-repo checkout looks like) is also `null`: a path to
|
|
68
|
+
* a file that is not there would be the worse answer, since it fails later, in a
|
|
69
|
+
* copy, with the target's name nowhere in the message.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveNodeRuntime(target: NodeRuntimeTarget, options?: ResolveNodeRuntimeOptions): ResolvedNodeRuntime | null;
|