🚧 Add build script
This commit is contained in:
@@ -7,3 +7,4 @@ dist/
|
|||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
output/
|
output/
|
||||||
*.xlsx
|
*.xlsx
|
||||||
|
*.spec
|
||||||
@@ -116,10 +116,52 @@ pip install -e .[dev]
|
|||||||
pytest
|
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
|
## TODO / Roadmap
|
||||||
|
|
||||||
Done:
|
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
|
- [x] **Expose struct members with addresses in Excel** — dotted member rows
|
||||||
inline under each structured variable (189 structured variables -> 1,220
|
inline under each structured variable (189 structured variables -> 1,220
|
||||||
member rows in the sample file).
|
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]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.0",
|
"pytest>=8.0",
|
||||||
|
"pyinstaller>=6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[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