diff --git a/Cargo.lock b/Cargo.lock index a387f3662..29f63d8a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,7 @@ dependencies = [ "nix 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)", "shell-escape 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "which 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -88,6 +89,11 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "itoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "lazy_static" version = "1.4.0" @@ -123,6 +129,11 @@ dependencies = [ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ryu" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "semver" version = "0.9.0" @@ -141,6 +152,16 @@ name = "serde" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "serde_json" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "shell-escape" version = "0.1.4" @@ -201,14 +222,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum error-chain 0.12.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" "checksum hermit-abi 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "e2c55f143919fbc0bc77e427fe2d74cf23786d7c1875666f2fde3ac3c659bb67" "checksum home 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" "checksum nix 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" "checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +"checksum serde_json 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" "checksum shell-escape 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "170a13e64f2a51b77a45702ba77287f5c6829375b04a69cf2222acd17d0cfab9" "checksum toml 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" "checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" diff --git a/Cargo.toml b/Cargo.toml index 6da537fed..7de9b652f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ semver = "0.9" toml = "0.5" which = { version = "3.1.0", default_features = false } shell-escape = "0.1.4" +serde_json = "1.0.48" [target.'cfg(not(windows))'.dependencies] nix = "0.15" diff --git a/README.md b/README.md index 773b01962..54b7eb20d 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,49 @@ RUN dpkg --add-architecture arm64 && \ $ docker build -t my/image:tag path/to/where/the/Dockerfile/resides ``` +### Docker in Docker + +When running `cross` from inside a docker container, `cross` needs access to +the hosts docker daemon itself. This is normally achieved by mounting the +docker daemons socket `/var/run/docker.sock`. For example: + +``` +$ docker run -v /var/run/docker.sock:/var/run/docker.sock -v .:/project \ + -w /project my/development-image:tag cross build --target mips64-unknown-linux-gnuabi64 +``` + +The image running `cross` requires the rust development tools to be installed. + +With this setup `cross` must find and mount the correct host paths into the +container used for cross compilation. This includes the original project directory as +well as the root path of the parent container to give access to the rust build +tools. + +To inform `cross` that it is running inside a container set `CROSS_DOCKER_IN_DOCKER=true`. + +A development or CI container can be created like this: + +``` +FROM rust:1 + +# set CROSS_DOCKER_IN_DOCKER to inform `cross` that it is executed from within a container +ENV CROSS_DOCKER_IN_DOCKER=true + +# install `cross` +RUN cargo install cross + +... + +``` + +**Limitations**: Finding the mount point for the containers root directory is +currently only available for the overlayfs2 storage driver. In order to access +the parent containers rust setup, the child container mounts the parents +overlayfs. The parent must not be stopped before the child container, as the +overlayfs can not be unmounted correctly by Docker if the child container still +accesses it. + + ### Passing environment variables into the build environment By default, `cross` does not pass any environment variables into the build diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4dc1c63ad..e1cb1e0cb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -79,6 +79,9 @@ jobs: - bash: echo "##vso[task.setvariable variable=TAG]${BUILD_SOURCEBRANCH##refs/tags/}" displayName: Set TAG Variable condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/') + - bash: cargo test + displayName: Run unit tests + timeoutInMinutes: 5 - bash: ./build-docker-image.sh "${TARGET}" displayName: Build Docker Image timeoutInMinutes: 360 diff --git a/src/cli.rs b/src/cli.rs index 19bc32652..1caf95ede 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,14 +1,16 @@ +use std::str::FromStr; use std::{env, path::PathBuf}; -use crate::Target; use crate::cargo::Subcommand; use crate::rustc::TargetList; +use crate::Target; pub struct Args { pub all: Vec, pub subcommand: Option, pub target: Option, pub target_dir: Option, + pub docker_in_docker: bool, } pub fn parse(target_list: &TargetList) -> Args { @@ -27,7 +29,10 @@ pub fn parse(target_list: &TargetList) -> Args { all.push(t); } } else if arg.starts_with("--target=") { - target = arg.splitn(2, '=').nth(1).map(|s| Target::from(&*s, target_list)); + target = arg + .splitn(2, '=') + .nth(1) + .map(|s| Target::from(&*s, target_list)); all.push(arg); } else if arg == "--target-dir" { all.push(arg); @@ -41,19 +46,24 @@ pub fn parse(target_list: &TargetList) -> Args { all.push(format!("--target-dir=/target")); } } else { - if !arg.starts_with('-') && sc.is_none() { - sc = Some(Subcommand::from(arg.as_ref())); - } + if !arg.starts_with('-') && sc.is_none() { + sc = Some(Subcommand::from(arg.as_ref())); + } - all.push(arg.to_string()); + all.push(arg.to_string()); } } } + let docker_in_docker = env::var("CROSS_DOCKER_IN_DOCKER") + .map(|s| bool::from_str(&s).unwrap_or_default()) + .unwrap_or_default(); + Args { all, subcommand: sc, target, target_dir, + docker_in_docker, } } diff --git a/src/docker.rs b/src/docker.rs index 349967c0b..84b99d2b7 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -1,15 +1,16 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus}; use std::{env, fs}; use atty::Stream; use error_chain::bail; +use serde_json; -use crate::{Target, Toml}; use crate::cargo::Root; use crate::errors::*; use crate::extensions::{CommandExt, SafeCommand}; use crate::id; +use crate::{Target, Toml}; const DOCKER_IMAGES: &[&str] = &include!(concat!(env!("OUT_DIR"), "/docker-images.rs")); const DOCKER: &str = "docker"; @@ -56,8 +57,15 @@ pub fn run(target: &Target, toml: Option<&Toml>, uses_xargo: bool, sysroot: &PathBuf, - verbose: bool) + verbose: bool, + docker_in_docker: bool) -> Result { + let mount_finder = if docker_in_docker { + MountFinder::new(docker_read_mount_paths()?) + } else { + MountFinder::default() + }; + let root = root.path(); let home_dir = home::home_dir().ok_or_else(|| "could not find home directory")?; let cargo_dir = home::cargo_home()?; @@ -72,6 +80,13 @@ pub fn run(target: &Target, 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); + let mount_root = mount_finder.find_mount_path(&root); + let sysroot = mount_finder.find_mount_path(&sysroot); + let mut cmd = if uses_xargo { SafeCommand::new("xargo") } else { @@ -137,7 +152,7 @@ pub fn run(target: &Target, .args(&["-v", &format!("{}:/cargo:Z", cargo_dir.display())]) // Prevent `bin` from being mounted inside the Docker container. .args(&["-v", "/cargo/bin"]) - .args(&["-v", &format!("{}:/project:Z", root.display())]) + .args(&["-v", &format!("{}:/project:Z", mount_root.display())]) .args(&["-v", &format!("{}:/rust:Z,ro", sysroot.display())]) .args(&["-v", &format!("{}:/target:Z", target_dir.display())]) .args(&["-w", "/project"]); @@ -179,3 +194,232 @@ pub fn image(toml: Option<&Toml>, target: &Target) -> Result { Ok(image) } + +fn docker_read_mount_paths() -> Result> { + let hostname = if let Ok(v) = env::var("HOSTNAME") { + Ok(v) + } else { + Err("HOSTNAME environment variable not found") + }?; + + let docker_path = which::which(DOCKER)?; + let mut docker: Command = { + let mut command = Command::new(docker_path); + command.arg("inspect"); + command.arg(hostname); + command + }; + + let output = docker.run_and_get_stdout(false)?; + let info = if let Ok(val) = serde_json::from_str(&output) { + Ok(val) + } else { + Err("failed to parse docker inspect output") + }?; + + dockerinfo_parse_mounts(&info) +} + +fn dockerinfo_parse_mounts(info: &serde_json::Value) -> Result> { + let mut mounts = dockerinfo_parse_user_mounts(info); + let root_info = dockerinfo_parse_root_mount_path(info)?; + mounts.push(root_info); + Ok(mounts) +} + +fn dockerinfo_parse_root_mount_path(info: &serde_json::Value) -> Result { + let driver_name = info + .pointer("/0/GraphDriver/Name") + .and_then(|v| v.as_str()) + .ok_or("No driver name found")?; + + if driver_name == "overlay2" { + let path = info + .pointer("/0/GraphDriver/Data/MergedDir") + .and_then(|v| v.as_str()) + .ok_or("No merge directory found")?; + + Ok(MountDetail { + source: PathBuf::from(&path), + destination: PathBuf::from("/"), + }) + } else { + Err(format!("want driver overlay2, got {}", driver_name).into()) + } +} + +fn dockerinfo_parse_user_mounts(info: &serde_json::Value) -> Vec { + info.pointer("/0/Mounts") + .and_then(|v| v.as_array()) + .map(|v| { + let make_path = |v: &serde_json::Value| PathBuf::from(&v.as_str().unwrap()); + let mut mounts = vec![]; + for details in v { + let source = make_path(&details["Source"]); + let destination = make_path(&details["Destination"]); + if source != destination { + mounts.push(MountDetail { + source, + destination, + }); + } + } + mounts + }) + .unwrap_or_else(|| Vec::new()) +} + +#[derive(Debug, Default)] +struct MountFinder { + mounts: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +struct MountDetail { + source: PathBuf, + destination: PathBuf, +} + +impl MountFinder { + fn new(mounts: Vec) -> MountFinder { + // sort by length (reverse), to give mounts with more path components a higher priority; + let mut mounts = mounts; + mounts.sort_by(|a, b| { + let la = a.destination.as_os_str().len(); + let lb = b.destination.as_os_str().len(); + la.cmp(&lb).reverse() + }); + MountFinder { mounts } + } + + fn find_mount_path(&self, path: &Path) -> PathBuf { + for info in &self.mounts { + if let Ok(stripped) = path.strip_prefix(&info.destination) { + return info.source.join(stripped); + } + } + return path.to_path_buf(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod mount_finder { + use super::*; + + #[test] + fn test_default_finder_returns_original() { + let finder = MountFinder::default(); + assert_eq!( + PathBuf::from("/test/path"), + finder.find_mount_path(&PathBuf::from("/test/path")), + ); + } + + #[test] + fn test_longest_destination_path_wins() { + let finder = MountFinder::new(vec![ + MountDetail { + source: PathBuf::from("/project/path"), + destination: PathBuf::from("/project"), + }, + MountDetail { + source: PathBuf::from("/target/path"), + destination: PathBuf::from("/project/target"), + }, + ]); + assert_eq!( + PathBuf::from("/target/path/test"), + finder.find_mount_path(&PathBuf::from("/project/target/test")) + ) + } + + #[test] + fn test_adjust_multiple_paths() { + let finder = MountFinder::new(vec![ + MountDetail { + source: PathBuf::from("/var/lib/docker/overlay2/container-id/merged"), + destination: PathBuf::from("/"), + }, + MountDetail { + source: PathBuf::from("/home/project/path"), + destination: PathBuf::from("/project"), + }, + ]); + assert_eq!( + PathBuf::from("/var/lib/docker/overlay2/container-id/merged/container/path"), + finder.find_mount_path(&PathBuf::from("/container/path")) + ); + assert_eq!( + PathBuf::from("/home/project/path"), + finder.find_mount_path(&PathBuf::from("/project")) + ); + assert_eq!( + PathBuf::from("/home/project/path/target"), + finder.find_mount_path(&PathBuf::from("/project/target")) + ); + } + } + + mod parse_docker_inspect { + use super::*; + use serde_json::json; + + #[test] + fn test_parse_container_root() { + let actual = dockerinfo_parse_root_mount_path(&json!([{ + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/f107af83b37bc0a182d3d2661f3d84684f0fffa1a243566b338a388d5e54bef4-init/diff:/var/lib/docker/overlay2/dfe81d459bbefada7aa897a9d05107a77145b0d4f918855f171ee85789ab04a0/diff:/var/lib/docker/overlay2/1f704696915c75cd081a33797ecc66513f9a7a3ffab42d01a3f17c12c8e2dc4c/diff:/var/lib/docker/overlay2/0a4f6cb88f4ace1471442f9053487a6392c90d2c6e206283d20976ba79b38a46/diff:/var/lib/docker/overlay2/1ee3464056f9cdc968fac8427b04e37ec96b108c5050812997fa83498f2499d1/diff:/var/lib/docker/overlay2/0ec5a47f1854c0f5cfe0e3f395b355b5a8bb10f6e622710ce95b96752625f874/diff:/var/lib/docker/overlay2/f24c8ad76303838b49043d17bf2423fe640836fd9562d387143e68004f8afba0/diff:/var/lib/docker/overlay2/462f89d5a0906805a6f2eec48880ed1e48256193ed506da95414448d435db2b7/diff", + "MergedDir": "/var/lib/docker/overlay2/f107af83b37bc0a182d3d2661f3d84684f0fffa1a243566b338a388d5e54bef4/merged", + "UpperDir": "/var/lib/docker/overlay2/f107af83b37bc0a182d3d2661f3d84684f0fffa1a243566b338a388d5e54bef4/diff", + "WorkDir": "/var/lib/docker/overlay2/f107af83b37bc0a182d3d2661f3d84684f0fffa1a243566b338a388d5e54bef4/work" + }, + "Name": "overlay2" + }, + }])).unwrap(); + let want = MountDetail { + source: PathBuf::from("/var/lib/docker/overlay2/f107af83b37bc0a182d3d2661f3d84684f0fffa1a243566b338a388d5e54bef4/merged"), + destination: PathBuf::from("/"), + }; + assert_eq!(want, actual); + } + + #[test] + fn test_parse_user_mounts_remove_if_source_dest_eq() { + let actual = dockerinfo_parse_user_mounts(&json!([{ + "Mounts": [ + { + "Type": "bind", + "Source": "/var/run/docker.sock", + "Destination": "/var/run/docker.sock", + }, + { + "Type": "bind", + "Source": "/mounted/path", + "Destination": "/mounted/path", + } + ], + }])); + assert_eq!(Vec::::new(), actual); + } + + #[test] + fn test_parse_empty_user_mounts() { + let actual = dockerinfo_parse_user_mounts(&json!([{ + "Mounts": [], + }])); + assert_eq!(Vec::::new(), actual); + } + + #[test] + fn test_parse_missing_user_moutns() { + let actual = dockerinfo_parse_user_mounts(&json!([{ + "Id": "test", + }])); + assert_eq!(Vec::::new(), actual); + } + } +} diff --git a/src/main.rs b/src/main.rs index b6b0da5c5..d3252863c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -285,7 +285,8 @@ fn run() -> Result { toml.as_ref(), uses_xargo, &sysroot, - verbose); + verbose, + args.docker_in_docker); } } }