From 93abfc97ef802166c7556f7240cc08af4b4a7ef8 Mon Sep 17 00:00:00 2001 From: Bben01 <52465698+Bben01@users.noreply.github.com> Date: Fri, 1 May 2026 11:21:53 +0300 Subject: [PATCH] Restore symlinked Python source traversal Follow symlinked directories when collecting Python package files for wheels and source distributions. This restores support for layouts that share Python code through symlinked package directories instead of duplicating files. --- src/module_writer/mod.rs | 1 + src/source_distribution/pyproject.rs | 2 +- tests/run/wheel.rs | 57 ++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/module_writer/mod.rs b/src/module_writer/mod.rs index 9cf136fe5..3a58e0a46 100644 --- a/src/module_writer/mod.rs +++ b/src/module_writer/mod.rs @@ -137,6 +137,7 @@ pub fn write_python_part( .parents(false) .git_global(false) .git_exclude(false) + .follow_links(true) .build() { let absolute = match absolute { diff --git a/src/source_distribution/pyproject.rs b/src/source_distribution/pyproject.rs index 0e0cb6fef..551a7d253 100644 --- a/src/source_distribution/pyproject.rs +++ b/src/source_distribution/pyproject.rs @@ -72,7 +72,7 @@ pub(super) fn add_python_sources( } for package in python_packages { - for entry in ignore::Walk::new(package) { + for entry in ignore::WalkBuilder::new(package).follow_links(true).build() { let source = entry?.into_path(); if is_compiled_artifact(&source) { debug!("Ignoring {}", source.display()); diff --git a/tests/run/wheel.rs b/tests/run/wheel.rs index 520b7a089..265a35353 100644 --- a/tests/run/wheel.rs +++ b/tests/run/wheel.rs @@ -1,4 +1,5 @@ use crate::common::{handle_result, other}; +use std::path::Path; #[test] #[cfg(feature = "sbom")] @@ -57,6 +58,62 @@ fn pyo3_mixed_py_subdir_include_wheel_files() { )) } +#[test] +#[cfg(unix)] +fn pyo3_mixed_py_subdir_includes_symlinked_python_files() { + use std::os::unix::fs::symlink; + + handle_result((|| { + let temp_dir = tempfile::tempdir()?; + let project_dir = temp_dir.path().join("pyo3-mixed-py-subdir"); + other::copy_dir_recursive(Path::new("test-crates/pyo3-mixed-py-subdir"), &project_dir)?; + + let external_sources = temp_dir.path().join("external-python"); + fs_err::create_dir_all(external_sources.join("linked_dir"))?; + fs_err::write(external_sources.join("linked_file.py"), "VALUE = 1\n")?; + fs_err::write( + external_sources.join("linked_dir").join("nested.py"), + "VALUE = 2\n", + )?; + + let package_dir = project_dir + .join("python") + .join("pyo3_mixed_py_subdir") + .join("python_module"); + symlink( + external_sources.join("linked_file.py"), + package_dir.join("linked_file.py"), + )?; + symlink( + external_sources.join("linked_dir"), + package_dir.join("linked_dir"), + )?; + + let mut expected = vec![ + "assets/extra_data.txt", + "pyo3_mixed_py_subdir-2.1.3.dist-info/METADATA", + "pyo3_mixed_py_subdir-2.1.3.dist-info/RECORD", + "pyo3_mixed_py_subdir-2.1.3.dist-info/WHEEL", + "pyo3_mixed_py_subdir-2.1.3.dist-info/entry_points.txt", + "pyo3_mixed_py_subdir/__init__.py", + "pyo3_mixed_py_subdir/python_module/__init__.py", + "pyo3_mixed_py_subdir/python_module/double.py", + "pyo3_mixed_py_subdir/python_module/linked_dir/nested.py", + "pyo3_mixed_py_subdir/python_module/linked_file.py", + ]; + #[cfg(feature = "sbom")] + expected + .push("pyo3_mixed_py_subdir-2.1.3.dist-info/sboms/pyo3-mixed-py-subdir.cyclonedx.json"); + + assert_eq!( + other::wheel_files(&project_dir, "wheel-files-pyo3-mixed-py-subdir-symlinks")?, + expected.into_iter().map(str::to_owned).collect() + ); + + Ok(()) + })()) +} + #[test] fn pyo3_wheel_record_has_normalized_paths() { handle_result(other::check_wheel_paths(