Squashed 'third_party/rules_rust/' content from commit bf59038ca

git-subtree-dir: third_party/rules_rust
git-subtree-split: bf59038cac11798cbaef9f3bf965bad8182b97fa
Signed-off-by: Brian Silverman <bsilver16384@gmail.com>
Change-Id: I5a20e403203d670df467ea97dde9a4ac40339a8d
diff --git a/tools/rust_analyzer/rust_project.rs b/tools/rust_analyzer/rust_project.rs
new file mode 100644
index 0000000..0cc9378
--- /dev/null
+++ b/tools/rust_analyzer/rust_project.rs
@@ -0,0 +1,311 @@
+//! Library for generating rust_project.json files from a `Vec<CrateSpec>`
+//! See official documentation of file format at https://rust-analyzer.github.io/manual.html
+
+use std::collections::{BTreeMap, BTreeSet, HashMap};
+use std::io::ErrorKind;
+use std::path::Path;
+
+use anyhow::anyhow;
+use serde::Serialize;
+
+use crate::aquery::CrateSpec;
+
+/// A `rust-project.json` workspace representation. See
+/// [rust-analyzer documentation][rd] for a thorough description of this interface.
+/// [rd]: https://rust-analyzer.github.io/manual.html#non-cargo-based-projects
+#[derive(Debug, Serialize)]
+pub struct RustProject {
+    /// Path to the directory with *source code* of
+    /// sysroot crates.
+    sysroot_src: Option<String>,
+
+    /// The set of crates comprising the current
+    /// project. Must include all transitive
+    /// dependencies as well as sysroot crate (libstd,
+    /// libcore and such).
+    crates: Vec<Crate>,
+}
+
+/// A `rust-project.json` crate representation. See
+/// [rust-analyzer documentation][rd] for a thorough description of this interface.
+/// [rd]: https://rust-analyzer.github.io/manual.html#non-cargo-based-projects
+#[derive(Debug, Serialize)]
+pub struct Crate {
+    /// A name used in the package's project declaration
+    #[serde(skip_serializing_if = "Option::is_none")]
+    display_name: Option<String>,
+
+    /// Path to the root module of the crate.
+    root_module: String,
+
+    /// Edition of the crate.
+    edition: String,
+
+    /// Dependencies
+    deps: Vec<Dependency>,
+
+    /// Should this crate be treated as a member of current "workspace".
+    #[serde(skip_serializing_if = "Option::is_none")]
+    is_workspace_member: Option<bool>,
+
+    /// Optionally specify the (super)set of `.rs` files comprising this crate.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    source: Option<Source>,
+
+    /// The set of cfgs activated for a given crate, like
+    /// `["unix", "feature=\"foo\"", "feature=\"bar\""]`.
+    cfg: Vec<String>,
+
+    /// Target triple for this Crate.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    target: Option<String>,
+
+    /// Environment variables, used for the `env!` macro
+    #[serde(skip_serializing_if = "Option::is_none")]
+    env: Option<BTreeMap<String, String>>,
+
+    /// Whether the crate is a proc-macro crate.
+    is_proc_macro: bool,
+
+    /// For proc-macro crates, path to compiled proc-macro (.so file).
+    #[serde(skip_serializing_if = "Option::is_none")]
+    proc_macro_dylib_path: Option<String>,
+}
+
+#[derive(Debug, Serialize)]
+pub struct Source {
+    include_dirs: Vec<String>,
+    exclude_dirs: Vec<String>,
+}
+
+#[derive(Debug, Serialize)]
+pub struct Dependency {
+    /// Index of a crate in the `crates` array.
+    #[serde(rename = "crate")]
+    crate_index: usize,
+
+    /// The display name of the crate.
+    name: String,
+}
+
+pub fn generate_rust_project(
+    sysroot_src: &str,
+    crates: &BTreeSet<CrateSpec>,
+) -> anyhow::Result<RustProject> {
+    let mut project = RustProject {
+        sysroot_src: Some(sysroot_src.into()),
+        crates: Vec::new(),
+    };
+
+    let mut unmerged_crates: Vec<&CrateSpec> = crates.iter().collect();
+    let mut skipped_crates: Vec<&CrateSpec> = Vec::new();
+    let mut merged_crates_index: HashMap<String, usize> = HashMap::new();
+
+    while !unmerged_crates.is_empty() {
+        for c in unmerged_crates.iter() {
+            if c.deps
+                .iter()
+                .any(|dep| !merged_crates_index.contains_key(dep))
+            {
+                log::trace!(
+                    "Skipped crate {} because missing deps: {:?}",
+                    &c.crate_id,
+                    c.deps
+                        .iter()
+                        .filter(|dep| !merged_crates_index.contains_key(*dep))
+                        .cloned()
+                        .collect::<Vec<_>>()
+                );
+                skipped_crates.push(c);
+            } else {
+                log::trace!("Merging crate {}", &c.crate_id);
+                merged_crates_index.insert(c.crate_id.clone(), project.crates.len());
+                project.crates.push(Crate {
+                    display_name: Some(c.display_name.clone()),
+                    root_module: c.root_module.clone(),
+                    edition: c.edition.clone(),
+                    deps: c
+                        .deps
+                        .iter()
+                        .map(|dep| {
+                            let crate_index = *merged_crates_index
+                                .get(dep)
+                                .expect("failed to find dependency on second lookup");
+                            let dep_crate = &project.crates[crate_index as usize];
+                            Dependency {
+                                crate_index,
+                                name: dep_crate
+                                    .display_name
+                                    .as_ref()
+                                    .expect("all crates should have display_name")
+                                    .clone(),
+                            }
+                        })
+                        .collect(),
+                    is_workspace_member: Some(c.is_workspace_member),
+                    source: c.source.as_ref().map(|s| Source {
+                        exclude_dirs: s.exclude_dirs.clone(),
+                        include_dirs: s.include_dirs.clone(),
+                    }),
+                    cfg: c.cfg.clone(),
+                    target: Some(c.target.clone()),
+                    env: Some(c.env.clone()),
+                    is_proc_macro: c.proc_macro_dylib_path.is_some(),
+                    proc_macro_dylib_path: c.proc_macro_dylib_path.clone(),
+                });
+            }
+        }
+
+        // This should not happen, but if it does exit to prevent infinite loop.
+        if unmerged_crates.len() == skipped_crates.len() {
+            log::debug!(
+                "Did not make progress on {} unmerged crates. Crates: {:?}",
+                skipped_crates.len(),
+                skipped_crates
+            );
+            return Err(anyhow!(
+                "Failed to make progress on building crate dependency graph"
+            ));
+        }
+        std::mem::swap(&mut unmerged_crates, &mut skipped_crates);
+        skipped_crates.clear();
+    }
+
+    Ok(project)
+}
+
+pub fn write_rust_project(
+    rust_project_path: &Path,
+    execution_root: &Path,
+    rust_project: &RustProject,
+) -> anyhow::Result<()> {
+    let execution_root = execution_root
+        .to_str()
+        .ok_or_else(|| anyhow!("execution_root is not valid UTF-8"))?;
+
+    // Try to remove the existing rust-project.json. It's OK if the file doesn't exist.
+    match std::fs::remove_file(rust_project_path) {
+        Ok(_) => {}
+        Err(err) if err.kind() == ErrorKind::NotFound => {}
+        Err(err) => {
+            return Err(anyhow!(
+                "Unexpected error removing old rust-project.json: {}",
+                err
+            ))
+        }
+    }
+
+    // Render the `rust-project.json` file and replace the exec root
+    // placeholders with the path to the local exec root.
+    let rust_project_content =
+        serde_json::to_string(rust_project)?.replace("__EXEC_ROOT__", execution_root);
+
+    // Write the new rust-project.json file.
+    std::fs::write(rust_project_path, rust_project_content)?;
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use std::collections::BTreeSet;
+
+    use crate::aquery::CrateSpec;
+
+    /// A simple example with a single crate and no dependencies.
+    #[test]
+    fn generate_rust_project_single() {
+        let project = generate_rust_project(
+            "sysroot",
+            &BTreeSet::from([CrateSpec {
+                crate_id: "ID-example".into(),
+                display_name: "example".into(),
+                edition: "2018".into(),
+                root_module: "example/lib.rs".into(),
+                is_workspace_member: true,
+                deps: BTreeSet::new(),
+                proc_macro_dylib_path: None,
+                source: None,
+                cfg: vec!["test".into(), "debug_assertions".into()],
+                env: BTreeMap::new(),
+                target: "x86_64-unknown-linux-gnu".into(),
+                crate_type: "rlib".into(),
+            }]),
+        )
+        .expect("expect success");
+
+        assert_eq!(project.crates.len(), 1);
+        let c = &project.crates[0];
+        assert_eq!(c.display_name, Some("example".into()));
+        assert_eq!(c.root_module, "example/lib.rs");
+        assert_eq!(c.deps.len(), 0);
+    }
+
+    /// An example with a one crate having two dependencies.
+    #[test]
+    fn generate_rust_project_with_deps() {
+        let project = generate_rust_project(
+            "sysroot",
+            &BTreeSet::from([
+                CrateSpec {
+                    crate_id: "ID-example".into(),
+                    display_name: "example".into(),
+                    edition: "2018".into(),
+                    root_module: "example/lib.rs".into(),
+                    is_workspace_member: true,
+                    deps: BTreeSet::from(["ID-dep_a".into(), "ID-dep_b".into()]),
+                    proc_macro_dylib_path: None,
+                    source: None,
+                    cfg: vec!["test".into(), "debug_assertions".into()],
+                    env: BTreeMap::new(),
+                    target: "x86_64-unknown-linux-gnu".into(),
+                    crate_type: "rlib".into(),
+                },
+                CrateSpec {
+                    crate_id: "ID-dep_a".into(),
+                    display_name: "dep_a".into(),
+                    edition: "2018".into(),
+                    root_module: "dep_a/lib.rs".into(),
+                    is_workspace_member: false,
+                    deps: BTreeSet::new(),
+                    proc_macro_dylib_path: None,
+                    source: None,
+                    cfg: vec!["test".into(), "debug_assertions".into()],
+                    env: BTreeMap::new(),
+                    target: "x86_64-unknown-linux-gnu".into(),
+                    crate_type: "rlib".into(),
+                },
+                CrateSpec {
+                    crate_id: "ID-dep_b".into(),
+                    display_name: "dep_b".into(),
+                    edition: "2018".into(),
+                    root_module: "dep_b/lib.rs".into(),
+                    is_workspace_member: false,
+                    deps: BTreeSet::new(),
+                    proc_macro_dylib_path: None,
+                    source: None,
+                    cfg: vec!["test".into(), "debug_assertions".into()],
+                    env: BTreeMap::new(),
+                    target: "x86_64-unknown-linux-gnu".into(),
+                    crate_type: "rlib".into(),
+                },
+            ]),
+        )
+        .expect("expect success");
+
+        assert_eq!(project.crates.len(), 3);
+        // Both dep_a and dep_b should be one of the first two crates.
+        assert!(
+            Some("dep_a".into()) == project.crates[0].display_name
+                || Some("dep_a".into()) == project.crates[1].display_name
+        );
+        assert!(
+            Some("dep_b".into()) == project.crates[0].display_name
+                || Some("dep_b".into()) == project.crates[1].display_name
+        );
+        let c = &project.crates[2];
+        assert_eq!(c.display_name, Some("example".into()));
+    }
+}