Files
exAichatbot_agent/chatbotDesktopApp-chatbot2.0/scripts/finalize-mac.mjs
T
2026-08-03 13:59:26 +09:00

227 lines
7.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Mac 산출물 정리
* - zip(포터블): .app + 실행하기.command
* - dmg(설치형): 동일 서명 .app + 「응용 프로그램에 설치하고 실행.command」
* (Gatekeeper 「손상됨」팝업 방지 — 설치 시 xattr -cr 수행)
*/
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DOWNLOAD_ARTIFACTS } from "./download-artifacts.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const distDir = path.join(root, "dist");
const appName = "EX AI Chatbot.app";
const packagingMac = path.join(root, "packaging", "mac");
const installCmdName = "응용 프로그램에 설치하고 실행.command";
const runCmdName = "실행하기.command";
const guideName = "설치안내.txt";
const TARGETS = [
{
appDirs: [path.join(distDir, "mac-x64", appName), path.join(distDir, "mac", appName)],
zipName: DOWNLOAD_ARTIFACTS.macIntelPortable,
dmgName: DOWNLOAD_ARTIFACTS.macIntelInstaller,
label: "intel (x64)",
},
{
appDirs: [path.join(distDir, "mac-arm64", appName)],
zipName: DOWNLOAD_ARTIFACTS.macSiliconPortable,
dmgName: DOWNLOAD_ARTIFACTS.macSiliconInstaller,
label: "silicon (arm64)",
},
];
function codesignApp(appPath, label) {
try {
execFileSync(
"codesign",
[
"--force",
"--deep",
"--sign",
"-",
"--entitlements",
path.join(packagingMac, "entitlements.plist"),
appPath,
],
{ stdio: "inherit" },
);
} catch (e) {
console.warn(`[finalize-mac] entitlements codesign 실패 (${label}), ad-hoc 재시도:`, e.message);
try {
execFileSync("codesign", ["--force", "--deep", "--sign", "-", appPath], {
stdio: "inherit",
});
} catch (e2) {
console.warn(`[finalize-mac] codesign 실패 (${label}):`, e2.message);
}
}
try {
execFileSync("xattr", ["-cr", appPath], { stdio: "ignore" });
} catch {
/* ignore */
}
}
function ensurePackagingFiles() {
for (const name of [runCmdName, installCmdName, guideName]) {
const p = path.join(packagingMac, name);
if (!fs.existsSync(p)) throw new Error(`packaging/mac 파일 없음: ${name}`);
}
}
function packZip(appPath, zipName, label) {
console.log(`[finalize-mac] zip ${label}: ${appPath}`);
const stage = fs.mkdtempSync(path.join(os.tmpdir(), "ex-ai-mac-zip-"));
try {
fs.cpSync(appPath, path.join(stage, appName), { recursive: true });
fs.copyFileSync(path.join(packagingMac, runCmdName), path.join(stage, runCmdName));
fs.copyFileSync(path.join(packagingMac, guideName), path.join(stage, guideName));
fs.chmodSync(path.join(stage, runCmdName), 0o755);
try {
execFileSync("xattr", ["-cr", stage], { stdio: "ignore" });
} catch {
/* ignore */
}
const zipPath = path.join(distDir, zipName);
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
execFileSync("zip", ["-r", "-y", zipPath, appName, runCmdName, guideName], {
cwd: stage,
stdio: "inherit",
});
const size = fs.statSync(zipPath).size;
console.log(`[finalize-mac] ✅ zip ${zipName} (${Math.round(size / 1024 / 1024)} MB)`);
return true;
} finally {
fs.rmSync(stage, { recursive: true, force: true });
}
}
function detachStaleVolumes() {
for (const vol of ["EX AI Chatbot", "EX AI Chatbot Intel", "EX AI Chatbot Silicon"]) {
try {
execFileSync("hdiutil", ["detach", `/Volumes/${vol}`, "-force"], { stdio: "ignore" });
} catch {
/* not mounted */
}
}
}
function packDmg(appPath, dmgName, label) {
console.log(`[finalize-mac] dmg ${label}: ${appPath}`);
const stage = fs.mkdtempSync(path.join(os.tmpdir(), "ex-ai-mac-dmg-"));
const rwDmg = path.join(os.tmpdir(), `ex-ai-mac-${Date.now()}-${process.pid}.dmg`);
// arch별 볼륨명 — 동시/잔여 마운트 충돌 방지
const volName = label.includes("silicon") ? "EX AI Chatbot Silicon" : "EX AI Chatbot Intel";
try {
fs.cpSync(appPath, path.join(stage, appName), { recursive: true });
fs.copyFileSync(path.join(packagingMac, installCmdName), path.join(stage, installCmdName));
fs.copyFileSync(path.join(packagingMac, guideName), path.join(stage, guideName));
fs.chmodSync(path.join(stage, installCmdName), 0o755);
// Applications 폴더 바로가기 (일반 dmg UX)
try {
fs.symlinkSync("/Applications", path.join(stage, "Applications"));
} catch {
/* ignore */
}
try {
execFileSync("xattr", ["-cr", stage], { stdio: "ignore" });
} catch {
/* ignore */
}
const outPath = path.join(distDir, dmgName);
if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
if (fs.existsSync(rwDmg)) fs.unlinkSync(rwDmg);
detachStaleVolumes();
// 읽기 전용 압축 dmg 생성 (electron-builder DMG 미사용 — hdiutil 경합 회피)
// Finder/Spotlight가 잠깐 잡고 있으면 "리소스 사용 중"이 나므로 재시도한다.
const createArgs = [
"create",
"-volname",
volName,
"-srcfolder",
stage,
"-ov",
"-format",
"UDZO",
outPath,
];
let lastErr = null;
for (let attempt = 1; attempt <= 4; attempt += 1) {
try {
if (attempt > 1) {
console.warn(`[finalize-mac] hdiutil create 재시도 ${attempt}/4 (${label})`);
detachStaleVolumes();
try {
execFileSync("sleep", [String(1.5 * attempt)], { stdio: "ignore" });
} catch {
/* ignore */
}
}
execFileSync("hdiutil", createArgs, { stdio: "inherit" });
lastErr = null;
break;
} catch (e) {
lastErr = e;
const msg = String(e?.stderr || e?.message || e);
const busy = /리소스 사용 중|Resource busy|Device busy|hdiutil/i.test(msg) || e?.status === 1;
if (!busy || attempt === 4) break;
}
}
if (lastErr) throw lastErr;
const size = fs.statSync(outPath).size;
console.log(`[finalize-mac] ✅ dmg ${dmgName} (${Math.round(size / 1024 / 1024)} MB)`);
return true;
} finally {
fs.rmSync(stage, { recursive: true, force: true });
try {
if (fs.existsSync(rwDmg)) fs.unlinkSync(rwDmg);
} catch {
/* ignore */
}
}
}
if (!fs.existsSync(distDir)) {
console.error("[finalize-mac] dist 없음");
process.exit(1);
}
ensurePackagingFiles();
let done = 0;
for (const t of TARGETS) {
const appPath = t.appDirs.find((p) => fs.existsSync(p));
if (!appPath) {
console.warn(`[finalize-mac] skip ${t.label}: .app 없음`);
continue;
}
codesignApp(appPath, t.label);
if (packDmg(appPath, t.dmgName, t.label)) done += 1;
if (packZip(appPath, t.zipName, t.label)) done += 1;
}
// 빌더 raw zip/dmg 잔여 정리
for (const raw of fs.readdirSync(distDir)) {
if (/^raw-mac-/.test(raw) && (raw.endsWith(".zip") || raw.endsWith(".dmg"))) {
fs.unlinkSync(path.join(distDir, raw));
console.log(`[finalize-mac] removed ${raw}`);
}
}
if (done === 0) {
console.error("[finalize-mac] 패키징할 Mac 산출물이 없습니다.");
process.exit(1);
}