176 lines
7.6 KiB
Python
176 lines
7.6 KiB
Python
import shutil
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import PyInstaller.__main__
|
|
import typer
|
|
from rich import print
|
|
import time
|
|
from datetime import datetime
|
|
|
|
|
|
app = typer.Typer()
|
|
|
|
|
|
@app.command()
|
|
def build(build: bool = True, copy: bool = True, test: bool = False, dest: str | None = None):
|
|
"""Windows-focused build script.
|
|
|
|
- `--test` creates a one-file test binary in `dist\test`.
|
|
- `--copy` (default) copies the artifact to the shared network path used previously.
|
|
"""
|
|
|
|
project_root = Path(__file__).resolve().parent
|
|
|
|
if build:
|
|
if test:
|
|
print("Building Windows test one-file bundle with PyInstaller")
|
|
# On Windows use ';' as add-data separator (src;dest)
|
|
# Use absolute path for the icon folder so PyInstaller can find it
|
|
icon_src = str(project_root / "icon")
|
|
add_data_args = ["--add-data", f"{icon_src};icon"]
|
|
|
|
# Try to include dicognito package data (release_notes.md) which
|
|
# PyInstaller may miss. This prevents runtime FileNotFoundError for
|
|
# dicognito/release_notes.md when the package expects the file at runtime.
|
|
try:
|
|
spec = importlib.util.find_spec("dicognito")
|
|
if spec and spec.submodule_search_locations:
|
|
dicognito_path = Path(spec.submodule_search_locations[0])
|
|
release_notes = dicognito_path / "release_notes.md"
|
|
if release_notes.exists():
|
|
# PyInstaller on Windows expects 'src;dest'
|
|
add_data_args += ["--add-data", f"{str(release_notes)};dicognito"]
|
|
else:
|
|
# include whole package folder if the single file wasn't found
|
|
add_data_args += ["--add-data", f"{str(dicognito_path)};dicognito"]
|
|
except Exception as e:
|
|
print(f"Warning: couldn't locate dicognito package: {e}")
|
|
|
|
# Try to include pythonnet runtime DLLs to avoid missing
|
|
# Python.Runtime.dll at runtime (used by pythonnet / webview).
|
|
add_binary_args = []
|
|
try:
|
|
spec_py = importlib.util.find_spec("pythonnet")
|
|
if spec_py and spec_py.submodule_search_locations:
|
|
pn_path = Path(spec_py.submodule_search_locations[0])
|
|
runtime_dir = pn_path / "runtime"
|
|
if runtime_dir.exists():
|
|
for f in runtime_dir.rglob("*.dll"):
|
|
# PyInstaller on Windows expects 'src;dest'
|
|
add_binary_args += ["--add-binary", f"{str(f)};pythonnet\\runtime"]
|
|
# also include the full pythonnet package folder as data so
|
|
# package-relative lookups succeed at runtime
|
|
add_data_args += ["--add-data", f"{str(pn_path)};pythonnet"]
|
|
# try to include clr_loader package as well
|
|
try:
|
|
spec_clr = importlib.util.find_spec("clr_loader")
|
|
if spec_clr and spec_clr.submodule_search_locations:
|
|
clr_path = Path(spec_clr.submodule_search_locations[0])
|
|
add_data_args += ["--add-data", f"{str(clr_path)};clr_loader"]
|
|
except Exception:
|
|
pass
|
|
except Exception as e:
|
|
print(f"Warning: couldn't locate pythonnet runtime: {e}")
|
|
|
|
# Ensure dist/work/spec directories exist
|
|
dist_dir = project_root / "dist" / "test"
|
|
work_dir = project_root / "build" / "test"
|
|
spec_dir = project_root / "build" / "specs"
|
|
dist_dir.mkdir(parents=True, exist_ok=True)
|
|
work_dir.mkdir(parents=True, exist_ok=True)
|
|
spec_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Decide build name and try to remove any locked previous artifact.
|
|
base_name = "Uploader_test"
|
|
output_exe = dist_dir / f"{base_name}.exe"
|
|
|
|
def try_remove_with_retries(path: Path, retries: int = 5, delay: float = 0.5) -> bool:
|
|
"""Try to remove a file with retries and backoff. Returns True if removed or not present."""
|
|
if not path.exists():
|
|
return True
|
|
for attempt in range(retries):
|
|
try:
|
|
path.unlink()
|
|
print(f"Removed existing file: {path}")
|
|
return True
|
|
except PermissionError:
|
|
# Try to move/rename it out of the way
|
|
try:
|
|
backup = path.with_suffix(f".old.{int(time.time())}")
|
|
path.rename(backup)
|
|
print(f"Renamed locked file to: {backup}")
|
|
return True
|
|
except Exception:
|
|
pass
|
|
print(f"File locked, retrying in {delay} seconds ({attempt+1}/{retries})")
|
|
time.sleep(delay)
|
|
delay *= 1.5
|
|
except Exception as e:
|
|
print(f"Failed to remove existing file {path}: {e}")
|
|
return False
|
|
return False
|
|
|
|
name_used = base_name
|
|
if not try_remove_with_retries(output_exe):
|
|
# If the previous artifact is locked, fall back to a unique name to avoid build failure
|
|
ts = datetime.now().strftime("%Y%m%dT%H%M%S")
|
|
name_used = f"{base_name}_{ts}"
|
|
print(f"Previous artifact appears locked; building as {name_used} to avoid PermissionError")
|
|
|
|
PyInstaller.__main__.run([
|
|
str(project_root / "nice.py"),
|
|
"--onefile",
|
|
"--name",
|
|
name_used,
|
|
"--distpath",
|
|
str(dist_dir),
|
|
"--workpath",
|
|
str(work_dir),
|
|
"--specpath",
|
|
str(spec_dir),
|
|
] + add_data_args + add_binary_args + [
|
|
# add some common hidden imports that PyInstaller sometimes misses
|
|
"--hidden-import",
|
|
"bs4",
|
|
"--hidden-import",
|
|
"requests",
|
|
"--hidden-import",
|
|
"nicegui",
|
|
])
|
|
else:
|
|
print("Building using spec: nice_uploader.spec")
|
|
PyInstaller.__main__.run([str(project_root / "nice_uploader.spec")])
|
|
|
|
if copy:
|
|
# Windows-only default destination used previously in the repo
|
|
default_dest = Path(r"\\ict\Go\RCH\Shared\Dragon-Xray\rad-tools\uploader\Uploader")
|
|
|
|
artifact_dir = project_root / "dist" / ("test" if test else "")
|
|
if not artifact_dir.exists():
|
|
artifact_dir = project_root / "dist"
|
|
|
|
# Look for a matching artifact
|
|
candidates = list(artifact_dir.glob("Uploader*")) + list(artifact_dir.glob("*.exe"))
|
|
if not candidates:
|
|
print(f"No build artifact found in {artifact_dir}, skipping copy")
|
|
return
|
|
|
|
artifact = candidates[0]
|
|
|
|
dest_path = Path(dest) if dest else default_dest
|
|
dest_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
if artifact.is_file():
|
|
target = dest_path / artifact.name
|
|
print(f"Copying {artifact} -> {target}")
|
|
shutil.copy2(artifact, target)
|
|
else:
|
|
target = dest_path / artifact.name
|
|
print(f"Copying directory {artifact} -> {target}")
|
|
if target.exists():
|
|
shutil.rmtree(target)
|
|
shutil.copytree(artifact, target)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app() |