diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f4b67d..8cc4e46 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -131,8 +131,7 @@ jobs: # END - name: Build - run: | - python3 -m zipapp src/jlmkr -p "/usr/bin/env python3" -o jlmkr + run: python3 build.py && ln dist/jlmkr . # Run multiple commands using the runners shell - name: Run the test script diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 0000000..2da7b21 --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,46 @@ +# Developer notes + +## Building Jailmaker + +No external dependencies are needed to perform a simple build from the project directory. + + python3 build.py + +Anything beyond this is *completely optional…* + +## Development mode + +Jailmaker's user-facing design and important safety features tend to get in the way of rapid development. To run directly from the editable source code, create an external configuration file. + + mkdir -p ~/.local/share + cat <~/.local/share/jailmaker.conf + [DEFAULT] + ignore_owner = 1 + jailmaker_dir = /mnt/pool/jailmaker + EOF + +If present, this file will override traditional self-detection of the Jailmaker directory. + +## Code quality tooling + +Additional tools for testing, coverage, and code quality review are available through [Hatch][1]. Install them in a self-contained, disposable virtual environment. + + python3 -m venv --without-pip .venv + curl -OL https://bootstrap.pypa.io/pip/pip.pyz + .venv/bin/python3 pip.pyz install pip hatch + rm pip.pyz + +Activate a session inside the virtual environment. (For more information see the `venv` [tutorial][2].) + + source .venv/bin/activate + +Use `hatch` to build, test, lint, etc. + + hatch build + +## Integration testing + +See [`test/README.md`](./test/README.md). + +[1]: https://hatch.pypa.io/ +[2]: https://docs.python.org/3/tutorial/venv.html diff --git a/src/jlmkr/actions/create.py b/src/jlmkr/actions/create.py index 635bbda..fc72981 100644 --- a/src/jlmkr/actions/create.py +++ b/src/jlmkr/actions/create.py @@ -10,7 +10,7 @@ from textwrap import dedent from data import DISCLAIMER -from paths import COMMAND_NAME, JAILS_DIR_PATH, SCRIPT_DIR_PATH, SCRIPT_NAME +from paths import COMMAND_NAME, JAILS_DIR_PATH, JAILMAKER_DIR_PATH, SCRIPT_NAME from utils.chroot import Chroot from utils.config_parser import DEFAULT_CONFIG, KeyValueParser from utils.console import BOLD, NORMAL, YELLOW, eprint @@ -33,19 +33,19 @@ def create_jail(**kwargs): print(DISCLAIMER) - if os.path.basename(SCRIPT_DIR_PATH) != "jailmaker": + if os.path.basename(JAILMAKER_DIR_PATH) != "jailmaker": eprint( dedent( f""" {COMMAND_NAME} needs to create files. Currently it can not decide if it is safe to create files in: - {SCRIPT_DIR_PATH} + {JAILMAKER_DIR_PATH} Please create a dedicated dataset called "jailmaker", store {SCRIPT_NAME} there and try again.""" ) ) return 1 - if not PurePath(get_mount_point(SCRIPT_DIR_PATH)).is_relative_to("/mnt"): + if not PurePath(get_mount_point(JAILMAKER_DIR_PATH)).is_relative_to("/mnt"): print( dedent( f""" @@ -54,7 +54,7 @@ def create_jail(**kwargs): {SCRIPT_NAME} should be on a dataset mounted under /mnt (it currently is not). Storing it on the boot-pool means losing all jails when updating TrueNAS. Jails will be stored under: - {SCRIPT_DIR_PATH} + {JAILMAKER_DIR_PATH} """ ) ) @@ -118,7 +118,7 @@ def create_jail(**kwargs): try: # Create the dir or dataset where to store the jails if not os.path.exists(JAILS_DIR_PATH): - if get_zfs_dataset(SCRIPT_DIR_PATH): + if get_zfs_dataset(JAILMAKER_DIR_PATH): # Creating "jails" dataset if "jailmaker" is a ZFS Dataset create_zfs_dataset(JAILS_DIR_PATH) else: diff --git a/src/jlmkr/paths.py b/src/jlmkr/paths.py index fbe98ad..3fd938b 100755 --- a/src/jlmkr/paths.py +++ b/src/jlmkr/paths.py @@ -2,20 +2,66 @@ # # SPDX-License-Identifier: LGPL-3.0-only -import os.path +import os +import sys -# When running as a zipapp, the script file is a parent -ZIPAPP_PATH = os.path.realpath(__file__) -while not os.path.exists(ZIPAPP_PATH): - ZIPAPP_PATH = os.path.dirname(ZIPAPP_PATH) +from configparser import ConfigParser +from pathlib import Path +from utils.console import fail -SCRIPT_PATH = os.path.realpath(ZIPAPP_PATH) -SCRIPT_NAME = os.path.basename(SCRIPT_PATH) -SCRIPT_DIR_PATH = os.path.dirname(SCRIPT_PATH) -COMMAND_NAME = os.path.basename(ZIPAPP_PATH) -JAILS_DIR_PATH = os.path.join(SCRIPT_DIR_PATH, "jails") -JAIL_CONFIG_NAME = "config" -JAIL_ROOTFS_NAME = "rootfs" +def _get_selected_jailmaker_directory() -> Path: + ''' + Determine the user's affirmative choice of parent jailmaker directory + ''' + # first choice: global --dir/-D argument + #TODO + + # next: JAILMAKER_DIR environment variable + envname = 'JAILMAKER_DIR' + if envname in os.environ: + return Path(os.environ[envname]) + + # next: ~/.local/share/jailmaker.conf + secname = 'DEFAULT' + cfgname = 'jailmaker_dir' + username = '' + if os.getuid() == 0 and 'SUDO_USER' in os.environ: + username = os.environ['SUDO_USER'] + cfgpath = Path(f'~{username}/.local/share/jailmaker.conf').expanduser() + cfg = ConfigParser() + cfg.read(cfgpath) + if 'ignore_owner' in cfg[secname]: + os.environ['JLMKR_DEBUG'] = cfg[secname]['ignore_owner'] + if cfgname in cfg[secname]: + return Path(cfg[secname][cfgname]) + + # next: current directory iff it's named jailmaker + script = Path(sys.argv[0]).resolve(True) + if script.parent.name == 'jailmaker': + return script.parent + + fail("Please specify a jailmaker directory path (JAILMAKER_DIR)") + + +def get_tool_path_on_disk() -> Path: + ''' + Determine the script's location on disk + ''' + # When running as a zipapp, the script file is an ancestor + path = Path(__file__).resolve(strict=False) + while path and not path.is_file(): + path = path.parent + return path + +SCRIPT_PATH = get_tool_path_on_disk() +SCRIPT_NAME = SCRIPT_PATH.name +COMMAND_NAME = SCRIPT_NAME SHORTNAME = "jlmkr" + +JAILMAKER_DIR_PATH = _get_selected_jailmaker_directory() + +JAILS_DIR_PATH = JAILMAKER_DIR_PATH.joinpath("jails") +JAIL_CONFIG_NAME = "config" +JAIL_ROOTFS_NAME = "rootfs" diff --git a/src/jlmkr/utils/dataset.py b/src/jlmkr/utils/dataset.py index 5afee9c..8a50bef 100644 --- a/src/jlmkr/utils/dataset.py +++ b/src/jlmkr/utils/dataset.py @@ -6,13 +6,13 @@ import subprocess from pathlib import PurePath -from paths import SCRIPT_DIR_PATH +from paths import JAILMAKER_DIR_PATH from utils.console import eprint, fail def _get_relative_path_in_jailmaker_dir(absolute_path): - return PurePath(absolute_path).relative_to(SCRIPT_DIR_PATH) + return PurePath(absolute_path).relative_to(JAILMAKER_DIR_PATH) def get_zfs_dataset(path): @@ -37,7 +37,7 @@ def get_zfs_base_path(): """ Get ZFS dataset path for jailmaker directory. """ - zfs_base_path = get_zfs_dataset(SCRIPT_DIR_PATH) + zfs_base_path = get_zfs_dataset(JAILMAKER_DIR_PATH) if not zfs_base_path: fail("Failed to get dataset path for jailmaker directory.")