Skip to content

Commit 639e3f4

Browse files
👷 [+build] Building binaries as a part of CI (#13)
2 parents edb202c + ab7dbbc commit 639e3f4

File tree

6 files changed

+404
-7
lines changed

6 files changed

+404
-7
lines changed

.github/actions/release_impl/action.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,15 @@ runs:
3636
with:
3737
enable-cache: false # No uv.lock or requirements.txt files, so nothing to cache on
3838

39-
- name: Download Python Package
39+
- name: Download Artifacts
4040
uses: actions/download-artifact@v4
4141
with:
42-
name: Python package
4342
path: dist
43+
merge-multiple: true
44+
45+
- name: Remove Coverage Files Artifacts
46+
shell: bash
47+
run: rm dist/.coverage*
4448

4549
# Create repository tag
4650
- name: Create Repository Tag

.github/workflows/CICD_impl.yml

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,91 @@ jobs:
222222
- name: Validate Python Package
223223
run: uv run python -c "import FileBackup as package; assert package.__version__ == '${{ env.PACKAGE_VERSION }}', package.__version__"
224224

225+
# ----------------------------------------------------------------------
226+
build_binary:
227+
needs: python_package
228+
229+
strategy:
230+
fail-fast: false
231+
232+
matrix:
233+
os: ${{ fromJson(inputs.operating_system_json_string) }}
234+
235+
name: Build Stand Alone Binary
236+
runs-on: ${{ matrix.os }}
237+
238+
env:
239+
PACKAGE_VERSION: ${{ needs.python_package.outputs.package_version }}
240+
241+
permissions:
242+
contents: read
243+
244+
steps:
245+
- uses: actions/checkout@v4
246+
247+
- name: Install uv and python
248+
uses: astral-sh/setup-uv@v5
249+
with:
250+
python-version: ${{ inputs.python_package_version }}
251+
enable-cache: true
252+
253+
- name: Build Binary
254+
run: uv run python BuildBinary.py Build --verbose
255+
256+
- name: Bundle Binary
257+
run: uv run python BuildBinary.py Bundle --custom-filename-suffix "-${{ env.PACKAGE_VERSION }}.${{ matrix.os }}" --verbose
258+
259+
- name: Upload Bundle
260+
uses: actions/upload-artifact@v4
261+
with:
262+
name: Binary.${{ matrix.os }}
263+
path: FileBackup-${{ env.PACKAGE_VERSION }}.*
264+
validate_binary:
265+
needs: [python_package, build_binary]
266+
267+
strategy:
268+
fail-fast: false
269+
270+
matrix:
271+
os: ${{ fromJson(inputs.operating_system_json_string) }}
272+
273+
name: Validate Stand Alone Binary
274+
runs-on: ${{ matrix.os }}
275+
276+
env:
277+
PACKAGE_VERSION: ${{ needs.python_package.outputs.package_version }}
278+
279+
permissions: {}
280+
281+
steps:
282+
- name: Download Binary
283+
uses: actions/download-artifact@v4
284+
with:
285+
name: Binary.${{ matrix.os }}
286+
path: binary
287+
288+
- name: Unzip Archive (Windows)
289+
if: ${{ startsWith(matrix.os, 'windows') }}
290+
run: |-
291+
cd binary
292+
tar -xf FileBackup-${{ env.PACKAGE_VERSION }}.windows.zip
293+
294+
- name: Unzip Archive (Not Windows)
295+
if: ${{ !startsWith(matrix.os, 'windows') }}
296+
run: |-
297+
cd binary
298+
299+
for file in ./*
300+
do
301+
tar -xvzf "$file"
302+
done
303+
304+
- name: Validate Binary
305+
run: binary/FileBackup version
306+
225307
# ----------------------------------------------------------------------
226308
release:
227-
needs: [package_coverage, python_package, validate_python_package]
309+
needs: [package_coverage, python_package, validate_python_package, validate_binary]
228310

229311
name: Release
230312
runs-on: ubuntu-latest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
__pycache__
55
minisign_key.pri
66
post_generation_instructions.html
7+
build/

BuildBinary.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import os
2+
import shutil
3+
4+
from pathlib import Path
5+
from typing import Annotated, Optional
6+
7+
import typer
8+
9+
from dbrownell_Common import SubprocessEx
10+
from dbrownell_Common.Streams.DoneManager import DoneManager, Flags as DoneManagerFlags
11+
from typer.core import TyperGroup
12+
13+
14+
# ----------------------------------------------------------------------
15+
class NaturalOrderGrouper(TyperGroup):
16+
# pylint: disable=missing-class-docstring
17+
# ----------------------------------------------------------------------
18+
def list_commands(self, *args, **kwargs): # pylint: disable=unused-argument
19+
return self.commands.keys()
20+
21+
22+
# ----------------------------------------------------------------------
23+
app = typer.Typer(
24+
cls=NaturalOrderGrouper,
25+
help=__doc__,
26+
no_args_is_help=True,
27+
pretty_exceptions_show_locals=False,
28+
pretty_exceptions_enable=False,
29+
)
30+
31+
32+
# ----------------------------------------------------------------------
33+
@app.command("Build", no_args_is_help=False)
34+
def Build(
35+
verbose: Annotated[
36+
bool,
37+
typer.Option("--verbose", help="Write verbose information to the terminal."),
38+
] = False,
39+
debug: Annotated[
40+
bool,
41+
typer.Option("--debug", help="Write debug information to the terminal."),
42+
] = False,
43+
) -> None:
44+
"""Build a standalone binary for the application."""
45+
46+
with DoneManager.CreateCommandLine(
47+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
48+
) as dm:
49+
command_line = "cxfreeze --script src/FileBackup/CommandLine/EntryPoint.py --target-name=FileBackup"
50+
51+
dm.WriteVerbose(f"Command line: {command_line}\n\n")
52+
53+
with dm.YieldStream() as stream:
54+
dm.result = SubprocessEx.Stream(command_line, stream)
55+
56+
57+
# ----------------------------------------------------------------------
58+
@app.command("Bundle", no_args_is_help=False)
59+
def Bundle(
60+
custom_filename_suffix: Annotated[
61+
Optional[str],
62+
typer.Option("--custom-filename-suffix", help="Custom suffix for the output filename."),
63+
] = None,
64+
verbose: Annotated[
65+
bool,
66+
typer.Option("--verbose", help="Write verbose information to the terminal."),
67+
] = False,
68+
debug: Annotated[
69+
bool,
70+
typer.Option("--debug", help="Write debug information to the terminal."),
71+
] = False,
72+
) -> None:
73+
"""Bundle a previously built standalone binary."""
74+
75+
custom_filename_suffix = (custom_filename_suffix or "").removesuffix("-latest")
76+
77+
with DoneManager.CreateCommandLine(
78+
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
79+
) as dm:
80+
build_dir = Path(__file__).parent / "build"
81+
82+
if not build_dir.is_dir():
83+
dm.WriteError(
84+
f"The build directory '{build_dir}' does not exist. Please run the 'Build' command first.\n"
85+
)
86+
return
87+
88+
subdirs = list(build_dir.iterdir())
89+
if len(subdirs) != 1 or not subdirs[0].is_dir():
90+
dm.WriteError(
91+
f"The build directory '{build_dir}' should contain exactly one subdirectory with the built binary and its dependencies.\n"
92+
)
93+
return
94+
95+
build_dir /= subdirs[0]
96+
97+
output_name = f"FileBackup{custom_filename_suffix or ''}"
98+
99+
if os.name == "nt":
100+
output_filename = Path(f"{output_name}.zip")
101+
format = "zip"
102+
else:
103+
output_filename = Path(f"{output_name}.tar.gz")
104+
format = "gztar"
105+
106+
with dm.Nested(f"Creating '{output_filename.name}'..."):
107+
output_filename.unlink(missing_ok=True)
108+
109+
shutil.make_archive(
110+
output_name,
111+
format,
112+
root_dir=build_dir,
113+
)
114+
115+
116+
# ----------------------------------------------------------------------
117+
# ----------------------------------------------------------------------
118+
# ----------------------------------------------------------------------
119+
if __name__ == "__main__":
120+
app()

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ keywords = [
2020
"restore",
2121
]
2222

23+
license = "MIT"
24+
2325
classifiers = [
2426
"Development Status :: 4 - Beta", # TODO
2527
"Environment :: Console",
2628
"Intended Audience :: Developers",
2729
"Intended Audience :: End Users/Desktop",
2830
"Intended Audience :: System Administrators",
29-
"License :: OSI Approved :: MIT License",
3031
"Natural Language :: English",
3132
"Operating System :: MacOS",
3233
"Operating System :: Microsoft :: Windows",
@@ -42,9 +43,6 @@ classifiers = [
4243
"Topic :: Utilities",
4344
]
4445

45-
[project.license]
46-
text = "MIT"
47-
4846
[project.urls]
4947
Homepage = "https://github.com/davidbrownell/FileBackup"
5048
Documentation = "https://github.com/davidbrownell/FileBackup"
@@ -61,6 +59,8 @@ build-backend = "hatchling.build"
6159
[dependency-groups]
6260
dev = [
6361
"autogitsemver>=0.8.0",
62+
"cx-freeze>=8.3.0",
63+
"dbrownell-commitemojis>=0.1.3",
6464
"pre-commit>=4.2.0",
6565
"pytest>=8.3.5",
6666
"pytest-cov>=6.1.1",

0 commit comments

Comments
 (0)