"""Validate and start the receipted native helpers of the `source-tools` build tool. A `source-tools` command is a subcommand name (`actionlint`, `shellcheck-orb`, `make-bats`, `kustomize-render`, `blueprint-lint`, `policy`) and, for the two that read directories, validated relative paths. The plan carries exactly that list. At run time this module requires the vendored helpers in the platform checkout to match `source_tools/receipt.json`, then starts the helper unchanged with `bash`. The only inputs the helpers read are variables derived here from validated data: the path list, the fixed check name and `INSTALL_TOOL`, which points at `require-installed-tool.sh`, so no pinned tool is ever downloaded: a missing tool fails the run. """ import re from collections.abc import Sequence from pathlib import Path, PurePosixPath from typing import Final from platform_ci.build_tools import BUILD_TOOLS, HelperSubcommand from platform_ci.source_helpers import SourceHelperError, verify_source_helpers SOURCE_TOOLS_TOOL: Final = "source-tools" RECEIPT_PATH: Final = Path(__file__).resolve().parent.parent / "source_tools" / "receipt.json" HELPER_DIRECTORY: Final = ".platform-ci/source_tools" """Where the platform checkout's `source_tools/` sits relative to the repository root, which is the component directory.""" RESOLVER_SCRIPT: Final = "require-installed-tool.sh" # The helpers' own rule (manifest-verify.sh, blueprint-lint.sh): only these characters, relative, inside the workspace. _PATH_PATTERN: Final = re.compile(r"[A-Za-z0-9._/-]+") def subcommand_named(name: str) -> HelperSubcommand | None: """Return the `source-tools` subcommand with this name, or None when it is not in the closed vocabulary.""" return next((subcommand for subcommand in BUILD_TOOLS[SOURCE_TOOLS_TOOL].helper_subcommands if subcommand.name == name), None) def validate_helper_command(command: Sequence[str], location: str) -> str | None: """Return a description of what is wrong with a declared `source-tools` command, or None when it is acceptable.""" if not command: return f"{location}: expected a subcommand name" subcommand = subcommand_named(command[0]) known = ", ".join(entry.name for entry in BUILD_TOOLS[SOURCE_TOOLS_TOOL].helper_subcommands) if subcommand is None: return f"{location}: expected one subcommand of {SOURCE_TOOLS_TOOL!r} ({known}), got {command[0]!r}" paths = list(command[1:]) if not subcommand.path_variable: return f"{location}: {subcommand.name!r} takes no arguments, got {paths!r}" if paths else None if not paths: return f"{location}: {subcommand.name!r} needs at least one relative directory" for path in paths: problem = _path_problem(path) if problem: return f"{location}: {path!r} {problem}" return None def _path_problem(path: str) -> str: """Return why a path argument is unsafe, or an empty string; the same rule the helpers enforce on their own input.""" if _PATH_PATTERN.fullmatch(path) is None: return "may only use letters, digits, '.', '_', '-' and '/'" if path.startswith("/"): return "must be relative to the repository root" parts = PurePosixPath(path).parts if ".." in parts: return "must stay inside the repository" if all(part == "." for part in parts): return "must name a directory below the repository root" return "" def helper_invocation(command: Sequence[str], working_directory: Path) -> tuple[list[str], dict[str, str]]: """Verify the receipted helpers, then return the helper's argument list and the validated variables it reads. Raises `SourceHelperError` when the helpers differ from the receipt or the command is not in the closed vocabulary. """ subcommand = subcommand_named(command[0]) if command else None if subcommand is None: raise SourceHelperError(f"{list(command)!r} is not a source-tools subcommand") problem = validate_helper_command(command, "command") if problem: raise SourceHelperError(problem) root = working_directory.resolve() helpers = (root / HELPER_DIRECTORY).resolve() if not helpers.is_relative_to(root) or not helpers.is_dir(): raise SourceHelperError(f"{HELPER_DIRECTORY} is not a directory inside the repository root") verify_source_helpers(helpers, RECEIPT_PATH) scripts = helpers / "scripts" environment = {"INSTALL_TOOL": str(scripts / RESOLVER_SCRIPT), **dict(subcommand.fixed_environment)} if subcommand.path_variable: environment[subcommand.path_variable] = ",".join(command[1:]) return [BUILD_TOOLS[SOURCE_TOOLS_TOOL].executable, str(scripts / subcommand.script)], environment