Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
298 changes: 10 additions & 288 deletions cli/python/base_setup/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,33 @@
from base_setup.github_manifest import GithubConfig
from base_setup.github_manifest import GithubManifestError
from base_setup.github_manifest import read_github_config
from base_setup.manifest_build import read_build_config
from base_setup.manifest_loader import ManifestError
from base_setup.manifest_loader import read_manifest_mapping
from base_setup.manifest_loader import yaml # pylint: disable=unused-import
from base_setup.manifest_model import ActivateConfig
from base_setup.manifest_model import ArtifactRequest
from base_setup.manifest_model import BaseManifest
from base_setup.manifest_model import BuildConfig
from base_setup.manifest_model import BuildTargetConfig
from base_setup.manifest_model import BuildConfig # pylint: disable=unused-import
from base_setup.manifest_model import BuildTargetConfig # pylint: disable=unused-import
from base_setup.manifest_model import CommandConfig
from base_setup.manifest_model import DemoConfig
from base_setup.manifest_model import HealthConfig
from base_setup.manifest_model import IdeConfig
from base_setup.manifest_model import PortHealthConfig
from base_setup.manifest_model import PythonConfig
from base_setup.manifest_model import ReleaseConfig
from base_setup.manifest_model import ReleaseGithubConfig
from base_setup.manifest_model import ReleaseHomebrewConfig
from base_setup.manifest_model import ReleaseConfig # pylint: disable=unused-import
from base_setup.manifest_model import ReleaseGithubConfig # pylint: disable=unused-import
from base_setup.manifest_model import ReleaseHomebrewConfig # pylint: disable=unused-import
from base_setup.manifest_model import TestConfig
from base_setup.manifest_reader_common import read_optional_runner as _read_optional_runner
from base_setup.manifest_release import read_release_config
from base_setup.manifest_schema import COMMAND_NAME_RE
from base_setup.manifest_schema import CURRENT_MANIFEST_SCHEMA_VERSION
from base_setup.manifest_schema import ENVIRONMENT_VARIABLE_NAME_RE
from base_setup.manifest_schema import GITHUB_REPOSITORY_RE
from base_setup.manifest_schema import HOMEBREW_PACKAGE_RE
from base_setup.manifest_schema import PORT_HEALTH_STATES
from base_setup.manifest_schema import SUPPORTED_COMMAND_RUNNERS
from base_setup.manifest_schema import SUPPORTED_PYTHON_MANAGERS
from base_setup.manifest_schema import has_control_line_break
from base_setup.release_title import release_title_template_error


def read_manifest(path: Path) -> BaseManifest:
Expand All @@ -54,8 +53,8 @@ def read_manifest(path: Path) -> BaseManifest:
python = _read_python(path, data.get("python"))
github = _read_github(path, data.get("github"))
demo = _read_demo(path, data.get("demo"))
build = _read_build(path, data.get("build"))
release = _read_release(path, data.get("release"))
build = read_build_config(path, data.get("build"))
release = read_release_config(path, data.get("release"))
artifacts = _read_artifacts(path, data.get("artifacts", []))

return BaseManifest(
Expand Down Expand Up @@ -203,271 +202,6 @@ def _read_demo(path: Path, demo_data: Any) -> DemoConfig | None:
return DemoConfig(script=script, description=description, runner=runner)


def _read_build(path: Path, build_data: Any) -> BuildConfig | None:
if build_data is None:
return None
if not isinstance(build_data, dict):
raise ManifestError(f"{path}: build must be a mapping when provided.")

allowed_keys = {"default", "targets"}
unknown_keys = sorted(set(build_data) - allowed_keys)
if unknown_keys:
raise ManifestError(f"{path}: build has unsupported keys: {', '.join(unknown_keys)}.")

targets = _read_build_targets(path, build_data.get("targets", {}))
default = _read_build_default(path, build_data.get("default", []), targets)
return BuildConfig(default=default, targets=targets)


def _read_release(path: Path, release_data: Any) -> ReleaseConfig | None:
if release_data is None:
return None
if not isinstance(release_data, dict):
raise ManifestError(f"{path}: release must be a mapping when provided.")

allowed_keys = {"version_file", "changelog", "tag_prefix", "github", "homebrew", "runner"}
unknown_keys = sorted(set(release_data) - allowed_keys)
if unknown_keys:
raise ManifestError(f"{path}: release has unsupported keys: {', '.join(unknown_keys)}.")

version_file = _read_release_relative_path(
path,
"release.version_file",
release_data.get("version_file", "VERSION"),
)
changelog = _read_release_relative_path(
path,
"release.changelog",
release_data.get("changelog", "CHANGELOG.md"),
)
tag_prefix = _read_release_string(path, "release.tag_prefix", release_data.get("tag_prefix", "v"))
github = _read_release_github(path, release_data.get("github"))
homebrew = _read_release_homebrew(path, release_data.get("homebrew"))

return ReleaseConfig(
version_file=version_file,
changelog=changelog,
tag_prefix=tag_prefix,
github=github,
homebrew=homebrew,
runner=_read_optional_runner(path, "release.runner", release_data.get("runner")),
)


def _read_release_github(path: Path, github_data: Any) -> ReleaseGithubConfig:
if not isinstance(github_data, dict):
raise ManifestError(f"{path}: release.github must be a mapping.")

allowed_keys = {"repository", "release_title"}
unknown_keys = sorted(set(github_data) - allowed_keys)
if unknown_keys:
raise ManifestError(f"{path}: release.github has unsupported keys: {', '.join(unknown_keys)}.")

repository = _read_release_repository(path, "release.github.repository", github_data.get("repository"))
release_title = _read_release_title(
path,
"release.github.release_title",
github_data.get("release_title", "{repository} v{version}"),
)
return ReleaseGithubConfig(repository=repository, release_title=release_title)


def _read_release_homebrew(path: Path, homebrew_data: Any) -> ReleaseHomebrewConfig | None:
if homebrew_data is None:
return None
if not isinstance(homebrew_data, dict):
raise ManifestError(f"{path}: release.homebrew must be a mapping when provided.")

allowed_keys = {"required", "tap_repository", "formula_path", "package"}
unknown_keys = sorted(set(homebrew_data) - allowed_keys)
if unknown_keys:
raise ManifestError(f"{path}: release.homebrew has unsupported keys: {', '.join(unknown_keys)}.")

required = homebrew_data.get("required", True)
if not isinstance(required, bool):
raise ManifestError(f"{path}: release.homebrew.required must be a boolean when provided.")

tap_repository = _read_optional_release_repository(
path,
"release.homebrew.tap_repository",
homebrew_data.get("tap_repository"),
)
formula_path = _read_optional_release_relative_path(
path,
"release.homebrew.formula_path",
homebrew_data.get("formula_path"),
)
package = _read_optional_homebrew_package(
path,
"release.homebrew.package",
homebrew_data.get("package"),
)

if required:
missing = [
field_name
for field_name, field_value in (
("tap_repository", tap_repository),
("formula_path", formula_path),
("package", package),
)
if field_value is None
]
if missing:
raise ManifestError(
f"{path}: release.homebrew required fields are missing: {', '.join(missing)}."
)

return ReleaseHomebrewConfig(
required=required,
tap_repository=tap_repository,
formula_path=formula_path,
package=package,
)


def _read_release_repository(path: Path, field_name: str, value: Any) -> str:
repository = _read_release_string(path, field_name, value)
if not GITHUB_REPOSITORY_RE.fullmatch(repository):
raise ManifestError(f"{path}: {field_name} must use owner/name format.")
return repository


def _read_optional_release_repository(path: Path, field_name: str, value: Any) -> str | None:
if value is None:
return None
return _read_release_repository(path, field_name, value)


def _read_optional_homebrew_package(path: Path, field_name: str, value: Any) -> str | None:
if value is None:
return None
package = _read_release_string(path, field_name, value)
if not HOMEBREW_PACKAGE_RE.fullmatch(package):
raise ManifestError(f"{path}: {field_name} must use owner/tap/formula format.")
return package


def _read_release_relative_path(path: Path, field_name: str, value: Any) -> str:
release_path = _read_release_string(path, field_name, value)
parsed_path = Path(release_path)
if parsed_path.is_absolute() or ".." in parsed_path.parts:
raise ManifestError(f"{path}: {field_name} must be a relative path inside the project.")
return release_path


def _read_optional_release_relative_path(path: Path, field_name: str, value: Any) -> str | None:
if value is None:
return None
return _read_release_relative_path(path, field_name, value)


def _read_release_string(path: Path, field_name: str, value: Any) -> str:
if not isinstance(value, str) or not value.strip():
raise ManifestError(f"{path}: {field_name} must be a non-empty string.")
value = value.strip()
if has_control_line_break(value):
raise ManifestError(f"{path}: {field_name} must not contain control line breaks.")
return value


def _read_release_title(path: Path, field_name: str, value: Any) -> str:
title = _read_release_string(path, field_name, value)
if error := release_title_template_error(title):
raise ManifestError(f"{path}: {field_name} {error}")
return title


def _read_build_default(
path: Path,
default_data: Any,
targets: dict[str, BuildTargetConfig],
) -> tuple[str, ...]:
if default_data is None:
return ()
if not isinstance(default_data, list):
raise ManifestError(f"{path}: build.default must be a list when provided.")

default: list[str] = []
seen: set[str] = set()
for index, target_data in enumerate(default_data, start=1):
if not isinstance(target_data, str) or not target_data.strip():
raise ManifestError(f"{path}: build.default[{index}] must be a non-empty string.")
target_name = target_data.strip()
if not COMMAND_NAME_RE.fullmatch(target_name):
raise ManifestError(f"{path}: build.default[{index}] must be a valid target name.")
if target_name in seen:
raise ManifestError(f"{path}: build.default[{index}] duplicates '{target_name}'.")
if target_name not in targets:
raise ManifestError(f"{path}: build.default[{index}] references unknown target '{target_name}'.")
seen.add(target_name)
default.append(target_name)
return tuple(default)


def _read_build_targets(path: Path, targets_data: Any) -> dict[str, BuildTargetConfig]:
if targets_data is None:
return {}
if not isinstance(targets_data, dict):
raise ManifestError(f"{path}: build.targets must be a mapping when provided.")

targets: dict[str, BuildTargetConfig] = {}
for target_name_data, target_data in targets_data.items():
if not isinstance(target_name_data, str) or not target_name_data.strip():
raise ManifestError(f"{path}: build.targets keys must be non-empty strings.")
target_name = target_name_data.strip()
if not COMMAND_NAME_RE.fullmatch(target_name):
raise ManifestError(f"{path}: build.targets.{target_name} must be a valid target name.")
if target_name in targets:
raise ManifestError(f"{path}: build.targets duplicates '{target_name}'.")
targets[target_name] = _read_build_target(path, target_name, target_data)
return targets


def _read_build_target(path: Path, target_name: str, target_data: Any) -> BuildTargetConfig:
if not isinstance(target_data, dict):
raise ManifestError(f"{path}: build.targets.{target_name} must be a mapping.")

allowed_keys = {"command", "working_dir", "description", "runner"}
unknown_keys = sorted(set(target_data) - allowed_keys)
if unknown_keys:
raise ManifestError(
f"{path}: build.targets.{target_name} has unsupported keys: {', '.join(unknown_keys)}."
)

command = target_data.get("command")
if not isinstance(command, str) or not command.strip():
raise ManifestError(f"{path}: build.targets.{target_name}.command must be a non-empty string.")
command = command.strip()
if has_control_line_break(command):
raise ManifestError(f"{path}: build.targets.{target_name}.command must not contain control line breaks.")

working_dir = target_data.get("working_dir", ".")
if not isinstance(working_dir, str) or not working_dir.strip():
raise ManifestError(f"{path}: build.targets.{target_name}.working_dir must be a non-empty string.")
working_dir = working_dir.strip()
if has_control_line_break(working_dir):
raise ManifestError(
f"{path}: build.targets.{target_name}.working_dir must not contain control line breaks."
)

description = target_data.get("description")
if description is not None:
if not isinstance(description, str) or not description.strip():
raise ManifestError(
f"{path}: build.targets.{target_name}.description must be a non-empty string when provided."
)
description = description.strip()
if has_control_line_break(description):
raise ManifestError(
f"{path}: build.targets.{target_name}.description must not contain control line breaks."
)

runner = _read_optional_runner(path, f"build.targets.{target_name}.runner", target_data.get("runner"))

return BuildTargetConfig(command=command, working_dir=working_dir, description=description, runner=runner)


def _read_health(path: Path, health_data: Any) -> HealthConfig:
if health_data is None:
return HealthConfig()
Expand Down Expand Up @@ -582,18 +316,6 @@ def _read_github(path: Path, github_data: Any) -> GithubConfig:
raise ManifestError(str(exc)) from exc


def _read_optional_runner(path: Path, field_name: str, runner_data: Any) -> str | None:
if runner_data is None:
return None
if not isinstance(runner_data, str) or not runner_data.strip():
raise ManifestError(f"{path}: {field_name} must be a non-empty string when provided.")
runner = runner_data.strip()
if runner not in SUPPORTED_COMMAND_RUNNERS:
supported = ", ".join(sorted(SUPPORTED_COMMAND_RUNNERS))
raise ManifestError(f"{path}: {field_name} must be one of: {supported}.")
return runner


def _read_activate_sources(path: Path, source_data: Any) -> tuple[str, ...]:
if source_data is None:
return ()
Expand Down
Loading
Loading