@nwutils/packager 0.4.0 → 0.6.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/README.md +130 -35
- package/package.json +15 -4
- package/src/appImage.js +307 -10
- package/src/index.js +13 -6
- package/src/nsis.js +563 -0
- package/src/util.js +202 -2
- package/types/appImage.d.ts +85 -14
- package/types/index.d.ts +21 -5
- package/types/nsis.d.ts +140 -0
- package/types/util.d.ts +102 -7
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Supported output formats, selected via `format`:
|
|
|
12
12
|
| `deb` | Linux | Planned |
|
|
13
13
|
| `rpm` | Linux | Planned |
|
|
14
14
|
| `msix` | Windows | Planned |
|
|
15
|
-
| `nsis` | Windows |
|
|
15
|
+
| `nsis` | Windows | Implemented |
|
|
16
16
|
| `dmg` | MacOS | Planned |
|
|
17
17
|
|
|
18
18
|
## Getting Started
|
|
@@ -21,8 +21,7 @@ Supported output formats, selected via `format`:
|
|
|
21
21
|
|
|
22
22
|
## Usage
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
that `@nwutils/builder` produced.
|
|
24
|
+
Build the application.
|
|
26
25
|
|
|
27
26
|
```js
|
|
28
27
|
import build from "@nwutils/builder";
|
|
@@ -41,42 +40,117 @@ await build({
|
|
|
41
40
|
categories: "Utility;",
|
|
42
41
|
},
|
|
43
42
|
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### AppImage
|
|
44
46
|
|
|
47
|
+
```js
|
|
45
48
|
const appImagePath = await packager({
|
|
46
49
|
format: "AppImage",
|
|
47
50
|
appDir: "./out",
|
|
48
51
|
appName: "Demo",
|
|
49
52
|
cacheDir: "./cache",
|
|
50
53
|
outDir: "./dist",
|
|
54
|
+
sign: true,
|
|
55
|
+
signKey: "ABCDEF1234567890",
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
console.log(`AppImage written to ${appImagePath}`);
|
|
54
59
|
```
|
|
55
60
|
|
|
56
|
-
`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
Signing needs `gpg` installed on the host and the secret key imported into its
|
|
62
|
+
keyring. If the key is protected by a passphrase, set it in the
|
|
63
|
+
`APPIMAGETOOL_SIGN_PASSPHRASE` environment variable
|
|
64
|
+
|
|
65
|
+
### Self updating AppImages
|
|
66
|
+
|
|
67
|
+
Set `publish` to where releases are uploaded, and `version` to the app's
|
|
68
|
+
version. This follows electron-builder's approach:
|
|
69
|
+
|
|
70
|
+
- an `app-update.yml` describing `publish` is embedded next to the NW.js
|
|
71
|
+
executable inside the AppImage, so the running app knows where to look
|
|
72
|
+
- a `latest-linux.yml` update info file (`latest-linux-<arch>.yml` for
|
|
73
|
+
architectures other than `x64`) with the version, file name, size and SHA-512
|
|
74
|
+
of the AppImage is written next to it in `outDir`
|
|
75
|
+
|
|
76
|
+
Upload both the AppImage and the update info file to every release.
|
|
77
|
+
[`@nwutils/updater`](https://www.npmjs.com/package/@nwutils/updater) reads them
|
|
78
|
+
at runtime to check for, download and install updates.
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
await packager({
|
|
82
|
+
format: "AppImage",
|
|
83
|
+
appDir: "./out",
|
|
84
|
+
appName: "Demo",
|
|
85
|
+
outDir: "./dist",
|
|
86
|
+
version: "1.2.0",
|
|
87
|
+
publish: { provider: "github", owner: "nwutils", repo: "demo" },
|
|
88
|
+
// Optional: also support zsync based updaters, see below.
|
|
89
|
+
updateInformation: true,
|
|
90
|
+
});
|
|
91
|
+
// ./dist/Demo-x86_64.AppImage
|
|
92
|
+
// ./dist/latest-linux.yml
|
|
93
|
+
// ./dist/Demo-x86_64.AppImage.zsync (only with updateInformation)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| `publish.provider` | Other keys | The latest release is looked up at |
|
|
97
|
+
| ------------------ | --------------- | ---------------------------------------------------------------- |
|
|
98
|
+
| `"github"` | `owner`, `repo` | The latest (non draft, non prerelease) GitHub release's assets |
|
|
99
|
+
| `"generic"` | `url` | `<url>/latest-linux.yml`, with the AppImage served from `<url>/` |
|
|
61
100
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
`updateInformation` additionally embeds AppImage update information, for zsync
|
|
102
|
+
based updaters such as AppImageUpdate, `appimageupdatetool` and AppImage
|
|
103
|
+
managers. `true` derives it from `publish`
|
|
104
|
+
(`gh-releases-zsync|<owner>|<repo>|latest|<AppImage>.zsync` or
|
|
105
|
+
`zsync|<url>/<AppImage>.zsync`); a string is embedded as-is. It needs
|
|
106
|
+
`zsyncmake` (usually packaged as `zsync`) installed on the host, and the
|
|
107
|
+
`.zsync` file must be uploaded alongside the AppImage.
|
|
67
108
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
### NSIS installer
|
|
110
|
+
|
|
111
|
+
Build the application with `platform: "win"` first, then:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
const installerPath = await packager({
|
|
115
|
+
format: "nsis",
|
|
116
|
+
appDir: "./out",
|
|
117
|
+
appName: "Demo",
|
|
118
|
+
version: "1.2.0",
|
|
119
|
+
outDir: "./dist",
|
|
120
|
+
icon: "./icon.ico",
|
|
121
|
+
publisher: "ACME",
|
|
122
|
+
license: "./LICENSE.txt",
|
|
123
|
+
});
|
|
124
|
+
// ./dist/Demo-x64-Setup.exe
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The installer walks the user through a wizard and installs for the current
|
|
128
|
+
user only, into `%LOCALAPPDATA%\Programs\<appName>`, so it never asks for
|
|
129
|
+
administrator rights. It adds Start Menu and desktop shortcuts, and registers
|
|
130
|
+
an uninstaller in Windows' installed apps list. The uninstaller removes exactly
|
|
131
|
+
the files that were installed. Installing a new version first uninstalls the
|
|
132
|
+
previous one, matched by `appId`, and stops if that fails. If the application
|
|
133
|
+
is running, the installer and uninstaller ask the user to close it first;
|
|
134
|
+
in silent mode (`/S`) they exit with an error instead.
|
|
135
|
+
|
|
136
|
+
The installer refuses to run on Windows machines that can't run `arch`: x64
|
|
137
|
+
builds need x64 Windows, or ARM64 Windows 11, which emulates x64.
|
|
138
|
+
|
|
139
|
+
Run the installer with `/S` to install silently, and `/D=<dir>` (last, unquoted)
|
|
140
|
+
to choose the install directory.
|
|
141
|
+
|
|
142
|
+
It needs the NSIS compiler, `makensis`, installed on the host. It runs on
|
|
143
|
+
Linux (`apt install nsis`) and MacOS (`brew install makensis`) too, so a Windows
|
|
144
|
+
installer can be built from any platform. On Windows, `choco install nsis`.
|
|
71
145
|
|
|
72
146
|
## API Reference
|
|
73
147
|
|
|
74
148
|
### `packager({ format, ...options })`
|
|
75
149
|
|
|
76
|
-
| Name | Type
|
|
77
|
-
| ------ |
|
|
78
|
-
| format | `"AppImage" \| "deb" \| "rpm" \| "
|
|
79
|
-
| ... | -
|
|
150
|
+
| Name | Type | Description |
|
|
151
|
+
| ------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
152
|
+
| format | `"AppImage" \| "deb" \| "rpm" \| "msix" \| "nsis" \| "dmg"` | Which packager to run. `"AppImage"` and `"nsis"` are implemented today. |
|
|
153
|
+
| ... | - | The remaining options are passed through to the packager for `format` as-is. |
|
|
80
154
|
|
|
81
155
|
Resolves with the path to the resulting packaged artifact.
|
|
82
156
|
|
|
@@ -84,24 +158,45 @@ Resolves with the path to the resulting packaged artifact.
|
|
|
84
158
|
|
|
85
159
|
Options
|
|
86
160
|
|
|
87
|
-
| Name
|
|
88
|
-
|
|
|
89
|
-
| appDir
|
|
90
|
-
| appName
|
|
91
|
-
| icon
|
|
92
|
-
| arch
|
|
93
|
-
| outDir
|
|
94
|
-
| cacheDir
|
|
95
|
-
| cache
|
|
96
|
-
| appImageToolUrl
|
|
161
|
+
| Name | Type | Description |
|
|
162
|
+
| ----------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
163
|
+
| appDir | `string` | Path to a built NW.js Linux application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "linux"` |
|
|
164
|
+
| appName | `string` | Name of the application. Must match the `app.name` value used to build `appDir`. Must not contain `/` or control characters |
|
|
165
|
+
| icon | `string` | Path to a `.png` or `.svg` icon. Defaults to the `Icon` value read from `<appDir>/<appName>.desktop` |
|
|
166
|
+
| arch | `"ia32" \| "x64" \| "arm64"` | Target architecture. Defaults to the host architecture |
|
|
167
|
+
| outDir | `string` | Directory the resulting `.AppImage` file is written to. Defaults to the parent directory of `appDir` |
|
|
168
|
+
| cacheDir | `string` | Directory used to cache the downloaded `appimagetool` binary. Defaults to `"./cache"` |
|
|
169
|
+
| cache | `boolean` | If true, reuse a cached `appimagetool` binary. Otherwise redownload it. Defaults to `true` |
|
|
170
|
+
| appImageToolUrl | `string` | Base URL `appimagetool-<arch>.AppImage` is downloaded from. Defaults to the `AppImage/appimagetool` "continuous" release. Must be https, except for localhost |
|
|
171
|
+
| sign | `boolean` | If true, embed a GPG signature in the AppImage. Defaults to `false` |
|
|
172
|
+
| signKey | `string` | ID of the GPG key to sign with. Defaults to `gpg`'s default secret key. Requires `sign: true` |
|
|
173
|
+
| version | `string` | Version of the application. Required with `publish` |
|
|
174
|
+
| publish | `PublishOptions` | Where releases are published. Embeds `app-update.yml` and writes `latest-linux[-<arch>].yml` to `outDir` |
|
|
175
|
+
| updateInformation | `boolean \| string` | AppImage zsync update information. `true` derives it from `publish`. Requires `zsyncmake` on the host |
|
|
97
176
|
|
|
98
177
|
Resolves with the path to the resulting `.AppImage` file.
|
|
99
178
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
179
|
+
### `nsis(options)`
|
|
180
|
+
|
|
181
|
+
Options
|
|
182
|
+
|
|
183
|
+
| Name | Type | Description |
|
|
184
|
+
| ----------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
185
|
+
| appDir | `string` | Path to a built NW.js Windows application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "win"` |
|
|
186
|
+
| appName | `string` | Name of the application. Must match the `app.name` value used to build `appDir`. Must be a valid Windows file name |
|
|
187
|
+
| version | `string` | Version of the application, eg. `"1.2.0"`. Required |
|
|
188
|
+
| arch | `"ia32" \| "x64" \| "arm64"` | Architecture `appDir` was built for. Defaults to the host architecture |
|
|
189
|
+
| outDir | `string` | Directory the resulting `<appName>-<arch>-Setup.exe` file is written to. Defaults to the parent directory of `appDir` |
|
|
190
|
+
| icon | `string` | Path to a `.ico` icon for the installer and uninstaller. Defaults to NSIS' own icon |
|
|
191
|
+
| appId | `string` | Name of the registry key the installation is recorded under. Defaults to `appName`. Keep it the same across releases |
|
|
192
|
+
| publisher | `string` | Publisher shown in Windows' installed apps list |
|
|
193
|
+
| copyright | `string` | Copyright notice written into the installer's version information |
|
|
194
|
+
| license | `string` | Path to a `.txt` or `.rtf` license file the user has to accept |
|
|
195
|
+
| desktopShortcut | `boolean` | If true, create a desktop shortcut. Defaults to `true` |
|
|
196
|
+
| startMenuShortcut | `boolean` | If true, create a Start Menu shortcut. Defaults to `true` |
|
|
197
|
+
| makensisPath | `string` | Path to the NSIS compiler. Defaults to `makensis` on the `PATH` |
|
|
198
|
+
|
|
199
|
+
Resolves with the path to the resulting installer `.exe` file.
|
|
105
200
|
|
|
106
201
|
## Contributing
|
|
107
202
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwutils/packager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Package NW.js applications for Linux, MacOS and Windows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"NW.js",
|
|
@@ -43,9 +43,20 @@
|
|
|
43
43
|
"lint": "eslint . ../../tests/specs/packager ../../tests/fixtures/packager",
|
|
44
44
|
"lint:fix": "eslint . ../../tests/specs/packager ../../tests/fixtures/packager --fix",
|
|
45
45
|
"type": "tsc",
|
|
46
|
-
"test": "node --test --test-concurrency=1 \"../../tests/specs/packager/appImage.test.js\" \"../../tests/specs/packager/request.test.js\" \"../../tests/specs/packager/util.test.js\"",
|
|
47
|
-
"test:real": "node --test \"../../tests/specs/packager/appImage.real.test.js\""
|
|
46
|
+
"test": "node --test --test-concurrency=1 \"../../tests/specs/packager/appImage.test.js\" \"../../tests/specs/packager/nsis.test.js\" \"../../tests/specs/packager/request.test.js\" \"../../tests/specs/packager/util.test.js\"",
|
|
47
|
+
"test:real": "node --test --test-concurrency=1 \"../../tests/specs/packager/appImage.real.test.js\" \"../../tests/specs/packager/nsis.real.test.js\""
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {},
|
|
50
|
-
"
|
|
50
|
+
"devEngines": {
|
|
51
|
+
"runtime": {
|
|
52
|
+
"name": "node",
|
|
53
|
+
"version": "26.7.0",
|
|
54
|
+
"onFail": "warn"
|
|
55
|
+
},
|
|
56
|
+
"packageManager": {
|
|
57
|
+
"name": "npm",
|
|
58
|
+
"version": "12.0.2",
|
|
59
|
+
"onFail": "warn"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
51
62
|
}
|
package/src/appImage.js
CHANGED
|
@@ -19,13 +19,40 @@ const APPIMAGETOOL_URL =
|
|
|
19
19
|
/**
|
|
20
20
|
* @typedef {object} AppImageOptions
|
|
21
21
|
* @property {string} appDir Path to a built NW.js Linux application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "linux"`.
|
|
22
|
-
* @property {string} appName Name of the application. Must match the `app.name` value used to build `appDir` - the executable and desktop entry are expected at `<appDir>/<appName>` and `<appDir>/<appName>.desktop`.
|
|
22
|
+
* @property {string} appName Name of the application. Must match the `app.name` value used to build `appDir` - the executable and desktop entry are expected at `<appDir>/<appName>` and `<appDir>/<appName>.desktop`. Must not contain `/` or control characters.
|
|
23
23
|
* @property {string} [icon] Path to a `.png` or `.svg` icon. Defaults to the `Icon` value read from `<appDir>/<appName>.desktop`.
|
|
24
24
|
* @property {"ia32" | "x64" | "arm64"} [arch] Target architecture. Defaults to the host architecture. `appimagetool` runs as a native binary, so this must match the host unless the host has emulation (eg. QEMU/binfmt) configured for the target architecture.
|
|
25
25
|
* @property {string} [outDir] Directory the resulting `.AppImage` file is written to. Defaults to the parent directory of `appDir`.
|
|
26
26
|
* @property {string} [cacheDir="./cache"] Directory used to cache the downloaded `appimagetool` binary.
|
|
27
27
|
* @property {boolean} [cache=true] If true, reuse a cached `appimagetool` binary. Otherwise redownload it.
|
|
28
|
-
* @property {string} [appImageToolUrl] Base URL `appimagetool-<arch>.AppImage` is downloaded from. Defaults to the `AppImage/appimagetool` "continuous" GitHub release.
|
|
28
|
+
* @property {string} [appImageToolUrl] Base URL `appimagetool-<arch>.AppImage` is downloaded from. Defaults to the `AppImage/appimagetool` "continuous" GitHub release. Must be https, except for localhost.
|
|
29
|
+
* @property {boolean} [sign=false] If true, embed a GPG signature in the AppImage. Requires `gpg` and a usable secret key on the host. A passphrase-protected key reads its passphrase from the `APPIMAGETOOL_SIGN_PASSPHRASE` environment variable.
|
|
30
|
+
* @property {string} [signKey] ID of the GPG key to sign with. Defaults to `gpg`'s default secret key. Requires `sign` to be true.
|
|
31
|
+
* @property {string} [version] Version of the application, eg. `"1.2.0"`. Required when `publish` is set: it is written to the update info file `@nwutils/updater` compares against.
|
|
32
|
+
* @property {PublishOptions} [publish] Where releases are published. When set, an `app-update.yml` pointing at it is embedded in the AppImage and a `latest-linux[-<arch>].yml` update info file is written next to it, for `@nwutils/updater` to self update from.
|
|
33
|
+
* @property {string | boolean} [updateInformation] AppImage update information for zsync based updaters (eg. AppImageUpdate, `appimageupdatetool`, AppImage managers). `true` derives it from `publish`. A string is embedded as-is, eg. `"gh-releases-zsync|owner|repo|latest|Demo-x86_64.AppImage.zsync"`. Requires `zsyncmake` on the host to produce the `.zsync` file.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Publish to a GitHub repository's releases. The AppImage and update info
|
|
38
|
+
* file must be uploaded as assets of the latest (non draft, non prerelease)
|
|
39
|
+
* release.
|
|
40
|
+
* @typedef {object} GitHubPublishOptions
|
|
41
|
+
* @property {"github"} provider
|
|
42
|
+
* @property {string} owner Owner of the GitHub repository.
|
|
43
|
+
* @property {string} repo Name of the GitHub repository.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Publish to any static file server. The AppImage and update info file must
|
|
48
|
+
* be served from `url`.
|
|
49
|
+
* @typedef {object} GenericPublishOptions
|
|
50
|
+
* @property {"generic"} provider
|
|
51
|
+
* @property {string} url Base URL the latest release's files are served from.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {GitHubPublishOptions | GenericPublishOptions} PublishOptions
|
|
29
56
|
*/
|
|
30
57
|
|
|
31
58
|
/**
|
|
@@ -47,6 +74,11 @@ async function appImage({
|
|
|
47
74
|
cacheDir = "./cache",
|
|
48
75
|
cache = true,
|
|
49
76
|
appImageToolUrl = APPIMAGETOOL_URL,
|
|
77
|
+
sign = false,
|
|
78
|
+
signKey,
|
|
79
|
+
version,
|
|
80
|
+
publish,
|
|
81
|
+
updateInformation,
|
|
50
82
|
}) {
|
|
51
83
|
if (process.platform !== "linux") {
|
|
52
84
|
throw new Error(
|
|
@@ -69,6 +101,68 @@ async function appImage({
|
|
|
69
101
|
`Expected "options.appName" to be a non-empty string. Received: ${JSON.stringify(appName)}`,
|
|
70
102
|
);
|
|
71
103
|
}
|
|
104
|
+
/*
|
|
105
|
+
* `appName` names files inside `appDir` and `outDir`, and is written into
|
|
106
|
+
* the desktop entry and `AppRun`: a path separator or `..` would reach
|
|
107
|
+
* outside those directories, and a line break would inject desktop entries.
|
|
108
|
+
*/
|
|
109
|
+
const hasControlCharacter = [...appName].some((character) => {
|
|
110
|
+
const code = character.charCodeAt(0);
|
|
111
|
+
return code < 0x20 || code === 0x7f;
|
|
112
|
+
});
|
|
113
|
+
if (
|
|
114
|
+
appName.includes("/") ||
|
|
115
|
+
hasControlCharacter ||
|
|
116
|
+
appName === "." ||
|
|
117
|
+
appName === ".."
|
|
118
|
+
) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Expected "options.appName" to be a file name without "/" or control characters. Received: ${JSON.stringify(appName)}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof sign !== "boolean") {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Expected "options.sign" to be a boolean. Received: ${JSON.stringify(sign)}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (signKey !== undefined) {
|
|
131
|
+
if (typeof signKey !== "string" || signKey === "") {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Expected "options.signKey" to be a non-empty string. Received: ${JSON.stringify(signKey)}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (sign === false) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
'"options.signKey" was passed but "options.sign" is false. Set "options.sign" to true to sign the AppImage.',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (publish !== undefined) {
|
|
144
|
+
validatePublish(publish);
|
|
145
|
+
if (typeof version !== "string" || version === "") {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Expected "options.version" to be a non-empty string when "options.publish" is set. Received: ${JSON.stringify(version)}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
updateInformation !== undefined &&
|
|
154
|
+
typeof updateInformation !== "boolean" &&
|
|
155
|
+
(typeof updateInformation !== "string" || updateInformation === "")
|
|
156
|
+
) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Expected "options.updateInformation" to be a boolean or a non-empty string. Received: ${JSON.stringify(updateInformation)}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (updateInformation === true && publish === undefined) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'"options.updateInformation" is true but "options.publish" is not set. Set "options.publish" to derive it, or pass the update information string directly.',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
72
166
|
|
|
73
167
|
const resolvedArch =
|
|
74
168
|
/** @type {"ia32" | "x64" | "arm64" | undefined} */ (arch) ??
|
|
@@ -76,7 +170,10 @@ async function appImage({
|
|
|
76
170
|
/** @type {unknown} */ (process.arch)
|
|
77
171
|
);
|
|
78
172
|
|
|
79
|
-
|
|
173
|
+
/* `Object.hasOwn` so inherited keys such as "constructor" aren't mistaken for architectures. */
|
|
174
|
+
const appImageToolArch = Object.hasOwn(util.APPIMAGE_ARCH_KV, resolvedArch)
|
|
175
|
+
? util.APPIMAGE_ARCH_KV[resolvedArch]
|
|
176
|
+
: undefined;
|
|
80
177
|
if (appImageToolArch === undefined) {
|
|
81
178
|
throw new Error(
|
|
82
179
|
`Expected "options.arch" to be "ia32", "x64" or "arm64". Received: ${JSON.stringify(arch)}`,
|
|
@@ -153,7 +250,7 @@ async function appImage({
|
|
|
153
250
|
const appImageDesktopEntry = {
|
|
154
251
|
...desktopEntry,
|
|
155
252
|
Name: desktopEntry.Name ?? appName,
|
|
156
|
-
Exec: appName,
|
|
253
|
+
Exec: util.desktopExecArg(appName),
|
|
157
254
|
Icon: path.basename(iconFileName, path.extname(iconFileName)),
|
|
158
255
|
Categories: desktopEntry.Categories || "Utility;",
|
|
159
256
|
};
|
|
@@ -168,21 +265,79 @@ async function appImage({
|
|
|
168
265
|
[
|
|
169
266
|
"#!/bin/sh",
|
|
170
267
|
'HERE="$(dirname "$(readlink -f "${0}")")"',
|
|
171
|
-
|
|
268
|
+
/* Single quoted, so the app name is never expanded or run by the shell. */
|
|
269
|
+
`exec "\${HERE}"/${util.shellQuote(appName)} "$@"`,
|
|
172
270
|
"",
|
|
173
271
|
].join("\n"),
|
|
174
272
|
);
|
|
175
273
|
await fs.promises.chmod(appRunPath, 0o755);
|
|
176
274
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
275
|
+
/*
|
|
276
|
+
* Sits next to the NW.js executable, ie. at
|
|
277
|
+
* `path.dirname(process.execPath)` at runtime, which is where
|
|
278
|
+
* `@nwutils/updater` looks for it.
|
|
279
|
+
*/
|
|
280
|
+
if (publish !== undefined) {
|
|
281
|
+
await fs.promises.writeFile(
|
|
282
|
+
path.resolve(appImageDir, "app-update.yml"),
|
|
283
|
+
util.stringifyYaml(
|
|
284
|
+
publish.provider === "github"
|
|
285
|
+
? {
|
|
286
|
+
provider: publish.provider,
|
|
287
|
+
owner: publish.owner,
|
|
288
|
+
repo: publish.repo,
|
|
289
|
+
}
|
|
290
|
+
: { provider: publish.provider, url: publish.url },
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const appImageFileName = `${appName}-${appImageToolArch}.AppImage`;
|
|
296
|
+
const appImageFilePath = path.resolve(resolvedOutDir, appImageFileName);
|
|
297
|
+
const zsyncFilePath = `${appImageFilePath}.zsync`;
|
|
298
|
+
|
|
299
|
+
const resolvedUpdateInformation =
|
|
300
|
+
updateInformation === true
|
|
301
|
+
? deriveUpdateInformation(
|
|
302
|
+
/** @type {PublishOptions} */ (publish),
|
|
303
|
+
path.basename(zsyncFilePath),
|
|
304
|
+
)
|
|
305
|
+
: updateInformation || undefined;
|
|
306
|
+
|
|
307
|
+
/*
|
|
308
|
+
* Signing happens inside the same `appimagetool` run that builds the
|
|
309
|
+
* file: an embedded signature can't be added to an existing AppImage.
|
|
310
|
+
*/
|
|
311
|
+
const signArgs = sign
|
|
312
|
+
? ["--sign", ...(signKey === undefined ? [] : ["--sign-key", signKey])]
|
|
313
|
+
: [];
|
|
314
|
+
|
|
315
|
+
const updateInformationArgs =
|
|
316
|
+
resolvedUpdateInformation === undefined
|
|
317
|
+
? []
|
|
318
|
+
: ["--updateinformation", resolvedUpdateInformation];
|
|
319
|
+
|
|
320
|
+
if (resolvedUpdateInformation !== undefined) {
|
|
321
|
+
/* Don't let a `.zsync` file left over from a previous build pass the check below. */
|
|
322
|
+
await fs.promises.rm(zsyncFilePath, { force: true });
|
|
323
|
+
}
|
|
181
324
|
|
|
182
325
|
child_process.execFileSync(
|
|
183
326
|
appImageToolPath,
|
|
184
|
-
[
|
|
327
|
+
[
|
|
328
|
+
"--no-appstream",
|
|
329
|
+
...signArgs,
|
|
330
|
+
...updateInformationArgs,
|
|
331
|
+
appImageDir,
|
|
332
|
+
appImageFilePath,
|
|
333
|
+
],
|
|
185
334
|
{
|
|
335
|
+
/*
|
|
336
|
+
* `appimagetool` runs `zsyncmake` without an output path, which
|
|
337
|
+
* writes the `.zsync` file to the working directory - run from
|
|
338
|
+
* `outDir` so it lands next to the AppImage.
|
|
339
|
+
*/
|
|
340
|
+
cwd: resolvedOutDir,
|
|
186
341
|
env: {
|
|
187
342
|
...process.env,
|
|
188
343
|
ARCH: appImageToolArch,
|
|
@@ -197,12 +352,152 @@ async function appImage({
|
|
|
197
352
|
},
|
|
198
353
|
);
|
|
199
354
|
|
|
355
|
+
/*
|
|
356
|
+
* `appimagetool` only warns, and still exits successfully, when it
|
|
357
|
+
* can't find `zsyncmake` - leaving an AppImage that advertises a
|
|
358
|
+
* `.zsync` file which was never produced.
|
|
359
|
+
*/
|
|
360
|
+
if (
|
|
361
|
+
resolvedUpdateInformation !== undefined &&
|
|
362
|
+
(await util.fileExists(zsyncFilePath)) === false
|
|
363
|
+
) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`"appimagetool" did not produce ${zsyncFilePath}. Install "zsyncmake" (usually packaged as "zsync") on the host to build AppImages with "options.updateInformation".`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (publish !== undefined) {
|
|
370
|
+
const appImageSha512 = await util.sha512(appImageFilePath);
|
|
371
|
+
const { size } = await fs.promises.stat(appImageFilePath);
|
|
372
|
+
await fs.promises.writeFile(
|
|
373
|
+
path.resolve(resolvedOutDir, util.updateInfoFileName(resolvedArch)),
|
|
374
|
+
util.stringifyYaml({
|
|
375
|
+
version: /** @type {string} */ (version),
|
|
376
|
+
files: [{ url: appImageFileName, sha512: appImageSha512, size }],
|
|
377
|
+
path: appImageFileName,
|
|
378
|
+
sha512: appImageSha512,
|
|
379
|
+
releaseDate: new Date().toISOString(),
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
200
384
|
return appImageFilePath;
|
|
201
385
|
} finally {
|
|
202
386
|
await fs.promises.rm(appImageDir, { recursive: true, force: true });
|
|
203
387
|
}
|
|
204
388
|
}
|
|
205
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Throw if `publish` isn't a valid `PublishOptions` object.
|
|
392
|
+
* @param {unknown} publish
|
|
393
|
+
* @returns {asserts publish is PublishOptions}
|
|
394
|
+
*/
|
|
395
|
+
function validatePublish(publish) {
|
|
396
|
+
if (typeof publish !== "object" || publish === null) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Expected "options.publish" to be an object. Received: ${JSON.stringify(publish)}`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const { provider } = /** @type {{provider?: unknown}} */ (publish);
|
|
403
|
+
/** @type {string[]} */
|
|
404
|
+
let requiredKeys;
|
|
405
|
+
if (provider === "github") {
|
|
406
|
+
requiredKeys = ["owner", "repo"];
|
|
407
|
+
} else if (provider === "generic") {
|
|
408
|
+
requiredKeys = ["url"];
|
|
409
|
+
} else {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`Expected "options.publish.provider" to be "github" or "generic". Received: ${JSON.stringify(provider)}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (const key of requiredKeys) {
|
|
416
|
+
const value = /** @type {Record<string, unknown>} */ (publish)[key];
|
|
417
|
+
if (typeof value !== "string" || value === "") {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Expected "options.publish.${key}" to be a non-empty string for provider "${provider}". Received: ${JSON.stringify(value)}`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (provider === "github") {
|
|
425
|
+
/*
|
|
426
|
+
* The characters GitHub allows in owner and repository names. Anything
|
|
427
|
+
* else - eg. "|", the update information's field separator - is rejected.
|
|
428
|
+
*/
|
|
429
|
+
for (const key of requiredKeys) {
|
|
430
|
+
const value = /** @type {Record<string, string>} */ (publish)[key];
|
|
431
|
+
if (/^[A-Za-z0-9._-]+$/.test(value) === false) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`Expected "options.publish.${key}" to be a GitHub ${key} name (letters, digits, ".", "-" and "_"). Received: ${JSON.stringify(value)}`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (provider === "generic") {
|
|
440
|
+
const { url } = /** @type {GenericPublishOptions} */ (publish);
|
|
441
|
+
let protocol;
|
|
442
|
+
try {
|
|
443
|
+
protocol = new URL(url).protocol;
|
|
444
|
+
} catch {
|
|
445
|
+
protocol = undefined;
|
|
446
|
+
}
|
|
447
|
+
if (protocol !== "https:" && protocol !== "http:") {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`Expected "options.publish.url" to be an http(s) URL. Received: ${JSON.stringify(url)}`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Build the AppImage update information string for `publish`, in the format
|
|
457
|
+
* zsync based AppImage updaters understand.
|
|
458
|
+
* @param {PublishOptions} publish
|
|
459
|
+
* @param {string} zsyncFileName Name the `.zsync` file is published under.
|
|
460
|
+
* @returns {string}
|
|
461
|
+
*/
|
|
462
|
+
function deriveUpdateInformation(publish, zsyncFileName) {
|
|
463
|
+
if (publish.provider === "github") {
|
|
464
|
+
return `gh-releases-zsync|${publish.owner}|${publish.repo}|latest|${zsyncFileName}`;
|
|
465
|
+
}
|
|
466
|
+
/* Trim trailing slashes with a linear scan rather than a backtracking regular expression. */
|
|
467
|
+
let end = publish.url.length;
|
|
468
|
+
while (end > 0 && publish.url[end - 1] === "/") {
|
|
469
|
+
end--;
|
|
470
|
+
}
|
|
471
|
+
return `zsync|${publish.url.slice(0, end)}/${zsyncFileName}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Throw unless `appImageToolUrl` is an https URL. The downloaded binary is
|
|
476
|
+
* executed, so a plain http download would let anyone on the network path
|
|
477
|
+
* replace it. http is only accepted for loopback hosts, eg. a local mirror.
|
|
478
|
+
* @param {string} appImageToolUrl
|
|
479
|
+
* @returns {void}
|
|
480
|
+
*/
|
|
481
|
+
function validateAppImageToolUrl(appImageToolUrl) {
|
|
482
|
+
let url;
|
|
483
|
+
try {
|
|
484
|
+
url = new URL(appImageToolUrl);
|
|
485
|
+
} catch {
|
|
486
|
+
url = undefined;
|
|
487
|
+
}
|
|
488
|
+
const isLoopback =
|
|
489
|
+
url !== undefined &&
|
|
490
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
491
|
+
if (
|
|
492
|
+
url === undefined ||
|
|
493
|
+
(url.protocol !== "https:" && (url.protocol !== "http:" || !isLoopback))
|
|
494
|
+
) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Expected "options.appImageToolUrl" to be an https URL (http is only allowed for localhost). Received: ${JSON.stringify(appImageToolUrl)}`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
206
501
|
/**
|
|
207
502
|
* Download (or reuse a cached) `appimagetool` binary for `appImageToolArch`.
|
|
208
503
|
* @async
|
|
@@ -220,6 +515,8 @@ async function getAppImageTool({
|
|
|
220
515
|
cacheDir,
|
|
221
516
|
cache,
|
|
222
517
|
}) {
|
|
518
|
+
validateAppImageToolUrl(appImageToolUrl);
|
|
519
|
+
|
|
223
520
|
const appImageToolPath = path.resolve(
|
|
224
521
|
cacheDir,
|
|
225
522
|
`appimagetool-${appImageToolArch}.AppImage`,
|