blob: ed4c89da34002c98b292f5aa5a0f1ee579d462af [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2025 Google LLC
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
import argparse
import hashlib
import os
import shutil
import subprocess
import sys
def add_common_cmake_args(parser):
"""Adds common arguments used for building with CMake."""
print(f"Running with Python executable: {sys.executable}")
parser.add_argument("--cc", required=True, help="Path to the C compiler.")
parser.add_argument("--cxx", required=True, help="Path to the C++ compiler.")
parser.add_argument(
"--cxx_flags",
default=[],
action="append",
help="C++ compiler flags. Can be specified multiple times.")
parser.add_argument(
"--ld_flags",
default=[],
action="append",
help="Linker flags. Can be specified multiple times.")
parser.add_argument(
"--output_path", required=True, help="Path to the output library.")
parser.add_argument(
"--depfile_path", required=True, help="Path to the depfile to generate.")
parser.add_argument(
"--target_os", required=True, help="Target OS for cross-compilation.")
parser.add_argument(
"--target_cpu", required=True, help="Target CPU for cross-compilation.")
parser.add_argument(
"--win_sdk", default="", help="Path to the Windows SDK.")
parser.add_argument(
"--win_sdk_version", default="", help="Version of the Windows SDK.")
parser.add_argument(
"--win_vc", default="", help="Path to Visual C++.")
parser.add_argument(
"--win_toolchain_version",
default="",
help="Version of the MSVC toolchain.")
parser.add_argument(
"--build_type", default="Release", help="CMake build type.")
parser.add_argument(
"--build_dir",
required=True,
help="A short name for the build directory.")
parser.add_argument("--is_clang", action=argparse.BooleanOptionalAction)
parser.add_argument(
"--enable_rtti", action=argparse.BooleanOptionalAction, help="Enable RTTI.")
def add_next_batch_to_command(base_cmd, workset):
if sys.platform != 'win32':
batch = list(workset)
return base_cmd + batch, batch
# Windows has a limit of about 8100 characters on command line commands.
# Thus we batch our commands to fit under this. 100 is usually short enough
# but with fluctuations on the order returned by ninja and set(), this can
# still sometimes be too long, so we ensure we don't go over the limit.
BATCH_SIZE = 100
batch = list(workset)[:BATCH_SIZE]
cmd = base_cmd + batch
while len(' '.join(cmd)) > 8100:
assert(len(batch) > 1)
batch.pop()
cmd = base_cmd + batch
return cmd, batch
def discover_dependencies(build_dir, targets):
"""Runs ninja -tinputs recursively to discover all targets, then uses
ninja -tdeps to get all source files and object files for those targets.
Returns a tuple of (list of source files, list of object files)."""
# The worklist contains ninja targets we need to find the inputs for.
worklist = set(targets)
# We keep track of all ninja targets we've ever seen to avoid cycles and
# redundant processing.
seen_targets = set(targets)
source_files = set()
ninja = shutil.which("ninja");
if not ninja:
print("Error: ninja not found in PATH.")
sys.exit(1)
while len(worklist) > 0:
cmd, current_batch = add_next_batch_to_command([ninja, "-C", build_dir, "-tinputs"],
worklist)
worklist = worklist.difference(current_batch)
inputs = subprocess.check_output(cmd).decode("utf-8").splitlines()
# inputs looks like:
# /home/user/skia/third_party/externals/dawn/src/tint/utils/text/styled_text_theme.cc
# /home/user/skia/third_party/externals/dawn/src/tint/utils/text/unicode.cc
# cmake_object_order_depends_target_tint_api_common
# cmake_object_order_depends_target_tint_lang_core
# src/tint/CMakeFiles/tint_api_common.dir/api/common/vertex_pulling_config.cc.o
# src/tint/CMakeFiles/tint_lang_core.dir/lang/core/binary_op.cc.o
# src/tint/libtint_api_common.a
# src/tint/libtint_lang_core.a
# Some of these are files (which we can add to the source file list), some are more targets,
# which we should recursively get targets for to make sure we have the whole dependency graph.
for line in inputs:
line = line.strip()
if os.path.isabs(line):
# Absolute paths are source files.
source_files.add(line)
continue
if line not in seen_targets:
worklist.add(line)
seen_targets.add(line)
object_files = []
for target in seen_targets:
if target.endswith(".o") or target.endswith(".obj"):
object_files.append(target)
object_files.sort()
# Now that we have all the targets and some of the source files, get the dependencies for them.
abs_build_dir = os.path.abspath(build_dir)
worklist = set(seen_targets)
while len(worklist) > 0:
cmd, current_batch = add_next_batch_to_command([ninja, "-C", build_dir, "-tdeps"],
worklist)
worklist = worklist.difference(current_batch)
output = subprocess.check_output(cmd).decode("utf-8").splitlines()
# When a target has deps, which are read from the .d files generated from the "-dkeepdepfile
# option earlier, the ninja command outputs something like this:
# third_party/spirv-tools/source/opt/CMakeFiles/SPIRV-Tools-opt.dir/eliminate_dead_functions_util.cpp.o: #deps 408, deps mtime 1755004530688332629 (VALID)
# /home/user/skia/third_party/externals/dawn/third_party/spirv-tools/src/source/opt/eliminate_dead_functions_util.cpp
# /usr/include/stdc-predef.h
# /home/user/skia/third_party/externals/dawn/third_party/spirv-tools/src/source/opt/eliminate_dead_functions_util.h
# /home/user/skia/third_party/externals/dawn/third_party/spirv-tools/src/source/opt/ir_context.h
# /usr/include/c++/14/algorithm
# These can sometimes be relative files (on Windows) with the base path being the build_dir
# src/tint/CMakeFiles/tint_lang_wgsl_sem.dir/lang/wgsl/sem/variable.cc.obj: #deps 300, deps mtime 7816349331123163 (VALID)
# ../../../../../cipd/clang_win/lib/clang/18/include/x86gprintrin.h
# ../../../../../cipd/win_toolchain/VC/Tools/MSVC/14.39.33519/include/__msvc_bit_utils.hpp
# ../../../../../cipd/win_toolchain/VC/Tools/MSVC/14.39.33519/include/cctype
# ../../../../../cipd/win_toolchain/win_sdk/Include/10.0.22621.0/ucrt/wchar.h
# ../../../third_party/externals/dawn/src/tint/api/common/binding_point.h
# ../../../third_party/externals/dawn/src/tint/lang/core/constant/clone_context.h
# If there's not a match, it's a simple message like:
# src/tint/libtint_utils_text_generator.a: deps not found
# We want to aggregate all the indented files
for line in output:
if not line.startswith(" ") or "deps not found" in line:
continue
line = line.strip()
if os.path.isabs(line):
source_files.add(line)
else:
dep = os.path.normpath(os.path.join(abs_build_dir, line))
source_files.add(dep)
result = list(source_files)
result.sort()
return result, object_files
def write_depfile(output_path, depfile_path, dependencies):
"""Generates a .d file that lists all discovered source files as
dependencies for the given output library."""
os.makedirs(os.path.dirname(depfile_path), exist_ok=True)
with open(depfile_path, "w") as f:
f.write(f"{output_path}:")
# Make sure to use forward slashes for paths in the depfile, even on
# Windows, for consistency.
for dep in dependencies:
f.write(" \\\n" + dep.replace("\\", "/"))
f.write("\n")
def quote_if_needed(path):
"""Adds quotes to a path if it contains spaces."""
if " " in path:
return f'"{path}"'
return path
def copy_if_changed(src, dest):
"""Copies the file from src to dest if dest doesn't exist or if the
contents of src and dest are different."""
if os.path.exists(dest):
src_hash = hashlib.sha256(open(src, "rb").read()).hexdigest()
dest_hash = hashlib.sha256(open(dest, "rb").read()).hexdigest()
if src_hash == dest_hash:
# The files are identical, no need to copy.
return
# Either the destination does not exist or it is different.
shutil.copyfile(src, dest)
os.chmod(dest, 0o755)
def get_cmake_os_cpu(os, cpu):
# https://stackoverflow.com/a/70498851
os = os.lower().strip()
cpu = cpu.lower().strip()
if os == "android":
target_cpu_map = {
"arm": "armeabi-v7a",
"arm64": "arm64-v8a",
"x64": "x86_64",
"x86": "x86",
}
return "Android", target_cpu_map[cpu]
if os == "linux":
target_cpu_map = {
"arm": "arm",
"arm64": "aarch64",
"x64": "x86_64",
"x86": "i686",
}
return "Linux", target_cpu_map[cpu]
if os == "mac":
target_cpu_map = {
"arm64": "arm64",
"x64": "x86_64",
}
return "Darwin", target_cpu_map[cpu]
if os == "win":
target_cpu_map = {
"arm64": "ARM64",
"x64": "AMD64",
}
return "Windows", target_cpu_map[cpu]
print("Unsupported OS")
sys.exit(1)
def get_windows_settings(args):
"""The Windows toolchain requires a lot of setup for cmake to use it.
This encapsulates all that setup.
"""
assert(args.win_vc)
assert(args.win_sdk)
assert(args.win_sdk_version)
win_cfgs, win_cxx, win_ld = [], [], []
# Set the Windows SDK version for CMake.
win_cfgs.append(
f"-DCMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION={args.win_sdk_version}")
# Explicitly tell CMake where to find the Resource Compiler, Manifest Tool, and Archiver.
rc_exe_path = os.path.join(args.win_sdk, "bin", args.win_sdk_version,
args.target_cpu, "rc.exe")
win_cfgs.append(f"-DCMAKE_RC_COMPILER={rc_exe_path.replace(os.sep, '/')}")
mt_exe_path = os.path.join(args.win_sdk, "bin", args.win_sdk_version,
args.target_cpu, "mt.exe")
win_cfgs.append(f"-DCMAKE_MT={mt_exe_path.replace(os.sep, '/')}")
ar_exe_path = os.path.join(args.win_vc, "Tools", "MSVC",
args.win_toolchain_version, "bin", "Hostx64",
args.target_cpu, "lib.exe")
win_cfgs.append(f"-DCMAKE_AR={ar_exe_path.replace(os.sep, '/')}")
# On Windows, we need to explicitly tell clang where to find the toolchain
# headers and libraries.
um_lib_path = os.path.join(args.win_sdk, "Lib", args.win_sdk_version, "um", args.target_cpu)
ucrt_lib_path = os.path.join(args.win_sdk, "Lib", args.win_sdk_version, "ucrt", args.target_cpu)
msvc_lib_path = os.path.join(args.win_vc, "Tools", "MSVC", args.win_toolchain_version, "lib", args.target_cpu)
win_ld += [
f"/LIBPATH:{quote_if_needed(um_lib_path.replace(os.sep, '/'))}",
f"/LIBPATH:{quote_if_needed(ucrt_lib_path.replace(os.sep, '/'))}",
f"/LIBPATH:{quote_if_needed(msvc_lib_path.replace(os.sep, '/'))}",
]
# Skia builds with exceptions and RTTI disabled so we must also build Dawn that way.
win_cxx += [
"-D_HAS_EXCEPTIONS=0",
"/GR-",
"/w", # Dawn's warnings are noisy
]
# Skia uses a hermetic toolchain, so we need to tell clang where to
# find the MSVC headers and libraries. If we pass the MSVC style flags (/I)
# to clang, then abseil fails to compile with errors about using a
# reinterpret_cast in a static_assert.
if args.is_clang:
win_cxx += [
"-imsvc",
quote_if_needed(os.path.join(args.win_vc, "Tools", "MSVC", args.win_toolchain_version, "include")),
"-imsvc",
quote_if_needed(os.path.join(args.win_sdk, "Include", args.win_sdk_version, "ucrt")),
"-imsvc",
quote_if_needed(os.path.join(args.win_sdk, "Include", args.win_sdk_version, "shared")),
"-imsvc",
quote_if_needed(os.path.join(args.win_sdk, "Include", args.win_sdk_version, "um")),
"-imsvc",
quote_if_needed(os.path.join(args.win_sdk, "Include", args.win_sdk_version, "winrt")),
]
else:
win_cxx += [
"/I" + quote_if_needed(os.path.join(args.win_vc, "Tools", "MSVC", args.win_toolchain_version, "include")),
"/I" + quote_if_needed(os.path.join(args.win_sdk, "Include", args.win_sdk_version, "ucrt")),
"/I" + quote_if_needed(os.path.join(args.win_sdk, "Include", args.win_sdk_version, "shared")),
"/I" + quote_if_needed(os.path.join(args.win_sdk, "Include", args.win_sdk_version, "um")),
"/I" + quote_if_needed(os.path.join(args.win_sdk, "Include", args.win_sdk_version, "winrt")),
]
# We want to build Dawn (and its dependencies) with /MT so we can statically
# link it into Skia.
win_cfgs.append("-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded")
win_cfgs.append("-DABSL_MSVC_STATIC_RUNTIME=ON")
return win_cfgs, win_cxx, win_ld
def combine_into_library(args, output_path, build_dir, target_os, object_files):
"""Combine all the object files into a single .a/.lib file so it's easier to
give this to GN."""
# Delete generated library if it exists already (otherwise ar sometimes chokes)
lib_name = os.path.basename(output_path)
gen_library_path = os.path.join(build_dir, lib_name)
if os.path.exists(gen_library_path):
os.remove(gen_library_path)
assert len(object_files) > 0
# Use ar/lib to join all the object files that comprise the necessary
# libraries and any transitive dependencies into one .a file.
if target_os == "Windows":
# On Windows, we use lld-link.exe for clang, and lib.exe for MSVC.
if args.is_clang:
linker_exe = os.path.join(os.path.dirname(args.cc), "lld-link.exe")
else:
# We can't just use ar.exe because it is not shipped with MSVC.
# We must use lib.exe, which has a different command line.
linker_exe = os.path.join(args.win_vc, "Tools", "MSVC",
args.win_toolchain_version, "bin", "Hostx64",
args.target_cpu, "lib.exe")
# The command line can be too long, so we use a response file.
response_file_name = "objects.rsp"
response_file_path = os.path.join(build_dir, response_file_name)
with open(response_file_path, "w") as f:
for obj in object_files:
f.write(f'"{obj}"\n')
combine_obj_cmd = [
linker_exe, "/LIB", f"/OUT:{lib_name}", f"@{response_file_name}"
]
else:
combine_obj_cmd = ["ar", "rcs", lib_name] + object_files
subprocess.run(combine_obj_cmd, cwd=build_dir, check=True)
copy_if_changed(gen_library_path, os.path.join(os.getcwd(), output_path))
def get_third_party_locations():
"""Return CMake configure arguments to point to or disable third_party deps"""
def verify_and_get(subpath):
third_party_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "externals")
third_party_dir = os.path.abspath(third_party_dir)
path = os.path.join(third_party_dir, subpath)
if not os.path.exists(path):
print(f"Third party path {path} not found - did you sync your DEPS?")
sys.exit(1)
return path
return [
# Actually downloading the 3p repos is handled by DEPS / tools/git-sync-deps
"-DDAWN_FETCH_DEPENDENCIES=OFF",
# Necessary 3p deps
f"-DDAWN_ABSEIL_DIR={verify_and_get('abseil-cpp')}",
f"-DDAWN_EGL_REGISTRY_DIR={verify_and_get('egl-registry')}",
f"-DDAWN_GLSLANG_DIR={verify_and_get('glslang')}",
f"-DDAWN_JINJA2_DIR={verify_and_get('jinja2')}",
f"-DDAWN_MARKUPSAFE_DIR={verify_and_get('markupsafe')}",
f"-DDAWN_OPENGL_REGISTRY_DIR={verify_and_get('opengl-registry')}",
f"-DDAWN_SPIRV_HEADERS_DIR={verify_and_get('spirv-headers')}",
f"-DDAWN_SPIRV_TOOLS_DIR={verify_and_get('spirv-tools')}",
f"-DDAWN_VULKAN_HEADERS_DIR={verify_and_get('vulkan-headers')}",
f"-DDAWN_VULKAN_UTILITY_LIBRARIES_DIR={verify_and_get('vulkan-utility-libraries')}",
f"-DDAWN_WEBGPU_HEADERS_DIR={verify_and_get('webgpu-headers')}",
f"-DDAWN_SWIFTSHADER_DIR={verify_and_get('swiftshader')}",
# Disable unnecessary deps
"-DDAWN_BUILD_BENCHMARKS=OFF",
"-DDAWN_BUILD_PROTOBUF=OFF",
"-DDAWN_BUILD_SAMPLES=OFF",
"-DDAWN_BUILD_TESTS=OFF",
"-DDAWN_USE_GLFW=OFF",
"-DTINT_BUILD_BENCHMARKS=OFF",
"-DTINT_BUILD_IR_BINARY=OFF",
"-DTINT_BUILD_TESTS=OFF",
"-DDAWN_USE_X11=OFF",
# Explicitly mark third_party deps as not here to make debugging easier
"-DDAWN_EMDAWNWEBGPU_DIR=NOT_SYNCED_BY_SKIA",
"-DDAWN_GLFW_DIR=NOT_SYNCED_BY_SKIA",
"-DDAWN_LPM_DIR=NOT_SYNCED_BY_SKIA",
"-DDAWN_PROTOBUF_DIR=NOT_SYNCED_BY_SKIA",
]