@rightkit/release 0.2.137 → 0.2.139

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,146 @@
1
+ // Assemble a macOS .app bundle from a SwiftPM executable, then sign (and
2
+ // optionally notarize) it through the existing RightKit signers. Signing is
3
+ // inside-out: nested code first (no entitlements), then the bundle. This module
4
+ // owns bundle assembly only; sign-macos.mjs and notarize-macos.mjs own signing.
5
+
6
+ import { cpSync, existsSync, mkdirSync, openSync, readSync, closeSync, readdirSync, rmSync, statSync, writeFileSync, lstatSync, chmodSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { spawnSync } from "./background-process.mjs";
10
+
11
+ const fail = (code, message) => Object.assign(new Error(message), { code });
12
+ const here = path.dirname(fileURLToPath(import.meta.url));
13
+ const xml = (v) => String(v).replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
14
+
15
+ function plistValue(value, indent) {
16
+ const pad = "\t".repeat(indent);
17
+ if (typeof value === "string") return `${pad}<string>${xml(value)}</string>`;
18
+ if (typeof value === "boolean") return `${pad}<${value}/>`;
19
+ if (Number.isInteger(value)) return `${pad}<integer>${value}</integer>`;
20
+ if (typeof value === "number") return `${pad}<real>${value}</real>`;
21
+ if (Array.isArray(value)) return `${pad}<array>\n${value.map((v) => plistValue(v, indent + 1)).join("\n")}\n${pad}</array>`;
22
+ if (value && typeof value === "object") {
23
+ const rows = Object.keys(value).sort().map((k) => `${pad}\t<key>${xml(k)}</key>\n${plistValue(value[k], indent + 1)}`);
24
+ return `${pad}<dict>\n${rows.join("\n")}\n${pad}</dict>`;
25
+ }
26
+ throw fail("MAC_APP_PLIST", `unsupported plist value: ${value}`);
27
+ }
28
+
29
+ export function generateInfoPlist(o) {
30
+ const errors = [];
31
+ if (!/^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(o.name ?? "")) errors.push("name must be a simple bundle name");
32
+ if (!/^[A-Za-z0-9.-]+\.[A-Za-z0-9.-]+$/.test(o.bundleId ?? "")) errors.push("bundleId must be reverse-DNS");
33
+ if (!/^\d+(\.\d+){0,2}$/.test(o.version ?? "")) errors.push("version must be 1-3 numeric parts");
34
+ if (!/^\d+(\.\d+){0,2}$/.test(o.build ?? o.version ?? "")) errors.push("build must be 1-3 numeric parts");
35
+ if (!o.executable || /[\\/]/.test(o.executable)) errors.push("executable must be a flat file name");
36
+ if (errors.length) throw fail("MAC_APP_INVALID", errors.join("; "));
37
+ const info = {
38
+ CFBundleDevelopmentRegion: "en", CFBundleExecutable: o.executable, CFBundleIdentifier: o.bundleId,
39
+ CFBundleInfoDictionaryVersion: "6.0", CFBundleName: o.name, CFBundleDisplayName: o.displayName ?? o.name,
40
+ CFBundlePackageType: "APPL", CFBundleShortVersionString: o.version, CFBundleVersion: o.build ?? o.version,
41
+ LSMinimumSystemVersion: o.minimumSystemVersion ?? "14.0", NSHighResolutionCapable: true, NSPrincipalClass: "NSApplication",
42
+ ...(o.iconFile ? { CFBundleIconFile: path.basename(o.iconFile) } : {}),
43
+ ...(o.infoPlistExtra ?? {}),
44
+ };
45
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n${plistValue(info, 0)}\n</plist>\n`;
46
+ }
47
+
48
+ const MACHO = new Set([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe, 0xbebafeca]);
49
+ function isMachO(file) {
50
+ const fd = openSync(file, "r"); const b = Buffer.alloc(4);
51
+ try { return readSync(fd, b, 0, 4, 0) === 4 && MACHO.has(b.readUInt32BE(0)); } finally { closeSync(fd); }
52
+ }
53
+ const BUNDLE_DIR = /\.(framework|app|xpc|bundle|appex)$/;
54
+
55
+ /** Nested code to sign before the app: Mach-O files and code bundles, deepest first. Main executable excluded. */
56
+ export function discoverNestedCode(app, mainName) {
57
+ const main = path.join(app, "Contents", "MacOS");
58
+ const found = [];
59
+ const walk = (dir) => {
60
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
61
+ const full = path.join(dir, entry.name);
62
+ if (entry.isSymbolicLink()) continue;
63
+ if (entry.isDirectory()) {
64
+ walk(full);
65
+ if (/\.framework$/.test(entry.name) || (BUNDLE_DIR.test(entry.name) && existsSync(path.join(full, "Contents")))) found.push(full);
66
+ } else if (entry.isFile() && full !== main && !full.startsWith(main + path.sep) && isMachO(full)) found.push(full);
67
+ }
68
+ };
69
+ for (const sub of ["Contents/Frameworks", "Contents/Resources", "Contents/PlugIns", "Contents/XPCServices", "Contents/Helpers", "Contents/Library"]) {
70
+ const dir = path.join(app, sub);
71
+ if (existsSync(dir)) walk(dir);
72
+ }
73
+ const mainDir = path.join(app, "Contents", "MacOS");
74
+ for (const entry of readdirSync(mainDir)) { /* helpers beside main: sign them too */
75
+ const full = path.join(mainDir, entry);
76
+ if (entry !== mainName && lstatSync(full).isFile() && isMachO(full)) found.push(full);
77
+ }
78
+ const depth = (p) => p.split(path.sep).length;
79
+ return [...new Set(found)].sort((a, b) => depth(b) - depth(a) || a.localeCompare(b));
80
+ }
81
+
82
+ /**
83
+ * Assemble `<out>/<name>.app`. options: { executable (SwiftPM binary), name, bundleId, version, build,
84
+ * resources[] (files/dirs, e.g. SwiftPM *.bundle, copied into Contents/Resources), frameworks[] (dylibs/.frameworks
85
+ * copied into Contents/Frameworks), iconFile, infoPlistExtra, out }.
86
+ */
87
+ export function assembleMacApp(o, { run = spawnSync, platform = process.platform } = {}) {
88
+ const exe = path.resolve(o.executable ?? "");
89
+ if (!existsSync(exe) || !statSync(exe).isFile()) throw fail("MAC_APP_INVALID", `executable not found: ${exe}`);
90
+ const execName = o.executableName ?? path.basename(exe);
91
+ const app = path.join(path.resolve(o.out), `${o.name}.app`);
92
+ const plist = generateInfoPlist({ ...o, executable: execName });
93
+ rmSync(app, { recursive: true, force: true });
94
+ const contents = path.join(app, "Contents");
95
+ for (const dir of ["MacOS", "Resources", ...(o.frameworks?.length ? ["Frameworks"] : [])]) mkdirSync(path.join(contents, dir), { recursive: true });
96
+ cpSync(exe, path.join(contents, "MacOS", execName)); chmodSync(path.join(contents, "MacOS", execName), 0o755);
97
+ const copyInto = (list, sub) => {
98
+ for (const item of list ?? []) {
99
+ const source = path.resolve(item);
100
+ if (!existsSync(source)) throw fail("MAC_APP_INVALID", `missing input: ${source}`);
101
+ cpSync(source, path.join(contents, sub, path.basename(source)), { recursive: true, verbatimSymlinks: true });
102
+ }
103
+ };
104
+ copyInto(o.resources, "Resources"); copyInto(o.frameworks, "Frameworks");
105
+ if (o.iconFile) copyInto([o.iconFile], "Resources");
106
+ writeFileSync(path.join(contents, "Info.plist"), plist);
107
+ writeFileSync(path.join(contents, "PkgInfo"), "APPL????");
108
+ if (o.frameworks?.length && platform === "darwin") {
109
+ // SwiftPM binaries do not search Contents/Frameworks by default.
110
+ const probe = run("otool", ["-l", path.join(contents, "MacOS", execName)], { encoding: "utf8" });
111
+ if (!probe.error && !/@executable_path\/\.\.\/Frameworks/.test(probe.stdout ?? "")) {
112
+ const r = run("install_name_tool", ["-add_rpath", "@executable_path/../Frameworks", path.join(contents, "MacOS", execName)], { encoding: "utf8" });
113
+ if (r.error || r.status !== 0) throw fail("MAC_APP_RPATH", `install_name_tool failed: ${r.error?.message ?? r.stderr}`);
114
+ }
115
+ }
116
+ return { app, nested: discoverNestedCode(app, execName), executable: execName };
117
+ }
118
+
119
+ /** Sign inside-out through sign-macos.mjs, then optionally notarize via notarize-macos.mjs. */
120
+ export function signAndNotarizeMacApp({ app, nested, entitlements, receiptDir, notarize = false, profile, dryRun = false },
121
+ { run = spawnSync, platform = process.platform, log = (e) => console.error(JSON.stringify(e)) } = {}) {
122
+ const plan = [];
123
+ const node = (script, args) => { plan.push([script, ...args]); return args; };
124
+ const receipts = path.resolve(receiptDir);
125
+ const dry = dryRun ? ["--dry-run"] : [];
126
+ if (!dryRun && platform !== "darwin") throw fail("MAC_APP_NATIVE_HOST_REQUIRED", "macOS signing and notarization must run on macOS");
127
+ const calls = [];
128
+ if (nested.length) calls.push(["sign-macos.mjs", [...dry, "--receipt", path.join(receipts, "app-nested-sign.receipt.json"), ...nested]]);
129
+ calls.push(["sign-macos.mjs", [...dry, "--receipt", path.join(receipts, "app-sign.receipt.json"), ...(entitlements ? ["--entitlements", path.resolve(entitlements)] : []), app]]);
130
+ let zip;
131
+ if (notarize) {
132
+ zip = `${app}.zip`;
133
+ calls.push(["__ditto__", ["-c", "-k", "--keepParent", app, zip]]);
134
+ calls.push(["notarize-macos.mjs", [...dry, "--receipt", path.join(receipts, "app-notarize.receipt.json"), ...(profile ? ["--profile", profile] : []), "--app", app, zip]]);
135
+ }
136
+ if (!dryRun) mkdirSync(receipts, { recursive: true });
137
+ for (const [script, args] of calls) {
138
+ node(script, args);
139
+ if (dryRun && script === "__ditto__") continue;
140
+ const r = script === "__ditto__" ? run("/usr/bin/ditto", args, { encoding: "utf8" })
141
+ : run(process.execPath, [path.join(here, script), ...args], { encoding: "utf8" });
142
+ if (r.error || r.status !== 0) throw fail("MAC_APP_COMMAND_FAILED", `${script} failed: ${r.error?.message ?? r.stderr ?? r.stdout ?? r.status}`);
143
+ log({ event: "mac-app-step", script, ok: true });
144
+ }
145
+ return { plan, zip };
146
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ // right-release swift-package <windows-stage|msix|mac-app|doctor> --config <swift-package.json> [options]
3
+ //
4
+ // One product config (schema 1) drives all three steps; paths resolve against the config file.
5
+ // Build-only: nothing here uploads or publishes.
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { assembleMacApp, signAndNotarizeMacApp } from "./swift-mac-app.mjs";
10
+ import { buildMsix } from "./msix-build.mjs";
11
+ import { stageSwiftWindows } from "./swift-windows-stage.mjs";
12
+
13
+ const fail = (code, message) => Object.assign(new Error(message), { code });
14
+
15
+ export function loadSwiftPackageConfig(file) {
16
+ const configPath = path.resolve(file ?? "");
17
+ if (!existsSync(configPath)) throw fail("SWIFT_PACKAGE_CONFIG", `config not found: ${configPath}`);
18
+ let config;
19
+ try { config = JSON.parse(readFileSync(configPath, "utf8")); } catch (e) { throw fail("SWIFT_PACKAGE_CONFIG", `invalid JSON: ${e.message}`); }
20
+ if (config.schema !== 1) throw fail("SWIFT_PACKAGE_CONFIG", "config.schema must be 1");
21
+ if (!config.name || !config.version) throw fail("SWIFT_PACKAGE_CONFIG", "config.name and config.version are required");
22
+ return { config, base: path.dirname(configPath) };
23
+ }
24
+
25
+ const rel = (base, value) => (value == null ? value : path.resolve(base, value));
26
+ const relList = (base, list) => (list ?? []).map((v) => rel(base, v));
27
+
28
+ export function windowsStageOptions({ config, base }) {
29
+ const w = config.windows;
30
+ if (!w?.exe || !w?.out) throw fail("SWIFT_PACKAGE_CONFIG", "windows.exe and windows.out are required");
31
+ return { exe: rel(base, w.exe), out: rel(base, w.stageDir ?? `${w.out}.stage`), searchDirs: relList(base, w.searchDirs),
32
+ include: relList(base, w.include), systemDlls: w.systemDlls, dependencies: w.dependencies, architecture: w.architecture };
33
+ }
34
+
35
+ export function msixOptions({ config, base }, staged) {
36
+ const w = config.windows;
37
+ const manifest = { name: w.packageName ?? config.name, version: w.msixVersion ?? `${config.version.replace(/^(\d+\.\d+\.\d+).*$/, "$1")}.0`,
38
+ architecture: staged.architecture, publisher: w.publisher, publisherDisplayName: w.publisherDisplayName,
39
+ displayName: config.displayName ?? config.name, description: config.description, executable: staged.exe,
40
+ dependencies: w.dependencies, capabilities: w.capabilities, minWindowsVersion: w.minWindowsVersion };
41
+ const assets = Object.fromEntries(Object.entries(w.assets ?? {}).map(([k, v]) => [k, rel(base, v)]));
42
+ return { manifest, assets, out: rel(base, w.out), stageDir: staged.out };
43
+ }
44
+
45
+ export function macOptions({ config, base }) {
46
+ const m = config.mac;
47
+ if (!m?.executable || !m?.bundleId || !m?.out) throw fail("SWIFT_PACKAGE_CONFIG", "mac.executable, mac.bundleId and mac.out are required");
48
+ return { executable: rel(base, m.executable), name: m.appName ?? config.displayName ?? config.name, displayName: config.displayName, bundleId: m.bundleId,
49
+ version: config.version, build: m.build, minimumSystemVersion: m.minimumSystemVersion, resources: relList(base, m.resources),
50
+ frameworks: relList(base, m.frameworks), iconFile: rel(base, m.iconFile), infoPlistExtra: m.infoPlist, out: rel(base, m.out) };
51
+ }
52
+
53
+ export async function main(argv, deps = {}) {
54
+ const [command, ...rest] = argv;
55
+ const flag = (name) => { const i = rest.indexOf(name); return i >= 0 ? rest[i + 1] : undefined; };
56
+ const has = (name) => rest.includes(name);
57
+ const log = deps.log ?? ((v) => console.log(JSON.stringify(v, null, 2)));
58
+ if (!["windows-stage", "msix", "mac-app"].includes(command)) {
59
+ throw fail("SWIFT_PACKAGE_USAGE", "usage: swift-package <windows-stage|msix|mac-app> --config <file> [--dry-run] [--no-sign] [--notarize]");
60
+ }
61
+ const loaded = loadSwiftPackageConfig(flag("--config"));
62
+ if (command === "windows-stage") return log(stageSwiftWindows(windowsStageOptions(loaded)));
63
+ if (command === "msix") {
64
+ const staged = stageSwiftWindows(windowsStageOptions(loaded));
65
+ const options = msixOptions(loaded, staged);
66
+ return log(buildMsix({ ...options, sign: !has("--no-sign"), dryRun: has("--dry-run") }, deps.msix));
67
+ }
68
+ const options = macOptions(loaded);
69
+ const assembled = assembleMacApp(options, deps.mac);
70
+ const out = { app: assembled.app, nested: assembled.nested };
71
+ if (has("--sign") || has("--notarize")) {
72
+ out.signing = signAndNotarizeMacApp({ app: assembled.app, nested: assembled.nested,
73
+ entitlements: rel(loaded.base, loaded.config.mac.entitlements), receiptDir: path.join(options.out, "receipts"),
74
+ notarize: has("--notarize"), profile: flag("--profile"), dryRun: has("--dry-run") }, deps.mac);
75
+ }
76
+ return log(out);
77
+ }
78
+
79
+ const self = (file) => { try { return path.resolve(file) === fileURLToPath(import.meta.url); } catch { return false; } };
80
+ if (process.argv[1] && self(process.argv[1])) {
81
+ main(process.argv.slice(2)).catch((error) => {
82
+ console.error(`right-release swift-package: [${error.code ?? "SWIFT_PACKAGE_FAILED"}] ${error.message}`);
83
+ process.exitCode = error.code === "SWIFT_PACKAGE_USAGE" ? 2 : 1;
84
+ });
85
+ }
@@ -0,0 +1,103 @@
1
+ // Stage a SwiftPM-built Windows app for packaging: copy the executable and every
2
+ // app-local DLL it needs (transitively) into one flat directory, and fail closed
3
+ // on any dependency that is neither staged, a Windows system DLL, nor provided by
4
+ // a declared package dependency. Runs on any host (pure file + PE parsing).
5
+
6
+ import { createHash } from "node:crypto";
7
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
8
+ import path from "node:path";
9
+ import { readPeImports } from "./pe-imports.mjs";
10
+
11
+ const fail = (code, message) => Object.assign(new Error(message), { code });
12
+
13
+ // Windows-owned DLLs present on every supported install. Anything else must ship.
14
+ export const WINDOWS_SYSTEM_DLLS = Object.freeze([
15
+ "kernel32.dll", "kernelbase.dll", "ntdll.dll", "user32.dll", "gdi32.dll", "gdi32full.dll", "advapi32.dll", "shell32.dll",
16
+ "shlwapi.dll", "ole32.dll", "oleaut32.dll", "combase.dll", "rpcrt4.dll", "sechost.dll", "ws2_32.dll", "wsock32.dll",
17
+ "mswsock.dll", "iphlpapi.dll", "bcrypt.dll", "bcryptprimitives.dll", "ncrypt.dll", "crypt32.dll", "secur32.dll", "wintrust.dll",
18
+ "userenv.dll", "version.dll", "winmm.dll", "comctl32.dll", "comdlg32.dll", "imm32.dll", "uxtheme.dll", "dwmapi.dll",
19
+ "d2d1.dll", "d3d11.dll", "d3d12.dll", "dxgi.dll", "dwrite.dll", "dcomp.dll", "uiautomationcore.dll", "propsys.dll",
20
+ "msctf.dll", "setupapi.dll", "cfgmgr32.dll", "shcore.dll", "powrprof.dll", "psapi.dll", "dbghelp.dll", "wldap32.dll",
21
+ "normaliz.dll", "netapi32.dll", "winhttp.dll", "wininet.dll", "ucrtbase.dll", "msvcrt.dll", "oleacc.dll", "avrt.dll",
22
+ "mf.dll", "mfplat.dll", "dnsapi.dll", "wtsapi32.dll", "winspool.drv", "ntmarta.dll", "dxcore.dll", "windows.ui.dll", "cabinet.dll",
23
+ ]);
24
+
25
+ const isApiSet = (name) => /^(api|ext)-ms-win-[a-z0-9-]+-l\d+-\d+-\d+\.dll$/.test(name);
26
+ const lower = (value) => String(value).toLowerCase();
27
+ const isPe = (file) => /\.(exe|dll)$/i.test(file);
28
+ const sha256 = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
29
+
30
+ function findInDirs(name, dirs) {
31
+ for (const dir of dirs) {
32
+ if (!existsSync(dir)) continue;
33
+ const match = readdirSync(dir).find((entry) => lower(entry) === lower(name));
34
+ if (match) return path.join(dir, match);
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * options: { exe, out, searchDirs[], include[] (extra files copied verbatim),
41
+ * systemDlls[] (extra allowed), dependencies[{ name, provides[] }], architecture }
42
+ * Returns a manifest; throws SWIFT_STAGE_UNRESOLVED listing every missing DLL.
43
+ */
44
+ export function stageSwiftWindows({ exe, out, searchDirs = [], include = [], systemDlls = [], dependencies = [], architecture, clean = true } = {}) {
45
+ if (!exe || !out) throw fail("SWIFT_STAGE_OPTIONS", "exe and out are required");
46
+ exe = path.resolve(exe); out = path.resolve(out);
47
+ if (!/\.exe$/i.test(exe) || !existsSync(exe) || !statSync(exe).isFile()) throw fail("SWIFT_STAGE_OPTIONS", `exe must be an existing .exe: ${exe}`);
48
+ const dirs = [path.dirname(exe), ...searchDirs.map((dir) => path.resolve(dir))];
49
+ const system = new Set([...WINDOWS_SYSTEM_DLLS, ...systemDlls].map(lower));
50
+ const provided = new Map();
51
+ for (const dep of dependencies) for (const dll of dep.provides ?? []) provided.set(lower(dll), dep.name);
52
+ if (clean) rmSync(out, { recursive: true, force: true });
53
+ mkdirSync(out, { recursive: true });
54
+
55
+ const staged = new Map(); // lower name -> { file(source), name }
56
+ const queue = [{ name: path.basename(exe), source: exe, from: "<root>" }];
57
+ const missing = new Map(); // lower name -> importers
58
+ const seenSystem = new Set(), seenProvided = new Map();
59
+ let arch = architecture;
60
+ const extra = include.map((file) => path.resolve(file));
61
+ for (const file of extra) {
62
+ if (!existsSync(file) || !statSync(file).isFile()) throw fail("SWIFT_STAGE_OPTIONS", `include file missing: ${file}`);
63
+ queue.push({ name: path.basename(file), source: file, from: "<include>" });
64
+ }
65
+ while (queue.length) {
66
+ const item = queue.shift();
67
+ const key = lower(item.name);
68
+ if (staged.has(key)) continue;
69
+ staged.set(key, item);
70
+ if (!isPe(item.name)) continue;
71
+ const info = readPeImports(readFileSync(item.source));
72
+ if (!["x64", "arm64"].includes(info.architecture)) throw fail("SWIFT_STAGE_ARCH", `${item.source} is ${info.architecture}; only x64 and arm64 are supported`);
73
+ arch ??= info.architecture;
74
+ if (info.architecture !== arch) throw fail("SWIFT_STAGE_ARCH", `${item.source} is ${info.architecture} but package architecture is ${arch}`);
75
+ for (const dep of [...info.imports, ...info.delayImports]) {
76
+ const dk = lower(dep);
77
+ if (staged.has(dk)) continue;
78
+ const found = findInDirs(dep, dirs);
79
+ if (found) { queue.push({ name: path.basename(found), source: found, from: item.name }); continue; }
80
+ if (isApiSet(dk) || system.has(dk)) { seenSystem.add(dk); continue; }
81
+ if (provided.has(dk)) { seenProvided.set(dk, provided.get(dk)); continue; }
82
+ if (!missing.has(dk)) missing.set(dk, new Set());
83
+ missing.get(dk).add(item.name);
84
+ }
85
+ }
86
+ if (missing.size) {
87
+ const detail = [...missing].map(([dll, by]) => `${dll} (needed by ${[...by].join(", ")})`).join("; ");
88
+ throw fail("SWIFT_STAGE_UNRESOLVED", `unresolved DLL dependencies: ${detail}. Add the directory to searchDirs, stage it via include, or declare a package dependency that provides it`);
89
+ }
90
+ const files = [];
91
+ for (const [, item] of [...staged].sort(([a], [b]) => a.localeCompare(b))) {
92
+ const target = path.join(out, item.name);
93
+ copyFileSync(item.source, target);
94
+ files.push({ name: item.name, source: item.source, sha256: sha256(target), sizeBytes: statSync(target).size, requiredBy: item.from, pe: isPe(item.name) });
95
+ }
96
+ const manifest = {
97
+ schema: 1, architecture: arch, exe: path.basename(exe), out, files,
98
+ systemDlls: [...seenSystem].sort(),
99
+ providedByDependencies: Object.fromEntries([...seenProvided].sort()),
100
+ };
101
+ writeFileSync(`${out}.stage.json`, `${JSON.stringify(manifest, null, 2)}\n`);
102
+ return manifest;
103
+ }