aboutsummaryrefslogtreecommitdiff
path: root/rust-analyzer-chromiumos-wrapper
diff options
context:
space:
mode:
Diffstat (limited to 'rust-analyzer-chromiumos-wrapper')
-rw-r--r--rust-analyzer-chromiumos-wrapper/Cargo.lock147
-rw-r--r--rust-analyzer-chromiumos-wrapper/Cargo.toml17
-rw-r--r--rust-analyzer-chromiumos-wrapper/README.md61
-rw-r--r--rust-analyzer-chromiumos-wrapper/src/main.rs364
4 files changed, 589 insertions, 0 deletions
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..f59af454
--- /dev/null
+++ b/rust-analyzer-chromiumos-wrapper/src/main.rs
@@ -0,0 +1,364 @@
+// 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<()> {
+ let args = env::args().skip(1);
+
+ 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").args(args).exec())?;
+ }
+ };
+
+ let args: Vec<String> = args.collect();
+ if !args.is_empty() {
+ // We've received command line arguments, and there are 3 possibilities:
+ // * We just forward the arguments to rust-analyzer and exit.
+ // * We don't support the arguments, so we bail.
+ // * We still need to do our path translation in the LSP protocol.
+ fn run(args: &[String]) -> Result<()> {
+ return Err(process::Command::new("cros_sdk")
+ .args(["--", "rust-analyzer"])
+ .args(args)
+ .exec())?;
+ }
+
+ if args.iter().any(|x| match x.as_str() {
+ "--version" | "--help" | "-h" | "--print-config-schema" => true,
+ _ => false,
+ }) {
+ // With any of these options rust-analyzer will just print something and exit.
+ return run(&args);
+ }
+
+ if !args[0].starts_with("-") {
+ // It's a subcommand, and seemingly none of these need the path translation
+ // rust-analyzer-chromiumos-wrapper provides.
+ return run(&args);
+ }
+
+ if args.iter().any(|x| x == "--log-file") {
+ bail!("rust-analyzer-chromiums_wrapper doesn't support --log-file");
+ }
+
+ // Otherwise it seems we're probably OK to proceed.
+ }
+
+ init_log()?;
+
+ 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 all_args = ["--", "rust-analyzer"]
+ .into_iter()
+ .chain(args.iter().map(|x| x.as_str()));
+ let mut child = KillOnDrop(run_command(cmd, all_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}",
+ )
+ }
+}