|
| 1 | +"""Emit the shell reproduction command for a single workspace script. |
| 2 | +
|
| 3 | +Given a path to a workspace script (e.g. |
| 4 | +`autogalaxy_workspace_test/scripts/imaging/visualization.py`), print the |
| 5 | +exact shell command `autobuild run_python` would have used to execute |
| 6 | +it — including all environment variables from the workspace's |
| 7 | +`config/build/env_vars.yaml` (defaults + matching per-pattern overrides). |
| 8 | +
|
| 9 | +Output format (single line): |
| 10 | +
|
| 11 | + (cd <workspace_name> && env KEY=val ... python3 <script_relpath>) |
| 12 | +
|
| 13 | +Run from the PyAutoLabs base directory. The output is portable as long |
| 14 | +as the caller `cd`s to PyAutoLabs root first. |
| 15 | +
|
| 16 | +Usage: |
| 17 | + autobuild repro_command <script_path> |
| 18 | +
|
| 19 | +Exit codes: |
| 20 | + 0 on success (command printed to stdout) |
| 21 | + 2 on usage error (script not found, workspace root not found) |
| 22 | +""" |
| 23 | + |
| 24 | +import argparse |
| 25 | +import shlex |
| 26 | +import sys |
| 27 | +from pathlib import Path |
| 28 | +from typing import Dict, Optional |
| 29 | + |
| 30 | +from env_config import _pattern_matches, load_env_config |
| 31 | + |
| 32 | + |
| 33 | +def _find_workspace_root(script: Path) -> Optional[Path]: |
| 34 | + """Walk up from `script` to find a dir containing config/build/env_vars.yaml.""" |
| 35 | + for candidate in (script.parent, *script.parents): |
| 36 | + if (candidate / "config" / "build" / "env_vars.yaml").is_file(): |
| 37 | + return candidate |
| 38 | + return None |
| 39 | + |
| 40 | + |
| 41 | +def canonical_env_for_script(file: Path, env_config: Optional[dict]) -> Dict[str, str]: |
| 42 | + """Like `env_config.build_env_for_script`, but starts from `{}` instead of |
| 43 | + `os.environ.copy()`. |
| 44 | +
|
| 45 | + The result is what autobuild *adds* to the environment, independent of the |
| 46 | + developer's local shell. This is the right form for a portable reproduction |
| 47 | + command — the chat-side reader inherits their shell's env and just gets the |
| 48 | + autobuild-specific overrides prepended. |
| 49 | + """ |
| 50 | + if env_config is None: |
| 51 | + return {} |
| 52 | + |
| 53 | + env: Dict[str, str] = {} |
| 54 | + |
| 55 | + for key, value in env_config.get("defaults", {}).items(): |
| 56 | + env[key] = str(value) |
| 57 | + |
| 58 | + file_path_no_ext = str(file.with_suffix("")) |
| 59 | + for override in env_config.get("overrides", []): |
| 60 | + pattern = override["pattern"] |
| 61 | + if _pattern_matches(file, file_path_no_ext, pattern): |
| 62 | + for var_name in override.get("unset", []): |
| 63 | + env.pop(var_name, None) |
| 64 | + for key, value in override.get("set", {}).items(): |
| 65 | + env[key] = str(value) |
| 66 | + |
| 67 | + return env |
| 68 | + |
| 69 | + |
| 70 | +def repro_command(script_path: str) -> str: |
| 71 | + """Compute the one-line shell repro command for `script_path`. |
| 72 | +
|
| 73 | + Raises FileNotFoundError if the script doesn't exist or no workspace |
| 74 | + root with env_vars.yaml is found walking up. |
| 75 | + """ |
| 76 | + script = Path(script_path).resolve() |
| 77 | + if not script.is_file(): |
| 78 | + raise FileNotFoundError(f"script not found: {script_path}") |
| 79 | + |
| 80 | + workspace_root = _find_workspace_root(script) |
| 81 | + if workspace_root is None: |
| 82 | + raise FileNotFoundError( |
| 83 | + f"no workspace root with config/build/env_vars.yaml found " |
| 84 | + f"walking up from {script_path}" |
| 85 | + ) |
| 86 | + |
| 87 | + env_config_path = workspace_root / "config" / "build" / "env_vars.yaml" |
| 88 | + env_config = load_env_config(env_config_path) |
| 89 | + env = canonical_env_for_script(script, env_config) |
| 90 | + |
| 91 | + workspace_name = workspace_root.name |
| 92 | + script_rel = script.relative_to(workspace_root) |
| 93 | + |
| 94 | + env_parts = [f"{k}={shlex.quote(v)}" for k, v in env.items()] |
| 95 | + if env_parts: |
| 96 | + env_prefix = "env " + " ".join(env_parts) + " " |
| 97 | + else: |
| 98 | + env_prefix = "" |
| 99 | + |
| 100 | + return f"(cd {shlex.quote(workspace_name)} && {env_prefix}python3 {shlex.quote(str(script_rel))})" |
| 101 | + |
| 102 | + |
| 103 | +def main(argv=None) -> int: |
| 104 | + parser = argparse.ArgumentParser( |
| 105 | + prog="autobuild repro_command", |
| 106 | + description=__doc__.strip().splitlines()[0], |
| 107 | + ) |
| 108 | + parser.add_argument( |
| 109 | + "script_path", |
| 110 | + help="Path to a workspace script (absolute or relative to cwd).", |
| 111 | + ) |
| 112 | + args = parser.parse_args(argv) |
| 113 | + |
| 114 | + try: |
| 115 | + print(repro_command(args.script_path)) |
| 116 | + except FileNotFoundError as e: |
| 117 | + print(f"autobuild repro_command: {e}", file=sys.stderr) |
| 118 | + return 2 |
| 119 | + return 0 |
| 120 | + |
| 121 | + |
| 122 | +if __name__ == "__main__": |
| 123 | + sys.exit(main()) |
0 commit comments