diff --git a/scripts/hookd/README.md b/scripts/hookd/README.md new file mode 100644 index 000000000..c4e902be8 --- /dev/null +++ b/scripts/hookd/README.md @@ -0,0 +1,105 @@ +# hookd + +A daemon process for PEP 517 build hook requests. + + +## Example +``` +PYTHONPATH=scripts/hookd/backends ./scripts/hookd/hookd.py < scripts/hookd/example.in +``` + +## Command line + + +Build hooks should be run from the source tree of the project. + +The script can either be invoked from the project source tree, or the source +tree path can be provided as an argument e.g. `hookd.py /path/to/source`. + +## Messages + +The daemon communicates with bidirectional messages over STDIN and STDOUT. +Each message is terminated with a newline. +Newlines in values will be escaped as `\\n`. + +`````` +READY + + Signals that the daemon is ready to do work. + Indicates that the daemon has finished running a hook. + +EXPECT + + Signals the input kind that the daemon expects. + The name MUST be one of: + - action + Either "run" or "shutdown" instruction to the daemon + - build_backend + A PEP 517 import path for the build backend + - hook_name + A PEP 517 hook function name + - wheel_directory + A path + - sdist_directory + A path + - config_settings + A JSON payload + - metadata_directory + A path + +DEBUG + + A debugging message. + +STDOUT + + Sent before a hook is imported. + The path to a file the hook's stdout will be directed to. + The caller SHOULD delete this file when done with it. + +STDERR + + Sent before a hook is imported. + The path to a file the hook's stderr will be directed to. + The caller SHOULD delete this file when done with it. + +OK + + Sent when a hook completes successfully. + The return value of the hook should follow. + +ERROR + + Sent when a hook fails. + The error kind MUST be one of: + - MissingBackendModule + - MissingBackendAttribute + - MalformedBackendName + - BackendImportError + - InvalidHookName + - InvalidAction + - UnsupportedHook + - MalformedHookArgument + - HookRuntimeError + The message is a string describing the error. + +FATAL + + Sent when the daemon crashes due to an unhandled error. + +TRACEBACK + + MAY be sent after a FATAL or ERROR message. + Contains the traceback for the error. + +SHUTDOWN + + Signals that the daemon is exiting cleanly. +``` + +## Caveats + +The following caveats apply when using hookd: + +- Imports are cached for the duration of the daemon. Changes to the build backend packages will not be detected without restart. +- `BaseExceptions` raised during hook execution will be swallowed, unless from a SIGINT or SIGTERM. diff --git a/scripts/hookd/example.in b/scripts/hookd/example.in new file mode 100644 index 000000000..d6f64da5b --- /dev/null +++ b/scripts/hookd/example.in @@ -0,0 +1,7 @@ +run +ok_backend +build_wheel +foo + + +shutdown diff --git a/scripts/hookd/hookd.py b/scripts/hookd/hookd.py old mode 100644 new mode 100755 index d7bd7317a..b9fa0265c --- a/scripts/hookd/hookd.py +++ b/scripts/hookd/hookd.py @@ -1,7 +1,8 @@ +#!/usr/bin/env python3 """ A daemon process for PEP 517 build hook requests. -See https://peps.python.org/pep-0517/ +See the `README` for details. """ from __future__ import annotations @@ -19,7 +20,7 @@ from functools import cache from pathlib import Path from typing import Any, Literal, Self, TextIO -DEBUG_ON = os.getenv("DAEMON_DEBUG") is not None +SOURCE_TREE = os.getenv("HOOKD_SOURCE_TREE") # Arbitrary nesting is allowed, but all keys and terminal values are strings StringDict = dict[str, "str | StringDict"] @@ -302,23 +303,6 @@ def parse_config_settings(buffer: TextIO) -> StringDict | None: raise MalformedHookArgument(data, HookArgument.config_settings) from exc -@contextmanager -def tmpchdir(path: str | Path) -> Path: - """ - Temporarily change the working directory for this process. - - WARNING: This function is not safe to concurrent usage. - """ - path = Path(path).resolve() - cwd = os.getcwd() - - try: - os.chdir(path) - yield path - finally: - os.chdir(cwd) - - @contextmanager def redirect_sys_stream(name: Literal["stdout", "stderr"]): """ @@ -386,7 +370,7 @@ def write_safe(file: TextIO, *args: str): def send_expect(file: TextIO, name: str): - write_safe(file, "EXPECT", name.replace("_", "-")) + write_safe(file, "EXPECT", name) def send_ready(file: TextIO): @@ -526,11 +510,26 @@ def main(): send_fatal(stdout, exc) raise - # Do not run multiple iterations in debug mode - # TODO(zanieb): Probably remove this after development is stable - if DEBUG_ON: - return - if __name__ == "__main__": + if len(sys.argv) > 2: + print( + "Invalid usage. Expected one argument specifying the path to the source tree.", + file=sys.stderr, + ) + sys.exit(1) + + try: + source_tree = Path(sys.argv[1]).resolve() + os.chdir(source_tree) + send_debug(sys.stdout, "changed working directory to", source_tree) + except IndexError: + pass + except ValueError as exc: + print( + f"Invalid usage. Expected path argument but validation failed: {exc}", + file=sys.stderr, + ) + sys.exit(1) + main()