#!/usr/bin/env python3
# Copyright 2024 The Chromium Authors, RobRich999, and Alex313031
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""This script is used to build clang binaries. It is used by package.py to
create the prebuilt binaries downloaded by update.py and used by developers.

The expectation is that update.py downloads prebuilt binaries for everyone, and
nobody should run this script as part of normal development.

DEAR MAC USER: YOU NEED XCODE INSTALLED TO BUILD LLVM/CLANG WITH THIS SCRIPT.
The Xcode command line tools that are installed as part of the Chromium
development setup process are not sufficient. CMake will fail to configure, as
the non-system Clang we use will not find any standard library headers. To use
this build script on Mac:
1. Download Xcode. (Visit http://go/xcode for googlers.)
2. Install to /Applications
3. sudo xcode-select --switch /Applications/Xcode.app
"""

import argparse
import glob
import io
import json
import multiprocessing
import os
import shlex
import platform
import re
import shutil
import subprocess
import sys
import tempfile
import urllib

from update import (CDS_URL, CHROMIUM_DIR, CLANG_REVISION, LLVM_BUILD_DIR,
                    FORCE_HEAD_REVISION_FILE, PACKAGE_VERSION, RELEASE_VERSION,
                    STAMP_FILE, THIS_DIR, DownloadUrl, DownloadAndUnpack,
                    DownloadAndUnpackPackage, EnsureDirExists, GetDefaultHostOs,
                    ReadStampFile, RmTree, WriteStampFile)

# Path constants. (All of these should be absolute paths.)
THIRD_PARTY_DIR = os.path.join(CHROMIUM_DIR, 'third_party')
LLVM_DIR = os.path.join(THIRD_PARTY_DIR, 'llvm')
COMPILER_RT_DIR = os.path.join(LLVM_DIR, 'compiler-rt')
LLVM_GIT_URL = ('https://chromium.googlesource.com/external/' +
                'github.com/llvm/llvm-project')
LLVM_BOOTSTRAP_DIR = os.path.join(THIRD_PARTY_DIR, 'llvm-bootstrap')
LLVM_BOOTSTRAP_INSTALL_DIR = os.path.join(THIRD_PARTY_DIR,
                                          'llvm-bootstrap-install')
LLVM_INSTRUMENTED_DIR = os.path.join(THIRD_PARTY_DIR, 'llvm-instrumented')
LLVM_PROFDATA_FILE = os.path.join(LLVM_INSTRUMENTED_DIR, 'profdata.prof')
LLVM_BUILD_TOOLS_DIR = os.path.abspath(
    os.path.join(LLVM_DIR, '..', 'llvm-build-tools'))
ANDROID_NDK_DIR = os.path.join(CHROMIUM_DIR, 'third_party',
                               'android_toolchain', 'ndk')
ANDROID_NDK_TOOLCHAIN_RELATIVE_DIR = os.path.join('toolchains', 'llvm',
                                                  'prebuilt', 'linux-x86_64')
ANDROID_NDK_TOOLCHAIN_DIR = os.path.join(ANDROID_NDK_DIR,
                                         ANDROID_NDK_TOOLCHAIN_RELATIVE_DIR)
# NOTE(nathaniel): Using the canary Android NDK in this manner is forecast
# to be temporary (~months; will be done differently by 2024).
ANDROID_NDK_CANARY_DIR = os.path.join(CHROMIUM_DIR, 'third_party',
                                      'android_toolchain_canary', 'ndk')
ANDROID_NDK_CANARY_TOOLCHAIN_DIR = os.path.join(
    ANDROID_NDK_CANARY_DIR, ANDROID_NDK_TOOLCHAIN_RELATIVE_DIR)
FUCHSIA_SDK_DIR = os.path.join(CHROMIUM_DIR, 'third_party', 'fuchsia-sdk',
                               'sdk')
PINNED_CLANG_DIR = os.path.join(LLVM_BUILD_TOOLS_DIR, 'pinned-clang')

BUG_REPORT_URL = ('https://crbug.com in the Tools>LLVM component,'
                  ' run tools/clang/scripts/process_crashreports.py'
                  ' (only if inside Google) to upload crash related files,')

LIBXML2_VERSION = 'libxml2-v2.9.12'
ZSTD_VERSION = 'zstd-1.5.5'

win_sdk_dir = None
def GetWinSDKDir():
  """Get the location of the current SDK."""
  global win_sdk_dir
  if win_sdk_dir:
    return win_sdk_dir

  # Don't let vs_toolchain overwrite our environment.
  environ_bak = dict(os.environ)

  sys.path.append(os.path.join(CHROMIUM_DIR, 'build'))
  import vs_toolchain
  win_sdk_dir = vs_toolchain.SetEnvironmentAndGetSDKDir()
  msvs_version = vs_toolchain.GetVisualStudioVersion()

  if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1'))):
    dia_path = os.path.join(win_sdk_dir, '..', 'DIA SDK', 'bin', 'amd64')
  else:
    if 'GYP_MSVS_OVERRIDE_PATH' not in os.environ:
      vs_path = vs_toolchain.DetectVisualStudioPath()
    else:
      vs_path = os.environ['GYP_MSVS_OVERRIDE_PATH']
    dia_path = os.path.join(vs_path, 'DIA SDK', 'bin', 'amd64')

  os.environ.clear()
  os.environ.update(environ_bak)
  return win_sdk_dir


def RunCommand(command, setenv=False, env=None, fail_hard=True):
  """Run command and return success (True) or failure; or if fail_hard is
     True, exit on failure.  If setenv is True, runs the command in a
     shell with the msvc tools for x64 architecture."""

  if setenv and sys.platform == 'win32':
    command = [os.path.join(CHROMIUM_DIR, 'tools', 'win', 'setenv.bat'), '&&'
               ] + command

  # https://docs.python.org/2/library/subprocess.html:
  # "On Unix with shell=True [...] if args is a sequence, the first item
  # specifies the command string, and any additional items will be treated as
  # additional arguments to the shell itself.  That is to say, Popen does the
  # equivalent of:
  #   Popen(['/bin/sh', '-c', args[0], args[1], ...])"
  #
  # We want to pass additional arguments to command[0], not to the shell,
  # so manually join everything into a single string.
  # Annoyingly, for "svn co url c:\path", shlex.quote() thinks that it should
  # quote c:\path but svn can't handle quoted paths on Windows.  Since on
  # Windows follow-on args are passed to args[0] instead of the shell, don't
  # do the single-string transformation there.
  if sys.platform != 'win32':
    command = ' '.join([shlex.quote(c) for c in command])
  print('Running', command)
  if subprocess.call(command, env=env, shell=True) == 0:
    return True
  print('Failed.')
  if fail_hard:
    sys.exit(1)
  return False


def CopyFile(src, dst):
  """Copy a file from src to dst."""
  print("Copying %s to %s" % (src, dst))
  shutil.copy(src, dst)


def CopyDirectoryContents(src, dst):
  """Copy the files from directory src to dst."""
  dst = os.path.realpath(dst)  # realpath() in case dst ends in /..
  EnsureDirExists(dst)
  for f in os.listdir(src):
    CopyFile(os.path.join(src, f), dst)


def CheckoutGitRepo(name, git_url, commit, dir):
  """Checkout the git repo at a certain git commit in dir. Any local
  modifications in dir will be lost."""

  print(f'Checking out {name} {commit} into {dir}')

  # Try updating the current repo if it exists and has no local diff.
  if os.path.isdir(dir):
    os.chdir(dir)
    # git diff-index --exit-code returns 0 when there is no diff.
    # Also check that the first commit is reachable.
    if (RunCommand(['git', 'diff-index', '--exit-code', 'HEAD'],
                   fail_hard=False)
        and RunCommand(['git', 'fetch'], fail_hard=False)
        and RunCommand(['git', 'checkout', commit], fail_hard=False)
        and RunCommand(['git', 'clean', '-f'], fail_hard=False)):
      return

    # If we can't use the current repo, delete it.
    os.chdir(CHROMIUM_DIR)  # Can't remove dir if we're in it.
    print('Removing %s.' % dir)
    RmTree(dir)

  clone_cmd = ['git', 'clone', git_url, dir]

  if RunCommand(clone_cmd, fail_hard=False):
    os.chdir(dir)
    if RunCommand(['git', 'checkout', commit], fail_hard=False):
      return

  print('CheckoutGitRepo failed.')
  sys.exit(1)


def GetLatestLLVMCommit():
  """Get the latest commit hash in the LLVM monorepo."""
  main = json.loads(
      urllib.request.urlopen('https://chromium.googlesource.com/external/' +
                             'github.com/llvm/llvm-project/' +
                             '+/refs/heads/main?format=JSON').read().decode(
                                 "utf-8").replace(")]}'", ""))
  return main['commit']


def GetCommitDescription(commit):
  """Get the output of `git describe`.

  Needs to be called from inside the git repository dir."""
  git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
  return subprocess.check_output([
      git_exe, 'describe', '--long', '--abbrev=8', '--match=*llvmorg-*-init',
      commit
  ], universal_newlines=True).rstrip()


def AddCMakeToPath():
  """Download CMake and add it to PATH."""
  if sys.platform == 'win32':
    zip_name = 'cmake-3.26.4-windows-x86_64.zip'
    dir_name = ['cmake-3.26.4-windows-x86_64', 'bin']
  elif sys.platform == 'darwin':
    zip_name = 'cmake-3.26.4-macos-universal.tar.gz'
    dir_name = ['cmake-3.26.4-macos-universal', 'CMake.app', 'Contents', 'bin']
  else:
    zip_name = 'cmake-3.26.4-linux-x86_64.tar.gz'
    dir_name = ['cmake-3.26.4-linux-x86_64', 'bin']

  cmake_dir = os.path.join(LLVM_BUILD_TOOLS_DIR, *dir_name)
  if not os.path.exists(cmake_dir):
    DownloadAndUnpack(CDS_URL + '/tools/' + zip_name, LLVM_BUILD_TOOLS_DIR)
  os.environ['PATH'] = cmake_dir + os.pathsep + os.environ.get('PATH', '')


def AddGnuWinToPath():
  """Download some GNU win tools and add them to PATH."""
  assert sys.platform == 'win32'

  gnuwin_dir = os.path.join(LLVM_BUILD_TOOLS_DIR, 'gnuwin')
  GNUWIN_VERSION = '14'
  GNUWIN_STAMP = os.path.join(gnuwin_dir, 'stamp')
  if ReadStampFile(GNUWIN_STAMP) == GNUWIN_VERSION:
    print('GNU Win tools already up to date.')
  else:
    zip_name = 'gnuwin-%s.zip' % GNUWIN_VERSION
    DownloadAndUnpack(CDS_URL + '/tools/' + zip_name, LLVM_BUILD_TOOLS_DIR)
    WriteStampFile(GNUWIN_VERSION, GNUWIN_STAMP)

  os.environ['PATH'] = gnuwin_dir + os.pathsep + os.environ.get('PATH', '')

  # find.exe, mv.exe and rm.exe are from MSYS (see crrev.com/389632). MSYS uses
  # Cygwin under the hood, and initializing Cygwin has a race-condition when
  # getting group and user data from the Active Directory is slow. To work
  # around this, use a horrible hack telling it not to do that.
  # See https://crbug.com/905289
  etc = os.path.join(gnuwin_dir, '..', '..', 'etc')
  EnsureDirExists(etc)
  with open(os.path.join(etc, 'nsswitch.conf'), 'w') as f:
    f.write('passwd: files\n')
    f.write('group: files\n')


def AddZlibToPath():
  """Download and build zlib, and add to PATH."""
  zlib_dir = os.path.join(LLVM_BUILD_TOOLS_DIR, 'zlib-1.2.11')
  if os.path.exists(zlib_dir):
    RmTree(zlib_dir)
  zip_name = 'zlib-1.2.11.tar.gz'
  DownloadAndUnpack(CDS_URL + '/tools/' + zip_name, LLVM_BUILD_TOOLS_DIR)
  os.chdir(zlib_dir)
  zlib_files = [
      'adler32', 'compress', 'crc32', 'deflate', 'gzclose', 'gzlib', 'gzread',
      'gzwrite', 'inflate', 'infback', 'inftrees', 'inffast', 'trees',
      'uncompr', 'zutil'
  ]
  cl_flags = [
      '/nologo', '/O2', '/DZLIB_DLL', '/c', '/D_CRT_SECURE_NO_DEPRECATE',
      '/D_CRT_NONSTDC_NO_DEPRECATE'
  ]
  RunCommand(['cl.exe'] + [f + '.c' for f in zlib_files] + cl_flags,
             setenv=True)
  RunCommand(['lib.exe'] + [f + '.obj'
                            for f in zlib_files] + ['/nologo', '/out:zlib.lib'],
             setenv=True)
  # Remove the test directory so it isn't found when trying to find
  # test.exe.
  shutil.rmtree('test')

  os.environ['PATH'] = zlib_dir + os.pathsep + os.environ.get('PATH', '')
  return zlib_dir


class LibXmlDirs:
  def __init__(self):
    self.unzip_dir = LLVM_BUILD_TOOLS_DIR
    # When unpacked in `unzip_dir`, this will be the directory where the
    # sources are found.
    self.src_dir = os.path.join(self.unzip_dir, LIBXML2_VERSION)
    # The lib is built in a directory under its sources.
    self.build_dir = os.path.join(self.src_dir, 'build')
    # The lib is installed in a directory under where its built.
    self.install_dir = os.path.join(self.build_dir, 'install')
    # The full path to installed include files.
    self.include_dir = os.path.join(self.install_dir, 'include', 'libxml2')
    # The full path to installed lib files.
    self.lib_dir = os.path.join(self.install_dir, 'lib')


def GetLibXml2Dirs():
  """Gets the set of directories where LibXml2 is located.

  Includes the diractories where the source is unpacked, where it is built,
  and installed."""
  return LibXmlDirs()


def BuildLibXml2():
  """Download and build libxml2"""
  # The .tar.gz on GCS was uploaded as follows.
  # The gitlab page has more up-to-date packages than http://xmlsoft.org/,
  # and the official releases on xmlsoft.org are only available over ftp too.
  # $ VER=v2.9.12
  # $ curl -O \
  #   https://gitlab.gnome.org/GNOME/libxml2/-/archive/$VER/libxml2-$VER.tar.gz
  # $ gsutil cp -n -a public-read libxml2-$VER.tar.gz \
  #   gs://chromium-browser-clang/tools

  dirs = GetLibXml2Dirs()
  if os.path.exists(dirs.src_dir):
    RmTree(dirs.src_dir)
  zip_name = LIBXML2_VERSION + '.tar.gz'
  DownloadAndUnpack(CDS_URL + '/tools/' + zip_name, dirs.unzip_dir)
  os.mkdir(dirs.build_dir)
  os.chdir(dirs.build_dir)

  # Disable everything except WITH_TREE and WITH_OUTPUT, both needed by LLVM's
  # WindowsManifestMerger.
  # Also enable WITH_THREADS, else libxml doesn't compile on Linux.
  RunCommand(
      [
          'cmake',
          '-GNinja',
          '-DCMAKE_BUILD_TYPE=Release',
          '-DCMAKE_INSTALL_PREFIX=install',
          # The mac_arm bot builds a clang arm binary, but currently on an intel
          # host. If we ever move it to run on an arm mac, this can go. We
          # could pass this only if args.build_mac_arm, but libxml is small, so
          # might as well build it universal always for a few years.
          '-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64',
          '-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded',  # /MT to match LLVM.
          '-DBUILD_SHARED_LIBS=OFF',
          '-DLIBXML2_WITH_C14N=OFF',
          '-DLIBXML2_WITH_CATALOG=OFF',
          '-DLIBXML2_WITH_DEBUG=OFF',
          '-DLIBXML2_WITH_DOCB=OFF',
          '-DLIBXML2_WITH_FTP=OFF',
          '-DLIBXML2_WITH_HTML=OFF',
          '-DLIBXML2_WITH_HTTP=OFF',
          '-DLIBXML2_WITH_ICONV=OFF',
          '-DLIBXML2_WITH_ICU=OFF',
          '-DLIBXML2_WITH_ISO8859X=OFF',
          '-DLIBXML2_WITH_LEGACY=OFF',
          '-DLIBXML2_WITH_LZMA=OFF',
          '-DLIBXML2_WITH_MEM_DEBUG=OFF',
          '-DLIBXML2_WITH_MODULES=OFF',
          '-DLIBXML2_WITH_OUTPUT=ON',
          '-DLIBXML2_WITH_PATTERN=OFF',
          '-DLIBXML2_WITH_PROGRAMS=OFF',
          '-DLIBXML2_WITH_PUSH=OFF',
          '-DLIBXML2_WITH_PYTHON=OFF',
          '-DLIBXML2_WITH_READER=OFF',
          '-DLIBXML2_WITH_REGEXPS=OFF',
          '-DLIBXML2_WITH_RUN_DEBUG=OFF',
          '-DLIBXML2_WITH_SAX1=OFF',
          '-DLIBXML2_WITH_SCHEMAS=OFF',
          '-DLIBXML2_WITH_SCHEMATRON=OFF',
          '-DLIBXML2_WITH_TESTS=OFF',
          '-DLIBXML2_WITH_THREADS=ON',
          '-DLIBXML2_WITH_THREAD_ALLOC=OFF',
          '-DLIBXML2_WITH_TREE=ON',
          '-DLIBXML2_WITH_VALID=OFF',
          '-DLIBXML2_WITH_WRITER=OFF',
          '-DLIBXML2_WITH_XINCLUDE=OFF',
          '-DLIBXML2_WITH_XPATH=OFF',
          '-DLIBXML2_WITH_XPTR=OFF',
          '-DLIBXML2_WITH_ZLIB=OFF',
          '-DCMAKE_ASM_FLAGS_RELEASE=-O3 -w -mavx -maes -DNDEBUG',
          '-DCMAKE_C_FLAGS_RELEASE=-O3 -w -mavx -maes -DNDEBUG',
          '-DCMAKE_CXX_FLAGS_RELEASE=-O3 -w -mavx -maes -DNDEBUG',
          '..',
      ],
      setenv=True)
  RunCommand(['ninja', 'install'], setenv=True)

  if sys.platform == 'win32':
    libxml2_lib = os.path.join(dirs.lib_dir, 'libxml2s.lib')
  else:
    libxml2_lib = os.path.join(dirs.lib_dir, 'libxml2.a')
  extra_cmake_flags = [
      '-DLLVM_ENABLE_LIBXML2=FORCE_ON',
      '-DLIBXML2_INCLUDE_DIR=' + dirs.include_dir.replace('\\', '/'),
      '-DLIBXML2_LIBRARIES=' + libxml2_lib.replace('\\', '/'),
      '-DLIBXML2_LIBRARY=' + libxml2_lib.replace('\\', '/'),

      # This hermetic libxml2 has enough features enabled for lld-link, but not
      # for the libxml2 usage in libclang. We don't need libxml2 support in
      # libclang, so just turn that off.
      '-DCLANG_ENABLE_LIBXML2=NO',
  ]
  extra_cflags = ['-DLIBXML_STATIC']

  return extra_cmake_flags, extra_cflags


class ZStdDirs:
  """
  The set of directories where zstd is located.

  Includes the diractories where the source is unpacked, where it is built,
  and installed.
  """
  def __init__(self):
    self.unzip_dir = LLVM_BUILD_TOOLS_DIR
    # When unpacked in `unzip_dir`, this will be the directory where the
    # sources are found.
    self.src_dir = os.path.join(self.unzip_dir, ZSTD_VERSION)
    # The lib is built in a directory under its sources. Note, zstd uses
    # build/cmake for cmake.
    self.build_dir = os.path.join(self.src_dir, 'cmake_build')
    # The lib is installed in a directory under where its built.
    self.install_dir = os.path.join(self.build_dir, 'install')
    # The full path to installed include files.
    self.include_dir = os.path.join(self.install_dir, 'include')
    # The full path to installed lib files.
    self.lib_dir = os.path.join(self.install_dir, 'lib')


def BuildZStd():
  """Download and build zstd lib"""
  # The zstd-1.5.5.tar.gz was downloaded from
  #   https://github.com/facebook/zstd/releases/
  # and uploaded as follows.
  # $ gsutil cp -n -a public-read zstd-$VER.tar.gz \
  #   gs://chromium-browser-clang/tools

  dirs = ZStdDirs()
  if os.path.exists(dirs.src_dir):
    RmTree(dirs.src_dir)
  zip_name = ZSTD_VERSION + '.tar.gz'
  DownloadAndUnpack(CDS_URL + '/tools/' + zip_name, dirs.unzip_dir)
  os.mkdir(dirs.build_dir)
  os.chdir(dirs.build_dir)

  RunCommand(
      [
          'cmake',
          '-GNinja',
          '-DCMAKE_BUILD_TYPE=Release',
          '-DCMAKE_INSTALL_PREFIX=install',
          # The mac_arm bot builds a clang arm binary, but currently on an intel
          # host. If we ever move it to run on an arm mac, this can go. We
          # could pass this only if args.build_mac_arm, but zstd is small, so
          # might as well build it universal always for a few years.
          '-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64',
          '-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded',  # /MT to match LLVM.
          '-DZSTD_BUILD_SHARED=OFF',
          '../build/cmake',
      ],
      setenv=True)
  RunCommand(['ninja', 'install'], setenv=True)

  if sys.platform == 'win32':
    zstd_lib = os.path.join(dirs.lib_dir, 'zstd_static.lib')
  else:
    zstd_lib = os.path.join(dirs.lib_dir, 'libzstd.a')
  extra_cmake_flags = [
      '-DLLVM_ENABLE_ZSTD=ON',
      '-DLLVM_USE_STATIC_ZSTD=ON',
      '-Dzstd_INCLUDE_DIR=' + dirs.include_dir.replace('\\', '/'),
      '-Dzstd_LIBRARY=' + zstd_lib.replace('\\', '/'),
  ]
  extra_cflags = []

  return extra_cmake_flags, extra_cflags


def DownloadRPMalloc():
  """Download rpmalloc."""
  rpmalloc_dir = os.path.join(LLVM_BUILD_TOOLS_DIR, 'rpmalloc')
  if os.path.exists(rpmalloc_dir):
    RmTree(rpmalloc_dir)

  # Using rpmalloc bc1923f rather than the latest release (1.4.1) because
  # it contains the fix for https://github.com/mjansson/rpmalloc/pull/186
  # which would cause lld to deadlock.
  # The zip file was created and uploaded as follows:
  # $ mkdir rpmalloc
  # $ curl -L https://github.com/mjansson/rpmalloc/archive/bc1923f436539327707b08ef9751a7a87bdd9d2f.tar.gz \
  #     | tar -C rpmalloc --strip-components=1 -xzf -
  # $ GZIP=-9 tar vzcf rpmalloc-bc1923f.tgz rpmalloc
  # $ gsutil.py cp -n -a public-read rpmalloc-bc1923f.tgz \
  #     gs://chromium-browser-clang/tools/
  zip_name = 'rpmalloc-bc1923f.tgz'
  DownloadAndUnpack(CDS_URL + '/tools/' + zip_name, LLVM_BUILD_TOOLS_DIR)
  rpmalloc_dir = rpmalloc_dir.replace('\\', '/')
  return rpmalloc_dir


def StartGomaAndGetGomaCCPath():
  bat_ext = '.bat' if sys.platform == 'win32' else ''
  exe_ext = '.exe' if sys.platform == 'win32' else ''
  subprocess.check_output(['goma_ctl' + bat_ext, 'ensure_start'])
  return os.path.join(
      subprocess.check_output(['goma_ctl' + bat_ext, 'goma_dir'],
                              universal_newlines=True).rstrip(),
      'gomacc' + exe_ext)


def DownloadPinnedClang():
  PINNED_CLANG_VERSION = 'llvmorg-17-init-16420-g0c545a44-1'
  DownloadAndUnpackPackage('clang', PINNED_CLANG_DIR, GetDefaultHostOs(),
                           PINNED_CLANG_VERSION)


def VerifyVersionOfBuiltClangMatchesVERSION():
  """Checks that `clang --version` outputs RELEASE_VERSION. If this
  fails, update.RELEASE_VERSION is out-of-date and needs to be updated (possibly
  in an `if args.llvm_force_head_revision:` block inupdate. main() first)."""
  clang = os.path.join(LLVM_BUILD_DIR, 'bin', 'clang')
  if sys.platform == 'win32':
    clang += '-cl.exe'
  version_out = subprocess.check_output([clang, '--version'],
                                        universal_newlines=True)
  version_out = re.match(r'clang version ([0-9]+)', version_out).group(1)
  if version_out != RELEASE_VERSION:
    print(('unexpected clang version %s (not %s), '
           'update RELEASE_VERSION in update.py')
          % (version_out, RELEASE_VERSION))
    sys.exit(1)


def VerifyZlibSupport():
  """Check that clang was built with zlib support enabled."""
  clang = os.path.join(LLVM_BUILD_DIR, 'bin', 'clang')
  test_file = '/dev/null'
  if sys.platform == 'win32':
    clang += '.exe'
    test_file = 'nul'

  print('Checking for zlib support')
  clang_out = subprocess.check_output([
      clang, '-target', 'x86_64-unknown-linux-gnu', '-gz', '-c', '-###', '-x',
      'c', test_file
  ],
                                      stderr=subprocess.STDOUT,
                                      universal_newlines=True)
  if (re.search(r'--compress-debug-sections', clang_out)):
    print('OK')
  else:
    print(('Failed to detect zlib support!\n\n(driver output: %s)') % clang_out)
    sys.exit(1)


def VerifyZStdSupport():
  """Check that lld was built with zstd support enabled."""
  lld = os.path.join(LLVM_BUILD_DIR, 'bin')
  if sys.platform == 'win32':
    lld = os.path.join(lld, 'lld-link.exe')
  elif sys.platform == 'linux':
    lld = os.path.join(lld, 'ld.lld')
  else:
    print('zstd support check cannot be performed on the unsupported ' \
          'platform ' + sys.platform)
    return

  print('Checking for zstd support')
  lld_out = subprocess.run([lld, '--compress-debug-sections=zstd'],
                           check=False,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT,
                           universal_newlines=True).stdout
  if '--compress-debug-sections: zstd is not available' in lld_out:
    print(('Failed to detect zlib support!\n\n(driver output: %s)') % lld_out)
    sys.exit(1)
  else:
    print('OK')


def DownloadDebianSysroot(platform_name, skip_download=False):
  # Download sysroots. This uses basically Chromium's sysroots, but with
  # minor changes:
  # - glibc version bumped to 2.18 to make __cxa_thread_atexit_impl
  #   work (clang can require 2.18; chromium currently doesn't)
  # - libcrypt.so.1 reversioned so that crypt() is picked up from glibc
  # The sysroot was built at
  # https://chromium-review.googlesource.com/c/chromium/src/+/3684954/1
  # and the hashes here are from sysroots.json in that CL.
  toolchain_bucket = 'https://commondatastorage.googleapis.com/chrome-linux-sysroot/toolchain/'

  hashes = {
      # hash from https://chromium-review.googlesource.com/c/chromium/src/+/3684954/1/build/linux/sysroot_scripts/sysroots.json#3
      'amd64': '2028cdaf24259d23adcff95393b8cc4f0eef714b',
      # hash from https://chromium-review.googlesource.com/c/chromium/src/+/3684954/1/build/linux/sysroot_scripts/sysroots.json#23
      'i386': 'a033618b5e092c86e96d62d3c43f7363df6cebe7',
      # hash from https://chromium-review.googlesource.com/c/chromium/src/+/3684954/1/build/linux/sysroot_scripts/sysroots.json#8
      'arm': '0b9a3c54d2d5f6b1a428369aaa8d7ba7b227f701',
      # hash from https://chromium-review.googlesource.com/c/chromium/src/+/3684954/1/build/linux/sysroot_scripts/sysroots.json#12
      'arm64': '0e28d9832614729bb5b731161ff96cb4d516f345',
  }

  toolchain_name = f'debian_bullseye_{platform_name}_sysroot'
  output = os.path.join(LLVM_BUILD_TOOLS_DIR, toolchain_name)
  U = toolchain_bucket + hashes[platform_name] + '/' + toolchain_name + \
      '.tar.xz'
  if not skip_download:
    DownloadAndUnpack(U, output)

  return output


def compiler_rt_cmake_flags(*, sanitizers, profile):
  # Don't set -DCOMPILER_RT_BUILD_BUILTINS=ON/OFF as it interferes with the
  # runtimes logic of building builtins.
  args = [
      # Build crtbegin/crtend. It's just two tiny TUs, so just enable this
      # everywhere, even though we only need it on Linux.
      'COMPILER_RT_BUILD_CRT=ON',
      'COMPILER_RT_BUILD_LIBFUZZER=OFF',
      'COMPILER_RT_BUILD_MEMPROF=OFF',
      'COMPILER_RT_BUILD_ORC=OFF',
      'COMPILER_RT_BUILD_PROFILE=' + ('ON' if profile else 'OFF'),
      'COMPILER_RT_BUILD_SANITIZERS=' + ('ON' if sanitizers else 'OFF'),
      'COMPILER_RT_BUILD_XRAY=OFF',
      # See crbug.com/1205046: don't build scudo (and others we don't need).
      'COMPILER_RT_SANITIZERS_TO_BUILD=asan;dfsan;msan;hwasan;tsan;cfi',
      # We explicitly list all targets we want to build, do not autodetect
      # targets.
      'COMPILER_RT_DEFAULT_TARGET_ONLY=ON',
  ]
  return args


def gn_arg(v):
  if v == 'True':
    return True
  if v == 'False':
    return False
  raise argparse.ArgumentTypeError('Expected one of %r or %r' % (
      'True', 'False'))


def main():
  parser = argparse.ArgumentParser(description='Build Clang.')
  parser.add_argument('--bootstrap', action='store_true',
                      help='first build clang with CC, then with itself.')
  parser.add_argument('--build-mac-arm', action='store_true',
                      help='Build arm binaries. Only valid on macOS.')
  parser.add_argument('--disable-asserts', action='store_true',
                      help='build with asserts disabled')
  parser.add_argument('--host-cc',
                      help='build with host C compiler, requires --host-cxx as '
                      'well')
  parser.add_argument('--host-cxx',
                      help='build with host C++ compiler, requires --host-cc '
                      'as well')
  parser.add_argument('--pgo', action='store_true', help='build with PGO')
  parser.add_argument('--thinlto',
                      action='store_true',
                      help='build with ThinLTO')
  parser.add_argument('--bolt', action='store_true', help='build with BOLT')
  parser.add_argument('--llvm-force-head-revision', action='store_true',
                      help='build the latest revision')
  parser.add_argument('--run-tests', action='store_true',
                      help='run tests after building')
  parser.add_argument('--skip-build', action='store_true',
                      help='do not build anything')
  parser.add_argument('--skip-checkout', action='store_true',
                      help='do not create or update any checkouts')
  parser.add_argument('--build-dir',
                      help='Override build directory')
  parser.add_argument('--install-dir',
                      help='override the install directory for the final '
                      'compiler. If not specified, no install happens for '
                      'the compiler.')
  parser.add_argument('--no-tools',
                      action='store_true',
                      help='don\'t build any chromium tools or '
                      'clang-extra-tools. Overrides --extra-tools.')
  parser.add_argument('--extra-tools', nargs='*', default=[],
                      help='select additional chrome tools to build')
  parser.add_argument('--use-system-cmake', action='store_true',
                      help='use the cmake from PATH instead of downloading '
                      'and using prebuilt cmake binaries')
  parser.add_argument('--tf-path',
                      help='path to python tensorflow pip package. '
                      'Used for embedding an MLGO model')
  parser.add_argument(
      '--with-ml-inliner-model',
      help='path to MLGO inliner model to embed. Setting to '
      '\'default\', will download an official model which was '
      'trained for Chrome on Android',
      default='default' if sys.platform.startswith('linux') else '')
  parser.add_argument('--with-android', type=gn_arg, nargs='?', const=True,
                      help='build the Android ASan runtime (linux only)',
                      default=sys.platform.startswith('linux'))
  parser.add_argument('--pic',
                      action='store_true',
                      help='Uses PIC when building LLVM')
  parser.add_argument('--with-fuchsia',
                      type=gn_arg,
                      nargs='?',
                      const=True,
                      help='build the Fuchsia runtimes (linux only)',
                      default=sys.platform.startswith('linux'))
  parser.add_argument('--without-android', action='store_false',
                      help='don\'t build Android ASan runtime (linux only)',
                      dest='with_android')
  parser.add_argument('--without-fuchsia', action='store_false',
                      help='don\'t build Fuchsia clang_rt runtime (linux/mac)',
                      dest='with_fuchsia',
                      default=sys.platform in ('linux2', 'darwin'))
  parser.add_argument('--with-goma',
                      action='store_true',
                      help='Use goma to build the stage 1 compiler')
  parser.add_argument('--without-zstd',
                      dest='with_zstd',
                      action='store_false',
                      help='Disable zstd in the build')

  args = parser.parse_args()

  global CLANG_REVISION, PACKAGE_VERSION, LLVM_BUILD_DIR

  if (args.pgo or args.thinlto) and not args.bootstrap:
    print('--pgo/--thinlto requires --bootstrap')
    return 1
  if args.with_android and not os.path.exists(ANDROID_NDK_DIR):
    print('Android NDK not found at ' + ANDROID_NDK_DIR)
    print('The Android NDK is needed to build a Clang whose -fsanitize=address')
    print('works on Android. See ')
    print('https://www.chromium.org/developers/how-tos/android-build-instructions')
    print('for how to install the NDK, or pass --without-android.')
    return 1

  if args.with_fuchsia and not os.path.exists(FUCHSIA_SDK_DIR):
    print('Fuchsia SDK not found at ' + FUCHSIA_SDK_DIR)
    print('The Fuchsia SDK is needed to build libclang_rt for Fuchsia.')
    print('Install the Fuchsia SDK by adding fuchsia to the ')
    print('target_os section in your .gclient and running hooks, ')
    print('or pass --without-fuchsia.')
    print(
        'https://chromium.googlesource.com/chromium/src/+/main/docs/fuchsia/build_instructions.md'
    )
    print('for general Fuchsia build instructions.')
    return 1

  if args.build_mac_arm and sys.platform != 'darwin':
    print('--build-mac-arm only valid on macOS')
    return 1
  if args.build_mac_arm and platform.machine() == 'arm64':
    print('--build-mac-arm only valid on intel to cross-build arm')
    return 1
  if args.with_ml_inliner_model and not sys.platform.startswith('linux'):
    print('--with-ml-inliner-model only supports linux hosts')
    return 1

  # Don't buffer stdout, so that print statements are immediately flushed.
  # LLVM tests print output without newlines, so with buffering they won't be
  # immediately printed.
  major, _, _, _, _ = sys.version_info
  if major == 3:
    # Python3 only allows unbuffered output for binary streams. This
    # workaround comes from https://stackoverflow.com/a/181654/4052492.
    sys.stdout = io.TextIOWrapper(open(sys.stdout.fileno(), 'wb', 0),
                                  write_through=True)
  else:
    sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)


  if args.build_dir:
    LLVM_BUILD_DIR = args.build_dir

  if args.llvm_force_head_revision:
    checkout_revision = GetLatestLLVMCommit()
  else:
    checkout_revision = CLANG_REVISION

  if not args.skip_checkout:
    CheckoutGitRepo('LLVM monorepo', LLVM_GIT_URL, checkout_revision, LLVM_DIR)

  if args.llvm_force_head_revision:
    CLANG_REVISION = GetCommitDescription(checkout_revision)
    PACKAGE_VERSION = '%s-0' % CLANG_REVISION

  print('Locally building clang %s...' % PACKAGE_VERSION)
  WriteStampFile('', STAMP_FILE)
  WriteStampFile('', FORCE_HEAD_REVISION_FILE)

  if not args.use_system_cmake:
    AddCMakeToPath()

  if sys.platform == 'win32':
    # CMake on Windows doesn't like depot_tools's ninja.bat wrapper.
    ninja_dir = os.path.join(THIRD_PARTY_DIR, 'ninja')
    os.environ['PATH'] = ninja_dir + os.pathsep + os.environ.get('PATH', '')

  if args.skip_build:
    return 0

  # The variable "lld" is only used on Windows because only there does setting
  # CMAKE_LINKER have an effect: On Windows, the linker is called directly,
  # while elsewhere it's called through the compiler driver, and we pass
  # -fuse-ld=lld there to make the compiler driver call the linker (by setting
  # LLVM_ENABLE_LLD).
  cc, cxx, lld = None, None, None

  cflags = []
  cxxflags = []
  ldflags = []

  targets = 'AArch64;ARM;LoongArch;Mips;PowerPC;RISCV;SystemZ;WebAssembly;X86'
  projects = 'clang;lld;clang-tools-extra;polly'
  if args.bolt:
    projects += ';bolt'

  pic_default = sys.platform == 'win32'
  pic_mode = 'ON' if args.pic or pic_default else 'OFF'

  base_cmake_args = [
      '-GNinja',
      '-DCMAKE_BUILD_TYPE=Release',
      '-DLLVM_ENABLE_ASSERTIONS=%s' % ('OFF' if args.disable_asserts else 'ON'),
      '-DLLVM_ENABLE_PROJECTS=' + projects,
      '-DLLVM_ENABLE_RUNTIMES=compiler-rt',
      '-DLLVM_TARGETS_TO_BUILD=' + targets,
      f'-DLLVM_ENABLE_PIC={pic_mode}',
      '-DLLVM_ENABLE_TERMINFO=OFF',
      '-DLLVM_ENABLE_Z3_SOLVER=OFF',
      '-DCLANG_PLUGIN_SUPPORT=OFF',
      '-DCLANG_ENABLE_STATIC_ANALYZER=OFF',
      '-DCLANG_ENABLE_ARCMT=OFF',
      '-DBUG_REPORT_URL=' + BUG_REPORT_URL,
      # See crbug.com/1126219: Use native symbolizer instead of DIA
      '-DLLVM_ENABLE_DIA_SDK=OFF',
      # Link all binaries with lld. Effectively passes -fuse-ld=lld to the
      # compiler driver. On Windows, cmake calls the linker directly, so there
      # the same is achieved by passing -DCMAKE_LINKER=$lld below.
      '-DLLVM_ENABLE_LLD=ON',
      # The default value differs per platform, force it off everywhere.
      '-DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF',
      # Don't use curl.
      '-DLLVM_ENABLE_CURL=OFF',
      # Build libclang.a as well as libclang.so
      '-DLIBCLANG_BUILD_STATIC=ON',
      '-DCMAKE_ASM_FLAGS_RELEASE=-O3 -w -mavx -maes -DNDEBUG',
      '-DCMAKE_C_FLAGS_RELEASE=-O3 -w -mavx -maes -DNDEBUG',
      '-DCMAKE_CXX_FLAGS_RELEASE=-O3 -w -mavx -maes -DNDEBUG',
      # The Rust build (on Mac ARM at least if not others) depends on the
      # FileCheck tool which is built but not installed by default, this
      # puts it in the path for the Rust build to find and matches the
      # `bootstrap` tool:
      # https://github.com/rust-lang/rust/blob/021861aea8de20c76c7411eb8ada7e8235e3d9b5/src/bootstrap/src/core/build_steps/llvm.rs#L348
      '-DLLVM_INSTALL_UTILS=ON',
      '-DLLVM_ENABLE_ZSTD=%s' % ('ON' if args.with_zstd else 'OFF'),
  ]

  if sys.platform == 'darwin':
    isysroot = subprocess.check_output(['xcrun', '--show-sdk-path'],
                                       universal_newlines=True).rstrip()
  base_cmake_args += ['-DLLVM_ENABLE_UNWIND_TABLES=OFF']

  # See https://crbug.com/1302636#c49 - #c56 -- intercepting crypt_r() does not
  # work with the sysroot for not fully understood reasons. Disable it.
  sanitizers_override = [
    '-DSANITIZER_OVERRIDE_INTERCEPTORS',
    '-I' + os.path.join(THIS_DIR, 'sanitizers'),
  ]
  cflags += sanitizers_override
  cxxflags += sanitizers_override

  goma_cmake_args = []
  goma_ninja_args = []
  if args.with_goma:
    goma_path = StartGomaAndGetGomaCCPath()
    goma_cmake_args.append('-DCMAKE_C_COMPILER_LAUNCHER=' + goma_path)
    goma_cmake_args.append('-DCMAKE_CXX_COMPILER_LAUNCHER=' + goma_path)
    goma_ninja_args = ['-j' + str(multiprocessing.cpu_count() * 50)]

  if args.host_cc or args.host_cxx:
    assert args.host_cc and args.host_cxx, \
           "--host-cc and --host-cxx need to be used together"
    cc = args.host_cc
    cxx = args.host_cxx
  else:
    if not args.skip_checkout:
      DownloadPinnedClang()
    if sys.platform == 'win32':
      cc = os.path.join(PINNED_CLANG_DIR, 'bin', 'clang-cl.exe')
      cxx = os.path.join(PINNED_CLANG_DIR, 'bin', 'clang-cl.exe')
      lld = os.path.join(PINNED_CLANG_DIR, 'bin', 'lld-link.exe')
      # CMake has a hard time with backslashes in compiler paths:
      # https://stackoverflow.com/questions/13050827
      cc = cc.replace('\\', '/')
      cxx = cxx.replace('\\', '/')
      lld = lld.replace('\\', '/')
    else:
      cc = os.path.join(PINNED_CLANG_DIR, 'bin', 'clang')
      cxx = os.path.join(PINNED_CLANG_DIR, 'bin', 'clang++')

    if sys.platform.startswith('linux'):
      base_cmake_args += [ '-DLLVM_STATIC_LINK_CXX_STDLIB=ON' ]

  if sys.platform.startswith('linux'):
    sysroot_amd64 = DownloadDebianSysroot('amd64', args.skip_checkout)
    sysroot_i386 = DownloadDebianSysroot('i386', args.skip_checkout)
    sysroot_arm = DownloadDebianSysroot('arm', args.skip_checkout)
    sysroot_arm64 = DownloadDebianSysroot('arm64', args.skip_checkout)

    # Add the sysroot to base_cmake_args.
    if platform.machine() == 'aarch64':
      base_cmake_args.append('-DCMAKE_SYSROOT=' + sysroot_arm64)
    else:
      # amd64 is the default toolchain.
      base_cmake_args.append('-DCMAKE_SYSROOT=' + sysroot_amd64)

  if sys.platform == 'win32':
    AddGnuWinToPath()

    base_cmake_args.append('-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded')

    # Require zlib compression.
    zlib_dir = AddZlibToPath()
    cflags.append('-I' + zlib_dir)
    cxxflags.append('-I' + zlib_dir)
    ldflags.append('-LIBPATH:' + zlib_dir)

    # Use rpmalloc. For faster ThinLTO linking.
    rpmalloc_dir = DownloadRPMalloc()
    base_cmake_args.append('-DLLVM_INTEGRATED_CRT_ALLOC=' + rpmalloc_dir)

    # Set a sysroot to make the build more hermetic.
    base_cmake_args.append('-DLLVM_WINSYSROOT="%s"' %
                           os.path.dirname(os.path.dirname(GetWinSDKDir())))

  # Statically link libxml2 to make lld-link not require mt.exe on Windows,
  # and to make sure lld-link output on other platforms is identical to
  # lld-link on Windows (for cross-builds).
  libxml_cmake_args, libxml_cflags = BuildLibXml2()
  base_cmake_args += libxml_cmake_args
  cflags += libxml_cflags
  cxxflags += libxml_cflags

  if args.with_zstd:
    # Statically link zstd to make lld support zstd compression for debug info.
    zstd_cmake_args, zstd_cflags = BuildZStd()
    base_cmake_args += zstd_cmake_args
    cflags += zstd_cflags
    cxxflags += zstd_cflags

  if args.bootstrap:
    print('Building bootstrap compiler')
    if os.path.exists(LLVM_BOOTSTRAP_DIR):
      RmTree(LLVM_BOOTSTRAP_DIR)
    EnsureDirExists(LLVM_BOOTSTRAP_DIR)
    os.chdir(LLVM_BOOTSTRAP_DIR)

    runtimes = []
    if args.pgo or sys.platform == 'darwin':
      # Need libclang_rt.profile for PGO.
      # On macOS, the bootstrap toolchain needs to have compiler-rt because
      # dsymutil's link needs libclang_rt.osx.a. Only the x86_64 osx
      # libraries are needed though, and only libclang_rt (i.e.
      # COMPILER_RT_BUILD_BUILTINS).
      runtimes.append('compiler-rt')

    bootstrap_targets = 'X86'
    if sys.platform == 'darwin':
      # Need ARM and AArch64 for building the ios clang_rt.
      bootstrap_targets += ';ARM;AArch64'
    bootstrap_args = base_cmake_args + goma_cmake_args + [
        '-DLLVM_TARGETS_TO_BUILD=' + bootstrap_targets,
        '-DLLVM_ENABLE_PROJECTS=clang;lld',
        '-DLLVM_ENABLE_RUNTIMES=' + ';'.join(runtimes),
        '-DCMAKE_INSTALL_PREFIX=' + LLVM_BOOTSTRAP_INSTALL_DIR,
        '-DCMAKE_C_FLAGS=' + ' '.join(cflags),
        '-DCMAKE_CXX_FLAGS=' + ' '.join(cxxflags),
        '-DCMAKE_EXE_LINKER_FLAGS=' + ' '.join(ldflags),
        '-DCMAKE_SHARED_LINKER_FLAGS=' + ' '.join(ldflags),
        '-DCMAKE_MODULE_LINKER_FLAGS=' + ' '.join(ldflags),
        # Ignore args.disable_asserts for the bootstrap compiler.
        '-DLLVM_ENABLE_ASSERTIONS=ON',
    ]
    # PGO needs libclang_rt.profile but none of the other compiler-rt stuff.
    bootstrap_args.extend([
        '-D' + f
        for f in compiler_rt_cmake_flags(sanitizers=False, profile=args.pgo)
    ])
    if sys.platform == 'darwin':
      bootstrap_args.extend([
          '-DCOMPILER_RT_ENABLE_IOS=OFF',
          '-DCOMPILER_RT_ENABLE_WATCHOS=OFF',
          '-DCOMPILER_RT_ENABLE_TVOS=OFF',
          ])
      if platform.machine() == 'arm64':
        bootstrap_args.extend(['-DDARWIN_osx_ARCHS=arm64'])
      else:
        bootstrap_args.extend(['-DDARWIN_osx_ARCHS=x86_64'])

    if cc is not None:  bootstrap_args.append('-DCMAKE_C_COMPILER=' + cc)
    if cxx is not None: bootstrap_args.append('-DCMAKE_CXX_COMPILER=' + cxx)
    if lld is not None: bootstrap_args.append('-DCMAKE_LINKER=' + lld)
    RunCommand(['cmake'] + bootstrap_args + [os.path.join(LLVM_DIR, 'llvm')],
               setenv=True)
    RunCommand(['ninja'] + goma_ninja_args, setenv=True)
    if args.run_tests:
      RunCommand(['ninja', 'check-all'], setenv=True)
    RunCommand(['ninja', 'install'], setenv=True)

    if sys.platform == 'win32':
      cc = os.path.join(LLVM_BOOTSTRAP_INSTALL_DIR, 'bin', 'clang-cl.exe')
      cxx = os.path.join(LLVM_BOOTSTRAP_INSTALL_DIR, 'bin', 'clang-cl.exe')
      lld = os.path.join(LLVM_BOOTSTRAP_INSTALL_DIR, 'bin', 'lld-link.exe')
      # CMake has a hard time with backslashes in compiler paths:
      # https://stackoverflow.com/questions/13050827
      cc = cc.replace('\\', '/')
      cxx = cxx.replace('\\', '/')
      lld = lld.replace('\\', '/')
    else:
      cc = os.path.join(LLVM_BOOTSTRAP_INSTALL_DIR, 'bin', 'clang')
      cxx = os.path.join(LLVM_BOOTSTRAP_INSTALL_DIR, 'bin', 'clang++')

    print('Bootstrap compiler installed.')

  if args.pgo:
    print('Building instrumented compiler')
    if os.path.exists(LLVM_INSTRUMENTED_DIR):
      RmTree(LLVM_INSTRUMENTED_DIR)
    EnsureDirExists(LLVM_INSTRUMENTED_DIR)
    os.chdir(LLVM_INSTRUMENTED_DIR)

    instrument_args = base_cmake_args + [
        '-DLLVM_ENABLE_PROJECTS=clang',
        '-DCMAKE_C_FLAGS=' + ' '.join(cflags),
        '-DCMAKE_CXX_FLAGS=' + ' '.join(cxxflags),
        '-DCMAKE_EXE_LINKER_FLAGS=' + ' '.join(ldflags),
        '-DCMAKE_SHARED_LINKER_FLAGS=' + ' '.join(ldflags),
        '-DCMAKE_MODULE_LINKER_FLAGS=' + ' '.join(ldflags),
        # Build with instrumentation.
        '-DLLVM_BUILD_INSTRUMENTED=IR',
    ]
    # Build with the bootstrap compiler.
    if cc is not None:  instrument_args.append('-DCMAKE_C_COMPILER=' + cc)
    if cxx is not None: instrument_args.append('-DCMAKE_CXX_COMPILER=' + cxx)
    if lld is not None: instrument_args.append('-DCMAKE_LINKER=' + lld)

    RunCommand(['cmake'] + instrument_args + [os.path.join(LLVM_DIR, 'llvm')],
               setenv=True)
    RunCommand(['ninja', 'clang'], setenv=True)
    print('Instrumented compiler built.')

    # Train by building some C++ code.
    #
    # pgo_training-1.ii is a preprocessed (on Linux) version of
    # src/third_party/blink/renderer/core/layout/layout_object.cc, selected
    # because it's a large translation unit in Blink, which is normally the
    # slowest part of Chromium to compile. Using this, we get ~20% shorter
    # build times for Linux, Android, and Mac, which is also what we got when
    # training by actually building a target in Chromium. (For comparison, a
    # C++-y "Hello World" program only resulted in 14% faster builds.)
    # See https://crbug.com/966403#c16 for all numbers.
    #
    # Although the training currently only exercises Clang, it does involve LLVM
    # internals, and so LLD also benefits when used for ThinLTO links.
    #
    # NOTE: Tidy uses binaries built with this profile, but doesn't seem to
    # gain much from it. If tidy's execution time becomes a concern, it might
    # be good to investigate that.
    #
    # TODO(hans): Enhance the training, perhaps by including preprocessed code
    # from more platforms, and by doing some linking so that lld can benefit
    # from PGO as well. Perhaps the training could be done asynchronously by
    # dedicated buildbots that upload profiles to the cloud.
    training_source = 'pgo_training-1.ii'
    with open(training_source, 'wb') as f:
      DownloadUrl(CDS_URL + '/' + training_source, f)
    train_cmd = [os.path.join(LLVM_INSTRUMENTED_DIR, 'bin', 'clang++'),
                '-target', 'x86_64-unknown-unknown', '-O3', '-g', '-std=c++14',
                 '-fno-exceptions', '-fno-rtti', '-w', '-c', training_source]
    if sys.platform == 'darwin':
      train_cmd.extend(['-isysroot', isysroot])
    RunCommand(train_cmd, setenv=True)

    # Merge profiles.
    profdata = os.path.join(LLVM_BOOTSTRAP_INSTALL_DIR, 'bin', 'llvm-profdata')
    RunCommand(
        [profdata, 'merge', '-output=' + LLVM_PROFDATA_FILE] +
        glob.glob(os.path.join(LLVM_INSTRUMENTED_DIR, 'profiles', '*.profraw')),
        setenv=True)
    print('Profile generated.')

  deployment_target = '10.12'

  # If building at head, define a macro that plugins can use for #ifdefing
  # out code that builds at head, but not at CLANG_REVISION or vice versa.
  if args.llvm_force_head_revision:
    cflags += ['-DLLVM_FORCE_HEAD_REVISION']
    cxxflags += ['-DLLVM_FORCE_HEAD_REVISION']

  # Build PDBs for archival on Windows.  Don't use RelWithDebInfo since it
  # has different optimization defaults than Release.
  # Also disable stack cookies (/GS-) for performance.
  if sys.platform == 'win32':
    cflags += ['/Zi', '/GS-']
    cxxflags += ['/Zi', '/GS-']
    ldflags += ['/DEBUG', '/OPT:REF', '/OPT:ICF']

  deployment_env = None
  if deployment_target:
    deployment_env = os.environ.copy()
    deployment_env['MACOSX_DEPLOYMENT_TARGET'] = deployment_target

  print('Building final compiler.')

  # Keep static relocations in the executable for BOLT to analyze. Resolve all
  # symbols on program start to allow BOLT's PLT optimization.
  if args.bolt:
    ldflags += ['-Wl,--emit-relocs', '-Wl,-znow']

  chrome_tools = []
  if not args.no_tools:
    default_tools = ['plugins', 'blink_gc_plugin', 'translation_unit']
    chrome_tools = list(set(default_tools + args.extra_tools))
  if cc is not None:  base_cmake_args.append('-DCMAKE_C_COMPILER=' + cc)
  if cxx is not None: base_cmake_args.append('-DCMAKE_CXX_COMPILER=' + cxx)
  if lld is not None: base_cmake_args.append('-DCMAKE_LINKER=' + lld)
  final_install_dir = args.install_dir if args.install_dir else LLVM_BUILD_DIR
  cmake_args = base_cmake_args + [
      '-DCMAKE_C_FLAGS=' + ' '.join(cflags),
      '-DCMAKE_CXX_FLAGS=' + ' '.join(cxxflags),
      '-DCMAKE_EXE_LINKER_FLAGS=' + ' '.join(ldflags),
      '-DCMAKE_SHARED_LINKER_FLAGS=' + ' '.join(ldflags),
      '-DCMAKE_MODULE_LINKER_FLAGS=' + ' '.join(ldflags),
      '-DCMAKE_INSTALL_PREFIX=' + final_install_dir,
  ]
  if not args.no_tools:
    cmake_args.extend([
        '-DLLVM_EXTERNAL_PROJECTS=chrometools',
        '-DLLVM_EXTERNAL_CHROMETOOLS_SOURCE_DIR=' +
        os.path.join(CHROMIUM_DIR, 'tools', 'clang'),
        '-DCHROMIUM_TOOLS=%s' % ';'.join(chrome_tools)
    ])
  if args.pgo:
    cmake_args.append('-DLLVM_PROFDATA_FILE=' + LLVM_PROFDATA_FILE)
  if args.thinlto:
    cmake_args.append('-DLLVM_ENABLE_LTO=Thin')
  if sys.platform == 'win32':
    cmake_args.append('-DLLVM_ENABLE_ZLIB=FORCE_ON')

  if args.build_mac_arm:
    assert platform.machine() != 'arm64', 'build_mac_arm for cross build only'
    cmake_args += [
        '-DCMAKE_OSX_ARCHITECTURES=arm64', '-DCMAKE_SYSTEM_NAME=Darwin'
    ]

  # The default LLVM_DEFAULT_TARGET_TRIPLE depends on the host machine.
  # Set it explicitly to make the build of clang more hermetic, and also to
  # set it to arm64 when cross-building clang for mac/arm.
  if sys.platform == 'darwin':
    if args.build_mac_arm or platform.machine() == 'arm64':
      cmake_args.append('-DLLVM_DEFAULT_TARGET_TRIPLE=arm64-apple-darwin')
    else:
      cmake_args.append('-DLLVM_DEFAULT_TARGET_TRIPLE=x86_64-apple-darwin')
  elif sys.platform.startswith('linux'):
    if platform.machine() == 'aarch64':
      cmake_args.append(
          '-DLLVM_DEFAULT_TARGET_TRIPLE=aarch64-unknown-linux-gnu')
    elif platform.machine() == 'riscv64':
      cmake_args.append(
          '-DLLVM_DEFAULT_TARGET_TRIPLE=riscv64-unknown-linux-gnu')
    elif platform.machine() == 'loongarch64':
      cmake_args.append(
          '-DLLVM_DEFAULT_TARGET_TRIPLE=loongarch64-unknown-linux-gnu')
    else:
      cmake_args.append('-DLLVM_DEFAULT_TARGET_TRIPLE=x86_64-unknown-linux-gnu')
    cmake_args.append('-DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=ON')
  elif sys.platform == 'win32':
    cmake_args.append('-DLLVM_DEFAULT_TARGET_TRIPLE=x86_64-pc-windows-msvc')

  # Map from triple to {
  #   "args": list of CMake vars without '-D' common to builtins and runtimes
  #   "profile": bool # build profile runtime
  #   "sanitizers": bool # build sanitizer runtimes
  # }
  runtimes_triples_args = {}

  if sys.platform.startswith('linux'):
    runtimes_triples_args['i386-unknown-linux-gnu'] = {
        "args": [
            'CMAKE_SYSROOT=%s' % sysroot_i386,
            # TODO(https://crbug.com/1374690): pass proper flags to i386 tests so they compile correctly
            'LLVM_INCLUDE_TESTS=OFF',
        ],
        "profile":
        True,
        "sanitizers":
        True,
    }
    runtimes_triples_args['x86_64-unknown-linux-gnu'] = {
        "args": [
            'CMAKE_SYSROOT=%s' % sysroot_amd64,
        ],
        "profile": True,
        "sanitizers": True,
    }
    # Using "armv7a-unknown-linux-gnueabhihf" confuses the compiler-rt
    # builtins build, since compiler-rt/cmake/builtin-config-ix.cmake
    # doesn't include "armv7a" in its `ARM32` list.
    # TODO(thakis): It seems to work for everything else though, see try
    # results on
    # https://chromium-review.googlesource.com/c/chromium/src/+/3702739/4
    # Maybe it should work for builtins too?
    runtimes_triples_args['armv7-unknown-linux-gnueabihf'] = {
        "args": [
            'CMAKE_SYSROOT=%s' % sysroot_arm,
            # Can't run tests on x86 host.
            'LLVM_INCLUDE_TESTS=OFF',
        ],
        "profile":
        True,
        "sanitizers":
        True,
    }
    runtimes_triples_args['aarch64-unknown-linux-gnu'] = {
        "args": [
            'CMAKE_SYSROOT=%s' % sysroot_arm64,
            # Can't run tests on x86 host.
            'LLVM_INCLUDE_TESTS=OFF',
        ],
        "profile":
        True,
        "sanitizers":
        True,
    }
  elif sys.platform == 'win32':
    sysroot = os.path.dirname(os.path.dirname(GetWinSDKDir()))
    runtimes_triples_args['i386-pc-windows-msvc'] = {
        "args": [
            'LLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF',
            'LLVM_WINSYSROOT="%s"' % sysroot,
        ],
        "profile":
        True,
        "sanitizers":
        False,
    }
    runtimes_triples_args['x86_64-pc-windows-msvc'] = {
        "args": [
            'LLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF',
            'LLVM_WINSYSROOT="%s"' % sysroot,
        ],
        "profile":
        True,
        "sanitizers":
        True,
    }
    runtimes_triples_args['aarch64-pc-windows-msvc'] = {
        "args": [
            'LLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF',
            'LLVM_WINSYSROOT="%s"' % sysroot,
            # Can't run tests on x86 host.
            'LLVM_INCLUDE_TESTS=OFF',
        ],
        "profile":
        True,
        "sanitizers":
        False,
    }
  elif sys.platform == 'darwin':
    # compiler-rt is built for all platforms/arches with a single
    # configuration, we should only specify one target triple. 'default' is
    # specially handled.
    runtimes_triples_args['default'] = {
        "args": [
            'SANITIZER_MIN_OSX_VERSION=' + deployment_target,
            'COMPILER_RT_ENABLE_MACCATALYST=ON',
            'COMPILER_RT_ENABLE_IOS=ON',
            'COMPILER_RT_ENABLE_WATCHOS=OFF',
            'COMPILER_RT_ENABLE_TVOS=OFF',
            'DARWIN_ios_ARCHS=arm64',
            'DARWIN_iossim_ARCHS=arm64;x86_64',
            'DARWIN_osx_ARCHS=arm64;x86_64',
        ],
        "sanitizers":
        True,
        "profile":
        True
    }

  if args.with_android:
    for target_arch in ['aarch64', 'arm', 'i686', 'riscv64', 'x86_64']:
      toolchain_dir = ANDROID_NDK_TOOLCHAIN_DIR
      target_triple = target_arch
      if target_arch == 'arm':
        target_triple = 'armv7'
      api_level = '21'
      if target_arch == 'riscv64':
        api_level = '35'
        toolchain_dir = ANDROID_NDK_CANARY_TOOLCHAIN_DIR
      target_triple += '-linux-android' + api_level
      android_cflags = [
          '--sysroot=%s/sysroot' % toolchain_dir,

          # We don't have an unwinder ready, and don't need it either.
          '--unwindlib=none',
      ]

      if target_arch == 'aarch64':
        # Use PAC/BTI instructions for AArch64
        android_cflags += ['-mbranch-protection=standard']

      android_args = [
          'LLVM_ENABLE_RUNTIMES=compiler-rt',
          # On Android, we want DWARF info for the builtins for unwinding. See
          # crbug.com/1311807.
          'CMAKE_BUILD_TYPE=RelWithDebInfo',
          'CMAKE_C_FLAGS=' + ' '.join(android_cflags),
          'CMAKE_CXX_FLAGS=' + ' '.join(android_cflags),
          'CMAKE_ASM_FLAGS=' + ' '.join(android_cflags),
          'COMPILER_RT_USE_BUILTINS_LIBRARY=ON',
          'SANITIZER_CXX_ABI=libcxxabi',
          'CMAKE_SHARED_LINKER_FLAGS=-Wl,-u__cxa_demangle',
          'ANDROID=1',
          'LLVM_ENABLE_PER_TARGET_RUNTIME_DIR=OFF',
          'LLVM_INCLUDE_TESTS=OFF',
          # This prevents static_asserts from firing in 32-bit builds.
          # TODO: remove once we only support API >=24.
          'ANDROID_NATIVE_API_LEVEL=' + api_level,
      ]
      runtimes_triples_args[target_triple] = {
          "args": android_args,
          "sanitizers": True,
          "profile": True
      }

  if args.with_fuchsia:
    # Fuchsia links against libclang_rt.builtins-<arch>.a instead of libgcc.a.
    for target_arch in ['aarch64', 'x86_64']:
      fuchsia_arch_name = {'aarch64': 'arm64', 'x86_64': 'x64'}[target_arch]
      toolchain_dir = os.path.join(
          FUCHSIA_SDK_DIR, 'arch', fuchsia_arch_name, 'sysroot')
      target_triple = target_arch + '-unknown-fuchsia'
      # Build the Fuchsia profile and asan runtimes.  This is done after the rt
      # builtins have been created because the CMake build runs link checks that
      # require that the builtins already exist to succeed.
      # TODO(thakis): Figure out why this doesn't build with the stage0
      # compiler in arm cross builds.
      build_profile = target_arch == 'x86_64' and not args.build_mac_arm
      # Build the asan runtime only on non-Mac platforms.  Macs are excluded
      # because the asan install changes library RPATHs which CMake only
      # supports on ELF platforms and MacOS uses Mach-O instead of ELF.
      build_sanitizers = build_profile and sys.platform != 'darwin'
      # TODO(thakis): Might have to pass -B here once sysroot contains
      # binaries (e.g. gas for arm64?)
      fuchsia_args = [
          'LLVM_ENABLE_RUNTIMES=compiler-rt',
          'CMAKE_SYSTEM_NAME=Fuchsia',
          'CMAKE_SYSROOT=%s' % toolchain_dir,
          # TODO(thakis|scottmg): Use PER_TARGET_RUNTIME_DIR for all platforms.
          # https://crbug.com/882485.
          'LLVM_ENABLE_PER_TARGET_RUNTIME_DIR=ON',
      ]
      if build_sanitizers:
        fuchsia_args.append('SANITIZER_NO_UNDEFINED_SYMBOLS=OFF')

      runtimes_triples_args[target_triple] = {
          "args": fuchsia_args,
          "sanitizers": build_sanitizers,
          "profile": build_profile
      }

  # Embed MLGO inliner model. If tf_path is not specified, a vpython3 env
  # will be created which contains the necessary source files for compilation.
  # MLGO is only officially supported on linux. This condition is checked at
  # the top of main()
  if args.with_ml_inliner_model:
    if args.with_ml_inliner_model == 'default':
      model_path = ('https://commondatastorage.googleapis.com/'
                    'chromium-browser-clang/tools/mlgo_model2.tgz')
    else:
      model_path = args.with_ml_inliner_model
    if not args.tf_path:
      tf_path = subprocess.check_output(
          ['vpython3', os.path.join(THIS_DIR, 'get_tensorflow.py')],
          universal_newlines=True).rstrip()
    else:
      tf_path = args.tf_path
    print('Embedding MLGO inliner model at %s using Tensorflow at %s' %
          (model_path, tf_path))
    cmake_args += [
        '-DLLVM_INLINER_MODEL_PATH=%s' % model_path,
        '-DTENSORFLOW_AOT_PATH=%s' % tf_path,
        # Disable Regalloc model generation since it is unused
        '-DLLVM_RAEVICT_MODEL_PATH=none'
    ]

  # Convert FOO=BAR CMake flags per triple into
  # -DBUILTINS_$triple_FOO=BAR/-DRUNTIMES_$triple_FOO=BAR and build up
  # -DLLVM_BUILTIN_TARGETS/-DLLVM_RUNTIME_TARGETS.
  all_triples = ''
  for triple in sorted(runtimes_triples_args.keys()):
    all_triples += triple + ';'
    for arg in runtimes_triples_args[triple]["args"]:
      assert not arg.startswith('-')
      # 'default' is specially handled to pass through relevant CMake flags.
      if triple == 'default':
        cmake_args.append('-D' + arg)
      else:
        cmake_args.append('-DRUNTIMES_' + triple + '_' + arg)
        cmake_args.append('-DBUILTINS_' + triple + '_' + arg)
    for arg in compiler_rt_cmake_flags(
        profile=runtimes_triples_args[triple]["profile"],
        sanitizers=runtimes_triples_args[triple]["sanitizers"]):
      # 'default' is specially handled to pass through relevant CMake flags.
      if triple == 'default':
        cmake_args.append('-D' + arg)
      else:
        cmake_args.append('-DRUNTIMES_' + triple + '_' + arg)

  cmake_args.append('-DLLVM_BUILTIN_TARGETS=' + all_triples)
  cmake_args.append('-DLLVM_RUNTIME_TARGETS=' + all_triples)

  # If we're bootstrapping, Goma doesn't know about the bootstrap compiler
  # we're using as the host compiler.
  if not args.bootstrap:
    cmake_args.extend(goma_cmake_args)

  if os.path.exists(LLVM_BUILD_DIR):
    RmTree(LLVM_BUILD_DIR)
  EnsureDirExists(LLVM_BUILD_DIR)
  os.chdir(LLVM_BUILD_DIR)
  RunCommand(['cmake'] + cmake_args + [os.path.join(LLVM_DIR, 'llvm')],
             setenv=True,
             env=deployment_env)
  RunCommand(['ninja'] + goma_ninja_args, setenv=True)

  if chrome_tools:
    # If any Chromium tools were built, install those now.
    RunCommand(['ninja', 'cr-install'], setenv=True)

  if args.bolt:
    print('Performing BOLT post-link optimizations.')
    bolt_profiles_dir = os.path.join(LLVM_BUILD_DIR, 'bolt-profiles')
    os.mkdir(bolt_profiles_dir)

    # Instrument.
    RunCommand([
        'bin/llvm-bolt', 'bin/clang', '-o', 'bin/clang-bolt.inst',
        '-instrument', '--instrumentation-file-append-pid',
        '--instrumentation-file=' +
        os.path.join(bolt_profiles_dir, 'prof.fdata')
    ])
    RunCommand([
        'ln', '-s',
        os.path.join(LLVM_BUILD_DIR, 'bin', 'clang-bolt.inst'),
        os.path.join(LLVM_BUILD_DIR, 'bin', 'clang++-bolt.inst')
    ])

    # Train by building a part of Clang.
    os.mkdir('bolt-training')
    os.chdir('bolt-training')
    bolt_train_cmake_args = base_cmake_args + [
        '-DLLVM_TARGETS_TO_BUILD=X86',
        '-DLLVM_ENABLE_PROJECTS=clang',
        '-DCMAKE_C_FLAGS=' + ' '.join(cflags),
        '-DCMAKE_CXX_FLAGS=' + ' '.join(cxxflags),
        '-DCMAKE_EXE_LINKER_FLAGS=' + ' '.join(ldflags),
        '-DCMAKE_SHARED_LINKER_FLAGS=' + ' '.join(ldflags),
        '-DCMAKE_MODULE_LINKER_FLAGS=' + ' '.join(ldflags),
        '-DCMAKE_C_COMPILER=' +
        os.path.join(LLVM_BUILD_DIR, 'bin/clang-bolt.inst'),
        '-DCMAKE_CXX_COMPILER=' +
        os.path.join(LLVM_BUILD_DIR, 'bin/clang++-bolt.inst'),
        '-DCMAKE_ASM_COMPILER=' +
        os.path.join(LLVM_BUILD_DIR, 'bin/clang-bolt.inst'),
        '-DCMAKE_ASM_COMPILER_ID=Clang',
    ]
    RunCommand(['cmake'] + bolt_train_cmake_args +
               [os.path.join(LLVM_DIR, 'llvm')])
    RunCommand([
        'ninja', 'tools/clang/lib/Sema/CMakeFiles/obj.clangSema.dir/Sema.cpp.o'
    ])
    os.chdir(LLVM_BUILD_DIR)

    # Optimize.
    RunCommand([
        sys.executable,
        os.path.join(LLVM_DIR, 'clang', 'utils', 'perf-training',
                     'perf-helper.py'), 'merge-fdata', 'bin/merge-fdata',
        'merged.fdata', bolt_profiles_dir
    ])
    RunCommand([
        'bin/llvm-bolt', 'bin/clang', '-o', 'bin/clang-bolt.opt', '-data',
        'merged.fdata', '-reorder-blocks=ext-tsp', '-reorder-functions=hfsort+',
        '-split-functions', '-split-all-cold', '-split-eh', '-dyno-stats',
        '-icf=1', '-use-gnu-stack', '-use-old-text'
    ])

    # Overwrite clang, preserving its timestamp so ninja doesn't rebuild it.
    RunCommand(['touch', '-r', 'bin/clang', 'bin/clang-bolt.opt'])
    RunCommand(['mv', 'bin/clang-bolt.opt', 'bin/clang'])

  if not args.build_mac_arm:
    VerifyVersionOfBuiltClangMatchesVERSION()
    VerifyZlibSupport()
  if args.with_zstd:
    VerifyZStdSupport()

  # Run tests.
  if (not args.build_mac_arm and
      (args.run_tests or args.llvm_force_head_revision)):
    RunCommand(['ninja', '-C', LLVM_BUILD_DIR, 'cr-check-all'], setenv=True)

  if not args.build_mac_arm and args.run_tests:
    env = None
    if sys.platform.startswith('linux'):
      env = os.environ.copy()
      lit_excludes = [
          # See SANITIZER_OVERRIDE_INTERCEPTORS above: We disable crypt_r()
          # interception, so its tests can't pass.
          '^SanitizerCommon-(a|l|m|ub|t)san-x86_64-Linux :: Linux/crypt_r.cpp$',
          # fstat and sunrpc tests fail due to sysroot/host mismatches
          # (crbug.com/1459187).
          '^MemorySanitizer-.* f?stat(at)?(64)?.cpp$',
          '^.*Sanitizer-.*sunrpc.*cpp$',
          # sysroot/host glibc version mismatch, crbug.com/1506551
          '^.*Sanitizer.*mallinfo2.cpp$'
      ]
      env['LIT_FILTER_OUT'] = '|'.join(lit_excludes)
    RunCommand(['ninja', '-C', LLVM_BUILD_DIR, 'check-all'],
               env=env,
               setenv=True)
  if args.install_dir:
    RunCommand(['ninja', 'install'], setenv=True)

  WriteStampFile(PACKAGE_VERSION, STAMP_FILE)
  WriteStampFile(PACKAGE_VERSION, FORCE_HEAD_REVISION_FILE)
  print('Clang build was successful.')
  return 0


if __name__ == '__main__':
  sys.exit(main())
