Skip to content

Commit

Permalink
Copier Template: Support CLI stub. Creation of standalone executables.
Browse files Browse the repository at this point in the history
(Coauthor: xAI grok-3)

("Backport" from emcd/python-mimeogram.)
  • Loading branch information
emcd committed Feb 24, 2025
1 parent 833b1fd commit a5a8885
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 38 deletions.
18 changes: 17 additions & 1 deletion copier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ extension_name:
Extension name must contain only lowercase letters, numbers, and underscores
{% endif %}
include_data_resources:
type: bool
help: 'Include data directory with package resources?'
default: false

enable_property_tests:
type: bool
help: 'Enable property-based testing?'
Expand All @@ -138,6 +143,17 @@ enable_publication:
help: 'Enable package and documentation publication?'
default: false

enable_cli:
type: bool
help: 'Include command-line interface support?'
default: false

enable_executables:
type: bool
help: 'Generate standalone executables for the CLI?'
default: false
when: "{{ enable_cli }}"

inject_foundations:
type: bool
help: 'Include foundational constructs?'
Expand All @@ -164,4 +180,4 @@ inject_docstring_utils:
gh_owner:
type: str
help: Github repository owner
default: 'emcd'
default: 'emcd'
93 changes: 87 additions & 6 deletions template/.github/workflows/releaser.yaml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,74 @@ jobs:
skip-existing: {% raw %}${{ inputs.which-pypi == 'testpypi' }}{% endraw %}
{%- endif %}

{% if enable_executables -%}
create-executables:
needs: [initialize, package]
strategy:
matrix:
include:
- os: ubuntu-22.04
platform-name: linux-x86_64
- os: windows-latest
platform-name: windows-x86_64
- os: macos-13
platform-name: macos-x86_64
- os: macos-latest
platform-name: macos-arm64
runs-on: {% raw %}${{ matrix.os }}{% endraw %}
env:
_PYI_EXECUTABLE_NAME: {{ distribution_name }}-{% raw %}${{ github.ref_name }}-${{ matrix.platform-name }}{% endraw %}
steps:
- name: Prepare Python
uses: emcd/python-project-common/.github/actions/python-hatch@{{ _commit }}
with:
python-version: {% raw %}${{ fromJSON(needs.initialize.outputs.python-versions)[0] }}{% endraw %}

# - name: Install UPX (Linux) # Pre-installed on GHA Ubuntu runners
# if: {% raw %}${{ runner.os == 'Linux' }}{% endraw %}
# run: sudo apt-get install --yes upx

- name: Install UPX (macOS)
if: {% raw %}${{ runner.os == 'macOS' }}{% endraw %}
run: brew install upx

- name: Install UPX (Windows)
if: {% raw %}${{ runner.os == 'Windows' }}{% endraw %}
run: choco install upx

- name: Create Executable
run: |
hatch --env develop run \
pyinstaller \
--clean --distpath=.auxiliary/artifacts/pyinstaller \
pyinstaller.spec
shell: bash

- name: Validate Executable (Non-Windows)
if: {% raw %}${{ runner.os != 'Windows' }}{% endraw %}
run: |
.auxiliary/artifacts/pyinstaller/${_PYI_EXECUTABLE_NAME} --help
shell: bash

- name: Save Executable
uses: actions/upload-artifact@v4
with:
name: executables--{% raw %}${{ matrix.platform-name }}--${{ github.run_id }}{% endraw %}
path: .auxiliary/artifacts/pyinstaller/{{ distribution_name }}-*
{%- endif %}

{% if enable_publication -%}
publish-github:
if: {% raw %}${{ startsWith(github.ref, 'refs/tags/') }}{% endraw %}
needs: [initialize, package, publish-pypi]
needs:
- initialize
- package
- publish-pypi
# --- BEGIN: Injected by Copier ---
{% if enable_executables -%}
- create-executables
{%- endif %}
# --- END: Injected by Copier ---
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -106,33 +170,50 @@ jobs:
name: python-package-distributions--{% raw %}${{ github.run_id }}{% endraw %}
path: {% raw %}${{ env.DISTRIBUTIONS_PATH }}{% endraw %}

{% if enable_executables %}
- name: Restore Executables
uses: actions/download-artifact@v4
with:
pattern: executables--*--{% raw %}${{ github.run_id }}{% endraw %}
path: {% raw %}${{ env.DISTRIBUTIONS_PATH }}{% endraw %}
merge-multiple: true
{% endif %}

- name: Generate Integrity Check Values
run: |
set -eu -o pipefail
cd {% raw %}${{ env.DISTRIBUTIONS_PATH }}{% endraw %}
sha256sum *.tar.gz *.whl >SHA256SUMS.txt
sha256sum {{ distribution_name }}-* >SHA256SUMS.txt

- name: Sign Distributions
uses: sigstore/gh-action-sigstore-python@v3.0.0
with:
inputs: >-
{% raw %}${{ env.DISTRIBUTIONS_PATH }}{% endraw %}/SHA256SUMS.txt
{% raw %}${{ env.DISTRIBUTIONS_PATH }}{% endraw %}/*.tar.gz
{% raw %}${{ env.DISTRIBUTIONS_PATH }}{% endraw %}/*.whl
{% raw %}${{ env.DISTRIBUTIONS_PATH }}{% endraw %}/{{ distribution_name }}-*

- name: Generate Release Notes
run: |
set -eu -o pipefail
hatch --env develop run \
towncrier build --draft --version ${GITHUB_REF_NAME} \
> release-notes.rst
>.auxiliary/artifacts/tc-release-notes.rst
{% if enable_executables %}
cat \
documentation/miscellany/executables.rst \
.auxiliary/artifacts/tc-release-notes.rst \
>.auxiliary/artifacts/release-notes.rst
{% else %}
cp .auxiliary/artifacts/tc-release-notes.rst .auxiliary/artifacts/release-notes.rst
{% endif %}

- name: Create Release
env:
GITHUB_TOKEN: {% raw %}${{ github.token }}{% endraw %}
run: |
gh release create '{% raw %}${{ github.ref_name }}{% endraw %}' \
--repo '{% raw %}${{ github.repository }}{% endraw %}' \
--notes-file release-notes.rst
--notes-file .auxiliary/artifacts/release-notes.rst

- name: Publish Artifacts
env:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Executables
===========

Linux
-----

The Linux executables are dynamically linked and require a Glibc-based OS
distribution from 2022 or later (Ubuntu 22.04+, RHEL 9+, or derivatives). For
older Glibc-based OS distributions and non-Glibc-based OS distributions
(Alpine, etc...), please install the Python package instead.

Other Platforms
---------------

The macOS and Windows executables should work on any recent version of their
respective operating systems.
67 changes: 55 additions & 12 deletions template/pyproject.toml.jinja
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# vim: set filetype=toml fileencoding=utf-8:
# -*- mode: toml ; coding: utf-8 -*-

[build-system]
requires = [
{%- if enable_rust_extension %}
Expand All @@ -21,6 +24,11 @@ readme = { 'file' = 'README.rst', 'content-type' = 'text/x-rst' }
requires-python = '>= {{ python_version_min }}'
dependencies = [
'typing-extensions',
# --- BEGIN: Injected by Copier ---
{% if enable_cli -%}
'tyro',
{%- endif %}
# --- END: Injected by Copier ---
]
classifiers = [ # https://pypi.org/classifiers
'Development Status :: {{ development_status }}',
Expand All @@ -41,6 +49,10 @@ keywords = [ ] # TODO: Add keywords.
[[project.authors]]
name = '{{ author_name }}'
email = '{{ author_email }}'
{%- if enable_cli %}
[project.scripts]
{{ distribution_name }} = '{{ package_name }}:main'
{%- endif %}
[project.urls]
'Homepage' = 'https://github.com/{{ gh_owner }}/{{ project_name }}'
{%- if enable_publication %}
Expand Down Expand Up @@ -73,14 +85,31 @@ output = '.auxiliary/artifacts/coverage-pytest/coverage.xml'
[tool.hatch.build]
directory = '.auxiliary/artifacts/hatch-build'
[tool.hatch.build.targets.sdist]
only-include = [ 'sources/{{ package_name }}' ]
only-include = [
'sources/{{ package_name }}',
# --- BEGIN: Injected by Copier ---
{% if include_data_resources -%}
'data',
{%- endif %}
# --- END: Injected by Copier ---
]
strict-naming = false
[tool.hatch.build.targets.wheel]
only-include = [ 'sources/{{ package_name }}' ]
only-include = [
'sources/{{ package_name }}',
# --- BEGIN: Injected by Copier ---
{% if include_data_resources -%}
'data',
{%- endif %}
# --- END: Injected by Copier ---
]
strict-naming = false
[tool.hatch.build.targets.wheel.sources]
'sources/{{ package_name }}' = '{{ package_name }}'
{%- if include_data_resources %}
'data' = '{{ package_name }}/data'
{%- endif %}
{%- endif %} {# enable_rust_extension #}
[tool.hatch.envs.default]
python = '{{ python_version_min }}'
[tool.hatch.envs.develop]
Expand All @@ -90,15 +119,8 @@ dependencies = [
'bandit',
'coverage[toml]',
'furo',
{%- if enable_property_tests %}
'hypothesis',
{%- endif %}
'icecream',
{%- if enable_rust_extension %}
'isort',
'maturin~=1.7',
#'maturin-import-hook',
{%- endif %}
'packaging',
'pre-commit',
'pylint',
Expand All @@ -113,11 +135,25 @@ dependencies = [
'towncrier',
'tryceratops',
'yapf',
# --- BEGIN: Injected by Copier ---
{% if enable_executables -%}
'pyinstaller',
{%- endif %}
{% if enable_property_tests -%}
'hypothesis',
{%- endif %}
{% if enable_rust_extension -%}
'maturin~=1.7',
#'maturin-import-hook',
{%- endif %}
# --- END: Injected by Copier ---
]
post-install-commands = [
{%- if enable_rust_extension %}
# --- BEGIN: Injected by Copier ---
{% if enable_rust_extension -%}
#'''python -m maturin_import_hook site install''',
{%- endif %}
# --- END: Injected by Copier ---
]
[tool.hatch.envs.develop.env-vars]
{%- if enable_property_tests %}
Expand Down Expand Up @@ -154,8 +190,15 @@ linters = [
{%- endif %}
]
packagers = [
'hatch build',
# TODO? Sign packages.
'''hatch build''',
# --- BEGIN: Injected by Copier ---
{% if enable_executables -%}
'''pyinstaller \
--clean \
--distpath=.auxiliary/artifacts/pyinstaller \
pyinstaller.spec''',
{%- endif %}
# --- END: Injected by Copier ---
]
testers = [
'coverage erase',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
import types

import typing_extensions as typx
# --- BEGIN: Injected by Copier ---
{% if enable_cli -%}
import tyro
{%- endif %}
# --- END: Injected by Copier ---


ComparisonResult: typx.TypeAlias = bool | types.NotImplementedType
12 changes: 11 additions & 1 deletion template/sources/{{ package_name }}/__init__.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,22 @@


from . import __
{%- if inject_exceptions %}
# --- BEGIN: Injected by Copier ---
{% if inject_exceptions -%}
from . import exceptions
{%- endif %}
# --- END: Injected by Copier ---


__version__ = '1.0a0'


{%- if enable_cli %}
def main( ):
''' Entrypoint. '''
from .cli import execute
execute( )
{%- endif %}


# TODO: Reclassify package modules as immutable and concealed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# vim: set filetype=python fileencoding=utf-8:
# -*- coding: utf-8 -*-

#============================================================================#
# #
# Licensed under the Apache License, Version 2.0 (the "License"); #
# you may not use this file except in compliance with the License. #
# You may obtain a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
# #
#============================================================================#


''' Entrypoint. '''


# Note: Use absolute import for PyInstaller happiness.
from {{ package_name }}.cli import execute


if '__main__' == __name__: execute( )
Loading

0 comments on commit a5a8885

Please sign in to comment.