use crate::metadata::Output;
use fs_err as fs;
use std::{
collections::HashSet,
path::{Component, Path, PathBuf},
};
use super::PackagingError;
pub fn filter_pyc(path: &Path, old_files: &HashSet<PathBuf>) -> bool {
if let (Some(ext), Some(parent)) = (path.extension(), path.parent())
&& ext == "pyc"
{
let has_pycache = parent.ends_with("__pycache__");
let pyfile = if has_pycache {
let stem = path
.file_name()
.expect("unreachable as extension doesn't exist without filename")
.to_string_lossy()
.to_string();
let py_stem = stem.rsplitn(3, '.').last().unwrap_or_default();
if let Some(pp) = parent.parent() {
pp.join(format!("{}.py", py_stem))
} else {
return false;
}
} else {
path.with_extension("py")
};
if old_files.contains(&pyfile) {
return true;
}
}
false
}
pub fn filter_file(relative_path: &Path) -> bool {
let ext = relative_path.extension().unwrap_or_default();
if relative_path.starts_with("share/info/dir") {
return true;
}
if ext == "pyo" {
return true;
}
if ext == "la" {
return true;
}
if relative_path.components().any(|c| {
let s = c.as_os_str().to_string_lossy();
s == ".DS_Store" || s == "conda-meta" || s == "CACHEDIR.TAG"
}) {
return true;
}
false
}
impl Output {
pub fn write_to_dest(
&self,
path: &Path,
prefix: &Path,
dest_folder: &Path,
) -> Result<Option<PathBuf>, PackagingError> {
let target_platform = &self.build_configuration.target_platform;
let entry_points = &self.recipe.build().python.entry_points;
let path_rel = path.strip_prefix(prefix)?;
if filter_file(path_rel) {
return Ok(None);
}
let mut dest_path = dest_folder.join(path_rel);
let ext = path.extension().unwrap_or_default();
if ext == "py" || ext == "pyc" {
let so_path = path.with_extension("so");
let pyd_path = path.with_extension("pyd");
if so_path.exists() || pyd_path.exists() {
return Ok(None);
}
}
if self.is_python_version_independent() {
if path_rel.starts_with("bin") {
if let Some(name) = path_rel.file_name()
&& entry_points
.iter()
.any(|ep| ep.command == name.to_string_lossy())
{
return Ok(None);
}
}
else if path_rel.starts_with("Scripts")
&& let Some(name) = path_rel.file_name()
&& entry_points.iter().any(|ep| {
format!("{}.exe", ep.command) == name.to_string_lossy()
|| format!("{}-script.py", ep.command) == name.to_string_lossy()
})
{
return Ok(None);
}
if ["pyc", "egg-info", "pyo"].iter().any(|s| ext.eq(*s)) {
return Ok(None); }
if path_rel
.components()
.any(|c| c == Component::Normal("__pycache__".as_ref()))
{
return Ok(None);
}
if path_rel
.components()
.any(|c| c == Component::Normal("site-packages".as_ref()))
{
let pat = std::path::Component::Normal("site-packages".as_ref());
let parts = path_rel.components();
let mut new_parts = Vec::new();
let mut found = false;
for part in parts {
if part == pat {
found = true;
}
if found {
new_parts.push(part);
}
}
dest_path = dest_folder.join(PathBuf::from_iter(new_parts));
} else if path_rel.starts_with("bin") || path_rel.starts_with("Scripts") {
let mut new_parts = path_rel.components().collect::<Vec<_>>();
new_parts[0] = Component::Normal("python-scripts".as_ref());
if let Some(Component::Normal(name)) = new_parts.last_mut()
&& let Some(name_str) = name.to_str()
&& target_platform.is_windows()
&& let Some(stripped_suffix) = name_str.strip_suffix("-script.py")
{
*name = stripped_suffix.as_ref();
}
dest_path = dest_folder.join(PathBuf::from_iter(new_parts));
} else {
dest_path = dest_folder.join(path_rel);
}
}
match dest_path.parent() {
Some(parent) => {
if fs::metadata(parent).is_err() {
fs::create_dir_all(parent)?;
}
}
None => {
return Err(PackagingError::IoError(std::io::Error::other(
"Could not get parent directory",
)));
}
}
let metadata = fs::symlink_metadata(path)?;
if metadata.file_type().is_symlink() {
if target_platform.is_windows() {
tracing::warn!("Symlink creation on Windows requires administrator privileges");
}
match fs::read_link(path) {
Ok(mut target) => {
if target.is_absolute() && target.starts_with(prefix) {
if let Some(parent) = path.parent()
&& let Some(rel) = pathdiff::diff_paths(&target, parent)
{
target = rel;
}
} else if target.is_absolute() {
tracing::warn!(
"Symlink {path:?} points to absolute path {target:?} outside of the $PREFIX",
);
}
#[cfg(unix)]
{
if let Err(e) = fs_err::os::unix::fs::symlink(&target, &dest_path) {
tracing::warn!(
"Failed to create symlink {:?} -> {:?}: {:?}",
dest_path,
target,
e
);
}
}
#[cfg(windows)]
{
let res = if target.is_dir() {
fs_err::os::windows::fs::symlink_dir(&target, &dest_path)
} else {
fs_err::os::windows::fs::symlink_file(&target, &dest_path)
};
if let Err(e) = res {
tracing::warn!(
"Failed to create symlink {:?} -> {:?}: {:?}",
dest_path,
target,
e
);
}
}
}
Err(e) => {
tracing::warn!("Failed to read symlink {:?}: {:?}", path, e);
}
}
Ok(Some(dest_path))
} else if metadata.is_dir() {
Ok(None)
} else {
tracing::trace!("Copying file {:?} to {:?}", path, dest_path);
fs::copy(path, &dest_path)?;
Ok(Some(dest_path))
}
}
}
#[cfg(test)]
mod test {
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use crate::packaging::file_mapper::filter_pyc;
#[test]
fn test_filter_file() {
let test_cases = vec![
("test.pyo", true),
("test.la", true),
(".DS_Store", true),
(".gitignore", false),
(".git/HEAD", false),
(".github/workflows/foo.yml", false),
("foo/.DS_Store", true),
("lib/libarchive.la", true),
("bla/.git/config", false),
("share/info/dir", true),
("share/info/dir/foo", true),
("lib/python3.9/site-packages/test/fast.pyo", true),
("lib/python3.9/site-packages/test/fast.py", false),
("lib/python3.9/site-packages/test/fast.pyc", false),
("lib/libarchive.a", false),
("lib/libarchive.so", false),
];
for (file, expected) in test_cases {
let path = std::path::Path::new(file);
assert_eq!(
super::filter_file(path),
expected,
"Failed for file: {}",
file
);
}
}
#[test]
fn test_filter_pyc() {
let mut old_files = HashSet::new();
old_files.insert(PathBuf::from("pkg/module.py"));
old_files.insert(PathBuf::from("pkg/other.py"));
old_files.insert(PathBuf::from("pkg/nested/deep.py"));
let test_cases = vec![
("pkg/__pycache__/module.cpython-311.pyc", true), ("pkg/__pycache__/missing.cpython-311.pyc", false), ("pkg/module.pyc", true), ("pkg/missing.pyc", false), ("pkg/nested/__pycache__/deep.cpython-311.pyc", true),
("pkg/nested/deep.pyc", true),
("pkg/not_python.txt", false), ("pkg/__pycache__/invalid", false), ("", false), ];
for (file, expected) in test_cases {
let path = Path::new(file);
assert_eq!(
filter_pyc(path, &old_files),
expected,
"Failed for path: {}",
file
);
}
}
}