diff --git a/src/build_context.rs b/src/build_context.rs index 5ae1d773d..36fa12607 100644 --- a/src/build_context.rs +++ b/src/build_context.rs @@ -42,13 +42,13 @@ use tracing::instrument; use zip::DateTime; /// Unpacks an sdist tarball into a temporary directory and returns the path -/// to the Cargo.toml inside it, along with the tempdir handle (which must -/// be kept alive for the duration of the build). +/// to the Cargo.toml and pyproject.toml inside it, along with the tempdir +/// handle (which must be kept alive for the duration of the build). /// /// The Cargo.toml path is resolved by checking `[tool.maturin.manifest-path]` /// in the sdist's `pyproject.toml`, falling back to `Cargo.toml` at the /// sdist root directory. -pub fn unpack_sdist(sdist_path: &Path) -> Result<(tempfile::TempDir, PathBuf)> { +pub fn unpack_sdist(sdist_path: &Path) -> Result<(tempfile::TempDir, PathBuf, PathBuf)> { let tmp = tempfile::tempdir().context("Failed to create temporary directory")?; let gz = flate2::read::GzDecoder::new( fs::File::open(sdist_path) @@ -66,7 +66,11 @@ pub fn unpack_sdist(sdist_path: &Path) -> Result<(tempfile::TempDir, PathBuf)> { .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) .collect(); let top_dir = match entries.len() { - 1 => entries[0].path(), + // Canonicalize to resolve symlinks (e.g. /var -> /private/var on macOS). + // Without this, `project_root` and `python_dir` may disagree after + // `normalize()` is applied to only some paths, causing python source + // files to be silently excluded from wheels. + 1 => dunce::canonicalize(entries[0].path()).unwrap_or_else(|_| entries[0].path()), n => bail!( "Expected exactly one top-level directory in sdist, found {}", n @@ -92,7 +96,7 @@ pub fn unpack_sdist(sdist_path: &Path) -> Result<(tempfile::TempDir, PathBuf)> { cargo_toml.display() ); } - Ok((tmp, cargo_toml)) + Ok((tmp, cargo_toml, pyproject_file)) } /// Contains all the metadata required to build the crate diff --git a/src/build_options.rs b/src/build_options.rs index a7327be28..ab59db2c0 100644 --- a/src/build_options.rs +++ b/src/build_options.rs @@ -610,6 +610,7 @@ pub struct BuildContextBuilder { strip: Option, editable: bool, sdist_only: bool, + pyproject_toml_path: Option, } impl BuildContextBuilder { @@ -619,6 +620,7 @@ impl BuildContextBuilder { strip: None, editable: false, sdist_only: false, + pyproject_toml_path: None, } } @@ -637,12 +639,18 @@ impl BuildContextBuilder { self } + pub fn pyproject_toml_path(mut self, path: Option) -> Self { + self.pyproject_toml_path = path; + self + } + pub fn build(self) -> Result { let Self { build_options, strip, editable, sdist_only, + pyproject_toml_path: explicit_pyproject_path, } = self; build_options.compression.validate(); let ProjectResolver { @@ -660,6 +668,7 @@ impl BuildContextBuilder { build_options.manifest_path.clone(), build_options.cargo.clone(), editable, + explicit_pyproject_path, )?; let pyproject = pyproject_toml.as_ref(); diff --git a/src/ci.rs b/src/ci.rs index 6db8d2150..f0d5b7e48 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -151,7 +151,7 @@ impl GenerateCI { pyproject_toml, project_layout, .. - } = ProjectResolver::resolve(self.manifest_path.clone(), cargo_options, false)?; + } = ProjectResolver::resolve(self.manifest_path.clone(), cargo_options, false, None)?; let pyproject = pyproject_toml.as_ref(); let extra_pyo3_features = crate::build_options::pyo3_features_from_conditional(pyproject); let bridge = find_bridge( diff --git a/src/main.rs b/src/main.rs index 9e5b1431c..a1ad0b620 100644 --- a/src/main.rs +++ b/src/main.rs @@ -421,24 +421,28 @@ fn run() -> Result<()> { } // Keep tempdir alive for the duration of the build let _sdist_tmp; + let sdist_pyproject_path; if sdist { // Build sdist first, then build wheels from the unpacked sdist // to verify that the source distribution is complete. let sdist_path = build_sdist(&build, strip)?; - let (tmp, cargo_toml) = unpack_sdist(&sdist_path)?; + let (tmp, cargo_toml, pyproject_toml) = unpack_sdist(&sdist_path)?; _sdist_tmp = Some(tmp); eprintln!( "📦 Building wheels from source distribution at {}", cargo_toml.parent().unwrap().display() ); build.cargo.manifest_path = Some(cargo_toml); + sdist_pyproject_path = Some(pyproject_toml); } else { _sdist_tmp = None; + sdist_pyproject_path = None; } let build_context = build .into_build_context() .strip(strip) .editable(false) + .pyproject_toml_path(sdist_pyproject_path) .build()?; let wheels = build_context.build_wheels()?; assert!(!wheels.is_empty()); @@ -461,25 +465,29 @@ fn run() -> Result<()> { // Keep tempdir alive for the duration of the build let _sdist_tmp; let mut sdist_path = None; + let sdist_pyproject_path; if !no_sdist { // Build sdist first, then build wheels from the unpacked sdist let path = build_sdist(&build, Some(!no_strip))?; - let (tmp, cargo_toml) = unpack_sdist(&path)?; + let (tmp, cargo_toml, pyproject_toml) = unpack_sdist(&path)?; _sdist_tmp = Some(tmp); eprintln!( "📦 Building wheels from source distribution at {}", cargo_toml.parent().unwrap().display() ); build.cargo.manifest_path = Some(cargo_toml); + sdist_pyproject_path = Some(pyproject_toml); sdist_path = Some(path); } else { _sdist_tmp = None; + sdist_pyproject_path = None; } let mut build_context = build .into_build_context() .strip(Some(!no_strip)) .editable(false) + .pyproject_toml_path(sdist_pyproject_path) .build()?; // ensure profile always set when publishing diff --git a/src/project_layout.rs b/src/project_layout.rs index 3a8ded107..88f804c9a 100644 --- a/src/project_layout.rs +++ b/src/project_layout.rs @@ -63,9 +63,29 @@ impl ProjectResolver { cargo_manifest_path: Option, mut cargo_options: CargoOptions, editable_install: bool, + pyproject_toml_path: Option, ) -> Result { - let (manifest_file, pyproject_file) = - Self::resolve_manifest_paths(cargo_manifest_path, &cargo_options)?; + let (manifest_file, pyproject_file) = if let Some(pyproject_path) = pyproject_toml_path { + // When an explicit pyproject.toml path is provided (e.g. from an + // unpacked sdist), use it directly instead of discovering it by + // walking up from the manifest. This is needed when the Cargo + // crate is excluded from the workspace and the pyproject.toml + // lives outside the cargo workspace boundary. + let cargo_toml = cargo_manifest_path + .expect("manifest_path must be set when pyproject_toml_path is provided"); + let cargo_toml = cargo_toml + .normalize() + .with_context(|| { + format!( + "manifest path `{}` does not exist or is invalid", + cargo_toml.display() + ) + })? + .into_path_buf(); + (cargo_toml, pyproject_path) + } else { + Self::resolve_manifest_paths(cargo_manifest_path, &cargo_options)? + }; if !manifest_file.is_file() { bail!( "{} is not the path to a Cargo.toml", diff --git a/tests/common/other.rs b/tests/common/other.rs index 1a3bff6e5..2e2804949 100644 --- a/tests/common/other.rs +++ b/tests/common/other.rs @@ -562,7 +562,7 @@ pub fn test_build_wheels_from_sdist(package: impl AsRef, unique_name: &str .context("Failed to build source distribution")?; // Step 2: Unpack sdist and build wheels from it - let (_tmp, cargo_toml) = unpack_sdist(&sdist_path)?; + let (_tmp, cargo_toml, pyproject_toml) = unpack_sdist(&sdist_path)?; let wheel_options = BuildOptions { out: Some(wheel_dir), cargo: CargoOptions { @@ -579,6 +579,7 @@ pub fn test_build_wheels_from_sdist(package: impl AsRef, unique_name: &str .into_build_context() .strip(Some(cfg!(feature = "faster-tests"))) .editable(false) + .pyproject_toml_path(Some(pyproject_toml)) .build()?; let wheels = wheel_context.build_wheels()?; assert!(