Files
MoShiro/ci/stage-node-image.py
T
justme 24ba05fbb6
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was successful
perf(ci): pin staged restore mtimes so the restore layer caches, drop build intermediates from the layer [CI]
2026-10-19 14:05:22 +08:00

112 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Stage source and restore files for a Linux Agent or Probe image build."""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FIXED_MTIME = 946684800 # 2000-01-01T00:00:00Z
BACKEND = ROOT / "backend"
STATIC_MSQUIC = ROOT / "packaging/native/msquic"
IGNORED_PARTS = {".git", ".release", "artifacts", "bin", "obj", "TestResults", "tests"}
GLOBAL_BACKEND_FILES = {
"Directory.Build.props",
"Directory.Build.targets",
"Directory.Packages.props",
"global.json",
"NuGet.Config",
"NuGet.config",
}
# The container stages backend as a standalone tree. Copy the repository-wide
# SDK and package versions into it so restore/publish use the same configuration.
ROOT_BUILD_FILES = {"Directory.Packages.props", "global.json"}
CONTAINERFILES = {
"agent": ROOT / "packaging/container/Agent.NativeBuild.Containerfile",
"probe": ROOT / "packaging/container/Probe.NativeBuild.Containerfile",
}
def copy_for_restore(path: Path, relative: Path, target: Path) -> None:
"""The restore layer must not change when only the build version does: CI rewrites <Version> in the workspace
Directory.Build.props (ci/version.sh), which used to invalidate the cached dependency restore on every commit.
Restore only needs the committed props (package versions); the source copy still carries the real version."""
if path.name == "Directory.Build.props":
committed = subprocess.run(
["git", "show", f"HEAD:backend/{relative.as_posix()}"], cwd=ROOT, capture_output=True, check=False
)
if committed.returncode == 0:
target.write_bytes(committed.stdout)
return
shutil.copy2(path, target)
def stage(component: str, runtime_identifier: str, destination: Path) -> None:
if destination.exists():
shutil.rmtree(destination)
restore = destination / "restore/backend"
source = destination / "source/backend"
restore.mkdir(parents=True)
source.mkdir(parents=True)
shutil.copy2(CONTAINERFILES[component], destination / "Containerfile")
shutil.copy2(ROOT / "packaging/container/entrypoint.sh", destination / "entrypoint.sh")
for path in BACKEND.rglob("*"):
if not path.is_file() or IGNORED_PARTS.intersection(path.parts):
continue
relative = path.relative_to(BACKEND)
if (
path.suffix in {".csproj", ".props", ".targets", ".slnx"}
or path.name in GLOBAL_BACKEND_FILES
or path.name == "packages.lock.json"
):
target = restore / relative
target.parent.mkdir(parents=True, exist_ok=True)
copy_for_restore(path, relative, target)
target = source / relative
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, target)
for name in ROOT_BUILD_FILES:
for directory in (restore, source):
shutil.copy2(ROOT / name, directory / name)
static_library = STATIC_MSQUIC / runtime_identifier / "libmsquic.a"
if not static_library.is_file():
raise FileNotFoundError(f"static MsQuic archive is missing for {runtime_identifier}: {static_library}")
staged_library = destination / "source/packaging/native/msquic" / runtime_identifier / "libmsquic.a"
staged_library.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(static_library, staged_library)
config = destination / "config"
config.mkdir()
(config / ".keep").write_text("", encoding="utf-8")
# 更新清单和请求放在这里;空的命名卷第一次挂载时会带上这个目录
(config / "update").mkdir()
(config / "update" / ".keep").write_text("", encoding="utf-8")
normalize_mtimes(destination / "restore")
def normalize_mtimes(directory: Path) -> None:
"""The Buildah layer cache keys COPY layers on file metadata as well as content. Files copied from a fresh checkout carry a new
mtime every run, so the restore layer (and the ~70 s of NuGet downloads under it) never hit the cache. Pin the mtimes."""
for path in [directory, *directory.rglob("*")]:
os.utime(path, (FIXED_MTIME, FIXED_MTIME))
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("component", choices=tuple(CONTAINERFILES))
parser.add_argument("runtime_identifier", choices=("linux-musl-x64", "linux-musl-arm64"))
parser.add_argument("destination", type=Path)
args = parser.parse_args()
stage(args.component, args.runtime_identifier, args.destination)
if __name__ == "__main__":
main()