AutoPercenty3/build_nuitka.py

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()