feat(builder): support project-local distribution scripts
This commit is contained in:
@@ -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.",
|
||||
]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user