use crate::{
errors::{
ArchiveExtractError, ArchiveReadError, MetadataMaterializeError, PathMapperConstructError,
PathMapperConstructKind,
},
list::BinaryList,
platform::PlatformLibdir,
};
use camino::{Utf8Path, Utf8PathBuf};
use camino_tempfile::Utf8TempDir;
use guppy::graph::PackageGraph;
use nextest_metadata::{BinaryListSummary, PlatformLibdirUnavailable};
use std::{fmt, fs, io, sync::Arc};
use tracing::debug;
mod archive_reporter;
mod archiver;
mod unarchiver;
pub use archive_reporter::*;
pub use archiver::*;
pub use unarchiver::*;
pub const CARGO_METADATA_FILE_NAME: &str = "target/nextest/cargo-metadata.json";
pub const BINARIES_METADATA_FILE_NAME: &str = "target/nextest/binaries-metadata.json";
pub const LIBDIRS_BASE_DIR: &str = "target/nextest/libdirs";
#[derive(Debug, Default)]
pub struct ReuseBuildInfo {
pub cargo_metadata: Option<MetadataWithRemap<ReusedCargoMetadata>>,
pub binaries_metadata: Option<MetadataWithRemap<ReusedBinaryList>>,
pub build_dir_remap: Option<Utf8PathBuf>,
pub libdir_mapper: LibdirMapper,
_temp_dir: Option<Utf8TempDir>,
}
impl ReuseBuildInfo {
pub fn new(
cargo_metadata: Option<MetadataWithRemap<ReusedCargoMetadata>>,
binaries_metadata: Option<MetadataWithRemap<ReusedBinaryList>>,
build_dir_remap: Option<Utf8PathBuf>,
) -> Self {
Self {
cargo_metadata,
binaries_metadata,
build_dir_remap,
libdir_mapper: LibdirMapper::default(),
_temp_dir: None,
}
}
pub fn extract_archive<F>(
archive_file: &Utf8Path,
format: ArchiveFormat,
dest: ExtractDestination,
callback: F,
workspace_remap: Option<&Utf8Path>,
) -> Result<Self, ArchiveExtractError>
where
F: for<'e> FnMut(ArchiveEvent<'e>) -> io::Result<()>,
{
let mut file = fs::File::open(archive_file)
.map_err(|err| ArchiveExtractError::Read(ArchiveReadError::Io(err)))?;
let mut unarchiver = Unarchiver::new(&mut file, format);
let ExtractInfo {
dest_dir,
temp_dir,
binary_list,
cargo_metadata_json,
graph,
libdir_mapper,
} = unarchiver.extract(dest, callback)?;
let cargo_metadata = MetadataWithRemap {
metadata: ReusedCargoMetadata::new((cargo_metadata_json, graph)),
remap: workspace_remap.map(|p| p.to_owned()),
};
let binaries_metadata = MetadataWithRemap {
metadata: ReusedBinaryList::new(binary_list),
remap: Some(dest_dir.join("target")),
};
Ok(Self {
cargo_metadata: Some(cargo_metadata),
binaries_metadata: Some(binaries_metadata),
build_dir_remap: None,
libdir_mapper,
_temp_dir: temp_dir,
})
}
pub fn cargo_metadata(&self) -> Option<&ReusedCargoMetadata> {
self.cargo_metadata.as_ref().map(|m| &m.metadata)
}
pub fn binaries_metadata(&self) -> Option<&ReusedBinaryList> {
self.binaries_metadata.as_ref().map(|m| &m.metadata)
}
#[inline]
pub fn is_active(&self) -> bool {
self.cargo_metadata.is_some() || self.binaries_metadata.is_some()
}
pub fn workspace_remap(&self) -> Option<&Utf8Path> {
self.cargo_metadata
.as_ref()
.and_then(|m| m.remap.as_deref())
}
pub fn target_dir_remap(&self) -> Option<&Utf8Path> {
self.binaries_metadata
.as_ref()
.and_then(|m| m.remap.as_deref())
}
pub fn build_dir_remap(&self) -> Option<&Utf8Path> {
self.build_dir_remap.as_deref()
}
}
#[derive(Clone, Debug)]
pub struct MetadataWithRemap<T> {
pub metadata: T,
pub remap: Option<Utf8PathBuf>,
}
pub trait MetadataKind: Clone + fmt::Debug {
type MetadataType: Sized;
fn new(metadata: Self::MetadataType) -> Self;
fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError>;
}
#[derive(Clone, Debug)]
pub struct ReusedBinaryList {
pub binary_list: Arc<BinaryList>,
}
impl MetadataKind for ReusedBinaryList {
type MetadataType = BinaryList;
fn new(binary_list: Self::MetadataType) -> Self {
Self {
binary_list: Arc::new(binary_list),
}
}
fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError> {
let contents =
fs::read_to_string(path).map_err(|error| MetadataMaterializeError::Read {
path: path.to_owned(),
error,
})?;
let summary: BinaryListSummary = serde_json::from_str(&contents).map_err(|error| {
MetadataMaterializeError::Deserialize {
path: path.to_owned(),
error,
}
})?;
let binary_list = BinaryList::from_summary(summary).map_err(|error| {
MetadataMaterializeError::RustBuildMeta {
path: path.to_owned(),
error: Box::new(error),
}
})?;
Ok(Self::new(binary_list))
}
}
#[derive(Clone, Debug)]
pub struct ReusedCargoMetadata {
pub json: Arc<String>,
pub graph: Arc<PackageGraph>,
}
impl MetadataKind for ReusedCargoMetadata {
type MetadataType = (String, PackageGraph);
fn new((json, graph): Self::MetadataType) -> Self {
Self {
json: Arc::new(json),
graph: Arc::new(graph),
}
}
fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError> {
let json =
std::fs::read_to_string(path).map_err(|error| MetadataMaterializeError::Read {
path: path.to_owned(),
error,
})?;
let graph = PackageGraph::from_json(&json).map_err(|error| {
MetadataMaterializeError::PackageGraphConstruct {
path: path.to_owned(),
error: Box::new(error),
}
})?;
Ok(Self::new((json, graph)))
}
}
#[derive(Clone, Debug, Default)]
pub struct PathMapper {
workspace: Option<(Utf8PathBuf, Utf8PathBuf)>,
target_dir: Option<(Utf8PathBuf, Utf8PathBuf)>,
build_dir: Option<(Utf8PathBuf, Utf8PathBuf)>,
libdir_mapper: LibdirMapper,
}
impl PathMapper {
pub fn new(
orig_workspace_root: impl Into<Utf8PathBuf>,
workspace_remap: Option<&Utf8Path>,
orig_target_dir: impl Into<Utf8PathBuf>,
target_dir_remap: Option<&Utf8Path>,
orig_build_dir: impl Into<Utf8PathBuf>,
build_dir_remap: Option<&Utf8Path>,
libdir_mapper: LibdirMapper,
) -> Result<Self, PathMapperConstructError> {
let workspace_root = workspace_remap
.map(|root| Self::canonicalize_dir(root, PathMapperConstructKind::WorkspaceRoot))
.transpose()?;
let target_dir = target_dir_remap
.map(|dir| Self::canonicalize_dir(dir, PathMapperConstructKind::TargetDir))
.transpose()?;
let build_dir = build_dir_remap
.map(|dir| Self::canonicalize_dir(dir, PathMapperConstructKind::BuildDir))
.transpose()?;
Ok(Self {
workspace: workspace_root.map(|w| (orig_workspace_root.into(), w)),
target_dir: target_dir.map(|d| (orig_target_dir.into(), d)),
build_dir: build_dir.map(|d| (orig_build_dir.into(), d)),
libdir_mapper,
})
}
pub fn noop() -> Self {
Self {
workspace: None,
target_dir: None,
build_dir: None,
libdir_mapper: LibdirMapper::default(),
}
}
pub fn libdir_mapper(&self) -> &LibdirMapper {
&self.libdir_mapper
}
fn canonicalize_dir(
input: &Utf8Path,
kind: PathMapperConstructKind,
) -> Result<Utf8PathBuf, PathMapperConstructError> {
let canonicalized_path =
input
.canonicalize()
.map_err(|err| PathMapperConstructError::Canonicalization {
kind,
input: input.into(),
err,
})?;
let canonicalized_path: Utf8PathBuf =
canonicalized_path
.try_into()
.map_err(|err| PathMapperConstructError::NonUtf8Path {
kind,
input: input.into(),
err,
})?;
if !canonicalized_path.is_dir() {
return Err(PathMapperConstructError::NotADirectory {
kind,
input: input.into(),
canonicalized_path,
});
}
Ok(os_imp::strip_verbatim(canonicalized_path))
}
pub fn new_workspace_root(&self) -> Option<&Utf8Path> {
self.workspace.as_ref().map(|(_, new)| new.as_path())
}
pub(super) fn new_target_dir(&self) -> Option<&Utf8Path> {
self.target_dir.as_ref().map(|(_, new)| new.as_path())
}
pub(super) fn new_build_dir(&self) -> Option<&Utf8Path> {
self.build_dir.as_ref().map(|(_, new)| new.as_path())
}
pub(crate) fn map_cwd(&self, path: Utf8PathBuf) -> Utf8PathBuf {
Self::remap_path(self.workspace.as_ref(), path)
}
pub(crate) fn map_target_path(&self, path: Utf8PathBuf) -> Utf8PathBuf {
Self::remap_path(self.target_dir.as_ref(), path)
}
pub(crate) fn map_build_path(&self, path: Utf8PathBuf) -> Utf8PathBuf {
Self::remap_path(self.build_dir.as_ref(), path)
}
fn remap_path(mapping: Option<&(Utf8PathBuf, Utf8PathBuf)>, path: Utf8PathBuf) -> Utf8PathBuf {
match mapping {
Some((from, to)) => match path.strip_prefix(from) {
Ok(p) if !p.as_str().is_empty() => to.join(p),
Ok(_) => to.clone(),
Err(_) => {
debug!(
target: "nextest-runner::reuse_build",
"path `{path}` does not start with remap prefix `{from}`, \
returning unchanged",
);
path
}
},
None => path,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct LibdirMapper {
pub(crate) host: PlatformLibdirMapper,
pub(crate) target: PlatformLibdirMapper,
}
#[derive(Clone, Debug, Default)]
pub(crate) enum PlatformLibdirMapper {
Path(Utf8PathBuf),
Unavailable,
#[default]
NotRequested,
}
impl PlatformLibdirMapper {
pub(crate) fn map(&self, original: &PlatformLibdir) -> PlatformLibdir {
match self {
PlatformLibdirMapper::Path(new) => {
PlatformLibdir::Available(new.clone())
}
PlatformLibdirMapper::Unavailable => {
PlatformLibdir::Unavailable(PlatformLibdirUnavailable::NOT_IN_ARCHIVE)
}
PlatformLibdirMapper::NotRequested => original.clone(),
}
}
}
#[cfg(windows)]
mod os_imp {
use camino::Utf8PathBuf;
use std::ptr;
use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
pub(super) fn strip_verbatim(path: Utf8PathBuf) -> Utf8PathBuf {
let path_str = String::from(path);
if path_str.starts_with(r"\\?\UNC") {
path_str.into()
} else if path_str.starts_with(r"\\?\") {
const START_LEN: usize = r"\\?\".len();
let is_absolute_exact = {
let mut v = path_str[START_LEN..].encode_utf16().collect::<Vec<u16>>();
v.push(0);
is_absolute_exact(&v)
};
if is_absolute_exact {
path_str[START_LEN..].into()
} else {
path_str.into()
}
} else {
path_str.into()
}
}
fn is_absolute_exact(path: &[u16]) -> bool {
if path.is_empty() || path.len() > u32::MAX as usize || path.last() != Some(&0) {
return false;
}
let buffer_len = path.len();
let mut new_path = Vec::with_capacity(buffer_len);
let result = unsafe {
GetFullPathNameW(
path.as_ptr(),
new_path.capacity() as u32,
new_path.as_mut_ptr(),
ptr::null_mut(),
)
};
if result == 0 || result as usize != buffer_len - 1 {
false
} else {
unsafe {
new_path.set_len((result as usize) + 1);
}
path == new_path
}
}
}
#[cfg(unix)]
mod os_imp {
use camino::Utf8PathBuf;
pub(super) fn strip_verbatim(path: Utf8PathBuf) -> Utf8PathBuf {
path
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{list::RustBuildMeta, platform::BuildPlatforms};
#[test]
fn test_path_mapper_relative() {
let current_dir: Utf8PathBuf = std::env::current_dir()
.expect("current dir obtained")
.try_into()
.expect("current dir is valid UTF-8");
let temp_workspace_root = Utf8TempDir::new().expect("new temp dir created");
let workspace_root_path: Utf8PathBuf = os_imp::strip_verbatim(
temp_workspace_root
.path()
.canonicalize()
.expect("workspace root canonicalized correctly")
.try_into()
.expect("workspace root is valid UTF-8"),
);
let rel_workspace_root = pathdiff::diff_utf8_paths(&workspace_root_path, ¤t_dir)
.expect("abs to abs diff is non-None");
let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
let target_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
temp_target_dir
.path()
.canonicalize()
.expect("target dir canonicalized correctly")
.try_into()
.expect("target dir is valid UTF-8"),
);
let rel_target_dir = pathdiff::diff_utf8_paths(&target_dir_path, ¤t_dir)
.expect("abs to abs diff is non-None");
let temp_build_dir = Utf8TempDir::new().expect("new temp dir created");
let build_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
temp_build_dir
.path()
.canonicalize()
.expect("build dir canonicalized correctly")
.try_into()
.expect("build dir is valid UTF-8"),
);
let rel_build_dir = pathdiff::diff_utf8_paths(&build_dir_path, ¤t_dir)
.expect("abs to abs diff is non-None");
let orig_workspace_root = Utf8PathBuf::from(
std::env::var("NEXTEST_WORKSPACE_ROOT")
.expect("NEXTEST_WORKSPACE_ROOT is set (running under cargo nextest run)"),
);
let orig_target_dir = orig_workspace_root.join("target");
let orig_build_dir = orig_workspace_root.join("build");
let path_mapper = PathMapper::new(
orig_workspace_root.as_path(),
Some(&rel_workspace_root),
&orig_target_dir,
Some(&rel_target_dir),
&orig_build_dir,
Some(&rel_build_dir),
LibdirMapper::default(),
)
.expect("remapped paths exist");
assert_eq!(
path_mapper.map_cwd(orig_workspace_root.join("foobar")),
workspace_root_path.join("foobar")
);
assert_eq!(
path_mapper.map_target_path(orig_target_dir.join("foobar")),
target_dir_path.join("foobar")
);
assert_eq!(
path_mapper.map_build_path(orig_build_dir.join("foobar")),
build_dir_path.join("foobar")
);
}
#[test]
fn test_path_mapper_separate_build_dir() {
let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
let target_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
temp_target_dir
.path()
.canonicalize()
.expect("target dir canonicalized correctly")
.try_into()
.expect("target dir is valid UTF-8"),
);
let temp_build_dir = Utf8TempDir::new().expect("new temp dir created");
let build_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
temp_build_dir
.path()
.canonicalize()
.expect("build dir canonicalized correctly")
.try_into()
.expect("build dir is valid UTF-8"),
);
let orig_workspace_root = Utf8PathBuf::from(
std::env::var("NEXTEST_WORKSPACE_ROOT")
.expect("NEXTEST_WORKSPACE_ROOT is set (running under cargo nextest run)"),
);
let orig_target_dir = orig_workspace_root.join("target");
let orig_build_dir = orig_workspace_root.join("build");
let path_mapper = PathMapper::new(
orig_workspace_root.as_path(),
None,
&orig_target_dir,
Some(target_dir_path.as_path()),
&orig_build_dir,
Some(build_dir_path.as_path()),
LibdirMapper::default(),
)
.expect("remapped paths exist");
assert_eq!(
path_mapper.map_target_path(orig_target_dir.join("debug/mybin")),
target_dir_path.join("debug/mybin"),
);
assert_eq!(
path_mapper.map_build_path(orig_build_dir.join("debug/deps/test-abc123")),
build_dir_path.join("debug/deps/test-abc123"),
);
let unrelated = Utf8PathBuf::from("/some/other/path");
assert_eq!(path_mapper.map_target_path(unrelated.clone()), unrelated,);
assert_eq!(path_mapper.map_build_path(unrelated.clone()), unrelated,);
}
#[test]
fn test_map_paths_build_dir_fallback() {
let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
let target_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
temp_target_dir
.path()
.canonicalize()
.expect("target dir canonicalized correctly")
.try_into()
.expect("target dir is valid UTF-8"),
);
let orig_workspace_root = Utf8PathBuf::from(
std::env::var("NEXTEST_WORKSPACE_ROOT")
.expect("NEXTEST_WORKSPACE_ROOT is set (running under cargo nextest run)"),
);
let orig_target_dir = orig_workspace_root.join("target");
let orig_build_dir = orig_workspace_root.join("build");
let path_mapper = PathMapper::new(
orig_workspace_root.as_path(),
None,
&orig_target_dir,
Some(target_dir_path.as_path()),
&orig_build_dir,
None,
LibdirMapper::default(),
)
.expect("remapped paths exist");
assert_eq!(path_mapper.new_build_dir(), None);
assert_eq!(
path_mapper.new_target_dir(),
Some(target_dir_path.as_path()),
);
let build_platforms = BuildPlatforms::new_with_no_target()
.expect("creating BuildPlatforms without target triple succeeds");
let meta = RustBuildMeta::new(&orig_target_dir, &orig_build_dir, build_platforms);
let mapped = meta.map_paths(&path_mapper);
assert_eq!(mapped.target_directory, target_dir_path);
assert_eq!(
mapped.build_directory, target_dir_path,
"build_directory should fall back to the target_dir remap",
);
}
}