blob: d81b2a2b1284d78167fb66fd16a36525abe75e83 [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.
import os
import argparse
import hashlib
import shutil
import re
import sys
import json
import datetime
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 = """\
[dependencies.%(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["targets"]
self.build_settings = project_json["build_settings"]
self.patches = None
self.third_party_features = {}
def rust_targets(self):
for target in self.targets.keys():
if "crate_root" in self.targets[target]:
yield target
def dereference_group(self, target):
"""Dereference proc macro shims.
If the target happens to be a group which just redirects you to a
different target, returns the real target label. Otherwise, returns
target.
"""
meta = self.targets[target]
if meta["type"] == "group":
if len(meta["deps"]) == 1:
dep = meta["deps"][0]
dep_meta = self.targets[dep]
return dep
return target
def expand_source_set(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"] == "source_set":
return meta["deps"]
def write_toml_file(fout, metadata, project, target, lookup):
root_path = project.build_settings["root_path"]
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,
})
if features:
fout.write("\n[features]\n")
# Filter 'default' feature out to avoid generating a duplicated entry.
features = filter(lambda x: x != "default", features)
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"]
while deps:
dep = deps.pop()
# handle proc macro shims:
dep = project.dereference_group(dep)
# If a dependency points to a source set, 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(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("[dependencies.\"%s\"]\n" % 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 = rebase_gn_path(
root_path, project.build_settings["build_dir"] + "cargo/" +
str(lookup[dep]))
fout.write(
CARGO_PACKAGE_DEP % {
"crate_path": dep_dir,
"crate_name": crate_name,
})
def main():
# TODO(tmandry): Remove all hardcoded paths and replace with args.
parser = argparse.ArgumentParser()
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 = project.build_settings["root_path"]
build_dir = project.build_settings["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.iteritems():
if isinstance(info, str) or isinstance(info, unicode):
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"].itervalues():
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 = rebase_gn_path(
root_path, project.build_settings["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()
# And a depfile so GN knows to run this script again when the json file
# changes. Dependencies on the third-party build are tracked within GN.
with open(os.path.join(gn_cargo_dir, "generate_cargo.stamp.d"), "w") as f:
f.write("cargo/generate_cargo.stamp: %s\n" % json_path)
for target in project.rust_targets():
cargo_toml_dir = rebase_gn_path(
root_path, project.build_settings["build_dir"] + "cargo/" +
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(cargo_toml_dir + "/Cargo.toml", "w") as fout:
write_toml_file(fout, metadata, project, target, lookup)
return 0
if __name__ == "__main__":
sys.exit(main())