Skip to content

API Reference

Pyrig plugin that builds and publishes standalone project executables.

Installing this package as a dependency extends a pyrig-built project with an entry-point and icon to build from, a PyInstaller-based executable builder, a release workflow job that compiles and attaches one binary per operating system to each GitHub release, and a CLI command to run the entry point locally the same way the built executable does.

main

Project entry point.

main()

Run the project.

Source code in src/pyrig_executables/main.py
4
5
def main() -> None:
    """Run the project."""

rig

Mirrored namespace through which this plugin extends pyrig's scaffolding.

Mirrors the pyrig.rig package layout so pyrig's cross-package plugin discovery finds this plugin's configuration, tooling, and CLI customizations automatically.

cli

CLI commands for this package.

commands

Backend implementations for this package's CLI commands.

Each module implements exactly one command as a plain callable, decoupled from the CLI registration layer. This separation lets the registration layer import commands lazily, loading each module only when its command is invoked.

run

CLI command implementation for running the project's entry-point module.

run_main()

Execute the project's main.py as the __main__ module.

Runs the file by path rather than importing it, so its if __name__ == "__main__" guard fires and calls main, mirroring how the built executable runs the same file.

Source code in src/pyrig_executables/rig/cli/commands/run.py
 8
 9
10
11
12
13
14
15
def run_main() -> None:
    """Execute the project's `main.py` as the `__main__` module.

    Runs the file by path rather than importing it, so its
    `if __name__ == "__main__"` guard fires and calls `main`, mirroring how the
    built executable runs the same file.
    """
    run_path(MainConfigFile.I.path().as_posix(), run_name="__main__")

subcommands

Project-specific CLI commands.

Functions defined directly in this module are discovered and registered as top-level CLI commands. Module-level typer.Typer instances are registered as command groups, with each group named after the kebab-case form of its variable name.

run()

Run the project.

It executes the main.py module as __main__. This is the same entry point the built executable uses.

Source code in src/pyrig_executables/rig/cli/subcommands.py
10
11
12
13
14
15
16
17
18
def run() -> None:
    """Run the project.

    It executes the `main.py` module as `__main__`.
    This is the same entry point the built executable uses.
    """
    from pyrig_executables.rig.cli.commands.run import run_main  # noqa: PLC0415

    run_main()

configs

Declarative definitions of the configuration files this plugin contributes.

This package is this plugin's discovery scope for pyrig's config-file management: every file this plugin manages is defined here and validated during pyrig sync.

icon

Config that scaffolds the executable's icon as rig/resources/icon.png.

IconConfigFile

Bases: DictConfigFile

Config file that scaffolds the icon.png used as the executable's icon.

The release workflow passes the resulting file's path to pyinstaller --icon, which converts the PNG to the per-OS icon format (.ico on Windows, .icns on macOS; ignored on Linux) at build time. The scaffolded file is a default -- replace it with your own; it is only created when missing, so a project's own icon is preserved.

Source code in src/pyrig_executables/rig/configs/icon.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class IconConfigFile(DictConfigFile):
    """Config file that scaffolds the `icon.png` used as the executable's icon.

    The release workflow passes the resulting file's path to
    `pyinstaller --icon`, which converts the PNG to the per-OS icon format
    (`.ico` on Windows, `.icns` on macOS; ignored on Linux) at build time. The
    scaffolded file is a default -- replace it with your own; it is only
    created when missing, so a project's own icon is preserved.
    """

    def parent_path(self) -> Path:
        """Return the directory the icon lives in.

        Returns:
            The project's `rig/resources` package directory, shared with the
            config file that scaffolds that package's `__init__.py`.
        """
        return ResourcesInitConfigFile.I.parent_path()

    def stem(self) -> str:
        """Return the icon filename stem.

        Returns:
            `"icon"`, producing `icon.png`.
        """
        return "icon"

    def extension(self) -> str:
        """Return the icon file extension.

        Returns:
            `"png"`.
        """
        return "png"

    def _configs(self) -> dict[str, Any]:
        """Return the required structured content.

        Returns:
            An empty dict; the icon is a binary file copied verbatim, with no
            structured content to enforce.
        """
        return {}

    def _load(self) -> dict[str, Any]:
        """Raise -- the icon is binary and should never be loaded.

        Raises:
            RuntimeError: Always; the icon is never loaded.
        """
        msg = "The icon is a binary PNG and should never be loaded."
        raise RuntimeError(msg)

    def _dump(self, configs: dict[str, Any]) -> None:
        """Copy this plugin's bundled default icon to the project's icon path.

        Overwrites whatever file is already at the destination.

        Args:
            configs: Ignored; the icon is a binary file copied verbatim.
        """
        del configs
        shutil.copy(
            resource_path(name=self.filename(), package=resources),
            self.path(),
        )

    def is_correct(self) -> bool:
        """Return whether the icon file is non-empty.

        Non-emptiness is the only requirement: the bundled default is a valid
        PNG and any user-provided icon is preserved, so the file's bytes are
        not otherwise validated.

        Returns:
            `True` if the icon file has content; `False` if it is empty.
        """
        return file_has_content(self.path())
_configs()

Return the required structured content.

Returns:

Type Description
dict[str, Any]

An empty dict; the icon is a binary file copied verbatim, with no

dict[str, Any]

structured content to enforce.

Source code in src/pyrig_executables/rig/configs/icon.py
50
51
52
53
54
55
56
57
def _configs(self) -> dict[str, Any]:
    """Return the required structured content.

    Returns:
        An empty dict; the icon is a binary file copied verbatim, with no
        structured content to enforce.
    """
    return {}
_dump(configs)

Copy this plugin's bundled default icon to the project's icon path.

Overwrites whatever file is already at the destination.

Parameters:

Name Type Description Default
configs dict[str, Any]

Ignored; the icon is a binary file copied verbatim.

required
Source code in src/pyrig_executables/rig/configs/icon.py
68
69
70
71
72
73
74
75
76
77
78
79
80
def _dump(self, configs: dict[str, Any]) -> None:
    """Copy this plugin's bundled default icon to the project's icon path.

    Overwrites whatever file is already at the destination.

    Args:
        configs: Ignored; the icon is a binary file copied verbatim.
    """
    del configs
    shutil.copy(
        resource_path(name=self.filename(), package=resources),
        self.path(),
    )
_load()

Raise -- the icon is binary and should never be loaded.

Raises:

Type Description
RuntimeError

Always; the icon is never loaded.

Source code in src/pyrig_executables/rig/configs/icon.py
59
60
61
62
63
64
65
66
def _load(self) -> dict[str, Any]:
    """Raise -- the icon is binary and should never be loaded.

    Raises:
        RuntimeError: Always; the icon is never loaded.
    """
    msg = "The icon is a binary PNG and should never be loaded."
    raise RuntimeError(msg)
extension()

Return the icon file extension.

Returns:

Type Description
str

"png".

Source code in src/pyrig_executables/rig/configs/icon.py
42
43
44
45
46
47
48
def extension(self) -> str:
    """Return the icon file extension.

    Returns:
        `"png"`.
    """
    return "png"
is_correct()

Return whether the icon file is non-empty.

Non-emptiness is the only requirement: the bundled default is a valid PNG and any user-provided icon is preserved, so the file's bytes are not otherwise validated.

Returns:

Type Description
bool

True if the icon file has content; False if it is empty.

Source code in src/pyrig_executables/rig/configs/icon.py
82
83
84
85
86
87
88
89
90
91
92
def is_correct(self) -> bool:
    """Return whether the icon file is non-empty.

    Non-emptiness is the only requirement: the bundled default is a valid
    PNG and any user-provided icon is preserved, so the file's bytes are
    not otherwise validated.

    Returns:
        `True` if the icon file has content; `False` if it is empty.
    """
    return file_has_content(self.path())
parent_path()

Return the directory the icon lives in.

Returns:

Type Description
Path

The project's rig/resources package directory, shared with the

Path

config file that scaffolds that package's __init__.py.

Source code in src/pyrig_executables/rig/configs/icon.py
25
26
27
28
29
30
31
32
def parent_path(self) -> Path:
    """Return the directory the icon lives in.

    Returns:
        The project's `rig/resources` package directory, shared with the
        config file that scaffolds that package's `__init__.py`.
    """
    return ResourcesInitConfigFile.I.parent_path()
stem()

Return the icon filename stem.

Returns:

Type Description
str

"icon", producing icon.png.

Source code in src/pyrig_executables/rig/configs/icon.py
34
35
36
37
38
39
40
def stem(self) -> str:
    """Return the icon filename stem.

    Returns:
        `"icon"`, producing `icon.png`.
    """
    return "icon"

main

Configuration for the project's main entry-point module.

Scaffolds a main.py containing a callable main function in every project that installs this plugin. The module provides the entry point that the executable builder bundles into a standalone binary, so this config guarantees that a suitable build target always exists.

MainConfigFile

Bases: CopyModuleConfigFile

Scaffolding for the project's main.py entry-point module.

Copies this plugin's pyrig_executables.main scaffolding into the target project (with the package prefix rewritten to the project's package name), producing a main.py with a main entry point. Once the file exists, validation only checks that a callable main and a __main__ guard are present, so any user-implemented entry point is preserved and never overwritten.

Source code in src/pyrig_executables/rig/configs/main.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class MainConfigFile(CopyModuleConfigFile):
    """Scaffolding for the project's `main.py` entry-point module.

    Copies this plugin's `pyrig_executables.main` scaffolding into the target
    project (with the package prefix rewritten to the project's package name),
    producing a `main.py` with a `main` entry point. Once the file exists,
    validation only checks that a callable `main` and a `__main__` guard are
    present, so any user-implemented entry point is preserved and never
    overwritten.
    """

    def copy_module(self) -> ModuleType:
        """Return the source module whose content is copied into the project.

        Returns:
            The `pyrig_executables.main` module used as the entry-point
            scaffolding.
        """
        return main_module

    def is_correct(self) -> bool:
        """Return whether the project's `main.py` is valid.

        Overrides the inherited content check to assert that `main.py` is only
        correct when the target module both exposes a callable named `main`
        and contains an `if __name__ == "__main__"` execution guard. The
        function body is deliberately ignored so that a project's own
        entry-point implementation is treated as valid and is never
        overwritten.

        Returns:
            `True` if the target module defines a callable `main` and a
            `__main__` guard is present.
        """
        return self.has_callable_main() and self.has_main_guard()

    def has_callable_main(self) -> bool:
        """Return whether the target module exposes a callable `main`.

        Returns:
            `True` if the module defines an attribute named `main` that is
            callable.
        """
        return callable(getattr(self.module(), main_func.__name__, None))

    def has_main_guard(self) -> bool:
        """Return whether the target module has a `__main__` execution guard.

        Checks that the file's content contains the `main_guard` snippet, the
        conventional guard that runs `main` when the module is executed
        directly.

        Returns:
            `True` if the `__main__` guard snippet is present in the file.
        """
        return self.main_guard() in self.read_content()

    def main_guard(self) -> str:
        """Return the canonical `__main__` execution guard snippet.

        Returns:
            The `if __name__ == "__main__"` block that calls `main`.
        """
        return f"""if __name__ == "__main__":
    {main_func.__name__}()"""
copy_module()

Return the source module whose content is copied into the project.

Returns:

Type Description
ModuleType

The pyrig_executables.main module used as the entry-point

ModuleType

scaffolding.

Source code in src/pyrig_executables/rig/configs/main.py
28
29
30
31
32
33
34
35
def copy_module(self) -> ModuleType:
    """Return the source module whose content is copied into the project.

    Returns:
        The `pyrig_executables.main` module used as the entry-point
        scaffolding.
    """
    return main_module
has_callable_main()

Return whether the target module exposes a callable main.

Returns:

Type Description
bool

True if the module defines an attribute named main that is

bool

callable.

Source code in src/pyrig_executables/rig/configs/main.py
53
54
55
56
57
58
59
60
def has_callable_main(self) -> bool:
    """Return whether the target module exposes a callable `main`.

    Returns:
        `True` if the module defines an attribute named `main` that is
        callable.
    """
    return callable(getattr(self.module(), main_func.__name__, None))
has_main_guard()

Return whether the target module has a __main__ execution guard.

Checks that the file's content contains the main_guard snippet, the conventional guard that runs main when the module is executed directly.

Returns:

Type Description
bool

True if the __main__ guard snippet is present in the file.

Source code in src/pyrig_executables/rig/configs/main.py
62
63
64
65
66
67
68
69
70
71
72
def has_main_guard(self) -> bool:
    """Return whether the target module has a `__main__` execution guard.

    Checks that the file's content contains the `main_guard` snippet, the
    conventional guard that runs `main` when the module is executed
    directly.

    Returns:
        `True` if the `__main__` guard snippet is present in the file.
    """
    return self.main_guard() in self.read_content()
is_correct()

Return whether the project's main.py is valid.

Overrides the inherited content check to assert that main.py is only correct when the target module both exposes a callable named main and contains an if __name__ == "__main__" execution guard. The function body is deliberately ignored so that a project's own entry-point implementation is treated as valid and is never overwritten.

Returns:

Type Description
bool

True if the target module defines a callable main and a

bool

__main__ guard is present.

Source code in src/pyrig_executables/rig/configs/main.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def is_correct(self) -> bool:
    """Return whether the project's `main.py` is valid.

    Overrides the inherited content check to assert that `main.py` is only
    correct when the target module both exposes a callable named `main`
    and contains an `if __name__ == "__main__"` execution guard. The
    function body is deliberately ignored so that a project's own
    entry-point implementation is treated as valid and is never
    overwritten.

    Returns:
        `True` if the target module defines a callable `main` and a
        `__main__` guard is present.
    """
    return self.has_callable_main() and self.has_main_guard()
main_guard()

Return the canonical __main__ execution guard snippet.

Returns:

Type Description
str

The if __name__ == "__main__" block that calls main.

Source code in src/pyrig_executables/rig/configs/main.py
74
75
76
77
78
79
80
81
def main_guard(self) -> str:
    """Return the canonical `__main__` execution guard snippet.

    Returns:
        The `if __name__ == "__main__"` block that calls `main`.
    """
    return f"""if __name__ == "__main__":
{main_func.__name__}()"""

version_control

Version control configuration adjusted for standalone executable distribution.

Extends the base version control setup, local or remote, to account for building and publishing standalone executables alongside a release.

remote

Overrides for the project's GitHub-hosted remote repository configuration.

workflows

Extensions to the project's GitHub Actions workflow configuration.

Overrides the base release workflow to build and attach standalone executables alongside the other release artifacts.

release

Extension of the release workflow that builds and attaches executables.

ReleaseWorkflowConfigFile

Bases: ReleaseWorkflowConfigFile

Release workflow that builds and attaches standalone executables.

Extends the base release workflow with a matrix job that builds a single-file executable for every supported operating system and attaches each one to the GitHub release as a release asset, alongside the generated changelog.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
class ReleaseWorkflowConfigFile(BaseReleaseWorkflowConfigFile):
    """Release workflow that builds and attaches standalone executables.

    Extends the base release workflow with a matrix job that builds a
    single-file executable for every supported operating system and attaches
    each one to the GitHub release as a release asset, alongside the
    generated changelog.
    """

    def priority(self) -> float:
        """Return a priority one step after the resources config's.

        Building the executable requires the project's resources package to
        already exist, so this config must validate after it. Deriving from
        its priority instead of hard-coding a value keeps this config's
        priority in step with any future change to the resources config's own
        priority.

        Returns:
            The resources config's priority lowered by one `Priority.STEP`.
        """
        return Priority.decrease(ResourcesInitConfigFile.I.priority())

    def jobs(self) -> dict[str, Any]:
        """Build the complete set of workflow jobs.

        Adds the executable build job to the base release jobs.

        Returns:
            Dict containing the executable build job together with the base
            release jobs.
        """
        return {
            **self.job_executable(),
            **super().jobs(),
        }

    def job_executable(self) -> dict[str, Any]:
        """Build the matrix job that compiles the executable on every OS.

        Runs across the default OS matrix (Linux, Windows, macOS), since
        `pyinstaller` cannot cross-compile and each binary must be built on
        its target platform.

        Returns:
            Job configuration with an OS matrix strategy, a dynamic `runs-on`
            value, and the build and upload steps.
        """
        return self.job(
            self.job_executable,
            strategy=self.strategy_matrix_os(),
            runs_on=self.insert_matrix_os(),
            steps=self.steps_executable(),
        )

    def job_publish(self) -> dict[str, Any]:
        """Build the release job, gated on the executable build job.

        Adds a `needs` dependency on `executable` so the release is
        only published once every platform's binary is available to attach.

        Returns:
            The base release job with a `needs` dependency added.
        """
        jobs = super().job_publish()
        jobs[self.id_from_method(self.job_publish)]["needs"] = [
            self.id_from_method(self.job_executable)
        ]
        return jobs

    def steps_executable(self) -> list[dict[str, Any]]:
        """Build the ordered steps for the executable build job.

        Returns:
            Steps that set up the environment, build the single-file
            executable, and upload it as a per-OS artifact.
        """
        return [
            *self.steps_core_installed_setup(),
            self.step_build_executable(),
            self.step_upload_executable(),
        ]

    def steps_publish(self) -> list[dict[str, Any]]:
        """Build the ordered steps for the release job.

        Extends the base steps with a download step that pulls every platform's
        executable into `dist/` immediately before the release is created.

        Returns:
            The base publish steps with the executable download step inserted
            just before the create-release step.
        """
        steps = super().steps_publish()
        create_release_id = self.id_from_method(self.step_create_release)
        create_release_index = next(
            index for index, step in enumerate(steps) if step["id"] == create_release_id
        )
        steps.insert(create_release_index, self.step_download_executables())
        return steps

    def step_build_executable(
        self,
        *,
        step: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Build a step that compiles the project into a single-file executable.

        Runs `pyinstaller --onefile` against the project's entry-point module,
        naming the binary after the project and the current runner OS so the
        per-platform assets do not collide on the release.

        Args:
            step: Additional keys to merge into the step configuration.

        Returns:
            Step that runs the executable builder via uv.
        """
        return self.step(
            self.step_build_executable,
            run=str(
                PackageManager.I.run_args(
                    *ExecutableBuilder.I.build_args(
                        name=self.executable_name(),
                        entry_point=MainConfigFile.I.path(),
                        icon=IconConfigFile.I.path(),
                        resource_modules=self.resource_modules(),
                    )
                )
            ),
            step=step,
        )

    def step_upload_executable(
        self,
        *,
        step: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Build a step that uploads the built executable as a workflow artifact.

        Uploads the contents of `dist/` under the per-OS `artifact_name` so
        the `publish` job can later download every platform's binary.

        Args:
            step: Additional keys to merge into the step configuration.

        Returns:
            Step using `actions/upload-artifact@main`.
        """
        return self.step(
            self.step_upload_executable,
            uses="actions/upload-artifact@main",
            with_={
                "name": self.artifact_name(self.insert_os()),
                "path": ExecutableBuilder.I.dist_dir().as_posix(),
            },
            step=step,
        )

    def step_download_executables(
        self,
        *,
        step: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Build a step that downloads every executable artifact into `dist/`.

        Merges every per-OS executable artifact, matched by the
        `artifact_name` glob, into a single `dist/` directory so they can be
        attached to the release with one glob. The narrow prefix avoids
        pulling in unrelated artifacts that other actions may name after the
        project.

        Args:
            step: Additional keys to merge into the step configuration.

        Returns:
            Step using `actions/download-artifact@main`.
        """
        return self.step(
            self.step_download_executables,
            uses="actions/download-artifact@main",
            with_={
                "pattern": self.artifact_name("*"),
                "path": ExecutableBuilder.I.dist_dir().as_posix(),
                "merge-multiple": "true",
            },
            step=step,
        )

    def step_create_release(
        self,
        *,
        step: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Build the create-release step, attaching the built executables.

        Extends the base release step by attaching every binary downloaded into
        `dist/` as a release asset.

        Args:
            step: Additional keys to merge into the step configuration.

        Returns:
            The base create-release step with `dist/*` added as artifacts.
        """
        step = super().step_create_release(step=step)
        step["with"]["artifacts"] = (ExecutableBuilder.I.dist_dir() / "*").as_posix()
        return step

    def executable_name(self) -> str:
        """Build the per-OS name of the executable binary and release asset.

        Combines the project name with the runner OS so each platform's binary
        gets a unique, recognizable, collision-free name (e.g.
        `pyrig-executables-Linux`). The OS is resolved at workflow runtime.

        Returns:
            The `<project>-<os>` name string.
        """
        return f"{PackageManager.I.project_name()}-{self.insert_os()}"

    def artifact_name(self, os: str) -> str:
        """Build the workflow-artifact name for the given runner OS.

        Single source of the `executable-<os>` artifact label, shared by the
        upload step (with the resolved runner OS) and the download step (with
        `"*"` to match every platform). It is deliberately generic and
        distinct from `executable_name` so it does not collide with
        artifacts that other actions name after the project.

        Args:
            os: The runner OS suffix, or `"*"` to form the download glob.

        Returns:
            The `executable-<os>` artifact name.
        """
        return f"executable-{os}"

    def insert_os(self) -> str:
        """Get the `${{ runner.os }}` expression.

        Returns:
            GitHub Actions expression resolving to the current runner's
            operating system (e.g. `Linux`, `Windows`, `macOS`).
        """
        return self.insert_expression("runner.os")

    def resource_modules(self) -> Iterable[ModuleType]:
        """Return the resource modules to bundle into the executable.

        Resolves the project's `rig/resources` package, the location the
        `pyrig-resources` plugin scaffolds and validates. Locating the
        project's resources is a config concern, so it lives here rather than
        in the project-agnostic executable builder tool. Override to bundle
        additional resource packages.

        Returns:
            The project's resource modules (the `rig/resources` package).
        """
        return (ResourcesInitConfigFile.I.module(),)
artifact_name(os)

Build the workflow-artifact name for the given runner OS.

Single source of the executable-<os> artifact label, shared by the upload step (with the resolved runner OS) and the download step (with "*" to match every platform). It is deliberately generic and distinct from executable_name so it does not collide with artifacts that other actions name after the project.

Parameters:

Name Type Description Default
os str

The runner OS suffix, or "*" to form the download glob.

required

Returns:

Type Description
str

The executable-<os> artifact name.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def artifact_name(self, os: str) -> str:
    """Build the workflow-artifact name for the given runner OS.

    Single source of the `executable-<os>` artifact label, shared by the
    upload step (with the resolved runner OS) and the download step (with
    `"*"` to match every platform). It is deliberately generic and
    distinct from `executable_name` so it does not collide with
    artifacts that other actions name after the project.

    Args:
        os: The runner OS suffix, or `"*"` to form the download glob.

    Returns:
        The `executable-<os>` artifact name.
    """
    return f"executable-{os}"
executable_name()

Build the per-OS name of the executable binary and release asset.

Combines the project name with the runner OS so each platform's binary gets a unique, recognizable, collision-free name (e.g. pyrig-executables-Linux). The OS is resolved at workflow runtime.

Returns:

Type Description
str

The <project>-<os> name string.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
228
229
230
231
232
233
234
235
236
237
238
def executable_name(self) -> str:
    """Build the per-OS name of the executable binary and release asset.

    Combines the project name with the runner OS so each platform's binary
    gets a unique, recognizable, collision-free name (e.g.
    `pyrig-executables-Linux`). The OS is resolved at workflow runtime.

    Returns:
        The `<project>-<os>` name string.
    """
    return f"{PackageManager.I.project_name()}-{self.insert_os()}"
insert_os()

Get the ${{ runner.os }} expression.

Returns:

Type Description
str

GitHub Actions expression resolving to the current runner's

str

operating system (e.g. Linux, Windows, macOS).

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
257
258
259
260
261
262
263
264
def insert_os(self) -> str:
    """Get the `${{ runner.os }}` expression.

    Returns:
        GitHub Actions expression resolving to the current runner's
        operating system (e.g. `Linux`, `Windows`, `macOS`).
    """
    return self.insert_expression("runner.os")
job_executable()

Build the matrix job that compiles the executable on every OS.

Runs across the default OS matrix (Linux, Windows, macOS), since pyinstaller cannot cross-compile and each binary must be built on its target platform.

Returns:

Type Description
dict[str, Any]

Job configuration with an OS matrix strategy, a dynamic runs-on

dict[str, Any]

value, and the build and upload steps.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def job_executable(self) -> dict[str, Any]:
    """Build the matrix job that compiles the executable on every OS.

    Runs across the default OS matrix (Linux, Windows, macOS), since
    `pyinstaller` cannot cross-compile and each binary must be built on
    its target platform.

    Returns:
        Job configuration with an OS matrix strategy, a dynamic `runs-on`
        value, and the build and upload steps.
    """
    return self.job(
        self.job_executable,
        strategy=self.strategy_matrix_os(),
        runs_on=self.insert_matrix_os(),
        steps=self.steps_executable(),
    )
job_publish()

Build the release job, gated on the executable build job.

Adds a needs dependency on executable so the release is only published once every platform's binary is available to attach.

Returns:

Type Description
dict[str, Any]

The base release job with a needs dependency added.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def job_publish(self) -> dict[str, Any]:
    """Build the release job, gated on the executable build job.

    Adds a `needs` dependency on `executable` so the release is
    only published once every platform's binary is available to attach.

    Returns:
        The base release job with a `needs` dependency added.
    """
    jobs = super().job_publish()
    jobs[self.id_from_method(self.job_publish)]["needs"] = [
        self.id_from_method(self.job_executable)
    ]
    return jobs
jobs()

Build the complete set of workflow jobs.

Adds the executable build job to the base release jobs.

Returns:

Type Description
dict[str, Any]

Dict containing the executable build job together with the base

dict[str, Any]

release jobs.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
42
43
44
45
46
47
48
49
50
51
52
53
54
def jobs(self) -> dict[str, Any]:
    """Build the complete set of workflow jobs.

    Adds the executable build job to the base release jobs.

    Returns:
        Dict containing the executable build job together with the base
        release jobs.
    """
    return {
        **self.job_executable(),
        **super().jobs(),
    }
priority()

Return a priority one step after the resources config's.

Building the executable requires the project's resources package to already exist, so this config must validate after it. Deriving from its priority instead of hard-coding a value keeps this config's priority in step with any future change to the resources config's own priority.

Returns:

Type Description
float

The resources config's priority lowered by one Priority.STEP.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
28
29
30
31
32
33
34
35
36
37
38
39
40
def priority(self) -> float:
    """Return a priority one step after the resources config's.

    Building the executable requires the project's resources package to
    already exist, so this config must validate after it. Deriving from
    its priority instead of hard-coding a value keeps this config's
    priority in step with any future change to the resources config's own
    priority.

    Returns:
        The resources config's priority lowered by one `Priority.STEP`.
    """
    return Priority.decrease(ResourcesInitConfigFile.I.priority())
resource_modules()

Return the resource modules to bundle into the executable.

Resolves the project's rig/resources package, the location the pyrig-resources plugin scaffolds and validates. Locating the project's resources is a config concern, so it lives here rather than in the project-agnostic executable builder tool. Override to bundle additional resource packages.

Returns:

Type Description
Iterable[ModuleType]

The project's resource modules (the rig/resources package).

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
266
267
268
269
270
271
272
273
274
275
276
277
278
def resource_modules(self) -> Iterable[ModuleType]:
    """Return the resource modules to bundle into the executable.

    Resolves the project's `rig/resources` package, the location the
    `pyrig-resources` plugin scaffolds and validates. Locating the
    project's resources is a config concern, so it lives here rather than
    in the project-agnostic executable builder tool. Override to bundle
    additional resource packages.

    Returns:
        The project's resource modules (the `rig/resources` package).
    """
    return (ResourcesInitConfigFile.I.module(),)
step_build_executable(*, step=None)

Build a step that compiles the project into a single-file executable.

Runs pyinstaller --onefile against the project's entry-point module, naming the binary after the project and the current runner OS so the per-platform assets do not collide on the release.

Parameters:

Name Type Description Default
step dict[str, Any] | None

Additional keys to merge into the step configuration.

None

Returns:

Type Description
dict[str, Any]

Step that runs the executable builder via uv.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def step_build_executable(
    self,
    *,
    step: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Build a step that compiles the project into a single-file executable.

    Runs `pyinstaller --onefile` against the project's entry-point module,
    naming the binary after the project and the current runner OS so the
    per-platform assets do not collide on the release.

    Args:
        step: Additional keys to merge into the step configuration.

    Returns:
        Step that runs the executable builder via uv.
    """
    return self.step(
        self.step_build_executable,
        run=str(
            PackageManager.I.run_args(
                *ExecutableBuilder.I.build_args(
                    name=self.executable_name(),
                    entry_point=MainConfigFile.I.path(),
                    icon=IconConfigFile.I.path(),
                    resource_modules=self.resource_modules(),
                )
            )
        ),
        step=step,
    )
step_create_release(*, step=None)

Build the create-release step, attaching the built executables.

Extends the base release step by attaching every binary downloaded into dist/ as a release asset.

Parameters:

Name Type Description Default
step dict[str, Any] | None

Additional keys to merge into the step configuration.

None

Returns:

Type Description
dict[str, Any]

The base create-release step with dist/* added as artifacts.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def step_create_release(
    self,
    *,
    step: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Build the create-release step, attaching the built executables.

    Extends the base release step by attaching every binary downloaded into
    `dist/` as a release asset.

    Args:
        step: Additional keys to merge into the step configuration.

    Returns:
        The base create-release step with `dist/*` added as artifacts.
    """
    step = super().step_create_release(step=step)
    step["with"]["artifacts"] = (ExecutableBuilder.I.dist_dir() / "*").as_posix()
    return step
step_download_executables(*, step=None)

Build a step that downloads every executable artifact into dist/.

Merges every per-OS executable artifact, matched by the artifact_name glob, into a single dist/ directory so they can be attached to the release with one glob. The narrow prefix avoids pulling in unrelated artifacts that other actions may name after the project.

Parameters:

Name Type Description Default
step dict[str, Any] | None

Additional keys to merge into the step configuration.

None

Returns:

Type Description
dict[str, Any]

Step using actions/download-artifact@main.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def step_download_executables(
    self,
    *,
    step: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Build a step that downloads every executable artifact into `dist/`.

    Merges every per-OS executable artifact, matched by the
    `artifact_name` glob, into a single `dist/` directory so they can be
    attached to the release with one glob. The narrow prefix avoids
    pulling in unrelated artifacts that other actions may name after the
    project.

    Args:
        step: Additional keys to merge into the step configuration.

    Returns:
        Step using `actions/download-artifact@main`.
    """
    return self.step(
        self.step_download_executables,
        uses="actions/download-artifact@main",
        with_={
            "pattern": self.artifact_name("*"),
            "path": ExecutableBuilder.I.dist_dir().as_posix(),
            "merge-multiple": "true",
        },
        step=step,
    )
step_upload_executable(*, step=None)

Build a step that uploads the built executable as a workflow artifact.

Uploads the contents of dist/ under the per-OS artifact_name so the publish job can later download every platform's binary.

Parameters:

Name Type Description Default
step dict[str, Any] | None

Additional keys to merge into the step configuration.

None

Returns:

Type Description
dict[str, Any]

Step using actions/upload-artifact@main.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def step_upload_executable(
    self,
    *,
    step: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Build a step that uploads the built executable as a workflow artifact.

    Uploads the contents of `dist/` under the per-OS `artifact_name` so
    the `publish` job can later download every platform's binary.

    Args:
        step: Additional keys to merge into the step configuration.

    Returns:
        Step using `actions/upload-artifact@main`.
    """
    return self.step(
        self.step_upload_executable,
        uses="actions/upload-artifact@main",
        with_={
            "name": self.artifact_name(self.insert_os()),
            "path": ExecutableBuilder.I.dist_dir().as_posix(),
        },
        step=step,
    )
steps_executable()

Build the ordered steps for the executable build job.

Returns:

Type Description
list[dict[str, Any]]

Steps that set up the environment, build the single-file

list[dict[str, Any]]

executable, and upload it as a per-OS artifact.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def steps_executable(self) -> list[dict[str, Any]]:
    """Build the ordered steps for the executable build job.

    Returns:
        Steps that set up the environment, build the single-file
        executable, and upload it as a per-OS artifact.
    """
    return [
        *self.steps_core_installed_setup(),
        self.step_build_executable(),
        self.step_upload_executable(),
    ]
steps_publish()

Build the ordered steps for the release job.

Extends the base steps with a download step that pulls every platform's executable into dist/ immediately before the release is created.

Returns:

Type Description
list[dict[str, Any]]

The base publish steps with the executable download step inserted

list[dict[str, Any]]

just before the create-release step.

Source code in src/pyrig_executables/rig/configs/version_control/remote/workflows/release.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def steps_publish(self) -> list[dict[str, Any]]:
    """Build the ordered steps for the release job.

    Extends the base steps with a download step that pulls every platform's
    executable into `dist/` immediately before the release is created.

    Returns:
        The base publish steps with the executable download step inserted
        just before the create-release step.
    """
    steps = super().steps_publish()
    create_release_id = self.id_from_method(self.step_create_release)
    create_release_index = next(
        index for index, step in enumerate(steps) if step["id"] == create_release_id
    )
    steps.insert(create_release_index, self.step_download_executables())
    return steps

resources

Bundled static resource files read when scaffolding executable projects.

Holds verbatim resource files that config file subclasses copy into a project that builds a standalone executable.

tools

Wrappers for the external CLI tools this plugin contributes to a project.

executable_builder

Tool wrapper for bundling the project into standalone release executables.

ExecutableBuilder

Bases: Tool

Wrapper for pyinstaller, the tool that builds standalone executables.

Exposes a project-info badge showing the cumulative download count across all GitHub release assets, linking to the releases page where the built executables are published.

Source code in src/pyrig_executables/rig/tools/executable_builder.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class ExecutableBuilder(Tool):
    """Wrapper for `pyinstaller`, the tool that builds standalone executables.

    Exposes a project-info badge showing the cumulative download count across
    all GitHub release assets, linking to the releases page where the built
    executables are published.
    """

    def name(self) -> str:
        """Return `'pyinstaller'`."""
        return "pyinstaller"

    def group(self) -> str:
        """Return `Group.PROJECT_INFO`."""
        return Group.PROJECT_INFO

    def image_url(self) -> str:
        """Return the shields.io URL for the GitHub release downloads badge."""
        owner, repo = (
            VersionController.I.repo_owner(),
            PackageManager.I.project_name(),
        )
        return f"https://img.shields.io/github/downloads/{owner}/{repo}/total?logo=github&label=downloads"

    def link_url(self) -> str:
        """Return the GitHub releases page URL where the binaries are published."""
        return RemoteVersionController.I.releases_url()

    def build_args(
        self,
        *args: str,
        name: str,
        entry_point: Path,
        icon: Path,
        resource_modules: Iterable[ModuleType],
    ) -> Args:
        """Build the `pyinstaller` command that bundles a single-file executable.

        Bundles each module in `resource_modules` with its own
        `--collect-data` flag rather than pointing `--add-data` at a path.
        This preserves the package layout so resources stay locatable at
        runtime through `importlib.resources` in both development and the
        frozen executable, and sidesteps the platform-specific separator
        `--add-data` requires. The build runs in console mode by default;
        pass `--windowed` through `*args` for a GUI application that should
        run without a console window (this instead produces a `.app` bundle
        directory on macOS).

        Args:
            *args: Additional arguments forwarded to `pyinstaller`, inserted
                after the resource flags and before `entry_point`.
            name: Output name for the executable (without an OS-specific
                extension; `pyinstaller` appends `.exe` on Windows).
            entry_point: Path to the entry-point script to bundle.
            icon: Path to the icon image. A non-native format (e.g. PNG) is
                converted to the per-OS format (`.ico` / `.icns`) at build
                time via Pillow; ignored on Linux.
            resource_modules: Modules whose data files are bundled, one
                `--collect-data` flag per module. Where the project keeps its
                resources is the caller's concern, so this is required.

        Returns:
            Args for the `pyinstaller` command.
        """
        collect_data = (
            arg
            for module in resource_modules
            for arg in ("--collect-data", module.__name__)
        )
        return self.args(
            "--onefile",
            "--name",
            name,
            "--icon",
            icon.as_posix(),
            *collect_data,
            *args,
            entry_point.as_posix(),
        )

    def dev_dependencies(self) -> tuple[str, ...]:
        """Return the dev dependencies required to build executables.

        Extends the default with `pillow` so `pyinstaller` can convert a
        non-native icon image (e.g. PNG) into the per-OS icon format (`.ico`
        / `.icns`) at build time.

        Returns:
            `pyinstaller` and `pillow`.
        """
        return (*super().dev_dependencies(), "pillow")

    def version_control_ignore_paths(self) -> tuple[str, ...]:
        """Return the build artifact paths to exclude from version control.

        Returns:
            The `pyinstaller` build artifacts: the `dist/` output directory
            (where the executables are written), the generated `*.spec` files,
            and the `build/` working directory.
        """
        return (f"{self.dist_dir().as_posix()}/", "*.spec", "build/")

    def dist_dir(self) -> Path:
        """Return the directory `pyinstaller` writes built executables to.

        Single source of truth for the output location, so it never drifts
        out of sync with other places that need it.

        Returns:
            The `dist` output directory, relative to the project root.
        """
        return Path("dist")
build_args(*args, name, entry_point, icon, resource_modules)

Build the pyinstaller command that bundles a single-file executable.

Bundles each module in resource_modules with its own --collect-data flag rather than pointing --add-data at a path. This preserves the package layout so resources stay locatable at runtime through importlib.resources in both development and the frozen executable, and sidesteps the platform-specific separator --add-data requires. The build runs in console mode by default; pass --windowed through *args for a GUI application that should run without a console window (this instead produces a .app bundle directory on macOS).

Parameters:

Name Type Description Default
*args str

Additional arguments forwarded to pyinstaller, inserted after the resource flags and before entry_point.

()
name str

Output name for the executable (without an OS-specific extension; pyinstaller appends .exe on Windows).

required
entry_point Path

Path to the entry-point script to bundle.

required
icon Path

Path to the icon image. A non-native format (e.g. PNG) is converted to the per-OS format (.ico / .icns) at build time via Pillow; ignored on Linux.

required
resource_modules Iterable[ModuleType]

Modules whose data files are bundled, one --collect-data flag per module. Where the project keeps its resources is the caller's concern, so this is required.

required

Returns:

Type Description
Args

Args for the pyinstaller command.

Source code in src/pyrig_executables/rig/tools/executable_builder.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def build_args(
    self,
    *args: str,
    name: str,
    entry_point: Path,
    icon: Path,
    resource_modules: Iterable[ModuleType],
) -> Args:
    """Build the `pyinstaller` command that bundles a single-file executable.

    Bundles each module in `resource_modules` with its own
    `--collect-data` flag rather than pointing `--add-data` at a path.
    This preserves the package layout so resources stay locatable at
    runtime through `importlib.resources` in both development and the
    frozen executable, and sidesteps the platform-specific separator
    `--add-data` requires. The build runs in console mode by default;
    pass `--windowed` through `*args` for a GUI application that should
    run without a console window (this instead produces a `.app` bundle
    directory on macOS).

    Args:
        *args: Additional arguments forwarded to `pyinstaller`, inserted
            after the resource flags and before `entry_point`.
        name: Output name for the executable (without an OS-specific
            extension; `pyinstaller` appends `.exe` on Windows).
        entry_point: Path to the entry-point script to bundle.
        icon: Path to the icon image. A non-native format (e.g. PNG) is
            converted to the per-OS format (`.ico` / `.icns`) at build
            time via Pillow; ignored on Linux.
        resource_modules: Modules whose data files are bundled, one
            `--collect-data` flag per module. Where the project keeps its
            resources is the caller's concern, so this is required.

    Returns:
        Args for the `pyinstaller` command.
    """
    collect_data = (
        arg
        for module in resource_modules
        for arg in ("--collect-data", module.__name__)
    )
    return self.args(
        "--onefile",
        "--name",
        name,
        "--icon",
        icon.as_posix(),
        *collect_data,
        *args,
        entry_point.as_posix(),
    )
dev_dependencies()

Return the dev dependencies required to build executables.

Extends the default with pillow so pyinstaller can convert a non-native icon image (e.g. PNG) into the per-OS icon format (.ico / .icns) at build time.

Returns:

Type Description
tuple[str, ...]

pyinstaller and pillow.

Source code in src/pyrig_executables/rig/tools/executable_builder.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def dev_dependencies(self) -> tuple[str, ...]:
    """Return the dev dependencies required to build executables.

    Extends the default with `pillow` so `pyinstaller` can convert a
    non-native icon image (e.g. PNG) into the per-OS icon format (`.ico`
    / `.icns`) at build time.

    Returns:
        `pyinstaller` and `pillow`.
    """
    return (*super().dev_dependencies(), "pillow")
dist_dir()

Return the directory pyinstaller writes built executables to.

Single source of truth for the output location, so it never drifts out of sync with other places that need it.

Returns:

Type Description
Path

The dist output directory, relative to the project root.

Source code in src/pyrig_executables/rig/tools/executable_builder.py
116
117
118
119
120
121
122
123
124
125
def dist_dir(self) -> Path:
    """Return the directory `pyinstaller` writes built executables to.

    Single source of truth for the output location, so it never drifts
    out of sync with other places that need it.

    Returns:
        The `dist` output directory, relative to the project root.
    """
    return Path("dist")
group()

Return Group.PROJECT_INFO.

Source code in src/pyrig_executables/rig/tools/executable_builder.py
26
27
28
def group(self) -> str:
    """Return `Group.PROJECT_INFO`."""
    return Group.PROJECT_INFO
image_url()

Return the shields.io URL for the GitHub release downloads badge.

Source code in src/pyrig_executables/rig/tools/executable_builder.py
30
31
32
33
34
35
36
def image_url(self) -> str:
    """Return the shields.io URL for the GitHub release downloads badge."""
    owner, repo = (
        VersionController.I.repo_owner(),
        PackageManager.I.project_name(),
    )
    return f"https://img.shields.io/github/downloads/{owner}/{repo}/total?logo=github&label=downloads"

Return the GitHub releases page URL where the binaries are published.

Source code in src/pyrig_executables/rig/tools/executable_builder.py
38
39
40
def link_url(self) -> str:
    """Return the GitHub releases page URL where the binaries are published."""
    return RemoteVersionController.I.releases_url()
name()

Return 'pyinstaller'.

Source code in src/pyrig_executables/rig/tools/executable_builder.py
22
23
24
def name(self) -> str:
    """Return `'pyinstaller'`."""
    return "pyinstaller"
version_control_ignore_paths()

Return the build artifact paths to exclude from version control.

Returns:

Type Description
str

The pyinstaller build artifacts: the dist/ output directory

...

(where the executables are written), the generated *.spec files,

tuple[str, ...]

and the build/ working directory.

Source code in src/pyrig_executables/rig/tools/executable_builder.py
106
107
108
109
110
111
112
113
114
def version_control_ignore_paths(self) -> tuple[str, ...]:
    """Return the build artifact paths to exclude from version control.

    Returns:
        The `pyinstaller` build artifacts: the `dist/` output directory
        (where the executables are written), the generated `*.spec` files,
        and the `build/` working directory.
    """
    return (f"{self.dist_dir().as_posix()}/", "*.spec", "build/")