blob: 7a12a5a67bc6cab632e36432497b673a6e0e7568 [file] [log] [blame]
#!/usr/bin/env python2.7
#
# Copyright 2020 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.
"""
Converts project.json (an artifact of gn gen --ide=json) into a rust-project.json file
which can be consumed by rust-analyzer (a language server for Rust)
Syntax of rust-project.json is roughly a path, and a list of edges to dependencies which
are marked by their index in the crates list and the name that rustc expects for complilation.
Three mechanisms:
1: Add syroot crates into the rust-project.json. This isn't expressed in the GN build graph so
it needs to be done manually. This step in finished before the others start.
2: Add crates that were expressed in GN to the rust-project.json. Traverse the build graph depth-first.
Cfgs that were marked in the rustflags are properly passed to rust-analyzer.
3: Traverse through dependencies of a rust target which are not rust dependencies. Any non-rust dependency
may contain rust dependencies (ex: a GN group) and these should be added to the dependency edges of
the initial target.
"""
import argparse
import os
import platform
import json
import re
# list of crates in a Rust sysroot
sysroot_crates = [
"std", "core", "alloc", "collections", "libc", "panic_unwind", "proc_macro",
"rustc_unicode", "std_unicode", "test", "alloc_jemalloc", "alloc_system",
"compiler_builtins", "getopts", "panic_unwind", "panic_abort", "unwind",
"build_helper", "rustc_asan", "rustc_lsan", "rustc_msan", "rustc_tsan",
"syntax"
]
sysroot_edition = "2018"
# if compiled with std, these deps are required
# for sysroot crates
std_deps = [
"alloc",
"core",
"panic_abort",
"unwind",
]
def strip_toolchain(target):
""" Remove the toolchain from GN Targets"""
# TODO Should be be removing the toolchain? If we don't rust-analyzer
# still works but the build graph is noisy. We might have per-toolchain
# changes to the target which requires us to have that however.
return re.search("[^(]*", target).group(0)
def extract_cfg_kv(metadata):
""" Extract any key value configs """
kv = {}
if "rustflags" not in metadata:
return kv
rustflags = metadata["rustflags"]
for flag in rustflags:
match = re.search("--cfg=(.*)=(.*)", flag)
if match:
kv[match.group(1)] = match.group(2)
return kv
def extract_cfg_atoms(metadata):
""" Extract any single token configs """
atoms = []
if "rustflags" not in metadata:
return atoms
rustflags = metadata["rustflags"]
for flag in rustflags:
match = re.search("--cfg=([^=]*)$", flag)
if match:
atoms.append(match.group(1))
return atoms
def extract_edition(rustflags):
""" Find the edition from the rustflags field """
for flag in rustflags:
match = re.search("--edition=([0-9]*)$", flag)
if match:
return match.group(1)
class Project(object):
def __init__(self, project_json):
self.targets = project_json['targets']
self.build_settings = project_json['build_settings']
def rust_targets(self):
for target in self.targets.keys():
if "crate_root" in self.targets[target]:
yield target
def rebase_gn_path(self, path):
assert path[0:2] == "//"
root_path = self.build_settings['root_path']
path = path[2:] # remove prefix //
return os.path.join(root_path, path)
def build_dir(self):
root_path = self.build_settings['root_path']
build_dir = self.build_settings['build_dir'][2:]
return os.path.join(root_path, build_dir)
def prebuilt_rust(self):
root_path = self.build_settings['root_path']
host_platform = "%s-%s" % (
platform.system().lower().replace("darwin", "mac"),
{
"x86_64": "x64",
"aarch64": "arm64",
}[platform.machine()],
)
return os.path.join(
root_path, "prebuilt/third_party/rust/%s/" % host_platform)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"project",
help=
"Path to project.json (usually contained within your GN out directory)")
parser.add_argument("--output", help="Output path for rust-project.json")
args = parser.parse_args()
project = None
with open(args.project, 'r') as json_file:
project = json.loads(json_file.read())
project = Project(project)
project_json = {"roots": [], "crates": []}
# Mapping from GN Target without it's toolchain appended to the index in the edge graph.
# Sysroot crates are mapped by their bare name
lookup = {}
# set of targets seen that aren't rust crates
non_rust_seen = set()
def add_sysroot_crate(crate_name):
""" Add as sysroot crate to the lookup table"""
if crate_name in lookup:
return
crate_path = os.path.join(
project.prebuilt_rust(),
"lib/rustlib/src/rust/src/lib%s/lib.rs" % crate_name)
crate = {}
crate["root_module"] = crate_path
crate["edition"] = sysroot_edition
crate["atom_cfgs"] = []
crate["key_value_cfgs"] = {}
crate["crate_id"] = len(project_json["crates"])
crate["deps"] = []
lookup[crate_name] = len(project_json["crates"])
project_json["crates"].append(crate)
if crate_name == "std":
for dependency in std_deps:
if dependency not in lookup:
add_sysroot_crate(dependency)
crate['deps'].append(
{
"crate": lookup[dependency],
"name": dependency
})
if crate_name == "alloc":
dependency = "core"
if dependency not in lookup:
add_sysroot_crate(dependency)
crate['deps'].append(
{
"crate": lookup[dependency],
"name": dependency
})
def add_transitive_crates(target):
""" Traverse through GN groups """
local_crate_lookup = []
metadata = project.targets[target]
for dependency in metadata['deps']:
dependency_metadata = project.targets[dependency]
if "crate_root" in dependency_metadata:
if dependency not in lookup:
add_crate(dependency)
local_crate_lookup += [
(
lookup[strip_toolchain(dependency)],
dependency_metadata['crate_name'])
]
else:
if dependency not in non_rust_seen:
non_rust_seen.add(dependency)
add_transitive_crates(target)
return local_crate_lookup
def add_crate(target):
""" Adds a crate to the lookup if it hasn't been seen already"""
# If we've already seen this target from another toolchain,
# skip it. rust-analyzer doesn't use toolchain info
if strip_toolchain(target) in lookup:
return
crate = {}
metadata = project.targets[target]
crate["root_module"] = project.rebase_gn_path(metadata["crate_root"])
crate["edition"] = extract_edition(metadata["rustflags"])
crate["atom_cfgs"] = extract_cfg_atoms(metadata)
crate["key_value_cfgs"] = extract_cfg_kv(metadata)
crate["crate_id"] = len(project_json['crates'])
crate["deps"] = []
# TODO programatically check for std (or core!)
crate['deps'].append({"crate": lookup["std"], "name": "std"})
lookup[strip_toolchain(target)] = len(project_json['crates'])
project_json["crates"].append(crate)
for dependency in metadata['deps']:
dependency_metadata = project.targets[dependency]
# This is a rust target built by GN
if "crate_name" in dependency_metadata:
if strip_toolchain(dependency) not in lookup:
add_crate(dependency)
crate['deps'].append(
{
"crate": lookup[strip_toolchain(dependency)],
"name": dependency_metadata['crate_name']
})
# This is not a rust target.
# We need to traverse to collect rust dependencies that may not propgate through.
else:
for transitive_dep in add_transitive_crates(dependency):
crate['deps'].append(
{
"crate": transitive_dep[0],
"name": transitive_dep[1]
})
for target in sysroot_crates:
add_sysroot_crate(target)
for target in project.rust_targets():
add_crate(target)
fout = os.path.join(project.build_dir(), "rust-project.json")
# overwrite the default path
if args.output:
fout = args.output
with open(fout, 'w') as f:
json.dump(project_json, f, ensure_ascii=True)
if __name__ == "__main__":
main()