From cff6b05473f3959f214baf12799f9bd63f6d9040 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Tue, 14 Jun 2022 20:14:31 -0500 Subject: [PATCH] Initial commit with basic support for remote docker. This supports the volume-based structure, and uses some nice optimizations to ensure that only the desired toolchain and cargo items are copied over. It also uses drops to ensure scoped deletion of resources, to avoid complex logic ensuring their cleanup. It also supports persistent data volumes, through `cross-util`. In order to setup a persistent data volume, use: ```bash cross-util create-crate-volume --target arm-unknown-linux-gnueabihf ``` Make sure you provide your `DOCKER_HOST` or correct engine type to ensure these are being made on the remote host. Then, run your command as before: ```bash CROSS_REMOTE=true cross build --target arm-unknown-linux-gnueabihf ``` Finally, you can clean up the generated volume using: ```bash cross-util remove-crate-volume --target arm-unknown-linux-gnueabihf ``` A few other utilities are present in `cross-util`: - `list-volumes`: list all volumes created by cross. - `remove-volumes`: remove all volumes created by cross. - `prune-volumes`: prune all volumes unassociated with a container. - `list-containers`: list all active containers created by cross. - `remove-containers`: remove all active containers created by cross. The initial implementation was done by Marc Schreiber, https://github.com/schrieveslaach. A few more environment variables exist to fine-tune performance, as well as handle private dependencies. - `CROSS_REMOTE_COPY_REGISTRY`: copy the cargo registry - `CROSS_REMOTE_COPY_CACHE`: copy cache directories, including the target directory. Fixes #248. Fixes #273. Closes #449. --- CHANGELOG.md | 1 + Cargo.lock | 58 +++ Cargo.toml | 3 + src/bin/commands/containers.rs | 460 ++++++++++++++++++++++ src/bin/commands/mod.rs | 2 + src/bin/cross-util.rs | 16 +- src/docker/engine.rs | 15 +- src/docker/local.rs | 6 +- src/docker/mod.rs | 41 +- src/docker/remote.rs | 649 ++++++++++++++++++++++++++++++++ src/docker/shared.rs | 12 +- src/file.rs | 1 + src/lib.rs | 59 +-- src/rustc.rs | 4 + xtask/src/build_docker_image.rs | 8 +- xtask/src/main.rs | 20 +- xtask/src/target_info.rs | 18 +- 17 files changed, 1306 insertions(+), 67 deletions(-) create mode 100644 src/bin/commands/containers.rs create mode 100644 src/docker/remote.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a61dbdf1b..fd264e7a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - #795 - added images for additional toolchains maintained by cross-rs. - #792 - added `CROSS_CONTAINER_IN_CONTAINER` environment variable to replace `CROSS_DOCKER_IN_DOCKER`. +- #785 - added support for remote container engines through data volumes. also adds in utility to commands to create and remove persistent data volumes. - #782 - added `build-std` config option, which builds the rust standard library from source if enabled. - #775 - forward Cargo exit code to host - #772 - added `CROSS_CONTAINER_OPTS` environment variable to replace `DOCKER_OPTS`. diff --git a/Cargo.lock b/Cargo.lock index f15812f40..4446c8278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,8 +160,10 @@ dependencies = [ "serde", "serde_ignored", "serde_json", + "sha1_smol", "shell-escape", "shell-words", + "tempfile", "thiserror", "toml", "walkdir", @@ -191,6 +193,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "gimli" version = "0.26.1" @@ -243,6 +254,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "1.0.1" @@ -368,6 +388,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.5.5" @@ -385,6 +414,15 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -473,6 +511,12 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sharded-slab" version = "0.1.4" @@ -511,6 +555,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index fe254fb55..0f33837dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_ignored = "0.1.2" shell-words = "1.1.0" +walkdir = { version = "2", optional = true } +sha1_smol = "1.0.0" +tempfile = "3.3.0" [target.'cfg(not(windows))'.dependencies] nix = { version = "0.24", default-features = false, features = ["user"] } diff --git a/src/bin/commands/containers.rs b/src/bin/commands/containers.rs new file mode 100644 index 000000000..c446b5cd7 --- /dev/null +++ b/src/bin/commands/containers.rs @@ -0,0 +1,460 @@ +use std::path::Path; + +use atty::Stream; +use clap::{Args, Subcommand}; +use cross::{CommandExt, VersionMetaExt}; + +#[derive(Args, Debug)] +pub struct ListVolumes { + /// Provide verbose diagnostic output. + #[clap(short, long)] + pub verbose: bool, + /// Container engine (such as docker or podman). + #[clap(long)] + pub engine: Option, +} + +#[derive(Args, Debug)] +pub struct RemoveVolumes { + /// Provide verbose diagnostic output. + #[clap(short, long)] + pub verbose: bool, + /// Force removal of volumes. + #[clap(short, long)] + pub force: bool, + /// Remove volumes. Default is a dry run. + #[clap(short, long)] + pub execute: bool, + /// Container engine (such as docker or podman). + #[clap(long)] + pub engine: Option, +} + +#[derive(Args, Debug)] +pub struct PruneVolumes { + /// Provide verbose diagnostic output. + #[clap(short, long)] + pub verbose: bool, + /// Container engine (such as docker or podman). + #[clap(long)] + pub engine: Option, +} + +#[derive(Args, Debug)] +pub struct CreateCrateVolume { + /// Triple for the target platform. + #[clap(long)] + pub target: String, + /// If cross is running inside a container. + #[clap(short, long)] + pub docker_in_docker: bool, + /// If we should copy the cargo registry to the volume. + #[clap(short, long)] + pub copy_registry: bool, + /// Provide verbose diagnostic output. + #[clap(short, long)] + pub verbose: bool, + /// Container engine (such as docker or podman). + #[clap(long)] + pub engine: Option, +} + +#[derive(Args, Debug)] +pub struct RemoveCrateVolume { + /// Triple for the target platform. + #[clap(long)] + pub target: String, + /// If cross is running inside a container. + #[clap(short, long)] + pub docker_in_docker: bool, + /// Provide verbose diagnostic output. + #[clap(short, long)] + pub verbose: bool, + /// Container engine (such as docker or podman). + #[clap(long)] + pub engine: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Volumes { + /// List cross data volumes in local storage. + List(ListVolumes), + /// Remove cross data volumes in local storage. + Remove(RemoveVolumes), + /// Prune volumes not used by any container. + Prune(PruneVolumes), + /// Create a persistent data volume for the current crate. + CreateCrate(CreateCrateVolume), + /// Remove a persistent data volume for the current crate. + RemoveCrate(RemoveCrateVolume), +} + +impl Volumes { + pub fn run(self, engine: cross::docker::Engine, toolchain: Option<&str>) -> cross::Result<()> { + match self { + Volumes::List(args) => list_volumes(args, &engine), + Volumes::Remove(args) => remove_volumes(args, &engine), + Volumes::Prune(args) => prune_volumes(args, &engine), + Volumes::CreateCrate(args) => create_crate_volume(args, &engine, toolchain), + Volumes::RemoveCrate(args) => remove_crate_volume(args, &engine, toolchain), + } + } + + pub fn engine(&self) -> Option<&str> { + match self { + Volumes::List(l) => l.engine.as_deref(), + Volumes::Remove(l) => l.engine.as_deref(), + Volumes::Prune(l) => l.engine.as_deref(), + Volumes::CreateCrate(l) => l.engine.as_deref(), + Volumes::RemoveCrate(l) => l.engine.as_deref(), + } + } + + pub fn verbose(&self) -> bool { + match self { + Volumes::List(l) => l.verbose, + Volumes::Remove(l) => l.verbose, + Volumes::Prune(l) => l.verbose, + Volumes::CreateCrate(l) => l.verbose, + Volumes::RemoveCrate(l) => l.verbose, + } + } +} + +#[derive(Args, Debug)] +pub struct ListContainers { + /// Provide verbose diagnostic output. + #[clap(short, long)] + pub verbose: bool, + /// Container engine (such as docker or podman). + #[clap(long)] + pub engine: Option, +} + +#[derive(Args, Debug)] +pub struct RemoveContainers { + /// Provide verbose diagnostic output. + #[clap(short, long)] + pub verbose: bool, + /// Force removal of containers. + #[clap(short, long)] + pub force: bool, + /// Remove containers. Default is a dry run. + #[clap(short, long)] + pub execute: bool, + /// Container engine (such as docker or podman). + #[clap(long)] + pub engine: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Containers { + /// List cross containers in local storage. + List(ListContainers), + /// Stop and remove cross containers in local storage. + Remove(RemoveContainers), +} + +impl Containers { + pub fn run(self, engine: cross::docker::Engine) -> cross::Result<()> { + match self { + Containers::List(args) => list_containers(args, &engine), + Containers::Remove(args) => remove_containers(args, &engine), + } + } + + pub fn engine(&self) -> Option<&str> { + match self { + Containers::List(l) => l.engine.as_deref(), + Containers::Remove(l) => l.engine.as_deref(), + } + } + + pub fn verbose(&self) -> bool { + match self { + Containers::List(l) => l.verbose, + Containers::Remove(l) => l.verbose, + } + } +} + +fn get_cross_volumes(engine: &cross::docker::Engine, verbose: bool) -> cross::Result> { + let stdout = cross::docker::subcommand(engine, "volume") + .arg("list") + .arg("--format") + .arg("{{.Name}}") + .arg("--filter") + // handles simple regex: ^ for start of line. + .arg("name=^cross-") + .run_and_get_stdout(verbose)?; + + let mut volumes: Vec = stdout.lines().map(|s| s.to_string()).collect(); + volumes.sort(); + + Ok(volumes) +} + +pub fn list_volumes( + ListVolumes { verbose, .. }: ListVolumes, + engine: &cross::docker::Engine, +) -> cross::Result<()> { + get_cross_volumes(engine, verbose)? + .iter() + .for_each(|line| println!("{}", line)); + + Ok(()) +} + +pub fn remove_volumes( + RemoveVolumes { + verbose, + force, + execute, + .. + }: RemoveVolumes, + engine: &cross::docker::Engine, +) -> cross::Result<()> { + let volumes = get_cross_volumes(engine, verbose)?; + + let mut command = cross::docker::subcommand(engine, "volume"); + command.arg("rm"); + if force { + command.arg("--force"); + } + command.args(&volumes); + if execute { + command.run(verbose).map_err(Into::into) + } else { + println!("{:?}", command); + Ok(()) + } +} + +pub fn prune_volumes( + PruneVolumes { verbose, .. }: PruneVolumes, + engine: &cross::docker::Engine, +) -> cross::Result<()> { + cross::docker::subcommand(engine, "volume") + .args(&["prune", "--force"]) + .run_and_get_status(verbose)?; + + Ok(()) +} + +fn get_package_info( + engine: &cross::docker::Engine, + target: &str, + channel: Option<&str>, + docker_in_docker: bool, + verbose: bool, +) -> cross::Result<( + cross::Target, + cross::CargoMetadata, + cross::docker::Directories, +)> { + let target_list = cross::target_list(false)?; + let target = cross::Target::from(target, &target_list); + let metadata = cross::cargo_metadata_with_args(None, None, verbose)? + .ok_or(eyre::eyre!("unable to get project metadata"))?; + let cwd = std::env::current_dir()?; + let host_meta = cross::version_meta()?; + let host = host_meta.host(); + let sysroot = cross::get_sysroot(&host, &target, channel, verbose)?.1; + let dirs = cross::docker::Directories::create( + engine, + &metadata, + &cwd, + &sysroot, + docker_in_docker, + verbose, + )?; + + Ok((target, metadata, dirs)) +} + +pub fn create_crate_volume( + CreateCrateVolume { + target, + docker_in_docker, + copy_registry, + verbose, + .. + }: CreateCrateVolume, + engine: &cross::docker::Engine, + channel: Option<&str>, +) -> cross::Result<()> { + let (target, metadata, dirs) = + get_package_info(engine, &target, channel, docker_in_docker, verbose)?; + let container = cross::docker::unique_container_identifier(&target, &metadata, &dirs)?; + let volume = format!("{container}-keep"); + + if cross::docker::volume_exists(engine, &volume, verbose)? { + eyre::bail!("error: volume {volume} already exists."); + } + + cross::docker::subcommand(engine, "volume") + .args(&["create", &volume]) + .run_and_get_status(verbose)?; + + // stop the container if it's already running + let state = cross::docker::container_state(engine, &container, verbose)?; + if !state.is_stopped() { + eprintln!("warning: container {container} was running."); + cross::docker::container_stop(engine, &container, verbose)?; + } + if state.exists() { + eprintln!("warning: container {container} was exited."); + cross::docker::container_rm(engine, &container, verbose)?; + } + + // create a dummy running container to copy data over + let mount_prefix = Path::new("/cross"); + let mut docker = cross::docker::subcommand(engine, "run"); + docker.args(&["--name", &container]); + docker.args(&["-v", &format!("{}:{}", volume, mount_prefix.display())]); + docker.arg("-d"); + if atty::is(Stream::Stdin) && atty::is(Stream::Stdout) && atty::is(Stream::Stderr) { + docker.arg("-t"); + } + docker.arg("ubuntu:16.04"); + // ensure the process never exits until we stop it + docker.args(&["sh", "-c", "sleep infinity"]); + docker.run_and_get_status(verbose)?; + + cross::docker::copy_volume_container_xargo( + engine, + &container, + &dirs.xargo, + &target, + mount_prefix, + verbose, + )?; + cross::docker::copy_volume_container_cargo( + engine, + &container, + &dirs.cargo, + mount_prefix, + copy_registry, + verbose, + )?; + cross::docker::copy_volume_container_rust( + engine, + &container, + &dirs.sysroot, + &target, + mount_prefix, + verbose, + )?; + + cross::docker::container_stop(engine, &container, verbose)?; + cross::docker::container_rm(engine, &container, verbose)?; + + Ok(()) +} + +pub fn remove_crate_volume( + RemoveCrateVolume { + target, + docker_in_docker, + verbose, + .. + }: RemoveCrateVolume, + engine: &cross::docker::Engine, + channel: Option<&str>, +) -> cross::Result<()> { + let (target, metadata, dirs) = + get_package_info(engine, &target, channel, docker_in_docker, verbose)?; + let container = cross::docker::unique_container_identifier(&target, &metadata, &dirs)?; + let volume = format!("{container}-keep"); + + if !cross::docker::volume_exists(engine, &volume, verbose)? { + eyre::bail!("error: volume {volume} does not exist."); + } + + cross::docker::volume_rm(engine, &volume, verbose)?; + + Ok(()) +} + +fn get_cross_containers( + engine: &cross::docker::Engine, + verbose: bool, +) -> cross::Result> { + let stdout = cross::docker::subcommand(engine, "ps") + .arg("-a") + .arg("--format") + .arg("{{.Names}}: {{.State}}") + .arg("--filter") + // handles simple regex: ^ for start of line. + .arg("name=^cross-") + .run_and_get_stdout(verbose)?; + + let mut containers: Vec = stdout.lines().map(|s| s.to_string()).collect(); + containers.sort(); + + Ok(containers) +} + +pub fn list_containers( + ListContainers { verbose, .. }: ListContainers, + engine: &cross::docker::Engine, +) -> cross::Result<()> { + get_cross_containers(engine, verbose)? + .iter() + .for_each(|line| println!("{}", line)); + + Ok(()) +} + +pub fn remove_containers( + RemoveContainers { + verbose, + force, + execute, + .. + }: RemoveContainers, + engine: &cross::docker::Engine, +) -> cross::Result<()> { + let containers = get_cross_containers(engine, verbose)?; + let mut running = vec![]; + let mut stopped = vec![]; + for container in containers.iter() { + // cannot fail, formatted as {{.Names}}: {{.State}} + let (name, state) = container.split_once(':').unwrap(); + let name = name.trim(); + let state = cross::docker::ContainerState::new(state.trim())?; + if state.is_stopped() { + stopped.push(name); + } else { + running.push(name); + } + } + + let mut commands = vec![]; + if !running.is_empty() { + let mut stop = cross::docker::subcommand(engine, "stop"); + stop.args(&running); + commands.push(stop); + } + + if !(stopped.is_empty() && running.is_empty()) { + let mut rm = cross::docker::subcommand(engine, "rm"); + if force { + rm.arg("--force"); + } + rm.args(&running); + rm.args(&stopped); + commands.push(rm); + } + if execute { + for mut command in commands { + command.run(verbose)?; + } + } else { + for command in commands { + println!("{:?}", command); + } + } + + Ok(()) +} diff --git a/src/bin/commands/mod.rs b/src/bin/commands/mod.rs index f2c4a675a..aa80a62f4 100644 --- a/src/bin/commands/mod.rs +++ b/src/bin/commands/mod.rs @@ -1,3 +1,5 @@ +mod containers; mod images; +pub use self::containers::*; pub use self::images::*; diff --git a/src/bin/cross-util.rs b/src/bin/cross-util.rs index 2549d2e0c..72d9564ed 100644 --- a/src/bin/cross-util.rs +++ b/src/bin/cross-util.rs @@ -16,9 +16,15 @@ struct Cli { #[derive(Subcommand, Debug)] enum Commands { - /// List cross images in local storage. + /// Work with cross images in local storage. #[clap(subcommand)] Images(commands::Images), + /// Work with cross volumes in local storage. + #[clap(subcommand)] + Volumes(commands::Volumes), + /// Work with cross containers in local storage. + #[clap(subcommand)] + Containers(commands::Containers), } fn is_toolchain(toolchain: &str) -> cross::Result { @@ -49,6 +55,14 @@ pub fn main() -> cross::Result<()> { let engine = get_container_engine(args.engine(), args.verbose())?; args.run(engine)?; } + Commands::Volumes(args) => { + let engine = get_container_engine(args.engine(), args.verbose())?; + args.run(engine, cli.toolchain.as_deref())?; + } + Commands::Containers(args) => { + let engine = get_container_engine(args.engine(), args.verbose())?; + args.run(engine)?; + } } Ok(()) diff --git a/src/docker/engine.rs b/src/docker/engine.rs index f2230e64a..ae66241eb 100644 --- a/src/docker/engine.rs +++ b/src/docker/engine.rs @@ -2,6 +2,7 @@ use std::env; use std::path::{Path, PathBuf}; use std::process::Command; +use crate::config::bool_from_envvar; use crate::errors::*; use crate::extensions::CommandExt; @@ -20,6 +21,7 @@ pub enum EngineType { pub struct Engine { pub kind: EngineType, pub path: PathBuf, + pub is_remote: bool, } impl Engine { @@ -32,7 +34,18 @@ impl Engine { pub fn from_path(path: PathBuf, verbose: bool) -> Result { let kind = get_engine_type(&path, verbose)?; - Ok(Engine { path, kind }) + let is_remote = env::var("CROSS_REMOTE") + .map(|s| bool_from_envvar(&s)) + .unwrap_or_default(); + Ok(Engine { + path, + kind, + is_remote, + }) + } + + pub fn needs_remote(&self) -> bool { + self.is_remote && self.kind == EngineType::Podman } } diff --git a/src/docker/local.rs b/src/docker/local.rs index 6ea91b93d..bbed0c030 100644 --- a/src/docker/local.rs +++ b/src/docker/local.rs @@ -11,6 +11,7 @@ use atty::Stream; #[allow(clippy::too_many_arguments)] // TODO: refactor pub(crate) fn run( + engine: &Engine, target: &Target, args: &[String], metadata: &CargoMetadata, @@ -21,13 +22,12 @@ pub(crate) fn run( docker_in_docker: bool, cwd: &Path, ) -> Result { - let engine = Engine::new(verbose)?; - let dirs = Directories::create(&engine, metadata, cwd, sysroot, docker_in_docker, verbose)?; + let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker, verbose)?; let mut cmd = cargo_cmd(uses_xargo); cmd.args(args); - let mut docker = subcommand(&engine, "run"); + let mut docker = subcommand(engine, "run"); docker.args(&["--userns", "host"]); docker_envvars(&mut docker, config, target)?; diff --git a/src/docker/mod.rs b/src/docker/mod.rs index ea075e7a0..04ba81a33 100644 --- a/src/docker/mod.rs +++ b/src/docker/mod.rs @@ -1,8 +1,10 @@ mod engine; mod local; +pub mod remote; mod shared; pub use self::engine::*; +pub use self::remote::*; pub use self::shared::*; use std::path::Path; @@ -14,6 +16,7 @@ use crate::{Config, Target}; #[allow(clippy::too_many_arguments)] // TODO: refactor pub fn run( + engine: &Engine, target: &Target, args: &[String], metadata: &CargoMetadata, @@ -24,15 +27,31 @@ pub fn run( docker_in_docker: bool, cwd: &Path, ) -> Result { - local::run( - target, - args, - metadata, - config, - uses_xargo, - sysroot, - verbose, - docker_in_docker, - cwd, - ) + if engine.is_remote { + remote_run( + engine, + target, + args, + metadata, + config, + uses_xargo, + sysroot, + verbose, + docker_in_docker, + cwd, + ) + } else { + local::run( + engine, + target, + args, + metadata, + config, + uses_xargo, + sysroot, + verbose, + docker_in_docker, + cwd, + ) + } } diff --git a/src/docker/remote.rs b/src/docker/remote.rs new file mode 100644 index 000000000..3a3718ed4 --- /dev/null +++ b/src/docker/remote.rs @@ -0,0 +1,649 @@ +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus}; +use std::{env, fs}; + +use super::engine::Engine; +use super::shared::*; +use crate::cargo::CargoMetadata; +use crate::config::{bool_from_envvar, Config}; +use crate::errors::Result; +use crate::extensions::CommandExt; +use crate::file; +use crate::rustc; +use crate::Target; +use atty::Stream; + +struct DeleteVolume<'a>(&'a Engine, &'a VolumeId, bool); + +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(); + } + } +} + +struct DeleteContainer<'a>(&'a Engine, &'a str, bool); + +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(); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ContainerState { + Created, + Running, + Paused, + Restarting, + Dead, + Exited, + DoesNotExist, +} + +impl ContainerState { + pub fn new(state: &str) -> Result { + match state { + "created" => Ok(ContainerState::Created), + "running" => Ok(ContainerState::Running), + "paused" => Ok(ContainerState::Paused), + "restarting" => Ok(ContainerState::Restarting), + "dead" => Ok(ContainerState::Dead), + "exited" => Ok(ContainerState::Exited), + "" => Ok(ContainerState::DoesNotExist), + _ => eyre::bail!("unknown container state: got {state}"), + } + } + + pub fn is_stopped(&self) -> bool { + matches!(self, Self::Exited | Self::DoesNotExist) + } + + pub fn exists(&self) -> bool { + !matches!(self, Self::DoesNotExist) + } +} + +#[derive(Debug)] +enum VolumeId { + Keep(String), + Discard(String), +} + +impl VolumeId { + fn create(engine: &Engine, container: &str, verbose: bool) -> Result { + let keep_id = format!("{container}-keep"); + if volume_exists(engine, &keep_id, verbose)? { + Ok(Self::Keep(keep_id)) + } else { + Ok(Self::Discard(container.to_string())) + } + } +} + +impl AsRef for VolumeId { + fn as_ref(&self) -> &str { + match self { + Self::Keep(s) => s, + Self::Discard(s) => s, + } + } +} + +fn create_volume_dir( + engine: &Engine, + container: &str, + dir: &Path, + verbose: bool, +) -> Result { + // make our parent directory if needed + subcommand(engine, "exec") + .arg(container) + .args(&["sh", "-c", &format!("mkdir -p '{}'", dir.display())]) + .run_and_get_status(verbose) + .map_err(Into::into) +} + +// copy files for a docker volume, for remote host support +fn copy_volume_files( + engine: &Engine, + container: &str, + src: &Path, + dst: &Path, + verbose: bool, +) -> Result { + subcommand(engine, "cp") + .arg("-a") + .arg(&src.display().to_string()) + .arg(format!("{container}:{}", dst.display())) + .run_and_get_status(verbose) + .map_err(Into::into) +} + +fn is_cachedir_tag(path: &Path) -> Result { + let mut buffer = [b'0'; 43]; + let mut file = fs::OpenOptions::new().read(true).open(path)?; + file.read_exact(&mut buffer)?; + + Ok(&buffer == b"Signature: 8a477f597d28d172789f06886806bc55") +} + +fn is_cachedir(entry: &fs::DirEntry) -> bool { + // avoid any cached directories when copying + // see https://bford.info/cachedir/ + if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { + let path = entry.path().join("CACHEDIR.TAG"); + path.exists() && is_cachedir_tag(&path).unwrap_or(false) + } else { + false + } +} + +// copy files for a docker volume, for remote host support +fn copy_volume_files_nocache( + engine: &Engine, + container: &str, + src: &Path, + dst: &Path, + verbose: bool, +) -> Result { + // avoid any cached directories when copying + // see https://bford.info/cachedir/ + let tempdir = tempfile::tempdir()?; + let temppath = tempdir.path(); + copy_dir(src, temppath, 0, |e, _| !is_cachedir(e))?; + copy_volume_files(engine, container, temppath, dst, verbose) +} + +pub fn copy_volume_container_xargo( + engine: &Engine, + container: &str, + xargo_dir: &Path, + target: &Target, + mount_prefix: &Path, + verbose: bool, +) -> Result<()> { + // only need to copy the rustlib files for our current target. + let triple = target.triple(); + let relpath = Path::new("lib").join("rustlib").join(&triple); + let src = xargo_dir.join(&relpath); + let dst = mount_prefix.join("xargo").join(&relpath); + if Path::new(&src).exists() { + create_volume_dir(engine, container, dst.parent().unwrap(), verbose)?; + copy_volume_files(engine, container, &src, &dst, verbose)?; + } + + Ok(()) +} + +pub fn copy_volume_container_cargo( + engine: &Engine, + container: &str, + cargo_dir: &Path, + mount_prefix: &Path, + copy_registry: bool, + verbose: bool, +) -> Result<()> { + let dst = mount_prefix.join("cargo"); + let copy_registry = env::var("CROSS_REMOTE_COPY_REGISTRY") + .map(|s| bool_from_envvar(&s)) + .unwrap_or(copy_registry); + + if copy_registry { + copy_volume_files(engine, container, cargo_dir, &dst, verbose)?; + } else { + // can copy a limit subset of files: the rest is present. + create_volume_dir(engine, container, &dst, verbose)?; + for entry in fs::read_dir(cargo_dir)? { + let file = entry?; + let basename = file.file_name().to_string_lossy().into_owned(); + if !basename.starts_with('.') && !matches!(basename.as_ref(), "git" | "registry") { + copy_volume_files(engine, container, &file.path(), &dst, verbose)?; + } + } + } + + Ok(()) +} + +// recursively copy a directory into another +fn copy_dir(src: &Path, dst: &Path, depth: u32, skip: Skip) -> Result<()> +where + Skip: Copy + Fn(&fs::DirEntry, u32) -> bool, +{ + for entry in fs::read_dir(src)? { + let file = entry?; + if skip(&file, depth) { + continue; + } + + let src_path = file.path(); + let dst_path = dst.join(file.file_name()); + if file.file_type()?.is_file() { + fs::copy(&src_path, &dst_path)?; + } else { + fs::create_dir(&dst_path).ok(); + copy_dir(&src_path, &dst_path, depth + 1, skip)?; + } + } + + Ok(()) +} + +pub fn copy_volume_container_rust( + engine: &Engine, + container: &str, + sysroot: &Path, + target: &Target, + mount_prefix: &Path, + verbose: bool, +) -> Result<()> { + // the rust toolchain is quite large, but most of it isn't needed + // we need the bin, libexec, and etc directories, and part of the lib directory. + let dst = mount_prefix.join("rust"); + create_volume_dir(engine, container, &dst, verbose)?; + for basename in ["bin", "libexec", "etc"] { + let file = sysroot.join(basename); + copy_volume_files(engine, container, &file, &dst, verbose)?; + } + + // the lib directories are rather large, so we want only a subset. + // now, we use a temp directory for everything else in the libdir + // we can pretty safely assume we don't have symlinks here. + let rustlib = Path::new("lib").join("rustlib"); + let src_rustlib = sysroot.join(&rustlib); + let dst_rustlib = dst.join(&rustlib); + + let tempdir = tempfile::tempdir()?; + let temppath = tempdir.path(); + copy_dir(&sysroot.join("lib"), temppath, 0, |e, d| { + d == 0 && e.file_name() == "rustlib" + })?; + fs::create_dir(&temppath.join("rustlib")).ok(); + copy_dir( + &src_rustlib, + &temppath.join("rustlib"), + 0, + |entry, depth| { + if depth != 0 { + return false; + } + let file_type = match entry.file_type() { + Ok(file_type) => file_type, + Err(_) => return true, + }; + let file_name = entry.file_name(); + !(file_type.is_file() || file_name == "src" || file_name == "etc") + }, + )?; + copy_volume_files(engine, container, temppath, &dst.join("lib"), verbose)?; + // must make the `dst.join("lib")` **after** here, or we copy temp into lib. + create_volume_dir(engine, container, &dst_rustlib, verbose)?; + + // we first copy over the toolchain file, then everything besides it. + // since we don't want to call docker 100x, we copy the intermediate + // files to a temp directory so they're cleaned up afterwards. + let toolchain_path = src_rustlib.join(&target.triple()); + if toolchain_path.exists() { + copy_volume_files(engine, container, &toolchain_path, &dst_rustlib, verbose)?; + } + + // now we need to copy over the host toolchain too, since it has + // some requirements to find std libraries, etc. + let rustc = sysroot.join("bin").join("rustc"); + let libdir = Command::new(rustc) + .args(&["--print", "target-libdir"]) + .run_and_get_stdout(verbose)?; + let host_toolchain_path = Path::new(libdir.trim()).parent().unwrap(); + copy_volume_files( + engine, + container, + host_toolchain_path, + &dst_rustlib, + verbose, + )?; + + Ok(()) +} + +pub fn volume_create(engine: &Engine, volume: &str, verbose: bool) -> Result { + subcommand(engine, "volume") + .args(&["create", volume]) + .run_and_get_status(verbose) + .map_err(Into::into) +} + +pub fn volume_rm(engine: &Engine, volume: &str, verbose: bool) -> Result { + subcommand(engine, "volume") + .args(&["rm", volume]) + .run_and_get_status(verbose) + .map_err(Into::into) +} + +pub fn volume_exists(engine: &Engine, volume: &str, verbose: bool) -> Result { + subcommand(engine, "volume") + .args(&["inspect", volume]) + .run_and_get_output(verbose) + .map(|output| output.status.success()) + .map_err(Into::into) +} + +pub fn container_stop(engine: &Engine, container: &str, verbose: bool) -> Result { + subcommand(engine, "stop") + .arg(container) + .run_and_get_status(verbose) + .map_err(Into::into) +} + +pub fn container_rm(engine: &Engine, container: &str, verbose: bool) -> Result { + subcommand(engine, "rm") + .arg(container) + .run_and_get_status(verbose) + .map_err(Into::into) +} + +pub fn container_state(engine: &Engine, container: &str, verbose: bool) -> Result { + let stdout = subcommand(engine, "ps") + .arg("-a") + .args(&["--filter", &format!("name={container}")]) + .args(&["--format", "{{.State}}"]) + .run_and_get_stdout(verbose)?; + ContainerState::new(stdout.trim()) +} + +fn path_hash(path: &Path) -> String { + sha1_smol::Sha1::from(path.display().to_string().as_bytes()) + .digest() + .to_string() + .get(..5) + .expect("sha1 is expected to be at least 5 characters long") + .to_string() +} + +pub fn unique_container_identifier( + target: &Target, + metadata: &CargoMetadata, + dirs: &Directories, +) -> Result { + let host_version_meta = rustc::version_meta()?; + let commit_hash = host_version_meta + .commit_hash + .unwrap_or(host_version_meta.short_version_string); + + let workspace_root = &metadata.workspace_root; + let package = metadata + .packages + .iter() + .find(|p| p.manifest_path.parent().unwrap() == workspace_root) + .unwrap_or_else(|| metadata.packages.get(0).unwrap()); + + let name = &package.name; + let triple = target.triple(); + let project_hash = path_hash(&package.manifest_path); + let toolchain_hash = path_hash(&dirs.sysroot); + Ok(format!( + "cross-{name}-{triple}-{project_hash}-{toolchain_hash}-{commit_hash}" + )) +} + +fn mount_path(val: &Path, verbose: bool) -> Result { + let host_path = file::canonicalize(val)?; + canonicalize_mount_path(&host_path, verbose) +} + +// keep the prefix since we glob import it into docker +#[allow(clippy::too_many_arguments)] // TODO: refactor +pub(crate) fn remote_run( + engine: &Engine, + target: &Target, + args: &[String], + metadata: &CargoMetadata, + config: &Config, + uses_xargo: bool, + sysroot: &Path, + verbose: bool, + docker_in_docker: bool, + cwd: &Path, +) -> Result { + let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker, verbose)?; + + let mut cmd = cargo_cmd(uses_xargo); + cmd.args(args); + + let mount_prefix = "/cross"; + + // the logic is broken into the following steps + // 1. get our unique identifiers and cleanup from a previous run. + // 2. create a data volume to store everything + // 3. start our container with the data volume and all envvars + // 4. copy all mounted volumes over + // 5. create symlinks for all mounted data + // 6. execute our cargo command inside the container + // 7. copy data from target dir back to host + // 8. stop container and delete data volume + // + // we use structs that wrap the resources to ensure they're dropped + // in the correct order even on error, to ensure safe cleanup + + // 1. get our unique identifiers and cleanup from a previous run. + // this can happen if we didn't gracefully exit before + let container = unique_container_identifier(target, metadata, &dirs)?; + let volume = VolumeId::create(engine, &container, verbose)?; + let state = container_state(engine, &container, verbose)?; + if !state.is_stopped() { + eprintln!("warning: container {container} was running."); + container_stop(engine, &container, verbose)?; + } + if state.exists() { + eprintln!("warning: container {container} was exited."); + container_rm(engine, &container, verbose)?; + } + if let VolumeId::Discard(ref id) = volume { + if volume_exists(engine, id, verbose)? { + eprintln!("warning: temporary volume {container} existed."); + volume_rm(engine, id, verbose)?; + } + } + + // 2. create our volume to copy all our data over to + if let VolumeId::Discard(ref id) = volume { + volume_create(engine, id, verbose)?; + } + let _volume_deletter = DeleteVolume(engine, &volume, verbose); + + // 3. create our start container command here + let mut docker = subcommand(engine, "run"); + docker.args(&["--userns", "host"]); + docker.args(&["--name", &container]); + docker.args(&["-v", &format!("{}:{mount_prefix}", volume.as_ref())]); + docker_envvars(&mut docker, config, target)?; + + let mut volumes = vec![]; + let mount_volumes = docker_mount( + &mut docker, + metadata, + config, + target, + cwd, + verbose, + |_, val, verbose| mount_path(val, verbose), + |(src, dst)| volumes.push((src, dst)), + )?; + + docker_seccomp(&mut docker, engine.kind, target, verbose)?; + + // Prevent `bin` from being mounted inside the Docker container. + docker.args(&["-v", &format!("{mount_prefix}/cargo/bin")]); + + // 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. + if let Some(ref nix_store) = dirs.nix_store { + volumes.push((nix_store.display().to_string(), nix_store.to_path_buf())) + } + + docker.arg("-d"); + if atty::is(Stream::Stdin) && atty::is(Stream::Stdout) && atty::is(Stream::Stderr) { + docker.arg("-t"); + } + + docker + .arg(&container_name(config, target)?) + // ensure the process never exits until we stop it + .args(&["sh", "-c", "sleep infinity"]) + .run_and_get_status(verbose)?; + let _container_deletter = DeleteContainer(engine, &container, verbose); + + // 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| { + if copy_cache { + copy_volume_files(engine, &container, src, dst, verbose) + } else { + copy_volume_files_nocache(engine, &container, src, dst, verbose) + } + }; + let mount_prefix_path = mount_prefix.as_ref(); + if let VolumeId::Discard(_) = volume { + copy_volume_container_xargo( + engine, + &container, + &dirs.xargo, + target, + mount_prefix_path, + verbose, + )?; + copy_volume_container_cargo( + engine, + &container, + &dirs.cargo, + mount_prefix_path, + false, + verbose, + )?; + copy_volume_container_rust( + engine, + &container, + &dirs.sysroot, + target, + mount_prefix_path, + verbose, + )?; + } + let mount_root = if mount_volumes { + // cannot panic: absolute unix path, must have root + let rel_mount_root = dirs.mount_root.strip_prefix("/").unwrap(); + let mount_root = mount_prefix_path.join(rel_mount_root); + if rel_mount_root != PathBuf::new() { + create_volume_dir(engine, &container, mount_root.parent().unwrap(), verbose)?; + } + mount_root + } else { + mount_prefix_path.join("project") + }; + copy(&dirs.host_root, &mount_root)?; + + let mut copied = vec![ + (&dirs.xargo, mount_prefix_path.join("xargo")), + (&dirs.cargo, mount_prefix_path.join("cargo")), + (&dirs.sysroot, mount_prefix_path.join("rust")), + (&dirs.host_root, mount_root.clone()), + ]; + let mut to_symlink = vec![]; + let target_dir = file::canonicalize(&dirs.target)?; + let target_dir = if let Ok(relpath) = target_dir.strip_prefix(&dirs.host_root) { + // target dir is in the project, just symlink it in + let target_dir = mount_root.join(relpath); + to_symlink.push((target_dir.clone(), "/target".to_string())); + target_dir + } else { + // outside project, need to copy the target data over + // 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)?; + } else { + create_volume_dir(engine, &container, &target_dir, verbose)?; + } + + copied.push((&dirs.target, target_dir.clone())); + target_dir + }; + for (src, dst) in volumes.iter() { + let src: &Path = src.as_ref(); + if let Some((psrc, pdst)) = copied.iter().find(|(p, _)| src.starts_with(p)) { + // path has already been copied over + let relpath = src.strip_prefix(psrc).unwrap(); + to_symlink.push((pdst.join(relpath), dst.display().to_string())); + } else { + let rel_dst = dst.strip_prefix("/").unwrap(); + let mount_dst = mount_prefix_path.join(rel_dst); + if rel_dst != PathBuf::new() { + create_volume_dir(engine, &container, mount_dst.parent().unwrap(), verbose)?; + } + copy(src, &mount_dst)?; + } + } + + // 5. create symlinks for copied data + let mut symlink = vec!["set -e pipefail".to_string()]; + if verbose { + symlink.push("set -x".to_string()); + } + symlink.push(format!( + "chown -R {uid}:{gid} {mount_prefix}/*", + uid = user_id(), + gid = group_id(), + )); + // need a simple script to add symlinks, but not override existing files. + symlink.push(format!( + "prefix=\"{mount_prefix}\" + +symlink_recurse() {{ + for f in \"${{1}}\"/*; do + dst=${{f#\"$prefix\"}} + if [ -f \"${{dst}}\" ]; then + echo \"invalid: got unexpected file at ${{dst}}\" 1>&2 + exit 1 + elif [ -d \"${{dst}}\" ]; then + symlink_recurse \"${{f}}\" + else + ln -s \"${{f}}\" \"${{dst}}\" + fi + done +}} + +symlink_recurse \"${{prefix}}\" +" + )); + for (src, dst) in to_symlink { + symlink.push(format!("ln -s \"{}\" \"{}\"", src.display(), dst)); + } + subcommand(engine, "exec") + .arg(&container) + .args(&["sh", "-c", &symlink.join("\n")]) + .run_and_get_status(verbose) + .map_err::(Into::into)?; + + // 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.arg(&container); + docker.args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)]); + let status = docker.run_and_get_status(verbose).map_err(Into::into); + + // 7. copy data from our target dir back to host + subcommand(engine, "cp") + .arg("-a") + .arg(&format!("{container}:{}", target_dir.display())) + .arg(&dirs.target.parent().unwrap()) + .run_and_get_status(verbose) + .map_err::(Into::into)?; + + status +} diff --git a/src/docker/shared.rs b/src/docker/shared.rs index 861283049..a2e6d71c1 100644 --- a/src/docker/shared.rs +++ b/src/docker/shared.rs @@ -111,7 +111,12 @@ impl Directories { } pub fn command(engine: &Engine) -> Command { - Command::new(&engine.path) + let mut command = Command::new(&engine.path); + if engine.needs_remote() { + // if we're using podman and not podman-remote, need `--remote`. + command.arg("--remote"); + } + command } pub fn subcommand(engine: &Engine, subcommand: &str) -> Command { @@ -121,7 +126,7 @@ pub fn subcommand(engine: &Engine, subcommand: &str) -> Command { } /// Register binfmt interpreters -pub(crate) fn register(target: &Target, verbose: bool) -> Result<()> { +pub(crate) fn register(engine: &Engine, target: &Target, verbose: bool) -> 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 && \ @@ -131,8 +136,7 @@ pub(crate) fn register(target: &Target, verbose: bool) -> Result<()> { binfmt-support qemu-user-static" }; - let engine = Engine::new(verbose)?; - subcommand(&engine, "run") + subcommand(engine, "run") .args(&["--userns", "host"]) .arg("--privileged") .arg("--rm") diff --git a/src/file.rs b/src/file.rs index 55468df81..ea49a3434 100644 --- a/src/file.rs +++ b/src/file.rs @@ -22,6 +22,7 @@ fn read_(path: &Path) -> Result { pub fn canonicalize(path: impl AsRef) -> Result { _canonicalize(path.as_ref()) + .wrap_err_with(|| format!("when canonicalizing path `{:?}`", path.as_ref())) } fn _canonicalize(path: &Path) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 536aac821..1c89cd752 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,13 +39,13 @@ use config::Config; use rustc_version::Channel; use serde::Deserialize; -pub use self::cargo::{cargo_metadata_with_args, CargoMetadata, Subcommand}; use self::cross_toml::CrossToml; use self::errors::Context; -use self::rustc::{TargetList, VersionMetaExt}; +pub use self::cargo::{cargo_metadata_with_args, CargoMetadata, Subcommand}; pub use self::errors::{install_panic_hook, Result}; -pub use self::extensions::{CommandExt, OutputExt}; +pub use self::extensions::*; +pub use self::rustc::{target_list, version_meta, TargetList, VersionMetaExt}; #[allow(non_camel_case_types)] #[derive(Debug, Clone, PartialEq, Eq)] @@ -244,7 +244,7 @@ impl std::fmt::Display for Target { } impl Target { - fn from(triple: &str, target_list: &TargetList) -> Target { + pub fn from(triple: &str, target_list: &TargetList) -> Target { if target_list.contains(triple) { Target::new_built_in(triple) } else { @@ -275,6 +275,32 @@ impl From<&str> for Target { } } +pub fn get_sysroot( + host: &Host, + target: &Target, + channel: Option<&str>, + verbose: bool, +) -> Result<(String, PathBuf)> { + let mut sysroot = rustc::sysroot(host, target, verbose)?; + let default_toolchain = sysroot + .file_name() + .and_then(|file_name| file_name.to_str()) + .ok_or_else(|| eyre::eyre!("couldn't get toolchain name"))?; + let toolchain = if let Some(channel) = channel { + [channel] + .iter() + .cloned() + .chain(default_toolchain.splitn(2, '-').skip(1)) + .collect::>() + .join("-") + } else { + default_toolchain.to_string() + }; + sysroot.set_file_name(&toolchain); + + Ok((toolchain, sysroot)) +} + pub fn run() -> Result { let target_list = rustc::target_list(false)?; let args = cli::parse(&target_list)?; @@ -291,8 +317,7 @@ pub fn run() -> Result { .iter() .any(|a| a == "--verbose" || a == "-v" || a == "-vv"); - let host_version_meta = - rustc_version::version_meta().wrap_err("couldn't fetch the `rustc` version")?; + let host_version_meta = rustc::version_meta()?; let cwd = std::env::current_dir()?; if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), verbose)? { let host = host_version_meta.host(); @@ -313,22 +338,8 @@ pub fn run() -> Result { }; if image_exists && host.is_supported(Some(&target)) { - let mut sysroot = rustc::sysroot(&host, &target, verbose)?; - let default_toolchain = sysroot - .file_name() - .and_then(|file_name| file_name.to_str()) - .ok_or_else(|| eyre::eyre!("couldn't get toolchain name"))?; - let toolchain = if let Some(channel) = args.channel { - [channel] - .iter() - .map(|c| c.as_str()) - .chain(default_toolchain.splitn(2, '-').skip(1)) - .collect::>() - .join("-") - } else { - default_toolchain.to_string() - }; - sysroot.set_file_name(&toolchain); + let (toolchain, sysroot) = + get_sysroot(&host, &target, args.channel.as_deref(), verbose)?; let mut is_nightly = toolchain.contains("nightly"); let installed_toolchains = rustup::installed_toolchains(verbose)?; @@ -422,15 +433,17 @@ pub fn run() -> Result { if target.needs_docker() && args.subcommand.map(|sc| sc.needs_docker()).unwrap_or(false) { + let engine = docker::Engine::new(verbose)?; if host_version_meta.needs_interpreter() && needs_interpreter && target.needs_interpreter() && !interpreter::is_registered(&target)? { - docker::register(&target, verbose)? + docker::register(&engine, &target, verbose)? } return docker::run( + &engine, &target, &filtered_args, &metadata, diff --git a/src/rustc.rs b/src/rustc.rs index 7f8d33bae..ce840fdc2 100644 --- a/src/rustc.rs +++ b/src/rustc.rs @@ -57,3 +57,7 @@ pub fn sysroot(host: &Host, target: &Target, verbose: bool) -> Result { Ok(PathBuf::from(stdout)) } + +pub fn version_meta() -> Result { + rustc_version::version_meta().wrap_err("couldn't fetch the `rustc` version") +} diff --git a/xtask/src/build_docker_image.rs b/xtask/src/build_docker_image.rs index 4a0d37770..1848a5ce9 100644 --- a/xtask/src/build_docker_image.rs +++ b/xtask/src/build_docker_image.rs @@ -1,4 +1,4 @@ -use std::{path::Path, process::Command}; +use std::path::Path; use clap::Args; use color_eyre::Section; @@ -21,7 +21,7 @@ pub struct BuildDockerImage { labels: Option, /// Provide verbose diagnostic output. #[clap(short, long)] - verbose: bool, + pub verbose: bool, #[clap(long)] dry_run: bool, #[clap(long)] @@ -83,7 +83,7 @@ pub fn build_docker_image( mut targets, .. }: BuildDockerImage, - engine: &Path, + engine: &cross::docker::Engine, ) -> cross::Result<()> { let metadata = cross::cargo_metadata_with_args( Some(Path::new(env!("CARGO_MANIFEST_DIR"))), @@ -131,7 +131,7 @@ pub fn build_docker_image( if gha && targets.len() > 1 { println!("::group::Build {target}"); } - let mut docker_build = Command::new(engine); + let mut docker_build = cross::docker::command(engine); docker_build.args(&["buildx", "build"]); docker_build.current_dir(&docker_root); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 988389de9..8644d3b8b 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -5,8 +5,6 @@ pub mod install_git_hooks; pub mod target_info; pub mod util; -use std::path::PathBuf; - use clap::{Parser, Subcommand}; use self::build_docker_image::BuildDockerImage; @@ -33,11 +31,11 @@ pub fn main() -> cross::Result<()> { let cli = Cli::parse(); match cli.command { Commands::TargetInfo(args) => { - let engine = get_container_engine(args.engine.as_deref())?; + let engine = get_container_engine(args.engine.as_deref(), args.verbose)?; target_info::target_info(args, &engine)?; } Commands::BuildDockerImage(args) => { - let engine = get_container_engine(args.engine.as_deref())?; + let engine = get_container_engine(args.engine.as_deref(), args.verbose)?; build_docker_image::build_docker_image(args, &engine)?; } Commands::InstallGitHooks(args) => { @@ -48,10 +46,14 @@ pub fn main() -> cross::Result<()> { Ok(()) } -fn get_container_engine(engine: Option<&str>) -> Result { - if let Some(ce) = engine { - which::which(ce) +fn get_container_engine( + engine: Option<&str>, + verbose: bool, +) -> cross::Result { + let engine = if let Some(ce) = engine { + which::which(ce)? } else { - cross::docker::get_container_engine() - } + cross::docker::get_container_engine()? + }; + cross::docker::Engine::from_path(engine, verbose) } diff --git a/xtask/src/target_info.rs b/xtask/src/target_info.rs index 249828705..79ceff6a2 100644 --- a/xtask/src/target_info.rs +++ b/xtask/src/target_info.rs @@ -1,8 +1,4 @@ -use std::{ - collections::BTreeMap, - path::Path, - process::{Command, Stdio}, -}; +use std::{collections::BTreeMap, process::Stdio}; use clap::Args; use cross::CommandExt; @@ -17,7 +13,7 @@ pub struct TargetInfo { targets: Vec, /// Provide verbose diagnostic output. #[clap(short, long)] - verbose: bool, + pub verbose: bool, /// Image registry. #[clap(long, default_value_t = String::from("ghcr.io"))] registry: String, @@ -49,8 +45,8 @@ fn format_image(registry: &str, repository: &str, target: &str, tag: &str) -> St output } -fn pull_image(engine: &Path, image: &str, verbose: bool) -> cross::Result<()> { - let mut command = Command::new(engine); +fn pull_image(engine: &cross::docker::Engine, image: &str, verbose: bool) -> cross::Result<()> { + let mut command = cross::docker::command(engine); command.arg("pull"); command.arg(image); if !verbose { @@ -62,7 +58,7 @@ fn pull_image(engine: &Path, image: &str, verbose: bool) -> cross::Result<()> { } fn image_info( - engine: &Path, + engine: &cross::docker::Engine, target: &str, image: &str, tag: &str, @@ -73,7 +69,7 @@ fn image_info( pull_image(engine, image, verbose)?; } - let mut command = Command::new(engine); + let mut command = cross::docker::command(engine); command.arg("run"); command.arg("-it"); command.arg("--rm"); @@ -100,7 +96,7 @@ pub fn target_info( tag, .. }: TargetInfo, - engine: &Path, + engine: &cross::docker::Engine, ) -> cross::Result<()> { let matrix = crate::util::get_matrix()?; let test_map: BTreeMap<&str, bool> = matrix