Files
StaticLink.Avalonia/build/linux-toolchain/zig-cross.py
T

294 lines
15 KiB
Python

#!/usr/bin/env python3
"""Pinned Zig compiler adapters for static-library cross-builds on Linux."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
import re
TARGETS = {
"linux-x64": "x86_64-linux-gnu.2.36.0", "linux-arm64": "aarch64-linux-gnu.2.36.0",
"linux-musl-x64": "x86_64-linux-musl", "linux-musl-arm64": "aarch64-linux-musl",
"win-x64": "x86_64-windows-gnu", "win-x86": "x86-windows-gnu",
"win-arm64": "aarch64-windows-gnu", "osx-x64": "x86_64-macos.10.13.0",
"osx-arm64": "aarch64-macos.11.0.0",
}
def mingw_overlay(zig_lib, output):
source = zig_lib / "libc/include/any-windows-any/dwrite_3.h"
text = source.read_text()
# Add only non-virtual overloads/default arguments. Keep IID, virtual order,
# virtual parameter types and C vtables exactly as supplied by MinGW.
for name in ("IDWritePaintReader", "IDWriteFontFace4"):
start = text.index(name + " : public")
end = text.index("#ifdef __CRT_UUID_DECL", start)
block = text[start:end]
if name == "IDWritePaintReader":
block = block.replace("UINT32 struct_size) = 0;",
"UINT32 struct_size = sizeof(DWRITE_PAINT_ELEMENT)) = 0;")
extra = """ HRESULT SetCurrentGlyph(UINT32 glyph, DWRITE_PAINT_ELEMENT *element,
D2D_RECT_F *clip, DWRITE_PAINT_ATTRIBUTES *attributes = 0) {
return SetCurrentGlyph(glyph, element, sizeof(DWRITE_PAINT_ELEMENT), clip, attributes);
}
HRESULT SetTextColor(const DWRITE_COLOR_F &color) { return SetTextColor(&color); }
"""
else:
extra = """ HRESULT GetGlyphImageFormats(UINT16 glyph, UINT32 first, UINT32 last,
DWRITE_GLYPH_IMAGE_FORMATS *formats) {
return GetGlyphImageFormats_(glyph, first, last, formats);
}
"""
index = block.rindex("};")
block = block[:index] + extra + block[index:]
text = text[:start] + block + text[end:]
output.mkdir(parents=True, exist_ok=True)
(output / "dwrite_3.h").write_text(text)
for name in ("XpsObjectModel.h", "ObjBase.h", "DispatcherQueue.h", "VersionHelpers.h", "T2EmbApi.h", "FontSub.h"):
alias = output / name
if alias.is_symlink():
alias.unlink()
alias.symlink_to(source.parent / name.lower())
return hashlib.sha256(source.read_bytes()).hexdigest()
def compiler(config, tool, incoming):
target = config["target"]
if "--version" in incoming and "linux-gnu." in target:
target = target.split("linux-gnu.")[0] + "linux-gnu"
windows = "windows" in target
args = ["-I" + config["include"]] if windows else [
"-isysroot", config["sdk"], "-idirafter", config["sdk"] + "/usr/include",
"-F", config["sdk"] + "/System/Library/Frameworks", "-Wno-elaborated-enum-base",
] if "macos" in target else []
if config.get('linux_abi_headers'):
args += ['-I' + config['linux_abi_headers']]
if config.get('linux_headers'):
args += ['-I' + config['linux_headers'], '-I' + config['linux_headers'] + '/freetype2']
if config.get('linux_sysroot') and '-c' not in incoming and '-o' in incoming:
root = Path(config['linux_sysroot'])
cpu = 'x86_64' if target.startswith('x86_64') else 'aarch64'
for path in (root / 'usr/lib' / (cpu + '-linux-gnu'), root / 'lib' / (cpu + '-linux-gnu'), root / 'usr/lib', root / 'lib'):
args += ['-L' + str(path)]
if 'linux' in target and '-c' not in incoming and '-o' in incoming:
args.append('-fuse-ld=lld')
if windows:
# Accept Windows header/Skia spellings such as __forceinline while
# retaining the GNU target ABI and leaving _MSC_VER undefined.
args.append("-fms-extensions")
if windows and config.get("mingw_headers"):
args.extend(("-idirafter", config["mingw_headers"]))
mapping = {
"/O2": "-O2", "/fp:precise": "-ffp-model=precise", "/std:c++20": "-std=c++20",
"/std:c11": "-std=c11", "/GR": "-frtti", "/GR-": "-fno-rtti", "/w": "-w",
"/Ob2": "-finline-functions", "/arch:AVX2": "-mcpu=x86_64_v3",
"/arch:AVX512": "-mcpu=x86_64_v4+evex512", "/TC": "-xc", "/TP": "-xc++",
"/Gy": "-ffunction-sections", "/Gw": "-fdata-sections",
"/Oy-": "-fno-omit-frame-pointer", "/guard:cf": "-mguard=cf",
"/W3": "-Wall",
"-march=x86-64-v3": "-mcpu=x86_64_v3", "-march=x86-64-v4": "-mcpu=x86_64_v4+evex512",
"-march=armv8-a+crc+crypto": "-mcpu=generic+crc+aes+sha2",
}
iterator = iter(incoming)
for flag in iterator:
if flag in ("-Wl,--fatal-warnings", "-Wl,--disable-new-dtags"):
continue
if flag in ("-o", "-MF", "-MT", "-MQ", "-I", "-isystem", "-isysroot", "-F", "-idirafter", "-include"):
args.extend((flag, next(iterator)))
continue
if flag in ("-target", "--target"):
next(iterator)
continue
if flag.startswith("--target="):
continue
if flag.startswith("/clang:"):
flag = flag[7:]
if flag.startswith(("-fdiagnostics-show-inlining-chain", "-fno-lifetime-dse",
"-fsanitize-ignore-for-ubsan-feature=", "-fmsc-version=")):
continue
if flag.startswith("/D"):
flag = "-D" + flag[2:]
if re.fullmatch(r"/std:(c\+\+\d+|c\d+)", flag):
flag = "-std=" + flag[5:]
if flag in ("/arch:SSE", "/arch:SSE2", "/arch:AVX"):
flag = {"/arch:SSE": "-msse", "/arch:SSE2": "-msse2", "/arch:AVX": "-mavx"}[flag]
if flag == "/W4":
args.extend(("-Wall", "-Wextra"))
continue
if flag in ("/bigobj", "/utf-8", "/Zc:inline", "/Zc:lambda", "/Zc:twoPhase", "/FS", "/MT"):
continue
if flag.startswith(("/wd", "/we")):
continue # MSVC diagnostic numbers have no GNU Clang equivalent.
if windows and flag.startswith("/") and flag not in mapping and not Path(flag).exists():
raise SystemExit("Unsupported MSVC option: " + flag)
if flag == '-stdlib=libstdc++':
flag = '-stdlib=libc++'
args.append(mapping.get(flag, flag))
if any(flag.startswith("-mavx512") for flag in args):
args.append("-mevex512")
if windows:
if "-DCRC32_SIMD_SSE42_PCLMUL" in args:
args.extend(("-msse4.2", "-mpclmul"))
if "-DADLER32_SIMD_SSSE3" in args:
args.append("-mssse3")
# Older MSVC GN rules express SSE dispatch through a macro: Clang needs
# matching CPU features on those units, never on ordinary baseline units.
if target.startswith(("x86", "i386")):
levels = {"SSE2": ["-msse2"], "SSE3": ["-msse3"], "SSSE3": ["-mssse3"],
"SSE41": ["-msse4.1"], "SSE42": ["-msse4.2", "-mcrc32"]}
for level, features in levels.items():
if "-DSK_CPU_SSE_LEVEL=SK_CPU_SSE_LEVEL_" + level in args:
args.extend(features)
return [config["zig"], "cc" if tool == "cc" else "c++", "-target", target, *args]
def execute(config, tool, incoming):
if tool in ("cc", "cxx"):
command = compiler(config, tool, incoming)
elif tool == "ar":
flags = [x.replace("rcsT", "rcs") for x in incoming if x not in ("/WX", "/ignore:4221", "-T", "--thin")]
command = [config["zig"], "ar"]
if "macos" in config["target"]:
command.append("--format=darwin")
command += flags
else:
output = None
inputs = []
iterator = iter(incoming)
for flag in iterator:
if flag in ("-static", "-no_warning_for_no_symbols"):
continue
if flag == "-o":
output = next(iterator)
elif flag == "-filelist":
inputs.extend(Path(next(iterator)).read_text().splitlines())
elif flag.startswith("@"):
inputs.extend(shlex.split(Path(flag[1:]).read_text()))
else:
inputs.append(flag)
if not output:
raise SystemExit("Darwin libtool adapter needs -o")
command = [config["zig"], "ar", "--format=darwin", "rcs", 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.
index = next((i for i, arg in enumerate(command[2:], 2)
if not arg.startswith('@') and Path(arg).suffix in ('.a', '.lib')), None)
if index is None:
raise SystemExit('Cannot identify archive output: ' + str(command))
dest = Path(command[index])
temporary = dest.with_name(dest.name + '.tmp-' + str(os.getpid()))
temporary.unlink(missing_ok=True)
objects = [x for x in command[index + 1:] if not x.startswith('-')]
if tool == 'ar' and dest.exists() and len(objects) == 1 and not objects[0].startswith('@'):
shutil.copy2(dest, temporary) # Explicit runtime/availability append.
command[index] = str(temporary)
try:
subprocess.run(command, check=True)
os.replace(temporary, dest)
finally:
temporary.unlink(missing_ok=True)
return
cache = shutil.which('ccache')
if tool in ('cc', 'cxx') and cache and os.environ.get('CCACHE_DIR') and '-c' in incoming:
os.environ['CCACHE_COMPILERTYPE'] = 'clang'
raw = str(Path(config['output']) / ('_cached-' + tool))
os.execv(cache, [cache, raw, *command[4:]])
os.execv(config["zig"], command)
def prepare(args):
zig = str(Path(shutil.which(args.zig) or args.zig).resolve())
version = subprocess.check_output([zig, "version"], text=True).strip()
if version != "0.14.1":
raise SystemExit("This adapter is verified with Zig 0.14.1; got " + version)
lib = Path(json.loads(subprocess.check_output([zig, "env"], text=True))["lib_dir"])
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=True)
config = {"zig": zig, "target": TARGETS[args.target], "version": version}
if args.target.startswith("win"):
include = output / "include"
# Regeneration is idempotent; remove only the generated SDK case alias.
alias = include / "XpsObjectModel.h"
if alias.is_symlink():
alias.unlink()
config["mingw_header_sha256"] = mingw_overlay(lib, include)
config["include"] = str(include)
if args.mingw_headers:
config["mingw_headers"] = str(args.mingw_headers.resolve())
header = args.mingw_headers / "windows.ui.composition.h"
if header.is_file():
original = header.read_text()
text = original
# WIDL 11 omitted namespace qualification on these imported
# types. Use the real declarations already imported by the
# header; preserve interface UUIDs and method/vtable order.
for name in ("DirectXAlphaMode", "DirectXPixelFormat"):
text = text.replace("enum " + name, "enum ABI::Windows::Graphics::DirectX::" + name)
text = text.replace("struct Size", "struct ABI::Windows::Foundation::Size")
for name in ("Vector2", "Vector3", "Vector4", "Matrix3x2", "Matrix4x4", "Quaternion"):
text = text.replace("struct " + name, "struct ABI::Windows::Foundation::Numerics::" + name)
(include / header.name).write_text(text)
config["composition_header_sha256"] = hashlib.sha256(header.read_bytes()).hexdigest()
elif args.target.startswith("osx"):
if not args.sdk or not (args.sdk / "SDKSettings.json").is_file():
raise SystemExit("macOS target requires --sdk pointing to a macOS SDK")
config["sdk"] = str(args.sdk.resolve())
settings = args.sdk / "SDKSettings.json"
if json.loads(settings.read_text()).get("Version") != "15.5":
raise SystemExit("ANGLE metadata adapter is pinned to macOS SDK 15.5")
config["sdk_settings_sha256"] = hashlib.sha256(settings.read_bytes()).hexdigest()
if args.target.startswith('linux'):
config['linux_sysroot'] = str(args.linux_sysroot.resolve()) if args.linux_sysroot else ''
config['linux_headers'] = str(args.linux_headers.resolve()) if args.linux_headers else ''
config['output'] = str(output)
if args.target.startswith('linux'):
root = Path(config['linux_sysroot'])
abi = output / 'include-linux'
abi.mkdir(exist_ok=True)
cpu = 'x86_64' if config['target'].startswith('x86_64') else 'aarch64'
for name in ('ffi.h', 'ffitarget.h'):
for prefix in (root / 'usr/include' / (cpu + '-linux-gnu'), root / 'usr/include'):
src = prefix / name
if src.is_file():
(abi / name).write_bytes(src.read_bytes())
break
config['linux_abi_headers'] = str(abi)
pc = output / 'pkg-config'
dirs = [root / 'usr/lib' / (cpu + '-linux-gnu') / 'pkgconfig', root / 'usr/lib/pkgconfig', root / 'usr/share/pkgconfig']
pc.write_text('#!/bin/sh\nexport PKG_CONFIG_SYSROOT_DIR=' + shlex.quote(str(root)) + '\nexport PKG_CONFIG_LIBDIR=' + shlex.quote(':'.join(map(str, dirs))) + '\nexec /usr/bin/pkg-config "$@"\n')
pc.chmod(0o755)
for kind in ('cc', 'cxx'):
raw = output / ('_cached-' + kind)
mode = 'cc' if kind == 'cc' else 'c++'
raw.write_text('#!/bin/sh\nexec ' + shlex.quote(zig) + ' ' + mode + ' -target ' + shlex.quote(config['target']) + ' "$@"\n')
raw.chmod(0o755)
descriptor = output / "toolchain.json"
descriptor.write_text(json.dumps(config, indent=2) + "\n")
for tool in ("cc", "cxx", "ar", "libtool"):
executable = output / tool
executable.write_text("#!/bin/sh\nexec python3 " + shlex.quote(str(Path(__file__).resolve())) +
" run " + shlex.quote(str(descriptor)) + " " + tool + ' "$@"\n')
executable.chmod(0o755)
print(descriptor)
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "run":
execute(json.loads(Path(sys.argv[2]).read_text()), sys.argv[3], sys.argv[4:])
else:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--zig", default="zig")
parser.add_argument("--target", choices=TARGETS, required=True)
parser.add_argument("--sdk", type=Path)
parser.add_argument("--mingw-headers", type=Path, help="Pinned extra WinRT headers, after Zig's headers")
parser.add_argument("--linux-sysroot", type=Path)
parser.add_argument("--linux-headers", type=Path)
parser.add_argument("--output", type=Path, required=True)
prepare(parser.parse_args())