Skip to content

feat(template): split standalone-executable toggle into launcher/compiler/freezer - #43

Merged
hasansezertasan merged 9 commits into
mainfrom
feat/atom-capricorn
Jun 26, 2026
Merged

feat(template): split standalone-executable toggle into launcher/compiler/freezer#43
hasansezertasan merged 9 commits into
mainfrom
feat/atom-capricorn

Conversation

@hasansezertasan

@hasansezertasan hasansezertasan commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces the single include_pycrucible toggle with three independent, combinable toggles, each mapped to a distinct standalone-executable architecture:

Toggle Tool Architecture Runtime promise
include_launcher PyCrucible uv-bootstrap launcher ~2 MB binary, downloads Python+deps on first run
include_compiler Nuitka source→machine code compiled native binary, faster + obfuscated
include_freezer PyInstaller interpreter+deps freezer self-contained, fully offline

The previous toggle conflated a brand (PyCrucible) with a capability and covered only one of three ecosystem architectures. Architecture-named booleans let a project combine paths (e.g. a small launcher for connected users and an offline freezer for air-gapped distribution) and survive a future tool swap without renaming. See ADR-007.

Changes

  • copier.yml — three flat booleans (no when: gating)
  • template/pyproject.toml.jinja — keywords, tool-group deps (pycrucible/nuitka/pyinstaller), [tool.pycrucible] guard renamed to include_launcher
  • new template/{% if include_freezer %}{{github_repo_name}}.spec{% endif %}.jinja — PyInstaller spec with entrypoint resolution (cli→gui→tui→web→__main__) and console=False for GUI
  • template/mise.toml.jinja — split the single {% raw %} block (fixes a latent bug where the PyCrucible task rendered unconditionally) into gated package/freeze/compile tasks, all via uv run --locked --group tool
  • template/.github/workflows/release-please.yml.jinjabuild-launcher/build-freezer/build-compiler jobs (fail-fast: false), needs: + download wiring
  • docs — new ADR-007; ADR-002, README, CLAUDE.md updated

Verification

  • Renders cleanly with all three on, a single toggle on, and all off (negative test confirms spec/tasks/jobs/tool-blocks are absent when off)
  • validate-pyproject ✅ · mise + pyproject parse as valid TOML ✅ · workflow parses as valid YAML ✅ · actionlint exit 0 ✅

Breaking change

include_pycrucible: trueinclude_launcher: true on copier update (Copier does not auto-migrate renamed variables; documented in ADR-007).

🤖 Generated with Claude Code

Summary by Sourcery

Split the previous single standalone-executable option into three independent launcher, compiler, and freezer toggles and wire them through configuration, build tooling, CI, and docs.

New Features:

  • Introduce separate include_launcher, include_compiler, and include_freezer template toggles for different standalone-executable architectures, each with its own tooling (PyCrucible, Nuitka, PyInstaller).
  • Add a conditional PyInstaller .spec template to build self-contained offline executables when the freezer option is enabled.

Enhancements:

  • Update pyproject tooling dependencies, keywords, and PyCrucible configuration to align with the new launcher/compiler/freezer toggles.
  • Extend the release workflow to run separate launcher, freezer, and compiler build jobs and attach their artifacts independently to releases.
  • Refine mise tasks into per-architecture package/freeze/compile commands guarded by the new toggles.
  • Adjust example input and helper documentation (README, CLAUDE guide, ADR-002) to describe and use the new executable options and workflows.
  • Document the new executable architecture choices and toggle semantics in ADR-007, including migration from the legacy include_pycrucible toggle.

Documentation:

  • Add ADR-007 describing the three standalone-executable architectures and how the new toggles map to them and to the build pipeline.

…iler/freezer

Replace the single `include_pycrucible` toggle with three independent,
combinable toggles mapped to distinct architectures: `include_launcher`
(PyCrucible uv-bootstrap launcher), `include_compiler` (Nuitka native
compiler), and `include_freezer` (PyInstaller offline freezer).

- copier.yml: three flat booleans (no `when:` gating)
- pyproject.toml.jinja: keywords, `tool`-group deps, renamed `[tool.pycrucible]` guard
- new conditional PyInstaller `.spec` file with entrypoint resolution
- mise.toml.jinja: split the single `{% raw %}` block (fixing a latent bug where
  the PyCrucible task rendered unconditionally) into gated package/freeze/compile tasks
- release-please.yml.jinja: build-launcher/build-freezer/build-compiler matrix
  jobs (fail-fast: false) with needs/download wiring
- docs: ADR-007, plus ADR-002/README/CLAUDE.md updates

See ADR-007 for the design rationale.
@sourcery-ai

sourcery-ai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Splits the previous single include_pycrucible toggle into three independent executable-architecture toggles (include_launcher, include_compiler, include_freezer), wires them through Copier prompts, pyproject dependencies/config, mise tasks, GitHub release workflow, a new PyInstaller spec file, and documentation/ADR updates, while renaming and extending the CI jobs and artifacts accordingly.

File-Level Changes

Change Details Files
Replace single PyCrucible toggle with three standalone-executable toggles and propagate them through configuration, tooling, and docs.
  • Rename include_pycrucible to include_launcher in Copier config, example input, and template conditionals, and add include_compiler and include_freezer booleans with descriptive help text.
  • Extend pyproject.toml template to add standalone-related keywords and conditionally include pycrucible, nuitka, and pyinstaller in the tool dependency group alongside a renamed [tool.pycrucible] block guarded by include_launcher.
  • Update README and CLAUDE documentation to describe the three toggles, their architectures and behavior, the new conditional files, and the adjusted release workflow narrative.
  • Introduce ADR-007 documenting the architectural split (launcher/compiler/freezer), rationale, chosen tools, and consequences, and update ADR-002 to reference the new jobs and ADR.
  • Add .example-input.yml entries for the three toggles to keep example answers in sync with Copier prompts.
copier.yml
.example-input.yml
template/pyproject.toml.jinja
README.md
CLAUDE.md
docs/adr/002-release-please-for-release-automation.md
docs/adr/007-offline-freezer-alongside-pycrucible.md
Add PyInstaller-based freezer support via a conditional spec file, local task, and CI job.
  • Create a conditional PyInstaller spec template that selects the entrypoint based on CLI/GUI/TUI/web flags or falls back to __main__, configures a single bundled executable, and disables the console for GUI apps.
  • Add a freeze task to mise.toml gated by include_freezer that runs pyinstaller via uv run --locked --group tool using the generated spec file.
  • Introduce a build-freezer matrix job in the release workflow that sets up uv, runs PyInstaller, and uploads -executable-freezer-<os> artifacts consumed by the attach-release step.
template/{{% if include_freezer %}}{{github_repo_name}}.spec{{% endif %}}.jinja
template/mise.toml.jinja
template/.github/workflows/release-please.yml.jinja
Add Nuitka-based compiler support via conditional tooling, task, and CI job sharing the template’s entrypoint resolution logic.
  • Include nuitka in the tool dependency group when include_compiler is enabled.
  • Add a compile task in the mise config (gated by include_compiler) that builds a one-file Nuitka binary via uv run, selecting the entrypoint using the same CLI/GUI/TUI/web logic as elsewhere.
  • Add a build-compiler matrix job in the release workflow that mirrors build-freezer’s setup, calls Nuitka with matching entrypoint selection, and uploads -executable-compiler-<os> artifacts for the release attach job.
template/pyproject.toml.jinja
template/mise.toml.jinja
template/.github/workflows/release-please.yml.jinja
Refactor PyCrucible launcher integration to align with the new include_launcher toggle and improve task execution.
  • Rename the CI job from build-executables to build-launcher, update its artifact naming to include -executable-launcher-<os>, and add fail-fast: false to its matrix strategy.
  • Change the mise package task to be conditional on include_launcher, run PyCrucible via uv run --locked --group tool, and output to a repo-name-based dist path.
  • Guard the [tool.pycrucible] configuration block in pyproject.toml with include_launcher and keep entrypoint resolution aligned with the other executable paths.
template/.github/workflows/release-please.yml.jinja
template/mise.toml.jinja
template/pyproject.toml.jinja
Update release workflow orchestration so attach/finalize jobs correctly depend on all enabled executable builds and download artifacts generically.
  • Replace the single build-executables job in the release workflow graph with separate build-launcher, build-freezer, and build-compiler jobs, all conditional, matrixed, and fail-fast: false.
  • Update the attach-github-release job’s needs list to optionally include each executable job based on the toggles, while leaving web/docker publishing behavior unchanged.
  • Broaden the condition for downloading executables in attach-github-release to trigger when any of the three executable toggles is enabled, aligning with artifact naming and the documented download pattern.
template/.github/workflows/release-please.yml.jinja
CLAUDE.md
README.md
docs/adr/002-release-please-for-release-automation.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The entrypoint resolution logic for CLI/GUI/TUI/web/main is duplicated across the PyInstaller spec, Nuitka commands, and workflow; consider extracting this into a shared Jinja macro or variable to keep it consistent and easier to change.
  • The Nuitka build command (flags and output conventions) is currently repeated in both the mise task and the GitHub Actions workflow; you could centralize this in a single template construct to avoid drift between local and CI builds.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The entrypoint resolution logic for CLI/GUI/TUI/web/__main__ is duplicated across the PyInstaller spec, Nuitka commands, and workflow; consider extracting this into a shared Jinja macro or variable to keep it consistent and easier to change.
- The Nuitka build command (flags and output conventions) is currently repeated in both the mise task and the GitHub Actions workflow; you could centralize this in a single template construct to avoid drift between local and CI builds.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

The ADR covers three architectures (launcher/compiler/freezer), not just
the PyCrucible/freezer angle, so the filename now matches the title.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92bb8069a7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread template/{% if include_freezer %}{{github_repo_name}}.spec{% endif %}.jinja Outdated
Comment thread template/.github/workflows/release-please.yml.jinja Outdated
…entrypoint

Address PR review (Codex P2s + Sourcery):

- Generalize __main__.py from CLI-only into the single runnable entrypoint that
  dispatches to whichever component is enabled (CLI/GUI/TUI/web/MCP/worker), each
  exposing a main() that actually invokes the app. Freezing/compiling a bare
  module like cli/app.py only defined `app` without calling it, so the binary
  exited without running anything.
- Point the PyInstaller spec, Nuitka (mise + CI), and [tool.pycrucible] all at
  src/<pkg>/__main__.py, so the component-selection logic lives in one place
  instead of being duplicated across three files.
- Un-ignore the committed PyInstaller spec in .gitignore (the default *.spec
  ignore would keep it uncommitted and break the release fresh-checkout build).
- Document the entrypoint/gitignore decisions in ADR-007.

Rendered output is ruff-clean across all component branches.
@hasansezertasan

Copy link
Copy Markdown
Owner Author

Thanks for the review — addressed in a5e03ec.

Codex P2 — *.spec was gitignored ✅ The generated .gitignore now un-ignores the committed spec when include_freezer is set (!{{github_repo_name}}.spec right after the default *.spec), so the release workflow's fresh checkout finds it.

Codex P2 — frozen/compiled binaries were no-ops ✅ The root cause was pointing the builders at modules like cli/app.py that only define app/main without invoking them. Fixed by generalizing src/<pkg>/__main__.py from CLI-only into the single runnable entrypoint: it now defines a main() that dispatches to whichever component is enabled (CLI/GUI/TUI/web/MCP/worker) and actually calls it. All three builders (PyInstaller spec, Nuitka mise+CI, [tool.pycrucible]) now target __main__.py.

Sourcery — duplicated entrypoint resolution across spec/Nuitka/workflow ✅ Same change resolves this: the CLI/GUI/TUI/web/__main__ selection logic now lives only in __main__.py; the three build configs reference the constant src/<pkg>/__main__.py with no per-tool if/elif chains.

Sourcery — Nuitka flags repeated in mise + CI — left as-is. With the entrypoint centralized, what remains duplicated is one short flag string (--onefile --assume-yes-for-downloads --output-dir=dist); the CI job runs uv run directly rather than shelling out to mise, and threading a shared value across the raw-TOML mise file and the YAML workflow adds more indirection than it removes. Happy to add a computed Copier variable if you'd prefer full dedup.

Verification: rendered __main__.py is ruff-clean (select = ALL) across all component branches (cli/gui/tui/web/mcp/worker/none); actionlint, validate-pyproject, and TOML/YAML parsing all pass.

…/OS and harden executable toggles

Give each build-launcher/freezer/compiler job a per-OS label/ext matrix and
name binaries <pkg>-<tool>-<label> so download-artifact's merge-multiple and
the release asset names never collide (previously a single toggle already
dropped a platform across OSes, and Nuitka shipped a generic __main__.bin).
Also propagate GUI/TUI exit codes via sys.exit, matrix-gate Nuitka's GUI
window flags, fix the gh release upload glob to skip the dist/executables dir,
redraw the README release-flow diagram, add executable-toggle CI scenarios with
a unique-naming assertion, add a __main__ entrypoint test, and clarify ADR-007.
…PR CI

Add build-{launcher,freezer,compiler}-check jobs to ci.yml, gated on the same
toggles as the release builds, so each enabled executable is built on every
PR/push (per-OS, fail-fast: false). The test suite runs the source tree and
never the built binary, so these are the only packaging guard before release:
each verifies the binary was produced, smoke-runs it with --help for a CLI
project, and uploads a 7-day preview artifact for reviewers. They mirror the
release-please.yml build commands (and unique per-OS naming) but never publish,
and they gate the check aggregation job. Add include_cli to the template-ci
launcher scenario so the CLI smoke-run steps get actionlint-ed, and document the
jobs in CLAUDE.md and ADR-007.
… no-op entrypoint

Address PR review findings on the launcher/compiler/freezer toggles:

- release-please: add setup-python to build-freezer/build-compiler, give
  the launcher checkout fetch-depth: 0 (correct hatch-vcs version), drop the
  unused ext field from the launcher matrix, and guard the release-attach
  step against an empty dist/ (xargs would otherwise publish zero assets).
- Nuitka: drop --macos-create-app-bundle (emits a .app directory that breaks
  the single-file verify/upload) and add --warn-implicit-exceptions /
  --warn-unusual-code so analysis gaps surface in the build log.
- PyInstaller spec: default upx=False (not on CI runners; corrupts some
  macOS/Windows binaries) with an opt-in TODO.
- __main__: no-component main() now raises SystemExit instead of a silent
  no-op, and documents the CLI>GUI>TUI>web>MCP>worker dispatch precedence.
- tests: pin the dispatch wiring (app/_entry resolved) and assert the
  no-component case exits loudly.
- template-ci: add a web+freezer render scenario and tighten the
  artifact-name assertion to require the matrix.label per-OS qualifier.
- docs: sync ADR-007 and CLAUDE.md to the above.
The previous commit failed template-CI style checks (ruff, run with autofix):

- __main__.py no-component branch raised SystemExit with a string literal,
  tripping EM101 (raw-string-in-exception) and DOC501 (raised exception
  missing from docstring). Switch to sys.exit("...") — same loud non-zero
  exit and message, but no `raise` statement, so neither rule fires.
- test_main.py asserted on main_module._entry, tripping SLF001
  (private-member-access). Read it from the module namespace via
  vars(main_module)["_entry"] instead.
- Fix the test's Jinja whitespace control so it renders PEP 8-clean
  (no stray blank lines / import grouping) in every toggle combination,
  rather than relying on the style env's autofix.

Verified with `ruff check --no-fix` and the entrypoint tests across the
no-component, CLI, GUI, and web renders.
basedpyright (reportImplicitStringConcatenation) rejected the two adjacent
string literals in the no-component sys.exit(...). Collapse to a single short
literal; drop the package-name prefix (the binary already identifies the app)
so the line stays under the 88-char limit regardless of repo name.
@hasansezertasan
hasansezertasan merged commit 68e3381 into main Jun 26, 2026
10 checks passed
@hasansezertasan
hasansezertasan deleted the feat/atom-capricorn branch June 26, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant