"use strict"; const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { execFileSync, spawn, spawnSync } = require("node:child_process"); const { signAsync } = require("@electron/osx-sign"); const { prepareMacNetworkIdentity, verifyMacNetworkIdentity } = require("./macNetworkIdentity"); const projectRoot = path.resolve(__dirname, ".."); const packageJson = require(path.join(projectRoot, "package.json")); const productName = packageJson.build?.productName || packageJson.productName; const distRoot = path.resolve(projectRoot, process.env.CHJ_DIST_DIR || "dist"); const appPath = path.join(distRoot, "mac-arm64", `${productName}.app`); const outputPath = path.join(distRoot, `${productName}-${packageJson.version}-arm64.dmg`); const networkIdentity = { appId: packageJson.build.appId, version: packageJson.version }; function run(command, args, options = {}) { return execFileSync(command, args, { encoding: "utf8", stdio: options.capture ? "pipe" : "inherit" }); } function signatureIsValid(bundlePath) { try { run("codesign", ["--verify", "--deep", "--strict", "--verbose=2", bundlePath], { capture: true }); return true; } catch (_) { return false; } } function signatureDescription(bundlePath) { const result = spawnSync("codesign", ["-dv", "--verbose=4", bundlePath], { encoding: "utf8" }); return `${result.stdout || ""}\n${result.stderr || ""}`; } async function ensureValidSignature() { const identities = run("security", ["find-identity", "-v", "-p", "codesigning"], { capture: true }); const developerId = identities.match(/\b([A-Fa-f0-9]{40})\s+"Developer ID Application:[^"]+"/); const hasDeveloperId = Boolean(developerId); if (signatureIsValid(appPath)) { const description = signatureDescription(appPath); if (description.includes("Authority=Developer ID Application")) return "developer-id"; if (!hasDeveloperId && description.includes("Signature=adhoc") && !description.includes("runtime)")) return "adhoc"; } const options = { app: appPath, platform: "darwin", // Preserve the link-signed, hash-pinned native client. The outer app seal covers it. ignore: (filePath) => /native-runtime\/.*\/curl$/.test(filePath), preAutoEntitlements: false, preEmbedProvisioningProfile: false, }; if (hasDeveloperId) options.identity = developerId[1]; if (!hasDeveloperId) { options.identity = "-"; options.identityValidation = false; options.optionsForFile = () => ({ hardenedRuntime: false, signatureFlags: [], timestamp: "none", }); } await signAsync(options); if (!signatureIsValid(appPath)) throw new Error("Kontrola codesign po podepsání .app selhala."); return hasDeveloperId ? "developer-id" : "adhoc"; } async function smokeTestApp(bundlePath) { const executable = path.join(bundlePath, "Contents", "MacOS", productName); const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "chj-mac-smoke-data-")); try { // Fixture consent for this isolated packaging smoke test only. Production // startup always checks the record; the environment does not bypass it. const { LicenseAcceptance } = require("../src/main/legal/licenseAcceptance"); const acceptance = new LicenseAcceptance({ storageRoot: userDataDir, licensePath: path.join(bundlePath, "Contents", "Resources", "licenses", "APPLICATION_LICENSE.txt"), docsRoot: path.join(bundlePath, "Contents", "Resources", "docs"), appVersion: packageJson.version, buildId: require("../src/shared/buildInfo.json").buildId }); acceptance.accept({ accepted: true, sha256: acceptance.identity.sha256, guidanceShown: true, guidanceSha256: acceptance.identity.guidanceSha256 }); await new Promise((resolve, reject) => { let output = ""; const smokeEnvironment = { ...process.env, CHJ_BUILD_SMOKE_USER_DATA_DIR: userDataDir, ELECTRON_ENABLE_LOGGING: "1", }; delete smokeEnvironment.ELECTRON_RUN_AS_NODE; delete smokeEnvironment.NODE_OPTIONS; const child = spawn(executable, [], { env: smokeEnvironment, stdio: ["ignore", "pipe", "pipe"], }); child.stdout.on("data", (chunk) => { output += chunk; }); child.stderr.on("data", (chunk) => { output += chunk; }); const timeout = setTimeout(() => { child.kill("SIGTERM"); reject(new Error(`Aplikace nedokončila smoke test do 12 sekund.\n${output.trim()}`)); }, 12000); child.once("error", (error) => { clearTimeout(timeout); reject(error); }); child.once("exit", (code, signal) => { clearTimeout(timeout); if (code === 0 && output.includes("CHJ_BUILD_SMOKE_READY")) resolve(); else reject(new Error(`Aplikace nedokončila smoke test (code=${code}, signal=${signal}).\n${output.trim()}`)); }); }); } finally { fs.rmSync(userDataDir, { recursive: true, force: true }); } } async function main() { if (process.platform !== "darwin") throw new Error("macOS instalační DMG lze vytvářet pouze na macOS."); if (!productName || !fs.statSync(appPath, { throwIfNoEntry: false })?.isDirectory()) { throw new Error(`Chybí sestavená aplikace ${appPath}. Nejdřív spusťte macOS build.`); } const { validateRuntime, runtimeEnvironment } = require("../src/main/diagnostics/httpRuntime"); const originalRuntime = validateRuntime({ resourcesPath: path.join(projectRoot, "build") }); const resourcesPath = path.join(appPath, "Contents", "Resources"); const packagedClient = path.join(resourcesPath, "native-runtime", "darwin-arm64", "curl"); // A packager may re-sign nested Mach-O files. Restore the verified client before // creating the app's final resource seal; never edit a finished/signed DMG. fs.copyFileSync(originalRuntime.executable, packagedClient); fs.chmodSync(packagedClient, 0o755); prepareMacNetworkIdentity(appPath, networkIdentity); const signature = await ensureValidSignature(); verifyMacNetworkIdentity(appPath, networkIdentity); const finalRuntime = validateRuntime({ resourcesPath }); const runtimeVersion = execFileSync(finalRuntime.executable, ["--disable", "--version"], { encoding: "utf8", env: runtimeEnvironment() }); if (!/Features:.*HTTP3/.test(runtimeVersion)) throw new Error("Signed app lacks its working HTTP/3 runtime"); fs.rmSync(outputPath, { force: true }); const staging = fs.mkdtempSync(path.join(os.tmpdir(), "chj-dmg-stage-")); try { run("ditto", [appPath, path.join(staging, `${productName}.app`)]); fs.symlinkSync("/Applications", path.join(staging, "Applications")); run("hdiutil", ["create", "-volname", productName, "-srcfolder", staging, "-format", "UDZO", "-ov", outputPath]); } finally { fs.rmSync(staging, { recursive: true, force: true }); } run("hdiutil", ["verify", outputPath]); const verifyRoot = fs.mkdtempSync(path.join(os.tmpdir(), "chj-mac-installer-")); try { run("hdiutil", ["attach", "-readonly", "-nobrowse", "-mountpoint", verifyRoot, outputPath]); if (fs.readlinkSync(path.join(verifyRoot, "Applications")) !== "/Applications") throw new Error("DMG lacks Applications shortcut"); const extractedApp = path.join(verifyRoot, `${productName}.app`); if (!fs.statSync(extractedApp, { throwIfNoEntry: false })?.isDirectory() || !signatureIsValid(extractedApp)) { throw new Error("Výsledný DMG neobsahuje kompletní aplikaci s platným podpisem bundle."); } verifyMacNetworkIdentity(extractedApp, networkIdentity); await smokeTestApp(extractedApp); } finally { run("hdiutil", ["detach", verifyRoot]); fs.rmSync(verifyRoot, { recursive: true, force: true }); } console.log(`Instalační balíček: ${outputPath}`); console.log(`Podpis: ${signature}`); if (signature !== "developer-id") { console.warn("VAROVÁNÍ: Jde o testovací ad-hoc sestavení. Pro bezobslužnou instalaci z webu je nutný Developer ID podpis a Apple notarizace."); } } main().catch((error) => { console.error(error.message || error); process.exitCode = 1; });