Deploy: exdev sync 2026-08-13 13:38:36
This commit is contained in:
Regular → Executable
+230
-230
@@ -1,230 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
공식 길통이+차로(ai-mascot.png)로 Electron 앱 아이콘 생성
|
||||
- build/icon.png (1024)
|
||||
- build/icon.icns (macOS)
|
||||
- build/icon.ico (Windows)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import plistlib
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILD = ROOT / "build"
|
||||
SRC_CANDIDATES = [
|
||||
ROOT.parent / "chatbotApp-chatbot2.0-agent" / "web" / "public" / "images" / "ai-mascot.png",
|
||||
ROOT.parent / "chatbotApp-chatbot2.0-agent" / "public-ai" / "images" / "ai-mascot.png",
|
||||
]
|
||||
|
||||
# EX 브랜드 블루
|
||||
BG = (36, 107, 235, 255)
|
||||
WHITE = (255, 255, 255, 255)
|
||||
|
||||
|
||||
def _venv_python(venv: Path) -> Path:
|
||||
if sys.platform == "win32":
|
||||
return venv / "Scripts" / "python.exe"
|
||||
py = venv / "bin" / "python3"
|
||||
return py if py.exists() else venv / "bin" / "python"
|
||||
|
||||
|
||||
def _in_venv(venv: Path) -> bool:
|
||||
"""Homebrew venv의 bin/python은 시스템 바이너리 symlink라 executable.resolve() 비교는 불가."""
|
||||
try:
|
||||
return Path(sys.prefix).resolve() == venv.resolve()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _ensure_pip(py: Path) -> None:
|
||||
try:
|
||||
subprocess.check_call([str(py), "-m", "pip", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
print("[build-icons] ensurepip으로 pip 부트스트랩…", flush=True)
|
||||
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
||||
|
||||
|
||||
def _pip_install_pillow(py: Path) -> None:
|
||||
_ensure_pip(py)
|
||||
cmd = [str(py), "-m", "pip", "install", "Pillow", "-q"]
|
||||
try:
|
||||
subprocess.check_call(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
print("[build-icons] pip 재시도 (ensurepip 후)…", flush=True)
|
||||
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def require_pillow():
|
||||
try:
|
||||
from PIL import Image, ImageDraw # noqa: F401
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
venv = ROOT / ".venv-icons"
|
||||
py = _venv_python(venv)
|
||||
if not py.exists():
|
||||
print("[build-icons] venv 생성 중…", flush=True)
|
||||
subprocess.check_call([sys.executable, "-m", "venv", str(venv)])
|
||||
py = _venv_python(venv)
|
||||
print("[build-icons] Pillow 설치 중…", flush=True)
|
||||
_pip_install_pillow(py)
|
||||
# 현재 프로세스에 Pillow가 없으면 venv로 재실행 (sys.prefix로 판별)
|
||||
if not _in_venv(venv):
|
||||
raise SystemExit(subprocess.call([str(py), str(Path(__file__).resolve()), *sys.argv[1:]]))
|
||||
from PIL import Image, ImageDraw # noqa: F401
|
||||
|
||||
|
||||
def rounded_rect_mask(size: int, radius: int):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
mask = Image.new("L", (size, size), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=255)
|
||||
return mask
|
||||
|
||||
|
||||
def make_icon_png(src: Path, out: Path, size: int = 1024) -> None:
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
mascot = Image.open(src).convert("RGBA")
|
||||
# 흰 배경 제거에 가깝게: 거의 흰 픽셀을 투명 처리
|
||||
pixels = mascot.load()
|
||||
w, h = mascot.size
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b, a = pixels[x, y]
|
||||
if r > 245 and g > 245 and b > 245:
|
||||
pixels[x, y] = (r, g, b, 0)
|
||||
|
||||
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
# macOS 아이콘용 라운드 사각 (약 22% radius)
|
||||
radius = int(size * 0.223)
|
||||
draw.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=BG)
|
||||
|
||||
# 안쪽 밝은 원형 배지
|
||||
badge = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
bd = ImageDraw.Draw(badge)
|
||||
pad = int(size * 0.08)
|
||||
bd.ellipse((pad, pad, size - pad, size - pad), fill=(255, 255, 255, 235))
|
||||
canvas = Image.alpha_composite(canvas, badge)
|
||||
|
||||
# 마스코트 배치 (여백 확보)
|
||||
max_side = int(size * 0.78)
|
||||
mw, mh = mascot.size
|
||||
scale = min(max_side / mw, max_side / mh)
|
||||
nw, nh = int(mw * scale), int(mh * scale)
|
||||
mascot = mascot.resize((nw, nh), Image.Resampling.LANCZOS)
|
||||
ox = (size - nw) // 2
|
||||
oy = (size - nh) // 2 + int(size * 0.02)
|
||||
canvas.alpha_composite(mascot, (ox, oy))
|
||||
|
||||
# 마스크로 모서리 정리
|
||||
mask = rounded_rect_mask(size, radius)
|
||||
out_img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
out_img.paste(canvas, (0, 0), mask=mask)
|
||||
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_img.save(out, "PNG")
|
||||
print(f"[build-icons] ✅ {out} ({size}x{size})")
|
||||
|
||||
|
||||
def build_icns(png_1024: Path, icns_path: Path) -> None:
|
||||
sizes = [
|
||||
(16, "icon_16x16.png"),
|
||||
(32, "diana.k@example.org"),
|
||||
(32, "icon_32x32.png"),
|
||||
(64, "ivan.p@example.net"),
|
||||
(128, "icon_128x128.png"),
|
||||
(256, "wendy.h@example.net"),
|
||||
(256, "icon_256x256.png"),
|
||||
(512, "wendy.h@example.net"),
|
||||
(512, "icon_512x512.png"),
|
||||
(1024, "walt.e@example.net"),
|
||||
]
|
||||
iconset = BUILD / "icon.iconset"
|
||||
if iconset.exists():
|
||||
shutil.rmtree(iconset)
|
||||
iconset.mkdir(parents=True)
|
||||
|
||||
from PIL import Image
|
||||
|
||||
base = Image.open(png_1024).convert("RGBA")
|
||||
for px, name in sizes:
|
||||
img = base.resize((px, px), Image.Resampling.LANCZOS)
|
||||
img.save(iconset / name, "PNG")
|
||||
|
||||
if icns_path.exists():
|
||||
icns_path.unlink()
|
||||
subprocess.check_call(["iconutil", "-c", "icns", str(iconset), "-o", str(icns_path)])
|
||||
shutil.rmtree(iconset)
|
||||
print(f"[build-icons] ✅ {icns_path}")
|
||||
|
||||
|
||||
def _png_chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def rgba_to_png_bytes(img) -> bytes:
|
||||
"""Pillow 이미지 → PNG bytes (ICO용)."""
|
||||
import io
|
||||
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def build_ico(png_1024: Path, ico_path: Path) -> None:
|
||||
"""여러 해상도 PNG를 담은 .ico 생성 (electron-builder는 256 필수)."""
|
||||
from PIL import Image
|
||||
|
||||
sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
|
||||
base = Image.open(png_1024).convert("RGBA")
|
||||
# Pillow: 원본에서 sizes= 로 리사이즈한 멀티 엔트리 ICO 생성
|
||||
# (append_images + 작은 첫 프레임은 16x16만 남는 버그 회피)
|
||||
base.save(ico_path, format="ICO", sizes=sizes)
|
||||
print(f"[build-icons] ✅ {ico_path}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
require_pillow()
|
||||
src = next((p for p in SRC_CANDIDATES if p.exists()), None)
|
||||
if not src:
|
||||
print("[build-icons] ai-mascot.png 없음", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
BUILD.mkdir(parents=True, exist_ok=True)
|
||||
png = BUILD / "icon.png"
|
||||
make_icon_png(src, png, 1024)
|
||||
# electron-builder 호환 별칭
|
||||
shutil.copy2(png, BUILD / "icon-1024.png")
|
||||
|
||||
if sys.platform == "darwin":
|
||||
try:
|
||||
build_icns(png, BUILD / "icon.icns")
|
||||
except Exception as e:
|
||||
print(f"[build-icons] icns 실패 (png만 사용): {e}", file=sys.stderr)
|
||||
else:
|
||||
print("[build-icons] icns는 macOS에서만 생성합니다")
|
||||
|
||||
try:
|
||||
build_ico(png, BUILD / "icon.ico")
|
||||
except Exception as e:
|
||||
print(f"[build-icons] ico 실패: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("[build-icons] 완료")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
공식 길통이+차로(ai-mascot.png)로 Electron 앱 아이콘 생성
|
||||
- build/icon.png (1024)
|
||||
- build/icon.icns (macOS)
|
||||
- build/icon.ico (Windows)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import plistlib
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILD = ROOT / "build"
|
||||
SRC_CANDIDATES = [
|
||||
ROOT.parent / "chatbotApp-chatbot2.0-agent" / "web" / "public" / "images" / "ai-mascot.png",
|
||||
ROOT.parent / "chatbotApp-chatbot2.0-agent" / "public-ai" / "images" / "ai-mascot.png",
|
||||
]
|
||||
|
||||
# EX 브랜드 블루
|
||||
BG = (36, 107, 235, 255)
|
||||
WHITE = (255, 255, 255, 255)
|
||||
|
||||
|
||||
def _venv_python(venv: Path) -> Path:
|
||||
if sys.platform == "win32":
|
||||
return venv / "Scripts" / "python.exe"
|
||||
py = venv / "bin" / "python3"
|
||||
return py if py.exists() else venv / "bin" / "python"
|
||||
|
||||
|
||||
def _in_venv(venv: Path) -> bool:
|
||||
"""Homebrew venv의 bin/python은 시스템 바이너리 symlink라 executable.resolve() 비교는 불가."""
|
||||
try:
|
||||
return Path(sys.prefix).resolve() == venv.resolve()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _ensure_pip(py: Path) -> None:
|
||||
try:
|
||||
subprocess.check_call([str(py), "-m", "pip", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
print("[build-icons] ensurepip으로 pip 부트스트랩…", flush=True)
|
||||
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
||||
|
||||
|
||||
def _pip_install_pillow(py: Path) -> None:
|
||||
_ensure_pip(py)
|
||||
cmd = [str(py), "-m", "pip", "install", "Pillow", "-q"]
|
||||
try:
|
||||
subprocess.check_call(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
print("[build-icons] pip 재시도 (ensurepip 후)…", flush=True)
|
||||
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def require_pillow():
|
||||
try:
|
||||
from PIL import Image, ImageDraw # noqa: F401
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
venv = ROOT / ".venv-icons"
|
||||
py = _venv_python(venv)
|
||||
if not py.exists():
|
||||
print("[build-icons] venv 생성 중…", flush=True)
|
||||
subprocess.check_call([sys.executable, "-m", "venv", str(venv)])
|
||||
py = _venv_python(venv)
|
||||
print("[build-icons] Pillow 설치 중…", flush=True)
|
||||
_pip_install_pillow(py)
|
||||
# 현재 프로세스에 Pillow가 없으면 venv로 재실행 (sys.prefix로 판별)
|
||||
if not _in_venv(venv):
|
||||
raise SystemExit(subprocess.call([str(py), str(Path(__file__).resolve()), *sys.argv[1:]]))
|
||||
from PIL import Image, ImageDraw # noqa: F401
|
||||
|
||||
|
||||
def rounded_rect_mask(size: int, radius: int):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
mask = Image.new("L", (size, size), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=255)
|
||||
return mask
|
||||
|
||||
|
||||
def make_icon_png(src: Path, out: Path, size: int = 1024) -> None:
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
mascot = Image.open(src).convert("RGBA")
|
||||
# 흰 배경 제거에 가깝게: 거의 흰 픽셀을 투명 처리
|
||||
pixels = mascot.load()
|
||||
w, h = mascot.size
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b, a = pixels[x, y]
|
||||
if r > 245 and g > 245 and b > 245:
|
||||
pixels[x, y] = (r, g, b, 0)
|
||||
|
||||
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
# macOS 아이콘용 라운드 사각 (약 22% radius)
|
||||
radius = int(size * 0.223)
|
||||
draw.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=BG)
|
||||
|
||||
# 안쪽 밝은 원형 배지
|
||||
badge = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
bd = ImageDraw.Draw(badge)
|
||||
pad = int(size * 0.08)
|
||||
bd.ellipse((pad, pad, size - pad, size - pad), fill=(255, 255, 255, 235))
|
||||
canvas = Image.alpha_composite(canvas, badge)
|
||||
|
||||
# 마스코트 배치 (여백 확보)
|
||||
max_side = int(size * 0.78)
|
||||
mw, mh = mascot.size
|
||||
scale = min(max_side / mw, max_side / mh)
|
||||
nw, nh = int(mw * scale), int(mh * scale)
|
||||
mascot = mascot.resize((nw, nh), Image.Resampling.LANCZOS)
|
||||
ox = (size - nw) // 2
|
||||
oy = (size - nh) // 2 + int(size * 0.02)
|
||||
canvas.alpha_composite(mascot, (ox, oy))
|
||||
|
||||
# 마스크로 모서리 정리
|
||||
mask = rounded_rect_mask(size, radius)
|
||||
out_img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
out_img.paste(canvas, (0, 0), mask=mask)
|
||||
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_img.save(out, "PNG")
|
||||
print(f"[build-icons] ✅ {out} ({size}x{size})")
|
||||
|
||||
|
||||
def build_icns(png_1024: Path, icns_path: Path) -> None:
|
||||
sizes = [
|
||||
(16, "icon_16x16.png"),
|
||||
(32, "diana.k@example.org"),
|
||||
(32, "icon_32x32.png"),
|
||||
(64, "ivan.p@example.net"),
|
||||
(128, "icon_128x128.png"),
|
||||
(256, "wendy.h@example.net"),
|
||||
(256, "icon_256x256.png"),
|
||||
(512, "wendy.h@example.net"),
|
||||
(512, "icon_512x512.png"),
|
||||
(1024, "walt.e@example.net"),
|
||||
]
|
||||
iconset = BUILD / "icon.iconset"
|
||||
if iconset.exists():
|
||||
shutil.rmtree(iconset)
|
||||
iconset.mkdir(parents=True)
|
||||
|
||||
from PIL import Image
|
||||
|
||||
base = Image.open(png_1024).convert("RGBA")
|
||||
for px, name in sizes:
|
||||
img = base.resize((px, px), Image.Resampling.LANCZOS)
|
||||
img.save(iconset / name, "PNG")
|
||||
|
||||
if icns_path.exists():
|
||||
icns_path.unlink()
|
||||
subprocess.check_call(["iconutil", "-c", "icns", str(iconset), "-o", str(icns_path)])
|
||||
shutil.rmtree(iconset)
|
||||
print(f"[build-icons] ✅ {icns_path}")
|
||||
|
||||
|
||||
def _png_chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def rgba_to_png_bytes(img) -> bytes:
|
||||
"""Pillow 이미지 → PNG bytes (ICO용)."""
|
||||
import io
|
||||
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def build_ico(png_1024: Path, ico_path: Path) -> None:
|
||||
"""여러 해상도 PNG를 담은 .ico 생성 (electron-builder는 256 필수)."""
|
||||
from PIL import Image
|
||||
|
||||
sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
|
||||
base = Image.open(png_1024).convert("RGBA")
|
||||
# Pillow: 원본에서 sizes= 로 리사이즈한 멀티 엔트리 ICO 생성
|
||||
# (append_images + 작은 첫 프레임은 16x16만 남는 버그 회피)
|
||||
base.save(ico_path, format="ICO", sizes=sizes)
|
||||
print(f"[build-icons] ✅ {ico_path}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
require_pillow()
|
||||
src = next((p for p in SRC_CANDIDATES if p.exists()), None)
|
||||
if not src:
|
||||
print("[build-icons] ai-mascot.png 없음", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
BUILD.mkdir(parents=True, exist_ok=True)
|
||||
png = BUILD / "icon.png"
|
||||
make_icon_png(src, png, 1024)
|
||||
# electron-builder 호환 별칭
|
||||
shutil.copy2(png, BUILD / "icon-1024.png")
|
||||
|
||||
if sys.platform == "darwin":
|
||||
try:
|
||||
build_icns(png, BUILD / "icon.icns")
|
||||
except Exception as e:
|
||||
print(f"[build-icons] icns 실패 (png만 사용): {e}", file=sys.stderr)
|
||||
else:
|
||||
print("[build-icons] icns는 macOS에서만 생성합니다")
|
||||
|
||||
try:
|
||||
build_ico(png, BUILD / "icon.ico")
|
||||
except Exception as e:
|
||||
print(f"[build-icons] ico 실패: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print("[build-icons] 완료")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Regular → Executable
+23
-23
@@ -1,23 +1,23 @@
|
||||
/** 다운로드 산출물 파일명 (공백 포함 — URL 인코딩 필요) */
|
||||
export const DOWNLOAD_ARTIFACTS = {
|
||||
macIntelInstaller: "exAIChatbot 2.0-mac-intel.dmg",
|
||||
macIntelPortable: "exAIChatbot 2.0-mac-intel.zip",
|
||||
macSiliconInstaller: "exAIChatbot 2.0-mac-silicon.dmg",
|
||||
macSiliconPortable: "exAIChatbot 2.0-mac-silicon.zip",
|
||||
win32Installer: "exAIChatbot 2.0-win32-Setup.exe",
|
||||
win32Portable: "exAIChatbot 2.0-win32-portable.exe",
|
||||
win64Installer: "exAIChatbot 2.0-win64-Setup.exe",
|
||||
win64Portable: "exAIChatbot 2.0-win64-portable.exe",
|
||||
};
|
||||
|
||||
export const DOWNLOAD_ARTIFACT_LIST = Object.values(DOWNLOAD_ARTIFACTS);
|
||||
|
||||
/** 이전 배포 파일명 (정리용) */
|
||||
export const OBSOLETE_DOWNLOAD_ARTIFACTS = [
|
||||
"exAIChatbot 2.0-win32.zip",
|
||||
"exAIChatbot 2.0-win64.zip",
|
||||
"EX-AI-Chatbot-mac-universal.zip",
|
||||
"EX-AI-Chatbot-Setup.exe",
|
||||
"EX-AI-Chatbot-Setup.exe.blockmap",
|
||||
"EX-AI-Chatbot-mac-universal.zip.blockmap",
|
||||
];
|
||||
/** 다운로드 산출물 파일명 (공백 포함 — URL 인코딩 필요) */
|
||||
export const DOWNLOAD_ARTIFACTS = {
|
||||
macIntelInstaller: "exAIChatbot 2.0-mac-intel.dmg",
|
||||
macIntelPortable: "exAIChatbot 2.0-mac-intel.zip",
|
||||
macSiliconInstaller: "exAIChatbot 2.0-mac-silicon.dmg",
|
||||
macSiliconPortable: "exAIChatbot 2.0-mac-silicon.zip",
|
||||
win32Installer: "exAIChatbot 2.0-win32-Setup.exe",
|
||||
win32Portable: "exAIChatbot 2.0-win32-portable.exe",
|
||||
win64Installer: "exAIChatbot 2.0-win64-Setup.exe",
|
||||
win64Portable: "exAIChatbot 2.0-win64-portable.exe",
|
||||
};
|
||||
|
||||
export const DOWNLOAD_ARTIFACT_LIST = Object.values(DOWNLOAD_ARTIFACTS);
|
||||
|
||||
/** 이전 배포 파일명 (정리용) */
|
||||
export const OBSOLETE_DOWNLOAD_ARTIFACTS = [
|
||||
"exAIChatbot 2.0-win32.zip",
|
||||
"exAIChatbot 2.0-win64.zip",
|
||||
"EX-AI-Chatbot-mac-universal.zip",
|
||||
"EX-AI-Chatbot-Setup.exe",
|
||||
"EX-AI-Chatbot-Setup.exe.blockmap",
|
||||
"EX-AI-Chatbot-mac-universal.zip.blockmap",
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Regular → Executable
+69
-69
@@ -1,69 +1,69 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Windows 산출물 정리
|
||||
* - Setup.exe (NSIS 설치형)
|
||||
* - portable.exe (포터블)
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
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 MAP = [
|
||||
{
|
||||
patterns: [/^raw-win-x64-Setup\.exe$/i],
|
||||
out: DOWNLOAD_ARTIFACTS.win64Installer,
|
||||
},
|
||||
{
|
||||
patterns: [/^raw-win-x64-portable\.exe$/i],
|
||||
out: DOWNLOAD_ARTIFACTS.win64Portable,
|
||||
},
|
||||
{
|
||||
patterns: [/^raw-win-ia32-Setup\.exe$/i, /^raw-win-i386-Setup\.exe$/i],
|
||||
out: DOWNLOAD_ARTIFACTS.win32Installer,
|
||||
},
|
||||
{
|
||||
patterns: [/^raw-win-ia32-portable\.exe$/i, /^raw-win-i386-portable\.exe$/i],
|
||||
out: DOWNLOAD_ARTIFACTS.win32Portable,
|
||||
},
|
||||
];
|
||||
|
||||
if (!fs.existsSync(distDir)) {
|
||||
console.error("[finalize-win] dist 없음");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(distDir);
|
||||
let done = 0;
|
||||
|
||||
for (const rule of MAP) {
|
||||
const srcName = files.find((f) => rule.patterns.some((re) => re.test(f)));
|
||||
if (!srcName) {
|
||||
console.warn(`[finalize-win] skip (원본 없음): ${rule.out}`);
|
||||
continue;
|
||||
}
|
||||
const src = path.join(distDir, srcName);
|
||||
const dest = path.join(distDir, rule.out);
|
||||
if (src !== dest) {
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
fs.renameSync(src, dest);
|
||||
}
|
||||
const size = fs.statSync(dest).size;
|
||||
console.log(`[finalize-win] ✅ ${rule.out} (${Math.round(size / 1024 / 1024)} MB) ← ${srcName}`);
|
||||
done += 1;
|
||||
}
|
||||
|
||||
for (const f of fs.readdirSync(distDir)) {
|
||||
if (/^raw-win-/.test(f)) {
|
||||
fs.unlinkSync(path.join(distDir, f));
|
||||
console.log(`[finalize-win] removed ${f}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (done === 0) {
|
||||
console.error("[finalize-win] Windows 설치/포터블 산출물을 찾지 못했습니다.");
|
||||
process.exit(1);
|
||||
}
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Windows 산출물 정리
|
||||
* - Setup.exe (NSIS 설치형)
|
||||
* - portable.exe (포터블)
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
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 MAP = [
|
||||
{
|
||||
patterns: [/^raw-win-x64-Setup\.exe$/i],
|
||||
out: DOWNLOAD_ARTIFACTS.win64Installer,
|
||||
},
|
||||
{
|
||||
patterns: [/^raw-win-x64-portable\.exe$/i],
|
||||
out: DOWNLOAD_ARTIFACTS.win64Portable,
|
||||
},
|
||||
{
|
||||
patterns: [/^raw-win-ia32-Setup\.exe$/i, /^raw-win-i386-Setup\.exe$/i],
|
||||
out: DOWNLOAD_ARTIFACTS.win32Installer,
|
||||
},
|
||||
{
|
||||
patterns: [/^raw-win-ia32-portable\.exe$/i, /^raw-win-i386-portable\.exe$/i],
|
||||
out: DOWNLOAD_ARTIFACTS.win32Portable,
|
||||
},
|
||||
];
|
||||
|
||||
if (!fs.existsSync(distDir)) {
|
||||
console.error("[finalize-win] dist 없음");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(distDir);
|
||||
let done = 0;
|
||||
|
||||
for (const rule of MAP) {
|
||||
const srcName = files.find((f) => rule.patterns.some((re) => re.test(f)));
|
||||
if (!srcName) {
|
||||
console.warn(`[finalize-win] skip (원본 없음): ${rule.out}`);
|
||||
continue;
|
||||
}
|
||||
const src = path.join(distDir, srcName);
|
||||
const dest = path.join(distDir, rule.out);
|
||||
if (src !== dest) {
|
||||
if (fs.existsSync(dest)) fs.unlinkSync(dest);
|
||||
fs.renameSync(src, dest);
|
||||
}
|
||||
const size = fs.statSync(dest).size;
|
||||
console.log(`[finalize-win] ✅ ${rule.out} (${Math.round(size / 1024 / 1024)} MB) ← ${srcName}`);
|
||||
done += 1;
|
||||
}
|
||||
|
||||
for (const f of fs.readdirSync(distDir)) {
|
||||
if (/^raw-win-/.test(f)) {
|
||||
fs.unlinkSync(path.join(distDir, f));
|
||||
console.log(`[finalize-win] removed ${f}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (done === 0) {
|
||||
console.error("[finalize-win] Windows 설치/포터블 산출물을 찾지 못했습니다.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Regular → Executable
+76
-76
@@ -1,76 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* electron-builder 산출물 → chatbotApp public-ai/downloads/ 복사
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
DOWNLOAD_ARTIFACTS,
|
||||
DOWNLOAD_ARTIFACT_LIST,
|
||||
OBSOLETE_DOWNLOAD_ARTIFACTS,
|
||||
} from "./download-artifacts.mjs";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const distDir = path.join(root, "dist");
|
||||
const downloadsDir = path.resolve(
|
||||
root,
|
||||
"../chatbotApp-chatbot2.0-agent/public-ai/downloads",
|
||||
);
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
||||
|
||||
if (!fs.existsSync(distDir)) {
|
||||
console.error(`[publish-downloads] dist 없음: ${distDir}`);
|
||||
console.error("먼저 npm run dist:mac 또는 dist:win 을 실행하세요.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
|
||||
for (const name of OBSOLETE_DOWNLOAD_ARTIFACTS) {
|
||||
const p = path.join(downloadsDir, name);
|
||||
if (fs.existsSync(p)) {
|
||||
fs.unlinkSync(p);
|
||||
console.log(`[publish-downloads] removed obsolete ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
const copied = [];
|
||||
for (const name of DOWNLOAD_ARTIFACT_LIST) {
|
||||
const src = path.join(distDir, name);
|
||||
if (!fs.existsSync(src)) {
|
||||
console.warn(`[publish-downloads] skip (없음): ${name}`);
|
||||
continue;
|
||||
}
|
||||
const dest = path.join(downloadsDir, name);
|
||||
fs.copyFileSync(src, dest);
|
||||
const stat = fs.statSync(dest);
|
||||
copied.push({
|
||||
file: name,
|
||||
size: stat.size,
|
||||
mtime: stat.mtime.toISOString(),
|
||||
});
|
||||
console.log(`[publish-downloads] ${name} → ${dest}`);
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
version: pkg.version,
|
||||
updatedAt: new Date().toISOString(),
|
||||
files: { ...DOWNLOAD_ARTIFACTS },
|
||||
artifacts: copied,
|
||||
note:
|
||||
"미서명 빌드 — Mac: dmg(설치) / zip(포터블·실행하기.command). Windows: Setup.exe(설치) / portable.exe(포터블).",
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(downloadsDir, "manifest.json"),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
if (copied.length === 0) {
|
||||
console.error("[publish-downloads] 복사된 파일이 없습니다.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[publish-downloads] ✅ ${copied.length}개 → ${downloadsDir}`);
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* electron-builder 산출물 → chatbotApp public-ai/downloads/ 복사
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
DOWNLOAD_ARTIFACTS,
|
||||
DOWNLOAD_ARTIFACT_LIST,
|
||||
OBSOLETE_DOWNLOAD_ARTIFACTS,
|
||||
} from "./download-artifacts.mjs";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const distDir = path.join(root, "dist");
|
||||
const downloadsDir = path.resolve(
|
||||
root,
|
||||
"../chatbotApp-chatbot2.0-agent/public-ai/downloads",
|
||||
);
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
||||
|
||||
if (!fs.existsSync(distDir)) {
|
||||
console.error(`[publish-downloads] dist 없음: ${distDir}`);
|
||||
console.error("먼저 npm run dist:mac 또는 dist:win 을 실행하세요.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
|
||||
for (const name of OBSOLETE_DOWNLOAD_ARTIFACTS) {
|
||||
const p = path.join(downloadsDir, name);
|
||||
if (fs.existsSync(p)) {
|
||||
fs.unlinkSync(p);
|
||||
console.log(`[publish-downloads] removed obsolete ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
const copied = [];
|
||||
for (const name of DOWNLOAD_ARTIFACT_LIST) {
|
||||
const src = path.join(distDir, name);
|
||||
if (!fs.existsSync(src)) {
|
||||
console.warn(`[publish-downloads] skip (없음): ${name}`);
|
||||
continue;
|
||||
}
|
||||
const dest = path.join(downloadsDir, name);
|
||||
fs.copyFileSync(src, dest);
|
||||
const stat = fs.statSync(dest);
|
||||
copied.push({
|
||||
file: name,
|
||||
size: stat.size,
|
||||
mtime: stat.mtime.toISOString(),
|
||||
});
|
||||
console.log(`[publish-downloads] ${name} → ${dest}`);
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
version: pkg.version,
|
||||
updatedAt: new Date().toISOString(),
|
||||
files: { ...DOWNLOAD_ARTIFACTS },
|
||||
artifacts: copied,
|
||||
note:
|
||||
"미서명 빌드 — Mac: dmg(설치) / zip(포터블·실행하기.command). Windows: Setup.exe(설치) / portable.exe(포터블).",
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(downloadsDir, "manifest.json"),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
if (copied.length === 0) {
|
||||
console.error("[publish-downloads] 복사된 파일이 없습니다.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[publish-downloads] ✅ ${copied.length}개 → ${downloadsDir}`);
|
||||
|
||||
Reference in New Issue
Block a user