use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use super::custom::{Dockerfile, PreBuild};
use super::engine::*;
use crate::cargo::{cargo_metadata_with_args, CargoMetadata};
use crate::config::{bool_from_envvar, Config};
use crate::errors::*;
use crate::extensions::{CommandExt, SafeCommand};
use crate::file::{self, write_file, PathExt, ToUtf8};
use crate::id;
use crate::rustc::{self, VersionMetaExt};
use crate::shell::{MessageInfo, Verbosity};
use crate::Target;
pub use super::custom::CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX;
pub const CROSS_IMAGE: &str = "ghcr.io/cross-rs";
// note: this is the most common base image for our images
pub const UBUNTU_BASE: &str = "ubuntu:16.04";
const DOCKER_IMAGES: &[&str] = &include!(concat!(env!("OUT_DIR"), "/docker-images.rs"));
// 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.
pub(crate) const SECCOMP: &str = include_str!("seccomp.json");
#[derive(Debug)]
pub struct DockerOptions {
pub engine: Engine,
pub target: Target,
pub config: Config,
pub uses_xargo: bool,
}
impl DockerOptions {
pub fn new(engine: Engine, target: Target, config: Config, uses_xargo: bool) -> DockerOptions {
DockerOptions {
engine,
target,
config,
uses_xargo,
}
}
#[must_use]
pub fn in_docker(&self) -> bool {
self.engine.in_docker
}
#[must_use]
pub fn is_remote(&self) -> bool {
self.engine.is_remote
}
#[must_use]
pub fn needs_custom_image(&self) -> bool {
self.config
.dockerfile(&self.target)
.unwrap_or_default()
.is_some()
|| self
.config
.pre_build(&self.target)
.unwrap_or_default()
.is_some()
}
pub(crate) fn custom_image_build(
&self,
paths: &DockerPaths,
msg_info: &mut MessageInfo,
) -> Result<String> {
let mut image = image_name(&self.config, &self.target)?;
if let Some(path) = self.config.dockerfile(&self.target)? {
let context = self.config.dockerfile_context(&self.target)?;
let name = self.config.image(&self.target)?;
let build = Dockerfile::File {
path: &path,
context: context.as_deref(),
name: name.as_deref(),
};
image = build
.build(
self,
paths,
self.config
.dockerfile_build_args(&self.target)?
.unwrap_or_default(),
msg_info,
)
.wrap_err("when building dockerfile")?;
}
let pre_build = self.config.pre_build(&self.target)?;
if let Some(pre_build) = pre_build {
match pre_build {
super::custom::PreBuild::Single {
line: pre_build_script,
env,
} if !env
|| !pre_build_script.contains('\n')
&& paths.host_root().join(&pre_build_script).is_file() =>
{
let custom = Dockerfile::Custom {
content: format!(
r#"
FROM {image}
ARG CROSS_DEB_ARCH=
ARG CROSS_SCRIPT
ARG CROSS_TARGET
COPY $CROSS_SCRIPT /pre-build-script
RUN chmod +x /pre-build-script
RUN ./pre-build-script $CROSS_TARGET"#
),
};
image = custom
.build(
self,
paths,
vec![
("CROSS_SCRIPT", &*pre_build_script),
("CROSS_TARGET", self.target.triple()),
],
msg_info,
)
.wrap_err("when pre-building")
.with_note(|| format!("CROSS_SCRIPT={pre_build_script}"))
.with_note(|| format!("CROSS_TARGET={}", self.target))?;
}
this => {
let pre_build = match this {
PreBuild::Single { line, .. } => vec![line],
PreBuild::Lines(lines) => lines,
};
if !pre_build.is_empty() {
let custom = Dockerfile::Custom {
content: format!(
r#"
FROM {image}
ARG CROSS_DEB_ARCH=
ARG CROSS_CMD
RUN eval "${{CROSS_CMD}}""#
),
};
image = custom
.build(
self,
paths,
Some(("CROSS_CMD", pre_build.join("\n"))),
msg_info,
)
.wrap_err("when pre-building")
.with_note(|| format!("CROSS_CMD={}", pre_build.join("\n")))?;
}
}
}
}
Ok(image)
}
pub(crate) fn image_name(&self) -> Result<String> {
if let Some(image) = self.config.image(&self.target)? {
return Ok(image);
}
if !DOCKER_IMAGES.contains(&self.target.triple()) {
eyre::bail!(
"`cross` does not provide a Docker image for target {target}, \
specify a custom image in `Cross.toml`.",
target = self.target
);
}
let version = if include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")).is_empty() {
env!("CARGO_PKG_VERSION")
} else {
"main"
};
Ok(format!(
"{CROSS_IMAGE}/{target}:{version}",
target = self.target
))
}
}
#[derive(Debug)]
pub struct DockerPaths {
pub mount_finder: MountFinder,
pub metadata: CargoMetadata,
pub cwd: PathBuf,
pub sysroot: PathBuf,
pub directories: Directories,
}
impl DockerPaths {
pub fn create(
engine: &Engine,
metadata: CargoMetadata,
cwd: PathBuf,
sysroot: PathBuf,
) -> Result<Self> {
let mount_finder = MountFinder::create(engine)?;
let directories = Directories::create(&mount_finder, &metadata, &cwd, &sysroot)?;
Ok(Self {
mount_finder,
metadata,
cwd,
sysroot,
directories,
})
}
pub fn workspace_root(&self) -> &Path {
&self.metadata.workspace_root
}
pub fn workspace_dependencies(&self) -> impl Iterator<Item = &Path> {
self.metadata.path_dependencies()
}
pub fn workspace_from_cwd(&self) -> Result<&Path> {
self.cwd
.strip_prefix(self.workspace_root())
.map_err(Into::into)
}
#[must_use]
pub fn in_workspace(&self) -> bool {
self.workspace_from_cwd().is_ok()
}
pub fn mount_cwd(&self) -> &str {
&self.directories.mount_cwd
}
pub fn host_root(&self) -> &Path {
&self.directories.host_root
}
}
#[derive(Debug)]
pub struct Directories {
pub cargo: PathBuf,
pub xargo: PathBuf,
pub target: PathBuf,
pub nix_store: Option<PathBuf>,
pub host_root: PathBuf,
// both mount fields are WSL paths on windows: they already are POSIX paths
pub mount_root: String,
pub mount_cwd: String,
pub sysroot: PathBuf,
}
impl Directories {
pub fn create(
mount_finder: &MountFinder,
metadata: &CargoMetadata,
cwd: &Path,
sysroot: &Path,
) -> Result<Self> {
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_or_else(|| home_dir.join(".xargo"), PathBuf::from);
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`
// cargo builds all intermediate directories, but fails
// if it has other issues (such as permission errors).
fs::create_dir_all(&cargo)?;
fs::create_dir_all(&xargo)?;
create_target_dir(target)?;
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: String;
#[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 = host_root.as_wslpath()?;
}
#[cfg(not(target_os = "windows"))]
{
// NOTE: host root has already found the mount path
mount_root = host_root.to_utf8()?.to_owned();
}
let mount_cwd = mount_finder.find_path(cwd, false)?;
let sysroot = mount_finder.find_mount_path(sysroot);
Ok(Directories {
cargo,
xargo,
target,
nix_store,
host_root,
mount_root,
mount_cwd,
sysroot,
})
}
}
const CACHEDIR_TAG: &str = "Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cross.
# For information about cache directory tags see https://bford.info/cachedir/";
fn create_target_dir(path: &Path) -> Result<()> {
// cargo creates all paths to the target directory, and writes
// a cache dir tag only if the path doesn't previously exist.
if !path.exists() {
fs::create_dir_all(&path)?;
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path.join("CACHEDIR.TAG"))?
.write_all(CACHEDIR_TAG.as_bytes())?;
}
Ok(())
}
pub fn command(engine: &Engine) -> Command {
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, cmd: &str) -> Command {
let mut command = command(engine);
command.arg(cmd);
command
}
pub fn get_package_info(
engine: &Engine,
target: &str,
channel: Option<&str>,
msg_info: &mut MessageInfo,
) -> Result<(Target, CargoMetadata, Directories)> {
let target_list = msg_info.as_quiet(rustc::target_list)?;
let target = Target::from(target, &target_list);
let metadata = cargo_metadata_with_args(None, None, msg_info)?
.ok_or(eyre::eyre!("unable to get project metadata"))?;
let cwd = std::env::current_dir()?;
let host_meta = rustc::version_meta()?;
let host = host_meta.host();
let sysroot = rustc::get_sysroot(&host, &target, channel, msg_info)?.1;
let mount_finder = MountFinder::create(engine)?;
let dirs = Directories::create(&mount_finder, &metadata, &cwd, &sysroot)?;
Ok((target, metadata, dirs))
}
/// Register binfmt interpreters
pub(crate) fn register(engine: &Engine, target: &Target, msg_info: &mut MessageInfo) -> Result<()> {
let cmd = if target.is_windows() {
// https://www.kernel.org/doc/html/latest/admin-guide/binfmt-misc.html
"mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc && \
echo ':wine:M::MZ::/usr/bin/run-detectors:' > /proc/sys/fs/binfmt_misc/register"
} else {
"apt-get update && apt-get install --no-install-recommends --assume-yes \
binfmt-support qemu-user-static"
};
let mut docker = subcommand(engine, "run");
docker_userns(&mut docker);
docker.arg("--privileged");
docker.arg("--rm");
docker.arg(UBUNTU_BASE);
docker.args(&["sh", "-c", cmd]);
docker.run(msg_info, false).map_err(Into::into)
}
fn validate_env_var(var: &str) -> Result<(&str, Option<&str>)> {
let (key, value) = match var.split_once('=') {
Some((key, value)) => (key, Some(value)),
_ => (var, None),
};
if key == "CROSS_RUNNER" {
eyre::bail!(
"CROSS_RUNNER environment variable name is reserved and cannot be pass through"
);
}
Ok((key, value))
}
pub fn parse_docker_opts(value: &str) -> Result<Vec<String>> {
shell_words::split(value).wrap_err_with(|| format!("could not parse docker opts of {}", value))
}
pub(crate) fn cargo_safe_command(uses_xargo: bool) -> SafeCommand {
if uses_xargo {
SafeCommand::new("xargo")
} else {
SafeCommand::new("cargo")
}
}
fn add_cargo_configuration_envvars(docker: &mut Command) {
let non_cargo_prefix = &[
"http_proxy",
"TERM",
"RUSTDOCFLAGS",
"RUSTFLAGS",
"BROWSER",
"HTTPS_PROXY",
"HTTP_TIMEOUT",
"https_proxy",
];
let cargo_prefix_skip = &[
"CARGO_HOME",
"CARGO_TARGET_DIR",
"CARGO_BUILD_TARGET_DIR",
"CARGO_BUILD_RUSTC",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER",
"CARGO_BUILD_RUSTDOC",
];
let is_cargo_passthrough = |key: &str| -> bool {
non_cargo_prefix.contains(&key)
|| key.starts_with("CARGO_") && !cargo_prefix_skip.contains(&key)
};
// also need to accept any additional flags used to configure
// cargo, but only pass what's actually present.
for (key, _) in env::vars() {
if is_cargo_passthrough(&key) {
docker.args(&["-e", &key]);
}
}
}
// NOTE: host path must be canonical
pub(crate) fn mount(docker: &mut Command, host_path: &Path, prefix: &str) -> Result<String> {
let mount_path = canonicalize_mount_path(host_path)?;
docker.args(&[
"-v",
&format!("{}:{prefix}{}", host_path.to_utf8()?, mount_path),
]);
Ok(mount_path)
}
pub(crate) fn docker_envvars(
docker: &mut Command,
config: &Config,
target: &Target,
msg_info: &mut MessageInfo,
) -> Result<()> {
for ref var in config.env_passthrough(target)?.unwrap_or_default() {
validate_env_var(var)?;
// Only specifying the environment variable name in the "-e"
// flag forwards the value from the parent shell
docker.args(&["-e", var]);
}
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]);
add_cargo_configuration_envvars(docker);
if let Some(username) = id::username().wrap_err("could not get username")? {
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() {
msg_info.warn("using both `CROSS_CONTAINER_OPTS` and `DOCKER_OPTS`.")?;
}
docker.args(&parse_docker_opts(&value)?);
} else if let Ok(value) = env::var("DOCKER_OPTS") {
// FIXME: remove this when we deprecate DOCKER_OPTS.
docker.args(&parse_docker_opts(&value)?);
};
Ok(())
}
pub(crate) fn docker_cwd(
docker: &mut Command,
paths: &DockerPaths,
mount_volumes: bool,
) -> Result<()> {
if mount_volumes {
docker.args(&["-w", paths.mount_cwd()]);
} else if paths.mount_cwd() == paths.workspace_root().to_utf8()? {
docker.args(&["-w", "/project"]);
} else {
// We do this to avoid clashes with path separators. Windows uses `\` as a path separator on Path::join
let working_dir = Path::new("/project").join(paths.workspace_from_cwd()?);
docker.args(&["-w", &working_dir.as_posix()?]);
}
Ok(())
}
pub(crate) fn docker_mount(
docker: &mut Command,
options: &DockerOptions,
paths: &DockerPaths,
mount_cb: impl Fn(&mut Command, &Path) -> Result<String>,
mut store_cb: impl FnMut((String, String)),
) -> Result<bool> {
let mut mount_volumes = false;
// FIXME(emilgardis 2022-04-07): This is a fallback so that if it's hard for us to do mounting logic, make it simple(r)
// Preferably we would not have to do this.
if !paths.in_workspace() {
mount_volumes = true;
}
for ref var in options
.config
.env_volumes(&options.target)?
.unwrap_or_default()
{
let (var, value) = validate_env_var(var)?;
let value = match value {
Some(v) => Ok(v.to_owned()),
None => env::var(var),
};
if let Ok(val) = value {
let canonical_val = file::canonicalize(&val)?;
let host_path = paths.mount_finder.find_path(&canonical_val, true)?;
let mount_path = mount_cb(docker, host_path.as_ref())?;
docker.args(&["-e", &format!("{}={}", host_path, mount_path)]);
store_cb((val, mount_path));
mount_volumes = true;
}
}
for path in paths.workspace_dependencies() {
let canonical_path = file::canonicalize(path)?;
let host_path = paths.mount_finder.find_path(&canonical_path, true)?;
let mount_path = mount_cb(docker, host_path.as_ref())?;
store_cb((path.to_utf8()?.to_owned(), mount_path));
mount_volumes = true;
}
Ok(mount_volumes)
}
pub(crate) fn canonicalize_mount_path(path: &Path) -> Result<String> {
#[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.
path.as_wslpath()
}
#[cfg(not(target_os = "windows"))]
{
path.to_utf8().map(|p| p.to_owned())
}
}
pub(crate) fn user_id() -> String {
env::var("CROSS_CONTAINER_UID").unwrap_or_else(|_| id::user().to_string())
}
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) {
// by default, docker runs as root so we need to specify the user
// so the resulting file permissions are for the current user.
// since we can have rootless docker, we provide an override.
let is_rootless = env::var("CROSS_ROOTLESS_CONTAINER_ENGINE")
.ok()
.and_then(|s| match s.as_ref() {
"auto" => None,
b => Some(bool_from_envvar(b)),
})
.unwrap_or_else(|| engine_type != EngineType::Docker);
if !is_rootless {
docker.args(&["--user", &format!("{}:{}", user_id(), group_id(),)]);
}
}
pub(crate) fn docker_userns(docker: &mut Command) {
let userns = match env::var("CROSS_CONTAINER_USER_NAMESPACE").ok().as_deref() {
Some("none") => None,
None | Some("auto") => Some("host".to_owned()),
Some(ns) => Some(ns.to_owned()),
};
if let Some(ns) = userns {
docker.args(&["--userns", &ns]);
}
}
#[allow(unused_mut, clippy::let_and_return)]
pub(crate) fn docker_seccomp(
docker: &mut Command,
engine_type: EngineType,
target: &Target,
metadata: &CargoMetadata,
) -> Result<()> {
// docker uses seccomp now on all installations
if target.needs_docker_seccomp() {
let seccomp = if engine_type == EngineType::Docker && cfg!(target_os = "windows") {
// docker on windows fails due to a bug in reading the profile
// https://github.com/docker/for-win/issues/12760
"unconfined".to_owned()
} else {
#[allow(unused_mut)] // target_os = "windows"
let mut path = metadata
.target_directory
.join(target.triple())
.join("seccomp.json");
if !path.exists() {
write_file(&path, false)?.write_all(SECCOMP.as_bytes())?;
}
let mut path_string = path.to_utf8()?.to_owned();
#[cfg(target_os = "windows")]
if matches!(engine_type, EngineType::Podman | EngineType::PodmanRemote) {
// podman weirdly expects a WSL path here, and fails otherwise
path_string = path.as_wslpath()?;
}
path_string
};
docker.args(&["--security-opt", &format!("seccomp={}", seccomp)]);
}
Ok(())
}
pub(crate) fn image_name(config: &Config, target: &Target) -> Result<String> {
if let Some(image) = config.image(target)? {
return Ok(image);
}
if !DOCKER_IMAGES.contains(&target.triple()) {
eyre::bail!(
"`cross` does not provide a Docker image for target {target}, \
specify a custom image in `Cross.toml`."
);
}
let version = if include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")).is_empty() {
env!("CARGO_PKG_VERSION")
} else {
"main"
};
Ok(format!("{CROSS_IMAGE}/{target}:{version}"))
}
fn docker_read_mount_paths(engine: &Engine) -> Result<Vec<MountDetail>> {
let hostname = env::var("HOSTNAME").wrap_err("HOSTNAME environment variable not found")?;
let mut docker: Command = {
let mut command = subcommand(engine, "inspect");
command.arg(hostname);
command
};
let output = docker.run_and_get_stdout(&mut Verbosity::Quiet.into())?;
let info = serde_json::from_str(&output).wrap_err("failed to parse docker inspect output")?;
dockerinfo_parse_mounts(&info)
}
fn dockerinfo_parse_mounts(info: &serde_json::Value) -> Result<Vec<MountDetail>> {
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<MountDetail> {
let driver_name = info
.pointer("/0/GraphDriver/Name")
.and_then(|v| v.as_str())
.ok_or_else(|| eyre::eyre!("no driver name found"))?;
if driver_name == "overlay2" {
let path = info
.pointer("/0/GraphDriver/Data/MergedDir")
.and_then(|v| v.as_str())
.ok_or_else(|| eyre::eyre!("No merge directory found"))?;
Ok(MountDetail {
source: PathBuf::from(&path),
destination: PathBuf::from("/"),
})
} else {
eyre::bail!("want driver overlay2, got {driver_name}")
}
}
fn dockerinfo_parse_user_mounts(info: &serde_json::Value) -> Vec<MountDetail> {
info.pointer("/0/Mounts")
.and_then(|v| v.as_array())
.map_or_else(Vec::new, |v| {
let make_path = |v: &serde_json::Value| {
PathBuf::from(&v.as_str().expect("docker mount should be defined"))
};
let mut mounts = vec![];
for details in v {
let source = make_path(&details["Source"]);
let destination = make_path(&details["Destination"]);
mounts.push(MountDetail {
source,
destination,
});
}
mounts
})
}
#[derive(Debug, Default)]
pub struct MountFinder {
mounts: Vec<MountDetail>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct MountDetail {
source: PathBuf,
destination: PathBuf,
}
impl MountFinder {
fn new(mounts: Vec<MountDetail>) -> 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 }
}
pub fn create(engine: &Engine) -> Result<MountFinder> {
Ok(if engine.in_docker {
MountFinder::new(docker_read_mount_paths(engine)?)
} else {
MountFinder::default()
})
}
pub fn find_mount_path(&self, path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
for info in &self.mounts {
if let Ok(stripped) = path.strip_prefix(&info.destination) {
return info.source.join(stripped);
}
}
path.to_path_buf()
}
#[allow(unused_variables, clippy::needless_return)]
fn find_path(&self, path: &Path, host: bool) -> Result<String> {
#[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.
if host {
return Ok(path.to_utf8()?.to_owned());
} else {
return path.as_wslpath();
}
}
#[cfg(not(target_os = "windows"))]
{
return Ok(self.find_mount_path(path).to_utf8()?.to_owned());
}
}
}
fn path_digest(path: &Path) -> Result<const_sha1::Digest> {
let buffer = const_sha1::ConstBuffer::from_slice(path.to_utf8()?.as_bytes());
Ok(const_sha1::sha1(&buffer))
}
pub fn path_hash(path: &Path) -> Result<String> {
Ok(path_digest(path)?
.to_string()
.get(..5)
.expect("sha1 is expected to be at least 5 characters long")
.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::id;
#[test]
fn test_docker_user_id() {
let var = "CROSS_ROOTLESS_CONTAINER_ENGINE";
let old = env::var(var);
env::remove_var(var);
let rootful = format!("\"engine\" \"--user\" \"{}:{}\"", id::user(), id::group());
let rootless = "\"engine\"".to_owned();
let test = |engine, expected| {
let mut cmd = Command::new("engine");
docker_user_id(&mut cmd, engine);
assert_eq!(expected, &format!("{cmd:?}"));
};
test(EngineType::Docker, &rootful);
test(EngineType::Podman, &rootless);
test(EngineType::PodmanRemote, &rootless);
test(EngineType::Other, &rootless);
env::set_var(var, "0");
test(EngineType::Docker, &rootful);
test(EngineType::Podman, &rootful);
test(EngineType::PodmanRemote, &rootful);
test(EngineType::Other, &rootful);
env::set_var(var, "1");
test(EngineType::Docker, &rootless);
test(EngineType::Podman, &rootless);
test(EngineType::PodmanRemote, &rootless);
test(EngineType::Other, &rootless);
env::set_var(var, "auto");
test(EngineType::Docker, &rootful);
test(EngineType::Podman, &rootless);
test(EngineType::PodmanRemote, &rootless);
test(EngineType::Other, &rootless);
match old {
Ok(v) => env::set_var(var, v),
Err(_) => env::remove_var(var),
}
}
#[test]
fn test_docker_userns() {
let var = "CROSS_CONTAINER_USER_NAMESPACE";
let old = env::var(var);
env::remove_var(var);
let host = "\"engine\" \"--userns\" \"host\"".to_owned();
let custom = "\"engine\" \"--userns\" \"custom\"".to_owned();
let none = "\"engine\"".to_owned();
let test = |expected| {
let mut cmd = Command::new("engine");
docker_userns(&mut cmd);
assert_eq!(expected, &format!("{cmd:?}"));
};
test(&host);
env::set_var(var, "auto");
test(&host);
env::set_var(var, "none");
test(&none);
env::set_var(var, "host");
test(&host);
env::set_var(var, "custom");
test(&custom);
match old {
Ok(v) => env::set_var(var, v),
Err(_) => env::remove_var(var),
}
}
mod directories {
use super::*;
use crate::cargo::cargo_metadata_with_args;
use crate::temp;
fn unset_env() -> Vec<(&'static str, Option<String>)> {
let mut result = vec![];
let envvars = ["CARGO_HOME", "XARGO_HOME", "NIX_STORE"];
for var in envvars {
result.push((var, env::var(var).ok()));
env::remove_var(var);
}
result
}
fn reset_env(vars: Vec<(&'static str, Option<String>)>) {
for (var, value) in vars {
if let Some(value) = value {
env::set_var(var, value);
}
}
}
fn create_engine(msg_info: &mut MessageInfo) -> Result<Engine> {
Engine::from_path(get_container_engine()?, None, Some(false), msg_info)
}
fn cargo_metadata(subdir: bool, msg_info: &mut MessageInfo) -> Result<CargoMetadata> {
let mut metadata = cargo_metadata_with_args(
Some(Path::new(env!("CARGO_MANIFEST_DIR"))),
None,
msg_info,
)?
.ok_or_else(|| eyre::eyre!("could not find cross workspace"))?;
let root = match subdir {
true => get_cwd()?.join("member"),
false => get_cwd()?
.parent()
.expect("current directory should have a parent")
.to_path_buf(),
};
fs::create_dir_all(&root)?;
metadata.workspace_root = root;
metadata.target_directory = metadata.workspace_root.join("target");
Ok(metadata)
}
fn home() -> Result<PathBuf> {
home::home_dir().ok_or_else(|| eyre::eyre!("could not find home directory"))
}
fn get_cwd() -> Result<PathBuf> {
// we need this directory to exist for Windows
let path = temp::dir()?.join("Documents").join("package");
fs::create_dir_all(&path)?;
Ok(path)
}
fn get_sysroot() -> Result<PathBuf> {
Ok(home()?
.join(".rustup")
.join("toolchains")
.join("stable-x86_64-unknown-linux-gnu"))
}
fn get_directories(
metadata: &CargoMetadata,
mount_finder: &MountFinder,
) -> Result<Directories> {
let cwd = get_cwd()?;
let sysroot = get_sysroot()?;
Directories::create(mount_finder, metadata, &cwd, &sysroot)
}
fn path_to_posix(path: &Path) -> Result<String> {
#[cfg(target_os = "windows")]
{
path.as_wslpath()
}
#[cfg(not(target_os = "windows"))]
{
path.as_posix()
}
}
#[track_caller]
fn paths_equal(x: &Path, y: &Path) -> Result<()> {
assert_eq!(path_to_posix(x)?, path_to_posix(y)?);
Ok(())
}
#[test]
fn test_host() -> Result<()> {
let vars = unset_env();
let mount_finder = MountFinder::new(vec![]);
let metadata = cargo_metadata(false, &mut MessageInfo::default())?;
let directories = get_directories(&metadata, &mount_finder)?;
paths_equal(&directories.cargo, &home()?.join(".cargo"))?;
paths_equal(&directories.xargo, &home()?.join(".xargo"))?;
paths_equal(&directories.host_root, &metadata.workspace_root)?;
assert_eq!(
&directories.mount_root,
&path_to_posix(&metadata.workspace_root)?
);
assert_eq!(&directories.mount_cwd, &path_to_posix(&get_cwd()?)?);
reset_env(vars);
Ok(())
}
#[test]
#[cfg_attr(not(target_os = "linux"), ignore)]
fn test_docker_in_docker() -> Result<()> {
let vars = unset_env();
let mut msg_info = MessageInfo::default();
let engine = create_engine(&mut msg_info);
let hostname = env::var("HOSTNAME");
if engine.is_err() || hostname.is_err() {
eprintln!("could not get container engine or no hostname found");
reset_env(vars);
return Ok(());
}
let engine = engine.unwrap();
if !engine.in_docker {
eprintln!("not in docker");
reset_env(vars);
return Ok(());
}
let hostname = hostname.unwrap();
let output = subcommand(&engine, "inspect")
.arg(hostname)
.run_and_get_output(&mut msg_info)?;
if !output.status.success() {
eprintln!("inspect failed");
reset_env(vars);
return Ok(());
}
let mount_finder = MountFinder::create(&engine)?;
let metadata = cargo_metadata(true, &mut msg_info)?;
let directories = get_directories(&metadata, &mount_finder)?;
let mount_finder = MountFinder::new(docker_read_mount_paths(&engine)?);
let mount_path = |p| mount_finder.find_mount_path(p);
paths_equal(&directories.cargo, &mount_path(home()?.join(".cargo")))?;
paths_equal(&directories.xargo, &mount_path(home()?.join(".xargo")))?;
paths_equal(&directories.host_root, &mount_path(get_cwd()?))?;
assert_eq!(
&directories.mount_root,
&path_to_posix(&mount_path(get_cwd()?))?
);
assert_eq!(
&directories.mount_cwd,
&path_to_posix(&mount_path(get_cwd()?))?
);
reset_env(vars);
Ok(())
}
}
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("/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("/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("/container/path")
);
assert_eq!(
PathBuf::from("/home/project/path"),
finder.find_mount_path("/project")
);
assert_eq!(
PathBuf::from("/home/project/path/target"),
finder.find_mount_path("/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_empty_user_mounts() {
let actual = dockerinfo_parse_user_mounts(&json!([{
"Mounts": [],
}]));
assert_eq!(Vec::<MountDetail>::new(), actual);
}
#[test]
fn test_parse_missing_user_moutns() {
let actual = dockerinfo_parse_user_mounts(&json!([{
"Id": "test",
}]));
assert_eq!(Vec::<MountDetail>::new(), actual);
}
}
}