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 5b15aeb5e2
commit 3f7082d37d
5 files changed
+139 -5

No files matched your search

+13 -1
View File
@@ -3,6 +3,10 @@ name: staticlink-avalonia
on:
workflow_dispatch:
inputs:
environment_only:
description: Validate the complete toolchain container without building release libraries
type: boolean
default: false
skiasharp_version:
description: Stable SkiaSharp version from this branch major; empty uses the branch version
type: string
@@ -33,7 +37,14 @@ concurrency:
cancel-in-progress: false
jobs:
environment:
permissions:
contents: read
packages: write
uses: ./.github/workflows/zig-toolchain-image.yml
preflight:
if: ${{ inputs.environment_only != true }}
uses: ./.github/workflows/static-graphics-preflight.yml
with:
skiasharp_version: ${{ inputs.skiasharp_version }}
@@ -42,11 +53,12 @@ jobs:
permissions:
contents: read
packages: write
needs: preflight
needs: [preflight, environment]
uses: ./.github/workflows/static-graphics-zig-cross.yml
with:
skiasharp_version: ${{ needs.preflight.outputs.skiasharp_version }}
source_lock: ${{ needs.preflight.outputs.source_lock }}
toolchain_image: ${{ needs.environment.outputs.image }}
pack:
runs-on: ubuntu-24.04
+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()