Deploy: exdev sync 2026-08-13 13:38:36
This commit is contained in:
Regular → Executable
+226
-226
@@ -1,226 +1,226 @@
|
||||
#!/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);
|
||||
}
|
||||
#!/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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user