@rightkit/release 0.2.138 → 0.2.140
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 +16 -0
- package/build-release.mjs +30 -21
- package/cache-policy.mjs +14 -0
- package/committed-source.mjs +2 -1
- package/dmg-build.mjs +10 -3
- package/hdiutil-detach.mjs +42 -0
- package/install-app.mjs +98 -37
- package/macos-notarization.mjs +21 -1
- package/managed-cargo.mjs +4 -1
- package/msix-appinstaller.mjs +24 -0
- package/msix-build.mjs +48 -12
- package/msix-config.mjs +70 -0
- package/msix-plan.mjs +68 -0
- package/msix-target.mjs +28 -0
- package/ownership.mjs +24 -0
- package/package.json +1 -1
- package/preflight.mjs +37 -23
- package/project-type.mjs +43 -0
- package/publish-update.mjs +11 -5
- package/release-invocation.mjs +8 -6
- package/release.mjs +55 -10
- package/rightkit-versions.json +18 -12
- package/source-gate.mjs +21 -0
- package/swift-build-context.mjs +43 -0
package/macos-notarization.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
@@ -70,6 +70,26 @@ export function notarizeMacosArtifact({ archive, receiptPath, authArgs, profile,
|
|
|
70
70
|
return receipt;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Notarize and staple a signed .app BEFORE it is packaged into a DMG, so a copy dragged out of the
|
|
75
|
+
* DMG passes `stapler validate` offline. The app is zipped with ditto (notarytool cannot take a bare
|
|
76
|
+
* bundle), submitted, and its own ticket stapled; the DMG is then notarized and stapled as usual.
|
|
77
|
+
* The caller removes the zip after validating the receipt, which hashes it. The receipt records the submitted zip; its stapling evidence names the app bundle.
|
|
78
|
+
*/
|
|
79
|
+
export function notarizeAppBundle({ app, receiptPath, profile, authArgs, zip, run = spawnSync, env = process.env, ...agentOptions }) {
|
|
80
|
+
app = path.resolve(app);
|
|
81
|
+
if (!/\.app$/i.test(app)) throw new Error(`notarize.app must name an app bundle: ${app}`);
|
|
82
|
+
zip = path.resolve(zip ?? `${app}.zip`);
|
|
83
|
+
rmSync(zip, { force: true });
|
|
84
|
+
checkedDitto(["-c", "-k", "--keepParent", app, zip], run);
|
|
85
|
+
return notarizeMacosArtifact({ archive: zip, receiptPath, profile, authArgs, app, run, env, ...agentOptions });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function checkedDitto(args, run) {
|
|
89
|
+
const result = run("/usr/bin/ditto", args, { encoding: "utf8" });
|
|
90
|
+
if (result.error || result.status !== 0) throw new Error(`ditto failed: ${result.error?.message ?? result.stderr ?? result.stdout ?? result.status}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
export function validateNotarizationReceipt({ receiptPath, archive, run = spawnSync }) {
|
|
74
94
|
let receipt;
|
|
75
95
|
try { receipt = JSON.parse(readFileSync(receiptPath, "utf8")); }
|
package/managed-cargo.mjs
CHANGED
|
@@ -56,7 +56,10 @@ export function runCargoSync(args, options = {}, { platform = process.platform,
|
|
|
56
56
|
}
|
|
57
57
|
return result;
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
// The Mac never falls back to direct Cargo, except on GitHub Actions runners, which have no
|
|
60
|
+
// RightKit runtime and use the toolchain's cargo (release-candidate signing jobs).
|
|
61
|
+
const hostedCi = env.GITHUB_ACTIONS === 'true' && !managedHost(env);
|
|
62
|
+
if ((platform === 'darwin' && !hostedCi) || (managedHost(env) && platform === 'win32')) return unavailable('installed RightKit runtime client is unavailable for managed Cargo metadata');
|
|
60
63
|
return spawn("cargo", args, options);
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// App Installer (.appinstaller) update route for MSIX packages. Trust comes from the
|
|
2
|
+
// MSIX Authenticode signature (publisher must match the manifest); no separate updater key.
|
|
3
|
+
|
|
4
|
+
const xml = (value) => String(value).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
5
|
+
const fail = (message) => Object.assign(new Error(message), { code: "APPINSTALLER_INVALID" });
|
|
6
|
+
|
|
7
|
+
export function generateAppInstaller(o) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
for (const key of ["uri", "msixUri"]) { try { if (new URL(o[key]).protocol !== "https:") throw 0; } catch { errors.push(`${key} must be an https URL`); } }
|
|
10
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(o.version ?? "")) errors.push("version must be four numeric parts");
|
|
11
|
+
if (!["x64", "arm64"].includes(o.architecture)) errors.push("architecture must be x64 or arm64");
|
|
12
|
+
if (!o.name || !o.publisher) errors.push("name and publisher are required");
|
|
13
|
+
const hours = o.hoursBetweenUpdateChecks ?? 24;
|
|
14
|
+
if (!Number.isInteger(hours) || hours < 0 || hours > 255) errors.push("hoursBetweenUpdateChecks must be an integer 0-255");
|
|
15
|
+
if (errors.length) throw fail(errors.join("; "));
|
|
16
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
17
|
+
<AppInstaller xmlns="http://schemas.microsoft.com/appx/appinstaller/2018" Version="${xml(o.version)}" Uri="${xml(o.uri)}">
|
|
18
|
+
<MainPackage Name="${xml(o.name)}" Publisher="${xml(o.publisher)}" Version="${xml(o.version)}" ProcessorArchitecture="${o.architecture}" Uri="${xml(o.msixUri)}" />
|
|
19
|
+
<UpdateSettings>
|
|
20
|
+
<OnLaunch HoursBetweenUpdateChecks="${hours}" ShowPrompt="${o.showPrompt ? "true" : "false"}" UpdateBlocksActivation="${o.updateBlocksActivation ? "true" : "false"}" />
|
|
21
|
+
</UpdateSettings>
|
|
22
|
+
</AppInstaller>
|
|
23
|
+
`;
|
|
24
|
+
}
|
package/msix-build.mjs
CHANGED
|
@@ -9,6 +9,9 @@ import path from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { spawnSync } from "./background-process.mjs";
|
|
11
11
|
import { findWindowsSdkTool } from "./preflight.mjs";
|
|
12
|
+
import { generateAppInstaller } from "./msix-appinstaller.mjs";
|
|
13
|
+
import { assertPublisherMatchesSigner, planMsixSigning, verifyPayloadInventory } from "./msix-plan.mjs";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
12
15
|
|
|
13
16
|
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
14
17
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -33,17 +36,25 @@ export function generateAppxManifest(o) {
|
|
|
33
36
|
validateMsixIdentity(o);
|
|
34
37
|
const deps = (o.dependencies ?? []).filter((d) => d.minVersion).map((d) =>
|
|
35
38
|
` <PackageDependency Name="${xml(d.name)}" MinVersion="${xml(d.minVersion)}" Publisher="${xml(d.publisher ?? "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US")}" />`);
|
|
36
|
-
const
|
|
39
|
+
const v = o.virtualization;
|
|
40
|
+
for (const key of ["fileSystemWrite", "registryWrite"]) {
|
|
41
|
+
if (v?.[key] !== undefined && !["enabled", "disabled"].includes(v[key])) throw fail("MSIX_INVALID_IDENTITY", `virtualization.${key} must be "enabled" or "disabled"`);
|
|
42
|
+
}
|
|
43
|
+
const virt = v ? [["fileSystemWrite", "FileSystemWriteVirtualization"], ["registryWrite", "RegistryWriteVirtualization"]]
|
|
44
|
+
.filter(([k]) => v[k]).map(([k, el]) => `\n <desktop6:${el}>${v[k]}</desktop6:${el}>`).join("") : "";
|
|
45
|
+
// Opting out of virtualization is a restricted capability.
|
|
46
|
+
const unvirtualized = v && (v.fileSystemWrite === "disabled" || v.registryWrite === "disabled") ? '\n <rescap:Capability Name="unvirtualizedResources" />' : "";
|
|
37
47
|
return `<?xml version="1.0" encoding="utf-8"?>
|
|
38
48
|
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
|
39
49
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
|
40
50
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
|
51
|
+
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
|
|
41
52
|
IgnorableNamespaces="uap rescap">
|
|
42
53
|
<Identity Name="${xml(o.name)}" Publisher="${xml(o.publisher)}" Version="${xml(o.version)}" ProcessorArchitecture="${o.architecture}" />
|
|
43
54
|
<Properties>
|
|
44
55
|
<DisplayName>${xml(o.displayName)}</DisplayName>
|
|
45
56
|
<PublisherDisplayName>${xml(o.publisherDisplayName)}</PublisherDisplayName>
|
|
46
|
-
<Logo>Assets\\StoreLogo.png</Logo
|
|
57
|
+
<Logo>Assets\\StoreLogo.png</Logo>${virt}
|
|
47
58
|
</Properties>
|
|
48
59
|
<Dependencies>
|
|
49
60
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="${xml(o.minWindowsVersion ?? "10.0.19041.0")}" MaxVersionTested="${xml(o.maxWindowsVersionTested ?? "10.0.26100.0")}" />
|
|
@@ -56,7 +67,7 @@ ${deps.join("\n")}${deps.length ? "\n" : ""} </Dependencies>
|
|
|
56
67
|
</Application>
|
|
57
68
|
</Applications>
|
|
58
69
|
<Capabilities>
|
|
59
|
-
<rescap:Capability Name="runFullTrust" />${(o.capabilities ?? []).map((c) => `\n <Capability Name="${xml(c)}" />`).join("")}
|
|
70
|
+
<rescap:Capability Name="runFullTrust" />${unvirtualized}${(o.capabilities ?? []).map((c) => `\n <Capability Name="${xml(c)}" />`).join("")}
|
|
60
71
|
</Capabilities>
|
|
61
72
|
</Package>
|
|
62
73
|
`;
|
|
@@ -89,19 +100,30 @@ export function peFilesIn(root) {
|
|
|
89
100
|
return readdirSync(root).filter((name) => /\.(exe|dll)$/i.test(name)).sort().map((name) => path.join(root, name));
|
|
90
101
|
}
|
|
91
102
|
|
|
103
|
+
const sha256 = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
104
|
+
|
|
105
|
+
function readSignerSubject(file, run) {
|
|
106
|
+
const r = run("powershell", ["-NoProfile", "-Command", `(Get-AuthenticodeSignature -LiteralPath '${file.replace(/'/g, "''")}').SignerCertificate.Subject`], { encoding: "utf8", windowsHide: true });
|
|
107
|
+
return r.status === 0 ? String(r.stdout ?? "").trim() : "";
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
/**
|
|
93
|
-
* Pack + sign. `run` is injectable for tests.
|
|
94
|
-
*
|
|
95
|
-
* Windows-only unless dryRun.
|
|
111
|
+
* Pack + sign. `run` is injectable for tests. Ordered per planMsixSigning():
|
|
112
|
+
* assemble, verify-inventory, sign-pe (DLLs then EXEs, raw bytes), pack, sign-msix, verify-msix,
|
|
113
|
+
* verify-publisher, appinstaller, seal. Windows-only unless dryRun.
|
|
114
|
+
* options.runtime = { dlls, systemDlls, dependencies } (declared inventory, verified fail-closed);
|
|
115
|
+
* options.appInstaller = { uri, msixUri, out, ... } generates the update route after the package is signed.
|
|
96
116
|
*/
|
|
97
|
-
export function buildMsix({ stageDir, out, workDir, manifest, assets, sign = true, dryRun = false, receiptDir },
|
|
98
|
-
{ platform = process.platform, run = spawnSync, findTool = findWindowsSdkTool, env = process.env, log = (e) => console.error(JSON.stringify(e)) } = {}) {
|
|
117
|
+
export function buildMsix({ stageDir, out, workDir, manifest, assets, runtime, appInstaller, sign = true, dryRun = false, receiptDir, peReceipt, msixReceipt },
|
|
118
|
+
{ platform = process.platform, run = spawnSync, findTool = findWindowsSdkTool, env = process.env, log = (e) => console.error(JSON.stringify(e)), readSubject = (file) => readSignerSubject(file, run) } = {}) {
|
|
99
119
|
out = path.resolve(out);
|
|
100
120
|
if (!out.endsWith(".msix")) throw fail("MSIX_INVALID_OPTIONS", "out must end with .msix");
|
|
101
121
|
validateMsixIdentity(manifest);
|
|
102
122
|
workDir = path.resolve(workDir ?? `${out}.root`);
|
|
103
123
|
const receipts = path.resolve(receiptDir ?? path.dirname(out));
|
|
104
|
-
const
|
|
124
|
+
const planFor = (files) => planMsixSigning({ files, sign, appInstaller: Boolean(appInstaller) });
|
|
125
|
+
const staged = existsSync(path.resolve(stageDir)) ? readdirSync(path.resolve(stageDir)) : [];
|
|
126
|
+
const steps = planFor(staged.filter((n) => /\.(exe|dll)$/i.test(n))).map((p) => p.step);
|
|
105
127
|
if (dryRun) { log({ event: "msix-dry-run", out, steps }); return { out, steps, dryRun: true }; }
|
|
106
128
|
if (platform !== "win32") throw fail("MSIX_NATIVE_HOST_REQUIRED", "MSIX packing and signing must run on Windows");
|
|
107
129
|
const makeappx = env.MAKEAPPX_PATH || findTool("makeappx.exe")?.toolPath;
|
|
@@ -113,16 +135,30 @@ export function buildMsix({ stageDir, out, workDir, manifest, assets, sign = tru
|
|
|
113
135
|
};
|
|
114
136
|
const signer = (files, receipt) => checked(process.execPath, [path.join(here, "sign-windows.mjs"), "--receipt", receipt, ...files]);
|
|
115
137
|
const root = assembleMsixRoot({ stageDir, root: workDir, manifest, assets });
|
|
138
|
+
const inventory = runtime ? verifyPayloadInventory({ root, inventory: runtime, architecture: manifest.architecture }) : null;
|
|
116
139
|
mkdirSync(receipts, { recursive: true });
|
|
117
|
-
|
|
140
|
+
const ordered = planFor(readdirSync(root).filter((n) => /\.(exe|dll)$/i.test(n))).find((p) => p.step === "sign-pe")?.files ?? [];
|
|
141
|
+
if (sign) signer(ordered.map((f) => path.join(root, path.basename(f))), peReceipt ?? path.join(receipts, "msix-pe-sign.receipt.json"));
|
|
118
142
|
mkdirSync(path.dirname(out), { recursive: true });
|
|
119
143
|
rmSync(out, { force: true });
|
|
120
144
|
checked(makeappx, ["pack", "/d", root, "/p", out, "/o"]);
|
|
121
145
|
if (!existsSync(out) || statSync(out).size === 0) throw fail("MSIX_PACK_EMPTY", `makeappx produced no package: ${out}`);
|
|
122
146
|
if (sign) {
|
|
123
|
-
signer([out], path.join(receipts, "msix-sign.receipt.json"));
|
|
147
|
+
signer([out], msixReceipt ?? path.join(receipts, "msix-sign.receipt.json"));
|
|
124
148
|
checked(process.execPath, [path.join(here, "sign-windows.mjs"), "--verify-only", out]);
|
|
149
|
+
assertPublisherMatchesSigner(manifest.publisher, readSubject(out));
|
|
150
|
+
}
|
|
151
|
+
let appInstallerFile = null;
|
|
152
|
+
if (appInstaller) {
|
|
153
|
+
appInstallerFile = path.resolve(appInstaller.out);
|
|
154
|
+
mkdirSync(path.dirname(appInstallerFile), { recursive: true });
|
|
155
|
+
writeFileSync(appInstallerFile, generateAppInstaller({ ...appInstaller, name: manifest.name, publisher: manifest.publisher, version: manifest.version, architecture: manifest.architecture }));
|
|
125
156
|
}
|
|
157
|
+
const seal = { schema: 1, msix: { file: out, sha256: sha256(out), sizeBytes: statSync(out).size }, signed: sign, identity: { name: manifest.name, version: manifest.version, publisher: manifest.publisher, architecture: manifest.architecture },
|
|
158
|
+
payload: readdirSync(root, { recursive: true }).map((n) => String(n).split(path.sep).join("/")).filter((n) => statSync(path.join(root, n)).isFile()).sort().map((n) => ({ name: n, sha256: sha256(path.join(root, n)) })), inventory,
|
|
159
|
+
appInstaller: appInstallerFile && { file: appInstallerFile, sha256: sha256(appInstallerFile) } };
|
|
160
|
+
const sealFile = path.join(receipts, "msix-seal.json");
|
|
161
|
+
writeFileSync(sealFile, `${JSON.stringify(seal, null, 2)}\n`);
|
|
126
162
|
log({ event: "msix-built", out, signed: sign });
|
|
127
|
-
return { out, steps, root, signed: sign };
|
|
163
|
+
return { out, steps, root, signed: sign, seal: sealFile, appInstaller: appInstallerFile };
|
|
128
164
|
}
|
package/msix-config.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// `msix` target schema for right-release configs (targets.win.msix) and the
|
|
2
|
+
// `windows-msix-signed-v1` signing contract. Pure validation; no I/O.
|
|
3
|
+
//
|
|
4
|
+
// targets.win = {
|
|
5
|
+
// signed: true, signingContract: "windows-msix-signed-v1",
|
|
6
|
+
// projectType: "swiftpm", // optional; auto-detected from Package.swift
|
|
7
|
+
// prePackage: { cmd, args }, // SwiftPM build + stage (swift-package windows-stage)
|
|
8
|
+
// msix: {
|
|
9
|
+
// stageDir, out, // flat payload dir produced by prePackage; .msix path
|
|
10
|
+
// identity: { name, publisher, publisherDisplayName, displayName, description?, version, architecture, executable },
|
|
11
|
+
// capabilities: ["internetClient"], minWindowsVersion?, maxWindowsVersionTested?,
|
|
12
|
+
// virtualization: { fileSystemWrite: "enabled|disabled", registryWrite: "enabled|disabled" }, // explicit, required
|
|
13
|
+
// assets: { storeLogo, square150, square44 },
|
|
14
|
+
// runtime: { dlls: ["swiftCore.dll", ...], systemDlls?: [], dependencies?: [{ name, minVersion, provides: [] }] },
|
|
15
|
+
// appInstaller?: { uri, msixUri, out, hoursBetweenUpdateChecks?, showPrompt?, updateBlocksActivation? },
|
|
16
|
+
// },
|
|
17
|
+
// updater?: { kind: "tauri" | "appinstaller", artifacts: [...] }, // tauri stays the default
|
|
18
|
+
// installer: { artifacts: [{ file: "<out>.msix", key }] },
|
|
19
|
+
// }
|
|
20
|
+
|
|
21
|
+
export const MSIX_SIGNING_CONTRACT = "windows-msix-signed-v1";
|
|
22
|
+
export const UPDATER_KINDS = Object.freeze(["tauri", "appinstaller"]);
|
|
23
|
+
export const VIRTUALIZATION_VALUES = Object.freeze(["enabled", "disabled"]);
|
|
24
|
+
|
|
25
|
+
export const isMsixTarget = (target) => target?.signingContract === MSIX_SIGNING_CONTRACT;
|
|
26
|
+
export const updaterKind = (target) => target?.updater?.kind ?? "tauri";
|
|
27
|
+
|
|
28
|
+
const isHttps = (value) => { try { return new URL(value).protocol === "https:"; } catch { return false; } };
|
|
29
|
+
|
|
30
|
+
/** Returns an array of human-readable problems (empty when valid). */
|
|
31
|
+
export function validateMsixTarget(target, app = "app") {
|
|
32
|
+
const e = [];
|
|
33
|
+
const at = (message) => e.push(`${app} win msix: ${message}`);
|
|
34
|
+
if (!isMsixTarget(target)) at(`signingContract must be ${MSIX_SIGNING_CONTRACT}`);
|
|
35
|
+
const m = target?.msix;
|
|
36
|
+
if (!m || typeof m !== "object") { at("msix block is required"); return e; }
|
|
37
|
+
if (!target.prePackage?.cmd) at("prePackage is required (SwiftPM build + payload staging)");
|
|
38
|
+
if (target.sign?.prePackageFiles?.length || target.sign?.files?.length) at("omit sign.prePackageFiles/sign.files: the msix step signs payload PE files, then the package");
|
|
39
|
+
if (!m.stageDir) at("msix.stageDir is required");
|
|
40
|
+
if (!/\.msix$/i.test(m.out ?? "")) at("msix.out must end with .msix");
|
|
41
|
+
const id = m.identity ?? {};
|
|
42
|
+
for (const key of ["name", "publisher", "publisherDisplayName", "displayName", "version", "architecture", "executable"]) if (!id[key]) at(`msix.identity.${key} is required`);
|
|
43
|
+
const v = m.virtualization ?? {};
|
|
44
|
+
for (const key of ["fileSystemWrite", "registryWrite"]) {
|
|
45
|
+
if (!VIRTUALIZATION_VALUES.includes(v[key])) at(`msix.virtualization.${key} must be explicitly "enabled" or "disabled"`);
|
|
46
|
+
}
|
|
47
|
+
for (const key of ["storeLogo", "square150", "square44"]) if (!m.assets?.[key]) at(`msix.assets.${key} (PNG) is required`);
|
|
48
|
+
const dlls = m.runtime?.dlls;
|
|
49
|
+
if (!Array.isArray(dlls)) at("msix.runtime.dlls (explicit runtime dependency inventory; use [] only for a dependency-free app) is required");
|
|
50
|
+
else if (dlls.some((d) => !/\.dll$/i.test(d) || /[\\/]/.test(d))) at("msix.runtime.dlls entries must be flat .dll names");
|
|
51
|
+
const kind = updaterKind(target);
|
|
52
|
+
if (!UPDATER_KINDS.includes(kind)) at(`updater.kind must be one of ${UPDATER_KINDS.join(", ")}`);
|
|
53
|
+
if (kind === "appinstaller") {
|
|
54
|
+
const a = m.appInstaller;
|
|
55
|
+
if (!a) at("updater.kind appinstaller requires msix.appInstaller");
|
|
56
|
+
else {
|
|
57
|
+
if (!isHttps(a.uri)) at("msix.appInstaller.uri must be an https URL");
|
|
58
|
+
if (!isHttps(a.msixUri)) at("msix.appInstaller.msixUri must be an https URL");
|
|
59
|
+
if (!/\.appinstaller$/i.test(a.out ?? "")) at("msix.appInstaller.out must end with .appinstaller");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const outName = String(m.out ?? "").split(/[\\/]/).pop();
|
|
63
|
+
if (outName && !(target.installer?.artifacts ?? []).some((a) => String(a.file).split(/[\\/]/).pop() === outName)) at("installer.artifacts must list the .msix so it is sealed");
|
|
64
|
+
return e;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function assertValidMsixTarget(target, app) {
|
|
68
|
+
const errors = validateMsixTarget(target, app);
|
|
69
|
+
if (errors.length) throw Object.assign(new Error(errors.join("; ")), { code: "MSIX_CONFIG_INVALID" });
|
|
70
|
+
}
|
package/msix-plan.mjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Pure planning/verification helpers for MSIX packaging: signing order, runtime
|
|
2
|
+
// dependency inventory, publisher/signer match. No signing, no process spawns.
|
|
3
|
+
|
|
4
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { readPeImports } from "./pe-imports.mjs";
|
|
7
|
+
import { WINDOWS_SYSTEM_DLLS } from "./swift-windows-stage.mjs";
|
|
8
|
+
|
|
9
|
+
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
10
|
+
const lower = (v) => String(v).toLowerCase();
|
|
11
|
+
const isApiSet = (name) => /^(api|ext)-ms-win-[a-z0-9-]+-l\d+-\d+-\d+\.dll$/.test(name);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Immutable ordering (docs/rules/release-signing.md): inner PE files (DLLs, then EXEs) are signed
|
|
15
|
+
* before packing, the package is signed after, verification follows, then seal. Returns step list.
|
|
16
|
+
*/
|
|
17
|
+
export function planMsixSigning({ files, sign = true, appInstaller = false }) {
|
|
18
|
+
const pe = files.filter((f) => /\.(dll|exe)$/i.test(f));
|
|
19
|
+
const rank = (f) => (/\.dll$/i.test(f) ? 0 : 1);
|
|
20
|
+
const ordered = [...pe].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
21
|
+
return [
|
|
22
|
+
{ step: "assemble" },
|
|
23
|
+
{ step: "verify-inventory" },
|
|
24
|
+
...(sign ? [{ step: "sign-pe", files: ordered }] : []),
|
|
25
|
+
{ step: "pack" },
|
|
26
|
+
...(sign ? [{ step: "sign-msix" }, { step: "verify-msix" }, { step: "verify-publisher" }] : []),
|
|
27
|
+
...(appInstaller ? [{ step: "appinstaller" }] : []),
|
|
28
|
+
{ step: "seal" },
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fail closed unless (a) every DLL in the declared inventory is in the payload and (b) every PE in the
|
|
34
|
+
* payload imports only payload files, Windows system DLLs/API sets, or DLLs provided by a declared
|
|
35
|
+
* package dependency. Equivalent to `dumpbin /dependents` closure, but host independent.
|
|
36
|
+
*/
|
|
37
|
+
export function verifyPayloadInventory({ root, inventory = {}, architecture }) {
|
|
38
|
+
const names = readdirSync(root);
|
|
39
|
+
const present = new Set(names.map(lower));
|
|
40
|
+
const problems = [];
|
|
41
|
+
for (const dll of inventory.dlls ?? []) if (!present.has(lower(dll))) problems.push(`declared runtime DLL missing from payload: ${dll}`);
|
|
42
|
+
const system = new Set([...WINDOWS_SYSTEM_DLLS, ...(inventory.systemDlls ?? [])].map(lower));
|
|
43
|
+
const provided = new Set((inventory.dependencies ?? []).flatMap((d) => d.provides ?? []).map(lower));
|
|
44
|
+
const observed = new Set();
|
|
45
|
+
for (const name of names.filter((n) => /\.(exe|dll)$/i.test(n))) {
|
|
46
|
+
const info = readPeImports(readFileSync(path.join(root, name)));
|
|
47
|
+
if (architecture && info.architecture !== architecture) problems.push(`${name} is ${info.architecture}, package is ${architecture}`);
|
|
48
|
+
for (const dep of [...info.imports, ...info.delayImports]) {
|
|
49
|
+
const key = lower(dep);
|
|
50
|
+
observed.add(key);
|
|
51
|
+
if (!present.has(key) && !system.has(key) && !isApiSet(key) && !provided.has(key)) problems.push(`${name} imports ${dep}, which is neither in the payload, a system DLL, nor provided by a declared dependency`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Declared-but-never-imported DLLs are reported (not fatal): they may be loaded dynamically.
|
|
55
|
+
const unreferenced = (inventory.dlls ?? []).filter((d) => !observed.has(lower(d)));
|
|
56
|
+
if (problems.length) throw fail("MSIX_INVENTORY_INCOMPLETE", problems.join("; "));
|
|
57
|
+
return { dlls: [...(inventory.dlls ?? [])], unreferenced };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const normalizeDn = (dn) => String(dn).split(/,(?=\s*[A-Za-z]+=)/).map((p) => p.trim().replace(/\s*=\s*/, "=")).sort().join(",");
|
|
61
|
+
|
|
62
|
+
/** The package signer's certificate subject must equal Identity/@Publisher or Windows refuses to install. */
|
|
63
|
+
export function assertPublisherMatchesSigner(publisher, signerSubject) {
|
|
64
|
+
if (!signerSubject) throw fail("MSIX_PUBLISHER_MISMATCH", "could not read the signer certificate subject from the signed package");
|
|
65
|
+
if (normalizeDn(publisher) !== normalizeDn(signerSubject)) {
|
|
66
|
+
throw fail("MSIX_PUBLISHER_MISMATCH", `manifest Publisher "${publisher}" does not equal signer subject "${signerSubject}"; set identity.publisher to the signer subject`);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/msix-target.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Map a validated targets.win.msix config block onto buildMsix() options.
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertValidMsixTarget, updaterKind } from "./msix-config.mjs";
|
|
4
|
+
|
|
5
|
+
export function msixBuildOptions({ root, target, app, sign = true, dryRun = false, receipts = {} }) {
|
|
6
|
+
assertValidMsixTarget(target, app);
|
|
7
|
+
const m = target.msix;
|
|
8
|
+
const at = (p) => path.resolve(root, p);
|
|
9
|
+
const manifest = {
|
|
10
|
+
...m.identity,
|
|
11
|
+
capabilities: m.capabilities,
|
|
12
|
+
dependencies: m.runtime?.dependencies,
|
|
13
|
+
minWindowsVersion: m.minWindowsVersion,
|
|
14
|
+
maxWindowsVersionTested: m.maxWindowsVersionTested,
|
|
15
|
+
virtualization: m.virtualization,
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
stageDir: at(m.stageDir),
|
|
19
|
+
out: at(m.out),
|
|
20
|
+
manifest,
|
|
21
|
+
assets: Object.fromEntries(Object.entries(m.assets).map(([k, v]) => [k, at(v)])),
|
|
22
|
+
runtime: m.runtime,
|
|
23
|
+
appInstaller: updaterKind(target) === "appinstaller" ? { ...m.appInstaller, out: at(m.appInstaller.out) } : undefined,
|
|
24
|
+
sign, dryRun,
|
|
25
|
+
peReceipt: receipts.pe, msixReceipt: receipts.msix,
|
|
26
|
+
receiptDir: receipts.dir,
|
|
27
|
+
};
|
|
28
|
+
}
|
package/ownership.mjs
CHANGED
|
@@ -234,7 +234,31 @@ function committedFileInventory(appRoot, trust, pathPrefix, committed) {
|
|
|
234
234
|
return { files, coverage, committed: true, pathPrefix, repositoryFiles, snapshotFiles: new Map(repositoryFiles.map((file) => [file.rel, file])) };
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// Blank out Swift `//` and nested `/* */` comments outside string literals, keeping offsets.
|
|
238
|
+
export function stripSwiftComments(text) {
|
|
239
|
+
const out = text.split("");
|
|
240
|
+
let quote = false, depth = 0;
|
|
241
|
+
for (let i = 0; i < text.length; i++) {
|
|
242
|
+
if (depth) {
|
|
243
|
+
if (text.startsWith("/*", i)) { depth++; out[i] = out[i + 1] = " "; i++; continue; }
|
|
244
|
+
if (text.startsWith("*/", i)) { depth--; out[i] = out[i + 1] = " "; i++; continue; }
|
|
245
|
+
if (text[i] !== "\n") out[i] = " ";
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (quote) {
|
|
249
|
+
if (text[i] === "\\") { i++; continue; }
|
|
250
|
+
if (text[i] === '"' || text[i] === "\n") quote = false;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (text[i] === '"') { quote = true; continue; }
|
|
254
|
+
if (text.startsWith("//", i)) { for (; i < text.length && text[i] !== "\n"; i++) out[i] = " "; continue; }
|
|
255
|
+
if (text.startsWith("/*", i)) { depth = 1; out[i] = out[i + 1] = " "; i++; }
|
|
256
|
+
}
|
|
257
|
+
return out.join("");
|
|
258
|
+
}
|
|
259
|
+
|
|
237
260
|
function swiftPackageArguments(text) {
|
|
261
|
+
text = stripSwiftComments(text);
|
|
238
262
|
const args = [];
|
|
239
263
|
for (const match of text.matchAll(/\.package\s*\(/g)) {
|
|
240
264
|
let at = match.index + match[0].length, start = at, depth = 1, quote = false;
|
package/package.json
CHANGED
package/preflight.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import path from "node:path";
|
|
|
23
23
|
import { spawnSync } from "./background-process.mjs";
|
|
24
24
|
import { brokerManagedHost } from "./cargo-contract.mjs";
|
|
25
25
|
import { createOwnershipEvidence } from "./ownership-evidence.mjs";
|
|
26
|
+
import { isSwiftProject } from "./project-type.mjs";
|
|
26
27
|
|
|
27
28
|
export const PREFLIGHT_OK = "ok";
|
|
28
29
|
export const PREFLIGHT_WARN = "warn";
|
|
@@ -179,11 +180,24 @@ export function collectPreflight({
|
|
|
179
180
|
isBrokerManagedHost = brokerManagedHost,
|
|
180
181
|
ownership = true,
|
|
181
182
|
ownershipEvidenceFactory = createOwnershipEvidence,
|
|
183
|
+
projectType,
|
|
184
|
+
msix = false,
|
|
185
|
+
findSdkTool = findWindowsSdkTool,
|
|
182
186
|
} = {}) {
|
|
183
187
|
const checks = [];
|
|
184
188
|
const windows = platform === "win" || platform === "win32";
|
|
189
|
+
const swift = isSwiftProject(projectType);
|
|
185
190
|
|
|
186
|
-
if (
|
|
191
|
+
if (swift) {
|
|
192
|
+
const swiftVersion = versionProbe("swift");
|
|
193
|
+
checks.push(
|
|
194
|
+
swiftVersion
|
|
195
|
+
? ok("swift", swiftVersion)
|
|
196
|
+
: fail("swift", "swift was not found on PATH; SwiftPM apps build with the Swift toolchain", "install the pinned Swift toolchain (swift.org / Swift on Windows) and verify: swift --version"),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (appRoot && !swift) {
|
|
187
201
|
const target = targetLink ?? nativeLayout?.targetLink ?? path.join(appRoot, "src-tauri", "target");
|
|
188
202
|
const targetLabel = nativeLayout?.targetPrefix || "src-tauri/target/";
|
|
189
203
|
let entry = null;
|
|
@@ -225,29 +239,27 @@ export function collectPreflight({
|
|
|
225
239
|
}
|
|
226
240
|
|
|
227
241
|
if (windows) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
makeappx
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
// makeappx/signtool are MSIX requirements; NSIS/portable targets sign through sign-windows.
|
|
243
|
+
if (msix) {
|
|
244
|
+
const makeappx = process.env.MAKEAPPX_PATH ? { version: "MAKEAPPX_PATH" } : findSdkTool("makeappx.exe");
|
|
245
|
+
checks.push(
|
|
246
|
+
makeappx
|
|
247
|
+
? ok("windows-sdk", `makeappx.exe from SDK ${makeappx.version}`)
|
|
248
|
+
: fail(
|
|
249
|
+
"windows-sdk",
|
|
250
|
+
"msix target configured but no makeappx.exe found under any installed Windows 10/11 SDK",
|
|
251
|
+
"install the Windows SDK, or set MAKEAPPX_PATH to an explicit makeappx.exe",
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
238
255
|
|
|
239
|
-
const signtool =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
"signtool",
|
|
245
|
-
"no signtool.exe found under any installed Windows SDK",
|
|
246
|
-
"install the Windows SDK Build Tools, or set AZURE_SIGNTOOL_PATH",
|
|
247
|
-
),
|
|
248
|
-
);
|
|
256
|
+
const signtool = findSdkTool("signtool.exe");
|
|
257
|
+
const signtoolMissing = "install the Windows SDK Build Tools, or set AZURE_SIGNTOOL_PATH";
|
|
258
|
+
if (signtool) checks.push(ok("signtool", `signtool.exe from SDK ${signtool.version}`));
|
|
259
|
+
else if (msix) checks.push(fail("signtool", "msix target configured but no signtool.exe found under any installed Windows SDK", signtoolMissing));
|
|
260
|
+
else checks.push(warn("signtool", "no signtool.exe found under any installed Windows SDK", signtoolMissing));
|
|
249
261
|
|
|
250
|
-
const vendorsOpenssl = cargoLockPaths.some((lockPath) => graphVendorsOpenssl(lockPath));
|
|
262
|
+
const vendorsOpenssl = !swift && cargoLockPaths.some((lockPath) => graphVendorsOpenssl(lockPath));
|
|
251
263
|
if (vendorsOpenssl) {
|
|
252
264
|
const perl = resolveWindowsPerl({ env });
|
|
253
265
|
checks.push(
|
|
@@ -262,7 +274,9 @@ export function collectPreflight({
|
|
|
262
274
|
}
|
|
263
275
|
}
|
|
264
276
|
|
|
265
|
-
if (
|
|
277
|
+
if (swift) {
|
|
278
|
+
// No Rust compiler cache applies to SwiftPM targets.
|
|
279
|
+
} else if (isBrokerManagedHost(env)) {
|
|
266
280
|
checks.push(ok("compiler-cache", "broker-managed; compiler-cache selection and status are recorded in build receipts"));
|
|
267
281
|
} else {
|
|
268
282
|
const sccache = versionProbe("sccache");
|
package/project-type.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Project-type awareness for release preflight: which toolchain a target needs.
|
|
2
|
+
// Pure; filesystem access is injectable. Swift (SwiftPM) apps need `swift`, not
|
|
3
|
+
// cargo/rustc; Tauri/Rust targets keep their existing requirements.
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
export const PROJECT_TYPES = Object.freeze(["tauri", "cargo", "swiftpm"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Explicit `target.projectType` / `config.projectType` wins. Otherwise a Package.swift with no Cargo
|
|
12
|
+
* manifest (root or src-tauri) is swiftpm; everything else is tauri/cargo (unchanged behaviour).
|
|
13
|
+
*/
|
|
14
|
+
export function resolveProjectType({ appRoot, config = {}, target = {}, exists = existsSync } = {}) {
|
|
15
|
+
const explicit = target.projectType ?? config.projectType;
|
|
16
|
+
if (explicit !== undefined) {
|
|
17
|
+
if (!PROJECT_TYPES.includes(explicit)) throw Object.assign(new Error(`projectType must be one of ${PROJECT_TYPES.join(", ")}; got ${explicit}`), { code: "PROJECT_TYPE_INVALID" });
|
|
18
|
+
return explicit;
|
|
19
|
+
}
|
|
20
|
+
if (appRoot) {
|
|
21
|
+
const roots = [appRoot, path.resolve(appRoot, config.workdir ?? ".")];
|
|
22
|
+
const hasSwift = roots.some((r) => exists(path.join(r, "Package.swift")));
|
|
23
|
+
const hasCargo = roots.some((r) => exists(path.join(r, "Cargo.toml")) || exists(path.join(r, "src-tauri", "Cargo.toml")));
|
|
24
|
+
if (hasSwift && !hasCargo) return "swiftpm";
|
|
25
|
+
}
|
|
26
|
+
return "tauri";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const isSwiftProject = (projectType) => projectType === "swiftpm";
|
|
30
|
+
|
|
31
|
+
/** Executables the build itself needs (the config's own preflight.executables are appended by callers). */
|
|
32
|
+
export function requiredBuildExecutables({ projectType, packageManager = "pnpm", usesPackageManager = true } = {}) {
|
|
33
|
+
if (isSwiftProject(projectType)) return ["git", "swift", ...(usesPackageManager && packageManager ? [packageManager] : [])];
|
|
34
|
+
return ["git", packageManager, "cargo", "rustc"];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** True when the Windows target packages an MSIX (needs makeappx and signtool). */
|
|
38
|
+
export function targetUsesMsix(target) {
|
|
39
|
+
return Boolean(target?.msix) || target?.signingContract === "windows-msix-signed-v1";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** SwiftPM targets build and package inside prePackage (mac DMG) or the MSIX step; no separate `package` command is required. */
|
|
43
|
+
export const packageCommandOptional = (target, projectType) => targetUsesMsix(target) || (projectType === "swiftpm" && Boolean(target?.prePackage?.cmd));
|
package/publish-update.mjs
CHANGED
|
@@ -75,6 +75,12 @@ const artifactFile = (declared) => {
|
|
|
75
75
|
return artifact.file;
|
|
76
76
|
};
|
|
77
77
|
const verifiedArtifact = (file) => sealed.artifacts.get(path.basename(file));
|
|
78
|
+
// MSIX updates (updater.kind appinstaller) trust the package Authenticode signature (publisher-pinned in
|
|
79
|
+
// the manifest), so no minisign .sig exists; the route records the sealed package digest instead.
|
|
80
|
+
const appInstallerKind = target?.updater?.kind === "appinstaller";
|
|
81
|
+
const signatureFor = (artifact) => appInstallerKind
|
|
82
|
+
? `msix-authenticode-sha256:${assertArtifactUnchanged(verifiedArtifact(artifact.file)) && verifiedArtifact(artifact.file).sha256}`
|
|
83
|
+
: signatureText(artifactFile(artifact.signature));
|
|
78
84
|
const signatureText = (file) => {
|
|
79
85
|
const signature = assertArtifactUnchanged(verifiedArtifact(file)).toString("utf8").trim();
|
|
80
86
|
if (!signature) throw new PublicationError("PUBLICATION_SIGNATURE_MISSING", `sealed updater signature is empty: ${file}`);
|
|
@@ -89,7 +95,7 @@ function git(args) {
|
|
|
89
95
|
if (tier === "patch" && !installers.length) fail(`${config?.app ?? "app"} ${platform} patch has no installer.artifacts`);
|
|
90
96
|
if (tier === "update" && !artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no updater.artifacts`);
|
|
91
97
|
|
|
92
|
-
if (sealed) for (const artifact of artifacts)
|
|
98
|
+
if (sealed) for (const artifact of artifacts) signatureFor(artifact);
|
|
93
99
|
let releaseToken = null;
|
|
94
100
|
if (!dryRun && (tier === "update" || artifacts.length > 0)) {
|
|
95
101
|
try {
|
|
@@ -114,13 +120,13 @@ if (tier === "patch") {
|
|
|
114
120
|
const installerKey = patchInstallerKey(artifact);
|
|
115
121
|
assertCurrentKey(installerKey, "installer");
|
|
116
122
|
const file = artifactFile(artifact.file);
|
|
117
|
-
|
|
123
|
+
if (!appInstallerKind) artifactFile(artifact.signature);
|
|
118
124
|
if (!uploadedKeys.has(installerKey)) {
|
|
119
125
|
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installerKey}`);
|
|
120
126
|
if (!dryRun) uploadVerifiedR2Artifact(verifiedArtifact(file), installerKey, "public", { cwd: root });
|
|
121
127
|
uploadedKeys.add(installerKey);
|
|
122
128
|
}
|
|
123
|
-
const signature = dryRun ? `<signature:${artifact.signature}>` :
|
|
129
|
+
const signature = dryRun ? `<signature:${artifact.signature ?? "msix-authenticode"}>` : signatureFor(artifact);
|
|
124
130
|
const artifactKey = artifact.artifactKey || installerKey.replaceAll("/", "__");
|
|
125
131
|
const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
|
|
126
132
|
platforms[artifact.platform] = {
|
|
@@ -210,13 +216,13 @@ const uploadedKeys = new Set();
|
|
|
210
216
|
for (const artifact of artifacts) {
|
|
211
217
|
assertCurrentKey(artifact.key, "updater");
|
|
212
218
|
const file = artifactFile(artifact.file);
|
|
213
|
-
|
|
219
|
+
if (!appInstallerKind) artifactFile(artifact.signature);
|
|
214
220
|
if (!uploadedKeys.has(artifact.key)) {
|
|
215
221
|
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD private/${artifact.key}`);
|
|
216
222
|
if (!dryRun) uploadVerifiedR2Artifact(verifiedArtifact(file), artifact.key, "private", { cwd: root });
|
|
217
223
|
uploadedKeys.add(artifact.key);
|
|
218
224
|
}
|
|
219
|
-
const signature = dryRun ? `<signature:${artifact.signature}>` :
|
|
225
|
+
const signature = dryRun ? `<signature:${artifact.signature ?? "msix-authenticode"}>` : signatureFor(artifact);
|
|
220
226
|
const artifactKey = artifact.artifactKey || artifact.key.replaceAll("/", "__");
|
|
221
227
|
const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
|
|
222
228
|
platforms[artifact.platform] = {
|
package/release-invocation.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "./background-process.mjs";
|
|
2
|
+
import { resolveProjectType } from "./project-type.mjs";
|
|
2
3
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
@@ -128,18 +129,19 @@ export function resolveConfiguredBuildInputs(config, target, label = "release co
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
export function resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target, env = process.env }) {
|
|
132
|
+
const swift = resolveProjectType({ appRoot, config, target }) === "swiftpm";
|
|
131
133
|
const nativeLayout = resolveNativeCargoLayout({
|
|
132
134
|
appRoot,
|
|
133
135
|
config,
|
|
134
|
-
requireManifest:
|
|
135
|
-
requireLock:
|
|
136
|
+
requireManifest: !swift,
|
|
137
|
+
requireLock: !swift,
|
|
136
138
|
});
|
|
137
139
|
const requiredInputs = [
|
|
138
140
|
configPath,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
...(swift
|
|
142
|
+
// SwiftPM: Package.swift is required; Package.resolved and the Node manifests only when they exist.
|
|
143
|
+
? [path.join(appRoot, "Package.swift"), ...["Package.resolved", "package.json", "pnpm-lock.yaml"].map((f) => path.join(appRoot, f)).filter((f) => existsSync(f))]
|
|
144
|
+
: [path.join(appRoot, "package.json"), path.join(appRoot, "pnpm-lock.yaml"), nativeLayout.manifestPath, nativeLayout.lockPath]),
|
|
143
145
|
...(target?.preflight?.files ?? []).map((file) => path.resolve(appRoot, expandEnv(file, env))),
|
|
144
146
|
].filter((file, index, files) => files.indexOf(file) === index);
|
|
145
147
|
for (const file of requiredInputs) {
|