704 lines
29 KiB
Python
704 lines
29 KiB
Python
import sys
|
|
import os
|
|
import glob
|
|
import shutil
|
|
import subprocess
|
|
import logging
|
|
import time
|
|
from tqdm import tqdm
|
|
|
|
import requests
|
|
|
|
from cx_Freeze import setup, Executable
|
|
from setuptools import find_packages
|
|
from setuptools.command.build_ext import build_ext
|
|
from cx_Freeze.command.build_exe import build_exe as _build_exe
|
|
|
|
from setuptools import setup as cy_setup, Extension
|
|
from Cython.Build import cythonize
|
|
|
|
|
|
|
|
# 버전 및 메타데이터 가져오기
|
|
from updateManager.__version__ import (
|
|
__title__, __version__, __description__, __author__,
|
|
__author_email__, __license__, __install_requires__
|
|
)
|
|
|
|
# ============================================================================
|
|
# [Configuration] 0. Gokapi 서버 설정 (업로드를 위해 필수)
|
|
# ============================================================================
|
|
GOKAPI_URL = "https://go.wrmc.cc" # 사용자님의 Gokapi 주소로 변경하세요
|
|
GOKAPI_APIKEY = "Pncy6s7pyi61rtUsV3kUwfPtOvU93I " # Gokapi 관리자 페이지에서 발급받은 API 키
|
|
|
|
# ============================================================================
|
|
# [Configuration] 설정 및 상수 정의
|
|
# ============================================================================
|
|
|
|
# 1. Cython으로 컴파일할 모듈 목록
|
|
CYTHON_MODULES = [
|
|
# "browser_control",
|
|
"locatorManager_by_SP",
|
|
"src/contents/details",
|
|
"src/contents/option",
|
|
"src/contents/price",
|
|
"src/contents/tags",
|
|
"src/contents/thumb",
|
|
"src/contents/titleGenerator",
|
|
"src/img_module/image_processor_dialog",
|
|
"src/img_module/image_processor_manager",
|
|
"src/modules/image_worker_client",
|
|
"src/lens/aliprice_lens_client",
|
|
"src/lens/naver_lens_adapter",
|
|
"src/lens/naver_lens_client",
|
|
"src/lens/naver_lens_parser",
|
|
"src/sp_manager",
|
|
"src/titleManager/gpt_client",
|
|
"src/titleManager/grok_client",
|
|
"src/titleManager/naverAPI",
|
|
"src/titleManager/naver_parser",
|
|
"src/titleManager/sp_ForbiddenM",
|
|
"src/translator/chinese_dict_manager",
|
|
"src/translator/papago_translator",
|
|
"src/AI_Module/ai_client",
|
|
"src/AI_Module/baseAIProvider",
|
|
"src/AI_Module/gpt_provider",
|
|
"src/AI_Module/grok_provider",
|
|
"src/AI_Module/openRouter_provider",
|
|
"src/AI_Module/gemini_provider",
|
|
]
|
|
|
|
# 2. 빌드 후 제거할 불필요한 파일/폴더 목록 (ImageProcessor3 관련 등)
|
|
CLEANUP_TARGETS = [
|
|
# 디렉토리
|
|
"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",
|
|
# 파일
|
|
"lib/src/modules/migan_traced.pt",
|
|
"lib/src/modules/image_processor3.py", "lib/src/modules/image_processor4.py",
|
|
"lib/src/modules/image_worker.py", "lib/src/modules/image_worker_manager.py",
|
|
"lib/src/modules/ocr_module.py", "lib/src/modules/mask_module_for_paddle.py",
|
|
"lib/src/modules/text_rendering_module.py", "lib/src/modules/postImageManager.py",
|
|
"lib/src/modules/request_inpaint.py", "lib/src/modules/migan_module.py",
|
|
"lib/src/modules/background_removal_module.py", "lib/src/modules/bria_background_removal_module.py",
|
|
"lib/src/modules/gemma_client.py",
|
|
# 컴파일된 잔재 (.pyc)
|
|
"lib/src/modules/image_processor3.pyc", "lib/src/modules/image_processor4.pyc",
|
|
"lib/src/modules/image_worker.pyc", "lib/src/modules/image_worker_manager.pyc",
|
|
"lib/src/modules/ocr_module.pyc", "lib/src/modules/mask_module_for_paddle.pyc",
|
|
"lib/src/modules/text_rendering_module.pyc", "lib/src/modules/postImageManager.pyc",
|
|
"lib/src/modules/request_inpaint.pyc", "lib/src/modules/migan_module.pyc",
|
|
"lib/src/modules/background_removal_module.pyc", "lib/src/modules/bria_background_removal_module.pyc",
|
|
"lib/src/modules/gemma_client.pyc",
|
|
|
|
# [PySide6 다이어트]
|
|
# 1. 번역 파일 (한국어/영어 외 제거하려면 통째로 지우거나 선별)
|
|
# 보통 프로그램 내 텍스트가 한글이라면 Qt 자체 번역 파일은 없어도 됨
|
|
"lib/PySide6/translations",
|
|
|
|
# 2. 불필요한 플러그인 (사용 안 하는 기능)
|
|
# 주의: 'platforms', 'styles', 'imageformats'는 지우면 안 됩니다!
|
|
"lib/PySide6/plugins/sqldrivers", # DB 안 쓰면 삭제
|
|
"lib/PySide6/plugins/multimedia", # 오디오/비디오 안 쓰면 삭제
|
|
"lib/PySide6/plugins/positioning", # GPS 안 쓰면 삭제
|
|
"lib/PySide6/plugins/sensors", # 센서 안 쓰면 삭제
|
|
"lib/PySide6/plugins/texttospeech", # TTS 안 쓰면 삭제
|
|
"lib/PySide6/plugins/webview", # WebView 안 쓰면 삭제 (중요)
|
|
|
|
# 3. Qt Quick / QML 관련 (위젯 기반 앱이면 필요 없음)
|
|
"lib/PySide6/qml",
|
|
"lib/PySide6/Qt/qml",
|
|
|
|
# 4. 개발용/디자이너용 도구 (배포 시 불필요)
|
|
"lib/PySide6/designer",
|
|
"lib/PySide6/scripts",
|
|
"lib/PySide6/examples",
|
|
"lib/PySide6/glue",
|
|
"lib/PySide6/include",
|
|
|
|
# 5. 대용량 DLL (사용 여부 확인 필요)
|
|
# OpenGL 소프트웨어 렌더러 (그래픽 카드 없는 PC용, 보통 없어도 됨)
|
|
"lib/PySide6/opengl32sw.dll",
|
|
# QtWebEngine (크롬 브라우저 내장). 만약 Selenium/Playwright만 쓰고
|
|
# Qt 위젯 내에서 브라우저를 띄우지 않는다면 지워도 됨 (엄청 큼!)
|
|
"lib/PySide6/Qt6WebEngineCore.dll",
|
|
"lib/PySide6/Qt6WebEngineCore.pyd",
|
|
"lib/PySide6/Qt6WebEngineWidgets.dll",
|
|
|
|
]
|
|
|
|
# 3. 소스 패키징에서 강제로 제외할 패키지들
|
|
BASE_EXCLUDES = [
|
|
'tkinter', 'PyQt4', 'PyQt5', 'AppKit', 'Foundation', 'IPython',
|
|
'OpenSSL', 'curses', 'test', 'matplotlib', 'asyncpg',
|
|
'importlib._bootstrap', 'importlib.machinery',
|
|
'pytest', 'hypothesis', 'mypy', 'coverage', 'tox',
|
|
'sympy', 'mpmath', 'gmpy2',
|
|
'paddle', 'paddleocr', 'paddlehub', 'onnxruntime', 'onnx',
|
|
'skimage', 'scikit-image', 'pyclipper', 'shapely',
|
|
'scipy', 'imgaug', 'albumentations', 'torch', 'tensorflow', 'keras',
|
|
'src.modules.image_processor3', 'src.modules.image_processor4',
|
|
'src.modules.image_worker', 'src.modules.image_worker_manager',
|
|
'src.modules.ocr_module', 'src.modules.mask_module_for_paddle',
|
|
'src.modules.text_rendering_module', 'src.modules.postImageManager',
|
|
'src.modules.request_inpaint', 'src.modules.migan_module',
|
|
'src.modules.background_removal_module', 'src.modules.bria_background_removal_module',
|
|
'src.modules.gemma_client', 'src.modules.onnx_ocr_module',
|
|
]
|
|
|
|
# 4. 강제 포함 패키지
|
|
BASE_INCLUDES = [
|
|
'browser_control',
|
|
'loggerModule', 'toggleSwitch',
|
|
'src.cmdDiag', 'src.inputDiag', 'src.keyword', 'src.priceSetDiag',
|
|
# 'src.titleManager',
|
|
# 'src.titleManager.naver_parser', 'src.titleManager.naverAPI', 'src.titleManager.gpt_client', 'src.titleManager.grok_client',
|
|
# 'src.translator',
|
|
# 'src.translator.chinese_dict_manager', 'src.translator.papago_translator',
|
|
'src.discord_manager', 'src.unwantedDiag', 'src.unwantedDiag.unwanted_words_dialog',
|
|
'src.logDialog', 'src.logDialog.log_dialog', 'src.logDialog.log_filter',
|
|
'src.modules.settings_manager',
|
|
'src.modules.gpu_status_checker', 'src.modules.gpu_utils', 'src.gpuDiag',
|
|
# 'src.modules.image_worker_client', 'src.img_module.image_processor_manager', 'src.img_module.image_processor_dialog',
|
|
'src.modules.fonts.fontSelectDialog',
|
|
# 'src.lens.naver_lens_client', 'src.lens.naver_lens_parser', 'src.lens.naver_lens_adapter', 'src.lens.aliprice_lens_client',
|
|
'translatepy', 'translatepy.translators', 'translatepy.translators.google',
|
|
'PIL', 'PIL.Image', 'PIL.ImageOps', 'PIL.ImageEnhance', 'PIL.ImageFilter', 'PIL.features', 'cv2',
|
|
'numpy', 'numpy.core', 'numpy.random',
|
|
'supabase', 'gotrue', 'storage3', 'postgrest', 'supafunc', 'realtime',
|
|
'pydantic', 'pydantic_core',
|
|
'json', 'json.encoder', 'json.decoder', 'json.scanner',
|
|
'httpx', 'httpx.__version__', 'httpx._models', 'httpx._client', 'httpx._config',
|
|
'curl_cffi', 'curl_cffi.requests',
|
|
'pathlib',
|
|
'shiboken6',
|
|
'greenlet',
|
|
# 'playwright','playwright.async_api', 'playwright._impl',
|
|
'PySide6.QtCore', 'PySide6.QtGui', 'PySide6.QtWidgets', 'PySide6.QtNetwork',
|
|
'requests',
|
|
'pandas',
|
|
'sqlite3',
|
|
'packaging',
|
|
'markdown',
|
|
'pyperclip',
|
|
'psutil',
|
|
'comtypes',
|
|
'comtypes.stream',
|
|
'win32com',
|
|
'win32com.client',
|
|
'win32com.server',
|
|
'pythoncom',
|
|
]
|
|
|
|
|
|
# ============================================================================
|
|
# [Helper Functions] 로직 분리
|
|
# ============================================================================
|
|
|
|
def run_setup_clean():
|
|
"""
|
|
기존 빌드찌꺼기 제거
|
|
"""
|
|
print("\n>>> [Cleaner] 기존 빌드찌꺼기 제거 시작...")
|
|
subprocess.run(["python", "setup_clean.py"])
|
|
print(">>> [Cleaner] 기존 빌드찌꺼기 제거 완료!\n")
|
|
|
|
|
|
def run_cython_build():
|
|
"""
|
|
setup_cython.py를 별도로 실행하지 않고,
|
|
여기서 직접 Cython 빌드를 수행합니다. (경로 꼬임 방지)
|
|
"""
|
|
print("\n>>> [Cython] 내부 빌드 프로세스 시작...")
|
|
|
|
extensions = []
|
|
|
|
# 전역변수 CYTHON_MODULES 리스트를 사용
|
|
for file_path in CYTHON_MODULES:
|
|
# 파일 확인 (.py가 없는 경우 붙여서 확인)
|
|
source_file = file_path
|
|
if not os.path.exists(source_file):
|
|
if os.path.exists(source_file + ".py"):
|
|
source_file += ".py"
|
|
else:
|
|
print(f" [Warning] 컴파일 대상 파일을 찾을 수 없음: {file_path}")
|
|
continue
|
|
|
|
# 모듈 이름 생성 (경로 -> 점 표기법)
|
|
# 예: src/contents/option -> src.contents.option
|
|
# 이렇게 해야 'src' 패키지 안의 모듈로 정확히 인식됩니다.
|
|
no_ext = os.path.splitext(source_file)[0]
|
|
module_name = no_ext.replace("/", ".").replace("\\", ".")
|
|
|
|
# Extension 객체 생성
|
|
ext = Extension(
|
|
name=module_name,
|
|
sources=[source_file]
|
|
)
|
|
extensions.append(ext)
|
|
|
|
if not extensions:
|
|
print(">>> [Cython] 컴파일할 대상이 없습니다.")
|
|
return
|
|
|
|
# setuptools의 setup을 직접 호출 (인자값으로 build_ext --inplace 전달)
|
|
try:
|
|
cy_setup(
|
|
name="CythonInternalBuild",
|
|
ext_modules=cythonize(
|
|
extensions,
|
|
compiler_directives={'language_level': "3"},
|
|
quiet=True
|
|
),
|
|
script_args=['build_ext', '--inplace']
|
|
)
|
|
print(">>> [Cython] 빌드 성공!\n")
|
|
except Exception as e:
|
|
print(f"\n>>> [Cython] 빌드 중 오류 발생: {e}")
|
|
sys.exit(1)
|
|
|
|
def get_cython_freeze_config(modules_list):
|
|
"""
|
|
Cython 모듈 리스트를 기반으로 cx_Freeze용 설정을 생성합니다.
|
|
Returns:
|
|
(excludes_list, include_files_list, include_packages_to_remove)
|
|
"""
|
|
excludes = [] # .py 소스 제외용
|
|
include_files = [] # .pyd 파일 포함용
|
|
module_names = set()
|
|
|
|
for module_path in modules_list:
|
|
# 모듈 이름 변환 (src/contents/price -> src.contents.price)
|
|
clean_path = os.path.splitext(module_path)[0]
|
|
module_name = clean_path.replace("/", ".").replace("\\", ".")
|
|
module_names.add(module_name)
|
|
|
|
excludes.append(module_name)
|
|
|
|
# .pyd 파일 찾기 및 매핑
|
|
dir_name = os.path.dirname(module_path)
|
|
base_name = os.path.basename(module_path)
|
|
pyd_pattern = os.path.join(dir_name, f"{base_name}*.pyd")
|
|
found_pyds = glob.glob(pyd_pattern)
|
|
|
|
if found_pyds:
|
|
src_pyd = found_pyds[0]
|
|
# 라이브러리 구조 유지: lib/src/...
|
|
dest_pyd = os.path.join("lib", dir_name, f"{base_name}.pyd")
|
|
include_files.append((src_pyd, dest_pyd))
|
|
print(f" [Protect] {module_name}: .py 제외, .pyd 포함")
|
|
else:
|
|
print(f" [Warning] .pyd 없음: {module_path}. (원본 .py가 포함될 수 있음)")
|
|
|
|
return excludes, include_files, module_names
|
|
|
|
def collect_include_files():
|
|
"""DLL, 리소스 파일 등 기타 include_files 리스트를 생성합니다."""
|
|
base_dir = os.path.dirname(__file__)
|
|
updater_file = os.path.join(base_dir, 'updateManager', 'updater.exe')
|
|
|
|
# 기본 리소스 파일들
|
|
files = [
|
|
('src/Edit_PartTimer3.ico', 'lib/src/Edit_PartTimer3.ico'),
|
|
('kiprisCategories.json', 'kiprisCategories.json'),
|
|
('src/keyword/kiprisCategories.json', 'lib/src/keyword/kiprisCategories.json'),
|
|
('src/modules/fonts', 'lib/src/modules/fonts'),
|
|
(updater_file, 'updater.exe'),
|
|
('퍼센티 다양한 카테고리 엑셀 수집(스스 기준).xlsx', '퍼센티 다양한 카테고리 엑셀 수집(스스 기준).xlsx'),
|
|
('src/Percenty_SS_Code.json', 'lib/src/Percenty_SS_Code.json'),
|
|
('src/browsers/chromium-1200', 'lib/src/browsers/chromium-1200'),
|
|
('src/browsers/extensions', 'lib/src/browsers/extensions'),
|
|
('C:/Windows/System32/vcomp140.dll', 'vcomp140.dll'),
|
|
]
|
|
|
|
# VC Runtime Files
|
|
vc_runtimes = [
|
|
('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'),
|
|
]
|
|
files.extend(vc_runtimes)
|
|
|
|
dll_files = [
|
|
# "LIBPQ.dll",
|
|
# "MIMAPI64.dll",
|
|
# "Qt63DQuickScene3D.dll",
|
|
"api-ms-win-core-com-l1-1-0.dll",
|
|
"api-ms-win-core-debug-l1-1-0.dll",
|
|
"api-ms-win-core-errorhandling-l1-1-0.dll",
|
|
"api-ms-win-core-handle-l1-1-0.dll",
|
|
"api-ms-win-core-heap-l1-1-0.dll",
|
|
# "api-ms-win-core-heap-l2-1-0.dll",
|
|
"api-ms-win-core-interlocked-l1-1-0.dll",
|
|
# "api-ms-win-core-libraryloader-l1-2-0.dll",
|
|
# "api-ms-win-core-libraryloader-l1-2-1.dll",
|
|
"api-ms-win-core-localization-l1-2-0.dll",
|
|
# "api-ms-win-core-path-l1-1-0.dll",
|
|
"api-ms-win-core-processthreads-l1-1-0.dll",
|
|
"api-ms-win-core-processthreads-l1-1-1.dll",
|
|
"api-ms-win-core-profile-l1-1-0.dll",
|
|
# "api-ms-win-core-realtime-l1-1-1.dll",
|
|
"api-ms-win-core-rtlsupport-l1-1-0.dll",
|
|
"api-ms-win-core-synch-l1-1-0.dll",
|
|
"api-ms-win-core-synch-l1-2-0.dll",
|
|
"api-ms-win-core-sysinfo-l1-1-0.dll",
|
|
# "api-ms-win-core-winrt-error-l1-1-0.dll",
|
|
# "api-ms-win-core-winrt-l1-1-0.dll",
|
|
# "api-ms-win-core-winrt-string-l1-1-0.dll",
|
|
"api-ms-win-crt-conio-l1-1-0.dll",
|
|
"api-ms-win-crt-environment-l1-1-0.dll",
|
|
"api-ms-win-crt-filesystem-l1-1-0.dll",
|
|
"api-ms-win-crt-multibyte-l1-1-0.dll",
|
|
"api-ms-win-crt-private-l1-1-0.dll",
|
|
"api-ms-win-crt-process-l1-1-0.dll",
|
|
"api-ms-win-crt-time-l1-1-0.dll",
|
|
"api-ms-win-crt-utility-l1-1-0.dll",
|
|
# "api-ms-win-power-base-l1-1-0.dll",
|
|
# "api-ms-win-power-setting-l1-1-0.dll",
|
|
# "api-ms-win-shcore-scaling-l1-1-1.dll",
|
|
]
|
|
|
|
# DLL 파일 처리
|
|
system32_path = "C:/Windows/System32/downlevel"
|
|
dll_include_files = [(os.path.join(system32_path, dll), dll) for dll in dll_files]
|
|
files.extend(dll_include_files)
|
|
|
|
# 경로 존재 확인
|
|
final_files = []
|
|
for src, dest in files:
|
|
if os.path.exists(src):
|
|
final_files.append((src, dest))
|
|
else:
|
|
print(f" [Error] 경로 없음(Skip): {src}")
|
|
|
|
return final_files
|
|
|
|
# ============================================================================
|
|
# [Main Logic] 실행 로직
|
|
# ============================================================================
|
|
|
|
# 0. 기존 빌드찌꺼기 제거
|
|
|
|
run_setup_clean()
|
|
|
|
# 1. Cython 빌드 실행
|
|
run_cython_build()
|
|
|
|
# 2. cx_Freeze 설정 준비
|
|
cy_excludes, cy_include_files, cy_module_names = get_cython_freeze_config(CYTHON_MODULES)
|
|
resource_include_files = collect_include_files()
|
|
|
|
# 3. 최종 옵션 조합
|
|
final_includes = [mod for mod in BASE_INCLUDES if mod not in cy_module_names] # 충돌 방지
|
|
final_excludes = BASE_EXCLUDES + cy_excludes
|
|
final_include_files = resource_include_files + cy_include_files
|
|
|
|
build_options = {
|
|
'packages': [
|
|
'ctypes', 'asyncio', 'subprocess', 'pyperclip', 'numpy',
|
|
'requests', 'PIL', 'bs4', 'psutil',
|
|
'openai',
|
|
'pandas', 'supabase', 'translatepy', 'markdown',
|
|
'json', 'json.encoder', 'json.decoder', 'json.scanner',
|
|
'playwright',
|
|
# 'PySide6',
|
|
],
|
|
'includes': final_includes,
|
|
'excludes': final_excludes,
|
|
'include_files': final_include_files,
|
|
'zip_include_packages': [],
|
|
'optimize': 0,
|
|
'silent': True,
|
|
'include_msvcr': True,
|
|
}
|
|
|
|
# ============================================================================
|
|
# [Custom Build Class] 빌드 프로세스 커스터마이징
|
|
# ============================================================================
|
|
|
|
class CustomBuildExe(_build_exe):
|
|
def run(self):
|
|
# 전체 진행률: 100
|
|
# 초기값 10: Cython 빌드는 이 클래스 실행 전(if __name__...)에 이미 완료됨
|
|
print("\n" + "="*60)
|
|
print(" AutoPercenty3 통합 빌드 시스템 시작")
|
|
print("="*60 + "\n")
|
|
try:
|
|
# 총 6단계로 구성 (Cython은 이미 완료됨 -> 10%)
|
|
with tqdm(total=100, initial=10, unit="pct",
|
|
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}% [{elapsed}]",
|
|
ncols=80, colour='green') as pbar:
|
|
|
|
# [Step 2] cx_Freeze 빌드 (10 -> 30%)
|
|
pbar.set_description("Step 2/6: cx_Freeze Packaging")
|
|
_build_exe.run(self)
|
|
pbar.update(20)
|
|
|
|
|
|
# [Step 3] 빌드 결과물 정리 (30 -> 35%)
|
|
pbar.set_description("Step 3/6: Cleaning Build Output")
|
|
self._cleanup_build_output()
|
|
pbar.update(5)
|
|
|
|
# [Step 4] Inno Setup 컴파일 (35 -> 70%)
|
|
pbar.set_description("Step 4/6: Inno Setup Compiling")
|
|
self._run_inno_setup() # .iss 생성
|
|
self._compile_inno_installer() # .exe 생성
|
|
pbar.update(35)
|
|
|
|
|
|
# [Step 5] 파일명 변경 (버전 정보 추가) (70 -> 75%)
|
|
pbar.set_description("Step 5/6: Renaming Installer")
|
|
final_installer_path = self._rename_installer_with_version()
|
|
pbar.update(5)
|
|
|
|
# [Step 6] Gokapi 업로드 (75 -> 95%)
|
|
if final_installer_path and GOKAPI_URL != "https://gokapi.your-domain.com":
|
|
pbar.set_description("Step 6/6: Uploading to Gokapi")
|
|
self._upload_to_gokapi(final_installer_path)
|
|
else:
|
|
print("\n[Skip] Gokapi 설정이 없거나 파일이 없어 업로드를 건너뜁니다.")
|
|
pbar.update(20)
|
|
|
|
# [Final] 소스 정리 (95 -> 100%)
|
|
pbar.set_description("Finalizing: Cleaning Source")
|
|
self._cleanup_cython_artifacts()
|
|
pbar.update(5)
|
|
|
|
pbar.set_description("Build Complete!")
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n\n>>> 빌드가 취소되었습니다.")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"\n\n>>> 빌드 중 치명적인 오류 발생: {e}")
|
|
# 에러 상세 내용을 보기 위해 traceback 출력
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|
|
|
|
def _get_dir_size(self, path):
|
|
total = 0
|
|
for dirpath, _, filenames in os.walk(path):
|
|
for f in filenames:
|
|
total += os.path.getsize(os.path.join(dirpath, f))
|
|
return total
|
|
|
|
def _rename_installer_with_version(self):
|
|
"""생성된 설치 파일 이름에 버전을 추가합니다."""
|
|
# Inno Setup의 기본 출력 경로 (dist/installer)
|
|
dist_dir = os.path.join(os.path.dirname(__file__), "dist", "installer")
|
|
|
|
# 원래 생성되는 파일명 (generate_iss.py 설정에 따름, 보통 Setup.exe)
|
|
# 로그에 찍힌 이름을 기준으로 찾습니다.
|
|
original_name = "Edit_PartTimer Setup.exe"
|
|
original_path = os.path.join(dist_dir, original_name)
|
|
|
|
if not os.path.exists(original_path):
|
|
print(f"\n[Error] 원본 설치 파일을 찾을 수 없습니다: {original_path}")
|
|
# 혹시 다른 이름일 수도 있으니 exe 파일을 검색해봅니다.
|
|
exe_files = glob.glob(os.path.join(dist_dir, "*.exe"))
|
|
if exe_files:
|
|
original_path = max(exe_files, key=os.path.getctime) # 가장 최신 파일
|
|
else:
|
|
return None
|
|
|
|
# 새 파일명: Edit_PartTimer Setup_V3.12.15.exe
|
|
new_name = f"Edit_PartTimer Setup_V{__version__}.exe"
|
|
new_path = os.path.join(dist_dir, new_name)
|
|
|
|
try:
|
|
# 기존에 같은 버전 파일이 있으면 삭제
|
|
if os.path.exists(new_path):
|
|
os.remove(new_path)
|
|
|
|
os.rename(original_path, new_path)
|
|
print(f"\n[Rename] 파일명이 변경되었습니다:\n -> {new_name}")
|
|
return new_path
|
|
except Exception as e:
|
|
print(f"\n[Error] 파일명 변경 실패: {e}")
|
|
return original_path
|
|
|
|
def _upload_to_gokapi(self, file_path):
|
|
"""Gokapi 서버로 파일을 업로드합니다."""
|
|
print("\n[Upload] Gokapi 서버로 업로드를 시작합니다...")
|
|
|
|
if not os.path.exists(file_path):
|
|
print(" [Error] 업로드할 파일이 없습니다.")
|
|
return
|
|
|
|
try:
|
|
url = f"{GOKAPI_URL}/api/v1/upload"
|
|
headers = {
|
|
"Authorization": f"Bearer {GOKAPI_APIKEY}"
|
|
}
|
|
|
|
|
|
with open(file_path, "rb") as f:
|
|
# 멀티파트 업로드
|
|
files = {"file": (os.path.basename(file_path), f)}
|
|
data = {
|
|
"expire": 0 # ✅ 영구 저장
|
|
} # 만료일 설정 (예: 0=무제한, 7d=7일) - 필요시 data={'expiry': '7d'} 추가
|
|
|
|
response = requests.post(
|
|
url,
|
|
headers=headers,
|
|
files=files,
|
|
data=data,
|
|
timeout=30
|
|
)
|
|
|
|
if response.status_code in (200, 201):
|
|
data = response.json()
|
|
|
|
# Gokapi 응답 구조
|
|
hotlink = data.get("DownloadUrl") or data.get("File", {}).get("Url")
|
|
|
|
print("\n" + "#" * 50)
|
|
print("업로드 성공!")
|
|
print(f"다운로드 링크: {hotlink}")
|
|
print("#" * 50 + "\n")
|
|
else:
|
|
print(f"\n[Error] 업로드 실패 (Status: {response.status_code})")
|
|
print("Server Response:", response.text)
|
|
|
|
except Exception as e:
|
|
print(f"\n[Error] 업로드 중 예외 발생: {e}")
|
|
|
|
def _cleanup_build_output(self):
|
|
"""빌드된 exe 폴더 내부의 불필요한 파일 삭제"""
|
|
print("\n[Cleaner] 빌드 결과물 정리 중...")
|
|
build_dir = os.path.join(os.path.dirname(__file__), "build")
|
|
|
|
if not os.path.exists(build_dir): return
|
|
|
|
removed_count = 0
|
|
total_saved = 0
|
|
|
|
for item in os.listdir(build_dir):
|
|
if not item.startswith("exe."): continue
|
|
|
|
exe_dir = os.path.join(build_dir, item)
|
|
for unnecessary in CLEANUP_TARGETS:
|
|
target_path = os.path.join(exe_dir, unnecessary)
|
|
try:
|
|
if os.path.isdir(target_path):
|
|
size = self._get_dir_size(target_path)
|
|
shutil.rmtree(target_path)
|
|
total_saved += size
|
|
print(f" ✓ 디렉토리 삭제: {unnecessary}")
|
|
elif os.path.isfile(target_path):
|
|
size = os.path.getsize(target_path)
|
|
os.remove(target_path)
|
|
total_saved += size
|
|
print(f" ✓ 파일 삭제: {unnecessary}")
|
|
removed_count += 1
|
|
except Exception:
|
|
pass # 없는 파일은 무시
|
|
|
|
print(f"[Cleaner] 정리 완료: {total_saved / (1024*1024):.1f} MB 절약\n")
|
|
|
|
def _cleanup_cython_artifacts(self):
|
|
"""소스 폴더의 .pyd, .c 임시 파일 삭제"""
|
|
print("[Cleaner] 소스 폴더 Cython 임시 파일 정리 중...")
|
|
deleted = 0
|
|
for module_path in CYTHON_MODULES:
|
|
dir_name = os.path.dirname(module_path)
|
|
base_name = os.path.basename(module_path)
|
|
patterns = [
|
|
os.path.join(dir_name, f"{base_name}*.pyd"),
|
|
os.path.join(dir_name, f"{base_name}.c")
|
|
]
|
|
for pattern in patterns:
|
|
for f in glob.glob(pattern):
|
|
try:
|
|
os.remove(f)
|
|
deleted += 1
|
|
except Exception: pass
|
|
print(f"[Cleaner] 소스 정리 완료: {deleted}개 파일 삭제\n")
|
|
|
|
def _run_inno_setup(self):
|
|
"""Inno Setup 스크립트 생성기 실행"""
|
|
print("[Inno Setup] 설치 스크립트 생성 시작...")
|
|
try:
|
|
result = subprocess.run([sys.executable, "generate_iss.py"], capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
print("Inno Setup 스크립트 생성 완료!")
|
|
else:
|
|
print(f"Error: {result.stderr}")
|
|
except Exception as e:
|
|
print(f"Exception: {e}")
|
|
|
|
def _compile_inno_installer(self):
|
|
"""
|
|
생성된 .iss 파일을 ISCC.exe로 컴파일하여 최종 Setup.exe를 만듭니다.
|
|
"""
|
|
print("\n[Inno Setup] 최종 설치 파일(Setup.exe) 컴파일 시작...")
|
|
|
|
# 1. ISCC.exe 경로 설정 (Inno Setup 6 기준 기본 경로)
|
|
# 만약 설치 경로가 다르다면 이 부분을 수정해주세요.
|
|
iscc_path = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
|
|
|
if not os.path.exists(iscc_path):
|
|
print(f" [Error] Inno Setup 컴파일러를 찾을 수 없습니다: {iscc_path}")
|
|
print(" Inno Setup 6가 설치되어 있는지 확인하거나, iscc_path를 수정하세요.")
|
|
return
|
|
|
|
# 2. 가장 최신 .iss 파일 찾기 (AutoPercenty_날짜.iss 패턴)
|
|
# generate_iss.py가 매번 새로운 이름으로 만들기 때문에 최신 파일을 찾아야 함
|
|
iss_files = glob.glob("AutoPercenty_*.iss")
|
|
if not iss_files:
|
|
print(" [Error] 컴파일할 .iss 파일을 찾을 수 없습니다.")
|
|
return
|
|
|
|
# 파일 생성 시간 기준으로 정렬하여 가장 마지막(최신) 파일 선택
|
|
latest_iss = max(iss_files, key=os.path.getctime)
|
|
print(f" Target Script: {latest_iss}")
|
|
|
|
# 3. 컴파일 명령어 실행
|
|
try:
|
|
# subprocess로 ISCC 실행
|
|
print(" Compiling... (시간이 조금 걸릴 수 있습니다)")
|
|
subprocess.run([iscc_path, latest_iss], check=True)
|
|
print(" [Success] ★★★ 모든 작업 완료! 설치 파일이 생성되었습니다. ★★★\n")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f" [Error] Inno Setup 컴파일 실패: {e}")
|
|
|
|
# ============================================================================
|
|
# [Setup] 최종 실행
|
|
# ============================================================================
|
|
|
|
base = 'Win32GUI' if sys.platform == 'win32' else None
|
|
|
|
setup(
|
|
name=__title__,
|
|
version=__version__,
|
|
description=__description__,
|
|
author=__author__,
|
|
author_email=__author_email__,
|
|
license=__license__,
|
|
packages=find_packages(),
|
|
install_requires=__install_requires__,
|
|
python_requires='>=3.11',
|
|
include_package_data=True,
|
|
zip_safe=False,
|
|
options={'build_exe': build_options},
|
|
executables=[
|
|
Executable(
|
|
'main.py',
|
|
base=base,
|
|
target_name='Edit_PartTimer3.exe',
|
|
icon="Edit_PartTimer3.ico"
|
|
)
|
|
],
|
|
cmdclass={
|
|
"build_exe": CustomBuildExe,
|
|
},
|
|
) |