227 lines
7.3 KiB
JavaScript
Executable File
227 lines
7.3 KiB
JavaScript
Executable File
#!/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);
|
|
}
|