🚧 Add build script

This commit is contained in:
Jonas Bengtsson
2026-10-02 07:19:13 +02:00
parent 75cc7cde25
commit 662a014104
5 changed files with 183 additions and 1 deletions
+2 -1
View File
@@ -6,4 +6,5 @@ build/
dist/
.pytest_cache/
output/
*.xlsx
*.xlsx
*.spec
+42
View File
@@ -116,10 +116,52 @@ pip install -e .[dev]
pytest
```
## Building a self-executing exe
The CLI can be packaged as a standalone Windows executable with PyInstaller.
```bash
pip install -e .[dev] # includes pyinstaller
# one-folder build (default): fast start-up, distribute the whole folder
.\build.ps1
# single-file build: one .exe, slower start-up (self-extracting)
.\build.ps1 -Onefile
# rebuild from scratch and smoke-test the exe against assets/
.\build.ps1 -Clean -Test
# explicit interpreter if several Pythons are installed
.\build.ps1 -Python C:\Path\To\python.exe
```
Results land in `dist/`:
- **one-folder** (default): `dist/mitsubishi-label-converter/mitsubishi-label-converter.exe`
plus supporting DLLs — distribute the whole folder; starts fast.
- **onefile**: `dist/mitsubishi-label-converter.exe` — a single 8.8 MB file to
copy anywhere; a little slower to start.
Notes:
- The script prefers the project `.venv`, then `python`/`py` on PATH; the
Microsoft Store `python` alias is detected and skipped automatically.
- `run.py` at the project root is the packaging entry point (PyInstaller cannot
execute `cli.py` directly because of its relative imports).
- Antivirus software may flag unsigned PyInstaller exes; sign the exe or add
an exclusion if this matters in your environment.
- Build artifacts (`build/`, `dist/`, `*.spec`) are git-ignored.
## TODO / Roadmap
Done:
- [x] **Self-executing exe** — `build.ps1` wraps PyInstaller with
one-folder/onefile modes, clean rebuild, built-in smoke test, and robust
Python resolution; verified all three subcommands and the lossless
round-trip through the packaged exe.
- [x] **Expose struct members with addresses in Excel** — dotted member rows
inline under each structured variable (189 structured variables -> 1,220
member rows in the sample file).
+127
View File
@@ -0,0 +1,127 @@
# Build script: packages the CLI as a single self-executing .exe
#
# Usage:
# .\build.ps1 -> dist\mitsubishi-label-converter.exe
# .\build.ps1 -Clean -> remove build artifacts first
# .\build.ps1 -Onefile -> single-file exe (slower start, one file)
# default is one-folder mode (fast start)
# .\build.ps1 -Test -> run the exe against the real assets afterwards
#
# Requirements: pyinstaller installed in the active environment
# pip install pyinstaller
[CmdletBinding()]
param(
[switch]$Clean,
[switch]$Onefile,
[switch]$Test,
# Explicit interpreter, e.g. -Python C:\Path\To\python.exe
[string]$Python
)
$ErrorActionPreference = "Stop"
$root = $PSScriptRoot
$exename = "mitsubishi-label-converter"
$dist = Join-Path $root "dist"
# Resolve a working Python interpreter -------------------------------
# The Microsoft Store 'python' alias can shadow a real install, so verify
# each candidate by actually running it.
function Test-Python([string]$cmd) {
if (-not $cmd) { return $false }
try {
& $cmd -c "import sys; sys.exit(0)" 2>$null
return ($LASTEXITCODE -eq 0)
} catch {
return $false
}
}
if (-not $Python) {
$candidates = @(
(Join-Path $root ".venv\Scripts\python.exe"), # project venv first
"python",
"py"
)
foreach ($c in $candidates) {
if (Test-Python $c) { $Python = $c; break }
}
}
if (-not $Python -or -not (Test-Python $Python)) {
Write-Error @"
No working Python interpreter found.
Tried: project .venv, 'python' on PATH, 'py' launcher.
Create a venv (python -m venv .venv) or pass one explicitly:
.\build.ps1 -Python C:\Path\To\python.exe
"@
}
Write-Host "==> Using Python: $Python"
# Sanity checks ------------------------------------------------------------
& $Python -m PyInstaller --version 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "PyInstaller not found in '$Python'. Run: $Python -m pip install pyinstaller"
}
if ($Clean) {
Write-Host "==> Cleaning build artifacts..."
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue `
(Join-Path $root "build"), $dist
# remove the egg-info generated by editable installs
Get-ChildItem $root -Filter "*.egg-info" -Directory -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
# Build --------------------------------------------------------------------
$modeArgs = @()
if ($Onefile) { $modeArgs += "--onefile" } else { $modeArgs += "--onedir" }
Write-Host "==> Building $exename ($($modeArgs -join ' '))..."
& $Python -m PyInstaller @modeArgs `
--name $exename `
--noconfirm `
--clean `
--console `
--paths "$root\src" `
"$root\run.py"
if ($LASTEXITCODE -ne 0) {
Write-Error "PyInstaller failed with exit code $LASTEXITCODE"
}
# Locate the exe -----------------------------------------------------------
if ($Onefile) {
$exe = Join-Path $dist "$exename.exe"
} else {
$exe = Join-Path $dist "$exename\$exename.exe"
}
if (-not (Test-Path $exe)) {
Write-Error "Build finished but exe not found at $exe"
}
$size = "{0:N1} MB" -f ((Get-Item $exe).Length / 1MB)
Write-Host "==> Built: $exe ($size)"
# Optional smoke test -------------------------------------------------------
if ($Test) {
Write-Host "==> Smoke testing against assets/Global.xml..."
$testOut = Join-Path $dist "smoketest.xlsx"
& $exe to-xlsx (Join-Path $root "assets\Global.xml") `
--sdt (Join-Path $root "assets\SDT.xml") `
-o $testOut --force
if ($LASTEXITCODE -ne 0) {
Write-Error "Smoke test failed"
}
& $exe report (Join-Path $root "assets\Global.xml") --sdt (Join-Path $root "assets\SDT.xml")
if ($LASTEXITCODE -ne 0) {
Write-Error "Smoke test (report) failed"
}
Remove-Item $testOut -Force -ErrorAction SilentlyContinue
Write-Host "==> Smoke test OK"
}
Write-Host ""
Write-Host "Done. Distribute: $exe"
if (-not $Onefile) {
Write-Host "Note: one-folder mode requires the whole dist\$exename folder."
}
+1
View File
@@ -15,6 +15,7 @@ dependencies = [
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pyinstaller>=6.0",
]
[project.scripts]
+11
View File
@@ -0,0 +1,11 @@
"""PyInstaller entry point (absolute imports, no relative package context).
Packaging tools cannot execute ``src/mitsubishi_label_converter/cli.py``
directly because it uses relative imports; this thin launcher works around
that by importing the package normally.
"""
from mitsubishi_label_converter.cli import main
if __name__ == "__main__":
raise SystemExit(main())