1use std::{
2 env,
3 ffi::OsStr,
4 path::{Path, PathBuf},
5 process::{Command, ExitStatus},
6};
7
8use anyhow::anyhow;
9use cargo_metadata::{
10 camino::{Utf8Path, Utf8PathBuf},
11 Artifact, Message, TargetKind,
12};
13use tests::{FLUX_SYSROOT, FLUX_SYSROOT_TEST};
14use xshell::{cmd, Shell};
15
16xflags::xflags! {
17 cmd xtask {
18 optional --offline
20
21 cmd test {
23 optional filter: String
25 optional --no-lib-tests
27 }
28 cmd run {
30 required input: PathBuf
32 repeated opts: String
34 }
35 cmd expand {
37 required input: PathBuf
39 }
40 cmd install {
42 optional --profile profile: Profile
44 optional --no-libs
46 }
47 cmd uninstall { }
49 cmd build-sysroot { }
51 cmd doc {
53 optional -o,--open
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug)]
59enum Profile {
60 Release,
61 Dev,
62 Profiling,
63}
64
65impl Profile {
66 fn as_str(self) -> &'static str {
67 match self {
68 Profile::Release => "release",
69 Profile::Dev => "dev",
70 Profile::Profiling => "profiling",
71 }
72 }
73}
74
75impl std::str::FromStr for Profile {
76 type Err = &'static str;
77
78 fn from_str(s: &str) -> Result<Self, Self::Err> {
79 match s {
80 "release" => Ok(Self::Release),
81 "dev" => Ok(Self::Dev),
82 "profiling" => Ok(Self::Profiling),
83 _ => Err("invalid profile"),
84 }
85 }
86}
87
88fn main() -> anyhow::Result<()> {
89 let cmd = match Xtask::from_env() {
90 Ok(cmd) => cmd,
91 Err(err) => {
92 println!("{}", Xtask::HELP_);
93 if err.is_help() {
94 std::process::exit(0);
95 } else {
96 println!("{}", Xtask::HELP_);
97 std::process::exit(2);
98 }
99 }
100 };
101
102 let sh = Shell::new()?;
103 sh.change_dir(project_root());
104
105 let mut extra = vec![];
106 if cmd.offline {
107 extra.push("--offline");
108 }
109 match cmd.subcommand {
110 XtaskCmd::Test(args) => test(sh, args),
111 XtaskCmd::Run(args) => run(sh, args),
112 XtaskCmd::Install(args) => install(&sh, &args, &extra),
113 XtaskCmd::Doc(args) => doc(args),
114 XtaskCmd::BuildSysroot(_) => {
115 let config = SysrootConfig {
116 profile: Profile::Dev,
117 dst: local_sysroot_dir()?,
118 build_libs: BuildLibs::Yes { force: true, tests: true },
119 };
120 install_sysroot(&sh, &config)?;
121 Ok(())
122 }
123 XtaskCmd::Uninstall(_) => uninstall(&sh),
124 XtaskCmd::Expand(args) => expand(&sh, args),
125 }
126}
127
128fn test(sh: Shell, args: Test) -> anyhow::Result<()> {
129 let config = SysrootConfig {
130 profile: Profile::Dev,
131 dst: local_sysroot_dir()?,
132 build_libs: BuildLibs::Yes { force: false, tests: !args.no_lib_tests },
133 };
134 let flux = build_binary("flux", config.profile)?;
135 install_sysroot(&sh, &config)?;
136
137 Command::new("cargo")
138 .args(["test", "-p", "tests", "--"])
139 .args(["--flux", flux.as_str()])
140 .args(["--sysroot".as_ref(), config.dst.as_os_str()])
141 .map_opt(args.filter.as_ref(), |filter, cmd| {
142 cmd.args(["--filter", filter]);
143 })
144 .run()
145}
146
147fn run(sh: Shell, args: Run) -> anyhow::Result<()> {
148 run_inner(
149 &sh,
150 args.input,
151 ["-Ztrack-diagnostics=y".to_string()]
152 .into_iter()
153 .chain(args.opts),
154 )?;
155 Ok(())
156}
157
158fn expand(sh: &Shell, args: Expand) -> Result<(), anyhow::Error> {
159 run_inner(sh, args.input, ["-Zunpretty=expanded".to_string()])?;
160 Ok(())
161}
162
163fn run_inner(
164 sh: &Shell,
165 input: PathBuf,
166 flags: impl IntoIterator<Item = String>,
167) -> Result<(), anyhow::Error> {
168 let config = SysrootConfig {
169 profile: Profile::Dev,
170 dst: local_sysroot_dir()?,
171 build_libs: BuildLibs::Yes { force: false, tests: false },
172 };
173
174 install_sysroot(sh, &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(sh: &Shell, args: &Install, extra: &[&str]) -> anyhow::Result<()> {
188 let config = SysrootConfig {
189 profile: args.profile(),
190 dst: default_sysroot_dir(),
191 build_libs: if args.no_libs {
192 BuildLibs::No
193 } else {
194 BuildLibs::Yes { force: false, tests: false }
195 },
196 };
197 install_sysroot(sh, &config)?;
198 Command::new("cargo")
199 .args(["install", "--path", "crates/flux-bin", "--force"])
200 .args(extra)
201 .run()
202}
203
204fn uninstall(sh: &Shell) -> anyhow::Result<()> {
205 cmd!(sh, "cargo uninstall -p flux-bin").run()?;
206 eprintln!("$ rm -rf ~/.flux");
207 sh.remove_path(default_sysroot_dir())?;
208 Ok(())
209}
210
211fn doc(args: Doc) -> anyhow::Result<()> {
212 Command::new("cargo")
213 .args(["doc", "--workspace", "--document-private-items", "--no-deps"])
214 .env("RUSTDOCFLAGS", "-Zunstable-options --enable-index-page")
215 .run()?;
216 if args.open {
217 opener::open("target/doc/index.html")?;
218 }
219 Ok(())
220}
221
222fn project_root() -> PathBuf {
223 Path::new(
224 &env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()),
225 )
226 .ancestors()
227 .nth(1)
228 .unwrap()
229 .to_path_buf()
230}
231
232fn build_binary(bin: &str, profile: Profile) -> anyhow::Result<Utf8PathBuf> {
233 Command::new("cargo")
234 .args(["build", "--bin", bin, "--profile", profile.as_str()])
235 .run_with_cargo_metadata()?
236 .into_iter()
237 .find(|artifact| artifact.target.name == bin && artifact.target.is_kind(TargetKind::Bin))
238 .and_then(|artifact| artifact.executable)
239 .ok_or_else(|| anyhow!("cannot find binary: `{bin}`"))
240}
241
242struct SysrootConfig {
243 profile: Profile,
245 dst: PathBuf,
247 build_libs: BuildLibs,
248}
249
250enum BuildLibs {
252 Yes { force: bool, tests: bool },
254 No,
256}
257
258fn install_sysroot(sh: &Shell, config: &SysrootConfig) -> anyhow::Result<()> {
259 sh.remove_path(&config.dst)?;
260 sh.create_dir(&config.dst)?;
261
262 copy_file(sh, build_binary("flux-driver", config.profile)?, &config.dst)?;
263
264 let cargo_flux = build_binary("cargo-flux", config.profile)?;
265
266 if let BuildLibs::Yes { force, tests } = config.build_libs {
267 if force {
268 Command::new(&cargo_flux)
269 .args(["flux", "clean"])
270 .env(FLUX_SYSROOT, &config.dst)
271 .run()?;
272 }
273
274 let artifacts = Command::new(cargo_flux)
275 .args(["flux", "-p", "flux-rs", "-p", "flux-core"])
276 .env(FLUX_SYSROOT, &config.dst)
277 .env_if(tests, FLUX_SYSROOT_TEST, "1")
278 .run_with_cargo_metadata()?;
279
280 copy_artifacts(sh, &artifacts, &config.dst)?;
281 }
282 Ok(())
283}
284
285fn copy_artifacts(sh: &Shell, artifacts: &[Artifact], sysroot: &Path) -> anyhow::Result<()> {
286 for artifact in artifacts {
287 if !is_flux_lib(artifact) {
288 continue;
289 }
290
291 for filename in &artifact.filenames {
292 copy_artifact(sh, filename, sysroot)?;
293 }
294 }
295 Ok(())
296}
297
298fn copy_artifact(sh: &Shell, filename: &Utf8Path, dst: &Path) -> anyhow::Result<()> {
299 copy_file(sh, filename, dst)?;
300 if filename.extension() == Some("rmeta") {
301 let fluxmeta = filename.with_extension("fluxmeta");
302 if sh.path_exists(&fluxmeta) {
303 copy_file(sh, &fluxmeta, dst)?;
304 }
305 }
306 Ok(())
307}
308
309fn is_flux_lib(artifact: &Artifact) -> bool {
310 matches!(&artifact.target.name[..], "flux_rs" | "flux_attrs" | "flux_core")
311}
312
313impl Install {
314 fn profile(&self) -> Profile {
315 self.profile.unwrap_or(Profile::Release)
316 }
317}
318
319fn default_sysroot_dir() -> PathBuf {
320 home::home_dir()
321 .expect("Couldn't find home directory")
322 .join(".flux")
323}
324
325fn local_sysroot_dir() -> anyhow::Result<PathBuf> {
326 Ok(Path::new(file!())
327 .canonicalize()?
328 .ancestors()
329 .nth(3)
330 .unwrap()
331 .join("sysroot"))
332}
333
334fn check_status(st: ExitStatus) -> anyhow::Result<()> {
335 if st.success() {
336 return Ok(());
337 }
338 let err = match st.code() {
339 Some(code) => anyhow!("command exited with non-zero code: {code}"),
340 #[cfg(unix)]
341 None => {
342 use std::os::unix::process::ExitStatusExt;
343 match st.signal() {
344 Some(sig) => anyhow!("command was terminated by a signal: {sig}"),
345 None => anyhow!("command was terminated by a signal"),
346 }
347 }
348 #[cfg(not(unix))]
349 None => anyhow!("command was terminated by a signal"),
350 };
351 Err(err)
352}
353
354fn display_command(cmd: &Command) {
355 for var in cmd.get_envs() {
356 if let Some(val) = var.1 {
357 eprintln!("$ export {}={}", var.0.to_string_lossy(), val.to_string_lossy());
358 }
359 }
360
361 let prog = cmd.get_program();
362 eprint!("$ {}", prog.to_string_lossy());
363 for arg in cmd.get_args() {
364 eprint!(" {}", arg.to_string_lossy());
365 }
366 eprintln!();
367}
368
369fn copy_file<S: AsRef<Path>, D: AsRef<Path>>(sh: &Shell, src: S, dst: D) -> anyhow::Result<()> {
370 let src = src.as_ref();
371 let dst = dst.as_ref();
372 eprintln!("$ cp {} {}", src.to_string_lossy(), dst.to_string_lossy());
373 sh.copy_file(src, dst)?;
374 Ok(())
375}
376
377trait CommandExt {
378 fn map_opt<T>(&mut self, b: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self;
379 fn run(&mut self) -> anyhow::Result<()>;
380 fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
381 where
382 K: AsRef<OsStr>,
383 V: AsRef<OsStr>;
384 fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>>;
385}
386
387impl CommandExt for Command {
388 fn map_opt<T>(&mut self, opt: Option<&T>, f: impl FnOnce(&T, &mut Self)) -> &mut Self {
389 if let Some(v) = opt {
390 f(v, self);
391 }
392 self
393 }
394
395 fn env_if<K, V>(&mut self, b: bool, k: K, v: V) -> &mut Self
396 where
397 K: AsRef<OsStr>,
398 V: AsRef<OsStr>,
399 {
400 if b {
401 self.env(k, v);
402 }
403 self
404 }
405
406 fn run(&mut self) -> anyhow::Result<()> {
407 display_command(self);
408 let mut child = self.spawn()?;
409 check_status(child.wait()?)
410 }
411
412 fn run_with_cargo_metadata(&mut self) -> anyhow::Result<Vec<Artifact>> {
413 self.arg("--message-format=json-render-diagnostics")
414 .stdout(std::process::Stdio::piped());
415
416 display_command(self);
417
418 let mut child = self.spawn()?;
419
420 let mut artifacts = vec![];
421 let reader = std::io::BufReader::new(child.stdout.take().unwrap());
422 for message in cargo_metadata::Message::parse_stream(reader) {
423 match message.unwrap() {
424 Message::CompilerMessage(msg) => {
425 println!("{msg}");
426 }
427 Message::CompilerArtifact(artifact) => {
428 artifacts.push(artifact);
429 }
430 _ => (),
431 }
432 }
433
434 check_status(child.wait()?)?;
435
436 Ok(artifacts)
437 }
438}