diff --git a/src/bin/commands/images.rs b/src/bin/commands/images.rs new file mode 100644 index 000000000..79112efd8 --- /dev/null +++ b/src/bin/commands/images.rs @@ -0,0 +1,213 @@ +use clap::Args; +use cross::CommandExt; + +// known image prefixes, with their registry +// the docker.io registry can also be implicit +const GHCR_IO: &str = cross::docker::CROSS_IMAGE; +const RUST_EMBEDDED: &str = "rustembedded/cross:"; +const DOCKER_IO: &str = "docker.io/rustembedded/cross:"; +const IMAGE_PREFIXES: &[&str] = &[GHCR_IO, DOCKER_IO, RUST_EMBEDDED]; + +#[derive(Args, Debug)] +pub struct ListImages { + /// 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 RemoveImages { + /// If not provided, remove all images. + pub targets: Vec, + /// Remove images matching provided targets. + #[clap(short, long)] + pub verbose: bool, + /// Force removal of images. + #[clap(short, long)] + pub force: bool, + /// Remove local (development) images. + #[clap(short, long)] + pub local: bool, + /// Remove images. Default is a dry run. + #[clap(short, long)] + pub execute: bool, + /// Container engine (such as docker or podman). + #[clap(long)] + pub engine: Option, +} + +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)] +struct Image { + repository: String, + tag: String, + // need to remove images by ID, not just tag + id: String, +} + +impl Image { + fn name(&self) -> String { + format!("{}:{}", self.repository, self.tag) + } +} + +fn parse_image(image: &str) -> Image { + // this cannot panic: we've formatted our image list as `${repo}:${tag} ${id}` + let (repository, rest) = image.split_once(':').unwrap(); + let (tag, id) = rest.split_once(' ').unwrap(); + Image { + repository: repository.to_string(), + tag: tag.to_string(), + id: id.to_string(), + } +} + +fn is_cross_image(repository: &str) -> bool { + IMAGE_PREFIXES.iter().any(|i| repository.starts_with(i)) +} + +fn is_local_image(tag: &str) -> bool { + tag.starts_with("local") +} + +fn get_cross_images( + engine: &cross::docker::Engine, + verbose: bool, + local: bool, +) -> cross::Result> { + let stdout = cross::docker::subcommand(engine, "images") + .arg("--format") + .arg("{{.Repository}}:{{.Tag}} {{.ID}}") + .run_and_get_stdout(verbose)?; + + let mut images: Vec = stdout + .lines() + .map(parse_image) + .filter(|image| is_cross_image(&image.repository)) + .filter(|image| local || !is_local_image(&image.tag)) + .collect(); + images.sort(); + + Ok(images) +} + +// the old rustembedded targets had the following format: +// repository = (${registry}/)?rustembedded/cross +// tag = ${target}(-${version})? +// the last component must match `[A-Za-z0-9_-]` and +// we must have at least 3 components. the first component +// may contain other characters, such as `thumbv8m.main-none-eabi`. +fn rustembedded_target(tag: &str) -> String { + let is_target_char = |c: char| c == '_' || c.is_ascii_alphanumeric(); + let mut components = vec![]; + for (index, component) in tag.split('-').enumerate() { + if index <= 2 || (!component.is_empty() && component.chars().all(is_target_char)) { + components.push(component) + } else { + break; + } + } + + components.join("-") +} + +fn get_image_target(image: &Image) -> cross::Result { + if let Some(stripped) = image.repository.strip_prefix(GHCR_IO) { + Ok(stripped.to_string()) + } else if let Some(tag) = image.tag.strip_prefix(RUST_EMBEDDED) { + Ok(rustembedded_target(tag)) + } else if let Some(tag) = image.tag.strip_prefix(DOCKER_IO) { + Ok(rustembedded_target(tag)) + } else { + eyre::bail!("cannot get target for image {}", image.name()) + } +} + +pub fn list_images( + ListImages { verbose, .. }: ListImages, + engine: &cross::docker::Engine, +) -> cross::Result<()> { + get_cross_images(engine, verbose, true)? + .iter() + .for_each(|line| println!("{}", line.name())); + + Ok(()) +} + +fn remove_images( + engine: &cross::docker::Engine, + images: &[&str], + verbose: bool, + force: bool, + execute: bool, +) -> cross::Result<()> { + let mut command = cross::docker::subcommand(engine, "rmi"); + if force { + command.arg("--force"); + } + command.args(images); + if execute { + command.run(verbose).map_err(Into::into) + } else { + println!("{:?}", command); + Ok(()) + } +} + +pub fn remove_all_images( + RemoveImages { + verbose, + force, + local, + execute, + .. + }: RemoveImages, + engine: &cross::docker::Engine, +) -> cross::Result<()> { + let images = get_cross_images(engine, verbose, local)?; + let ids: Vec<&str> = images.iter().map(|i| i.id.as_ref()).collect(); + remove_images(engine, &ids, verbose, force, execute) +} + +pub fn remove_target_images( + RemoveImages { + targets, + verbose, + force, + local, + execute, + .. + }: RemoveImages, + engine: &cross::docker::Engine, +) -> cross::Result<()> { + let images = get_cross_images(engine, verbose, local)?; + let mut ids = vec![]; + for image in images.iter() { + let target = get_image_target(image)?; + if targets.contains(&target) { + ids.push(image.id.as_ref()); + } + } + remove_images(engine, &ids, verbose, force, execute) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_rustembedded_target() { + let targets = [ + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "thumbv8m.main-none-eabi", + ]; + for target in targets { + let versioned = format!("{target}-0.2.1"); + assert_eq!(rustembedded_target(target), target.to_string()); + assert_eq!(rustembedded_target(&versioned), target.to_string()); + } + } +} diff --git a/src/bin/commands/mod.rs b/src/bin/commands/mod.rs new file mode 100644 index 000000000..f2c4a675a --- /dev/null +++ b/src/bin/commands/mod.rs @@ -0,0 +1,3 @@ +mod images; + +pub use self::images::*; diff --git a/src/bin/cross-util.rs b/src/bin/cross-util.rs index 8cbb4a11e..19452d55a 100644 --- a/src/bin/cross-util.rs +++ b/src/bin/cross-util.rs @@ -1,21 +1,15 @@ #![deny(missing_debug_implementations, rust_2018_idioms)] -use std::path::{Path, PathBuf}; -use std::process::Command; - use clap::{Parser, Subcommand}; -use cross::CommandExt; -// known image prefixes, with their registry -// the docker.io registry can also be implicit -const GHCR_IO: &str = "ghcr.io/cross-rs/"; -const RUST_EMBEDDED: &str = "rustembedded/cross:"; -const DOCKER_IO: &str = "docker.io/rustembedded/cross:"; -const IMAGE_PREFIXES: &[&str] = &[GHCR_IO, DOCKER_IO, RUST_EMBEDDED]; +mod commands; #[derive(Parser, Debug)] #[clap(version, about, long_about = None)] struct Cli { + /// Toolchain name/version to use (such as stable or 1.59.0). + #[clap(value_parser = is_toolchain)] + toolchain: Option, #[clap(subcommand)] command: Commands, } @@ -23,230 +17,48 @@ struct Cli { #[derive(Subcommand, Debug)] enum Commands { /// List cross images in local storage. - ListImages { - /// Provide verbose diagnostic output. - #[clap(short, long)] - verbose: bool, - /// Container engine (such as docker or podman). - #[clap(long)] - engine: Option, - }, + ListImages(commands::ListImages), /// Remove cross images in local storage. - RemoveImages { - /// If not provided, remove all images. - targets: Vec, - /// Remove images matching provided targets. - #[clap(short, long)] - verbose: bool, - /// Force removal of images. - #[clap(short, long)] - force: bool, - /// Remove local (development) images. - #[clap(short, long)] - local: bool, - /// Remove images. Default is a dry run. - #[clap(short, long)] - execute: bool, - /// Container engine (such as docker or podman). - #[clap(long)] - engine: Option, - }, -} - -#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)] -struct Image { - repository: String, - tag: String, - // need to remove images by ID, not just tag - id: String, -} - -impl Image { - fn name(&self) -> String { - format!("{}:{}", self.repository, self.tag) - } + RemoveImages(commands::RemoveImages), } -fn get_container_engine(engine: Option<&str>) -> Result { - if let Some(ce) = engine { - which::which(ce) +fn is_toolchain(toolchain: &str) -> cross::Result { + if toolchain.starts_with('+') { + Ok(toolchain.chars().skip(1).collect()) } else { - cross::get_container_engine() + eyre::bail!("not a toolchain") } } -fn parse_image(image: &str) -> Image { - // this cannot panic: we've formatted our image list as `${repo}:${tag} ${id}` - let (repository, rest) = image.split_once(':').unwrap(); - let (tag, id) = rest.split_once(' ').unwrap(); - Image { - repository: repository.to_string(), - tag: tag.to_string(), - id: id.to_string(), - } -} - -fn is_cross_image(repository: &str) -> bool { - IMAGE_PREFIXES.iter().any(|i| repository.starts_with(i)) -} - -fn is_local_image(tag: &str) -> bool { - tag.starts_with("local") -} - -fn get_cross_images(engine: &Path, verbose: bool, local: bool) -> cross::Result> { - let stdout = Command::new(engine) - .arg("images") - .arg("--format") - .arg("{{.Repository}}:{{.Tag}} {{.ID}}") - .run_and_get_stdout(verbose)?; - - let mut images: Vec = stdout - .lines() - .map(parse_image) - .filter(|image| is_cross_image(&image.repository)) - .filter(|image| local || !is_local_image(&image.tag)) - .collect(); - images.sort(); - - Ok(images) -} - -// the old rustembedded targets had the following format: -// repository = (${registry}/)?rustembedded/cross -// tag = ${target}(-${version})? -// the last component must match `[A-Za-z0-9_-]` and -// we must have at least 3 components. the first component -// may contain other characters, such as `thumbv8m.main-none-eabi`. -fn rustembedded_target(tag: &str) -> String { - let is_target_char = |c: char| c == '_' || c.is_ascii_alphanumeric(); - let mut components = vec![]; - for (index, component) in tag.split('-').enumerate() { - if index <= 2 || (!component.is_empty() && component.chars().all(is_target_char)) { - components.push(component) - } else { - break; - } - } - - components.join("-") -} - -fn get_image_target(image: &Image) -> cross::Result { - if let Some(stripped) = image.repository.strip_prefix(GHCR_IO) { - Ok(stripped.to_string()) - } else if let Some(tag) = image.tag.strip_prefix(RUST_EMBEDDED) { - Ok(rustembedded_target(tag)) - } else if let Some(tag) = image.tag.strip_prefix(DOCKER_IO) { - Ok(rustembedded_target(tag)) - } else { - eyre::bail!("cannot get target for image {}", image.name()) - } -} - -fn list_images(engine: &Path, verbose: bool) -> cross::Result<()> { - get_cross_images(engine, verbose, true)? - .iter() - .for_each(|line| println!("{}", line.name())); - - Ok(()) -} - -fn remove_images( - engine: &Path, - images: &[&str], +fn get_container_engine( + engine: Option<&str>, verbose: bool, - force: bool, - execute: bool, -) -> cross::Result<()> { - let mut command = Command::new(engine); - command.arg("rmi"); - if force { - command.arg("--force"); - } - command.args(images); - if execute { - command.run(verbose).map_err(Into::into) +) -> cross::Result { + let engine = if let Some(ce) = engine { + which::which(ce)? } else { - println!("{:?}", command); - Ok(()) - } -} - -fn remove_all_images( - engine: &Path, - verbose: bool, - force: bool, - local: bool, - execute: bool, -) -> cross::Result<()> { - let images = get_cross_images(engine, verbose, local)?; - let ids: Vec<&str> = images.iter().map(|i| i.id.as_ref()).collect(); - remove_images(engine, &ids, verbose, force, execute) -} - -fn remove_target_images( - engine: &Path, - targets: &[String], - verbose: bool, - force: bool, - local: bool, - execute: bool, -) -> cross::Result<()> { - let images = get_cross_images(engine, verbose, local)?; - let mut ids = vec![]; - for image in images.iter() { - let target = get_image_target(image)?; - if targets.contains(&target) { - ids.push(image.id.as_ref()); - } - } - remove_images(engine, &ids, verbose, force, execute) + cross::docker::get_container_engine()? + }; + cross::docker::Engine::from_path(engine, verbose) } pub fn main() -> cross::Result<()> { cross::install_panic_hook()?; let cli = Cli::parse(); - match &cli.command { - Commands::ListImages { verbose, engine } => { - let engine = get_container_engine(engine.as_deref())?; - list_images(&engine, *verbose)?; + match cli.command { + Commands::ListImages(args) => { + let engine = get_container_engine(args.engine.as_deref(), args.verbose)?; + commands::list_images(args, &engine)?; } - Commands::RemoveImages { - targets, - verbose, - force, - local, - execute, - engine, - } => { - let engine = get_container_engine(engine.as_deref())?; - if targets.is_empty() { - remove_all_images(&engine, *verbose, *force, *local, *execute)?; + Commands::RemoveImages(args) => { + let engine = get_container_engine(args.engine.as_deref(), args.verbose)?; + if args.targets.is_empty() { + commands::remove_all_images(args, &engine)?; } else { - remove_target_images(&engine, targets, *verbose, *force, *local, *execute)?; + commands::remove_target_images(args, &engine)?; } } } Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_rustembedded_target() { - let targets = [ - "x86_64-unknown-linux-gnu", - "x86_64-apple-darwin", - "thumbv8m.main-none-eabi", - ]; - for target in targets { - let versioned = format!("{target}-0.2.1"); - assert_eq!(rustembedded_target(target), target.to_string()); - assert_eq!(rustembedded_target(&versioned), target.to_string()); - } - } -} diff --git a/src/docker/engine.rs b/src/docker/engine.rs new file mode 100644 index 000000000..f2230e64a --- /dev/null +++ b/src/docker/engine.rs @@ -0,0 +1,64 @@ +use std::env; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use crate::errors::*; +use crate::extensions::CommandExt; + +const DOCKER: &str = "docker"; +const PODMAN: &str = "podman"; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum EngineType { + Docker, + Podman, + PodmanRemote, + Other, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Engine { + pub kind: EngineType, + pub path: PathBuf, +} + +impl Engine { + pub fn new(verbose: bool) -> Result { + let path = get_container_engine() + .map_err(|_| eyre::eyre!("no container engine found")) + .with_suggestion(|| "is docker or podman installed?")?; + Self::from_path(path, verbose) + } + + pub fn from_path(path: PathBuf, verbose: bool) -> Result { + let kind = get_engine_type(&path, verbose)?; + Ok(Engine { path, kind }) + } +} + +// determine if the container engine is docker. this fixes issues with +// any aliases (#530), and doesn't fail if an executable suffix exists. +fn get_engine_type(ce: &Path, verbose: bool) -> Result { + let stdout = Command::new(ce) + .arg("--help") + .run_and_get_stdout(verbose)? + .to_lowercase(); + + if stdout.contains("podman-remote") { + Ok(EngineType::PodmanRemote) + } else if stdout.contains("podman") { + Ok(EngineType::Podman) + } else if stdout.contains("docker") && !stdout.contains("emulate") { + Ok(EngineType::Docker) + } else { + Ok(EngineType::Other) + } +} + +pub fn get_container_engine() -> Result { + if let Ok(ce) = env::var("CROSS_CONTAINER_ENGINE") { + which::which(ce) + } else { + which::which(DOCKER).or_else(|_| which::which(PODMAN)) + } +} diff --git a/src/docker/local.rs b/src/docker/local.rs new file mode 100644 index 000000000..6ea91b93d --- /dev/null +++ b/src/docker/local.rs @@ -0,0 +1,93 @@ +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::{Config, Target}; +use atty::Stream; + +#[allow(clippy::too_many_arguments)] // TODO: refactor +pub(crate) fn run( + target: &Target, + args: &[String], + metadata: &CargoMetadata, + config: &Config, + uses_xargo: bool, + sysroot: &Path, + verbose: bool, + 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 mut cmd = cargo_cmd(uses_xargo); + cmd.args(args); + + let mut docker = subcommand(&engine, "run"); + docker.args(&["--userns", "host"]); + docker_envvars(&mut docker, config, target)?; + + let mount_volumes = docker_mount( + &mut docker, + metadata, + config, + target, + cwd, + verbose, + |docker, val, verbose| mount(docker, val, "", verbose), + |_| {}, + )?; + + docker.arg("--rm"); + + docker_seccomp(&mut docker, engine.kind, target, verbose)?; + docker_user_id(&mut docker, engine.kind); + + docker + .args(&["-v", &format!("{}:/xargo:Z", dirs.xargo.display())]) + .args(&["-v", &format!("{}:/cargo:Z", dirs.cargo.display())]) + // Prevent `bin` from being mounted inside the Docker container. + .args(&["-v", "/cargo/bin"]); + if mount_volumes { + docker.args(&[ + "-v", + &format!( + "{}:{}:Z", + dirs.host_root.display(), + dirs.mount_root.display() + ), + ]); + } else { + docker.args(&["-v", &format!("{}:/project:Z", dirs.host_root.display())]); + } + docker + .args(&["-v", &format!("{}:/rust:Z,ro", dirs.sysroot.display())]) + .args(&["-v", &format!("{}:/target:Z", dirs.target.display())]); + docker_cwd(&mut docker, metadata, &dirs, cwd, 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. + if let Some(ref nix_store) = dirs.nix_store { + docker.args(&[ + "-v", + &format!("{}:{}:Z", nix_store.display(), nix_store.display()), + ]); + } + + if atty::is(Stream::Stdin) { + docker.arg("-i"); + if atty::is(Stream::Stdout) && atty::is(Stream::Stderr) { + docker.arg("-t"); + } + } + + docker + .arg(&container_name(config, target)?) + .args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)]) + .run_and_get_status(verbose) + .map_err(Into::into) +} diff --git a/src/docker/mod.rs b/src/docker/mod.rs new file mode 100644 index 000000000..ea075e7a0 --- /dev/null +++ b/src/docker/mod.rs @@ -0,0 +1,38 @@ +mod engine; +mod local; +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::{Config, Target}; + +#[allow(clippy::too_many_arguments)] // TODO: refactor +pub fn run( + target: &Target, + args: &[String], + metadata: &CargoMetadata, + config: &Config, + uses_xargo: bool, + sysroot: &Path, + verbose: bool, + docker_in_docker: bool, + cwd: &Path, +) -> Result { + local::run( + target, + args, + metadata, + config, + uses_xargo, + sysroot, + verbose, + docker_in_docker, + cwd, + ) +} diff --git a/src/seccomp.json b/src/docker/seccomp.json similarity index 100% rename from src/seccomp.json rename to src/docker/seccomp.json diff --git a/src/docker.rs b/src/docker/shared.rs similarity index 67% rename from src/docker.rs rename to src/docker/shared.rs index 9408e01ee..861283049 100644 --- a/src/docker.rs +++ b/src/docker/shared.rs @@ -1,68 +1,127 @@ use std::io::Write; use std::path::{Path, PathBuf}; -use std::process::{Command, ExitStatus}; +use std::process::Command; use std::{env, fs}; +use super::engine::*; use crate::cargo::CargoMetadata; +use crate::config::Config; +use crate::errors::*; use crate::extensions::{CommandExt, SafeCommand}; -use crate::file::write_file; +use crate::file::{self, write_file}; use crate::id; -use crate::{errors::*, file}; -use crate::{Config, Target}; -use atty::Stream; -use eyre::bail; +use crate::Target; pub const CROSS_IMAGE: &str = "ghcr.io/cross-rs"; const DOCKER_IMAGES: &[&str] = &include!(concat!(env!("OUT_DIR"), "/docker-images.rs")); -const DOCKER: &str = "docker"; -const PODMAN: &str = "podman"; + // secured profile based off the docker documentation for denied syscalls: // https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile // note that we've allow listed `clone` and `clone3`, which is necessary // to fork the process, and which podman allows by default. -const SECCOMP: &str = include_str!("seccomp.json"); - -#[derive(Debug, PartialEq, Eq)] -enum EngineType { - Docker, - Podman, - Other, +pub(crate) const SECCOMP: &str = include_str!("seccomp.json"); + +#[derive(Debug)] +pub struct Directories { + pub cargo: PathBuf, + pub xargo: PathBuf, + pub target: PathBuf, + pub nix_store: Option, + pub host_root: PathBuf, + pub mount_root: PathBuf, + pub mount_cwd: PathBuf, + pub sysroot: PathBuf, } -// determine if the container engine is docker. this fixes issues with -// any aliases (#530), and doesn't fail if an executable suffix exists. -fn get_engine_type(ce: &Path, verbose: bool) -> Result { - let stdout = Command::new(ce) - .arg("--help") - .run_and_get_stdout(verbose)? - .to_lowercase(); - - if stdout.contains("podman") { - Ok(EngineType::Podman) - } else if stdout.contains("docker") && !stdout.contains("emulate") { - Ok(EngineType::Docker) - } else { - Ok(EngineType::Other) +impl Directories { + #[allow(unused_variables)] + pub fn create( + engine: &Engine, + metadata: &CargoMetadata, + cwd: &Path, + sysroot: &Path, + docker_in_docker: bool, + verbose: bool, + ) -> Result { + let mount_finder = if docker_in_docker { + MountFinder::new(docker_read_mount_paths(engine)?) + } else { + MountFinder::default() + }; + let home_dir = + home::home_dir().ok_or_else(|| eyre::eyre!("could not find home directory"))?; + let cargo = home::cargo_home()?; + let xargo = env::var_os("XARGO_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| home_dir.join(".xargo")); + let nix_store = env::var_os("NIX_STORE").map(PathBuf::from); + let target = &metadata.target_directory; + + // create the directories we are going to mount before we mount them, + // otherwise `docker` will create them but they will be owned by `root` + fs::create_dir(&cargo).ok(); + fs::create_dir(&xargo).ok(); + fs::create_dir(&target).ok(); + + let cargo = mount_finder.find_mount_path(cargo); + let xargo = mount_finder.find_mount_path(xargo); + let target = mount_finder.find_mount_path(target); + + // root is either workspace_root, or, if we're outside the workspace root, the current directory + let host_root = mount_finder.find_mount_path(if metadata.workspace_root.starts_with(cwd) { + cwd + } else { + &metadata.workspace_root + }); + + // root is either workspace_root, or, if we're outside the workspace root, the current directory + let mount_root: PathBuf; + #[cfg(target_os = "windows")] + { + // On Windows, we can not mount the directory name directly. Instead, we use wslpath to convert the path to a linux compatible path. + mount_root = wslpath(&host_root, verbose)?; + } + #[cfg(not(target_os = "windows"))] + { + mount_root = mount_finder.find_mount_path(host_root.clone()); + } + let mount_cwd: PathBuf; + #[cfg(target_os = "windows")] + { + // On Windows, we can not mount the directory name directly. Instead, we use wslpath to convert the path to a linux compatible path. + mount_cwd = wslpath(cwd, verbose)?; + } + #[cfg(not(target_os = "windows"))] + { + mount_cwd = mount_finder.find_mount_path(cwd); + } + let sysroot = mount_finder.find_mount_path(sysroot); + + Ok(Directories { + cargo, + xargo, + target, + nix_store, + host_root, + mount_root, + mount_cwd, + sysroot, + }) } } -pub fn get_container_engine() -> Result { - if let Ok(ce) = env::var("CROSS_CONTAINER_ENGINE") { - which::which(ce) - } else { - which::which(DOCKER).or_else(|_| which::which(PODMAN)) - } +pub fn command(engine: &Engine) -> Command { + Command::new(&engine.path) } -pub fn docker_command(engine: &Path, subcommand: &str) -> Result { - let mut command = Command::new(engine); +pub fn subcommand(engine: &Engine, subcommand: &str) -> Command { + let mut command = command(engine); command.arg(subcommand); - command.args(&["--userns", "host"]); - Ok(command) + command } /// Register binfmt interpreters -pub fn register(target: &Target, verbose: bool) -> Result<()> { +pub(crate) fn register(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 && \ @@ -72,8 +131,9 @@ pub fn register(target: &Target, verbose: bool) -> Result<()> { binfmt-support qemu-user-static" }; - let engine = get_container_engine()?; - docker_command(&engine, "run")? + let engine = Engine::new(verbose)?; + subcommand(&engine, "run") + .args(&["--userns", "host"]) .arg("--privileged") .arg("--rm") .arg("ubuntu:16.04") @@ -89,7 +149,9 @@ fn validate_env_var(var: &str) -> Result<(&str, Option<&str>)> { }; if key == "CROSS_RUNNER" { - bail!("CROSS_RUNNER environment variable name is reserved and cannot be pass through"); + eyre::bail!( + "CROSS_RUNNER environment variable name is reserved and cannot be pass through" + ); } Ok((key, value)) @@ -99,115 +161,110 @@ fn parse_docker_opts(value: &str) -> Result> { shell_words::split(value).wrap_err_with(|| format!("could not parse docker opts of {}", value)) } -#[allow(unused_variables)] -pub fn mount(cmd: &mut Command, val: &Path, verbose: bool) -> Result { - let host_path = file::canonicalize(&val) - .wrap_err_with(|| format!("when canonicalizing path `{}`", val.display()))?; - let mount_path: PathBuf; - #[cfg(target_os = "windows")] - { - // On Windows, we can not mount the directory name directly. Instead, we use wslpath to convert the path to a linux compatible path. - mount_path = wslpath(&host_path, verbose)?; - } - #[cfg(not(target_os = "windows"))] - { - mount_path = host_path.clone(); +pub(crate) fn cargo_cmd(uses_xargo: bool) -> SafeCommand { + if uses_xargo { + SafeCommand::new("xargo") + } else { + SafeCommand::new("cargo") } - cmd.args(&[ +} + +pub(crate) fn mount( + docker: &mut Command, + val: &Path, + prefix: &str, + verbose: bool, +) -> Result { + let host_path = file::canonicalize(val)?; + let mount_path = canonicalize_mount_path(&host_path, verbose)?; + docker.args(&[ "-v", - &format!("{}:{}", host_path.display(), mount_path.display()), + &format!("{}:{prefix}{}", host_path.display(), mount_path.display()), ]); Ok(mount_path) } -#[allow(clippy::too_many_arguments)] // TODO: refactor -pub fn run( - target: &Target, - args: &[String], - metadata: &CargoMetadata, - config: &Config, - uses_xargo: bool, - sysroot: &Path, - verbose: bool, - docker_in_docker: bool, - cwd: &Path, -) -> Result { - let engine = get_container_engine() - .map_err(|_| eyre::eyre!("no container engine found")) - .with_suggestion(|| "is docker or podman installed?")?; - let engine_type = get_engine_type(&engine, verbose)?; - - let mount_finder = if docker_in_docker { - MountFinder::new(docker_read_mount_paths(&engine)?) - } else { - MountFinder::default() - }; +pub(crate) fn docker_envvars(docker: &mut Command, config: &Config, target: &Target) -> Result<()> { + for ref var in config.env_passthrough(target)? { + validate_env_var(var)?; - let home_dir = home::home_dir().ok_or_else(|| eyre::eyre!("could not find home directory"))?; - let cargo_dir = home::cargo_home()?; - let xargo_dir = env::var_os("XARGO_HOME") - .map(PathBuf::from) - .unwrap_or_else(|| home_dir.join(".xargo")); - let nix_store_dir = env::var_os("NIX_STORE").map(PathBuf::from); - let target_dir = &metadata.target_directory; - - // create the directories we are going to mount before we mount them, - // otherwise `docker` will create them but they will be owned by `root` - fs::create_dir(&target_dir).ok(); - fs::create_dir(&cargo_dir).ok(); - fs::create_dir(&xargo_dir).ok(); - - // update paths to the host mounts path. - let cargo_dir = mount_finder.find_mount_path(cargo_dir); - let xargo_dir = mount_finder.find_mount_path(xargo_dir); - let target_dir = mount_finder.find_mount_path(target_dir); - // root is either workspace_root, or, if we're outside the workspace root, the current directory - let host_root = mount_finder.find_mount_path(if metadata.workspace_root.starts_with(cwd) { - cwd - } else { - &metadata.workspace_root - }); - let mount_root: PathBuf; - #[cfg(target_os = "windows")] - { - // On Windows, we can not mount the directory name directly. Instead, we use wslpath to convert the path to a linux compatible path. - mount_root = wslpath(&host_root, verbose)?; + // Only specifying the environment variable name in the "-e" + // flag forwards the value from the parent shell + docker.args(&["-e", var]); } - #[cfg(not(target_os = "windows"))] - { - mount_root = mount_finder.find_mount_path(host_root.clone()); + + let runner = config.runner(target)?; + let cross_runner = format!("CROSS_RUNNER={}", runner.unwrap_or_default()); + docker + .args(&["-e", "PKG_CONFIG_ALLOW_CROSS=1"]) + .args(&["-e", "XARGO_HOME=/xargo"]) + .args(&["-e", "CARGO_HOME=/cargo"]) + .args(&["-e", "CARGO_TARGET_DIR=/target"]) + .args(&["-e", &cross_runner]); + + if let Some(username) = id::username().unwrap() { + docker.args(&["-e", &format!("USER={username}")]); } - let mount_cwd: PathBuf; - #[cfg(target_os = "windows")] - { - // On Windows, we can not mount the directory name directly. Instead, we use wslpath to convert the path to a linux compatible path. - mount_cwd = wslpath(cwd, verbose)?; + + if let Ok(value) = env::var("QEMU_STRACE") { + docker.args(&["-e", &format!("QEMU_STRACE={value}")]); } - #[cfg(not(target_os = "windows"))] - { - mount_cwd = mount_finder.find_mount_path(cwd); + + if let Ok(value) = env::var("CROSS_DEBUG") { + docker.args(&["-e", &format!("CROSS_DEBUG={value}")]); } - let sysroot = mount_finder.find_mount_path(sysroot); - let mut cmd = if uses_xargo { - SafeCommand::new("xargo") - } else { - SafeCommand::new("cargo") + if let Ok(value) = env::var("CROSS_CONTAINER_OPTS") { + if env::var("DOCKER_OPTS").is_ok() { + eprintln!("Warning: using both `CROSS_CONTAINER_OPTS` and `DOCKER_OPTS`."); + } + docker.args(&parse_docker_opts(&value)?); + } else if let Ok(value) = env::var("DOCKER_OPTS") { + // FIXME: remove this when we deprecate DOCKER_OPTS. + docker.args(&parse_docker_opts(&value)?); }; - cmd.args(args); - - let runner = config.runner(target)?; + Ok(()) +} - let mut docker = docker_command(&engine, "run")?; +pub(crate) fn docker_cwd( + docker: &mut Command, + metadata: &CargoMetadata, + dirs: &Directories, + cwd: &Path, + mount_volumes: bool, +) -> Result<()> { + if mount_volumes { + docker.args(&["-w".as_ref(), dirs.mount_cwd.as_os_str()]); + } else if dirs.mount_cwd == metadata.workspace_root { + 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)?); + // No [T].join for OsStr + let mut mount_wd = std::ffi::OsString::new(); + for part in working_dir.iter() { + mount_wd.push("/"); + mount_wd.push(part); + } + docker.args(&["-w".as_ref(), mount_wd.as_os_str()]); + } - for ref var in config.env_passthrough(target)? { - validate_env_var(var)?; + Ok(()) +} - // Only specifying the environment variable name in the "-e" - // flag forwards the value from the parent shell - docker.args(&["-e", var]); - } +#[allow(clippy::too_many_arguments)] // TODO: refactor +pub(crate) fn docker_mount( + docker: &mut Command, + metadata: &CargoMetadata, + config: &Config, + target: &Target, + cwd: &Path, + verbose: bool, + mount_cb: impl Fn(&mut Command, &Path, bool) -> Result, + mut store_cb: impl FnMut((String, PathBuf)), +) -> Result { 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. @@ -223,21 +280,79 @@ pub fn run( }; if let Ok(val) = value { - let mount_path = mount(&mut docker, val.as_ref(), verbose)?; + let mount_path = mount_cb(docker, val.as_ref(), verbose)?; docker.args(&["-e", &format!("{}={}", var, mount_path.display())]); + store_cb((val, mount_path)); mount_volumes = true; } } for path in metadata.path_dependencies() { - mount(&mut docker, path, verbose)?; + let mount_path = mount_cb(docker, path, verbose)?; + store_cb((path.display().to_string(), mount_path)); mount_volumes = true; } - docker.args(&["-e", "PKG_CONFIG_ALLOW_CROSS=1"]); + Ok(mount_volumes) +} + +#[cfg(target_os = "windows")] +fn wslpath(path: &Path, verbose: bool) -> Result { + let wslpath = which::which("wsl.exe") + .map_err(|_| eyre::eyre!("could not find wsl.exe")) + .warning("usage of `env.volumes` requires WSL on Windows") + .suggestion("is WSL installed on the host?")?; + + Command::new(wslpath) + .arg("-e") + .arg("wslpath") + .arg("-a") + .arg(path) + .run_and_get_stdout(verbose) + .wrap_err_with(|| { + format!( + "could not get linux compatible path for `{}`", + path.display() + ) + }) + .map(|s| s.trim().into()) +} + +#[allow(unused_variables)] +pub(crate) fn canonicalize_mount_path(path: &Path, verbose: bool) -> Result { + #[cfg(target_os = "windows")] + { + // On Windows, we can not mount the directory name directly. Instead, we use wslpath to convert the path to a linux compatible path. + wslpath(path, verbose) + } + #[cfg(not(target_os = "windows"))] + { + Ok(path.to_path_buf()) + } +} + +pub(crate) fn user_id() -> String { + env::var("CROSS_CONTAINER_UID").unwrap_or_else(|_| id::user().to_string()) +} - docker.arg("--rm"); +pub(crate) fn group_id() -> String { + env::var("CROSS_CONTAINER_GID").unwrap_or_else(|_| id::group().to_string()) +} +pub(crate) fn docker_user_id(docker: &mut Command, engine_type: EngineType) { + // We need to specify the user for Docker, but not for Podman. + if engine_type == EngineType::Docker { + docker.args(&["--user", &format!("{}:{}", user_id(), group_id(),)]); + } +} + +#[allow(unused_variables)] +pub(crate) fn docker_seccomp( + docker: &mut Command, + engine_type: EngineType, + target: &Target, + verbose: bool, +) -> Result<()> { // docker uses seccomp now on all installations if target.needs_docker_seccomp() { let seccomp = if engine_type == EngineType::Docker && cfg!(target_os = "windows") { @@ -257,7 +372,7 @@ pub fn run( write_file(&path, false)?.write_all(SECCOMP.as_bytes())?; } #[cfg(target_os = "windows")] - if engine_type == EngineType::Podman { + if matches!(engine_type, EngineType::Podman | EngineType::PodmanRemote) { // podman weirdly expects a WSL path here, and fails otherwise path = wslpath(&path, verbose)?; } @@ -267,113 +382,16 @@ pub fn run( docker.args(&["--security-opt", &format!("seccomp={}", seccomp)]); } - // We need to specify the user for Docker, but not for Podman. - if engine_type == EngineType::Docker { - docker.args(&[ - "--user", - &format!( - "{}:{}", - env::var("CROSS_CONTAINER_UID").unwrap_or_else(|_| id::user().to_string()), - env::var("CROSS_CONTAINER_GID").unwrap_or_else(|_| id::group().to_string()), - ), - ]); - } - - docker - .args(&["-e", "XARGO_HOME=/xargo"]) - .args(&["-e", "CARGO_HOME=/cargo"]) - .args(&["-e", "CARGO_TARGET_DIR=/target"]); - - if let Some(username) = id::username().unwrap() { - docker.args(&["-e", &format!("USER={username}")]); - } - - if let Ok(value) = env::var("QEMU_STRACE") { - docker.args(&["-e", &format!("QEMU_STRACE={value}")]); - } - - if let Ok(value) = env::var("CROSS_DEBUG") { - docker.args(&["-e", &format!("CROSS_DEBUG={value}")]); - } - - if let Ok(value) = env::var("CROSS_CONTAINER_OPTS") { - if env::var("DOCKER_OPTS").is_ok() { - eprintln!("Warning: using both `CROSS_CONTAINER_OPTS` and `DOCKER_OPTS`."); - } - docker.args(&parse_docker_opts(&value)?); - } else if let Ok(value) = env::var("DOCKER_OPTS") { - // FIXME: remove this when we deprecate DOCKER_OPTS. - docker.args(&parse_docker_opts(&value)?); - }; - - docker - .args(&[ - "-e", - &format!("CROSS_RUNNER={}", runner.unwrap_or_default()), - ]) - .args(&["-v", &format!("{}:/xargo:Z", xargo_dir.display())]) - .args(&["-v", &format!("{}:/cargo:Z", cargo_dir.display())]) - // Prevent `bin` from being mounted inside the Docker container. - .args(&["-v", "/cargo/bin"]); - if mount_volumes { - docker.args(&[ - "-v", - &format!("{}:{}:Z", host_root.display(), mount_root.display()), - ]); - } else { - docker.args(&["-v", &format!("{}:/project:Z", host_root.display())]); - } - docker - .args(&["-v", &format!("{}:/rust:Z,ro", sysroot.display())]) - .args(&["-v", &format!("{}:/target:Z", target_dir.display())]); - - if mount_volumes { - docker.args(&["-w".as_ref(), mount_cwd.as_os_str()]); - } else if mount_cwd == metadata.workspace_root { - 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)?); - // No [T].join for OsStr - let mut mount_wd = std::ffi::OsString::new(); - for part in working_dir.iter() { - mount_wd.push("/"); - mount_wd.push(part); - } - docker.args(&["-w".as_ref(), mount_wd.as_os_str()]); - } - - // 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(nix_store) = nix_store_dir { - docker.args(&[ - "-v", - &format!("{}:{}:Z", nix_store.display(), nix_store.display()), - ]); - } - - if atty::is(Stream::Stdin) { - docker.arg("-i"); - if atty::is(Stream::Stdout) && atty::is(Stream::Stderr) { - docker.arg("-t"); - } - } - - docker - .arg(&image(config, target)?) - .args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)]) - .run_and_get_status(verbose) - .map_err(Into::into) + Ok(()) } -pub fn image(config: &Config, target: &Target) -> Result { +pub(crate) fn container_name(config: &Config, target: &Target) -> Result { if let Some(image) = config.image(target)? { return Ok(image); } if !DOCKER_IMAGES.contains(&target.triple()) { - bail!( + eyre::bail!( "`cross` does not provide a Docker image for target {target}, \ specify a custom image in `Cross.toml`." ); @@ -388,33 +406,11 @@ pub fn image(config: &Config, target: &Target) -> Result { Ok(format!("{CROSS_IMAGE}/{target}:{version}")) } -#[cfg(target_os = "windows")] -fn wslpath(path: &Path, verbose: bool) -> Result { - let wslpath = which::which("wsl.exe") - .map_err(|_| eyre::eyre!("could not find wsl.exe")) - .warning("usage of `env.volumes` requires WSL on Windows") - .suggestion("is WSL installed on the host?")?; - - Command::new(wslpath) - .arg("-e") - .arg("wslpath") - .arg("-a") - .arg(path) - .run_and_get_stdout(verbose) - .wrap_err_with(|| { - format!( - "could not get linux compatible path for `{}`", - path.display() - ) - }) - .map(|s| s.trim().into()) -} - -fn docker_read_mount_paths(engine: &Path) -> Result> { +fn docker_read_mount_paths(engine: &Engine) -> Result> { let hostname = env::var("HOSTNAME").wrap_err("HOSTNAME environment variable not found")?; let mut docker: Command = { - let mut command = docker_command(engine, "inspect")?; + let mut command = subcommand(engine, "inspect"); command.arg(hostname); command }; diff --git a/src/lib.rs b/src/lib.rs index 085f3da7a..536aac821 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ mod cargo; mod cli; mod config; mod cross_toml; -mod docker; +pub mod docker; pub mod errors; mod extensions; mod file; @@ -44,8 +44,6 @@ use self::cross_toml::CrossToml; use self::errors::Context; use self::rustc::{TargetList, VersionMetaExt}; -pub use self::docker::get_container_engine; -pub use self::docker::CROSS_IMAGE; pub use self::errors::{install_panic_hook, Result}; pub use self::extensions::{CommandExt, OutputExt}; @@ -306,7 +304,7 @@ pub fn run() -> Result { .unwrap_or_else(|| Target::from(host.triple(), &target_list)); config.confusable_target(&target); - let image_exists = match docker::image(&config, &target) { + let image_exists = match docker::container_name(&config, &target) { Ok(_) => true, Err(err) => { eprintln!("Warning: {}", err); diff --git a/xtask/src/build_docker_image.rs b/xtask/src/build_docker_image.rs index b38d98a3c..a60e52834 100644 --- a/xtask/src/build_docker_image.rs +++ b/xtask/src/build_docker_image.rs @@ -14,7 +14,7 @@ pub struct BuildDockerImage { /// Specify a tag to use instead of the derived one, eg `local` #[clap(long)] tag: Option, - #[clap(long, default_value = cross::CROSS_IMAGE)] + #[clap(long, default_value = cross::docker::CROSS_IMAGE)] repository: String, /// Newline separated labels #[clap(long, env = "LABELS")] diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 3f60b239d..988389de9 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -52,6 +52,6 @@ fn get_container_engine(engine: Option<&str>) -> Result { if let Some(ce) = engine { which::which(ce) } else { - cross::get_container_engine() + cross::docker::get_container_engine() } }