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 70a76384e6
commit c58a4f3573
6 files changed
+139 -5

No files matched your search

+13 -1
View File
@@ -3,6 +3,10 @@ name: staticlink-avalonia
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
environment_only:
description: Validate the complete toolchain container without building release libraries
type: boolean
default: false
skiasharp_version: skiasharp_version:
description: Stable SkiaSharp version from this branch major; empty uses the branch version description: Stable SkiaSharp version from this branch major; empty uses the branch version
type: string type: string
@@ -33,7 +37,14 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
environment:
permissions:
contents: read
packages: write
uses: ./.github/workflows/zig-toolchain-image.yml
preflight: preflight:
if: ${{ inputs.environment_only != true }}
uses: ./.github/workflows/static-graphics-preflight.yml uses: ./.github/workflows/static-graphics-preflight.yml
with: with:
skiasharp_version: ${{ inputs.skiasharp_version }} skiasharp_version: ${{ inputs.skiasharp_version }}
@@ -42,11 +53,12 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
needs: preflight needs: [preflight, environment]
uses: ./.github/workflows/static-graphics-zig-cross.yml uses: ./.github/workflows/static-graphics-zig-cross.yml
with: with:
skiasharp_version: ${{ needs.preflight.outputs.skiasharp_version }} skiasharp_version: ${{ needs.preflight.outputs.skiasharp_version }}
source_lock: ${{ needs.preflight.outputs.source_lock }} source_lock: ${{ needs.preflight.outputs.source_lock }}
toolchain_image: ${{ needs.environment.outputs.image }}
pack: pack:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
+16
View File
@@ -34,6 +34,7 @@ jobs:
repository="ghcr.io/${REPOSITORY,,}-zig" repository="ghcr.io/${REPOSITORY,,}-zig"
image="$repository:recipe-$RECIPE_HASH" image="$repository:recipe-$RECIPE_HASH"
base="$repository:debian12-$BASE_HASH" base="$repository:debian12-$BASE_HASH"
built_image=false
if ! docker pull "$image"; then if ! docker pull "$image"; then
if ! docker pull "$base"; then if ! docker pull "$base"; then
docker build --platform linux/amd64 \ docker build --platform linux/amd64 \
@@ -48,6 +49,14 @@ jobs:
--label "org.opencontainers.image.source=https://github.com/$REPOSITORY" \ --label "org.opencontainers.image.source=https://github.com/$REPOSITORY" \
-f build/linux-toolchain/Dockerfile.cross-probe \ -f build/linux-toolchain/Dockerfile.cross-probe \
-t "$image" build/linux-toolchain -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 push "$image"
docker pull "$image" docker pull "$image"
fi fi
@@ -55,3 +64,10 @@ jobs:
case "$digest" in "$repository"@sha256:*) ;; *) echo 'No immutable image digest' >&2; exit 1 ;; esac case "$digest" in "$repository"@sha256:*) ;; *) echo 'No immutable image digest' >&2; exit 1 ;; esac
echo "image=$digest" >> "$GITHUB_OUTPUT" echo "image=$digest" >> "$GITHUB_OUTPUT"
echo "Toolchain: $digest" >> "$GITHUB_STEP_SUMMARY" 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 Daily schedules are disabled during validation. No NuGet.org publication or
GitHub Release is requested by the manual dispatcher on main. 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. - skia3: nuget-static-graphics.yml; latest stable SkiaSharp 3.
- skia4: nuget-static-graphics.yml; latest stable SkiaSharp 4. - skia4: nuget-static-graphics.yml; latest stable SkiaSharp 4.
- avalonia-native: nuget-avalonia-native.yml; latest stable Avalonia 11 and 12. - 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 While publication is disabled, an unpublished package may be rebuilt on every
check; add successful-run tracking before enabling artifact-only daily builds check; add successful-run tracking before enabling artifact-only daily builds
if avoiding repeated builds is required. if avoiding repeated builds is required.
+7 -3
View File
@@ -2,7 +2,11 @@
ARG TOOLCHAIN_IMAGE=staticlink-debian12:probe-base 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:10.0.302@sha256:72dd743782f2ae7e5476fd64f6a460045e3998dc862218b80e6944cba79a01b0 AS dotnet10
FROM mcr.microsoft.com/dotnet/sdk:8.0.416@sha256:8b5eece40d8a8050c09332eab1e65563fd4c928c5881e9e72bca7a4b0339f881 AS dotnet8 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} 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 ARG TARGETARCH
RUN case "$TARGETARCH" in \ RUN case "$TARGETARCH" in \
@@ -44,9 +48,6 @@ RUN mkdir -p /opt/staticlink \
&& printf '%s /opt/staticlink/os_version_check.c\n' \ && printf '%s /opt/staticlink/os_version_check.c\n' \
bc0b9f10e8d1a1ac5905d710f1c9fead23878e31233748d580393edc4e3bfc18 | sha256sum --check 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"] ENTRYPOINT ["python3", "/opt/staticlink/probe-cross.py"]
COPY --from=dotnet8 /usr/share/dotnet /opt/dotnet 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/ COPY fetch-linux-sysroots.py linux-sysroots*.lock.json /opt/staticlink/
RUN python3 /opt/staticlink/fetch-linux-sysroots.py --output /opt/sysroots \ 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 --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()