1 // Copyright 2016 Mozilla Foundation
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 use crate::errors::*;
16 use clap::{App, AppSettings, Arg};
17 use std::env;
18 use std::ffi::OsString;
19 use std::path::PathBuf;
20 use which::which_in;
21
22 arg_enum! {
23 #[derive(Debug)]
24 #[allow(non_camel_case_types)]
25 pub enum StatsFormat {
26 text,
27 json
28 }
29 }
30
31 /// A specific command to run.
32 pub enum Command {
33 /// Show cache statistics and exit.
34 ShowStats(StatsFormat),
35 /// Run background server.
36 InternalStartServer,
37 /// Start background server as a subprocess.
38 StartServer,
39 /// Stop background server.
40 StopServer,
41 /// Zero cache statistics and exit.
42 ZeroStats,
43 /// Show the status of the distributed client.
44 DistStatus,
45 /// Perform a login to authenticate for distributed compilation.
46 DistAuth,
47 /// Package a toolchain for distributed compilation (executable, out)
48 PackageToolchain(PathBuf, PathBuf),
49 /// Run a compiler command.
50 Compile {
51 /// The binary to execute.
52 exe: OsString,
53 /// The commandline arguments to pass to `exe`.
54 cmdline: Vec<OsString>,
55 /// The directory in which to execute the command.
56 cwd: PathBuf,
57 /// The environment variables to use for execution.
58 env_vars: Vec<(OsString, OsString)>,
59 },
60 }
61
62 /// Get the `App` used for argument parsing.
get_app<'a, 'b>() -> App<'a, 'b>63 pub fn get_app<'a, 'b>() -> App<'a, 'b> {
64 App::new(env!("CARGO_PKG_NAME"))
65 .version(env!("CARGO_PKG_VERSION"))
66 .setting(AppSettings::TrailingVarArg)
67 .after_help(concat!(
68 "Enabled features:\n",
69 " S3: ", cfg!(feature = "s3"), "\n",
70 " Redis: ", cfg!(feature = "redis"), "\n",
71 " Memcached: ", cfg!(feature = "memcached"), "\n",
72 " GCS: ", cfg!(feature = "gcs"), "\n",
73 " Azure: ", cfg!(feature = "azure"), "\n")
74 )
75 .args_from_usage(
76 "-s --show-stats 'show cache statistics'
77 --start-server 'start background server'
78 --stop-server 'stop background server'
79 -z, --zero-stats 'zero statistics counters'
80 --dist-auth 'authenticate for distributed compilation'
81 --dist-status 'show status of the distributed client'"
82 )
83 .arg(Arg::from_usage("--package-toolchain <executable> <out> 'package toolchain for distributed compilation'")
84 .required(false))
85 .arg(Arg::from_usage("--stats-format 'set output format of statistics'")
86 .possible_values(&StatsFormat::variants())
87 .default_value("text"))
88 .arg(
89 Arg::with_name("cmd")
90 .multiple(true)
91 .use_delimiter(false)
92 )
93 }
94
95 /// Parse the commandline into a `Command` to execute.
parse() -> Result<Command>96 pub fn parse() -> Result<Command> {
97 trace!("parse");
98 let cwd =
99 env::current_dir().context("sccache: Couldn't determine current working directory")?;
100 // The internal start server command is passed in the environment.
101 let internal_start_server = match env::var("SCCACHE_START_SERVER") {
102 Ok(val) => val == "1",
103 Err(_) => false,
104 };
105 let mut args: Vec<_> = env::args_os().collect();
106 if !internal_start_server {
107 if let Ok(exe) = env::current_exe() {
108 match exe
109 .file_stem()
110 .and_then(|s| s.to_str())
111 .map(|s| s.to_lowercase())
112 {
113 // If the executable has its standard name, do nothing.
114 Some(ref e) if e == env!("CARGO_PKG_NAME") => {}
115 // Otherwise, if it was copied/hardlinked under a different $name, act
116 // as if it were invoked with `sccache $name`, but avoid $name resolving
117 // to ourselves again if it's in the PATH.
118 _ => {
119 if let (Some(path), Some(exe_filename)) = (env::var_os("PATH"), exe.file_name())
120 {
121 match which_in(exe_filename, Some(&path), &cwd) {
122 Ok(ref full_path)
123 if full_path.canonicalize()? == exe.canonicalize()? =>
124 {
125 if let Some(dir) = full_path.parent() {
126 let path = env::join_paths(
127 env::split_paths(&path).filter(|p| p != dir),
128 )
129 .ok();
130 if let Ok(full_path) = which_in(exe_filename, path, &cwd) {
131 args[0] = full_path.into();
132 }
133 }
134 }
135 Ok(full_path) => args[0] = full_path.into(),
136 Err(_) => {}
137 }
138 args.insert(0, env!("CARGO_PKG_NAME").into());
139 }
140 }
141 }
142 }
143 }
144 let matches = get_app().get_matches_from(args);
145
146 let show_stats = matches.is_present("show-stats");
147 let start_server = matches.is_present("start-server");
148 let stop_server = matches.is_present("stop-server");
149 let zero_stats = matches.is_present("zero-stats");
150 let dist_auth = matches.is_present("dist-auth");
151 let dist_status = matches.is_present("dist-status");
152 let package_toolchain = matches.is_present("package-toolchain");
153 let cmd = matches.values_of_os("cmd");
154 // Ensure that we've only received one command to run.
155 fn is_some<T>(x: &Option<T>) -> bool {
156 x.is_some()
157 }
158 if [
159 internal_start_server,
160 show_stats,
161 start_server,
162 stop_server,
163 zero_stats,
164 package_toolchain,
165 is_some(&cmd),
166 ]
167 .iter()
168 .filter(|&&x| x)
169 .count()
170 > 1
171 {
172 bail!("Too many commands specified");
173 }
174 if internal_start_server {
175 Ok(Command::InternalStartServer)
176 } else if show_stats {
177 let fmt =
178 value_t!(matches.value_of("stats-format"), StatsFormat).unwrap_or_else(|e| e.exit());
179 Ok(Command::ShowStats(fmt))
180 } else if start_server {
181 Ok(Command::StartServer)
182 } else if stop_server {
183 Ok(Command::StopServer)
184 } else if zero_stats {
185 Ok(Command::ZeroStats)
186 } else if dist_auth {
187 Ok(Command::DistAuth)
188 } else if dist_status {
189 Ok(Command::DistStatus)
190 } else if package_toolchain {
191 let mut values = matches
192 .values_of_os("package-toolchain")
193 .expect("Parsed package-toolchain but no values");
194 assert!(values.len() == 2);
195 let (executable, out) = (
196 values.next().expect("package-toolchain missing value 1"),
197 values.next().expect("package-toolchain missing value 2"),
198 );
199 Ok(Command::PackageToolchain(executable.into(), out.into()))
200 } else if let Some(mut args) = cmd {
201 if let Some(exe) = args.next() {
202 let cmdline = args.map(|s| s.to_owned()).collect::<Vec<_>>();
203 let mut env_vars = env::vars_os().collect::<Vec<_>>();
204
205 // If we're running under rr, avoid the `LD_PRELOAD` bits, as it will
206 // almost surely do the wrong thing, as the compiler gets executed
207 // in a different process tree.
208 //
209 // FIXME: Maybe we should strip out `LD_PRELOAD` always?
210 if env::var_os("RUNNING_UNDER_RR").is_some() {
211 env_vars.retain(|(k, _v)| k != "LD_PRELOAD" && k != "RUNNING_UNDER_RR");
212 }
213
214 Ok(Command::Compile {
215 exe: exe.to_owned(),
216 cmdline,
217 cwd,
218 env_vars,
219 })
220 } else {
221 bail!("No compile command");
222 }
223 } else {
224 bail!("No command specified");
225 }
226 }
227