@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,84 @@
1
+ import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { escapeXml, msiVersion } from "./common.js";
5
+ import { requireTool, runTool } from "./tools.js";
6
+
7
+ function copyContents(source, destination) {
8
+ mkdirSync(destination, { recursive: true });
9
+ for (const entry of readdirSync(source)) {
10
+ cpSync(path.join(source, entry), path.join(destination, entry), { recursive: true, force: true });
11
+ }
12
+ }
13
+
14
+ export function buildAppBundle({
15
+ config,
16
+ productName,
17
+ version,
18
+ payload,
19
+ binaryName,
20
+ icons,
21
+ licenseSource,
22
+ outputDirectory,
23
+ logger
24
+ }) {
25
+ const app = path.join(outputDirectory, `${productName.replace(/[\\/:*?"<>|]/g, "-")}.app`);
26
+ const contents = path.join(app, "Contents");
27
+ const macOS = path.join(contents, "MacOS");
28
+ const resources = path.join(contents, "Resources");
29
+
30
+ mkdirSync(macOS, { recursive: true });
31
+ mkdirSync(resources, { recursive: true });
32
+ copyContents(payload, macOS);
33
+ cpSync(icons.icns, path.join(resources, "icon.icns"));
34
+ if (licenseSource) cpSync(licenseSource, path.join(resources, "LICENSE"));
35
+ chmodSync(path.join(macOS, binaryName), 0o755);
36
+
37
+ const plist = [
38
+ '<?xml version="1.0" encoding="UTF-8"?>',
39
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
40
+ '<plist version="1.0">',
41
+ "<dict>",
42
+ ` <key>CFBundleIdentifier</key><string>${escapeXml(config.app.identifier)}</string>`,
43
+ ` <key>CFBundleName</key><string>${escapeXml(productName)}</string>`,
44
+ ` <key>CFBundleDisplayName</key><string>${escapeXml(productName)}</string>`,
45
+ ` <key>CFBundleExecutable</key><string>${escapeXml(binaryName)}</string>`,
46
+ " <key>CFBundleIconFile</key><string>icon</string>",
47
+ " <key>CFBundlePackageType</key><string>APPL</string>",
48
+ ` <key>CFBundleShortVersionString</key><string>${escapeXml(version)}</string>`,
49
+ ` <key>CFBundleVersion</key><string>${escapeXml(msiVersion(version))}</string>`,
50
+ ` <key>LSMinimumSystemVersion</key><string>${escapeXml(config.bundle.macos?.minimumSystemVersion || "10.15")}</string>`,
51
+ " <key>NSHighResolutionCapable</key><true/>",
52
+ "</dict>",
53
+ "</plist>",
54
+ ""
55
+ ].join("\n");
56
+
57
+ writeFileSync(path.join(contents, "Info.plist"), plist);
58
+ writeFileSync(path.join(contents, "PkgInfo"), "APPL????");
59
+ if (logger) logger.info(`[app] wrote ${app}`);
60
+ return app;
61
+ }
62
+
63
+ export async function signApp({ app, identity, logger }) {
64
+ if (!identity) return;
65
+ const codesign = requireTool("codesign", "install Xcode command line tools");
66
+ await runTool(codesign, ["--force", "--deep", "--options", "runtime", "--sign", identity, app], { logger });
67
+ }
68
+
69
+ export async function buildDmg({ app, productName, outputDirectory, logger }) {
70
+ const hdiutil = requireTool("hdiutil", "run packaging on macOS");
71
+ mkdirSync(outputDirectory, { recursive: true });
72
+ const artifact = path.join(outputDirectory, `${productName.replace(/[\\/:*?"<>|]/g, "-")}.dmg`);
73
+ if (logger) logger.info(`[dmg] compiling ${path.basename(artifact)}`);
74
+ await runTool(
75
+ hdiutil,
76
+ ["create", "-volname", productName, "-srcfolder", app, "-ov", "-format", "UDZO", artifact],
77
+ { logger }
78
+ );
79
+ return artifact;
80
+ }
81
+
82
+ export function appExists(app) {
83
+ return existsSync(app);
84
+ }
@@ -0,0 +1,143 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { escapeXml, msiVersion, stableGuid } from "./common.js";
6
+ import { requireTool, runTool } from "./tools.js";
7
+
8
+ function componentId(relative) {
9
+ return `C_${createHash("sha1").update(relative).digest("hex").slice(0, 16)}`;
10
+ }
11
+
12
+ function fileId(relative) {
13
+ return `F_${createHash("sha1").update(relative).digest("hex").slice(0, 16)}`;
14
+ }
15
+
16
+ function directoryId(relative) {
17
+ return `D_${createHash("sha1").update(relative).digest("hex").slice(0, 16)}`;
18
+ }
19
+
20
+ function emitDirectory(directory, relative) {
21
+ const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) =>
22
+ left.name.localeCompare(right.name)
23
+ );
24
+ const lines = [];
25
+ const components = [];
26
+
27
+ for (const entry of entries) {
28
+ if (entry.isDirectory()) {
29
+ const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
30
+ const child = emitDirectory(path.join(directory, entry.name), childRelative);
31
+ lines.push(`<Directory Id="${directoryId(childRelative)}" Name="${escapeXml(entry.name)}">`);
32
+ lines.push(...child.xml);
33
+ lines.push("</Directory>");
34
+ components.push(...child.components);
35
+ }
36
+ }
37
+
38
+ for (const entry of entries) {
39
+ if (!entry.isFile()) continue;
40
+ const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
41
+ const id = componentId(childRelative);
42
+ lines.push(`<Component Id="${id}" Guid="*">`);
43
+ lines.push(
44
+ `<File Id="${fileId(childRelative)}" Source="${escapeXml(path.join(directory, entry.name))}" KeyPath="yes" />`
45
+ );
46
+ lines.push("</Component>");
47
+ components.push(id);
48
+ }
49
+
50
+ return { xml: lines, components };
51
+ }
52
+
53
+ export async function buildMsi({
54
+ config,
55
+ productName,
56
+ version,
57
+ payload,
58
+ binaryName,
59
+ icons,
60
+ outputDirectory,
61
+ stageDirectory,
62
+ logger
63
+ }) {
64
+ const candle = requireTool("candle", "install WiX Toolset 3.14 and put candle.exe on PATH");
65
+ const light = requireTool("light", "install WiX Toolset 3.14 and put light.exe on PATH");
66
+ const architecture = process.arch === "arm64" ? "arm64" : "x64";
67
+ const source = path.join(stageDirectory, "installer.wxs");
68
+ const object = path.join(stageDirectory, "installer.wixobj");
69
+ const artifact = path.join(
70
+ outputDirectory,
71
+ `${String(productName).replace(/[^A-Za-z0-9._-]+/g, "-")}_${version}_${architecture}_en-US.msi`
72
+ );
73
+ const payloadTree = emitDirectory(payload, "");
74
+ const upgradeCode = stableGuid(`flare:${config.app.identifier}`);
75
+ const description = config.bundle.shortDescription || config.app.description || `${productName} built with flare`;
76
+
77
+ const lines = [
78
+ '<?xml version="1.0" encoding="utf-8"?>',
79
+ '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">',
80
+ `<Product Id="*" Name="${escapeXml(productName)}" Language="1033" Version="${msiVersion(version)}" Manufacturer="${escapeXml(config.bundle.publisher || productName)}" UpgradeCode="${upgradeCode}">`,
81
+ '<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" Platform="' + architecture + '" />',
82
+ '<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />',
83
+ '<MediaTemplate EmbedCab="yes" />',
84
+ '<Property Id="ARPCOMMENTS" Value="' + escapeXml(description) + '" />',
85
+ config.bundle.homepage ? `<Property Id="ARPHELPLINK" Value="${escapeXml(config.bundle.homepage)}" />` : "",
86
+ '<Property Id="ARPPRODUCTICON" Value="AppIcon" />',
87
+ '<Icon Id="AppIcon" SourceFile="' + escapeXml(icons.ico) + '" />',
88
+ '<Directory Id="TARGETDIR" Name="SourceDir">',
89
+ '<Directory Id="ProgramFiles64Folder">',
90
+ `<Directory Id="INSTALLDIR" Name="${escapeXml(productName)}">`,
91
+ ...payloadTree.xml,
92
+ "</Directory>",
93
+ "</Directory>",
94
+ '<Directory Id="ProgramMenuFolder">',
95
+ `<Directory Id="AppMenuFolder" Name="${escapeXml(productName)}">`,
96
+ '<Component Id="ApplicationShortcut" Guid="*">',
97
+ `<Shortcut Id="ApplicationStartMenuShortcut" Name="${escapeXml(productName)}" Target="[INSTALLDIR]${escapeXml(binaryName)}" WorkingDirectory="INSTALLDIR" Icon="AppIcon" />`,
98
+ '<RemoveFolder Id="RemoveAppMenuFolder" On="uninstall" />',
99
+ '<RegistryValue Root="HKCU" Key="Software\\' + escapeXml(config.app.identifier) + '" Name="installed" Type="integer" Value="1" KeyPath="yes" />',
100
+ "</Component>",
101
+ "</Directory>",
102
+ "</Directory>",
103
+ "</Directory>",
104
+ '<Feature Id="Complete" Title="' + escapeXml(productName) + '" Level="1">',
105
+ ...payloadTree.components.map((id) => `<ComponentRef Id="${id}" />`),
106
+ '<ComponentRef Id="ApplicationShortcut" />',
107
+ "</Feature>",
108
+ '<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />',
109
+ '<UIRef Id="WixUI_InstallDir" />',
110
+ "</Product>",
111
+ "</Wix>"
112
+ ].filter(Boolean);
113
+
114
+ mkdirSync(outputDirectory, { recursive: true });
115
+ writeFileSync(source, `${lines.join("\n")}\n`, "utf8");
116
+
117
+ if (logger) logger.info(`[msi] compiling ${path.basename(artifact)}`);
118
+ await runTool(candle, ["-nologo", "-arch", architecture, "-out", object, source], {
119
+ cwd: stageDirectory,
120
+ logger
121
+ });
122
+
123
+ const extension = path.join(path.dirname(light), "WixUIExtension.dll");
124
+ const extensionArgument = existsSync(extension) ? extension : "WixUIExtension";
125
+ await runTool(
126
+ light,
127
+ [
128
+ "-nologo",
129
+ "-ext",
130
+ extensionArgument,
131
+ "-cultures:en-us",
132
+ "-spdb",
133
+ "-out",
134
+ artifact,
135
+ object
136
+ ],
137
+ {
138
+ cwd: stageDirectory,
139
+ logger
140
+ }
141
+ );
142
+ return artifact;
143
+ }
@@ -0,0 +1,106 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { escapeNsis, fourPartVersion } from "./common.js";
5
+ import { requireTool, runTool } from "./tools.js";
6
+ import { directorySize } from "./staging.js";
7
+
8
+ export async function buildNsis({
9
+ config,
10
+ productName,
11
+ version,
12
+ payload,
13
+ binaryName,
14
+ icons,
15
+ licenseSource,
16
+ outputDirectory,
17
+ stageDirectory,
18
+ logger
19
+ }) {
20
+ const makensis = requireTool("makensis", "install NSIS 3 from https://nsis.sourceforge.io");
21
+ const packageName = config.app.identifier || productName;
22
+ const estimatedSize = Math.ceil(directorySize(payload) / 1024);
23
+ const script = path.join(stageDirectory, "installer.nsi");
24
+ const artifact = path.join(
25
+ outputDirectory,
26
+ `${String(productName).replace(/[^A-Za-z0-9._-]+/g, "-")}_${version}_${process.arch === "arm64" ? "arm64" : "x64"}-setup.exe`
27
+ );
28
+
29
+ const lines = [
30
+ "Unicode true",
31
+ '!include "MUI2.nsh"',
32
+ `!define APP_NAME "${escapeNsis(productName)}"`,
33
+ `!define APP_VERSION "${escapeNsis(version)}"`,
34
+ `!define APP_VERSION4 "${fourPartVersion(version)}"`,
35
+ `!define APP_PUBLISHER "${escapeNsis(config.bundle.publisher || productName)}"`,
36
+ `!define APP_ID "${escapeNsis(packageName)}"`,
37
+ `!define APP_EXE "${escapeNsis(binaryName)}"`,
38
+ `!define APP_PAYLOAD "${escapeNsis(payload)}"`,
39
+ `!define APP_ICON "${escapeNsis(icons.ico)}"`,
40
+ `!define APP_SIZE ${estimatedSize}`,
41
+ `Name "\${APP_NAME}"`,
42
+ `OutFile "${escapeNsis(artifact)}"`,
43
+ `InstallDir "$PROGRAMFILES64\\\${APP_NAME}"`,
44
+ `InstallDirRegKey HKLM "Software\\\${APP_ID}" "InstallDir"`,
45
+ "RequestExecutionLevel admin",
46
+ "SetCompressor /SOLID lzma",
47
+ 'VIProductVersion "${APP_VERSION4}"',
48
+ 'VIAddVersionKey "ProductName" "${APP_NAME}"',
49
+ 'VIAddVersionKey "ProductVersion" "${APP_VERSION}"',
50
+ 'VIAddVersionKey "CompanyName" "${APP_PUBLISHER}"',
51
+ 'VIAddVersionKey "FileDescription" "${APP_NAME} installer"',
52
+ 'VIAddVersionKey "FileVersion" "${APP_VERSION4}"',
53
+ '!define MUI_ABORTWARNING',
54
+ '!define MUI_ICON "${APP_ICON}"',
55
+ '!define MUI_UNICON "${APP_ICON}"',
56
+ '!define MUI_FINISHPAGE_RUN "$INSTDIR\\${APP_EXE}"',
57
+ "!insertmacro MUI_PAGE_WELCOME"
58
+ ];
59
+
60
+ if (licenseSource) lines.push('!insertmacro MUI_PAGE_LICENSE "${APP_LICENSE}"');
61
+ lines.push(
62
+ "!insertmacro MUI_PAGE_DIRECTORY",
63
+ "!insertmacro MUI_PAGE_INSTFILES",
64
+ "!insertmacro MUI_PAGE_FINISH",
65
+ "!insertmacro MUI_UNPAGE_CONFIRM",
66
+ "!insertmacro MUI_UNPAGE_INSTFILES",
67
+ '!insertmacro MUI_LANGUAGE "English"',
68
+ '!insertmacro MUI_LANGUAGE "Russian"',
69
+ 'Section "Install"',
70
+ " SetShellVarContext all",
71
+ ' SetOutPath "$INSTDIR"',
72
+ ' File /r "${APP_PAYLOAD}\\*"',
73
+ ' WriteUninstaller "$INSTDIR\\Uninstall.exe"',
74
+ ' CreateDirectory "$SMPROGRAMS\\${APP_NAME}"',
75
+ ' CreateShortCut "$SMPROGRAMS\\${APP_NAME}\\${APP_NAME}.lnk" "$INSTDIR\\${APP_EXE}"',
76
+ ' CreateShortCut "$SMPROGRAMS\\${APP_NAME}\\Uninstall.lnk" "$INSTDIR\\Uninstall.exe"',
77
+ ' WriteRegStr HKLM "Software\\${APP_ID}" "InstallDir" "$INSTDIR"',
78
+ ' WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}" "DisplayName" "${APP_NAME}"',
79
+ ' WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}" "DisplayVersion" "${APP_VERSION}"',
80
+ ' WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}" "Publisher" "${APP_PUBLISHER}"',
81
+ ' WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}" "DisplayIcon" "$INSTDIR\\${APP_EXE}"',
82
+ ' WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}" "UninstallString" \'"$INSTDIR\\Uninstall.exe"\'',
83
+ ' WriteRegDWORD HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}" "EstimatedSize" ${APP_SIZE}',
84
+ ' WriteRegDWORD HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}" "NoModify" 1',
85
+ ' WriteRegDWORD HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}" "NoRepair" 1',
86
+ "SectionEnd",
87
+ 'Section "Uninstall"',
88
+ " SetShellVarContext all",
89
+ ' Delete "$SMPROGRAMS\\${APP_NAME}\\${APP_NAME}.lnk"',
90
+ ' Delete "$SMPROGRAMS\\${APP_NAME}\\Uninstall.lnk"',
91
+ ' RMDir "$SMPROGRAMS\\${APP_NAME}"',
92
+ ' DeleteRegKey HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}"',
93
+ ' DeleteRegKey HKLM "Software\\${APP_ID}"',
94
+ ' RMDir /r "$INSTDIR"',
95
+ "SectionEnd"
96
+ );
97
+
98
+ if (licenseSource) lines.splice(lines.indexOf("!insertmacro MUI_PAGE_WELCOME") + 1, 0, `!define APP_LICENSE "${escapeNsis(licenseSource)}"`);
99
+
100
+ mkdirSync(outputDirectory, { recursive: true });
101
+ writeFileSync(script, `${lines.join("\n")}\n`, "utf8");
102
+
103
+ if (logger) logger.info(`[nsis] compiling ${path.basename(artifact)}`);
104
+ await runTool(makensis, ["-V2", "-INPUTCHARSET", "UTF8", script], { cwd: stageDirectory, logger });
105
+ return artifact;
106
+ }
@@ -0,0 +1,123 @@
1
+ import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { FlareError } from "../errors.js";
5
+ import { buildIconSet } from "../icons.js";
6
+ import { readPng } from "../png.js";
7
+
8
+ export function targetTriple(platform = process.platform, architecture = process.arch) {
9
+ if (platform === "win32") return `${architecture === "arm64" ? "aarch64" : "x86_64"}-pc-windows-msvc`;
10
+ if (platform === "darwin") return `${architecture === "arm64" ? "aarch64" : "x86_64"}-apple-darwin`;
11
+ if (platform === "linux") return `${architecture === "arm64" ? "aarch64" : "x86_64"}-unknown-linux-gnu`;
12
+ return `${architecture}-unknown-${platform}`;
13
+ }
14
+
15
+ function projectPath(projectDirectory, value) {
16
+ return path.isAbsolute(value) ? value : path.resolve(projectDirectory, value);
17
+ }
18
+
19
+ function copyEntry(source, destination) {
20
+ mkdirSync(path.dirname(destination), { recursive: true });
21
+ cpSync(source, destination, { recursive: true, force: true });
22
+ }
23
+
24
+ function executableFile(file) {
25
+ if (process.platform !== "win32") chmodSync(file, 0o755);
26
+ }
27
+
28
+ export function stageIcons({ config, projectDirectory, stageDirectory }) {
29
+ const source = projectPath(projectDirectory, config.bundle?.icon || "icons/icon.png");
30
+ if (!existsSync(source)) {
31
+ throw new FlareError(`bundle icon '${source}' does not exist; run 'flare icon' or set bundle.icon`);
32
+ }
33
+ if (path.extname(source).toLowerCase() !== ".png") {
34
+ throw new FlareError(`bundle icon '${source}' must be a png file`);
35
+ }
36
+
37
+ const image = readPng(readFileSync(source));
38
+ const files = buildIconSet(image);
39
+ const directory = path.join(stageDirectory, "icons");
40
+ mkdirSync(directory, { recursive: true });
41
+ for (const file of files) writeFileSync(path.join(directory, file.name), file.data);
42
+
43
+ return {
44
+ source,
45
+ png: path.join(directory, "icon.png"),
46
+ ico: path.join(directory, "icon.ico"),
47
+ icns: path.join(directory, "icon.icns")
48
+ };
49
+ }
50
+
51
+ function externalBinary({ entry, projectDirectory, triple }) {
52
+ const base = projectPath(projectDirectory, entry);
53
+ const extension = path.extname(base);
54
+ const stem = extension ? base.slice(0, -extension.length) : base;
55
+ const candidates = [
56
+ base,
57
+ `${stem}-${triple}${extension}`,
58
+ `${base}-${triple}`,
59
+ `${stem}-${triple}${extension || ".exe"}`
60
+ ];
61
+ const source = candidates.find((candidate) => existsSync(candidate));
62
+ if (!source) {
63
+ throw new FlareError(`external binary '${entry}' was not found; expected '${base}' or '${stem}-${triple}${extension}'`);
64
+ }
65
+ return { source, name: path.basename(base) };
66
+ }
67
+
68
+ export function stagePayload({ config, projectDirectory, binary, stageDirectory, logger }) {
69
+ const payload = path.join(stageDirectory, "payload");
70
+ rmSync(payload, { recursive: true, force: true });
71
+ mkdirSync(payload, { recursive: true });
72
+
73
+ if (!existsSync(binary)) throw new FlareError(`built binary '${binary}' does not exist`);
74
+ const binaryName = path.basename(binary);
75
+ const stagedBinary = path.join(payload, binaryName);
76
+ copyEntry(binary, stagedBinary);
77
+ executableFile(stagedBinary);
78
+
79
+ for (const resource of config.bundle?.resources ?? []) {
80
+ const source = projectPath(projectDirectory, resource);
81
+ if (!existsSync(source)) throw new FlareError(`bundle resource '${resource}' does not exist`);
82
+ const destination = path.join(payload, path.basename(source));
83
+ if (logger) logger.debug(`packaging resource ${source}`);
84
+ copyEntry(source, destination);
85
+ }
86
+
87
+ const triple = targetTriple();
88
+ for (const entry of config.bundle?.externalBin ?? []) {
89
+ const { source, name } = externalBinary({ entry, projectDirectory, triple });
90
+ const destination = path.join(payload, name);
91
+ if (logger) logger.debug(`packaging external binary ${source}`);
92
+ copyEntry(source, destination);
93
+ executableFile(destination);
94
+ }
95
+
96
+ let licenseSource = null;
97
+ if (config.bundle?.licenseFile) {
98
+ licenseSource = projectPath(projectDirectory, config.bundle.licenseFile);
99
+ if (!existsSync(licenseSource) || !statSync(licenseSource).isFile()) {
100
+ throw new FlareError(`bundle license file '${config.bundle.licenseFile}' does not exist`);
101
+ }
102
+ copyEntry(licenseSource, path.join(payload, "LICENSE"));
103
+ }
104
+
105
+ return { directory: payload, binaryName, binary: stagedBinary, licenseSource };
106
+ }
107
+
108
+ export function directorySize(directory) {
109
+ let total = 0;
110
+ const walk = (current) => {
111
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
112
+ const target = path.join(current, entry.name);
113
+ if (entry.isDirectory()) walk(target);
114
+ else if (entry.isFile()) total += statSync(target).size;
115
+ }
116
+ };
117
+ walk(directory);
118
+ return total;
119
+ }
120
+
121
+ export function cleanStage(stageDirectory) {
122
+ rmSync(stageDirectory, { recursive: true, force: true });
123
+ }
@@ -0,0 +1,75 @@
1
+ import { spawn } from "node:child_process";
2
+ import { accessSync, constants } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { FlareError } from "../errors.js";
6
+
7
+ const OVERRIDES = {
8
+ makensis: "FLARE_MAKENSIS",
9
+ candle: "FLARE_CANDLE",
10
+ light: "FLARE_LIGHT",
11
+ "dpkg-deb": "FLARE_DPKG_DEB",
12
+ rpmbuild: "FLARE_RPMBUILD",
13
+ appimagetool: "FLARE_APPIMAGETOOL",
14
+ hdiutil: "FLARE_HDIUTIL",
15
+ codesign: "FLARE_CODESIGN"
16
+ };
17
+
18
+ const WINDOWS_EXTENSIONS = [".exe", ".cmd", ".bat", ""];
19
+
20
+ function executable(directory, name) {
21
+ const extensions = process.platform === "win32" ? WINDOWS_EXTENSIONS : [""];
22
+ for (const extension of extensions) {
23
+ const candidate = path.join(directory, `${name}${extension}`);
24
+ try {
25
+ accessSync(candidate, constants.X_OK);
26
+ return candidate;
27
+ } catch {
28
+ continue;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ export function findTool(name) {
35
+ const variable = OVERRIDES[name];
36
+ if (variable && process.env[variable]) return process.env[variable];
37
+
38
+ const directories = String(process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
39
+ for (const directory of directories) {
40
+ const candidate = executable(directory, name);
41
+ if (candidate) return candidate;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ export function requireTool(name, hint) {
47
+ const location = findTool(name);
48
+ if (location) return location;
49
+ const variable = OVERRIDES[name];
50
+ const suffix = variable ? `, or set ${variable} to its path` : "";
51
+ throw new FlareError(`'${name}' was not found in PATH; ${hint}${suffix}`);
52
+ }
53
+
54
+ export async function runTool(command, args, { cwd, env, logger } = {}) {
55
+ const output = [];
56
+ const result = await new Promise((resolve, reject) => {
57
+ const child = spawn(command, args, {
58
+ cwd,
59
+ env: env ? { ...process.env, ...env } : process.env,
60
+ stdio: ["ignore", "pipe", "pipe"]
61
+ });
62
+ child.stdout.on("data", (chunk) => output.push(chunk.toString()));
63
+ child.stderr.on("data", (chunk) => output.push(chunk.toString()));
64
+ child.on("error", reject);
65
+ child.on("close", (code, signal) => resolve({ code: code ?? 1, signal }));
66
+ });
67
+
68
+ const text = output.join("").trim();
69
+ if (result.code !== 0) {
70
+ const tail = text.split(/\r?\n/).slice(-20).join("\n");
71
+ throw new FlareError(`'${path.basename(command)}' exited with code ${result.code}${tail ? `:\n${tail}` : ""}`);
72
+ }
73
+ if (logger && text) logger.debug(text);
74
+ return { command, args, output: text };
75
+ }