Skip to content

Commit ee4f164

Browse files
authored
Create a binding generator trait (#2872)
The goal is to deduplicate the code building the archive and adding type stubs, etc. This PR only adds the base trait and implements it for the PyO3 bindings. Once this PR looks good, I'll migrate the other bindings one at a time in separate PRs.
1 parent fcd8da8 commit ee4f164

File tree

4 files changed

+275
-135
lines changed

4 files changed

+275
-135
lines changed

src/binding_generator/mod.rs

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,190 @@
1+
use std::collections::HashMap;
2+
use std::io::Write as _;
13
use std::path::Path;
24
use std::path::PathBuf;
35

6+
use anyhow::Context as _;
47
use anyhow::Result;
8+
use fs_err as fs;
9+
use fs_err::File;
10+
use tempfile::TempDir;
11+
use tempfile::tempdir;
12+
use tracing::debug;
513

14+
use crate::BuildArtifact;
15+
use crate::BuildContext;
616
use crate::Metadata24;
717
use crate::ModuleWriter;
18+
use crate::PythonInterpreter;
819
use crate::module_writer::ModuleWriterExt;
20+
use crate::module_writer::write_python_part;
921

1022
mod cffi_binding;
1123
mod pyo3_binding;
1224
mod uniffi_binding;
1325
mod wasm_binding;
1426

1527
pub use cffi_binding::write_cffi_module;
16-
pub use pyo3_binding::write_bindings_module;
28+
pub use pyo3_binding::Pyo3BindingGenerator;
1729
pub use uniffi_binding::write_uniffi_module;
1830
pub use wasm_binding::write_wasm_launcher;
1931

32+
///A trait to generate the binding files to be included in the built module
33+
///
34+
/// This trait is used to generate the support files necessary to build a python
35+
/// module for any [crate::BridgeModel]
36+
pub(crate) trait BindingGenerator {
37+
fn generate_bindings(
38+
&self,
39+
context: &BuildContext,
40+
interpreter: Option<&PythonInterpreter>,
41+
artifact: &BuildArtifact,
42+
module: &Path,
43+
temp_dir: &TempDir,
44+
) -> Result<GeneratorOutput>;
45+
}
46+
47+
#[derive(Debug)]
48+
pub(crate) struct GeneratorOutput {
49+
/// The path, relative to the archive root, where the built artifact/module
50+
/// should be installed
51+
artifact_target: PathBuf,
52+
53+
/// In some cases, the source path of the artifact is altered
54+
/// (e.g. when the build output is an archive which needs to be unpacked)
55+
artifact_source_override: Option<PathBuf>,
56+
57+
/// Additional files to be installed (e.g. __init__.py)
58+
/// The provided PathBuf should be relative to the archive root
59+
additional_files: Option<HashMap<PathBuf, Vec<u8>>>,
60+
}
61+
62+
/// Every binding generator ultimately has to install the following:
63+
/// 1. The python files (if any)
64+
/// 2. The artifact
65+
/// 3. Additional files
66+
/// 4. Type stubs (if any/pure rust only)
67+
///
68+
/// Additionally, the above are installed to 2 potential locations:
69+
/// 1. The archive
70+
/// 2. The filesystem
71+
///
72+
/// For editable installs:
73+
/// If the project is pure rust, the wheel is built as normal and installed
74+
/// If the project has python, the artifact is installed into the project and a pth is written to the archive
75+
///
76+
/// So the full matrix comes down to:
77+
/// 1. editable, has python => install to fs, write pth to archive
78+
/// 2. everything else => install to archive/build as normal
79+
///
80+
/// Note: Writing the pth to the archive is handled by [BuildContext], not here
81+
pub fn generate_binding(
82+
writer: &mut impl ModuleWriter,
83+
generator: &impl BindingGenerator,
84+
context: &BuildContext,
85+
interpreter: Option<&PythonInterpreter>,
86+
artifact: &BuildArtifact,
87+
) -> Result<()> {
88+
// 1. Install the python files
89+
if !context.editable {
90+
write_python_part(
91+
writer,
92+
&context.project_layout,
93+
context.pyproject_toml.as_ref(),
94+
)?;
95+
}
96+
97+
let base_path = context
98+
.project_layout
99+
.python_module
100+
.as_ref()
101+
.map(|python_module| python_module.parent().unwrap().to_path_buf());
102+
103+
let module = match &base_path {
104+
Some(base_path) => context
105+
.project_layout
106+
.rust_module
107+
.strip_prefix(base_path)
108+
.unwrap()
109+
.to_path_buf(),
110+
None => PathBuf::from(&context.project_layout.extension_name),
111+
};
112+
113+
let temp_dir = tempdir()?;
114+
let GeneratorOutput {
115+
artifact_target,
116+
artifact_source_override,
117+
additional_files,
118+
} = generator.generate_bindings(context, interpreter, artifact, &module, &temp_dir)?;
119+
120+
match (context.editable, &base_path) {
121+
(true, Some(base_path)) => {
122+
let target = base_path.join(&artifact_target);
123+
debug!("Removing previously built module {}", target.display());
124+
fs::create_dir_all(target.parent().unwrap())?;
125+
// Remove existing so file to avoid triggering SIGSEV in running process
126+
// See https://github.com/PyO3/maturin/issues/758
127+
let _ = fs::remove_file(&target);
128+
let source = artifact_source_override.unwrap_or_else(|| artifact.path.clone());
129+
130+
// 2. Install the artifact
131+
debug!("Installing {} from {}", target.display(), source.display());
132+
fs::copy(&source, &target).with_context(|| {
133+
format!(
134+
"Failed to copy {} to {}",
135+
source.display(),
136+
target.display(),
137+
)
138+
})?;
139+
140+
// 3. Install additional files
141+
if let Some(additional_files) = additional_files {
142+
for (target, data) in additional_files {
143+
let target = base_path.join(target);
144+
fs::create_dir_all(target.parent().unwrap())?;
145+
debug!("Generating file {}", target.display());
146+
let mut file = File::options().create(true).truncate(true).open(&target)?;
147+
file.write_all(data.as_slice())?;
148+
}
149+
}
150+
}
151+
_ => {
152+
// 2. Install the artifact
153+
let source = artifact_source_override.unwrap_or_else(|| artifact.path.clone());
154+
debug!(
155+
"Adding to archive {} from {}",
156+
artifact_target.display(),
157+
source.display()
158+
);
159+
writer.add_file(artifact_target, source, true)?;
160+
161+
// 3. Install additional files
162+
if let Some(additional_files) = additional_files {
163+
for (target, data) in additional_files {
164+
debug!("Generating archive entry {}", target.display());
165+
writer.add_bytes(target, None, data.as_slice(), false)?;
166+
}
167+
}
168+
}
169+
}
170+
171+
// 4. Install type stubs
172+
if context.project_layout.python_module.is_none() {
173+
let ext_name = &context.project_layout.extension_name;
174+
let type_stub = context
175+
.project_layout
176+
.rust_module
177+
.join(format!("{ext_name}.pyi"));
178+
if type_stub.exists() {
179+
eprintln!("📖 Found type stub file at {ext_name}.pyi");
180+
writer.add_file(module.join("__init__.pyi"), type_stub, false)?;
181+
writer.add_empty_file(module.join("py.typed"))?;
182+
}
183+
}
184+
185+
Ok(())
186+
}
187+
20188
/// Adds a data directory with a scripts directory with the binary inside it
21189
pub fn write_bin(
22190
writer: &mut impl ModuleWriter,

0 commit comments

Comments
 (0)