]> git.lizzy.rs Git - rust.git/blob - src/bin/cargo-fmt.rs
Do not propagate io error when dependencies are not found
[rust.git] / src / bin / cargo-fmt.rs
1 // Copyright 2015-2016 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
4 //
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
10
11 // Inspired by Paul Woolcock's cargo-fmt (https://github.com/pwoolcoc/cargo-fmt/)
12
13 #![cfg(not(test))]
14 #![deny(warnings)]
15
16 extern crate getopts;
17 extern crate serde_json as json;
18
19 use std::env;
20 use std::io::{self, Write};
21 use std::path::PathBuf;
22 use std::process::{Command, ExitStatus};
23 use std::str;
24 use std::collections::HashSet;
25 use std::iter::FromIterator;
26
27 use getopts::{Matches, Options};
28 use json::Value;
29
30 fn main() {
31     let exit_status = execute();
32     std::io::stdout().flush().unwrap();
33     std::process::exit(exit_status);
34 }
35
36 fn execute() -> i32 {
37     let success = 0;
38     let failure = 1;
39
40     let mut opts = getopts::Options::new();
41     opts.optflag("h", "help", "show this message");
42     opts.optflag("q", "quiet", "no output printed to stdout");
43     opts.optflag("v", "verbose", "use verbose output");
44     opts.optmulti(
45         "p",
46         "package",
47         "specify package to format (only usable in workspaces)",
48         "<package>",
49     );
50     opts.optflag("", "all", "format all packages (only usable in workspaces)");
51
52     // If there is any invalid argument passed to `cargo fmt`, return without formatting.
53     if let Some(arg) = env::args()
54         .skip(2)
55         .take_while(|a| a != "--")
56         .find(|a| !a.starts_with('-'))
57     {
58         print_usage_to_stderr(&opts, &format!("Invalid argument: `{}`.", arg));
59         return failure;
60     }
61
62     let matches = match opts.parse(env::args().skip(1).take_while(|a| a != "--")) {
63         Ok(m) => m,
64         Err(e) => {
65             print_usage_to_stderr(&opts, &e.to_string());
66             return failure;
67         }
68     };
69
70     let verbosity = match (matches.opt_present("v"), matches.opt_present("q")) {
71         (false, false) => Verbosity::Normal,
72         (false, true) => Verbosity::Quiet,
73         (true, false) => Verbosity::Verbose,
74         (true, true) => {
75             print_usage_to_stderr(&opts, "quiet mode and verbose mode are not compatible");
76             return failure;
77         }
78     };
79
80     if matches.opt_present("h") {
81         print_usage_to_stdout(&opts, "");
82         return success;
83     }
84
85     let workspace_hitlist = WorkspaceHitlist::from_matches(&matches);
86
87     match format_crate(verbosity, &workspace_hitlist) {
88         Err(e) => {
89             print_usage_to_stderr(&opts, &e.to_string());
90             failure
91         }
92         Ok(status) => if status.success() {
93             success
94         } else {
95             status.code().unwrap_or(failure)
96         },
97     }
98 }
99
100 macro_rules! print_usage {
101     ($print:ident, $opts:ident, $reason:expr) => ({
102         let msg = format!("{}\nusage: cargo fmt [options]", $reason);
103         $print!(
104             "{}\nThis utility formats all bin and lib files of the current crate using rustfmt. \
105              Arguments after `--` are passed to rustfmt.",
106             $opts.usage(&msg)
107         );
108     })
109 }
110
111 fn print_usage_to_stdout(opts: &Options, reason: &str) {
112     print_usage!(println, opts, reason);
113 }
114
115 fn print_usage_to_stderr(opts: &Options, reason: &str) {
116     print_usage!(eprintln, opts, reason);
117 }
118
119 #[derive(Debug, Clone, Copy, PartialEq)]
120 pub enum Verbosity {
121     Verbose,
122     Normal,
123     Quiet,
124 }
125
126 fn format_crate(
127     verbosity: Verbosity,
128     workspace_hitlist: &WorkspaceHitlist,
129 ) -> Result<ExitStatus, io::Error> {
130     let targets = get_targets(workspace_hitlist)?;
131
132     // Currently only bin and lib files get formatted
133     let files: Vec<_> = targets
134         .into_iter()
135         .filter(|t| t.kind.should_format())
136         .inspect(|t| if verbosity == Verbosity::Verbose {
137             println!("[{:?}] {:?}", t.kind, t.path)
138         })
139         .map(|t| t.path)
140         .collect();
141
142     format_files(&files, &get_fmt_args(), verbosity)
143 }
144
145 fn get_fmt_args() -> Vec<String> {
146     // All arguments after -- are passed to rustfmt
147     env::args().skip_while(|a| a != "--").skip(1).collect()
148 }
149
150 #[derive(Debug)]
151 enum TargetKind {
152     Lib,         // dylib, staticlib, lib
153     Bin,         // bin
154     Example,     // example file
155     Test,        // test file
156     Bench,       // bench file
157     CustomBuild, // build script
158     ProcMacro,   // a proc macro implementation
159     Other,       // plugin,...
160 }
161
162 impl TargetKind {
163     fn should_format(&self) -> bool {
164         match *self {
165             TargetKind::Lib |
166             TargetKind::Bin |
167             TargetKind::Example |
168             TargetKind::Test |
169             TargetKind::Bench |
170             TargetKind::CustomBuild |
171             TargetKind::ProcMacro => true,
172             _ => false,
173         }
174     }
175 }
176
177 #[derive(Debug)]
178 pub struct Target {
179     path: PathBuf,
180     kind: TargetKind,
181 }
182
183 impl Target {
184     pub fn from_json(json_val: &Value) -> Option<Self> {
185         let jtarget = json_val.as_object()?;
186         let path = PathBuf::from(jtarget.get("src_path")?.as_str()?);
187         let kinds = jtarget.get("kind")?.as_array()?;
188         let kind = match kinds[0].as_str()? {
189             "bin" => TargetKind::Bin,
190             "lib" | "dylib" | "staticlib" | "cdylib" | "rlib" => TargetKind::Lib,
191             "test" => TargetKind::Test,
192             "example" => TargetKind::Example,
193             "bench" => TargetKind::Bench,
194             "custom-build" => TargetKind::CustomBuild,
195             "proc-macro" => TargetKind::ProcMacro,
196             _ => TargetKind::Other,
197         };
198
199         Some(Target {
200             path: path,
201             kind: kind,
202         })
203     }
204 }
205
206 #[derive(Debug, PartialEq, Eq)]
207 pub enum WorkspaceHitlist {
208     All,
209     Some(Vec<String>),
210     None,
211 }
212
213 impl WorkspaceHitlist {
214     pub fn get_some(&self) -> Option<&[String]> {
215         if let WorkspaceHitlist::Some(ref hitlist) = *self {
216             Some(hitlist)
217         } else {
218             None
219         }
220     }
221
222     pub fn from_matches(matches: &Matches) -> WorkspaceHitlist {
223         match (matches.opt_present("all"), matches.opt_present("p")) {
224             (false, false) => WorkspaceHitlist::None,
225             (true, _) => WorkspaceHitlist::All,
226             (false, true) => WorkspaceHitlist::Some(matches.opt_strs("p")),
227         }
228     }
229 }
230
231 fn get_cargo_metadata_from_utf8(v: &[u8]) -> Option<Value> {
232     json::from_str(str::from_utf8(v).ok()?).ok()
233 }
234
235 fn get_json_array_with<'a>(v: &'a Value, key: &str) -> Option<&'a Vec<Value>> {
236     v.as_object()?.get(key)?.as_array()
237 }
238
239 // `cargo metadata --no-deps | jq '.["packages"]'`
240 fn get_packages(v: &[u8]) -> Result<Vec<Value>, io::Error> {
241     let e = io::Error::new(
242         io::ErrorKind::NotFound,
243         String::from("`cargo metadata` returned json without a 'packages' key"),
244     );
245     match get_cargo_metadata_from_utf8(v) {
246         Some(ref json_obj) => get_json_array_with(json_obj, "packages").cloned().ok_or(e),
247         None => Err(e),
248     }
249 }
250
251 fn extract_target_from_package(package: &Value) -> Option<Vec<Target>> {
252     let jtargets = get_json_array_with(package, "targets")?;
253     let mut targets: Vec<Target> = vec![];
254     for jtarget in jtargets {
255         targets.push(Target::from_json(&jtarget)?);
256     }
257     Some(targets)
258 }
259
260 fn filter_packages_with_hitlist<'a>(
261     packages: Vec<Value>,
262     workspace_hitlist: &'a WorkspaceHitlist,
263 ) -> Result<Vec<Value>, &'a String> {
264     if *workspace_hitlist == WorkspaceHitlist::All {
265         return Ok(packages);
266     }
267     let mut hitlist: HashSet<&String> = workspace_hitlist
268         .get_some()
269         .map_or(HashSet::new(), HashSet::from_iter);
270     let members: Vec<Value> = packages
271         .into_iter()
272         .filter(|member| {
273             member
274                 .as_object()
275                 .and_then(|member_obj| {
276                     member_obj
277                         .get("name")
278                         .and_then(Value::as_str)
279                         .map(|member_name| {
280                             hitlist.take(&member_name.to_string()).is_some()
281                         })
282                 })
283                 .unwrap_or(false)
284         })
285         .collect();
286     if hitlist.is_empty() {
287         Ok(members)
288     } else {
289         Err(hitlist.into_iter().next().unwrap())
290     }
291 }
292
293 fn get_dependencies_from_package(package: &Value) -> Option<Vec<PathBuf>> {
294     let jdependencies = get_json_array_with(package, "dependencies")?;
295     let root_path = env::current_dir().ok()?;
296     let mut dependencies: Vec<PathBuf> = vec![];
297     for jdep in jdependencies {
298         let jdependency = jdep.as_object()?;
299         if !jdependency.get("source")?.is_null() {
300             continue;
301         }
302         let name = jdependency.get("name")?.as_str()?;
303         let mut path = root_path.clone();
304         path.push(&name);
305         dependencies.push(path);
306     }
307     Some(dependencies)
308 }
309
310 // Returns a vector of local dependencies under this crate
311 fn get_path_to_local_dependencies(packages: &[Value]) -> Vec<PathBuf> {
312     let mut local_dependencies: Vec<PathBuf> = vec![];
313     for package in packages {
314         if let Some(mut d) = get_dependencies_from_package(package) {
315             local_dependencies.append(&mut d);
316         }
317     }
318     local_dependencies
319 }
320
321 // Returns a vector of all compile targets of a crate
322 fn get_targets(workspace_hitlist: &WorkspaceHitlist) -> Result<Vec<Target>, io::Error> {
323     let output = Command::new("cargo")
324         .args(&["metadata", "--no-deps", "--format-version=1"])
325         .output()?;
326     if output.status.success() {
327         let cur_dir = env::current_dir()?;
328         let mut targets: Vec<Target> = vec![];
329         let packages = get_packages(&output.stdout)?;
330
331         // If we can find any local dependencies, we will try to get targets from those as well.
332         for path in get_path_to_local_dependencies(&packages) {
333             match env::set_current_dir(path) {
334                 Ok(..) => match get_targets(workspace_hitlist) {
335                     Ok(ref mut t) => targets.append(t),
336                     Err(..) => continue,
337                 },
338                 Err(..) => continue,
339             }
340         }
341
342         env::set_current_dir(cur_dir)?;
343         match filter_packages_with_hitlist(packages, workspace_hitlist) {
344             Ok(packages) => {
345                 for package in packages {
346                     if let Some(mut target) = extract_target_from_package(&package) {
347                         targets.append(&mut target);
348                     }
349                 }
350                 Ok(targets)
351             }
352             Err(package) => {
353                 // Mimick cargo of only outputting one <package> spec.
354                 Err(io::Error::new(
355                     io::ErrorKind::InvalidInput,
356                     format!("package `{}` is not a member of the workspace", package),
357                 ))
358             }
359         }
360     } else {
361         Err(io::Error::new(
362             io::ErrorKind::NotFound,
363             str::from_utf8(&output.stderr).unwrap(),
364         ))
365     }
366 }
367
368 fn format_files(
369     files: &[PathBuf],
370     fmt_args: &[String],
371     verbosity: Verbosity,
372 ) -> Result<ExitStatus, io::Error> {
373     let stdout = if verbosity == Verbosity::Quiet {
374         std::process::Stdio::null()
375     } else {
376         std::process::Stdio::inherit()
377     };
378     if verbosity == Verbosity::Verbose {
379         print!("rustfmt");
380         for a in fmt_args {
381             print!(" {}", a);
382         }
383         for f in files {
384             print!(" {}", f.display());
385         }
386         println!("");
387     }
388     let mut command = Command::new("rustfmt")
389         .stdout(stdout)
390         .args(files)
391         .args(fmt_args)
392         .spawn()
393         .map_err(|e| match e.kind() {
394             io::ErrorKind::NotFound => io::Error::new(
395                 io::ErrorKind::Other,
396                 "Could not run rustfmt, please make sure it is in your PATH.",
397             ),
398             _ => e,
399         })?;
400     command.wait()
401 }