#!/usr/bin/env python3
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
'''Builds the Crubit tools.

Builds the Crubit tools for generating Rust/C++ FFI bindings.

This script must be run after //tools/rust/build_rust.py as it uses the outputs
of that script in the compilation of Crubit. In particular it uses:
- The rust toolchain binaries and libraries in `RUST_TOOLCHAIN_OUT_DIR`.
- In the future (if/when building `rs_bindings_from_cc`) it may also use:
  The LLVM and Clang libraries and headers in `RUST_HOST_LLVM_INSTALL_DIR`.

This script:
- Clones the Crubit repository, checks out a defined revision.
- Builds Crubit's `cc_bindings_from_rs` using Cargo.
- Copies cc_bindings_from_rs into `RUST_TOOLCHAIN_OUT_DIR`.

The `rs_bindings_from_cs` binary is not yet built,
as Cargo builds of `rs_bindings_from_cs` are not yet
officially supported by the Crubit team.
'''

import argparse
import json
import os
import platform
import shutil
import sys
import tempfile
import urllib

from pathlib import Path

# Get variables and helpers from `//tools/clang/scripts/build.py`.
sys.path.append(
    os.path.join(
        os.path.dirname(os.path.abspath(__file__)), '..', 'clang', 'scripts'
    )
)
from build import (
    AddZlibToPath,
    CheckoutGitRepo,
    GetLatestCommit,
    GetLibXml2Dirs,
)

from update_rust import CHROMIUM_DIR, CRUBIT_REVISION, RUST_TOOLCHAIN_OUT_DIR

# Get `RunCargo` from `//tools/crates/run_cargo.py`.
sys.path.append(
    os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'crates')
)
from run_cargo import RunCargo

CRUBIT_GIT = (
    'https://chromium.googlesource.com/external/github.com/google/crubit'
)

CRUBIT_SRC_DIR = os.path.join(
    CHROMIUM_DIR, 'third_party', 'rust-toolchain-intermediate', 'crubit'
)
CC_BINDINGS_FROM_RS_CARGO_TOML_PATH = os.path.join(
    CRUBIT_SRC_DIR,
    "cargo",
    "cc_bindings_from_rs",
    "cc_bindings_from_rs",
    "Cargo.toml",
)

EXE = '.exe' if sys.platform == 'win32' else ''


def GetLatestCrubitCommit():
    """Get the latest commit hash in the Crubit repo."""
    url = CRUBIT_GIT + '/+/refs/heads/upstream/main?format=JSON'
    return GetLatestCommit(url)


def GetCcBindingsFromRsRustFlags():
    # Need to help the runtime linker find the path to
    # `librustc_driver-xxxxxxxxxxxxxxxx.so`.  This mimics how `rustc` is built
    # as seen in
    # https://github.com/rust-lang/rust/blob/b889870082dd0b0e3594bbfbebb4545d54710829/src/bootstrap/src/core/builder/cargo.rs#L285-L306
    # See also https://crbug.com/460482110#comment14 - #comment16
    if sys.platform == 'darwin':
        return [
            "-Zosx-rpath-install-name",
            "-Clink-args=-Wl,-rpath,@loader_path/../lib",
        ]
    elif sys.platform != 'win32':
        return [
            "-Clink-args=-Wl,-z,origin",
            "-Clink-args=-Wl,-rpath,$ORIGIN/../lib",
        ]
    else:
        return []


def GetNativeLibsRustFlags():
    if sys.platform == 'win32':
        # See https://crbug.com/481661885 to learn why adding `zlib.lib` and
        # `libxml2s.lib` paths is required to build `cc_bindings_from_rs` on
        # Windows when using Chromium-built Rust sysroot.
        #
        # Note that some of the calls below may be expensive (e.g. downloading
        # zlib sources and building it) so `GetNativeLibsRustFlags` probably
        # shouldn't be called in incremental builds (e.g. when
        # `--skip-checkout` is present).
        libxml2_lib_path = GetLibXml2Dirs().lib_dir
        zlib_lib_path = AddZlibToPath()
        return [
            f'-Clink-arg=/LIBPATH:{libxml2_lib_path}',
            f'-Clink-arg=/LIBPATH:{zlib_lib_path}',
        ]

    # No native libs needed on other platforms:
    return []


def BuildCrubit(rust_sysroot, out_dir, skip_checkout):
    target_dir = os.path.abspath(os.path.join(out_dir, 'target'))
    release_dir = os.path.join(target_dir, 'release')
    home_dir = os.path.join(target_dir, 'cargo_home')

    print(f'Building cc_bindings_from_rs ...')
    cargo_args = ['build', '--release', '--verbose']
    cargo_args += ['--locked']  # `Cargo.lock` added to Crubit in b/510364826
    cargo_args += ['--bin', 'cc_bindings_from_rs']
    cargo_args += ['--target-dir', target_dir]
    cargo_args += ['--manifest-path', CC_BINDINGS_FROM_RS_CARGO_TOML_PATH]
    extra_rustflags = GetCcBindingsFromRsRustFlags()
    if not skip_checkout:
        extra_rustflags += GetNativeLibsRustFlags()
    cargo_result = RunCargo(rust_sysroot, home_dir, cargo_args, extra_rustflags)
    print(f'Building cc_bindings_from_rs ... done.  Result: {cargo_result}')
    if cargo_result:
        return cargo_result

    print(f'Installing Crubit to {RUST_TOOLCHAIN_OUT_DIR} ...')
    CRUBIT_BINS = ['cc_bindings_from_rs']
    for bin in CRUBIT_BINS:
        bin = bin + EXE
        print(f'    Copying {bin} ...')
        shutil.copy(
            os.path.join(release_dir, bin),
            os.path.join(RUST_TOOLCHAIN_OUT_DIR, 'bin', bin),
        )

    # `crubit_target_dir` below helps ensure that Chromium can use the same
    # `#include` paths as other Crubit clients like google3 - e.g.
    # `#include "third_party/crubit/support/rs_std/slice_ref.h"`.
    print(f'Installing `crubit/support` to {RUST_TOOLCHAIN_OUT_DIR} ...')
    crubit_target_dir = os.path.join(
        RUST_TOOLCHAIN_OUT_DIR, 'lib', 'third_party', 'crubit'
    )
    for item in ["BUILD.gn", "LICENSE", "crubit.gni", "support"]:
        source_path = os.path.join(CRUBIT_SRC_DIR, item)
        target_path = os.path.join(crubit_target_dir, item)
        os.makedirs(os.path.dirname(target_path), exist_ok=True)
        if os.path.isdir(source_path):
            shutil.copytree(source_path, target_path, dirs_exist_ok=True)
        else:
            shutil.copy2(source_path, target_path)

    return 0


def main():
    parser = argparse.ArgumentParser(
        description='Build and package Crubit tools'
    )
    parser.add_argument(
        '--skip-checkout',
        action='store_true',
        help=('skip checking out source code. Useful for trying localchanges'),
    )
    parser.add_argument(
        '--out-dir',
        help='cache artifacts in specified directory instead of a temp dir.',
    )
    parser.add_argument(
        '--debug', action='store_true', help=('build Crubit in debug mode')
    )
    parser.add_argument(
        '--crubit-force-head-revision',
        action='store_true',
        help=(
            'build the most recent commit of crubit '
            'instead of the current pinned version'
        ),
    )
    args = parser.parse_args()

    if args.crubit_force_head_revision:
        crubit_revision = GetLatestCrubitCommit()
    else:
        crubit_revision = CRUBIT_REVISION

    if not args.skip_checkout:
        CheckoutGitRepo("crubit", CRUBIT_GIT, crubit_revision, CRUBIT_SRC_DIR)

    if args.out_dir:
        return BuildCrubit(
            RUST_TOOLCHAIN_OUT_DIR, args.out_dir, args.skip_checkout
        )
    else:
        with tempfile.TemporaryDirectory() as out_dir:
            return BuildCrubit(
                RUST_TOOLCHAIN_OUT_DIR, out_dir, args.skip_checkout
            )


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