1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use std::{
io::{Read, pipe},
path::PathBuf,
process::{Command, Stdio},
};
/// Run a command with a description for a possible error and append the merged stdout and stderr.
/// The boolean in the returned `Result` is true if the command's exit status is success.
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
let spawn = |mut cmd: Command| {
// NOTE: The closure drops `cmd` which prevents a pipe deadlock.
cmd.stdin(Stdio::null())
.spawn()
.with_context(|| format!("Failed to run the command `{description}`"))
};
let mut handle = if let Some(output) = output {
let (mut reader, writer) = pipe().with_context(|| {
format!("Failed to create a pipe to run the command `{description}``")
})?;
let writer_clone = writer.try_clone().with_context(|| {
format!("Failed to clone the pipe writer for the command `{description}`")
})?;
cmd.stdout(writer_clone).stderr(writer);
let handle = spawn(cmd)?;
reader
.read_to_end(output)
.with_context(|| format!("Failed to read the output of the command `{description}`"))?;
output.push(b'\n');
handle
} else {
cmd.stdout(Stdio::null()).stderr(Stdio::null());
spawn(cmd)?
};
handle
.wait()
.with_context(|| format!("Failed to wait on the command `{description}` to exit"))
.map(|status| status.success())
}
// Parses parts of the output of `cargo metadata`.
#[derive(Deserialize)]
struct CargoMetadata {
target_directory: PathBuf,
}
pub struct CmdRunner {
target_dir: PathBuf,
}
impl CmdRunner {
pub fn build() -> Result<Self> {
// Get the target directory from Cargo.
let metadata_output = Command::new("cargo")
.arg("metadata")
.arg("-q")
.arg("--format-version")
.arg("1")
.arg("--no-deps")
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.output()
.context(CARGO_METADATA_ERR)?;
if !metadata_output.status.success() {
bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?");
}
let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout)
.context(
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
)?;
Ok(Self {
target_dir: metadata.target_directory,
})
}
pub fn cargo<'out>(
&self,
subcommand: &str,
bin_name: &str,
output: Option<&'out mut Vec<u8>>,
) -> CargoSubcommand<'out> {
let mut cmd = Command::new("cargo");
cmd.arg(subcommand).arg("-q").arg("--bin").arg(bin_name);
// A hack to make `cargo run` work when developing Rustlings.
#[cfg(debug_assertions)]
cmd.arg("--manifest-path")
.arg("dev/Cargo.toml")
.arg("--target-dir")
.arg(&self.target_dir);
if output.is_some() {
cmd.arg("--color").arg("always");
}
CargoSubcommand { cmd, output }
}
/// The boolean in the returned `Result` is true if the command's exit status is success.
pub fn run_debug_bin(&self, bin_name: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
// 7 = "/debug/".len()
let mut bin_path =
PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len());
bin_path.push(&self.target_dir);
bin_path.push("debug");
bin_path.push(bin_name);
run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)
}
}
pub struct CargoSubcommand<'out> {
cmd: Command,
output: Option<&'out mut Vec<u8>>,
}
impl CargoSubcommand<'_> {
#[inline]
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = &'arg str>,
{
self.cmd.args(args);
self
}
/// The boolean in the returned `Result` is true if the command's exit status is success.
#[inline]
pub fn run(self, description: &str) -> Result<bool> {
run_cmd(self.cmd, description, self.output)
}
}
const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …`
Did you already install Rust?
Try running `cargo --version` to diagnose the problem.";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_cmd() {
let mut cmd = Command::new("echo");
cmd.arg("Hello");
let mut output = Vec::with_capacity(8);
run_cmd(cmd, "echo …", Some(&mut output)).unwrap();
assert_eq!(output, b"Hello\n\n");
}
}
|