maturin 1.12.0

Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages
Documentation
use std::io::Read as _;
use std::path::Path;
use std::path::PathBuf;
use std::str;

use anyhow::Result;
use anyhow::bail;
use fs_err::File;
use ignore::overrides::Override;
use indexmap::IndexMap;
use indexmap::map::Entry;
use insta::assert_snapshot;
use itertools::Itertools as _;

use crate::BuildOptions;
use crate::CargoOptions;
use crate::Metadata24;
use crate::archive_source::ArchiveSource;
use crate::write_dist_info;

use super::ModuleWriterInternal;
use super::VirtualWriter;

#[derive(Default)]
pub(crate) struct MockWriter {
    files: IndexMap<PathBuf, Vec<u8>>,
}

impl super::private::Sealed for MockWriter {}

impl ModuleWriterInternal for MockWriter {
    fn add_entry(&mut self, target: impl AsRef<Path>, source: ArchiveSource) -> Result<()> {
        let target = target.as_ref().to_path_buf();
        let Entry::Vacant(entry) = self.files.entry(target.clone()) else {
            bail!("Duplicate file {target:?} written to mock writer");
        };

        let buffer = match source {
            ArchiveSource::Generated(source) => source.data,
            ArchiveSource::File(source) => {
                let mut file = File::options().read(true).open(source.path)?;
                let mut buffer = Vec::new();
                file.read_to_end(&mut buffer)?;
                buffer
            }
        };

        entry.insert(buffer);
        Ok(())
    }
}

impl MockWriter {
    pub fn finish(self) -> IndexMap<PathBuf, Vec<u8>> {
        self.files
    }
}

#[test]
fn metadata_hello_world_pep639() -> Result<()> {
    let build_options = BuildOptions {
        cargo: CargoOptions {
            manifest_path: Some(
                PathBuf::from("test-crates")
                    .join("hello-world")
                    .join("Cargo.toml"),
            ),
            ..CargoOptions::default()
        },
        ..BuildOptions::default()
    };
    let context = build_options.into_build_context().build().unwrap();

    let mut writer = VirtualWriter::new(MockWriter::default(), Override::empty());
    write_dist_info(
        &mut writer,
        &context.project_layout.project_root,
        &context.metadata24,
        &context.tags_from_bridge().unwrap(),
    )
    .unwrap();

    let files = writer.finish()?;
    assert_snapshot!(files.keys().map(|p| p.to_string_lossy()).collect_vec().join("\n").replace("\\", "/"), @r"
    hello_world-0.1.0.dist-info/METADATA
    hello_world-0.1.0.dist-info/WHEEL
    hello_world-0.1.0.dist-info/licenses/LICENSE
    hello_world-0.1.0.dist-info/licenses/licenses/AUTHORS.txt
    ");
    let metadata_path = Path::new("hello_world-0.1.0.dist-info").join("METADATA");
    // Remove the README in the body of the email
    let metadata = str::from_utf8(&files[&metadata_path])
        .unwrap()
        .split_once("\n\n")
        .unwrap()
        .0;
    assert_snapshot!(metadata, @r"
    Metadata-Version: 2.4
    Name: hello-world
    Version: 0.1.0
    License-File: LICENSE
    License-File: licenses/AUTHORS.txt
    Author: konstin <konstin@mailbox.org>
    Author-email: konstin <konstin@mailbox.org>
    Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
    ");

    Ok(())
}

#[test]
fn write_dist_info_uses_license_file_sources() -> Result<()> {
    use pep440_rs::Version;
    use std::str::FromStr;

    let temp_dir = tempfile::tempdir()?;
    let pyproject_dir = temp_dir.path().join("crate");
    let workspace_root = temp_dir.path();
    fs_err::create_dir_all(&pyproject_dir)?;

    // Create a workspace-level license file (outside pyproject_dir)
    let license_content = b"MIT License - workspace level";
    fs_err::write(workspace_root.join("LICENSE"), license_content)?;

    let mut metadata = Metadata24::new("test-pkg".to_string(), Version::from_str("1.0.0").unwrap());
    metadata.license_files.push(PathBuf::from("LICENSE"));
    metadata
        .license_file_sources
        .insert(PathBuf::from("LICENSE"), workspace_root.join("LICENSE"));

    let mut writer = VirtualWriter::new(MockWriter::default(), Override::empty());
    write_dist_info(
        &mut writer,
        &pyproject_dir,
        &metadata,
        &["py3-none-any".to_string()],
    )?;

    let files = writer.finish()?;
    let license_key = Path::new("test_pkg-1.0.0.dist-info/licenses/LICENSE");
    assert!(
        files.contains_key(license_key),
        "expected license file in dist-info, got keys: {:?}",
        files.keys().collect::<Vec<_>>()
    );
    assert_eq!(files[license_key], license_content);

    Ok(())
}

#[test]
fn write_dist_info_rejects_absolute_license_paths() {
    use pep440_rs::Version;
    use std::str::FromStr;

    let temp_dir = tempfile::tempdir().unwrap();
    let pyproject_dir = temp_dir.path();

    let mut metadata = Metadata24::new("test-pkg".to_string(), Version::from_str("1.0.0").unwrap());
    metadata.license_files.push(temp_dir.path().join("LICENSE"));

    let mut writer = VirtualWriter::new(MockWriter::default(), Override::empty());
    let err = write_dist_info(
        &mut writer,
        pyproject_dir,
        &metadata,
        &["py3-none-any".to_string()],
    )
    .unwrap_err();

    assert!(
        err.to_string().contains("unsafe path"),
        "unexpected error: {err:#}"
    );
}