243 lines
15 KiB
Python
243 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""Build pinned, already dependency-synced SkiaSharp/ANGLE sources from Linux."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from skia_compat import archive, configure, parse_args, repair_xps, repair_skcms
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
|
|
|
|
def run(args, cwd=None, env=None):
|
|
print('+', ' '.join(map(str, args)), flush=True)
|
|
subprocess.run(list(map(str, args)), cwd=cwd, env=env, check=True)
|
|
|
|
|
|
def patch(source, filename):
|
|
path = HERE / 'patches' / filename
|
|
cmd = ['git', '-C', str(source), 'apply']
|
|
if subprocess.run(cmd + ['--reverse', '--check', str(path)], capture_output=True).returncode == 0:
|
|
return
|
|
run(cmd + ['--check', path])
|
|
run(cmd + [path])
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description=__doc__)
|
|
p.add_argument('--target', choices=['linux-x64', 'linux-arm64', 'linux-musl-x64', 'linux-musl-arm64', 'win-x64', 'win-x86', 'win-arm64', 'osx-x64', 'osx-arm64'], required=True)
|
|
p.add_argument('--sources-manifest', type=Path, help='cross-sources.json from prepare-cross-sources.sh')
|
|
p.add_argument('--source-lock', type=Path, help='Exact version profile; defaults to sources.lock.json')
|
|
p.add_argument('--skia', type=Path, help='SkiaSharp externals/skia, with its pinned dependencies synced')
|
|
p.add_argument('--angle', type=Path, help='ANGLE source with pinned DEPS synced, including tools/win')
|
|
p.add_argument('--angle-gn', type=Path, help='GN from the pinned ANGLE DEPS')
|
|
p.add_argument('--zig', default='zig')
|
|
p.add_argument('--sdk', type=Path, default=Path('/opt/macos-sdk/MacOSX15.5.sdk'))
|
|
p.add_argument('--mingw-headers', type=Path, default=Path('/opt/mingw/usr/share/mingw-w64/include'))
|
|
p.add_argument('--availability-source', type=Path, default=Path('/opt/staticlink/os_version_check.c'))
|
|
p.add_argument('--linux-sysroots', type=Path, default=Path('/opt/sysroots'))
|
|
p.add_argument('--linux-headers', type=Path, default=Path('/opt/staticlink/linux-headers'))
|
|
p.add_argument('--work', type=Path, required=True)
|
|
p.add_argument('--output', type=Path, required=True)
|
|
p.add_argument('--jobs', type=int, default=4)
|
|
p.add_argument('--configure-only', action='store_true')
|
|
p.add_argument('--component', choices=['all', 'skia'], default='all', help='Build Skia alone when validating a new SkiaSharp release')
|
|
a = p.parse_args()
|
|
if a.sources_manifest:
|
|
sources = json.loads(a.sources_manifest.read_text())
|
|
if a.source_lock is None and sources.get('source_lock'):
|
|
a.source_lock = Path(sources['source_lock'])
|
|
for name in ('skia', 'angle', 'angle_gn'):
|
|
if getattr(a, name) is None:
|
|
setattr(a, name, Path(sources[name]))
|
|
if any(getattr(a, name) is None for name in ('skia', 'angle', 'angle_gn')):
|
|
p.error('Provide --sources-manifest or all of --skia, --angle and --angle-gn')
|
|
if platform.system() != 'Linux' or a.jobs < 1:
|
|
p.error('Requires Linux and positive --jobs')
|
|
for name in ('skia', 'angle', 'angle_gn', 'sdk', 'work', 'output', 'availability_source', 'mingw_headers'):
|
|
setattr(a, name, getattr(a, name).resolve())
|
|
lock = json.loads((a.source_lock or HERE / 'sources.lock.json').read_text())
|
|
for src, key in ((a.skia, 'skia_revision'), (a.angle, 'angle_revision')):
|
|
revision = subprocess.check_output(['git', '-C', str(src), 'rev-parse', 'HEAD'], text=True).strip()
|
|
if revision != lock[key]:
|
|
raise SystemExit(f'{src}: expected {lock[key]}, got {revision}; update the lock and revalidate first')
|
|
if 'skiasharp_build("SkiaSharp")' not in (a.skia / 'BUILD.gn').read_text():
|
|
raise SystemExit('--skia must be the SkiaSharp-patched Skia source, not vanilla Google Skia')
|
|
a.work.mkdir(parents=True, exist_ok=True)
|
|
a.output.mkdir(parents=True, exist_ok=True)
|
|
windows = a.target.startswith('win')
|
|
mac = a.target.startswith('osx')
|
|
linux = a.target.startswith('linux')
|
|
cpu = a.target.rsplit('-', 1)[1]
|
|
os_name = 'win' if windows else ('mac' if mac else 'linux')
|
|
if windows and not (a.mingw_headers / 'windows.ui.composition.h').is_file():
|
|
raise SystemExit('Windows ANGLE requires --mingw-headers containing the pinned WinRT headers')
|
|
recipe = hashlib.sha256()
|
|
for path in sorted(HERE.rglob('*')):
|
|
if path.is_file() and (path.suffix in ('.py', '.in', '.patch', '.c') or path.name in ('cross-toolchain.lock.json', 'linux-sysroots.lock.json', 'linux-sysroots-musl.lock.json')):
|
|
recipe.update(str(path.relative_to(HERE)).encode())
|
|
recipe.update(path.read_bytes())
|
|
variant = hashlib.sha256((json.dumps(lock, sort_keys=True) + recipe.hexdigest()).encode()).hexdigest()[:16]
|
|
shared = {'angle_revision': lock['angle_revision'], 'recipe': recipe.hexdigest(), 'toolchain': json.loads((HERE / 'cross-toolchain.lock.json').read_text())}
|
|
angle_variant = hashlib.sha256(json.dumps(shared, sort_keys=True).encode()).hexdigest()[:16]
|
|
tools = a.work / angle_variant / a.target
|
|
host_cpu = 'arm64' if platform.machine() == 'aarch64' else 'x64'
|
|
host_tools = a.work / angle_variant / ('linux-' + host_cpu)
|
|
for target, output in ((a.target, tools), ('linux-' + host_cpu, host_tools)):
|
|
command = [sys.executable, HERE / 'zig-cross.py', '--zig', a.zig, '--target', target,
|
|
'--output', output, '--sdk', a.sdk, '--mingw-headers', a.mingw_headers]
|
|
if target.startswith('linux'):
|
|
command += ['--linux-sysroot', a.linux_sysroots / target, '--linux-headers', a.linux_headers]
|
|
run(command)
|
|
config = json.loads((tools / 'toolchain.json').read_text())
|
|
zig = config['zig']
|
|
zig_lib = Path(json.loads(subprocess.check_output([zig, 'env'], text=True))['lib_dir'])
|
|
print('skcms Clang compatibility:', repair_skcms(a.skia), flush=True)
|
|
if windows:
|
|
print('XPS compatibility:', repair_xps(a.skia), flush=True)
|
|
# First apply the project's static ANGLE target patch if necessary.
|
|
if 'angle_static_library("libANGLE_static")' not in (a.angle / 'BUILD.gn').read_text():
|
|
patch(a.angle, 'angle-static-targets.patch')
|
|
for filename in ('BUILDCONFIG.gn-zig.patch', 'BUILD.gn-zig.patch',
|
|
'sdk_info-linux.patch', 'find_sdk-linux.patch', 'gcc-toolchain-objc.patch'):
|
|
patch(a.angle, filename)
|
|
run([sys.executable, a.angle / 'build/util/lastchange.py', '-o', a.angle / 'build/util/LASTCHANGE'], cwd=a.angle)
|
|
# Separate runtime paths prevent architectures from sharing an incompatible cache.
|
|
clang = tools / 'clang'
|
|
runtime = clang / 'lib/clang/19/lib' / ('windows' if windows else ('darwin' if mac else 'linux')) / (
|
|
'clang_rt.builtins-' + {'x64': 'x86_64', 'x86': 'i386', 'arm64': 'aarch64'}[cpu] + '.lib'
|
|
if windows else ('libclang_rt.osx.a' if mac else 'libclang_rt.builtins-' + ('x86_64' if cpu == 'x64' else 'aarch64') + '.a'))
|
|
runtime.parent.mkdir(parents=True, exist_ok=True)
|
|
run([zig, 'build-lib', zig_lib / 'compiler_rt.zig', '-target', config['target'],
|
|
'-O', 'ReleaseFast', '-fPIC', '-femit-bin=' + str(runtime)])
|
|
if linux:
|
|
for runtime_cpu, runtime_target in ((cpu, config['target']), (host_cpu, json.loads((host_tools / 'toolchain.json').read_text())['target'])):
|
|
triple = 'x86_64-unknown-linux-gnu' if runtime_cpu == 'x64' else 'aarch64-unknown-linux-gnu'
|
|
alias = clang / 'lib/clang/19/lib' / triple / 'libclang_rt.builtins.a'
|
|
alias.parent.mkdir(parents=True, exist_ok=True)
|
|
run([zig, 'build-lib', zig_lib / 'compiler_rt.zig', '-target', runtime_target,
|
|
'-O', 'ReleaseFast', '-fPIC', '-femit-bin=' + str(alias)])
|
|
availability = None
|
|
if mac:
|
|
if hashlib.sha256(a.availability_source.read_bytes()).hexdigest() != 'bc0b9f10e8d1a1ac5905d710f1c9fead23878e31233748d580393edc4e3bfc18':
|
|
raise SystemExit('LLVM availability source checksum mismatch')
|
|
availability = tools / 'os_version_check.o'
|
|
run([tools / 'cc', '-c', a.availability_source, '-o', availability])
|
|
run([tools / 'ar', 'rcs', runtime, availability])
|
|
env = os.environ.copy()
|
|
env['PATH'] = str(tools) + os.pathsep + env['PATH']
|
|
env['CCACHE_BASEDIR'] = str(a.skia)
|
|
env['CCACHE_NOHASHDIR'] = 'true'
|
|
env['CCACHE_COMPILERCHECK'] = 'content'
|
|
env['STATICLINK_MACOS_SDK'] = str(a.sdk)
|
|
env['STATICLINK_DARWIN_TOOLS'] = str(tools)
|
|
skia_out = a.skia / 'out' / ('staticlink-zig-' + a.target + '-' + variant)
|
|
angle_out = a.angle / 'out' / ('staticlink-zig-' + a.target + '-' + angle_variant)
|
|
for out in (skia_out, angle_out):
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
skia_args = (HERE / ('skia-' + os_name + '.gn.in')).read_text().replace('@CPU@', cpu)
|
|
if mac:
|
|
skia_args += f'min_macos_version = "{"11.0" if cpu == "arm64" else "10.13"}"\n'
|
|
skia_args += f'xcode_sysroot = {json.dumps(str(a.sdk))}\n'
|
|
for name in ('cc', 'cxx', 'ar'):
|
|
skia_args += name + ' = ' + json.dumps(str(tools / name)) + '\n'
|
|
if linux:
|
|
for key in ('cc', 'cxx', 'ar'):
|
|
skia_args = skia_args.replace('@' + key.upper() + '@', str(tools / key))
|
|
capabilities = configure(a.skia, skia_out, parse_args(skia_args), a.skia / 'bin/gn', env)
|
|
for target in ('skia', 'SkiaSharp', 'HarfBuzzSharp'):
|
|
description = json.loads(subprocess.check_output([str(a.skia / 'bin/gn'), 'desc', str(skia_out.relative_to(a.skia)),
|
|
'//:' + target, '--format=json'], cwd=a.skia, env=env, text=True))
|
|
kind = next(iter(description.values()))['type']
|
|
if kind != 'static_library':
|
|
raise SystemExit(f'{target} is {kind}; expected static_library')
|
|
if windows:
|
|
# Upstream Skia fixes Windows to the MSVC rules; retain its graph and
|
|
# replace compiler/archive commands explicitly with the GNU adapters.
|
|
path = skia_out / 'toolchain.ninja'
|
|
body = path.read_text()
|
|
index = body.index('subninja ')
|
|
rules = ''
|
|
for name, flags in (('cc', 'cflags_c'), ('cxx', 'cflags_cc')):
|
|
rules += f'rule {name}\n command = {tools / name} -MD -MF ${{out}}.d ${{defines}} ${{include_dirs}} ${{cflags}} ${{{flags}}} -c ${{in}} -o ${{out}}\n depfile = ${{out}}.d\n deps = gcc\n'
|
|
rules += f'rule alink\n command = {tools / "ar"} rcs ${{out}} @${{out}}.rsp\n rspfile = ${{out}}.rsp\n rspfile_content = ${{in_newline}}\n'
|
|
rules += 'rule stamp\n command = touch ${out}\nrule copy\n command = cp ${in} ${out}\nrule asm\n command = false\n'
|
|
path.write_text(rules + '\n' + body[index:])
|
|
sdk_link = angle_out / 'sdk/xcode_links/MacOSX15.5.sdk'
|
|
if mac:
|
|
sdk_link.parent.mkdir(parents=True, exist_ok=True)
|
|
if sdk_link.is_symlink():
|
|
sdk_link.unlink()
|
|
sdk_link.symlink_to(a.sdk)
|
|
# Each architecture gets an independent toolchain file; concurrent builds
|
|
# do not rewrite another target's generated GN toolchain.
|
|
toolchain_dir = a.angle / ('staticlink_zig_' + a.target.replace('-', '_') + '_' + angle_variant)
|
|
toolchain_dir.mkdir(exist_ok=True)
|
|
text = 'import("//build/toolchain/gcc_toolchain.gni")\n'
|
|
for name, folder, target_os, target_cpu in (('target', tools, os_name, cpu), ('host', host_tools, 'linux', host_cpu)):
|
|
text += f'gcc_toolchain("{name}") {{\n'
|
|
for var in ('cc', 'cxx', 'ar'):
|
|
text += ' ' + var + ' = ' + json.dumps(str(folder / var)) + '\n'
|
|
text += f' ld = cxx\n nm = "nm"\n readelf = "readelf"\n toolchain_args = {{ current_os = "{target_os}" current_cpu = "{target_cpu}" is_clang = true use_remoteexec = false }}\n}}\n'
|
|
(toolchain_dir / 'BUILD.gn').write_text(text)
|
|
angle_args = (HERE / ('angle-' + os_name + '.gn.in')).read_text().replace('@CPU@', cpu)
|
|
angle_args += f'custom_toolchain = "//{toolchain_dir.name}:target"\nhost_toolchain = "//{toolchain_dir.name}:host"\n'
|
|
angle_args += 'clang_base_path = ' + json.dumps(str(clang)) + '\n'
|
|
if linux:
|
|
angle_args += 'pkg_config = ' + json.dumps(str(tools / 'pkg-config')) + '\n'
|
|
angle_args += 'host_pkg_config = ' + json.dumps(str(host_tools / 'pkg-config')) + '\n'
|
|
if windows:
|
|
# These mandatory GN metadata paths refer to the real MinGW headers.
|
|
# The SDK/version strings satisfy GN; MSVC/Windows SDK are not used.
|
|
for name, path in (('visual_studio_path', Path(zig).parent), ('wdk_path', Path(zig).parent),
|
|
('windows_sdk_path', zig_lib / 'libc/include/any-windows-any')):
|
|
angle_args += name + ' = ' + json.dumps(str(path)) + '\n'
|
|
(angle_out / 'args.gn').write_text(angle_args)
|
|
run([a.angle_gn, 'gen', angle_out.relative_to(a.angle)], cwd=a.angle, env=env)
|
|
if a.configure_only:
|
|
print(f'Configured {lock["skiasharp_version"]} {a.target}: all three Skia targets are static libraries')
|
|
return
|
|
run(['ninja', '-C', skia_out, '-j', a.jobs, 'skia', 'SkiaSharp', 'HarfBuzzSharp'], env=env)
|
|
if a.component == 'all':
|
|
run(['ninja', '-C', angle_out, '-j', a.jobs, 'libANGLE_static', 'libGLESv2_static', 'libEGL_static'], env=env)
|
|
if availability:
|
|
run([tools / 'ar', 'rcs', archive(skia_out, 'skia'), availability])
|
|
files = [(archive(skia_out, name), name)
|
|
for name in ('skia', 'SkiaSharp', 'HarfBuzzSharp')]
|
|
if a.component == 'all':
|
|
files += [(angle_out / ('obj/lib' + name + '_static.a'), name) for name in ('ANGLE', 'GLESv2', 'EGL')]
|
|
manifest = dict(target=a.target, sources=lock, toolchain=config, archives={})
|
|
manifest['build_variant'] = variant
|
|
manifest['skia_capabilities'] = capabilities
|
|
if linux:
|
|
manifest['linux_sdk_packages'] = json.loads((HERE / ('linux-sysroots-musl.lock.json' if 'musl' in a.target else 'linux-sysroots.lock.json')).read_text())[a.target]
|
|
manifest['toolchain_inputs'] = json.loads((HERE / 'cross-toolchain.lock.json').read_text())
|
|
manifest['gn_sha256'] = {str(path): hashlib.sha256(path.read_bytes()).hexdigest()
|
|
for path in (a.skia / 'bin/gn', a.angle_gn)}
|
|
manifest['recipe_sha256'] = {str(path.relative_to(HERE)): hashlib.sha256(path.read_bytes()).hexdigest()
|
|
for path in sorted(HERE.rglob('*')) if path.is_file() and (path.suffix in ('.py', '.in', '.patch', '.c') or path.name in ('cross-toolchain.lock.json', 'linux-sysroots.lock.json', 'linux-sysroots-musl.lock.json'))}
|
|
packages = Path('/opt/staticlink/packages.lock')
|
|
if packages.is_file():
|
|
shutil.copy2(packages, a.output / 'toolchain-packages.lock')
|
|
manifest['packages_sha256'] = hashlib.sha256(packages.read_bytes()).hexdigest()
|
|
for source, name in files:
|
|
dest = a.output / ('lib' + name + '.a')
|
|
shutil.copy2(source, dest)
|
|
manifest['archives'][dest.name] = hashlib.sha256(dest.read_bytes()).hexdigest()
|
|
run([sys.executable, HERE / 'link-cross.py', '--toolchain', tools, '--skia', a.skia,
|
|
'--angle', a.angle, '--libraries', a.output, '--output', a.output / 'link-tests', '--component', a.component])
|
|
if a.target == 'win-x86':
|
|
run([sys.executable, HERE / 'x86-thunks.py', '--source', a.skia.parent.parent,
|
|
'--toolchain', tools, '--libraries', a.output])
|
|
manifest['link_tests'] = 'passed; target runtime tests still required'
|
|
(a.output / 'cross-build.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|