Skip to content

Commit 037442b

Browse files
bors[bot]messense
andauthored
Merge #1380
1380: Detect src-layout for pure Rust projects with multiple Python packages r=messense a=messense Follow-up of #1378, `python-packages` actually slightly changes the meaning of mixed Rust/Python project layout. Co-authored-by: messense <[email protected]>
2 parents c627d88 + 79f2c17 commit 037442b

4 files changed

+108
-89
lines changed

src/build_context.rs

+12-10
Original file line numberDiff line numberDiff line change
@@ -926,16 +926,18 @@ impl BuildContext {
926926
self.excludes(Format::Wheel)?,
927927
)?;
928928

929-
if let Some(python_module) = &self.project_layout.python_module {
930-
if self.target.is_wasi() {
931-
// TODO: Can we have python code and the wasm launchers coexisting
932-
// without clashes?
933-
bail!("Sorry, adding python code to a wasm binary is currently not supported")
934-
}
935-
if !self.editable {
936-
write_python_part(&mut writer, python_module, self.pyproject_toml.as_ref())
937-
.context("Failed to add the python module to the package")?;
938-
}
929+
if self.project_layout.python_module.is_some() && self.target.is_wasi() {
930+
// TODO: Can we have python code and the wasm launchers coexisting
931+
// without clashes?
932+
bail!("Sorry, adding python code to a wasm binary is currently not supported")
933+
}
934+
if !self.editable {
935+
write_python_part(
936+
&mut writer,
937+
&self.project_layout,
938+
self.pyproject_toml.as_ref(),
939+
)
940+
.context("Failed to add the python module to the package")?;
939941
}
940942

941943
let mut artifacts_ref = Vec::with_capacity(artifacts.len());

src/module_writer.rs

+30-33
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,9 @@ impl WheelWriter {
295295
project_layout: &ProjectLayout,
296296
metadata21: &Metadata21,
297297
) -> Result<()> {
298-
if let Some(python_module) = &project_layout.python_module {
299-
let absolute_path = python_module.normalize()?.into_path_buf();
300-
if let Some(python_path) = absolute_path.parent().and_then(|p| p.to_str()) {
298+
if project_layout.python_module.is_some() || !project_layout.python_packages.is_empty() {
299+
let absolute_path = project_layout.python_dir.normalize()?.into_path_buf();
300+
if let Some(python_path) = absolute_path.to_str() {
301301
let name = metadata21.get_distribution_escaped();
302302
let target = format!("{}.pth", name);
303303
debug!("Adding {} from {}", target, python_path);
@@ -720,6 +720,10 @@ pub fn write_bindings_module(
720720
}
721721
};
722722

723+
if !editable {
724+
write_python_part(writer, project_layout, pyproject_toml)
725+
.context("Failed to add the python module to the package")?;
726+
}
723727
if let Some(python_module) = &project_layout.python_module {
724728
if editable {
725729
let target = project_layout.rust_module.join(&so_filename);
@@ -735,9 +739,6 @@ pub fn write_bindings_module(
735739
target.display()
736740
))?;
737741
} else {
738-
write_python_part(writer, python_module, pyproject_toml)
739-
.context("Failed to add the python module to the package")?;
740-
741742
let relative = project_layout
742743
.rust_module
743744
.strip_prefix(python_module.parent().unwrap())
@@ -789,14 +790,13 @@ pub fn write_cffi_module(
789790
) -> Result<()> {
790791
let cffi_declarations = generate_cffi_declarations(crate_dir, target_dir, python)?;
791792

792-
let module;
793+
if !editable {
794+
write_python_part(writer, project_layout, pyproject_toml)
795+
.context("Failed to add the python module to the package")?;
796+
}
793797

798+
let module;
794799
if let Some(python_module) = &project_layout.python_module {
795-
if !editable {
796-
write_python_part(writer, python_module, pyproject_toml)
797-
.context("Failed to add the python module to the package")?;
798-
}
799-
800800
if editable {
801801
let base_path = python_module.join(module_name);
802802
fs::create_dir_all(&base_path)?;
@@ -956,14 +956,13 @@ pub fn write_uniffi_module(
956956
} = generate_uniffi_bindings(crate_dir, target_dir, target_os)?;
957957
let py_init = format!("from .{} import * # NOQA\n", binding_name);
958958

959-
let module;
959+
if !editable {
960+
write_python_part(writer, project_layout, pyproject_toml)
961+
.context("Failed to add the python module to the package")?;
962+
}
960963

964+
let module;
961965
if let Some(python_module) = &project_layout.python_module {
962-
if !editable {
963-
write_python_part(writer, python_module, pyproject_toml)
964-
.context("Failed to add the python module to the package")?;
965-
}
966-
967966
if editable {
968967
let base_path = python_module.join(module_name);
969968
fs::create_dir_all(&base_path)?;
@@ -1095,28 +1094,26 @@ if __name__ == '__main__':
10951094
/// Adds the python part of a mixed project to the writer,
10961095
pub fn write_python_part(
10971096
writer: &mut impl ModuleWriter,
1098-
python_module: impl AsRef<Path>,
1097+
project_layout: &ProjectLayout,
10991098
pyproject_toml: Option<&PyProjectToml>,
11001099
) -> Result<()> {
1101-
let python_module = python_module.as_ref().to_path_buf();
1102-
let python_dir = python_module.parent().unwrap().to_path_buf();
1103-
let mut python_packages = vec![python_module];
1104-
if let Some(pyproject_toml) = pyproject_toml {
1105-
if let Some(packages) = pyproject_toml.python_packages() {
1106-
for package in packages {
1107-
let package_path = python_dir.join(package);
1108-
if python_packages.iter().any(|p| *p == package_path) {
1109-
continue;
1110-
}
1111-
python_packages.push(package_path);
1112-
}
1100+
let python_dir = &project_layout.python_dir;
1101+
let mut python_packages = Vec::new();
1102+
if let Some(python_module) = project_layout.python_module.as_ref() {
1103+
python_packages.push(python_module.to_path_buf());
1104+
}
1105+
for package in &project_layout.python_packages {
1106+
let package_path = python_dir.join(package);
1107+
if python_packages.iter().any(|p| *p == package_path) {
1108+
continue;
11131109
}
1110+
python_packages.push(package_path);
11141111
}
11151112

11161113
for package in python_packages {
11171114
for absolute in WalkBuilder::new(package).hidden(false).build() {
11181115
let absolute = absolute?.into_path();
1119-
let relative = absolute.strip_prefix(&python_dir).unwrap();
1116+
let relative = absolute.strip_prefix(python_dir).unwrap();
11201117
if absolute.is_dir() {
11211118
writer.add_directory(relative)?;
11221119
} else {
@@ -1137,7 +1134,7 @@ pub fn write_python_part(
11371134
// Include additional files
11381135
if let Some(pyproject) = pyproject_toml {
11391136
// FIXME: in src-layout pyproject.toml isn't located directly in python dir
1140-
let pyproject_dir = &python_dir;
1137+
let pyproject_dir = python_dir;
11411138
if let Some(glob_patterns) = pyproject.include() {
11421139
for pattern in glob_patterns
11431140
.iter()

src/project_layout.rs

+37-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::{CargoToml, Metadata21, PyProjectToml};
33
use anyhow::{bail, format_err, Context, Result};
44
use cargo_metadata::{Metadata, MetadataCommand};
55
use normpath::PathExt as _;
6+
use std::collections::HashSet;
67
use std::env;
78
use std::io;
89
use std::path::{Path, PathBuf};
@@ -19,6 +20,8 @@ pub struct ProjectLayout {
1920
/// If none, we have a rust crate compiled into a shared library with only some glue python for cffi
2021
/// If some, we have a python package that is extended by a native rust module.
2122
pub python_module: Option<PathBuf>,
23+
/// Python packages to include
24+
pub python_packages: Vec<String>,
2225
/// Contains the canonicalized (i.e. absolute) path to the rust part of the project
2326
pub rust_module: PathBuf,
2427
/// Rust extension name
@@ -137,19 +140,30 @@ impl ProjectResolver {
137140
} else {
138141
manifest_dir
139142
};
143+
let python_packages = pyproject
144+
.and_then(|x| x.python_packages())
145+
.unwrap_or_default()
146+
.to_vec();
140147
let py_root = match pyproject.and_then(|x| x.python_source()) {
141148
Some(py_src) => project_root.join(py_src),
142149
None => match pyproject.and_then(|x| x.project_name()) {
143150
Some(project_name) => {
144151
// Detect src layout
145-
let import_name = project_name.replace('-', "_");
146152
let rust_cargo_toml_found =
147153
project_root.join("rust").join("Cargo.toml").is_file();
148-
let python_src_found = project_root
149-
.join("src")
150-
.join(import_name)
151-
.join("__init__.py")
152-
.is_file();
154+
let import_name = project_name.replace('-', "_");
155+
let mut package_init = HashSet::new();
156+
package_init.insert(
157+
project_root
158+
.join("src")
159+
.join(import_name)
160+
.join("__init__.py"),
161+
);
162+
for package in &python_packages {
163+
package_init
164+
.insert(project_root.join("src").join(package).join("__init__.py"));
165+
}
166+
let python_src_found = package_init.iter().any(|x| x.is_file());
153167
if rust_cargo_toml_found && python_src_found {
154168
project_root.join("src")
155169
} else {
@@ -176,7 +190,8 @@ impl ProjectResolver {
176190
}
177191
}),
178192
};
179-
let project_layout = ProjectLayout::determine(project_root, extension_name, py_root, data)?;
193+
let project_layout =
194+
ProjectLayout::determine(project_root, extension_name, py_root, python_packages, data)?;
180195
Ok(Self {
181196
project_layout,
182197
cargo_toml_path: manifest_file,
@@ -259,12 +274,18 @@ impl ProjectResolver {
259274
} else if let Some(project_name) = pyproject.project_name() {
260275
// Check if python source directory in `src/<project_name>`
261276
let import_name = project_name.replace('-', "_");
262-
if current_dir
263-
.join("src")
264-
.join(import_name)
265-
.join("__init__.py")
266-
.is_file()
267-
{
277+
let mut package_init = HashSet::new();
278+
package_init.insert(
279+
current_dir
280+
.join("src")
281+
.join(import_name)
282+
.join("__init__.py"),
283+
);
284+
for package in pyproject.python_packages().unwrap_or_default() {
285+
package_init
286+
.insert(current_dir.join("src").join(package).join("__init__.py"));
287+
}
288+
if package_init.iter().any(|x| x.is_file()) {
268289
return Ok((path, pyproject_file));
269290
}
270291
}
@@ -322,6 +343,7 @@ impl ProjectLayout {
322343
project_root: impl AsRef<Path>,
323344
module_name: &str,
324345
python_root: PathBuf,
346+
python_packages: Vec<String>,
325347
data: Option<PathBuf>,
326348
) -> Result<ProjectLayout> {
327349
// A dot in the module name means the extension module goes into the module folder specified by the path
@@ -373,6 +395,7 @@ impl ProjectLayout {
373395

374396
Ok(ProjectLayout {
375397
python_dir: python_root,
398+
python_packages,
376399
python_module: Some(python_module),
377400
rust_module,
378401
extension_name,
@@ -381,6 +404,7 @@ impl ProjectLayout {
381404
} else {
382405
Ok(ProjectLayout {
383406
python_dir: python_root,
407+
python_packages,
384408
python_module: None,
385409
rust_module: project_root.to_path_buf(),
386410
extension_name,

src/source_distribution.rs

+29-33
Original file line numberDiff line numberDiff line change
@@ -620,42 +620,38 @@ pub fn source_distribution(
620620

621621
let pyproject_dir = pyproject_toml_path.parent().unwrap();
622622
// Add python source files
623+
let mut python_packages = Vec::new();
623624
if let Some(python_module) = build_context.project_layout.python_module.as_ref() {
624-
let mut python_packages = vec![python_module.to_path_buf()];
625-
for package in build_context
626-
.pyproject_toml
627-
.as_ref()
628-
.and_then(|toml| toml.python_packages())
629-
.unwrap_or_default()
630-
{
631-
let package_path = build_context.project_layout.python_dir.join(package);
632-
if python_packages.iter().any(|p| *p == package_path) {
633-
continue;
634-
}
635-
python_packages.push(package_path);
625+
python_packages.push(python_module.to_path_buf());
626+
}
627+
for package in &build_context.project_layout.python_packages {
628+
let package_path = build_context.project_layout.python_dir.join(package);
629+
if python_packages.iter().any(|p| *p == package_path) {
630+
continue;
636631
}
632+
python_packages.push(package_path);
633+
}
637634

638-
for package in python_packages {
639-
for entry in ignore::Walk::new(package) {
640-
let source = entry?.into_path();
641-
// Technically, `ignore` crate should handle this,
642-
// but somehow it doesn't on Alpine Linux running in GitHub Actions,
643-
// so we do it manually here.
644-
// See https://github.com/PyO3/maturin/pull/1187#issuecomment-1273987013
645-
if source
646-
.extension()
647-
.map(|ext| ext == "pyc" || ext == "pyd" || ext == "so")
648-
.unwrap_or_default()
649-
{
650-
debug!("Ignoring {}", source.display());
651-
continue;
652-
}
653-
let target = root_dir.join(source.strip_prefix(pyproject_dir).unwrap());
654-
if source.is_dir() {
655-
writer.add_directory(target)?;
656-
} else {
657-
writer.add_file(target, &source)?;
658-
}
635+
for package in python_packages {
636+
for entry in ignore::Walk::new(package) {
637+
let source = entry?.into_path();
638+
// Technically, `ignore` crate should handle this,
639+
// but somehow it doesn't on Alpine Linux running in GitHub Actions,
640+
// so we do it manually here.
641+
// See https://github.com/PyO3/maturin/pull/1187#issuecomment-1273987013
642+
if source
643+
.extension()
644+
.map(|ext| ext == "pyc" || ext == "pyd" || ext == "so")
645+
.unwrap_or_default()
646+
{
647+
debug!("Ignoring {}", source.display());
648+
continue;
649+
}
650+
let target = root_dir.join(source.strip_prefix(pyproject_dir).unwrap());
651+
if source.is_dir() {
652+
writer.add_directory(target)?;
653+
} else {
654+
writer.add_file(target, &source)?;
659655
}
660656
}
661657
}

0 commit comments

Comments
 (0)