Repository: cross-rs/cross
Pull request #1015 (2022-09-08 20:11:45)

Improve error diagnostics and other changes
- don't hide hostname querying in verbose
- output cross images as json
- associate `CROSS_DEBUG` with `MessageInfo`
<img width="490" alt="image" src="https://user-images.githubusercontent.com/1502855/195405356-1f673155-6be1-4f9f-9cc7-2cbf50604345.png">
- track caller when doing debug or error prints from `MessageInfo` and print the line it comes from




Emilgardis: this might be a bug, it doesn't work on my mac at least.

```
Error: 
   0: could not run container
   1: could not complete remote run
   2: when copying project
   3: copy of files failed

Location:
   src/docker/remote.rs:710
```

bors try --target x86_64-unknown-linux-gnu
Emilgardis: bors cancel

bad test
Emilgardis: bors try-
Emilgardis: bors try --target x86_64-unknown-linux-gnu

I can't test this rn, so ci is my tester :)
Emilgardis: bors try-

bors try --target x86_64-unknown-linux-gnu
Emilgardis: yep, it's wrong and replicates, opening an issue
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/8285199044?check_suite_focus=true)
Emilgardis: opened #1016 
Emilgardis: bors try --none
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/actions/runs/3233615165/jobs/5295810578)
Emilgardis: bors try --none
bors[bot]: ## try

Build succeeded:
  * [conclusion](https://github.com/cross-rs/cross/actions/runs/3236351907/jobs/5302176683)

Patch:
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..39a109d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,1 @@
+Dockerfile.* linguist-language=Dockerfile
diff --git a/src/bin/commands/containers.rs b/src/bin/commands/containers.rs
index 65f121c..0043280 100644
--- a/src/bin/commands/containers.rs
+++ b/src/bin/commands/containers.rs
@@ -396,7 +396,7 @@ pub fn create_persistent_volume(
     if let Some(channel) = channel {
         toolchain.channel = channel.channel.clone();
     };
-    let mount_finder = docker::MountFinder::create(engine)?;
+    let mount_finder = docker::MountFinder::create(engine, msg_info)?;
     let dirs = docker::ToolchainDirectories::assemble(&mount_finder, toolchain.clone())?;
     let container_id = dirs.unique_container_identifier(&toolchain.host().target)?;
     let volume_id = dirs.unique_toolchain_identifier()?;
@@ -464,7 +464,7 @@ pub fn remove_persistent_volume(
     if let Some(channel) = channel {
         toolchain.channel = channel.channel.clone();
     };
-    let mount_finder = docker::MountFinder::create(engine)?;
+    let mount_finder = docker::MountFinder::create(engine, msg_info)?;
     let dirs = docker::ToolchainDirectories::assemble(&mount_finder, toolchain)?;
     let volume_id = dirs.unique_toolchain_identifier()?;
     let volume = docker::DockerVolume::new(engine, &volume_id);
diff --git a/src/bin/commands/images.rs b/src/bin/commands/images.rs
index fa59f83..c511b50 100644
--- a/src/bin/commands/images.rs
+++ b/src/bin/commands/images.rs
@@ -1,5 +1,6 @@
 use std::collections::{BTreeMap, BTreeSet};
 
+use clap::builder::PossibleValue;
 use clap::{Args, Subcommand};
 use cross::docker::{self, CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX};
 use cross::shell::MessageInfo;
@@ -26,6 +27,9 @@ pub struct ListImages {
     /// Container engine (such as docker or podman).
     #[clap(long)]
     pub engine: Option<String>,
+    /// Output format
+    #[clap(long, default_value = "human")]
+    pub format: OutputFormat,
     /// Only list images for specific target(s). By default, list all targets.
     pub targets: Vec<String>,
 }
@@ -36,6 +40,25 @@ impl ListImages {
     }
 }
 
+#[derive(Clone, Debug)]
+pub enum OutputFormat {
+    Human,
+    Json,
+}
+
+impl clap::ValueEnum for OutputFormat {
+    fn value_variants<'a>() -> &'a [Self] {
+        &[Self::Human, Self::Json]
+    }
+
+    fn to_possible_value(&self) -> Option<PossibleValue> {
+        match self {
+            OutputFormat::Human => Some(PossibleValue::new("human")),
+            OutputFormat::Json => Some(PossibleValue::new("json")),
+        }
+    }
+}
+
 #[derive(Args, Debug)]
 pub struct RemoveImages {
     /// If not provided, remove all images.
@@ -118,7 +141,7 @@ impl Images {
     }
 }
 
-#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
+#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, serde::Serialize)]
 struct Image {
     repository: String,
     tag: String,
@@ -258,7 +281,9 @@ fn get_image_target(
 }
 
 pub fn list_images(
-    ListImages { targets, .. }: ListImages,
+    ListImages {
+        targets, format, ..
+    }: ListImages,
     engine: &docker::Engine,
     msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
@@ -281,45 +306,55 @@ pub fn list_images(
     let mut keys: Vec<&str> = map.keys().map(|k| k.as_ref()).collect();
     keys.sort_unstable();
 
-    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_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_str(col2);
-            let spaces = max_image_len.max(col2.len()) + 1 - col2.len();
-            for _ in 0..spaces {
-                row.push(fill);
+    match format {
+        OutputFormat::Json => {
+            msg_info.info(format_args!("{}", serde_json::to_string(&map)?))?;
+        }
+        OutputFormat::Human => {
+            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_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_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", ' ', msg_info)?;
+                print_string("-------", "------", '-', msg_info)?;
             }
-            row.push('|');
-            info.print(row)
-        };
-
-    if targets.len() != 1 {
-        print_string("Targets", "Images", ' ', msg_info)?;
-        print_string("-------", "------", '-', msg_info)?;
-    }
 
-    let print_single =
-        |_: &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, ' ', 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, msg_info)?;
-            } else {
-                print_table(target, image, msg_info)?;
+            let print_single = |_: &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, ' ', 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, msg_info)?;
+                    } else {
+                        print_table(target, image, msg_info)?;
+                    }
+                }
             }
         }
     }
diff --git a/src/bin/cross.rs b/src/bin/cross.rs
index 9210683..64536d2 100644
--- a/src/bin/cross.rs
+++ b/src/bin/cross.rs
@@ -18,13 +18,7 @@ pub fn main() -> cross::Result<()> {
     let target_list = rustc::target_list(&mut Verbosity::Quiet.into())?;
     let args = cli::parse(&target_list)?;
     let subcommand = args.subcommand;
-    let mut msg_info = shell::MessageInfo::create(
-        args.verbose
-            + cross::config::bool_from_envvar(&std::env::var("CROSS_DEBUG").unwrap_or_default())
-                as u8,
-        args.quiet,
-        args.color.as_deref(),
-    )?;
+    let mut msg_info = shell::MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?;
     let status = match cross::run(args, target_list, &mut msg_info)? {
         Some(status) => status,
         None => {
diff --git a/src/docker/remote.rs b/src/docker/remote.rs
index 8f4dede..2a05553 100644
--- a/src/docker/remote.rs
+++ b/src/docker/remote.rs
@@ -29,6 +29,7 @@ macro_rules! bail_container_exited {
     }};
 }
 
+#[track_caller]
 fn subcommand_or_exit(engine: &Engine, cmd: &str) -> Result<Command> {
     bail_container_exited!();
     Ok(engine.subcommand(cmd))
@@ -42,6 +43,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
     // NOTE: `reldir` should be a relative POSIX path to the root directory
     // on windows, this should be something like `mnt/c`. that is, all paths
     // inside the container should not have the mount prefix.
+    #[track_caller]
     fn create_dir(
         &self,
         reldir: &str,
@@ -57,6 +59,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
 
     // copy files for a docker volume, for remote host support
     // NOTE: `reldst` has the same caveats as `reldir` in `create_dir`.
+    #[track_caller]
     fn copy_files(
         &self,
         src: &Path,
@@ -72,6 +75,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
     }
 
     // copy files for a docker volume, for remote host support
+    #[track_caller]
     fn copy_files_nocache(
         &self,
         src: &Path,
@@ -92,6 +96,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
 
     // copy files for a docker volume, for remote host support
     // provides a list of files relative to src.
+    #[track_caller]
     fn copy_file_list(
         &self,
         src: &Path,
@@ -116,6 +121,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
 
     // removed files from a docker volume, for remote host support
     // provides a list of files relative to src.
+    #[track_caller]
     fn remove_file_list(
         &self,
         reldst: &str,
@@ -156,6 +162,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
             .run_and_get_status(msg_info, true)
     }
 
+    #[track_caller]
     fn container_path_exists(
         &self,
         relpath: &str,
@@ -173,6 +180,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
             .success())
     }
 
+    #[track_caller]
     pub fn copy_xargo(&self, mount_prefix: &str, msg_info: &mut MessageInfo) -> Result<()> {
         let dirs = &self.toolchain_dirs;
         let reldst = dirs.xargo_mount_path_relative()?;
@@ -194,6 +202,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
         Ok(())
     }
 
+    #[track_caller]
     pub fn copy_cargo(
         &self,
         mount_prefix: &str,
@@ -230,6 +239,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
     }
 
     // copy over files needed for all targets in the toolchain that should never change
+    #[track_caller]
     fn copy_rust_base(&self, mount_prefix: &str, msg_info: &mut MessageInfo) -> Result<()> {
         let dirs = &self.toolchain_dirs;
 
@@ -273,6 +283,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
         warn_symlinks(had_symlinks, msg_info)
     }
 
+    #[track_caller]
     fn copy_rust_manifest(&self, mount_prefix: &str, msg_info: &mut MessageInfo) -> Result<()> {
         let dirs = &self.toolchain_dirs;
 
@@ -298,6 +309,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
     }
 
     // copy over the toolchain for a specific triple
+    #[track_caller]
     fn copy_rust_triple(
         &self,
         target_triple: &TargetTriple,
@@ -335,6 +347,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
         Ok(())
     }
 
+    #[track_caller]
     pub fn copy_rust(
         &self,
         target_triple: Option<&TargetTriple>,
@@ -355,6 +368,7 @@ impl<'a, 'b, 'c> ContainerDataVolume<'a, 'b, 'c> {
         Ok(())
     }
 
+    #[track_caller]
     fn copy_mount(
         &self,
         src: &Path,
diff --git a/src/docker/shared.rs b/src/docker/shared.rs
index e4874ee..8f41f9b 100644
--- a/src/docker/shared.rs
+++ b/src/docker/shared.rs
@@ -200,8 +200,9 @@ impl DockerPaths {
         metadata: CargoMetadata,
         cwd: PathBuf,
         toolchain: QualifiedToolchain,
+        msg_info: &mut MessageInfo,
     ) -> Result<Self> {
-        let mount_finder = MountFinder::create(engine)?;
+        let mount_finder = MountFinder::create(engine, msg_info)?;
         let (directories, metadata) =
             Directories::assemble(&mount_finder, metadata, &cwd, toolchain)?;
         Ok(Self {
@@ -695,22 +696,26 @@ impl<'a, 'b> DockerVolume<'a, 'b> {
         Self { engine, name }
     }
 
+    #[track_caller]
     pub fn create(&self, msg_info: &mut MessageInfo) -> Result<ExitStatus> {
         self.engine
             .run_and_get_status(&["volume", "create", self.name], msg_info)
     }
 
+    #[track_caller]
     pub fn remove(&self, msg_info: &mut MessageInfo) -> Result<ExitStatus> {
         self.engine
             .run_and_get_status(&["volume", "rm", self.name], msg_info)
     }
 
+    #[track_caller]
     pub fn exists(&self, msg_info: &mut MessageInfo) -> Result<bool> {
         self.engine
             .run_and_get_output(&["volume", "inspect", self.name], msg_info)
             .map(|output| output.status.success())
     }
 
+    #[track_caller]
     pub fn existing(
         engine: &Engine,
         toolchain: &QualifiedToolchain,
@@ -833,6 +838,7 @@ impl Engine {
         command
     }
 
+    #[track_caller]
     pub(crate) fn run_and_get_status(
         &self,
         args: &[&str],
@@ -841,6 +847,7 @@ impl Engine {
         self.command().args(args).run_and_get_status(msg_info, true)
     }
 
+    #[track_caller]
     pub(crate) fn run_and_get_output(
         &self,
         args: &[&str],
@@ -1325,7 +1332,10 @@ pub(crate) fn get_image(config: &Config, target: &Target, uses_zig: bool) -> Res
     Ok(image)
 }
 
-fn docker_read_mount_paths(engine: &Engine) -> Result<Vec<MountDetail>> {
+fn docker_read_mount_paths(
+    engine: &Engine,
+    msg_info: &mut MessageInfo,
+) -> Result<Vec<MountDetail>> {
     let hostname = env::var("HOSTNAME").wrap_err("HOSTNAME environment variable not found")?;
 
     let mut docker: Command = {
@@ -1334,7 +1344,7 @@ fn docker_read_mount_paths(engine: &Engine) -> Result<Vec<MountDetail>> {
         command
     };
 
-    let output = docker.run_and_get_stdout(&mut Verbosity::Quiet.into())?;
+    let output = docker.run_and_get_stdout(msg_info)?;
     let info = serde_json::from_str(&output).wrap_err("failed to parse docker inspect output")?;
     dockerinfo_parse_mounts(&info)
 }
@@ -1410,9 +1420,9 @@ impl MountFinder {
         MountFinder { mounts }
     }
 
-    pub fn create(engine: &Engine) -> Result<MountFinder> {
+    pub fn create(engine: &Engine, msg_info: &mut MessageInfo) -> Result<MountFinder> {
         Ok(if engine.in_docker {
-            MountFinder::new(docker_read_mount_paths(engine)?)
+            MountFinder::new(docker_read_mount_paths(engine, msg_info)?)
         } else {
             MountFinder::default()
         })
@@ -1695,12 +1705,12 @@ mod tests {
                 return Ok(());
             }
 
-            let mount_finder = MountFinder::create(&engine)?;
+            let mount_finder = MountFinder::create(&engine, &mut msg_info)?;
             let metadata = cargo_metadata(true, &mut msg_info)?;
             let (directories, _) = get_directories(metadata, &mount_finder)?;
             let toolchain_dirs = directories.toolchain_directories();
             let package_dirs = directories.package_directories();
-            let mount_finder = MountFinder::new(docker_read_mount_paths(&engine)?);
+            let mount_finder = MountFinder::new(docker_read_mount_paths(&engine, &mut msg_info)?);
             let mount_path = |p| mount_finder.find_mount_path(p);
 
             paths_equal(toolchain_dirs.cargo(), &mount_path(home()?.join(".cargo")))?;
diff --git a/src/extensions.rs b/src/extensions.rs
index a9c2e8c..e8f7d23 100644
--- a/src/extensions.rs
+++ b/src/extensions.rs
@@ -10,16 +10,19 @@ pub const STRIPPED_BINS: &[&str] = &[crate::docker::DOCKER, crate::docker::PODMA
 pub trait CommandExt {
     fn fmt_message(&self, msg_info: &mut MessageInfo) -> String;
 
+    #[track_caller]
     fn print(&self, msg_info: &mut MessageInfo) -> Result<()> {
         let msg = self.fmt_message(msg_info);
         msg_info.print(&msg)
     }
 
+    #[track_caller]
     fn info(&self, msg_info: &mut MessageInfo) -> Result<()> {
         let msg = self.fmt_message(msg_info);
         msg_info.info(&msg)
     }
 
+    #[track_caller]
     fn debug(&self, msg_info: &mut MessageInfo) -> Result<()> {
         let msg = self.fmt_message(msg_info);
         msg_info.debug(&msg)
@@ -31,13 +34,17 @@ pub trait CommandExt {
         status: ExitStatus,
         output: Option<&Output>,
     ) -> Result<(), CommandError>;
+    #[track_caller]
     fn run(&mut self, msg_info: &mut MessageInfo, silence_stdout: bool) -> Result<()>;
+    #[track_caller]
     fn run_and_get_status(
         &mut self,
         msg_info: &mut MessageInfo,
         silence_stdout: bool,
     ) -> Result<ExitStatus>;
+    #[track_caller]
     fn run_and_get_stdout(&mut self, msg_info: &mut MessageInfo) -> Result<String>;
+    #[track_caller]
     fn run_and_get_output(&mut self, msg_info: &mut MessageInfo) -> Result<std::process::Output>;
     fn command_pretty(
         &self,
@@ -84,20 +91,33 @@ impl CommandExt for Command {
         format!("{}", C(self, msg_info, strip))
     }
 
+    #[track_caller]
     fn fmt_message(&self, mut msg_info: &mut MessageInfo) -> String {
+        use std::fmt::Write;
         let msg_info = &mut msg_info;
+        let mut string = String::new();
+        if let Some(caller) = msg_info.caller() {
+            write!(string, "[{}] ->\n+ ", caller).unwrap();
+        } else {
+            write!(string, "+ ").unwrap();
+        };
         if let Some(cwd) = self.get_current_dir() {
-            format!(
-                "+ {:?} {}",
+            write!(
+                string,
+                "{:?} {}",
                 cwd,
                 msg_info.as_verbose(|info| self.command_pretty(info, |_| false))
             )
+            .unwrap();
         } else {
-            format!(
-                "+ {}",
+            write!(
+                string,
+                "{}",
                 msg_info.as_verbose(|info| self.command_pretty(info, |_| false))
             )
+            .unwrap();
         }
+        string
     }
 
     fn status_result(
@@ -127,6 +147,7 @@ impl CommandExt for Command {
     }
 
     /// Runs the command to completion
+    #[track_caller]
     fn run_and_get_status(
         &mut self,
         msg_info: &mut MessageInfo,
@@ -146,6 +167,7 @@ impl CommandExt for Command {
     }
 
     /// Runs the command to completion and returns its stdout
+    #[track_caller]
     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))
@@ -158,6 +180,7 @@ impl CommandExt for Command {
     /// # Notes
     ///
     /// This command does not check the status.
+    #[track_caller]
     fn run_and_get_output(&mut self, msg_info: &mut MessageInfo) -> Result<std::process::Output> {
         self.debug(msg_info)?;
         self.output().map_err(|e| {
diff --git a/src/lib.rs b/src/lib.rs
index 363b013..99bb900 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -707,7 +707,13 @@ To override the toolchain mounted in the image, set `target.{}.image.toolchain =
                     engine.register_binfmt(&target, msg_info)?;
                 }
 
-                let paths = docker::DockerPaths::create(&engine, metadata, cwd, toolchain.clone())?;
+                let paths = docker::DockerPaths::create(
+                    &engine,
+                    metadata,
+                    cwd,
+                    toolchain.clone(),
+                    msg_info,
+                )?;
                 let options = docker::DockerOptions::new(
                     engine,
                     target.clone(),
diff --git a/src/shell.rs b/src/shell.rs
index b32eaca..0ec20ae 100644
--- a/src/shell.rs
+++ b/src/shell.rs
@@ -6,6 +6,7 @@ use std::fmt;
 use std::io::{self, Write};
 use std::str::FromStr;
 
+use crate::config::bool_from_envvar;
 use crate::errors::Result;
 use owo_colors::{self, OwoColorize};
 
@@ -39,8 +40,11 @@ macro_rules! message {
     (@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);
+        if let Some(caller) = $msg_info.caller() {
+            write!($stream, " [{}]", caller)?;
+        }
         match $message {
-            Some(message) => writeln!($stream, " {}", message)?,
+            Some(message) => writeln!($stream, " {}", message, )?,
             None => write!($stream, " ")?,
         }
 
@@ -139,15 +143,20 @@ pub struct MessageInfo {
     pub verbosity: Verbosity,
     pub stdout_needs_erase: bool,
     pub stderr_needs_erase: bool,
+    pub cross_debug: bool,
 }
 
 impl MessageInfo {
-    pub const fn new(color_choice: ColorChoice, verbosity: Verbosity) -> MessageInfo {
+    pub fn new(color_choice: ColorChoice, verbosity: Verbosity) -> MessageInfo {
         MessageInfo {
             color_choice,
             verbosity,
             stdout_needs_erase: false,
             stderr_needs_erase: false,
+            cross_debug: std::env::var("CROSS_DEBUG")
+                .as_deref()
+                .map(bool_from_envvar)
+                .unwrap_or_default(),
         }
     }
 
@@ -155,12 +164,17 @@ impl MessageInfo {
         let color_choice = get_color_choice(color)?;
         let verbosity = get_verbosity(color_choice, verbose, quiet)?;
 
-        Ok(MessageInfo {
-            color_choice,
-            verbosity,
-            stdout_needs_erase: false,
-            stderr_needs_erase: false,
-        })
+        Ok(Self::new(color_choice, verbosity))
+    }
+
+    #[track_caller]
+    pub fn caller(&mut self) -> Option<impl fmt::Display> {
+        if self.cross_debug {
+            let loc = std::panic::Location::caller();
+            Some(format!("{}:{}", loc.file(), loc.line()))
+        } else {
+            None
+        }
     }
 
     #[must_use]
@@ -211,6 +225,7 @@ impl MessageInfo {
     }
 
     /// prints a red 'error' message and terminates.
+    #[track_caller]
     pub fn fatal<T: fmt::Display>(&mut self, message: T, code: i32) -> ! {
         self.error(message)
             .expect("could not display fatal message");
@@ -218,12 +233,14 @@ impl MessageInfo {
     }
 
     /// prints a red 'error' message.
+    #[track_caller]
     pub fn error<T: fmt::Display>(&mut self, message: T) -> Result<()> {
         self.stderr_check_erase()?;
         status!(@stderr cross_prefix!("error"), Some(&message), red, self)
     }
 
     /// prints an amber 'warning' message.
+    #[track_caller]
     pub fn warn<T: fmt::Display>(&mut self, message: T) -> Result<()> {
         match self.verbosity {
             Verbosity::Quiet => Ok(()),
@@ -237,6 +254,7 @@ impl MessageInfo {
     }
 
     /// prints a cyan 'note' message.
+    #[track_caller]
     pub fn note<T: fmt::Display>(&mut self, message: T) -> Result<()> {
         match self.verbosity {
             Verbosity::Quiet => Ok(()),
@@ -255,6 +273,7 @@ impl MessageInfo {
     }
 
     /// prints a high-priority message to stdout.
+    #[track_caller]
     pub fn print<T: fmt::Display>(&mut self, message: T) -> Result<()> {
         self.stdout_check_erase()?;
         println!("{}", message);
@@ -262,6 +281,7 @@ impl MessageInfo {
     }
 
     /// prints a normal message to stdout.
+    #[track_caller]
     pub fn info<T: fmt::Display>(&mut self, message: T) -> Result<()> {
         match self.verbosity {
             Verbosity::Quiet => Ok(()),
@@ -273,6 +293,7 @@ impl MessageInfo {
     }
 
     /// prints a debugging message to stdout.
+    #[track_caller]
     pub fn debug<T: fmt::Display>(&mut self, message: T) -> Result<()> {
         match self.verbosity {
             Verbosity::Quiet | Verbosity::Normal => Ok(()),


Test patch:
diff --git a/ci/test-remote.sh b/ci/test-remote.sh
index 8b676f3..b6e506c 100755
--- a/ci/test-remote.sh
+++ b/ci/test-remote.sh
@@ -45,7 +45,9 @@ cross_test_cpp() {
 
     pushd "${td}"
     retry cargo fetch
-    "${CROSS}" run --target "${TARGET}"
+    "${CROSS}" run --target "${TARGET}" | grep "Hello, world!"
+    sed -i 's/Hello, world/Hello, test/g' hellopp.cc
+    "${CROSS}" run --target "${TARGET}" | grep "Hello, test!"
     popd
 
     rm -rf "${td}"