feat(builder): Android/iOS 构建与 copy_artifact 发布
新增 APK/IPA 轨道和 copy_artifact.sh(本地目录或 container:/dir)。 push 必须委托该脚本,禁止在 makefile 里内联 docker cp。
This commit is contained in:
+129
-18
@@ -32,8 +32,8 @@ ARCH_VALUES = ("amd64", "arm64")
|
||||
REQUIRED_TARGETS = ("help", "version", "clean", "build")
|
||||
UPLOAD_TOKENS = (
|
||||
"curl ", "curl\t", "scp ", "rsync ", "aptly ", "reprepro ",
|
||||
"docker push", "buildx build --push", "buildx --push", "upload_deb.sh",
|
||||
"publish_docker.sh",
|
||||
"docker push", "docker cp", "buildx build --push", "buildx --push",
|
||||
"upload_deb.sh", "publish_docker.sh", "copy_artifact.sh",
|
||||
)
|
||||
SECRET_PATTERNS = (
|
||||
re.compile(r"(TOKEN|PASSWORD|SECRET|API_KEY|PASSWD)[A-Z_]*\s*[:?]?=\s*['\"]?[^\s$({\"']+", re.IGNORECASE),
|
||||
@@ -41,7 +41,7 @@ SECRET_PATTERNS = (
|
||||
)
|
||||
FLOATING_TAGS = (":latest", ":stable")
|
||||
DEB_SHAPE = re.compile(r"^[^_\s]+_[^_\s]+_[^_\s]+\.deb$")
|
||||
VALID_SCRIPT_NAMES = ("upload_deb.sh", "publish_docker.sh")
|
||||
VALID_SCRIPT_NAMES = ("upload_deb.sh", "publish_docker.sh", "copy_artifact.sh")
|
||||
BUILDER_MAKEFILE = "makefile.builder"
|
||||
BUILDER_ENV = ".env.builder"
|
||||
|
||||
@@ -285,6 +285,45 @@ def detect_docker_project(project: Path) -> bool:
|
||||
return (project / "Dockerfile").exists() or (project / "docker-compose.yaml").exists()
|
||||
|
||||
|
||||
def detect_android_project(recipe_all: str) -> bool:
|
||||
return ".apk" in recipe_all or ".aab" in recipe_all
|
||||
|
||||
|
||||
def detect_ios_project(recipe_all: str) -> bool:
|
||||
return ".ipa" in recipe_all
|
||||
|
||||
|
||||
def detect_copy_project(recipe_all: str) -> bool:
|
||||
return "copy_artifact.sh" in recipe_all
|
||||
|
||||
|
||||
def check_mobile_recipe(
|
||||
report: Report, project: Path, *, kind: str, number: int, ext: str
|
||||
) -> None:
|
||||
target = "build"
|
||||
dry = run_make(project, "build")
|
||||
text = chr(10).join(clean_make_output(dry))
|
||||
problems = []
|
||||
if dry.returncode != 0:
|
||||
problems.append(f"`make -n {target}` failed: {dry.stderr.strip() or 'unknown error'}")
|
||||
else:
|
||||
if "dist/" not in text and "$(DIST_DIR)" not in text:
|
||||
problems.append("recipe does not reference dist/ ($(DIST_DIR)) as artifact location")
|
||||
wanted = (".apk", ".aab") if kind == "android" else (ext,)
|
||||
if not any(item in text for item in wanted):
|
||||
problems.append(f"recipe does not mention {' or '.join(wanted)}")
|
||||
hits = [token for token in UPLOAD_TOKENS if token in text]
|
||||
if hits:
|
||||
problems.append("recipe contains upload commands: " + ", ".join(hits))
|
||||
if "rm -rf /" in text or "rm -rf ~" in text:
|
||||
problems.append("recipe contains unrestricted rm -rf")
|
||||
title = f"{kind} 目标产物形状与纯构建"
|
||||
if problems:
|
||||
report.add(FAIL, number, title, "\n".join(problems))
|
||||
else:
|
||||
report.add(PASS, number, title, f"{target} writes {ext} under dist/")
|
||||
|
||||
|
||||
def check_docker_recipe(report: Report, project: Path) -> None:
|
||||
dry = run_make(project, "docker")
|
||||
text = chr(10).join(clean_make_output(dry))
|
||||
@@ -316,24 +355,42 @@ SCRIPT_RESOLVE_SNIPPETS = tuple(
|
||||
|
||||
|
||||
def check_push_delegates(
|
||||
report: Report, project: Path, dual_artifact: bool, deb_project: bool
|
||||
report: Report,
|
||||
project: Path,
|
||||
*,
|
||||
deb_project: bool,
|
||||
docker_project: bool,
|
||||
copy_project: bool,
|
||||
) -> None:
|
||||
targets = ("push-deb", "push-docker") if dual_artifact else ("push",)
|
||||
tracks = []
|
||||
if deb_project:
|
||||
tracks.append("deb")
|
||||
if docker_project:
|
||||
tracks.append("docker")
|
||||
if copy_project:
|
||||
tracks.append("copy")
|
||||
multi = len(tracks) > 1
|
||||
targets = tuple(f"push-{t}" for t in tracks) if multi else ("push",)
|
||||
missing = []
|
||||
inline = []
|
||||
thin = []
|
||||
builder_names = ("upload_deb.sh", "publish_docker.sh", "copy_artifact.sh")
|
||||
for target in targets:
|
||||
dry = run_make(project, target)
|
||||
if has_no_rule(dry):
|
||||
missing.append(target)
|
||||
continue
|
||||
text = chr(10).join(clean_make_output(dry))
|
||||
bad_tokens = [token for token in ("curl ", "scp ", "aptly ", "reprepro ") if token in text]
|
||||
bad_tokens = [
|
||||
token
|
||||
for token in ("curl ", "scp ", "aptly ", "reprepro ", "docker cp")
|
||||
if token in text
|
||||
]
|
||||
if bad_tokens:
|
||||
inline.append(f"{target}: inline upload command ({', '.join(bad_tokens)})")
|
||||
elif not any(snippet in text for snippet in SCRIPT_RESOLVE_SNIPPETS) \
|
||||
and "$(BUILDER_SCRIPT)" not in text and "upload_deb.sh" not in text \
|
||||
and "publish_docker.sh" not in text:
|
||||
and "$(BUILDER_SCRIPT)" not in text \
|
||||
and not any(name in text for name in builder_names):
|
||||
inline.append(f"{target}: does not call a builder script (expected $BUILDER_SKILL_DIR/... or ~/.pouch/... path)")
|
||||
else:
|
||||
thin.append(target)
|
||||
@@ -342,7 +399,7 @@ def check_push_delegates(
|
||||
problems.append("missing targets: " + ", ".join(missing))
|
||||
problems.extend(inline)
|
||||
if deb_project:
|
||||
push_deb_target = "push-deb" if dual_artifact else "push"
|
||||
push_deb_target = "push-deb" if multi else "push"
|
||||
if push_deb_target not in missing:
|
||||
arches = read_arches(project)
|
||||
if arches and len(arches) > 1:
|
||||
@@ -385,6 +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",)
|
||||
ENV_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
|
||||
|
||||
|
||||
@@ -416,7 +474,7 @@ def key_present(key: str, env_keys: set[str]) -> bool:
|
||||
|
||||
|
||||
def check_ready_toolchain(
|
||||
report: Report, *, deb_project: bool, docker_project: bool
|
||||
report: Report, *, deb_project: bool, docker_project: bool, copy_project: bool = False
|
||||
) -> None:
|
||||
lines = [f"make: {'found' if shutil.which('make') else 'MISSING'}"]
|
||||
problems = []
|
||||
@@ -436,6 +494,11 @@ def check_ready_toolchain(
|
||||
problems.append("install dpkg-dev (or equivalent) to build .deb packages")
|
||||
else:
|
||||
lines.append("dpkg-deb: skipped (no deb track)")
|
||||
if copy_project:
|
||||
docker_ok = shutil.which("docker") is not None
|
||||
lines.append(
|
||||
f"docker (for container COPY_DEST): {'found' if docker_ok else 'optional; needed only for container:/dir dest'}"
|
||||
)
|
||||
report.add(
|
||||
FAIL if problems else PASS,
|
||||
10,
|
||||
@@ -445,13 +508,18 @@ def check_ready_toolchain(
|
||||
|
||||
|
||||
def check_ready_env_keys(
|
||||
report: Report, project: Path, *, deb_project: bool, docker_project: bool
|
||||
report: Report,
|
||||
project: Path,
|
||||
*,
|
||||
deb_project: bool,
|
||||
docker_project: bool,
|
||||
copy_project: bool = False,
|
||||
) -> None:
|
||||
env_keys = env_file_keys(project)
|
||||
lines = []
|
||||
missing: list[str] = []
|
||||
if not deb_project and not docker_project:
|
||||
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "no deb/docker track")
|
||||
if not deb_project and not docker_project and not copy_project:
|
||||
report.add(SKIP, 11, "发布环境变量键名(不读取值)", "no deb/docker/copy track")
|
||||
return
|
||||
if deb_project:
|
||||
for key in DEB_ENV_KEYS:
|
||||
@@ -469,6 +537,14 @@ def check_ready_env_keys(
|
||||
missing.append(key)
|
||||
else:
|
||||
lines.append("DOCKER_*: skipped (no docker track)")
|
||||
if copy_project:
|
||||
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)
|
||||
else:
|
||||
lines.append("COPY_*: skipped (no copy track)")
|
||||
if missing:
|
||||
lines.extend(
|
||||
[
|
||||
@@ -585,7 +661,17 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
# Gather every recipe once via dry-running all known targets (best effort).
|
||||
recipe_all_parts = []
|
||||
for target in (*REQUIRED_TARGETS, "deb", "docker", "push", "push-deb", "push-docker"):
|
||||
for target in (
|
||||
*REQUIRED_TARGETS,
|
||||
"deb",
|
||||
"docker",
|
||||
"android",
|
||||
"ios",
|
||||
"push",
|
||||
"push-deb",
|
||||
"push-docker",
|
||||
"push-copy",
|
||||
):
|
||||
result = run_make(project, target)
|
||||
if result.returncode == 0:
|
||||
recipe_all_parts.append(result.stdout)
|
||||
@@ -596,6 +682,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
built_deb: Path | None = None
|
||||
deb_project = detect_deb_project(recipe_all, project)
|
||||
docker_project = detect_docker_project(project)
|
||||
android_project = detect_android_project(recipe_all)
|
||||
ios_project = detect_ios_project(recipe_all)
|
||||
copy_project = detect_copy_project(recipe_all)
|
||||
|
||||
if present["build"]:
|
||||
check_arch_guard(report, project)
|
||||
@@ -621,16 +710,38 @@ def main(argv: list[str] | None = None) -> int:
|
||||
else:
|
||||
report.add(SKIP, 6, "docker 目标为本地单平台构建", "(no Dockerfile)")
|
||||
|
||||
dual = deb_project and docker_project
|
||||
check_push_delegates(report, project, dual, deb_project)
|
||||
if android_project:
|
||||
check_mobile_recipe(report, project, kind="android", number=12, ext=".apk")
|
||||
else:
|
||||
report.add(SKIP, 12, "android 目标产物形状与纯构建", "(not an Android project)")
|
||||
|
||||
if ios_project:
|
||||
check_mobile_recipe(report, project, kind="ios", number=13, ext=".ipa")
|
||||
else:
|
||||
report.add(SKIP, 13, "ios 目标产物形状与纯构建", "(not an iOS project)")
|
||||
|
||||
check_push_delegates(
|
||||
report,
|
||||
project,
|
||||
deb_project=deb_project,
|
||||
docker_project=docker_project,
|
||||
copy_project=copy_project,
|
||||
)
|
||||
check_secrets_and_tags(report, project)
|
||||
check_script_paths(report)
|
||||
if args.ready:
|
||||
check_ready_toolchain(
|
||||
report, deb_project=deb_project, docker_project=docker_project
|
||||
report,
|
||||
deb_project=deb_project,
|
||||
docker_project=docker_project,
|
||||
copy_project=copy_project,
|
||||
)
|
||||
check_ready_env_keys(
|
||||
report, project, deb_project=deb_project, docker_project=docker_project
|
||||
report,
|
||||
project,
|
||||
deb_project=deb_project,
|
||||
docker_project=docker_project,
|
||||
copy_project=copy_project,
|
||||
)
|
||||
|
||||
total_fail = report.failures
|
||||
|
||||
Reference in New Issue
Block a user