1#![doc = include_str!("../README.md")]
2#![doc(
3 test(attr(allow(unused_variables), deny(warnings))),
4 html_logo_url = "https://raw.githubusercontent.com/asaaki/wargo/main/.assets/logo-temp.png"
6)]
7#![cfg_attr(feature = "docs", feature(doc_cfg))]
8#![forbid(unsafe_code)]
9
10use anyhow::Context;
11use cargo_metadata::{Message, MetadataCommand};
12use cprint::{cprintln, Color};
13use filetime::{set_symlink_file_times, FileTime};
14use globwalk::DirEntry;
15use serde::Deserialize;
16use std::{
17 env,
18 ffi::OsStr,
19 fs,
20 path::{Path, PathBuf},
21 process::{Command, Stdio},
22 vec,
23};
24
25mod check;
26mod paths;
27mod progress;
28
29type GenericResult<T> = anyhow::Result<T>;
30pub type NullResult = GenericResult<()>;
31
32const SKIPPABLES: [&str; 4] = ["wargo", "cargo-wsl", "cargo", "wsl"];
33
34const HELP_TEXT: &str = r#"wargo
35
36cargo's evil twin to work with projects in the twilight zone of WSL2
37
38HELP TEXT WENT MISSING IN THE DARK …
39
40Maybe you find more helpful information at:
41
42https://github.com/asaaki/wargo
43"#;
44
45#[derive(Debug, Default, Deserialize)]
46#[serde(default, deny_unknown_fields)]
47struct WargoConfig {
48 project_dir: Option<String>,
51
52 dest_base_dir: Option<String>,
54
55 ignore_git: Option<bool>,
57
58 include_git: Option<bool>,
61
62 ignore_target: Option<bool>,
64
65 include_target: Option<bool>,
68
69 clean: bool,
72
73 #[serde(skip)]
75 clean_git: bool,
76}
77
78pub fn run(_from: &str) -> NullResult {
79 #[cfg(target_os = "windows")]
80 if wsl2_subshell()? {
81 cprintln!("wargo", "WSL2 subshell done.", Color::Cyan);
82 return Ok(());
83 }
84 check::wsl2_or_exit()?;
85
86 let args = parse_args();
87
88 if args.is_empty() || args[0] == "--help" {
89 println!("{HELP_TEXT}");
90 return Ok(());
91 }
92
93 let workspace_root = MetadataCommand::new()
94 .exec()?
95 .workspace_root
96 .into_std_path_buf()
97 .canonicalize()?;
98 let mut wargo_config = get_wargo_config(&workspace_root)?;
99 let dest_dir = get_destination_dir(&wargo_config, &workspace_root);
100
101 let entries = collect_entries(&mut wargo_config, &workspace_root)?;
102 copy_files(entries, &wargo_config, &workspace_root, &dest_dir)?;
103
104 let artifacts = exec_cargo_command(&dest_dir, &workspace_root, args)?;
105 copy_artifacts(&dest_dir, &workspace_root, artifacts)?;
106
107 Ok(())
108}
109
110#[cfg_attr(target_os = "linux", allow(dead_code))]
113fn wsl2_subshell() -> GenericResult<bool> {
114 if !check::is_wsl2() {
115 let wargo_args = parse_args()[1..].join(" ");
116 let wargo_and_args = format!("wargo {}", wargo_args);
117 let args = ["--shell-type", "login", "--", "bash", "-c", &wargo_and_args];
118
119 cprintln!("wargo", "WSL2 subshelling ...", Color::Cyan);
120 Command::new("wsl")
121 .env("WARGO_RUN", "1")
122 .args(args)
123 .spawn()?
124 .wait()?;
125 Ok(true)
126 } else {
127 Ok(false)
128 }
129}
130
131fn parse_args() -> Vec<String> {
132 if env::args().count() == 0 {
133 return Vec::new();
134 }
135 let args: Vec<String> = env::args()
136 .skip_while(|arg| match arg.split('/').last() {
137 Some(a) => SKIPPABLES.contains(&a),
138 None => false,
139 })
140 .collect();
141 args
142}
143
144fn get_wargo_config<P>(workspace_root: &P) -> GenericResult<WargoConfig>
145where
146 P: AsRef<Path>,
147{
148 let wargo_config = workspace_root.as_ref().join("Wargo.toml");
149
150 let wargo_config: WargoConfig = if wargo_config.exists() {
151 let wargo_config = fs::read_to_string(wargo_config)?;
152 toml::from_str(&wargo_config)?
153 } else {
154 WargoConfig::default()
155 };
156
157 Ok(wargo_config)
158}
159
160fn get_destination_dir<P>(wargo_config: &WargoConfig, workspace_root: &P) -> PathBuf
161where
162 P: AsRef<Path>,
163{
164 let project_dir = get_project_dir(wargo_config, &workspace_root);
165
166 let dest = if let Some(dir) = &wargo_config.dest_base_dir {
167 paths::untilde(dir)
168 } else {
169 let home = dirs::home_dir().unwrap();
171 home.join("tmp")
172 }
173 .join(project_dir);
174
175 paths::normalize_path(&dest)
176}
177
178fn get_project_dir<'a, P>(wargo_config: &'a WargoConfig, workspace_root: &'a P) -> &'a OsStr
179where
180 P: AsRef<Path>,
181{
182 let project_dir = if let Some(dir) = &wargo_config.project_dir {
183 OsStr::new(dir)
184 } else {
185 workspace_root.as_ref().iter().last().unwrap()
186 };
187 project_dir
188}
189
190fn collect_entries<P>(
191 wargo_config: &mut WargoConfig,
192 workspace_root: &P,
193) -> GenericResult<Vec<DirEntry>>
194where
195 P: AsRef<Path>,
196{
197 let mut patterns = vec!["**"];
198
199 if let Some(include_git) = wargo_config.include_git {
202 if !include_git {
203 patterns.push("!.git");
204 } else {
205 wargo_config.clean_git = true;
206 }
207 } else if let Some(ignore_git) = wargo_config.ignore_git {
208 if ignore_git {
209 patterns.push("!.git");
210 } else {
211 wargo_config.clean_git = true;
212 }
213 } else {
214 patterns.push("!.git");
216 }
217
218 if let Some(include_target) = wargo_config.include_target {
219 if !include_target {
220 patterns.push("!target");
221 }
222 } else if let Some(ignore_target) = wargo_config.ignore_target {
223 if ignore_target {
224 patterns.push("!target");
225 }
226 } else {
227 patterns.push("!target");
229 }
230
231 let entries: Vec<DirEntry> =
232 globwalk::GlobWalkerBuilder::from_patterns(workspace_root, &patterns)
233 .contents_first(false)
234 .build()?
235 .filter_map(Result::ok)
236 .collect();
237 Ok(entries)
238}
239
240fn copy_files<P>(
241 entries: Vec<DirEntry>,
242 wargo_config: &WargoConfig,
243 workspace_root: &P,
244 dest_dir: &P,
245) -> NullResult
246where
247 P: AsRef<Path>,
248{
249 if wargo_config.clean && dest_dir.as_ref().exists() {
250 fs::remove_dir_all(dest_dir).context("dest_dir cleaning failed")?;
251 }
252
253 fs::create_dir_all(dest_dir).context("dest_dir creation failed")?;
254
255 let git_dir = &dest_dir.as_ref().join(".git");
256 if wargo_config.clean_git && git_dir.exists() {
257 fs::remove_dir_all(git_dir).context("dest_dir/.git cleaning failed")?;
258 }
259
260 let bar = progress::bar(entries.len() as u64);
261 for entry in bar.wrap_iter(entries.iter()) {
262 let is_dir = entry.file_type().is_dir();
263 let src_path = entry.path();
264 let prj_path = src_path.strip_prefix(workspace_root)?;
265 let dst_path = &dest_dir.as_ref().to_path_buf().join(prj_path);
266
267 let metadata = entry.metadata()?;
268 let mtime = FileTime::from_last_modification_time(&metadata);
269 let atime = FileTime::from_last_access_time(&metadata);
270
271 if is_dir {
272 fs::create_dir_all(dst_path).context("Directory creation failed")?;
273 } else {
274 fs::copy(src_path, dst_path).with_context(|| {
277 format!(
278 "Copying failed: {} -> {}",
279 &src_path.display(),
280 &dst_path.display()
281 )
282 })?;
283 }
284
285 set_symlink_file_times(dst_path, atime, mtime).with_context(|| {
286 format!("Setting file timestamps failed for {}", &dst_path.display())
287 })?;
288 }
289 bar.finish_with_message("Files copied");
290 Ok(())
291}
292
293fn exec_cargo_command<P>(
294 dest_dir: &P,
295 workspace_root: &P,
296 args: Vec<String>,
297) -> GenericResult<Vec<PathBuf>>
298where
299 P: AsRef<Path>,
300{
301 let ws_rel_location = env::current_dir()?
303 .canonicalize()?
304 .strip_prefix(workspace_root)?
305 .to_path_buf();
306 let exec_dest = dest_dir.as_ref().join(ws_rel_location).canonicalize()?;
307
308 let mut files: Vec<PathBuf> = Vec::new();
309
310 let mut cargo_args = args;
311 if let Some(arg) = cargo_args.first() {
312 if ["b", "build"].contains(&arg.as_str()) {
315 cargo_args.insert(1, "--message-format=json-render-diagnostics".into());
316
317 let mut cmd = Command::new("cargo")
318 .args(cargo_args)
319 .current_dir(&exec_dest)
320 .stdout(Stdio::piped())
321 .spawn()?;
322
323 let reader = std::io::BufReader::new(cmd.stdout.take().expect("no stdout captured"));
324 for message in Message::parse_stream(reader) {
325 if let Message::CompilerArtifact(artifact) = message.unwrap() {
326 if ["bin", "dylib", "cdylib", "staticlib"]
327 .contains(&artifact.target.kind[0].as_str())
328 {
329 for filename in artifact.filenames {
330 files.push(filename.into_std_path_buf())
331 }
332 }
333 }
334 }
335 cmd.wait()?;
336 } else {
337 let mut cmd = Command::new("cargo")
338 .args(cargo_args)
339 .current_dir(&exec_dest)
340 .spawn()?;
341 cmd.wait()?;
342 }
343 };
344 Ok(files)
345}
346
347fn copy_artifacts<P>(dest_dir: &P, workspace_root: &P, artifacts: Vec<PathBuf>) -> NullResult
348where
349 P: AsRef<Path>,
350{
351 if !artifacts.is_empty() {
352 for artifact in artifacts {
353 let rel_artifact = artifact.strip_prefix(dest_dir)?;
354 let origin_location = &workspace_root.as_ref().join(rel_artifact);
355
356 if let Some(parent) = origin_location.parent() {
357 fs::create_dir_all(parent)?;
358 fs::copy(artifact, origin_location)?;
359 cprintln!(
360 "Copied",
361 format!("compile artifact to:\n{}", origin_location.display()),
362 Color::Green
363 );
364 }
365 }
366 };
367 Ok(())
368}