🚧 Add build script
This commit is contained in:
+2
-1
@@ -6,4 +6,5 @@ build/
|
||||
dist/
|
||||
.pytest_cache/
|
||||
output/
|
||||
*.xlsx
|
||||
*.xlsx
|
||||
*.spec
|
||||
@@ -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).
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -15,6 +15,7 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
"pyinstaller>=6.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user