feat: Tier B operator scaffolding — bundle, copy SoT, posts, emails
Pick up and finish yesterday's cut-off Tier B pass. - build/: PyInstaller scaffold (datatools.spec + launcher.py + hook-streamlit.py + README) — folder-mode bundle, locked 127.0.0.1, per-OS recipe - marketing/COPY.md: single source of truth for every customer-facing string — landing H1/sub/CTAs, demo CTAs, email subjects, Gumroad listing, banned phrases - marketing/community-posts/: 9 drafts (3 posts × 3 niches: bookkeeper, revops, shopify-pet) — story / tip / soft-offer - marketing/emails/: 18 drafts (Gumroad delivery + 5-touch onboarding × 3 niches), per-niche segmentation guidance - docs/NEXT-STEPS.md: flip 2.2 / 2.4 / 3.1 / 3.4 to done with pointers to the new assets; add Phase 0 inventory rows - .gitignore: narrow `build/` ignore so PyInstaller spec + launcher + hooks get tracked, only generated artifacts (build/build/, build/__pycache__/, build/dist/) stay ignored Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
153
build/datatools.spec
Normal file
153
build/datatools.spec
Normal file
@@ -0,0 +1,153 @@
|
||||
# PyInstaller spec for DataTools.
|
||||
#
|
||||
# Build (from the repo root, after ``pip install pyinstaller``):
|
||||
#
|
||||
# pyinstaller build/datatools.spec
|
||||
#
|
||||
# Output: ``dist/DataTools/`` (folder mode) and ``dist/DataTools.exe``
|
||||
# (or platform equivalent) on Windows; ``dist/DataTools.app`` on macOS
|
||||
# when packaged via ``--target-arch universal2``. See ``build/README.md``
|
||||
# for the full per-platform recipe.
|
||||
#
|
||||
# Why folder-mode (one-dir) is the default:
|
||||
# * Streamlit's static assets + Python interpreter + ~300 MB of deps
|
||||
# compress poorly into onefile. Onefile mode unpacks every launch
|
||||
# to a temp dir — adds 5-15 s startup latency that confuses
|
||||
# non-technical buyers ("did it crash?").
|
||||
# * Folder mode lets the installer (Inno Setup on Win, .dmg on Mac)
|
||||
# run a one-time copy. Subsequent launches are instant.
|
||||
#
|
||||
# Cross-platform note: this single spec file is built ON each target
|
||||
# platform. Cross-compilation isn't supported — Mac builds need a
|
||||
# Mac, Windows builds need a Windows machine (or a Windows GitHub
|
||||
# Actions runner). See build/README.md for the matrix recipe.
|
||||
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
from pathlib import Path
|
||||
from PyInstaller.utils.hooks import (
|
||||
collect_all,
|
||||
collect_data_files,
|
||||
collect_submodules,
|
||||
)
|
||||
|
||||
# Repo root from this spec's location (PyInstaller sets SPECPATH).
|
||||
REPO = Path(SPECPATH).resolve().parent
|
||||
|
||||
# ----- Hidden imports ------------------------------------------------
|
||||
# PyInstaller's static analyser misses everything Streamlit reaches
|
||||
# through ``importlib`` and the per-tool registries our app uses. We
|
||||
# exhaustively pull every submodule of the libraries that bridge
|
||||
# user code to runtime — better a 50 MB-bigger bundle than a runtime
|
||||
# ImportError on the buyer's machine.
|
||||
|
||||
hidden_imports: list[str] = []
|
||||
hidden_imports += collect_submodules("streamlit")
|
||||
hidden_imports += collect_submodules("pandas")
|
||||
hidden_imports += collect_submodules("phonenumbers")
|
||||
hidden_imports += collect_submodules("rapidfuzz")
|
||||
hidden_imports += collect_submodules("charset_normalizer")
|
||||
hidden_imports += collect_submodules("openpyxl")
|
||||
hidden_imports += collect_submodules("loguru")
|
||||
|
||||
# Our own engine + GUI modules. Even though we import them directly
|
||||
# at the top of ``launcher.py`` / ``app.py``, the Streamlit
|
||||
# session-state and per-page page discovery layers re-import via
|
||||
# names that PyInstaller doesn't see.
|
||||
hidden_imports += collect_submodules("src")
|
||||
|
||||
# ----- Data files ---------------------------------------------------
|
||||
# Streamlit's static assets (the JS / CSS / fonts the browser fetches
|
||||
# from the bundled HTTP server) are NOT Python files; PyInstaller
|
||||
# can't auto-find them.
|
||||
|
||||
datas: list[tuple[str, str]] = []
|
||||
|
||||
# Streamlit's runtime assets.
|
||||
datas += collect_data_files("streamlit", include_py_files=False)
|
||||
|
||||
# phonenumbers ships its country/area-code metadata as resources.
|
||||
datas += collect_data_files("phonenumbers", include_py_files=False)
|
||||
|
||||
# Our application files. PyInstaller's bundler treats source as code
|
||||
# (.pyc) by default; we add it again as data so the launcher's
|
||||
# ``Path(sys._MEIPASS) / "src" / "gui" / "app.py"`` resolution works.
|
||||
datas += [
|
||||
(str(REPO / "src"), "src"),
|
||||
(str(REPO / "samples" / "demo"), "samples/demo"),
|
||||
(str(REPO / ".streamlit" / "config.toml"),".streamlit"),
|
||||
]
|
||||
|
||||
# ----- Analysis ------------------------------------------------------
|
||||
|
||||
a = Analysis(
|
||||
[str(REPO / "build" / "launcher.py")],
|
||||
pathex=[str(REPO)],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hidden_imports,
|
||||
hookspath=[str(REPO / "build" / "hooks")],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[
|
||||
# Ship-trim — PyInstaller pulls these in but we never need
|
||||
# them, and they add ~80 MB combined.
|
||||
"tkinter",
|
||||
"matplotlib",
|
||||
"scipy",
|
||||
"IPython",
|
||||
"jupyter",
|
||||
"notebook",
|
||||
"test",
|
||||
"tests",
|
||||
],
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="DataTools",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False, # GUI app — no terminal window on Win/Mac
|
||||
disable_windowed_traceback=False,
|
||||
icon=str(REPO / "build" / "icon.icns") if (REPO / "build" / "icon.icns").exists() else None,
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name="DataTools",
|
||||
)
|
||||
|
||||
# macOS .app bundle wrapper. PyInstaller produces it only on Mac;
|
||||
# this block is a no-op on Win/Linux.
|
||||
import sys as _sys
|
||||
if _sys.platform == "darwin":
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name="DataTools.app",
|
||||
icon=str(REPO / "build" / "icon.icns") if (REPO / "build" / "icon.icns").exists() else None,
|
||||
bundle_identifier="com.datatools.desktop",
|
||||
info_plist={
|
||||
"CFBundleDisplayName": "DataTools",
|
||||
"CFBundleVersion": "1.0.0",
|
||||
"CFBundleShortVersionString": "1.0.0",
|
||||
"NSHighResolutionCapable": True,
|
||||
# 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
|
||||
# see the app is running while the browser tab is open.
|
||||
"LSUIElement": False,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user