From 837b09573febd84cb200a169f9881048c9b767d7 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 26 Jun 2022 16:21:30 -0500 Subject: [PATCH] Add cargo-style output diagnostics. Adds the quiet and color command-line flags, where color supports `auto`, always`, and `never`. These command-line flags are parsed to a verbosity which can be quiet, normal, or verbose. With these, we then have the stderr message formatters: - `fatal_usage`: print a fatal error message with the failing argument, and add a help context menu for how to use cross. - `fatal`: print red 'error' message and exit with an error code - `error`: print red 'error' message - `warn`: print amber 'warning' message - `note`: print cyan 'note' message - `status`: print an uncolored and unprefixed 'status' message We have the stdout message formatters: - `print`: always print the message - `info`: print the message as long as the verbosity is not quiet - `debug`: only print the message if the output is not quiet We also have a few specialized error handlers, and methods to help ensure we can have flexible error reporting in the future: - `status_stderr` - `status_stdout` The command extensions now have, `print`, `info`, and `debug`, which formats the command and sends it to the shell. This allows us to avoid using `print_verbose` where we sometimes manually override the default setting. Closes #797. --- CHANGELOG.md | 1 + Cargo.lock | 20 +++ Cargo.toml | 1 + deny.toml | 13 +- src/bin/commands/clean.rs | 27 ++- src/bin/commands/containers.rs | 205 ++++++++++++++++----- src/bin/commands/images.rs | 121 ++++++++----- src/bin/cross-util.rs | 20 ++- src/cargo.rs | 13 +- src/cli.rs | 48 ++++- src/config.rs | 13 +- src/cross_toml.rs | 68 ++++--- src/docker/custom.rs | 5 +- src/docker/engine.rs | 17 +- src/docker/local.rs | 19 +- src/docker/mod.rs | 7 +- src/docker/remote.rs | 199 ++++++++++---------- src/docker/shared.rs | 40 +++-- src/extensions.rs | 93 ++++++---- src/lib.rs | 87 +++++---- src/rustc.rs | 13 +- src/rustup.rs | 51 ++++-- src/shell.rs | 310 ++++++++++++++++++++++++++++++++ src/tests.rs | 21 ++- src/tests/toml.rs | 11 +- xtask/src/build_docker_image.rs | 76 ++++---- xtask/src/ci.rs | 13 +- xtask/src/hooks.rs | 92 ++++++---- xtask/src/install_git_hooks.rs | 21 ++- xtask/src/main.rs | 16 +- xtask/src/target_info.rs | 35 ++-- xtask/src/util.rs | 30 +++- 32 files changed, 1230 insertions(+), 476 deletions(-) create mode 100644 src/shell.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f828ac5db..28c875c33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed +- #859 - added color diagnostic output and error messages. - #838 - re-enabled the solaris targets. - #807 - update Qemu to 6.1.0 on images using Ubuntu 18.04+ with python3.6+. - #775 - forward Cargo exit code to host diff --git a/Cargo.lock b/Cargo.lock index 94910a2b8..e2e058a5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,7 @@ dependencies = [ "libc", "nix", "once_cell", + "owo-colors", "regex", "rustc_version", "serde", @@ -312,6 +313,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + [[package]] name = "itoa" version = "1.0.2" @@ -388,6 +395,9 @@ name = "owo-colors" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b" +dependencies = [ + "supports-color", +] [[package]] name = "pin-project-lite" @@ -598,6 +608,16 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "supports-color" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872ced36b91d47bae8a214a683fe54e7078875b399dfa251df346c9b547d1f9" +dependencies = [ + "atty", + "is_ci", +] + [[package]] name = "syn" version = "1.0.96" diff --git a/Cargo.toml b/Cargo.toml index 133ded7aa..53492a4aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ ctrlc = { version = "3.2.2", features = ["termination"] } directories = "4.0.1" walkdir = { version = "2", optional = true } tempfile = "3.3.0" +owo-colors = { version = "3.4.0", features = ["supports-colors"] } [target.'cfg(not(windows))'.dependencies] nix = { version = "0.24", default-features = false, features = ["user"] } diff --git a/deny.toml b/deny.toml index c2ef04509..3f7755317 100644 --- a/deny.toml +++ b/deny.toml @@ -24,11 +24,22 @@ unknown-git = "deny" allow-git = [] [licenses] +# need this since to suppress errors in case we add crates with these allowed licenses +unused-allowed-license = "allow" unlicensed = "deny" allow-osi-fsf-free = "neither" copyleft = "deny" confidence-threshold = 0.93 -allow = ["Apache-2.0", "MIT", "CC0-1.0"] +allow = [ + "Apache-2.0", + "MIT", + "CC0-1.0", + "ISC", + "0BSD", + "BSD-2-Clause", + "BSD-3-Clause", + "Unlicense" +] [licenses.private] ignore = true diff --git a/src/bin/commands/clean.rs b/src/bin/commands/clean.rs index fec49a683..35c9db7db 100644 --- a/src/bin/commands/clean.rs +++ b/src/bin/commands/clean.rs @@ -3,12 +3,19 @@ use std::fs; use super::containers::*; use super::images::*; use clap::Args; +use cross::shell::{self, MessageInfo}; #[derive(Args, Debug)] pub struct Clean { /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Force removal of images. #[clap(short, long)] pub force: bool, @@ -25,6 +32,7 @@ pub struct Clean { impl Clean { pub fn run(self, engine: cross::docker::Engine) -> cross::Result<()> { + let msg_info = MessageInfo::create(self.verbose, self.quiet, self.color.as_deref())?; let tempdir = cross::temp::dir()?; match self.execute { true => { @@ -32,15 +40,20 @@ impl Clean { fs::remove_dir_all(tempdir)?; } } - false => println!( - "fs::remove_dir_all({})", - cross::pretty_path(&tempdir, |_| false) - ), + false => shell::print( + format!( + "fs::remove_dir_all({})", + cross::pretty_path(&tempdir, |_| false) + ), + msg_info, + )?, } // containers -> images -> volumes -> prune to ensure no conflicts. let remove_containers = RemoveAllContainers { verbose: self.verbose, + quiet: self.quiet, + color: self.color.clone(), force: self.force, execute: self.execute, engine: None, @@ -50,6 +63,8 @@ impl Clean { let remove_images = RemoveImages { targets: vec![], verbose: self.verbose, + quiet: self.quiet, + color: self.color.clone(), force: self.force, local: self.local, execute: self.execute, @@ -59,6 +74,8 @@ impl Clean { let remove_volumes = RemoveAllVolumes { verbose: self.verbose, + quiet: self.quiet, + color: self.color.clone(), force: self.force, execute: self.execute, engine: None, @@ -67,6 +84,8 @@ impl Clean { let prune_volumes = PruneVolumes { verbose: self.verbose, + quiet: self.quiet, + color: self.color.clone(), execute: self.execute, engine: None, }; diff --git a/src/bin/commands/containers.rs b/src/bin/commands/containers.rs index fefda5062..fc0582e68 100644 --- a/src/bin/commands/containers.rs +++ b/src/bin/commands/containers.rs @@ -1,5 +1,7 @@ -use atty::Stream; +use std::io; + use clap::{Args, Subcommand}; +use cross::shell::{self, MessageInfo, Stream}; use cross::{docker, CommandExt}; #[derive(Args, Debug)] @@ -7,6 +9,12 @@ pub struct ListVolumes { /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Container engine (such as docker or podman). #[clap(long)] pub engine: Option, @@ -23,6 +31,12 @@ pub struct RemoveAllVolumes { /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Force removal of volumes. #[clap(short, long)] pub force: bool, @@ -45,6 +59,12 @@ pub struct PruneVolumes { /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Remove volumes. Default is a dry run. #[clap(short, long)] pub execute: bool, @@ -70,6 +90,12 @@ pub struct CreateVolume { /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Container engine (such as docker or podman). #[clap(long)] pub engine: Option, @@ -92,6 +118,12 @@ pub struct RemoveVolume { /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Container engine (such as docker or podman). #[clap(long)] pub engine: Option, @@ -147,6 +179,26 @@ impl Volumes { Volumes::Remove(l) => l.verbose, } } + + pub fn quiet(&self) -> bool { + match self { + Volumes::List(l) => l.quiet, + Volumes::RemoveAll(l) => l.quiet, + Volumes::Prune(l) => l.quiet, + Volumes::Create(l) => l.quiet, + Volumes::Remove(l) => l.quiet, + } + } + + pub fn color(&self) -> Option<&str> { + match self { + Volumes::List(l) => l.color.as_deref(), + Volumes::RemoveAll(l) => l.color.as_deref(), + Volumes::Prune(l) => l.color.as_deref(), + Volumes::Create(l) => l.color.as_deref(), + Volumes::Remove(l) => l.color.as_deref(), + } + } } #[derive(Args, Debug)] @@ -154,6 +206,12 @@ pub struct ListContainers { /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Container engine (such as docker or podman). #[clap(long)] pub engine: Option, @@ -170,6 +228,12 @@ pub struct RemoveAllContainers { /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Force removal of containers. #[clap(short, long)] pub force: bool, @@ -216,15 +280,29 @@ impl Containers { Containers::RemoveAll(l) => l.verbose, } } + + pub fn quiet(&self) -> bool { + match self { + Containers::List(l) => l.quiet, + Containers::RemoveAll(l) => l.quiet, + } + } + + pub fn color(&self) -> Option<&str> { + match self { + Containers::List(l) => l.color.as_deref(), + Containers::RemoveAll(l) => l.color.as_deref(), + } + } } -fn get_cross_volumes(engine: &docker::Engine, verbose: bool) -> cross::Result> { +fn get_cross_volumes(engine: &docker::Engine, msg_info: MessageInfo) -> cross::Result> { let stdout = docker::subcommand(engine, "volume") .arg("list") .args(&["--format", "{{.Name}}"]) // handles simple regex: ^ for start of line. .args(&["--filter", "name=^cross-"]) - .run_and_get_stdout(verbose)?; + .run_and_get_stdout(msg_info)?; let mut volumes: Vec = stdout.lines().map(|s| s.to_string()).collect(); volumes.sort(); @@ -233,12 +311,18 @@ fn get_cross_volumes(engine: &docker::Engine, verbose: bool) -> cross::Result cross::Result<()> { - get_cross_volumes(engine, verbose)? - .iter() - .for_each(|line| println!("{}", line)); + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + for line in get_cross_volumes(engine, msg_info)?.iter() { + shell::print(line, msg_info)?; + } Ok(()) } @@ -246,13 +330,16 @@ pub fn list_volumes( pub fn remove_all_volumes( RemoveAllVolumes { verbose, + quiet, + color, force, execute, .. }: RemoveAllVolumes, engine: &docker::Engine, ) -> cross::Result<()> { - let volumes = get_cross_volumes(engine, verbose)?; + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + let volumes = get_cross_volumes(engine, msg_info)?; let mut command = docker::subcommand(engine, "volume"); command.arg("rm"); @@ -263,27 +350,38 @@ pub fn remove_all_volumes( if volumes.is_empty() { Ok(()) } else if execute { - command.run(verbose, false).map_err(Into::into) + command.run(msg_info, false).map_err(Into::into) } else { - eprintln!("Note: this is a dry run. to remove the volumes, pass the `--execute` flag."); - command.print_verbose(true); + shell::note( + "this is a dry run. to remove the volumes, pass the `--execute` flag.", + msg_info, + )?; + command.print(msg_info)?; Ok(()) } } pub fn prune_volumes( PruneVolumes { - verbose, execute, .. + verbose, + quiet, + color, + execute, + .. }: PruneVolumes, engine: &docker::Engine, ) -> cross::Result<()> { + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; let mut command = docker::subcommand(engine, "volume"); command.args(&["prune", "--force"]); if execute { - command.run(verbose, false).map_err(Into::into) + command.run(msg_info, false).map_err(Into::into) } else { - eprintln!("Note: this is a dry run. to prune the volumes, pass the `--execute` flag."); - command.print_verbose(true); + shell::note( + "this is a dry run. to prune the volumes, pass the `--execute` flag.", + msg_info, + )?; + command.print(msg_info)?; Ok(()) } } @@ -293,35 +391,38 @@ pub fn create_persistent_volume( docker_in_docker, copy_registry, verbose, + quiet, + color, .. }: CreateVolume, engine: &docker::Engine, channel: Option<&str>, ) -> cross::Result<()> { + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; // we only need a triple that needs docker: the actual target doesn't matter. let triple = cross::Host::X86_64UnknownLinuxGnu.triple(); let (target, metadata, dirs) = - docker::get_package_info(engine, triple, channel, docker_in_docker, verbose)?; + docker::get_package_info(engine, triple, channel, docker_in_docker, msg_info)?; let container = docker::remote::unique_container_identifier(&target, &metadata, &dirs)?; let volume = docker::remote::unique_toolchain_identifier(&dirs.sysroot)?; - if docker::remote::volume_exists(engine, &volume, verbose)? { + if docker::remote::volume_exists(engine, &volume, msg_info)? { eyre::bail!("Error: volume {volume} already exists."); } docker::subcommand(engine, "volume") .args(&["create", &volume]) - .run_and_get_status(verbose, false)?; + .run_and_get_status(msg_info, false)?; // stop the container if it's already running - let state = docker::remote::container_state(engine, &container, verbose)?; + let state = docker::remote::container_state(engine, &container, msg_info)?; if !state.is_stopped() { - eprintln!("Warning: container {container} was running."); - docker::remote::container_stop(engine, &container, verbose)?; + shell::warn("container {container} was running.", msg_info)?; + docker::remote::container_stop(engine, &container, msg_info)?; } if state.exists() { - eprintln!("Warning: container {container} was exited."); - docker::remote::container_rm(engine, &container, verbose)?; + shell::warn("container {container} was exited.", msg_info)?; + docker::remote::container_rm(engine, &container, msg_info)?; } // create a dummy running container to copy data over @@ -330,13 +431,13 @@ pub fn create_persistent_volume( docker.args(&["--name", &container]); docker.args(&["-v", &format!("{}:{}", volume, mount_prefix)]); docker.arg("-d"); - if atty::is(Stream::Stdin) && atty::is(Stream::Stdout) && atty::is(Stream::Stderr) { + if io::Stdin::is_atty() && io::Stdout::is_atty() && io::Stderr::is_atty() { docker.arg("-t"); } docker.arg(docker::UBUNTU_BASE); // ensure the process never exits until we stop it docker.args(&["sh", "-c", "sleep infinity"]); - docker.run_and_get_status(verbose, false)?; + docker.run_and_get_status(msg_info, false)?; docker::remote::copy_volume_container_xargo( engine, @@ -344,7 +445,7 @@ pub fn create_persistent_volume( &dirs.xargo, &target, mount_prefix.as_ref(), - verbose, + msg_info, )?; docker::remote::copy_volume_container_cargo( engine, @@ -352,7 +453,7 @@ pub fn create_persistent_volume( &dirs.cargo, mount_prefix.as_ref(), copy_registry, - verbose, + msg_info, )?; docker::remote::copy_volume_container_rust( engine, @@ -361,11 +462,11 @@ pub fn create_persistent_volume( &target, mount_prefix.as_ref(), true, - verbose, + msg_info, )?; - docker::remote::container_stop(engine, &container, verbose)?; - docker::remote::container_rm(engine, &container, verbose)?; + docker::remote::container_stop(engine, &container, msg_info)?; + docker::remote::container_rm(engine, &container, msg_info)?; Ok(()) } @@ -375,31 +476,37 @@ pub fn remove_persistent_volume( target, docker_in_docker, verbose, + quiet, + color, .. }: RemoveVolume, engine: &docker::Engine, channel: Option<&str>, ) -> cross::Result<()> { + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; let (_, _, dirs) = - docker::get_package_info(engine, &target, channel, docker_in_docker, verbose)?; + docker::get_package_info(engine, &target, channel, docker_in_docker, msg_info)?; let volume = docker::remote::unique_toolchain_identifier(&dirs.sysroot)?; - if !docker::remote::volume_exists(engine, &volume, verbose)? { + if !docker::remote::volume_exists(engine, &volume, msg_info)? { eyre::bail!("Error: volume {volume} does not exist."); } - docker::remote::volume_rm(engine, &volume, verbose)?; + docker::remote::volume_rm(engine, &volume, msg_info)?; Ok(()) } -fn get_cross_containers(engine: &docker::Engine, verbose: bool) -> cross::Result> { +fn get_cross_containers( + engine: &docker::Engine, + msg_info: MessageInfo, +) -> cross::Result> { let stdout = docker::subcommand(engine, "ps") .arg("-a") .args(&["--format", "{{.Names}}: {{.State}}"]) // handles simple regex: ^ for start of line. .args(&["--filter", "name=^cross-"]) - .run_and_get_stdout(verbose)?; + .run_and_get_stdout(msg_info)?; let mut containers: Vec = stdout.lines().map(|s| s.to_string()).collect(); containers.sort(); @@ -408,12 +515,18 @@ fn get_cross_containers(engine: &docker::Engine, verbose: bool) -> cross::Result } pub fn list_containers( - ListContainers { verbose, .. }: ListContainers, + ListContainers { + verbose, + quiet, + color, + .. + }: ListContainers, engine: &docker::Engine, ) -> cross::Result<()> { - get_cross_containers(engine, verbose)? - .iter() - .for_each(|line| println!("{}", line)); + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + for line in get_cross_containers(engine, msg_info)?.iter() { + shell::print(line, msg_info)?; + } Ok(()) } @@ -421,13 +534,16 @@ pub fn list_containers( pub fn remove_all_containers( RemoveAllContainers { verbose, + quiet, + color, force, execute, .. }: RemoveAllContainers, engine: &docker::Engine, ) -> cross::Result<()> { - let containers = get_cross_containers(engine, verbose)?; + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + let containers = get_cross_containers(engine, msg_info)?; let mut running = vec![]; let mut stopped = vec![]; for container in containers.iter() { @@ -460,12 +576,15 @@ pub fn remove_all_containers( } if execute { for mut command in commands { - command.run(verbose, false)?; + command.run(msg_info, false)?; } } else { - eprintln!("Note: this is a dry run. to remove the containers, pass the `--execute` flag."); + shell::note( + "this is a dry run. to remove the containers, pass the `--execute` flag.", + msg_info, + )?; for command in commands { - command.print_verbose(true); + command.print(msg_info)?; } } diff --git a/src/bin/commands/images.rs b/src/bin/commands/images.rs index e361c018e..46ebec3e7 100644 --- a/src/bin/commands/images.rs +++ b/src/bin/commands/images.rs @@ -1,10 +1,9 @@ use std::collections::{BTreeMap, BTreeSet}; use clap::{Args, Subcommand}; -use cross::{ - docker::{self, CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX}, - CommandExt, TargetList, -}; +use cross::docker::{self, CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX}; +use cross::shell::{self, MessageInfo, Verbosity}; +use cross::{CommandExt, TargetList}; // known image prefixes, with their registry // the docker.io registry can also be implicit @@ -18,6 +17,12 @@ pub struct ListImages { /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Container engine (such as docker or podman). #[clap(long)] pub engine: Option, @@ -38,6 +43,12 @@ pub struct RemoveImages { /// Remove images matching provided targets. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Force removal of images. #[clap(short, long)] pub force: bool, @@ -91,6 +102,20 @@ impl Images { Images::Remove(l) => l.verbose, } } + + pub fn quiet(&self) -> bool { + match self { + Images::List(l) => l.quiet, + Images::Remove(l) => l.quiet, + } + } + + pub fn color(&self) -> Option<&str> { + match self { + Images::List(l) => l.color.as_deref(), + Images::Remove(l) => l.color.as_deref(), + } + } } #[derive(Debug, PartialOrd, Ord, PartialEq, Eq)] @@ -138,7 +163,7 @@ fn is_local_image(tag: &str) -> bool { fn get_cross_images( engine: &docker::Engine, - verbose: bool, + msg_info: MessageInfo, local: bool, ) -> cross::Result> { let mut images: BTreeSet<_> = cross::docker::subcommand(engine, "images") @@ -147,14 +172,14 @@ fn get_cross_images( "--filter", &format!("label={}.for-cross-target", cross::CROSS_LABEL_DOMAIN), ]) - .run_and_get_stdout(verbose)? + .run_and_get_stdout(msg_info)? .lines() .map(parse_image) .collect(); let stdout = cross::docker::subcommand(engine, "images") .args(&["--format", "{{.Repository}}:{{.Tag}} {{.ID}}"]) - .run_and_get_stdout(verbose)?; + .run_and_get_stdout(msg_info)?; let ids: Vec<_> = images.iter().map(|i| i.id.to_string()).collect(); images.extend( stdout @@ -192,6 +217,7 @@ fn get_image_target( engine: &cross::docker::Engine, image: &Image, target_list: &TargetList, + msg_info: MessageInfo, ) -> cross::Result { if let Some(stripped) = image.repository.strip_prefix(&format!("{GHCR_IO}/")) { return Ok(stripped.to_string()); @@ -222,8 +248,7 @@ fn get_image_target( ]); command.arg(&image.id); - // TODO: verbosity = 3? - let target = command.run_and_get_stdout(true)?; + let target = command.run_and_get_stdout(msg_info)?; if target.trim().is_empty() { eyre::bail!("cannot get target for image {}", image) } @@ -232,17 +257,22 @@ fn get_image_target( pub fn list_images( ListImages { - targets, verbose, .. + targets, + verbose, + quiet, + color, + .. }: ListImages, engine: &docker::Engine, ) -> cross::Result<()> { - let cross_images = get_cross_images(engine, verbose, true)?; - let target_list = cross::rustc::target_list(false)?; + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + let cross_images = get_cross_images(engine, msg_info, true)?; + let target_list = cross::rustc::target_list((msg_info.color_choice, Verbosity::Quiet).into())?; let mut map: BTreeMap> = BTreeMap::new(); let mut max_target_len = 0; let mut max_image_len = 0; for image in cross_images { - let target = get_image_target(engine, &image, &target_list)?; + let target = get_image_target(engine, &image, &target_list, msg_info)?; if targets.is_empty() || targets.contains(&target) { if !map.contains_key(&target) { map.insert(target.clone(), vec![]); @@ -255,7 +285,7 @@ pub fn list_images( let mut keys: Vec<&str> = map.iter().map(|(k, _)| k.as_ref()).collect(); keys.sort_unstable(); - let print_string = |col1: &str, col2: &str, fill: char| { + let print_string = |col1: &str, col2: &str, fill: char| -> cross::Result<()> { let mut row = String::new(); row.push('|'); row.push(fill); @@ -272,32 +302,30 @@ pub fn list_images( row.push(fill); } row.push('|'); - println!("{}", row); + shell::print(row, msg_info) }; if targets.len() != 1 { - print_string("Targets", "Images", ' '); - print_string("-------", "------", '-'); + print_string("Targets", "Images", ' ')?; + print_string("-------", "------", '-')?; } - let print_single = |_: &str, image: &Image| println!("{}", image); - let print_table = |target: &str, image: &Image| { + let print_single = + |_: &str, image: &Image| -> cross::Result<()> { shell::print(image, msg_info) }; + let print_table = |target: &str, image: &Image| -> cross::Result<()> { let name = image.name(); - print_string(target, &name, ' '); + print_string(target, &name, ' ') }; - keys.iter().for_each(|&target| { - map.get(target) - .expect("map must have key") - .iter() - .for_each(|image| { - if targets.len() == 1 { - print_single(target, image); - } else { - print_table(target, image); - } - }); - }); + for target in keys { + for image in map.get(target).expect("map must have key").iter() { + if targets.len() == 1 { + print_single(target, image)?; + } else { + print_table(target, image)?; + } + } + } Ok(()) } @@ -305,7 +333,7 @@ pub fn list_images( fn remove_images( engine: &docker::Engine, images: &[Image], - verbose: bool, + msg_info: MessageInfo, force: bool, execute: bool, ) -> cross::Result<()> { @@ -317,10 +345,13 @@ fn remove_images( if images.is_empty() { Ok(()) } else if execute { - command.run(verbose, false).map_err(Into::into) + command.run(msg_info, false).map_err(Into::into) } else { - eprintln!("Note: this is a dry run. to remove the images, pass the `--execute` flag."); - command.print_verbose(true); + shell::note( + "this is a dry run. to remove the images, pass the `--execute` flag.", + msg_info, + )?; + command.print(msg_info)?; Ok(()) } } @@ -328,6 +359,8 @@ fn remove_images( pub fn remove_all_images( RemoveImages { verbose, + quiet, + color, force, local, execute, @@ -335,14 +368,17 @@ pub fn remove_all_images( }: RemoveImages, engine: &docker::Engine, ) -> cross::Result<()> { - let images = get_cross_images(engine, verbose, local)?; - remove_images(engine, &images, verbose, force, execute) + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + let images = get_cross_images(engine, msg_info, local)?; + remove_images(engine, &images, msg_info, force, execute) } pub fn remove_target_images( RemoveImages { targets, verbose, + quiet, + color, force, local, execute, @@ -350,16 +386,17 @@ pub fn remove_target_images( }: RemoveImages, engine: &docker::Engine, ) -> cross::Result<()> { - let cross_images = get_cross_images(engine, verbose, local)?; - let target_list = cross::rustc::target_list(false)?; + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + let cross_images = get_cross_images(engine, msg_info, local)?; + let target_list = cross::rustc::target_list((msg_info.color_choice, Verbosity::Quiet).into())?; let mut images = vec![]; for image in cross_images { - let target = dbg!(get_image_target(engine, &image, &target_list)?); + let target = dbg!(get_image_target(engine, &image, &target_list, msg_info)?); if targets.contains(&target) { images.push(image); } } - remove_images(engine, &images, verbose, force, execute) + remove_images(engine, &images, msg_info, force, execute) } #[cfg(test)] diff --git a/src/bin/cross-util.rs b/src/bin/cross-util.rs index 5f3919faa..e5be6ae18 100644 --- a/src/bin/cross-util.rs +++ b/src/bin/cross-util.rs @@ -2,6 +2,7 @@ use clap::{CommandFactory, Parser, Subcommand}; use cross::docker; +use cross::shell::MessageInfo; mod commands; @@ -46,13 +47,16 @@ fn is_toolchain(toolchain: &str) -> cross::Result { } } -fn get_container_engine(engine: Option<&str>, verbose: bool) -> cross::Result { +fn get_container_engine( + engine: Option<&str>, + msg_info: MessageInfo, +) -> cross::Result { let engine = if let Some(ce) = engine { which::which(ce)? } else { docker::get_container_engine()? }; - docker::Engine::from_path(engine, None, verbose) + docker::Engine::from_path(engine, None, msg_info) } pub fn main() -> cross::Result<()> { @@ -60,19 +64,23 @@ pub fn main() -> cross::Result<()> { let cli = Cli::parse(); match cli.command { Commands::Images(args) => { - let engine = get_container_engine(args.engine(), args.verbose())?; + let msg_info = MessageInfo::create(args.verbose(), args.quiet(), args.color())?; + let engine = get_container_engine(args.engine(), msg_info)?; args.run(engine)?; } Commands::Volumes(args) => { - let engine = get_container_engine(args.engine(), args.verbose())?; + let msg_info = MessageInfo::create(args.verbose(), args.quiet(), args.color())?; + let engine = get_container_engine(args.engine(), msg_info)?; args.run(engine, cli.toolchain.as_deref())?; } Commands::Containers(args) => { - let engine = get_container_engine(args.engine(), args.verbose())?; + let msg_info = MessageInfo::create(args.verbose(), args.quiet(), args.color())?; + let engine = get_container_engine(args.engine(), msg_info)?; args.run(engine)?; } Commands::Clean(args) => { - let engine = get_container_engine(args.engine.as_deref(), args.verbose)?; + let msg_info = MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?; + let engine = get_container_engine(args.engine.as_deref(), msg_info)?; args.run(engine)?; } } diff --git a/src/cargo.rs b/src/cargo.rs index b75d3a5d1..8afe8ca09 100644 --- a/src/cargo.rs +++ b/src/cargo.rs @@ -5,6 +5,7 @@ use std::process::{Command, ExitStatus}; use crate::cli::Args; use crate::errors::*; use crate::extensions::CommandExt; +use crate::shell::MessageInfo; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Subcommand { @@ -119,7 +120,7 @@ pub fn cargo_command() -> Command { pub fn cargo_metadata_with_args( cd: Option<&Path>, args: Option<&Args>, - verbose: bool, + msg_info: MessageInfo, ) -> Result> { let mut command = cargo_command(); command.arg("metadata").args(&["--format-version", "1"]); @@ -139,7 +140,7 @@ pub fn cargo_metadata_with_args( if let Some(features) = args.map(|a| &a.features).filter(|v| !v.is_empty()) { command.args([String::from("--features"), features.join(",")]); } - let output = command.run_and_get_output(verbose)?; + let output = command.run_and_get_output(msg_info)?; if !output.status.success() { // TODO: logging return Ok(None); @@ -158,13 +159,13 @@ pub fn cargo_metadata_with_args( } /// Pass-through mode -pub fn run(args: &[String], verbose: bool) -> Result { +pub fn run(args: &[String], msg_info: MessageInfo) -> Result { cargo_command() .args(args) - .run_and_get_status(verbose, false) + .run_and_get_status(msg_info, false) } /// run cargo and get the output, does not check the exit status -pub fn run_and_get_output(args: &[String], verbose: bool) -> Result { - cargo_command().args(args).run_and_get_output(verbose) +pub fn run_and_get_output(args: &[String], msg_info: MessageInfo) -> Result { + cargo_command().args(args).run_and_get_output(msg_info) } diff --git a/src/cli.rs b/src/cli.rs index ad8d6fa6a..7e8b69ac9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,6 +4,7 @@ use crate::cargo::Subcommand; use crate::config::bool_from_envvar; use crate::errors::Result; use crate::rustc::TargetList; +use crate::shell::{self, MessageInfo}; use crate::Target; #[derive(Debug)] @@ -17,6 +18,8 @@ pub struct Args { pub docker_in_docker: bool, pub enable_doctests: bool, pub manifest_path: Option, + pub version: bool, + pub msg_info: MessageInfo, } // Fix for issue #581. target_dir must be absolute. @@ -49,16 +52,21 @@ pub fn group_subcommands(stdout: &str) -> (Vec<&str>, Vec<&str>) { (cross, host) } -pub fn fmt_subcommands(stdout: &str) { +pub fn fmt_subcommands(stdout: &str, msg_info: MessageInfo) -> Result<()> { let (cross, host) = group_subcommands(stdout); if !cross.is_empty() { - println!("Cross Commands:"); - cross.iter().for_each(|line| println!("{}", line)); + shell::print("Cross Commands:", msg_info)?; + for line in cross.iter() { + shell::print(line, msg_info)?; + } } if !host.is_empty() { - println!("Host Commands:"); - host.iter().for_each(|line| println!("{}", line)); + shell::print("Host Commands:", msg_info)?; + for line in cross.iter() { + shell::print(line, msg_info)?; + } } + Ok(()) } pub fn parse(target_list: &TargetList) -> Result { @@ -69,6 +77,11 @@ pub fn parse(target_list: &TargetList) -> Result { let mut target_dir = None; let mut sc = None; let mut all: Vec = Vec::new(); + let mut version = false; + let mut quiet = false; + let mut verbose = false; + let mut color = None; + let default_msg_info = MessageInfo::default(); { let mut args = env::args().skip(1); @@ -76,7 +89,20 @@ pub fn parse(target_list: &TargetList) -> Result { if arg.is_empty() { continue; } - if arg == "--manifest-path" { + if matches!(arg.as_str(), "--verbose" | "-v" | "-vv") { + verbose = true; + } else if matches!(arg.as_str(), "--version" | "-V") { + version = true; + } else if matches!(arg.as_str(), "--quiet" | "-q") { + quiet = true; + } else if arg == "--color" { + match args.next() { + Some(arg) => color = Some(arg), + None => { + shell::fatal_usage("--color ", default_msg_info, 1); + } + } + } else if arg == "--manifest-path" { all.push(arg); if let Some(m) = args.next() { let p = PathBuf::from(&m); @@ -133,11 +159,13 @@ pub fn parse(target_list: &TargetList) -> Result { } } + let msg_info = shell::MessageInfo::create(verbose, quiet, color.as_deref())?; let docker_in_docker = if let Ok(value) = env::var("CROSS_CONTAINER_IN_CONTAINER") { if env::var("CROSS_DOCKER_IN_DOCKER").is_ok() { - eprintln!( - "Warning: using both `CROSS_CONTAINER_IN_CONTAINER` and `CROSS_DOCKER_IN_DOCKER`." - ); + shell::warn( + "using both `CROSS_CONTAINER_IN_CONTAINER` and `CROSS_DOCKER_IN_DOCKER`.", + msg_info, + )?; } bool_from_envvar(&value) } else if let Ok(value) = env::var("CROSS_DOCKER_IN_DOCKER") { @@ -161,5 +189,7 @@ pub fn parse(target_list: &TargetList) -> Result { docker_in_docker, enable_doctests, manifest_path, + version, + msg_info, }) } diff --git a/src/config.rs b/src/config.rs index 0c29fbb57..4c2ae734d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use crate::shell::{self, MessageInfo}; use crate::{CrossToml, Result, Target, TargetList}; use std::collections::HashMap; @@ -131,7 +132,7 @@ impl Config { } } - pub fn confusable_target(&self, target: &Target) { + pub fn confusable_target(&self, target: &Target, msg_info: MessageInfo) -> Result<()> { if let Some(keys) = self.toml.as_ref().map(|t| t.targets.keys()) { for mentioned_target in keys { let mentioned_target_norm = mentioned_target @@ -143,11 +144,15 @@ impl Config { .replace(|c| c == '-' || c == '_', "") .to_lowercase(); if mentioned_target != target && mentioned_target_norm == target_norm { - eprintln!("Warning: a target named \"{mentioned_target}\" is mentioned in the Cross configuration, but the current specified target is \"{target}\"."); - eprintln!(" > Is the target misspelled in the Cross configuration?"); + shell::warn("a target named \"{mentioned_target}\" is mentioned in the Cross configuration, but the current specified target is \"{target}\".", msg_info)?; + shell::status( + " > Is the target misspelled in the Cross configuration?", + msg_info, + )?; } } } + Ok(()) } fn bool_from_config( @@ -436,7 +441,7 @@ mod tests { } fn toml(content: &str) -> Result { - Ok(CrossToml::parse_from_cross(content) + Ok(CrossToml::parse_from_cross(content, MessageInfo::default()) .wrap_err("couldn't parse toml")? .0) } diff --git a/src/cross_toml.rs b/src/cross_toml.rs index 453359cda..0eed16216 100644 --- a/src/cross_toml.rs +++ b/src/cross_toml.rs @@ -1,5 +1,6 @@ #![doc = include_str!("../docs/cross_toml.md")] +use crate::shell::{self, MessageInfo}; use crate::{config, errors::*}; use crate::{Target, TargetList}; use serde::de::DeserializeOwned; @@ -75,10 +76,14 @@ pub struct CrossToml { impl CrossToml { /// Parses the [`CrossToml`] from all of the config sources - pub fn parse(cargo_toml: &str, cross_toml: &str) -> Result<(Self, BTreeSet)> { - let (cross_toml, mut unused) = Self::parse_from_cross(cross_toml)?; - - if let Some((cargo_toml, u_cargo)) = Self::parse_from_cargo(cargo_toml)? { + pub fn parse( + cargo_toml: &str, + cross_toml: &str, + msg_info: MessageInfo, + ) -> Result<(Self, BTreeSet)> { + let (cross_toml, mut unused) = Self::parse_from_cross(cross_toml, msg_info)?; + + if let Some((cargo_toml, u_cargo)) = Self::parse_from_cargo(cargo_toml, msg_info)? { unused.extend(u_cargo.into_iter()); Ok((cargo_toml.merge(cross_toml)?, unused)) } else { @@ -87,13 +92,19 @@ impl CrossToml { } /// Parses the [`CrossToml`] from a string - pub fn parse_from_cross(toml_str: &str) -> Result<(Self, BTreeSet)> { + pub fn parse_from_cross( + toml_str: &str, + msg_info: MessageInfo, + ) -> Result<(Self, BTreeSet)> { let mut tomld = toml::Deserializer::new(toml_str); - Self::parse_from_deserializer(&mut tomld) + Self::parse_from_deserializer(&mut tomld, msg_info) } /// Parses the [`CrossToml`] from a string containing the Cargo.toml contents - pub fn parse_from_cargo(cargo_toml_str: &str) -> Result)>> { + pub fn parse_from_cargo( + cargo_toml_str: &str, + msg_info: MessageInfo, + ) -> Result)>> { let cargo_toml: toml::Value = toml::from_str(cargo_toml_str)?; let cross_metadata_opt = cargo_toml .get("package") @@ -101,14 +112,20 @@ impl CrossToml { .and_then(|m| m.get("cross")); if let Some(cross_meta) = cross_metadata_opt { - Ok(Some(Self::parse_from_deserializer(cross_meta.clone())?)) + Ok(Some(Self::parse_from_deserializer( + cross_meta.clone(), + msg_info, + )?)) } else { Ok(None) } } /// Parses the [`CrossToml`] from a [`Deserializer`] - fn parse_from_deserializer<'de, D>(deserializer: D) -> Result<(Self, BTreeSet)> + fn parse_from_deserializer<'de, D>( + deserializer: D, + msg_info: MessageInfo, + ) -> Result<(Self, BTreeSet)> where D: Deserializer<'de>, D::Error: Send + Sync + 'static, @@ -119,10 +136,13 @@ impl CrossToml { })?; if !unused.is_empty() { - eprintln!( - "Warning: found unused key(s) in Cross configuration:\n > {}", - unused.clone().into_iter().collect::>().join(", ") - ); + shell::warn( + format!( + "found unused key(s) in Cross configuration:\n > {}", + unused.clone().into_iter().collect::>().join(", ") + ), + msg_info, + )?; } Ok((cfg, unused)) @@ -385,6 +405,10 @@ where #[cfg(test)] mod tests { use super::*; + const MSG_INFO: MessageInfo = MessageInfo { + color_choice: shell::ColorChoice::Never, + verbosity: shell::Verbosity::Quiet, + }; macro_rules! s { ($x:literal) => { @@ -398,7 +422,7 @@ mod tests { targets: HashMap::new(), build: CrossBuildConfig::default(), }; - let (parsed_cfg, unused) = CrossToml::parse_from_cross("")?; + let (parsed_cfg, unused) = CrossToml::parse_from_cross("", MSG_INFO)?; assert_eq!(parsed_cfg, cfg); assert!(unused.is_empty()); @@ -432,7 +456,7 @@ mod tests { volumes = ["VOL1_ARG", "VOL2_ARG"] passthrough = ["VAR1", "VAR2"] "#; - let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str)?; + let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, MSG_INFO)?; assert_eq!(parsed_cfg, cfg); assert!(unused.is_empty()); @@ -476,7 +500,7 @@ mod tests { image = "test-image" pre-build = [] "#; - let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str)?; + let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, MSG_INFO)?; assert_eq!(parsed_cfg, cfg); assert!(unused.is_empty()); @@ -540,7 +564,7 @@ mod tests { [target.aarch64-unknown-linux-gnu.env] volumes = ["VOL"] "#; - let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str)?; + let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, MSG_INFO)?; assert_eq!(parsed_cfg, cfg); assert!(unused.is_empty()); @@ -559,7 +583,7 @@ mod tests { cross = "1.2.3" "#; - let res = CrossToml::parse_from_cargo(test_str)?; + let res = CrossToml::parse_from_cargo(test_str, MSG_INFO)?; assert!(res.is_none()); Ok(()) @@ -594,7 +618,7 @@ mod tests { xargo = true "#; - if let Some((parsed_cfg, _unused)) = CrossToml::parse_from_cargo(test_str)? { + if let Some((parsed_cfg, _unused)) = CrossToml::parse_from_cargo(test_str, MSG_INFO)? { assert_eq!(parsed_cfg, cfg); } else { panic!("Parsing result is None"); @@ -702,9 +726,9 @@ mod tests { "#; // Parses configs - let (cfg1, _) = CrossToml::parse_from_cross(cfg1_str)?; - let (cfg2, _) = CrossToml::parse_from_cross(cfg2_str)?; - let (cfg_expected, _) = CrossToml::parse_from_cross(cfg_expected_str)?; + let (cfg1, _) = CrossToml::parse_from_cross(cfg1_str, MSG_INFO)?; + let (cfg2, _) = CrossToml::parse_from_cross(cfg2_str, MSG_INFO)?; + let (cfg_expected, _) = CrossToml::parse_from_cross(cfg_expected_str, MSG_INFO)?; // Merges config and compares let cfg_merged = cfg1.merge(cfg2)?; diff --git a/src/docker/custom.rs b/src/docker/custom.rs index 5e4b32d58..5079de5ea 100644 --- a/src/docker/custom.rs +++ b/src/docker/custom.rs @@ -2,6 +2,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use crate::docker::Engine; +use crate::shell::MessageInfo; use crate::{config::Config, docker, CargoMetadata, Target}; use crate::{errors::*, file, CommandExt, ToUtf8}; @@ -31,7 +32,7 @@ impl<'a> Dockerfile<'a> { host_root: &Path, build_args: impl IntoIterator, impl AsRef)>, target_triple: &Target, - verbose: bool, + msg_info: MessageInfo, ) -> Result { let mut docker_build = docker::subcommand(engine, "build"); docker_build.current_dir(host_root); @@ -100,7 +101,7 @@ impl<'a> Dockerfile<'a> { docker_build.arg("."); } - docker_build.run(verbose, true)?; + docker_build.run(msg_info, true)?; Ok(image_name) } diff --git a/src/docker/engine.rs b/src/docker/engine.rs index 7c1f6924c..37a15dd38 100644 --- a/src/docker/engine.rs +++ b/src/docker/engine.rs @@ -5,6 +5,7 @@ use std::process::Command; use crate::config::bool_from_envvar; use crate::errors::*; use crate::extensions::CommandExt; +use crate::shell::MessageInfo; pub const DOCKER: &str = "docker"; pub const PODMAN: &str = "podman"; @@ -25,15 +26,19 @@ pub struct Engine { } impl Engine { - pub fn new(is_remote: Option, verbose: bool) -> Result { + pub fn new(is_remote: Option, msg_info: MessageInfo) -> Result { let path = get_container_engine() .map_err(|_| eyre::eyre!("no container engine found")) .with_suggestion(|| "is docker or podman installed?")?; - Self::from_path(path, is_remote, verbose) + Self::from_path(path, is_remote, msg_info) } - pub fn from_path(path: PathBuf, is_remote: Option, verbose: bool) -> Result { - let kind = get_engine_type(&path, verbose)?; + pub fn from_path( + path: PathBuf, + is_remote: Option, + msg_info: MessageInfo, + ) -> Result { + let kind = get_engine_type(&path, msg_info)?; let is_remote = is_remote.unwrap_or_else(Self::is_remote); Ok(Engine { path, @@ -55,10 +60,10 @@ impl Engine { // determine if the container engine is docker. this fixes issues with // any aliases (#530), and doesn't fail if an executable suffix exists. -fn get_engine_type(ce: &Path, verbose: bool) -> Result { +fn get_engine_type(ce: &Path, msg_info: MessageInfo) -> Result { let stdout = Command::new(ce) .arg("--help") - .run_and_get_stdout(verbose)? + .run_and_get_stdout(msg_info)? .to_lowercase(); if stdout.contains("podman-remote") { diff --git a/src/docker/local.rs b/src/docker/local.rs index f56c49e98..7282a3232 100644 --- a/src/docker/local.rs +++ b/src/docker/local.rs @@ -1,3 +1,4 @@ +use std::io; use std::path::Path; use std::process::ExitStatus; @@ -7,8 +8,8 @@ use crate::cargo::CargoMetadata; use crate::errors::Result; use crate::extensions::CommandExt; use crate::file::{PathExt, ToUtf8}; +use crate::shell::{MessageInfo, Stream}; use crate::{Config, Target}; -use atty::Stream; use eyre::Context; #[allow(clippy::too_many_arguments)] // TODO: refactor @@ -20,18 +21,18 @@ pub(crate) fn run( config: &Config, uses_xargo: bool, sysroot: &Path, - verbose: bool, + msg_info: MessageInfo, docker_in_docker: bool, cwd: &Path, ) -> Result { - let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker, verbose)?; + let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker)?; let mut cmd = cargo_safe_command(uses_xargo); cmd.args(args); let mut docker = subcommand(engine, "run"); docker.args(&["--userns", "host"]); - docker_envvars(&mut docker, config, target)?; + docker_envvars(&mut docker, config, target, msg_info)?; let mount_volumes = docker_mount( &mut docker, @@ -45,7 +46,7 @@ pub(crate) fn run( docker.arg("--rm"); - docker_seccomp(&mut docker, engine.kind, target, metadata, verbose)?; + docker_seccomp(&mut docker, engine.kind, target, metadata)?; docker_user_id(&mut docker, engine.kind); docker @@ -75,21 +76,21 @@ pub(crate) fn run( ]); } - if atty::is(Stream::Stdin) { + if io::Stdin::is_atty() { docker.arg("-i"); - if atty::is(Stream::Stdout) && atty::is(Stream::Stderr) { + if io::Stdout::is_atty() && io::Stderr::is_atty() { docker.arg("-t"); } } let mut image = image_name(config, target)?; if needs_custom_image(target, config) { - image = custom_image_build(target, config, metadata, dirs, engine, verbose) + image = custom_image_build(target, config, metadata, dirs, engine, msg_info) .wrap_err("when building custom image")? } docker .arg(&image) .args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)]) - .run_and_get_status(verbose, false) + .run_and_get_status(msg_info, false) .map_err(Into::into) } diff --git a/src/docker/mod.rs b/src/docker/mod.rs index f55a97802..b341736f4 100644 --- a/src/docker/mod.rs +++ b/src/docker/mod.rs @@ -12,6 +12,7 @@ use std::process::ExitStatus; use crate::cargo::CargoMetadata; use crate::errors::*; +use crate::shell::MessageInfo; use crate::{Config, Target}; #[allow(clippy::too_many_arguments)] // TODO: refactor @@ -23,7 +24,7 @@ pub fn run( config: &Config, uses_xargo: bool, sysroot: &Path, - verbose: bool, + msg_info: MessageInfo, docker_in_docker: bool, cwd: &Path, ) -> Result { @@ -36,7 +37,7 @@ pub fn run( config, uses_xargo, sysroot, - verbose, + msg_info, docker_in_docker, cwd, ) @@ -49,7 +50,7 @@ pub fn run( config, uses_xargo, sysroot, - verbose, + msg_info, docker_in_docker, cwd, ) diff --git a/src/docker/remote.rs b/src/docker/remote.rs index 96780f2ae..2167ed513 100644 --- a/src/docker/remote.rs +++ b/src/docker/remote.rs @@ -13,14 +13,14 @@ use crate::extensions::CommandExt; use crate::file::{self, PathExt, ToUtf8}; use crate::rustc::{self, VersionMetaExt}; use crate::rustup; +use crate::shell::{self, MessageInfo, Stream}; use crate::temp; use crate::{Host, Target}; -use atty::Stream; // the mount directory for the data volume. pub const MOUNT_PREFIX: &str = "/cross"; -struct DeleteVolume<'a>(&'a Engine, &'a VolumeId, bool); +struct DeleteVolume<'a>(&'a Engine, &'a VolumeId, MessageInfo); impl<'a> Drop for DeleteVolume<'a> { fn drop(&mut self) { @@ -30,7 +30,7 @@ impl<'a> Drop for DeleteVolume<'a> { } } -struct DeleteContainer<'a>(&'a Engine, &'a str, bool); +struct DeleteContainer<'a>(&'a Engine, &'a str, MessageInfo); impl<'a> Drop for DeleteContainer<'a> { fn drop(&mut self) { @@ -80,8 +80,13 @@ enum VolumeId { } impl VolumeId { - fn create(engine: &Engine, toolchain: &str, container: &str, verbose: bool) -> Result { - if volume_exists(engine, toolchain, verbose)? { + fn create( + engine: &Engine, + toolchain: &str, + container: &str, + msg_info: MessageInfo, + ) -> Result { + if volume_exists(engine, toolchain, msg_info)? { Ok(Self::Keep(toolchain.to_string())) } else { Ok(Self::Discard(container.to_string())) @@ -102,13 +107,13 @@ fn create_volume_dir( engine: &Engine, container: &str, dir: &Path, - verbose: bool, + msg_info: MessageInfo, ) -> Result { // make our parent directory if needed subcommand(engine, "exec") .arg(container) .args(&["sh", "-c", &format!("mkdir -p '{}'", dir.as_posix()?)]) - .run_and_get_status(verbose, false) + .run_and_get_status(msg_info, false) .map_err(Into::into) } @@ -118,13 +123,13 @@ fn copy_volume_files( container: &str, src: &Path, dst: &Path, - verbose: bool, + msg_info: MessageInfo, ) -> Result { subcommand(engine, "cp") .arg("-a") .arg(src.to_utf8()?) .arg(format!("{container}:{}", dst.as_posix()?)) - .run_and_get_status(verbose, false) + .run_and_get_status(msg_info, false) .map_err(Into::into) } @@ -151,12 +156,12 @@ fn container_path_exists( engine: &Engine, container: &str, path: &Path, - verbose: bool, + msg_info: MessageInfo, ) -> Result { Ok(subcommand(engine, "exec") .arg(container) .args(&["bash", "-c", &format!("[[ -d '{}' ]]", path.as_posix()?)]) - .run_and_get_status(verbose, true)? + .run_and_get_status(msg_info, true)? .success()) } @@ -166,7 +171,7 @@ fn copy_volume_files_nocache( container: &str, src: &Path, dst: &Path, - verbose: bool, + msg_info: MessageInfo, ) -> Result { // avoid any cached directories when copying // see https://bford.info/cachedir/ @@ -174,7 +179,7 @@ fn copy_volume_files_nocache( let tempdir = unsafe { temp::TempDir::new()? }; let temppath = tempdir.path(); copy_dir(src, temppath, 0, |e, _| is_cachedir(e))?; - copy_volume_files(engine, container, temppath, dst, verbose) + copy_volume_files(engine, container, temppath, dst, msg_info) } pub fn copy_volume_container_xargo( @@ -183,7 +188,7 @@ pub fn copy_volume_container_xargo( xargo_dir: &Path, target: &Target, mount_prefix: &Path, - verbose: bool, + msg_info: MessageInfo, ) -> Result<()> { // only need to copy the rustlib files for our current target. let triple = target.triple(); @@ -191,8 +196,8 @@ pub fn copy_volume_container_xargo( let src = xargo_dir.join(&relpath); let dst = mount_prefix.join("xargo").join(&relpath); if Path::new(&src).exists() { - create_volume_dir(engine, container, dst.parent().unwrap(), verbose)?; - copy_volume_files(engine, container, &src, &dst, verbose)?; + create_volume_dir(engine, container, dst.parent().unwrap(), msg_info)?; + copy_volume_files(engine, container, &src, &dst, msg_info)?; } Ok(()) @@ -204,7 +209,7 @@ pub fn copy_volume_container_cargo( cargo_dir: &Path, mount_prefix: &Path, copy_registry: bool, - verbose: bool, + msg_info: MessageInfo, ) -> Result<()> { let dst = mount_prefix.join("cargo"); let copy_registry = env::var("CROSS_REMOTE_COPY_REGISTRY") @@ -212,15 +217,15 @@ pub fn copy_volume_container_cargo( .unwrap_or(copy_registry); if copy_registry { - copy_volume_files(engine, container, cargo_dir, &dst, verbose)?; + copy_volume_files(engine, container, cargo_dir, &dst, msg_info)?; } else { // can copy a limit subset of files: the rest is present. - create_volume_dir(engine, container, &dst, verbose)?; + create_volume_dir(engine, container, &dst, msg_info)?; for entry in fs::read_dir(cargo_dir)? { let file = entry?; let basename = file.file_name().to_utf8()?.to_string(); if !basename.starts_with('.') && !matches!(basename.as_ref(), "git" | "registry") { - copy_volume_files(engine, container, &file.path(), &dst, verbose)?; + copy_volume_files(engine, container, &file.path(), &dst, msg_info)?; } } } @@ -258,16 +263,16 @@ fn copy_volume_container_rust_base( container: &str, sysroot: &Path, mount_prefix: &Path, - verbose: bool, + msg_info: MessageInfo, ) -> Result<()> { // the rust toolchain is quite large, but most of it isn't needed // we need the bin, libexec, and etc directories, and part of the lib directory. let dst = mount_prefix.join("rust"); let rustlib = Path::new("lib").join("rustlib"); - create_volume_dir(engine, container, &dst.join(&rustlib), verbose)?; + create_volume_dir(engine, container, &dst.join(&rustlib), msg_info)?; for basename in ["bin", "libexec", "etc"] { let file = sysroot.join(basename); - copy_volume_files(engine, container, &file, &dst, verbose)?; + copy_volume_files(engine, container, &file, &dst, msg_info)?; } // the lib directories are rather large, so we want only a subset. @@ -290,7 +295,7 @@ fn copy_volume_container_rust_base( 0, |e, d| d == 0 && !(e.file_name() == "src" || e.file_name() == "etc"), )?; - copy_volume_files(engine, container, &temppath.join("lib"), &dst, verbose)?; + copy_volume_files(engine, container, &temppath.join("lib"), &dst, msg_info)?; Ok(()) } @@ -300,7 +305,7 @@ fn copy_volume_container_rust_manifest( container: &str, sysroot: &Path, mount_prefix: &Path, - verbose: bool, + msg_info: MessageInfo, ) -> Result<()> { // copy over all the manifest files in rustlib // these are small text files containing names/paths to toolchains @@ -317,7 +322,7 @@ fn copy_volume_container_rust_manifest( 0, |e, d| d != 0 || e.file_type().map(|t| !t.is_file()).unwrap_or(true), )?; - copy_volume_files(engine, container, &temppath.join("lib"), &dst, verbose)?; + copy_volume_files(engine, container, &temppath.join("lib"), &dst, msg_info)?; Ok(()) } @@ -330,7 +335,7 @@ pub fn copy_volume_container_rust_triple( triple: &str, mount_prefix: &Path, skip_exists: bool, - verbose: bool, + msg_info: MessageInfo, ) -> Result<()> { // copy over the files for a specific triple let dst = mount_prefix.join("rust"); @@ -343,15 +348,15 @@ pub fn copy_volume_container_rust_triple( // or the first run of the target toolchain, we know it doesn't exist. let mut skip = false; if skip_exists { - skip = container_path_exists(engine, container, &dst_toolchain, verbose)?; + skip = container_path_exists(engine, container, &dst_toolchain, msg_info)?; } if !skip { - copy_volume_files(engine, container, &src_toolchain, &dst_rustlib, verbose)?; + copy_volume_files(engine, container, &src_toolchain, &dst_rustlib, msg_info)?; } if !skip && skip_exists { // this means we have a persistent data volume and we have a // new target, meaning we might have new manifests as well. - copy_volume_container_rust_manifest(engine, container, sysroot, mount_prefix, verbose)?; + copy_volume_container_rust_manifest(engine, container, sysroot, mount_prefix, msg_info)?; } Ok(()) @@ -364,13 +369,13 @@ pub fn copy_volume_container_rust( target: &Target, mount_prefix: &Path, skip_target: bool, - verbose: bool, + msg_info: MessageInfo, ) -> Result<()> { let target_triple = target.triple(); let image_triple = Host::X86_64UnknownLinuxGnu.triple(); - copy_volume_container_rust_base(engine, container, sysroot, mount_prefix, verbose)?; - copy_volume_container_rust_manifest(engine, container, sysroot, mount_prefix, verbose)?; + copy_volume_container_rust_base(engine, container, sysroot, mount_prefix, msg_info)?; + copy_volume_container_rust_manifest(engine, container, sysroot, mount_prefix, msg_info)?; copy_volume_container_rust_triple( engine, container, @@ -378,7 +383,7 @@ pub fn copy_volume_container_rust( image_triple, mount_prefix, false, - verbose, + msg_info, )?; if !skip_target && target_triple != image_triple { copy_volume_container_rust_triple( @@ -388,7 +393,7 @@ pub fn copy_volume_container_rust( target_triple, mount_prefix, false, - verbose, + msg_info, )?; } @@ -495,7 +500,7 @@ fn copy_volume_file_list( src: &Path, dst: &Path, files: &[&str], - verbose: bool, + msg_info: MessageInfo, ) -> Result { // SAFETY: safe, single-threaded execution. let tempdir = unsafe { temp::TempDir::new()? }; @@ -506,7 +511,7 @@ fn copy_volume_file_list( fs::create_dir_all(dst_path.parent().expect("must have parent"))?; fs::copy(&src_path, &dst_path)?; } - copy_volume_files(engine, container, temppath, dst, verbose) + copy_volume_files(engine, container, temppath, dst, msg_info) } // removed files from a docker volume, for remote host support @@ -516,11 +521,11 @@ fn remove_volume_file_list( container: &str, dst: &Path, files: &[&str], - verbose: bool, + msg_info: MessageInfo, ) -> Result { const PATH: &str = "/tmp/remove_list"; let mut script = vec![]; - if verbose { + if msg_info.verbose() { script.push("set -x".to_string()); } script.push(format!( @@ -543,12 +548,12 @@ rm \"{PATH}\" subcommand(engine, "cp") .arg(tempfile.path()) .arg(format!("{container}:{PATH}")) - .run_and_get_status(verbose, true)?; + .run_and_get_status(msg_info, true)?; subcommand(engine, "exec") .arg(container) .args(&["sh", "-c", &script.join("\n")]) - .run_and_get_status(verbose, true) + .run_and_get_status(msg_info, true) .map_err(Into::into) } @@ -559,13 +564,13 @@ fn copy_volume_container_project( dst: &Path, volume: &VolumeId, copy_cache: bool, - verbose: bool, + msg_info: MessageInfo, ) -> Result<()> { let copy_all = || { if copy_cache { - copy_volume_files(engine, container, src, dst, verbose) + copy_volume_files(engine, container, src, dst, msg_info) } else { - copy_volume_files_nocache(engine, container, src, dst, verbose) + copy_volume_files_nocache(engine, container, src, dst, msg_info) } }; match volume { @@ -580,10 +585,10 @@ fn copy_volume_container_project( write_project_fingerprint(&fingerprint, ¤t)?; if !changed.is_empty() { - copy_volume_file_list(engine, container, src, dst, &changed, verbose)?; + copy_volume_file_list(engine, container, src, dst, &changed, msg_info)?; } if !removed.is_empty() { - remove_volume_file_list(engine, container, dst, &removed, verbose)?; + remove_volume_file_list(engine, container, dst, &removed, msg_info)?; } } else { write_project_fingerprint(&fingerprint, ¤t)?; @@ -598,43 +603,51 @@ fn copy_volume_container_project( Ok(()) } -fn run_and_get_status(engine: &Engine, args: &[&str], verbose: bool) -> Result { +fn run_and_get_status(engine: &Engine, args: &[&str], msg_info: MessageInfo) -> Result { command(engine) .args(args) - .run_and_get_status(verbose, true) + .run_and_get_status(msg_info, true) .map_err(Into::into) } -pub fn volume_create(engine: &Engine, volume: &str, verbose: bool) -> Result { - run_and_get_status(engine, &["volume", "create", volume], verbose) +pub fn volume_create(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Result { + run_and_get_status(engine, &["volume", "create", volume], msg_info) } -pub fn volume_rm(engine: &Engine, volume: &str, verbose: bool) -> Result { - run_and_get_status(engine, &["volume", "rm", volume], verbose) +pub fn volume_rm(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Result { + run_and_get_status(engine, &["volume", "rm", volume], msg_info) } -pub fn volume_exists(engine: &Engine, volume: &str, verbose: bool) -> Result { +pub fn volume_exists(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Result { command(engine) .args(&["volume", "inspect", volume]) - .run_and_get_output(verbose) + .run_and_get_output(msg_info) .map(|output| output.status.success()) .map_err(Into::into) } -pub fn container_stop(engine: &Engine, container: &str, verbose: bool) -> Result { - run_and_get_status(engine, &["stop", container], verbose) +pub fn container_stop( + engine: &Engine, + container: &str, + msg_info: MessageInfo, +) -> Result { + run_and_get_status(engine, &["stop", container], msg_info) } -pub fn container_rm(engine: &Engine, container: &str, verbose: bool) -> Result { - run_and_get_status(engine, &["rm", container], verbose) +pub fn container_rm(engine: &Engine, container: &str, msg_info: MessageInfo) -> Result { + run_and_get_status(engine, &["rm", container], msg_info) } -pub fn container_state(engine: &Engine, container: &str, verbose: bool) -> Result { +pub fn container_state( + engine: &Engine, + container: &str, + msg_info: MessageInfo, +) -> Result { let stdout = command(engine) .args(&["ps", "-a"]) .args(&["--filter", &format!("name={container}")]) .args(&["--format", "{{.State}}"]) - .run_and_get_stdout(verbose)?; + .run_and_get_stdout(msg_info)?; ContainerState::new(stdout.trim()) } @@ -693,11 +706,11 @@ pub(crate) fn run( config: &Config, uses_xargo: bool, sysroot: &Path, - verbose: bool, + msg_info: MessageInfo, docker_in_docker: bool, cwd: &Path, ) -> Result { - let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker, verbose)?; + let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker)?; let mount_prefix = MOUNT_PREFIX; @@ -723,35 +736,35 @@ pub(crate) fn run( // this can happen if we didn't gracefully exit before let toolchain_id = unique_toolchain_identifier(&dirs.sysroot)?; let container = unique_container_identifier(target, metadata, &dirs)?; - let volume = VolumeId::create(engine, &toolchain_id, &container, verbose)?; - let state = container_state(engine, &container, verbose)?; + let volume = VolumeId::create(engine, &toolchain_id, &container, msg_info)?; + let state = container_state(engine, &container, msg_info)?; if !state.is_stopped() { - eprintln!("Warning: container {container} was running."); - container_stop(engine, &container, verbose)?; + shell::warn("container {container} was running.", msg_info)?; + container_stop(engine, &container, msg_info)?; } if state.exists() { - eprintln!("Warning: container {container} was exited."); - container_rm(engine, &container, verbose)?; + shell::warn("container {container} was exited.", msg_info)?; + container_rm(engine, &container, msg_info)?; } if let VolumeId::Discard(ref id) = volume { - if volume_exists(engine, id, verbose)? { - eprintln!("Warning: temporary volume {container} existed."); - volume_rm(engine, id, verbose)?; + if volume_exists(engine, id, msg_info)? { + shell::warn("temporary volume {container} existed.", msg_info)?; + volume_rm(engine, id, msg_info)?; } } // 2. create our volume to copy all our data over to if let VolumeId::Discard(ref id) = volume { - volume_create(engine, id, verbose)?; + volume_create(engine, id, msg_info)?; } - let _volume_deletter = DeleteVolume(engine, &volume, verbose); + let _volume_deletter = DeleteVolume(engine, &volume, msg_info); // 3. create our start container command here let mut docker = subcommand(engine, "run"); docker.args(&["--userns", "host"]); docker.args(&["--name", &container]); docker.args(&["-v", &format!("{}:{mount_prefix}", volume.as_ref())]); - docker_envvars(&mut docker, config, target)?; + docker_envvars(&mut docker, config, target, msg_info)?; let mut volumes = vec![]; let mount_volumes = docker_mount( @@ -764,7 +777,7 @@ pub(crate) fn run( |(src, dst)| volumes.push((src, dst)), )?; - docker_seccomp(&mut docker, engine.kind, target, metadata, verbose)?; + docker_seccomp(&mut docker, engine.kind, target, metadata)?; // Prevent `bin` from being mounted inside the Docker container. docker.args(&["-v", &format!("{mount_prefix}/cargo/bin")]); @@ -777,7 +790,7 @@ pub(crate) fn run( } docker.arg("-d"); - if atty::is(Stream::Stdin) && atty::is(Stream::Stdout) && atty::is(Stream::Stderr) { + if io::Stdin::is_atty() && io::Stdout::is_atty() && io::Stderr::is_atty() { docker.arg("-t"); } @@ -785,8 +798,8 @@ pub(crate) fn run( .arg(&image_name(config, target)?) // ensure the process never exits until we stop it .args(&["sh", "-c", "sleep infinity"]) - .run_and_get_status(verbose, true)?; - let _container_deletter = DeleteContainer(engine, &container, verbose); + .run_and_get_status(msg_info, true)?; + let _container_deletter = DeleteContainer(engine, &container, msg_info); // 4. copy all mounted volumes over let copy_cache = env::var("CROSS_REMOTE_COPY_CACHE") @@ -794,9 +807,9 @@ pub(crate) fn run( .unwrap_or_default(); let copy = |src, dst: &PathBuf| { if copy_cache { - copy_volume_files(engine, &container, src, dst, verbose) + copy_volume_files(engine, &container, src, dst, msg_info) } else { - copy_volume_files_nocache(engine, &container, src, dst, verbose) + copy_volume_files_nocache(engine, &container, src, dst, msg_info) } }; let mount_prefix_path = mount_prefix.as_ref(); @@ -807,7 +820,7 @@ pub(crate) fn run( &dirs.xargo, target, mount_prefix_path, - verbose, + msg_info, )?; copy_volume_container_cargo( engine, @@ -815,7 +828,7 @@ pub(crate) fn run( &dirs.cargo, mount_prefix_path, false, - verbose, + msg_info, )?; copy_volume_container_rust( engine, @@ -824,7 +837,7 @@ pub(crate) fn run( target, mount_prefix_path, false, - verbose, + msg_info, )?; } else { // need to copy over the target triple if it hasn't been previously copied @@ -835,7 +848,7 @@ pub(crate) fn run( target.triple(), mount_prefix_path, true, - verbose, + msg_info, )?; } let mount_root = if mount_volumes { @@ -843,7 +856,7 @@ pub(crate) fn run( let rel_mount_root = dirs.mount_root.strip_prefix('/').unwrap(); let mount_root = mount_prefix_path.join(rel_mount_root); if !rel_mount_root.is_empty() { - create_volume_dir(engine, &container, mount_root.parent().unwrap(), verbose)?; + create_volume_dir(engine, &container, mount_root.parent().unwrap(), msg_info)?; } mount_root } else { @@ -856,7 +869,7 @@ pub(crate) fn run( &mount_root, &volume, copy_cache, - verbose, + msg_info, )?; let mut copied = vec![ @@ -876,7 +889,7 @@ pub(crate) fn run( if copy_cache { copy(&dirs.target, &target_dir)?; } else { - create_volume_dir(engine, &container, &target_dir, verbose)?; + create_volume_dir(engine, &container, &target_dir, msg_info)?; } copied.push((&dirs.target, target_dir.clone())); @@ -892,7 +905,7 @@ pub(crate) fn run( let rel_dst = dst.strip_prefix('/').unwrap(); let mount_dst = mount_prefix_path.join(rel_dst); if !rel_dst.is_empty() { - create_volume_dir(engine, &container, mount_dst.parent().unwrap(), verbose)?; + create_volume_dir(engine, &container, mount_dst.parent().unwrap(), msg_info)?; } copy(src, &mount_dst)?; } @@ -930,7 +943,7 @@ pub(crate) fn run( // 5. create symlinks for copied data let mut symlink = vec!["set -e pipefail".to_string()]; - if verbose { + if msg_info.verbose() { symlink.push("set -x".to_string()); } symlink.push(format!( @@ -965,7 +978,7 @@ symlink_recurse \"${{prefix}}\" subcommand(engine, "exec") .arg(&container) .args(&["sh", "-c", &symlink.join("\n")]) - .run_and_get_status(verbose, false) + .run_and_get_status(msg_info, false) .map_err::(Into::into)?; // 6. execute our cargo command inside the container @@ -975,17 +988,17 @@ symlink_recurse \"${{prefix}}\" docker.arg(&container); docker.args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)]); let status = docker - .run_and_get_status(verbose, false) + .run_and_get_status(msg_info, false) .map_err(Into::into); // 7. copy data from our target dir back to host // this might not exist if we ran `clean`. - if container_path_exists(engine, &container, &target_dir, verbose)? { + if container_path_exists(engine, &container, &target_dir, msg_info)? { subcommand(engine, "cp") .arg("-a") .arg(&format!("{container}:{}", target_dir.as_posix()?)) .arg(&dirs.target.parent().unwrap()) - .run_and_get_status(verbose, false) + .run_and_get_status(msg_info, false) .map_err::(Into::into)?; } diff --git a/src/docker/shared.rs b/src/docker/shared.rs index 766d7d1f9..8288f3e30 100644 --- a/src/docker/shared.rs +++ b/src/docker/shared.rs @@ -12,6 +12,7 @@ use crate::extensions::{CommandExt, SafeCommand}; use crate::file::{self, write_file, PathExt, ToUtf8}; use crate::id; use crate::rustc::{self, VersionMetaExt}; +use crate::shell::{self, MessageInfo, Verbosity}; use crate::Target; pub use super::custom::CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX; @@ -41,14 +42,12 @@ pub struct Directories { } impl Directories { - #[allow(unused_variables)] pub fn create( engine: &Engine, metadata: &CargoMetadata, cwd: &Path, sysroot: &Path, docker_in_docker: bool, - verbose: bool, ) -> Result { let mount_finder = if docker_in_docker { MountFinder::new(docker_read_mount_paths(engine)?) @@ -158,23 +157,23 @@ pub fn get_package_info( target: &str, channel: Option<&str>, docker_in_docker: bool, - verbose: bool, + msg_info: MessageInfo, ) -> Result<(Target, CargoMetadata, Directories)> { - let target_list = rustc::target_list(false)?; + let target_list = rustc::target_list((msg_info.color_choice, Verbosity::Quiet).into())?; let target = Target::from(target, &target_list); - let metadata = cargo_metadata_with_args(None, None, verbose)? + let metadata = cargo_metadata_with_args(None, None, msg_info)? .ok_or(eyre::eyre!("unable to get project metadata"))?; let cwd = std::env::current_dir()?; let host_meta = rustc::version_meta()?; let host = host_meta.host(); - let sysroot = rustc::get_sysroot(&host, &target, channel, verbose)?.1; - let dirs = Directories::create(engine, &metadata, &cwd, &sysroot, docker_in_docker, verbose)?; + let sysroot = rustc::get_sysroot(&host, &target, channel, msg_info)?.1; + let dirs = Directories::create(engine, &metadata, &cwd, &sysroot, docker_in_docker)?; Ok((target, metadata, dirs)) } /// Register binfmt interpreters -pub(crate) fn register(engine: &Engine, target: &Target, verbose: bool) -> Result<()> { +pub(crate) fn register(engine: &Engine, target: &Target, msg_info: MessageInfo) -> Result<()> { let cmd = if target.is_windows() { // https://www.kernel.org/doc/html/latest/admin-guide/binfmt-misc.html "mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc && \ @@ -190,7 +189,7 @@ pub(crate) fn register(engine: &Engine, target: &Target, verbose: bool) -> Resul .arg("--rm") .arg(UBUNTU_BASE) .args(&["sh", "-c", cmd]) - .run(verbose, false) + .run(msg_info, false) .map_err(Into::into) } @@ -265,7 +264,12 @@ pub(crate) fn mount(docker: &mut Command, val: &Path, prefix: &str) -> Result Result<()> { +pub(crate) fn docker_envvars( + docker: &mut Command, + config: &Config, + target: &Target, + msg_info: MessageInfo, +) -> Result<()> { for ref var in config.env_passthrough(target)?.unwrap_or_default() { validate_env_var(var)?; @@ -298,7 +302,10 @@ pub(crate) fn docker_envvars(docker: &mut Command, config: &Config, target: &Tar if let Ok(value) = env::var("CROSS_CONTAINER_OPTS") { if env::var("DOCKER_OPTS").is_ok() { - eprintln!("Warning: using both `CROSS_CONTAINER_OPTS` and `DOCKER_OPTS`."); + shell::warn( + "using both `CROSS_CONTAINER_OPTS` and `DOCKER_OPTS`.", + msg_info, + )?; } docker.args(&parse_docker_opts(&value)?); } else if let Ok(value) = env::var("DOCKER_OPTS") { @@ -398,13 +405,12 @@ pub(crate) fn docker_user_id(docker: &mut Command, engine_type: EngineType) { } } -#[allow(unused_variables, unused_mut, clippy::let_and_return)] +#[allow(unused_mut, clippy::let_and_return)] pub(crate) fn docker_seccomp( docker: &mut Command, engine_type: EngineType, target: &Target, metadata: &CargoMetadata, - verbose: bool, ) -> Result<()> { // docker uses seccomp now on all installations if target.needs_docker_seccomp() { @@ -451,7 +457,7 @@ pub(crate) fn custom_image_build( metadata: &CargoMetadata, Directories { host_root, .. }: Directories, engine: &Engine, - verbose: bool, + msg_info: MessageInfo, ) -> Result { let mut image = image_name(config, target)?; @@ -473,7 +479,7 @@ pub(crate) fn custom_image_build( &host_root, config.dockerfile_build_args(target)?.unwrap_or_default(), target, - verbose, + msg_info, ) .wrap_err("when building dockerfile")?; } @@ -498,7 +504,7 @@ pub(crate) fn custom_image_build( &host_root, Some(("CROSS_CMD", pre_build.join("\n"))), target, - verbose, + msg_info, ) .wrap_err("when pre-building") .with_note(|| format!("CROSS_CMD={}", pre_build.join("\n")))?; @@ -538,7 +544,7 @@ fn docker_read_mount_paths(engine: &Engine) -> Result> { command }; - let output = docker.run_and_get_stdout(false)?; + let output = docker.run_and_get_stdout(Verbosity::Quiet.into())?; let info = serde_json::from_str(&output).wrap_err("failed to parse docker inspect output")?; dockerinfo_parse_mounts(&info) } diff --git a/src/extensions.rs b/src/extensions.rs index 8948c1f71..64f194c54 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -3,32 +3,54 @@ use std::fmt; use std::process::{Command, ExitStatus, Output}; use crate::errors::*; +use crate::shell::{self, MessageInfo, Verbosity}; pub const STRIPPED_BINS: &[&str] = &[crate::docker::DOCKER, crate::docker::PODMAN, "cargo"]; pub trait CommandExt { - fn print_verbose(&self, verbose: bool); + fn fmt_message(&self, msg_info: MessageInfo) -> String; + + fn print(&self, msg_info: MessageInfo) -> Result<()> { + shell::print(&self.fmt_message(msg_info), msg_info) + } + + fn info(&self, msg_info: MessageInfo) -> Result<()> { + shell::info(&self.fmt_message(msg_info), msg_info) + } + + fn debug(&self, msg_info: MessageInfo) -> Result<()> { + shell::debug(&self.fmt_message(msg_info), msg_info) + } + fn status_result( &self, - verbose: bool, + msg_info: MessageInfo, status: ExitStatus, output: Option<&Output>, ) -> Result<(), CommandError>; - fn run(&mut self, verbose: bool, silence_stdout: bool) -> Result<(), CommandError>; + fn run(&mut self, msg_info: MessageInfo, silence_stdout: bool) -> Result<()>; fn run_and_get_status( &mut self, - verbose: bool, + msg_info: MessageInfo, silence_stdout: bool, - ) -> Result; - fn run_and_get_stdout(&mut self, verbose: bool) -> Result; - fn run_and_get_output(&mut self, verbose: bool) -> Result; - fn command_pretty(&self, verbose: bool, strip: impl for<'a> Fn(&'a str) -> bool) -> String; + ) -> Result; + fn run_and_get_stdout(&mut self, msg_info: MessageInfo) -> Result; + fn run_and_get_output(&mut self, msg_info: MessageInfo) -> Result; + fn command_pretty( + &self, + msg_info: MessageInfo, + strip: impl for<'a> Fn(&'a str) -> bool, + ) -> String; } impl CommandExt for Command { - fn command_pretty(&self, verbose: bool, strip: impl for<'a> Fn(&'a str) -> bool) -> String { + fn command_pretty( + &self, + msg_info: MessageInfo, + strip: impl for<'a> Fn(&'a str) -> bool, + ) -> String { // a dummy implementor of display to avoid using unwraps - struct C<'c, F>(&'c Command, bool, F); + struct C<'c, F>(&'c Command, MessageInfo, F); impl std::fmt::Display for C<'_, F> where F: for<'a> Fn(&'a str) -> bool, @@ -39,7 +61,7 @@ impl CommandExt for Command { f, "{}", // if verbose, never strip, if not, let user choose - crate::file::pretty_path(cmd.get_program(), move |c| if self.1 { + crate::file::pretty_path(cmd.get_program(), move |c| if self.1.verbose() { false } else { self.2(c) @@ -56,23 +78,21 @@ impl CommandExt for Command { Ok(()) } } - format!("{}", C(self, verbose, strip)) + format!("{}", C(self, msg_info, strip)) } - fn print_verbose(&self, verbose: bool) { - // TODO: introduce verbosity levels, v = 1, strip cmd, v > 1, don't strip cmd - if verbose { - if let Some(cwd) = self.get_current_dir() { - println!("+ {:?} {}", cwd, self.command_pretty(true, |_| false)); - } else { - println!("+ {}", self.command_pretty(true, |_| false)); - } + fn fmt_message(&self, msg_info: MessageInfo) -> String { + let verbose = (msg_info.color_choice, Verbosity::Verbose).into(); + if let Some(cwd) = self.get_current_dir() { + format!("+ {:?} {}", cwd, self.command_pretty(verbose, |_| false)) + } else { + format!("+ {}", self.command_pretty(verbose, |_| false)) } } fn status_result( &self, - verbose: bool, + msg_info: MessageInfo, status: ExitStatus, output: Option<&Output>, ) -> Result<(), CommandError> { @@ -82,7 +102,7 @@ impl CommandExt for Command { Err(CommandError::NonZeroExitCode { status, command: self - .command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)), + .command_pretty(msg_info, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)), stderr: output.map(|out| out.stderr.clone()).unwrap_or_default(), stdout: output.map(|out| out.stdout.clone()).unwrap_or_default(), }) @@ -90,34 +110,35 @@ impl CommandExt for Command { } /// Runs the command to completion - fn run(&mut self, verbose: bool, silence_stdout: bool) -> Result<(), CommandError> { - let status = self.run_and_get_status(verbose, silence_stdout)?; - self.status_result(verbose, status, None) + fn run(&mut self, msg_info: MessageInfo, silence_stdout: bool) -> Result<()> { + let status = self.run_and_get_status(msg_info, silence_stdout)?; + self.status_result(msg_info, status, None) + .map_err(Into::into) } /// Runs the command to completion fn run_and_get_status( &mut self, - verbose: bool, + msg_info: MessageInfo, silence_stdout: bool, - ) -> Result { - self.print_verbose(verbose); - if silence_stdout && !verbose { + ) -> Result { + self.debug(msg_info)?; + if silence_stdout && !msg_info.verbose() { self.stdout(std::process::Stdio::null()); } self.status() .map_err(|e| CommandError::CouldNotExecute { source: Box::new(e), command: self - .command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)), + .command_pretty(msg_info, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)), }) .map_err(Into::into) } /// Runs the command to completion and returns its stdout - fn run_and_get_stdout(&mut self, verbose: bool) -> Result { - let out = self.run_and_get_output(verbose)?; - self.status_result(verbose, out.status, Some(&out)) + fn run_and_get_stdout(&mut self, msg_info: MessageInfo) -> Result { + let out = self.run_and_get_output(msg_info)?; + self.status_result(msg_info, out.status, Some(&out)) .map_err(CommandError::to_section_report)?; out.stdout().map_err(Into::into) } @@ -127,13 +148,13 @@ impl CommandExt for Command { /// # Notes /// /// This command does not check the status. - fn run_and_get_output(&mut self, verbose: bool) -> Result { - self.print_verbose(verbose); + fn run_and_get_output(&mut self, msg_info: MessageInfo) -> Result { + self.debug(msg_info)?; self.output().map_err(|e| { CommandError::CouldNotExecute { source: Box::new(e), command: self - .command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)), + .command_pretty(msg_info, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)), } .to_section_report() }) diff --git a/src/lib.rs b/src/lib.rs index 08f2fe3f9..c3db3954d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ mod id; mod interpreter; pub mod rustc; mod rustup; +pub mod shell; pub mod temp; use std::env; @@ -43,6 +44,7 @@ use serde::{Deserialize, Serialize, Serializer}; pub use self::cargo::{cargo_command, cargo_metadata_with_args, CargoMetadata, Subcommand}; use self::cross_toml::CrossToml; use self::errors::Context; +use self::shell::{MessageInfo, Verbosity}; pub use self::errors::{install_panic_hook, install_termination_hook, Result}; pub use self::extensions::{CommandExt, OutputExt}; @@ -334,7 +336,10 @@ impl From for Target { Host::Aarch64AppleDarwin => Target::new_built_in("aarch64-apple-darwin"), Host::Aarch64UnknownLinuxGnu => Target::new_built_in("aarch64-unknown-linux-gnu"), Host::Aarch64UnknownLinuxMusl => Target::new_built_in("aarch64-unknown-linux-musl"), - Host::Other(s) => Target::from(s.as_str(), &rustc::target_list(false).unwrap()), + Host::Other(s) => Target::from( + s.as_str(), + &rustc::target_list(Verbosity::Quiet.into()).unwrap(), + ), } } } @@ -356,50 +361,49 @@ impl Serialize for Target { } pub fn run() -> Result { - let target_list = rustc::target_list(false)?; + let target_list = rustc::target_list(Verbosity::Quiet.into())?; let args = cli::parse(&target_list)?; - if args.all.iter().any(|a| a == "--version" || a == "-V") && args.subcommand.is_none() { - println!( - concat!("cross ", env!("CARGO_PKG_VERSION"), "{}"), - include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")) - ); + if args.version && args.subcommand.is_none() { + let commit_info = include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")); + shell::print( + format!( + concat!("cross ", env!("CARGO_PKG_VERSION"), "{}"), + commit_info + ), + args.msg_info, + )?; } - let verbose = args - .all - .iter() - .any(|a| a == "--verbose" || a == "-v" || a == "-vv"); - let host_version_meta = rustc::version_meta()?; let cwd = std::env::current_dir()?; - if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), verbose)? { + if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), args.msg_info)? { let host = host_version_meta.host(); - let toml = toml(&metadata)?; + let toml = toml(&metadata, args.msg_info)?; let config = Config::new(toml); let target = args .target .or_else(|| config.target(&target_list)) .unwrap_or_else(|| Target::from(host.triple(), &target_list)); - config.confusable_target(&target); + config.confusable_target(&target, args.msg_info)?; let image_exists = match docker::image_name(&config, &target) { Ok(_) => true, Err(err) => { - eprintln!("Warning: {}", err); + shell::warn(err, args.msg_info)?; false } }; if image_exists && host.is_supported(Some(&target)) { let (toolchain, sysroot) = - rustc::get_sysroot(&host, &target, args.channel.as_deref(), verbose)?; + rustc::get_sysroot(&host, &target, args.channel.as_deref(), args.msg_info)?; let mut is_nightly = toolchain.contains("nightly"); - let installed_toolchains = rustup::installed_toolchains(verbose)?; + let installed_toolchains = rustup::installed_toolchains(args.msg_info)?; if !installed_toolchains.into_iter().any(|t| t == toolchain) { - rustup::install_toolchain(&toolchain, verbose)?; + rustup::install_toolchain(&toolchain, args.msg_info)?; } // TODO: Provide a way to pick/match the toolchain version as a consumer of `cross`. if let Some((rustc_version, channel, rustc_commit)) = rustup::rustc_version(&sysroot)? { @@ -408,6 +412,7 @@ pub fn run() -> Result { &toolchain, &rustc_version, &rustc_commit, + args.msg_info, )?; is_nightly = channel == Channel::Nightly; } @@ -418,7 +423,7 @@ pub fn run() -> Result { if std::env::var("CROSS_CUSTOM_TOOLCHAIN").is_err() { // build-std overrides xargo, but only use it if it's a built-in // tool but not an available target or doesn't have rust-std. - let available_targets = rustup::available_targets(&toolchain, verbose)?; + let available_targets = rustup::available_targets(&toolchain, args.msg_info)?; if !is_nightly && uses_build_std { eyre::bail!( @@ -431,17 +436,17 @@ pub fn run() -> Result { && !available_targets.is_installed(&target) && available_targets.contains(&target) { - rustup::install(&target, &toolchain, verbose)?; - } else if !rustup::component_is_installed("rust-src", &toolchain, verbose)? { - rustup::install_component("rust-src", &toolchain, verbose)?; + rustup::install(&target, &toolchain, args.msg_info)?; + } else if !rustup::component_is_installed("rust-src", &toolchain, args.msg_info)? { + rustup::install_component("rust-src", &toolchain, args.msg_info)?; } if args .subcommand .map(|sc| sc == Subcommand::Clippy) .unwrap_or(false) - && !rustup::component_is_installed("clippy", &toolchain, verbose)? + && !rustup::component_is_installed("clippy", &toolchain, args.msg_info)? { - rustup::install_component("clippy", &toolchain, verbose)?; + rustup::install_component("clippy", &toolchain, args.msg_info)?; } } @@ -493,13 +498,13 @@ pub fn run() -> Result { .map(|sc| sc.needs_docker(is_remote)) .unwrap_or(false); if target.needs_docker() && needs_docker { - let engine = docker::Engine::new(Some(is_remote), verbose)?; + let engine = docker::Engine::new(Some(is_remote), args.msg_info)?; if host_version_meta.needs_interpreter() && needs_interpreter && target.needs_interpreter() && !interpreter::is_registered(&target)? { - docker::register(&engine, &target, verbose)? + docker::register(&engine, &target, args.msg_info)? } let status = docker::run( @@ -510,7 +515,7 @@ pub fn run() -> Result { &config, uses_xargo, &sysroot, - verbose, + args.msg_info, args.docker_in_docker, &cwd, )?; @@ -527,14 +532,14 @@ pub fn run() -> Result { // if we fallback to the host cargo, use the same invocation that was made to cross let argv: Vec = env::args().skip(1).collect(); - eprintln!("Warning: Falling back to `cargo` on the host."); + shell::note("Falling back to `cargo` on the host.", args.msg_info)?; match args.subcommand { Some(Subcommand::List) => { // this won't print in order if we have both stdout and stderr. - let out = cargo::run_and_get_output(&argv, verbose)?; + let out = cargo::run_and_get_output(&argv, args.msg_info)?; let stdout = out.stdout()?; if out.status.success() && cli::is_subcommand_list(&stdout) { - cli::fmt_subcommands(&stdout); + cli::fmt_subcommands(&stdout, args.msg_info)?; } else { // Not a list subcommand, which can happen with weird edge-cases. print!("{}", stdout); @@ -542,7 +547,7 @@ pub fn run() -> Result { } Ok(out.status) } - _ => cargo::run(&argv, verbose).map_err(Into::into), + _ => cargo::run(&argv, args.msg_info).map_err(Into::into), } } @@ -559,6 +564,7 @@ pub(crate) fn warn_host_version_mismatch( toolchain: &str, rustc_version: &rustc_version::Version, rustc_commit: &str, + msg_info: MessageInfo, ) -> Result { let host_commit = (&host_version_meta.short_version_string) .splitn(3, ' ') @@ -577,13 +583,16 @@ pub(crate) fn warn_host_version_mismatch( host_version_meta.short_version_string ); if versions.is_lt() || (versions.is_eq() && dates.is_lt()) { - eprintln!("Warning: using older {rustc_warning}.\n > Update with `rustup update --force-non-host {toolchain}`"); + shell::warn(format!("using older {rustc_warning}.\n > Update with `rustup update --force-non-host {toolchain}`"), msg_info)?; return Ok(VersionMatch::OlderTarget); } else if versions.is_gt() || (versions.is_eq() && dates.is_gt()) { - eprintln!("Warning: using newer {rustc_warning}.\n > Update with `rustup update`"); + shell::warn( + format!("using newer {rustc_warning}.\n > Update with `rustup update`"), + msg_info, + )?; return Ok(VersionMatch::NewerTarget); } else { - eprintln!("Warning: using {rustc_warning}."); + shell::warn(format!("using {rustc_warning}."), msg_info)?; return Ok(VersionMatch::Different); } } @@ -599,7 +608,7 @@ pub(crate) fn warn_host_version_mismatch( /// /// The values from `CROSS_CONFIG` or `Cross.toml` are concatenated with the package /// metadata in `Cargo.toml`, with `Cross.toml` having the highest priority. -fn toml(metadata: &CargoMetadata) -> Result> { +fn toml(metadata: &CargoMetadata, msg_info: MessageInfo) -> Result> { let root = &metadata.workspace_root; let cross_config_path = match env::var("CROSS_CONFIG") { Ok(var) => PathBuf::from(var), @@ -614,17 +623,17 @@ fn toml(metadata: &CargoMetadata) -> Result> { let cross_toml_str = file::read(&cross_config_path) .wrap_err_with(|| format!("could not read file `{cross_config_path:?}`"))?; - let (config, _) = CrossToml::parse(&cargo_toml_str, &cross_toml_str) + let (config, _) = CrossToml::parse(&cargo_toml_str, &cross_toml_str, msg_info) .wrap_err_with(|| format!("failed to parse file `{cross_config_path:?}` as TOML",))?; Ok(Some(config)) } else { // Checks if there is a lowercase version of this file if root.join("cross.toml").exists() { - eprintln!("There's a file named cross.toml, instead of Cross.toml. You may want to rename it, or it won't be considered."); + shell::warn("There's a file named cross.toml, instead of Cross.toml. You may want to rename it, or it won't be considered.", msg_info)?; } - if let Some((cfg, _)) = CrossToml::parse_from_cargo(&cargo_toml_str)? { + if let Some((cfg, _)) = CrossToml::parse_from_cargo(&cargo_toml_str, msg_info)? { Ok(Some(cfg)) } else { Ok(None) diff --git a/src/rustc.rs b/src/rustc.rs index 661355912..fe5cb135c 100644 --- a/src/rustc.rs +++ b/src/rustc.rs @@ -5,6 +5,7 @@ use rustc_version::{Version, VersionMeta}; use crate::errors::*; use crate::extensions::{env_program, CommandExt}; +use crate::shell::MessageInfo; use crate::{Host, Target}; #[derive(Debug)] @@ -80,19 +81,19 @@ pub fn rustc_command() -> Command { Command::new(env_program("RUSTC", "rustc")) } -pub fn target_list(verbose: bool) -> Result { +pub fn target_list(msg_info: MessageInfo) -> Result { rustc_command() .args(&["--print", "target-list"]) - .run_and_get_stdout(verbose) + .run_and_get_stdout(msg_info) .map(|s| TargetList { triples: s.lines().map(|l| l.to_owned()).collect(), }) } -pub fn sysroot(host: &Host, target: &Target, verbose: bool) -> Result { +pub fn sysroot(host: &Host, target: &Target, msg_info: MessageInfo) -> Result { let mut stdout = rustc_command() .args(&["--print", "sysroot"]) - .run_and_get_stdout(verbose)? + .run_and_get_stdout(msg_info)? .trim() .to_owned(); @@ -108,9 +109,9 @@ pub fn get_sysroot( host: &Host, target: &Target, channel: Option<&str>, - verbose: bool, + msg_info: MessageInfo, ) -> Result<(String, PathBuf)> { - let mut sysroot = sysroot(host, target, verbose)?; + let mut sysroot = sysroot(host, target, msg_info)?; let default_toolchain = sysroot .file_name() .and_then(|file_name| file_name.to_str()) diff --git a/src/rustup.rs b/src/rustup.rs index ed54412d4..5825bb9f9 100644 --- a/src/rustup.rs +++ b/src/rustup.rs @@ -5,6 +5,7 @@ use rustc_version::{Channel, Version}; use crate::errors::*; pub use crate::extensions::{CommandExt, OutputExt}; +use crate::shell::{MessageInfo, Verbosity}; use crate::Target; #[derive(Debug)] @@ -26,10 +27,24 @@ impl AvailableTargets { } } -pub fn installed_toolchains(verbose: bool) -> Result> { +fn rustup_command(msg_info: MessageInfo) -> Command { + let mut cmd = Command::new("rustup"); + match msg_info.verbosity { + Verbosity::Quiet => { + cmd.arg("--quiet"); + } + Verbosity::Verbose => { + cmd.arg("--verbose"); + } + _ => (), + } + cmd +} + +pub fn installed_toolchains(msg_info: MessageInfo) -> Result> { let out = Command::new("rustup") .args(&["toolchain", "list"]) - .run_and_get_stdout(verbose)?; + .run_and_get_stdout(msg_info)?; Ok(out .lines() @@ -42,11 +57,11 @@ pub fn installed_toolchains(verbose: bool) -> Result> { .collect()) } -pub fn available_targets(toolchain: &str, verbose: bool) -> Result { +pub fn available_targets(toolchain: &str, msg_info: MessageInfo) -> Result { let mut cmd = Command::new("rustup"); cmd.args(&["target", "list", "--toolchain", toolchain]); let output = cmd - .run_and_get_output(verbose) + .run_and_get_output(msg_info) .suggestion("is rustup installed?")?; if !output.status.success() { @@ -54,7 +69,7 @@ pub fn available_targets(toolchain: &str, verbose: bool) -> Result Result Result<()> { - Command::new("rustup") +pub fn install_toolchain(toolchain: &str, msg_info: MessageInfo) -> Result<()> { + rustup_command(msg_info) .args(&["toolchain", "add", toolchain, "--profile", "minimal"]) - .run(verbose, false) + .run(msg_info, false) .wrap_err_with(|| format!("couldn't install toolchain `{toolchain}`")) } -pub fn install(target: &Target, toolchain: &str, verbose: bool) -> Result<()> { +pub fn install(target: &Target, toolchain: &str, msg_info: MessageInfo) -> Result<()> { let target = target.triple(); - Command::new("rustup") + rustup_command(msg_info) .args(&["target", "add", target, "--toolchain", toolchain]) - .run(verbose, false) + .run(msg_info, false) .wrap_err_with(|| format!("couldn't install `std` for {target}")) } -pub fn install_component(component: &str, toolchain: &str, verbose: bool) -> Result<()> { - Command::new("rustup") +pub fn install_component(component: &str, toolchain: &str, msg_info: MessageInfo) -> Result<()> { + rustup_command(msg_info) .args(&["component", "add", component, "--toolchain", toolchain]) - .run(verbose, false) + .run(msg_info, false) .wrap_err_with(|| format!("couldn't install the `{component}` component")) } -pub fn component_is_installed(component: &str, toolchain: &str, verbose: bool) -> Result { +pub fn component_is_installed( + component: &str, + toolchain: &str, + msg_info: MessageInfo, +) -> Result { Ok(Command::new("rustup") .args(&["component", "list", "--toolchain", toolchain]) - .run_and_get_stdout(verbose)? + .run_and_get_stdout(msg_info)? .lines() .any(|l| l.starts_with(component) && l.contains("installed"))) } diff --git a/src/shell.rs b/src/shell.rs new file mode 100644 index 000000000..14af76222 --- /dev/null +++ b/src/shell.rs @@ -0,0 +1,310 @@ +// This file was adapted from: +// https://github.com/rust-lang/cargo/blob/ca4edabb28fc96fdf2a1d56fe3851831ac166f8a/src/cargo/core/shell.rs + +use std::fmt; +use std::io::{self, Write}; + +use crate::errors::Result; +use owo_colors::{self, OwoColorize}; + +/// the requested verbosity of output. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Verbosity { + Quiet, + Normal, + Verbose, +} + +impl Verbosity { + pub fn verbose(self) -> bool { + match self { + Self::Verbose => true, + Self::Normal | Self::Quiet => false, + } + } +} + +/// Whether messages should use color output +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColorChoice { + /// force color output + Always, + /// force disable color output + Never, + /// intelligently guess whether to use color output + Auto, +} + +// Should simplify the APIs a lot. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MessageInfo { + pub color_choice: ColorChoice, + pub verbosity: Verbosity, +} + +impl MessageInfo { + pub fn create(verbose: bool, quiet: bool, color: Option<&str>) -> Result { + let color_choice = get_color_choice(color)?; + let verbosity = get_verbosity(color_choice, verbose, quiet)?; + + Ok(MessageInfo { + color_choice, + verbosity, + }) + } + + pub fn verbose(self) -> bool { + self.verbosity.verbose() + } +} + +impl Default for MessageInfo { + fn default() -> MessageInfo { + MessageInfo { + color_choice: ColorChoice::Auto, + verbosity: Verbosity::Normal, + } + } +} + +impl From for MessageInfo { + fn from(color_choice: ColorChoice) -> MessageInfo { + MessageInfo { + color_choice, + verbosity: Verbosity::Normal, + } + } +} + +impl From for MessageInfo { + fn from(verbosity: Verbosity) -> MessageInfo { + MessageInfo { + color_choice: ColorChoice::Auto, + verbosity, + } + } +} + +impl From<(ColorChoice, Verbosity)> for MessageInfo { + fn from(info: (ColorChoice, Verbosity)) -> MessageInfo { + MessageInfo { + color_choice: info.0, + verbosity: info.1, + } + } +} + +// get the prefix for stderr messages +macro_rules! cross_prefix { + ($s:literal) => { + concat!("[cross]", " ", $s) + }; +} + +// generate the color style +macro_rules! write_style { + ($stream:ident, $msg_info:expr, $message:expr $(, $style:ident)* $(,)?) => {{ + match $msg_info.color_choice { + ColorChoice::Always => write!($stream, "{}", $message $(.$style())*), + ColorChoice::Never => write!($stream, "{}", $message), + ColorChoice::Auto => write!( + $stream, + "{}", + $message $(.if_supports_color($stream.owo(), |text| text.$style()))* + ), + }?; + }}; +} + +// low-level interface for printing colorized messages +macro_rules! message { + // write a status message, which has the following format: + // "{status}: {message}" + // both status and ':' are bold. + (@status $stream:ident, $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{ + write_style!($stream, $msg_info, $status, bold, $color); + write_style!($stream, $msg_info, ":", bold); + match $message { + Some(message) => writeln!($stream, " {}", message)?, + None => write!($stream, " ")?, + } + + Ok(()) + }}; + + (@status @name $name:ident, $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{ + let mut stream = io::$name(); + message!(@status stream, $status, $message, $color, $msg_info) + }}; +} + +// high-level interface to message +macro_rules! status { + (@stderr $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{ + message!(@status @name stderr, $status, $message, $color, $msg_info) + }}; + + (@stdout $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{ + message!(@status @name stdout, $status, $message, $color, $msg_info) + }}; +} + +/// prints a red 'error' message and terminates. +pub fn fatal(message: T, msg_info: MessageInfo, code: i32) -> ! { + error(message, msg_info).unwrap(); + std::process::exit(code); +} + +/// prints a red 'error' message. +pub fn error(message: T, msg_info: MessageInfo) -> Result<()> { + status!(@stderr cross_prefix!("error"), Some(&message), red, msg_info) +} + +/// prints an amber 'warning' message. +pub fn warn(message: T, msg_info: MessageInfo) -> Result<()> { + match msg_info.verbosity { + Verbosity::Quiet => Ok(()), + _ => status!(@stderr + cross_prefix!("warning"), + Some(&message), + yellow, + msg_info, + ), + } +} + +/// prints a cyan 'note' message. +pub fn note(message: T, msg_info: MessageInfo) -> Result<()> { + match msg_info.verbosity { + Verbosity::Quiet => Ok(()), + _ => status!(@stderr cross_prefix!("note"), Some(&message), cyan, msg_info), + } +} + +pub fn status(message: T, msg_info: MessageInfo) -> Result<()> { + match msg_info.verbosity { + Verbosity::Quiet => Ok(()), + _ => { + eprintln!("{}", message); + Ok(()) + } + } +} + +/// prints a high-priority message to stdout. +pub fn print(message: T, _: MessageInfo) -> Result<()> { + println!("{}", message); + Ok(()) +} + +/// prints a normal message to stdout. +pub fn info(message: T, msg_info: MessageInfo) -> Result<()> { + match msg_info.verbosity { + Verbosity::Quiet => Ok(()), + _ => { + println!("{}", message); + Ok(()) + } + } +} + +/// prints a debugging message to stdout. +pub fn debug(message: T, msg_info: MessageInfo) -> Result<()> { + match msg_info.verbosity { + Verbosity::Quiet | Verbosity::Normal => Ok(()), + _ => { + println!("{}", message); + Ok(()) + } + } +} + +pub fn fatal_usage(arg: T, msg_info: MessageInfo, code: i32) -> ! { + error_usage(arg, msg_info).unwrap(); + std::process::exit(code); +} + +fn error_usage(arg: T, msg_info: MessageInfo) -> Result<()> { + let mut stream = io::stderr(); + write_style!(stream, msg_info, cross_prefix!("error"), bold, red); + write_style!(stream, msg_info, ":", bold); + write_style!(stream, msg_info, " The argument '"); + write_style!(stream, msg_info, arg, yellow); + write_style!( + stream, + msg_info, + "' requires a value but none was supplied\n" + ); + write_style!(stream, msg_info, "Usage:\n"); + write_style!( + stream, + msg_info, + " cross [+toolchain] [OPTIONS] [SUBCOMMAND]\n" + ); + write_style!(stream, msg_info, "\n"); + write_style!(stream, msg_info, "For more information try "); + write_style!(stream, msg_info, "--help", green); + write_style!(stream, msg_info, "\n"); + + stream.flush()?; + + Ok(()) +} + +fn get_color_choice(color: Option<&str>) -> Result { + match color { + Some("always") => Ok(ColorChoice::Always), + Some("never") => Ok(ColorChoice::Never), + Some("auto") | None => Ok(ColorChoice::Auto), + Some(arg) => { + eyre::bail!("argument for --color must be auto, always, or never, but found `{arg}`") + } + } +} + +fn get_verbosity(color_choice: ColorChoice, verbose: bool, quiet: bool) -> Result { + match (verbose, quiet) { + (true, true) => { + let verbosity = Verbosity::Normal; + error( + "cannot set both --verbose and --quiet", + MessageInfo { + color_choice, + verbosity, + }, + )?; + std::process::exit(101); + } + (true, false) => Ok(Verbosity::Verbose), + (false, true) => Ok(Verbosity::Quiet), + (false, false) => Ok(Verbosity::Normal), + } +} + +pub trait Stream { + const TTY: atty::Stream; + const OWO: owo_colors::Stream; + + fn is_atty() -> bool { + atty::is(Self::TTY) + } + + fn owo(&self) -> owo_colors::Stream { + Self::OWO + } +} + +impl Stream for io::Stdin { + const TTY: atty::Stream = atty::Stream::Stdin; + const OWO: owo_colors::Stream = owo_colors::Stream::Stdin; +} + +impl Stream for io::Stdout { + const TTY: atty::Stream = atty::Stream::Stdout; + const OWO: owo_colors::Stream = owo_colors::Stream::Stdout; +} + +impl Stream for io::Stderr { + const TTY: atty::Stream = atty::Stream::Stderr; + const OWO: owo_colors::Stream = owo_colors::Stream::Stderr; +} diff --git a/src/tests.rs b/src/tests.rs index 92b59babb..98f30be44 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -14,10 +14,14 @@ static WORKSPACE: OnceCell = OnceCell::new(); pub fn get_cargo_workspace() -> &'static Path { let manifest_dir = env!("CARGO_MANIFEST_DIR"); WORKSPACE.get_or_init(|| { - crate::cargo_metadata_with_args(Some(manifest_dir.as_ref()), None, true) - .unwrap() - .unwrap() - .workspace_root + crate::cargo_metadata_with_args( + Some(manifest_dir.as_ref()), + None, + crate::shell::Verbosity::Verbose.into(), + ) + .unwrap() + .unwrap() + .workspace_root }) } @@ -71,7 +75,14 @@ release: {version} let target_meta = dbg!(make_rustc_version(targ)); assert_eq!( expected, - warn_host_version_mismatch(&host_meta, "xxxx", &target_meta.0, &target_meta.1).unwrap(), + warn_host_version_mismatch( + &host_meta, + "xxxx", + &target_meta.0, + &target_meta.1, + crate::shell::MessageInfo::default() + ) + .unwrap(), "\nhost = {}\ntarg = {}", host, targ diff --git a/src/tests/toml.rs b/src/tests/toml.rs index da5369a58..45017553d 100644 --- a/src/tests/toml.rs +++ b/src/tests/toml.rs @@ -41,11 +41,12 @@ fn toml_check() -> Result<(), Box> { dir_entry.path(), text_line_no(&contents, fence.range().start), ); - assert!( - crate::cross_toml::CrossToml::parse_from_cross(fence.as_str())? - .1 - .is_empty() - ); + assert!(crate::cross_toml::CrossToml::parse_from_cross( + fence.as_str(), + crate::shell::MessageInfo::default() + )? + .1 + .is_empty()); } } Ok(()) diff --git a/xtask/src/build_docker_image.rs b/xtask/src/build_docker_image.rs index 6e33e1e64..302b66304 100644 --- a/xtask/src/build_docker_image.rs +++ b/xtask/src/build_docker_image.rs @@ -1,67 +1,74 @@ +use std::fmt::Write; use std::path::Path; +use crate::util::{gha_error, gha_output, gha_print}; use clap::Args; -use color_eyre::Section; +use cross::shell::{self, MessageInfo}; use cross::{docker, CommandExt, ToUtf8}; -use std::fmt::Write; #[derive(Args, Debug)] pub struct BuildDockerImage { #[clap(long, hide = true, env = "GITHUB_REF_TYPE")] - ref_type: Option, + pub ref_type: Option, #[clap(long, hide = true, env = "GITHUB_REF_NAME")] ref_name: Option, #[clap(action, long = "latest", hide = true, env = "LATEST")] is_latest: bool, /// Specify a tag to use instead of the derived one, eg `local` #[clap(long)] - tag: Option, + pub tag: Option, /// Repository name for image. #[clap(long, default_value = docker::CROSS_IMAGE)] - repository: String, + pub repository: String, /// Newline separated labels #[clap(long, env = "LABELS")] - labels: Option, + pub labels: Option, /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Print but do not execute the build commands. #[clap(long)] - dry_run: bool, + pub dry_run: bool, /// Force a push when `--push` is set, but not `--tag` #[clap(long, hide = true)] - force: bool, + pub force: bool, /// Push build to registry. #[clap(short, long)] - push: bool, + pub push: bool, /// Set output to /dev/null #[clap(short, long)] - no_output: bool, + pub no_output: bool, /// Docker build progress output type. #[clap( long, value_parser = clap::builder::PossibleValuesParser::new(["auto", "plain", "tty"]), default_value = "auto" )] - progress: String, + pub progress: String, /// Do not load from cache when building the image. #[clap(long)] - no_cache: bool, + pub no_cache: bool, /// Continue building images even if an image fails to build. #[clap(long)] - no_fastfail: bool, + pub no_fastfail: bool, /// Container engine (such as docker or podman). #[clap(long)] pub engine: Option, /// If no target list is provided, parse list from CI. #[clap(long)] - from_ci: bool, + pub from_ci: bool, /// Additional build arguments to pass to Docker. #[clap(long)] - build_arg: Vec, + pub build_arg: Vec, /// Targets to build for #[clap()] - targets: Vec, + pub targets: Vec, } fn locate_dockerfile( @@ -90,6 +97,8 @@ pub fn build_docker_image( repository, labels, verbose, + quiet, + color, dry_run, force, push, @@ -104,10 +113,11 @@ pub fn build_docker_image( }: BuildDockerImage, engine: &docker::Engine, ) -> cross::Result<()> { + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; let metadata = cross::cargo_metadata_with_args( Some(Path::new(env!("CARGO_MANIFEST_DIR"))), None, - verbose, + msg_info, )? .ok_or_else(|| eyre::eyre!("could not find cross workspace and its current version"))?; let version = metadata @@ -149,7 +159,7 @@ pub fn build_docker_image( let mut results = vec![]; for (target, dockerfile) in &targets { if gha && targets.len() > 1 { - println!("::group::Build {target}"); + gha_print("::group::Build {target}"); } let mut docker_build = docker::command(engine); docker_build.args(&["buildx", "build"]); @@ -235,14 +245,11 @@ pub fn build_docker_image( docker_build.arg("."); if !dry_run && (force || !push || gha) { - let result = docker_build.run(verbose, false); + let result = docker_build.run(msg_info, false); if gha && targets.len() > 1 { if let Err(e) = &result { // TODO: Determine what instruction errorred, and place warning on that line with appropriate warning - println!( - "::error file=docker/{},title=Build failed::{}", - dockerfile, e - ) + gha_error(&format!("file=docker/{dockerfile},title=Build failed::{e}")); } } results.push( @@ -254,19 +261,16 @@ pub fn build_docker_image( break; } } else { - docker_build.print_verbose(true); + docker_build.print(msg_info)?; if !dry_run { - panic!("refusing to push, use --force to override"); + shell::fatal("refusing to push, use --force to override", msg_info, 1); } } if gha { - println!("::set-output name=image::{}", &tags[0]); - println!( - "::set-output name=images::'{}'", - serde_json::to_string(&tags)? - ); + gha_output("image", &tags[0]); + gha_output("images", &serde_json::to_string(&tags)?); if targets.len() > 1 { - println!("::endgroup::"); + gha_print("::endgroup::"); } } } @@ -274,10 +278,10 @@ pub fn build_docker_image( std::env::set_var("GITHUB_STEP_SUMMARY", job_summary(&results)?); } if results.iter().any(|r| r.is_err()) { - results.into_iter().filter_map(Result::err).fold( - Err(eyre::eyre!("encountered error(s)")), - |report: Result<(), color_eyre::Report>, e| report.error(e.1), - )?; + results + .into_iter() + .filter_map(Result::err) + .fold(Err(eyre::eyre!("encountered error(s)")), |_, e| Err(e.1))?; } Ok(()) } @@ -321,7 +325,7 @@ pub fn determine_image_name( } pub fn job_summary( - results: &[Result], + results: &[Result], ) -> cross::Result { let mut summary = "# SUMMARY\n\n".to_string(); let success: Vec<_> = results.iter().filter_map(|r| r.as_ref().ok()).collect(); diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs index 7eb902caa..d32b2c02f 100644 --- a/xtask/src/ci.rs +++ b/xtask/src/ci.rs @@ -1,4 +1,6 @@ +use crate::util::gha_output; use clap::Subcommand; +use cross::shell::Verbosity; use cross::{cargo_command, CargoMetadata, CommandExt}; #[derive(Subcommand, Debug)] @@ -77,7 +79,7 @@ pub fn ci(args: CiJob, metadata: CargoMetadata) -> cross::Result<()> { let search = cargo_command() .args(&["search", "--limit", "1"]) .arg("cross") - .run_and_get_stdout(true)?; + .run_and_get_stdout(Verbosity::Verbose.into())?; let (cross, rest) = search .split_once(" = ") .ok_or_else(|| eyre::eyre!("cargo search failed"))?; @@ -96,12 +98,3 @@ pub fn ci(args: CiJob, metadata: CargoMetadata) -> cross::Result<()> { } Ok(()) } - -#[track_caller] -fn gha_output(tag: &str, content: &str) { - if content.contains('\n') { - // https://github.com/actions/toolkit/issues/403 - panic!("output `{tag}` contains newlines, consider serializing with json and deserializing in gha with fromJSON()") - } - println!("::set-output name={tag}::{}", content) -} diff --git a/xtask/src/hooks.rs b/xtask/src/hooks.rs index b2b702838..047d545b7 100644 --- a/xtask/src/hooks.rs +++ b/xtask/src/hooks.rs @@ -4,6 +4,7 @@ use std::path::Path; use std::process::Command; use clap::Args; +use cross::shell::{self, MessageInfo}; use cross::CommandExt; const CARGO_FLAGS: &[&str] = &["--all-features", "--all-targets", "--workspace"]; @@ -12,7 +13,13 @@ const CARGO_FLAGS: &[&str] = &["--all-features", "--all-targets", "--workspace"] pub struct Check { /// Provide verbose diagnostic output. #[clap(short, long)] - verbose: bool, + pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Run shellcheck on all files, not just staged files. #[clap(short, long)] all: bool, @@ -22,24 +29,30 @@ pub struct Check { pub struct Test { /// Provide verbose diagnostic output. #[clap(short, long)] - verbose: bool, + pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, } -fn has_nightly(verbose: bool) -> cross::Result { +fn has_nightly(msg_info: MessageInfo) -> cross::Result { cross::cargo_command() .arg("+nightly") - .run_and_get_output(verbose) + .run_and_get_output(msg_info) .map(|o| o.status.success()) .map_err(Into::into) } fn get_channel_prefer_nightly( - verbose: bool, + msg_info: MessageInfo, toolchain: Option<&str>, ) -> cross::Result> { Ok(match toolchain { Some(t) => Some(t), - None => match has_nightly(verbose)? { + None => match has_nightly(msg_info)? { true => Some("nightly"), false => None, }, @@ -54,27 +67,27 @@ fn cargo(channel: Option<&str>) -> Command { command } -fn cargo_fmt(verbose: bool, channel: Option<&str>) -> cross::Result<()> { +fn cargo_fmt(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<()> { cargo(channel) .args(&["fmt", "--", "--check"]) - .run(verbose, false) + .run(msg_info, false) .map_err(Into::into) } -fn cargo_clippy(verbose: bool, channel: Option<&str>) -> cross::Result<()> { +fn cargo_clippy(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<()> { cargo(channel) .arg("clippy") .args(CARGO_FLAGS) .args(&["--", "--deny", "warnings"]) - .run(verbose, false) + .run(msg_info, false) .map_err(Into::into) } -fn cargo_test(verbose: bool, channel: Option<&str>) -> cross::Result<()> { +fn cargo_test(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<()> { cargo(channel) .arg("test") .args(CARGO_FLAGS) - .run(verbose, false) + .run(msg_info, false) .map_err(Into::into) } @@ -82,17 +95,17 @@ fn splitlines(string: String) -> Vec { string.lines().map(|l| l.to_string()).collect() } -fn staged_files(verbose: bool) -> cross::Result> { +fn staged_files(msg_info: MessageInfo) -> cross::Result> { Command::new("git") .args(&["diff", "--cached", "--name-only", "--diff-filter=ACM"]) - .run_and_get_stdout(verbose) + .run_and_get_stdout(msg_info) .map(splitlines) } -fn all_files(verbose: bool) -> cross::Result> { +fn all_files(msg_info: MessageInfo) -> cross::Result> { Command::new("git") .arg("ls-files") - .run_and_get_stdout(verbose) + .run_and_get_stdout(msg_info) .map(splitlines) } @@ -115,11 +128,11 @@ fn is_shell_script(path: impl AsRef) -> cross::Result { } } -fn shellcheck(all: bool, verbose: bool) -> cross::Result<()> { +fn shellcheck(all: bool, msg_info: MessageInfo) -> cross::Result<()> { if which::which("shellcheck").is_ok() { let files = match all { - true => all_files(verbose), - false => staged_files(verbose), + true => all_files(msg_info), + false => staged_files(msg_info), }?; let mut scripts = vec![]; for file in files { @@ -130,30 +143,47 @@ fn shellcheck(all: bool, verbose: bool) -> cross::Result<()> { if !scripts.is_empty() { Command::new("shellcheck") .args(&scripts) - .run(verbose, false)?; + .run(msg_info, false)?; } } Ok(()) } -pub fn check(Check { verbose, all }: Check, toolchain: Option<&str>) -> cross::Result<()> { - println!("Running rustfmt, clippy, and shellcheck checks."); +pub fn check( + Check { + verbose, + quiet, + color, + all, + }: Check, + toolchain: Option<&str>, +) -> cross::Result<()> { + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + shell::info("Running rustfmt, clippy, and shellcheck checks.", msg_info)?; - let channel = get_channel_prefer_nightly(verbose, toolchain)?; - cargo_fmt(verbose, channel)?; - cargo_clippy(verbose, channel)?; - shellcheck(all, verbose)?; + let channel = get_channel_prefer_nightly(msg_info, toolchain)?; + cargo_fmt(msg_info, channel)?; + cargo_clippy(msg_info, channel)?; + shellcheck(all, msg_info)?; Ok(()) } -pub fn test(Test { verbose }: Test, toolchain: Option<&str>) -> cross::Result<()> { - println!("Running cargo fmt and tests"); +pub fn test( + Test { + verbose, + quiet, + color, + }: Test, + toolchain: Option<&str>, +) -> cross::Result<()> { + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + shell::info("Running cargo fmt and tests", msg_info)?; - let channel = get_channel_prefer_nightly(verbose, toolchain)?; - cargo_fmt(verbose, channel)?; - cargo_test(verbose, channel)?; + let channel = get_channel_prefer_nightly(msg_info, toolchain)?; + cargo_fmt(msg_info, channel)?; + cargo_test(msg_info, channel)?; Ok(()) } diff --git a/xtask/src/install_git_hooks.rs b/xtask/src/install_git_hooks.rs index 60a190dbe..6999081a2 100644 --- a/xtask/src/install_git_hooks.rs +++ b/xtask/src/install_git_hooks.rs @@ -1,4 +1,5 @@ use clap::Args; +use cross::shell::MessageInfo; use std::path::Path; @@ -6,14 +7,28 @@ use std::path::Path; pub struct InstallGitHooks { /// Provide verbose diagnostic output. #[clap(short, long)] - verbose: bool, + pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, } -pub fn install_git_hooks(InstallGitHooks { verbose }: InstallGitHooks) -> cross::Result<()> { +pub fn install_git_hooks( + InstallGitHooks { + verbose, + quiet, + color, + }: InstallGitHooks, +) -> cross::Result<()> { + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; + let metadata = cross::cargo_metadata_with_args( Some(Path::new(env!("CARGO_MANIFEST_DIR"))), None, - verbose, + msg_info, )? .ok_or_else(|| eyre::eyre!("could not find cross workspace"))?; let git_hooks = metadata.workspace_root.join(".git").join("hooks"); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 858c759e3..4c0e8398b 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -10,6 +10,7 @@ pub mod util; use ci::CiJob; use clap::{CommandFactory, Parser, Subcommand}; use cross::docker; +use cross::shell::{MessageInfo, Verbosity}; use util::ImageTarget; use self::build_docker_image::BuildDockerImage; @@ -65,11 +66,13 @@ pub fn main() -> cross::Result<()> { let cli = Cli::parse(); match cli.command { Commands::TargetInfo(args) => { - let engine = get_container_engine(args.engine.as_deref(), args.verbose)?; + let msg_info = MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?; + let engine = get_container_engine(args.engine.as_deref(), msg_info)?; target_info::target_info(args, &engine)?; } Commands::BuildDockerImage(args) => { - let engine = get_container_engine(args.engine.as_deref(), args.verbose)?; + let msg_info = MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?; + let engine = get_container_engine(args.engine.as_deref(), msg_info)?; build_docker_image::build_docker_image(args, &engine)?; } Commands::InstallGitHooks(args) => { @@ -85,7 +88,7 @@ pub fn main() -> cross::Result<()> { let metadata = cross::cargo_metadata_with_args( Some(std::path::Path::new(env!("CARGO_MANIFEST_DIR"))), None, - true, + Verbosity::Verbose.into(), )? .ok_or_else(|| eyre::eyre!("could not find cross workspace"))?; ci::ci(args, metadata)? @@ -95,11 +98,14 @@ pub fn main() -> cross::Result<()> { Ok(()) } -fn get_container_engine(engine: Option<&str>, verbose: bool) -> cross::Result { +fn get_container_engine( + engine: Option<&str>, + msg_info: MessageInfo, +) -> cross::Result { let engine = if let Some(ce) = engine { which::which(ce)? } else { docker::get_container_engine()? }; - docker::Engine::from_path(engine, None, verbose) + docker::Engine::from_path(engine, None, msg_info) } diff --git a/xtask/src/target_info.rs b/xtask/src/target_info.rs index d5493d08e..fa1699173 100644 --- a/xtask/src/target_info.rs +++ b/xtask/src/target_info.rs @@ -1,7 +1,8 @@ -use std::{collections::BTreeMap, process::Stdio}; +use std::collections::BTreeMap; use crate::util::{format_repo, pull_image}; use clap::Args; +use cross::shell::MessageInfo; use cross::{docker, CommandExt}; // Store raw text data in the binary so we don't need a data directory @@ -11,19 +12,25 @@ const TARGET_INFO_SCRIPT: &str = include_str!("target_info.sh"); #[derive(Args, Debug)] pub struct TargetInfo { /// If not provided, get info for all targets. - targets: Vec, + pub targets: Vec, /// Provide verbose diagnostic output. #[clap(short, long)] pub verbose: bool, + /// Do not print cross log messages. + #[clap(short, long)] + pub quiet: bool, + /// Whether messages should use color output. + #[clap(long)] + pub color: Option, /// Image registry. #[clap(long, default_value_t = String::from("ghcr.io"))] - registry: String, + pub registry: String, /// Image repository. #[clap(long, default_value_t = String::from("cross-rs"))] - repository: String, + pub repository: String, /// Image tag. #[clap(long, default_value_t = String::from("main"))] - tag: String, + pub tag: String, /// Container engine (such as docker or podman). #[clap(long)] pub engine: Option, @@ -34,11 +41,11 @@ fn image_info( target: &crate::ImageTarget, image: &str, tag: &str, - verbose: bool, + msg_info: MessageInfo, has_test: bool, ) -> cross::Result<()> { if !tag.starts_with("local") { - pull_image(engine, image, verbose)?; + pull_image(engine, image, msg_info)?; } let mut command = docker::command(engine); @@ -52,18 +59,17 @@ fn image_info( } command.arg(image); command.args(&["bash", "-c", TARGET_INFO_SCRIPT]); - - if !verbose { - // capture stderr to avoid polluting table - command.stderr(Stdio::null()); - } - command.run(verbose, false).map_err(Into::into) + command + .run(msg_info, !msg_info.verbose()) + .map_err(Into::into) } pub fn target_info( TargetInfo { mut targets, verbose, + quiet, + color, registry, repository, tag, @@ -71,6 +77,7 @@ pub fn target_info( }: TargetInfo, engine: &docker::Engine, ) -> cross::Result<()> { + let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; let matrix = crate::util::get_matrix(); let test_map: BTreeMap = matrix .iter() @@ -91,7 +98,7 @@ pub fn target_info( .get(&target) .cloned() .ok_or_else(|| eyre::eyre!("invalid target name {}", target))?; - image_info(engine, &target, &image, &tag, verbose, has_test)?; + image_info(engine, &target, &image, &tag, msg_info, has_test)?; } Ok(()) diff --git a/xtask/src/util.rs b/xtask/src/util.rs index 1aa7e6720..2b5f0ba94 100644 --- a/xtask/src/util.rs +++ b/xtask/src/util.rs @@ -1,3 +1,4 @@ +use cross::shell::MessageInfo; use cross::{docker, CommandExt}; use once_cell::sync::OnceCell; use serde::Deserialize; @@ -81,11 +82,15 @@ pub fn format_repo(registry: &str, repository: &str) -> String { output } -pub fn pull_image(engine: &docker::Engine, image: &str, verbose: bool) -> cross::Result<()> { +pub fn pull_image( + engine: &docker::Engine, + image: &str, + msg_info: MessageInfo, +) -> cross::Result<()> { let mut command = docker::subcommand(engine, "pull"); command.arg(image); - let out = command.run_and_get_output(verbose)?; - command.status_result(verbose, out.status, Some(&out))?; + let out = command.run_and_get_output(msg_info)?; + command.status_result(msg_info, out.status, Some(&out))?; Ok(()) } @@ -148,3 +153,22 @@ impl std::fmt::Display for ImageTarget { } } } + +// note: for GHA actions we need to output these tags no matter the verbosity level +pub fn gha_print(content: &str) { + println!("{}", content) +} + +// note: for GHA actions we need to output these tags no matter the verbosity level +pub fn gha_error(content: &str) { + println!("::error {}", content) +} + +#[track_caller] +pub fn gha_output(tag: &str, content: &str) { + if content.contains('\n') { + // https://github.com/actions/toolkit/issues/403 + panic!("output `{tag}` contains newlines, consider serializing with json and deserializing in gha with fromJSON()") + } + println!("::set-output name={tag}::{}", content) +}