use std::path::{Component, Path, PathBuf};
use anyhow::Context;
use path_absolutize::Absolutize;
use pathdiff::diff_paths;
pub fn resolve_output_path(
out_dir: &Path,
raw_inputs: &[PathBuf],
file_path: &Path,
file_extension: Option<&str>,
strip_leading_paths: bool,
) -> anyhow::Result<PathBuf> {
let out_dir = out_dir.absolutize()?.into_owned();
let mut relative_path =
resolve_relative_output_path(raw_inputs, file_path, strip_leading_paths)?;
if let Some(file_extension) = file_extension {
relative_path.set_extension(file_extension);
}
Ok(out_dir.join(relative_path))
}
fn resolve_relative_output_path(
raw_inputs: &[PathBuf],
file_path: &Path,
strip_leading_paths: bool,
) -> anyhow::Result<PathBuf> {
let file_path = file_path.absolutize()?.into_owned();
if let Some(relative_to_input_dir) = resolve_relative_to_input_dir(raw_inputs, &file_path)? {
return Ok(finalize_relative_path(
&file_path,
relative_to_input_dir,
strip_leading_paths,
));
}
let cwd = std::env::current_dir()?;
let cwd_relative = diff_paths(&file_path, &cwd).with_context(|| {
format!(
"failed to make {} relative to {}",
file_path.display(),
cwd.display()
)
})?;
let base_path = if file_path.is_absolute() && starts_with_parent_component(&cwd_relative) {
fallback_relative_path(&file_path)?
} else {
cwd_relative
};
Ok(finalize_relative_path(
&file_path,
base_path,
strip_leading_paths,
))
}
fn resolve_relative_to_input_dir(
raw_inputs: &[PathBuf],
file_path: &Path,
) -> anyhow::Result<Option<PathBuf>> {
let input_dir = match raw_inputs.iter().find(|path| path.is_dir()) {
Some(input_dir) => input_dir,
None => return Ok(None),
};
let input_dir = input_dir.absolutize()?.into_owned();
let stripped = match file_path.strip_prefix(&input_dir) {
Ok(stripped) => stripped,
Err(_) => return Ok(None),
};
let cwd = std::env::current_dir()?.absolutize()?.into_owned();
let mut relative_path = PathBuf::new();
if input_dir != cwd {
if let Some(dir_name) = input_dir.file_name() {
relative_path.push(dir_name);
}
}
if !stripped.as_os_str().is_empty() {
relative_path.push(stripped);
}
Ok(Some(relative_path))
}
fn finalize_relative_path(
file_path: &Path,
base_path: PathBuf,
strip_leading_paths: bool,
) -> PathBuf {
let mut relative_path = sanitize_relative_path(base_path, strip_leading_paths);
if relative_path.as_os_str().is_empty() {
relative_path = fallback_file_name(file_path);
}
relative_path
}
fn sanitize_relative_path(path: PathBuf, strip_leading_paths: bool) -> PathBuf {
let mut sanitized = PathBuf::new();
let mut stripped_first_component = !strip_leading_paths;
for component in path.components() {
match component {
Component::Normal(component) => {
if !stripped_first_component {
stripped_first_component = true;
continue;
}
sanitized.push(component);
}
Component::CurDir
| Component::ParentDir
| Component::RootDir
| Component::Prefix(_) => {
}
}
}
sanitized
}
fn fallback_relative_path(file_path: &Path) -> anyhow::Result<PathBuf> {
let mut path = PathBuf::new();
if let Some(parent_name) = file_path.parent().and_then(Path::file_name) {
path.push(parent_name);
}
path.push(
file_path
.file_name()
.context("Filename should be available for output path fallback")?,
);
Ok(path)
}
fn fallback_file_name(file_path: &Path) -> PathBuf {
file_path
.file_name()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("unknown"))
}
fn starts_with_parent_component(path: &Path) -> bool {
matches!(path.components().next(), Some(Component::ParentDir))
}