blob: 81beecc8a6ae86be359c8922db513a210cbc18f3 [file] [log] [blame] [edit]
#!/usr/bin/env python3.8
#
# 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.
import os
import argparse
import hashlib
import shutil
import re
import sys
import json
import datetime
import functools
import collections
ROOT_PATH = os.path.abspath(__file__ + "/../..")
sys.path += [os.path.join(ROOT_PATH, "third_party", "pytoml")]
import pytoml as toml
CARGO_PACKAGE_CONTENTS = """\
# Copyright %(year)s 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.
# source GN: %(target)s"
[package]
name = "%(package_name)s"
version = "%(version)s"
license = "BSD-3-Clause"
authors = ["rust-fuchsia@fuchsia.com"]
description = "Rust crate for Fuchsia OS"
repository = "https://fuchsia.googlesource.com"
edition = "%(edition)s"
%(bin_or_lib)s
%(is_proc_macro)s
name = "%(crate_name)s"
path = "%(source_root)s"
"""
CARGO_PACKAGE_DEP = """\
[%(dep_type)s.%(crate_name)s]
version = "0.0.1"
path = "%(crate_path)s"
"""
def strip_toolchain(target):
return re.search("[^(]*", target)[0]
def lookup_gn_pkg_name(project, target):
metadata = project.targets[target]
return metadata["output_name"]
def rebase_gn_path(root_path, location, directory=False):
assert location[0:2] == "//"
# remove the prefix //
path = location[2:]
target = os.path.dirname(path) if directory else path
return os.path.join(root_path, target)
class FeatureSpec(object):
def __init__(self, features, default_features):
self.features = features
self.default_features = default_features
class Project(object):
def __init__(self, project_json):
self.targets = project_json
self.patches = None
self.third_party_features = {}
@functools.cached_property
def rust_targets(self):
return {
target: meta
for target, meta in self.targets.items()
if "crate_root" in meta
}
@functools.cached_property
def rust_targets_by_source_root(self):
result = collections.defaultdict(list)
for target, meta in self.rust_targets.items():
source_root = meta["crate_root"]
result[source_root].append(target)
return dict(result)
@functools.cached_property
def reachable_targets(self):
result = set(["//:default"])
pending = ["//:default"]
while pending:
current = pending.pop()
meta = self.targets[current]
for dep_kind in ["deps", "public_deps", "data_deps"]:
for dep in meta.get(dep_kind, []):
if dep not in result:
result.add(dep)
pending.append(dep)
return result
def expand_source_set_or_group(self, target):
"""Returns a list of dependencies if the target is a source_set.
Returns dependencies as a list of strings if the target is a
source_set, or None otherwise.
"""
meta = self.targets[target]
if meta["type"] in ("source_set", "group"):
return meta["deps"]
def find_test_targets(self, source_root):
overlapping_targets = self.rust_targets_by_source_root.get(
source_root, [])
return [
t for t in overlapping_targets
if "--test" in self.targets[t]["rustflags"]
]
def write_toml_file(
fout, metadata, project, target, lookup, root_path, root_build_dir,
gn_cargo_dir):
rust_crates_path = os.path.join(root_path, "third_party/rust_crates")
edition = "2018" if "--edition=2018" in metadata["rustflags"] else "2015"
if metadata["type"] in ["rust_library", "rust_proc_macro",
"static_library"]:
target_type = "[lib]"
else:
if "--test" in metadata["rustflags"]:
target_type = "[[test]]"
else:
target_type = "[[bin]]"
if metadata["type"] == "rust_proc_macro":
is_proc_macro = "proc-macro = true"
else:
is_proc_macro = ""
features = []
feature_pat = re.compile(r"--cfg=feature=\"(.*)\"$")
for flag in metadata["rustflags"]:
match = feature_pat.match(flag)
if match:
features.append(match.group(1))
crate_type = "rlib"
package_name = lookup_gn_pkg_name(project, target)
fout.write(
CARGO_PACKAGE_CONTENTS % {
"target": target,
"package_name": package_name,
"crate_name": metadata["crate_name"],
"version": "0.0.1",
"year": datetime.datetime.now().year,
"bin_or_lib": target_type,
"is_proc_macro": is_proc_macro,
"lib_crate_type": crate_type,
"edition": edition,
"source_root": rebase_gn_path(root_path, metadata["crate_root"]),
"crate_name": metadata["crate_name"],
"rust_crates_path": rust_crates_path,
})
extra_test_deps = set()
if target_type in {"[lib]", "[[bin]]"}:
test_targets = project.find_test_targets(metadata["crate_root"])
# hack to filter to just matching toolchains:
test_targets = [
t for t in test_targets if t.split("(")[1:] == target.split("(")[1:]
]
test_deps = set()
for test_target in test_targets:
test_deps.update(project.targets[test_target]["deps"])
unreachable_test_deps = sorted(
[dep for dep in test_deps if dep not in project.reachable_targets])
if unreachable_test_deps:
fout.write(
"# Note: disabling tests because test deps are not included in the build: %s\n"
% unreachable_test_deps)
fout.write("test = false\n")
elif not test_targets:
fout.write(
"# Note: disabling tests because no test target was found with the same source root\n"
)
fout.write("test = false\n")
else:
fout.write(
"# Note: using extra deps from discovered test target(s): %s\n"
% test_targets)
extra_test_deps = sorted(test_deps - set(metadata["deps"]))
if features:
fout.write("\n[features]\n")
# Filter 'default' feature out to avoid generating a duplicated entry.
features = [x for x in features if x != "default"]
fout.write("default = %s\n" % json.dumps(features))
for feature in features:
fout.write("%s = []\n" % feature)
fout.write("\n[patch.crates-io]\n")
for patch in project.patches:
path = project.patches[patch]["path"]
fout.write(
"%s = { path = \"%s/%s\" }\n" % (patch, rust_crates_path, path))
fout.write("\n")
# collect all dependencies
deps = metadata["deps"][:]
def write_deps(deps, dep_type):
while deps:
dep = deps.pop()
# If a dependency points to a source set or group, expand it into a list
# of its deps, and append them to the deps list. Finally, continue
# to the next item, since a source set itself is not considered a
# dependency for our purposes.
expanded_deps = project.expand_source_set_or_group(dep)
if expanded_deps:
deps.extend(expanded_deps)
continue
# this is a third-party dependency
# TODO remove this when all things use GN. temporary hack?
if "third_party/rust_crates:" in dep:
has_third_party_deps = True
match = re.search("rust_crates:([\w-]*)", dep)
crate_name, version = str(match.group(1)).rsplit("-v", 1)
version = version.replace("_", ".")
feature_spec = project.third_party_features.get(crate_name)
fout.write("[%s.\"%s\"]\n" % (dep_type, crate_name))
fout.write("version = \"%s\"\n" % version)
if feature_spec:
fout.write(
"features = %s\n" % json.dumps(feature_spec.features))
if feature_spec.default_features is False:
fout.write("default-features = false\n")
# this is a in-tree rust target
elif "crate_name" in project.targets[dep]:
crate_name = lookup_gn_pkg_name(project, dep)
output_name = project.targets[dep]["crate_name"]
dep_dir = os.path.join(gn_cargo_dir, str(lookup[dep]))
fout.write(
CARGO_PACKAGE_DEP % {
"dep_type": dep_type,
"crate_path": dep_dir,
"crate_name": crate_name,
})
write_deps(deps, "dependencies")
write_deps(extra_test_deps, "dev-dependencies")
def main():
# TODO(tmandry): Remove all hardcoded paths and replace with args.
parser = argparse.ArgumentParser()
parser.add_argument("--root_build_dir", required=True)
parser.add_argument("--fuchsia_dir", required=True)
parser.add_argument("json_path")
args = parser.parse_args()
json_path = args.json_path
project = None
try:
with open(json_path, "r") as json_file:
project = json.loads(json_file.read())
except IOError:
print("Failed to generate Cargo.toml files")
print("No project.json in the root of your out directory!")
print("Run gn with the --ide=json flag set")
# returns 0 so that CQ doesn't fail if this isn't set properly
return 0
project = Project(project)
root_path = os.path.abspath(args.fuchsia_dir)
root_build_dir = os.path.abspath(args.root_build_dir)
rust_crates_path = os.path.join(root_path, "third_party/rust_crates")
# this will be removed eventually?
with open(rust_crates_path + "/Cargo.toml", "r") as f:
cargo_toml = toml.load(f)
project.patches = cargo_toml["patch"]["crates-io"]
# Map from crate name to FeatureSpec. We don't include the version because we don't directly
# depend on more than one version of the same crate.
def collect_features(deps):
for dep, info in deps.items():
if isinstance(info, str):
continue
project.third_party_features[dep] = FeatureSpec(
info.get("features", []), info.get("default-features", True))
collect_features(cargo_toml["dependencies"])
for target_info in cargo_toml["target"].values():
collect_features(target_info.get("dependencies", {}))
host_binaries = []
target_binaries = []
lookup = {}
for idx, target in enumerate(project.rust_targets):
# hash is the GN target name without the prefixed //
lookup[target] = hashlib.sha1(target[2:].encode("utf-8")).hexdigest()
# remove the priorly generated rust crates
gn_cargo_dir = os.path.join(root_build_dir, "cargo")
shutil.rmtree(gn_cargo_dir, ignore_errors=True)
os.makedirs(gn_cargo_dir)
# Write a stamp file with a predictable name so the build system knows the
# step ran successfully.
with open(os.path.join(gn_cargo_dir, "generate_cargo.stamp"), "w") as f:
f.truncate()
for target in project.rust_targets:
cargo_toml_dir = os.path.join(gn_cargo_dir, str(lookup[target]))
try:
os.makedirs(cargo_toml_dir)
except OSError:
print("Failed to create directory for Cargo: %s" % cargo_toml_dir)
metadata = project.targets[target]
with open(os.path.join(cargo_toml_dir, "Cargo.toml"), "w") as fout:
write_toml_file(
fout, metadata, project, target, lookup, root_path,
root_build_dir, gn_cargo_dir)
return 0
if __name__ == "__main__":
sys.exit(main())