Set up to build Rust code with dependencies

cargo-raze is like gazelle, but for Rust code. It generates BUILD files
for external dependencies.

I'm putting this in a separate change from running it and setting up CI
to do that for ease of telling what's generated vs not.

Change-Id: I87a07255dca475514cae7bdbf9b0b62ce46e2512
Signed-off-by: Brian Silverman <bsilver16384@gmail.com>
diff --git a/tools/rust/BUILD b/tools/rust/BUILD
index e58bb19..fccc3e4 100644
--- a/tools/rust/BUILD
+++ b/tools/rust/BUILD
@@ -119,3 +119,16 @@
     toolchain = ":noop_rust_toolchain_impl",
     toolchain_type = "@rules_rust//rust:toolchain",
 )
+
+'''
+# TODO(Brian): Uncomment in the next change once dependencies exist.
+rust_binary(
+    name = "tweak_cargo_raze_output",
+    srcs = ["tweak_cargo_raze_output.rs"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//third_party/cargo:anyhow",
+        "//third_party/cargo:toml",
+    ],
+)
+'''
diff --git a/tools/rust/tweak_cargo_raze_output.rs b/tools/rust/tweak_cargo_raze_output.rs
new file mode 100644
index 0000000..88a9d0a
--- /dev/null
+++ b/tools/rust/tweak_cargo_raze_output.rs
@@ -0,0 +1,102 @@
+//! A tool that postprocesses cargo-raze output to do what we want.
+//!
+//! Currently this is limited to removing extraneous BUILD files which cargo-raze places in all the
+//! third_party packages we feed to it, which we don't want to use. We're hand-writing BUILD files
+//! for these dependencies, without intending them to be used as separate Bazel workspaces, so it's
+//! easiest for them to all reference the top-level //third_party/cargo package.
+use std::{
+    env, fs,
+    io::{self, ErrorKind},
+    path::Path,
+};
+
+use anyhow::Context;
+use toml::Value;
+
+fn filter_not_found(result: io::Result<()>) -> io::Result<()> {
+    match result {
+        Err(e) if matches!(e.kind(), ErrorKind::NotFound) => Ok(()),
+        r => r,
+    }
+}
+
+fn main() -> anyhow::Result<()> {
+    let argv: Vec<_> = env::args().collect();
+    let workspace_path = Path::new(&argv[1]);
+    let cargo_toml_path = workspace_path.join("Cargo.toml");
+    eprintln!("Loading Cargo.toml from {:?}", cargo_toml_path);
+    let cargo_toml_contents = fs::read(&cargo_toml_path)
+        .with_context(|| format!("Failed to read Cargo.toml: {:?}", cargo_toml_path))?;
+    let cargo_toml_contents = std::str::from_utf8(&cargo_toml_contents).with_context(|| {
+        format!(
+            "Failed to interpret Cargo.toml contents as UTF-8: {:?}",
+            cargo_toml_path
+        )
+    })?;
+    let cargo_toml: Value = cargo_toml_contents
+        .parse()
+        .with_context(|| format!("Failed to parse Cargo.toml contents: {:?}", cargo_toml_path))?;
+
+    let package_aliases_dir = cargo_toml["workspace"]["metadata"]["raze"]["package_aliases_dir"]
+        .as_str()
+        .with_context(|| {
+            format!(
+                "Found non-string package_aliases_dir in Cargo.toml: {:?}",
+                cargo_toml_path
+            )
+        })?;
+
+    let workspace_members = cargo_toml["workspace"]["members"]
+        .as_array()
+        .with_context(|| {
+            format!(
+                "Did not find workspace members in Cargo.toml: {:?}",
+                cargo_toml_path
+            )
+        })?;
+    for member in workspace_members.iter() {
+        let member = member.as_str().with_context(|| {
+            format!(
+                "Found non-string workspace member in Cargo.toml: {:?}",
+                cargo_toml_path
+            )
+        })?;
+
+        // First delete the BUILD file.
+        let member_build = workspace_path
+            .join(member)
+            .join(package_aliases_dir)
+            .join("BUILD.bazel");
+        filter_not_found(fs::remove_file(&member_build)).with_context(|| {
+            format!(
+                "Failed to remove workspace member BUILD.bazel: {:?}",
+                member_build
+            )
+        })?;
+
+        // Then go and delete each folder in reverse order, but only if it's now empty to avoid
+        // overeager deletion. The file we're deleting should be the only thing in these folders,
+        // but if somebody wrote something else for some reason then we don't want to silently
+        // delete it.
+        let mut folder_path = workspace_path.join(member);
+        for d in Path::new(package_aliases_dir)
+            .components()
+            .scan(&mut folder_path, |a, b| {
+                **a = a.join(b);
+                Some(a.clone())
+            })
+            .collect::<Vec<_>>()
+            .iter()
+            .rev()
+        {
+            filter_not_found(fs::remove_dir(d)).with_context(|| {
+                format!(
+                    "Failed to remove workspace member package_aliases directory: {:?}",
+                    d
+                )
+            })?;
+        }
+    }
+    eprintln!("All done");
+    Ok(())
+}