This commit is contained in:
Zanie 2024-01-12 16:45:39 -06:00
parent cf700dfe2c
commit 4ca8775372
3 changed files with 136 additions and 25 deletions

105
scripts/hookd/README.md Normal file
View File

@ -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 <name>
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 <message>
A debugging message.
STDOUT <path>
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 <path>
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 <data>
Sent when a hook completes successfully.
The return value of the hook should follow.
ERROR <kind> <message>
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 <kind> <message>
Sent when the daemon crashes due to an unhandled error.
TRACEBACK <lines>
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.

7
scripts/hookd/example.in Normal file
View File

@ -0,0 +1,7 @@
run
ok_backend
build_wheel
foo
shutdown

49
scripts/hookd/hookd.py Normal file → Executable file
View File

@ -1,7 +1,8 @@
#!/usr/bin/env python3
""" """
A daemon process for PEP 517 build hook requests. 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 from __future__ import annotations
@ -19,7 +20,7 @@ from functools import cache
from pathlib import Path from pathlib import Path
from typing import Any, Literal, Self, TextIO 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 # Arbitrary nesting is allowed, but all keys and terminal values are strings
StringDict = dict[str, "str | StringDict"] 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 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 @contextmanager
def redirect_sys_stream(name: Literal["stdout", "stderr"]): 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): def send_expect(file: TextIO, name: str):
write_safe(file, "EXPECT", name.replace("_", "-")) write_safe(file, "EXPECT", name)
def send_ready(file: TextIO): def send_ready(file: TextIO):
@ -526,11 +510,26 @@ def main():
send_fatal(stdout, exc) send_fatal(stdout, exc)
raise 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 __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() main()