@nwutils/packager 0.1.0 → 0.2.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 CHANGED
@@ -2,10 +2,82 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@nwutils/packager/latest)](https://www.npmjs.com/package/@nwutils/packager/v/latest)
4
4
 
5
+ Package NW.js applications for Linux, MacOS and Windows.
6
+
7
+ Currently supported: packaging a Linux application as an [AppImage](https://appimage.org/).
8
+
5
9
  ## Getting Started
6
10
 
7
11
  1. `npm i` to install third party dependencies
8
12
 
13
+ ## Usage
14
+
15
+ This composes with `@nwutils/builder`: run it against the `outDir` (and `app.name`)
16
+ that `@nwutils/builder` produced for `platform: "linux"`.
17
+
18
+ ```js
19
+ import build from "@nwutils/builder";
20
+ import { appImage } from "@nwutils/packager";
21
+
22
+ await build({
23
+ version: "0.115.0",
24
+ platform: "linux",
25
+ arch: "x64",
26
+ srcDir: "./src",
27
+ cacheDir: "./cache",
28
+ outDir: "./out",
29
+ app: {
30
+ name: "Demo",
31
+ icon: "icon.png",
32
+ categories: "Utility;",
33
+ },
34
+ });
35
+
36
+ const appImagePath = await appImage({
37
+ appDir: "./out",
38
+ appName: "Demo",
39
+ cacheDir: "./cache",
40
+ outDir: "./dist",
41
+ });
42
+
43
+ console.log(`AppImage written to ${appImagePath}`);
44
+ ```
45
+
46
+ The first time it runs for a given architecture, `appImage()` downloads
47
+ [`appimagetool`](https://github.com/AppImage/appimagetool) into `cacheDir` and
48
+ reuses it on subsequent calls. `appimagetool` is a native Linux binary, so
49
+ `appImage()` only runs on a Linux host, targeting the host's own architecture
50
+ by default.
51
+
52
+ `appimagetool` is invoked with `APPIMAGE_EXTRACT_AND_RUN=1`, so a FUSE mount is
53
+ never required - useful in containers, CI runners and other sandboxes where
54
+ FUSE is typically unavailable.
55
+
56
+ ## API Reference
57
+
58
+ ### `appImage(options)`
59
+
60
+ Options
61
+
62
+ | Name | Type | Description |
63
+ | --------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
64
+ | appDir | `string` | Path to a built NW.js Linux application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "linux"` |
65
+ | appName | `string` | Name of the application. Must match the `app.name` value used to build `appDir` |
66
+ | icon | `string` | Path to a `.png` or `.svg` icon. Defaults to the `Icon` value read from `<appDir>/<appName>.desktop` |
67
+ | arch | `"ia32" \| "x64" \| "arm64"` | Target architecture. Defaults to the host architecture |
68
+ | outDir | `string` | Directory the resulting `.AppImage` file is written to. Defaults to the parent directory of `appDir` |
69
+ | cacheDir | `string` | Directory used to cache the downloaded `appimagetool` binary. Defaults to `"./cache"` |
70
+ | cache | `boolean` | If true, reuse a cached `appimagetool` binary. Otherwise redownload it. Defaults to `true` |
71
+ | appImageToolUrl | `string` | Base URL `appimagetool-<arch>.AppImage` is downloaded from. Defaults to the `AppImage/appimagetool` "continuous" release |
72
+
73
+ Resolves with the path to the resulting `.AppImage` file.
74
+
75
+ The application's desktop entry file (`<appDir>/<appName>.desktop`, as written
76
+ by `@nwutils/builder`) is copied into the AppImage with its `Exec` and `Icon`
77
+ keys rewritten to match the AppImage's own layout, and a `Categories` key
78
+ added (defaulting to `Utility;`) if one isn't already present, since
79
+ `appimagetool` refuses to build an AppImage without one.
80
+
9
81
  ## Contributing
10
82
 
11
83
  ### External contributor
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nwutils/packager",
3
- "version": "0.1.0",
4
- "description": "",
3
+ "version": "0.2.0",
4
+ "description": "Package NW.js applications for Linux, MacOS and Windows.",
5
5
  "keywords": [
6
6
  "NW.js",
7
7
  "node-webkit",
@@ -26,10 +26,12 @@
26
26
  ],
27
27
  "license": "MIT",
28
28
  "main": "./src/index.js",
29
+ "types": "types/index.d.ts",
29
30
  "type": "module",
30
31
  "files": [
31
32
  "LICENSE",
32
- "src"
33
+ "src",
34
+ "types"
33
35
  ],
34
36
  "homepage": "https://github.com/nwutils/nw-builder",
35
37
  "repository": {
@@ -37,10 +39,11 @@
37
39
  "url": "git+https://github.com/nwutils/nw-builder.git"
38
40
  },
39
41
  "scripts": {
40
- "lint": "eslint .",
41
- "lint:fix": "eslint . --fix",
42
+ "lint": "eslint . ../../tests/specs/packager ../../tests/fixtures/packager",
43
+ "lint:fix": "eslint . ../../tests/specs/packager ../../tests/fixtures/packager --fix",
42
44
  "type": "tsc",
43
- "test": "node --test \"../../tests/specs/packager/*.test.js\""
45
+ "test": "node --test --test-concurrency=1 \"../../tests/specs/packager/appImage.test.js\" \"../../tests/specs/packager/request.test.js\" \"../../tests/specs/packager/util.test.js\"",
46
+ "test:real": "node --test \"../../tests/specs/packager/appImage.real.test.js\""
44
47
  },
45
48
  "dependencies": {},
46
49
  "devDependencies": {}
@@ -0,0 +1,266 @@
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 request from "./request.js";
8
+ import util from "./util.js";
9
+
10
+ /**
11
+ * Default location `appimagetool` is downloaded from. The `continuous` tag
12
+ * is rebuilt on every push to the default branch and publishes one asset per
13
+ * architecture, eg. `appimagetool-x86_64.AppImage`.
14
+ * @type {string}
15
+ */
16
+ const APPIMAGETOOL_URL =
17
+ "https://github.com/AppImage/appimagetool/releases/download/continuous";
18
+
19
+ /**
20
+ * @typedef {object} AppImageOptions
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`.
23
+ * @property {string} [icon] Path to a `.png` or `.svg` icon. Defaults to the `Icon` value read from `<appDir>/<appName>.desktop`.
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
+ * @property {string} [outDir] Directory the resulting `.AppImage` file is written to. Defaults to the parent directory of `appDir`.
26
+ * @property {string} [cacheDir="./cache"] Directory used to cache the downloaded `appimagetool` binary.
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.
29
+ */
30
+
31
+ /**
32
+ * Package a built NW.js Linux application as an AppImage.
33
+ *
34
+ * This composes with `@nwutils/builder`: run it against the `outDir` (and
35
+ * `app.name`) that `@nwutils/builder` produced for `platform: "linux"`.
36
+ * @async
37
+ * @function
38
+ * @param {AppImageOptions} options
39
+ * @returns {Promise<string>} Path to the resulting `.AppImage` file.
40
+ */
41
+ async function appImage({
42
+ appDir,
43
+ appName,
44
+ icon,
45
+ arch,
46
+ outDir,
47
+ cacheDir = "./cache",
48
+ cache = true,
49
+ appImageToolUrl = APPIMAGETOOL_URL,
50
+ }) {
51
+ if (process.platform !== "linux") {
52
+ throw new Error(
53
+ `AppImage packaging requires a Linux host, since "appimagetool" is a native Linux binary. Received host platform: ${process.platform}`,
54
+ );
55
+ }
56
+
57
+ if (typeof appDir !== "string" || appDir === "") {
58
+ throw new Error(
59
+ `Expected "options.appDir" to be a non-empty string. Received: ${JSON.stringify(appDir)}`,
60
+ );
61
+ }
62
+
63
+ if ((await util.fileExists(appDir)) === false) {
64
+ throw new Error(`"options.appDir" does not exist: ${appDir}`);
65
+ }
66
+
67
+ if (typeof appName !== "string" || appName === "") {
68
+ throw new Error(
69
+ `Expected "options.appName" to be a non-empty string. Received: ${JSON.stringify(appName)}`,
70
+ );
71
+ }
72
+
73
+ const resolvedArch =
74
+ /** @type {"ia32" | "x64" | "arm64" | undefined} */ (arch) ??
75
+ /** @type {"ia32" | "x64" | "arm64"} */ (
76
+ /** @type {unknown} */ (process.arch)
77
+ );
78
+
79
+ const appImageToolArch = util.APPIMAGE_ARCH_KV[resolvedArch];
80
+ if (appImageToolArch === undefined) {
81
+ throw new Error(
82
+ `Expected "options.arch" to be "ia32", "x64" or "arm64". Received: ${JSON.stringify(arch)}`,
83
+ );
84
+ }
85
+
86
+ const resolvedAppDir = path.resolve(appDir);
87
+ const executablePath = path.resolve(resolvedAppDir, appName);
88
+ if ((await util.fileExists(executablePath)) === false) {
89
+ throw new Error(
90
+ `Expected the NW.js executable to exist at ${executablePath}. Does "options.appName" match the "app.name" used to build "options.appDir"?`,
91
+ );
92
+ }
93
+
94
+ const desktopFilePath = path.resolve(resolvedAppDir, `${appName}.desktop`);
95
+ if ((await util.fileExists(desktopFilePath)) === false) {
96
+ throw new Error(
97
+ `Expected a desktop entry file to exist at ${desktopFilePath}. Build "options.appDir" with "@nwutils/builder" for "platform: \\"linux\\"" first.`,
98
+ );
99
+ }
100
+ const desktopEntry = util.parseDesktopEntry(
101
+ await fs.promises.readFile(desktopFilePath, "utf-8"),
102
+ );
103
+
104
+ const resolvedIcon = icon ?? desktopEntry.Icon;
105
+ if (resolvedIcon === undefined || resolvedIcon === "") {
106
+ throw new Error(
107
+ 'AppImage packaging requires an icon. Pass "options.icon", or ensure the "Icon" key in the desktop entry file points at an existing file.',
108
+ );
109
+ }
110
+ const resolvedIconPath = path.resolve(resolvedIcon);
111
+ if ((await util.fileExists(resolvedIconPath)) === false) {
112
+ throw new Error(`"options.icon" does not exist: ${resolvedIconPath}`);
113
+ }
114
+
115
+ const resolvedOutDir = path.resolve(outDir ?? path.dirname(resolvedAppDir));
116
+ await fs.promises.mkdir(resolvedOutDir, { recursive: true });
117
+
118
+ const resolvedCacheDir = path.resolve(cacheDir);
119
+ const appImageToolPath = await getAppImageTool({
120
+ appImageToolArch,
121
+ appImageToolUrl,
122
+ cacheDir: resolvedCacheDir,
123
+ cache,
124
+ });
125
+
126
+ const appImageDir = await fs.promises.mkdtemp(
127
+ path.join(os.tmpdir(), "nwutils-packager-"),
128
+ );
129
+
130
+ try {
131
+ await fs.promises.cp(resolvedAppDir, appImageDir, {
132
+ recursive: true,
133
+ force: true,
134
+ verbatimSymlinks: true,
135
+ });
136
+
137
+ const iconFileName = path.basename(resolvedIconPath);
138
+ await fs.promises.cp(
139
+ resolvedIconPath,
140
+ path.resolve(appImageDir, iconFileName),
141
+ { force: true },
142
+ );
143
+
144
+ /*
145
+ * `Exec` and `Icon` are rewritten unconditionally: `Exec` may be unset or
146
+ * point at something other than the renamed binary, and `Icon` is an
147
+ * absolute build-time path - neither survives being relocated into an
148
+ * AppImage, whose root is what ends up mounted at runtime. `Categories`
149
+ * is only filled in when absent: `appimagetool` refuses to build an
150
+ * AppImage without one, but `@nwutils/builder`'s `app.categories` is
151
+ * optional, so most callers won't have set it.
152
+ */
153
+ const appImageDesktopEntry = {
154
+ ...desktopEntry,
155
+ Name: desktopEntry.Name ?? appName,
156
+ Exec: appName,
157
+ Icon: path.basename(iconFileName, path.extname(iconFileName)),
158
+ Categories: desktopEntry.Categories || "Utility;",
159
+ };
160
+ await fs.promises.writeFile(
161
+ path.resolve(appImageDir, `${appName}.desktop`),
162
+ util.stringifyDesktopEntry(appImageDesktopEntry),
163
+ );
164
+
165
+ const appRunPath = path.resolve(appImageDir, "AppRun");
166
+ await fs.promises.writeFile(
167
+ appRunPath,
168
+ [
169
+ "#!/bin/sh",
170
+ 'HERE="$(dirname "$(readlink -f "${0}")")"',
171
+ `exec "\${HERE}/${appName}" "$@"`,
172
+ "",
173
+ ].join("\n"),
174
+ );
175
+ await fs.promises.chmod(appRunPath, 0o755);
176
+
177
+ const appImageFilePath = path.resolve(
178
+ resolvedOutDir,
179
+ `${appName}-${appImageToolArch}.AppImage`,
180
+ );
181
+
182
+ child_process.execFileSync(
183
+ appImageToolPath,
184
+ ["--no-appstream", appImageDir, appImageFilePath],
185
+ {
186
+ env: {
187
+ ...process.env,
188
+ ARCH: appImageToolArch,
189
+ /*
190
+ * `appimagetool` is itself distributed as an AppImage. Extract and
191
+ * run it directly instead of mounting it via FUSE, which is
192
+ * frequently unavailable in containers, CI runners and sandboxes.
193
+ */
194
+ APPIMAGE_EXTRACT_AND_RUN: "1",
195
+ },
196
+ stdio: "inherit",
197
+ },
198
+ );
199
+
200
+ return appImageFilePath;
201
+ } finally {
202
+ await fs.promises.rm(appImageDir, { recursive: true, force: true });
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Download (or reuse a cached) `appimagetool` binary for `appImageToolArch`.
208
+ * @async
209
+ * @function
210
+ * @param {object} options
211
+ * @param {string} options.appImageToolArch `appimagetool` architecture identifier, eg. "x86_64".
212
+ * @param {string} options.appImageToolUrl Base URL to download `appimagetool-<arch>.AppImage` from.
213
+ * @param {string} options.cacheDir Directory used to cache the downloaded binary.
214
+ * @param {boolean} options.cache If true, reuse a cached binary. Otherwise redownload it.
215
+ * @returns {Promise<string>} Path to an executable `appimagetool` binary.
216
+ */
217
+ async function getAppImageTool({
218
+ appImageToolArch,
219
+ appImageToolUrl,
220
+ cacheDir,
221
+ cache,
222
+ }) {
223
+ const appImageToolPath = path.resolve(
224
+ cacheDir,
225
+ `appimagetool-${appImageToolArch}.AppImage`,
226
+ );
227
+
228
+ if (cache === false) {
229
+ await fs.promises.rm(appImageToolPath, { force: true });
230
+ }
231
+
232
+ if ((await util.fileExists(appImageToolPath)) === false) {
233
+ await fs.promises.mkdir(cacheDir, { recursive: true });
234
+ await request(
235
+ `${appImageToolUrl}/appimagetool-${appImageToolArch}.AppImage`,
236
+ appImageToolPath,
237
+ );
238
+
239
+ /*
240
+ * `appimagetool-<arch>.AppImage` has no published checksum to verify
241
+ * against (the "continuous" release is rebuilt in place). Checking the
242
+ * ELF magic bytes at least catches an HTML error page or truncated
243
+ * download being saved and later executed as though it were the tool.
244
+ */
245
+ const magic = Buffer.alloc(4);
246
+ const fileHandle = await fs.promises.open(appImageToolPath, "r");
247
+ try {
248
+ await fileHandle.read(magic, 0, 4, 0);
249
+ } finally {
250
+ await fileHandle.close();
251
+ }
252
+ if (magic.toString("hex") !== "7f454c46") {
253
+ await fs.promises.rm(appImageToolPath, { force: true });
254
+ throw new Error(
255
+ `Downloaded "appimagetool" from ${appImageToolUrl}/appimagetool-${appImageToolArch}.AppImage does not look like an ELF binary.`,
256
+ );
257
+ }
258
+ }
259
+
260
+ await fs.promises.chmod(appImageToolPath, 0o755);
261
+
262
+ return appImageToolPath;
263
+ }
264
+
265
+ export default appImage;
266
+ export { getAppImageTool };
package/src/index.js CHANGED
@@ -1 +1,7 @@
1
- export default {};
1
+ import appImage from "./appImage.js";
2
+
3
+ export default {
4
+ appImage,
5
+ };
6
+
7
+ export { appImage };
package/src/request.js ADDED
@@ -0,0 +1,132 @@
1
+ import fs from "node:fs";
2
+ import http from "node:http";
3
+ import https from "node:https";
4
+ import process from "node:process";
5
+ import { URL } from "node:url";
6
+
7
+ /**
8
+ * Maximum number of HTTP redirects to follow before giving up.
9
+ * @type {number}
10
+ */
11
+ const MAX_REDIRECTS = 5;
12
+
13
+ /**
14
+ * Download from `url` and save at `filePath`.
15
+ * @param {string} url
16
+ * @param {string} filePath
17
+ * @param {number} [redirectCount] - Number of redirects already followed. Used internally for recursive calls.
18
+ * @returns {Promise<void>}
19
+ */
20
+ export default function request(url, filePath, redirectCount = 0) {
21
+ const parsedUrl = new URL(url);
22
+ const client = parsedUrl.protocol === "https:" ? https : http;
23
+
24
+ return new Promise((resolve, reject) => {
25
+ const writeStream = fs.createWriteStream(filePath);
26
+
27
+ /* Handle writeStream errors immediately */
28
+ writeStream.on("error", (err) => {
29
+ cleanup();
30
+ reject(err);
31
+ });
32
+
33
+ /* Ctrl+C cleanup */
34
+ const onSigInt = () => {
35
+ writeStream.destroy();
36
+ /*
37
+ * Unlink unconditionally and swallow ENOENT: checking with existsSync
38
+ * first is a check-then-act race (CWE-367) - the file can vanish between
39
+ * the two calls, and on SIGINT there is no useful recovery either way.
40
+ */
41
+ try {
42
+ fs.unlinkSync(filePath);
43
+ } catch {
44
+ /* Nothing to clean up. */
45
+ }
46
+ process.exit();
47
+ };
48
+ process.once("SIGINT", onSigInt);
49
+
50
+ const req = client.get(
51
+ {
52
+ hostname: parsedUrl.hostname,
53
+ port: parsedUrl.port || (parsedUrl.protocol === "https:" ? 443 : 80),
54
+ path: parsedUrl.pathname + parsedUrl.search,
55
+ headers: {
56
+ "User-Agent": "node:http(s)",
57
+ },
58
+ },
59
+ (res) => {
60
+ /* Redirect handling */
61
+ if (
62
+ res.statusCode &&
63
+ res.statusCode >= 300 &&
64
+ res.statusCode < 400 &&
65
+ res.headers.location
66
+ ) {
67
+ cleanup();
68
+
69
+ if (redirectCount >= MAX_REDIRECTS) {
70
+ return reject(
71
+ new Error(
72
+ `Too many redirects (> ${MAX_REDIRECTS}) while requesting ${url}`,
73
+ ),
74
+ );
75
+ }
76
+
77
+ const redirectedUrl = new URL(res.headers.location, parsedUrl);
78
+
79
+ /*
80
+ * A redirect response can be injected by a network attacker even
81
+ * when the original request can't be, so refuse one that would
82
+ * silently strip transport security from the rest of the chain.
83
+ */
84
+ if (
85
+ parsedUrl.protocol === "https:" &&
86
+ redirectedUrl.protocol === "http:"
87
+ ) {
88
+ return reject(
89
+ new Error(
90
+ `Refusing to follow redirect from ${url} to ${redirectedUrl.toString()}: downgrades from https to http.`,
91
+ ),
92
+ );
93
+ }
94
+
95
+ return resolve(
96
+ request(redirectedUrl.toString(), filePath, redirectCount + 1),
97
+ );
98
+ }
99
+
100
+ if (res.statusCode !== 200) {
101
+ cleanup();
102
+ return reject(
103
+ new Error(`Request failed. Status code: ${res.statusCode}`),
104
+ );
105
+ }
106
+
107
+ res.pipe(writeStream);
108
+
109
+ writeStream.on("finish", () => {
110
+ cleanup();
111
+ resolve();
112
+ });
113
+
114
+ res.on("error", (err) => {
115
+ cleanup();
116
+ reject(err);
117
+ });
118
+ },
119
+ );
120
+
121
+ req.on("error", (err) => {
122
+ cleanup();
123
+ reject(err);
124
+ });
125
+
126
+ function cleanup() {
127
+ process.removeListener("SIGINT", onSigInt);
128
+ req.destroy();
129
+ writeStream.destroy();
130
+ }
131
+ });
132
+ }
package/src/util.js ADDED
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs";
2
+
3
+ /**
4
+ * Map nw-builder's architecture identifiers to the identifiers used in
5
+ * `appimagetool` release asset names and its `ARCH` environment variable.
6
+ * @type {Record<"ia32" | "x64" | "arm64", string>}
7
+ */
8
+ const APPIMAGE_ARCH_KV = {
9
+ ia32: "i686",
10
+ x64: "x86_64",
11
+ arm64: "aarch64",
12
+ };
13
+
14
+ /**
15
+ * Check if a file or directory exists.
16
+ * @async
17
+ * @function
18
+ * @param {string} filePath
19
+ * @returns {Promise<boolean>}
20
+ */
21
+ async function fileExists(filePath) {
22
+ let exists = true;
23
+ try {
24
+ await fs.promises.stat(filePath);
25
+ } catch {
26
+ exists = false;
27
+ }
28
+ return exists;
29
+ }
30
+
31
+ /**
32
+ * Parse a freedesktop.org desktop entry file's `[Desktop Entry]` group into
33
+ * a key/value map. Other groups (eg. `[Desktop Action ...]`) are ignored.
34
+ * @param {string} contents
35
+ * @returns {Record<string, string>}
36
+ */
37
+ function parseDesktopEntry(contents) {
38
+ /** @type {Record<string, string>} */
39
+ const entries = {};
40
+ let inDesktopEntryGroup = false;
41
+
42
+ for (const rawLine of contents.split("\n")) {
43
+ const line = rawLine.trim();
44
+ if (line === "" || line.startsWith("#")) {
45
+ continue;
46
+ }
47
+ if (line.startsWith("[")) {
48
+ inDesktopEntryGroup = line === "[Desktop Entry]";
49
+ continue;
50
+ }
51
+ if (inDesktopEntryGroup === false) {
52
+ continue;
53
+ }
54
+ const separatorIndex = line.indexOf("=");
55
+ if (separatorIndex === -1) {
56
+ continue;
57
+ }
58
+ const key = line.slice(0, separatorIndex).trim();
59
+ const value = line.slice(separatorIndex + 1).trim();
60
+ entries[key] = value;
61
+ }
62
+
63
+ return entries;
64
+ }
65
+
66
+ /**
67
+ * Serialise a `[Desktop Entry]` key/value map back into a desktop entry file.
68
+ * @param {Record<string, string | undefined>} entries
69
+ * @returns {string}
70
+ */
71
+ function stringifyDesktopEntry(entries) {
72
+ let fileContent = "[Desktop Entry]\n";
73
+ for (const key of Object.keys(entries)) {
74
+ if (entries[key] !== undefined) {
75
+ fileContent += `${key}=${entries[key]}\n`;
76
+ }
77
+ }
78
+ return fileContent;
79
+ }
80
+
81
+ export default {
82
+ APPIMAGE_ARCH_KV,
83
+ fileExists,
84
+ parseDesktopEntry,
85
+ stringifyDesktopEntry,
86
+ };
@@ -0,0 +1,75 @@
1
+ export type AppImageOptions = {
2
+ /**
3
+ * Path to a built NW.js Linux application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "linux"`.
4
+ */
5
+ appDir: string;
6
+ /**
7
+ * 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`.
8
+ */
9
+ appName: string;
10
+ /**
11
+ * Path to a `.png` or `.svg` icon. Defaults to the `Icon` value read from `<appDir>/<appName>.desktop`.
12
+ */
13
+ icon?: string;
14
+ /**
15
+ * 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.
16
+ */
17
+ arch?: "ia32" | "x64" | "arm64";
18
+ /**
19
+ * Directory the resulting `.AppImage` file is written to. Defaults to the parent directory of `appDir`.
20
+ */
21
+ outDir?: string;
22
+ /**
23
+ * Directory used to cache the downloaded `appimagetool` binary.
24
+ */
25
+ cacheDir?: string;
26
+ /**
27
+ * If true, reuse a cached `appimagetool` binary. Otherwise redownload it.
28
+ */
29
+ cache?: boolean;
30
+ /**
31
+ * Base URL `appimagetool-<arch>.AppImage` is downloaded from. Defaults to the `AppImage/appimagetool` "continuous" GitHub release.
32
+ */
33
+ appImageToolUrl?: string;
34
+ };
35
+ /**
36
+ * @typedef {object} AppImageOptions
37
+ * @property {string} appDir Path to a built NW.js Linux application, ie. the `outDir` produced by `@nwutils/builder` for `platform: "linux"`.
38
+ * @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`.
39
+ * @property {string} [icon] Path to a `.png` or `.svg` icon. Defaults to the `Icon` value read from `<appDir>/<appName>.desktop`.
40
+ * @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.
41
+ * @property {string} [outDir] Directory the resulting `.AppImage` file is written to. Defaults to the parent directory of `appDir`.
42
+ * @property {string} [cacheDir="./cache"] Directory used to cache the downloaded `appimagetool` binary.
43
+ * @property {boolean} [cache=true] If true, reuse a cached `appimagetool` binary. Otherwise redownload it.
44
+ * @property {string} [appImageToolUrl] Base URL `appimagetool-<arch>.AppImage` is downloaded from. Defaults to the `AppImage/appimagetool` "continuous" GitHub release.
45
+ */
46
+ /**
47
+ * Package a built NW.js Linux application as an AppImage.
48
+ *
49
+ * This composes with `@nwutils/builder`: run it against the `outDir` (and
50
+ * `app.name`) that `@nwutils/builder` produced for `platform: "linux"`.
51
+ * @async
52
+ * @function
53
+ * @param {AppImageOptions} options
54
+ * @returns {Promise<string>} Path to the resulting `.AppImage` file.
55
+ */
56
+ declare function appImage({ appDir, appName, icon, arch, outDir, cacheDir, cache, appImageToolUrl, }: AppImageOptions): Promise<string>;
57
+ /**
58
+ * Download (or reuse a cached) `appimagetool` binary for `appImageToolArch`.
59
+ * @async
60
+ * @function
61
+ * @param {object} options
62
+ * @param {string} options.appImageToolArch `appimagetool` architecture identifier, eg. "x86_64".
63
+ * @param {string} options.appImageToolUrl Base URL to download `appimagetool-<arch>.AppImage` from.
64
+ * @param {string} options.cacheDir Directory used to cache the downloaded binary.
65
+ * @param {boolean} options.cache If true, reuse a cached binary. Otherwise redownload it.
66
+ * @returns {Promise<string>} Path to an executable `appimagetool` binary.
67
+ */
68
+ declare function getAppImageTool({ appImageToolArch, appImageToolUrl, cacheDir, cache, }: {
69
+ appImageToolArch: string;
70
+ appImageToolUrl: string;
71
+ cacheDir: string;
72
+ cache: boolean;
73
+ }): Promise<string>;
74
+ export default appImage;
75
+ export { getAppImageTool };
@@ -0,0 +1,6 @@
1
+ import appImage from "./appImage.js";
2
+ declare const _default: {
3
+ appImage: typeof appImage;
4
+ };
5
+ export default _default;
6
+ export { appImage };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Download from `url` and save at `filePath`.
3
+ * @param {string} url
4
+ * @param {string} filePath
5
+ * @param {number} [redirectCount] - Number of redirects already followed. Used internally for recursive calls.
6
+ * @returns {Promise<void>}
7
+ */
8
+ export default function request(url: string, filePath: string, redirectCount?: number): Promise<void>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Check if a file or directory exists.
3
+ * @async
4
+ * @function
5
+ * @param {string} filePath
6
+ * @returns {Promise<boolean>}
7
+ */
8
+ declare function fileExists(filePath: string): Promise<boolean>;
9
+ /**
10
+ * Parse a freedesktop.org desktop entry file's `[Desktop Entry]` group into
11
+ * a key/value map. Other groups (eg. `[Desktop Action ...]`) are ignored.
12
+ * @param {string} contents
13
+ * @returns {Record<string, string>}
14
+ */
15
+ declare function parseDesktopEntry(contents: string): Record<string, string>;
16
+ /**
17
+ * Serialise a `[Desktop Entry]` key/value map back into a desktop entry file.
18
+ * @param {Record<string, string | undefined>} entries
19
+ * @returns {string}
20
+ */
21
+ declare function stringifyDesktopEntry(entries: Record<string, string | undefined>): string;
22
+ declare const _default: {
23
+ APPIMAGE_ARCH_KV: Record<"arm64" | "ia32" | "x64", string>;
24
+ fileExists: typeof fileExists;
25
+ parseDesktopEntry: typeof parseDesktopEntry;
26
+ stringifyDesktopEntry: typeof stringifyDesktopEntry;
27
+ };
28
+ export default _default;