@nwutils/packager 0.7.0 → 0.8.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 +83 -6
- package/package.json +3 -3
- package/src/dmg.js +277 -0
- package/src/index.js +8 -6
- package/src/util.js +12 -8
- package/types/dmg.d.ts +80 -0
- package/types/index.d.ts +7 -7
- package/types/util.d.ts +7 -8
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Supported output formats, selected via `format`:
|
|
|
13
13
|
| `rpm` | Linux | Planned |
|
|
14
14
|
| `msix` | Windows | Planned |
|
|
15
15
|
| `nsis` | Windows | Implemented |
|
|
16
|
-
| `dmg` | MacOS |
|
|
16
|
+
| `dmg` | MacOS | Implemented |
|
|
17
17
|
|
|
18
18
|
## Getting Started
|
|
19
19
|
|
|
@@ -65,7 +65,7 @@ keyring. If the key is protected by a passphrase, set it in the
|
|
|
65
65
|
### Self updating AppImages
|
|
66
66
|
|
|
67
67
|
Set `publish` to where releases are uploaded, and `version` to the app's
|
|
68
|
-
version.
|
|
68
|
+
version.
|
|
69
69
|
|
|
70
70
|
- an `app-update.yml` describing `publish` is embedded next to the NW.js
|
|
71
71
|
executable inside the AppImage, so the running app knows where to look
|
|
@@ -174,14 +174,71 @@ running application to exit instead of asking the user to close it, updates it
|
|
|
174
174
|
silently, and starts the new version. Because it installs for the current user,
|
|
175
175
|
updating never asks for administrator rights.
|
|
176
176
|
|
|
177
|
+
### DMG
|
|
178
|
+
|
|
179
|
+
Build the application with `platform: "osx"` first, then, on a MacOS host:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const dmgPath = await packager({
|
|
183
|
+
format: "dmg",
|
|
184
|
+
appDir: "./out",
|
|
185
|
+
appName: "Demo",
|
|
186
|
+
arch: "arm64",
|
|
187
|
+
outDir: "./dist",
|
|
188
|
+
signIdentity: "Developer ID Application: ACME (TEAMID)",
|
|
189
|
+
});
|
|
190
|
+
// ./dist/Demo-arm64.dmg
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The DMG holds the app next to a link to `/Applications`, so users install it by
|
|
194
|
+
dragging the app onto the link. It is made with `hdiutil`, and the app is signed
|
|
195
|
+
with `codesign` when `signIdentity` is set (`"-"` signs ad-hoc). Both are part
|
|
196
|
+
of MacOS, so DMGs are built on a MacOS host. Notarize the DMG afterwards, eg.
|
|
197
|
+
with `xcrun notarytool submit --wait` and `xcrun stapler staple`, so Gatekeeper
|
|
198
|
+
lets users open it.
|
|
199
|
+
|
|
200
|
+
### Self updating DMGs
|
|
201
|
+
|
|
202
|
+
Set `publish` and `version`. Updates don't use the DMG:
|
|
203
|
+
|
|
204
|
+
- an `app-update.yml` describing `publish` is added to the app's
|
|
205
|
+
`Contents/Resources`
|
|
206
|
+
- a `<appName>-<arch>-mac.zip` of the app, made with `ditto` so permissions,
|
|
207
|
+
symbolic links and the signature survive, is written next to the DMG
|
|
208
|
+
- a `latest-mac.yml` update info file (`latest-mac-<arch>.yml` for
|
|
209
|
+
architectures other than `x64`) describing the zip is written next to it
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
await packager({
|
|
213
|
+
format: "dmg",
|
|
214
|
+
appDir: "./out",
|
|
215
|
+
appName: "Demo",
|
|
216
|
+
arch: "arm64",
|
|
217
|
+
outDir: "./dist",
|
|
218
|
+
version: "1.2.0",
|
|
219
|
+
signIdentity: "Developer ID Application: ACME (TEAMID)",
|
|
220
|
+
publish: { provider: "github", owner: "nwutils", repo: "demo" },
|
|
221
|
+
});
|
|
222
|
+
// ./dist/Demo-arm64.dmg
|
|
223
|
+
// ./dist/Demo-arm64-mac.zip
|
|
224
|
+
// ./dist/latest-mac-arm64.yml
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Upload all three to every release.
|
|
228
|
+
[`@nwutils/updater`](https://www.npmjs.com/package/@nwutils/updater)'s
|
|
229
|
+
`MacUpdater` downloads the zip, checks it, swaps it in for the running app and
|
|
230
|
+
restarts it. Adding `app-update.yml` changes the app, so sign it here with
|
|
231
|
+
`signIdentity` rather than before packaging. A signed app only accepts updates
|
|
232
|
+
signed by the same team.
|
|
233
|
+
|
|
177
234
|
## API Reference
|
|
178
235
|
|
|
179
236
|
### `packager({ format, ...options })`
|
|
180
237
|
|
|
181
|
-
| Name | Type | Description
|
|
182
|
-
| ------ | ----------------------------------------------------------- |
|
|
183
|
-
| format | `"AppImage" \| "deb" \| "rpm" \| "msix" \| "nsis" \| "dmg"` | Which packager to run. `"AppImage"` and `"
|
|
184
|
-
| ... | - | The remaining options are passed through to the packager for `format` as-is.
|
|
238
|
+
| Name | Type | Description |
|
|
239
|
+
| ------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
240
|
+
| format | `"AppImage" \| "deb" \| "rpm" \| "msix" \| "nsis" \| "dmg"` | Which packager to run. `"AppImage"`, `"nsis"` and `"dmg"` are implemented today. |
|
|
241
|
+
| ... | - | The remaining options are passed through to the packager for `format` as-is. |
|
|
185
242
|
|
|
186
243
|
Resolves with the path to the resulting packaged artifact.
|
|
187
244
|
|
|
@@ -230,6 +287,26 @@ Options
|
|
|
230
287
|
|
|
231
288
|
Resolves with the path to the resulting installer `.exe` file.
|
|
232
289
|
|
|
290
|
+
### `dmg(options)`
|
|
291
|
+
|
|
292
|
+
Options
|
|
293
|
+
|
|
294
|
+
| Name | Type | Description |
|
|
295
|
+
| ------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
296
|
+
| appDir | `string` | Path to a built NW.js MacOS application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "osx"` |
|
|
297
|
+
| appName | `string` | Name of the application. Must match the `app.name` value used to build `appDir`: the bundle is expected at `<appDir>/<appName>.app` |
|
|
298
|
+
| arch | `"x64" \| "arm64"` | Architecture `appDir` was built for. Defaults to the host architecture |
|
|
299
|
+
| outDir | `string` | Directory the resulting `<appName>-<arch>.dmg` file is written to. Defaults to the parent directory of `appDir` |
|
|
300
|
+
| volumeName | `string` | Name of the volume the DMG mounts as. Defaults to `appName` |
|
|
301
|
+
| signIdentity | `string` | Code signing identity to sign the app with, or `"-"` to sign ad-hoc. Not signed by default |
|
|
302
|
+
| version | `string` | Version of the application. Required with `publish` |
|
|
303
|
+
| publish | `PublishOptions` | Where releases are published. Adds `app-update.yml` and writes `<appName>-<arch>-mac.zip` and `latest-mac[-<arch>].yml` to `outDir` |
|
|
304
|
+
| hdiutilPath | `string` | Path to `hdiutil`. Defaults to `hdiutil` on the `PATH` |
|
|
305
|
+
| dittoPath | `string` | Path to `ditto`. Defaults to `ditto` on the `PATH` |
|
|
306
|
+
| codesignPath | `string` | Path to `codesign`. Defaults to `codesign` on the `PATH` |
|
|
307
|
+
|
|
308
|
+
Resolves with the path to the resulting `.dmg` file.
|
|
309
|
+
|
|
233
310
|
## Contributing
|
|
234
311
|
|
|
235
312
|
### External contributor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwutils/packager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Package NW.js applications for Linux, MacOS and Windows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"NW.js",
|
|
@@ -43,8 +43,8 @@
|
|
|
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/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\""
|
|
46
|
+
"test": "node --test --test-concurrency=1 \"../../tests/specs/packager/appImage.test.js\" \"../../tests/specs/packager/dmg.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/dmg.real.test.js\" \"../../tests/specs/packager/nsis.real.test.js\""
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {},
|
|
50
50
|
"devEngines": {
|
package/src/dmg.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import child_process from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
import util from "./util.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} DmgOptions
|
|
11
|
+
* @property {string} appDir Path to a built NW.js MacOS application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "osx"`.
|
|
12
|
+
* @property {string} appName Name of the application. Must match the `app.name` value used to build `appDir` - the bundle is expected at `<appDir>/<appName>.app`.
|
|
13
|
+
* @property {"x64" | "arm64"} [arch] Architecture `appDir` was built for. Defaults to the host architecture.
|
|
14
|
+
* @property {string} [outDir] Directory the resulting `<appName>-<arch>.dmg` file is written to. Defaults to the parent directory of `appDir`.
|
|
15
|
+
* @property {string} [volumeName] Name of the volume the DMG mounts as. Defaults to `appName`.
|
|
16
|
+
* @property {string} [signIdentity] Code signing identity, eg. `"Developer ID Application: ACME (TEAMID)"`, or `"-"` to sign ad-hoc. When set, the app is signed with `codesign` before it is put in the DMG. Sign here rather than before packaging when `publish` is set, since adding `app-update.yml` changes the bundle.
|
|
17
|
+
* @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.
|
|
18
|
+
* @property {import("./appImage.js").PublishOptions} [publish] Where releases are published. When set, an `app-update.yml` pointing at it is added to the app's `Contents/Resources`, and a `<appName>-<arch>-mac.zip` update archive and a `latest-mac[-<arch>].yml` update info file are written next to the DMG, for `@nwutils/updater` to self update from.
|
|
19
|
+
* @property {string} [hdiutilPath="hdiutil"] Path to `hdiutil`, which creates the DMG.
|
|
20
|
+
* @property {string} [dittoPath="ditto"] Path to `ditto`, which creates the update archive.
|
|
21
|
+
* @property {string} [codesignPath="codesign"] Path to `codesign`, which signs the app.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Package a built NW.js MacOS application as a DMG disk image.
|
|
26
|
+
*
|
|
27
|
+
* The DMG holds the app next to a link to `/Applications`, so users install it
|
|
28
|
+
* by dragging the app onto the link. Updates don't use the DMG: with
|
|
29
|
+
* `publish` set, a zip of the app is written next to it, which
|
|
30
|
+
* `@nwutils/updater`'s `MacUpdater` downloads and swaps in for the running
|
|
31
|
+
* app.
|
|
32
|
+
*
|
|
33
|
+
* This composes with `@nwutils/builder`: run it against the `outDir` (and
|
|
34
|
+
* `app.name`) that `@nwutils/builder` produced for `platform: "osx"`.
|
|
35
|
+
* `hdiutil`, `ditto` and `codesign` are part of MacOS, so this runs on a MacOS
|
|
36
|
+
* host.
|
|
37
|
+
* @async
|
|
38
|
+
* @function
|
|
39
|
+
* @param {DmgOptions} options
|
|
40
|
+
* @returns {Promise<string>} Path to the resulting `.dmg` file.
|
|
41
|
+
*/
|
|
42
|
+
async function dmg({
|
|
43
|
+
appDir,
|
|
44
|
+
appName,
|
|
45
|
+
arch,
|
|
46
|
+
outDir,
|
|
47
|
+
volumeName,
|
|
48
|
+
signIdentity,
|
|
49
|
+
version,
|
|
50
|
+
publish,
|
|
51
|
+
hdiutilPath = "hdiutil",
|
|
52
|
+
dittoPath = "ditto",
|
|
53
|
+
codesignPath = "codesign",
|
|
54
|
+
}) {
|
|
55
|
+
if (typeof appDir !== "string" || appDir === "") {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Expected "options.appDir" to be a non-empty string. Received: ${JSON.stringify(appDir)}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ((await util.fileExists(appDir)) === false) {
|
|
62
|
+
throw new Error(`"options.appDir" does not exist: ${appDir}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* `appName` names the bundle and the files written to `outDir`. */
|
|
66
|
+
if (
|
|
67
|
+
typeof appName !== "string" ||
|
|
68
|
+
appName === "" ||
|
|
69
|
+
appName.includes("/") ||
|
|
70
|
+
appName.startsWith(".") ||
|
|
71
|
+
hasControlCharacter(appName)
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Expected "options.appName" to be a non-empty file name without "/" or control characters, not starting with ".". Received: ${JSON.stringify(appName)}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const resolvedArch =
|
|
79
|
+
/** @type {"x64" | "arm64" | undefined} */ (arch) ??
|
|
80
|
+
/** @type {"x64" | "arm64"} */ (/** @type {unknown} */ (process.arch));
|
|
81
|
+
if (resolvedArch !== "x64" && resolvedArch !== "arm64") {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Expected "options.arch" to be "x64" or "arm64". Received: ${JSON.stringify(arch ?? process.arch)}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const resolvedVolumeName = volumeName ?? appName;
|
|
88
|
+
if (
|
|
89
|
+
typeof resolvedVolumeName !== "string" ||
|
|
90
|
+
resolvedVolumeName === "" ||
|
|
91
|
+
hasControlCharacter(resolvedVolumeName)
|
|
92
|
+
) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Expected "options.volumeName" to be a non-empty string without control characters. Received: ${JSON.stringify(volumeName)}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
signIdentity !== undefined &&
|
|
100
|
+
(typeof signIdentity !== "string" || signIdentity === "")
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Expected "options.signIdentity" to be a non-empty string. Received: ${JSON.stringify(signIdentity)}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (publish !== undefined) {
|
|
108
|
+
util.validatePublish(publish);
|
|
109
|
+
if (typeof version !== "string" || version === "") {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Expected "options.version" to be a non-empty string when "options.publish" is set. Received: ${JSON.stringify(version)}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const [key, value] of Object.entries({
|
|
117
|
+
hdiutilPath,
|
|
118
|
+
dittoPath,
|
|
119
|
+
codesignPath,
|
|
120
|
+
})) {
|
|
121
|
+
if (typeof value !== "string" || value === "") {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Expected "options.${key}" to be a non-empty string. Received: ${JSON.stringify(value)}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const resolvedAppDir = path.resolve(appDir);
|
|
129
|
+
const appBundleName = `${appName}.app`;
|
|
130
|
+
const appBundlePath = path.resolve(resolvedAppDir, appBundleName);
|
|
131
|
+
if ((await util.fileExists(appBundlePath)) === false) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Expected the app bundle to exist at ${appBundlePath}. Does "options.appName" match the "app.name" used to build "options.appDir" for "platform: \\"osx\\""?`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const resolvedOutDir = path.resolve(outDir ?? path.dirname(resolvedAppDir));
|
|
138
|
+
await fs.promises.mkdir(resolvedOutDir, { recursive: true });
|
|
139
|
+
const dmgPath = path.resolve(
|
|
140
|
+
resolvedOutDir,
|
|
141
|
+
`${appName}-${resolvedArch}.dmg`,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const stagingDir = await fs.promises.mkdtemp(
|
|
145
|
+
path.join(os.tmpdir(), "nwutils-packager-dmg-"),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
/* NW.js' frameworks are full of symbolic links, which must stay links for the signature to hold. */
|
|
150
|
+
const stagedAppPath = path.resolve(stagingDir, appBundleName);
|
|
151
|
+
await fs.promises.cp(appBundlePath, stagedAppPath, {
|
|
152
|
+
recursive: true,
|
|
153
|
+
verbatimSymlinks: true,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/*
|
|
157
|
+
* `Contents/Resources` is where `@nwutils/updater` looks for it.
|
|
158
|
+
* It's inside the signed bundle, so
|
|
159
|
+
* signing comes after.
|
|
160
|
+
*/
|
|
161
|
+
if (publish !== undefined) {
|
|
162
|
+
await fs.promises.writeFile(
|
|
163
|
+
path.resolve(stagedAppPath, "Contents", "Resources", "app-update.yml"),
|
|
164
|
+
util.appUpdateYaml(publish),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (signIdentity !== undefined) {
|
|
169
|
+
run(
|
|
170
|
+
codesignPath,
|
|
171
|
+
[
|
|
172
|
+
"--force",
|
|
173
|
+
"--deep",
|
|
174
|
+
"--sign",
|
|
175
|
+
signIdentity,
|
|
176
|
+
/* Notarization requires the hardened runtime and a secure timestamp, which ad-hoc signatures can't have. */
|
|
177
|
+
...(signIdentity === "-"
|
|
178
|
+
? []
|
|
179
|
+
: ["--options", "runtime", "--timestamp"]),
|
|
180
|
+
stagedAppPath,
|
|
181
|
+
],
|
|
182
|
+
"codesign",
|
|
183
|
+
"codesignPath",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Lets users install by dragging the app onto it. */
|
|
188
|
+
await fs.promises.symlink(
|
|
189
|
+
"/Applications",
|
|
190
|
+
path.resolve(stagingDir, "Applications"),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
run(
|
|
194
|
+
hdiutilPath,
|
|
195
|
+
[
|
|
196
|
+
"create",
|
|
197
|
+
"-volname",
|
|
198
|
+
resolvedVolumeName,
|
|
199
|
+
"-srcfolder",
|
|
200
|
+
stagingDir,
|
|
201
|
+
"-fs",
|
|
202
|
+
"HFS+",
|
|
203
|
+
"-format",
|
|
204
|
+
"UDZO",
|
|
205
|
+
"-ov",
|
|
206
|
+
dmgPath,
|
|
207
|
+
],
|
|
208
|
+
"hdiutil",
|
|
209
|
+
"hdiutilPath",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (publish !== undefined) {
|
|
213
|
+
/*
|
|
214
|
+
* `ditto` keeps the permissions, symbolic links and extended attributes
|
|
215
|
+
* a signed app bundle needs, unlike most zip tools. `--keepParent` puts
|
|
216
|
+
* `<appName>.app` itself at the archive's root.
|
|
217
|
+
*/
|
|
218
|
+
const zipPath = path.resolve(
|
|
219
|
+
resolvedOutDir,
|
|
220
|
+
`${appName}-${resolvedArch}-mac.zip`,
|
|
221
|
+
);
|
|
222
|
+
run(
|
|
223
|
+
dittoPath,
|
|
224
|
+
["-c", "-k", "--sequesterRsrc", "--keepParent", stagedAppPath, zipPath],
|
|
225
|
+
"ditto",
|
|
226
|
+
"dittoPath",
|
|
227
|
+
);
|
|
228
|
+
await util.writeUpdateInfo({
|
|
229
|
+
filePath: zipPath,
|
|
230
|
+
updateInfoPath: path.resolve(
|
|
231
|
+
resolvedOutDir,
|
|
232
|
+
util.updateInfoFileName(resolvedArch, "osx"),
|
|
233
|
+
),
|
|
234
|
+
version: /** @type {string} */ (version),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return dmgPath;
|
|
239
|
+
} finally {
|
|
240
|
+
await fs.promises.rm(stagingDir, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {string} value
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
function hasControlCharacter(value) {
|
|
249
|
+
return [...value].some((character) => {
|
|
250
|
+
const code = character.charCodeAt(0);
|
|
251
|
+
return code < 0x20 || code === 0x7f;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Run a MacOS tool, turning a missing tool into an error that says so.
|
|
257
|
+
* @param {string} toolPath Path or name of the tool.
|
|
258
|
+
* @param {string[]} args
|
|
259
|
+
* @param {string} toolName eg. `"hdiutil"`.
|
|
260
|
+
* @param {string} optionName Option that overrides `toolPath`, eg. `"hdiutilPath"`.
|
|
261
|
+
* @returns {void}
|
|
262
|
+
*/
|
|
263
|
+
function run(toolPath, args, toolName, optionName) {
|
|
264
|
+
try {
|
|
265
|
+
child_process.execFileSync(toolPath, args, { stdio: "inherit" });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (/** @type {NodeJS.ErrnoException} */ (error).code === "ENOENT") {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Could not find ${JSON.stringify(toolPath)}. "${toolName}" is part of MacOS, so DMGs are built on a MacOS host. Otherwise pass its path as "options.${optionName}".`,
|
|
270
|
+
{ cause: error },
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default dmg;
|
package/src/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import appImage from "./appImage.js";
|
|
2
|
+
import dmg from "./dmg.js";
|
|
2
3
|
import nsis from "./nsis.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Supported packaged output formats.
|
|
6
7
|
*
|
|
7
8
|
* `AppImage`, `deb` and `rpm` package a Linux build. `MSIX` and `NSIS`
|
|
8
|
-
* package a Windows build. `
|
|
9
|
-
*
|
|
10
|
-
* they land.
|
|
9
|
+
* package a Windows build. `dmg` packages a MacOS build. `"AppImage"`,
|
|
10
|
+
* `"nsis"` and `"dmg"` are implemented today - the others are reserved so
|
|
11
|
+
* this option doesn't need to change shape once they land.
|
|
11
12
|
* @typedef {"AppImage" | "deb" | "rpm" | "msix" | "nsis" | "dmg"} PackageFormat
|
|
12
13
|
*/
|
|
13
14
|
|
|
@@ -20,7 +21,7 @@ import nsis from "./nsis.js";
|
|
|
20
21
|
* Package a built NW.js application.
|
|
21
22
|
* @async
|
|
22
23
|
* @function
|
|
23
|
-
* @param {{format: PackageFormat} & Record<string, any>} options `format`, plus the options the matching packager (eg. `appImage` or `
|
|
24
|
+
* @param {{format: PackageFormat} & Record<string, any>} options `format`, plus the options the matching packager (eg. `appImage`, `nsis` or `dmg`) expects.
|
|
24
25
|
* @returns {Promise<string>} Path to the resulting packaged artifact.
|
|
25
26
|
*/
|
|
26
27
|
async function packager({ format, ...options }) {
|
|
@@ -29,12 +30,13 @@ async function packager({ format, ...options }) {
|
|
|
29
30
|
return appImage(/** @type {any} */ (options));
|
|
30
31
|
case "nsis":
|
|
31
32
|
return nsis(/** @type {any} */ (options));
|
|
33
|
+
case "dmg":
|
|
34
|
+
return dmg(/** @type {any} */ (options));
|
|
32
35
|
case "deb":
|
|
33
36
|
case "rpm":
|
|
34
37
|
case "msix":
|
|
35
|
-
case "dmg":
|
|
36
38
|
throw new Error(
|
|
37
|
-
`options.format ${JSON.stringify(format)} is not implemented yet. Currently only "AppImage" and "
|
|
39
|
+
`options.format ${JSON.stringify(format)} is not implemented yet. Currently only "AppImage", "nsis" and "dmg" are supported.`,
|
|
38
40
|
);
|
|
39
41
|
default:
|
|
40
42
|
throw new Error(
|
package/src/util.js
CHANGED
|
@@ -116,15 +116,20 @@ function desktopExecArg(value) {
|
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
118
|
* Name of the update info file `@nwutils/updater` downloads for `arch` on
|
|
119
|
-
* `platform`,
|
|
120
|
-
* `latest-linux.yml` (Linux)
|
|
121
|
-
* architecture gets an `-<arch>` suffix.
|
|
122
|
-
* @param {string}
|
|
123
|
-
* @param {"linux" | "win"}
|
|
119
|
+
* `platform`, gets the bare
|
|
120
|
+
* `latest-linux.yml` (Linux), `latest.yml` (Windows) or `latest-mac.yml`
|
|
121
|
+
* (MacOS), every other architecture gets an `-<arch>` suffix.
|
|
122
|
+
* @param {string} arch
|
|
123
|
+
* @param {"linux" | "win" | "osx"} [platform="linux"]
|
|
124
124
|
* @returns {string}
|
|
125
125
|
*/
|
|
126
126
|
function updateInfoFileName(arch, platform = "linux") {
|
|
127
|
-
const base =
|
|
127
|
+
const base =
|
|
128
|
+
platform === "win"
|
|
129
|
+
? "latest"
|
|
130
|
+
: platform === "osx"
|
|
131
|
+
? "latest-mac"
|
|
132
|
+
: "latest-linux";
|
|
128
133
|
return arch === "x64" ? `${base}.yml` : `${base}-${arch}.yml`;
|
|
129
134
|
}
|
|
130
135
|
|
|
@@ -169,8 +174,7 @@ function stringifyYaml(entries) {
|
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
/**
|
|
172
|
-
* Compute the base64 encoded SHA-512 digest of the file at `filePath
|
|
173
|
-
* same encoding electron-builder writes into its update info files.
|
|
177
|
+
* Compute the base64 encoded SHA-512 digest of the file at `filePath`.
|
|
174
178
|
* @async
|
|
175
179
|
* @function
|
|
176
180
|
* @param {string} filePath
|
package/types/dmg.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export default dmg;
|
|
2
|
+
export type DmgOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Path to a built NW.js MacOS application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "osx"`.
|
|
5
|
+
*/
|
|
6
|
+
appDir: string;
|
|
7
|
+
/**
|
|
8
|
+
* Name of the application. Must match the `app.name` value used to build `appDir` - the bundle is expected at `<appDir>/<appName>.app`.
|
|
9
|
+
*/
|
|
10
|
+
appName: string;
|
|
11
|
+
/**
|
|
12
|
+
* Architecture `appDir` was built for. Defaults to the host architecture.
|
|
13
|
+
*/
|
|
14
|
+
arch?: "x64" | "arm64" | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Directory the resulting `<appName>-<arch>.dmg` file is written to. Defaults to the parent directory of `appDir`.
|
|
17
|
+
*/
|
|
18
|
+
outDir?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Name of the volume the DMG mounts as. Defaults to `appName`.
|
|
21
|
+
*/
|
|
22
|
+
volumeName?: string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Code signing identity, eg. `"Developer ID Application: ACME (TEAMID)"`, or `"-"` to sign ad-hoc. When set, the app is signed with `codesign` before it is put in the DMG. Sign here rather than before packaging when `publish` is set, since adding `app-update.yml` changes the bundle.
|
|
25
|
+
*/
|
|
26
|
+
signIdentity?: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* 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.
|
|
29
|
+
*/
|
|
30
|
+
version?: string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Where releases are published. When set, an `app-update.yml` pointing at it is added to the app's `Contents/Resources`, and a `<appName>-<arch>-mac.zip` update archive and a `latest-mac[-<arch>].yml` update info file are written next to the DMG, for `@nwutils/updater` to self update from.
|
|
33
|
+
*/
|
|
34
|
+
publish?: import("./appImage.js").PublishOptions | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Path to `hdiutil`, which creates the DMG.
|
|
37
|
+
*/
|
|
38
|
+
hdiutilPath?: string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Path to `ditto`, which creates the update archive.
|
|
41
|
+
*/
|
|
42
|
+
dittoPath?: string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Path to `codesign`, which signs the app.
|
|
45
|
+
*/
|
|
46
|
+
codesignPath?: string | undefined;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {object} DmgOptions
|
|
50
|
+
* @property {string} appDir Path to a built NW.js MacOS application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "osx"`.
|
|
51
|
+
* @property {string} appName Name of the application. Must match the `app.name` value used to build `appDir` - the bundle is expected at `<appDir>/<appName>.app`.
|
|
52
|
+
* @property {"x64" | "arm64"} [arch] Architecture `appDir` was built for. Defaults to the host architecture.
|
|
53
|
+
* @property {string} [outDir] Directory the resulting `<appName>-<arch>.dmg` file is written to. Defaults to the parent directory of `appDir`.
|
|
54
|
+
* @property {string} [volumeName] Name of the volume the DMG mounts as. Defaults to `appName`.
|
|
55
|
+
* @property {string} [signIdentity] Code signing identity, eg. `"Developer ID Application: ACME (TEAMID)"`, or `"-"` to sign ad-hoc. When set, the app is signed with `codesign` before it is put in the DMG. Sign here rather than before packaging when `publish` is set, since adding `app-update.yml` changes the bundle.
|
|
56
|
+
* @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.
|
|
57
|
+
* @property {import("./appImage.js").PublishOptions} [publish] Where releases are published. When set, an `app-update.yml` pointing at it is added to the app's `Contents/Resources`, and a `<appName>-<arch>-mac.zip` update archive and a `latest-mac[-<arch>].yml` update info file are written next to the DMG, for `@nwutils/updater` to self update from.
|
|
58
|
+
* @property {string} [hdiutilPath="hdiutil"] Path to `hdiutil`, which creates the DMG.
|
|
59
|
+
* @property {string} [dittoPath="ditto"] Path to `ditto`, which creates the update archive.
|
|
60
|
+
* @property {string} [codesignPath="codesign"] Path to `codesign`, which signs the app.
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* Package a built NW.js MacOS application as a DMG disk image.
|
|
64
|
+
*
|
|
65
|
+
* The DMG holds the app next to a link to `/Applications`, so users install it
|
|
66
|
+
* by dragging the app onto the link. Updates don't use the DMG: with
|
|
67
|
+
* `publish` set, a zip of the app is written next to it, which
|
|
68
|
+
* `@nwutils/updater`'s `MacUpdater` downloads and swaps in for the running
|
|
69
|
+
* app.
|
|
70
|
+
*
|
|
71
|
+
* This composes with `@nwutils/builder`: run it against the `outDir` (and
|
|
72
|
+
* `app.name`) that `@nwutils/builder` produced for `platform: "osx"`.
|
|
73
|
+
* `hdiutil`, `ditto` and `codesign` are part of MacOS, so this runs on a MacOS
|
|
74
|
+
* host.
|
|
75
|
+
* @async
|
|
76
|
+
* @function
|
|
77
|
+
* @param {DmgOptions} options
|
|
78
|
+
* @returns {Promise<string>} Path to the resulting `.dmg` file.
|
|
79
|
+
*/
|
|
80
|
+
declare function dmg({ appDir, appName, arch, outDir, volumeName, signIdentity, version, publish, hdiutilPath, dittoPath, codesignPath, }: DmgOptions): Promise<string>;
|
package/types/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ export default packager;
|
|
|
3
3
|
* Supported packaged output formats.
|
|
4
4
|
*
|
|
5
5
|
* `AppImage`, `deb` and `rpm` package a Linux build. `MSIX` and `NSIS`
|
|
6
|
-
* package a Windows build. `
|
|
7
|
-
*
|
|
8
|
-
* they land.
|
|
6
|
+
* package a Windows build. `dmg` packages a MacOS build. `"AppImage"`,
|
|
7
|
+
* `"nsis"` and `"dmg"` are implemented today - the others are reserved so
|
|
8
|
+
* this option doesn't need to change shape once they land.
|
|
9
9
|
*/
|
|
10
10
|
export type PackageFormat = "AppImage" | "deb" | "rpm" | "msix" | "nsis" | "dmg";
|
|
11
11
|
/**
|
|
@@ -16,9 +16,9 @@ export type PublishOptions = import("./appImage.js").PublishOptions;
|
|
|
16
16
|
* Supported packaged output formats.
|
|
17
17
|
*
|
|
18
18
|
* `AppImage`, `deb` and `rpm` package a Linux build. `MSIX` and `NSIS`
|
|
19
|
-
* package a Windows build. `
|
|
20
|
-
*
|
|
21
|
-
* they land.
|
|
19
|
+
* package a Windows build. `dmg` packages a MacOS build. `"AppImage"`,
|
|
20
|
+
* `"nsis"` and `"dmg"` are implemented today - the others are reserved so
|
|
21
|
+
* this option doesn't need to change shape once they land.
|
|
22
22
|
* @typedef {"AppImage" | "deb" | "rpm" | "msix" | "nsis" | "dmg"} PackageFormat
|
|
23
23
|
*/
|
|
24
24
|
/**
|
|
@@ -29,7 +29,7 @@ export type PublishOptions = import("./appImage.js").PublishOptions;
|
|
|
29
29
|
* Package a built NW.js application.
|
|
30
30
|
* @async
|
|
31
31
|
* @function
|
|
32
|
-
* @param {{format: PackageFormat} & Record<string, any>} options `format`, plus the options the matching packager (eg. `appImage` or `
|
|
32
|
+
* @param {{format: PackageFormat} & Record<string, any>} options `format`, plus the options the matching packager (eg. `appImage`, `nsis` or `dmg`) expects.
|
|
33
33
|
* @returns {Promise<string>} Path to the resulting packaged artifact.
|
|
34
34
|
*/
|
|
35
35
|
declare function packager({ format, ...options }: {
|
package/types/util.d.ts
CHANGED
|
@@ -85,8 +85,7 @@ declare function nsisPath(value: string): string;
|
|
|
85
85
|
*/
|
|
86
86
|
declare function parseDesktopEntry(contents: string): Record<string, string>;
|
|
87
87
|
/**
|
|
88
|
-
* Compute the base64 encoded SHA-512 digest of the file at `filePath
|
|
89
|
-
* same encoding electron-builder writes into its update info files.
|
|
88
|
+
* Compute the base64 encoded SHA-512 digest of the file at `filePath`.
|
|
90
89
|
* @async
|
|
91
90
|
* @function
|
|
92
91
|
* @param {string} filePath
|
|
@@ -116,14 +115,14 @@ declare function stringifyDesktopEntry(entries: Record<string, string | undefine
|
|
|
116
115
|
declare function stringifyYaml(entries: Record<string, string | number | boolean | Record<string, string | number | boolean>[] | undefined>): string;
|
|
117
116
|
/**
|
|
118
117
|
* Name of the update info file `@nwutils/updater` downloads for `arch` on
|
|
119
|
-
* `platform`,
|
|
120
|
-
* `latest-linux.yml` (Linux)
|
|
121
|
-
* architecture gets an `-<arch>` suffix.
|
|
122
|
-
* @param {string}
|
|
123
|
-
* @param {"linux" | "win"}
|
|
118
|
+
* `platform`, gets the bare
|
|
119
|
+
* `latest-linux.yml` (Linux), `latest.yml` (Windows) or `latest-mac.yml`
|
|
120
|
+
* (MacOS), every other architecture gets an `-<arch>` suffix.
|
|
121
|
+
* @param {string} arch
|
|
122
|
+
* @param {"linux" | "win" | "osx"} [platform="linux"]
|
|
124
123
|
* @returns {string}
|
|
125
124
|
*/
|
|
126
|
-
declare function updateInfoFileName(arch: string, platform?: "linux" | "win"): string;
|
|
125
|
+
declare function updateInfoFileName(arch: string, platform?: "linux" | "win" | "osx"): string;
|
|
127
126
|
/**
|
|
128
127
|
* Throw if `publish` isn't a valid `PublishOptions` object.
|
|
129
128
|
* @param {unknown} publish
|