pyrig-executables 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrig_executables-1.0.0/LICENSE +21 -0
- pyrig_executables-1.0.0/PKG-INFO +60 -0
- pyrig_executables-1.0.0/README.md +35 -0
- pyrig_executables-1.0.0/pyproject.toml +104 -0
- pyrig_executables-1.0.0/src/pyrig_executables/__init__.py +1 -0
- pyrig_executables-1.0.0/src/pyrig_executables/main.py +9 -0
- pyrig_executables-1.0.0/src/pyrig_executables/py.typed +0 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/__init__.py +1 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/cli/__init__.py +1 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/cli/commands/__init__.py +1 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/cli/commands/run.py +17 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/cli/subcommands.py +17 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/configs/__init__.py +1 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/configs/main.py +80 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/configs/remote_version_control/__init__.py +1 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/configs/remote_version_control/workflows/__init__.py +1 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/configs/remote_version_control/workflows/release.py +234 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/tools/__init__.py +1 -0
- pyrig_executables-1.0.0/src/pyrig_executables/rig/tools/executable_builder.py +87 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Winipedia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrig-executables
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A pyrig plugin to build executables.
|
|
5
|
+
Keywords: pyrig
|
|
6
|
+
Author: Winipedia
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: pyrig>=12.12.0
|
|
17
|
+
Maintainer: Winipedia
|
|
18
|
+
Requires-Python: >=3.13
|
|
19
|
+
Project-URL: Homepage, https://github.com/Winipedia/pyrig-executables
|
|
20
|
+
Project-URL: Documentation, https://Winipedia.github.io/pyrig-executables
|
|
21
|
+
Project-URL: Source, https://github.com/Winipedia/pyrig-executables
|
|
22
|
+
Project-URL: Issues, https://github.com/Winipedia/pyrig-executables/issues
|
|
23
|
+
Project-URL: Changelog, https://github.com/Winipedia/pyrig-executables/releases
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pyrig-executables
|
|
27
|
+
|
|
28
|
+
<!-- security -->
|
|
29
|
+
[](https://github.com/pypa/pip-audit)
|
|
30
|
+
[](https://github.com/PyCQA/bandit)
|
|
31
|
+
<!-- ci/cd -->
|
|
32
|
+
[](https://github.com/Winipedia/pyrig-executables/actions/workflows/health_check.yml)
|
|
33
|
+
[](https://github.com/Winipedia/pyrig-executables/actions/workflows/deploy.yml)
|
|
34
|
+
<!-- code-quality -->
|
|
35
|
+
[](https://github.com/rvben/rumdl)
|
|
36
|
+
[](https://github.com/astral-sh/ruff)
|
|
37
|
+
[](https://github.com/astral-sh/ty)
|
|
38
|
+
[](https://github.com/j178/prek)
|
|
39
|
+
<!-- testing -->
|
|
40
|
+
[](https://codecov.io/gh/Winipedia/pyrig-executables)
|
|
41
|
+
[](https://pytest.org)
|
|
42
|
+
<!-- tooling -->
|
|
43
|
+
[](https://github.com/astral-sh/uv)
|
|
44
|
+
[](https://github.com/Winipedia/pyrig)
|
|
45
|
+
[](https://github.com/Winipedia/pyrig-executables)
|
|
46
|
+
[](https://git-scm.com)
|
|
47
|
+
<!-- documentation -->
|
|
48
|
+
[](https://www.mkdocs.org)
|
|
49
|
+
[](https://Winipedia.github.io/pyrig-executables)
|
|
50
|
+
<!-- project-info -->
|
|
51
|
+
[](https://github.com/Winipedia/pyrig-executables/releases)
|
|
52
|
+
[](https://pypi.org/project/pyrig-executables)
|
|
53
|
+
[](https://www.python.org)
|
|
54
|
+
[](https://github.com/Winipedia/pyrig-executables/blob/main/LICENSE)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
> A pyrig plugin to build executables.
|
|
59
|
+
|
|
60
|
+
---
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# pyrig-executables
|
|
2
|
+
|
|
3
|
+
<!-- security -->
|
|
4
|
+
[](https://github.com/pypa/pip-audit)
|
|
5
|
+
[](https://github.com/PyCQA/bandit)
|
|
6
|
+
<!-- ci/cd -->
|
|
7
|
+
[](https://github.com/Winipedia/pyrig-executables/actions/workflows/health_check.yml)
|
|
8
|
+
[](https://github.com/Winipedia/pyrig-executables/actions/workflows/deploy.yml)
|
|
9
|
+
<!-- code-quality -->
|
|
10
|
+
[](https://github.com/rvben/rumdl)
|
|
11
|
+
[](https://github.com/astral-sh/ruff)
|
|
12
|
+
[](https://github.com/astral-sh/ty)
|
|
13
|
+
[](https://github.com/j178/prek)
|
|
14
|
+
<!-- testing -->
|
|
15
|
+
[](https://codecov.io/gh/Winipedia/pyrig-executables)
|
|
16
|
+
[](https://pytest.org)
|
|
17
|
+
<!-- tooling -->
|
|
18
|
+
[](https://github.com/astral-sh/uv)
|
|
19
|
+
[](https://github.com/Winipedia/pyrig)
|
|
20
|
+
[](https://github.com/Winipedia/pyrig-executables)
|
|
21
|
+
[](https://git-scm.com)
|
|
22
|
+
<!-- documentation -->
|
|
23
|
+
[](https://www.mkdocs.org)
|
|
24
|
+
[](https://Winipedia.github.io/pyrig-executables)
|
|
25
|
+
<!-- project-info -->
|
|
26
|
+
[](https://github.com/Winipedia/pyrig-executables/releases)
|
|
27
|
+
[](https://pypi.org/project/pyrig-executables)
|
|
28
|
+
[](https://www.python.org)
|
|
29
|
+
[](https://github.com/Winipedia/pyrig-executables/blob/main/LICENSE)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
> A pyrig plugin to build executables.
|
|
34
|
+
|
|
35
|
+
---
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyrig-executables"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "A pyrig plugin to build executables."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pyrig>=12.12.0",
|
|
9
|
+
]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = [
|
|
12
|
+
"LICENSE",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
keywords = [
|
|
24
|
+
"pyrig",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[[project.authors]]
|
|
28
|
+
name = "Winipedia"
|
|
29
|
+
|
|
30
|
+
[[project.maintainers]]
|
|
31
|
+
name = "Winipedia"
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Winipedia/pyrig-executables"
|
|
35
|
+
Documentation = "https://Winipedia.github.io/pyrig-executables"
|
|
36
|
+
Source = "https://github.com/Winipedia/pyrig-executables"
|
|
37
|
+
Issues = "https://github.com/Winipedia/pyrig-executables/issues"
|
|
38
|
+
Changelog = "https://github.com/Winipedia/pyrig-executables/releases"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
pyrig-executables = "pyrig.rig.cli.main:main"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"bandit>=1.9.4",
|
|
46
|
+
"mkdocs>=1.6.1",
|
|
47
|
+
"mkdocs-material>=9.7.6",
|
|
48
|
+
"mkdocs-mermaid2-plugin>=1.2.3",
|
|
49
|
+
"mkdocstrings[python]>=1.0.4",
|
|
50
|
+
"pip-audit>=2.10.0",
|
|
51
|
+
"prek>=0.4.4",
|
|
52
|
+
"pygithub>=2.9.1",
|
|
53
|
+
"pyinstaller>=6.21.0",
|
|
54
|
+
"pyrig-codecov>=1.3.2",
|
|
55
|
+
"pyrig-dev>=2.8.0",
|
|
56
|
+
"pyrig-pypi>=1.5.1",
|
|
57
|
+
"pytest>=9.0.3",
|
|
58
|
+
"pytest-cov>=7.1.0",
|
|
59
|
+
"pytest-mock>=3.15.1",
|
|
60
|
+
"ruff>=0.15.16",
|
|
61
|
+
"rumdl>=0.2.11",
|
|
62
|
+
"ty>=0.0.47",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[build-system]
|
|
66
|
+
requires = [
|
|
67
|
+
"uv_build",
|
|
68
|
+
]
|
|
69
|
+
build-backend = "uv_build"
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
select = [
|
|
73
|
+
"ALL",
|
|
74
|
+
]
|
|
75
|
+
ignore = [
|
|
76
|
+
"COM812",
|
|
77
|
+
"ANN401",
|
|
78
|
+
]
|
|
79
|
+
fixable = [
|
|
80
|
+
"ALL",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint.per-file-ignores]
|
|
84
|
+
"**/tests/**/*.py" = [
|
|
85
|
+
"S101",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
[tool.ruff.lint.pydocstyle]
|
|
89
|
+
convention = "google"
|
|
90
|
+
|
|
91
|
+
[tool.ty.terminal]
|
|
92
|
+
error-on-warning = true
|
|
93
|
+
|
|
94
|
+
[tool.pytest.ini_options]
|
|
95
|
+
testpaths = [
|
|
96
|
+
"tests",
|
|
97
|
+
]
|
|
98
|
+
addopts = "--cov=pyrig_executables --cov-branch --cov-report=term-missing --cov-fail-under=100 --cov-report=xml"
|
|
99
|
+
|
|
100
|
+
[tool.bandit.assert_used]
|
|
101
|
+
skips = [
|
|
102
|
+
"*/tests/*.py",
|
|
103
|
+
"*/test_*/*.py",
|
|
104
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The top-level package for the project."""
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package initialization."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package initialization."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package initialization."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""CLI command implementation for running the project's entry-point module."""
|
|
2
|
+
|
|
3
|
+
from runpy import run_path
|
|
4
|
+
|
|
5
|
+
from pyrig_executables.rig.configs.main import MainConfigFile
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_main() -> None:
|
|
9
|
+
"""Execute the project's ``main.py`` as the ``__main__`` module.
|
|
10
|
+
|
|
11
|
+
Resolves the path to the project's entry-point file via
|
|
12
|
+
:class:`~pyrig_executables.rig.configs.main.MainConfigFile` and runs it with
|
|
13
|
+
``runpy`` under the ``__main__`` name. Running by path avoids importing the
|
|
14
|
+
module first, so its ``if __name__ == "__main__"`` guard fires and calls
|
|
15
|
+
``main``, mirroring how the built executable runs the same file.
|
|
16
|
+
"""
|
|
17
|
+
run_path(MainConfigFile.I.path().as_posix(), run_name="__main__")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Project-specific CLI commands.
|
|
2
|
+
|
|
3
|
+
All functions in this module are automatically discovered and registered
|
|
4
|
+
as CLI commands for this project.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run() -> None:
|
|
9
|
+
"""Run the project by executing its ``main.py`` as the entry module.
|
|
10
|
+
|
|
11
|
+
Runs the project's ``main.py`` under the ``__main__`` name so its
|
|
12
|
+
``if __name__ == "__main__"`` guard fires and calls ``main``, mirroring how
|
|
13
|
+
the built executable runs.
|
|
14
|
+
"""
|
|
15
|
+
from pyrig_executables.rig.cli.commands.run import run_main # noqa: PLC0415
|
|
16
|
+
|
|
17
|
+
run_main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package initialization."""
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Configuration for the project's ``main`` entry-point module.
|
|
2
|
+
|
|
3
|
+
Scaffolds a ``main.py`` containing a callable ``main`` function in every
|
|
4
|
+
project that installs this plugin. The module provides the entry point that the
|
|
5
|
+
executable builder bundles into a standalone binary, so this config guarantees
|
|
6
|
+
that a suitable build target always exists.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
|
|
11
|
+
from pyrig.rig.configs.base.copy_module import CopyModuleConfigFile
|
|
12
|
+
|
|
13
|
+
from pyrig_executables import main as main_module
|
|
14
|
+
from pyrig_executables.main import main as main_func
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MainConfigFile(CopyModuleConfigFile):
|
|
18
|
+
"""Manages the project's ``main.py`` entry-point module.
|
|
19
|
+
|
|
20
|
+
Copies this plugin's :mod:`pyrig_executables.main` scaffolding into the
|
|
21
|
+
target project (with the package prefix rewritten to the project's package
|
|
22
|
+
name), producing a ``main.py`` with a ``main`` entry point. Once the file
|
|
23
|
+
exists, validation only requires that a callable ``main`` is present, so any
|
|
24
|
+
user-implemented entry point is preserved and never overwritten.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def copy_module(self) -> ModuleType:
|
|
28
|
+
"""Return the source module whose content is copied into the project.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The :mod:`pyrig_executables.main` module used as the entry-point
|
|
32
|
+
scaffolding.
|
|
33
|
+
"""
|
|
34
|
+
return main_module
|
|
35
|
+
|
|
36
|
+
def is_correct(self) -> bool:
|
|
37
|
+
"""Return whether the project's ``main.py`` is valid.
|
|
38
|
+
|
|
39
|
+
Overrides the base :meth:`is_correct` to assert that ``main.py`` is only
|
|
40
|
+
correct when the target module both exposes a callable named ``main``
|
|
41
|
+
and contains an ``if __name__ == "__main__"`` execution guard. The
|
|
42
|
+
function body is deliberately ignored so that a project's own
|
|
43
|
+
entry-point implementation is treated as valid and is never overwritten.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
``True`` if the target module defines a callable ``main`` and a
|
|
47
|
+
``__main__`` guard is present.
|
|
48
|
+
"""
|
|
49
|
+
return self.has_callable_main() and self.has_main_guard()
|
|
50
|
+
|
|
51
|
+
def has_callable_main(self) -> bool:
|
|
52
|
+
"""Return whether the target module exposes a callable ``main``.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
``True`` if the module defines an attribute named ``main`` that is
|
|
56
|
+
callable.
|
|
57
|
+
"""
|
|
58
|
+
return callable(getattr(self.module(), main_func.__name__, None))
|
|
59
|
+
|
|
60
|
+
def has_main_guard(self) -> bool:
|
|
61
|
+
"""Return whether the target module has a ``__main__`` execution guard.
|
|
62
|
+
|
|
63
|
+
Checks that the file's content contains the :meth:`main_guard` snippet,
|
|
64
|
+
the conventional guard that runs ``main`` when the module is executed
|
|
65
|
+
directly.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
``True`` if the ``__main__`` guard snippet is present in the file.
|
|
69
|
+
"""
|
|
70
|
+
return self.main_guard() in self.read_content()
|
|
71
|
+
|
|
72
|
+
def main_guard(self) -> str:
|
|
73
|
+
"""Return the canonical ``__main__`` execution guard snippet.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The ``if __name__ == "__main__"`` block that calls ``main``, used to
|
|
77
|
+
check for and scaffold the guard in the target module.
|
|
78
|
+
"""
|
|
79
|
+
return f"""if __name__ == "__main__":
|
|
80
|
+
{main_func.__name__}()"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package initialization."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package initialization."""
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Workflow configuration for automated GitHub release creation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pyrig.rig.configs.base.config_file import ConfigDict
|
|
6
|
+
from pyrig.rig.configs.remote_version_control.workflows.release import (
|
|
7
|
+
ReleaseWorkflowConfigFile as BaseReleaseWorkflowConfigFile,
|
|
8
|
+
)
|
|
9
|
+
from pyrig.rig.tools.package_manager import PackageManager
|
|
10
|
+
|
|
11
|
+
from pyrig_executables.rig.configs.main import MainConfigFile
|
|
12
|
+
from pyrig_executables.rig.tools.executable_builder import ExecutableBuilder
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ReleaseWorkflowConfigFile(BaseReleaseWorkflowConfigFile):
|
|
16
|
+
"""Release workflow that builds and attaches standalone executables.
|
|
17
|
+
|
|
18
|
+
Extends the base release workflow with a matrix job that builds a
|
|
19
|
+
single-file executable on every supported operating system, then attaches
|
|
20
|
+
all of them to the GitHub release created by the base ``publish`` job.
|
|
21
|
+
|
|
22
|
+
Two jobs run, in order:
|
|
23
|
+
|
|
24
|
+
1. ``executable`` — a matrix job that fans out into one job per OS
|
|
25
|
+
(Linux, Windows, macOS), each building the project into a single
|
|
26
|
+
``pyinstaller`` ``--onefile`` binary and uploading it as a per-OS
|
|
27
|
+
workflow artifact.
|
|
28
|
+
2. ``publish`` — the base release job, now gated on ``executable``.
|
|
29
|
+
It downloads every executable artifact into ``dist/`` and attaches them
|
|
30
|
+
to the GitHub release alongside the generated changelog.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def jobs(self) -> ConfigDict:
|
|
34
|
+
"""Build the complete jobs configuration for the workflow.
|
|
35
|
+
|
|
36
|
+
Prepends the executable build job to the base jobs so it runs before
|
|
37
|
+
the ``publish`` job that consumes its artifacts.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dict containing the build job followed by the base release jobs.
|
|
41
|
+
"""
|
|
42
|
+
return {
|
|
43
|
+
**self.job_executable(),
|
|
44
|
+
**super().jobs(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def job_executable(self) -> ConfigDict:
|
|
48
|
+
"""Build the matrix job that compiles the executable on every OS.
|
|
49
|
+
|
|
50
|
+
Runs across the default OS matrix (Linux, Windows, macOS), since
|
|
51
|
+
``pyinstaller`` cannot cross-compile and each binary must be built on
|
|
52
|
+
its target platform.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Job configuration with an OS matrix strategy, a dynamic
|
|
56
|
+
``runs-on`` value, and the build and upload steps.
|
|
57
|
+
"""
|
|
58
|
+
return self.job(
|
|
59
|
+
job_func=self.job_executable,
|
|
60
|
+
strategy=self.strategy_matrix_os(),
|
|
61
|
+
runs_on=self.insert_matrix_os(),
|
|
62
|
+
steps=self.steps_executable(),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def job_publish(self) -> ConfigDict:
|
|
66
|
+
"""Build the release job, gated on the executable build job.
|
|
67
|
+
|
|
68
|
+
Adds a ``needs`` dependency on ``executable`` so the release is
|
|
69
|
+
only published once every platform's binary is available to attach.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The base release job with a ``needs`` dependency added.
|
|
73
|
+
"""
|
|
74
|
+
jobs = super().job_publish()
|
|
75
|
+
jobs[self.make_id_from_func(self.job_publish)]["needs"] = [
|
|
76
|
+
self.make_id_from_func(self.job_executable)
|
|
77
|
+
]
|
|
78
|
+
return jobs
|
|
79
|
+
|
|
80
|
+
def steps_executable(self) -> list[dict[str, Any]]:
|
|
81
|
+
"""Build the ordered steps for the executable build job.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Steps that set up the environment, build the single-file
|
|
85
|
+
executable, and upload it as a per-OS artifact.
|
|
86
|
+
"""
|
|
87
|
+
return [
|
|
88
|
+
*self.steps_core_installed_setup(),
|
|
89
|
+
self.step_build_executable(),
|
|
90
|
+
self.step_upload_executable(),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
def steps_publish(self) -> list[dict[str, Any]]:
|
|
94
|
+
"""Build the ordered steps for the release job.
|
|
95
|
+
|
|
96
|
+
Extends the base steps with a download step that pulls every platform's
|
|
97
|
+
executable into ``dist/`` immediately before the release is created.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The base publish steps with the executable download step inserted
|
|
101
|
+
just before the create-release step.
|
|
102
|
+
"""
|
|
103
|
+
steps = super().steps_publish()
|
|
104
|
+
create_release_id = self.make_id_from_func(self.step_create_release)
|
|
105
|
+
create_release_index = next(
|
|
106
|
+
index for index, step in enumerate(steps) if step["id"] == create_release_id
|
|
107
|
+
)
|
|
108
|
+
steps.insert(create_release_index, self.step_download_executables())
|
|
109
|
+
return steps
|
|
110
|
+
|
|
111
|
+
def step_build_executable(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
step: dict[str, Any] | None = None,
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
"""Build a step that compiles the project into a single-file executable.
|
|
117
|
+
|
|
118
|
+
Runs ``pyinstaller --onefile`` against the project's entry-point module,
|
|
119
|
+
naming the binary after the project and the current runner OS so the
|
|
120
|
+
per-platform assets do not collide on the release.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
step: Additional keys to merge into the step configuration.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Step that runs the executable builder via uv.
|
|
127
|
+
"""
|
|
128
|
+
return self.step(
|
|
129
|
+
step_func=self.step_build_executable,
|
|
130
|
+
run=str(
|
|
131
|
+
PackageManager.I.run_args(
|
|
132
|
+
*ExecutableBuilder.I.build_args(
|
|
133
|
+
name=self.executable_name(),
|
|
134
|
+
entry_point=MainConfigFile.I.path(),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
),
|
|
138
|
+
step=step,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def step_upload_executable(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
step: dict[str, Any] | None = None,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""Build a step that uploads the built executable as a workflow artifact.
|
|
147
|
+
|
|
148
|
+
Uploads the contents of ``dist/`` under a per-OS artifact name so the
|
|
149
|
+
``publish`` job can later download every platform's binary.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
step: Additional keys to merge into the step configuration.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Step using ``actions/upload-artifact@main``.
|
|
156
|
+
"""
|
|
157
|
+
return self.step(
|
|
158
|
+
step_func=self.step_upload_executable,
|
|
159
|
+
uses="actions/upload-artifact@main",
|
|
160
|
+
with_={
|
|
161
|
+
"name": self.executable_name(),
|
|
162
|
+
"path": ExecutableBuilder.I.dist_dir().as_posix(),
|
|
163
|
+
},
|
|
164
|
+
step=step,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def step_download_executables(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
step: dict[str, Any] | None = None,
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""Build a step that downloads every executable artifact into ``dist/``.
|
|
173
|
+
|
|
174
|
+
Merges every per-OS executable artifact (matched by the project-name
|
|
175
|
+
prefix) into a single ``dist/`` directory so they can be attached to
|
|
176
|
+
the release with one glob.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
step: Additional keys to merge into the step configuration.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Step using ``actions/download-artifact@main``.
|
|
183
|
+
"""
|
|
184
|
+
return self.step(
|
|
185
|
+
step_func=self.step_download_executables,
|
|
186
|
+
uses="actions/download-artifact@main",
|
|
187
|
+
with_={
|
|
188
|
+
"pattern": f"{PackageManager.I.project_name()}*",
|
|
189
|
+
"path": ExecutableBuilder.I.dist_dir().as_posix(),
|
|
190
|
+
"merge-multiple": "true",
|
|
191
|
+
},
|
|
192
|
+
step=step,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def step_create_release(
|
|
196
|
+
self,
|
|
197
|
+
*,
|
|
198
|
+
step: dict[str, Any] | None = None,
|
|
199
|
+
) -> dict[str, Any]:
|
|
200
|
+
"""Build the create-release step, attaching the built executables.
|
|
201
|
+
|
|
202
|
+
Extends the base release step by attaching every binary downloaded into
|
|
203
|
+
``dist/`` as a release asset.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
step: Additional keys to merge into the step configuration.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The base create-release step with ``dist/*`` added as artifacts.
|
|
210
|
+
"""
|
|
211
|
+
step = super().step_create_release(step=step)
|
|
212
|
+
step["with"]["artifacts"] = (ExecutableBuilder.I.dist_dir() / "*").as_posix()
|
|
213
|
+
return step
|
|
214
|
+
|
|
215
|
+
def executable_name(self) -> str:
|
|
216
|
+
"""Build the per-OS name shared by the executable and its artifact.
|
|
217
|
+
|
|
218
|
+
Combines the project name with the runner OS so each platform's binary
|
|
219
|
+
and its upload artifact get a unique, collision-free name (e.g.
|
|
220
|
+
``pyrig-executables-Linux``). The OS is resolved at workflow runtime.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The ``<project>-<os>`` name string.
|
|
224
|
+
"""
|
|
225
|
+
return f"{PackageManager.I.project_name()}-{self.insert_os()}"
|
|
226
|
+
|
|
227
|
+
def insert_os(self) -> str:
|
|
228
|
+
"""Get the ``${{ runner.os }}`` expression.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
GitHub Actions expression resolving to the current runner's
|
|
232
|
+
operating system (e.g. ``Linux``, ``Windows``, ``macOS``).
|
|
233
|
+
"""
|
|
234
|
+
return self.insert_var("runner.os")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package initialization."""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Executable builder tool wrapper.
|
|
2
|
+
|
|
3
|
+
Wraps commands and information of the executable builder tool, which bundles
|
|
4
|
+
the project into standalone binaries that are attached to GitHub releases.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pyrig.core.subprocesses import Args
|
|
10
|
+
from pyrig.rig.tools.base.tool import Group, Tool
|
|
11
|
+
from pyrig.rig.tools.package_manager import PackageManager
|
|
12
|
+
from pyrig.rig.tools.version_control.remote import RemoteVersionController
|
|
13
|
+
from pyrig.rig.tools.version_control.version_controller import VersionController
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExecutableBuilder(Tool):
|
|
17
|
+
"""Executable builder wrapper.
|
|
18
|
+
|
|
19
|
+
Builds standalone executables of the project and exposes a project-specific
|
|
20
|
+
badge that advertises the downloadable release binaries. The badge shows the
|
|
21
|
+
cumulative download count of all release assets and links to the GitHub
|
|
22
|
+
releases page, where the executables produced by this plugin are published.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def name(self) -> str:
|
|
26
|
+
"""Get tool name."""
|
|
27
|
+
return "pyinstaller"
|
|
28
|
+
|
|
29
|
+
def group(self) -> str:
|
|
30
|
+
"""Returns the group the tool belongs to."""
|
|
31
|
+
return Group.PROJECT_INFO
|
|
32
|
+
|
|
33
|
+
def image_url(self) -> str:
|
|
34
|
+
"""Get the GitHub release downloads badge URL."""
|
|
35
|
+
owner, repo = (
|
|
36
|
+
VersionController.I.repo_owner(check_repo_url=False),
|
|
37
|
+
PackageManager.I.project_name(),
|
|
38
|
+
)
|
|
39
|
+
return f"https://img.shields.io/github/downloads/{owner}/{repo}/total?logo=github&label=downloads"
|
|
40
|
+
|
|
41
|
+
def link_url(self) -> str:
|
|
42
|
+
"""Get the GitHub releases page URL where the binaries are published."""
|
|
43
|
+
return RemoteVersionController.I.releases_url()
|
|
44
|
+
|
|
45
|
+
def build_args(self, *args: str, name: str, entry_point: Path) -> Args:
|
|
46
|
+
"""Construct the command that bundles a single-file executable.
|
|
47
|
+
|
|
48
|
+
Produces ``pyinstaller --onefile --windowed --name <name>
|
|
49
|
+
<entry_point>``, which builds one self-contained, windowed binary (no
|
|
50
|
+
console window) from the given entry-point script. The result is written
|
|
51
|
+
to the ``dist/`` directory. Override this method for a console
|
|
52
|
+
application, which keeps a terminal attached on Windows.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
name: Output name for the executable (without an OS-specific
|
|
56
|
+
extension; ``pyinstaller`` appends ``.exe`` on Windows).
|
|
57
|
+
entry_point: Path to the entry-point script to bundle.
|
|
58
|
+
*args: Additional arguments forwarded to ``pyinstaller``.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Args for ``pyinstaller --onefile --windowed --name <name>
|
|
62
|
+
<entry_point>``.
|
|
63
|
+
"""
|
|
64
|
+
return self.args(
|
|
65
|
+
"--onefile", "--windowed", "--name", name, *args, entry_point.as_posix()
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def version_control_ignore_paths(self) -> tuple[str, ...]:
|
|
69
|
+
"""Return the build artifact paths to exclude from version control.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The ``dist`` directory that ``pyinstaller`` writes the built
|
|
73
|
+
executables to.
|
|
74
|
+
"""
|
|
75
|
+
return (f"{self.dist_dir().as_posix()}/", "*.spec", "build/")
|
|
76
|
+
|
|
77
|
+
def dist_dir(self) -> Path:
|
|
78
|
+
"""Return the directory ``pyinstaller`` writes built executables to.
|
|
79
|
+
|
|
80
|
+
Single source of truth for the output location, reused by the version
|
|
81
|
+
control ignore paths and by the release workflow when uploading,
|
|
82
|
+
downloading, and attaching the binaries.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The ``dist`` output directory, relative to the project root.
|
|
86
|
+
"""
|
|
87
|
+
return Path("dist")
|