diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f82226b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,56 @@ +# Inspiration from + +name: Build + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + + workflow_dispatch: + +jobs: + + build: + name: Build jlmkr tool + runs-on: ubuntu-24.04 + strategy: + matrix: + python-version: + - "3.11" # TrueNAS SCALE 24.04 Dragonfish + steps: + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Set up pip cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }} + restore-keys: ${{ runner.os }}-pip- + + - name: Install Hatch + uses: pypa/hatch@install + + - name: Run unit tests + run: hatch run +py=${{ matrix.python-version }} test:test + + - name: Build distribution + run: hatch build -t zipapp -t appzip + +# - name: Upload artifacts +# uses: actions/upload-artifact@v4 +# with: +# path: +# - dist/jlmkr +# - dist/jlmkr-*.zip +# if-no-files-found: error diff --git a/.gitignore b/.gitignore index 6049381..6c6dbf4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,23 @@ -/.venv/ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +dist/ + +# jail-specific /.lxc/ /jails/ + +# Mac-specific .DS_Store +._.DS_Store + +# + +/.venv/ + +__pycache__/ +*.py[cod] + +.pytest_cache/ +.ruff_cache/ diff --git a/README.md b/README.md index d5c1f69..4d626fd 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,7 @@ After cloning the project, navigate into its working directory and create a self > .venv/bin/virtualenv --no-setuptools .venv > rm -rf microvenv pip.pyz > ``` +*Note: This process and the resulting build environment will cache some items under `~/.local/share` in addition to the project directory.* Activate the venv into your *current* shell session. @@ -239,7 +240,25 @@ Develop away. Note that when you're done, you can undo this activation and retur For more information on Python standard venvs, go to [the source](https://packaging.python.org/en/latest/tutorials/installing-packages/#creating-and-using-virtual-environments). -*TODO: introduce the tools we use for building, testing, and stylechecking.* +### Hatching a build + +While in an *active* session, install the [Hatch](https://hatch.pypa.io) project manager. This will load quite a flurry of dependencies, but will only do so into the new `.venv` directory. + + pip install hatch + +Build the "zipapp" target. This will create a `dist/jlmkr` tool which is the direct descendant of Jip-Hop's original `jlmkr.py` script. + + hatch build -t zipapp + +Now build the "appzip" target. This bundles the tool, `README.md` and `LICENSING` into a downloadable zip archive. + + hatch build -t appzip + +If you make any changes *to the embedded builder plugins* that perform the above, then you will need to clear caches between builds. Otherwise and generally, you will not need to do so. + + hatch env prune + +Hatch has oodles more features yet to be explored, such as: automated testing, code coverage, and style checking. For now, we've gotten it building. ## Filing Issues and Community Support diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2088b9d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +[project] +name = "jlmkr" +description = "Build and manage jails on TrueNAS SCALE" +dynamic = ["version"] +readme = "README.md" +classifiers = [ + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Private :: Do Not Upload", +] +authors = [ + {name = "Jip-Hop"}, + {name = "Lockszmith-GH"}, + {name = "jonct"}, +] +maintainers = [ + {name = "Jip-Hop"}, +] +requires-python = ">=3.8" + +[project.urls] +GitHub = "https://github.com/Jip-Hop/jailmaker" + +[build-system] +requires = [ + "hatchling", +# "hatch-zipapp @ file:src/builder/app", +# "hatch-appzip @ file:src/builder/zip", +] +build-backend = "hatchling.build" + +[project.scripts] +jlmkr = "jlmkr.donor:main" + +[tool.hatch.version] +path = "src/jlmkr/__about__.py" # or source = "vcs" + +[tool.hatch.build.zipapp] +dependencies = ["hatch-zipapp-builder @ file:src/builder/app"] + +[tool.hatch.build.appzip] +dependencies = ["hatch-appzip-builder @ file:src/builder/zip"] + +[tool.hatch.build.targets.custom] +path = "src/builder/builder.py" + +[tool.hatch.env] +requires = [ + "hatch-zipapp @ file:src/builder/app", + "hatch-appzip @ file:src/builder/zip", +] + +[[tool.hatch.envs.test.matrix]] +python = [ + "3.11", # TrueNAS SCALE 24.04 Dragonfish +] + +[tool.hatch.envs.types] +extra-dependencies = [ + "mypy>=1.0.0", +] +[tool.hatch.envs.types.scripts] +check = "mypy --install-types --non-interactive {args:src/jlmkr tests}" + +[tool.coverage.run] +source_pkgs = ["jlmkr", "tests"] +branch = true +parallel = true +omit = [ + "src/jlmkr/__about__.py", +] + +[tool.coverage.paths] +jlmkr = ["src/jlmkr"] +tests = ["src/tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/src/builder/app/__init__.py b/src/builder/app/__init__.py new file mode 100644 index 0000000..4662c4f --- /dev/null +++ b/src/builder/app/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only diff --git a/src/builder/app/build_app.py b/src/builder/app/build_app.py new file mode 100644 index 0000000..22675ed --- /dev/null +++ b/src/builder/app/build_app.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +import os +from io import BytesIO +from pathlib import Path +from typing import Any, Callable, Iterable +from zipapp import create_archive + +from hatchling.builders.plugin.interface import BuilderInterface + + +class ZipAppBuilder(BuilderInterface): + PLUGIN_NAME = "zipapp" + + def get_version_api(self) -> dict[str, Callable[..., str]]: + return {"standard": self.build_standard} + + def clean(self, directory: str, versions: Iterable[str]) -> None: + try: + os.remove(Path(directory, 'jlmkr')) + except: + pass + + def build_standard(self, directory: str, **build_data: Any) -> str: + + # generate zipapp source archive + pyzbuffer = BytesIO() + create_archive('src/jlmkr', target=pyzbuffer, + interpreter='=PLACEHOLDER=', +# main='donor.jlmkr:main', + compressed=True) + zipdata = pyzbuffer.getvalue() #.removeprefix(b"#!=PLACEHOLDER=\n") + + # output with preamble + outpath = Path(directory, 'jlmkr') + with open(outpath, 'wb') as f: + f.write(preamble(self.metadata.version).encode()) + f.write(zipdata) + os.chmod(outpath, 0o755) + return os.fspath(outpath) + + +# 10 lines will conveniently match the default of head(1) +def preamble(version): return f'''#!/usr/bin/env python3 + +jlmkr {version} + +Persistent Linux 'jails' on TrueNAS SCALE to install software (k3s, docker, portainer, podman, etc.) with full access to all files via bind mounts. + +SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +SPDX-License-Identifier: LGPL-3.0-only + +-=-=-=- this is a zip file -=-=-=- what follows is binary -=-=-=- +''' diff --git a/src/builder/app/hooks_app.py b/src/builder/app/hooks_app.py new file mode 100644 index 0000000..beb4253 --- /dev/null +++ b/src/builder/app/hooks_app.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +from hatchling.plugin import hookimpl + +from build_app import ZipAppBuilder + +@hookimpl +def hatch_register_builder(): + return ZipAppBuilder diff --git a/src/builder/app/pyproject.toml b/src/builder/app/pyproject.toml new file mode 100644 index 0000000..8af91bc --- /dev/null +++ b/src/builder/app/pyproject.toml @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[project] +name = "hatch-zipapp" +version = "0.0.dev0" +classifiers = [ + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Private :: Do Not Upload", +] +dependencies = ["hatchling"] + +[project.entry-points.hatch] +zipapp = "hooks_app" + +[tool.hatch.build.targets.wheel] +packages = ["."] diff --git a/src/builder/zip/__init__.py b/src/builder/zip/__init__.py new file mode 100644 index 0000000..4662c4f --- /dev/null +++ b/src/builder/zip/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only diff --git a/src/builder/zip/build_zip.py b/src/builder/zip/build_zip.py new file mode 100644 index 0000000..6c982a5 --- /dev/null +++ b/src/builder/zip/build_zip.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +# hat tip: + +import os +from hatchling.builders.config import BuilderConfig +from hatchling.builders.plugin.interface import BuilderInterface +from hatchling.builders.plugin.interface import IncludedFile +from hatchling.builders.utils import normalize_relative_path +from pathlib import Path +from typing import Any, Callable, Iterable +from zipfile import ZipFile, ZIP_DEFLATED + + +class AppZipBuilderConfig(BuilderConfig): pass + +class AppZipBuilder(BuilderInterface): + PLUGIN_NAME = "appzip" + + @classmethod + def get_config_class(cls): + return AppZipBuilderConfig + + def get_version_api(self) -> dict[str, Callable[..., str]]: + return {'standard': self.build_standard} + + def clean(self, directory: str, versions: Iterable[str]) -> None: + for filename in os.listdir(directory): + if filename.startswith('jlmkr-') and filename.endswith('.zip'): + os.remove(Path(directory, filename)) + + def build_standard(self, directory: str, **build_data: Any) -> str: + outpath = Path(directory, f'jlmkr-{self.metadata.version}.zip') + with ZipFile(outpath, 'w') as zip: + zip.write(Path(directory, 'jlmkr'), 'jlmkr') + force_map = build_data['force_include'] + for included_file in self.recurse_forced_files(force_map): + zip.write( + included_file.relative_path, + included_file.distribution_path, + ZIP_DEFLATED) + return os.fspath(outpath) + + def get_default_build_data(self) -> dict[str, Any]: + build_data: dict[str, Any] = super().get_default_build_data() + + extra_files = [] + if self.metadata.core.readme_path: + extra_files.append(self.metadata.core.readme_path) + if self.metadata.core.license_files: + extra_files.extend(self.metadata.core.license_files) + + force_include = build_data.setdefault("force_include", {}) + for fn in map(normalize_relative_path, extra_files): + force_include[os.path.join(self.root, fn)] = Path(fn).name + build_data['force_include'] = force_include + + return build_data diff --git a/src/builder/zip/hooks_zip.py b/src/builder/zip/hooks_zip.py new file mode 100644 index 0000000..5bd8a60 --- /dev/null +++ b/src/builder/zip/hooks_zip.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +from hatchling.plugin import hookimpl + +from build_zip import AppZipBuilder + +@hookimpl +def hatch_register_builder(): + return AppZipBuilder diff --git a/src/builder/zip/pyproject.toml b/src/builder/zip/pyproject.toml new file mode 100644 index 0000000..1aa6674 --- /dev/null +++ b/src/builder/zip/pyproject.toml @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[project] +name = "hatch-appzip" +version = "0.0.dev0" +classifiers = [ + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Private :: Do Not Upload", +] +dependencies = ["hatchling"] + +[project.entry-points.hatch] +appzip = "hooks_zip" + +[tool.hatch.build.targets.wheel] +packages = ["."] diff --git a/src/jlmkr/__about__.py b/src/jlmkr/__about__.py new file mode 100644 index 0000000..8ac8fa5 --- /dev/null +++ b/src/jlmkr/__about__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +__version__ = "3.0.0.dev1" + +__author__ = "Jip-Hop" +__copyright__ = "Copyright © 2023, Jip-Hop and the Jailmakers" +__license__ = "LGPL-3.0-only" + +__disclaimer__ = """USE THIS SCRIPT AT YOUR OWN RISK! +IT COMES WITHOUT WARRANTY AND IS NOT SUPPORTED BY IXSYSTEMS.""" diff --git a/src/jlmkr/__main__.py b/src/jlmkr/__main__.py new file mode 100644 index 0000000..0c3eb92 --- /dev/null +++ b/src/jlmkr/__main__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +import donor +import sys + +if __name__ == "__main__": + try: + sys.exit(donor.main()) + except KeyboardInterrupt: + sys.exit(130) diff --git a/src/jlmkr/donor/__init__.py b/src/jlmkr/donor/__init__.py new file mode 100644 index 0000000..de56c40 --- /dev/null +++ b/src/jlmkr/donor/__init__.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: © 2024 Jip-Hop and the Jailmakers +# +# SPDX-License-Identifier: LGPL-3.0-only + +from .jlmkr import main + +#### +# +# Transitional, as we split up jlmkr.py into smaller components +# +# +# +#### diff --git a/jlmkr.py b/src/jlmkr/donor/jlmkr.py similarity index 100% rename from jlmkr.py rename to src/jlmkr/donor/jlmkr.py