build: add single-command release script + portable zip artifacts
One-developer workflow: ``python build/make_release.py`` on each target OS produces both the installer and a portable .zip for that platform. Preflight checks PyInstaller / Pillow / iscc / hdiutil / ditto / appimagetool and bails with install hints if anything is missing — no half-built dist/. New scripts: - build/make_release.py — orchestrator, auto-detects host OS. - build/generate_icons.py — icon.ico / icon.icns / icon.png from src/gui/assets/datatools_icon_256.png (Pillow ships ICO + ICNS writers; no platform tooling needed). - build/build_portable_zip.py — Win/Linux portable zip via stdlib. - build/macos/build_zip.sh — Mac portable .app via ditto so bundle metadata survives. installer.iss now adds: Quick Launch task (opt-in, legacy Win 7), App Paths registry entry (Win+R "DataTools" works), SetupIconFile, UninstallDisplayIcon, AppSupportURL, AppUpdatesURL. CI workflow uploads installer + portable per platform and attaches both to GitHub Releases on tag push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
78
build/generate_icons.py
Normal file
78
build/generate_icons.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Generate platform-specific app icons from the source PNG asset.
|
||||
|
||||
Outputs:
|
||||
build/icon.ico Windows multi-resolution icon (16..256 px sizes).
|
||||
build/icon.icns macOS icon bundle (16..1024 px scaled tiers).
|
||||
build/icon.png Plain 256x256 PNG used by the Linux AppImage.
|
||||
|
||||
Source: ``src/gui/assets/datatools_icon_256.png`` (the same icon
|
||||
``st.set_page_config`` uses, so the installer / Dock / Taskbar match
|
||||
the in-app tab favicon).
|
||||
|
||||
Run manually:
|
||||
python build/generate_icons.py
|
||||
|
||||
CI runs this automatically before invoking PyInstaller (see
|
||||
``.github/workflows/build.yml``). Both files are .gitignored — they
|
||||
are build artifacts derived from the committed PNG.
|
||||
|
||||
Self-contained: pulls only Pillow (already a transitive dep of
|
||||
``pdfplumber``) so no extra installs are required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# Repo layout: this script lives at <REPO>/build/. The source PNG is at
|
||||
# <REPO>/src/gui/assets/datatools_icon_256.png.
|
||||
BUILD_DIR = Path(__file__).resolve().parent
|
||||
REPO = BUILD_DIR.parent
|
||||
SOURCE_PNG = REPO / "src" / "gui" / "assets" / "datatools_icon_256.png"
|
||||
|
||||
# Windows ICO needs every size the OS might render at: taskbar (16/24),
|
||||
# Start Menu (32/48), tile (64/128), shell properties dialog (256).
|
||||
ICO_SIZES = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64),
|
||||
(128, 128), (256, 256)]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not SOURCE_PNG.exists():
|
||||
sys.stderr.write(
|
||||
f"Source icon not found at {SOURCE_PNG}.\n"
|
||||
"Add a 256x256 (or larger) RGBA PNG there and re-run.\n"
|
||||
)
|
||||
return 1
|
||||
|
||||
src = Image.open(SOURCE_PNG).convert("RGBA")
|
||||
if src.size[0] < 256 or src.size[1] < 256:
|
||||
sys.stderr.write(
|
||||
f"Source icon is {src.size}; recommend 256x256 or larger "
|
||||
"so downscaled tiers look crisp.\n"
|
||||
)
|
||||
|
||||
ico_path = BUILD_DIR / "icon.ico"
|
||||
src.save(ico_path, format="ICO", sizes=ICO_SIZES)
|
||||
print(f"wrote {ico_path} ({ico_path.stat().st_size:,} bytes)")
|
||||
|
||||
icns_path = BUILD_DIR / "icon.icns"
|
||||
# Pillow's ICNS writer derives the per-tier sizes from the source
|
||||
# image; passing a 256x256 source yields ic07..ic12 entries which
|
||||
# cover Finder, Dock, and the Get Info panel.
|
||||
src.save(icns_path, format="ICNS")
|
||||
print(f"wrote {icns_path} ({icns_path.stat().st_size:,} bytes)")
|
||||
|
||||
# AppImage uses a plain PNG for its desktop entry. Copy the source
|
||||
# so the AppImage build script doesn't have to know the asset path.
|
||||
png_path = BUILD_DIR / "icon.png"
|
||||
src.save(png_path, format="PNG")
|
||||
print(f"wrote {png_path} ({png_path.stat().st_size:,} bytes)")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user