1#![feature(variant_count)]
2
3use std::{
4 ffi::OsStr,
5 fs, io,
6 mem::variant_count,
7 path::{Path, PathBuf},
8 process::{Command, ExitStatus},
9};
10
11use anyhow::anyhow;
12use cargo_metadata::{
13 camino::{Utf8Path, Utf8PathBuf},
14 Artifact, Message, TargetKind,
15};
16use tests::{FLUX_SYSROOT, FLUX_SYSROOT_TEST};
17
18xflags::xflags! {
19 cmd xtask {
20 optional --offline
22
23 cmd test {
25 optional filter: String
27 optional --no-lib-tests
29 }
30 cmd run {
32 required input: PathBuf
34 repeated opts: String
36 optional --no-extern-specs
38 }
39 cmd expand {
41 required input: PathBuf
43 }
44 cmd install {
46 optional --profile profile: Profile
48 optional --no-extern-specs
50 }
51 cmd uninstall { }
53 cmd build-sysroot { }
55 cmd doc { }
57 }
58}
59
60#[derive(Clone, Copy, Debug)]
61enum Profile {
62 Release,
63 Dev,
64 Profiling,
65}
66
67impl Profile {
68 fn as_str(self) -> &'static str {
69 match self {
70 Profile::Release => "release",
71 Profile::Dev => "dev",
72 Profile::Profiling => "profiling",
73 }
74 }
75}
76
77impl std::str::FromStr for Profile {
78 type Err = &'static str;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 match s {
82 "release" => Ok(Self::Release),
83 "dev" => Ok(Self::Dev),
84 "profiling" => Ok(Self::Profiling),
85 _ => Err("invalid profile"),
86 }
87 }
88}
89
90fn main() -> anyhow::Result<()> {
91 let cmd = match Xtask::from_env() {
92 Ok(cmd) => cmd,
93 Err(err) => {
94 println!("{}", Xtask::HELP_);
95 if err.is_help() {
96 std::process::exit(0);
97 } else {
98 println!("{}", Xtask::HELP_);
99 std::process::exit(2);
100 }
101 }
102 };
103
104 let mut extra = vec![];
105 if cmd.offline {
106 extra.push("--offline");
107 }
108 match cmd.subcommand {
109 XtaskCmd::Test(args) => test(args),
110 XtaskCmd::Run(args) => run(args),
111 XtaskCmd::Install(args) => install(&args, &extra),
112 XtaskCmd::Doc(args) => doc(args),
113 XtaskCmd::BuildSysroot(_) => {
114 let config = SysrootConfig {
115 profile: Profile::Dev,
116 dst: local_sysroot_dir()?,
117 build_libs: BuildLibs { force: true, tests: true, libs: FluxLib::ALL },
118 };
119 install_sysroot(&config)?;
120 Ok(())
121 }
122 XtaskCmd::Uninstall(_) => uninstall(),
123 XtaskCmd::Expand(args) => expand(args),
124 }
125}
126
127fn test(args: Test) -> anyhow::Result<()> {
128 let config = SysrootConfig {
129 profile: Profile::Dev,
130 dst: local_sysroot_dir()?,
131 build_libs: BuildLibs { force: false, tests: !args.no_lib_tests, libs: FluxLib::ALL },
132 };
133 let flux = build_binary("flux", config.profile)?;
134 install_sysroot(&config)?;
135
136 Command::new("cargo")
137 .args(["test", "-p", "tests", "--"])
138 .args(["--flux", flux.as_str()])
139 .args(["--sysroot".as_ref(), config.dst.as_os_str()])
140 .map_opt(args.filter.as_ref(), |filter, cmd| {
141 cmd.args(["--filter", filter]);
142 })
143 .run()
144}
145
146fn run(args: Run) -> anyhow::Result<()> {
147 let libs = if args.no_extern_specs { &[FluxLib::FluxRs] } else { FluxLib::ALL };
148 run_inner(
149 args.input,
150 BuildLibs { force: false, tests: false, libs },
151 ["-Ztrack-diagnostics=y".to_string()]
152 .into_iter()
153 .chain(args.opts),
154 )?;
155 Ok(())
156}
157
158fn expand(args: Expand) -> Result<(), anyhow::Error> {
159 run_inner(
160 args.input,
161 BuildLibs { force: false, tests: false, libs: &[FluxLib::FluxRs] },
162 ["-Zunpretty=expanded".to_string()],
163 )?;
164 Ok(())
165}
166
167fn run_inner(
168 input: PathBuf,
169 build_libs: BuildLibs,
170 flags: impl IntoIterator<Item = String>,
171) -> Result<(), anyhow::Error> {
172 let config = SysrootConfig { profile: Profile::Dev, dst: local_sysroot_dir()?, build_libs };
173
174 install_sysroot(&config)?;
175 let flux = build_binary("flux", config.profile)?;
176
177 let mut rustc_flags = tests::default_flags();
178 rustc_flags.extend(flags);
179
180 Command::new(flux)
181 .args(&rustc_flags)
182 .arg(&input)
183 .env(FLUX_SYSROOT, &config.dst)
184 .run()
185}
186
187fn install(args: &Install, extra: &[&str]) -> anyhow::Result<()> {
188 let libs = if args.no_extern_specs { &[FluxLib::FluxRs] } else { FluxLib::ALL };
189 let config = SysrootConfig {
190 profile: args.profile(),
191 dst: default_sysroot_dir(),
192 build_libs: BuildLibs { force: false, tests: false, libs },
193 };
194 install_sysroot(&config)?;
195 Command::new("cargo")
196 .args(["install", "--path", "crates/flux-bin", "--force"])
197 .args(extra)
198 .run()
199}
200
201fn uninstall() -> anyhow::Result<()> {
202 Command::new("cargo")
203 .args(["uninstall", "-p", "flux-bin"])
204 .run()?;
205 eprintln!("$ rm -rf ~/.flux");
206 remove_path(&default_sysroot_dir())?;
207 Ok(())
208}
209
210fn doc(_args: Doc) -> anyhow::Result<()> {
211 Command::new("cargo")
212 .args(["doc", "--workspace", "--document-private-items", "--no-deps"])
213 .env("RUSTDOCFLAGS", "-Zunstable-options --enable-index-page")
214 .run()?;
215 Ok(())
216}
217
218fn build_binary(bin: &str, profile: Profile) -> anyhow::Result<Utf8PathBuf> {
219 Command::new("cargo")
220 .args(["build", "--bin", bin, "--profile", profile.as_str()])
221 .run_with_cargo_metadata()?
222 .into_iter()
223 .find(|artifact| artifact.target.name == bin && artifact.target.is_kind(TargetKind::Bin))
224 .and_then(|artifact| artifact.executable)
225 .ok_or_else(|| anyhow!("cannot find binary: `{bin}`"))
226}
227
228struct SysrootConfig {
229 profile: Profile,
231 dst: PathBuf,
233 build_libs: BuildLibs,
234}
235
236struct BuildLibs {
237 force: bool,
239 tests: bool,
241 libs: &'static [FluxLib],
243}
244
245#[allow(clippy::enum_variant_names)]
246#[derive(Clone, Copy)]
247enum FluxLib {
248 FluxAlloc,
249 FluxAttrs,
250 FluxCore,
251 FluxRs,
252}
253
254impl FluxLib {
255 const ALL: &[FluxLib] = &[Self::FluxAlloc, Self::FluxAttrs, Self::FluxCore, Self::FluxRs];
256
257 const _ASSERT_ALL: () = { assert!(Self::ALL.len() == variant_count::<Self>()) };
258
259 const fn package_name(self) -> &'static str {
260 match self {
261 FluxLib::FluxAlloc => "flux-alloc",
262 FluxLib::FluxAttrs => "flux-attrs",
263 FluxLib::FluxCore => "flux-core",
264 FluxLib::FluxRs => "flux-rs",
265 }
266 }
267
268 const fn target_name(self) -> &'static str {
269 match self {
270 FluxLib::FluxAlloc => "flux_alloc",
271 FluxLib::FluxAttrs => "flux_attrs",
272 FluxLib::FluxCore => "flux_core",
273 FluxLib::FluxRs => "flux_rs",
274 }
275 }
276
277 fn is_flux_lib(artifact: &Artifact) -> bool {
278 Self::ALL
279 .iter()
280 .any(|lib| artifact.target.name == lib.target_name())
281 }
282}
283
284fn install_sysroot(config: &SysrootConfig) -> anyhow::Result<()> {
285 remove_path(&config.dst)?;
286 create_dir(&config.dst)?;
287
288 copy_file(build_binary("flux-driver", config.profile)?, &config.dst)?;
289
290 let cargo_flux = build_binary("cargo-flux", config.profile)?;
291
292 if config.build_libs.force {
293 Command::new(&cargo_flux)
294 .args(["flux", "clean"])
295 .env(FLUX_SYSROOT, &config.dst)
296 .run()?;
297 }
298
299 let artifacts = Command::new(cargo_flux)
300 .arg("flux")
301 .args(
302 config
303 .build_libs
304 .libs
305 .iter()
306 .flat_map(|lib| ["-p", lib.package_name()]),
307 )
308 .env(FLUX_SYSROOT, &config.dst)
309 .env_if(config.build_libs.tests, FLUX_SYSROOT_TEST, "1")
310 .run_with_cargo_metadata()?;
311
312 copy_artifacts(&artifacts, &config.dst)?;
313 Ok(())
314}
315
316fn copy_artifacts(artifacts: &[Artifact], sysroot: &Path) -> anyhow::Result<()> {
317 for artifact in artifacts {
318 if !FluxLib::is_flux_lib(artifact) {
319 continue;
320 }
321
322 for filename in &artifact.filenames {
323 copy_artifact(filename, sysroot)?;
324 }
325 }
326 Ok(())
327}
328
329fn copy_artifact(filename: &Utf8Path, dst: &Path) -> anyhow::Result<()> {
330 copy_file(filename, dst)?;
331 if filename.extension() == Some("rmeta") {
332 let fluxmeta = filename.with_extension("fluxmeta");
333 if fluxmeta.exists() {
334 copy_file(&fluxmeta, dst)?;
335 }
336 }
337 Ok(())
338}
339
340impl Install {
341 fn profile(&self) -> Profile {
342 self.profile.unwrap_or(Profile::Release)
343 }
344}
345
346fn default_sysroot_dir() -> PathBuf {
347 home::home_dir()
348 .expect("Couldn't find home directory")
349 .join(".flux")
350}
351
352fn local_sysroot_dir() -> anyhow::Result<PathBuf> {
353 Ok(Path::new(file!())
354 .canonicalize()?
355 .ancestors()
356 .nth(3)
357 .unwrap()
358 .join("sysroot"))
359}
360
361fn check_status(st: ExitStatus) -> anyhow::Result<()> {
362 if st.success() {
363 return Ok(());
364 }
365 let err = match st.code() {
366 Some(code) => anyhow!("command exited with non-zero code: {code}"),
367 #[cfg(unix)]
368 None => {
369 use std::os::unix::process::ExitStatusExt;
370 match st.signal() {
371 Some(sig) => anyhow!("command was terminated by a signal: {sig}"),
372 None => anyhow!("command was terminated by a signal"),
373 }
374 }
375 #[cfg(not(unix))]
376 None => anyhow!("command was terminated by a signal"),
377 };
378 Err(err)
379}
380
381fn display_command(cmd: &Command) {
382 for var in cmd.get_envs() {
383 if let Some(val) = var.1 {
384 eprintln!("$ export {}={}", var.0.display(), val.display());
385 }
386 }
387
388 let prog = cmd.get_program();
389 eprint!("$ {}", prog.display());
390 for arg in cmd.get_args() {
391 eprint!(" {}", arg.display());
392 }
393 eprintln!();
394}
395
396fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(src: S, dst: D) -> anyhow::Result<()> {
397 let src = src.as_ref();
398 let dst = dst.as_ref();
399 eprintln!("$ cp {} {}", src.display(), dst.display());
400
401 let mut _tmp;
402 let mut dst = dst;
403 if dst.is_dir() {
404 if let Some(file_name) = src.file_name() {
405 _tmp = dst.join(file_name);
406 dst = &_tmp;
407 }
408 }
409 std::fs::copy(src, dst).map_err(|err| {
410 anyhow!("failed to copy `{}` to `{}`: {err}", src.display(), dst.display())
411 })?;
412
413 Ok(())
414}
415
416trait CommandExt {
417 fn map_opt<T>(&mut self, b: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self;
418 fn run(&mut self) -> anyhow::Result<()>;
419 fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
420 where
421 K: AsRef<OsStr>,
422 V: AsRef<OsStr>;
423 fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>>;
424}
425
426impl CommandExt for Command {
427 fn map_opt<T>(&mut self, opt: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self {
428 if let Some(v) = opt {
429 f(v, self);
430 }
431 self
432 }
433
434 fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
435 where
436 K: AsRef<OsStr>,
437 V: AsRef<OsStr>,
438 {
439 if b {
440 self.env(k, v);
441 }
442 self
443 }
444
445 fn run(&mut self) -> anyhow::Result<()> {
446 display_command(self);
447 let mut child = self.spawn()?;
448 check_status(child.wait()?)
449 }
450
451 fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>> {
452 self.arg("--message-format=json-render-diagnostics")
453 .stdout(std::process::Stdio::piped());
454
455 display_command(self);
456
457 let mut child = self.spawn()?;
458
459 let mut artifacts = vec![];
460 let reader = std::io::BufReader::new(child.stdout.take().unwrap());
461 for message in cargo_metadata::Message::parse_stream(reader) {
462 match message.unwrap() {
463 Message::CompilerMessage(msg) => {
464 println!("{msg}");
465 }
466 Message::CompilerArtifact(artifact) => {
467 artifacts.push(artifact);
468 }
469 _ => (),
470 }
471 }
472
473 check_status(child.wait()?)?;
474
475 Ok(artifacts)
476 }
477}
478
479fn remove_path(path: &Path) -> anyhow::Result<()> {
480 match path.metadata() {
481 Ok(meta) => {
482 if meta.is_dir() { remove_dir_all(path) } else { fs::remove_file(path) }
483 .map_err(|err| anyhow!("failed to remove path `{}`: {err}", path.display()))
484 }
485 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
486 Err(err) => Err(anyhow!("failed to remove path `{}`: {err}", path.display())),
487 }
488}
489
490#[cfg(not(windows))]
491fn remove_dir_all(path: &Path) -> io::Result<()> {
492 std::fs::remove_dir_all(path)
493}
494
495#[cfg(windows)]
497fn remove_dir_all(path: &Path) -> io::Result<()> {
498 for _ in 0..99 {
499 if fs::remove_dir_all(path).is_ok() {
500 return Ok(());
501 }
502 std::thread::sleep(std::time::Duration::from_millis(10))
503 }
504 fs::remove_dir_all(path)
505}
506
507fn create_dir(path: &Path) -> anyhow::Result<()> {
508 match fs::create_dir_all(path) {
509 Ok(()) => Ok(()),
510 Err(err) => Err(anyhow!("failed to create directory `{}`: {err}", path.display())),
511 }
512}