-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve postinstall script resilience
* Allow postinstall scripts to be executed with any Python interpreter (not just the deployed base runtime interpreter) * Generate layer config file as part of layers * Use a common postinstall script in all layers * Generate the deployed `sitecustomize.py` file from the layer config in the postinstall script * Add unit tests for the common postinstall script Closes #66. Implements initial steps towards #19.
- Loading branch information
Showing
10 changed files
with
496 additions
and
185 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
changelog.d/20241105_141935_ncoghlan_more_resilient_postinstall.rst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
Fixed | ||
----- | ||
|
||
- Post-installation scripts for layered environments now work | ||
correctly even when run with a Python installation other | ||
than the expected base runtime (resolved in :issue:`66`) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
Files injected into deployed environments | ||
========================================= | ||
|
||
Files in this folder are injected into the deployed | ||
environments when publishing artifacts or locally | ||
exporting environments. | ||
|
||
They are also designed to be importable so that | ||
the build process can access their functionality | ||
without need to duplicate the implementation, | ||
and to make them more amenable to unit testing. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
"""venvstacks layer post-installation script | ||
* Loads `./share/venv/metadata/venvstacks_layer.json` | ||
* Generates `pyvenv.cfg` for layered environments | ||
* Generates `sitecustomize.py` for layered environments | ||
* Precompiles all Python files in the library folder | ||
This post-installation script is automatically injected when packing environments. | ||
""" | ||
|
||
import json | ||
import os | ||
|
||
from compileall import compile_dir | ||
from os.path import abspath | ||
from pathlib import Path | ||
from typing import cast, NotRequired, Sequence, TypedDict | ||
|
||
DEPLOYED_LAYER_CONFIG = "share/venv/metadata/venvstacks_layer.json" | ||
|
||
|
||
class LayerConfig(TypedDict): | ||
"""Additional details needed to fully configure deployed environments""" | ||
|
||
# fmt: off | ||
python: str # Relative path to this layer's Python executable | ||
py_version: str # Expected X.Y.Z Python version for this environment | ||
base_python: str # Relative path from layer dir to base Python executable | ||
site_dir: str # Relative path to site-packages within this layer | ||
pylib_dirs: Sequence[str] # Relative paths to additional sys.path entries | ||
dynlib_dirs: Sequence[str] # Relative paths to additional Windows DLL directories | ||
launch_module: NotRequired[str] # Module to run with `-m` to launch the application | ||
# fmt: on | ||
|
||
# All relative paths are relative to the layer folder (and may refer to peer folders) | ||
# Base runtime layers will have "python" and "base_python" set to the same value | ||
# Application layers will have "launch_module" set | ||
|
||
|
||
class ResolvedLayerConfig(TypedDict): | ||
"""LayerConfig with relative paths resolved for a specific layer location""" | ||
|
||
# fmt: off | ||
layer_path: Path # Absolute path to layer environment | ||
python_path: Path # Absolute path to this layer's Python executable | ||
py_version: str # Expected X.Y.Z Python version for this environment | ||
base_python_path: Path # Absolute path from layer dir to base Python executable | ||
site_path: Path # Absolute path to site-packages within this layer | ||
pylib_paths: Sequence[Path] # Absolute paths to additional sys.path entries | ||
dynlib_paths: Sequence[Path] # Absolute paths to additional Windows DLL directories | ||
launch_module: str|None # Module to run with `-m` to launch the application | ||
# fmt: on | ||
|
||
|
||
def load_layer_config(layer_path: Path) -> ResolvedLayerConfig: | ||
"""Read and resolve config for the specified layer environment""" | ||
|
||
def deployed_path(relative_path: str) -> Path: | ||
"""Normalize path and make it absolute, *without* resolving symlinks""" | ||
return Path(abspath(layer_path / relative_path)) | ||
|
||
config_path = layer_path / DEPLOYED_LAYER_CONFIG | ||
config_text = config_path.read_text(encoding="utf-8") | ||
# Tolerate runtime errors for incorrectly generated config files | ||
config = cast(LayerConfig, json.loads(config_text)) | ||
return ResolvedLayerConfig( | ||
layer_path=layer_path, | ||
python_path=deployed_path(config["python"]), | ||
py_version=config["py_version"], | ||
base_python_path=deployed_path(config["base_python"]), | ||
site_path=deployed_path(config["site_dir"]), | ||
pylib_paths=[deployed_path(d) for d in config["pylib_dirs"]], | ||
dynlib_paths=[deployed_path(d) for d in config["dynlib_dirs"]], | ||
launch_module=config.get("launch_module", None), | ||
) | ||
|
||
|
||
def generate_pyvenv_cfg(base_python_path: Path, py_version: str) -> str: | ||
"""Generate `pyvenv.cfg` contents for given base Python path and version""" | ||
venv_config_lines = [ | ||
f"home = {base_python_path.parent}", | ||
"include-system-site-packages = false", | ||
f"version = {py_version}", | ||
f"executable = {base_python_path}", | ||
"", | ||
] | ||
return "\n".join(venv_config_lines) | ||
|
||
|
||
_SITE_CUSTOMIZE_HEADER = '''\ | ||
"""venvstacks layered environment site customization script | ||
* Calls `site.addsitedir` for any configured Python path entries | ||
* Calls `os.add_dll_directory` for any configured Windows dynlib paths | ||
This venv module is automatically generated by the post-installation script. | ||
""" | ||
''' | ||
|
||
|
||
def generate_sitecustomize( | ||
pylib_paths: Sequence[Path], dynlib_paths: Sequence[Path] | ||
) -> str | None: | ||
"""Generate `sitecustomize.py` contents for given linked environment directories""" | ||
sc_contents = [_SITE_CUSTOMIZE_HEADER] | ||
if pylib_paths: | ||
pylib_contents = [ | ||
"# Allow loading modules and packages from linked environments", | ||
"from site import addsitedir", | ||
] | ||
for path_entry in pylib_paths: | ||
pylib_contents.append(f"addsitedir({str(path_entry)!r})") | ||
pylib_contents.append("") | ||
sc_contents.extend(pylib_contents) | ||
if dynlib_paths and hasattr(os, "add_dll_directory"): | ||
dynlib_contents = [ | ||
"# Allow loading misplaced DLLs on Windows", | ||
"from os import add_dll_directory", | ||
] | ||
for dynlib_path in dynlib_paths: | ||
if not dynlib_path.exists(): | ||
# Nothing added DLLs to this folder at build time, so skip it | ||
continue | ||
dynlib_contents.append(f"add_dll_directory({str(dynlib_path)!r})") | ||
dynlib_contents.append("") | ||
sc_contents.extend(dynlib_contents) | ||
if len(sc_contents) == 1: | ||
# Environment layer doesn't actually need customizing | ||
return None | ||
return "\n".join(sc_contents) | ||
|
||
|
||
def _run_postinstall(layer_path: Path) -> None: | ||
"""Run the required post-installation steps in a deployed environment""" | ||
|
||
# Read the layer config file | ||
config = load_layer_config(layer_path) | ||
|
||
base_python_path = config["base_python_path"] | ||
if base_python_path != config["python_path"]: | ||
# Generate `pyvenv.cfg` for layered environments | ||
venv_config = generate_pyvenv_cfg(base_python_path, config["py_version"]) | ||
venv_config_path = layer_path / "pyvenv.cfg" | ||
venv_config_path.write_text(venv_config, encoding="utf-8") | ||
|
||
# Generate `sitecustomize.py` for layered environments | ||
sc_contents = generate_sitecustomize( | ||
config["pylib_paths"], config["dynlib_paths"] | ||
) | ||
if sc_contents is not None: | ||
sc_path = config["site_path"] / "sitecustomize.py" | ||
sc_path.write_text(sc_contents, encoding="utf-8") | ||
|
||
# Precompile Python library modules | ||
pylib_path = ( | ||
layer_path / "lib" | ||
) # "Lib" on Windows, but Windows is not case sensitive | ||
compile_dir(pylib_path, optimize=0, quiet=True) | ||
|
||
|
||
if __name__ == "__main__": | ||
# Actually executing the post-installation step in a deployed environment | ||
_run_postinstall(Path(__file__).parent) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.