diff --git a/.woodpecker.yml b/.woodpecker.yml index 04f305b..9e33e6d 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -16,7 +16,7 @@ steps: - git -C source remote add origin https://git.oeo.one/greepar/StaticLink.Avalonia.git - git -C source fetch --depth=1 origin "$CI_COMMIT_SHA" - git -C source checkout --detach FETCH_HEAD - - name: environment-and-linux-x64 + - name: nine-platform-build-link-and-pack image: bash commands: - bash source/scripts/woodpecker-cross-test.sh diff --git a/build/linux-toolchain/zig-cross.py b/build/linux-toolchain/zig-cross.py index a7ad23a..5219828 100644 --- a/build/linux-toolchain/zig-cross.py +++ b/build/linux-toolchain/zig-cross.py @@ -173,7 +173,10 @@ def execute(config, tool, incoming): inputs.append(flag) if not output: raise SystemExit("Darwin libtool adapter needs -o") - command = [config["zig"], "ar", "--format=darwin", "rcs", output, *inputs] + libtool = shutil.which('llvm-libtool-darwin') or shutil.which('llvm-libtool-darwin-16') + if not libtool: + raise SystemExit('The pinned image must provide llvm-libtool-darwin to flatten archive dependencies') + command = [libtool, '-static', '-o', output, *inputs] if tool in ('ar', 'libtool'): # Never leave a truncated archive behind when disk/network/job failures # interrupt archiving. GN response files contain a complete member list. diff --git a/scripts/woodpecker-cross-test.sh b/scripts/woodpecker-cross-test.sh index ea8013b..bb1b447 100644 --- a/scripts/woodpecker-cross-test.sh +++ b/scripts/woodpecker-cross-test.sh @@ -19,7 +19,7 @@ if [ ! -d "$CACHE_ROOT/source/.git" ]; then git clone --no-hardlinks "$SCRIPT_ROOT" "$CACHE_ROOT/source" fi git -C "$CACHE_ROOT/source" fetch --depth=1 "$SCRIPT_ROOT" HEAD -git -C "$CACHE_ROOT/source" checkout --detach FETCH_HEAD +git -C "$CACHE_ROOT/source" checkout --force --detach FETCH_HEAD cd "$CACHE_ROOT/source" cp build/linux-toolchain/sources.lock.json build/linux-toolchain/release.lock.json mkdir -p "$RESULTS/environment" @@ -38,7 +38,7 @@ if [ "$AVAILABLE_KB" -lt 41943040 ]; then fi echo "Persistent test results: $RESULTS" exec > >(tee "$RESULTS/build.log") 2>&1 -trap 'result=$?; trap - EXIT; if [ "$result" -eq 0 ] && [ -d External/NativeStatic/linux-x64/native ]; then mkdir -p "$RESULTS/native"; cp -a External/NativeStatic/linux-x64/native/. "$RESULTS/native/"; fi; printf "%s\n" "$result" > "$RESULTS/exit-code"; echo "Results retained at $RESULTS"; exit "$result"' EXIT +trap 'result=$?; trap - EXIT; printf "%s\n" "$result" > "$RESULTS/exit-code"; echo "Results retained at $RESULTS"; exit "$result"' EXIT git rev-parse HEAD > "$RESULTS/commit" printf '%s\n' "$TOOLCHAIN_IMAGE" > "$RESULTS/toolchain-image" docker pull "$TOOLCHAIN_IMAGE" @@ -46,24 +46,16 @@ 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 "$RESULTS/environment:/validation" --entrypoint python3 \ "$TOOLCHAIN_IMAGE" /opt/staticlink/validate-toolchain.py \ - --output /validation --rids '["linux-x64"]' + --output /validation --rids '[]' mkdir -p External/NativeStatic/.work/home docker run --rm --user "$(id -u):$(id -g)" \ -e HOME=/src/External/NativeStatic/.work/home -e CROSS_RID \ -e CCACHE_DIR=/src/External/NativeStatic/.work/ccache -e CCACHE_MAXSIZE=1G \ -e ZIG_GLOBAL_CACHE_DIR=/src/External/NativeStatic/.work/zig-global \ - -v "$PWD:/src" -w /src --entrypoint bash "$TOOLCHAIN_IMAGE" -c ' + -v "$PWD:/src" -v "$RESULTS:/results" -w /src --entrypoint bash "$TOOLCHAIN_IMAGE" -c ' set -euo pipefail export SOURCE_LOCK=/src/build/linux-toolchain/release.lock.json source scripts/prepare-cross-sources.sh - BUILD_JOBS="$(nproc)" - echo "Full CPU parallelism: $BUILD_JOBS compilation jobs" - python3 build/linux-toolchain/build-cross.py \ - --target "$CROSS_RID" --sources-manifest "$WORK_DIR/cross-sources.json" \ - --work /src/External/NativeStatic/.work/toolchains \ - --output "/src/External/NativeStatic/zig-cross/$SKIASHARP_VERSION/$CROSS_RID" --jobs "$BUILD_JOBS" - python3 build/linux-toolchain/package-zig.py \ - --rid "$CROSS_RID" \ - --input "/src/External/NativeStatic/zig-cross/$SKIASHARP_VERSION/$CROSS_RID" \ - --output "/src/External/NativeStatic/$CROSS_RID/native" + python3 scripts/woodpecker-nine-platforms.py \ + --sources-manifest "$WORK_DIR/cross-sources.json" --results /results ' diff --git a/scripts/woodpecker-nine-platforms.py b/scripts/woodpecker-nine-platforms.py new file mode 100644 index 0000000..e913eeb --- /dev/null +++ b/scripts/woodpecker-nine-platforms.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import time +import zipfile + +RIDS = ( + 'osx-arm64', 'osx-x64', 'linux-x64', 'linux-arm64', + 'linux-musl-x64', 'linux-musl-arm64', 'win-x64', 'win-x86', 'win-arm64', +) + + +def run(command, logfile): + print('+', ' '.join(map(str, command)), flush=True) + with logfile.open('a') as output: + process = subprocess.Popen(list(map(str, command)), stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True) + for line in process.stdout: + print(line, end='', flush=True) + output.write(line) + return process.wait() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--sources-manifest', type=Path, required=True) + parser.add_argument('--results', type=Path, required=True) + args = parser.parse_args() + args.results.mkdir(parents=True, exist_ok=True) + lock = json.loads(Path('build/linux-toolchain/release.lock.json').read_text()) + jobs = subprocess.check_output(['nproc'], text=True).strip() + report = {'sources': lock, 'jobs': int(jobs), 'targets': {}, 'package': 'not-built', + 'runtime_tests': 'not performed; cross compilation and executable linking only'} + descriptor = args.results / 'nine-platform-results.json' + + def save(): + descriptor.write_text(json.dumps(report, indent=2) + '\n') + + save() + for rid in RIDS: + print(f'=== {rid}: compile and link with {jobs} jobs ===', flush=True) + started = time.monotonic() + logfile = args.results / (rid + '.log') + libraries = Path('External/NativeStatic/zig-cross') / lock['skiasharp_version'] / rid + native = Path('External/NativeStatic') / rid / 'native' + command = [sys.executable, 'build/linux-toolchain/build-cross.py', + '--target', rid, '--sources-manifest', args.sources_manifest, + '--work', 'External/NativeStatic/.work/toolchains', + '--output', libraries, '--jobs', jobs] + status = run(command, logfile) + if status == 0: + status = run([sys.executable, 'build/linux-toolchain/package-zig.py', + '--rid', rid, '--input', libraries, '--output', native], logfile) + if status == 0: + shutil.copytree(native, args.results / rid / 'native', dirs_exist_ok=True) + shutil.copytree(libraries / 'link-tests', args.results / rid / 'link-tests', + dirs_exist_ok=True) + report['targets'][rid] = {'exit_code': status, 'passed': status == 0, + 'elapsed_seconds': round(time.monotonic() - started, 1)} + save() + print(f'=== {rid}: {"PASS" if status == 0 else "FAIL"} ===', flush=True) + + failed = [rid for rid in RIDS if not report['targets'][rid]['passed']] + if failed: + print('Failed platforms: ' + ', '.join(failed), flush=True) + return 1 + os.environ['SOURCE_LOCK_JSON'] = json.dumps(lock) + os.environ['PACKAGE_VERSION'] = lock['skiasharp_version'] + '-' + lock['angle_branch'] + '.2' + logfile = args.results / 'pack.log' + status = run([sys.executable, 'scripts/apply-release-version.py'], logfile) + if status == 0: + status = run(['dotnet', 'pack', 'NuGet/StaticGraphics/StaticLink.Avalonia.csproj', + '-c', 'Release', '-o', args.results / 'nuget'], logfile) + if status == 0: + package = next((args.results / 'nuget').glob('*.nupkg')) + with zipfile.ZipFile(package) as archive: + names = set(archive.namelist()) + required = {'buildTransitive/StaticLink.Avalonia.props', + 'buildTransitive/StaticLink.Avalonia.targets'} + required.update('static/' + rid + '/native/' + + ('skia.lib' if rid.startswith('win-') else 'libskia.a') for rid in RIDS) + if not required.issubset(names) or any('\\' in name for name in names): + print('NuGet package is missing target archives or has non-portable paths', flush=True) + status = 1 + report['package'] = 'passed' if status == 0 else 'failed' + save() + print('Nine-platform build/link/package: ' + report['package'], flush=True) + return status + + +if __name__ == '__main__': + raise SystemExit(main())