Validate the complete container toolchain before release builds
This commit is contained in:
1 parent
70a76384e6
commit
c58a4f3573
6 files changed
+139
-5
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
@@ -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/
|
||||||
Binary file not shown.
@@ -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()
|
||||||
Reference in new issue
Block a user