Skip to content

Commit 719696e

Browse files
committed
build: add standalone executable packaging
add PyInstaller-based packaging for java-inventory - add cross-platform packaging script and PyInstaller spec - bundle java components, resources, and project metadata - validate required checkstyle and maven build artifacts before packaging - addg github actions builds for linux, windows, and macOS - test packaged executables with help, setup, and test coverage commands - document local packaging and platform build requirements
1 parent 5f46838 commit 719696e

8 files changed

Lines changed: 570 additions & 3 deletions

File tree

‎.gitattributes‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.spec linguist-language=Python

‎.github/workflows/build.yml‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- main
8+
pull_request:
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
build:
16+
name: Build (${{ matrix.name }})
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
include:
21+
- name: linux-x86_64
22+
os: ubuntu-24.04
23+
executable: java-inventory
24+
25+
- name: windows-x86_64
26+
os: windows-2025
27+
executable: java-inventory.exe
28+
29+
- name: macos-x86_64
30+
os: macos-13
31+
executable: java-inventory
32+
33+
- name: macos-arm64
34+
os: macos-14
35+
executable: java-inventory
36+
37+
runs-on: ${{ matrix.os }}
38+
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@v5
42+
43+
- name: Set up Python
44+
uses: actions/setup-python@v6
45+
with:
46+
python-version: "3.11"
47+
48+
- name: Set up JDK
49+
uses: actions/setup-java@v5
50+
with:
51+
distribution: temurin
52+
java-version: "25"
53+
54+
- name: Set up Maven
55+
uses: stCarolas/setup-maven@v5
56+
with:
57+
maven-version: "3.9.11"
58+
59+
- name: Build executable
60+
run: python package.py
61+
62+
- name: Test help
63+
shell: bash
64+
run: ./dist/${{ matrix.executable }} --help
65+
66+
- name: Test setup
67+
shell: bash
68+
run: ./dist/${{ matrix.executable }} setup
69+
70+
- name: Test coverage
71+
shell: bash
72+
run: |
73+
./dist/${{ matrix.executable }} test-coverage \
74+
dev-test/src/main \
75+
dev-test/src/test
76+
test -s test_coverage_report.html
77+
78+
- name: Upload build artifact
79+
uses: actions/upload-artifact@v4
80+
with:
81+
name: java-inventory-${{ matrix.name }}
82+
path: dist/${{ matrix.executable }}
83+
if-no-files-found: error
84+
retention-days: 14

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ venv/
44
**__pycache__/
55
bin/
66
**target/
7+
dist/
8+
9+
*.patch
10+
*.diff

‎.vscode/settings.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"files.associations": {
3+
"*.spec": "python"
4+
}
5+
}

‎README.md‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,74 @@ python java_inventory.py test-coverage --help
326326
python java_inventory.py inventory-dependency --help
327327
```
328328

329+
## Packaging
330+
331+
Build `java-inventory` as a standalone executable with PyInstaller:
332+
333+
```sh
334+
python build.py
335+
```
336+
337+
The build script:
338+
339+
1. Creates a project-local `.venv` if it does not exist.
340+
2. Installs PyInstaller into the build environment if necessary.
341+
3. Runs `python java_inventory.py setup`.
342+
4. Verifies that the required Checkstyle and Maven build artifacts exist.
343+
5. Packages the application and its required resources into a standalone executable.
344+
345+
The resulting executable is written to:
346+
347+
```shell
348+
dist/java-inventory
349+
```
350+
351+
On Windows, the executable is:
352+
353+
```powershell
354+
dist\java-inventory.exe
355+
```
356+
357+
Run the packaged executable with:
358+
359+
```shell
360+
./dist/java-inventory --help
361+
```
362+
363+
On Windows:
364+
365+
```powershell
366+
dist\java-inventory.exe --help
367+
```
368+
369+
### Packaging options
370+
371+
Skip cleanup of previous PyInstaller build artifacts:
372+
373+
```shell
374+
python build.py --no-cleanup
375+
```
376+
377+
Use a different PyInstaller spec file:
378+
379+
```shell
380+
python build.py --spec path/to/custom.spec
381+
```
382+
383+
Enable debug output:
384+
385+
```shell
386+
python build.py --debug
387+
```
388+
389+
The build requires Python, JDK, and Maven. The `setup` step builds the Java components required by the packaged application.
390+
391+
### Platform builds
392+
393+
PyInstaller produces a native executable for the platform on which it runs. To produce binaries for multiple platforms, run the build on each target platform.
394+
395+
For official releases, the project uses GitHub Actions to build platform-specific binaries and attach them to GitHub Releases.
396+
329397
## Checkstyle
330398

331399
The linter uses the project's Checkstyle configuration and custom Checkstyle checks:

‎java_inventory.py‎

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import argparse
88
import importlib.util
99
import sys
10+
import textwrap
11+
import tomllib
1012
from pathlib import Path
1113

12-
from common.inventory_common import error, require_command, run, set_debug
14+
from common.inventory_common import error, require_command, set_debug
1315

1416
ROOT = Path(__file__).resolve().parent
1517

@@ -18,6 +20,12 @@
1820
sys.path.insert(0, str(ROOT))
1921

2022

23+
def project_metadata() -> dict:
24+
"""Load project metadata from pyproject.toml."""
25+
with (ROOT / "pyproject.toml").open("rb") as file:
26+
return tomllib.load(file)["project"]
27+
28+
2129
def load_module(name: str, path: Path):
2230
"""Load a Python module from a path without requiring package layout."""
2331
if not path.is_file():
@@ -32,10 +40,39 @@ def load_module(name: str, path: Path):
3240
return module
3341

3442

35-
def subcommand_setup(_args, root: Path):
43+
def subcommand_setup(_args, _root: Path):
3644
require_command("git")
3745
require_command("mvn")
38-
run(["git", "submodule", "update", "--init", "--recursive"], cwd=root)
46+
47+
48+
def subcommand_version(_args):
49+
metadata = project_metadata()
50+
print(f"{metadata['name']} {metadata['version']}")
51+
52+
53+
def subcommand_about(_args):
54+
metadata = project_metadata()
55+
name = metadata["name"]
56+
version = metadata["version"]
57+
description = metadata["description"]
58+
authors = metadata.get("authors", [])
59+
license_info = metadata.get("license", {})
60+
urls = metadata.get("urls", {})
61+
62+
print(f"{name} {version}")
63+
print()
64+
print(textwrap.fill(description, width=80))
65+
print()
66+
if authors:
67+
print(f"Author: {authors[0]['name']}")
68+
if license_info:
69+
print(f"License: {license_info}")
70+
if "Repository" in urls:
71+
print(f"Repository: {urls['Repository']}")
72+
if "Issues" in urls:
73+
print(f"Issues: {urls['Issues']}")
74+
print()
75+
print(f"Run '{name} --help' for help.")
3976

4077

4178
def build_parser():
@@ -62,10 +99,22 @@ def build_parser():
6299
help="set up all inventory components",
63100
)
64101

102+
info = subparsers.add_parser(
103+
"about",
104+
help="show tool information and available commands",
105+
)
106+
107+
version = subparsers.add_parser(
108+
"version",
109+
help="show the tool version",
110+
)
111+
65112
checkstyle.register_commands(subparsers, ROOT, setup_handlers)
66113
maven.register_commands(subparsers, ROOT, setup_handlers)
67114

68115
setup.set_defaults(setup_handlers=setup_handlers)
116+
info.set_defaults(handler=subcommand_about)
117+
version.set_defaults(handler=subcommand_version)
69118

70119
return parser
71120

@@ -82,6 +131,10 @@ def main() -> int:
82131
handler(args)
83132
return 0
84133

134+
if args.command == "info":
135+
subcommand_about(args)
136+
return 0
137+
85138
args.handler(args)
86139
return 0
87140
except Exception as exc: # pylint: disable=broad-exception-caught # noqa: BLE001

‎java_inventory.spec‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""PyInstaller spec for java_inventory."""
2+
3+
from pathlib import Path
4+
5+
BLOCK_CIPHER = None
6+
7+
PROJECT_ROOT = Path.cwd()
8+
9+
CHECKSTYLE_TARGET = PROJECT_ROOT / "checkstyle" / "target"
10+
MAVEN_TARGET = PROJECT_ROOT / "maven" / "target"
11+
12+
if not CHECKSTYLE_TARGET.is_dir():
13+
raise RuntimeError(
14+
f"Required directory not found: {CHECKSTYLE_TARGET}. "
15+
"Run `python java_inventory.py setup` before building."
16+
)
17+
18+
if not MAVEN_TARGET.is_dir():
19+
raise RuntimeError(
20+
f"Required directory not found: {MAVEN_TARGET}. "
21+
"Run `python java_inventory.py setup` before building."
22+
)
23+
24+
25+
datas = [
26+
("common", "common"),
27+
("checkstyle/src", "checkstyle/src"),
28+
("checkstyle/pom.xml", "checkstyle"),
29+
("checkstyle/target", "checkstyle/target"),
30+
("maven/src", "maven/src"),
31+
("maven/pom.xml", "maven"),
32+
("maven/target", "maven/target"),
33+
("resources", "resources"),
34+
("pyproject.toml", "."),
35+
]
36+
37+
38+
a = Analysis(
39+
[str(PROJECT_ROOT / "java_inventory.py")],
40+
pathex=[str(PROJECT_ROOT)],
41+
binaries=[],
42+
datas=datas,
43+
hiddenimports=[
44+
"json",
45+
],
46+
hookspath=[],
47+
hooksconfig={},
48+
runtime_hooks=[],
49+
excludedimports=[],
50+
win_no_prefer_redirects=False,
51+
win_private_assemblies=False,
52+
cipher=BLOCK_CIPHER,
53+
noarchive=False,
54+
)
55+
56+
57+
pyz = PYZ(
58+
a.pure,
59+
a.zipped_data,
60+
cipher=BLOCK_CIPHER,
61+
)
62+
63+
64+
exe = EXE(
65+
pyz,
66+
a.scripts,
67+
a.binaries,
68+
a.zipfiles,
69+
a.datas,
70+
[],
71+
name="java-inventory",
72+
debug=False,
73+
bootloader_ignore_signals=False,
74+
strip=False,
75+
upx=True,
76+
upx_exclude=[],
77+
runtime_tmpdir=None,
78+
console=True,
79+
disable_windowed_traceback=False,
80+
target_arch=None,
81+
codesign_identity=None,
82+
entitlements_file=None,
83+
)

0 commit comments

Comments
 (0)