diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e881f3e..43c7bd17f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +- #494 - Parse Cargo's --manifest-path option to determine mounted docker root - #629 - Update Android NDK version and API version - #681 - Warn on unknown fields and confusable targets - #665 - when not using [env.volumes](https://github.com/cross-rs/cross#mounting-volumes-into-the-build-environment), mount project in /project diff --git a/README.md b/README.md index 9ae5418a5..e3f4215b9 100644 --- a/README.md +++ b/README.md @@ -355,6 +355,9 @@ $ QEMU_STRACE=1 cross run --target aarch64-unknown-linux-gnu - path dependencies (in Cargo.toml) that point outside the Cargo project won't work because `cross` use docker containers only mounts the Cargo project so the container doesn't have access to the rest of the filesystem. + However, you may use Cargo's `--manifest-path` option to reference your + target crate, executed from a common root directory from which all your + dependencies are available. ## Minimum Supported Rust Version (MSRV) diff --git a/ci/test.sh b/ci/test.sh index 9931fdda5..137830a60 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -135,6 +135,21 @@ EOF popd rm -rf "${td}" + td=$(mktemp -d) + git clone \ + --depth 1 \ + --recursive \ + https://github.com/cross-rs/test-workspace "${td}" + + pushd "${td}" + cross run --target "${TARGET}" -p binary --manifest-path="./workspace/Cargo.toml" + pushd "workspace" + cross run --target "${TARGET}" -p binary + pushd "binary" + cross run --target "${TARGET}" + popd + popd + popd ;; esac diff --git a/src/cargo.rs b/src/cargo.rs index 14e9585e6..9cd902880 100644 --- a/src/cargo.rs +++ b/src/cargo.rs @@ -1,7 +1,8 @@ +use serde::Deserialize; use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus}; -use std::{env, fs}; +use crate::cli::Args; use crate::errors::*; use crate::extensions::CommandExt; @@ -52,38 +53,96 @@ impl<'a> From<&'a str> for Subcommand { } } -#[derive(Debug)] -pub struct Root { - path: PathBuf, +#[derive(Debug, Deserialize)] +pub struct CargoMetadata { + pub workspace_root: PathBuf, + pub target_directory: PathBuf, + pub packages: Vec, + pub workspace_members: Vec, } -impl Root { - pub fn path(&self) -> &Path { - &self.path +impl CargoMetadata { + fn non_workspace_members(&self) -> impl Iterator { + self.packages + .iter() + .filter(|p| !self.workspace_members.iter().any(|m| m == &p.id)) } -} -/// Cargo project root -pub fn root() -> Result> { - let cd = env::current_dir().wrap_err("couldn't get current directory")?; + pub fn path_dependencies(&self) -> impl Iterator { + // TODO: Also filter out things that are in workspace, but not a workspace member + self.non_workspace_members().filter_map(|p| p.crate_path()) + } +} - let mut dir = &*cd; - loop { - let toml = dir.join("Cargo.toml"); +#[derive(Debug, Deserialize)] +pub struct Package { + id: String, + manifest_path: PathBuf, + source: Option, +} - if fs::metadata(&toml).is_ok() { - return Ok(Some(Root { - path: dir.to_owned(), - })); +impl Package { + /// Returns the absolute path to the packages manifest "folder" + fn crate_path(&self) -> Option<&Path> { + // when source is none, this package is a path dependency or a workspace member + if self.source.is_none() { + self.manifest_path.parent() + } else { + None } + } +} - match dir.parent() { - Some(p) => dir = p, - None => break, +/// Cargo metadata with specific invocation +pub fn cargo_metadata_with_args( + cd: Option<&Path>, + args: Option<&Args>, +) -> Result> { + let mut command = std::process::Command::new( + std::env::var("CARGO") + .ok() + .unwrap_or_else(|| "cargo".to_string()), + ); + command.arg("metadata").arg("--format-version=1"); + if let Some(cd) = cd { + command.current_dir(cd); + } + if let Some(config) = args { + if let Some(ref manifest_path) = config.manifest_path { + command.args(["--manifest-path".as_ref(), manifest_path.as_os_str()]); } + } else { + command.arg("--no-deps"); } - - Ok(None) + if let Some(target) = args.and_then(|a| a.target.as_ref()) { + command.args(["--filter-platform", target.triple()]); + } + let output = command.output()?; + let manifest: Option = + serde_json::from_slice(&output.stdout).wrap_err_with(|| { + format!( + "{command:?} returned nothing. {:?}", + String::from_utf8(output.stderr) + ) + })?; + manifest + .map(|m| -> Result<_> { + Ok(CargoMetadata { + target_directory: args + .and_then(|a| a.target_dir.clone()) + .map(|p| { + if p.is_relative() { + cd.expect("this is a bug, working directory should be provided here") + .join(p) + } else { + p + } + }) + .unwrap_or(m.target_directory), + ..m + }) + }) + .transpose() } /// Pass-through mode diff --git a/src/cli.rs b/src/cli.rs index f62ee82de..91c0c83f0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,11 +13,13 @@ pub struct Args { pub target: Option, pub target_dir: Option, pub docker_in_docker: bool, + pub manifest_path: Option, } pub fn parse(target_list: &TargetList) -> Args { let mut channel = None; let mut target = None; + let mut manifest_path: Option = None; let mut target_dir = None; let mut sc = None; let mut all: Vec = Vec::new(); @@ -28,7 +30,21 @@ pub fn parse(target_list: &TargetList) -> Args { if arg.is_empty() { continue; } - if let ("+", ch) = arg.split_at(1) { + if arg == "--manifest-path" { + all.push(arg); + if let Some(m) = args.next() { + let p = PathBuf::from(&m); + all.push(m); + manifest_path = env::current_dir().ok().map(|cwd| cwd.join(p)); + } + } else if arg.starts_with("--manifest-path=") { + manifest_path = arg + .split_once('=') + .map(|x| x.1) + .map(PathBuf::from) + .and_then(|p| env::current_dir().ok().map(|cwd| cwd.join(p))); + all.push(arg); + } else if let ("+", ch) = arg.split_at(1) { channel = Some(ch.to_string()); } else if arg == "--target" { all.push(arg); @@ -73,5 +89,6 @@ pub fn parse(target_list: &TargetList) -> Args { target, target_dir, docker_in_docker, + manifest_path, } } diff --git a/src/docker.rs b/src/docker.rs index d9e0ba394..e1e90f965 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -2,10 +2,10 @@ use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus}; use std::{env, fs}; -use crate::cargo::Root; -use crate::errors::*; +use crate::cargo::CargoMetadata; use crate::extensions::{CommandExt, SafeCommand}; use crate::id; +use crate::{errors::*, file}; use crate::{Config, Target}; use atty::Stream; use eyre::bail; @@ -52,17 +52,38 @@ pub fn register(target: &Target, verbose: bool) -> Result<()> { .run(verbose) } +#[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:?}`"))?; + 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(); + } + cmd.args(&[ + "-v", + &format!("{}:{}", host_path.display(), mount_path.display()), + ]); + Ok(mount_path) +} + #[allow(clippy::too_many_arguments)] // TODO: refactor pub fn run( target: &Target, args: &[String], - target_dir: &Option, - root: &Root, + metadata: &CargoMetadata, config: &Config, uses_xargo: bool, sysroot: &Path, verbose: bool, docker_in_docker: bool, + cwd: &Path, ) -> Result { let mount_finder = if docker_in_docker { MountFinder::new(docker_read_mount_paths()?) @@ -70,14 +91,13 @@ pub fn run( MountFinder::default() }; - let root = root.path(); 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 = target_dir.clone().unwrap_or_else(|| root.join("target")); + 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` @@ -89,7 +109,12 @@ pub fn run( 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 host_root = mount_finder.find_mount_path(root); + // 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")] { @@ -98,7 +123,17 @@ pub fn run( } #[cfg(not(target_os = "windows"))] { - mount_root = host_root.clone(); + 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); @@ -133,38 +168,28 @@ pub fn run( // flag forwards the value from the parent shell docker.args(&["-e", var]); } - let mut env_volumes = false; + let mut mount_volumes = false; + // FIXME(emilgardis 2022-04-07): This is a fallback so that if it's hard for use to do mounting logic, make it simple(r) + // Preferably we would not have to do this. + if cwd.strip_prefix(&metadata.workspace_root).is_err() { + mount_volumes = true; + } + for ref var in config.env_volumes(target)? { validate_env_var(var)?; if let Ok(val) = env::var(var) { - let host_path: PathBuf; - let mount_path: PathBuf; - - #[cfg(target_os = "windows")] - { - // Docker does not support UNC paths, this will try to not use UNC paths - host_path = dunce::canonicalize(&val) - .wrap_err_with(|| format!("when canonicalizing path `{val}`"))?; - // 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"))] - { - host_path = Path::new(&val) - .canonicalize() - .wrap_err_with(|| format!("when canonicalizing path `{val}`"))?; - mount_path = host_path.clone(); - } - docker.args(&[ - "-v", - &format!("{}:{}", host_path.display(), mount_path.display()), - ]); + let mount_path = mount(&mut docker, val.as_ref(), verbose)?; docker.args(&["-e", &format!("{}={}", var, mount_path.display())]); - env_volumes = true; + mount_volumes = true; } } + for path in metadata.path_dependencies() { + mount(&mut docker, path, verbose)?; + mount_volumes = true; + } + docker.args(&["-e", "PKG_CONFIG_ALLOW_CROSS=1"]); docker.arg("--rm"); @@ -218,7 +243,7 @@ pub fn run( .args(&["-v", &format!("{}:/cargo:Z", cargo_dir.display())]) // Prevent `bin` from being mounted inside the Docker container. .args(&["-v", "/cargo/bin"]); - if env_volumes { + if mount_volumes { docker.args(&[ "-v", &format!("{}:{}:Z", host_root.display(), mount_root.display()), @@ -230,10 +255,21 @@ pub fn run( .args(&["-v", &format!("{}:/rust:Z,ro", sysroot.display())]) .args(&["-v", &format!("{}:/target:Z", target_dir.display())]); - if env_volumes { - docker.args(&["-w", &mount_root.display().to_string()]); - } else { + 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 diff --git a/src/file.rs b/src/file.rs index 2386cef53..f8a0181ef 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,6 +1,6 @@ use std::fs::File; use std::io::Read; -use std::path::Path; +use std::path::{Path, PathBuf}; use crate::errors::*; @@ -19,3 +19,19 @@ fn read_(path: &Path) -> Result { .wrap_err_with(|| format!("couldn't read {}", path.display()))?; Ok(s) } + +pub fn canonicalize(path: impl AsRef) -> Result { + _canonicalize(path.as_ref()) +} + +fn _canonicalize(path: &Path) -> Result { + #[cfg(target_os = "windows")] + { + // Docker does not support UNC paths, this will try to not use UNC paths + dunce::canonicalize(&path).map_err(Into::into) + } + #[cfg(not(target_os = "windows"))] + { + Path::new(&path).canonicalize().map_err(Into::into) + } +} diff --git a/src/main.rs b/src/main.rs index 15df03aff..bb24dcf03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ use std::process::ExitStatus; use config::Config; use serde::Deserialize; -use self::cargo::{Root, Subcommand}; +use self::cargo::{CargoMetadata, Subcommand}; use self::cross_toml::CrossToml; use self::errors::*; use self::rustc::{TargetList, VersionMetaExt}; @@ -279,9 +279,10 @@ fn run() -> Result { let version_meta = rustc_version::version_meta().wrap_err("couldn't fetch the `rustc` version")?; - if let Some(root) = cargo::root()? { + let cwd = std::env::current_dir()?; + if let Some(metadata) = cargo::cargo_metadata_with_args(Some(&cwd), Some(&args))? { let host = version_meta.host(); - let toml = toml(&root)?; + let toml = toml(&metadata)?; let config = Config::new(toml); let target = args .target @@ -389,13 +390,13 @@ fn run() -> Result { return docker::run( &target, &filtered_args, - &args.target_dir, - &root, + &metadata, &config, uses_xargo, &sysroot, verbose, args.docker_in_docker, + &cwd, ); } } @@ -406,10 +407,10 @@ fn run() -> Result { /// Parses the `Cross.toml` at the root of the Cargo project or from the /// `CROSS_CONFIG` environment variable (if any exist in either location). -fn toml(root: &Root) -> Result> { +fn toml(root: &CargoMetadata) -> Result> { let path = match env::var("CROSS_CONFIG") { Ok(var) => PathBuf::from(var), - Err(_) => root.path().join("Cross.toml"), + Err(_) => root.workspace_root.join("Cross.toml"), }; if path.exists() { @@ -422,7 +423,7 @@ fn toml(root: &Root) -> Result> { Ok(Some(config)) } else { // Checks if there is a lowercase version of this file - if root.path().join("cross.toml").exists() { + if root.workspace_root.join("cross.toml").exists() { eprintln!("There's a file named cross.toml, instead of Cross.toml. You may want to rename it, or it won't be considered."); } Ok(None) diff --git a/src/tests.rs b/src/tests.rs index 5fa299105..5c43f175d 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -6,7 +6,6 @@ use std::{ }; use once_cell::sync::OnceCell; -use serde::Deserialize; static WORKSPACE: OnceCell = OnceCell::new(); @@ -14,23 +13,10 @@ static WORKSPACE: OnceCell = OnceCell::new(); pub fn get_cargo_workspace() -> &'static Path { let manifest_dir = env!("CARGO_MANIFEST_DIR"); WORKSPACE.get_or_init(|| { - #[derive(Deserialize)] - struct Manifest { - workspace_root: PathBuf, - } - let output = std::process::Command::new( - std::env::var("CARGO") - .ok() - .unwrap_or_else(|| "cargo".to_string()), - ) - .arg("metadata") - .arg("--format-version=1") - .arg("--no-deps") - .current_dir(manifest_dir) - .output() - .unwrap(); - let manifest: Manifest = serde_json::from_slice(&output.stdout).unwrap(); - manifest.workspace_root + crate::cargo::cargo_metadata_with_args(Some(manifest_dir.as_ref()), None) + .unwrap() + .unwrap() + .workspace_root }) }