Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the user to specify the base directory for all installs. #80

Merged
merged 2 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,21 @@ In this execution phase, the *unpacker* script put by PyEmpaq inside the packed

- check if the needed setup exists from a previous run; if yes, it will just run the payload project with almost no extra work; otherwise...

- create a directory in the user data dir, and expand the `.pyz` file there
- create a directory in the user data dir (or the indicated one, see below), and expand the `.pyz` file there

- create a virtualenv in that new directory, and install all the payload's project dependencies

- run the payload's project inside that virtualenv

The verification that the unpacker does to see if has a reusable setup from the past is based on the `.pyz` timestamp; if it changed (a new file was distributed), a new setup will be created and used.

PyEmpaq transparently returns the payload's exit code except when it must exit before the execution of the payload.
In that case, it uses special codes equal or above 64, see below for their meaning.
The environment variable `PYEMPAQ_UNPACK_BASE_PATH` can be used to specify in which base directory PyEmpaq will unpack the different projects. If indicated, the path must exist and be a directory.

PyEmpaq transparently returns the payload's exit code except when it must exit before the execution of the payload. In that case, it uses special codes equal or above 64, meaning:

- `64`: `unpack-restrictions` are not met on the final user's system.
- `65`: the indicated `PYEMPAQ_UNPACK_BASE_PATH` does not exist
- `66`: the indicated `PYEMPAQ_UNPACK_BASE_PATH` is not a directory


## Command line options
Expand Down
30 changes: 27 additions & 3 deletions pyempaq/unpacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class ReturnCode(enum.IntEnum):
"""Codes that the unpacker may return."""

restrictions_not_met = 64
unpack_basedir_missing = 65
unpack_basedir_notdir = 66

def __init__(self, code):
self.returncode = code
Expand Down Expand Up @@ -216,6 +218,29 @@ def build_project_install_dir(zip_path: pathlib.Path, metadata: Dict[str, str]):
return name


def get_base_dir(platformdirs: ModuleType) -> pathlib.Path:
"""Get the base directory for all PyEmpaq installs.

If it's indicated by the user, it must exist and be a directory (less error prone
this way). If it's the default, this function ensures it is created.

It needs to receive the 'platformdirs' because it's imported from the builtin
virtualenv (not generically available).
"""
custom_basedir = os.environ.get("PYEMPAQ_UNPACK_BASE_PATH")
if custom_basedir is None:
basedir = pathlib.Path(platformdirs.user_data_dir()) / 'pyempaq'
basedir.mkdir(parents=True, exist_ok=True)
else:
basedir = pathlib.Path(custom_basedir)
if not basedir.exists():
raise FatalError(FatalError.ReturnCode.unpack_basedir_missing)
if not basedir.is_dir():
raise FatalError(FatalError.ReturnCode.unpack_basedir_notdir)

return basedir


def run():
"""Run the unpacker."""
log("PyEmpaq start")
Expand All @@ -235,9 +260,8 @@ def run():
# check all restrictions are met
enforce_restrictions(version, metadata["unpack_restrictions"])

pyempaq_dir = pathlib.Path(platformdirs.user_data_dir()) / 'pyempaq'
pyempaq_dir.mkdir(parents=True, exist_ok=True)
log("Temp base dir: %r", str(pyempaq_dir))
pyempaq_dir = get_base_dir(platformdirs)
log("Base directory: %r", str(pyempaq_dir))

# create a temp dir and extract the project there
project_dir = pyempaq_dir / build_project_install_dir(pyempaq_filepath, metadata)
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ codespell
flake8
logassert
packaging
platformdirs
pydocstyle
pytest
pytest-mock
Expand Down
45 changes: 45 additions & 0 deletions tests/test_unpacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from subprocess import CompletedProcess
from unittest.mock import patch

import platformdirs
import pytest
from logassert import Exact, NOTHING
from packaging import version
Expand All @@ -25,6 +26,7 @@
build_command,
build_project_install_dir,
enforce_restrictions,
get_base_dir,
run_command,
setup_project_directory,
)
Expand Down Expand Up @@ -338,3 +340,46 @@ def test_installdirname_complete(mocker, tmp_path):
dirname = build_project_install_dir(zip_path, fake_metadata)

assert dirname == f"testproj-{content_hash[:20]}-pypy.3.18.xyz"


def test_installdirname_custombase_default(mocker, tmp_path):
"""The location of base directory is the default."""
mocker.patch.object(platformdirs, "user_data_dir", return_value=tmp_path)

dirpath = get_base_dir(platformdirs)
assert dirpath == tmp_path / "pyempaq"
assert dirpath.exists()
assert dirpath.is_dir()


def test_installdirname_custombase_set_ok(monkeypatch, tmp_path):
"""Set the location of base directory through the env var."""
custom_basedir = tmp_path / "testbase"
custom_basedir.mkdir()
monkeypatch.setenv("PYEMPAQ_UNPACK_BASE_PATH", str(custom_basedir))

dirpath = get_base_dir(platformdirs)
assert dirpath == custom_basedir
assert dirpath.exists()
assert dirpath.is_dir()


def test_installdirname_custombase_missing(monkeypatch, tmp_path):
"""The indicated base directory location does not exist."""
custom_basedir = tmp_path / "testbase"
monkeypatch.setenv("PYEMPAQ_UNPACK_BASE_PATH", str(custom_basedir))

with pytest.raises(FatalError) as cm:
get_base_dir(platformdirs)
assert cm.value.returncode is FatalError.ReturnCode.unpack_basedir_missing


def test_installdirname_custombase_not_dir(monkeypatch, tmp_path):
"""The indicated base directory location is not a directory."""
custom_basedir = tmp_path / "testbase"
custom_basedir.touch() # not a directory!!
monkeypatch.setenv("PYEMPAQ_UNPACK_BASE_PATH", str(custom_basedir))

with pytest.raises(FatalError) as cm:
get_base_dir(platformdirs)
assert cm.value.returncode is FatalError.ReturnCode.unpack_basedir_notdir