| #!/usr/bin/env python |
| # Copyright 2017 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| import fileinput |
| import os |
| import platform |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import uuid |
| |
| FUCHSIA_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| |
| sys.path += [os.path.join(FUCHSIA_ROOT, "third_party", "pytoml")] |
| import pytoml |
| sys.path += [os.path.join(FUCHSIA_ROOT, "build", "rust")] |
| import local_crates |
| |
| from check_rust_licenses import check_licenses |
| |
| # The following crates are ignored and their dependencies will not be accounted during vendoring. |
| EXCLUDED_CRATES = [ |
| "topaz/app/xi/modules/xi-core", |
| # llui is disabled because: a) it uses a non-path dependency on fuchsia-zircon, b) it uses a git dependency |
| "garnet/public/rust/crates/fuchsia-llui", |
| # XXX(raggi): rand is disabled from consideration here because it contains a workspace. |
| "third_party/rust-mirrors/rand", |
| "third_party/rust-mirrors/rand/rand-derive", |
| # XXX(raggi): cargo-vendor is excluded because it has a large dependency tree that isn't used/considered at build time. |
| "third_party/rust-mirrors/cargo-vendor", |
| ] |
| |
| NATIVE_LIBS = { |
| # miniz is built inline. the links declaration linsk a lib output by the build.rs. nothing to depend on. |
| "miniz": None, |
| } |
| |
| |
| def get_cargo_bin(): |
| host_os = platform.system() |
| if host_os == "Darwin": |
| platform_dir = "mac-x64" |
| elif host_os == "Linux": |
| platform_dir = "linux-x64" |
| else: |
| raise Exception("Platform not supported: %s" % host_os) |
| return os.path.join(FUCHSIA_ROOT, "buildtools", platform_dir, "rust", |
| "bin", "cargo") |
| |
| |
| def parse_dependencies(lock_path): |
| """Extracts the crate dependency tree from a lockfile.""" |
| result = [] |
| with open(lock_path, "r") as lock_file: |
| content = pytoml.load(lock_file) |
| dep_matcher = re.compile("^([^\s]+)\s([^\s]+)") |
| crates_source = "registry+https://github.com/rust-lang/crates.io-index" |
| for package in content["package"]: |
| name = package["name"] |
| if "source" in package and package["source"].startswith("git"): |
| raise Exception("Found git dependency on %s, " |
| "use an explicit version instead." % name) |
| from_crates_io = ("source" in package and |
| package["source"] == crates_source) |
| deps = [] |
| if "dependencies" in package: |
| for dep in package["dependencies"]: |
| match = dep_matcher.match(dep) |
| if match: |
| deps.append("%s-%s" % (match.group(1), match.group(2))) |
| label = "%s-%s" % (package["name"], package["version"]) |
| result.append({ |
| "name": name, |
| "version": package["version"], |
| "label": label, |
| "deps": deps, |
| "from_crates_io": from_crates_io, |
| }) |
| return result |
| |
| |
| def update_crates(crates): |
| """Adjusts the list of crates and adds more data.""" |
| print("Updating crates data...") |
| result = [] |
| |
| # Account for local crates. |
| for crate in crates: |
| name = crate["name"] |
| version = crate["version"] |
| is_mirror = name in local_crates.RUST_CRATES["mirrors"] |
| is_from_crates_io = crate["from_crates_io"] |
| |
| # Never generate build rules for Fuchsia crates. |
| if name in local_crates.RUST_CRATES["published"]: |
| published = local_crates.RUST_CRATES["published"][name] |
| if published["version"] == version: |
| print("Ignoring published crate '%s'" % crate["label"]) |
| continue |
| |
| if is_mirror: |
| if not is_from_crates_io: |
| # A build rule is needed for this crate. |
| print("Generating build rule for mirror '%s'" % name) |
| crate["path"] = os.path.join(FUCHSIA_ROOT, "third_party", |
| "rust-mirrors", name) |
| result.append(crate) |
| continue |
| |
| if not is_from_crates_io: |
| print("Ignoring local crate '%s'" % name) |
| continue |
| |
| crate["path"] = os.path.join(FUCHSIA_ROOT, "third_party", |
| "rust-crates", "vendor", crate["label"]) |
| result.append(crate) |
| |
| # Map deps to GN deps. |
| source_crates = dict(map(lambda (k, v): ("%s-%s" % (k, v["version"]), |
| v["target"]), |
| local_crates.RUST_CRATES["published"].iteritems())) |
| for crate in result: |
| deps = [] |
| for dep in crate["deps"]: |
| if dep in source_crates: |
| dep_target = source_crates[dep] |
| else: |
| dep_target = ":%s" % dep |
| deps.append(dep_target) |
| crate["deps"] = deps |
| return result |
| |
| |
| def add_native_libraries(crates, vendor_dir): |
| """Returns true if all native libraries could be identified and added to |
| the given crate metadata.""" |
| result = True |
| for crate in crates: |
| config_path = os.path.join(crate["path"], "Cargo.toml") |
| with open(config_path, "r") as config_file: |
| config = pytoml.load(config_file) |
| if "links" in config["package"]: |
| library = config["package"]["links"] |
| if library not in NATIVE_LIBS: |
| print("Unknown native library: %s" % library) |
| result = False |
| continue |
| crate["native_lib"] = library |
| return result |
| |
| |
| def generate_build_file(build_path, crates): |
| """Creates a BUILD.gn file for the given crates.""" |
| crates.sort(key=lambda c: c["label"]) |
| with open(build_path, "w") as build_file: |
| build_file.write("""# Generated by //scripts/update_rust_crates.py. |
| |
| import("//build/rust/rust_info.gni") |
| """) |
| for info in crates: |
| build_file.write(""" |
| rust_info("%s") { |
| name = "%s" |
| """ |
| % (info["label"], info["name"])) |
| if info["deps"]: |
| build_file.write("\n deps = [\n") |
| for dep in info["deps"]: |
| build_file.write(" \"%s\",\n" % dep) |
| build_file.write(" ]\n") |
| if "native_lib" in info: |
| lib = info["native_lib"] |
| if NATIVE_LIBS[lib] is not None: |
| build_file.write(""" |
| native_lib = \"%s\" |
| |
| non_rust_deps = [ |
| \"%s\", |
| ] |
| """ % (lib, NATIVE_LIBS[lib])) |
| build_file.write("}\n") |
| |
| |
| def fix_build_files(crates): |
| """Updates BUILD.gn files with newer versions of third-party crates.""" |
| dep_pattern = re.compile( |
| r"^(\s*)\"//third_party/rust-crates:([a-z\-]+)-(\d[\d\.]+)\",\s*$") |
| not_found = set() |
| for root, dirs, files in os.walk(os.path.join(FUCHSIA_ROOT)): |
| for file in files: |
| _, ext = os.path.splitext(file) |
| if file != "BUILD.gn" and ext != ".gni": |
| continue |
| base = os.path.relpath(root, FUCHSIA_ROOT) |
| if base == "third_party/rust-crates": |
| # The build file defining the crates is up-to-date, thank you |
| # very much. |
| continue |
| path = os.path.join(root, file) |
| for line in fileinput.input(path, inplace=1): |
| match = dep_pattern.match(line) |
| if not match: |
| sys.stdout.write(line) |
| continue |
| crate_name = match.group(2) |
| new_version = next((x["version"] for x in crates |
| if x["name"] == crate_name), None) |
| if not new_version: |
| sys.stdout.write(line) |
| not_found.add(crate_name) |
| continue |
| sys.stdout.write("%s\"//third_party/rust-crates:%s-%s\",\n" % |
| (match.group(1), crate_name, new_version)) |
| if not_found: |
| print("Unable to find new versions for:") |
| for crate in not_found: |
| print(" - %s" % crate) |
| return False |
| return True |
| |
| |
| def call_or_exit(args, dir): |
| if subprocess.call(args, cwd=dir) != 0: |
| raise Exception("Command failed in %s: %s" % (dir, " ".join(args))) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser("Updates third-party Rust crates") |
| parser.add_argument("--cargo-vendor", |
| help="Path to the cargo-vendor command", |
| default=os.path.join(FUCHSIA_ROOT, "out", |
| "cargo-vendor", "debug", |
| "cargo-vendor")) |
| parser.add_argument("--debug", |
| help="Debug mode", |
| action="store_true") |
| args = parser.parse_args() |
| |
| if not os.path.isfile(args.cargo_vendor): |
| print("!!! No cargo-vendor binary at %s !!!" % args.cargo_vendor) |
| print("You might need to run //scripts/build_cargo_vendor.sh first.") |
| return 1 |
| |
| # Use the root of the tree as the working directory. Ideally a temporary |
| # directory would be used, but unfortunately this would break the flow as |
| # the configs used to seed the vendor directory must be under a common |
| # parent directory. |
| base_dir = FUCHSIA_ROOT |
| |
| toml_path = os.path.join(base_dir, "Cargo.toml") |
| lock_path = os.path.join(base_dir, "Cargo.lock") |
| |
| all_configs = local_crates.get_really_all_paths() |
| for path in EXCLUDED_CRATES: |
| all_configs.remove(path) |
| |
| |
| try: |
| print("Downloading dependencies for:") |
| for config in all_configs: |
| print(" - %s" % config) |
| |
| config = { |
| "workspace": { |
| "members": list(all_configs) |
| } |
| } |
| with open(toml_path, "w") as config_file: |
| pytoml.dump(config, config_file) |
| |
| cargo_bin = get_cargo_bin() |
| |
| # Generate Cargo.lock. |
| lockfile_args = [ |
| cargo_bin, |
| "generate-lockfile", |
| ] |
| call_or_exit(lockfile_args, base_dir) |
| |
| crates = parse_dependencies(lock_path) |
| |
| # Populate the vendor directory. |
| vendor_args = [ |
| args.cargo_vendor, |
| "-x", |
| "--sync", |
| lock_path, |
| "--frozen", |
| "--locked", |
| "vendor", |
| ] |
| call_or_exit(vendor_args, base_dir) |
| finally: |
| if not args.debug: |
| os.remove(toml_path) |
| os.remove(lock_path) |
| |
| crates_dir = os.path.join(FUCHSIA_ROOT, "third_party", "rust-crates") |
| vendor_dir = os.path.join(crates_dir, "vendor") |
| shutil.rmtree(vendor_dir) |
| shutil.move(os.path.join(FUCHSIA_ROOT, "vendor"), vendor_dir) |
| |
| crates = update_crates(crates) |
| |
| if not add_native_libraries(crates, vendor_dir): |
| print("Unable to identify all required native libraries.") |
| return 1 |
| |
| build_path = os.path.join(crates_dir, "BUILD.gn") |
| generate_build_file(build_path, crates) |
| |
| print("Verifying licenses...") |
| if not check_licenses(vendor_dir): |
| print("Some licenses are missing!") |
| return 1 |
| |
| update_path = os.path.join(crates_dir, ".vendor-update.stamp") |
| # Write the timestamp file. |
| # This file is necessary in order to trigger rebuilds of Rust artifacts |
| # whenever third-party dependencies are updated. |
| with open(update_path, "w") as update_file: |
| update_file.write("%s\n" % uuid.uuid1()) |
| |
| print("Fixing build files") |
| if not fix_build_files(crates): |
| print("Failed to update build files") |
| return 1 |
| |
| print("Vendor directory updated at %s" % vendor_dir) |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |