""" 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 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", # ===== 제외할 패키지 (용량 절감 & 불필요) ===== "--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", # 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", # ===== 최적화 옵션 ===== "--remove-output", # 이전 빌드 결과 삭제 "--show-progress", # 진행 상황 표시 "--show-memory", # 메모리 사용량 표시 # ===== 메인 스크립트 ===== 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 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: 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 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") result = subprocess.run(cmd, cwd=BASE_DIR) 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빌드 실패!") sys.exit(1) if __name__ == "__main__": main()