diff options
author | Michael Benfield <mbenfield@google.com> | 2022-06-21 21:29:50 +0000 |
---|---|---|
committer | Chromeos LUCI <chromeos-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2022-07-20 15:49:32 +0000 |
commit | 197d2f545c0ad3d2e06deab2f1eac7d2b9bce1ee (patch) | |
tree | 51ca7df6424fbcd63ee2bd2d3f19637e24c1cf5c | |
parent | d1360003a478eaf067aa469b8f11eb5aaa599b0a (diff) | |
download | toolchain-utils-197d2f545c0ad3d2e06deab2f1eac7d2b9bce1ee.tar.gz |
rust-analyzer-chromiumos-wrapper: add
This is a wrapper program enabling use of `rust-analyzer` running in the
chroot with an editor outside the chroot.
BUG=b:235120448
TEST=tested with Neovim
Change-Id: I64287071ce6cc26c6848b6fb09743f6df9fac311
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/3715832
Reviewed-by: George Burgess <gbiv@chromium.org>
Commit-Queue: Michael Benfield <mbenfield@google.com>
Tested-by: Michael Benfield <mbenfield@google.com>
Reviewed-by: Michael Benfield <mbenfield@google.com>
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | rust-analyzer-chromiumos-wrapper/Cargo.lock | 147 | ||||
-rw-r--r-- | rust-analyzer-chromiumos-wrapper/Cargo.toml | 17 | ||||
-rw-r--r-- | rust-analyzer-chromiumos-wrapper/README.md | 61 | ||||
-rw-r--r-- | rust-analyzer-chromiumos-wrapper/src/main.rs | 330 |
5 files changed, 556 insertions, 0 deletions
@@ -3,3 +3,4 @@ logs .mypy_cache/ llvm-project-copy/ compiler_wrapper/compiler_wrapper +/rust-analyzer-chromiumos-wrapper/target diff --git a/rust-analyzer-chromiumos-wrapper/Cargo.lock b/rust-analyzer-chromiumos-wrapper/Cargo.lock new file mode 100644 index 00000000..aedf8bfc --- /dev/null +++ b/rust-analyzer-chromiumos-wrapper/Cargo.lock @@ -0,0 +1,147 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "rust-analyzer-chromiumos-wrapper" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "serde_json", + "simplelog", +] + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" + +[[package]] +name = "serde_json" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "simplelog" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dfff04aade74dd495b007c831cd6f4e0cee19c344dd9dc0884c0289b70a786" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/rust-analyzer-chromiumos-wrapper/Cargo.toml b/rust-analyzer-chromiumos-wrapper/Cargo.toml new file mode 100644 index 00000000..91d0f9a9 --- /dev/null +++ b/rust-analyzer-chromiumos-wrapper/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rust-analyzer-chromiumos-wrapper" +version = "0.1.0" +edition = "2021" + +[profile.release] +panic = "abort" + +[dependencies] +anyhow = "1.0" +log = { version = "0.4.17" } +serde_json = "1.0" +simplelog = { version = "0.12.0" } + +[features] +default = ["no_debug_log"] +no_debug_log = ["log/max_level_off", "log/release_max_level_off"] diff --git a/rust-analyzer-chromiumos-wrapper/README.md b/rust-analyzer-chromiumos-wrapper/README.md new file mode 100644 index 00000000..e834ff34 --- /dev/null +++ b/rust-analyzer-chromiumos-wrapper/README.md @@ -0,0 +1,61 @@ +# rust-analyzer-chromiumos-wrapper + +## Intro + +rust-analyzer is an LSP server for the Rust language. It allows editors like +vim, emacs, or VS Code to provide IDE-like features for Rust. + +This program, `rust-analyzer-chromiumos-wrapper`, is a wrapper around +`rust-analyzer`. It exists to translate paths between an instance of +rust-analyzer running inside the chromiumos chroot and a client running outside +the chroot. + +It is of course possible to simply run `rust-analyzer` outside the chroot, but +version mismatch issues may lead to a suboptimal experience. + +It should run outside the chroot. If invoked in a `chromiumos` repo in a +subdirectory of either `chromiumos/src` or `chromiumos/chroot`, it will attempt +to invoke `rust-analyzer` inside the chroot and translate paths. Otherwise, it +will attempt to invoke a `rust-analyzer` outside the chroot and will not +translate paths. + +It supports none of rust-analyzer's command line options, which aren't +necessary for acting as a LSP server anyway. + +## Quickstart + +*Outside* the chroot, install the `rust-analyzer-chromiumos-wrapper` binary: + +``` +cargo install --path /path-to-a-chromiumos-checkout/src/third_party/toolchain-utils/rust-analyzer-chromiumos-wrapper +``` + +Make sure `~/.cargo/bin' is in your PATH, or move/symlink `~/.cargo/bin/rust-analyzer-chromiumos-wrapper` to a location in your PATH. + +Configure your editor to use the binary `rust-analyzer-chromiumos-wrapper` as +`rust-analyzer`. In Neovim, if you're using +[nvim-lspconfig](https://github.com/neovim/nvim-lspconfig), this can be done by +putting the following in your `init.lua`: + +``` +require('lspconfig')['rust_analyzer'].setup { + cmd = {'rust-analyzer-chromiumos-wrapper'}, +} +``` + +This configuration is specific to your editor, but see the +[Rust analyzer manual](https://rust-analyzer.github.io/manual.html) for +more about several different editors. + +Once the above general configuration is set up, you'll need to install +`rust-analyzer` inside each chroot where you want to edit code: +``` +sudo emerge rust-analyzer +``` + +## Misc + +A wrapper isn't necessary for clangd, because clangd supports the option +`--path-mappings` to translate paths. In principle a similar option could be +added to `rust-analyzer`, obviating the need for this wrapper. See this +[issue on github](https://github.com/rust-lang/rust-analyzer/issues/12485). diff --git a/rust-analyzer-chromiumos-wrapper/src/main.rs b/rust-analyzer-chromiumos-wrapper/src/main.rs new file mode 100644 index 00000000..7bc52d26 --- /dev/null +++ b/rust-analyzer-chromiumos-wrapper/src/main.rs @@ -0,0 +1,330 @@ +// Copyright 2022 The ChromiumOS Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::env; +use std::fs::File; +use std::io::{self, BufRead, BufReader, BufWriter, Write}; +use std::os::unix::process::CommandExt; +use std::path::{Path, PathBuf}; +use std::process::{self, Child}; +use std::str::from_utf8; +use std::thread; + +use anyhow::{anyhow, bail, Context, Result}; + +use log::trace; + +use simplelog::{Config, LevelFilter, WriteLogger}; + +use serde_json::{from_slice, to_writer, Value}; + +fn main() -> Result<()> { + if env::args().len() > 1 { + bail!("rust-analyzer-chromiumos-wrapper doesn't support command line arguments"); + } + + init_log()?; + + let d = env::current_dir()?; + let chromiumos_root = match find_chromiumos_root(&d) { + Some(x) => x, + None => { + // It doesn't appear that we're in a chroot. Run the + // regular rust-analyzer. + return Err(process::Command::new("rust-analyzer").exec())?; + } + }; + + let outside_prefix: &'static str = { + let path = chromiumos_root + .to_str() + .ok_or_else(|| anyhow!("Path is not valid UTF-8"))?; + + let mut tmp = format!("file://{}", path); + if Some(&b'/') != tmp.as_bytes().last() { + tmp.push('/'); + } + + // No need to ever free this memory, so let's get a static reference. + Box::leak(tmp.into_boxed_str()) + }; + + trace!("Found chromiumos root {}", outside_prefix); + + let inside_prefix: &'static str = "file:///mnt/host/source/"; + + let cmd = "cros_sdk"; + let args: [&str; 2] = ["--", "rust-analyzer"]; + let mut child = KillOnDrop(run_command(cmd, args)?); + + let mut child_stdin = BufWriter::new(child.0.stdin.take().unwrap()); + let mut child_stdout = BufReader::new(child.0.stdout.take().unwrap()); + + let join_handle = { + thread::spawn(move || { + let mut stdin = io::stdin().lock(); + stream_with_replacement(&mut stdin, &mut child_stdin, outside_prefix, inside_prefix) + .context("Streaming from stdin into rust-analyzer") + }) + }; + + let mut stdout = BufWriter::new(io::stdout().lock()); + stream_with_replacement( + &mut child_stdout, + &mut stdout, + inside_prefix, + outside_prefix, + ) + .context("Streaming from rust-analyzer into stdout")?; + + join_handle.join().unwrap()?; + + let code = child.0.wait().context("Running rust-analyzer")?.code(); + std::process::exit(code.unwrap_or(127)); +} + +fn init_log() -> Result<()> { + if !cfg!(feature = "no_debug_log") { + let filename = env::var("RUST_ANALYZER_CHROMIUMOS_WRAPPER_LOG") + .context("Obtaining RUST_ANALYZER_CHROMIUMOS_WRAPPER_LOG environment variable")?; + let file = File::create(&filename).with_context(|| { + format!( + "Opening log file `{}` (value of RUST_ANALYZER_WRAPPER_LOG)", + filename + ) + })?; + WriteLogger::init(LevelFilter::Trace, Config::default(), file) + .with_context(|| format!("Creating WriteLogger with log file `{}`", filename))?; + } + Ok(()) +} + +#[derive(Debug, Default)] +struct Header { + length: Option<usize>, + other_fields: Vec<u8>, +} + +/// Read the `Content-Length` (if present) into `header.length`, and the text of every other header +/// field into `header.other_fields`. +fn read_header<R: BufRead>(r: &mut R, header: &mut Header) -> Result<()> { + header.length = None; + header.other_fields.clear(); + const CONTENT_LENGTH: &[u8] = b"Content-Length:"; + let slen = CONTENT_LENGTH.len(); + loop { + let index = header.other_fields.len(); + + // HTTP header spec says line endings are supposed to be '\r\n' but recommends + // implementations accept just '\n', so let's not worry whether a '\r' is present. + r.read_until(b'\n', &mut header.other_fields) + .context("Reading a header")?; + + let new_len = header.other_fields.len(); + + if new_len <= index + 2 { + // Either we've just received EOF, or just a newline, indicating end of the header. + return Ok(()); + } + if header + .other_fields + .get(index..index + slen) + .map_or(false, |v| v == CONTENT_LENGTH) + { + let s = from_utf8(&header.other_fields[index + slen..]) + .context("Parsing Content-Length")?; + header.length = Some(s.trim().parse().context("Parsing Content-Length")?); + header.other_fields.truncate(index); + } + } +} + +/// Extend `dest` with `contents`, replacing any occurrence of `pattern` in a json string in +/// `contents` with `replacement`. +fn replace(contents: &[u8], pattern: &str, replacement: &str, dest: &mut Vec<u8>) -> Result<()> { + fn map_value(val: Value, pattern: &str, replacement: &str) -> Value { + match val { + Value::String(s) => + // `s.replace` is very likely doing more work than necessary. Probably we only need + // to look for the pattern at the beginning of the string. + { + Value::String(s.replace(pattern, replacement)) + } + Value::Array(mut v) => { + for val_ref in v.iter_mut() { + let value = std::mem::replace(val_ref, Value::Null); + *val_ref = map_value(value, pattern, replacement); + } + Value::Array(v) + } + Value::Object(mut map) => { + // Surely keys can't be paths. + for val_ref in map.values_mut() { + let value = std::mem::replace(val_ref, Value::Null); + *val_ref = map_value(value, pattern, replacement); + } + Value::Object(map) + } + x => x, + } + } + + let init_val: Value = from_slice(contents).with_context(|| match from_utf8(contents) { + Err(_) => format!( + "JSON parsing content of length {} that's not valid UTF-8", + contents.len() + ), + Ok(s) => format!("JSON parsing content of length {}:\n{}", contents.len(), s), + })?; + let mapped_val = map_value(init_val, pattern, replacement); + to_writer(dest, &mapped_val)?; + Ok(()) +} + +/// Read LSP messages from `r`, replacing each occurrence of `pattern` in a json string in the +/// payload with `replacement`, adjusting the `Content-Length` in the header to match, and writing +/// the result to `w`. +fn stream_with_replacement<R: BufRead, W: Write>( + r: &mut R, + w: &mut W, + pattern: &str, + replacement: &str, +) -> Result<()> { + let mut head = Header::default(); + let mut buf = Vec::with_capacity(1024); + let mut buf2 = Vec::with_capacity(1024); + loop { + read_header(r, &mut head)?; + if head.length.is_none() && head.other_fields.len() == 0 { + // No content in the header means we're apparently done. + return Ok(()); + } + let len = head + .length + .ok_or_else(|| anyhow!("No Content-Length in header"))?; + + trace!("Received header with length {}", head.length.unwrap()); + trace!( + "Received header with contents\n{}", + from_utf8(&head.other_fields)? + ); + + buf.resize(len, 0); + r.read_exact(&mut buf) + .with_context(|| format!("Reading payload expecting size {}", len))?; + + trace!("Received payload\n{}", from_utf8(&buf)?); + + buf2.clear(); + replace(&buf, pattern, replacement, &mut buf2)?; + + trace!("After replacements payload\n{}", from_utf8(&buf2)?); + + write!(w, "Content-Length: {}\r\n", buf2.len())?; + w.write_all(&head.other_fields)?; + w.write_all(&buf2)?; + w.flush()?; + } +} + +fn run_command<'a, I>(cmd: &'a str, args: I) -> Result<process::Child> +where + I: IntoIterator<Item = &'a str>, +{ + Ok(process::Command::new(cmd) + .args(args) + .stdin(process::Stdio::piped()) + .stdout(process::Stdio::piped()) + .spawn()?) +} + +fn find_chromiumos_root(start: &Path) -> Option<PathBuf> { + let mut buf = start.to_path_buf(); + loop { + buf.push(".chroot_lock"); + if buf.exists() { + buf.pop(); + return Some(buf); + } + buf.pop(); + if !buf.pop() { + return None; + } + } +} + +struct KillOnDrop(Child); + +impl Drop for KillOnDrop { + fn drop(&mut self) { + let _ = self.0.kill(); + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn test_stream_with_replacement( + read: &str, + pattern: &str, + replacement: &str, + json_expected: &str, + ) -> Result<()> { + let mut w = Vec::<u8>::with_capacity(read.len()); + stream_with_replacement(&mut read.as_bytes(), &mut w, pattern, replacement)?; + + // serde_json may not format the json output the same as we do, so we can't just compare + // as strings or slices. + + let (w1, w2) = { + let mut split = w.rsplitn(2, |&c| c == b'\n'); + let w2 = split.next().unwrap(); + (split.next().unwrap(), w2) + }; + + assert_eq!( + from_utf8(w1)?, + format!("Content-Length: {}\r\n\r", w2.len()) + ); + + let v1: Value = from_slice(w2)?; + let v2: Value = serde_json::from_str(json_expected)?; + assert_eq!(v1, v2); + + Ok(()) + } + + #[test] + fn test_stream_with_replacement_1() -> Result<()> { + test_stream_with_replacement( + // read + "Content-Length: 93\r\n\r\n{\"somekey\": {\"somepath\": \"XYZXYZabc\",\ + \"anotherpath\": \"somestring\"}, \"anotherkey\": \"XYZXYZdef\"}", + // pattern + "XYZXYZ", + // replacement + "REPLACE", + // json_expected + "{\"somekey\": {\"somepath\": \"REPLACEabc\", \"anotherpath\": \"somestring\"},\ + \"anotherkey\": \"REPLACEdef\"}", + ) + } + + #[test] + fn test_stream_with_replacement_2() -> Result<()> { + test_stream_with_replacement( + // read + "Content-Length: 83\r\n\r\n{\"key0\": \"sometextABCDEF\",\ + \"key1\": {\"key2\": 5, \"key3\": \"moreABCDEFtext\"}, \"key4\": 1}", + // pattern + "ABCDEF", + // replacement + "replacement", + // json_expected + "{\"key0\": \"sometextreplacement\", \"key1\": {\"key2\": 5,\ + \"key3\": \"morereplacementtext\"}, \"key4\": 1}", + ) + } +} |