Validate the complete container toolchain before release builds

This commit is contained in:
geruipu committed 2026-10-05 13:29:41 +08:00
1 parent a7bd911548
commit 03e186ba1e
6 files changed
+143 -17

No files matched your search

+2 -13
View File
@@ -140,19 +140,8 @@ jobs:
mkdir -p "$WORK_DIR" "$OUTPUT_DIR"
docker run --rm --user "$(id -u):$(id -g)" \
-e HOME=/tmp/staticlink-home -e RID -e AVALONIA_VERSION \
-v "$PWD:/src" -w /src --entrypoint bash "$TOOLCHAIN_IMAGE" -c '
set -euo pipefail
mkdir -p "$HOME"
src="/src/External/AvaloniaNative/.work/Avalonia-$AVALONIA_VERSION"
git clone --depth 1 --branch "$AVALONIA_VERSION" https://github.com/AvaloniaUI/Avalonia.git "$src"
if git -C "$src" config -f .gitmodules --get-regexp path | awk '{print $2}' | grep -qx external/Numerge; then
git -C "$src" submodule update --init --depth 1 external/Numerge
fi
python3 build/linux-toolchain/build-avalonia-native-cross.py \
--source "$src" --rid "$RID" --sdk /opt/macos-sdk/MacOSX15.5.sdk \
--work "/src/External/AvaloniaNative/.work/zig-$AVALONIA_VERSION-$RID" \
--output "/src/External/AvaloniaNative/$RID/native"
'
-v "$PWD:/src" -w /src --entrypoint bash "$TOOLCHAIN_IMAGE" \
scripts/build-avalonia-native-container.sh
- name: Upload static AvaloniaNative
uses: actions/upload-artifact@v4
+16
View File
@@ -34,6 +34,7 @@ jobs:
repository="ghcr.io/${REPOSITORY,,}-zig"
image="$repository:recipe-$RECIPE_HASH"
base="$repository:debian12-$BASE_HASH"
built_image=false
if ! docker pull "$image"; then
if ! docker pull "$base"; then
docker build --platform linux/amd64 \
@@ -48,6 +49,14 @@ jobs:
--label "org.opencontainers.image.source=https://github.com/$REPOSITORY" \
-f build/linux-toolchain/Dockerfile.cross-probe \
-t "$image" build/linux-toolchain
built_image=true
fi
mkdir -p artifacts/toolchain-environment
docker run --rm --user "$(id -u):$(id -g)" \
-e HOME=/tmp -e DOTNET_CLI_HOME=/tmp -e ZIG_GLOBAL_CACHE_DIR=/tmp/zig-cache \
-v "$PWD/artifacts/toolchain-environment:/validation" \
--entrypoint python3 "$image" /opt/staticlink/validate-toolchain.py --output /validation
if [ "$built_image" = true ]; then
docker push "$image"
docker pull "$image"
fi
@@ -55,3 +64,10 @@ jobs:
case "$digest" in "$repository"@sha256:*) ;; *) echo 'No immutable image digest' >&2; exit 1 ;; esac
echo "image=$digest" >> "$GITHUB_OUTPUT"
echo "Toolchain: $digest" >> "$GITHUB_STEP_SUMMARY"
- name: Upload environment validation report
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: toolchain-environment-validation
path: artifacts/toolchain-environment/
if-no-files-found: warn
+7 -1
View File
@@ -3,6 +3,13 @@
Daily schedules are disabled during validation. No NuGet.org publication or
GitHub Release is requested by the manual dispatcher on main.
Before release builds, run nuget-static-graphics.yml on skia4 with
environment_only=true. This checks the actual container's Python/depot_tools,
.NET 8/10, SDKs, sysroots, and C/C++ compilation plus C executable linking for
all nine RIDs. Download toolchain-environment-validation to inspect the report.
The same environment gate runs before every graphics and native build and
must pass before the toolchain image is published for release compilation.
- skia3: nuget-static-graphics.yml; latest stable SkiaSharp 3.
- skia4: nuget-static-graphics.yml; latest stable SkiaSharp 4.
- avalonia-native: nuget-avalonia-native.yml; latest stable Avalonia 11 and 12.
@@ -29,4 +36,3 @@ run their own GitHub cron schedules. Publication remains an explicit opt-in.
While publication is disabled, an unpublished package may be rebuilt on every
check; add successful-run tracking before enabling artifact-only daily builds
if avoiding repeated builds is required.
+7 -3
View File
@@ -2,7 +2,11 @@
ARG TOOLCHAIN_IMAGE=staticlink-debian12:probe-base
FROM mcr.microsoft.com/dotnet/sdk:10.0.302@sha256:72dd743782f2ae7e5476fd64f6a460045e3998dc862218b80e6944cba79a01b0 AS dotnet10
FROM mcr.microsoft.com/dotnet/sdk:8.0.416@sha256:8b5eece40d8a8050c09332eab1e65563fd4c928c5881e9e72bca7a4b0339f881 AS dotnet8
FROM python:3.12.12-slim-bookworm@sha256:593bd06efe90efa80dc4eee3948be7c0fde4134606dd40d8dd8dbcade98e669c AS python312
FROM ${TOOLCHAIN_IMAGE}
COPY --from=python312 /usr/local/ /usr/local/
ENV PYTHONPATH=/usr/lib/python3/dist-packages
RUN ldconfig && python3 -c 'import inspect, tarfile, six, requests, httplib2; assert "filter" in inspect.signature(tarfile.TarFile.extractall).parameters'
ARG TARGETARCH
RUN case "$TARGETARCH" in \
@@ -44,9 +48,6 @@ RUN mkdir -p /opt/staticlink \
&& printf '%s /opt/staticlink/os_version_check.c\n' \
bc0b9f10e8d1a1ac5905d710f1c9fead23878e31233748d580393edc4e3bfc18 | sha256sum --check
COPY *.py smoke-*.c sources.lock.json cross-toolchain.lock.json *.gn.in /opt/staticlink/
COPY patches/ /opt/staticlink/patches/
COPY versions/ /opt/staticlink/versions/
ENTRYPOINT ["python3", "/opt/staticlink/probe-cross.py"]
COPY --from=dotnet8 /usr/share/dotnet /opt/dotnet
@@ -55,3 +56,6 @@ ENV DOTNET_ROOT=/opt/dotnet PATH=/opt/dotnet:${PATH}
COPY fetch-linux-sysroots.py linux-sysroots*.lock.json /opt/staticlink/
RUN python3 /opt/staticlink/fetch-linux-sysroots.py --output /opt/sysroots \
--locks /opt/staticlink/linux-sysroots.lock.json /opt/staticlink/linux-sysroots-musl.lock.json
COPY *.py smoke-*.c sources.lock.json cross-toolchain.lock.json *.gn.in /opt/staticlink/
COPY patches/ /opt/staticlink/patches/
COPY versions/ /opt/staticlink/versions/
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Validate the actual release container before compiling native libraries."""
import argparse
import inspect
import json
from pathlib import Path
import runpy
import shutil
import subprocess
import sys
import tarfile
import tempfile
def run(command):
print("+", " ".join(map(str, command)), flush=True)
return subprocess.check_output(list(map(str, command)), text=True, stderr=subprocess.STDOUT)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=True)
report = {"python": sys.version, "targets": {}, "passed": False}
try:
if "filter" not in inspect.signature(tarfile.TarFile.extractall).parameters:
raise RuntimeError("depot_tools requires tarfile extraction filters")
for module in ("six", "requests", "httplib2"):
__import__(module)
run([sys.executable, "/opt/depot_tools/gclient.py", "help"])
for tool in ("git", "ninja", "clang", "llvm-ar", "lld", "pkg-config", "cmake", "patch", "file", "dotnet", "zig"):
if not shutil.which(tool):
raise RuntimeError("Missing tool: " + tool)
report["zig"] = run(["zig", "version"]).strip()
if report["zig"] != "0.14.1":
raise RuntimeError("Unexpected Zig version")
report["dotnet_sdks"] = run(["dotnet", "--list-sdks"]).splitlines()
for major in ("8.", "10."):
if not any(version.startswith(major) for version in report["dotnet_sdks"]):
raise RuntimeError("Missing .NET SDK " + major)
adapter = Path(__file__).with_name("zig-cross.py")
targets = runpy.run_path(str(adapter))["TARGETS"]
sdk = Path("/opt/macos-sdk/MacOSX15.5.sdk")
if json.loads((sdk / "SDKSettings.json").read_text())["Version"] != "15.5":
raise RuntimeError("Unexpected macOS SDK version")
with tempfile.TemporaryDirectory(prefix="staticlink-environment-") as temporary:
work = Path(temporary)
plain = work / "main.c"
plain.write_text("int main(void) { return 0; }\n")
cpp = work / "probe.cpp"
for rid in targets:
target_work = work / rid
command = [sys.executable, adapter, "--target", rid, "--output", target_work,
"--sdk", sdk, "--mingw-headers", "/opt/mingw/usr/share/mingw-w64/include"]
source = "#include <string>\nint value() { return int(std::string(\\\"ok\\\").size()); }\n".replace('\\\"', '"')
if rid.startswith("linux"):
sysroot = Path("/opt/sysroots") / rid
if not (sysroot / "usr/include").is_dir():
raise RuntimeError("Missing sysroot: " + rid)
if not (sysroot / "usr/include/wayland-client.h").is_file():
raise RuntimeError("Missing Wayland headers: " + rid)
command += ["--linux-sysroot", sysroot, "--linux-headers", "/opt/staticlink/linux-headers"]
source += "#include <fontconfig/fontconfig.h>\n#include <ffi.h>\n#include <ft2build.h>\n#include FT_FREETYPE_H\n"
elif rid.startswith("win"):
source += "#include <windows.h>\n#include <dwrite_3.h>\n#include <XpsObjectModel.h>\n#include <windows.ui.composition.h>\n"
cpp.write_text(source)
run(command)
run([target_work / "cxx", "-std=c++17", "-c", cpp, "-o", target_work / "cpp.o"])
run([target_work / "cc", "-c", plain, "-o", target_work / "main.o"])
run([target_work / "ar", "rcs", target_work / "probe.a", target_work / "main.o"])
executable = target_work / ("probe.exe" if rid.startswith("win") else "probe")
run([target_work / "cc", plain, "-o", executable])
if rid.startswith("osx"):
objc = work / "probe.mm"
objc.write_text("#import <Cocoa/Cocoa.h>\n#import <Metal/Metal.h>\nint value() { return sizeof(CFIndex); }\n")
run([target_work / "cxx", "-fobjc-arc", "-fblocks", "-c", objc, "-o", target_work / "objc.o"])
report["targets"][rid] = {
"passed": True,
"object": run(["file", target_work / "main.o"]).strip(),
"executable": run(["file", executable]).strip(),
}
print(rid + ": C/C++ compile, archive and executable link PASS", flush=True)
report["passed"] = True
except Exception as error:
report["error"] = str(error)
if isinstance(error, subprocess.CalledProcessError):
report["error_output"] = error.output
print(error.output, flush=True)
raise
finally:
(args.output / "environment-validation.json").write_text(json.dumps(report, indent=2) + "\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
: "${RID:?RID is required}"
: "${AVALONIA_VERSION:?AVALONIA_VERSION is required}"
mkdir -p "$HOME"
source_dir="/src/External/AvaloniaNative/.work/Avalonia-$AVALONIA_VERSION"
git clone --depth 1 --branch "$AVALONIA_VERSION" https://github.com/AvaloniaUI/Avalonia.git "$source_dir"
if git -C "$source_dir" config -f .gitmodules --get-regexp path | grep -q "[[:space:]]external/Numerge$"; then
git -C "$source_dir" submodule update --init --depth 1 external/Numerge
fi
python3 build/linux-toolchain/build-avalonia-native-cross.py \
--source "$source_dir" --rid "$RID" --sdk /opt/macos-sdk/MacOSX15.5.sdk \
--work "/src/External/AvaloniaNative/.work/zig-$AVALONIA_VERSION-$RID" \
--output "/src/External/AvaloniaNative/$RID/native"