1033 lines
38 KiB
Python
1033 lines
38 KiB
Python
"""
|
|
Nuitka 빌드 스크립트 - 코드 보호를 위한 C 컴파일 빌드
|
|
cx_Freeze 대비 강력한 코드 보호 (리버스 엔지니어링 어려움)
|
|
|
|
사용법:
|
|
python build_nuitka.py
|
|
|
|
필수 설치:
|
|
pip install nuitka ordered-set zstandard
|
|
|
|
참고:
|
|
- 첫 빌드 시 C 컴파일러 설치 필요 (MinGW64 또는 Visual Studio)
|
|
- Nuitka가 자동으로 MinGW64 다운로드 제안함
|
|
"""
|
|
|
|
# 인코딩 설정 (Windows PowerShell 호환성)
|
|
import sys
|
|
import io
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
|
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
import shutil
|
|
import time
|
|
from updateManager.__version__ import (
|
|
__title__, __version__, __description__, __author__,
|
|
__exe_name__, __icon_file__
|
|
)
|
|
|
|
# 기본 경로 설정
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
OUTPUT_DIR = os.path.join(BASE_DIR, "build_nuitka")
|
|
DIST_DIR = os.path.join(OUTPUT_DIR, f"{__exe_name__}.dist")
|
|
|
|
|
|
def get_nuitka_command():
|
|
"""Nuitka 빌드 명령어 생성"""
|
|
|
|
cmd = [
|
|
sys.executable, "-m", "nuitka",
|
|
|
|
# ===== 기본 빌드 옵션 =====
|
|
"--standalone", # 독립 실행 파일 (Python 없이 실행 가능)
|
|
"--assume-yes-for-downloads", # 필요한 도구 자동 다운로드
|
|
|
|
# ===== Windows GUI 설정 =====
|
|
"--windows-console-mode=disable", # 콘솔 창 숨김 (GUI 앱)
|
|
f"--windows-icon-from-ico={os.path.join(BASE_DIR, 'Edit_PartTimer3.ico')}",
|
|
|
|
# ===== 회사/제품 정보 =====
|
|
f"--windows-company-name=WhenRideMyCar",
|
|
f"--windows-product-name={__title__}",
|
|
f"--windows-file-version={__version__}",
|
|
f"--windows-product-version={__version__}",
|
|
f"--windows-file-description={__description__}",
|
|
|
|
# ===== 출력 설정 =====
|
|
f"--output-dir={OUTPUT_DIR}",
|
|
f"--output-filename={__exe_name__}.exe",
|
|
|
|
# ===== 플러그인 =====
|
|
"--enable-plugin=pyside6", # PySide6 지원
|
|
# multiprocessing 플러그인은 Nuitka에서 자동으로 활성화되므로 명시할 필요 없음
|
|
|
|
# ===== 포함할 패키지 =====
|
|
"--include-package=PySide6",
|
|
"--include-package=shiboken6",
|
|
"--include-package=PIL",
|
|
"--include-package=PIL.Image",
|
|
"--include-package=PIL.ImageOps",
|
|
"--include-package=PIL.ImageEnhance",
|
|
"--include-package=PIL.ImageFilter",
|
|
"--include-package=PIL.features",
|
|
"--include-package=numpy",
|
|
"--include-package=numpy.core",
|
|
"--include-package=numpy.random",
|
|
"--include-package=pandas",
|
|
"--include-package=requests",
|
|
"--include-package=bs4",
|
|
"--include-package=supabase",
|
|
"--include-package=gotrue",
|
|
"--include-package=storage3",
|
|
"--include-package=postgrest",
|
|
"--include-package=supafunc",
|
|
"--include-package=realtime",
|
|
"--include-package=pydantic",
|
|
"--include-package=pydantic_core",
|
|
"--include-package=httpx",
|
|
"--include-package=httpx.__version__",
|
|
"--include-package=httpx._models",
|
|
"--include-package=httpx._client",
|
|
"--include-package=httpx._config",
|
|
"--include-package=translatepy",
|
|
"--include-package=markdown",
|
|
"--include-package=psutil",
|
|
"--include-package=pyperclip",
|
|
"--include-package=playwright",
|
|
"--include-package=comtypes",
|
|
"--include-package=win32com",
|
|
"--include-package=pythoncom",
|
|
"--include-package=curl_cffi",
|
|
"--include-package=json",
|
|
"--include-package=json.encoder",
|
|
"--include-package=json.decoder",
|
|
"--include-package=json.scanner",
|
|
"--include-package=pathlib",
|
|
|
|
# ===== 포함할 모듈 (프로젝트 내부) =====
|
|
"--include-module=loggerModule",
|
|
"--include-module=toggleSwitch",
|
|
"--include-module=browser_control",
|
|
"--include-module=locatorManager",
|
|
"--include-module=sleep_control",
|
|
"--include-module=login_dialog",
|
|
"--include-module=mainUI_SP",
|
|
"--include-module=limited_gui",
|
|
|
|
# src 하위 모듈들 (자세한 모듈 지정으로 누락 방지)
|
|
"--include-package=src",
|
|
"--include-package=src.cmdDiag",
|
|
"--include-package=src.inputDiag",
|
|
"--include-package=src.keyword",
|
|
"--include-package=src.priceSetDiag",
|
|
"--include-package=src.contents",
|
|
"--include-package=src.contents.option",
|
|
"--include-package=src.contents.price",
|
|
"--include-package=src.contents.details",
|
|
"--include-package=src.contents.titleGenerator",
|
|
"--include-package=src.contents.thumb",
|
|
"--include-package=src.contents.tags",
|
|
"--include-package=src.titleManager",
|
|
"--include-package=src.titleManager.sp_ForbiddenM",
|
|
"--include-package=src.titleManager.gpt_client",
|
|
"--include-package=src.translator",
|
|
"--include-package=src.translator.papago_translator",
|
|
"--include-package=src.discord_manager",
|
|
"--include-package=src.unwantedDiag",
|
|
"--include-package=src.unwantedDiag.unwanted_words_dialog",
|
|
"--include-package=src.logDialog",
|
|
"--include-package=src.logDialog.log_dialog",
|
|
"--include-package=src.logDialog.log_filter",
|
|
"--include-package=src.modules",
|
|
"--include-package=src.modules.settings_manager",
|
|
"--include-package=src.modules.gpu_status_checker",
|
|
"--include-package=src.modules.gpu_utils",
|
|
"--include-package=src.modules.image_worker_client",
|
|
"--include-package=src.modules.fonts.fontSelectDialog",
|
|
"--include-package=src.gpuDiag",
|
|
"--include-package=src.img_module",
|
|
"--include-package=src.img_module.image_processor_manager",
|
|
"--include-package=src.img_module.image_processor_dialog",
|
|
"--include-package=src.lens",
|
|
"--include-package=src.lens.naver_lens_client",
|
|
"--include-package=src.lens.naver_lens_parser",
|
|
"--include-package=src.lens.naver_lens_adapter",
|
|
"--include-package=src.lens.aliprice_lens_client",
|
|
"--include-package=src.ui",
|
|
"--include-package=src.loggerModules",
|
|
"--include-package=updateManager",
|
|
|
|
# ===== 제외할 패키지 (용량 절감 & 불필요) =====
|
|
# GUI 프레임워크 (PySide6만 사용)
|
|
"--nofollow-import-to=tkinter",
|
|
"--nofollow-import-to=PyQt4",
|
|
"--nofollow-import-to=PyQt5",
|
|
"--nofollow-import-to=AppKit",
|
|
"--nofollow-import-to=Foundation",
|
|
|
|
# 개발/테스트 도구
|
|
"--nofollow-import-to=matplotlib",
|
|
"--nofollow-import-to=IPython",
|
|
"--nofollow-import-to=OpenSSL",
|
|
"--nofollow-import-to=curses",
|
|
"--nofollow-import-to=test",
|
|
"--nofollow-import-to=asyncpg",
|
|
"--nofollow-import-to=pytest",
|
|
"--nofollow-import-to=hypothesis",
|
|
"--nofollow-import-to=mypy",
|
|
"--nofollow-import-to=coverage",
|
|
"--nofollow-import-to=tox",
|
|
|
|
# 수학/과학 라이브러리
|
|
"--nofollow-import-to=sympy",
|
|
"--nofollow-import-to=sympy.core",
|
|
"--nofollow-import-to=sympy.ntheory",
|
|
"--nofollow-import-to=sympy.external",
|
|
"--nofollow-import-to=sympy.polys",
|
|
"--nofollow-import-to=sympy.*",
|
|
"--nofollow-import-to=mpmath",
|
|
"--nofollow-import-to=gmpy2",
|
|
"--nofollow-import-to=scipy",
|
|
"--nofollow-import-to=scipy.special",
|
|
"--nofollow-import-to=scipy.sparse",
|
|
"--nofollow-import-to=scipy.linalg",
|
|
"--nofollow-import-to=scipy.ndimage",
|
|
|
|
# 머신러닝 프레임워크
|
|
"--nofollow-import-to=torch",
|
|
"--nofollow-import-to=tensorflow",
|
|
"--nofollow-import-to=keras",
|
|
"--nofollow-import-to=paddle",
|
|
"--nofollow-import-to=paddleocr",
|
|
"--nofollow-import-to=paddlehub",
|
|
"--nofollow-import-to=onnxruntime",
|
|
"--nofollow-import-to=onnx",
|
|
"--nofollow-import-to=skimage",
|
|
"--nofollow-import-to=scikit-image",
|
|
"--nofollow-import-to=pyclipper",
|
|
"--nofollow-import-to=shapely",
|
|
"--nofollow-import-to=imgaug",
|
|
"--nofollow-import-to=albumentations",
|
|
|
|
# 사용하지 않는 Windows 자동화 도구
|
|
"--nofollow-import-to=pywinauto",
|
|
"--nofollow-import-to=selenium", # Playwright 사용 중
|
|
|
|
# 사용하지 않는 CLI/터미널 도구
|
|
"--nofollow-import-to=ansicon",
|
|
"--nofollow-import-to=blessed",
|
|
"--nofollow-import-to=jinxed",
|
|
"--nofollow-import-to=readchar",
|
|
"--nofollow-import-to=pyreadline",
|
|
"--nofollow-import-to=pyreadline3",
|
|
|
|
# 빌드/패키징 도구 (런타임에 불필요)
|
|
"--nofollow-import-to=cx_Freeze",
|
|
"--nofollow-import-to=pyinstaller",
|
|
"--nofollow-import-to=setuptools",
|
|
"--nofollow-import-to=wheel",
|
|
"--nofollow-import-to=build",
|
|
"--nofollow-import-to=poetry",
|
|
"--nofollow-import-to=poetry.core",
|
|
|
|
# 문서/마크다운 처리 (markdown은 사용하지만 일부는 제외)
|
|
"--nofollow-import-to=markdown2",
|
|
"--nofollow-import-to=docx2txt",
|
|
"--nofollow-import-to=python-docx",
|
|
"--nofollow-import-to=pdf2docx",
|
|
"--nofollow-import-to=PyPDF2",
|
|
"--nofollow-import-to=PyMuPDF",
|
|
|
|
# 기타 사용하지 않는 패키지
|
|
"--nofollow-import-to=Flask",
|
|
"--nofollow-import-to=flask-babel",
|
|
"--nofollow-import-to=Werkzeug",
|
|
"--nofollow-import-to=itsdangerous",
|
|
"--nofollow-import-to=Jinja2",
|
|
"--nofollow-import-to=MarkupSafe",
|
|
"--nofollow-import-to=openai", # 사용하지 않으면 제외
|
|
"--nofollow-import-to=opencv-contrib-python",
|
|
"--nofollow-import-to=opencv-python-headless",
|
|
"--nofollow-import-to=visualdl",
|
|
"--nofollow-import-to=nltk",
|
|
"--nofollow-import-to=numba",
|
|
"--nofollow-import-to=llvmlite",
|
|
"--nofollow-import-to=joblib",
|
|
"--nofollow-import-to=scikit-learn",
|
|
"--nofollow-import-to=networkx",
|
|
"--nofollow-import-to=contourpy",
|
|
"--nofollow-import-to=cycler",
|
|
"--nofollow-import-to=kiwisolver",
|
|
"--nofollow-import-to=pyparsing",
|
|
"--nofollow-import-to=python-dateutil",
|
|
"--nofollow-import-to=pytz",
|
|
"--nofollow-import-to=tzdata",
|
|
"--nofollow-import-to=imageio",
|
|
"--nofollow-import-to=tifffile",
|
|
"--nofollow-import-to=PyMatting",
|
|
"--nofollow-import-to=lmdb",
|
|
"--nofollow-import-to=rapidfuzz",
|
|
"--nofollow-import-to=stringzilla",
|
|
"--nofollow-import-to=simsimd",
|
|
|
|
# ImageProcessor3 관련 모듈 제외
|
|
"--nofollow-import-to=src.modules.image_processor3",
|
|
"--nofollow-import-to=src.modules.image_processor4",
|
|
"--nofollow-import-to=src.modules.image_worker",
|
|
"--nofollow-import-to=src.modules.image_worker_manager",
|
|
"--nofollow-import-to=src.modules.ocr_module",
|
|
"--nofollow-import-to=src.modules.mask_module_for_paddle",
|
|
"--nofollow-import-to=src.modules.text_rendering_module",
|
|
"--nofollow-import-to=src.modules.postImageManager",
|
|
"--nofollow-import-to=src.modules.request_inpaint",
|
|
"--nofollow-import-to=src.modules.migan_module",
|
|
"--nofollow-import-to=src.modules.background_removal_module",
|
|
"--nofollow-import-to=src.modules.bria_background_removal_module",
|
|
"--nofollow-import-to=src.modules.gemma_client",
|
|
"--nofollow-import-to=src.modules.onnx_ocr_module",
|
|
|
|
# 사용하지 않는 레거시 모듈 제외
|
|
"--nofollow-import-to=src.wh_Controller",
|
|
"--nofollow-import-to=src.wh_Controller2",
|
|
"--nofollow-import-to=src.wh_con",
|
|
"--nofollow-import-to=src.wh_search",
|
|
"--nofollow-import-to=src.wh_img_trans",
|
|
"--nofollow-import-to=whale_new",
|
|
"--nofollow-import-to=whale_test",
|
|
"--nofollow-import-to=whale_translator",
|
|
"--nofollow-import-to=whale_translator2",
|
|
|
|
# ===== 최적화 옵션 =====
|
|
"--remove-output", # 이전 빌드 결과 삭제
|
|
"--show-progress", # 진행 상황 표시
|
|
"--show-memory", # 메모리 사용량 표시
|
|
|
|
# ===== 추가 최적화 옵션 =====
|
|
# "--no-pyi-file", # Nuitka 2.8.9에서 지원하지 않음
|
|
|
|
# ===== 링킹 최적화 옵션 (대용량 빌드용) =====
|
|
"--lto=no", # LTO 비활성화 (링킹 시간 단축, 메모리 절약)
|
|
"--jobs=2", # 병렬 작업 수 제한 (메모리 부족 방지, 링킹 안정성 향상)
|
|
"--static-libpython=no", # 정적 링킹 비활성화 (빌드 시간 단축)
|
|
|
|
# ===== 메인 스크립트 =====
|
|
os.path.join(BASE_DIR, "main.py"),
|
|
]
|
|
|
|
return cmd
|
|
|
|
|
|
def copy_data_files():
|
|
"""데이터 파일들을 빌드 디렉토리로 복사"""
|
|
|
|
print("\n데이터 파일 복사 중...")
|
|
|
|
# 복사할 파일/디렉토리 목록 (소스, 대상)
|
|
data_files = [
|
|
# 메인 아이콘 (실행 파일 아이콘)
|
|
("Edit_PartTimer3.ico", "Edit_PartTimer3.ico"),
|
|
# 내부 아이콘
|
|
("src/Edit_PartTimer3.ico", "lib/src/Edit_PartTimer3.ico"),
|
|
|
|
# JSON 설정 파일
|
|
("kiprisCategories.json", "kiprisCategories.json"),
|
|
("src/keyword/kiprisCategories.json", "lib/src/keyword/kiprisCategories.json"),
|
|
("src/Percenty_SS_Code.json", "lib/src/Percenty_SS_Code.json"),
|
|
|
|
# 엑셀 파일
|
|
("퍼센티 다양한 카테고리 엑셀 수집(스스 기준).xlsx", "퍼센티 다양한 카테고리 엑셀 수집(스스 기준).xlsx"),
|
|
|
|
# 업데이터
|
|
("updateManager/updater.exe", "updater.exe"),
|
|
|
|
# VC++ 런타임 (시스템에서 복사)
|
|
("C:/Windows/System32/vcruntime140.dll", "vcruntime140.dll"),
|
|
("C:/Windows/System32/vcruntime140_1.dll", "vcruntime140_1.dll"),
|
|
("C:/Windows/System32/msvcp140.dll", "msvcp140.dll"),
|
|
("C:/Windows/System32/msvcp140_1.dll", "msvcp140_1.dll"),
|
|
("C:/Windows/System32/msvcp140_2.dll", "msvcp140_2.dll"),
|
|
("C:/Windows/System32/concrt140.dll", "concrt140.dll"),
|
|
("C:/Windows/System32/vcomp140.dll", "vcomp140.dll"),
|
|
]
|
|
|
|
# 복사할 디렉토리 목록
|
|
data_dirs = [
|
|
# 폰트
|
|
("src/modules/fonts", "lib/src/modules/fonts"),
|
|
|
|
# 브라우저 관련
|
|
("src/browsers/chromium-1200", "lib/src/browsers/chromium-1200"),
|
|
("src/browsers/extensions", "lib/src/browsers/extensions"),
|
|
]
|
|
|
|
# 파일 복사
|
|
for src, dest in data_files:
|
|
src_path = src if os.path.isabs(src) else os.path.join(BASE_DIR, src)
|
|
dest_path = os.path.join(DIST_DIR, dest)
|
|
|
|
if os.path.exists(src_path):
|
|
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
try:
|
|
shutil.copy2(src_path, dest_path)
|
|
print(f" [OK] 복사: {src} -> {dest}")
|
|
except Exception as e:
|
|
print(f" [ERROR] 실패: {src} - {e}")
|
|
else:
|
|
print(f" [WARN] 없음: {src}")
|
|
|
|
# 디렉토리 복사
|
|
for src, dest in data_dirs:
|
|
src_path = os.path.join(BASE_DIR, src)
|
|
dest_path = os.path.join(DIST_DIR, dest)
|
|
|
|
if os.path.exists(src_path):
|
|
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
try:
|
|
if os.path.exists(dest_path):
|
|
shutil.rmtree(dest_path)
|
|
shutil.copytree(src_path, dest_path)
|
|
print(f" [OK] 복사 (디렉토리): {src} -> {dest}")
|
|
except Exception as e:
|
|
print(f" [ERROR] 실패: {src} - {e}")
|
|
else:
|
|
print(f" [WARN] 없음: {src}")
|
|
|
|
print("데이터 파일 복사 완료!\n")
|
|
|
|
|
|
def copy_exe_to_dist():
|
|
"""Nuitka가 생성한 실행 파일을 최종 dist 폴더로 복사"""
|
|
main_dist_exe = os.path.join(OUTPUT_DIR, "main.dist", f"{__exe_name__}.exe")
|
|
final_exe_path = os.path.join(DIST_DIR, f"{__exe_name__}.exe")
|
|
|
|
if os.path.exists(main_dist_exe):
|
|
try:
|
|
# 대상 디렉토리가 없으면 생성
|
|
os.makedirs(os.path.dirname(final_exe_path), exist_ok=True)
|
|
shutil.copy2(main_dist_exe, final_exe_path)
|
|
print(f" [OK] 실행 파일 복사: {main_dist_exe} -> {final_exe_path}")
|
|
return True
|
|
except Exception as e:
|
|
print(f" [ERROR] 실행 파일 복사 실패: {e}")
|
|
return False
|
|
else:
|
|
print(f" [WARN] 실행 파일을 찾을 수 없습니다: {main_dist_exe}")
|
|
return False
|
|
|
|
|
|
def cleanup_unnecessary_files():
|
|
"""불필요한 파일 정리 (ImageProcessor3 관련 등)"""
|
|
|
|
print("\n불필요한 파일 정리 중...")
|
|
|
|
unnecessary_items = [
|
|
"lib/src/modules/briaaiModel",
|
|
"lib/src/modules/migan_onnx",
|
|
"lib/src/modules/modules",
|
|
"lib/src/modules/ocr_backends",
|
|
"lib/src/modules/onnx_ocr_module",
|
|
"lib/src/modules/output",
|
|
"lib/src/modules/outputs",
|
|
"lib/src/modules/PP_Models",
|
|
"lib/src/modules/rembg_models",
|
|
"lib/src/PP_Models",
|
|
"lib/paddle",
|
|
"lib/paddleocr",
|
|
"lib/onnxruntime",
|
|
"lib/skimage",
|
|
"lib/scipy",
|
|
]
|
|
|
|
removed_count = 0
|
|
total_size_saved = 0
|
|
|
|
for item in unnecessary_items:
|
|
target_path = os.path.join(DIST_DIR, item)
|
|
|
|
try:
|
|
if os.path.exists(target_path):
|
|
if os.path.isdir(target_path):
|
|
dir_size = sum(
|
|
os.path.getsize(os.path.join(dirpath, filename))
|
|
for dirpath, dirnames, filenames in os.walk(target_path)
|
|
for filename in filenames
|
|
)
|
|
total_size_saved += dir_size
|
|
shutil.rmtree(target_path)
|
|
print(f" [OK] 삭제: {item} ({dir_size / (1024*1024):.1f} MB)")
|
|
else:
|
|
file_size = os.path.getsize(target_path)
|
|
total_size_saved += file_size
|
|
os.remove(target_path)
|
|
print(f" [OK] 삭제: {item} ({file_size / (1024*1024):.2f} MB)")
|
|
removed_count += 1
|
|
except Exception as e:
|
|
print(f" [ERROR] 삭제 실패: {item} - {e}")
|
|
|
|
if removed_count > 0:
|
|
print(f"\n정리 완료: {removed_count}개 항목 삭제, 약 {total_size_saved / (1024*1024):.1f} MB 절약")
|
|
else:
|
|
print("정리할 항목이 없습니다.")
|
|
|
|
|
|
def generate_inno_setup_script():
|
|
"""Nuitka 빌드 결과물을 위한 Inno Setup 스크립트 생성"""
|
|
try:
|
|
import datetime
|
|
from updateManager.__version__ import (
|
|
__program_id__, __program_name__, __company_name__,
|
|
__exe_name__, __setup_name__, __icon_file__, __setup_output_dir__,
|
|
__version__, __description__, __copyright__
|
|
)
|
|
|
|
# 현재 날짜와 시간
|
|
now = datetime.datetime.now()
|
|
date_str = now.strftime("%Y%m%d_%H%M%S")
|
|
|
|
# 생성할 .iss 파일 이름
|
|
iss_filename = f"AutoPercenty_Nuitka_{date_str}.iss"
|
|
|
|
# Nuitka 빌드 결과물 경로 (cx_Freeze와 다른 경로)
|
|
# 상대 경로로 변환 (프로젝트 루트 기준) - Inno Setup은 프로젝트 루트에서 실행됨
|
|
nuitka_build_path_rel = os.path.relpath(DIST_DIR, BASE_DIR).replace("\\", "\\\\")
|
|
|
|
# 템플릿 작성 (cx_Freeze 버전과 동일하지만 소스 경로만 변경)
|
|
iss_template = fr"""; AutoPercenty3 Inno Setup Script (Nuitka Build)
|
|
; 이 스크립트는 Nuitka로 빌드된 결과물이 있는 "{DIST_DIR}" 폴더를 기반으로 인스톨러를 제작합니다.
|
|
; {date_str}에 생성됨
|
|
|
|
#define AppId "{__program_id__}"
|
|
#define MyAppName "{__title__}"
|
|
#define MyAppVersion "{__version__}"
|
|
#define MyAppPublisher "{__company_name__}"
|
|
#define MyAppProgramName "{__program_name__}"
|
|
#define MyAppDescription "{__description__}"
|
|
#define MyAppCopyright "{__copyright__}"
|
|
#define MyAppExeName "{__exe_name__}"
|
|
#define MySetupName "{__setup_name__}"
|
|
#define MySetupIcon "{__icon_file__}"
|
|
#define MySetupOutputDir "{__setup_output_dir__}"
|
|
#define NuitkaBuildPath "{nuitka_build_path_rel}"
|
|
|
|
[Setup]
|
|
; 기본 설정
|
|
AppId={{#AppId}}
|
|
AppName={{#MyAppProgramName}}
|
|
AppVersion={{#MyAppVersion}}
|
|
AppPublisher={{#MyAppPublisher}}
|
|
DefaultDirName={{autopf}}\{{#MyAppName}}
|
|
DefaultGroupName={{#MyAppPublisher}}
|
|
OutputDir={{#MySetupOutputDir}}
|
|
OutputBaseFilename={{#MySetupName}}
|
|
SetupIconFile={{#MySetupIcon}}
|
|
Compression=lzma
|
|
SolidCompression=yes
|
|
|
|
; 업데이트 관련 설정 - 권한 최적화
|
|
PrivilegesRequired=admin
|
|
PrivilegesRequiredOverridesAllowed=dialog
|
|
UpdateUninstallLogAppName=yes
|
|
AppMutex={{#MyAppName}}
|
|
CloseApplications=yes
|
|
RestartApplications=no
|
|
|
|
; 보안 및 호환성 설정
|
|
ArchitecturesAllowed=x64
|
|
ArchitecturesInstallIn64BitMode=x64
|
|
AllowNoIcons=yes
|
|
|
|
; 버전 정보
|
|
VersionInfoVersion={{#MyAppVersion}}
|
|
VersionInfoCompany={{#MyAppPublisher}}
|
|
VersionInfoDescription={{#MyAppDescription}}
|
|
VersionInfoCopyright={{#MyAppCopyright}}
|
|
VersionInfoProductName={{#MyAppProgramName}}
|
|
VersionInfoProductVersion={{#MyAppVersion}}
|
|
|
|
[Languages]
|
|
Name: "korean"; MessagesFile: "compiler:Languages\Korean.isl"
|
|
|
|
[Tasks]
|
|
Name: "desktopicon"; Description: "{{cm:CreateDesktopIcon}}"; GroupDescription: "{{cm:AdditionalIcons}}"
|
|
|
|
[Dirs]
|
|
; 설치 시 {{app}}\logs 폴더를 생성하고,
|
|
; Users 그룹에 'modify' 권한(=쓰기 가능)을 부여
|
|
Name: "{{app}}\logs"; Permissions: users-modify
|
|
; 설치 시 {{app}}\user_data 폴더를 생성하고,
|
|
; Users 그룹에 'modify' 권한(=쓰기 가능)을 부여
|
|
Name: "{{app}}\user_data"; Permissions: users-modify
|
|
; Playwright 브라우저 폴더를 Program Files 내부에 생성
|
|
Name: "{{app}}\lib\src\browsers\chromium-1200"; Permissions: users-modify
|
|
; Playwright 브라우저 사용자폴더를 Program Files 내부에 생성
|
|
Name: "{{app}}\lib\src\browsers\user_data"; Permissions: users-modify
|
|
|
|
[Files]
|
|
; Nuitka 빌드 결과물 전체 설치 (항상 덮어쓰기)
|
|
Source: "{{#NuitkaBuildPath}}\*"; DestDir: "{{app}}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
|
; VC++ 재배포 패키지 파일을 임시 폴더({{tmp}})에 복사
|
|
Source: "VC_redist.x64.exe"; DestDir: "{{tmp}}"; Flags: deleteafterinstall
|
|
|
|
[Registry]
|
|
; Playwright 브라우저 경로를 Program Files 내부로 설정
|
|
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "PLAYWRIGHT_BROWSERS_PATH"; ValueData: "{{app}}\lib\src\browsers"; Flags: preservestringtype
|
|
|
|
[Icons]
|
|
; 시작 메뉴 바로가기
|
|
Name: "{{group}}\{{#MyAppProgramName}}"; Filename: "{{app}}\{{#MyAppExeName}}.exe"
|
|
; 바탕화면 바로가기
|
|
Name: "{{autodesktop}}\{{#MyAppProgramName}}"; Filename: "{{app}}\{{#MyAppExeName}}.exe"; Tasks: desktopicon
|
|
; 프로그램 제거 바로가기
|
|
Name: "{{group}}\{{#MyAppProgramName}} 제거"; Filename: "{{uninstallexe}}"
|
|
|
|
[Run]
|
|
; VC++ 재배포 패키지 설치 (필요할 경우)
|
|
Filename: "{{tmp}}\VC_redist.x64.exe"; Parameters: "/install /passive /norestart"; StatusMsg: "VC++ 재배포 패키지 설치 중..."; Check: NeedsVCredist
|
|
; 설치 후 프로그램 실행 (원할 경우)
|
|
Filename: "{{app}}\{{#MyAppExeName}}.exe"; Description: "{{cm:LaunchProgram,{{#MyAppProgramName}}}}"; Flags: nowait postinstall skipifsilent
|
|
|
|
[Code]
|
|
function CompareVersion(V1, V2: string): Integer;
|
|
var
|
|
P1, P2, N1, N2: Integer;
|
|
begin
|
|
P1 := 1;
|
|
P2 := 1;
|
|
Result := 0;
|
|
while (Result = 0) and ((P1 <= Length(V1)) or (P2 <= Length(V2))) do begin
|
|
while (P1 <= Length(V1)) and (V1[P1] = '.') do Inc(P1);
|
|
while (P2 <= Length(V2)) and (V2[P2] = '.') do Inc(P2);
|
|
if (P1 <= Length(V1)) and (P2 <= Length(V2)) then begin
|
|
N1 := 0; while (P1 <= Length(V1)) and (V1[P1] >= '0') and (V1[P1] <= '9') do begin N1 := N1 * 10 + Ord(V1[P1]) - Ord('0'); Inc(P1); end;
|
|
N2 := 0; while (P2 <= Length(V2)) and (V2[P2] >= '0') and (V2[P2] <= '9') do begin N2 := N2 * 10 + Ord(V2[P2]) - Ord('0'); Inc(P2); end;
|
|
if N1 < N2 then Result := -1 else if N1 > N2 then Result := 1;
|
|
end else begin
|
|
if P1 <= Length(V1) then Result := 1 else if P2 <= Length(V2) then Result := -1;
|
|
end;
|
|
while (P1 <= Length(V1)) and (V1[P1] <> '.') do Inc(P1);
|
|
while (P2 <= Length(V2)) and (V2[P2] <> '.') do Inc(P2);
|
|
end;
|
|
end;
|
|
|
|
// 파일 또는 폴더 복사 함수
|
|
procedure CopyDir(const SourcePath, DestPath: string);
|
|
var
|
|
FindRec: TFindRec;
|
|
SourceFilePath: string;
|
|
DestFilePath: string;
|
|
begin
|
|
ForceDirectories(DestPath);
|
|
|
|
if FindFirst(SourcePath + '\*', FindRec) then
|
|
begin
|
|
try
|
|
repeat
|
|
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
|
|
begin
|
|
SourceFilePath := SourcePath + '\' + FindRec.Name;
|
|
DestFilePath := DestPath + '\' + FindRec.Name;
|
|
|
|
if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then
|
|
begin
|
|
if FileCopy(SourceFilePath, DestFilePath, False) then
|
|
Log('파일 복사 성공: ' + SourceFilePath + ' -> ' + DestFilePath)
|
|
else
|
|
Log('파일 복사 실패: ' + SourceFilePath);
|
|
end
|
|
else
|
|
CopyDir(SourceFilePath, DestFilePath);
|
|
end;
|
|
until not FindNext(FindRec);
|
|
finally
|
|
FindClose(FindRec);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// 디렉토리 삭제 함수
|
|
procedure DeleteDir(const DirPath: string);
|
|
var
|
|
FindRec: TFindRec;
|
|
FilePath: string;
|
|
begin
|
|
if not DirExists(DirPath) then Exit;
|
|
|
|
if FindFirst(DirPath + '\*', FindRec) then
|
|
begin
|
|
try
|
|
repeat
|
|
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
|
|
begin
|
|
FilePath := DirPath + '\' + FindRec.Name;
|
|
|
|
if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then
|
|
begin
|
|
if DeleteFile(FilePath) then
|
|
Log('파일 삭제 성공: ' + FilePath)
|
|
else
|
|
Log('파일 삭제 실패: ' + FilePath);
|
|
end
|
|
else
|
|
DeleteDir(FilePath);
|
|
end;
|
|
until not FindNext(FindRec);
|
|
finally
|
|
FindClose(FindRec);
|
|
end;
|
|
end;
|
|
|
|
if RemoveDir(DirPath) then
|
|
Log('디렉토리 삭제 성공: ' + DirPath)
|
|
else
|
|
Log('디렉토리 삭제 실패: ' + DirPath);
|
|
end;
|
|
|
|
// 프로그램 실행 여부 확인
|
|
function IsAppRunning(const FileName: string): Boolean;
|
|
var
|
|
Handle: THandle;
|
|
begin
|
|
Handle := FindWindowByWindowName('{{#MyAppProgramName}}');
|
|
Result := (Handle <> 0);
|
|
end;
|
|
|
|
// 프로그램 종료
|
|
procedure CloseApplication(const FileName: string);
|
|
var
|
|
Handle: THandle;
|
|
begin
|
|
Handle := FindWindowByWindowName('{{#MyAppProgramName}}');
|
|
if Handle <> 0 then
|
|
begin
|
|
PostMessage(Handle, 18, 0, 0);
|
|
Sleep(1000);
|
|
end;
|
|
end;
|
|
|
|
// VC++ 재배포 패키지 필요 여부 확인
|
|
function NeedsVCredist: Boolean;
|
|
begin
|
|
if RegKeyExists(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64') then
|
|
Result := False
|
|
else
|
|
Result := True;
|
|
end;
|
|
|
|
// 설치 완료 후 실행 여부 확인
|
|
function InitializeFinish(): Boolean;
|
|
var
|
|
ResultCode: Integer;
|
|
begin
|
|
Result := True;
|
|
if MsgBox('설치가 완료되었습니다. 프로그램을 실행하시겠습니까?' + #13#10 +
|
|
'(실행 시 서버와 동기화하여 설정이 업데이트됩니다)',
|
|
mbConfirmation, MB_YESNO) = IDYES then
|
|
begin
|
|
Exec(ExpandConstant('{{app}}\{{#MyAppExeName}}.exe'), '', '', SW_SHOW, ewNoWait, ResultCode);
|
|
end;
|
|
end;
|
|
|
|
function InitializeSetup(): Boolean;
|
|
var
|
|
OldVersion: String;
|
|
NewVersion: String;
|
|
OldAppPath: String;
|
|
UserDataSourcePath, UserDataBackupPath: String;
|
|
ResultCode: Integer;
|
|
begin
|
|
Result := True;
|
|
NewVersion := '{{#MyAppVersion}}';
|
|
UserDataBackupPath := ExpandConstant('{{tmp}}\user_data_backup');
|
|
|
|
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{#MyAppName}}_is1',
|
|
'DisplayVersion', OldVersion) then
|
|
begin
|
|
if CompareVersion(OldVersion, NewVersion) >= 0 then
|
|
begin
|
|
MsgBox('현재 설치된 버전(' + OldVersion + ')이 이 설치 프로그램의 버전(' +
|
|
NewVersion + ')과 같거나 더 높습니다.' + #13#10 +
|
|
'설치를 계속할 수 없습니다.', mbInformation, MB_OK);
|
|
Result := False;
|
|
exit;
|
|
end;
|
|
|
|
if CompareVersion(OldVersion, NewVersion) < 0 then
|
|
begin
|
|
Log('업데이트 설치 진행: ' + OldVersion + ' -> ' + NewVersion);
|
|
|
|
if IsAppRunning('{{#MyAppExeName}}.exe') then
|
|
begin
|
|
if MsgBox('프로그램을 업데이트하기 위해 실행 중인 프로그램을 종료해야 합니다.' + #13#10 +
|
|
'계속하시겠습니까?', mbConfirmation, MB_YESNO) = IDNO then
|
|
begin
|
|
Result := False;
|
|
exit;
|
|
end;
|
|
CloseApplication('{{#MyAppExeName}}.exe');
|
|
Sleep(2000);
|
|
|
|
Log('프로세스 강제 종료 시도: {{#MyAppExeName}}.exe');
|
|
Exec('taskkill', '/f /im {{#MyAppExeName}}.exe /t', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
|
Sleep(3000);
|
|
end;
|
|
|
|
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{#MyAppName}}_is1',
|
|
'InstallLocation', OldAppPath) then
|
|
begin
|
|
Log('기존 설치 경로: ' + OldAppPath);
|
|
|
|
UserDataSourcePath := OldAppPath + '\lib\src\user_data';
|
|
if DirExists(UserDataSourcePath) then
|
|
begin
|
|
Log('사용자 데이터 백업 중: ' + UserDataSourcePath + ' -> ' + UserDataBackupPath);
|
|
ForceDirectories(UserDataBackupPath);
|
|
CopyDir(UserDataSourcePath, UserDataBackupPath);
|
|
end
|
|
else
|
|
begin
|
|
Log('사용자 데이터 폴더가 존재하지 않음: ' + UserDataSourcePath);
|
|
end;
|
|
|
|
if DirExists(OldAppPath) then
|
|
begin
|
|
Log('기존 설치 폴더 삭제 중: ' + OldAppPath);
|
|
DeleteDir(OldAppPath);
|
|
Sleep(2000);
|
|
if DirExists(OldAppPath) then
|
|
begin
|
|
Log('설치 폴더 삭제 재시도 (Windows 명령어 사용): ' + OldAppPath);
|
|
Exec('cmd.exe', '/c rmdir /s /q "' + OldAppPath + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
|
Sleep(2000);
|
|
end;
|
|
|
|
if DirExists(OldAppPath) then
|
|
begin
|
|
Log('설치 폴더 삭제 3차 시도 (PowerShell 사용): ' + OldAppPath);
|
|
Exec('powershell.exe', '-Command "Remove-Item -Path ''' + OldAppPath + ''' -Recurse -Force -ErrorAction SilentlyContinue"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
|
Sleep(2000);
|
|
end;
|
|
|
|
if DirExists(OldAppPath) then
|
|
begin
|
|
Log('경고: 설치 폴더 삭제 실패: ' + OldAppPath);
|
|
if MsgBox('기존 설치 폴더를 완전히 삭제하지 못했습니다.' + #13#10 +
|
|
'이전 버전의 파일들이 남아있어 새 버전과 충돌할 수 있습니다.' + #13#10 + #13#10 +
|
|
'폴더: ' + OldAppPath + #13#10 + #13#10 +
|
|
'설치를 계속하시겠습니까?',
|
|
mbConfirmation, MB_YESNO) = IDNO then
|
|
begin
|
|
Result := False;
|
|
exit;
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
Log('기존 설치 폴더 삭제 완료: ' + OldAppPath);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure CurStepChanged(CurStep: TSetupStep);
|
|
var
|
|
UserDataBackupPath, UserDataDestPath: String;
|
|
begin
|
|
if CurStep = ssPostInstall then
|
|
begin
|
|
UserDataBackupPath := ExpandConstant('{{tmp}}\user_data_backup');
|
|
UserDataDestPath := ExpandConstant('{{app}}\lib\src\user_data');
|
|
|
|
if DirExists(UserDataBackupPath) then
|
|
begin
|
|
Log('사용자 데이터 복원 중: ' + UserDataBackupPath + ' -> ' + UserDataDestPath);
|
|
ForceDirectories(UserDataDestPath);
|
|
CopyDir(UserDataBackupPath, UserDataDestPath);
|
|
Log('사용자 데이터 복원 완료');
|
|
end;
|
|
end;
|
|
end;
|
|
"""
|
|
|
|
# .iss 파일 저장
|
|
with open(iss_filename, 'w', encoding='utf-8') as f:
|
|
f.write(iss_template)
|
|
|
|
print(f"\n[OK] Inno Setup 스크립트 생성 완료: {iss_filename}")
|
|
|
|
# 매니페스트 파일도 생성
|
|
manifest = {
|
|
"app_id": __program_id__,
|
|
"app_name": __program_name__,
|
|
"publisher": __company_name__,
|
|
"appexe": f"{__exe_name__}.exe",
|
|
"default_dirname": __title__,
|
|
"version": __version__
|
|
}
|
|
|
|
out_dir = __setup_output_dir__
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
manifest_path = os.path.join(out_dir, f"{__setup_name__}.manifest.json")
|
|
|
|
import json
|
|
with open(manifest_path, "w", encoding="utf-8") as mf:
|
|
json.dump(manifest, mf, ensure_ascii=False, indent=2)
|
|
|
|
print(f"[OK] 매니페스트 생성: {manifest_path}")
|
|
|
|
return iss_filename
|
|
|
|
except Exception as e:
|
|
print(f"[ERROR] Inno Setup 스크립트 생성 중 오류: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return None
|
|
|
|
|
|
def main():
|
|
print("=" * 60)
|
|
print(f" Nuitka 빌드 시작 - {__title__} v{__version__}")
|
|
print("=" * 60)
|
|
print("\n[주의] 첫 빌드 시 C 컴파일러(MinGW64) 다운로드가 필요할 수 있습니다.")
|
|
print("[주의] 빌드 시간: 약 10-30분 소요 (PC 사양에 따라 다름)\n")
|
|
|
|
# Nuitka 설치 확인
|
|
try:
|
|
import nuitka
|
|
# Nuitka 버전 확인 (다양한 방법 시도)
|
|
try:
|
|
version = nuitka.__version__
|
|
except AttributeError:
|
|
try:
|
|
import nuitka.Version
|
|
version = nuitka.Version.getNuitkaVersion()
|
|
except (AttributeError, ImportError):
|
|
try:
|
|
import pkg_resources
|
|
version = pkg_resources.get_distribution('nuitka').version
|
|
except:
|
|
version = "알 수 없음"
|
|
print(f"[OK] Nuitka 버전: {version}")
|
|
except ImportError:
|
|
print("[ERROR] Nuitka가 설치되어 있지 않습니다.")
|
|
print(" 설치 명령어: pip install nuitka ordered-set zstandard")
|
|
sys.exit(1)
|
|
|
|
# 빌드 명령어 생성 및 실행
|
|
cmd = get_nuitka_command()
|
|
|
|
print("\n빌드 명령어:")
|
|
print(" ".join(cmd[:10]) + " ...")
|
|
print("\n빌드 시작...\n")
|
|
print("[참고] 링킹 단계는 시간이 오래 걸릴 수 있습니다 (5700+ 파일)...")
|
|
print("[참고] 진행 상황은 실시간으로 표시됩니다. 몇십 분이 걸릴 수 있습니다.\n")
|
|
|
|
# 빌드 실행 (실시간 출력)
|
|
# subprocess.run 대신 Popen을 사용하여 실시간 출력 표시
|
|
process = subprocess.Popen(
|
|
cmd,
|
|
cwd=BASE_DIR,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
encoding='utf-8',
|
|
errors='replace',
|
|
bufsize=1 # 라인 버퍼링
|
|
)
|
|
|
|
# 실시간 출력 표시
|
|
output_lines = []
|
|
last_progress_time = None
|
|
|
|
try:
|
|
for line in iter(process.stdout.readline, ''):
|
|
if line:
|
|
line = line.rstrip()
|
|
output_lines.append(line)
|
|
|
|
# 진행 상황이 있는 줄만 표시 (너무 많은 출력 방지)
|
|
if any(keyword in line.lower() for keyword in [
|
|
'nuitka', 'compiling', 'linking', 'progress',
|
|
'error', 'warning', 'fatal', 'complete',
|
|
'scons', 'backend', '%'
|
|
]):
|
|
print(line)
|
|
last_progress_time = time.time()
|
|
# 10초마다 한 번씩 출력 (진행 중임을 알림)
|
|
elif last_progress_time and (time.time() - last_progress_time) > 10:
|
|
print(f"[진행 중... {time.strftime('%H:%M:%S')}]")
|
|
last_progress_time = time.time()
|
|
|
|
process.wait()
|
|
result = type('obj', (object,), {
|
|
'returncode': process.returncode,
|
|
'stdout': '\n'.join(output_lines)
|
|
})()
|
|
except KeyboardInterrupt:
|
|
print("\n\n빌드가 사용자에 의해 중단되었습니다.")
|
|
process.terminate()
|
|
process.wait()
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"\n\n빌드 실행 중 오류 발생: {e}")
|
|
if process.poll() is None:
|
|
process.terminate()
|
|
process.wait()
|
|
sys.exit(1)
|
|
|
|
if result.returncode == 0:
|
|
print("\n" + "=" * 60)
|
|
print(" Nuitka 컴파일 완료!")
|
|
print("=" * 60)
|
|
|
|
# 데이터 파일 복사
|
|
copy_data_files()
|
|
|
|
# 실행 파일을 최종 dist 폴더로 복사
|
|
print("\n실행 파일 복사 중...")
|
|
if copy_exe_to_dist():
|
|
print("실행 파일 복사 완료!\n")
|
|
else:
|
|
print("실행 파일 복사 실패!\n")
|
|
|
|
# 불필요한 파일 정리
|
|
cleanup_unnecessary_files()
|
|
|
|
# Inno Setup 스크립트 생성
|
|
print("\nInno Setup 스크립트 생성 중...")
|
|
iss_file = generate_inno_setup_script()
|
|
if iss_file:
|
|
print(f"\n[OK] Inno Setup 스크립트 생성 완료: {iss_file}")
|
|
else:
|
|
print("\n[WARN] Inno Setup 스크립트 생성 실패")
|
|
|
|
print("\n" + "=" * 60)
|
|
print(f" 빌드 완료!")
|
|
print(f" 출력 경로: {DIST_DIR}")
|
|
print(f" 실행 파일: {os.path.join(DIST_DIR, __exe_name__ + '.exe')}")
|
|
if iss_file:
|
|
print(f" Inno Setup 스크립트: {iss_file}")
|
|
print("=" * 60)
|
|
else:
|
|
print("\n" + "=" * 60)
|
|
print(" 빌드 실패!")
|
|
print("=" * 60)
|
|
print("\n[문제 해결 방법]")
|
|
print("1. 메모리 부족: 다른 프로그램을 종료하고 다시 시도")
|
|
print("2. 링킹 타임아웃: 빌드 스크립트의 --jobs 값을 더 낮춰보세요 (현재: 4)")
|
|
print("3. 디스크 공간: 빌드 디렉토리에 충분한 공간이 있는지 확인")
|
|
print("4. Visual Studio: C++ 빌드 도구가 제대로 설치되어 있는지 확인")
|
|
print("\n[에러 로그 확인]")
|
|
print(f" 빌드 디렉토리: {OUTPUT_DIR}")
|
|
print(" 상세 로그는 위의 출력을 확인하세요.\n")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|