Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ jobs:
tools/install_llvm_toolchain.py | tools/validate_llvm_toolchain.py | \
tools/tests/test_install_llvm_toolchain.py | tools/tests/test_workflow_contracts.py | .github/workflows/measure-classic-check.yml | \
.github/workflows/publish-windows.yml | \
.github/workflows/windows-glib-qualification.yml | \
.github/workflows/validate.yml | windows/* | \
audio-toolchain.json | audio-toolchain.spdx.json | \
classic-curl-toolchain.json | tools/verify-classic-curl.sh | \
Expand Down
220 changes: 220 additions & 0 deletions .github/workflows/windows-glib-qualification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Copyright (c) 2026 Atrinik contributors
# SPDX-License-Identifier: MIT
name: Qualify Windows GLib and Cairo TLS repairs

on:
pull_request:
paths:
- 'windows/**'
- 'llvm-toolchain.json'
- 'tools/tests/test_windows_glib_workflow.py'
- '.github/workflows/windows-glib-qualification.yml'
Comment thread
zoeyrose marked this conversation as resolved.
workflow_dispatch:

permissions:
contents: read

concurrency:
group: windows-glib-qualification-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

# This scoped proof never publishes or promotes an SDK. Full image qualification
# remains owned by validate.yml and publish-windows.yml.
jobs:
cross-build:
name: Build scoped GLib and Cairo TLS qualification bundles
runs-on: ubuntu-26.04
timeout-minutes: 10
env:
SDK_IMAGE: ghcr.io/atrinik/windows-build:candidate-sha-1d82af6036b831d97cc470e2230357047e4c78f8@sha256:914acf5c3c05546ca4fb45e47362d754ec717a9062f6ebcc1b81b3ab7d0ad325
steps:
- name: Check out qualification sources
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Stage and verify immutable library source
shell: bash
run: |
set -euo pipefail
mkdir -p build/glib-qualification/{source,downloads,provenance}
git archive HEAD | tar -x -C build/glib-qualification/source
# Only this pinned archive is fetched on the host; the build worker
# has neither registry credentials nor network access.
python3 - <<'PY'
import json
from pathlib import Path
records = json.loads(Path('windows/dependencies.json').read_text())['dependencies']
inputs = {
'glib': {
'version': '2.89.3',
'url': 'https://download.gnome.org/sources/glib/2.89/glib-2.89.3.tar.xz',
'archive': 'glib-2.89.3.tar.xz',
'sha256': '09fd1e99f991067749ad66090e482ec4bd6514ad53abb4e9fbbdc2a4d2753532',
},
'cairo': {
'version': '1.18.4',
'url': 'https://cairographics.org/releases/cairo-1.18.4.tar.xz',
'archive': 'cairo-1.18.4.tar.xz',
'sha256': '445ed8208a6e4823de1226a74ca319d3600e83f6369f99b14265006599c32ccb',
},
}
for library, expected in inputs.items():
package, = [item for item in records if item['name'] == library]
if any(package.get(key) != value for key, value in expected.items()):
raise SystemExit('Library source differs from the scoped qualification input')
Path(f'build/glib-qualification/provenance/{library}-source.json').write_text(
json.dumps({'library': library, **expected}, indent=2) + '\n')
Path(f'build/glib-qualification/downloads/{library}-url.txt').write_text(expected['url'])
Path(f'build/glib-qualification/downloads/{library}.sha256').write_text(
expected['sha256'] + f' build/glib-qualification/downloads/{library}.tar.xz\n')
PY
for library in glib cairo; do
curl --fail --location --proto '=https' --tlsv1.2 \
--connect-timeout 15 --max-time 60 \
"$(cat "build/glib-qualification/downloads/${library}-url.txt")" \
-o "build/glib-qualification/downloads/${library}.tar.xz"
sha256sum --check --strict "build/glib-qualification/downloads/${library}.sha256"
done
git rev-parse HEAD > build/glib-qualification/provenance/source-commit.txt
git rev-parse 'HEAD^{tree}' > build/glib-qualification/provenance/source-tree.txt
printf '%s\n' "${SDK_IMAGE}" > build/glib-qualification/provenance/sdk-image.txt
sha256sum windows/qualify-glib-tls.py windows/run-glib-tls-qualification.ps1 \
> build/glib-qualification/provenance/qualification-scripts.sha256

- name: Cross-build in an isolated LLVM SDK worker
shell: bash
run: |
set -euo pipefail
# Anonymous public digest access only; no login or package permissions.
timeout 120s docker pull "${SDK_IMAGE}"
cpuset=$(python3 - <<'PY'
import os
cpus = sorted(os.sched_getaffinity(0))
if len(cpus) < 2:
raise SystemExit('Qualification requires two available runner CPUs')
print(','.join(map(str, cpus[:2])))
PY
)
printf 'cpuset=%s\ncpus=2\nmemory=8g\nmemory-swap=8g\npids=512\nnofile=4096\n' \
"${cpuset}" > build/glib-qualification/provenance/worker-limits.txt
worker=$(docker create --network none --user 1000:1000 \
--cpuset-cpus "${cpuset}" --cpus 2 --memory 8g --memory-swap 8g \
--pids-limit 512 --ulimit nofile=4096:4096 --cap-drop ALL \
--security-opt no-new-privileges --env HOME=/tmp/glib-home \
--workdir /tmp/qualification/source "${SDK_IMAGE}" \
sh -eu -c '
python3 windows/qualify-glib-tls.py --library glib \
--source-archive /tmp/qualification/downloads/glib.tar.xz \
--work-dir /tmp/qualification/work/glib --output /tmp/qualification/bundle/glib
python3 windows/qualify-glib-tls.py --library cairo \
--source-archive /tmp/qualification/downloads/cairo.tar.xz \
--work-dir /tmp/qualification/work/cairo --output /tmp/qualification/bundle/cairo \
--glib-bundle /tmp/qualification/bundle/glib
')
cleanup() { docker rm --force "${worker}" >/dev/null; }
trap cleanup EXIT
# Preserve the fixed worker UID without mounting the runner workspace.
tar --owner=1000 --group=1000 --transform='s,^glib-qualification,qualification,' \
-c -C build glib-qualification | docker cp --archive - "${worker}:/tmp"
status=0
timeout 300s docker start --attach "${worker}" \
> build/glib-cross-build.log 2>&1 || status=$?
cat build/glib-cross-build.log
if [[ "${status}" -eq 0 ]]; then
status=$(docker inspect --format '{{.State.ExitCode}}' "${worker}")
fi
if [[ "${status}" -ne 0 ]]; then
exit "${status}"
fi
docker cp "${worker}:/tmp/qualification/bundle" build/glib-native-bundle
for library in glib cairo; do
cp -R build/glib-qualification/provenance "build/glib-native-bundle/${library}/provenance"
cp windows/qualify-glib-tls.py windows/run-glib-tls-qualification.ps1 \
"build/glib-native-bundle/${library}/provenance/"
cp "build/glib-qualification/downloads/${library}.tar.xz" \
"build/glib-native-bundle/${library}/provenance/"
done
# Bound artifact size and reject links before uploading executable input.
python3 - <<'PY'
from pathlib import Path
root = Path('build/glib-native-bundle')
files = list(root.rglob('*'))
if any(item.is_symlink() for item in files):
raise SystemExit('Native bundle contains a link')
if sum(item.stat().st_size for item in files if item.is_file()) > 100 * 1024 * 1024:
raise SystemExit('Native bundle exceeds 100 MiB')
for library in ('glib', 'cairo'):
for lane in ('candidate', 'original', 'relinked'):
if not (root / library / lane).is_dir():
raise SystemExit(f'Missing qualification lane: {library}/{lane}')
for name in ('qualification.json', 'run-glib-tls-qualification.ps1'):
if not (root / library / name).is_file():
raise SystemExit(f'Missing qualification artifact: {library}/{name}')
PY

- name: Upload bounded native library bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-glib-cairo-tls-native-bundle
path: build/glib-native-bundle
if-no-files-found: error
retention-days: 3

- name: Retain cross-build evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-glib-cairo-tls-cross-build-evidence
path: |
build/glib-cross-build.log
build/glib-qualification/provenance
if-no-files-found: warn
retention-days: 3

native-windows:
name: Verify GLib and Cairo TLS repairs and negative controls on Windows
needs: cross-build
permissions: {}
runs-on: windows-2025
timeout-minutes: 5
steps:
- name: Download scoped native GLib and Cairo bundles
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: windows-glib-cairo-tls-native-bundle
path: build/glib-native-bundle

- name: Require native TLS lifecycle and negative controls
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
foreach ($library in @('glib', 'cairo')) {
$bundle = (Resolve-Path "build/glib-native-bundle/$library").Path
$metadata = Get-Content -Raw (Join-Path $bundle 'qualification.json') | ConvertFrom-Json
if ($metadata.library -ne $library) { throw "Bundle library mismatch: $library" }
& pwsh -NoLogo -NoProfile -NonInteractive -File (Join-Path $bundle 'run-glib-tls-qualification.ps1')
if ($LASTEXITCODE -ne 0) { throw "Native $library qualification failed: $LASTEXITCODE" }
$results = @(Get-Content -Raw (Join-Path $bundle 'native-results.json') | ConvertFrom-Json)
if ($results.Count -ne 3 -or @($results | Where-Object { $_.passed -ne $true }).Count -ne 0) {
throw "Native $library qualification did not pass all three lanes"
}
$lanes = @($results.lane | Sort-Object)
if (($lanes -join ',') -ne 'candidate,original,relinked') {
throw "Native $library qualification lane mismatch"
}
}

- name: Retain native TLS qualification evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-glib-tls-native-evidence
path: |
build/glib-native-bundle/*/qualification.json
build/glib-native-bundle/*/provenance
build/glib-native-bundle/**/*.json
build/glib-native-bundle/**/*.stdout.txt
build/glib-native-bundle/**/*.stderr.txt
if-no-files-found: error
retention-days: 3
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
into both Linux images, and records the WSLg host mounts/environment. Its
pinned Lavapipe/Xvfb packages remain the fork-safe headless path; consumers
still pin a released image digest and supply WSLg adapter selection.
- Windows GLib/Cairo qualification requires actual PE TLS callback registration and
the hash-bound native callback/destructor or mutex-initialization probes. Keep original and
relinked failing DLL controls in the focused diagnostic; it does not replace
full candidate SDK and consumer qualification.
- Keep a stable numeric runner UID when restoring a Classic ccache directory;
the mode-1777 mount root supports non-root initialization but does not make
ccache's owner-writable nested directories reusable across different UIDs.
Expand Down
46 changes: 46 additions & 0 deletions tools/run-classic-native-tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,52 @@ function Invoke-QualifiedNativeTest {
}
}

foreach ($library in @('glib', 'cairo')) {
$dllName = if ($library -eq 'glib') { 'libglib-2.0-0.dll' } else { 'libcairo-2.dll' }
$probeName = "atrinik-$library-tls-probe.exe"
# Check the same DLL whose private callback/flag RVAs are compiled into the probe.
$tlsReceipt = Get-Content -Raw (Join-Path $bundle "$library-tls.json") | ConvertFrom-Json
$dllHash = (Get-FileHash -Algorithm SHA256 (Join-Path $bundle $dllName)).Hash.ToLowerInvariant()
if ($tlsReceipt.schema_version -ne 1 -or $tlsReceipt.dll -ne $dllName -or $dllHash -ne $tlsReceipt.sha256) {
throw 'Library TLS probe does not match the staged DLL'
}
$probeHash = (Get-FileHash -Algorithm SHA256 (Join-Path $bundle $probeName)).Hash.ToLowerInvariant()
if ($probeHash -ne $tlsReceipt.probe_sha256) { throw "Library TLS probe executable differs from its producer receipt" }
$probeStart = [Diagnostics.ProcessStartInfo]::new()
$probeStart.FileName = Join-Path $bundle $probeName
$probeStart.WorkingDirectory = $bundle
$probeStart.UseShellExecute = $false
$probeStart.RedirectStandardOutput = $true
$probeStart.RedirectStandardError = $true
$probeStart.Environment['G_DEBUG'] = 'fatal-criticals'
$probeProcess = [Diagnostics.Process]::Start($probeStart)
try {
Write-Host "$library TLS qualification pid=$($probeProcess.Id) dll_sha256=$dllHash"
$stdout = $probeProcess.StandardOutput.ReadToEndAsync()
$stderr = $probeProcess.StandardError.ReadToEndAsync()
$timedOut = -not $probeProcess.WaitForExit(15000)
if ($timedOut) {
$probeProcess.Kill($true)
if (-not $probeProcess.WaitForExit(5000)) { throw 'Library TLS probe did not stop after timeout' }
}
$out = $stdout.GetAwaiter().GetResult()
$err = $stderr.GetAwaiter().GetResult()
[IO.File]::WriteAllText((Join-Path $bundle "$library-tls.stdout.log"), $out)
[IO.File]::WriteAllText((Join-Path $bundle "$library-tls.stderr.log"), $err)
Write-Host $out
if ($err) { Write-Host $err }
if ($timedOut) { throw 'Library TLS probe exceeded 15000 ms' }
if ($probeProcess.ExitCode -ne 0) { throw "Library TLS probe failed: $($probeProcess.ExitCode)" }
$expected = if ($library -eq 'glib') { 'process_attach_invoked=1 private_destructors=1 explicit_unload=1' } else { 'process_attach_initialized=2 surface_font_smoke=1 explicit_unload=1 reload=1' }
if ($out -notmatch $expected -or $err) {
throw 'Library TLS callback/destructor/unload evidence is incomplete or emitted diagnostics'
}
} finally {
$probeProcess.Dispose()
}

}

foreach ($test in $tests) {
$arguments = @($test.arguments | ForEach-Object {
if ([IO.Path]::IsPathRooted($_) -or $_ -like '../*' -or $_ -like '..\*') {
Expand Down
8 changes: 7 additions & 1 deletion tools/smoke-classic-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,13 @@ docker run --rm --user "$(id -u):$(id -g)" --network none \
--runtime-dir "${ATRINIK_WINDOWS_RUNTIME_DIR}" \
--output-dir "${stage}" \
"${native_sources[@]}" \
"${ATRINIK_WINDOWS_RUNTIME_DIR}/atrinik-sdl3-mixer-probe.exe"
"${ATRINIK_WINDOWS_RUNTIME_DIR}/atrinik-sdl3-mixer-probe.exe" \
"${ATRINIK_WINDOWS_RUNTIME_DIR}/atrinik-glib-tls-probe.exe" \
"${ATRINIK_WINDOWS_RUNTIME_DIR}/libglib-2.0-0.dll" \
"${ATRINIK_WINDOWS_RUNTIME_DIR}/atrinik-cairo-tls-probe.exe" \
"${ATRINIK_WINDOWS_RUNTIME_DIR}/libcairo-2.dll"
cmake -E copy /opt/atrinik-windows/share/atrinik/windows-dependencies/glib-tls.json "${stage}/glib-tls.json"
cmake -E copy /opt/atrinik-windows/share/atrinik/windows-dependencies/cairo-tls.json "${stage}/cairo-tls.json"
cmake -E copy_directory libatrinik/tests/fixtures "${stage}/fixtures"
cmake -E copy /opt/atrinik-windows/share/atrinik/audio/opus-probe.opus "${stage}/opus-probe.opus"
cmake -E copy /image-source/audio-toolchain.json "${stage}/audio-toolchain.json"
Expand Down
Loading
Loading