feat(builder): support project-local distribution scripts

This commit is contained in:
2026-10-12 12:26:10 +08:00
parent a35cc916cb
commit 544cbcca32
8 changed files with 193 additions and 23 deletions
+75 -4
View File
@@ -442,7 +442,7 @@ def check_secrets_and_tags(report: Report, project: Path) -> None:
DEB_ENV_KEYS = ("DEB_SERVER_URL", "DEB_TOKEN", "DEB_REPOSITORY")
DOCKER_ENV_KEYS = ("DOCKER_REGISTRY",)
COPY_ENV_KEYS = ("COPY_DEST",)
COPY_ENV_KEYS = ("COPY_DEST", "COPY_SCRIPT")
ENV_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
@@ -469,10 +469,72 @@ def env_file_keys(project: Path) -> set[str]:
return keys
def env_file_values(project: Path) -> dict[str, str]:
"""Return nonempty `.env.builder` values for internal validation only."""
path = project / BUILDER_ENV
values: dict[str, str] = {}
if not path.is_file():
return values
try:
text = path.read_text(encoding="utf-8")
except OSError:
return values
for raw in text.splitlines():
stripped = raw.strip()
if not stripped or stripped.startswith("#"):
continue
match = ENV_KEY_LINE.match(stripped)
if not match:
continue
value = match.group(2).strip().strip("'\"")
if value:
values[match.group(1)] = value
return values
def key_present(key: str, env_keys: set[str]) -> bool:
return bool(os.environ.get(key)) or key in env_keys
def configured_value(key: str, env_values: dict[str, str]) -> str:
"""Read a configured value without ever printing it."""
return os.environ.get(key, "") or env_values.get(key, "")
def resolve_copy_script(project: Path, script: str) -> tuple[Path | None, str | None]:
"""Resolve COPY_SCRIPT without allowing arbitrary project-relative paths."""
configured = Path(script).expanduser()
if configured.is_absolute():
return configured, None
try:
resolved = (project / configured).resolve()
allowed_root = (project / ".pouch" / "builder").resolve()
resolved.relative_to(allowed_root)
except ValueError:
return None, "relative COPY_SCRIPT must be under .pouch/builder/"
return resolved, None
def check_copy_script(report: Report, project: Path, env_values: dict[str, str]) -> None:
script = configured_value("COPY_SCRIPT", env_values)
if not script:
return
path, resolve_error = resolve_copy_script(project, script)
problems = []
if resolve_error:
problems.append(resolve_error)
elif path is not None and not path.is_file():
problems.append("COPY_SCRIPT does not point to a file")
elif path is not None and not os.access(path, os.X_OK):
problems.append("COPY_SCRIPT is not executable")
report.add(
FAIL if problems else PASS,
14,
"COPY_SCRIPT 可执行且路径明确",
"\n".join(problems) if problems else "configured custom copy script is executable",
)
def check_ready_toolchain(
report: Report, *, deb_project: bool, docker_project: bool, copy_project: bool = False
) -> None:
@@ -516,6 +578,7 @@ def check_ready_env_keys(
copy_project: bool = False,
) -> None:
env_keys = env_file_keys(project)
env_values = env_file_values(project)
lines = []
missing: list[str] = []
if not deb_project and not docker_project and not copy_project:
@@ -541,16 +604,24 @@ def check_ready_env_keys(
for key in COPY_ENV_KEYS:
found = key_present(key, env_keys)
lines.append(f"{key}: {'present' if found else 'MISSING'}")
if not found:
missing.append(key)
if not any(key_present(key, env_keys) for key in COPY_ENV_KEYS):
missing.append("COPY_DEST or COPY_SCRIPT")
check_copy_script(report, project, env_values)
else:
lines.append("COPY_*: skipped (no copy track)")
if missing:
if "COPY_DEST or COPY_SCRIPT" in missing:
config_hints = [
" COPY_DEST= # local/container destination, or",
" COPY_SCRIPT=.pouch/builder/publish-apk.sh # choose one",
]
else:
config_hints = [f" {key}=" for key in missing]
lines.extend(
[
"",
f"blocks publish, not build. Put keys in the environment or {BUILDER_ENV}:",
*[f" {key}=" for key in missing],
*config_hints,
f"Do not commit {BUILDER_ENV}. Do not put these keys in `.env`. Never print values.",
]
)