"""Run the planned native commands of one target in the ordinary sandbox. Commands are token lists started without a shell, with a minimal environment that cannot carry release or runner credentials into source-controlled build code. """ import logging import os import subprocess from collections.abc import Callable, Mapping, Sequence from pathlib import Path from platform_ci.build_tools import BUILD_TOOLS, POWERSHELL_LAUNCHER, POWERSHELL_PROGRAM_SUFFIX from platform_ci.maven_settings import install_public_central_settings from platform_ci.plan import Target from platform_ci.source_helpers import verify_source_helpers from platform_ci.source_tools import helper_invocation, subcommand_named from platform_ci.workspace import PipelineError, resolve_component_directory LOG = logging.getLogger(__name__) # Host runners (Windows, macOS) need their own system variables to start a tool at all; none of them is a credential. INHERITED_VARIABLES = ( "PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "JAVA_HOME", "MAVEN_OPTS", "MAVEN_CONFIG", "CI", "SYSTEMROOT", "USERPROFILE", "TEMP", "TMP", "PATHEXT", "COMSPEC", "APPDATA", "LOCALAPPDATA", "PROGRAMFILES", "PROGRAMDATA", "DEVELOPER_DIR", ) RUN_IDENTITY_VARIABLES = ("CI_RUN_ID", "CI_RUN_ATTEMPT") """Passed from the workflow's own context, only to a target whose environment carries the event facts; deliberately outside the target, so the build-input identity (and with it reuse and the publisher's equivalence proof) does not change with every run.""" CommandRunner = Callable[[Sequence[str], Path, Mapping[str, str]], int] class ExecutionError(PipelineError): """Raised when a planned command cannot be started at all.""" def sandbox_environment(parent_environment: Mapping[str, str]) -> dict[str, str]: """Keep only the allow-listed variables, so tokens injected by the runner never reach build code.""" return {name: value for name, value in parent_environment.items() if name in INHERITED_VARIABLES} def git_trust_environment(directory: Path) -> dict[str, str]: """Return the environment that lets subprocess Git inspect exactly one checkout, without touching any host or user Git state. The checkout is made by the runner's builtin checkout, whose owner can differ from the user the job runs as, so Git refuses it as `dubious ownership`. Command-scope configuration through `GIT_CONFIG_COUNT` is honored by Git for `safe.directory` and exists only in the environment of the one process started here: no global or system file is written, nothing is a wildcard, and only this exact resolved path is trusted (a parent, a subdirectory or a sibling is not). `safe.directory` matches by path text, so forward slashes are used on every host. """ return { "GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "safe.directory", "GIT_CONFIG_VALUE_0": directory.resolve().as_posix(), } def run_process(command: Sequence[str], working_directory: Path, environment: Mapping[str, str]) -> int: """Start one tokenized command without a shell and return its exit code.""" try: return subprocess.run(list(command), cwd=working_directory, env=dict(environment), shell=False, check=False).returncode # noqa: S603 except OSError as error: raise ExecutionError(f"cannot start {command[0]!r}: {error}") from error def launch_command(command: Sequence[str]) -> list[str]: """Return what to start for one declared program: the program itself, or a `.ps1` file behind the fixed PowerShell launcher. The launcher is `pwsh -NoProfile -NonInteractive -File ` on any platform: no shell, no execution-policy override, nothing downloaded. A runtime without `pwsh` fails as the ordinary `cannot start` error. """ if command[0].lower().endswith(POWERSHELL_PROGRAM_SUFFIX): return [*POWERSHELL_LAUNCHER, *command] return list(command) def _prepare_command(target: Target, planned_command: list[str], working_directory: Path, environment: dict[str, str]) -> tuple[list[str], dict[str, str]]: """Return the command to start and its environment: helper and program checks run first, so nothing outside the component directory is ever started.""" command, command_environment = planned_command, environment if BUILD_TOOLS[target["build_tool"]].helper_subcommands: command, helper_environment = helper_invocation(planned_command, working_directory) command_environment = environment | helper_environment subcommand = subcommand_named(planned_command[0]) if subcommand is not None and subcommand.inspects_checkout_with_git: command_environment |= git_trust_environment(working_directory) if BUILD_TOOLS[target["build_tool"]].subcommands: program = (working_directory / command[1]).resolve() if not program.is_relative_to(working_directory) or not program.is_file(): raise ExecutionError(f"{command[1]!r} is not a file inside the component directory") verify_source_helpers(program.parent) if BUILD_TOOLS[target["build_tool"]].program_in_arguments: program = (working_directory / command[0]).resolve() if not program.is_relative_to(working_directory) or not program.is_file(): raise ExecutionError(f"{command[0]!r} is not a file inside the component directory") command = launch_command(command) return command, command_environment def run_target_commands( target: Target, source_directory: Path, run_command: CommandRunner = run_process, parent_environment: Mapping[str, str] = os.environ, ) -> bool: """Run the target's commands in order, stopping at the first failure; return whether all succeeded. The environment is the allow-listed runner variables plus the target's own fixed, non-secret tool variables. A Maven target first gets the credential-free public-repository settings in its home directory. """ working_directory = resolve_component_directory(source_directory, target["directory"]) environment = sandbox_environment(parent_environment) | target["environment"] if "CI_EVENT_NAME" in target["environment"]: # a script that opted in to event facts also gets the run identity the workflow supplies, and nothing else from the parent: two numeric values for name in RUN_IDENTITY_VARIABLES: value = parent_environment.get(name, "") if not value.isascii() or not value.isdigit() or int(value) <= 0: raise ExecutionError(f"{name} must be the workflow's positive run number for a target that reads event facts") environment[name] = value if BUILD_TOOLS[target["build_tool"]].inspects_checkout_with_git: environment |= git_trust_environment(working_directory) if BUILD_TOOLS[target["build_tool"]].public_central_settings or (BUILD_TOOLS[target["build_tool"]].toolchain_choices and target["toolchain"] == "java"): home = environment.get("HOME", "") if not home: raise ExecutionError("HOME is not set, so the Maven settings location is unknown") LOG.info("verify %s: Maven settings written to %s", target["name"], install_public_central_settings(Path(home))) for planned_command in target["commands"]: command, command_environment = _prepare_command(target, planned_command, working_directory, environment) LOG.info("verify %s: running %s", target["name"], command) exit_code = run_command(command, working_directory, command_environment) if exit_code != 0: LOG.error("verify %s: %s exited with %d", target["name"], command[0], exit_code) return False return True