@flare-desktop/cli 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,131 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { FlareError } from "../errors.js";
4
+
5
+ export function numericVersion(version) {
6
+ const match = String(version).match(/^(\d+)\.(\d+)\.(\d+)/);
7
+ if (!match) throw new FlareError(`'${version}' is not a numeric semantic version`);
8
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
9
+ }
10
+
11
+ export function msiVersion(version) {
12
+ const [major, minor, patch] = numericVersion(version);
13
+ if (major > 255 || minor > 255 || patch > 65535) {
14
+ throw new FlareError(`'${version}' cannot be represented as an msi version`);
15
+ }
16
+ return `${major}.${minor}.${patch}`;
17
+ }
18
+
19
+ export function fourPartVersion(version) {
20
+ const [major, minor, patch] = numericVersion(version);
21
+ return `${major}.${minor}.${patch}.0`;
22
+ }
23
+
24
+ export function debVersion(version) {
25
+ return String(version).replace(/-/g, "~");
26
+ }
27
+
28
+ export function rpmVersion(version) {
29
+ return String(version).replace(/-/g, "~");
30
+ }
31
+
32
+ export function safeName(value) {
33
+ const name = String(value)
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9+.-]+/g, "-")
36
+ .replace(/^[^a-z0-9]+/, "")
37
+ .replace(/-{2,}/g, "-")
38
+ .replace(/[.-]+$/, "");
39
+ return name || "flare-app";
40
+ }
41
+
42
+ export function artifactName(value) {
43
+ return String(value)
44
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
45
+ .replace(/-{2,}/g, "-")
46
+ .replace(/^-+|-+$/g, "");
47
+ }
48
+
49
+ export function windowsArchitecture(architecture = process.arch) {
50
+ if (architecture === "x64") return "x64";
51
+ if (architecture === "arm64") return "arm64";
52
+ if (architecture === "ia32") return "x86";
53
+ return architecture;
54
+ }
55
+
56
+ export function debArchitecture(architecture = process.arch) {
57
+ if (architecture === "x64") return "amd64";
58
+ if (architecture === "arm64") return "arm64";
59
+ if (architecture === "ia32") return "i386";
60
+ if (architecture === "arm") return "armhf";
61
+ return architecture;
62
+ }
63
+
64
+ export function rpmArchitecture(architecture = process.arch) {
65
+ if (architecture === "x64") return "x86_64";
66
+ if (architecture === "arm64") return "aarch64";
67
+ if (architecture === "ia32") return "i686";
68
+ return architecture;
69
+ }
70
+
71
+ export function appImageArchitecture(architecture = process.arch) {
72
+ if (architecture === "x64") return "x86_64";
73
+ if (architecture === "arm64") return "aarch64";
74
+ if (architecture === "ia32") return "i686";
75
+ return architecture;
76
+ }
77
+
78
+ export function stableGuid(seed) {
79
+ const bytes = createHash("sha1").update(String(seed)).digest().subarray(0, 16);
80
+ bytes[6] = (bytes[6] & 0x0f) | 0x50;
81
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
82
+ const hex = bytes.toString("hex").toUpperCase();
83
+ return `{${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}}`;
84
+ }
85
+
86
+ export function escapeXml(value) {
87
+ return String(value)
88
+ .replace(/&/g, "&")
89
+ .replace(/</g, "&lt;")
90
+ .replace(/>/g, "&gt;")
91
+ .replace(/"/g, "&quot;")
92
+ .replace(/'/g, "&apos;");
93
+ }
94
+
95
+ export function escapeNsis(value) {
96
+ return String(value).replace(/\$/g, "$$").replace(/"/g, '$\\"');
97
+ }
98
+
99
+ export function escapeDesktop(value) {
100
+ return String(value).replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "");
101
+ }
102
+
103
+ export function desktopCategory(category) {
104
+ const categories = {
105
+ DeveloperTool: "Development;",
106
+ Development: "Development;",
107
+ Utility: "Utility;",
108
+ Productivity: "Office;",
109
+ Graphics: "Graphics;",
110
+ Music: "Audio;",
111
+ Video: "AudioVideo;",
112
+ Network: "Network;",
113
+ Game: "Game;",
114
+ Education: "Education;",
115
+ Science: "Science;"
116
+ };
117
+ return categories[category] || "Utility;";
118
+ }
119
+
120
+ export function maintainer(config) {
121
+ const value = config.bundle?.publisher || config.app?.authors?.[0] || "flare-desktop";
122
+ return value.includes("@") ? value : `${value} <noreply@flare-desktop.dev>`;
123
+ }
124
+
125
+ export function description(config) {
126
+ return config.bundle?.shortDescription || config.app?.description || `${config.app?.productName || "flare"} built with flare`;
127
+ }
128
+
129
+ export function longDescription(config) {
130
+ return config.bundle?.longDescription || description(config);
131
+ }
@@ -0,0 +1,228 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { FlareError } from "../errors.js";
5
+ import { formatsForPlatform, platformDisplayName } from "../project.js";
6
+ import {
7
+ appImageArchitecture,
8
+ debArchitecture,
9
+ rpmArchitecture,
10
+ windowsArchitecture
11
+ } from "./common.js";
12
+ import { buildNsis } from "./nsis.js";
13
+ import { buildMsi } from "./msi.js";
14
+ import { buildAppBundle, buildDmg, signApp } from "./macos.js";
15
+ import { buildAppImage, buildDeb, buildRpm } from "./linux.js";
16
+ import { cleanStage, stageIcons, stagePayload } from "./staging.js";
17
+ import { requireTool } from "./tools.js";
18
+
19
+ const HOST_TARGETS = {
20
+ win32: ["msi", "nsis"],
21
+ darwin: ["app", "dmg"],
22
+ linux: ["deb", "rpm", "appimage"]
23
+ };
24
+
25
+ const TOOL_HINTS = {
26
+ makensis: "install NSIS 3 from https://nsis.sourceforge.io",
27
+ candle: "install WiX Toolset 3.14 and put candle.exe on PATH",
28
+ light: "install WiX Toolset 3.14 and put light.exe on PATH",
29
+ "dpkg-deb": "install dpkg-dev on Debian/Ubuntu",
30
+ rpmbuild: "install rpm-build on Fedora/RHEL or rpm on Debian/Ubuntu",
31
+ appimagetool: "download appimagetool from https://github.com/AppImage/appimagetool",
32
+ hdiutil: "run packaging on macOS",
33
+ codesign: "install Xcode command line tools"
34
+ };
35
+
36
+ export function bundleTargets(config, requested, platform = process.platform) {
37
+ if (!config.bundle?.active) throw new FlareError("bundle.active is false for this project");
38
+
39
+ const available = HOST_TARGETS[platform] ?? [];
40
+ const source =
41
+ requested === undefined ||
42
+ requested === null ||
43
+ requested === "" ||
44
+ (Array.isArray(requested) && requested.length === 0)
45
+ ? config.bundle.targets ?? []
46
+ : requested;
47
+ const values = (Array.isArray(source) ? source : String(source).split(","))
48
+ .map((value) => String(value).trim().toLowerCase())
49
+ .filter(Boolean);
50
+ const selected = values.length === 0 || values.includes("all") ? formatsForPlatform(platform) : values;
51
+
52
+ if (selected.length === 0) {
53
+ throw new FlareError(`no bundle targets are available for ${platformDisplayName(platform)}`);
54
+ }
55
+
56
+ const unsupported = selected.filter((target) => !available.includes(target));
57
+ if (unsupported.length > 0) {
58
+ throw new FlareError(
59
+ `bundle target${unsupported.length === 1 ? "" : "s"} ${unsupported.map((target) => `'${target}'`).join(", ")} ` +
60
+ `not available on ${platformDisplayName(platform)}; available: ${available.join(", ")}`
61
+ );
62
+ }
63
+
64
+ return [...new Set(selected)];
65
+ }
66
+
67
+ export function bundleTools(targets, config, platform = process.platform) {
68
+ const tools = [];
69
+ for (const target of targets) {
70
+ if (target === "nsis") tools.push("makensis");
71
+ if (target === "msi") tools.push("candle", "light");
72
+ if (target === "deb") tools.push("dpkg-deb");
73
+ if (target === "rpm") tools.push("rpmbuild");
74
+ if (target === "appimage") tools.push("appimagetool");
75
+ if (target === "dmg") tools.push("hdiutil");
76
+ if (target === "app" && config.bundle?.macos?.signingIdentity) tools.push("codesign");
77
+ }
78
+ return [...new Set(tools)];
79
+ }
80
+
81
+ export function assertBundleTools(targets, config, platform = process.platform) {
82
+ for (const tool of bundleTools(targets, config, platform)) {
83
+ requireTool(tool, TOOL_HINTS[tool]);
84
+ }
85
+ }
86
+
87
+ export function bundleArchitecture(platform = process.platform) {
88
+ if (platform === "win32") return windowsArchitecture();
89
+ if (platform === "darwin") return process.arch === "arm64" ? "arm64" : "x64";
90
+ if (platform === "linux") return debArchitecture();
91
+ return process.arch;
92
+ }
93
+
94
+ export async function createBundles({
95
+ config,
96
+ projectDirectory,
97
+ binary,
98
+ targets,
99
+ output,
100
+ logger
101
+ }) {
102
+ const outputRoot = path.resolve(projectDirectory, output || path.join("target", "bundle"));
103
+ const stageRoot = path.join(outputRoot, ".stage");
104
+ const icons = stageIcons({ config, projectDirectory, stageDirectory: stageRoot });
105
+ const payload = stagePayload({ config, projectDirectory, binary, stageDirectory: stageRoot, logger });
106
+ const platform = process.platform;
107
+ const artifacts = [];
108
+ let macApp = null;
109
+
110
+ mkdirSync(outputRoot, { recursive: true });
111
+ logger.info(`bundling '${config.app.productName}' ${config.app.version} for ${platformDisplayName(platform)}`);
112
+
113
+ try {
114
+ for (const target of targets) {
115
+ const outputDirectory = path.join(outputRoot, target);
116
+ const stageDirectory = path.join(stageRoot, target);
117
+ mkdirSync(stageDirectory, { recursive: true });
118
+ mkdirSync(outputDirectory, { recursive: true });
119
+
120
+ let artifact = null;
121
+ if (target === "nsis") {
122
+ artifact = await buildNsis({
123
+ config,
124
+ productName: config.app.productName,
125
+ version: config.app.version,
126
+ payload: payload.directory,
127
+ binaryName: payload.binaryName,
128
+ icons,
129
+ licenseSource: payload.licenseSource,
130
+ outputDirectory,
131
+ stageDirectory,
132
+ logger
133
+ });
134
+ } else if (target === "msi") {
135
+ artifact = await buildMsi({
136
+ config,
137
+ productName: config.app.productName,
138
+ version: config.app.version,
139
+ payload: payload.directory,
140
+ binaryName: payload.binaryName,
141
+ icons,
142
+ outputDirectory,
143
+ stageDirectory,
144
+ logger
145
+ });
146
+ } else if (target === "deb") {
147
+ artifact = await buildDeb({
148
+ config,
149
+ productName: config.app.productName,
150
+ version: config.app.version,
151
+ payload: payload.directory,
152
+ binaryName: payload.binaryName,
153
+ icons,
154
+ licenseSource: payload.licenseSource,
155
+ outputDirectory,
156
+ stageDirectory,
157
+ logger
158
+ });
159
+ } else if (target === "rpm") {
160
+ artifact = await buildRpm({
161
+ config,
162
+ productName: config.app.productName,
163
+ version: config.app.version,
164
+ payload: payload.directory,
165
+ binaryName: payload.binaryName,
166
+ icons,
167
+ licenseSource: payload.licenseSource,
168
+ outputDirectory,
169
+ stageDirectory,
170
+ logger
171
+ });
172
+ } else if (target === "appimage") {
173
+ artifact = await buildAppImage({
174
+ config,
175
+ productName: config.app.productName,
176
+ version: config.app.version,
177
+ payload: payload.directory,
178
+ binaryName: payload.binaryName,
179
+ icons,
180
+ licenseSource: payload.licenseSource,
181
+ outputDirectory,
182
+ stageDirectory,
183
+ logger
184
+ });
185
+ } else if (target === "app" || target === "dmg") {
186
+ if (!macApp) {
187
+ macApp = buildAppBundle({
188
+ config,
189
+ productName: config.app.productName,
190
+ version: config.app.version,
191
+ payload: payload.directory,
192
+ binaryName: payload.binaryName,
193
+ icons,
194
+ licenseSource: payload.licenseSource,
195
+ outputDirectory: path.join(outputRoot, "app"),
196
+ logger
197
+ });
198
+ if (config.bundle.macos?.signingIdentity) {
199
+ await signApp({
200
+ app: macApp,
201
+ identity: config.bundle.macos.signingIdentity,
202
+ logger
203
+ });
204
+ }
205
+ }
206
+ artifact =
207
+ target === "app"
208
+ ? macApp
209
+ : await buildDmg({
210
+ app: macApp,
211
+ productName: config.app.productName,
212
+ outputDirectory,
213
+ logger
214
+ });
215
+ }
216
+
217
+ artifacts.push({ target, path: artifact });
218
+ logger.step(`prepared ${target}: ${artifact}`);
219
+ }
220
+ } finally {
221
+ if (artifacts.length === targets.length) cleanStage(stageRoot);
222
+ }
223
+
224
+ logger.info(`artifacts written to '${outputRoot}'`);
225
+ return artifacts;
226
+ }
227
+
228
+ export { appImageArchitecture, debArchitecture, rpmArchitecture, windowsArchitecture };
@@ -0,0 +1,283 @@
1
+ import { chmodSync, cpSync, mkdirSync, readdirSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ appImageArchitecture,
6
+ debArchitecture,
7
+ debVersion,
8
+ description,
9
+ desktopCategory,
10
+ escapeDesktop,
11
+ longDescription,
12
+ maintainer,
13
+ rpmArchitecture,
14
+ rpmVersion,
15
+ safeName
16
+ } from "./common.js";
17
+ import { requireTool, runTool } from "./tools.js";
18
+ import { directorySize } from "./staging.js";
19
+
20
+ function copyContents(source, destination) {
21
+ mkdirSync(destination, { recursive: true });
22
+ for (const entry of readdirSync(source)) {
23
+ cpSync(path.join(source, entry), path.join(destination, entry), { recursive: true, force: true });
24
+ }
25
+ }
26
+
27
+ function desktopEntry({ config, packageName, binaryName, exec }) {
28
+ return [
29
+ "[Desktop Entry]",
30
+ "Type=Application",
31
+ `Name=${escapeDesktop(config.app.productName)}`,
32
+ `Comment=${escapeDesktop(description(config))}`,
33
+ `Exec=${escapeDesktop(exec)}`,
34
+ `Icon=${packageName}`,
35
+ "Terminal=false",
36
+ `Categories=${desktopCategory(config.bundle.category)}`,
37
+ ""
38
+ ].join("\n");
39
+ }
40
+
41
+ function rpmSafe(value) {
42
+ return String(value).replace(/%/g, "%%").replace(/\n/g, " ");
43
+ }
44
+
45
+ export async function buildDeb({
46
+ config,
47
+ productName,
48
+ version,
49
+ payload,
50
+ binaryName,
51
+ icons,
52
+ licenseSource,
53
+ outputDirectory,
54
+ stageDirectory,
55
+ logger
56
+ }) {
57
+ const dpkgDeb = requireTool("dpkg-deb", "install dpkg-dev on Debian/Ubuntu");
58
+ const packageName = safeName(productName);
59
+ const architecture = debArchitecture();
60
+ const root = path.join(stageDirectory, "deb");
61
+ const packageRoot = path.join(root, "package");
62
+ const installRoot = path.join(packageRoot, "opt", packageName);
63
+ const artifact = path.join(outputDirectory, `${packageName}_${debVersion(version)}_${architecture}.deb`);
64
+
65
+ mkdirSync(installRoot, { recursive: true });
66
+ copyContents(payload, installRoot);
67
+
68
+ mkdirSync(path.join(packageRoot, "usr", "bin"), { recursive: true });
69
+ symlinkSync(path.join("..", "..", "opt", packageName, binaryName), path.join(packageRoot, "usr", "bin", packageName));
70
+
71
+ const applications = path.join(packageRoot, "usr", "share", "applications");
72
+ mkdirSync(applications, { recursive: true });
73
+ writeFileSync(
74
+ path.join(applications, `${packageName}.desktop`),
75
+ desktopEntry({ config, packageName, binaryName, exec: path.join("/opt", packageName, binaryName) })
76
+ );
77
+
78
+ const iconDirectory = path.join(packageRoot, "usr", "share", "icons", "hicolor", "512x512", "apps");
79
+ mkdirSync(iconDirectory, { recursive: true });
80
+ cpSync(icons.png, path.join(iconDirectory, `${packageName}.png`));
81
+
82
+ const docDirectory = path.join(packageRoot, "usr", "share", "doc", packageName);
83
+ mkdirSync(docDirectory, { recursive: true });
84
+ if (licenseSource) cpSync(licenseSource, path.join(docDirectory, "copyright"));
85
+
86
+ const size = Math.ceil(directorySize(packageRoot) / 1024);
87
+ const descriptionLines = String(longDescription(config)).split(/\r?\n/);
88
+ const control = [
89
+ `Package: ${packageName}`,
90
+ `Version: ${debVersion(version)}`,
91
+ `Architecture: ${architecture}`,
92
+ `Maintainer: ${maintainer(config)}`,
93
+ `Installed-Size: ${size}`,
94
+ "Section: utils",
95
+ "Priority: optional",
96
+ ...(config.bundle.homepage ? [`Homepage: ${config.bundle.homepage}`] : []),
97
+ `Description: ${description(config)}`,
98
+ ...descriptionLines.slice(1).map((line) => ` ${line || "."}`),
99
+ ""
100
+ ].join("\n");
101
+
102
+ mkdirSync(path.join(packageRoot, "DEBIAN"), { recursive: true });
103
+ writeFileSync(path.join(packageRoot, "DEBIAN", "control"), control);
104
+
105
+ if (logger) logger.info(`[deb] compiling ${path.basename(artifact)}`);
106
+ await runTool(dpkgDeb, ["--build", "--root-owner-group", packageRoot, artifact], {
107
+ cwd: stageDirectory,
108
+ logger
109
+ });
110
+ return artifact;
111
+ }
112
+
113
+ function findRpm(directory) {
114
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
115
+ const target = path.join(directory, entry.name);
116
+ if (entry.isDirectory()) {
117
+ const found = findRpm(target);
118
+ if (found) return found;
119
+ } else if (entry.isFile() && entry.name.endsWith(".rpm")) {
120
+ return target;
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+
126
+ export async function buildRpm({
127
+ config,
128
+ productName,
129
+ version,
130
+ payload,
131
+ binaryName,
132
+ icons,
133
+ licenseSource,
134
+ outputDirectory,
135
+ stageDirectory,
136
+ logger
137
+ }) {
138
+ const rpmbuild = requireTool("rpmbuild", "install rpm-build on Fedora/RHEL or rpm on Debian/Ubuntu");
139
+ const packageName = safeName(productName);
140
+ const architecture = rpmArchitecture();
141
+ const root = path.join(stageDirectory, "rpm");
142
+ const top = path.join(root, "rpmbuild");
143
+ const specDirectory = path.join(top, "SPECS");
144
+ const buildDirectory = path.join(top, "BUILD");
145
+ const rpmDirectory = path.join(outputDirectory, "rpms");
146
+ const spec = path.join(specDirectory, `${packageName}.spec`);
147
+ const docLine = licenseSource ? `/usr/share/doc/${packageName}/LICENSE` : "";
148
+ const size = Math.ceil(directorySize(payload) / 1024);
149
+
150
+ for (const directory of [specDirectory, buildDirectory, path.join(top, "SOURCES"), path.join(top, "BUILDROOT"), path.join(top, "SRPMS"), rpmDirectory]) {
151
+ mkdirSync(directory, { recursive: true });
152
+ }
153
+
154
+ const lines = [
155
+ `%define _binary_payload w9.gzdio`,
156
+ `%define _unpackaged_files_terminate_build 1`,
157
+ `Name: ${packageName}`,
158
+ `Version: ${rpmVersion(version)}`,
159
+ "Release: 1",
160
+ `Summary: ${rpmSafe(description(config))}`,
161
+ "License: MIT",
162
+ ...(config.bundle.homepage ? [`URL: ${config.bundle.homepage}`] : []),
163
+ `BuildArch: ${architecture}`,
164
+ "AutoReqProv: no",
165
+ "Group: Applications/Development",
166
+ "",
167
+ "%description",
168
+ rpmSafe(longDescription(config)),
169
+ "",
170
+ "%install",
171
+ `mkdir -p "%{buildroot}/opt/${packageName}"`,
172
+ `cp -R "${payload}/." "%{buildroot}/opt/${packageName}/"`,
173
+ `mkdir -p "%{buildroot}/usr/bin"`,
174
+ `ln -sf "../../opt/${packageName}/${binaryName}" "%{buildroot}/usr/bin/${packageName}"`,
175
+ `mkdir -p "%{buildroot}/usr/share/applications"`,
176
+ `cat > "%{buildroot}/usr/share/applications/${packageName}.desktop" <<'FLARE_DESKTOP_EOF'`,
177
+ desktopEntry({ config, packageName, binaryName, exec: path.join("/opt", packageName, binaryName) }),
178
+ "FLARE_DESKTOP_EOF",
179
+ `mkdir -p "%{buildroot}/usr/share/icons/hicolor/512x512/apps"`,
180
+ `cp "${icons.png}" "%{buildroot}/usr/share/icons/hicolor/512x512/apps/${packageName}.png"`,
181
+ ...(licenseSource
182
+ ? [
183
+ `mkdir -p "%{buildroot}/usr/share/doc/${packageName}"`,
184
+ `cp "${licenseSource}" "%{buildroot}/usr/share/doc/${packageName}/LICENSE"`
185
+ ]
186
+ : []),
187
+ "",
188
+ "%files",
189
+ `/opt/${packageName}`,
190
+ `/usr/bin/${packageName}`,
191
+ `/usr/share/applications/${packageName}.desktop`,
192
+ `/usr/share/icons/hicolor/512x512/apps/${packageName}.png`,
193
+ ...(docLine ? [docLine] : []),
194
+ "",
195
+ `# installed size: ${size} KiB`
196
+ ];
197
+
198
+ writeFileSync(spec, `${lines.join("\n")}\n`, "utf8");
199
+
200
+ if (logger) logger.info(`[rpm] compiling ${packageName}-${rpmVersion(version)}-1.${architecture}.rpm`);
201
+ await runTool(
202
+ rpmbuild,
203
+ [
204
+ "-bb",
205
+ "--define",
206
+ `_topdir ${top}`,
207
+ "--define",
208
+ `_builddir ${buildDirectory}`,
209
+ "--define",
210
+ `_rpmdir ${rpmDirectory}`,
211
+ "--define",
212
+ `_srcrpmdir ${path.join(top, "SRPMS")}`,
213
+ "--define",
214
+ `_specdir ${specDirectory}`,
215
+ "--define",
216
+ `_sourcedir ${path.join(top, "SOURCES")}`,
217
+ "--define",
218
+ `_buildrootdir ${path.join(top, "BUILDROOT")}`,
219
+ spec
220
+ ],
221
+ { cwd: stageDirectory, logger }
222
+ );
223
+
224
+ const artifact = findRpm(rpmDirectory);
225
+ if (!artifact) throw new Error(`rpmbuild did not produce an rpm in '${rpmDirectory}'`);
226
+ return artifact;
227
+ }
228
+
229
+ export async function buildAppImage({
230
+ config,
231
+ productName,
232
+ version,
233
+ payload,
234
+ binaryName,
235
+ icons,
236
+ licenseSource,
237
+ outputDirectory,
238
+ stageDirectory,
239
+ logger
240
+ }) {
241
+ const appimagetool = requireTool("appimagetool", "download appimagetool from https://github.com/AppImage/appimagetool");
242
+ const packageName = safeName(productName);
243
+ const architecture = appImageArchitecture();
244
+ const appDir = path.join(stageDirectory, "appimage", "AppDir");
245
+ const binaryDirectory = path.join(appDir, "usr", "bin");
246
+ const applications = path.join(appDir, "usr", "share", "applications");
247
+ const iconDirectory = path.join(appDir, "usr", "share", "icons", "hicolor", "512x512", "apps");
248
+ const artifact = path.join(outputDirectory, `${packageName}_${version}_${architecture}.AppImage`);
249
+
250
+ copyContents(payload, binaryDirectory);
251
+ mkdirSync(applications, { recursive: true });
252
+ mkdirSync(iconDirectory, { recursive: true });
253
+
254
+ const desktop = desktopEntry({ config, packageName, binaryName, exec: packageName });
255
+ writeFileSync(path.join(applications, `${packageName}.desktop`), desktop);
256
+ writeFileSync(path.join(appDir, `${packageName}.desktop`), desktop);
257
+ cpSync(icons.png, path.join(iconDirectory, `${packageName}.png`));
258
+ cpSync(icons.png, path.join(appDir, `${packageName}.png`));
259
+ symlinkSync(`${packageName}.png`, path.join(appDir, ".DirIcon"));
260
+
261
+ if (licenseSource) {
262
+ const doc = path.join(appDir, "usr", "share", "doc", packageName);
263
+ mkdirSync(doc, { recursive: true });
264
+ cpSync(licenseSource, path.join(doc, "LICENSE"));
265
+ }
266
+
267
+ const appRun = path.join(appDir, "AppRun");
268
+ writeFileSync(
269
+ appRun,
270
+ `#!/bin/sh\nHERE="$(dirname "$(readlink -f "$0")")"\nexec "$HERE/usr/bin/${binaryName}" "$@"\n`
271
+ );
272
+ chmodSync(appRun, 0o755);
273
+ chmodSync(path.join(binaryDirectory, binaryName), 0o755);
274
+
275
+ mkdirSync(outputDirectory, { recursive: true });
276
+ if (logger) logger.info(`[appimage] compiling ${path.basename(artifact)}`);
277
+ await runTool(appimagetool, [appDir, artifact], {
278
+ cwd: stageDirectory,
279
+ env: { ARCH: architecture },
280
+ logger
281
+ });
282
+ return artifact;
283
+ }