Skip to content

Commit c5a6b8f

Browse files
committed
Respect metadata_directory in build_wheel per PEP 517
When a PEP 517 frontend passes metadata_directory to build_wheel, the built wheel should use the pre-generated metadata from that directory. This enables wrapping build backends that modify metadata in prepare_metadata_for_build_wheel. Python side: when metadata_directory is not None, set MATURIN_PEP517_METADATA_DIR in the subprocess environment. Rust side: write_dist_info() checks the env var and copies files from the pre-existing .dist-info directory instead of regenerating them. The WHEEL file is always regenerated to ensure correct tags, and RECORD is skipped since it is generated by WheelWriter::finish(). If the env var is set but the expected directory does not exist, an error is raised instead of silently falling back. Fixes #1973
1 parent e2ee46d commit c5a6b8f

File tree

3 files changed

+157
-2
lines changed

3 files changed

+157
-2
lines changed

maturin/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,15 @@ def _build_wheel(
108108

109109
command = [*base_command, *options]
110110

111+
env = _get_env()
112+
if metadata_directory is not None:
113+
if env is None:
114+
env = os.environ.copy()
115+
env["MATURIN_PEP517_METADATA_DIR"] = metadata_directory
116+
111117
print("Running `{}`".format(" ".join(command)))
112118
sys.stdout.flush()
113-
result = subprocess.run(command, stdout=subprocess.PIPE, env=_get_env())
119+
result = subprocess.run(command, stdout=subprocess.PIPE, env=env)
114120
sys.stdout.buffer.write(result.stdout)
115121
sys.stdout.flush()
116122
if result.returncode != 0:

src/module_writer/mock_writer.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,82 @@ fn write_dist_info_rejects_absolute_license_paths() {
173173
"unexpected error: {err:#}"
174174
);
175175
}
176+
177+
#[test]
178+
fn write_dist_info_respects_metadata_directory_env_var() -> Result<()> {
179+
use pep440_rs::Version;
180+
use std::str::FromStr;
181+
182+
let temp_dir = tempfile::tempdir()?;
183+
let pyproject_dir = temp_dir.path().join("crate");
184+
fs_err::create_dir_all(&pyproject_dir)?;
185+
186+
let metadata = Metadata24::new("test-pkg".to_string(), Version::from_str("1.0.0").unwrap());
187+
let dist_info_name = "test_pkg-1.0.0.dist-info";
188+
189+
// Create a pre-generated .dist-info directory with custom METADATA
190+
let metadata_dir = temp_dir.path().join("metadata");
191+
let pre_existing_dir = metadata_dir.join(dist_info_name);
192+
fs_err::create_dir_all(&pre_existing_dir)?;
193+
194+
let custom_metadata =
195+
"Metadata-Version: 2.4\nName: test-pkg\nVersion: 1.0.0\nClassifier: Custom :: Classifier\n";
196+
fs_err::write(pre_existing_dir.join("METADATA"), custom_metadata)?;
197+
fs_err::write(pre_existing_dir.join("WHEEL"), "should be overwritten")?;
198+
fs_err::write(pre_existing_dir.join("RECORD"), "should be skipped")?;
199+
fs_err::write(
200+
pre_existing_dir.join("entry_points.txt"),
201+
"[console_scripts]\nfoo=bar:main\n",
202+
)?;
203+
204+
// Create a licenses/ subdirectory
205+
let licenses_dir = pre_existing_dir.join("licenses");
206+
fs_err::create_dir_all(&licenses_dir)?;
207+
fs_err::write(licenses_dir.join("LICENSE"), "MIT License")?;
208+
209+
// SAFETY: This test is single-threaded and the env var is removed before returning.
210+
unsafe { std::env::set_var("MATURIN_PEP517_METADATA_DIR", &metadata_dir) };
211+
let mut writer = VirtualWriter::new(MockWriter::default(), Override::empty());
212+
let tags = &["cp310-cp310-manylinux_2_17_x86_64".to_string()];
213+
let result = write_dist_info(&mut writer, &pyproject_dir, &metadata, tags);
214+
unsafe { std::env::remove_var("MATURIN_PEP517_METADATA_DIR") };
215+
result?;
216+
217+
let files = writer.finish()?;
218+
219+
// METADATA should be the custom one, not regenerated
220+
let metadata_key = Path::new(dist_info_name).join("METADATA");
221+
assert_eq!(
222+
str::from_utf8(&files[&metadata_key]).unwrap(),
223+
custom_metadata
224+
);
225+
226+
// WHEEL should be regenerated with correct tags, not the pre-existing content
227+
let wheel_key = Path::new(dist_info_name).join("WHEEL");
228+
let wheel_content = str::from_utf8(&files[&wheel_key]).unwrap();
229+
assert!(
230+
wheel_content.contains("Tag: cp310-cp310-manylinux_2_17_x86_64"),
231+
"WHEEL should contain the correct tag, got: {wheel_content}"
232+
);
233+
assert!(
234+
!wheel_content.contains("should be overwritten"),
235+
"WHEEL should be regenerated, not copied"
236+
);
237+
238+
// RECORD should not be present (it's generated later by WheelWriter)
239+
let record_key = Path::new(dist_info_name).join("RECORD");
240+
assert!(!files.contains_key(&record_key));
241+
242+
// entry_points.txt should be copied
243+
let ep_key = Path::new(dist_info_name).join("entry_points.txt");
244+
assert_eq!(
245+
str::from_utf8(&files[&ep_key]).unwrap(),
246+
"[console_scripts]\nfoo=bar:main\n"
247+
);
248+
249+
// License file from subdirectory should be copied
250+
let license_key = Path::new(dist_info_name).join("licenses").join("LICENSE");
251+
assert_eq!(str::from_utf8(&files[&license_key]).unwrap(), "MIT License");
252+
253+
Ok(())
254+
}

src/module_writer/mod.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,12 @@ fn add_data_subdir(
289289
Ok(())
290290
}
291291

292-
/// Creates the .dist-info directory and fills it with all metadata files except RECORD
292+
/// Creates the .dist-info directory and fills it with all metadata files except RECORD.
293+
///
294+
/// If the `MATURIN_PEP517_METADATA_DIR` environment variable is set, copies the
295+
/// pre-generated metadata files from that directory instead of regenerating them.
296+
/// The WHEEL file is always regenerated to ensure correct tags. This implements
297+
/// the PEP 517 requirement that `build_wheel` respects `metadata_directory`.
293298
pub fn write_dist_info(
294299
writer: &mut VirtualWriter<impl ModuleWriterInternal>,
295300
pyproject_dir: &Path,
@@ -298,6 +303,23 @@ pub fn write_dist_info(
298303
) -> Result<PathBuf> {
299304
let dist_info_dir = metadata24.get_dist_info_dir();
300305

306+
if let Ok(metadata_dir) = std::env::var("MATURIN_PEP517_METADATA_DIR") {
307+
let pre_existing = Path::new(&metadata_dir).join(&dist_info_dir);
308+
if pre_existing.is_dir() {
309+
debug!(
310+
"Using pre-generated metadata from {}",
311+
pre_existing.display()
312+
);
313+
return write_dist_info_from_dir(writer, &dist_info_dir, &pre_existing, tags);
314+
} else {
315+
bail!(
316+
"MATURIN_PEP517_METADATA_DIR is set to '{}' but expected directory '{}' does not exist",
317+
metadata_dir,
318+
pre_existing.display()
319+
);
320+
}
321+
}
322+
301323
writer.add_bytes(
302324
dist_info_dir.join("METADATA"),
303325
None,
@@ -362,6 +384,54 @@ pub fn write_dist_info(
362384
Ok(dist_info_dir)
363385
}
364386

387+
/// Copies pre-generated metadata files from a `.dist-info` directory on disk into the wheel,
388+
/// but always regenerates the WHEEL file to ensure correct tags.
389+
fn write_dist_info_from_dir(
390+
writer: &mut VirtualWriter<impl ModuleWriterInternal>,
391+
dist_info_dir: &Path,
392+
source_dir: &Path,
393+
tags: &[String],
394+
) -> Result<PathBuf> {
395+
// Always regenerate WHEEL to ensure correct tags for the built wheel
396+
writer.add_bytes(
397+
dist_info_dir.join("WHEEL"),
398+
None,
399+
wheel_file(tags)?.as_bytes(),
400+
false,
401+
)?;
402+
403+
// Copy all other files from the pre-generated .dist-info directory
404+
for entry in fs::read_dir(source_dir)? {
405+
let entry = entry?;
406+
let file_name = entry.file_name();
407+
let file_name_str = file_name.to_string_lossy();
408+
409+
// Skip WHEEL (already regenerated) and RECORD (will be regenerated by WheelWriter)
410+
if file_name_str == "WHEEL" || file_name_str == "RECORD" {
411+
continue;
412+
}
413+
414+
let entry_path = entry.path();
415+
if entry_path.is_dir() {
416+
// Recursively add subdirectories (e.g. licenses/)
417+
for sub_entry in walkdir::WalkDir::new(&entry_path) {
418+
let sub_entry = sub_entry?;
419+
if sub_entry.file_type().is_file() {
420+
let rel = sub_entry
421+
.path()
422+
.strip_prefix(source_dir)
423+
.expect("walkdir entry must be under source_dir");
424+
writer.add_file(dist_info_dir.join(rel), sub_entry.path(), false)?;
425+
}
426+
}
427+
} else {
428+
writer.add_file(dist_info_dir.join(&file_name), &entry_path, false)?;
429+
}
430+
}
431+
432+
Ok(dist_info_dir.to_owned())
433+
}
434+
365435
/// Add a pth file to wheel root for editable installs
366436
pub fn write_pth(
367437
writer: &mut VirtualWriter<WheelWriter>,

0 commit comments

Comments
 (0)