294 lines
15 KiB
Python
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())
|