Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

175 changes: 96 additions & 79 deletions crates/ra_cargo_watch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use lsp_types::{
};
use std::{
io::{BufRead, BufReader},
path::PathBuf,
process::{Command, Stdio},
path::{Path, PathBuf},
process::{Child, Command, Stdio},
thread::JoinHandle,
time::Instant,
};
Expand Down Expand Up @@ -246,102 +246,119 @@ enum CheckEvent {
End,
}

pub fn run_cargo(
args: &[String],
current_dir: Option<&Path>,
on_message: &mut dyn FnMut(cargo_metadata::Message) -> bool,
) -> Child {
let mut command = Command::new("cargo");
if let Some(current_dir) = current_dir {
command.current_dir(current_dir);
}

let mut child = command
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stdin(Stdio::null())
.spawn()
.expect("couldn't launch cargo");

// We manually read a line at a time, instead of using serde's
// stream deserializers, because the deserializer cannot recover
// from an error, resulting in it getting stuck, because we try to
// be resillient against failures.
//
// Because cargo only outputs one JSON object per line, we can
// simply skip a line if it doesn't parse, which just ignores any
// erroneus output.
let stdout = BufReader::new(child.stdout.take().unwrap());
for line in stdout.lines() {
let line = match line {
Ok(line) => line,
Err(err) => {
log::error!("Couldn't read line from cargo: {}", err);
continue;
}
};

let message = serde_json::from_str::<cargo_metadata::Message>(&line);
let message = match message {
Ok(message) => message,
Err(err) => {
log::error!("Invalid json from cargo check, ignoring ({}): {:?} ", err, line);
continue;
}
};

if !on_message(message) {
break;
}
}

child
}

impl WatchThread {
fn dummy() -> WatchThread {
WatchThread { handle: None, message_recv: never() }
}

fn new(options: &CheckOptions, workspace_root: &PathBuf) -> WatchThread {
fn new(options: &CheckOptions, workspace_root: &Path) -> WatchThread {
let mut args: Vec<String> = vec![
options.command.clone(),
"--workspace".to_string(),
"--message-format=json".to_string(),
"--manifest-path".to_string(),
format!("{}/Cargo.toml", workspace_root.to_string_lossy()),
format!("{}/Cargo.toml", workspace_root.display()),
];
if options.all_targets {
args.push("--all-targets".to_string());
}
args.extend(options.args.iter().cloned());

let (message_send, message_recv) = unbounded();
let enabled = options.enable;
let handle = std::thread::spawn(move || {
if !enabled {
return;
}

let mut command = Command::new("cargo")
.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.stdin(Stdio::null())
.spawn()
.expect("couldn't launch cargo");

// If we trigger an error here, we will do so in the loop instead,
// which will break out of the loop, and continue the shutdown
let _ = message_send.send(CheckEvent::Begin);

// We manually read a line at a time, instead of using serde's
// stream deserializers, because the deserializer cannot recover
// from an error, resulting in it getting stuck, because we try to
// be resillient against failures.
//
// Because cargo only outputs one JSON object per line, we can
// simply skip a line if it doesn't parse, which just ignores any
// erroneus output.
let stdout = BufReader::new(command.stdout.take().unwrap());
for line in stdout.lines() {
let line = match line {
Ok(line) => line,
Err(err) => {
log::error!("Couldn't read line from cargo: {}", err);
continue;
}
};

let message = serde_json::from_str::<cargo_metadata::Message>(&line);
let message = match message {
Ok(message) => message,
Err(err) => {
log::error!(
"Invalid json from cargo check, ignoring ({}): {:?} ",
err,
line
);
continue;
let workspace_root = workspace_root.to_owned();
let handle = if options.enable {
Some(std::thread::spawn(move || {
// If we trigger an error here, we will do so in the loop instead,
// which will break out of the loop, and continue the shutdown
let _ = message_send.send(CheckEvent::Begin);

let mut child = run_cargo(&args, Some(&workspace_root), &mut |message| {
// Skip certain kinds of messages to only spend time on what's useful
match &message {
Message::CompilerArtifact(artifact) if artifact.fresh => return true,
Message::BuildScriptExecuted(_) => return true,
Message::Unknown => return true,
_ => {}
}
};

// Skip certain kinds of messages to only spend time on what's useful
match &message {
Message::CompilerArtifact(artifact) if artifact.fresh => continue,
Message::BuildScriptExecuted(_) => continue,
Message::Unknown => continue,
_ => {}
}

match message_send.send(CheckEvent::Msg(message)) {
Ok(()) => {}
Err(_err) => {
// The send channel was closed, so we want to shutdown
break;
}
}
}

// We can ignore any error here, as we are already in the progress
// of shutting down.
let _ = message_send.send(CheckEvent::End);

// It is okay to ignore the result, as it only errors if the process is already dead
let _ = command.kill();

// Again, we don't care about the exit status so just ignore the result
let _ = command.wait();
});
WatchThread { handle: Some(handle), message_recv }
match message_send.send(CheckEvent::Msg(message)) {
Ok(()) => {}
Err(_err) => {
// The send channel was closed, so we want to shutdown
return false;
}
};

true
});

// We can ignore any error here, as we are already in the progress
// of shutting down.
let _ = message_send.send(CheckEvent::End);

// It is okay to ignore the result, as it only errors if the process is already dead
let _ = child.kill();

// Again, we don't care about the exit status so just ignore the result
let _ = child.wait();
}))
} else {
None
};
WatchThread { handle, message_recv }
}
}

Expand Down
21 changes: 11 additions & 10 deletions crates/ra_db/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
//! actual IO. See `vfs` and `project_model` in the `rust-analyzer` crate for how
//! actual IO is done and lowered to input.

use std::{fmt, ops, str::FromStr};
use std::{
fmt, ops,
path::{Path, PathBuf},
str::FromStr,
};

use ra_cfg::CfgOptions;
use ra_syntax::SmolStr;
Expand Down Expand Up @@ -144,7 +148,7 @@ pub struct Env {
// crate. We store a map to allow remap it to ExternSourceId
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct ExternSource {
extern_paths: FxHashMap<String, ExternSourceId>,
extern_paths: FxHashMap<PathBuf, ExternSourceId>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -294,13 +298,10 @@ impl Env {
}

impl ExternSource {
pub fn extern_path(&self, path: &str) -> Option<(ExternSourceId, RelativePathBuf)> {
pub fn extern_path(&self, path: impl AsRef<Path>) -> Option<(ExternSourceId, RelativePathBuf)> {
let path = path.as_ref();
self.extern_paths.iter().find_map(|(root_path, id)| {
if path.starts_with(root_path) {
let mut rel_path = &path[root_path.len()..];
if rel_path.starts_with("/") {
rel_path = &rel_path[1..];
}
if let Ok(rel_path) = path.strip_prefix(root_path) {
let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
Some((id.clone(), rel_path))
} else {
Expand All @@ -309,8 +310,8 @@ impl ExternSource {
})
}

pub fn set_extern_path(&mut self, root_path: &str, root: ExternSourceId) {
self.extern_paths.insert(root_path.to_owned(), root);
pub fn set_extern_path(&mut self, root_path: &Path, root: ExternSourceId) {
self.extern_paths.insert(root_path.to_path_buf(), root);
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/ra_project_model/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ cargo_metadata = "0.9.1"
ra_arena = { path = "../ra_arena" }
ra_db = { path = "../ra_db" }
ra_cfg = { path = "../ra_cfg" }
ra_cargo_watch = { path = "../ra_cargo_watch" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the right direction of the dependency should be. It would be cool if there were exactly one crate that invokes cargo/rust. Perhaps we need to introduce a ra_toolchain crate... But this is mostly unrelated to the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had the same exact consideration. With VFS changes, we can keep it contained to ra_cargo_watch, but we need a good way to add roots and extern source on-the-go.

The dream scenario would be cargo metadata providing the needed information, but in previous issues the cargo folks don't seem to keen on that.


serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.48"
Expand Down
67 changes: 65 additions & 2 deletions crates/ra_project_model/src/cargo_workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use cargo_metadata::{CargoOpt, MetadataCommand};
use cargo_metadata::{CargoOpt, Message, MetadataCommand, PackageId};
use ra_arena::{impl_arena_id, Arena, RawId};
use ra_cargo_watch::run_cargo;
use ra_db::Edition;
use rustc_hash::FxHashMap;
use serde::Deserialize;
Expand Down Expand Up @@ -35,11 +36,19 @@ pub struct CargoFeatures {
/// List of features to activate.
/// This will be ignored if `cargo_all_features` is true.
pub features: Vec<String>,

/// Runs cargo check on launch to figure out the correct values of OUT_DIR
pub load_out_dirs_from_check: bool,
}

impl Default for CargoFeatures {
fn default() -> Self {
CargoFeatures { no_default_features: false, all_features: true, features: Vec::new() }
CargoFeatures {
no_default_features: false,
all_features: true,
features: Vec::new(),
load_out_dirs_from_check: false,
}
}
}

Expand All @@ -60,6 +69,7 @@ struct PackageData {
dependencies: Vec<PackageDependency>,
edition: Edition,
features: Vec<String>,
out_dir: Option<PathBuf>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -131,6 +141,9 @@ impl Package {
) -> impl Iterator<Item = &'a PackageDependency> + 'a {
ws.packages[self].dependencies.iter()
}
pub fn out_dir(self, ws: &CargoWorkspace) -> Option<&Path> {
ws.packages[self].out_dir.as_ref().map(PathBuf::as_path)
}
}

impl Target {
Expand Down Expand Up @@ -173,6 +186,12 @@ impl CargoWorkspace {
let meta = meta.exec().with_context(|| {
format!("Failed to run `cargo metadata --manifest-path {}`", cargo_toml.display())
})?;

let mut out_dir_by_id = FxHashMap::default();
if cargo_features.load_out_dirs_from_check {
out_dir_by_id = load_out_dirs(cargo_toml, cargo_features);
}

let mut pkg_by_id = FxHashMap::default();
let mut packages = Arena::default();
let mut targets = Arena::default();
Expand All @@ -193,6 +212,7 @@ impl CargoWorkspace {
edition,
dependencies: Vec::new(),
features: Vec::new(),
out_dir: out_dir_by_id.get(&id).cloned(),
});
let pkg_data = &mut packages[pkg];
pkg_by_id.insert(id, pkg);
Expand Down Expand Up @@ -252,3 +272,46 @@ impl CargoWorkspace {
&self.workspace_root
}
}

pub fn load_out_dirs(
cargo_toml: &Path,
cargo_features: &CargoFeatures,
) -> FxHashMap<PackageId, PathBuf> {
let mut args: Vec<String> = vec![
"check".to_string(),
"--message-format=json".to_string(),
"--manifest-path".to_string(),
format!("{}", cargo_toml.display()),
];

if cargo_features.all_features {
args.push("--all-features".to_string());
} else if cargo_features.no_default_features {
// FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures`
// https://github.com/oli-obk/cargo_metadata/issues/79
args.push("--no-default-features".to_string());
} else if !cargo_features.features.is_empty() {
for feature in &cargo_features.features {
args.push(feature.clone());
}
}

let mut res = FxHashMap::default();
let mut child = run_cargo(&args, cargo_toml.parent(), &mut |message| {
match message {
Message::BuildScriptExecuted(message) => {
let package_id = message.package_id;
let out_dir = message.out_dir;
res.insert(package_id, out_dir);
}

Message::CompilerArtifact(_) => (),
Message::CompilerMessage(_) => (),
Message::Unknown => (),
}
true
});

let _ = child.wait();
res
}
1 change: 1 addition & 0 deletions crates/ra_project_model/src/json_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct Crate {
pub(crate) deps: Vec<Dep>,
pub(crate) atom_cfgs: FxHashSet<String>,
pub(crate) key_value_cfgs: FxHashMap<String, String>,
pub(crate) out_dir: Option<PathBuf>,
}

#[derive(Clone, Copy, Debug, Deserialize)]
Expand Down
Loading