aboutsummaryrefslogtreecommitdiff
path: root/crates/project_model
diff options
context:
space:
mode:
Diffstat (limited to 'crates/project_model')
-rw-r--r--crates/project_model/src/build_data.rs206
-rw-r--r--crates/project_model/src/cargo_workspace.rs208
-rw-r--r--crates/project_model/src/lib.rs1
-rw-r--r--crates/project_model/src/workspace.rs22
4 files changed, 236 insertions, 201 deletions
diff --git a/crates/project_model/src/build_data.rs b/crates/project_model/src/build_data.rs
new file mode 100644
index 000000000..cf32995e0
--- /dev/null
+++ b/crates/project_model/src/build_data.rs
@@ -0,0 +1,206 @@
1//! Handles build script specific information
2
3use std::{
4 ffi::OsStr,
5 io::BufReader,
6 path::{Path, PathBuf},
7 process::{Command, Stdio},
8};
9
10use anyhow::Result;
11use cargo_metadata::{BuildScript, Message, Package, PackageId};
12use itertools::Itertools;
13use paths::{AbsPath, AbsPathBuf};
14use rustc_hash::FxHashMap;
15use stdx::JodChild;
16
17use crate::{cfg_flag::CfgFlag, CargoConfig};
18
19#[derive(Debug, Clone, Default)]
20pub(crate) struct BuildDataMap {
21 data: FxHashMap<PackageId, BuildData>,
22}
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
24pub struct BuildData {
25 /// List of config flags defined by this package's build script
26 pub cfgs: Vec<CfgFlag>,
27 /// List of cargo-related environment variables with their value
28 ///
29 /// If the package has a build script which defines environment variables,
30 /// they can also be found here.
31 pub envs: Vec<(String, String)>,
32 /// Directory where a build script might place its output
33 pub out_dir: Option<AbsPathBuf>,
34 /// Path to the proc-macro library file if this package exposes proc-macros
35 pub proc_macro_dylib_path: Option<AbsPathBuf>,
36}
37
38impl BuildDataMap {
39 pub(crate) fn new(
40 cargo_toml: &AbsPath,
41 cargo_features: &CargoConfig,
42 packages: &Vec<Package>,
43 progress: &dyn Fn(String),
44 ) -> Result<BuildDataMap> {
45 let mut cmd = Command::new(toolchain::cargo());
46 cmd.args(&["check", "--workspace", "--message-format=json", "--manifest-path"])
47 .arg(cargo_toml.as_ref());
48
49 // --all-targets includes tests, benches and examples in addition to the
50 // default lib and bins. This is an independent concept from the --targets
51 // flag below.
52 cmd.arg("--all-targets");
53
54 if let Some(target) = &cargo_features.target {
55 cmd.args(&["--target", target]);
56 }
57
58 if cargo_features.all_features {
59 cmd.arg("--all-features");
60 } else {
61 if cargo_features.no_default_features {
62 // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures`
63 // https://github.com/oli-obk/cargo_metadata/issues/79
64 cmd.arg("--no-default-features");
65 }
66 if !cargo_features.features.is_empty() {
67 cmd.arg("--features");
68 cmd.arg(cargo_features.features.join(" "));
69 }
70 }
71
72 cmd.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null());
73
74 let mut child = cmd.spawn().map(JodChild)?;
75 let child_stdout = child.stdout.take().unwrap();
76 let stdout = BufReader::new(child_stdout);
77
78 let mut res = BuildDataMap::default();
79 for message in cargo_metadata::Message::parse_stream(stdout) {
80 if let Ok(message) = message {
81 match message {
82 Message::BuildScriptExecuted(BuildScript {
83 package_id,
84 out_dir,
85 cfgs,
86 env,
87 ..
88 }) => {
89 let cfgs = {
90 let mut acc = Vec::new();
91 for cfg in cfgs {
92 match cfg.parse::<CfgFlag>() {
93 Ok(it) => acc.push(it),
94 Err(err) => {
95 anyhow::bail!("invalid cfg from cargo-metadata: {}", err)
96 }
97 };
98 }
99 acc
100 };
101 let res = res.data.entry(package_id.clone()).or_default();
102 // cargo_metadata crate returns default (empty) path for
103 // older cargos, which is not absolute, so work around that.
104 if out_dir != PathBuf::default() {
105 let out_dir = AbsPathBuf::assert(out_dir);
106 res.out_dir = Some(out_dir);
107 res.cfgs = cfgs;
108 }
109
110 res.envs = env;
111 }
112 Message::CompilerArtifact(message) => {
113 progress(format!("metadata {}", message.target.name));
114
115 if message.target.kind.contains(&"proc-macro".to_string()) {
116 let package_id = message.package_id;
117 // Skip rmeta file
118 if let Some(filename) =
119 message.filenames.iter().find(|name| is_dylib(name))
120 {
121 let filename = AbsPathBuf::assert(filename.clone());
122 let res = res.data.entry(package_id.clone()).or_default();
123 res.proc_macro_dylib_path = Some(filename);
124 }
125 }
126 }
127 Message::CompilerMessage(message) => {
128 progress(message.target.name.clone());
129 }
130 Message::Unknown => (),
131 Message::BuildFinished(_) => {}
132 Message::TextLine(_) => {}
133 }
134 }
135 }
136 res.inject_cargo_env(packages);
137 Ok(res)
138 }
139
140 pub(crate) fn with_cargo_env(packages: &Vec<Package>) -> Self {
141 let mut res = Self::default();
142 res.inject_cargo_env(packages);
143 res
144 }
145
146 pub(crate) fn get(&self, id: &PackageId) -> Option<&BuildData> {
147 self.data.get(id)
148 }
149
150 fn inject_cargo_env(&mut self, packages: &Vec<Package>) {
151 for meta_pkg in packages {
152 let resource = self.data.entry(meta_pkg.id.clone()).or_default();
153 inject_cargo_env(meta_pkg, &mut resource.envs);
154
155 if let Some(out_dir) = &resource.out_dir {
156 // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
157 if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) {
158 resource.envs.push(("OUT_DIR".to_string(), out_dir));
159 }
160 }
161 }
162 }
163}
164
165// FIXME: File a better way to know if it is a dylib
166fn is_dylib(path: &Path) -> bool {
167 match path.extension().and_then(OsStr::to_str).map(|it| it.to_string().to_lowercase()) {
168 None => false,
169 Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"),
170 }
171}
172
173/// Recreates the compile-time environment variables that Cargo sets.
174///
175/// Should be synced with <https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates>
176fn inject_cargo_env(package: &cargo_metadata::Package, env: &mut Vec<(String, String)>) {
177 // FIXME: Missing variables:
178 // CARGO, CARGO_PKG_HOMEPAGE, CARGO_CRATE_NAME, CARGO_BIN_NAME, CARGO_BIN_EXE_<name>
179
180 let mut manifest_dir = package.manifest_path.clone();
181 manifest_dir.pop();
182 if let Some(cargo_manifest_dir) = manifest_dir.to_str() {
183 env.push(("CARGO_MANIFEST_DIR".into(), cargo_manifest_dir.into()));
184 }
185
186 env.push(("CARGO_PKG_VERSION".into(), package.version.to_string()));
187 env.push(("CARGO_PKG_VERSION_MAJOR".into(), package.version.major.to_string()));
188 env.push(("CARGO_PKG_VERSION_MINOR".into(), package.version.minor.to_string()));
189 env.push(("CARGO_PKG_VERSION_PATCH".into(), package.version.patch.to_string()));
190
191 let pre = package.version.pre.iter().map(|id| id.to_string()).format(".");
192 env.push(("CARGO_PKG_VERSION_PRE".into(), pre.to_string()));
193
194 let authors = package.authors.join(";");
195 env.push(("CARGO_PKG_AUTHORS".into(), authors));
196
197 env.push(("CARGO_PKG_NAME".into(), package.name.clone()));
198 env.push(("CARGO_PKG_DESCRIPTION".into(), package.description.clone().unwrap_or_default()));
199 //env.push(("CARGO_PKG_HOMEPAGE".into(), package.homepage.clone().unwrap_or_default()));
200 env.push(("CARGO_PKG_REPOSITORY".into(), package.repository.clone().unwrap_or_default()));
201 env.push(("CARGO_PKG_LICENSE".into(), package.license.clone().unwrap_or_default()));
202
203 let license_file =
204 package.license_file.as_ref().map(|buf| buf.display().to_string()).unwrap_or_default();
205 env.push(("CARGO_PKG_LICENSE_FILE".into(), license_file));
206}
diff --git a/crates/project_model/src/cargo_workspace.rs b/crates/project_model/src/cargo_workspace.rs
index c0ed37fc1..c8a5333c4 100644
--- a/crates/project_model/src/cargo_workspace.rs
+++ b/crates/project_model/src/cargo_workspace.rs
@@ -1,24 +1,15 @@
1//! FIXME: write short doc here 1//! FIXME: write short doc here
2 2
3use std::{ 3use std::{convert::TryInto, ops, process::Command};
4 convert::TryInto,
5 ffi::OsStr,
6 io::BufReader,
7 ops,
8 path::{Path, PathBuf},
9 process::{Command, Stdio},
10};
11 4
12use anyhow::{Context, Result}; 5use anyhow::{Context, Result};
13use base_db::Edition; 6use base_db::Edition;
14use cargo_metadata::{BuildScript, CargoOpt, Message, MetadataCommand, PackageId}; 7use cargo_metadata::{CargoOpt, MetadataCommand};
15use itertools::Itertools;
16use la_arena::{Arena, Idx}; 8use la_arena::{Arena, Idx};
17use paths::{AbsPath, AbsPathBuf}; 9use paths::{AbsPath, AbsPathBuf};
18use rustc_hash::FxHashMap; 10use rustc_hash::FxHashMap;
19use stdx::JodChild;
20 11
21use crate::cfg_flag::CfgFlag; 12use crate::build_data::{BuildData, BuildDataMap};
22use crate::utf8_stdout; 13use crate::utf8_stdout;
23 14
24/// `CargoWorkspace` represents the logical structure of, well, a Cargo 15/// `CargoWorkspace` represents the logical structure of, well, a Cargo
@@ -99,19 +90,12 @@ pub struct PackageData {
99 pub dependencies: Vec<PackageDependency>, 90 pub dependencies: Vec<PackageDependency>,
100 /// Rust edition for this package 91 /// Rust edition for this package
101 pub edition: Edition, 92 pub edition: Edition,
102 /// List of features to activate 93 /// Features provided by the crate, mapped to the features required by that feature.
103 pub features: Vec<String>, 94 pub features: FxHashMap<String, Vec<String>>,
104 /// List of config flags defined by this package's build script 95 /// List of features enabled on this package
105 pub cfgs: Vec<CfgFlag>, 96 pub active_features: Vec<String>,
106 /// List of cargo-related environment variables with their value 97 /// Build script related data for this package
107 /// 98 pub build_data: BuildData,
108 /// If the package has a build script which defines environment variables,
109 /// they can also be found here.
110 pub envs: Vec<(String, String)>,
111 /// Directory where a build script might place its output
112 pub out_dir: Option<AbsPathBuf>,
113 /// Path to the proc-macro library file if this package exposes proc-macros
114 pub proc_macro_dylib_path: Option<AbsPathBuf>,
115} 99}
116 100
117#[derive(Debug, Clone, Eq, PartialEq)] 101#[derive(Debug, Clone, Eq, PartialEq)]
@@ -244,17 +228,11 @@ impl CargoWorkspace {
244 ) 228 )
245 })?; 229 })?;
246 230
247 let mut out_dir_by_id = FxHashMap::default(); 231 let resources = if config.load_out_dirs_from_check {
248 let mut cfgs = FxHashMap::default(); 232 BuildDataMap::new(cargo_toml, config, &meta.packages, progress)?
249 let mut envs = FxHashMap::default(); 233 } else {
250 let mut proc_macro_dylib_paths = FxHashMap::default(); 234 BuildDataMap::with_cargo_env(&meta.packages)
251 if config.load_out_dirs_from_check { 235 };
252 let resources = load_extern_resources(cargo_toml, config, progress)?;
253 out_dir_by_id = resources.out_dirs;
254 cfgs = resources.cfgs;
255 envs = resources.env;
256 proc_macro_dylib_paths = resources.proc_dylib_paths;
257 }
258 236
259 let mut pkg_by_id = FxHashMap::default(); 237 let mut pkg_by_id = FxHashMap::default();
260 let mut packages = Arena::default(); 238 let mut packages = Arena::default();
@@ -265,7 +243,7 @@ impl CargoWorkspace {
265 meta.packages.sort_by(|a, b| a.id.cmp(&b.id)); 243 meta.packages.sort_by(|a, b| a.id.cmp(&b.id));
266 for meta_pkg in meta.packages { 244 for meta_pkg in meta.packages {
267 let id = meta_pkg.id.clone(); 245 let id = meta_pkg.id.clone();
268 inject_cargo_env(&meta_pkg, envs.entry(id).or_default()); 246 let build_data = resources.get(&id).cloned().unwrap_or_default();
269 247
270 let cargo_metadata::Package { id, edition, name, manifest_path, version, .. } = 248 let cargo_metadata::Package { id, edition, name, manifest_path, version, .. } =
271 meta_pkg; 249 meta_pkg;
@@ -281,11 +259,9 @@ impl CargoWorkspace {
281 is_member, 259 is_member,
282 edition, 260 edition,
283 dependencies: Vec::new(), 261 dependencies: Vec::new(),
284 features: Vec::new(), 262 features: meta_pkg.features.into_iter().collect(),
285 cfgs: cfgs.get(&id).cloned().unwrap_or_default(), 263 active_features: Vec::new(),
286 envs: envs.get(&id).cloned().unwrap_or_default(), 264 build_data,
287 out_dir: out_dir_by_id.get(&id).cloned(),
288 proc_macro_dylib_path: proc_macro_dylib_paths.get(&id).cloned(),
289 }); 265 });
290 let pkg_data = &mut packages[pkg]; 266 let pkg_data = &mut packages[pkg];
291 pkg_by_id.insert(id, pkg); 267 pkg_by_id.insert(id, pkg);
@@ -328,7 +304,7 @@ impl CargoWorkspace {
328 let dep = PackageDependency { name: dep_node.name, pkg }; 304 let dep = PackageDependency { name: dep_node.name, pkg };
329 packages[source].dependencies.push(dep); 305 packages[source].dependencies.push(dep);
330 } 306 }
331 packages[source].features.extend(node.features); 307 packages[source].active_features.extend(node.features);
332 } 308 }
333 309
334 let workspace_root = AbsPathBuf::assert(meta.workspace_root); 310 let workspace_root = AbsPathBuf::assert(meta.workspace_root);
@@ -362,149 +338,3 @@ impl CargoWorkspace {
362 self.packages.iter().filter(|(_, v)| v.name == name).count() == 1 338 self.packages.iter().filter(|(_, v)| v.name == name).count() == 1
363 } 339 }
364} 340}
365
366#[derive(Debug, Clone, Default)]
367pub(crate) struct ExternResources {
368 out_dirs: FxHashMap<PackageId, AbsPathBuf>,
369 proc_dylib_paths: FxHashMap<PackageId, AbsPathBuf>,
370 cfgs: FxHashMap<PackageId, Vec<CfgFlag>>,
371 env: FxHashMap<PackageId, Vec<(String, String)>>,
372}
373
374pub(crate) fn load_extern_resources(
375 cargo_toml: &Path,
376 cargo_features: &CargoConfig,
377 progress: &dyn Fn(String),
378) -> Result<ExternResources> {
379 let mut cmd = Command::new(toolchain::cargo());
380 cmd.args(&["check", "--workspace", "--message-format=json", "--manifest-path"]).arg(cargo_toml);
381
382 // --all-targets includes tests, benches and examples in addition to the
383 // default lib and bins. This is an independent concept from the --targets
384 // flag below.
385 cmd.arg("--all-targets");
386
387 if let Some(target) = &cargo_features.target {
388 cmd.args(&["--target", target]);
389 }
390
391 if cargo_features.all_features {
392 cmd.arg("--all-features");
393 } else {
394 if cargo_features.no_default_features {
395 // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures`
396 // https://github.com/oli-obk/cargo_metadata/issues/79
397 cmd.arg("--no-default-features");
398 }
399 if !cargo_features.features.is_empty() {
400 cmd.arg("--features");
401 cmd.arg(cargo_features.features.join(" "));
402 }
403 }
404
405 cmd.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null());
406
407 let mut child = cmd.spawn().map(JodChild)?;
408 let child_stdout = child.stdout.take().unwrap();
409 let stdout = BufReader::new(child_stdout);
410
411 let mut res = ExternResources::default();
412 for message in cargo_metadata::Message::parse_stream(stdout) {
413 if let Ok(message) = message {
414 match message {
415 Message::BuildScriptExecuted(BuildScript {
416 package_id,
417 out_dir,
418 cfgs,
419 env,
420 ..
421 }) => {
422 let cfgs = {
423 let mut acc = Vec::new();
424 for cfg in cfgs {
425 match cfg.parse::<CfgFlag>() {
426 Ok(it) => acc.push(it),
427 Err(err) => {
428 anyhow::bail!("invalid cfg from cargo-metadata: {}", err)
429 }
430 };
431 }
432 acc
433 };
434 // cargo_metadata crate returns default (empty) path for
435 // older cargos, which is not absolute, so work around that.
436 if out_dir != PathBuf::default() {
437 let out_dir = AbsPathBuf::assert(out_dir);
438 res.out_dirs.insert(package_id.clone(), out_dir);
439 res.cfgs.insert(package_id.clone(), cfgs);
440 }
441
442 res.env.insert(package_id, env);
443 }
444 Message::CompilerArtifact(message) => {
445 progress(format!("metadata {}", message.target.name));
446
447 if message.target.kind.contains(&"proc-macro".to_string()) {
448 let package_id = message.package_id;
449 // Skip rmeta file
450 if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name))
451 {
452 let filename = AbsPathBuf::assert(filename.clone());
453 res.proc_dylib_paths.insert(package_id, filename);
454 }
455 }
456 }
457 Message::CompilerMessage(message) => {
458 progress(message.target.name.clone());
459 }
460 Message::Unknown => (),
461 Message::BuildFinished(_) => {}
462 Message::TextLine(_) => {}
463 }
464 }
465 }
466 Ok(res)
467}
468
469// FIXME: File a better way to know if it is a dylib
470fn is_dylib(path: &Path) -> bool {
471 match path.extension().and_then(OsStr::to_str).map(|it| it.to_string().to_lowercase()) {
472 None => false,
473 Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"),
474 }
475}
476
477/// Recreates the compile-time environment variables that Cargo sets.
478///
479/// Should be synced with <https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates>
480fn inject_cargo_env(package: &cargo_metadata::Package, env: &mut Vec<(String, String)>) {
481 // FIXME: Missing variables:
482 // CARGO, CARGO_PKG_HOMEPAGE, CARGO_CRATE_NAME, CARGO_BIN_NAME, CARGO_BIN_EXE_<name>
483
484 let mut manifest_dir = package.manifest_path.clone();
485 manifest_dir.pop();
486 if let Some(cargo_manifest_dir) = manifest_dir.to_str() {
487 env.push(("CARGO_MANIFEST_DIR".into(), cargo_manifest_dir.into()));
488 }
489
490 env.push(("CARGO_PKG_VERSION".into(), package.version.to_string()));
491 env.push(("CARGO_PKG_VERSION_MAJOR".into(), package.version.major.to_string()));
492 env.push(("CARGO_PKG_VERSION_MINOR".into(), package.version.minor.to_string()));
493 env.push(("CARGO_PKG_VERSION_PATCH".into(), package.version.patch.to_string()));
494
495 let pre = package.version.pre.iter().map(|id| id.to_string()).format(".");
496 env.push(("CARGO_PKG_VERSION_PRE".into(), pre.to_string()));
497
498 let authors = package.authors.join(";");
499 env.push(("CARGO_PKG_AUTHORS".into(), authors));
500
501 env.push(("CARGO_PKG_NAME".into(), package.name.clone()));
502 env.push(("CARGO_PKG_DESCRIPTION".into(), package.description.clone().unwrap_or_default()));
503 //env.push(("CARGO_PKG_HOMEPAGE".into(), package.homepage.clone().unwrap_or_default()));
504 env.push(("CARGO_PKG_REPOSITORY".into(), package.repository.clone().unwrap_or_default()));
505 env.push(("CARGO_PKG_LICENSE".into(), package.license.clone().unwrap_or_default()));
506
507 let license_file =
508 package.license_file.as_ref().map(|buf| buf.display().to_string()).unwrap_or_default();
509 env.push(("CARGO_PKG_LICENSE_FILE".into(), license_file));
510}
diff --git a/crates/project_model/src/lib.rs b/crates/project_model/src/lib.rs
index 970a7e140..525c336e6 100644
--- a/crates/project_model/src/lib.rs
+++ b/crates/project_model/src/lib.rs
@@ -6,6 +6,7 @@ mod project_json;
6mod sysroot; 6mod sysroot;
7mod workspace; 7mod workspace;
8mod rustc_cfg; 8mod rustc_cfg;
9mod build_data;
9 10
10use std::{ 11use std::{
11 fs::{read_dir, ReadDir}, 12 fs::{read_dir, ReadDir},
diff --git a/crates/project_model/src/workspace.rs b/crates/project_model/src/workspace.rs
index 8e0481ae9..bc5041e5a 100644
--- a/crates/project_model/src/workspace.rs
+++ b/crates/project_model/src/workspace.rs
@@ -178,7 +178,7 @@ impl ProjectWorkspace {
178 let pkg_root = cargo[pkg].root().to_path_buf(); 178 let pkg_root = cargo[pkg].root().to_path_buf();
179 179
180 let mut include = vec![pkg_root.clone()]; 180 let mut include = vec![pkg_root.clone()];
181 include.extend(cargo[pkg].out_dir.clone()); 181 include.extend(cargo[pkg].build_data.out_dir.clone());
182 182
183 let mut exclude = vec![pkg_root.join(".git")]; 183 let mut exclude = vec![pkg_root.join(".git")];
184 if is_member { 184 if is_member {
@@ -481,26 +481,24 @@ fn add_target_crate_root(
481 let edition = pkg.edition; 481 let edition = pkg.edition;
482 let cfg_options = { 482 let cfg_options = {
483 let mut opts = cfg_options.clone(); 483 let mut opts = cfg_options.clone();
484 for feature in pkg.features.iter() { 484 for feature in pkg.active_features.iter() {
485 opts.insert_key_value("feature".into(), feature.into()); 485 opts.insert_key_value("feature".into(), feature.into());
486 } 486 }
487 opts.extend(pkg.cfgs.iter().cloned()); 487 opts.extend(pkg.build_data.cfgs.iter().cloned());
488 opts 488 opts
489 }; 489 };
490 490
491 let mut env = Env::default(); 491 let mut env = Env::default();
492 for (k, v) in &pkg.envs { 492 for (k, v) in &pkg.build_data.envs {
493 env.set(k, v.clone()); 493 env.set(k, v.clone());
494 } 494 }
495 if let Some(out_dir) = &pkg.out_dir {
496 // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!()
497 if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) {
498 env.set("OUT_DIR", out_dir);
499 }
500 }
501 495
502 let proc_macro = 496 let proc_macro = pkg
503 pkg.proc_macro_dylib_path.as_ref().map(|it| proc_macro_loader(&it)).unwrap_or_default(); 497 .build_data
498 .proc_macro_dylib_path
499 .as_ref()
500 .map(|it| proc_macro_loader(&it))
501 .unwrap_or_default();
504 502
505 let display_name = CrateDisplayName::from_canonical_name(pkg.name.clone()); 503 let display_name = CrateDisplayName::from_canonical_name(pkg.name.clone());
506 let crate_id = crate_graph.add_crate_root( 504 let crate_id = crate_graph.add_crate_root(