77 python build.py # Build with defaults
88 APP_VERSION=1.5 python build.py # Build with specific version
99"""
10- import sys
1110import os
1211import platform
1312import shutil
1413import subprocess
14+ import sys
15+ import textwrap
1516from pathlib import Path
1617
1718# Project root
1819PROJECT_ROOT = Path (__file__ ).parent
1920BUILD_DIR = PROJECT_ROOT / "build_nuitka"
2021DIST_DIR = PROJECT_ROOT / "dist"
2122OUTPUT_NAME = "TranslatorHoi4"
23+ BUILD_META_FILE = PROJECT_ROOT / "translatorhoi4" / "_build_meta.py"
2224
2325# Get version from environment or git
2426APP_VERSION = os .environ .get ("APP_VERSION" , "dev" )
3335 "aiohttp" ,
3436 "requests" ,
3537 "qfluentwidgets" ,
36- "jinja2" , # Prevent Nuitka inline copy conflict with pkg_resources
38+ "jinja2" ,
3739]
3840
3941# Packages to follow imports (optional/lazily loaded - Nuitka includes only what's actually used)
7577
7678
7779def parse_version_tuple (version_str ):
78- """Convert version string to tuple for Nuitka --file-version.
79-
80- Nuitka requires file-version to be a numeric tuple like 1.5.0.0
81- """
80+ """Convert version string to tuple for Nuitka --file-version."""
8281 parts = version_str .replace ("v" , "" ).split ("." )
8382 numeric_parts = []
84- for p in parts :
83+ for part in parts :
8584 try :
86- numeric_parts .append (int (p ))
85+ numeric_parts .append (int (part ))
8786 except ValueError :
8887 break
8988 if not numeric_parts :
90- return None # Invalid version like "dev"
91- # Pad to max 4 parts
89+ return None
9290 while len (numeric_parts ) < 4 :
9391 numeric_parts .append (0 )
9492 return "." .join (str (x ) for x in numeric_parts [:4 ])
@@ -107,19 +105,15 @@ def get_nuitka_command():
107105 "--include-data-dir=assets=assets" ,
108106 ]
109107
110- # Include packages
111108 for pkg in INCLUDE_PACKAGES :
112109 cmd .append (f"--include-package={ pkg } " )
113110
114- # Follow imports (only include actually used modules from these packages)
115111 for pkg in FOLLOW_IMPORTS :
116112 cmd .append (f"--follow-import-to={ pkg } " )
117113
118- # Exclude modules
119114 for mod in EXCLUDE_MODULES :
120115 cmd .append (f"--nofollow-import-to={ mod } " )
121116
122- # Version info - only include if we have a valid numeric version
123117 file_version = parse_version_tuple (APP_VERSION )
124118 if file_version :
125119 cmd .extend (
@@ -133,31 +127,25 @@ def get_nuitka_command():
133127 ]
134128 )
135129
136- # Parallel compilation (use all available CPU cores)
137130 cpu_count = os .cpu_count () or 1
138131 cmd .append (f"--jobs={ cpu_count } " )
139132
140- # macOS: Use clang directly as compiler
141133 if sys .platform == "darwin" :
142134 cmd .append ("--clang" )
143135
144- # Optimization
145136 cmd .extend (
146137 [
147138 "--assume-yes-for-downloads" ,
148139 "--remove-output" ,
149140 ]
150141 )
151142
152- # Platform-specific options
153143 if sys .platform == "win32" :
154144 cmd .extend (
155145 [
156146 "--windows-icon-from-ico=assets/icon.png" ,
157147 "--windows-console-mode=disable" ,
158- # Windows: disable LTO for faster builds (MSVC LTO is very slow)
159148 "--lto=no" ,
160- # Disable clcache - it fails with paths containing spaces on CI
161149 "--disable-ccache" ,
162150 ]
163151 )
@@ -167,67 +155,51 @@ def get_nuitka_command():
167155 "--macos-create-app-bundle" ,
168156 "--macos-app-icon=assets/icon.png" ,
169157 "--macos-app-name=TranslatorHoi4" ,
170- # macOS: let Nuitka picks the best LTO for clang
171158 "--lto=auto" ,
172- # ccache works automatically via clang symlinks set up in CI
173159 ]
174160 )
175161 elif sys .platform == "linux" :
176162 cmd .extend (
177163 [
178164 "--linux-icon=assets/icon.png" ,
179- # Linux: keep full LTO (gcc handles it well)
180165 "--lto=yes" ,
181- # ccache works automatically via gcc symlinks set up in CI
182166 ]
183167 )
184168
185- # Add the entry point
186- # Use --module-name-base to ensure .dist folder is named after OUTPUT_NAME
187169 cmd .append (str (PROJECT_ROOT / "translatorhoi4" / "app.py" ))
188-
189170 return cmd
190171
191172
192173def find_nuitka_output ():
193- """Find the actual Nuitka output directory.
194-
195- Nuitka names the .dist folder after the entry point script (e.g. app.dist),
196- not after --output-filename. This function searches for the correct path.
197- """
174+ """Find the actual Nuitka output directory."""
198175 if not BUILD_DIR .exists ():
199176 return None
200177
201- # Platform-specific expected outputs
202178 if sys .platform == "darwin" :
203- # macOS: look for .app bundle
204179 candidates = [
205180 BUILD_DIR / f"{ OUTPUT_NAME } .app" ,
206181 BUILD_DIR / "app.app" ,
207182 ]
208- # Also check inside any *.dist folder for .app
209- for d in BUILD_DIR .iterdir ():
210- if d .is_dir () and d .name .endswith (".dist" ):
211- app_in_dist = d / f"{ OUTPUT_NAME } .app"
183+ for directory in BUILD_DIR .iterdir ():
184+ if directory .is_dir () and directory .name .endswith (".dist" ):
185+ app_in_dist = directory / f"{ OUTPUT_NAME } .app"
212186 if app_in_dist .exists ():
213187 return app_in_dist
214- for c in candidates :
215- if c .exists ():
216- return c
188+ for candidate in candidates :
189+ if candidate .exists ():
190+ return candidate
217191 else :
218- # Windows/Linux: look for .dist folder
219192 candidates = [
220193 BUILD_DIR / f"{ OUTPUT_NAME } .dist" ,
221194 BUILD_DIR / "app.dist" ,
222195 ]
223- for c in candidates :
224- if c .exists () and c .is_dir ():
225- return c
196+ for candidate in candidates :
197+ if candidate .exists () and candidate .is_dir ():
198+ return candidate
226199
227- # Fallback: find any *.dist or *.app in BUILD_DIR
228- for d in BUILD_DIR .iterdir ():
229- if d .is_dir () and (d .name .endswith (".dist" ) or d .name .endswith (".app" )):
230- return d
200+ for directory in BUILD_DIR .iterdir ():
201+ if directory .is_dir () and (directory .name .endswith (".dist" ) or directory .name .endswith (".app" )):
202+ return directory
231203
232204 return None
233205
@@ -238,17 +210,52 @@ def clean_build_dirs():
238210 shutil .rmtree (BUILD_DIR )
239211 BUILD_DIR .mkdir (exist_ok = True )
240212
241- # Clean old dist
242213 dist_path = DIST_DIR / OUTPUT_NAME
243214 if dist_path .exists ():
244215 shutil .rmtree (dist_path )
245216
246- # Clean old macOS .app bundle if present
247217 app_path = DIST_DIR / f"{ OUTPUT_NAME } .app"
248218 if app_path .exists ():
249219 shutil .rmtree (app_path )
250220
251221
222+ def _normalize_arch (machine : str ) -> str :
223+ machine = machine .lower ()
224+ if machine in {"amd64" , "x86_64" , "x64" }:
225+ return "x64"
226+ if machine in {"arm64" , "aarch64" }:
227+ return "arm64"
228+ return machine
229+
230+
231+ def write_build_metadata () -> str | None :
232+ """Write temporary embedded build metadata for packaged runs."""
233+ previous = None
234+ if BUILD_META_FILE .exists ():
235+ previous = BUILD_META_FILE .read_text (encoding = "utf-8" )
236+
237+ content = textwrap .dedent (
238+ f"""\
239+ # Auto-generated by build.py
240+ BUILD_VERSION = { APP_VERSION !r}
241+ BUILD_CHANNEL = { "release" if APP_VERSION != "dev" else "dev" !r}
242+ BUILD_PLATFORM = { sys .platform !r}
243+ BUILD_ARCH = { _normalize_arch (platform .machine ())!r}
244+ """
245+ )
246+ BUILD_META_FILE .write_text (content , encoding = "utf-8" )
247+ return previous
248+
249+
250+ def restore_build_metadata (previous : str | None ) -> None :
251+ """Restore build metadata file to its original state."""
252+ if previous is None :
253+ if BUILD_META_FILE .exists ():
254+ BUILD_META_FILE .unlink ()
255+ return
256+ BUILD_META_FILE .write_text (previous , encoding = "utf-8" )
257+
258+
252259def check_macos_openssl ():
253260 if sys .platform != "darwin" :
254261 return
@@ -274,7 +281,6 @@ def check_macos_openssl():
274281
275282
276283def main ():
277- # Fix Windows console encoding issues
278284 if sys .platform == "win32" and hasattr (sys .stdout , "reconfigure" ):
279285 sys .stdout .reconfigure (encoding = "utf-8" )
280286
@@ -284,48 +290,37 @@ def main():
284290
285291 check_macos_openssl ()
286292 clean_build_dirs ()
287-
288- cmd = get_nuitka_command ()
289- print (f"\n Running Nuitka..." )
290-
291- # Execute Nuitka with proper argument handling
292- result = subprocess .run (cmd , cwd = str (PROJECT_ROOT ))
293-
294- if result .returncode != 0 :
295- print ("ERROR: Build failed!" , file = sys .stderr )
296- sys .exit (1 )
297-
298- # Move output to dist directory
299- DIST_DIR .mkdir (exist_ok = True )
300-
301- # Find actual Nuitka output (handles app.dist vs TranslatorHoi4.dist naming)
302- output_path = find_nuitka_output ()
303-
304- if output_path is None :
305- print ("ERROR: Build output not found!" , file = sys .stderr )
306- print (f"Searched in: { BUILD_DIR } " , file = sys .stderr )
307- if BUILD_DIR .exists ():
308- print (f"Build dir contents: { list (BUILD_DIR .iterdir ())} " , file = sys .stderr )
309- sys .exit (1 )
310-
311- # Determine final path based on platform
312- if sys .platform == "darwin" :
313- final_path = DIST_DIR / f"{ OUTPUT_NAME } .app"
314- else :
315- final_path = DIST_DIR / OUTPUT_NAME
316-
317- # Remove existing output if present
318- if final_path .exists ():
319- shutil .rmtree (final_path )
320- shutil .move (str (output_path ), str (final_path ))
321- print (f"\n ✓ Build successful!" )
322- print (f"Output: { final_path } " )
323-
324- # Print size info
325- total_size = sum (
326- f .stat ().st_size for f in final_path .rglob ("*" ) if f .is_file ()
327- )
328- print (f"Total size: { total_size / (1024 * 1024 ):.1f} MB" )
293+ previous_build_meta = write_build_metadata ()
294+
295+ try :
296+ cmd = get_nuitka_command ()
297+ print ("\n Running Nuitka..." )
298+ result = subprocess .run (cmd , cwd = str (PROJECT_ROOT ))
299+
300+ if result .returncode != 0 :
301+ print ("ERROR: Build failed!" , file = sys .stderr )
302+ sys .exit (1 )
303+
304+ DIST_DIR .mkdir (exist_ok = True )
305+ output_path = find_nuitka_output ()
306+ if output_path is None :
307+ print ("ERROR: Build output not found!" , file = sys .stderr )
308+ print (f"Searched in: { BUILD_DIR } " , file = sys .stderr )
309+ if BUILD_DIR .exists ():
310+ print (f"Build dir contents: { list (BUILD_DIR .iterdir ())} " , file = sys .stderr )
311+ sys .exit (1 )
312+
313+ final_path = DIST_DIR / (f"{ OUTPUT_NAME } .app" if sys .platform == "darwin" else OUTPUT_NAME )
314+ if final_path .exists ():
315+ shutil .rmtree (final_path )
316+ shutil .move (str (output_path ), str (final_path ))
317+
318+ print ("\n Build successful!" )
319+ print (f"Output: { final_path } " )
320+ total_size = sum (file .stat ().st_size for file in final_path .rglob ("*" ) if file .is_file ())
321+ print (f"Total size: { total_size / (1024 * 1024 ):.1f} MB" )
322+ finally :
323+ restore_build_metadata (previous_build_meta )
329324
330325
331326if __name__ == "__main__" :
0 commit comments