Repository: cross-rs/cross
Pull request #911 (2022-07-05 17:03:39)

Structify more of the internals to simplify the implementation.
Reduces the number of arguments to the function signatures, making the code much easier to understand, and relocates some things. For example, `docker_in_docker` is now associated with `Engine`, and now with `cli::Args`. Likewise, unstable doctests is now a part of `Environment` and not `cli::Args`. `MessageInfo` now stores mutable state (needs to clear lines) and is no longer copyable, and a single instance is created in lib and passed around everywhere as a mutable reference.

The changes with `MessageInfo` will allow us to erase lines in the future, both from `stderr` and `stdout`, which we currently don't use but will allow us to close #859.

 This also fixes an issue where `-vv+` was not recognized as a valid verbose flag: `cargo` can handle 1 or more `v` characters. Also allowed parsing of `--color=value` args, when we previously only accepted `--color value`.


Patch:
diff --git a/src/bin/commands/clean.rs b/src/bin/commands/clean.rs
index 35c9db7..e5b4f69 100644
--- a/src/bin/commands/clean.rs
+++ b/src/bin/commands/clean.rs
@@ -3,7 +3,7 @@ use std::fs;
 use super::containers::*;
 use super::images::*;
 use clap::Args;
-use cross::shell::{self, MessageInfo};
+use cross::shell::MessageInfo;
 
 #[derive(Args, Debug)]
 pub struct Clean {
@@ -31,8 +31,11 @@ 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())?;
+    pub fn run(
+        self,
+        engine: cross::docker::Engine,
+        msg_info: &mut MessageInfo,
+    ) -> cross::Result<()> {
         let tempdir = cross::temp::dir()?;
         match self.execute {
             true => {
@@ -40,13 +43,10 @@ impl Clean {
                     fs::remove_dir_all(tempdir)?;
                 }
             }
-            false => shell::print(
-                format!(
-                    "fs::remove_dir_all({})",
-                    cross::pretty_path(&tempdir, |_| false)
-                ),
-                msg_info,
-            )?,
+            false => msg_info.print(format_args!(
+                "fs::remove_dir_all({})",
+                cross::pretty_path(&tempdir, |_| false)
+            ))?,
         }
 
         // containers -> images -> volumes -> prune to ensure no conflicts.
@@ -58,7 +58,7 @@ impl Clean {
             execute: self.execute,
             engine: None,
         };
-        remove_containers.run(engine.clone())?;
+        remove_containers.run(engine.clone(), msg_info)?;
 
         let remove_images = RemoveImages {
             targets: vec![],
@@ -70,7 +70,7 @@ impl Clean {
             execute: self.execute,
             engine: None,
         };
-        remove_images.run(engine.clone())?;
+        remove_images.run(engine.clone(), msg_info)?;
 
         let remove_volumes = RemoveAllVolumes {
             verbose: self.verbose,
@@ -80,7 +80,7 @@ impl Clean {
             execute: self.execute,
             engine: None,
         };
-        remove_volumes.run(engine.clone())?;
+        remove_volumes.run(engine.clone(), msg_info)?;
 
         let prune_volumes = PruneVolumes {
             verbose: self.verbose,
@@ -89,8 +89,24 @@ impl Clean {
             execute: self.execute,
             engine: None,
         };
-        prune_volumes.run(engine)?;
+        prune_volumes.run(engine, msg_info)?;
 
         Ok(())
     }
+
+    pub fn engine(&self) -> Option<&str> {
+        self.engine.as_deref()
+    }
+
+    pub fn verbose(&self) -> bool {
+        self.verbose
+    }
+
+    pub fn quiet(&self) -> bool {
+        self.quiet
+    }
+
+    pub fn color(&self) -> Option<&str> {
+        self.color.as_deref()
+    }
 }
diff --git a/src/bin/commands/containers.rs b/src/bin/commands/containers.rs
index a6a3712..7968946 100644
--- a/src/bin/commands/containers.rs
+++ b/src/bin/commands/containers.rs
@@ -1,7 +1,7 @@
 use std::io;
 
 use clap::{Args, Subcommand};
-use cross::shell::{self, MessageInfo, Stream};
+use cross::shell::{MessageInfo, Stream};
 use cross::{docker, CommandExt};
 
 #[derive(Args, Debug)]
@@ -21,8 +21,8 @@ pub struct ListVolumes {
 }
 
 impl ListVolumes {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        list_volumes(self, &engine)
+    pub fn run(self, engine: docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
+        list_volumes(&engine, msg_info)
     }
 }
 
@@ -49,8 +49,8 @@ pub struct RemoveAllVolumes {
 }
 
 impl RemoveAllVolumes {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        remove_all_volumes(self, &engine)
+    pub fn run(self, engine: docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
+        remove_all_volumes(self, &engine, msg_info)
     }
 }
 
@@ -74,8 +74,8 @@ pub struct PruneVolumes {
 }
 
 impl PruneVolumes {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        prune_volumes(self, &engine)
+    pub fn run(self, engine: docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
+        prune_volumes(self, &engine, msg_info)
     }
 }
 
@@ -102,8 +102,13 @@ pub struct CreateVolume {
 }
 
 impl CreateVolume {
-    pub fn run(self, engine: docker::Engine, channel: Option<&str>) -> cross::Result<()> {
-        create_persistent_volume(self, &engine, channel)
+    pub fn run(
+        self,
+        engine: docker::Engine,
+        channel: Option<&str>,
+        msg_info: &mut MessageInfo,
+    ) -> cross::Result<()> {
+        create_persistent_volume(self, &engine, channel, msg_info)
     }
 }
 
@@ -130,8 +135,13 @@ pub struct RemoveVolume {
 }
 
 impl RemoveVolume {
-    pub fn run(self, engine: docker::Engine, channel: Option<&str>) -> cross::Result<()> {
-        remove_persistent_volume(self, &engine, channel)
+    pub fn run(
+        self,
+        engine: docker::Engine,
+        channel: Option<&str>,
+        msg_info: &mut MessageInfo,
+    ) -> cross::Result<()> {
+        remove_persistent_volume(&engine, channel, msg_info)
     }
 }
 
@@ -149,55 +159,59 @@ pub enum Volumes {
     Remove(RemoveVolume),
 }
 
+macro_rules! volumes_get_field {
+    ($self:ident, $field:ident $(.$cb:ident)?) => {{
+        match $self {
+            Volumes::List(l) => l.$field$(.$cb())?,
+            Volumes::RemoveAll(l) => l.$field$(.$cb())?,
+            Volumes::Prune(l) => l.$field$(.$cb())?,
+            Volumes::Create(l) => l.$field$(.$cb())?,
+            Volumes::Remove(l) => l.$field$(.$cb())?,
+        }
+    }};
+}
+
 impl Volumes {
-    pub fn run(self, engine: docker::Engine, toolchain: Option<&str>) -> cross::Result<()> {
+    pub fn run(
+        self,
+        engine: docker::Engine,
+        toolchain: Option<&str>,
+        msg_info: &mut MessageInfo,
+    ) -> cross::Result<()> {
         match self {
-            Volumes::List(args) => args.run(engine),
-            Volumes::RemoveAll(args) => args.run(engine),
-            Volumes::Prune(args) => args.run(engine),
-            Volumes::Create(args) => args.run(engine, toolchain),
-            Volumes::Remove(args) => args.run(engine, toolchain),
+            Volumes::List(args) => args.run(engine, msg_info),
+            Volumes::RemoveAll(args) => args.run(engine, msg_info),
+            Volumes::Prune(args) => args.run(engine, msg_info),
+            Volumes::Create(args) => args.run(engine, toolchain, msg_info),
+            Volumes::Remove(args) => args.run(engine, toolchain, msg_info),
         }
     }
 
     pub fn engine(&self) -> Option<&str> {
+        volumes_get_field!(self, engine.as_deref)
+    }
+
+    // FIXME: remove this in v0.3.0.
+    pub fn docker_in_docker(&self) -> bool {
         match self {
-            Volumes::List(l) => l.engine.as_deref(),
-            Volumes::RemoveAll(l) => l.engine.as_deref(),
-            Volumes::Prune(l) => l.engine.as_deref(),
-            Volumes::Create(l) => l.engine.as_deref(),
-            Volumes::Remove(l) => l.engine.as_deref(),
+            Volumes::List(_) => false,
+            Volumes::RemoveAll(_) => false,
+            Volumes::Prune(_) => false,
+            Volumes::Create(l) => l.docker_in_docker,
+            Volumes::Remove(l) => l.docker_in_docker,
         }
     }
 
     pub fn verbose(&self) -> bool {
-        match self {
-            Volumes::List(l) => l.verbose,
-            Volumes::RemoveAll(l) => l.verbose,
-            Volumes::Prune(l) => l.verbose,
-            Volumes::Create(l) => l.verbose,
-            Volumes::Remove(l) => l.verbose,
-        }
+        volumes_get_field!(self, 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,
-        }
+        volumes_get_field!(self, 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(),
-        }
+        volumes_get_field!(self, color.as_deref)
     }
 }
 
@@ -218,8 +232,8 @@ pub struct ListContainers {
 }
 
 impl ListContainers {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        list_containers(self, &engine)
+    pub fn run(self, engine: docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
+        list_containers(&engine, msg_info)
     }
 }
 
@@ -246,8 +260,8 @@ pub struct RemoveAllContainers {
 }
 
 impl RemoveAllContainers {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        remove_all_containers(self, &engine)
+    pub fn run(self, engine: docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
+        remove_all_containers(self, &engine, msg_info)
     }
 }
 
@@ -259,44 +273,44 @@ pub enum Containers {
     RemoveAll(RemoveAllContainers),
 }
 
+macro_rules! containers_get_field {
+    ($self:ident, $field:ident $(.$cb:ident)?) => {{
+        match $self {
+            Containers::List(l) => l.$field$(.$cb())?,
+            Containers::RemoveAll(l) => l.$field$(.$cb())?,
+        }
+    }};
+}
+
 impl Containers {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
+    pub fn run(self, engine: docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
         match self {
-            Containers::List(args) => args.run(engine),
-            Containers::RemoveAll(args) => args.run(engine),
+            Containers::List(args) => args.run(engine, msg_info),
+            Containers::RemoveAll(args) => args.run(engine, msg_info),
         }
     }
 
     pub fn engine(&self) -> Option<&str> {
-        match self {
-            Containers::List(l) => l.engine.as_deref(),
-            Containers::RemoveAll(l) => l.engine.as_deref(),
-        }
+        containers_get_field!(self, engine.as_deref)
     }
 
     pub fn verbose(&self) -> bool {
-        match self {
-            Containers::List(l) => l.verbose,
-            Containers::RemoveAll(l) => l.verbose,
-        }
+        containers_get_field!(self, verbose)
     }
 
     pub fn quiet(&self) -> bool {
-        match self {
-            Containers::List(l) => l.quiet,
-            Containers::RemoveAll(l) => l.quiet,
-        }
+        containers_get_field!(self, quiet)
     }
 
     pub fn color(&self) -> Option<&str> {
-        match self {
-            Containers::List(l) => l.color.as_deref(),
-            Containers::RemoveAll(l) => l.color.as_deref(),
-        }
+        containers_get_field!(self, color.as_deref)
     }
 }
 
-fn get_cross_volumes(engine: &docker::Engine, msg_info: MessageInfo) -> cross::Result<Vec<String>> {
+fn get_cross_volumes(
+    engine: &docker::Engine,
+    msg_info: &mut MessageInfo,
+) -> cross::Result<Vec<String>> {
     let stdout = docker::subcommand(engine, "volume")
         .arg("list")
         .args(&["--format", "{{.Name}}"])
@@ -310,35 +324,19 @@ fn get_cross_volumes(engine: &docker::Engine, msg_info: MessageInfo) -> cross::R
     Ok(volumes)
 }
 
-pub fn list_volumes(
-    ListVolumes {
-        verbose,
-        quiet,
-        color,
-        ..
-    }: ListVolumes,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
+pub fn list_volumes(engine: &docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
     for line in get_cross_volumes(engine, msg_info)?.iter() {
-        shell::print(line, msg_info)?;
+        msg_info.print(line)?;
     }
 
     Ok(())
 }
 
 pub fn remove_all_volumes(
-    RemoveAllVolumes {
-        verbose,
-        quiet,
-        color,
-        force,
-        execute,
-        ..
-    }: RemoveAllVolumes,
+    RemoveAllVolumes { force, execute, .. }: RemoveAllVolumes,
     engine: &docker::Engine,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    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");
@@ -352,57 +350,37 @@ pub fn remove_all_volumes(
     } else if execute {
         command.run(msg_info, false).map_err(Into::into)
     } else {
-        shell::note(
-            "this is a dry run. to remove the volumes, pass the `--execute` flag.",
-            msg_info,
-        )?;
+        msg_info.note("this is a dry run. to remove the volumes, pass the `--execute` flag.")?;
         command.print(msg_info)?;
         Ok(())
     }
 }
 
 pub fn prune_volumes(
-    PruneVolumes {
-        verbose,
-        quiet,
-        color,
-        execute,
-        ..
-    }: PruneVolumes,
+    PruneVolumes { execute, .. }: PruneVolumes,
     engine: &docker::Engine,
+    msg_info: &mut MessageInfo,
 ) -> 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(msg_info, false).map_err(Into::into)
     } else {
-        shell::note(
-            "this is a dry run. to prune the volumes, pass the `--execute` flag.",
-            msg_info,
-        )?;
+        msg_info.note("this is a dry run. to prune the volumes, pass the `--execute` flag.")?;
         command.print(msg_info)?;
         Ok(())
     }
 }
 
 pub fn create_persistent_volume(
-    CreateVolume {
-        docker_in_docker,
-        copy_registry,
-        verbose,
-        quiet,
-        color,
-        ..
-    }: CreateVolume,
+    CreateVolume { copy_registry, .. }: CreateVolume,
     engine: &docker::Engine,
     channel: Option<&str>,
+    msg_info: &mut MessageInfo,
 ) -> 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, msg_info)?;
+    let (target, metadata, dirs) = docker::get_package_info(engine, triple, channel, msg_info)?;
     let container = docker::remote::unique_container_identifier(&target, &metadata, &dirs)?;
     let volume = docker::remote::unique_toolchain_identifier(&dirs.sysroot)?;
 
@@ -417,11 +395,11 @@ pub fn create_persistent_volume(
     // stop the container if it's already running
     let state = docker::remote::container_state(engine, &container, msg_info)?;
     if !state.is_stopped() {
-        shell::warn("container {container} was running.", msg_info)?;
+        msg_info.warn("container {container} was running.")?;
         docker::remote::container_stop(engine, &container, msg_info)?;
     }
     if state.exists() {
-        shell::warn("container {container} was exited.", msg_info)?;
+        msg_info.warn("container {container} was exited.")?;
         docker::remote::container_rm(engine, &container, msg_info)?;
     }
 
@@ -472,21 +450,13 @@ pub fn create_persistent_volume(
 }
 
 pub fn remove_persistent_volume(
-    RemoveVolume {
-        docker_in_docker,
-        verbose,
-        quiet,
-        color,
-        ..
-    }: RemoveVolume,
     engine: &docker::Engine,
     channel: Option<&str>,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
     // we only need a triple that needs docker: the actual target doesn't matter.
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
     let triple = cross::Host::X86_64UnknownLinuxGnu.triple();
-    let (_, _, dirs) =
-        docker::get_package_info(engine, triple, channel, docker_in_docker, msg_info)?;
+    let (_, _, dirs) = docker::get_package_info(engine, triple, channel, msg_info)?;
     let volume = docker::remote::unique_toolchain_identifier(&dirs.sysroot)?;
 
     if !docker::remote::volume_exists(engine, &volume, msg_info)? {
@@ -500,7 +470,7 @@ pub fn remove_persistent_volume(
 
 fn get_cross_containers(
     engine: &docker::Engine,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<Vec<String>> {
     let stdout = docker::subcommand(engine, "ps")
         .arg("-a")
@@ -515,35 +485,19 @@ fn get_cross_containers(
     Ok(containers)
 }
 
-pub fn list_containers(
-    ListContainers {
-        verbose,
-        quiet,
-        color,
-        ..
-    }: ListContainers,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
+pub fn list_containers(engine: &docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
     for line in get_cross_containers(engine, msg_info)?.iter() {
-        shell::print(line, msg_info)?;
+        msg_info.print(line)?;
     }
 
     Ok(())
 }
 
 pub fn remove_all_containers(
-    RemoveAllContainers {
-        verbose,
-        quiet,
-        color,
-        force,
-        execute,
-        ..
-    }: RemoveAllContainers,
+    RemoveAllContainers { force, execute, .. }: RemoveAllContainers,
     engine: &docker::Engine,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    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![];
@@ -580,10 +534,7 @@ pub fn remove_all_containers(
             command.run(msg_info, false)?;
         }
     } else {
-        shell::note(
-            "this is a dry run. to remove the containers, pass the `--execute` flag.",
-            msg_info,
-        )?;
+        msg_info.note("this is a dry run. to remove the containers, pass the `--execute` flag.")?;
         for command in commands {
             command.print(msg_info)?;
         }
diff --git a/src/bin/commands/images.rs b/src/bin/commands/images.rs
index 46ebec3..1401292 100644
--- a/src/bin/commands/images.rs
+++ b/src/bin/commands/images.rs
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
 
 use clap::{Args, Subcommand};
 use cross::docker::{self, CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX};
-use cross::shell::{self, MessageInfo, Verbosity};
+use cross::shell::MessageInfo;
 use cross::{CommandExt, TargetList};
 
 // known image prefixes, with their registry
@@ -31,8 +31,8 @@ pub struct ListImages {
 }
 
 impl ListImages {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        list_images(self, &engine)
+    pub fn run(self, engine: docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
+        list_images(self, &engine, msg_info)
     }
 }
 
@@ -64,11 +64,11 @@ pub struct RemoveImages {
 }
 
 impl RemoveImages {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
+    pub fn run(self, engine: docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
         if self.targets.is_empty() {
-            remove_all_images(self, &engine)
+            remove_all_images(self, &engine, msg_info)
         } else {
-            remove_target_images(self, &engine)
+            remove_target_images(self, &engine, msg_info)
         }
     }
 }
@@ -82,10 +82,10 @@ pub enum Images {
 }
 
 impl Images {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
+    pub fn run(self, engine: docker::Engine, msg_info: &mut MessageInfo) -> cross::Result<()> {
         match self {
-            Images::List(args) => args.run(engine),
-            Images::Remove(args) => args.run(engine),
+            Images::List(args) => args.run(engine, msg_info),
+            Images::Remove(args) => args.run(engine, msg_info),
         }
     }
 
@@ -163,7 +163,7 @@ fn is_local_image(tag: &str) -> bool {
 
 fn get_cross_images(
     engine: &docker::Engine,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
     local: bool,
 ) -> cross::Result<Vec<Image>> {
     let mut images: BTreeSet<_> = cross::docker::subcommand(engine, "images")
@@ -217,7 +217,7 @@ fn get_image_target(
     engine: &cross::docker::Engine,
     image: &Image,
     target_list: &TargetList,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<String> {
     if let Some(stripped) = image.repository.strip_prefix(&format!("{GHCR_IO}/")) {
         return Ok(stripped.to_string());
@@ -256,18 +256,12 @@ fn get_image_target(
 }
 
 pub fn list_images(
-    ListImages {
-        targets,
-        verbose,
-        quiet,
-        color,
-        ..
-    }: ListImages,
+    ListImages { targets, .. }: ListImages,
     engine: &docker::Engine,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    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 target_list = msg_info.as_quiet(cross::rustc::target_list)?;
     let mut map: BTreeMap<String, Vec<Image>> = BTreeMap::new();
     let mut max_target_len = 0;
     let mut max_image_len = 0;
@@ -285,44 +279,45 @@ 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| -> cross::Result<()> {
-        let mut row = String::new();
-        row.push('|');
-        row.push(fill);
-        row.push_str(col1);
-        let spaces = max_target_len.max(col1.len()) + 1 - col1.len();
-        for _ in 0..spaces {
+    let print_string =
+        |col1: &str, col2: &str, fill: char, info: &mut MessageInfo| -> cross::Result<()> {
+            let mut row = String::new();
+            row.push('|');
             row.push(fill);
-        }
-        row.push('|');
-        row.push(fill);
-        row.push_str(col2);
-        let spaces = max_image_len.max(col2.len()) + 1 - col2.len();
-        for _ in 0..spaces {
+            row.push_str(col1);
+            let spaces = max_target_len.max(col1.len()) + 1 - col1.len();
+            for _ in 0..spaces {
+                row.push(fill);
+            }
+            row.push('|');
             row.push(fill);
-        }
-        row.push('|');
-        shell::print(row, msg_info)
-    };
+            row.push_str(col2);
+            let spaces = max_image_len.max(col2.len()) + 1 - col2.len();
+            for _ in 0..spaces {
+                row.push(fill);
+            }
+            row.push('|');
+            info.print(row)
+        };
 
     if targets.len() != 1 {
-        print_string("Targets", "Images", ' ')?;
-        print_string("-------", "------", '-')?;
+        print_string("Targets", "Images", ' ', msg_info)?;
+        print_string("-------", "------", '-', msg_info)?;
     }
 
     let print_single =
-        |_: &str, image: &Image| -> cross::Result<()> { shell::print(image, msg_info) };
-    let print_table = |target: &str, image: &Image| -> cross::Result<()> {
+        |_: &str, image: &Image, info: &mut MessageInfo| -> cross::Result<()> { info.print(image) };
+    let print_table = |target: &str, image: &Image, info: &mut MessageInfo| -> cross::Result<()> {
         let name = image.name();
-        print_string(target, &name, ' ')
+        print_string(target, &name, ' ', info)
     };
 
     for target in keys {
         for image in map.get(target).expect("map must have key").iter() {
             if targets.len() == 1 {
-                print_single(target, image)?;
+                print_single(target, image, msg_info)?;
             } else {
-                print_table(target, image)?;
+                print_table(target, image, msg_info)?;
             }
         }
     }
@@ -333,7 +328,7 @@ pub fn list_images(
 fn remove_images(
     engine: &docker::Engine,
     images: &[Image],
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
     force: bool,
     execute: bool,
 ) -> cross::Result<()> {
@@ -347,10 +342,7 @@ fn remove_images(
     } else if execute {
         command.run(msg_info, false).map_err(Into::into)
     } else {
-        shell::note(
-            "this is a dry run. to remove the images, pass the `--execute` flag.",
-            msg_info,
-        )?;
+        msg_info.note("this is a dry run. to remove the images, pass the `--execute` flag.")?;
         command.print(msg_info)?;
         Ok(())
     }
@@ -358,17 +350,14 @@ fn remove_images(
 
 pub fn remove_all_images(
     RemoveImages {
-        verbose,
-        quiet,
-        color,
         force,
         local,
         execute,
         ..
     }: RemoveImages,
     engine: &docker::Engine,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    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)
 }
@@ -376,19 +365,16 @@ pub fn remove_all_images(
 pub fn remove_target_images(
     RemoveImages {
         targets,
-        verbose,
-        quiet,
-        color,
         force,
         local,
         execute,
         ..
     }: RemoveImages,
     engine: &docker::Engine,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    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 target_list = msg_info.as_quiet(cross::rustc::target_list)?;
     let mut images = vec![];
     for image in cross_images {
         let target = dbg!(get_image_target(engine, &image, &target_list, msg_info)?);
diff --git a/src/bin/cross-util.rs b/src/bin/cross-util.rs
index e5be6ae..8468c4c 100644
--- a/src/bin/cross-util.rs
+++ b/src/bin/cross-util.rs
@@ -49,14 +49,31 @@ fn is_toolchain(toolchain: &str) -> cross::Result<String> {
 
 fn get_container_engine(
     engine: Option<&str>,
-    msg_info: MessageInfo,
+    docker_in_docker: bool,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<docker::Engine> {
     let engine = if let Some(ce) = engine {
         which::which(ce)?
     } else {
         docker::get_container_engine()?
     };
-    docker::Engine::from_path(engine, None, msg_info)
+    let in_docker = match docker_in_docker {
+        true => Some(true),
+        false => None,
+    };
+    docker::Engine::from_path(engine, in_docker, None, msg_info)
+}
+
+macro_rules! get_engine {
+    ($args:ident, $docker_in_docker:expr, $msg_info: ident) => {{
+        get_container_engine($args.engine(), $docker_in_docker, &mut $msg_info)
+    }};
+}
+
+macro_rules! get_msg_info {
+    ($args:ident) => {{
+        MessageInfo::create($args.verbose(), $args.quiet(), $args.color())
+    }};
 }
 
 pub fn main() -> cross::Result<()> {
@@ -64,24 +81,24 @@ pub fn main() -> cross::Result<()> {
     let cli = Cli::parse();
     match cli.command {
         Commands::Images(args) => {
-            let msg_info = MessageInfo::create(args.verbose(), args.quiet(), args.color())?;
-            let engine = get_container_engine(args.engine(), msg_info)?;
-            args.run(engine)?;
+            let mut msg_info = get_msg_info!(args)?;
+            let engine = get_engine!(args, false, msg_info)?;
+            args.run(engine, &mut msg_info)?;
         }
         Commands::Volumes(args) => {
-            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())?;
+            let mut msg_info = get_msg_info!(args)?;
+            let engine = get_engine!(args, args.docker_in_docker(), msg_info)?;
+            args.run(engine, cli.toolchain.as_deref(), &mut msg_info)?;
         }
         Commands::Containers(args) => {
-            let msg_info = MessageInfo::create(args.verbose(), args.quiet(), args.color())?;
-            let engine = get_container_engine(args.engine(), msg_info)?;
-            args.run(engine)?;
+            let mut msg_info = get_msg_info!(args)?;
+            let engine = get_engine!(args, false, msg_info)?;
+            args.run(engine, &mut msg_info)?;
         }
         Commands::Clean(args) => {
-            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)?;
+            let mut msg_info = get_msg_info!(args)?;
+            let engine = get_engine!(args, false, msg_info)?;
+            args.run(engine, &mut msg_info)?;
         }
     }
 
diff --git a/src/cargo.rs b/src/cargo.rs
index 51a35bd..90662cf 100644
--- a/src/cargo.rs
+++ b/src/cargo.rs
@@ -120,7 +120,7 @@ pub fn cargo_command() -> Command {
 pub fn cargo_metadata_with_args(
     cd: Option<&Path>,
     args: Option<&Args>,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<Option<CargoMetadata>> {
     let mut command = cargo_command();
     if let Some(channel) = args.and_then(|x| x.channel.as_deref()) {
@@ -145,9 +145,9 @@ pub fn cargo_metadata_with_args(
     }
     let output = command.run_and_get_output(msg_info)?;
     if !output.status.success() {
-        shell::warn("unable to get metadata for package", msg_info)?;
+        msg_info.warn("unable to get metadata for package")?;
         let indented = shell::indent(&String::from_utf8(output.stderr)?, shell::default_ident());
-        shell::debug(indented, msg_info)?;
+        msg_info.debug(indented)?;
         return Ok(None);
     }
     let manifest: Option<CargoMetadata> = serde_json::from_slice(&output.stdout)?;
@@ -164,13 +164,16 @@ pub fn cargo_metadata_with_args(
 }
 
 /// Pass-through mode
-pub fn run(args: &[String], msg_info: MessageInfo) -> Result<ExitStatus> {
+pub fn run(args: &[String], msg_info: &mut MessageInfo) -> Result<ExitStatus> {
     cargo_command()
         .args(args)
         .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], msg_info: MessageInfo) -> Result<std::process::Output> {
+pub fn run_and_get_output(
+    args: &[String],
+    msg_info: &mut MessageInfo,
+) -> Result<std::process::Output> {
     cargo_command().args(args).run_and_get_output(msg_info)
 }
diff --git a/src/cli.rs b/src/cli.rs
index 82f956a..c0f9120 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,10 +1,9 @@
 use std::{env, path::PathBuf};
 
 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::shell::MessageInfo;
 use crate::Target;
 
 #[derive(Debug)]
@@ -15,11 +14,11 @@ pub struct Args {
     pub target: Option<Target>,
     pub features: Vec<String>,
     pub target_dir: Option<PathBuf>,
-    pub docker_in_docker: bool,
-    pub enable_doctests: bool,
     pub manifest_path: Option<PathBuf>,
     pub version: bool,
-    pub msg_info: MessageInfo,
+    pub verbose: bool,
+    pub quiet: bool,
+    pub color: Option<String>,
 }
 
 // Fix for issue #581. target_dir must be absolute.
@@ -52,23 +51,86 @@ pub fn group_subcommands(stdout: &str) -> (Vec<&str>, Vec<&str>) {
     (cross, host)
 }
 
-pub fn fmt_subcommands(stdout: &str, msg_info: MessageInfo) -> Result<()> {
+pub fn fmt_subcommands(stdout: &str, msg_info: &mut MessageInfo) -> Result<()> {
     let (cross, host) = group_subcommands(stdout);
     if !cross.is_empty() {
-        shell::print("Cross Commands:", msg_info)?;
+        msg_info.print("Cross Commands:")?;
         for line in cross.iter() {
-            shell::print(line, msg_info)?;
+            msg_info.print(line)?;
         }
     }
     if !host.is_empty() {
-        shell::print("Host Commands:", msg_info)?;
+        msg_info.print("Host Commands:")?;
         for line in cross.iter() {
-            shell::print(line, msg_info)?;
+            msg_info.print(line)?;
         }
     }
     Ok(())
 }
 
+fn is_verbose(arg: &str) -> bool {
+    match arg {
+        "--verbose" => true,
+        // cargo can handle any number of "v"s
+        a => a
+            .get(1..)
+            .map(|a| a.chars().all(|x| x == 'v'))
+            .unwrap_or_default(),
+    }
+}
+
+enum ArgKind {
+    Next,
+    Equal,
+}
+
+fn is_value_arg(arg: &str, field: &str) -> Option<ArgKind> {
+    if arg == field {
+        Some(ArgKind::Next)
+    } else if arg
+        .strip_prefix(field)
+        .map(|a| a.starts_with('='))
+        .unwrap_or_default()
+    {
+        Some(ArgKind::Equal)
+    } else {
+        None
+    }
+}
+
+fn parse_next_arg<T>(
+    arg: String,
+    out: &mut Vec<String>,
+    parse: impl Fn(&str) -> T,
+    iter: &mut impl Iterator<Item = String>,
+) -> Option<T> {
+    out.push(arg);
+    match iter.next() {
+        Some(next) => {
+            let result = parse(&next);
+            out.push(next);
+            Some(result)
+        }
+        None => None,
+    }
+}
+
+fn parse_equal_arg<T>(arg: String, out: &mut Vec<String>, parse: impl Fn(&str) -> T) -> T {
+    let result = parse(arg.split_once('=').unwrap().1);
+    out.push(arg);
+
+    result
+}
+
+fn parse_manifest_path(path: &str) -> Option<PathBuf> {
+    let p = PathBuf::from(path);
+    env::current_dir().ok().map(|cwd| cwd.join(p))
+}
+
+fn parse_target_dir(path: &str) -> Result<PathBuf> {
+    absolute_path(PathBuf::from(path))
+}
+
 pub fn parse(target_list: &TargetList) -> Result<Args> {
     let mut channel = None;
     let mut target = None;
@@ -81,7 +143,7 @@ pub fn parse(target_list: &TargetList) -> Result<Args> {
     let mut quiet = false;
     let mut verbose = false;
     let mut color = None;
-    let default_msg_info = MessageInfo::default();
+    let mut default_msg_info = MessageInfo::default();
 
     {
         let mut args = env::args().skip(1);
@@ -89,7 +151,7 @@ pub fn parse(target_list: &TargetList) -> Result<Args> {
             if arg.is_empty() {
                 continue;
             }
-            if matches!(arg.as_str(), "--verbose" | "-v" | "-vv") {
+            if is_verbose(arg.as_str()) {
                 verbose = true;
                 all.push(arg);
             } else if matches!(arg.as_str(), "--version" | "-V") {
@@ -97,65 +159,59 @@ pub fn parse(target_list: &TargetList) -> Result<Args> {
             } else if matches!(arg.as_str(), "--quiet" | "-q") {
                 quiet = true;
                 all.push(arg);
-            } else if arg == "--color" {
-                all.push(arg);
-                match args.next() {
-                    Some(arg) => {
-                        color = {
-                            all.push(arg.clone());
-                            Some(arg)
+            } else if let Some(kind) = is_value_arg(&arg, "--color") {
+                color = match kind {
+                    ArgKind::Next => {
+                        match parse_next_arg(arg, &mut all, ToOwned::to_owned, &mut args) {
+                            Some(c) => Some(c),
+                            None => default_msg_info.fatal_usage("--color <WHEN>", 1),
                         }
                     }
-                    None => {
-                        shell::fatal_usage("--color <WHEN>", default_msg_info, 1);
+                    ArgKind::Equal => Some(parse_equal_arg(arg, &mut all, ToOwned::to_owned)),
+                };
+            } else if let Some(kind) = is_value_arg(&arg, "--manifest-path") {
+                manifest_path = match kind {
+                    ArgKind::Next => {
+                        parse_next_arg(arg, &mut all, parse_manifest_path, &mut args).flatten()
                     }
-                }
-            } else if arg == "--manifest-path" {
-                all.push(arg);
-                if let Some(m) = args.next() {
-                    let p = PathBuf::from(&m);
-                    all.push(m);
-                    manifest_path = env::current_dir().ok().map(|cwd| cwd.join(p));
-                }
-            } else if arg.starts_with("--manifest-path=") {
-                manifest_path = arg
-                    .split_once('=')
-                    .map(|x| x.1)
-                    .map(PathBuf::from)
-                    .and_then(|p| env::current_dir().ok().map(|cwd| cwd.join(p)));
-                all.push(arg);
+                    ArgKind::Equal => parse_equal_arg(arg, &mut all, parse_manifest_path),
+                };
             } else if let ("+", ch) = arg.split_at(1) {
                 channel = Some(ch.to_string());
-            } else if arg == "--target" {
-                all.push(arg);
-                if let Some(t) = args.next() {
-                    target = Some(Target::from(&t, target_list));
-                    all.push(t);
-                }
-            } else if arg.starts_with("--target=") {
-                target = arg
-                    .split_once('=')
-                    .map(|(_, t)| Target::from(t, target_list));
-                all.push(arg);
-            } else if arg == "--features" {
-                all.push(arg);
-                if let Some(t) = args.next() {
-                    features.push(t.clone());
-                    all.push(t);
-                }
-            } else if arg.starts_with("--features=") {
-                features.extend(arg.split_once('=').map(|(_, t)| t.to_owned()));
-                all.push(arg);
-            } else if arg == "--target-dir" {
-                all.push(arg);
-                if let Some(td) = args.next() {
-                    target_dir = Some(absolute_path(PathBuf::from(&td))?);
-                    all.push("/target".to_string());
+            } else if let Some(kind) = is_value_arg(&arg, "--target") {
+                target = match kind {
+                    ArgKind::Next => {
+                        parse_next_arg(arg, &mut all, |t| Target::from(t, target_list), &mut args)
+                    }
+                    ArgKind::Equal => Some(parse_equal_arg(arg, &mut all, |t| {
+                        Target::from(t, target_list)
+                    })),
+                };
+            } else if let Some(kind) = is_value_arg(&arg, "--features") {
+                match kind {
+                    ArgKind::Next => {
+                        let next = parse_next_arg(arg, &mut all, ToOwned::to_owned, &mut args);
+                        if let Some(feature) = next {
+                            features.push(feature);
+                        }
+                    }
+                    ArgKind::Equal => {
+                        features.push(parse_equal_arg(arg, &mut all, ToOwned::to_owned))
+                    }
                 }
-            } else if arg.starts_with("--target-dir=") {
-                if let Some((_, td)) = arg.split_once('=') {
-                    target_dir = Some(absolute_path(PathBuf::from(&td))?);
-                    all.push("--target-dir=/target".into());
+            } else if let Some(kind) = is_value_arg(&arg, "--target-dir") {
+                match kind {
+                    ArgKind::Next => {
+                        all.push(arg);
+                        if let Some(td) = args.next() {
+                            target_dir = Some(parse_target_dir(&td)?);
+                            all.push("/target".to_string());
+                        }
+                    }
+                    ArgKind::Equal => {
+                        target_dir = Some(parse_target_dir(arg.split_once('=').unwrap().1)?);
+                        all.push("--target-dir=/target".into());
+                    }
                 }
             } else {
                 if (!arg.starts_with('-') || arg == "--list") && sc.is_none() {
@@ -167,26 +223,6 @@ pub fn parse(target_list: &TargetList) -> Result<Args> {
         }
     }
 
-    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() {
-            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") {
-        // FIXME: remove this when we deprecate CROSS_DOCKER_IN_DOCKER.
-        bool_from_envvar(&value)
-    } else {
-        false
-    };
-
-    let enable_doctests = env::var("CROSS_UNSTABLE_ENABLE_DOCTESTS")
-        .map(|s| bool_from_envvar(&s))
-        .unwrap_or_default();
-
     Ok(Args {
         all,
         subcommand: sc,
@@ -194,10 +230,10 @@ pub fn parse(target_list: &TargetList) -> Result<Args> {
         target,
         features,
         target_dir,
-        docker_in_docker,
-        enable_doctests,
         manifest_path,
         version,
-        msg_info,
+        verbose,
+        quiet,
+        color,
     })
 }
diff --git a/src/config.rs b/src/config.rs
index ecc7edf..8a9e6c8 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,4 +1,4 @@
-use crate::shell::{self, MessageInfo};
+use crate::shell::MessageInfo;
 use crate::{CrossToml, Result, Target, TargetList};
 
 use std::collections::HashMap;
@@ -99,6 +99,16 @@ impl Environment {
         self.get_build_var("TARGET")
             .or_else(|| std::env::var("CARGO_BUILD_TARGET").ok())
     }
+
+    fn doctests(&self) -> Option<bool> {
+        env::var("CROSS_UNSTABLE_ENABLE_DOCTESTS")
+            .map(|s| bool_from_envvar(&s))
+            .ok()
+    }
+
+    fn custom_toolchain(&self) -> bool {
+        std::env::var("CROSS_CUSTOM_TOOLCHAIN").is_ok()
+    }
 }
 
 fn split_to_cloned_by_ws(string: &str) -> Vec<String> {
@@ -129,7 +139,7 @@ impl Config {
         }
     }
 
-    pub fn confusable_target(&self, target: &Target, msg_info: MessageInfo) -> Result<()> {
+    pub fn confusable_target(&self, target: &Target, msg_info: &mut 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
@@ -141,11 +151,8 @@ impl Config {
                     .replace(|c| c == '-' || c == '_', "")
                     .to_lowercase();
                 if mentioned_target != target && mentioned_target_norm == target_norm {
-                    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,
-                    )?;
+                    msg_info.warn("a target named \"{mentioned_target}\" is mentioned in the Cross configuration, but the current specified target is \"{target}\".")?;
+                    msg_info.status(" > Is the target misspelled in the Cross configuration?")?;
                 }
             }
         }
@@ -267,6 +274,14 @@ impl Config {
         self.string_from_config(target, Environment::runner, CrossToml::runner)
     }
 
+    pub fn doctests(&self) -> Option<bool> {
+        self.env.doctests()
+    }
+
+    pub fn custom_toolchain(&self) -> bool {
+        self.env.custom_toolchain()
+    }
+
     pub fn env_passthrough(&self, target: &Target) -> Result<Option<Vec<String>>> {
         self.vec_from_config(
             target,
@@ -457,9 +472,11 @@ mod tests {
         }
 
         fn toml(content: &str) -> Result<crate::CrossToml> {
-            Ok(CrossToml::parse_from_cross(content, MessageInfo::default())
-                .wrap_err("couldn't parse toml")?
-                .0)
+            Ok(
+                CrossToml::parse_from_cross(content, &mut MessageInfo::default())
+                    .wrap_err("couldn't parse toml")?
+                    .0,
+            )
         }
 
         #[test]
diff --git a/src/cross_toml.rs b/src/cross_toml.rs
index 770fa15..edb9cb9 100644
--- a/src/cross_toml.rs
+++ b/src/cross_toml.rs
@@ -1,6 +1,6 @@
 #![doc = include_str!("../docs/cross_toml.md")]
 
-use crate::shell::{self, MessageInfo};
+use crate::shell::MessageInfo;
 use crate::{config, errors::*};
 use crate::{Target, TargetList};
 use serde::de::DeserializeOwned;
@@ -79,7 +79,7 @@ impl CrossToml {
     pub fn parse(
         cargo_toml: &str,
         cross_toml: &str,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
     ) -> Result<(Self, BTreeSet<String>)> {
         let (cross_toml, mut unused) = Self::parse_from_cross(cross_toml, msg_info)?;
 
@@ -94,7 +94,7 @@ impl CrossToml {
     /// Parses the [`CrossToml`] from a string
     pub fn parse_from_cross(
         toml_str: &str,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
     ) -> Result<(Self, BTreeSet<String>)> {
         let mut tomld = toml::Deserializer::new(toml_str);
         Self::parse_from_deserializer(&mut tomld, msg_info)
@@ -103,7 +103,7 @@ impl CrossToml {
     /// Parses the [`CrossToml`] from a string containing the Cargo.toml contents
     pub fn parse_from_cargo(
         cargo_toml_str: &str,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
     ) -> Result<Option<(Self, BTreeSet<String>)>> {
         let cargo_toml: toml::Value = toml::from_str(cargo_toml_str)?;
         let cross_metadata_opt = cargo_toml
@@ -124,7 +124,7 @@ impl CrossToml {
     /// Parses the [`CrossToml`] from a [`Deserializer`]
     fn parse_from_deserializer<'de, D>(
         deserializer: D,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
     ) -> Result<(Self, BTreeSet<String>)>
     where
         D: Deserializer<'de>,
@@ -136,13 +136,10 @@ impl CrossToml {
         })?;
 
         if !unused.is_empty() {
-            shell::warn(
-                format!(
-                    "found unused key(s) in Cross configuration:\n > {}",
-                    unused.clone().into_iter().collect::<Vec<_>>().join(", ")
-                ),
-                msg_info,
-            )?;
+            msg_info.warn(format!(
+                "found unused key(s) in Cross configuration:\n > {}",
+                unused.clone().into_iter().collect::<Vec<_>>().join(", ")
+            ))?;
         }
 
         Ok((cfg, unused))
@@ -401,10 +398,13 @@ where
 #[cfg(test)]
 mod tests {
     use super::*;
-    const MSG_INFO: MessageInfo = MessageInfo {
-        color_choice: shell::ColorChoice::Never,
-        verbosity: shell::Verbosity::Quiet,
-    };
+    use crate::shell;
+
+    macro_rules! m {
+        () => {
+            MessageInfo::new(shell::ColorChoice::Never, shell::Verbosity::Quiet)
+        };
+    }
 
     macro_rules! s {
         ($x:literal) => {
@@ -418,7 +418,7 @@ mod tests {
             targets: HashMap::new(),
             build: CrossBuildConfig::default(),
         };
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross("", MSG_INFO)?;
+        let (parsed_cfg, unused) = CrossToml::parse_from_cross("", &mut m!())?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -452,7 +452,7 @@ mod tests {
           volumes = ["VOL1_ARG", "VOL2_ARG"]
           passthrough = ["VAR1", "VAR2"]
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, MSG_INFO)?;
+        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, &mut m!())?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -496,7 +496,7 @@ mod tests {
             image = "test-image"
             pre-build = []
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, MSG_INFO)?;
+        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, &mut m!())?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -560,7 +560,7 @@ mod tests {
             [target.aarch64-unknown-linux-gnu.env]
             volumes = ["VOL"]
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, MSG_INFO)?;
+        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, &mut m!())?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -579,7 +579,7 @@ mod tests {
           cross = "1.2.3"
         "#;
 
-        let res = CrossToml::parse_from_cargo(test_str, MSG_INFO)?;
+        let res = CrossToml::parse_from_cargo(test_str, &mut m!())?;
         assert!(res.is_none());
 
         Ok(())
@@ -614,7 +614,7 @@ mod tests {
           xargo = true
         "#;
 
-        if let Some((parsed_cfg, _unused)) = CrossToml::parse_from_cargo(test_str, MSG_INFO)? {
+        if let Some((parsed_cfg, _unused)) = CrossToml::parse_from_cargo(test_str, &mut m!())? {
             assert_eq!(parsed_cfg, cfg);
         } else {
             panic!("Parsing result is None");
@@ -722,9 +722,9 @@ mod tests {
         "#;
 
         // Parses configs
-        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)?;
+        let (cfg1, _) = CrossToml::parse_from_cross(cfg1_str, &mut m!())?;
+        let (cfg2, _) = CrossToml::parse_from_cross(cfg2_str, &mut m!())?;
+        let (cfg_expected, _) = CrossToml::parse_from_cross(cfg_expected_str, &mut m!())?;
 
         // Merges config and compares
         let cfg_merged = cfg1.merge(cfg2)?;
diff --git a/src/docker/custom.rs b/src/docker/custom.rs
index 856c4bb..1e6f3cf 100644
--- a/src/docker/custom.rs
+++ b/src/docker/custom.rs
@@ -1,9 +1,9 @@
 use std::io::Write;
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 
-use crate::docker::Engine;
+use crate::docker::{DockerOptions, DockerPaths};
 use crate::shell::MessageInfo;
-use crate::{config::Config, docker, CargoMetadata, Target};
+use crate::{docker, CargoMetadata, Target};
 use crate::{errors::*, file, CommandExt, ToUtf8};
 
 use super::{image_name, parse_docker_opts, path_hash};
@@ -23,25 +23,22 @@ pub enum Dockerfile<'a> {
 }
 
 impl<'a> Dockerfile<'a> {
-    #[allow(clippy::too_many_arguments)]
     pub fn build(
         &self,
-        config: &Config,
-        metadata: &CargoMetadata,
-        engine: &Engine,
-        host_root: &Path,
+        options: &DockerOptions,
+        paths: &DockerPaths,
         build_args: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>,
-        target_triple: &Target,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
     ) -> Result<String> {
-        let mut docker_build = docker::subcommand(engine, "build");
-        docker_build.current_dir(host_root);
+        let mut docker_build = docker::subcommand(&options.engine, "build");
+        docker_build.current_dir(paths.host_root());
         docker_build.env("DOCKER_SCAN_SUGGEST", "false");
         docker_build.args([
             "--label",
             &format!(
-                "{}.for-cross-target={target_triple}",
-                crate::CROSS_LABEL_DOMAIN
+                "{}.for-cross-target={}",
+                crate::CROSS_LABEL_DOMAIN,
+                options.target,
             ),
         ]);
 
@@ -50,28 +47,29 @@ impl<'a> Dockerfile<'a> {
             &format!(
                 "{}.workspace_root={}",
                 crate::CROSS_LABEL_DOMAIN,
-                metadata.workspace_root.to_utf8()?
+                paths.workspace_root().to_utf8()?
             ),
         ]);
 
-        let image_name = self.image_name(target_triple, metadata)?;
+        let image_name = self.image_name(&options.target, &paths.metadata)?;
         docker_build.args(["--tag", &image_name]);
 
         for (key, arg) in build_args.into_iter() {
             docker_build.args(["--build-arg", &format!("{}={}", key.as_ref(), arg.as_ref())]);
         }
 
-        if let Some(arch) = target_triple.deb_arch() {
+        if let Some(arch) = options.target.deb_arch() {
             docker_build.args(["--build-arg", &format!("CROSS_DEB_ARCH={arch}")]);
         }
 
         let path = match self {
             Dockerfile::File { path, .. } => PathBuf::from(path),
             Dockerfile::Custom { content } => {
-                let path = metadata
+                let path = paths
+                    .metadata
                     .target_directory
-                    .join(target_triple.to_string())
-                    .join(format!("Dockerfile.{}-custom", target_triple,));
+                    .join(options.target.to_string())
+                    .join(format!("Dockerfile.{}-custom", &options.target));
                 {
                     let mut file = file::write_file(&path, true)?;
                     file.write_all(content.as_bytes())?;
@@ -81,7 +79,7 @@ impl<'a> Dockerfile<'a> {
         };
 
         if matches!(self, Dockerfile::File { .. }) {
-            if let Ok(cross_base_image) = self::image_name(config, target_triple) {
+            if let Ok(cross_base_image) = self::image_name(&options.config, &options.target) {
                 docker_build.args([
                     "--build-arg",
                     &format!("CROSS_BASE_IMAGE={cross_base_image}"),
diff --git a/src/docker/engine.rs b/src/docker/engine.rs
index 37a15dd..1694acf 100644
--- a/src/docker/engine.rs
+++ b/src/docker/engine.rs
@@ -22,27 +22,38 @@ pub enum EngineType {
 pub struct Engine {
     pub kind: EngineType,
     pub path: PathBuf,
+    pub in_docker: bool,
     pub is_remote: bool,
 }
 
 impl Engine {
-    pub fn new(is_remote: Option<bool>, msg_info: MessageInfo) -> Result<Engine> {
+    pub fn new(
+        in_docker: Option<bool>,
+        is_remote: Option<bool>,
+        msg_info: &mut MessageInfo,
+    ) -> Result<Engine> {
         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, msg_info)
+        Self::from_path(path, in_docker, is_remote, msg_info)
     }
 
     pub fn from_path(
         path: PathBuf,
+        in_docker: Option<bool>,
         is_remote: Option<bool>,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
     ) -> Result<Engine> {
         let kind = get_engine_type(&path, msg_info)?;
+        let in_docker = match in_docker {
+            Some(v) => v,
+            None => Self::in_docker(msg_info)?,
+        };
         let is_remote = is_remote.unwrap_or_else(Self::is_remote);
         Ok(Engine {
             path,
             kind,
+            in_docker,
             is_remote,
         })
     }
@@ -51,6 +62,24 @@ impl Engine {
         self.is_remote && self.kind == EngineType::Podman
     }
 
+    pub fn in_docker(msg_info: &mut MessageInfo) -> Result<bool> {
+        Ok(
+            if let Ok(value) = env::var("CROSS_CONTAINER_IN_CONTAINER") {
+                if env::var("CROSS_DOCKER_IN_DOCKER").is_ok() {
+                    msg_info.warn(
+                        "using both `CROSS_CONTAINER_IN_CONTAINER` and `CROSS_DOCKER_IN_DOCKER`.",
+                    )?;
+                }
+                bool_from_envvar(&value)
+            } else if let Ok(value) = env::var("CROSS_DOCKER_IN_DOCKER") {
+                // FIXME: remove this when we deprecate CROSS_DOCKER_IN_DOCKER.
+                bool_from_envvar(&value)
+            } else {
+                false
+            },
+        )
+    }
+
     pub fn is_remote() -> bool {
         env::var("CROSS_REMOTE")
             .map(|s| bool_from_envvar(&s))
@@ -60,7 +89,7 @@ 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, msg_info: MessageInfo) -> Result<EngineType> {
+fn get_engine_type(ce: &Path, msg_info: &mut MessageInfo) -> Result<EngineType> {
     let stdout = Command::new(ce)
         .arg("--help")
         .run_and_get_stdout(msg_info)?
diff --git a/src/docker/local.rs b/src/docker/local.rs
index a87666f..1e1b4f8 100644
--- a/src/docker/local.rs
+++ b/src/docker/local.rs
@@ -1,54 +1,40 @@
 use std::io;
-use std::path::Path;
 use std::process::ExitStatus;
 
-use super::engine::*;
 use super::shared::*;
-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 eyre::Context;
 
-#[allow(clippy::too_many_arguments)] // TODO: refactor
 pub(crate) fn run(
-    engine: &Engine,
-    target: &Target,
+    options: DockerOptions,
+    paths: DockerPaths,
     args: &[String],
-    metadata: &CargoMetadata,
-    config: &Config,
-    uses_xargo: bool,
-    sysroot: &Path,
-    msg_info: MessageInfo,
-    docker_in_docker: bool,
-    cwd: &Path,
+    msg_info: &mut MessageInfo,
 ) -> Result<ExitStatus> {
-    let mount_finder = MountFinder::create(engine, docker_in_docker)?;
-    let dirs = Directories::create(&mount_finder, metadata, cwd, sysroot)?;
+    let engine = &options.engine;
+    let dirs = &paths.directories;
 
-    let mut cmd = cargo_safe_command(uses_xargo);
+    let mut cmd = cargo_safe_command(options.uses_xargo);
     cmd.args(args);
 
     let mut docker = subcommand(engine, "run");
     docker_userns(&mut docker);
-    docker_envvars(&mut docker, config, target, msg_info)?;
+    docker_envvars(&mut docker, &options.config, &options.target, msg_info)?;
 
     let mount_volumes = docker_mount(
         &mut docker,
-        metadata,
-        &mount_finder,
-        config,
-        target,
-        cwd,
+        &options,
+        &paths,
         |docker, val| mount(docker, val, ""),
         |_| {},
     )?;
 
     docker.arg("--rm");
 
-    docker_seccomp(&mut docker, engine.kind, target, metadata)
+    docker_seccomp(&mut docker, engine.kind, &options.target, &paths.metadata)
         .wrap_err("when copying seccomp profile")?;
     docker_user_id(&mut docker, engine.kind);
 
@@ -68,7 +54,7 @@ pub(crate) fn run(
     docker
         .args(&["-v", &format!("{}:/rust:Z,ro", dirs.sysroot.to_utf8()?)])
         .args(&["-v", &format!("{}:/target:Z", dirs.target.to_utf8()?)]);
-    docker_cwd(&mut docker, metadata, &dirs, cwd, mount_volumes)?;
+    docker_cwd(&mut docker, &paths, mount_volumes)?;
 
     // When running inside NixOS or using Nix packaging we need to add the Nix
     // Store to the running container so it can load the needed binaries.
@@ -85,9 +71,10 @@ pub(crate) fn run(
             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, msg_info)
+    let mut image = options.image_name()?;
+    if options.needs_custom_image() {
+        image = options
+            .custom_image_build(&paths, msg_info)
             .wrap_err("when building custom image")?
     }
 
diff --git a/src/docker/mod.rs b/src/docker/mod.rs
index 1e5cbb0..b0b3de4 100644
--- a/src/docker/mod.rs
+++ b/src/docker/mod.rs
@@ -7,60 +7,26 @@ mod shared;
 pub use self::engine::*;
 pub use self::shared::*;
 
-use std::path::Path;
 use std::process::ExitStatus;
 
-use crate::cargo::CargoMetadata;
 use crate::errors::*;
-use crate::shell::{self, MessageInfo};
-use crate::{Config, Target};
+use crate::shell::MessageInfo;
 
-#[allow(clippy::too_many_arguments)] // TODO: refactor
 pub fn run(
-    engine: &Engine,
-    target: &Target,
+    options: DockerOptions,
+    paths: DockerPaths,
     args: &[String],
-    metadata: &CargoMetadata,
-    config: &Config,
-    uses_xargo: bool,
-    sysroot: &Path,
-    msg_info: MessageInfo,
-    docker_in_docker: bool,
-    cwd: &Path,
+    msg_info: &mut MessageInfo,
 ) -> Result<ExitStatus> {
-    if cfg!(target_os = "windows") && docker_in_docker {
-        shell::fatal(
+    if cfg!(target_os = "windows") && options.in_docker() {
+        msg_info.fatal(
             "running cross insider a container running windows is currently unsupported",
-            msg_info,
             1,
         );
     }
-    if engine.is_remote {
-        remote::run(
-            engine,
-            target,
-            args,
-            metadata,
-            config,
-            uses_xargo,
-            sysroot,
-            msg_info,
-            docker_in_docker,
-            cwd,
-        )
-        .wrap_err("could not complete remote run")
+    if options.is_remote() {
+        remote::run(options, paths, args, msg_info).wrap_err("could not complete remote run")
     } else {
-        local::run(
-            engine,
-            target,
-            args,
-            metadata,
-            config,
-            uses_xargo,
-            sysroot,
-            msg_info,
-            docker_in_docker,
-            cwd,
-        )
+        local::run(options, paths, args, msg_info)
     }
 }
diff --git a/src/docker/remote.rs b/src/docker/remote.rs
index b013fcf..31c487b 100644
--- a/src/docker/remote.rs
+++ b/src/docker/remote.rs
@@ -9,35 +9,40 @@ use eyre::Context;
 use super::engine::Engine;
 use super::shared::*;
 use crate::cargo::CargoMetadata;
-use crate::config::{bool_from_envvar, Config};
+use crate::config::bool_from_envvar;
 use crate::errors::Result;
 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::shell::{ColorChoice, MessageInfo, Stream, Verbosity};
 use crate::temp;
 use crate::{Host, Target};
 
 // the mount directory for the data volume.
 pub const MOUNT_PREFIX: &str = "/cross";
 
-struct DeleteVolume<'a>(&'a Engine, &'a VolumeId, MessageInfo);
+// it's unlikely that we ever need to erase a line in the destructors,
+// and it's better than keep global state everywhere, or keeping a ref
+// cell which could have already deleted a line
+struct DeleteVolume<'a>(&'a Engine, &'a VolumeId, ColorChoice, Verbosity);
 
 impl<'a> Drop for DeleteVolume<'a> {
     fn drop(&mut self) {
         if let VolumeId::Discard(id) = self.1 {
-            volume_rm(self.0, id, self.2).ok();
+            let mut msg_info = MessageInfo::new(self.2, self.3);
+            volume_rm(self.0, id, &mut msg_info).ok();
         }
     }
 }
 
-struct DeleteContainer<'a>(&'a Engine, &'a str, MessageInfo);
+struct DeleteContainer<'a>(&'a Engine, &'a str, ColorChoice, Verbosity);
 
 impl<'a> Drop for DeleteContainer<'a> {
     fn drop(&mut self) {
-        container_stop(self.0, self.1, self.2).ok();
-        container_rm(self.0, self.1, self.2).ok();
+        let mut msg_info = MessageInfo::new(self.2, self.3);
+        container_stop(self.0, self.1, &mut msg_info).ok();
+        container_rm(self.0, self.1, &mut msg_info).ok();
     }
 }
 
@@ -86,7 +91,7 @@ impl VolumeId {
         engine: &Engine,
         toolchain: &str,
         container: &str,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
     ) -> Result<Self> {
         if volume_exists(engine, toolchain, msg_info)? {
             Ok(Self::Keep(toolchain.to_string()))
@@ -109,7 +114,7 @@ fn create_volume_dir(
     engine: &Engine,
     container: &str,
     dir: &Path,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<ExitStatus> {
     // make our parent directory if needed
     subcommand(engine, "exec")
@@ -124,7 +129,7 @@ fn copy_volume_files(
     container: &str,
     src: &Path,
     dst: &Path,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<ExitStatus> {
     subcommand(engine, "cp")
         .arg("-a")
@@ -156,7 +161,7 @@ fn container_path_exists(
     engine: &Engine,
     container: &str,
     path: &Path,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<bool> {
     Ok(subcommand(engine, "exec")
         .arg(container)
@@ -172,7 +177,7 @@ fn copy_volume_files_nocache(
     src: &Path,
     dst: &Path,
     copy_symlinks: bool,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<ExitStatus> {
     // avoid any cached directories when copying
     // see https://bford.info/cachedir/
@@ -190,7 +195,7 @@ pub fn copy_volume_container_xargo(
     xargo_dir: &Path,
     target: &Target,
     mount_prefix: &Path,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<()> {
     // only need to copy the rustlib files for our current target.
     let triple = target.triple();
@@ -211,7 +216,7 @@ pub fn copy_volume_container_cargo(
     cargo_dir: &Path,
     mount_prefix: &Path,
     copy_registry: bool,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<()> {
     let dst = mount_prefix.join("cargo");
     let copy_registry = env::var("CROSS_REMOTE_COPY_REGISTRY")
@@ -300,9 +305,9 @@ where
     Ok(had_symlinks)
 }
 
-fn warn_symlinks(had_symlinks: bool, msg_info: MessageInfo) -> Result<()> {
+fn warn_symlinks(had_symlinks: bool, msg_info: &mut MessageInfo) -> Result<()> {
     if had_symlinks {
-        shell::warn("copied directory contained symlinks. if the volume the link points to was not mounted, the remote build may fail", msg_info)
+        msg_info.warn("copied directory contained symlinks. if the volume the link points to was not mounted, the remote build may fail")
     } else {
         Ok(())
     }
@@ -314,7 +319,7 @@ fn copy_volume_container_rust_base(
     container: &str,
     sysroot: &Path,
     mount_prefix: &Path,
-    msg_info: MessageInfo,
+    msg_info: &mut 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.
@@ -361,7 +366,7 @@ fn copy_volume_container_rust_manifest(
     container: &str,
     sysroot: &Path,
     mount_prefix: &Path,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<()> {
     // copy over all the manifest files in rustlib
     // these are small text files containing names/paths to toolchains
@@ -392,7 +397,7 @@ pub fn copy_volume_container_rust_triple(
     triple: &str,
     mount_prefix: &Path,
     skip_exists: bool,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<()> {
     // copy over the files for a specific triple
     let dst = mount_prefix.join("rust");
@@ -426,7 +431,7 @@ pub fn copy_volume_container_rust(
     target: &Target,
     mount_prefix: &Path,
     skip_target: bool,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<()> {
     let target_triple = target.triple();
     let image_triple = Host::X86_64UnknownLinuxGnu.triple();
@@ -557,7 +562,7 @@ fn copy_volume_file_list(
     src: &Path,
     dst: &Path,
     files: &[&str],
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<ExitStatus> {
     // SAFETY: safe, single-threaded execution.
     let tempdir = unsafe { temp::TempDir::new()? };
@@ -578,11 +583,11 @@ fn remove_volume_file_list(
     container: &str,
     dst: &Path,
     files: &[&str],
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<ExitStatus> {
     const PATH: &str = "/tmp/remove_list";
     let mut script = vec![];
-    if msg_info.verbose() {
+    if msg_info.is_verbose() {
         script.push("set -x".to_string());
     }
     script.push(format!(
@@ -620,13 +625,13 @@ fn copy_volume_container_project(
     dst: &Path,
     volume: &VolumeId,
     copy_cache: bool,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<()> {
-    let copy_all = || {
+    let copy_all = |info: &mut MessageInfo| {
         if copy_cache {
-            copy_volume_files(engine, container, src, dst, msg_info)
+            copy_volume_files(engine, container, src, dst, info)
         } else {
-            copy_volume_files_nocache(engine, container, src, dst, true, msg_info)
+            copy_volume_files_nocache(engine, container, src, dst, true, info)
         }
     };
     match volume {
@@ -648,32 +653,40 @@ fn copy_volume_container_project(
                 }
             } else {
                 write_project_fingerprint(&fingerprint, &current)?;
-                copy_all()?;
+                copy_all(msg_info)?;
             }
         }
         VolumeId::Discard(_) => {
-            copy_all()?;
+            copy_all(msg_info)?;
         }
     }
 
     Ok(())
 }
 
-fn run_and_get_status(engine: &Engine, args: &[&str], msg_info: MessageInfo) -> Result<ExitStatus> {
+fn run_and_get_status(
+    engine: &Engine,
+    args: &[&str],
+    msg_info: &mut MessageInfo,
+) -> Result<ExitStatus> {
     command(engine)
         .args(args)
         .run_and_get_status(msg_info, true)
 }
 
-pub fn volume_create(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Result<ExitStatus> {
+pub fn volume_create(
+    engine: &Engine,
+    volume: &str,
+    msg_info: &mut MessageInfo,
+) -> Result<ExitStatus> {
     run_and_get_status(engine, &["volume", "create", volume], msg_info)
 }
 
-pub fn volume_rm(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Result<ExitStatus> {
+pub fn volume_rm(engine: &Engine, volume: &str, msg_info: &mut MessageInfo) -> Result<ExitStatus> {
     run_and_get_status(engine, &["volume", "rm", volume], msg_info)
 }
 
-pub fn volume_exists(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Result<bool> {
+pub fn volume_exists(engine: &Engine, volume: &str, msg_info: &mut MessageInfo) -> Result<bool> {
     command(engine)
         .args(&["volume", "inspect", volume])
         .run_and_get_output(msg_info)
@@ -683,19 +696,23 @@ pub fn volume_exists(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Re
 pub fn container_stop(
     engine: &Engine,
     container: &str,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<ExitStatus> {
     run_and_get_status(engine, &["stop", container], msg_info)
 }
 
-pub fn container_rm(engine: &Engine, container: &str, msg_info: MessageInfo) -> Result<ExitStatus> {
+pub fn container_rm(
+    engine: &Engine,
+    container: &str,
+    msg_info: &mut MessageInfo,
+) -> Result<ExitStatus> {
     run_and_get_status(engine, &["rm", container], msg_info)
 }
 
 pub fn container_state(
     engine: &Engine,
     container: &str,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<ContainerState> {
     let stdout = command(engine)
         .args(&["ps", "-a"])
@@ -751,21 +768,15 @@ fn mount_path(val: &Path) -> Result<String> {
     canonicalize_mount_path(&host_path)
 }
 
-#[allow(clippy::too_many_arguments)] // TODO: refactor
 pub(crate) fn run(
-    engine: &Engine,
-    target: &Target,
+    options: DockerOptions,
+    paths: DockerPaths,
     args: &[String],
-    metadata: &CargoMetadata,
-    config: &Config,
-    uses_xargo: bool,
-    sysroot: &Path,
-    msg_info: MessageInfo,
-    docker_in_docker: bool,
-    cwd: &Path,
+    msg_info: &mut MessageInfo,
 ) -> Result<ExitStatus> {
-    let mount_finder = MountFinder::create(engine, docker_in_docker)?;
-    let dirs = Directories::create(&mount_finder, metadata, cwd, sysroot)?;
+    let engine = &options.engine;
+    let target = &options.target;
+    let dirs = &paths.directories;
 
     let mount_prefix = MOUNT_PREFIX;
 
@@ -790,20 +801,20 @@ pub(crate) fn run(
     // 1. get our unique identifiers and cleanup from a previous 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 container = unique_container_identifier(target, &paths.metadata, dirs)?;
     let volume = VolumeId::create(engine, &toolchain_id, &container, msg_info)?;
     let state = container_state(engine, &container, msg_info)?;
     if !state.is_stopped() {
-        shell::warn(format_args!("container {container} was running."), msg_info)?;
+        msg_info.warn(format_args!("container {container} was running."))?;
         container_stop(engine, &container, msg_info)?;
     }
     if state.exists() {
-        shell::warn(format_args!("container {container} was exited."), msg_info)?;
+        msg_info.warn(format_args!("container {container} was exited."))?;
         container_rm(engine, &container, msg_info)?;
     }
     if let VolumeId::Discard(ref id) = volume {
         if volume_exists(engine, id, msg_info)? {
-            shell::warn(format_args!("temporary volume {id} existed."), msg_info)?;
+            msg_info.warn(format_args!("temporary volume {id} existed."))?;
             volume_rm(engine, id, msg_info)?;
         }
     }
@@ -812,29 +823,26 @@ pub(crate) fn run(
     if let VolumeId::Discard(ref id) = volume {
         volume_create(engine, id, msg_info).wrap_err("when creating volume")?;
     }
-    let _volume_deletter = DeleteVolume(engine, &volume, msg_info);
+    let _volume_deletter = DeleteVolume(engine, &volume, msg_info.color_choice, msg_info.verbosity);
 
     // 3. create our start container command here
     let mut docker = subcommand(engine, "run");
     docker_userns(&mut docker);
     docker.args(&["--name", &container]);
     docker.args(&["-v", &format!("{}:{mount_prefix}", volume.as_ref())]);
-    docker_envvars(&mut docker, config, target, msg_info)?;
+    docker_envvars(&mut docker, &options.config, target, msg_info)?;
 
     let mut volumes = vec![];
     let mount_volumes = docker_mount(
         &mut docker,
-        metadata,
-        &mount_finder,
-        config,
-        target,
-        cwd,
+        &options,
+        &paths,
         |_, val| mount_path(val),
         |(src, dst)| volumes.push((src, dst)),
     )
     .wrap_err("could not determine mount points")?;
 
-    docker_seccomp(&mut docker, engine.kind, target, metadata)
+    docker_seccomp(&mut docker, engine.kind, target, &paths.metadata)
         .wrap_err("when copying seccomp profile")?;
 
     // Prevent `bin` from being mounted inside the Docker container.
@@ -853,21 +861,26 @@ pub(crate) fn run(
     }
 
     docker
-        .arg(&image_name(config, target)?)
+        .arg(&image_name(&options.config, target)?)
         // ensure the process never exits until we stop it
         .args(&["sh", "-c", "sleep infinity"])
         .run_and_get_status(msg_info, true)?;
-    let _container_deletter = DeleteContainer(engine, &container, msg_info);
+    let _container_deletter = DeleteContainer(
+        engine,
+        &container,
+        msg_info.color_choice,
+        msg_info.verbosity,
+    );
 
     // 4. copy all mounted volumes over
     let copy_cache = env::var("CROSS_REMOTE_COPY_CACHE")
         .map(|s| bool_from_envvar(&s))
         .unwrap_or_default();
-    let copy = |src, dst: &PathBuf| {
+    let copy = |src, dst: &PathBuf, info: &mut MessageInfo| {
         if copy_cache {
-            copy_volume_files(engine, &container, src, dst, msg_info)
+            copy_volume_files(engine, &container, src, dst, info)
         } else {
-            copy_volume_files_nocache(engine, &container, src, dst, true, msg_info)
+            copy_volume_files_nocache(engine, &container, src, dst, true, info)
         }
     };
     let mount_prefix_path = mount_prefix.as_ref();
@@ -951,7 +964,7 @@ pub(crate) fn run(
         // only do if we're copying over cached files.
         let target_dir = mount_prefix_path.join("target");
         if copy_cache {
-            copy(&dirs.target, &target_dir)?;
+            copy(&dirs.target, &target_dir, msg_info)?;
         } else {
             create_volume_dir(engine, &container, &target_dir, msg_info)?;
         }
@@ -971,7 +984,7 @@ pub(crate) fn run(
             if !rel_dst.is_empty() {
                 create_volume_dir(engine, &container, mount_dst.parent().unwrap(), msg_info)?;
             }
-            copy(src, &mount_dst)?;
+            copy(src, &mount_dst, msg_info)?;
         }
     }
 
@@ -1002,12 +1015,12 @@ pub(crate) fn run(
         final_args.push("--target-dir".to_string());
         final_args.push(target_dir_string);
     }
-    let mut cmd = cargo_safe_command(uses_xargo);
+    let mut cmd = cargo_safe_command(options.uses_xargo);
     cmd.args(final_args);
 
     // 5. create symlinks for copied data
     let mut symlink = vec!["set -e pipefail".to_string()];
-    if msg_info.verbose() {
+    if msg_info.is_verbose() {
         symlink.push("set -x".to_string());
     }
     symlink.push(format!(
@@ -1048,7 +1061,7 @@ symlink_recurse \"${{prefix}}\"
     // 6. execute our cargo command inside the container
     let mut docker = subcommand(engine, "exec");
     docker_user_id(&mut docker, engine.kind);
-    docker_cwd(&mut docker, metadata, &dirs, cwd, mount_volumes)?;
+    docker_cwd(&mut docker, &paths, mount_volumes)?;
     docker.arg(&container);
     docker.args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)]);
     let status = docker
diff --git a/src/docker/shared.rs b/src/docker/shared.rs
index 9e8cece..d04b1da 100644
--- a/src/docker/shared.rs
+++ b/src/docker/shared.rs
@@ -12,7 +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::shell::{MessageInfo, Verbosity};
 use crate::Target;
 
 pub use super::custom::CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX;
@@ -28,6 +28,181 @@ const DOCKER_IMAGES: &[&str] = &include!(concat!(env!("OUT_DIR"), "/docker-image
 // to fork the process, and which podman allows by default.
 pub(crate) const SECCOMP: &str = include_str!("seccomp.json");
 
+#[derive(Debug)]
+pub struct DockerOptions {
+    pub engine: Engine,
+    pub target: Target,
+    pub config: Config,
+    pub uses_xargo: bool,
+}
+
+impl DockerOptions {
+    pub fn new(engine: Engine, target: Target, config: Config, uses_xargo: bool) -> DockerOptions {
+        DockerOptions {
+            engine,
+            target,
+            config,
+            uses_xargo,
+        }
+    }
+
+    pub fn in_docker(&self) -> bool {
+        self.engine.in_docker
+    }
+
+    pub fn is_remote(&self) -> bool {
+        self.engine.is_remote
+    }
+
+    pub fn needs_custom_image(&self) -> bool {
+        self.config
+            .dockerfile(&self.target)
+            .unwrap_or_default()
+            .is_some()
+            || !self
+                .config
+                .pre_build(&self.target)
+                .unwrap_or_default()
+                .unwrap_or_default()
+                .is_empty()
+    }
+
+    pub(crate) fn custom_image_build(
+        &self,
+        paths: &DockerPaths,
+        msg_info: &mut MessageInfo,
+    ) -> Result<String> {
+        let mut image = image_name(&self.config, &self.target)?;
+
+        if let Some(path) = self.config.dockerfile(&self.target)? {
+            let context = self.config.dockerfile_context(&self.target)?;
+            let name = self.config.image(&self.target)?;
+
+            let build = Dockerfile::File {
+                path: &path,
+                context: context.as_deref(),
+                name: name.as_deref(),
+            };
+
+            image = build
+                .build(
+                    self,
+                    paths,
+                    self.config
+                        .dockerfile_build_args(&self.target)?
+                        .unwrap_or_default(),
+                    msg_info,
+                )
+                .wrap_err("when building dockerfile")?;
+        }
+        let pre_build = self.config.pre_build(&self.target)?;
+
+        if let Some(pre_build) = pre_build {
+            if !pre_build.is_empty() {
+                let custom = Dockerfile::Custom {
+                    content: format!(
+                        r#"
+        FROM {image}
+        ARG CROSS_DEB_ARCH=
+        ARG CROSS_CMD
+        RUN eval "${{CROSS_CMD}}""#
+                    ),
+                };
+                custom
+                    .build(
+                        self,
+                        paths,
+                        Some(("CROSS_CMD", pre_build.join("\n"))),
+                        msg_info,
+                    )
+                    .wrap_err("when pre-building")
+                    .with_note(|| format!("CROSS_CMD={}", pre_build.join("\n")))?;
+                image = custom.image_name(&self.target, &paths.metadata)?;
+            }
+        }
+        Ok(image)
+    }
+
+    pub(crate) fn image_name(&self) -> Result<String> {
+        if let Some(image) = self.config.image(&self.target)? {
+            return Ok(image);
+        }
+
+        if !DOCKER_IMAGES.contains(&self.target.triple()) {
+            eyre::bail!(
+                "`cross` does not provide a Docker image for target {target}, \
+                   specify a custom image in `Cross.toml`.",
+                target = self.target
+            );
+        }
+
+        let version = if include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")).is_empty() {
+            env!("CARGO_PKG_VERSION")
+        } else {
+            "main"
+        };
+
+        Ok(format!(
+            "{CROSS_IMAGE}/{target}:{version}",
+            target = self.target
+        ))
+    }
+}
+
+#[derive(Debug)]
+pub struct DockerPaths {
+    pub mount_finder: MountFinder,
+    pub metadata: CargoMetadata,
+    pub cwd: PathBuf,
+    pub sysroot: PathBuf,
+    pub directories: Directories,
+}
+
+impl DockerPaths {
+    pub fn create(
+        engine: &Engine,
+        metadata: CargoMetadata,
+        cwd: PathBuf,
+        sysroot: PathBuf,
+    ) -> Result<Self> {
+        let mount_finder = MountFinder::create(engine)?;
+        let directories = Directories::create(&mount_finder, &metadata, &cwd, &sysroot)?;
+        Ok(Self {
+            mount_finder,
+            metadata,
+            cwd,
+            sysroot,
+            directories,
+        })
+    }
+
+    pub fn workspace_root(&self) -> &Path {
+        &self.metadata.workspace_root
+    }
+
+    pub fn workspace_dependencies(&self) -> impl Iterator<Item = &Path> {
+        self.metadata.path_dependencies()
+    }
+
+    pub fn workspace_from_cwd(&self) -> Result<&Path> {
+        self.cwd
+            .strip_prefix(self.workspace_root())
+            .map_err(Into::into)
+    }
+
+    pub fn in_workspace(&self) -> bool {
+        self.workspace_from_cwd().is_ok()
+    }
+
+    pub fn mount_cwd(&self) -> &str {
+        &self.directories.mount_cwd
+    }
+
+    pub fn host_root(&self) -> &Path {
+        &self.directories.host_root
+    }
+}
+
 #[derive(Debug)]
 pub struct Directories {
     pub cargo: PathBuf,
@@ -141,10 +316,9 @@ pub fn get_package_info(
     engine: &Engine,
     target: &str,
     channel: Option<&str>,
-    docker_in_docker: bool,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<(Target, CargoMetadata, Directories)> {
-    let target_list = rustc::target_list((msg_info.color_choice, Verbosity::Quiet).into())?;
+    let target_list = msg_info.as_quiet(rustc::target_list)?;
     let target = Target::from(target, &target_list);
     let metadata = cargo_metadata_with_args(None, None, msg_info)?
         .ok_or(eyre::eyre!("unable to get project metadata"))?;
@@ -153,14 +327,14 @@ pub fn get_package_info(
     let host = host_meta.host();
 
     let sysroot = rustc::get_sysroot(&host, &target, channel, msg_info)?.1;
-    let mount_finder = MountFinder::create(engine, docker_in_docker)?;
+    let mount_finder = MountFinder::create(engine)?;
     let dirs = Directories::create(&mount_finder, &metadata, &cwd, &sysroot)?;
 
     Ok((target, metadata, dirs))
 }
 
 /// Register binfmt interpreters
-pub(crate) fn register(engine: &Engine, target: &Target, msg_info: MessageInfo) -> Result<()> {
+pub(crate) fn register(engine: &Engine, target: &Target, msg_info: &mut 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 && \
@@ -255,7 +429,7 @@ pub(crate) fn docker_envvars(
     docker: &mut Command,
     config: &Config,
     target: &Target,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<()> {
     for ref var in config.env_passthrough(target)?.unwrap_or_default() {
         validate_env_var(var)?;
@@ -289,10 +463,7 @@ pub(crate) fn docker_envvars(
 
     if let Ok(value) = env::var("CROSS_CONTAINER_OPTS") {
         if env::var("DOCKER_OPTS").is_ok() {
-            shell::warn(
-                "using both `CROSS_CONTAINER_OPTS` and `DOCKER_OPTS`.",
-                msg_info,
-            )?;
+            msg_info.warn("using both `CROSS_CONTAINER_OPTS` and `DOCKER_OPTS`.")?;
         }
         docker.args(&parse_docker_opts(&value)?);
     } else if let Ok(value) = env::var("DOCKER_OPTS") {
@@ -305,44 +476,41 @@ pub(crate) fn docker_envvars(
 
 pub(crate) fn docker_cwd(
     docker: &mut Command,
-    metadata: &CargoMetadata,
-    dirs: &Directories,
-    cwd: &Path,
+    paths: &DockerPaths,
     mount_volumes: bool,
 ) -> Result<()> {
     if mount_volumes {
-        docker.args(&["-w", &dirs.mount_cwd]);
-    } else if dirs.mount_cwd == metadata.workspace_root.to_utf8()? {
+        docker.args(&["-w", paths.mount_cwd()]);
+    } else if paths.mount_cwd() == paths.workspace_root().to_utf8()? {
         docker.args(&["-w", "/project"]);
     } else {
         // We do this to avoid clashes with path separators. Windows uses `\` as a path separator on Path::join
-        let cwd = &cwd;
-        let working_dir = Path::new("/project").join(cwd.strip_prefix(&metadata.workspace_root)?);
+        let working_dir = Path::new("/project").join(paths.workspace_from_cwd()?);
         docker.args(&["-w", &working_dir.as_posix()?]);
     }
 
     Ok(())
 }
 
-#[allow(clippy::too_many_arguments)] // TODO: refactor
 pub(crate) fn docker_mount(
     docker: &mut Command,
-    metadata: &CargoMetadata,
-    mount_finder: &MountFinder,
-    config: &Config,
-    target: &Target,
-    cwd: &Path,
+    options: &DockerOptions,
+    paths: &DockerPaths,
     mount_cb: impl Fn(&mut Command, &Path) -> Result<String>,
     mut store_cb: impl FnMut((String, String)),
 ) -> Result<bool> {
     let mut mount_volumes = false;
     // FIXME(emilgardis 2022-04-07): This is a fallback so that if it's hard for us to do mounting logic, make it simple(r)
     // Preferably we would not have to do this.
-    if cwd.strip_prefix(&metadata.workspace_root).is_err() {
+    if !paths.in_workspace() {
         mount_volumes = true;
     }
 
-    for ref var in config.env_volumes(target)?.unwrap_or_default() {
+    for ref var in options
+        .config
+        .env_volumes(&options.target)?
+        .unwrap_or_default()
+    {
         let (var, value) = validate_env_var(var)?;
         let value = match value {
             Some(v) => Ok(v.to_string()),
@@ -351,7 +519,7 @@ pub(crate) fn docker_mount(
 
         if let Ok(val) = value {
             let canonical_val = file::canonicalize(&val)?;
-            let host_path = mount_finder.find_path(&canonical_val, true)?;
+            let host_path = paths.mount_finder.find_path(&canonical_val, true)?;
             let mount_path = mount_cb(docker, host_path.as_ref())?;
             docker.args(&["-e", &format!("{}={}", host_path, mount_path)]);
             store_cb((val, mount_path));
@@ -359,9 +527,9 @@ pub(crate) fn docker_mount(
         }
     }
 
-    for path in metadata.path_dependencies() {
+    for path in paths.workspace_dependencies() {
         let canonical_path = file::canonicalize(path)?;
-        let host_path = mount_finder.find_path(&canonical_path, true)?;
+        let host_path = paths.mount_finder.find_path(&canonical_path, true)?;
         let mount_path = mount_cb(docker, host_path.as_ref())?;
         store_cb((path.to_utf8()?.to_string(), mount_path));
         mount_volumes = true;
@@ -454,78 +622,6 @@ pub(crate) fn docker_seccomp(
     Ok(())
 }
 
-pub fn needs_custom_image(target: &Target, config: &Config) -> bool {
-    config.dockerfile(target).unwrap_or_default().is_some()
-        || !config
-            .pre_build(target)
-            .unwrap_or_default()
-            .unwrap_or_default()
-            .is_empty()
-}
-
-pub(crate) fn custom_image_build(
-    target: &Target,
-    config: &Config,
-    metadata: &CargoMetadata,
-    Directories { host_root, .. }: Directories,
-    engine: &Engine,
-    msg_info: MessageInfo,
-) -> Result<String> {
-    let mut image = image_name(config, target)?;
-
-    if let Some(path) = config.dockerfile(target)? {
-        let context = config.dockerfile_context(target)?;
-        let name = config.image(target)?;
-
-        let build = Dockerfile::File {
-            path: &path,
-            context: context.as_deref(),
-            name: name.as_deref(),
-        };
-
-        image = build
-            .build(
-                config,
-                metadata,
-                engine,
-                &host_root,
-                config.dockerfile_build_args(target)?.unwrap_or_default(),
-                target,
-                msg_info,
-            )
-            .wrap_err("when building dockerfile")?;
-    }
-    let pre_build = config.pre_build(target)?;
-
-    if let Some(pre_build) = pre_build {
-        if !pre_build.is_empty() {
-            let custom = Dockerfile::Custom {
-                content: format!(
-                    r#"
-    FROM {image}
-    ARG CROSS_DEB_ARCH=
-    ARG CROSS_CMD
-    RUN eval "${{CROSS_CMD}}""#
-                ),
-            };
-            custom
-                .build(
-                    config,
-                    metadata,
-                    engine,
-                    &host_root,
-                    Some(("CROSS_CMD", pre_build.join("\n"))),
-                    target,
-                    msg_info,
-                )
-                .wrap_err("when pre-building")
-                .with_note(|| format!("CROSS_CMD={}", pre_build.join("\n")))?;
-            image = custom.image_name(target, metadata)?;
-        }
-    }
-    Ok(image)
-}
-
 pub(crate) fn image_name(config: &Config, target: &Target) -> Result<String> {
     if let Some(image) = config.image(target)? {
         return Ok(image);
@@ -556,7 +652,7 @@ fn docker_read_mount_paths(engine: &Engine) -> Result<Vec<MountDetail>> {
         command
     };
 
-    let output = docker.run_and_get_stdout(Verbosity::Quiet.into())?;
+    let output = docker.run_and_get_stdout(&mut Verbosity::Quiet.into())?;
     let info = serde_json::from_str(&output).wrap_err("failed to parse docker inspect output")?;
     dockerinfo_parse_mounts(&info)
 }
@@ -631,8 +727,8 @@ impl MountFinder {
         MountFinder { mounts }
     }
 
-    pub fn create(engine: &Engine, docker_in_docker: bool) -> Result<MountFinder> {
-        Ok(if docker_in_docker {
+    pub fn create(engine: &Engine) -> Result<MountFinder> {
+        Ok(if engine.in_docker {
             MountFinder::new(docker_read_mount_paths(engine)?)
         } else {
             MountFinder::default()
@@ -789,11 +885,11 @@ mod tests {
             }
         }
 
-        fn create_engine(msg_info: MessageInfo) -> Result<Engine> {
-            Engine::from_path(get_container_engine()?, Some(false), msg_info)
+        fn create_engine(msg_info: &mut MessageInfo) -> Result<Engine> {
+            Engine::from_path(get_container_engine()?, None, Some(false), msg_info)
         }
 
-        fn cargo_metadata(subdir: bool, msg_info: MessageInfo) -> Result<CargoMetadata> {
+        fn cargo_metadata(subdir: bool, msg_info: &mut MessageInfo) -> Result<CargoMetadata> {
             let mut metadata = cargo_metadata_with_args(
                 Some(Path::new(env!("CARGO_MANIFEST_DIR"))),
                 None,
@@ -860,7 +956,7 @@ mod tests {
         fn test_host() -> Result<()> {
             let vars = unset_env();
             let mount_finder = MountFinder::new(vec![]);
-            let metadata = cargo_metadata(false, MessageInfo::default())?;
+            let metadata = cargo_metadata(false, &mut MessageInfo::default())?;
             let directories = get_directories(&metadata, &mount_finder)?;
             paths_equal(&directories.cargo, &home()?.join(".cargo"))?;
             paths_equal(&directories.xargo, &home()?.join(".xargo"))?;
@@ -880,7 +976,8 @@ mod tests {
         fn test_docker_in_docker() -> Result<()> {
             let vars = unset_env();
 
-            let engine = create_engine(MessageInfo::default());
+            let mut msg_info = MessageInfo::default();
+            let engine = create_engine(&mut msg_info);
             let hostname = env::var("HOSTNAME");
             if engine.is_err() || hostname.is_err() {
                 eprintln!("could not get container engine or no hostname found");
@@ -888,18 +985,23 @@ mod tests {
                 return Ok(());
             }
             let engine = engine.unwrap();
+            if !engine.in_docker {
+                eprintln!("not in docker");
+                reset_env(vars);
+                return Ok(());
+            }
             let hostname = hostname.unwrap();
             let output = subcommand(&engine, "inspect")
                 .arg(hostname)
-                .run_and_get_output(MessageInfo::default())?;
+                .run_and_get_output(&mut msg_info)?;
             if !output.status.success() {
                 eprintln!("inspect failed");
                 reset_env(vars);
                 return Ok(());
             }
 
-            let mount_finder = MountFinder::create(&engine, true)?;
-            let metadata = cargo_metadata(true, MessageInfo::default())?;
+            let mount_finder = MountFinder::create(&engine)?;
+            let metadata = cargo_metadata(true, &mut msg_info)?;
             let directories = get_directories(&metadata, &mount_finder)?;
             let mount_finder = MountFinder::new(docker_read_mount_paths(&engine)?);
             let mount_path = |p| mount_finder.find_mount_path(p);
diff --git a/src/extensions.rs b/src/extensions.rs
index 64f194c..85be3c7 100644
--- a/src/extensions.rs
+++ b/src/extensions.rs
@@ -3,42 +3,45 @@ use std::fmt;
 use std::process::{Command, ExitStatus, Output};
 
 use crate::errors::*;
-use crate::shell::{self, MessageInfo, Verbosity};
+use crate::shell::MessageInfo;
 
 pub const STRIPPED_BINS: &[&str] = &[crate::docker::DOCKER, crate::docker::PODMAN, "cargo"];
 
 pub trait CommandExt {
-    fn fmt_message(&self, msg_info: MessageInfo) -> String;
+    fn fmt_message(&self, msg_info: &mut MessageInfo) -> String;
 
-    fn print(&self, msg_info: MessageInfo) -> Result<()> {
-        shell::print(&self.fmt_message(msg_info), msg_info)
+    fn print(&self, msg_info: &mut MessageInfo) -> Result<()> {
+        let msg = self.fmt_message(msg_info);
+        msg_info.print(&msg)
     }
 
-    fn info(&self, msg_info: MessageInfo) -> Result<()> {
-        shell::info(&self.fmt_message(msg_info), msg_info)
+    fn info(&self, msg_info: &mut MessageInfo) -> Result<()> {
+        let msg = self.fmt_message(msg_info);
+        msg_info.info(&msg)
     }
 
-    fn debug(&self, msg_info: MessageInfo) -> Result<()> {
-        shell::debug(&self.fmt_message(msg_info), msg_info)
+    fn debug(&self, msg_info: &mut MessageInfo) -> Result<()> {
+        let msg = self.fmt_message(msg_info);
+        msg_info.debug(&msg)
     }
 
     fn status_result(
         &self,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
         status: ExitStatus,
         output: Option<&Output>,
     ) -> Result<(), CommandError>;
-    fn run(&mut self, msg_info: MessageInfo, silence_stdout: bool) -> Result<()>;
+    fn run(&mut self, msg_info: &mut MessageInfo, silence_stdout: bool) -> Result<()>;
     fn run_and_get_status(
         &mut self,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
         silence_stdout: bool,
     ) -> Result<ExitStatus>;
-    fn run_and_get_stdout(&mut self, msg_info: MessageInfo) -> Result<String>;
-    fn run_and_get_output(&mut self, msg_info: MessageInfo) -> Result<std::process::Output>;
+    fn run_and_get_stdout(&mut self, msg_info: &mut MessageInfo) -> Result<String>;
+    fn run_and_get_output(&mut self, msg_info: &mut MessageInfo) -> Result<std::process::Output>;
     fn command_pretty(
         &self,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
         strip: impl for<'a> Fn(&'a str) -> bool,
     ) -> String;
 }
@@ -46,12 +49,12 @@ pub trait CommandExt {
 impl CommandExt for Command {
     fn command_pretty(
         &self,
-        msg_info: MessageInfo,
+        msg_info: &mut 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, MessageInfo, F);
-        impl<F> std::fmt::Display for C<'_, F>
+        struct C<'c, 'd, F>(&'c Command, &'d mut MessageInfo, F);
+        impl<'e, 'f, F> std::fmt::Display for C<'e, 'f, F>
         where
             F: for<'a> Fn(&'a str) -> bool,
         {
@@ -61,7 +64,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.verbose() {
+                    crate::file::pretty_path(cmd.get_program(), move |c| if self.1.is_verbose() {
                         false
                     } else {
                         self.2(c)
@@ -81,18 +84,25 @@ impl CommandExt for Command {
         format!("{}", C(self, msg_info, strip))
     }
 
-    fn fmt_message(&self, msg_info: MessageInfo) -> String {
-        let verbose = (msg_info.color_choice, Verbosity::Verbose).into();
+    fn fmt_message(&self, mut msg_info: &mut MessageInfo) -> String {
+        let msg_info = &mut msg_info;
         if let Some(cwd) = self.get_current_dir() {
-            format!("+ {:?} {}", cwd, self.command_pretty(verbose, |_| false))
+            format!(
+                "+ {:?} {}",
+                cwd,
+                msg_info.as_verbose(|info| self.command_pretty(info, |_| false))
+            )
         } else {
-            format!("+ {}", self.command_pretty(verbose, |_| false))
+            format!(
+                "+ {}",
+                msg_info.as_verbose(|info| self.command_pretty(info, |_| false))
+            )
         }
     }
 
     fn status_result(
         &self,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
         status: ExitStatus,
         output: Option<&Output>,
     ) -> Result<(), CommandError> {
@@ -110,7 +120,7 @@ impl CommandExt for Command {
     }
 
     /// Runs the command to completion
-    fn run(&mut self, msg_info: MessageInfo, silence_stdout: bool) -> Result<()> {
+    fn run(&mut self, msg_info: &mut 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)
@@ -119,11 +129,11 @@ impl CommandExt for Command {
     /// Runs the command to completion
     fn run_and_get_status(
         &mut self,
-        msg_info: MessageInfo,
+        msg_info: &mut MessageInfo,
         silence_stdout: bool,
     ) -> Result<ExitStatus> {
         self.debug(msg_info)?;
-        if silence_stdout && !msg_info.verbose() {
+        if silence_stdout && msg_info.is_verbose() {
             self.stdout(std::process::Stdio::null());
         }
         self.status()
@@ -136,7 +146,7 @@ impl CommandExt for Command {
     }
 
     /// Runs the command to completion and returns its stdout
-    fn run_and_get_stdout(&mut self, msg_info: MessageInfo) -> Result<String> {
+    fn run_and_get_stdout(&mut self, msg_info: &mut MessageInfo) -> Result<String> {
         let out = self.run_and_get_output(msg_info)?;
         self.status_result(msg_info, out.status, Some(&out))
             .map_err(CommandError::to_section_report)?;
@@ -148,7 +158,7 @@ impl CommandExt for Command {
     /// # Notes
     ///
     /// This command does not check the status.
-    fn run_and_get_output(&mut self, msg_info: MessageInfo) -> Result<std::process::Output> {
+    fn run_and_get_output(&mut self, msg_info: &mut MessageInfo) -> Result<std::process::Output> {
         self.debug(msg_info)?;
         self.output().map_err(|e| {
             CommandError::CouldNotExecute {
diff --git a/src/lib.rs b/src/lib.rs
index e7999df..1f5c946 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -338,7 +338,7 @@ impl From<Host> for Target {
             Host::Aarch64UnknownLinuxMusl => Target::new_built_in("aarch64-unknown-linux-musl"),
             Host::Other(s) => Target::from(
                 s.as_str(),
-                &rustc::target_list(Verbosity::Quiet.into()).unwrap(),
+                &rustc::target_list(&mut Verbosity::Quiet.into()).unwrap(),
             ),
         }
     }
@@ -360,18 +360,17 @@ impl Serialize for Target {
     }
 }
 
-fn warn_on_failure(target: &Target, toolchain: &str, msg_info: MessageInfo) -> Result<()> {
+fn warn_on_failure(target: &Target, toolchain: &str, msg_info: &mut MessageInfo) -> Result<()> {
     let rust_std = format!("rust-std-{target}");
     if target.is_builtin() {
         let component = rustup::check_component(&rust_std, toolchain, msg_info)?;
         if component.is_not_available() {
-            shell::warn(format!("rust-std is not available for {target}"), msg_info)?;
-            shell::note(
+            msg_info.warn(format!("rust-std is not available for {target}"))?;
+            msg_info.note(
                 format_args!(
                     r#"you may need to build components for the target via `-Z build-std=<components>` or in your cross configuration specify `target.{target}.build-std`
               the available components are core, std, alloc, and proc_macro"#
                 ),
-                msg_info,
             )?;
         }
     }
@@ -379,49 +378,47 @@ fn warn_on_failure(target: &Target, toolchain: &str, msg_info: MessageInfo) -> R
 }
 
 pub fn run() -> Result<ExitStatus> {
-    let target_list = rustc::target_list(Verbosity::Quiet.into())?;
+    let target_list = rustc::target_list(&mut Verbosity::Quiet.into())?;
     let args = cli::parse(&target_list)?;
+    let mut msg_info = shell::MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?;
 
     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,
-        )?;
+        msg_info.print(format!(
+            concat!("cross ", env!("CARGO_PKG_VERSION"), "{}"),
+            commit_info
+        ))?;
     }
 
     let host_version_meta = rustc::version_meta()?;
     let cwd = std::env::current_dir()?;
-    if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), args.msg_info)? {
+    if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), &mut msg_info)? {
         let host = host_version_meta.host();
-        let toml = toml(&metadata, args.msg_info)?;
+        let toml = toml(&metadata, &mut 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, args.msg_info)?;
+        config.confusable_target(&target, &mut msg_info)?;
 
         let image_exists = match docker::image_name(&config, &target) {
             Ok(_) => true,
             Err(err) => {
-                shell::warn(err, args.msg_info)?;
+                msg_info.warn(err)?;
                 false
             }
         };
 
         if image_exists && host.is_supported(Some(&target)) {
             let (toolchain, sysroot) =
-                rustc::get_sysroot(&host, &target, args.channel.as_deref(), args.msg_info)?;
+                rustc::get_sysroot(&host, &target, args.channel.as_deref(), &mut msg_info)?;
             let mut is_nightly = toolchain.contains("nightly");
 
-            let installed_toolchains = rustup::installed_toolchains(args.msg_info)?;
+            let installed_toolchains = rustup::installed_toolchains(&mut msg_info)?;
 
             if !installed_toolchains.into_iter().any(|t| t == toolchain) {
-                rustup::install_toolchain(&toolchain, args.msg_info)?;
+                rustup::install_toolchain(&toolchain, &mut 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)? {
@@ -430,7 +427,7 @@ pub fn run() -> Result<ExitStatus> {
                     &toolchain,
                     &rustc_version,
                     &rustc_commit,
-                    args.msg_info,
+                    &mut msg_info,
                 )?;
                 is_nightly = channel == Channel::Nightly;
             }
@@ -438,10 +435,10 @@ pub fn run() -> Result<ExitStatus> {
             let uses_build_std = config.build_std(&target).unwrap_or(false);
             let uses_xargo =
                 !uses_build_std && config.xargo(&target).unwrap_or(!target.is_builtin());
-            if std::env::var("CROSS_CUSTOM_TOOLCHAIN").is_err() {
+            if !config.custom_toolchain() {
                 // 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, args.msg_info)?;
+                let available_targets = rustup::available_targets(&toolchain, &mut msg_info)?;
 
                 if !is_nightly && uses_build_std {
                     eyre::bail!(
@@ -454,17 +451,17 @@ pub fn run() -> Result<ExitStatus> {
                     && !available_targets.is_installed(&target)
                     && available_targets.contains(&target)
                 {
-                    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)?;
+                    rustup::install(&target, &toolchain, &mut msg_info)?;
+                } else if !rustup::component_is_installed("rust-src", &toolchain, &mut msg_info)? {
+                    rustup::install_component("rust-src", &toolchain, &mut msg_info)?;
                 }
                 if args
                     .subcommand
                     .map(|sc| sc == Subcommand::Clippy)
                     .unwrap_or(false)
-                    && !rustup::component_is_installed("clippy", &toolchain, args.msg_info)?
+                    && !rustup::component_is_installed("clippy", &toolchain, &mut msg_info)?
                 {
-                    rustup::install_component("clippy", &toolchain, args.msg_info)?;
+                    rustup::install_component("clippy", &toolchain, &mut msg_info)?;
                 }
             }
 
@@ -503,7 +500,7 @@ pub fn run() -> Result<ExitStatus> {
                 .subcommand
                 .map(|sc| sc == Subcommand::Test)
                 .unwrap_or(false);
-            if is_test && args.enable_doctests && is_nightly {
+            if is_test && config.doctests().unwrap_or_default() && is_nightly {
                 filtered_args.push("-Zdoctest-xcompile".to_string());
             }
             if uses_build_std {
@@ -516,34 +513,26 @@ pub fn run() -> Result<ExitStatus> {
                 .map(|sc| sc.needs_docker(is_remote))
                 .unwrap_or(false);
             if target.needs_docker() && needs_docker {
-                let engine = docker::Engine::new(Some(is_remote), args.msg_info)?;
+                let engine = docker::Engine::new(None, Some(is_remote), &mut msg_info)?;
                 if host_version_meta.needs_interpreter()
                     && needs_interpreter
                     && target.needs_interpreter()
                     && !interpreter::is_registered(&target)?
                 {
-                    docker::register(&engine, &target, args.msg_info)?
+                    docker::register(&engine, &target, &mut msg_info)?
                 }
 
-                let status = docker::run(
-                    &engine,
-                    &target,
-                    &filtered_args,
-                    &metadata,
-                    &config,
-                    uses_xargo,
-                    &sysroot,
-                    args.msg_info,
-                    args.docker_in_docker,
-                    &cwd,
-                )
-                .wrap_err("could not run container")?;
+                let paths = docker::DockerPaths::create(&engine, metadata, cwd, sysroot)?;
+                let options =
+                    docker::DockerOptions::new(engine, target.clone(), config, uses_xargo);
+                let status = docker::run(options, paths, &filtered_args, &mut msg_info)
+                    .wrap_err("could not run container")?;
                 let needs_host = args
                     .subcommand
                     .map(|sc| sc.needs_host(is_remote))
                     .unwrap_or(false);
                 if !status.success() {
-                    warn_on_failure(&target, &toolchain, args.msg_info)?;
+                    warn_on_failure(&target, &toolchain, &mut msg_info)?;
                 }
                 if !(status.success() && needs_host) {
                     return Ok(status);
@@ -554,14 +543,14 @@ pub fn run() -> Result<ExitStatus> {
 
     // if we fallback to the host cargo, use the same invocation that was made to cross
     let argv: Vec<String> = env::args().skip(1).collect();
-    shell::note("Falling back to `cargo` on the host.", args.msg_info)?;
+    msg_info.note("Falling back to `cargo` on the host.")?;
     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, args.msg_info)?;
+            let out = cargo::run_and_get_output(&argv, &mut msg_info)?;
             let stdout = out.stdout()?;
             if out.status.success() && cli::is_subcommand_list(&stdout) {
-                cli::fmt_subcommands(&stdout, args.msg_info)?;
+                cli::fmt_subcommands(&stdout, &mut msg_info)?;
             } else {
                 // Not a list subcommand, which can happen with weird edge-cases.
                 print!("{}", stdout);
@@ -569,7 +558,7 @@ pub fn run() -> Result<ExitStatus> {
             }
             Ok(out.status)
         }
-        _ => cargo::run(&argv, args.msg_info).map_err(Into::into),
+        _ => cargo::run(&argv, &mut msg_info).map_err(Into::into),
     }
 }
 
@@ -586,7 +575,7 @@ pub(crate) fn warn_host_version_mismatch(
     toolchain: &str,
     rustc_version: &rustc_version::Version,
     rustc_commit: &str,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<VersionMatch> {
     let host_commit = (&host_version_meta.short_version_string)
         .splitn(3, ' ')
@@ -605,16 +594,15 @@ pub(crate) fn warn_host_version_mismatch(
             host_version_meta.short_version_string
         );
         if versions.is_lt() || (versions.is_eq() && dates.is_lt()) {
-            shell::warn(format!("using older {rustc_warning}.\n > Update with `rustup update --force-non-host {toolchain}`"), msg_info)?;
+            msg_info.warn(format!("using older {rustc_warning}.\n > Update with `rustup update --force-non-host {toolchain}`"))?;
             return Ok(VersionMatch::OlderTarget);
         } else if versions.is_gt() || (versions.is_eq() && dates.is_gt()) {
-            shell::warn(
-                format!("using newer {rustc_warning}.\n > Update with `rustup update`"),
-                msg_info,
-            )?;
+            msg_info.warn(format!(
+                "using newer {rustc_warning}.\n > Update with `rustup update`"
+            ))?;
             return Ok(VersionMatch::NewerTarget);
         } else {
-            shell::warn(format!("using {rustc_warning}."), msg_info)?;
+            msg_info.warn(format!("using {rustc_warning}."))?;
             return Ok(VersionMatch::Different);
         }
     }
@@ -630,7 +618,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, msg_info: MessageInfo) -> Result<Option<CrossToml>> {
+fn toml(metadata: &CargoMetadata, msg_info: &mut MessageInfo) -> Result<Option<CrossToml>> {
     let root = &metadata.workspace_root;
     let cross_config_path = match env::var("CROSS_CONFIG") {
         Ok(var) => PathBuf::from(var),
@@ -652,7 +640,7 @@ fn toml(metadata: &CargoMetadata, msg_info: MessageInfo) -> Result<Option<CrossT
     } else {
         // Checks if there is a lowercase version of this file
         if root.join("cross.toml").exists() {
-            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)?;
+            msg_info.warn("There's a file named cross.toml, instead of Cross.toml. You may want to rename it, or it won't be considered.")?;
         }
 
         if let Some((cfg, _)) = CrossToml::parse_from_cargo(&cargo_toml_str, msg_info)? {
diff --git a/src/rustc.rs b/src/rustc.rs
index fe5cb13..a68fd26 100644
--- a/src/rustc.rs
+++ b/src/rustc.rs
@@ -81,7 +81,7 @@ pub fn rustc_command() -> Command {
     Command::new(env_program("RUSTC", "rustc"))
 }
 
-pub fn target_list(msg_info: MessageInfo) -> Result<TargetList> {
+pub fn target_list(msg_info: &mut MessageInfo) -> Result<TargetList> {
     rustc_command()
         .args(&["--print", "target-list"])
         .run_and_get_stdout(msg_info)
@@ -90,7 +90,7 @@ pub fn target_list(msg_info: MessageInfo) -> Result<TargetList> {
         })
 }
 
-pub fn sysroot(host: &Host, target: &Target, msg_info: MessageInfo) -> Result<PathBuf> {
+pub fn sysroot(host: &Host, target: &Target, msg_info: &mut MessageInfo) -> Result<PathBuf> {
     let mut stdout = rustc_command()
         .args(&["--print", "sysroot"])
         .run_and_get_stdout(msg_info)?
@@ -109,7 +109,7 @@ pub fn get_sysroot(
     host: &Host,
     target: &Target,
     channel: Option<&str>,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<(String, PathBuf)> {
     let mut sysroot = sysroot(host, target, msg_info)?;
     let default_toolchain = sysroot
diff --git a/src/rustup.rs b/src/rustup.rs
index 4872eb8..f04fd07 100644
--- a/src/rustup.rs
+++ b/src/rustup.rs
@@ -27,8 +27,11 @@ impl AvailableTargets {
     }
 }
 
-fn rustup_command(msg_info: MessageInfo) -> Command {
+fn rustup_command(msg_info: &mut MessageInfo, no_flags: bool) -> Command {
     let mut cmd = Command::new("rustup");
+    if no_flags {
+        return cmd;
+    }
     match msg_info.verbosity {
         Verbosity::Quiet => {
             cmd.arg("--quiet");
@@ -41,8 +44,8 @@ fn rustup_command(msg_info: MessageInfo) -> Command {
     cmd
 }
 
-pub fn installed_toolchains(msg_info: MessageInfo) -> Result<Vec<String>> {
-    let out = Command::new("rustup")
+pub fn installed_toolchains(msg_info: &mut MessageInfo) -> Result<Vec<String>> {
+    let out = rustup_command(msg_info, true)
         .args(&["toolchain", "list"])
         .run_and_get_stdout(msg_info)?;
 
@@ -57,8 +60,8 @@ pub fn installed_toolchains(msg_info: MessageInfo) -> Result<Vec<String>> {
         .collect())
 }
 
-pub fn available_targets(toolchain: &str, msg_info: MessageInfo) -> Result<AvailableTargets> {
-    let mut cmd = Command::new("rustup");
+pub fn available_targets(toolchain: &str, msg_info: &mut MessageInfo) -> Result<AvailableTargets> {
+    let mut cmd = rustup_command(msg_info, true);
     cmd.args(&["target", "list", "--toolchain", toolchain]);
     let output = cmd
         .run_and_get_output(msg_info)
@@ -97,24 +100,28 @@ pub fn available_targets(toolchain: &str, msg_info: MessageInfo) -> Result<Avail
     })
 }
 
-pub fn install_toolchain(toolchain: &str, msg_info: MessageInfo) -> Result<()> {
-    rustup_command(msg_info)
+pub fn install_toolchain(toolchain: &str, msg_info: &mut MessageInfo) -> Result<()> {
+    rustup_command(msg_info, false)
         .args(&["toolchain", "add", toolchain, "--profile", "minimal"])
         .run(msg_info, false)
         .wrap_err_with(|| format!("couldn't install toolchain `{toolchain}`"))
 }
 
-pub fn install(target: &Target, toolchain: &str, msg_info: MessageInfo) -> Result<()> {
+pub fn install(target: &Target, toolchain: &str, msg_info: &mut MessageInfo) -> Result<()> {
     let target = target.triple();
 
-    rustup_command(msg_info)
+    rustup_command(msg_info, false)
         .args(&["target", "add", target, "--toolchain", toolchain])
         .run(msg_info, false)
         .wrap_err_with(|| format!("couldn't install `std` for {target}"))
 }
 
-pub fn install_component(component: &str, toolchain: &str, msg_info: MessageInfo) -> Result<()> {
-    rustup_command(msg_info)
+pub fn install_component(
+    component: &str,
+    toolchain: &str,
+    msg_info: &mut MessageInfo,
+) -> Result<()> {
+    rustup_command(msg_info, false)
         .args(&["component", "add", component, "--toolchain", toolchain])
         .run(msg_info, false)
         .wrap_err_with(|| format!("couldn't install the `{component}` component"))
@@ -139,7 +146,7 @@ impl<'a> Component<'a> {
 pub fn check_component<'a>(
     component: &'a str,
     toolchain: &str,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<Component<'a>> {
     Ok(Command::new("rustup")
         .args(&["component", "list", "--toolchain", toolchain])
@@ -163,7 +170,7 @@ pub fn check_component<'a>(
 pub fn component_is_installed(
     component: &str,
     toolchain: &str,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> Result<bool> {
     Ok(check_component(component, toolchain, msg_info)?.is_installed())
 }
diff --git a/src/shell.rs b/src/shell.rs
index 66e9edb..3805b00 100644
--- a/src/shell.rs
+++ b/src/shell.rs
@@ -7,6 +7,61 @@ use std::io::{self, Write};
 use crate::errors::Result;
 use owo_colors::{self, OwoColorize};
 
+// 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)
+    }};
+}
+
 /// the requested verbosity of output.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum Verbosity {
@@ -36,13 +91,24 @@ pub enum ColorChoice {
 }
 
 // Should simplify the APIs a lot.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct MessageInfo {
     pub color_choice: ColorChoice,
     pub verbosity: Verbosity,
+    pub stdout_needs_erase: bool,
+    pub stderr_needs_erase: bool,
 }
 
 impl MessageInfo {
+    pub const fn new(color_choice: ColorChoice, verbosity: Verbosity) -> MessageInfo {
+        MessageInfo {
+            color_choice,
+            verbosity,
+            stdout_needs_erase: false,
+            stderr_needs_erase: false,
+        }
+    }
+
     pub fn create(verbose: bool, quiet: bool, color: Option<&str>) -> Result<MessageInfo> {
         let color_choice = get_color_choice(color)?;
         let verbosity = get_verbosity(color_choice, verbose, quiet)?;
@@ -50,205 +116,180 @@ impl MessageInfo {
         Ok(MessageInfo {
             color_choice,
             verbosity,
+            stdout_needs_erase: false,
+            stderr_needs_erase: false,
         })
     }
 
-    pub fn verbose(self) -> bool {
+    pub fn is_verbose(&self) -> bool {
         self.verbosity.verbose()
     }
-}
 
-impl Default for MessageInfo {
-    fn default() -> MessageInfo {
-        MessageInfo {
-            color_choice: ColorChoice::Auto,
-            verbosity: Verbosity::Normal,
-        }
-    }
-}
+    fn as_verbosity<T, C: Fn(&mut MessageInfo) -> T>(&mut self, call: C, new: Verbosity) -> T {
+        let old = self.verbosity;
+        self.verbosity = new;
+        let result = call(self);
+        self.verbosity = old;
 
-impl From<ColorChoice> for MessageInfo {
-    fn from(color_choice: ColorChoice) -> MessageInfo {
-        MessageInfo {
-            color_choice,
-            verbosity: Verbosity::Normal,
-        }
+        result
     }
-}
 
-impl From<Verbosity> for MessageInfo {
-    fn from(verbosity: Verbosity) -> MessageInfo {
-        MessageInfo {
-            color_choice: ColorChoice::Auto,
-            verbosity,
-        }
+    pub fn as_quiet<T, C: Fn(&mut MessageInfo) -> T>(&mut self, call: C) -> T {
+        self.as_verbosity(call, Verbosity::Quiet)
     }
-}
 
-impl From<(ColorChoice, Verbosity)> for MessageInfo {
-    fn from(info: (ColorChoice, Verbosity)) -> MessageInfo {
-        MessageInfo {
-            color_choice: info.0,
-            verbosity: info.1,
-        }
+    pub fn as_normal<T, C: Fn(&mut MessageInfo) -> T>(&mut self, call: C) -> T {
+        self.as_verbosity(call, Verbosity::Normal)
     }
-}
 
-// get the prefix for stderr messages
-macro_rules! cross_prefix {
-    ($s:literal) => {
-        concat!("[cross]", " ", $s)
-    };
-}
+    pub fn as_verbose<T, C: Fn(&mut MessageInfo) -> T>(&mut self, call: C) -> T {
+        self.as_verbosity(call, Verbosity::Verbose)
+    }
 
-// 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()))*
-            ),
-        }?;
-    }};
-}
+    fn erase_line<S: Stream + Write>(&mut self, stream: &mut S) -> Result<()> {
+        // this is the Erase in Line sequence
+        stream.write_all(b"\x1B[K").map_err(Into::into)
+    }
 
-// 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, " ")?,
+    fn stdout_check_erase(&mut self) -> Result<()> {
+        if self.stdout_needs_erase {
+            self.erase_line(&mut io::stdout())?;
+            self.stdout_needs_erase = false;
         }
+        Ok(())
+    }
 
+    fn stderr_check_erase(&mut self) -> Result<()> {
+        if self.stderr_needs_erase {
+            self.erase_line(&mut io::stderr())?;
+            self.stderr_needs_erase = false;
+        }
         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)
-    }};
-}
+    /// prints a red 'error' message and terminates.
+    pub fn fatal<T: fmt::Display>(&mut self, message: T, code: i32) -> ! {
+        self.error(message).unwrap();
+        std::process::exit(code);
+    }
 
-// 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)
-    }};
+    /// prints a red 'error' message.
+    pub fn error<T: fmt::Display>(&mut self, message: T) -> Result<()> {
+        self.stderr_check_erase()?;
+        status!(@stderr cross_prefix!("error"), Some(&message), red, self)
+    }
 
-    (@stdout $status:expr, $message:expr, $color:ident, $msg_info:expr  $(,)?) => {{
-        message!(@status @name stdout, $status, $message, $color, $msg_info)
-    }};
-}
+    /// prints an amber 'warning' message.
+    pub fn warn<T: fmt::Display>(&mut self, message: T) -> Result<()> {
+        match self.verbosity {
+            Verbosity::Quiet => Ok(()),
+            _ => status!(@stderr
+                cross_prefix!("warning"),
+                Some(&message),
+                yellow,
+                self,
+            ),
+        }
+    }
 
-/// prints a red 'error' message and terminates.
-pub fn fatal<T: fmt::Display>(message: T, msg_info: MessageInfo, code: i32) -> ! {
-    error(message, msg_info).unwrap();
-    std::process::exit(code);
-}
+    /// prints a cyan 'note' message.
+    pub fn note<T: fmt::Display>(&mut self, message: T) -> Result<()> {
+        match self.verbosity {
+            Verbosity::Quiet => Ok(()),
+            _ => status!(@stderr cross_prefix!("note"), Some(&message), cyan, self),
+        }
+    }
 
-/// prints a red 'error' message.
-pub fn error<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    status!(@stderr cross_prefix!("error"), Some(&message), red, msg_info)
-}
+    pub fn status<T: fmt::Display>(&mut self, message: T) -> Result<()> {
+        match self.verbosity {
+            Verbosity::Quiet => Ok(()),
+            _ => {
+                eprintln!("{}", message);
+                Ok(())
+            }
+        }
+    }
 
-/// prints an amber 'warning' message.
-pub fn warn<T: fmt::Display>(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 high-priority message to stdout.
+    pub fn print<T: fmt::Display>(&mut self, message: T) -> Result<()> {
+        self.stdout_check_erase()?;
+        println!("{}", message);
+        Ok(())
     }
-}
 
-/// prints a cyan 'note' message.
-pub fn note<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    match msg_info.verbosity {
-        Verbosity::Quiet => Ok(()),
-        _ => status!(@stderr cross_prefix!("note"), Some(&message), cyan, msg_info),
+    /// prints a normal message to stdout.
+    pub fn info<T: fmt::Display>(&mut self, message: T) -> Result<()> {
+        match self.verbosity {
+            Verbosity::Quiet => Ok(()),
+            _ => {
+                println!("{}", message);
+                Ok(())
+            }
+        }
     }
-}
 
-pub fn status<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    match msg_info.verbosity {
-        Verbosity::Quiet => Ok(()),
-        _ => {
-            eprintln!("{}", message);
-            Ok(())
+    /// prints a debugging message to stdout.
+    pub fn debug<T: fmt::Display>(&mut self, message: T) -> Result<()> {
+        match self.verbosity {
+            Verbosity::Quiet | Verbosity::Normal => Ok(()),
+            _ => {
+                println!("{}", message);
+                Ok(())
+            }
         }
     }
-}
 
-/// prints a high-priority message to stdout.
-pub fn print<T: fmt::Display>(message: T, _: MessageInfo) -> Result<()> {
-    println!("{}", message);
-    Ok(())
+    pub fn fatal_usage<T: fmt::Display>(&mut self, arg: T, code: i32) -> ! {
+        self.error_usage(arg).unwrap();
+        std::process::exit(code);
+    }
+
+    fn error_usage<T: fmt::Display>(&mut self, arg: T) -> Result<()> {
+        let mut stream = io::stderr();
+        write_style!(stream, self, cross_prefix!("error"), bold, red);
+        write_style!(stream, self, ":", bold);
+        write_style!(stream, self, " The argument '");
+        write_style!(stream, self, arg, yellow);
+        write_style!(stream, self, "' requires a value but none was supplied\n");
+        write_style!(stream, self, "Usage:\n");
+        write_style!(
+            stream,
+            self,
+            "    cross [+toolchain] [OPTIONS] [SUBCOMMAND]\n"
+        );
+        write_style!(stream, self, "\n");
+        write_style!(stream, self, "For more information try ");
+        write_style!(stream, self, "--help", green);
+        write_style!(stream, self, "\n");
+
+        stream.flush()?;
+
+        Ok(())
+    }
 }
 
-/// prints a normal message to stdout.
-pub fn info<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    match msg_info.verbosity {
-        Verbosity::Quiet => Ok(()),
-        _ => {
-            println!("{}", message);
-            Ok(())
-        }
+impl Default for MessageInfo {
+    fn default() -> MessageInfo {
+        MessageInfo::new(ColorChoice::Auto, Verbosity::Normal)
     }
 }
 
-/// prints a debugging message to stdout.
-pub fn debug<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    match msg_info.verbosity {
-        Verbosity::Quiet | Verbosity::Normal => Ok(()),
-        _ => {
-            println!("{}", message);
-            Ok(())
-        }
+impl From<ColorChoice> for MessageInfo {
+    fn from(color_choice: ColorChoice) -> MessageInfo {
+        MessageInfo::new(color_choice, Verbosity::Normal)
     }
 }
 
-pub fn fatal_usage<T: fmt::Display>(arg: T, msg_info: MessageInfo, code: i32) -> ! {
-    error_usage(arg, msg_info).unwrap();
-    std::process::exit(code);
+impl From<Verbosity> for MessageInfo {
+    fn from(verbosity: Verbosity) -> MessageInfo {
+        MessageInfo::new(ColorChoice::Auto, verbosity)
+    }
 }
 
-fn error_usage<T: fmt::Display>(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(())
+impl From<(ColorChoice, Verbosity)> for MessageInfo {
+    fn from(info: (ColorChoice, Verbosity)) -> MessageInfo {
+        MessageInfo::new(info.0, info.1)
+    }
 }
 
 fn get_color_choice(color: Option<&str>) -> Result<ColorChoice> {
@@ -265,14 +306,7 @@ fn get_color_choice(color: Option<&str>) -> Result<ColorChoice> {
 fn get_verbosity(color_choice: ColorChoice, verbose: bool, quiet: bool) -> Result<Verbosity> {
     match (verbose, quiet) {
         (true, true) => {
-            let verbosity = Verbosity::Normal;
-            error(
-                "cannot set both --verbose and --quiet",
-                MessageInfo {
-                    color_choice,
-                    verbosity,
-                },
-            )?;
+            MessageInfo::from(color_choice).error("cannot set both --verbose and --quiet")?;
             std::process::exit(101);
         }
         (true, false) => Ok(Verbosity::Verbose),
diff --git a/xtask/src/build_docker_image.rs b/xtask/src/build_docker_image.rs
index c1e056b..49d4404 100644
--- a/xtask/src/build_docker_image.rs
+++ b/xtask/src/build_docker_image.rs
@@ -3,7 +3,7 @@ use std::path::Path;
 
 use crate::util::{cargo_metadata, gha_error, gha_output, gha_print};
 use clap::Args;
-use cross::shell::{self, MessageInfo};
+use cross::shell::MessageInfo;
 use cross::{docker, CommandExt, ToUtf8};
 
 #[derive(Args, Debug)]
@@ -97,8 +97,6 @@ pub fn build_docker_image(
         repository,
         labels,
         verbose,
-        quiet,
-        color,
         dry_run,
         force,
         push,
@@ -112,8 +110,8 @@ pub fn build_docker_image(
         ..
     }: BuildDockerImage,
     engine: &docker::Engine,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose != 0, quiet, color.as_deref())?;
     let metadata = cargo_metadata(msg_info)?;
     let version = metadata
         .get_package("cross")
@@ -261,7 +259,7 @@ pub fn build_docker_image(
         } else {
             docker_build.print(msg_info)?;
             if !dry_run {
-                shell::fatal("refusing to push, use --force to override", msg_info, 1);
+                msg_info.fatal("refusing to push, use --force to override", 1);
             }
         }
         if gha {
diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs
index d32b2c0..102ce45 100644
--- a/xtask/src/ci.rs
+++ b/xtask/src/ci.rs
@@ -79,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(Verbosity::Verbose.into())?;
+                    .run_and_get_stdout(&mut Verbosity::Verbose.into())?;
                 let (cross, rest) = search
                     .split_once(" = ")
                     .ok_or_else(|| eyre::eyre!("cargo search failed"))?;
diff --git a/xtask/src/crosstool.rs b/xtask/src/crosstool.rs
index b694f5a..6c26455 100644
--- a/xtask/src/crosstool.rs
+++ b/xtask/src/crosstool.rs
@@ -200,17 +200,14 @@ CT_LINUX_VERSION=\"{linux_major}.{linux_minor}.{linux_patch}\""
 
 pub fn configure_crosstool(
     ConfigureCrosstool {
-        verbose,
-        quiet,
-        color,
         gcc_version,
         glibc_version,
         linux_version,
         mut targets,
         ..
     }: ConfigureCrosstool,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
     let gcc_version = gcc_version.as_deref().unwrap_or(DEFAULT_GCC_VERSION);
     let glibc_version = glibc_version.as_deref().unwrap_or(DEFAULT_GLIBC_VERSION);
     let linux_version = linux_version.as_deref().unwrap_or(DEFAULT_LINUX_VERSION);
diff --git a/xtask/src/hooks.rs b/xtask/src/hooks.rs
index 35d3d96..e1af048 100644
--- a/xtask/src/hooks.rs
+++ b/xtask/src/hooks.rs
@@ -5,7 +5,7 @@ use std::process::Command;
 
 use crate::util::{cargo, get_channel_prefer_nightly};
 use clap::Args;
-use cross::shell::{self, MessageInfo};
+use cross::shell::MessageInfo;
 use cross::CommandExt;
 
 const CARGO_FLAGS: &[&str] = &["--all-features", "--all-targets", "--workspace"];
@@ -39,14 +39,14 @@ pub struct Test {
     pub color: Option<String>,
 }
 
-fn cargo_fmt(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<()> {
+fn cargo_fmt(msg_info: &mut MessageInfo, channel: Option<&str>) -> cross::Result<()> {
     cargo(channel)
         .args(&["fmt", "--", "--check"])
         .run(msg_info, false)
         .map_err(Into::into)
 }
 
-fn cargo_clippy(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<()> {
+fn cargo_clippy(msg_info: &mut MessageInfo, channel: Option<&str>) -> cross::Result<()> {
     cargo(channel)
         .arg("clippy")
         .args(CARGO_FLAGS)
@@ -55,7 +55,7 @@ fn cargo_clippy(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<(
         .map_err(Into::into)
 }
 
-fn cargo_test(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<()> {
+fn cargo_test(msg_info: &mut MessageInfo, channel: Option<&str>) -> cross::Result<()> {
     cargo(channel)
         .arg("test")
         .args(CARGO_FLAGS)
@@ -67,14 +67,14 @@ fn splitlines(string: String) -> Vec<String> {
     string.lines().map(|l| l.to_string()).collect()
 }
 
-fn staged_files(msg_info: MessageInfo) -> cross::Result<Vec<String>> {
+fn staged_files(msg_info: &mut MessageInfo) -> cross::Result<Vec<String>> {
     Command::new("git")
         .args(&["diff", "--cached", "--name-only", "--diff-filter=ACM"])
         .run_and_get_stdout(msg_info)
         .map(splitlines)
 }
 
-fn all_files(msg_info: MessageInfo) -> cross::Result<Vec<String>> {
+fn all_files(msg_info: &mut MessageInfo) -> cross::Result<Vec<String>> {
     Command::new("git")
         .arg("ls-files")
         .run_and_get_stdout(msg_info)
@@ -100,7 +100,7 @@ fn is_shell_script(path: impl AsRef<Path>) -> cross::Result<bool> {
     }
 }
 
-fn shellcheck(all: bool, msg_info: MessageInfo) -> cross::Result<()> {
+fn shellcheck(all: bool, msg_info: &mut MessageInfo) -> cross::Result<()> {
     if which::which("shellcheck").is_ok() {
         let files = match all {
             true => all_files(msg_info),
@@ -123,16 +123,11 @@ fn shellcheck(all: bool, msg_info: MessageInfo) -> cross::Result<()> {
 }
 
 pub fn check(
-    Check {
-        verbose,
-        quiet,
-        color,
-        all,
-    }: Check,
+    Check { all, .. }: Check,
     toolchain: Option<&str>,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    shell::info("Running rustfmt, clippy, and shellcheck checks.", msg_info)?;
+    msg_info.info("Running rustfmt, clippy, and shellcheck checks.")?;
 
     let channel = get_channel_prefer_nightly(msg_info, toolchain)?;
     cargo_fmt(msg_info, channel)?;
@@ -142,16 +137,8 @@ pub fn check(
     Ok(())
 }
 
-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)?;
+pub fn test(toolchain: Option<&str>, msg_info: &mut MessageInfo) -> cross::Result<()> {
+    msg_info.info("Running cargo fmt and tests")?;
 
     let channel = get_channel_prefer_nightly(msg_info, toolchain)?;
     cargo_fmt(msg_info, channel)?;
diff --git a/xtask/src/install_git_hooks.rs b/xtask/src/install_git_hooks.rs
index 41c097e..6f52418 100644
--- a/xtask/src/install_git_hooks.rs
+++ b/xtask/src/install_git_hooks.rs
@@ -15,14 +15,7 @@ pub struct InstallGitHooks {
     pub color: Option<String>,
 }
 
-pub fn install_git_hooks(
-    InstallGitHooks {
-        verbose,
-        quiet,
-        color,
-    }: InstallGitHooks,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
+pub fn install_git_hooks(msg_info: &mut MessageInfo) -> cross::Result<()> {
     let root = project_dir(msg_info)?;
     let git_hooks = root.join(".git").join("hooks");
     let cross_dev = root.join("xtask").join("src");
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 5e45f3a..d2c68e2 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -65,35 +65,52 @@ fn is_toolchain(toolchain: &str) -> cross::Result<String> {
     }
 }
 
+macro_rules! get_engine {
+    ($args:ident, $msg_info:ident) => {{
+        get_container_engine($args.engine.as_deref(), &mut $msg_info)
+    }};
+}
+
+macro_rules! get_msg_info {
+    ($args:ident, $verbose:expr) => {{
+        MessageInfo::create($verbose, $args.quiet, $args.color.as_deref())
+    }};
+}
+
 pub fn main() -> cross::Result<()> {
     cross::install_panic_hook()?;
     let cli = Cli::parse();
     match cli.command {
         Commands::TargetInfo(args) => {
-            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)?;
+            let mut msg_info = get_msg_info!(args, args.verbose)?;
+            let engine = get_engine!(args, msg_info)?;
+            target_info::target_info(args, &engine, &mut msg_info)?;
         }
         Commands::BuildDockerImage(args) => {
-            let msg_info =
-                MessageInfo::create(args.verbose != 0, 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)?;
+            let mut msg_info = get_msg_info!(args, args.verbose != 0)?;
+            let engine = get_engine!(args, msg_info)?;
+            build_docker_image::build_docker_image(args, &engine, &mut msg_info)?;
         }
         Commands::InstallGitHooks(args) => {
-            install_git_hooks::install_git_hooks(args)?;
+            let mut msg_info = get_msg_info!(args, args.verbose)?;
+            install_git_hooks::install_git_hooks(&mut msg_info)?;
         }
         Commands::Check(args) => {
-            hooks::check(args, cli.toolchain.as_deref())?;
+            let mut msg_info = get_msg_info!(args, args.verbose)?;
+            hooks::check(args, cli.toolchain.as_deref(), &mut msg_info)?;
         }
         Commands::Test(args) => {
-            hooks::test(args, cli.toolchain.as_deref())?;
+            let mut msg_info = get_msg_info!(args, args.verbose)?;
+            hooks::test(cli.toolchain.as_deref(), &mut msg_info)?;
         }
         Commands::CiJob(args) => {
-            let metadata = cargo_metadata(Verbosity::Verbose.into())?;
-            ci::ci(args, metadata)?
+            let metadata = cargo_metadata(&mut Verbosity::Verbose.into())?;
+            ci::ci(args, metadata)?;
+        }
+        Commands::ConfigureCrosstool(args) => {
+            let mut msg_info = get_msg_info!(args, args.verbose)?;
+            crosstool::configure_crosstool(args, &mut msg_info)?;
         }
-        Commands::ConfigureCrosstool(args) => crosstool::configure_crosstool(args)?,
     }
 
     Ok(())
@@ -101,12 +118,12 @@ pub fn main() -> cross::Result<()> {
 
 fn get_container_engine(
     engine: Option<&str>,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<docker::Engine> {
     let engine = if let Some(ce) = engine {
         which::which(ce)?
     } else {
         docker::get_container_engine()?
     };
-    docker::Engine::from_path(engine, None, msg_info)
+    docker::Engine::from_path(engine, None, None, msg_info)
 }
diff --git a/xtask/src/target_info.rs b/xtask/src/target_info.rs
index fa16991..c54393b 100644
--- a/xtask/src/target_info.rs
+++ b/xtask/src/target_info.rs
@@ -41,7 +41,7 @@ fn image_info(
     target: &crate::ImageTarget,
     image: &str,
     tag: &str,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
     has_test: bool,
 ) -> cross::Result<()> {
     if !tag.starts_with("local") {
@@ -60,24 +60,21 @@ fn image_info(
     command.arg(image);
     command.args(&["bash", "-c", TARGET_INFO_SCRIPT]);
     command
-        .run(msg_info, !msg_info.verbose())
+        .run(msg_info, msg_info.is_verbose())
         .map_err(Into::into)
 }
 
 pub fn target_info(
     TargetInfo {
         mut targets,
-        verbose,
-        quiet,
-        color,
         registry,
         repository,
         tag,
         ..
     }: TargetInfo,
     engine: &docker::Engine,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
     let matrix = crate::util::get_matrix();
     let test_map: BTreeMap<crate::ImageTarget, bool> = matrix
         .iter()
diff --git a/xtask/src/util.rs b/xtask/src/util.rs
index 8eafeb3..0e6c9d0 100644
--- a/xtask/src/util.rs
+++ b/xtask/src/util.rs
@@ -90,7 +90,7 @@ pub fn format_repo(registry: &str, repository: &str) -> String {
 pub fn pull_image(
     engine: &docker::Engine,
     image: &str,
-    msg_info: MessageInfo,
+    msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
     let mut command = docker::subcommand(engine, "pull");
     command.arg(image);
@@ -159,7 +159,7 @@ impl std::fmt::Display for ImageTarget {
     }
 }
 
-pub fn has_nightly(msg_info: MessageInfo) -> cross::Result<bool> {
+pub fn has_nightly(msg_info: &mut MessageInfo) -> cross::Result<bool> {
     cross::cargo_command()
         .arg("+nightly")
         .run_and_get_output(msg_info)
@@ -167,10 +167,10 @@ pub fn has_nightly(msg_info: MessageInfo) -> cross::Result<bool> {
         .map_err(Into::into)
 }
 
-pub fn get_channel_prefer_nightly(
-    msg_info: MessageInfo,
-    toolchain: Option<&str>,
-) -> cross::Result<Option<&str>> {
+pub fn get_channel_prefer_nightly<'a>(
+    msg_info: &mut MessageInfo,
+    toolchain: Option<&'a str>,
+) -> cross::Result<Option<&'a str>> {
     Ok(match toolchain {
         Some(t) => Some(t),
         None => match has_nightly(msg_info)? {
@@ -188,12 +188,12 @@ pub fn cargo(channel: Option<&str>) -> Command {
     command
 }
 
-pub fn cargo_metadata(msg_info: MessageInfo) -> cross::Result<cross::CargoMetadata> {
+pub fn cargo_metadata(msg_info: &mut MessageInfo) -> cross::Result<cross::CargoMetadata> {
     cross::cargo_metadata_with_args(Some(Path::new(env!("CARGO_MANIFEST_DIR"))), None, msg_info)?
         .ok_or_else(|| eyre::eyre!("could not find cross workspace"))
 }
 
-pub fn project_dir(msg_info: MessageInfo) -> cross::Result<PathBuf> {
+pub fn project_dir(msg_info: &mut MessageInfo) -> cross::Result<PathBuf> {
     Ok(cargo_metadata(msg_info)?.workspace_root)
 }
 
@@ -216,7 +216,7 @@ pub fn gha_output(tag: &str, content: &str) {
     println!("::set-output name={tag}::{}", content)
 }
 
-pub fn read_dockerfiles(msg_info: MessageInfo) -> cross::Result<Vec<(PathBuf, String)>> {
+pub fn read_dockerfiles(msg_info: &mut MessageInfo) -> cross::Result<Vec<(PathBuf, String)>> {
     let root = project_dir(msg_info)?;
     let docker = root.join("docker");
     let mut dockerfiles = vec![];
@@ -244,7 +244,8 @@ mod tests {
     fn check_ubuntu_base() -> cross::Result<()> {
         // count all the entries of FROM for our images
         let mut counts = BTreeMap::new();
-        let dockerfiles = read_dockerfiles(Verbosity::Verbose.into())?;
+        let mut msg_info = Verbosity::Verbose.into();
+        let dockerfiles = read_dockerfiles(&mut msg_info)?;
         for (path, dockerfile) in dockerfiles {
             let lines: Vec<&str> = dockerfile.lines().collect();
             let index = lines


Test patch:
diff --git a/src/tests.rs b/src/tests.rs
index 98f30be..23067fb 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -13,15 +13,12 @@ static WORKSPACE: OnceCell<PathBuf> = OnceCell::new();
 /// Returns the cargo workspace for the manifest
 pub fn get_cargo_workspace() -> &'static Path {
     let manifest_dir = env!("CARGO_MANIFEST_DIR");
+    let mut msg_info = crate::shell::Verbosity::Verbose.into();
     WORKSPACE.get_or_init(|| {
-        crate::cargo_metadata_with_args(
-            Some(manifest_dir.as_ref()),
-            None,
-            crate::shell::Verbosity::Verbose.into(),
-        )
-        .unwrap()
-        .unwrap()
-        .workspace_root
+        crate::cargo_metadata_with_args(Some(manifest_dir.as_ref()), None, &mut msg_info)
+            .unwrap()
+            .unwrap()
+            .workspace_root
     })
 }
 
@@ -73,6 +70,7 @@ release: {version}
     fn compare(expected: VersionMatch, host: &str, targ: &str) {
         let host_meta = dbg!(make_meta(host));
         let target_meta = dbg!(make_rustc_version(targ));
+        let mut msg_info = crate::shell::MessageInfo::default();
         assert_eq!(
             expected,
             warn_host_version_mismatch(
@@ -80,7 +78,7 @@ release: {version}
                 "xxxx",
                 &target_meta.0,
                 &target_meta.1,
-                crate::shell::MessageInfo::default()
+                &mut msg_info,
             )
             .unwrap(),
             "\nhost = {}\ntarg = {}",
diff --git a/src/tests/toml.rs b/src/tests/toml.rs
index e8cd68f..5dac790 100644
--- a/src/tests/toml.rs
+++ b/src/tests/toml.rs
@@ -59,17 +59,12 @@ fn toml_check() -> Result<(), Box<dyn std::error::Error>> {
                 dir_entry.path().to_utf8()?,
                 text_line_no(&contents, fence.range().start),
             );
+            let mut msg_info = crate::shell::MessageInfo::default();
             assert!(if !cargo {
-                crate::cross_toml::CrossToml::parse_from_cross(
-                    &fence_content,
-                    crate::shell::MessageInfo::default(),
-                )?
+                crate::cross_toml::CrossToml::parse_from_cross(&fence_content, &mut msg_info)?
             } else {
-                crate::cross_toml::CrossToml::parse_from_cargo(
-                    &fence_content,
-                    crate::shell::MessageInfo::default(),
-                )?
-                .unwrap_or_default()
+                crate::cross_toml::CrossToml::parse_from_cargo(&fence_content, &mut msg_info)?
+                    .unwrap_or_default()
             }
             .1
             .is_empty());