1use std::{collections::BTreeMap, ffi::OsString, path::PathBuf, rc::Rc};
12
13#[doc(hidden)]
15pub(crate) fn ct_force_new_env() -> Rc<CTEnv> {
16 CT_ENV.with(|env| {
17 let mut env = env.lock().unwrap();
18 *env = priv_new_env();
19 env.clone()
20 })
21}
22
23pub fn ct_env() -> Rc<CTEnv> {
25 CT_ENV.with(|env| env.lock().unwrap().clone())
26}
27
28#[macro_export]
30macro_rules! ct_info {
31 ($($tt:tt)*) => { $crate::_cargo_task_util::ct_info(&format!($($tt)*)) };
32}
33
34#[macro_export]
36macro_rules! ct_warn {
37 ($($tt:tt)*) => { $crate::_cargo_task_util::ct_warn(&format!($($tt)*)) };
38}
39
40#[macro_export]
42macro_rules! ct_fatal {
43 ($($tt:tt)*) => { $crate::_cargo_task_util::ct_fatal(&format!($($tt)*)) };
44}
45
46#[macro_export]
48macro_rules! ct_check_fatal {
49 ($code:expr) => {
50 match { $code } {
51 Err(e) => $crate::ct_fatal!("{:#?}", e),
52 Ok(r) => r,
53 }
54 };
55}
56
57#[derive(Debug)]
59pub struct CTEnv {
60 pub cargo_path: PathBuf,
62
63 pub cargo_task_path: PathBuf,
65
66 pub cargo_task_target: PathBuf,
68
69 pub work_dir: PathBuf,
71
72 pub task_list: Vec<String>,
74
75 pub arg_list: Vec<String>,
77
78 pub tasks: BTreeMap<String, CTTaskMeta>,
80}
81
82impl CTEnv {
83 pub fn cargo(&self) -> std::process::Command {
85 std::process::Command::new(&self.cargo_path)
86 }
87
88 pub fn exec(&self, mut cmd: std::process::Command) -> std::io::Result<()> {
90 let non_zero_err = format!("{:?} exited non-zero", cmd);
91 if !cmd.spawn()?.wait()?.success() {
92 return Err(std::io::Error::new(
93 std::io::ErrorKind::Other,
94 non_zero_err,
95 ));
96 }
97 Ok(())
98 }
99
100 pub fn set_env<N: AsRef<str>, V: AsRef<str>>(&self, name: N, val: V) {
103 let name = name.as_ref();
104 let val = val.as_ref();
105
106 std::env::set_var(name, val);
107
108 let directive = format!("@ct-set-env@ {}={} @@\n", name, val);
109
110 let mut p = self.cargo_task_target.clone();
111 let directive_file_name =
112 format!("task-directive-{}.atat", std::process::id());
113 p.push(directive_file_name);
114
115 FRESH_DIRECTIVE.call_once(|| {
116 let _ = std::fs::remove_file(&p);
117 });
118
119 let mut f = ct_check_fatal!(std::fs::OpenOptions::new()
120 .write(true)
121 .append(true)
122 .create(true)
123 .open(&p));
124
125 use std::io::Write;
126 ct_check_fatal!(f.write_all(directive.as_bytes()));
127 ct_check_fatal!(f.sync_all());
128 }
129}
130
131static FRESH_DIRECTIVE: std::sync::Once = std::sync::Once::new();
132
133#[derive(Debug)]
135pub struct CTTaskMeta {
136 pub name: String,
138
139 pub is_script: bool,
142
143 pub min_version: Option<String>,
145
146 pub path: PathBuf,
148
149 pub default: bool,
151
152 pub bootstrap: bool,
155
156 pub help: String,
158
159 pub cargo_deps: Option<String>,
161
162 pub task_deps: Vec<String>,
164}
165
166#[derive(Clone, Copy)]
168pub enum CTLogLevel {
169 Info,
171
172 Warn,
174
175 Fatal,
177}
178
179pub fn ct_log(lvl: CTLogLevel, text: &str) {
181 let with_color = std::env::var_os("CT_NO_COLOR").is_none()
182 && (std::env::var_os("CT_WITH_COLOR").is_some() || DEFAULT_WITH_COLOR);
183
184 let task_name = std::env::var_os("CT_CUR_TASK")
185 .map(|s| s.to_string_lossy().to_string())
186 .unwrap_or_else(|| "".to_string());
187
188 let t_colon = if task_name.is_empty() { "" } else { ":" };
189
190 let base = if with_color { "\x1b[97m" } else { "" };
191 let reset = if with_color { "\x1b[0m" } else { "" };
192
193 let (lvl_name, log) = match lvl {
194 CTLogLevel::Info => ("INFO", "\x1b[92m"),
195 CTLogLevel::Warn => ("WARN", "\x1b[93m"),
196 CTLogLevel::Fatal => ("FATAL", "\x1b[91m"),
197 };
198
199 let log = if with_color { log } else { "" };
200
201 for line in text.split('\n') {
202 eprintln!(
203 "{}[ct:{}{}{}{}{}]{} {}",
204 base, log, lvl_name, base, t_colon, task_name, reset, line
205 );
206 }
207
208 if let CTLogLevel::Fatal = lvl {
209 std::process::exit(1);
210 }
211}
212
213pub fn ct_info(text: &str) {
215 ct_log(CTLogLevel::Info, text)
216}
217
218pub fn ct_warn(text: &str) {
220 ct_log(CTLogLevel::Warn, text)
221}
222
223pub fn ct_fatal(text: &str) -> ! {
225 ct_log(CTLogLevel::Fatal, text);
226 std::process::exit(1);
227}
228
229#[cfg(windows)]
232const DEFAULT_WITH_COLOR: bool = false;
233#[cfg(not(windows))]
234const DEFAULT_WITH_COLOR: bool = true;
235
236thread_local! {
237 static CT_ENV: std::sync::Mutex<Rc<CTEnv>> = std::sync::Mutex::new(priv_new_env());
238}
239
240fn priv_new_env() -> Rc<CTEnv> {
242 let cargo_path = match std::env::var_os("CARGO").map(PathBuf::from) {
243 Some(cargo_path) => cargo_path,
244 None => ct_fatal!("CARGO binary path not set in environment"),
245 };
246 let work_dir = match std::env::var_os("CT_WORK_DIR").map(PathBuf::from) {
247 Some(work_dir) => work_dir,
248 None => ct_fatal!("CT_WORK_DIR environment variable not set"),
249 };
250 let cargo_task_path = match std::env::var_os("CT_PATH").map(PathBuf::from) {
251 Some(cargo_task_path) => cargo_task_path,
252 None => ct_fatal!("CT_PATH environment variable not set"),
253 };
254 let cargo_task_target =
255 match std::env::var_os("CT_TARGET").map(PathBuf::from) {
256 Some(cargo_task_target) => cargo_task_target,
257 None => ct_fatal!("CT_TARGET environment variable not set"),
258 };
259 let task_list = match std::env::var_os("CT_TASKS") {
260 Some(args) => args
261 .to_string_lossy()
262 .split_whitespace()
263 .map(|s| s.to_string())
264 .collect::<Vec<_>>(),
265 None => Vec::with_capacity(0),
266 };
267 let arg_list = match std::env::var_os("CT_ARGS") {
268 Some(args) => args
269 .to_string_lossy()
270 .split_whitespace()
271 .map(|s| s.to_string())
272 .collect::<Vec<_>>(),
273 None => Vec::with_capacity(0),
274 };
275 let tasks = ct_check_fatal!(enumerate_task_metadata());
276
277 Rc::new(CTEnv {
278 cargo_path,
279 work_dir,
280 cargo_task_path,
281 cargo_task_target,
282 task_list,
283 arg_list,
284 tasks,
285 })
286}
287
288#[allow(clippy::unnecessary_wraps)]
290fn enumerate_task_metadata(
291) -> Result<BTreeMap<String, CTTaskMeta>, &'static str> {
292 let mut out = BTreeMap::new();
293
294 let env = std::env::vars_os().collect::<BTreeMap<OsString, OsString>>();
295 for (env_k, env_v) in env.iter() {
296 let env_k = env_k.to_string_lossy();
297 if env_k.starts_with("CT_TASK_") && env_k.ends_with("_PATH") {
298 let name = env_k[8..env_k.len() - 5].to_string();
299 let script_name = format!("CT_TASK_{}_IS_SCRIPT", name);
300 let is_script = env.contains_key(&OsString::from(script_name));
301 let mv_name = format!("CT_TASK_{}_MIN_VER", name);
302 let min_version = env
303 .get(&OsString::from(mv_name))
304 .map(|v| v.to_string_lossy().to_string());
305 let def_name = format!("CT_TASK_{}_DEFAULT", name);
306 let default = env.contains_key(&OsString::from(def_name));
307 let bs_name = format!("CT_TASK_{}_BOOTSTRAP", name);
308 let bootstrap = env.contains_key(&OsString::from(bs_name));
309 let help_name = format!("CT_TASK_{}_HELP", name);
310 let help = env
311 .get(&OsString::from(help_name))
312 .map(|s| s.to_string_lossy().to_string())
313 .unwrap_or_else(|| "".to_string());
314 let deps_name = format!("CT_TASK_{}_CARGO_DEPS", name);
315 let cargo_deps = env
316 .get(&OsString::from(deps_name))
317 .map(|v| v.to_string_lossy().to_string());
318 let deps_name = format!("CT_TASK_{}_TASK_DEPS", name);
319 let mut task_deps = Vec::new();
320 if let Some(deps) = env.get(&OsString::from(deps_name)) {
321 for dep in deps.to_string_lossy().split_whitespace() {
322 task_deps.push(dep.to_string());
323 }
324 }
325 let path = PathBuf::from(env_v);
326 out.insert(
327 name.clone(),
328 CTTaskMeta {
329 name,
330 is_script,
331 min_version,
332 path,
333 default,
334 bootstrap,
335 help,
336 cargo_deps,
337 task_deps,
338 },
339 );
340 }
341 }
342
343 Ok(out)
344}