use itertools::Itertools;
mod metadata;
mod serde_verbatim;
mod settings;
mod source_dist;
mod wheel;
pub use metadata::{PyProjectToml, check_direct_build};
pub use settings::{BuildBackendSettings, WheelDataIncludes};
pub use source_dist::{build_source_dist, list_source_dist};
use uv_warnings::warn_user_once;
pub use wheel::{build_editable, build_wheel, list_wheel, metadata};
use std::collections::HashSet;
use std::ffi::OsStr;
use std::io;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error;
use tracing::debug;
use walkdir::DirEntry;
use uv_fs::{Simplified, normalize_path};
use uv_globfilter::PortableGlobError;
use uv_normalize::PackageName;
use uv_pypi_types::{Identifier, IdentifierParseError};
use crate::metadata::ValidationError;
use crate::settings::ModuleName;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error("Failed to persist temporary file to {}", _0.user_display())]
Persist(PathBuf, #[source] io::Error),
#[error("Invalid metadata format in: {}", _0.user_display())]
Toml(PathBuf, #[source] toml::de::Error),
#[error("Invalid project metadata")]
Validation(#[from] ValidationError),
#[error("Invalid module name: {0}")]
InvalidModuleName(String, #[source] IdentifierParseError),
#[error("Unsupported glob expression in: {field}")]
PortableGlob {
field: String,
#[source]
source: PortableGlobError,
},
#[error("Glob expressions caused to large regex in: {field}")]
GlobSetTooLarge {
field: String,
#[source]
source: globset::Error,
},
#[error("`pyproject.toml` must not be excluded from source distribution build")]
PyprojectTomlExcluded,
#[error("Failed to walk source tree: {}", root.user_display())]
WalkDir {
root: PathBuf,
#[source]
err: walkdir::Error,
},
#[error("Failed to write wheel zip archive")]
Zip(#[from] zip::result::ZipError),
#[error("Failed to write RECORD file")]
Csv(#[from] csv::Error),
#[error("Failed to write JSON metadata file")]
Json(#[source] serde_json::Error),
#[error("Expected a Python module at: {}", _0.user_display())]
MissingInitPy(PathBuf),
#[error("For namespace packages, `__init__.py[i]` is not allowed in parent directory: {}", _0.user_display())]
NotANamespace(PathBuf),
#[error("Module root must be inside the project: {}", _0.user_display())]
InvalidModuleRoot(PathBuf),
#[error("The path for the data directory {} must be inside the project: {}", name, path.user_display())]
InvalidDataRoot { name: String, path: PathBuf },
#[error("Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: {}", _0.user_display())]
VenvInSourceTree(PathBuf),
#[error("Inconsistent metadata between prepare and build step: {0}")]
InconsistentSteps(&'static str),
#[error("Failed to write to {}", _0.user_display())]
TarWrite(PathBuf, #[source] io::Error),
}
trait DirectoryWriter {
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error>;
fn write_dir_entry(&mut self, entry: &DirEntry, target_path: &str) -> Result<(), Error> {
if entry.file_type().is_dir() {
self.write_directory(target_path)?;
} else {
self.write_file(target_path, entry.path())?;
}
Ok(())
}
fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error>;
fn write_directory(&mut self, directory: &str) -> Result<(), Error>;
fn close(self, dist_info_dir: &str) -> Result<(), Error>;
}
pub(crate) type FileList = Vec<(String, Option<PathBuf>)>;
pub(crate) struct ListWriter<'a> {
files: &'a mut FileList,
}
impl<'a> ListWriter<'a> {
pub(crate) fn new(files: &'a mut FileList) -> Self {
Self { files }
}
}
impl DirectoryWriter for ListWriter<'_> {
fn write_bytes(&mut self, path: &str, _bytes: &[u8]) -> Result<(), Error> {
self.files.push((path.to_string(), None));
Ok(())
}
fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> {
self.files
.push((path.to_string(), Some(file.to_path_buf())));
Ok(())
}
fn write_directory(&mut self, _directory: &str) -> Result<(), Error> {
Ok(())
}
fn close(self, _dist_info_dir: &str) -> Result<(), Error> {
Ok(())
}
}
fn check_metadata_directory(
source_tree: &Path,
metadata_directory: Option<&Path>,
pyproject_toml: &PyProjectToml,
) -> Result<(), Error> {
let Some(metadata_directory) = metadata_directory else {
return Ok(());
};
debug!(
"Checking metadata directory {}",
metadata_directory.user_display()
);
let current = pyproject_toml
.to_metadata(source_tree)?
.core_metadata_format();
let previous = fs_err::read_to_string(metadata_directory.join("METADATA"))?;
if previous != current {
return Err(Error::InconsistentSteps("METADATA"));
}
let entrypoints_path = metadata_directory.join("entry_points.txt");
match pyproject_toml.to_entry_points()? {
None => {
if entrypoints_path.is_file() {
return Err(Error::InconsistentSteps("entry_points.txt"));
}
}
Some(entrypoints) => {
if fs_err::read_to_string(&entrypoints_path)? != entrypoints {
return Err(Error::InconsistentSteps("entry_points.txt"));
}
}
}
Ok(())
}
fn prune_redundant_modules(mut names: Vec<String>) -> Vec<String> {
names.sort();
let mut pruned = Vec::with_capacity(names.len());
for name in names {
if let Some(last) = pruned.last() {
if name == *last {
continue;
}
if name
.strip_prefix(last)
.is_some_and(|suffix| suffix.starts_with('.'))
{
continue;
}
}
pruned.push(name);
}
pruned
}
fn prune_redundant_modules_warn(names: &[String], show_warnings: bool) -> Vec<String> {
let pruned = prune_redundant_modules(names.to_vec());
if show_warnings && names.len() != pruned.len() {
let mut pruned: HashSet<_> = pruned.iter().collect();
let ignored: Vec<_> = names.iter().filter(|name| !pruned.remove(name)).collect();
let s = if ignored.len() == 1 { "" } else { "s" };
warn_user_once!(
"Ignoring redundant module name{s} in `tool.uv.build-backend.module-name`: `{}`",
ignored.into_iter().join("`, `")
);
}
pruned
}
fn find_roots(
source_tree: &Path,
pyproject_toml: &PyProjectToml,
relative_module_root: &Path,
module_name: Option<&ModuleName>,
namespace: bool,
show_warnings: bool,
) -> Result<(PathBuf, Vec<PathBuf>), Error> {
let relative_module_root = normalize_path(relative_module_root);
let src_root = source_tree.join(&relative_module_root);
if !normalize_path(&src_root).starts_with(normalize_path(source_tree)) {
return Err(Error::InvalidModuleRoot(relative_module_root.to_path_buf()));
}
debug!("Source root: {}", src_root.user_display());
if namespace {
let modules_relative = if let Some(module_name) = module_name {
match module_name {
ModuleName::Name(name) => {
vec![name.split('.').collect::<PathBuf>()]
}
ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings)
.into_iter()
.map(|name| name.split('.').collect::<PathBuf>())
.collect(),
}
} else {
vec![PathBuf::from(
pyproject_toml.name().as_dist_info_name().to_string(),
)]
};
for module_relative in &modules_relative {
debug!("Namespace module path: {}", module_relative.user_display());
}
return Ok((src_root, modules_relative));
}
let modules_relative = if let Some(module_name) = module_name {
match module_name {
ModuleName::Name(name) => vec![module_path_from_module_name(&src_root, name)?],
ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings)
.into_iter()
.map(|name| module_path_from_module_name(&src_root, &name))
.collect::<Result<_, _>>()?,
}
} else {
vec![find_module_path_from_package_name(
&src_root,
pyproject_toml.name(),
)?]
};
for module_relative in &modules_relative {
debug!("Module path: {}", module_relative.user_display());
}
Ok((src_root, modules_relative))
}
fn find_module_path_from_package_name(
src_root: &Path,
package_name: &PackageName,
) -> Result<PathBuf, Error> {
if let Some(stem) = package_name.to_string().strip_suffix("-stubs") {
debug!("Building stubs package instead of a regular package");
let module_name = PackageName::from_str(stem)
.expect("non-empty package name prefix must be valid package name")
.as_dist_info_name()
.to_string();
let module_relative = PathBuf::from(format!("{module_name}-stubs"));
let init_pyi = src_root.join(&module_relative).join("__init__.pyi");
if !init_pyi.is_file() {
return Err(Error::MissingInitPy(init_pyi));
}
Ok(module_relative)
} else {
let module_relative = PathBuf::from(package_name.as_dist_info_name().to_string());
let init_py = src_root.join(&module_relative).join("__init__.py");
if !init_py.is_file() {
return Err(Error::MissingInitPy(init_py));
}
Ok(module_relative)
}
}
fn module_path_from_module_name(src_root: &Path, module_name: &str) -> Result<PathBuf, Error> {
let module_relative = module_name.split('.').collect::<PathBuf>();
let (root_name, namespace_segments) =
if let Some((root_name, namespace_segments)) = module_name.split_once('.') {
(
root_name,
namespace_segments.split('.').collect::<Vec<&str>>(),
)
} else {
(module_name, Vec::new())
};
let stubs = if let Some(stem) = root_name.strip_suffix("-stubs") {
Identifier::from_str(stem)
.map_err(|err| Error::InvalidModuleName(module_name.to_string(), err))?;
true
} else {
Identifier::from_str(root_name)
.map_err(|err| Error::InvalidModuleName(module_name.to_string(), err))?;
false
};
for segment in namespace_segments {
Identifier::from_str(segment)
.map_err(|err| Error::InvalidModuleName(module_name.to_string(), err))?;
}
let init_py =
src_root
.join(&module_relative)
.join(if stubs { "__init__.pyi" } else { "__init__.py" });
if !init_py.is_file() {
return Err(Error::MissingInitPy(init_py));
}
for namespace_dir in module_relative.ancestors().skip(1) {
if src_root.join(namespace_dir).join("__init__.py").exists()
|| src_root.join(namespace_dir).join("__init__.pyi").exists()
{
return Err(Error::NotANamespace(src_root.join(namespace_dir)));
}
}
Ok(module_relative)
}
pub(crate) fn error_on_venv(file_name: &OsStr, path: &Path) -> Result<(), Error> {
if !(file_name == "pyvenv.cfg" || file_name == "lib64") {
return Ok(());
}
let Some(parent) = path.parent() else {
return Ok(());
};
if parent.join("bin").join("python").is_symlink()
|| parent.join("Scripts").join("python.exe").is_file()
{
return Err(Error::VenvInSourceTree(parent.to_path_buf()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::bufread::GzDecoder;
use fs_err::File;
use indoc::indoc;
use insta::assert_snapshot;
use itertools::Itertools;
use regex::Regex;
use sha2::Digest;
use std::io::{BufReader, Read};
use std::iter;
use tempfile::TempDir;
use uv_distribution_filename::{SourceDistFilename, WheelFilename};
use uv_fs::{copy_dir_all, relative_to};
use uv_preview::PreviewFeature;
const MOCK_UV_VERSION: &str = "1.0.0+test";
fn format_err(err: &Error) -> String {
let context = iter::successors(std::error::Error::source(&err), |&err| err.source())
.map(|err| format!(" Caused by: {err}"))
.join("\n");
err.to_string() + "\n" + &context
}
#[derive(Debug, PartialEq, Eq)]
struct BuildResults {
source_dist_list_files: FileList,
source_dist_filename: SourceDistFilename,
source_dist_contents: Vec<String>,
wheel_list_files: FileList,
wheel_filename: WheelFilename,
wheel_contents: Vec<String>,
}
fn build(
source_root: &Path,
dist: &Path,
preview_features: &[PreviewFeature],
) -> Result<BuildResults, Error> {
let (_name, direct_wheel_list_files) = {
let _preview = uv_preview::test::with_features(preview_features);
list_wheel(source_root, MOCK_UV_VERSION, false)?
};
let direct_wheel_filename = {
let _preview = uv_preview::test::with_features(preview_features);
build_wheel(source_root, dist, None, MOCK_UV_VERSION, false)?
};
let direct_wheel_path = dist.join(direct_wheel_filename.to_string());
let direct_wheel_contents = wheel_contents(&direct_wheel_path);
let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?);
fs_err::remove_file(&direct_wheel_path)?;
let (_name, source_dist_list_files) =
list_source_dist(source_root, MOCK_UV_VERSION, false)?;
let (_name, wheel_list_files) = {
let _preview = uv_preview::test::with_features(preview_features);
list_wheel(source_root, MOCK_UV_VERSION, false)?
};
let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION, false)?;
let source_dist_path = dist.join(source_dist_filename.to_string());
let source_dist_contents = sdist_contents(&source_dist_path);
let sdist_tree = TempDir::new()?;
let sdist_reader = BufReader::new(File::open(&source_dist_path)?);
let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
source_dist.unpack(sdist_tree.path())?;
let sdist_top_level_directory = sdist_tree.path().join(format!(
"{}-{}",
source_dist_filename.name.as_dist_info_name(),
source_dist_filename.version
));
let wheel_filename = {
let _preview = uv_preview::test::with_features(preview_features);
build_wheel(
&sdist_top_level_directory,
dist,
None,
MOCK_UV_VERSION,
false,
)?
};
let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string()));
assert_eq!(direct_wheel_filename, wheel_filename);
assert_eq!(direct_wheel_contents, wheel_contents);
assert_eq!(direct_wheel_list_files, wheel_list_files);
assert_eq!(
direct_wheel_hash,
sha2::Sha256::digest(fs_err::read(dist.join(wheel_filename.to_string()))?)
);
Ok(BuildResults {
source_dist_list_files,
source_dist_filename,
source_dist_contents,
wheel_list_files,
wheel_filename,
wheel_contents,
})
}
fn build_err(source_root: &Path) -> String {
let dist = TempDir::new().unwrap();
let build_err = build(source_root, dist.path(), &[]).unwrap_err();
let err_message: String = format_err(&build_err)
.replace(&source_root.user_display().to_string(), "[TEMP_PATH]")
.replace('\\', "/");
err_message
}
fn sdist_contents(source_dist_path: &Path) -> Vec<String> {
let sdist_reader = BufReader::new(File::open(source_dist_path).unwrap());
let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
let mut source_dist_contents: Vec<_> = source_dist
.entries()
.unwrap()
.map(|entry| {
entry
.unwrap()
.path()
.unwrap()
.to_str()
.unwrap()
.replace('\\', "/")
})
.collect();
source_dist_contents.sort();
source_dist_contents
}
fn wheel_contents(direct_output_dir: &Path) -> Vec<String> {
let wheel = zip::ZipArchive::new(File::open(direct_output_dir).unwrap()).unwrap();
let mut wheel_contents: Vec<_> = wheel
.file_names()
.map(|path| path.replace('\\', "/"))
.collect();
wheel_contents.sort_unstable();
wheel_contents
}
fn format_file_list(file_list: FileList, src: &Path) -> String {
file_list
.into_iter()
.map(|(path, source)| {
let path = path.replace('\\', "/");
if let Some(source) = source {
let source = relative_to(source, src)
.unwrap()
.portable_display()
.to_string();
format!("{path} ({source})")
} else {
format!("{path} (generated)")
}
})
.join("\n")
}
#[test]
fn built_by_uv_building() {
let built_by_uv = Path::new("../../test/packages/built-by-uv");
let src = TempDir::new().unwrap();
for dir in [
"src",
"tests",
"data-dir",
"third-party-licenses",
"assets",
"header",
"scripts",
] {
copy_dir_all(built_by_uv.join(dir), src.path().join(dir)).unwrap();
}
for filename in [
"pyproject.toml",
"README.md",
"uv.lock",
"LICENSE-APACHE",
"LICENSE-MIT",
] {
fs_err::copy(built_by_uv.join(filename), src.path().join(filename)).unwrap();
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let path = src.path().join("scripts").join("whoami.sh");
let metadata = fs_err::metadata(&path).unwrap();
let mut perms = metadata.permissions();
perms.set_mode(perms.mode() & !0o111);
fs_err::set_permissions(&path, perms).unwrap();
}
let pyproject_toml = fs_err::read_to_string(src.path().join("pyproject.toml")).unwrap();
let current_requires =
Regex::new(r#"requires = \["uv_build>=[0-9.]+,<[0-9.]+"\]"#).unwrap();
let mocked_requires = r#"requires = ["uv_build>=1,<2"]"#;
let pyproject_toml = current_requires.replace(pyproject_toml.as_str(), mocked_requires);
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml.as_bytes()).unwrap();
let module_root = src.path().join("src").join("built_by_uv");
fs_err::create_dir_all(module_root.join("__pycache__")).unwrap();
File::create(module_root.join("__pycache__").join("compiled.pyc")).unwrap();
File::create(module_root.join("arithmetic").join("circle.pyc")).unwrap();
let dist = TempDir::new().unwrap();
let build = build(src.path(), dist.path(), &[]).unwrap();
let source_dist_path = dist.path().join(build.source_dist_filename.to_string());
assert_eq!(
build.source_dist_filename.to_string(),
"built_by_uv-0.1.0.tar.gz"
);
assert_snapshot!(
format!("{:x}", sha2::Sha256::digest(fs_err::read(&source_dist_path).unwrap())),
@"8bed1f7a8059064bcbeedb61a867cca7f63a474306011d0114280de631ac705e"
);
assert_snapshot!(format_file_list(build.source_dist_list_files, src.path()), @"
built_by_uv-0.1.0/PKG-INFO (generated)
built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE)
built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT)
built_by_uv-0.1.0/README.md (README.md)
built_by_uv-0.1.0/assets/data.csv (assets/data.csv)
built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h)
built_by_uv-0.1.0/pyproject.toml (pyproject.toml)
built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh)
built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py)
built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt)
built_by_uv-0.1.0/src/built_by_uv/build-only.h (src/built_by_uv/build-only.h)
built_by_uv-0.1.0/src/built_by_uv/cli.py (src/built_by_uv/cli.py)
built_by_uv-0.1.0/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt)
");
assert_snapshot!(build.source_dist_contents.iter().join("\n"), @"
built_by_uv-0.1.0/
built_by_uv-0.1.0/LICENSE-APACHE
built_by_uv-0.1.0/LICENSE-MIT
built_by_uv-0.1.0/PKG-INFO
built_by_uv-0.1.0/README.md
built_by_uv-0.1.0/assets
built_by_uv-0.1.0/assets/data.csv
built_by_uv-0.1.0/header
built_by_uv-0.1.0/header/built_by_uv.h
built_by_uv-0.1.0/pyproject.toml
built_by_uv-0.1.0/scripts
built_by_uv-0.1.0/scripts/whoami.sh
built_by_uv-0.1.0/src
built_by_uv-0.1.0/src/built_by_uv
built_by_uv-0.1.0/src/built_by_uv/__init__.py
built_by_uv-0.1.0/src/built_by_uv/arithmetic
built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py
built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py
built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt
built_by_uv-0.1.0/src/built_by_uv/build-only.h
built_by_uv-0.1.0/src/built_by_uv/cli.py
built_by_uv-0.1.0/third-party-licenses
built_by_uv-0.1.0/third-party-licenses/PEP-401.txt
");
let wheel_path = dist.path().join(build.wheel_filename.to_string());
assert_eq!(
build.wheel_filename.to_string(),
"built_by_uv-0.1.0-py3-none-any.whl"
);
assert_snapshot!(
format!("{:x}", sha2::Sha256::digest(fs_err::read(&wheel_path).unwrap())),
@"dbe56fd8bd52184095b2e0ea3e83c95d1bc8b4aa53cf469cec5af62251b24abb"
);
assert_snapshot!(build.wheel_contents.join("\n"), @"
built_by_uv-0.1.0.data/data/
built_by_uv-0.1.0.data/data/data.csv
built_by_uv-0.1.0.data/headers/
built_by_uv-0.1.0.data/headers/built_by_uv.h
built_by_uv-0.1.0.data/scripts/
built_by_uv-0.1.0.data/scripts/whoami.sh
built_by_uv-0.1.0.dist-info/
built_by_uv-0.1.0.dist-info/METADATA
built_by_uv-0.1.0.dist-info/RECORD
built_by_uv-0.1.0.dist-info/WHEEL
built_by_uv-0.1.0.dist-info/entry_points.txt
built_by_uv-0.1.0.dist-info/licenses/
built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE
built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT
built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/
built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt
built_by_uv/
built_by_uv/__init__.py
built_by_uv/arithmetic/
built_by_uv/arithmetic/__init__.py
built_by_uv/arithmetic/circle.py
built_by_uv/arithmetic/pi.txt
built_by_uv/cli.py
");
assert_snapshot!(format_file_list(build.wheel_list_files, src.path()), @"
built_by_uv/__init__.py (src/built_by_uv/__init__.py)
built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt)
built_by_uv/cli.py (src/built_by_uv/cli.py)
built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE (LICENSE-APACHE)
built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT (LICENSE-MIT)
built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt)
built_by_uv-0.1.0.data/headers/built_by_uv.h (header/built_by_uv.h)
built_by_uv-0.1.0.data/scripts/whoami.sh (scripts/whoami.sh)
built_by_uv-0.1.0.data/data/data.csv (assets/data.csv)
built_by_uv-0.1.0.dist-info/WHEEL (generated)
built_by_uv-0.1.0.dist-info/entry_points.txt (generated)
built_by_uv-0.1.0.dist-info/METADATA (generated)
");
let mut wheel = zip::ZipArchive::new(File::open(wheel_path).unwrap()).unwrap();
let mut record = String::new();
wheel
.by_name("built_by_uv-0.1.0.dist-info/RECORD")
.unwrap()
.read_to_string(&mut record)
.unwrap();
assert_snapshot!(record, @"
built_by_uv/__init__.py,sha256=AJ7XpTNWxYktP97ydb81UpnNqoebH7K4sHRakAMQKG4,44
built_by_uv/arithmetic/__init__.py,sha256=x2agwFbJAafc9Z6TdJ0K6b6bLMApQdvRSQjP4iy7IEI,67
built_by_uv/arithmetic/circle.py,sha256=FYZkv6KwrF9nJcwGOKigjke1dm1Fkie7qW1lWJoh3AE,287
built_by_uv/arithmetic/pi.txt,sha256=-4HqoLoIrSKGf0JdTrM8BTTiIz8rq-MSCDL6LeF0iuU,8
built_by_uv/cli.py,sha256=Jcm3PxSb8wTAN3dGm5vKEDQwCgoUXkoeggZeF34QyKM,44
built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT,sha256=F5Z0Cpu8QWyblXwXhrSo0b9WmYXQxd1LwLjVLJZwbiI,1077
built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt,sha256=KN-KAx829G2saLjVmByc08RFFtIDWvHulqPyD0qEBZI,270
built_by_uv-0.1.0.data/headers/built_by_uv.h,sha256=p5-HBunJ1dY-xd4dMn03PnRClmGyRosScIp8rT46kg4,144
built_by_uv-0.1.0.data/scripts/whoami.sh,sha256=T2cmhuDFuX-dTkiSkuAmNyIzvv8AKopjnuTCcr9o-eE,20
built_by_uv-0.1.0.data/data/data.csv,sha256=7z7u-wXu7Qr2eBZFVpBILlNUiGSngv_1vYqZHVWOU94,265
built_by_uv-0.1.0.dist-info/WHEEL,sha256=JBpLtoa_WBz5WPGpRsAUTD4Dz6H0KkkdiKWCkfMSS1U,84
built_by_uv-0.1.0.dist-info/entry_points.txt,sha256=-IO6yaq6x6HSl-zWH96rZmgYvfyHlH00L5WQoCpz-YI,50
built_by_uv-0.1.0.dist-info/METADATA,sha256=m6EkVvKrGmqx43b_VR45LHD37IZxPYC0NI6Qx9_UXLE,474
built_by_uv-0.1.0.dist-info/RECORD,,
");
}
#[test]
fn license_file_pre_pep639() {
let src = TempDir::new().unwrap();
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "pep-pep639-license"
version = "1.0.0"
license = { file = "license.txt" }
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
},
)
.unwrap();
fs_err::create_dir_all(src.path().join("src").join("pep_pep639_license")).unwrap();
File::create(
src.path()
.join("src")
.join("pep_pep639_license")
.join("__init__.py"),
)
.unwrap();
fs_err::write(
src.path().join("license.txt"),
"Copy carefully.\nSincerely, the authors",
)
.unwrap();
let output_dir = TempDir::new().unwrap();
build_source_dist(src.path(), output_dir.path(), "0.5.15", false).unwrap();
let sdist_tree = TempDir::new().unwrap();
let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz");
let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap());
let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
source_dist.unpack(sdist_tree.path()).unwrap();
{
let _preview = uv_preview::test::with_features(&[]);
build_wheel(
&sdist_tree.path().join("pep_pep639_license-1.0.0"),
output_dir.path(),
None,
"0.5.15",
false,
)
.unwrap();
}
let wheel = output_dir
.path()
.join("pep_pep639_license-1.0.0-py3-none-any.whl");
let mut wheel = zip::ZipArchive::new(File::open(wheel).unwrap()).unwrap();
let mut metadata = String::new();
wheel
.by_name("pep_pep639_license-1.0.0.dist-info/METADATA")
.unwrap()
.read_to_string(&mut metadata)
.unwrap();
assert_snapshot!(metadata, @"
Metadata-Version: 2.3
Name: pep-pep639-license
Version: 1.0.0
License: Copy carefully.
Sincerely, the authors
");
}
#[test]
fn prepare_metadata_then_build_wheel() {
let src = TempDir::new().unwrap();
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "two-step-build"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
},
)
.unwrap();
fs_err::create_dir_all(src.path().join("src").join("two_step_build")).unwrap();
File::create(
src.path()
.join("src")
.join("two_step_build")
.join("__init__.py"),
)
.unwrap();
let metadata_dir = {
let _preview = uv_preview::test::with_features(&[]);
TempDir::new().unwrap()
};
let dist_info_dir = {
let _preview = uv_preview::test::with_features(&[]);
metadata(src.path(), metadata_dir.path(), "0.5.15").unwrap()
};
let metadata_prepared =
fs_err::read_to_string(metadata_dir.path().join(&dist_info_dir).join("METADATA"))
.unwrap();
let output_dir = TempDir::new().unwrap();
{
let _preview = uv_preview::test::with_features(&[]);
build_wheel(
src.path(),
output_dir.path(),
Some(&metadata_dir.path().join(&dist_info_dir)),
"0.5.15",
false,
)
.unwrap();
}
let wheel = output_dir
.path()
.join("two_step_build-1.0.0-py3-none-any.whl");
let mut wheel = zip::ZipArchive::new(File::open(wheel).unwrap()).unwrap();
let mut metadata_wheel = String::new();
wheel
.by_name("two_step_build-1.0.0.dist-info/METADATA")
.unwrap()
.read_to_string(&mut metadata_wheel)
.unwrap();
assert_eq!(metadata_prepared, metadata_wheel);
assert_snapshot!(metadata_wheel, @"
Metadata-Version: 2.3
Name: two-step-build
Version: 1.0.0
");
}
#[test]
fn test_glob_path_normalization() {
let src = TempDir::new().unwrap();
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "two-step-build"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-root = "./"
"#
},
)
.unwrap();
fs_err::create_dir_all(src.path().join("two_step_build")).unwrap();
File::create(src.path().join("two_step_build").join("__init__.py")).unwrap();
let dist = TempDir::new().unwrap();
let build1 = build(src.path(), dist.path(), &[]).unwrap();
assert_snapshot!(build1.source_dist_contents.join("\n"), @"
two_step_build-1.0.0/
two_step_build-1.0.0/PKG-INFO
two_step_build-1.0.0/pyproject.toml
two_step_build-1.0.0/two_step_build
two_step_build-1.0.0/two_step_build/__init__.py
");
assert_snapshot!(build1.wheel_contents.join("\n"), @"
two_step_build-1.0.0.dist-info/
two_step_build-1.0.0.dist-info/METADATA
two_step_build-1.0.0.dist-info/RECORD
two_step_build-1.0.0.dist-info/WHEEL
two_step_build/
two_step_build/__init__.py
");
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "two-step-build"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-root = "two_step_build/.././"
"#
},
)
.unwrap();
let dist = TempDir::new().unwrap();
let build2 = build(src.path(), dist.path(), &[]).unwrap();
assert_eq!(build1, build2);
}
#[test]
fn test_camel_case() {
let src = TempDir::new().unwrap();
let pyproject_toml = indoc! {r#"
[project]
name = "camelcase"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-name = "camelCase"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
fs_err::create_dir_all(src.path().join("src").join("camelCase")).unwrap();
File::create(src.path().join("src").join("camelCase").join("__init__.py")).unwrap();
let dist = TempDir::new().unwrap();
let build1 = build(src.path(), dist.path(), &[]).unwrap();
assert_snapshot!(build1.wheel_contents.join("\n"), @"
camelCase/
camelCase/__init__.py
camelcase-1.0.0.dist-info/
camelcase-1.0.0.dist-info/METADATA
camelcase-1.0.0.dist-info/RECORD
camelcase-1.0.0.dist-info/WHEEL
");
fs_err::write(
src.path().join("pyproject.toml"),
pyproject_toml.replace("camelCase", "camel_case"),
)
.unwrap();
let build_err = build(src.path(), dist.path(), &[]).unwrap_err();
let err_message = format_err(&build_err)
.replace(&src.path().user_display().to_string(), "[TEMP_PATH]")
.replace('\\', "/");
assert_snapshot!(
err_message,
@"Expected a Python module at: [TEMP_PATH]/src/camel_case/__init__.py"
);
}
#[test]
fn no_partial_files_on_build_failure() {
let src = TempDir::new().unwrap();
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "failing-build"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#},
)
.unwrap();
let dist = TempDir::new().unwrap();
let sdist_result = build_source_dist(src.path(), dist.path(), MOCK_UV_VERSION, false);
assert!(sdist_result.is_err());
let wheel_result = {
let _preview = uv_preview::test::with_features(&[]);
build_wheel(src.path(), dist.path(), None, MOCK_UV_VERSION, false)
};
assert!(wheel_result.is_err());
let dist_contents: Vec<_> = fs_err::read_dir(dist.path()).unwrap().collect();
assert!(
dist_contents.is_empty(),
"Expected empty dist directory, but found: {dist_contents:?}"
);
}
#[test]
fn existing_files_deleted_on_build_failure() {
let src = TempDir::new().unwrap();
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "failing-build"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#},
)
.unwrap();
let dist = TempDir::new().unwrap();
let sdist_path = dist.path().join("failing_build-1.0.0.tar.gz");
let wheel_path = dist.path().join("failing_build-1.0.0-py3-none-any.whl");
let old_content = b"old content";
fs_err::write(&sdist_path, old_content).unwrap();
fs_err::write(&wheel_path, old_content).unwrap();
let sdist_result = build_source_dist(src.path(), dist.path(), MOCK_UV_VERSION, false);
assert!(sdist_result.is_err());
let wheel_result = {
let _preview = uv_preview::test::with_features(&[]);
build_wheel(src.path(), dist.path(), None, MOCK_UV_VERSION, false)
};
assert!(wheel_result.is_err());
assert!(
!sdist_path.exists(),
"Pre-existing sdist should have been deleted"
);
assert!(
!wheel_path.exists(),
"Pre-existing wheel should have been deleted"
);
}
#[test]
fn existing_files_overwritten_on_success() {
let src = TempDir::new().unwrap();
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "overwrite-test"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#},
)
.unwrap();
fs_err::create_dir_all(src.path().join("src").join("overwrite_test")).unwrap();
File::create(
src.path()
.join("src")
.join("overwrite_test")
.join("__init__.py"),
)
.unwrap();
let dist = TempDir::new().unwrap();
let sdist_path = dist.path().join("overwrite_test-1.0.0.tar.gz");
let wheel_path = dist.path().join("overwrite_test-1.0.0-py3-none-any.whl");
let old_content = b"old content";
fs_err::write(&sdist_path, old_content).unwrap();
fs_err::write(&wheel_path, old_content).unwrap();
build_source_dist(src.path(), dist.path(), MOCK_UV_VERSION, false).unwrap();
{
let _preview = uv_preview::test::with_features(&[]);
build_wheel(src.path(), dist.path(), None, MOCK_UV_VERSION, false).unwrap();
}
assert_ne!(
&fs_err::read(&sdist_path).unwrap()[..],
&old_content[..],
"Source dist should have been overwritten"
);
assert_ne!(
&fs_err::read(&wheel_path).unwrap()[..],
&old_content[..],
"Wheel should have been overwritten"
);
assert!(
!sdist_contents(&sdist_path).is_empty(),
"sdist should be a valid archive"
);
assert!(
!wheel_contents(&wheel_path).is_empty(),
"wheel should be a valid archive"
);
}
#[test]
fn invalid_stubs_name() {
let src = TempDir::new().unwrap();
let pyproject_toml = indoc! {r#"
[project]
name = "camelcase"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-name = "django@home-stubs"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
let dist = TempDir::new().unwrap();
let build_err = build(src.path(), dist.path(), &[]).unwrap_err();
let err_message = format_err(&build_err);
assert_snapshot!(
err_message,
@"
Invalid module name: django@home-stubs
Caused by: Invalid character `@` at position 7 for identifier `django@home`, expected an underscore or an alphanumeric character
"
);
}
#[test]
fn stubs_package() {
let src = TempDir::new().unwrap();
let pyproject_toml = indoc! {r#"
[project]
name = "stuffed-bird-stubs"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
fs_err::create_dir_all(src.path().join("src").join("stuffed_bird-stubs")).unwrap();
let regular_init_py = src
.path()
.join("src")
.join("stuffed_bird-stubs")
.join("__init__.py");
File::create(®ular_init_py).unwrap();
let dist = TempDir::new().unwrap();
let build_err = build(src.path(), dist.path(), &[]).unwrap_err();
let err_message = format_err(&build_err)
.replace(&src.path().user_display().to_string(), "[TEMP_PATH]")
.replace('\\', "/");
assert_snapshot!(
err_message,
@"Expected a Python module at: [TEMP_PATH]/src/stuffed_bird-stubs/__init__.pyi"
);
fs_err::remove_file(regular_init_py).unwrap();
File::create(
src.path()
.join("src")
.join("stuffed_bird-stubs")
.join("__init__.pyi"),
)
.unwrap();
let build1 = build(src.path(), dist.path(), &[]).unwrap();
assert_snapshot!(build1.wheel_contents.join("\n"), @"
stuffed_bird-stubs/
stuffed_bird-stubs/__init__.pyi
stuffed_bird_stubs-1.0.0.dist-info/
stuffed_bird_stubs-1.0.0.dist-info/METADATA
stuffed_bird_stubs-1.0.0.dist-info/RECORD
stuffed_bird_stubs-1.0.0.dist-info/WHEEL
");
let pyproject_toml = indoc! {r#"
[project]
name = "stuffed-bird-stubs"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-name = "stuffed_bird-stubs"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
let build2 = build(src.path(), dist.path(), &[]).unwrap();
assert_eq!(build1.wheel_contents, build2.wheel_contents);
}
#[test]
fn simple_namespace_package() {
let src = TempDir::new().unwrap();
let pyproject_toml = indoc! {r#"
[project]
name = "simple-namespace-part"
version = "1.0.0"
[tool.uv.build-backend]
module-name = "simple_namespace.part"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
fs_err::create_dir_all(src.path().join("src").join("simple_namespace").join("part"))
.unwrap();
assert_snapshot!(
build_err(src.path()),
@"Expected a Python module at: [TEMP_PATH]/src/simple_namespace/part/__init__.py"
);
File::create(
src.path()
.join("src")
.join("simple_namespace")
.join("part")
.join("__init__.py"),
)
.unwrap();
let bogus_init_py = src
.path()
.join("src")
.join("simple_namespace")
.join("__init__.py");
File::create(&bogus_init_py).unwrap();
assert_snapshot!(
build_err(src.path()),
@"For namespace packages, `__init__.py[i]` is not allowed in parent directory: [TEMP_PATH]/src/simple_namespace"
);
fs_err::remove_file(bogus_init_py).unwrap();
let dist = TempDir::new().unwrap();
let build1 = build(src.path(), dist.path(), &[]).unwrap();
assert_snapshot!(build1.source_dist_contents.join("\n"), @"
simple_namespace_part-1.0.0/
simple_namespace_part-1.0.0/PKG-INFO
simple_namespace_part-1.0.0/pyproject.toml
simple_namespace_part-1.0.0/src
simple_namespace_part-1.0.0/src/simple_namespace
simple_namespace_part-1.0.0/src/simple_namespace/part
simple_namespace_part-1.0.0/src/simple_namespace/part/__init__.py
");
assert_snapshot!(build1.wheel_contents.join("\n"), @"
simple_namespace/
simple_namespace/part/
simple_namespace/part/__init__.py
simple_namespace_part-1.0.0.dist-info/
simple_namespace_part-1.0.0.dist-info/METADATA
simple_namespace_part-1.0.0.dist-info/RECORD
simple_namespace_part-1.0.0.dist-info/WHEEL
");
let pyproject_toml = indoc! {r#"
[project]
name = "simple-namespace-part"
version = "1.0.0"
[tool.uv.build-backend]
module-name = "simple_namespace.part"
namespace = true
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
let build2 = build(src.path(), dist.path(), &[]).unwrap();
assert_eq!(build1, build2);
}
#[test]
fn complex_namespace_package() {
let src = TempDir::new().unwrap();
let pyproject_toml = indoc! {r#"
[project]
name = "complex-namespace"
version = "1.0.0"
[tool.uv.build-backend]
namespace = true
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
fs_err::create_dir_all(
src.path()
.join("src")
.join("complex_namespace")
.join("part_a"),
)
.unwrap();
File::create(
src.path()
.join("src")
.join("complex_namespace")
.join("part_a")
.join("__init__.py"),
)
.unwrap();
fs_err::create_dir_all(
src.path()
.join("src")
.join("complex_namespace")
.join("part_b"),
)
.unwrap();
File::create(
src.path()
.join("src")
.join("complex_namespace")
.join("part_b")
.join("__init__.py"),
)
.unwrap();
let dist = TempDir::new().unwrap();
let build1 = build(src.path(), dist.path(), &[]).unwrap();
assert_snapshot!(build1.wheel_contents.join("\n"), @"
complex_namespace-1.0.0.dist-info/
complex_namespace-1.0.0.dist-info/METADATA
complex_namespace-1.0.0.dist-info/RECORD
complex_namespace-1.0.0.dist-info/WHEEL
complex_namespace/
complex_namespace/part_a/
complex_namespace/part_a/__init__.py
complex_namespace/part_b/
complex_namespace/part_b/__init__.py
");
let pyproject_toml = indoc! {r#"
[project]
name = "complex-namespace"
version = "1.0.0"
[tool.uv.build-backend]
module-name = "complex_namespace"
namespace = true
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
let build2 = build(src.path(), dist.path(), &[]).unwrap();
assert_eq!(build1, build2);
}
#[test]
fn stubs_namespace() {
let src = TempDir::new().unwrap();
let pyproject_toml = indoc! {r#"
[project]
name = "cloud.db.schema-stubs"
version = "1.0.0"
[tool.uv.build-backend]
module-name = "cloud-stubs.db.schema"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
fs_err::create_dir_all(
src.path()
.join("src")
.join("cloud-stubs")
.join("db")
.join("schema"),
)
.unwrap();
File::create(
src.path()
.join("src")
.join("cloud-stubs")
.join("db")
.join("schema")
.join("__init__.pyi"),
)
.unwrap();
let dist = TempDir::new().unwrap();
let build = build(src.path(), dist.path(), &[]).unwrap();
assert_snapshot!(build.wheel_contents.join("\n"), @"
cloud-stubs/
cloud-stubs/db/
cloud-stubs/db/schema/
cloud-stubs/db/schema/__init__.pyi
cloud_db_schema_stubs-1.0.0.dist-info/
cloud_db_schema_stubs-1.0.0.dist-info/METADATA
cloud_db_schema_stubs-1.0.0.dist-info/RECORD
cloud_db_schema_stubs-1.0.0.dist-info/WHEEL
");
}
#[test]
fn multiple_module_names() {
let src = TempDir::new().unwrap();
let pyproject_toml = indoc! {r#"
[project]
name = "simple-namespace-part"
version = "1.0.0"
[tool.uv.build-backend]
module-name = ["foo", "simple_namespace.part_a", "simple_namespace.part_b"]
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
fs_err::create_dir_all(src.path().join("src").join("foo")).unwrap();
fs_err::create_dir_all(
src.path()
.join("src")
.join("simple_namespace")
.join("part_a"),
)
.unwrap();
fs_err::create_dir_all(
src.path()
.join("src")
.join("simple_namespace")
.join("part_b"),
)
.unwrap();
assert_snapshot!(
build_err(src.path()),
@"Expected a Python module at: [TEMP_PATH]/src/foo/__init__.py"
);
File::create(src.path().join("src").join("foo").join("__init__.py")).unwrap();
assert_snapshot!(
build_err(src.path()),
@"Expected a Python module at: [TEMP_PATH]/src/simple_namespace/part_a/__init__.py"
);
File::create(
src.path()
.join("src")
.join("simple_namespace")
.join("part_a")
.join("__init__.py"),
)
.unwrap();
File::create(
src.path()
.join("src")
.join("simple_namespace")
.join("part_b")
.join("__init__.py"),
)
.unwrap();
let bogus_init_py = src
.path()
.join("src")
.join("simple_namespace")
.join("__init__.py");
File::create(&bogus_init_py).unwrap();
assert_snapshot!(
build_err(src.path()),
@"For namespace packages, `__init__.py[i]` is not allowed in parent directory: [TEMP_PATH]/src/simple_namespace"
);
fs_err::remove_file(bogus_init_py).unwrap();
let dist = TempDir::new().unwrap();
let build = build(src.path(), dist.path(), &[]).unwrap();
assert_snapshot!(build.source_dist_contents.join("\n"), @"
simple_namespace_part-1.0.0/
simple_namespace_part-1.0.0/PKG-INFO
simple_namespace_part-1.0.0/pyproject.toml
simple_namespace_part-1.0.0/src
simple_namespace_part-1.0.0/src/foo
simple_namespace_part-1.0.0/src/foo/__init__.py
simple_namespace_part-1.0.0/src/simple_namespace
simple_namespace_part-1.0.0/src/simple_namespace/part_a
simple_namespace_part-1.0.0/src/simple_namespace/part_a/__init__.py
simple_namespace_part-1.0.0/src/simple_namespace/part_b
simple_namespace_part-1.0.0/src/simple_namespace/part_b/__init__.py
");
assert_snapshot!(build.wheel_contents.join("\n"), @"
foo/
foo/__init__.py
simple_namespace/
simple_namespace/part_a/
simple_namespace/part_a/__init__.py
simple_namespace/part_b/
simple_namespace/part_b/__init__.py
simple_namespace_part-1.0.0.dist-info/
simple_namespace_part-1.0.0.dist-info/METADATA
simple_namespace_part-1.0.0.dist-info/RECORD
simple_namespace_part-1.0.0.dist-info/WHEEL
");
}
#[test]
fn test_prune_redundant_modules() {
fn check(input: &[&str], expect: &[&str]) {
let input = input.iter().map(|s| (*s).to_string()).collect();
let expect: Vec<_> = expect.iter().map(|s| (*s).to_string()).collect();
assert_eq!(prune_redundant_modules(input), expect);
}
check(&[], &[]);
check(&["foo"], &["foo"]);
check(&["foo", "bar"], &["bar", "foo"]);
check(&["foo", "foo.bar"], &["foo"]);
check(&["foo.bar", "foo"], &["foo"]);
check(
&["foo.bar.a", "foo.bar.b", "foo.bar", "foo", "foo.bar.a.c"],
&["foo"],
);
check(
&["bar.one", "bar.two", "baz", "bar", "baz.one"],
&["bar", "baz"],
);
check(&["foo", "foobar"], &["foo", "foobar"]);
check(
&["foo", "foobar", "foo.bar", "foobar.baz"],
&["foo", "foobar"],
);
check(&["foo.bar", "foo.baz"], &["foo.bar", "foo.baz"]);
check(&["foo", "foo", "foo.bar", "foo.bar"], &["foo"]);
check(
&[
"foo.inner",
"foo.inner.deeper",
"foo",
"bar",
"bar.sub",
"bar.sub.deep",
"foobar",
"baz.baz.bar",
"baz.baz",
"qux",
],
&["bar", "baz.baz", "foo", "foobar", "qux"],
);
}
#[test]
fn duplicate_module_names() {
let src = TempDir::new().unwrap();
let pyproject_toml = indoc! {r#"
[project]
name = "duplicate"
version = "1.0.0"
[tool.uv.build-backend]
module-name = ["foo", "foo", "bar.baz", "bar.baz.submodule"]
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
fs_err::create_dir_all(src.path().join("src").join("foo")).unwrap();
File::create(src.path().join("src").join("foo").join("__init__.py")).unwrap();
fs_err::create_dir_all(src.path().join("src").join("bar").join("baz")).unwrap();
File::create(
src.path()
.join("src")
.join("bar")
.join("baz")
.join("__init__.py"),
)
.unwrap();
let dist = TempDir::new().unwrap();
let build = build(src.path(), dist.path(), &[]).unwrap();
assert_snapshot!(build.source_dist_contents.join("\n"), @"
duplicate-1.0.0/
duplicate-1.0.0/PKG-INFO
duplicate-1.0.0/pyproject.toml
duplicate-1.0.0/src
duplicate-1.0.0/src/bar
duplicate-1.0.0/src/bar/baz
duplicate-1.0.0/src/bar/baz/__init__.py
duplicate-1.0.0/src/foo
duplicate-1.0.0/src/foo/__init__.py
");
assert_snapshot!(build.wheel_contents.join("\n"), @"
bar/
bar/baz/
bar/baz/__init__.py
duplicate-1.0.0.dist-info/
duplicate-1.0.0.dist-info/METADATA
duplicate-1.0.0.dist-info/RECORD
duplicate-1.0.0.dist-info/WHEEL
foo/
foo/__init__.py
");
}
#[test]
fn metadata_json_preview() {
let src = TempDir::new().unwrap();
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "metadata-json-preview"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
},
)
.unwrap();
fs_err::create_dir_all(src.path().join("src").join("metadata_json_preview")).unwrap();
File::create(
src.path()
.join("src")
.join("metadata_json_preview")
.join("__init__.py"),
)
.unwrap();
let dist = TempDir::new().unwrap();
let build = build(src.path(), dist.path(), &[PreviewFeature::MetadataJson]).unwrap();
assert_snapshot!(build.wheel_contents.join("\n"), @"
metadata_json_preview-1.0.0.dist-info/
metadata_json_preview-1.0.0.dist-info/METADATA
metadata_json_preview-1.0.0.dist-info/METADATA.json
metadata_json_preview-1.0.0.dist-info/RECORD
metadata_json_preview-1.0.0.dist-info/WHEEL
metadata_json_preview-1.0.0.dist-info/WHEEL.json
metadata_json_preview/
metadata_json_preview/__init__.py
");
}
}