build: wire desktop-bundle pipeline (CI matrix + per-platform installers)
Stand up the seamless-download path for non-technical buyers:
* .github/workflows/build.yml — matrix CI (mac/win/linux) that builds
PyInstaller bundles and packages them per platform on tag push,
attaching the resulting installers to a GitHub Release.
* build/installer.iss — Inno Setup script for the Windows installer
(per-user install, optional desktop shortcut, runs on finish).
* build/macos/build_dmg.sh — wraps DataTools.app into a .dmg with a
drag-to-/Applications layout.
* build/appimage/{AppRun,datatools.desktop,build.sh} — AppImage recipe.
* src/__init__.py — single source of truth for __version__; the spec
reads it (was hardcoded), CI passes it through to all packagers.
Buyer download path now lives in the top-level README. Per-build
README documents the Phase 2 step (signing/notarization) that needs
the owner's Apple Developer + Windows code-signing credentials —
those are intentionally not in CI yet because they require setup
outside this repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
108
.github/workflows/build.yml
vendored
Normal file
108
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
name: Build installers
|
||||||
|
|
||||||
|
# Triggers:
|
||||||
|
# * Tag push (v*) → produces installers, attaches to a GitHub Release.
|
||||||
|
# * Manual dispatch → produces installers as workflow artifacts only.
|
||||||
|
#
|
||||||
|
# What this workflow doesn't do (yet):
|
||||||
|
# * Code signing (Mac Developer ID, Windows code-signing cert).
|
||||||
|
# Those need GitHub Secrets the owner sets up first. See
|
||||||
|
# build/README.md "Signing" for the secret names this workflow
|
||||||
|
# will read once they exist.
|
||||||
|
# * Auto-update endpoint generation. v1 distributes via Gumroad;
|
||||||
|
# buyers re-download for updates.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # needed to create the release on tag push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build (${{ matrix.os }})
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macos-latest
|
||||||
|
artifact_name: DataTools-mac.dmg
|
||||||
|
artifact_path: dist/DataTools-*-mac.dmg
|
||||||
|
- os: windows-latest
|
||||||
|
artifact_name: DataTools-win.exe
|
||||||
|
artifact_path: dist/DataTools-*-win-setup.exe
|
||||||
|
- os: ubuntu-latest
|
||||||
|
artifact_name: DataTools-linux.AppImage
|
||||||
|
artifact_path: dist/DataTools-*-linux-x86_64.AppImage
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
cache: pip
|
||||||
|
|
||||||
|
- name: Install build deps
|
||||||
|
run: |
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install pyinstaller
|
||||||
|
|
||||||
|
- name: Read version
|
||||||
|
id: version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VER=$(python -c "import re; print(re.search(r'__version__\s*=\s*\"([^\"]+)\"', open('src/__init__.py').read()).group(1))")
|
||||||
|
echo "version=$VER" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build PyInstaller bundle
|
||||||
|
run: pyinstaller build/datatools.spec --clean --noconfirm
|
||||||
|
|
||||||
|
# ---- Per-platform packaging ----------------------------------
|
||||||
|
|
||||||
|
- name: Package macOS DMG
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: bash build/macos/build_dmg.sh "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Install Inno Setup (Windows)
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: choco install innosetup --no-progress -y
|
||||||
|
|
||||||
|
- name: Package Windows installer
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
shell: cmd
|
||||||
|
run: |
|
||||||
|
iscc /DAppVersion=${{ steps.version.outputs.version }} build\installer.iss
|
||||||
|
|
||||||
|
- name: Install AppImage tooling (Linux)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libfuse2 wget
|
||||||
|
wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /usr/local/bin/appimagetool
|
||||||
|
sudo chmod +x /usr/local/bin/appimagetool
|
||||||
|
|
||||||
|
- name: Package Linux AppImage
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: bash build/appimage/build.sh "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
# ---- Upload + release ----------------------------------------
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact_name }}
|
||||||
|
path: ${{ matrix.artifact_path }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Attach to Release (tag push only)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: ${{ matrix.artifact_path }}
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
generate_release_notes: true
|
||||||
14
README.md
14
README.md
@@ -16,7 +16,19 @@ Local CSV / Excel cleaning. CLI + browser GUI, no cloud, no install ceremony.
|
|||||||
| 08 | Validator & Reporter | Coming Soon |
|
| 08 | Validator & Reporter | Coming Soon |
|
||||||
| 09 | **Pipeline Runner** — chain tools with recommended (not forced) order, save/load JSON, automate weekly cleanups | Ready |
|
| 09 | **Pipeline Runner** — chain tools with recommended (not forced) order, save/load JSON, automate weekly cleanups | Ready |
|
||||||
|
|
||||||
## Install
|
## Download (non-technical users)
|
||||||
|
|
||||||
|
Pre-built installers — no Python required:
|
||||||
|
|
||||||
|
| Platform | Download | First-launch note |
|
||||||
|
|---|---|---|
|
||||||
|
| **macOS** | `DataTools-X.Y.Z-mac.dmg` | Drag DataTools.app into /Applications, then double-click. |
|
||||||
|
| **Windows** | `DataTools-X.Y.Z-win-setup.exe` | Run the installer; launches from Start Menu. |
|
||||||
|
| **Linux** | `DataTools-X.Y.Z-linux-x86_64.AppImage` | `chmod +x` the file, then double-click. |
|
||||||
|
|
||||||
|
Latest release: see [GitHub Releases](https://git.invixiom.com/giteadmin/datatools-dev/releases) (or the Gumroad listing). The installers are ~150–200 MB; the launcher boots a local server at http://127.0.0.1:8501 and opens your browser. Nothing is sent to the cloud.
|
||||||
|
|
||||||
|
## Install from source (developers)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|||||||
@@ -16,15 +16,75 @@ build/
|
|||||||
│ Streamlit server, opens browser, locks server
|
│ Streamlit server, opens browser, locks server
|
||||||
│ to 127.0.0.1 so the privacy claim holds.
|
│ to 127.0.0.1 so the privacy claim holds.
|
||||||
├── datatools.spec PyInstaller spec — hidden imports, data files,
|
├── datatools.spec PyInstaller spec — hidden imports, data files,
|
||||||
│ Mac .app bundle config.
|
│ Mac .app bundle config. Reads the version
|
||||||
|
│ from src/__init__.py.
|
||||||
|
├── installer.iss Inno Setup script — Windows .exe installer.
|
||||||
|
├── macos/
|
||||||
|
│ └── build_dmg.sh Wraps dist/DataTools.app into a .dmg with a
|
||||||
|
│ drag-to-/Applications layout.
|
||||||
|
├── appimage/
|
||||||
|
│ ├── AppRun Entry point invoked when the AppImage runs.
|
||||||
|
│ ├── datatools.desktop Linux desktop-entry metadata.
|
||||||
|
│ └── build.sh Wraps dist/DataTools/ into an .AppImage.
|
||||||
├── hooks/ PyInstaller hooks for libs the static analyser
|
├── hooks/ PyInstaller hooks for libs the static analyser
|
||||||
│ └── hook-streamlit.py misses (Streamlit's dynamic imports).
|
│ └── hook-streamlit.py misses (Streamlit's dynamic imports).
|
||||||
├── icon.icns macOS app icon (TODO: produce from a 1024×1024
|
├── icon.icns macOS app icon (TODO: produce from a 1024×1024
|
||||||
│ PNG. Optional — bundle still builds without).
|
│ PNG. Optional — bundle still builds without).
|
||||||
├── icon.ico Windows app icon (TODO).
|
├── icon.ico Windows app icon (TODO).
|
||||||
|
├── icon.png Linux AppImage icon (TODO — build.sh generates
|
||||||
|
│ a placeholder if missing).
|
||||||
└── README.md this file
|
└── README.md this file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
CI: `.github/workflows/build.yml` runs the full pipeline on tag push
|
||||||
|
(matrix: macos-latest, windows-latest, ubuntu-latest) and attaches
|
||||||
|
the resulting installers to a GitHub Release. Manual
|
||||||
|
`workflow_dispatch` runs upload them as workflow artifacts only.
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
1. Bump `__version__` in `src/__init__.py`.
|
||||||
|
2. `git commit -am "release: vX.Y.Z" && git tag vX.Y.Z`.
|
||||||
|
3. `git push && git push --tags`.
|
||||||
|
4. CI builds all three platforms and creates a GitHub Release with
|
||||||
|
the installers attached.
|
||||||
|
5. Mirror the GitHub Release assets to Gumroad (manual until v2).
|
||||||
|
|
||||||
|
## Signing (Phase 2 — needs accounts/credentials)
|
||||||
|
|
||||||
|
Both code-signing steps are intentionally not in CI yet because they
|
||||||
|
require credentials the owner sets up first.
|
||||||
|
|
||||||
|
**macOS** — Apple Developer Program enrollment ($99/yr). Once enrolled,
|
||||||
|
add these GitHub Secrets and uncomment the `codesign` + `notarytool`
|
||||||
|
steps in `build.yml`:
|
||||||
|
|
||||||
|
| Secret | Value |
|
||||||
|
|---|---|
|
||||||
|
| `MACOS_DEVELOPER_ID_CERT_P12_BASE64` | base64-encoded `.p12` cert |
|
||||||
|
| `MACOS_DEVELOPER_ID_CERT_PASSWORD` | password for the .p12 |
|
||||||
|
| `MACOS_NOTARY_APPLE_ID` | Apple ID email |
|
||||||
|
| `MACOS_NOTARY_TEAM_ID` | 10-char team ID |
|
||||||
|
| `MACOS_NOTARY_PASSWORD` | app-specific password |
|
||||||
|
|
||||||
|
**Windows** — Code-signing cert from Sectigo / DigiCert (~$200-400/yr,
|
||||||
|
or ~$300-500 for an EV cert that bypasses SmartScreen). Add:
|
||||||
|
|
||||||
|
| Secret | Value |
|
||||||
|
|---|---|
|
||||||
|
| `WINDOWS_CERT_PFX_BASE64` | base64-encoded `.pfx` cert |
|
||||||
|
| `WINDOWS_CERT_PASSWORD` | password for the .pfx |
|
||||||
|
|
||||||
|
Until those are wired, buyers will see:
|
||||||
|
- macOS: "DataTools is damaged and can't be opened" — fix by removing
|
||||||
|
the quarantine attribute (`xattr -cr /Applications/DataTools.app`).
|
||||||
|
Acceptable for the technical buyer; **blocking** for the
|
||||||
|
non-technical buyer. Don't ship to non-technical without notarization.
|
||||||
|
- Windows: SmartScreen "Windows protected your PC" — buyer clicks
|
||||||
|
"More info → Run anyway". Friction but not blocking.
|
||||||
|
- Linux: AppImage runs without complaint (Linux has no equivalent
|
||||||
|
trust-store).
|
||||||
|
|
||||||
## Per-platform recipe
|
## Per-platform recipe
|
||||||
|
|
||||||
Each platform builds on its own machine — PyInstaller does **not**
|
Each platform builds on its own machine — PyInstaller does **not**
|
||||||
|
|||||||
8
build/appimage/AppRun
Executable file
8
build/appimage/AppRun
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# AppImage entry point. AppImage mounts the bundle and runs this
|
||||||
|
# script. We chdir into the embedded usr/bin so the PyInstaller
|
||||||
|
# bundle's relative paths resolve, then exec the launcher binary.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
HERE="$(dirname -- "$(readlink -f -- "${0}")")"
|
||||||
|
exec "${HERE}/usr/bin/DataTools/DataTools" "$@"
|
||||||
62
build/appimage/build.sh
Executable file
62
build/appimage/build.sh
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Wrap dist/DataTools/ (PyInstaller folder mode) into a distributable
|
||||||
|
# AppImage.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash build/appimage/build.sh <version>
|
||||||
|
#
|
||||||
|
# Requires ``appimagetool`` on PATH (CI installs it; locally grab the
|
||||||
|
# latest release from https://github.com/AppImage/AppImageKit/releases).
|
||||||
|
#
|
||||||
|
# Output: dist/DataTools-<version>-linux-x86_64.AppImage
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="${1:-0.0.0-dev}"
|
||||||
|
DIST="dist/DataTools"
|
||||||
|
OUT="dist/DataTools-${VERSION}-linux-x86_64.AppImage"
|
||||||
|
|
||||||
|
if [[ ! -d "$DIST" ]]; then
|
||||||
|
echo "Error: $DIST not found. Run pyinstaller build/datatools.spec first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v appimagetool >/dev/null 2>&1; then
|
||||||
|
echo "Error: appimagetool not on PATH. See build/appimage/build.sh header." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Lay out the AppDir.
|
||||||
|
APPDIR="$(mktemp -d)/DataTools.AppDir"
|
||||||
|
trap 'rm -rf "$(dirname -- "$APPDIR")"' EXIT
|
||||||
|
mkdir -p "$APPDIR/usr/bin"
|
||||||
|
|
||||||
|
cp -R "$DIST" "$APPDIR/usr/bin/"
|
||||||
|
cp build/appimage/AppRun "$APPDIR/AppRun"
|
||||||
|
chmod +x "$APPDIR/AppRun"
|
||||||
|
cp build/appimage/datatools.desktop "$APPDIR/datatools.desktop"
|
||||||
|
|
||||||
|
# Icon. AppImage requires a top-level <appname>.png next to the
|
||||||
|
# .desktop. Use the build/icon.png if present, otherwise generate a
|
||||||
|
# blank placeholder so the build doesn't fail on a fresh checkout.
|
||||||
|
if [[ -f build/icon.png ]]; then
|
||||||
|
cp build/icon.png "$APPDIR/datatools.png"
|
||||||
|
else
|
||||||
|
# 256x256 single-colour PNG via printf — appimagetool needs *some*
|
||||||
|
# icon present. Replace with a real 1024x1024 PNG before launch.
|
||||||
|
python3 - <<'PY'
|
||||||
|
import struct, zlib, os
|
||||||
|
def chunk(t, d): return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xffffffff)
|
||||||
|
W = H = 256
|
||||||
|
ihdr = struct.pack(">IIBBBBB", W, H, 8, 2, 0, 0, 0) # 8-bit RGB
|
||||||
|
raw = b"".join(b"\x00" + b"\x16\x19\x22" * W for _ in range(H)) # filter byte + dark pixels
|
||||||
|
idat = zlib.compress(raw, 9)
|
||||||
|
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", idat) + chunk(b"IEND", b"")
|
||||||
|
out = os.environ["APPDIR"] + "/datatools.png"
|
||||||
|
open(out, "wb").write(png)
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
export APPDIR
|
||||||
|
|
||||||
|
ARCH=x86_64 appimagetool "$APPDIR" "$OUT"
|
||||||
|
echo "Built $OUT"
|
||||||
8
build/appimage/datatools.desktop
Normal file
8
build/appimage/datatools.desktop
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=DataTools
|
||||||
|
Comment=Local CSV / Excel cleaning suite
|
||||||
|
Exec=DataTools
|
||||||
|
Icon=datatools
|
||||||
|
Categories=Office;Utility;
|
||||||
|
Terminal=false
|
||||||
@@ -34,6 +34,14 @@ from PyInstaller.utils.hooks import (
|
|||||||
# Repo root from this spec's location (PyInstaller sets SPECPATH).
|
# Repo root from this spec's location (PyInstaller sets SPECPATH).
|
||||||
REPO = Path(SPECPATH).resolve().parent
|
REPO = Path(SPECPATH).resolve().parent
|
||||||
|
|
||||||
|
# Single source of truth for the version string. Read directly from
|
||||||
|
# src/__init__.py instead of importing src/ — importing pulls in
|
||||||
|
# heavy deps (pandas etc) that PyInstaller's spec parser doesn't need.
|
||||||
|
import re as _re
|
||||||
|
_init_py = (REPO / "src" / "__init__.py").read_text(encoding="utf-8")
|
||||||
|
_m = _re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', _init_py)
|
||||||
|
VERSION = _m.group(1) if _m else "0.0.0"
|
||||||
|
|
||||||
# ----- Hidden imports ------------------------------------------------
|
# ----- Hidden imports ------------------------------------------------
|
||||||
# PyInstaller's static analyser misses everything Streamlit reaches
|
# PyInstaller's static analyser misses everything Streamlit reaches
|
||||||
# through ``importlib`` and the per-tool registries our app uses. We
|
# through ``importlib`` and the per-tool registries our app uses. We
|
||||||
@@ -142,8 +150,8 @@ if _sys.platform == "darwin":
|
|||||||
bundle_identifier="com.datatools.desktop",
|
bundle_identifier="com.datatools.desktop",
|
||||||
info_plist={
|
info_plist={
|
||||||
"CFBundleDisplayName": "DataTools",
|
"CFBundleDisplayName": "DataTools",
|
||||||
"CFBundleVersion": "1.0.0",
|
"CFBundleVersion": VERSION,
|
||||||
"CFBundleShortVersionString": "1.0.0",
|
"CFBundleShortVersionString": VERSION,
|
||||||
"NSHighResolutionCapable": True,
|
"NSHighResolutionCapable": True,
|
||||||
# Buyer's macOS will not show the app's window in the dock
|
# Buyer's macOS will not show the app's window in the dock
|
||||||
# if this is True. We want the dock icon so the buyer can
|
# if this is True. We want the dock icon so the buyer can
|
||||||
|
|||||||
49
build/installer.iss
Normal file
49
build/installer.iss
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
; Inno Setup script for DataTools — Windows installer.
|
||||||
|
;
|
||||||
|
; Compile from the repo root:
|
||||||
|
; iscc /DAppVersion=1.0.0 build\installer.iss
|
||||||
|
;
|
||||||
|
; CI passes the version via /DAppVersion to keep src/__init__.py the
|
||||||
|
; single source of truth. Local manual builds: pass /DAppVersion or
|
||||||
|
; let the default kick in.
|
||||||
|
|
||||||
|
#ifndef AppVersion
|
||||||
|
#define AppVersion "0.0.0-dev"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
AppId={{D4A07001-DA7A-4001-8001-DA7A70013700}}
|
||||||
|
AppName=DataTools
|
||||||
|
AppVersion={#AppVersion}
|
||||||
|
AppVerName=DataTools {#AppVersion}
|
||||||
|
AppPublisher=DataTools
|
||||||
|
AppPublisherURL=https://datatools.app
|
||||||
|
DefaultDirName={autopf}\DataTools
|
||||||
|
DefaultGroupName=DataTools
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
OutputDir=..\dist
|
||||||
|
OutputBaseFilename=DataTools-{#AppVersion}-win-setup
|
||||||
|
Compression=lzma2/max
|
||||||
|
SolidCompression=yes
|
||||||
|
WizardStyle=modern
|
||||||
|
ArchitecturesInstallIn64BitMode=x64
|
||||||
|
PrivilegesRequired=lowest
|
||||||
|
PrivilegesRequiredOverridesAllowed=dialog
|
||||||
|
; Allow per-user install (no UAC prompt) when admin isn't available.
|
||||||
|
|
||||||
|
[Languages]
|
||||||
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
|
[Tasks]
|
||||||
|
Name: "desktopicon"; Description: "Create a &desktop shortcut"; GroupDescription: "Additional shortcuts:"
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: "..\dist\DataTools\*"; DestDir: "{app}"; Flags: recursesubdirs ignoreversion
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\DataTools"; Filename: "{app}\DataTools.exe"
|
||||||
|
Name: "{group}\Uninstall DataTools"; Filename: "{uninstallexe}"
|
||||||
|
Name: "{autodesktop}\DataTools"; Filename: "{app}\DataTools.exe"; Tasks: desktopicon
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{app}\DataTools.exe"; Description: "Launch DataTools"; Flags: nowait postinstall skipifsilent
|
||||||
41
build/macos/build_dmg.sh
Executable file
41
build/macos/build_dmg.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Wrap dist/DataTools.app into a distributable .dmg.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash build/macos/build_dmg.sh <version>
|
||||||
|
#
|
||||||
|
# Run after ``pyinstaller build/datatools.spec --clean --noconfirm``
|
||||||
|
# has produced ``dist/DataTools.app``. The output DMG goes to
|
||||||
|
# ``dist/DataTools-<version>-mac.dmg``.
|
||||||
|
#
|
||||||
|
# Code signing + notarization happen separately (see build/README.md
|
||||||
|
# "Signing"). This script only handles the packaging step.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="${1:-0.0.0-dev}"
|
||||||
|
APP="dist/DataTools.app"
|
||||||
|
DMG="dist/DataTools-${VERSION}-mac.dmg"
|
||||||
|
|
||||||
|
if [[ ! -d "$APP" ]]; then
|
||||||
|
echo "Error: $APP not found. Run pyinstaller build/datatools.spec first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Drag-target convenience: a /Applications symlink inside the DMG so
|
||||||
|
# the buyer can drag the app icon to it without leaving the DMG.
|
||||||
|
STAGE="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
|
|
||||||
|
cp -R "$APP" "$STAGE/"
|
||||||
|
ln -s /Applications "$STAGE/Applications"
|
||||||
|
|
||||||
|
# UDZO = compressed read-only DMG, the standard distribution format.
|
||||||
|
hdiutil create \
|
||||||
|
-volname "DataTools" \
|
||||||
|
-srcfolder "$STAGE" \
|
||||||
|
-ov \
|
||||||
|
-format UDZO \
|
||||||
|
"$DMG"
|
||||||
|
|
||||||
|
echo "Built $DMG"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
"""DataTools — local CSV / Excel cleaning suite.
|
||||||
|
|
||||||
|
Version is the single source of truth read by the PyInstaller spec,
|
||||||
|
the Windows Inno Setup script, the macOS DMG packaging script, and
|
||||||
|
the AppImage build. Bump here on release and CI propagates it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user