1use itertools::Itertools;
2mod metadata;
3mod serde_verbatim;
4mod settings;
5mod source_dist;
6mod wheel;
7
8pub use metadata::{PyProjectToml, check_direct_build};
9pub use settings::{BuildBackendSettings, WheelDataIncludes};
10pub use source_dist::{build_source_dist, list_source_dist};
11use uv_warnings::warn_user_once;
12pub use wheel::{build_editable, build_wheel, list_wheel, metadata};
13
14use std::collections::HashSet;
15use std::ffi::OsStr;
16use std::io;
17use std::path::{Path, PathBuf};
18use std::str::FromStr;
19use thiserror::Error;
20use tracing::debug;
21use walkdir::DirEntry;
22
23use uv_fs::Simplified;
24use uv_globfilter::PortableGlobError;
25use uv_normalize::PackageName;
26use uv_pypi_types::{Identifier, IdentifierParseError};
27
28use crate::metadata::ValidationError;
29use crate::settings::ModuleName;
30
31#[derive(Debug, Error)]
32pub enum Error {
33 #[error(transparent)]
34 Io(#[from] io::Error),
35 #[error("Failed to persist temporary file to {}", _0.user_display())]
36 Persist(PathBuf, #[source] io::Error),
37 #[error("Invalid metadata format in: {}", _0.user_display())]
38 Toml(PathBuf, #[source] toml::de::Error),
39 #[error("Invalid project metadata")]
40 Validation(#[from] ValidationError),
41 #[error("Invalid module name: {0}")]
42 InvalidModuleName(String, #[source] IdentifierParseError),
43 #[error("Unsupported glob expression in: {field}")]
44 PortableGlob {
45 field: String,
46 #[source]
47 source: PortableGlobError,
48 },
49 #[error("Glob expressions caused to large regex in: {field}")]
51 GlobSetTooLarge {
52 field: String,
53 #[source]
54 source: globset::Error,
55 },
56 #[error("`pyproject.toml` must not be excluded from source distribution build")]
57 PyprojectTomlExcluded,
58 #[error("Failed to walk source tree: {}", root.user_display())]
59 WalkDir {
60 root: PathBuf,
61 #[source]
62 err: walkdir::Error,
63 },
64 #[error("Failed to write wheel zip archive")]
65 Zip(#[from] zip::result::ZipError),
66 #[error("Failed to write RECORD file")]
67 Csv(#[from] csv::Error),
68 #[error("Failed to write JSON metadata file")]
69 Json(#[source] serde_json::Error),
70 #[error("Expected a Python module at: {}", _0.user_display())]
71 MissingInitPy(PathBuf),
72 #[error("For namespace packages, `__init__.py[i]` is not allowed in parent directory: {}", _0.user_display())]
73 NotANamespace(PathBuf),
74 #[error("Module root must be inside the project: {}", _0.user_display())]
76 InvalidModuleRoot(PathBuf),
77 #[error("The path for the data directory {} must be inside the project: {}", name, path.user_display())]
79 InvalidDataRoot { name: String, path: PathBuf },
80 #[error("Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: {}", _0.user_display())]
81 VenvInSourceTree(PathBuf),
82 #[error("Inconsistent metadata between prepare and build step: {0}")]
83 InconsistentSteps(&'static str),
84 #[error("Failed to write to {}", _0.user_display())]
85 TarWrite(PathBuf, #[source] io::Error),
86}
87
88trait DirectoryWriter {
96 fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error>;
100
101 fn write_dir_entry(&mut self, entry: &DirEntry, target_path: &str) -> Result<(), Error> {
103 if entry.file_type().is_dir() {
104 self.write_directory(target_path)?;
105 } else {
106 self.write_file(target_path, entry.path())?;
107 }
108 Ok(())
109 }
110
111 fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error>;
113
114 fn write_directory(&mut self, directory: &str) -> Result<(), Error>;
116
117 fn close(self, dist_info_dir: &str) -> Result<(), Error>;
119}
120
121pub(crate) type FileList = Vec<(String, Option<PathBuf>)>;
123
124pub(crate) struct ListWriter<'a> {
126 files: &'a mut FileList,
127}
128
129impl<'a> ListWriter<'a> {
130 pub(crate) fn new(files: &'a mut FileList) -> Self {
132 Self { files }
133 }
134}
135
136impl DirectoryWriter for ListWriter<'_> {
137 fn write_bytes(&mut self, path: &str, _bytes: &[u8]) -> Result<(), Error> {
138 self.files.push((path.to_string(), None));
139 Ok(())
140 }
141
142 fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> {
143 self.files
144 .push((path.to_string(), Some(file.to_path_buf())));
145 Ok(())
146 }
147
148 fn write_directory(&mut self, _directory: &str) -> Result<(), Error> {
149 Ok(())
150 }
151
152 fn close(self, _dist_info_dir: &str) -> Result<(), Error> {
153 Ok(())
154 }
155}
156
157fn check_metadata_directory(
161 source_tree: &Path,
162 metadata_directory: Option<&Path>,
163 pyproject_toml: &PyProjectToml,
164) -> Result<(), Error> {
165 let Some(metadata_directory) = metadata_directory else {
166 return Ok(());
167 };
168
169 debug!(
170 "Checking metadata directory {}",
171 metadata_directory.user_display()
172 );
173
174 let current = pyproject_toml
176 .to_metadata(source_tree)?
177 .core_metadata_format();
178 let previous = fs_err::read_to_string(metadata_directory.join("METADATA"))?;
179 if previous != current {
180 return Err(Error::InconsistentSteps("METADATA"));
181 }
182
183 let entrypoints_path = metadata_directory.join("entry_points.txt");
185 match pyproject_toml.to_entry_points()? {
186 None => {
187 if entrypoints_path.is_file() {
188 return Err(Error::InconsistentSteps("entry_points.txt"));
189 }
190 }
191 Some(entrypoints) => {
192 if fs_err::read_to_string(&entrypoints_path)? != entrypoints {
193 return Err(Error::InconsistentSteps("entry_points.txt"));
194 }
195 }
196 }
197
198 Ok(())
199}
200
201fn prune_redundant_modules(mut names: Vec<String>) -> Vec<String> {
220 names.sort();
221 let mut pruned = Vec::with_capacity(names.len());
222 for name in names {
223 if let Some(last) = pruned.last() {
224 if name == *last {
225 continue;
226 }
227 if name
229 .strip_prefix(last)
230 .is_some_and(|suffix| suffix.starts_with('.'))
231 {
232 continue;
233 }
234 }
235 pruned.push(name);
236 }
237 pruned
238}
239
240fn prune_redundant_modules_warn(names: &[String], show_warnings: bool) -> Vec<String> {
242 let pruned = prune_redundant_modules(names.to_vec());
243 if show_warnings && names.len() != pruned.len() {
244 let mut pruned: HashSet<_> = pruned.iter().collect();
245 let ignored: Vec<_> = names.iter().filter(|name| !pruned.remove(name)).collect();
246 let s = if ignored.len() == 1 { "" } else { "s" };
247 warn_user_once!(
248 "Ignoring redundant module name{s} in `tool.uv.build-backend.module-name`: `{}`",
249 ignored.into_iter().join("`, `")
250 );
251 }
252 pruned
253}
254
255fn find_roots(
272 source_tree: &Path,
273 pyproject_toml: &PyProjectToml,
274 relative_module_root: &Path,
275 module_name: Option<&ModuleName>,
276 namespace: bool,
277 show_warnings: bool,
278) -> Result<(PathBuf, Vec<PathBuf>), Error> {
279 let relative_module_root = uv_fs::normalize_path(relative_module_root);
280 if !uv_fs::normalize_path(&source_tree.join(&relative_module_root))
282 .starts_with(uv_fs::normalize_path(source_tree))
283 {
284 return Err(Error::InvalidModuleRoot(relative_module_root.to_path_buf()));
285 }
286 let src_root = source_tree.join(&relative_module_root);
287 debug!("Source root: {}", src_root.user_display());
288
289 if namespace {
290 let modules_relative = if let Some(module_name) = module_name {
292 match module_name {
293 ModuleName::Name(name) => {
294 vec![name.split('.').collect::<PathBuf>()]
295 }
296 ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings)
297 .into_iter()
298 .map(|name| name.split('.').collect::<PathBuf>())
299 .collect(),
300 }
301 } else {
302 vec![PathBuf::from(
303 pyproject_toml.name().as_dist_info_name().to_string(),
304 )]
305 };
306 for module_relative in &modules_relative {
307 debug!("Namespace module path: {}", module_relative.user_display());
308 }
309 return Ok((src_root, modules_relative));
310 }
311
312 let modules_relative = if let Some(module_name) = module_name {
313 match module_name {
314 ModuleName::Name(name) => vec![module_path_from_module_name(&src_root, name)?],
315 ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings)
316 .into_iter()
317 .map(|name| module_path_from_module_name(&src_root, &name))
318 .collect::<Result<_, _>>()?,
319 }
320 } else {
321 vec![find_module_path_from_package_name(
322 &src_root,
323 pyproject_toml.name(),
324 )?]
325 };
326 for module_relative in &modules_relative {
327 debug!("Module path: {}", module_relative.user_display());
328 }
329 Ok((src_root, modules_relative))
330}
331
332fn find_module_path_from_package_name(
338 src_root: &Path,
339 package_name: &PackageName,
340) -> Result<PathBuf, Error> {
341 if let Some(stem) = package_name.to_string().strip_suffix("-stubs") {
342 debug!("Building stubs package instead of a regular package");
343 let module_name = PackageName::from_str(stem)
344 .expect("non-empty package name prefix must be valid package name")
345 .as_dist_info_name()
346 .to_string();
347 let module_relative = PathBuf::from(format!("{module_name}-stubs"));
348 let init_pyi = src_root.join(&module_relative).join("__init__.pyi");
349 if !init_pyi.is_file() {
350 return Err(Error::MissingInitPy(init_pyi));
351 }
352 Ok(module_relative)
353 } else {
354 let module_relative = PathBuf::from(package_name.as_dist_info_name().to_string());
356 let init_py = src_root.join(&module_relative).join("__init__.py");
357 if !init_py.is_file() {
358 return Err(Error::MissingInitPy(init_py));
359 }
360 Ok(module_relative)
361 }
362}
363
364fn module_path_from_module_name(src_root: &Path, module_name: &str) -> Result<PathBuf, Error> {
366 let module_relative = module_name.split('.').collect::<PathBuf>();
368
369 let (root_name, namespace_segments) =
371 if let Some((root_name, namespace_segments)) = module_name.split_once('.') {
372 (
373 root_name,
374 namespace_segments.split('.').collect::<Vec<&str>>(),
375 )
376 } else {
377 (module_name, Vec::new())
378 };
379
380 let stubs = if let Some(stem) = root_name.strip_suffix("-stubs") {
383 Identifier::from_str(stem)
385 .map_err(|err| Error::InvalidModuleName(module_name.to_string(), err))?;
386 true
387 } else {
388 Identifier::from_str(root_name)
389 .map_err(|err| Error::InvalidModuleName(module_name.to_string(), err))?;
390 false
391 };
392
393 for segment in namespace_segments {
395 Identifier::from_str(segment)
396 .map_err(|err| Error::InvalidModuleName(module_name.to_string(), err))?;
397 }
398
399 let init_py =
401 src_root
402 .join(&module_relative)
403 .join(if stubs { "__init__.pyi" } else { "__init__.py" });
404 if !init_py.is_file() {
405 return Err(Error::MissingInitPy(init_py));
406 }
407
408 for namespace_dir in module_relative.ancestors().skip(1) {
410 if src_root.join(namespace_dir).join("__init__.py").exists()
411 || src_root.join(namespace_dir).join("__init__.pyi").exists()
412 {
413 return Err(Error::NotANamespace(src_root.join(namespace_dir)));
414 }
415 }
416
417 Ok(module_relative)
418}
419
420pub(crate) fn error_on_venv(file_name: &OsStr, path: &Path) -> Result<(), Error> {
422 if !(file_name == "pyvenv.cfg" || file_name == "lib64") {
425 return Ok(());
426 }
427
428 let Some(parent) = path.parent() else {
429 return Ok(());
430 };
431
432 if parent.join("bin").join("python").is_symlink()
433 || parent.join("Scripts").join("python.exe").is_file()
434 {
435 return Err(Error::VenvInSourceTree(parent.to_path_buf()));
436 }
437
438 Ok(())
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use flate2::bufread::GzDecoder;
445 use fs_err::File;
446 use indoc::indoc;
447 use insta::assert_snapshot;
448 use itertools::Itertools;
449 use regex::Regex;
450 use sha2::Digest;
451 use std::io::{BufReader, Read};
452 use std::iter;
453 use tempfile::TempDir;
454 use uv_distribution_filename::{SourceDistFilename, WheelFilename};
455 use uv_fs::{copy_dir_all, relative_to};
456 use uv_preview::{Preview, PreviewFeature};
457
458 const MOCK_UV_VERSION: &str = "1.0.0+test";
459
460 fn format_err(err: &Error) -> String {
461 let context = iter::successors(std::error::Error::source(&err), |&err| err.source())
462 .map(|err| format!(" Caused by: {err}"))
463 .join("\n");
464 err.to_string() + "\n" + &context
465 }
466
467 #[derive(Debug, PartialEq, Eq)]
472 struct BuildResults {
473 source_dist_list_files: FileList,
474 source_dist_filename: SourceDistFilename,
475 source_dist_contents: Vec<String>,
476 wheel_list_files: FileList,
477 wheel_filename: WheelFilename,
478 wheel_contents: Vec<String>,
479 }
480
481 fn build(source_root: &Path, dist: &Path, preview: Preview) -> Result<BuildResults, Error> {
484 let (_name, direct_wheel_list_files) =
487 list_wheel(source_root, MOCK_UV_VERSION, false, preview)?;
488 let direct_wheel_filename =
489 build_wheel(source_root, dist, None, MOCK_UV_VERSION, false, preview)?;
490 let direct_wheel_path = dist.join(direct_wheel_filename.to_string());
491 let direct_wheel_contents = wheel_contents(&direct_wheel_path);
492 let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?);
493 fs_err::remove_file(&direct_wheel_path)?;
494
495 let (_name, source_dist_list_files) =
497 list_source_dist(source_root, MOCK_UV_VERSION, false)?;
498 let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false, preview)?;
501 let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION, false)?;
502 let source_dist_path = dist.join(source_dist_filename.to_string());
503 let source_dist_contents = sdist_contents(&source_dist_path);
504
505 let sdist_tree = TempDir::new()?;
507 let sdist_reader = BufReader::new(File::open(&source_dist_path)?);
508 let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
509 source_dist.unpack(sdist_tree.path())?;
510 let sdist_top_level_directory = sdist_tree.path().join(format!(
511 "{}-{}",
512 source_dist_filename.name.as_dist_info_name(),
513 source_dist_filename.version
514 ));
515 let wheel_filename = build_wheel(
516 &sdist_top_level_directory,
517 dist,
518 None,
519 MOCK_UV_VERSION,
520 false,
521 preview,
522 )?;
523 let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string()));
524
525 assert_eq!(direct_wheel_filename, wheel_filename);
527 assert_eq!(direct_wheel_contents, wheel_contents);
528 assert_eq!(direct_wheel_list_files, wheel_list_files);
529 assert_eq!(
530 direct_wheel_hash,
531 sha2::Sha256::digest(fs_err::read(dist.join(wheel_filename.to_string()))?)
532 );
533
534 Ok(BuildResults {
535 source_dist_list_files,
536 source_dist_filename,
537 source_dist_contents,
538 wheel_list_files,
539 wheel_filename,
540 wheel_contents,
541 })
542 }
543
544 fn build_err(source_root: &Path) -> String {
545 let dist = TempDir::new().unwrap();
546 let build_err = build(source_root, dist.path(), Preview::default()).unwrap_err();
547 let err_message: String = format_err(&build_err)
548 .replace(&source_root.user_display().to_string(), "[TEMP_PATH]")
549 .replace('\\', "/");
550 err_message
551 }
552
553 fn sdist_contents(source_dist_path: &Path) -> Vec<String> {
554 let sdist_reader = BufReader::new(File::open(source_dist_path).unwrap());
555 let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
556 let mut source_dist_contents: Vec<_> = source_dist
557 .entries()
558 .unwrap()
559 .map(|entry| {
560 entry
561 .unwrap()
562 .path()
563 .unwrap()
564 .to_str()
565 .unwrap()
566 .replace('\\', "/")
567 })
568 .collect();
569 source_dist_contents.sort();
570 source_dist_contents
571 }
572
573 fn wheel_contents(direct_output_dir: &Path) -> Vec<String> {
574 let wheel = zip::ZipArchive::new(File::open(direct_output_dir).unwrap()).unwrap();
575 let mut wheel_contents: Vec<_> = wheel
576 .file_names()
577 .map(|path| path.replace('\\', "/"))
578 .collect();
579 wheel_contents.sort_unstable();
580 wheel_contents
581 }
582
583 fn format_file_list(file_list: FileList, src: &Path) -> String {
584 file_list
585 .into_iter()
586 .map(|(path, source)| {
587 let path = path.replace('\\', "/");
588 if let Some(source) = source {
589 let source = relative_to(source, src)
590 .unwrap()
591 .portable_display()
592 .to_string();
593 format!("{path} ({source})")
594 } else {
595 format!("{path} (generated)")
596 }
597 })
598 .join("\n")
599 }
600
601 #[test]
609 fn built_by_uv_building() {
610 let built_by_uv = Path::new("../../test/packages/built-by-uv");
611 let src = TempDir::new().unwrap();
612 for dir in [
613 "src",
614 "tests",
615 "data-dir",
616 "third-party-licenses",
617 "assets",
618 "header",
619 "scripts",
620 ] {
621 copy_dir_all(built_by_uv.join(dir), src.path().join(dir)).unwrap();
622 }
623 for filename in [
624 "pyproject.toml",
625 "README.md",
626 "uv.lock",
627 "LICENSE-APACHE",
628 "LICENSE-MIT",
629 ] {
630 fs_err::copy(built_by_uv.join(filename), src.path().join(filename)).unwrap();
631 }
632
633 #[cfg(unix)]
638 {
639 use std::os::unix::fs::PermissionsExt;
640 let path = src.path().join("scripts").join("whoami.sh");
641 let metadata = fs_err::metadata(&path).unwrap();
642 let mut perms = metadata.permissions();
643 perms.set_mode(perms.mode() & !0o111);
644 fs_err::set_permissions(&path, perms).unwrap();
645 }
646
647 let pyproject_toml = fs_err::read_to_string(src.path().join("pyproject.toml")).unwrap();
649 let current_requires =
650 Regex::new(r#"requires = \["uv_build>=[0-9.]+,<[0-9.]+"\]"#).unwrap();
651 let mocked_requires = r#"requires = ["uv_build>=1,<2"]"#;
652 let pyproject_toml = current_requires.replace(pyproject_toml.as_str(), mocked_requires);
653 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml.as_bytes()).unwrap();
654
655 let module_root = src.path().join("src").join("built_by_uv");
657 fs_err::create_dir_all(module_root.join("__pycache__")).unwrap();
658 File::create(module_root.join("__pycache__").join("compiled.pyc")).unwrap();
659 File::create(module_root.join("arithmetic").join("circle.pyc")).unwrap();
660
661 let dist = TempDir::new().unwrap();
663 let build = build(src.path(), dist.path(), Preview::default()).unwrap();
664
665 let source_dist_path = dist.path().join(build.source_dist_filename.to_string());
666 assert_eq!(
667 build.source_dist_filename.to_string(),
668 "built_by_uv-0.1.0.tar.gz"
669 );
670 assert_snapshot!(
672 format!("{:x}", sha2::Sha256::digest(fs_err::read(&source_dist_path).unwrap())),
673 @"bb74bff575b135bb39e5c9bce56349441fb0923bb8857e32a5eaf34ec1843967"
674 );
675 assert_snapshot!(format_file_list(build.source_dist_list_files, src.path()), @"
677 built_by_uv-0.1.0/PKG-INFO (generated)
678 built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE)
679 built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT)
680 built_by_uv-0.1.0/README.md (README.md)
681 built_by_uv-0.1.0/assets/data.csv (assets/data.csv)
682 built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h)
683 built_by_uv-0.1.0/pyproject.toml (pyproject.toml)
684 built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh)
685 built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py)
686 built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
687 built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
688 built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt)
689 built_by_uv-0.1.0/src/built_by_uv/build-only.h (src/built_by_uv/build-only.h)
690 built_by_uv-0.1.0/src/built_by_uv/cli.py (src/built_by_uv/cli.py)
691 built_by_uv-0.1.0/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt)
692 ");
693 assert_snapshot!(build.source_dist_contents.iter().join("\n"), @"
694 built_by_uv-0.1.0/
695 built_by_uv-0.1.0/LICENSE-APACHE
696 built_by_uv-0.1.0/LICENSE-MIT
697 built_by_uv-0.1.0/PKG-INFO
698 built_by_uv-0.1.0/README.md
699 built_by_uv-0.1.0/assets
700 built_by_uv-0.1.0/assets/data.csv
701 built_by_uv-0.1.0/header
702 built_by_uv-0.1.0/header/built_by_uv.h
703 built_by_uv-0.1.0/pyproject.toml
704 built_by_uv-0.1.0/scripts
705 built_by_uv-0.1.0/scripts/whoami.sh
706 built_by_uv-0.1.0/src
707 built_by_uv-0.1.0/src/built_by_uv
708 built_by_uv-0.1.0/src/built_by_uv/__init__.py
709 built_by_uv-0.1.0/src/built_by_uv/arithmetic
710 built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py
711 built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py
712 built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt
713 built_by_uv-0.1.0/src/built_by_uv/build-only.h
714 built_by_uv-0.1.0/src/built_by_uv/cli.py
715 built_by_uv-0.1.0/third-party-licenses
716 built_by_uv-0.1.0/third-party-licenses/PEP-401.txt
717 ");
718
719 let wheel_path = dist.path().join(build.wheel_filename.to_string());
720 assert_eq!(
721 build.wheel_filename.to_string(),
722 "built_by_uv-0.1.0-py3-none-any.whl"
723 );
724 assert_snapshot!(
726 format!("{:x}", sha2::Sha256::digest(fs_err::read(&wheel_path).unwrap())),
727 @"dbe56fd8bd52184095b2e0ea3e83c95d1bc8b4aa53cf469cec5af62251b24abb"
728 );
729 assert_snapshot!(build.wheel_contents.join("\n"), @"
730 built_by_uv-0.1.0.data/data/
731 built_by_uv-0.1.0.data/data/data.csv
732 built_by_uv-0.1.0.data/headers/
733 built_by_uv-0.1.0.data/headers/built_by_uv.h
734 built_by_uv-0.1.0.data/scripts/
735 built_by_uv-0.1.0.data/scripts/whoami.sh
736 built_by_uv-0.1.0.dist-info/
737 built_by_uv-0.1.0.dist-info/METADATA
738 built_by_uv-0.1.0.dist-info/RECORD
739 built_by_uv-0.1.0.dist-info/WHEEL
740 built_by_uv-0.1.0.dist-info/entry_points.txt
741 built_by_uv-0.1.0.dist-info/licenses/
742 built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE
743 built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT
744 built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/
745 built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt
746 built_by_uv/
747 built_by_uv/__init__.py
748 built_by_uv/arithmetic/
749 built_by_uv/arithmetic/__init__.py
750 built_by_uv/arithmetic/circle.py
751 built_by_uv/arithmetic/pi.txt
752 built_by_uv/cli.py
753 ");
754 assert_snapshot!(format_file_list(build.wheel_list_files, src.path()), @"
755 built_by_uv/__init__.py (src/built_by_uv/__init__.py)
756 built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
757 built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
758 built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt)
759 built_by_uv/cli.py (src/built_by_uv/cli.py)
760 built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE (LICENSE-APACHE)
761 built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT (LICENSE-MIT)
762 built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt)
763 built_by_uv-0.1.0.data/headers/built_by_uv.h (header/built_by_uv.h)
764 built_by_uv-0.1.0.data/scripts/whoami.sh (scripts/whoami.sh)
765 built_by_uv-0.1.0.data/data/data.csv (assets/data.csv)
766 built_by_uv-0.1.0.dist-info/WHEEL (generated)
767 built_by_uv-0.1.0.dist-info/entry_points.txt (generated)
768 built_by_uv-0.1.0.dist-info/METADATA (generated)
769 ");
770
771 let mut wheel = zip::ZipArchive::new(File::open(wheel_path).unwrap()).unwrap();
772 let mut record = String::new();
773 wheel
774 .by_name("built_by_uv-0.1.0.dist-info/RECORD")
775 .unwrap()
776 .read_to_string(&mut record)
777 .unwrap();
778 assert_snapshot!(record, @"
779 built_by_uv/__init__.py,sha256=AJ7XpTNWxYktP97ydb81UpnNqoebH7K4sHRakAMQKG4,44
780 built_by_uv/arithmetic/__init__.py,sha256=x2agwFbJAafc9Z6TdJ0K6b6bLMApQdvRSQjP4iy7IEI,67
781 built_by_uv/arithmetic/circle.py,sha256=FYZkv6KwrF9nJcwGOKigjke1dm1Fkie7qW1lWJoh3AE,287
782 built_by_uv/arithmetic/pi.txt,sha256=-4HqoLoIrSKGf0JdTrM8BTTiIz8rq-MSCDL6LeF0iuU,8
783 built_by_uv/cli.py,sha256=Jcm3PxSb8wTAN3dGm5vKEDQwCgoUXkoeggZeF34QyKM,44
784 built_by_uv-0.1.0.dist-info/licenses/LICENSE-APACHE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
785 built_by_uv-0.1.0.dist-info/licenses/LICENSE-MIT,sha256=F5Z0Cpu8QWyblXwXhrSo0b9WmYXQxd1LwLjVLJZwbiI,1077
786 built_by_uv-0.1.0.dist-info/licenses/third-party-licenses/PEP-401.txt,sha256=KN-KAx829G2saLjVmByc08RFFtIDWvHulqPyD0qEBZI,270
787 built_by_uv-0.1.0.data/headers/built_by_uv.h,sha256=p5-HBunJ1dY-xd4dMn03PnRClmGyRosScIp8rT46kg4,144
788 built_by_uv-0.1.0.data/scripts/whoami.sh,sha256=T2cmhuDFuX-dTkiSkuAmNyIzvv8AKopjnuTCcr9o-eE,20
789 built_by_uv-0.1.0.data/data/data.csv,sha256=7z7u-wXu7Qr2eBZFVpBILlNUiGSngv_1vYqZHVWOU94,265
790 built_by_uv-0.1.0.dist-info/WHEEL,sha256=JBpLtoa_WBz5WPGpRsAUTD4Dz6H0KkkdiKWCkfMSS1U,84
791 built_by_uv-0.1.0.dist-info/entry_points.txt,sha256=-IO6yaq6x6HSl-zWH96rZmgYvfyHlH00L5WQoCpz-YI,50
792 built_by_uv-0.1.0.dist-info/METADATA,sha256=m6EkVvKrGmqx43b_VR45LHD37IZxPYC0NI6Qx9_UXLE,474
793 built_by_uv-0.1.0.dist-info/RECORD,,
794 ");
795 }
796
797 #[test]
799 fn license_file_pre_pep639() {
800 let src = TempDir::new().unwrap();
801 fs_err::write(
802 src.path().join("pyproject.toml"),
803 indoc! {r#"
804 [project]
805 name = "pep-pep639-license"
806 version = "1.0.0"
807 license = { file = "license.txt" }
808
809 [build-system]
810 requires = ["uv_build>=0.5.15,<0.6.0"]
811 build-backend = "uv_build"
812 "#
813 },
814 )
815 .unwrap();
816 fs_err::create_dir_all(src.path().join("src").join("pep_pep639_license")).unwrap();
817 File::create(
818 src.path()
819 .join("src")
820 .join("pep_pep639_license")
821 .join("__init__.py"),
822 )
823 .unwrap();
824 fs_err::write(
825 src.path().join("license.txt"),
826 "Copy carefully.\nSincerely, the authors",
827 )
828 .unwrap();
829
830 let output_dir = TempDir::new().unwrap();
832 build_source_dist(src.path(), output_dir.path(), "0.5.15", false).unwrap();
833 let sdist_tree = TempDir::new().unwrap();
834 let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz");
835 let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap());
836 let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader));
837 source_dist.unpack(sdist_tree.path()).unwrap();
838 build_wheel(
839 &sdist_tree.path().join("pep_pep639_license-1.0.0"),
840 output_dir.path(),
841 None,
842 "0.5.15",
843 false,
844 Preview::default(),
845 )
846 .unwrap();
847 let wheel = output_dir
848 .path()
849 .join("pep_pep639_license-1.0.0-py3-none-any.whl");
850 let mut wheel = zip::ZipArchive::new(File::open(wheel).unwrap()).unwrap();
851
852 let mut metadata = String::new();
853 wheel
854 .by_name("pep_pep639_license-1.0.0.dist-info/METADATA")
855 .unwrap()
856 .read_to_string(&mut metadata)
857 .unwrap();
858
859 assert_snapshot!(metadata, @"
860 Metadata-Version: 2.3
861 Name: pep-pep639-license
862 Version: 1.0.0
863 License: Copy carefully.
864 Sincerely, the authors
865 ");
866 }
867
868 #[test]
870 fn prepare_metadata_then_build_wheel() {
871 let src = TempDir::new().unwrap();
872 fs_err::write(
873 src.path().join("pyproject.toml"),
874 indoc! {r#"
875 [project]
876 name = "two-step-build"
877 version = "1.0.0"
878
879 [build-system]
880 requires = ["uv_build>=0.5.15,<0.6.0"]
881 build-backend = "uv_build"
882 "#
883 },
884 )
885 .unwrap();
886 fs_err::create_dir_all(src.path().join("src").join("two_step_build")).unwrap();
887 File::create(
888 src.path()
889 .join("src")
890 .join("two_step_build")
891 .join("__init__.py"),
892 )
893 .unwrap();
894
895 let metadata_dir = TempDir::new().unwrap();
897 let dist_info_dir = metadata(
898 src.path(),
899 metadata_dir.path(),
900 "0.5.15",
901 Preview::default(),
902 )
903 .unwrap();
904 let metadata_prepared =
905 fs_err::read_to_string(metadata_dir.path().join(&dist_info_dir).join("METADATA"))
906 .unwrap();
907
908 let output_dir = TempDir::new().unwrap();
910 build_wheel(
911 src.path(),
912 output_dir.path(),
913 Some(&metadata_dir.path().join(&dist_info_dir)),
914 "0.5.15",
915 false,
916 Preview::default(),
917 )
918 .unwrap();
919 let wheel = output_dir
920 .path()
921 .join("two_step_build-1.0.0-py3-none-any.whl");
922 let mut wheel = zip::ZipArchive::new(File::open(wheel).unwrap()).unwrap();
923
924 let mut metadata_wheel = String::new();
925 wheel
926 .by_name("two_step_build-1.0.0.dist-info/METADATA")
927 .unwrap()
928 .read_to_string(&mut metadata_wheel)
929 .unwrap();
930
931 assert_eq!(metadata_prepared, metadata_wheel);
932
933 assert_snapshot!(metadata_wheel, @"
934 Metadata-Version: 2.3
935 Name: two-step-build
936 Version: 1.0.0
937 ");
938 }
939
940 #[test]
942 fn test_glob_path_normalization() {
943 let src = TempDir::new().unwrap();
944 fs_err::write(
945 src.path().join("pyproject.toml"),
946 indoc! {r#"
947 [project]
948 name = "two-step-build"
949 version = "1.0.0"
950
951 [build-system]
952 requires = ["uv_build>=0.5.15,<0.6.0"]
953 build-backend = "uv_build"
954
955 [tool.uv.build-backend]
956 module-root = "./"
957 "#
958 },
959 )
960 .unwrap();
961
962 fs_err::create_dir_all(src.path().join("two_step_build")).unwrap();
963 File::create(src.path().join("two_step_build").join("__init__.py")).unwrap();
964
965 let dist = TempDir::new().unwrap();
966 let build1 = build(src.path(), dist.path(), Preview::default()).unwrap();
967
968 assert_snapshot!(build1.source_dist_contents.join("\n"), @"
969 two_step_build-1.0.0/
970 two_step_build-1.0.0/PKG-INFO
971 two_step_build-1.0.0/pyproject.toml
972 two_step_build-1.0.0/two_step_build
973 two_step_build-1.0.0/two_step_build/__init__.py
974 ");
975
976 assert_snapshot!(build1.wheel_contents.join("\n"), @"
977 two_step_build-1.0.0.dist-info/
978 two_step_build-1.0.0.dist-info/METADATA
979 two_step_build-1.0.0.dist-info/RECORD
980 two_step_build-1.0.0.dist-info/WHEEL
981 two_step_build/
982 two_step_build/__init__.py
983 ");
984
985 fs_err::write(
987 src.path().join("pyproject.toml"),
988 indoc! {r#"
989 [project]
990 name = "two-step-build"
991 version = "1.0.0"
992
993 [build-system]
994 requires = ["uv_build>=0.5.15,<0.6.0"]
995 build-backend = "uv_build"
996
997 [tool.uv.build-backend]
998 module-root = "two_step_build/.././"
999 "#
1000 },
1001 )
1002 .unwrap();
1003
1004 let dist = TempDir::new().unwrap();
1005 let build2 = build(src.path(), dist.path(), Preview::default()).unwrap();
1006 assert_eq!(build1, build2);
1007 }
1008
1009 #[test]
1011 fn test_camel_case() {
1012 let src = TempDir::new().unwrap();
1013 let pyproject_toml = indoc! {r#"
1014 [project]
1015 name = "camelcase"
1016 version = "1.0.0"
1017
1018 [build-system]
1019 requires = ["uv_build>=0.5.15,<0.6.0"]
1020 build-backend = "uv_build"
1021
1022 [tool.uv.build-backend]
1023 module-name = "camelCase"
1024 "#
1025 };
1026 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1027
1028 fs_err::create_dir_all(src.path().join("src").join("camelCase")).unwrap();
1029 File::create(src.path().join("src").join("camelCase").join("__init__.py")).unwrap();
1030
1031 let dist = TempDir::new().unwrap();
1032 let build1 = build(src.path(), dist.path(), Preview::default()).unwrap();
1033
1034 assert_snapshot!(build1.wheel_contents.join("\n"), @"
1035 camelCase/
1036 camelCase/__init__.py
1037 camelcase-1.0.0.dist-info/
1038 camelcase-1.0.0.dist-info/METADATA
1039 camelcase-1.0.0.dist-info/RECORD
1040 camelcase-1.0.0.dist-info/WHEEL
1041 ");
1042
1043 fs_err::write(
1045 src.path().join("pyproject.toml"),
1046 pyproject_toml.replace("camelCase", "camel_case"),
1047 )
1048 .unwrap();
1049 let build_err = build(src.path(), dist.path(), Preview::default()).unwrap_err();
1050 let err_message = format_err(&build_err)
1051 .replace(&src.path().user_display().to_string(), "[TEMP_PATH]")
1052 .replace('\\', "/");
1053 assert_snapshot!(
1054 err_message,
1055 @"Expected a Python module at: [TEMP_PATH]/src/camel_case/__init__.py"
1056 );
1057 }
1058
1059 #[test]
1061 fn no_partial_files_on_build_failure() {
1062 let src = TempDir::new().unwrap();
1063
1064 fs_err::write(
1066 src.path().join("pyproject.toml"),
1067 indoc! {r#"
1068 [project]
1069 name = "failing-build"
1070 version = "1.0.0"
1071
1072 [build-system]
1073 requires = ["uv_build>=0.5.15,<0.6.0"]
1074 build-backend = "uv_build"
1075 "#},
1076 )
1077 .unwrap();
1078
1079 let dist = TempDir::new().unwrap();
1080
1081 let sdist_result = build_source_dist(src.path(), dist.path(), MOCK_UV_VERSION, false);
1083 assert!(sdist_result.is_err());
1084
1085 let wheel_result = build_wheel(
1087 src.path(),
1088 dist.path(),
1089 None,
1090 MOCK_UV_VERSION,
1091 false,
1092 Preview::default(),
1093 );
1094 assert!(wheel_result.is_err());
1095
1096 let dist_contents: Vec<_> = fs_err::read_dir(dist.path()).unwrap().collect();
1098 assert!(
1099 dist_contents.is_empty(),
1100 "Expected empty dist directory, but found: {dist_contents:?}"
1101 );
1102 }
1103
1104 #[test]
1106 fn existing_files_deleted_on_build_failure() {
1107 let src = TempDir::new().unwrap();
1108
1109 fs_err::write(
1111 src.path().join("pyproject.toml"),
1112 indoc! {r#"
1113 [project]
1114 name = "failing-build"
1115 version = "1.0.0"
1116
1117 [build-system]
1118 requires = ["uv_build>=0.5.15,<0.6.0"]
1119 build-backend = "uv_build"
1120 "#},
1121 )
1122 .unwrap();
1123
1124 let dist = TempDir::new().unwrap();
1125
1126 let sdist_path = dist.path().join("failing_build-1.0.0.tar.gz");
1128 let wheel_path = dist.path().join("failing_build-1.0.0-py3-none-any.whl");
1129 let old_content = b"old content";
1130 fs_err::write(&sdist_path, old_content).unwrap();
1131 fs_err::write(&wheel_path, old_content).unwrap();
1132
1133 let sdist_result = build_source_dist(src.path(), dist.path(), MOCK_UV_VERSION, false);
1135 assert!(sdist_result.is_err());
1136
1137 let wheel_result = build_wheel(
1138 src.path(),
1139 dist.path(),
1140 None,
1141 MOCK_UV_VERSION,
1142 false,
1143 Preview::default(),
1144 );
1145 assert!(wheel_result.is_err());
1146
1147 assert!(
1149 !sdist_path.exists(),
1150 "Pre-existing sdist should have been deleted"
1151 );
1152 assert!(
1153 !wheel_path.exists(),
1154 "Pre-existing wheel should have been deleted"
1155 );
1156 }
1157
1158 #[test]
1160 fn existing_files_overwritten_on_success() {
1161 let src = TempDir::new().unwrap();
1162
1163 fs_err::write(
1165 src.path().join("pyproject.toml"),
1166 indoc! {r#"
1167 [project]
1168 name = "overwrite-test"
1169 version = "1.0.0"
1170
1171 [build-system]
1172 requires = ["uv_build>=0.5.15,<0.6.0"]
1173 build-backend = "uv_build"
1174 "#},
1175 )
1176 .unwrap();
1177 fs_err::create_dir_all(src.path().join("src").join("overwrite_test")).unwrap();
1178 File::create(
1179 src.path()
1180 .join("src")
1181 .join("overwrite_test")
1182 .join("__init__.py"),
1183 )
1184 .unwrap();
1185
1186 let dist = TempDir::new().unwrap();
1187
1188 let sdist_path = dist.path().join("overwrite_test-1.0.0.tar.gz");
1190 let wheel_path = dist.path().join("overwrite_test-1.0.0-py3-none-any.whl");
1191 let old_content = b"old content";
1192 fs_err::write(&sdist_path, old_content).unwrap();
1193 fs_err::write(&wheel_path, old_content).unwrap();
1194
1195 build_source_dist(src.path(), dist.path(), MOCK_UV_VERSION, false).unwrap();
1197 build_wheel(
1198 src.path(),
1199 dist.path(),
1200 None,
1201 MOCK_UV_VERSION,
1202 false,
1203 Preview::default(),
1204 )
1205 .unwrap();
1206
1207 assert_ne!(
1209 &fs_err::read(&sdist_path).unwrap()[..],
1210 &old_content[..],
1211 "Source dist should have been overwritten"
1212 );
1213 assert_ne!(
1214 &fs_err::read(&wheel_path).unwrap()[..],
1215 &old_content[..],
1216 "Wheel should have been overwritten"
1217 );
1218
1219 assert!(
1221 !sdist_contents(&sdist_path).is_empty(),
1222 "sdist should be a valid archive"
1223 );
1224 assert!(
1225 !wheel_contents(&wheel_path).is_empty(),
1226 "wheel should be a valid archive"
1227 );
1228 }
1229
1230 #[test]
1231 fn invalid_stubs_name() {
1232 let src = TempDir::new().unwrap();
1233 let pyproject_toml = indoc! {r#"
1234 [project]
1235 name = "camelcase"
1236 version = "1.0.0"
1237
1238 [build-system]
1239 requires = ["uv_build>=0.5.15,<0.6.0"]
1240 build-backend = "uv_build"
1241
1242 [tool.uv.build-backend]
1243 module-name = "django@home-stubs"
1244 "#
1245 };
1246 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1247
1248 let dist = TempDir::new().unwrap();
1249 let build_err = build(src.path(), dist.path(), Preview::default()).unwrap_err();
1250 let err_message = format_err(&build_err);
1251 assert_snapshot!(
1252 err_message,
1253 @"
1254 Invalid module name: django@home-stubs
1255 Caused by: Invalid character `@` at position 7 for identifier `django@home`, expected an underscore or an alphanumeric character
1256 "
1257 );
1258 }
1259
1260 #[test]
1262 fn stubs_package() {
1263 let src = TempDir::new().unwrap();
1264 let pyproject_toml = indoc! {r#"
1265 [project]
1266 name = "stuffed-bird-stubs"
1267 version = "1.0.0"
1268
1269 [build-system]
1270 requires = ["uv_build>=0.5.15,<0.6.0"]
1271 build-backend = "uv_build"
1272 "#
1273 };
1274 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1275 fs_err::create_dir_all(src.path().join("src").join("stuffed_bird-stubs")).unwrap();
1276 let regular_init_py = src
1278 .path()
1279 .join("src")
1280 .join("stuffed_bird-stubs")
1281 .join("__init__.py");
1282 File::create(®ular_init_py).unwrap();
1283
1284 let dist = TempDir::new().unwrap();
1285 let build_err = build(src.path(), dist.path(), Preview::default()).unwrap_err();
1286 let err_message = format_err(&build_err)
1287 .replace(&src.path().user_display().to_string(), "[TEMP_PATH]")
1288 .replace('\\', "/");
1289 assert_snapshot!(
1290 err_message,
1291 @"Expected a Python module at: [TEMP_PATH]/src/stuffed_bird-stubs/__init__.pyi"
1292 );
1293
1294 fs_err::remove_file(regular_init_py).unwrap();
1296 File::create(
1297 src.path()
1298 .join("src")
1299 .join("stuffed_bird-stubs")
1300 .join("__init__.pyi"),
1301 )
1302 .unwrap();
1303
1304 let build1 = build(src.path(), dist.path(), Preview::default()).unwrap();
1305 assert_snapshot!(build1.wheel_contents.join("\n"), @"
1306 stuffed_bird-stubs/
1307 stuffed_bird-stubs/__init__.pyi
1308 stuffed_bird_stubs-1.0.0.dist-info/
1309 stuffed_bird_stubs-1.0.0.dist-info/METADATA
1310 stuffed_bird_stubs-1.0.0.dist-info/RECORD
1311 stuffed_bird_stubs-1.0.0.dist-info/WHEEL
1312 ");
1313
1314 let pyproject_toml = indoc! {r#"
1316 [project]
1317 name = "stuffed-bird-stubs"
1318 version = "1.0.0"
1319
1320 [build-system]
1321 requires = ["uv_build>=0.5.15,<0.6.0"]
1322 build-backend = "uv_build"
1323
1324 [tool.uv.build-backend]
1325 module-name = "stuffed_bird-stubs"
1326 "#
1327 };
1328 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1329
1330 let build2 = build(src.path(), dist.path(), Preview::default()).unwrap();
1331 assert_eq!(build1.wheel_contents, build2.wheel_contents);
1332 }
1333
1334 #[test]
1336 fn simple_namespace_package() {
1337 let src = TempDir::new().unwrap();
1338 let pyproject_toml = indoc! {r#"
1339 [project]
1340 name = "simple-namespace-part"
1341 version = "1.0.0"
1342
1343 [tool.uv.build-backend]
1344 module-name = "simple_namespace.part"
1345
1346 [build-system]
1347 requires = ["uv_build>=0.5.15,<0.6.0"]
1348 build-backend = "uv_build"
1349 "#
1350 };
1351 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1352 fs_err::create_dir_all(src.path().join("src").join("simple_namespace").join("part"))
1353 .unwrap();
1354
1355 assert_snapshot!(
1356 build_err(src.path()),
1357 @"Expected a Python module at: [TEMP_PATH]/src/simple_namespace/part/__init__.py"
1358 );
1359
1360 File::create(
1362 src.path()
1363 .join("src")
1364 .join("simple_namespace")
1365 .join("part")
1366 .join("__init__.py"),
1367 )
1368 .unwrap();
1369
1370 let bogus_init_py = src
1372 .path()
1373 .join("src")
1374 .join("simple_namespace")
1375 .join("__init__.py");
1376 File::create(&bogus_init_py).unwrap();
1377 assert_snapshot!(
1378 build_err(src.path()),
1379 @"For namespace packages, `__init__.py[i]` is not allowed in parent directory: [TEMP_PATH]/src/simple_namespace"
1380 );
1381 fs_err::remove_file(bogus_init_py).unwrap();
1382
1383 let dist = TempDir::new().unwrap();
1384 let build1 = build(src.path(), dist.path(), Preview::default()).unwrap();
1385 assert_snapshot!(build1.source_dist_contents.join("\n"), @"
1386 simple_namespace_part-1.0.0/
1387 simple_namespace_part-1.0.0/PKG-INFO
1388 simple_namespace_part-1.0.0/pyproject.toml
1389 simple_namespace_part-1.0.0/src
1390 simple_namespace_part-1.0.0/src/simple_namespace
1391 simple_namespace_part-1.0.0/src/simple_namespace/part
1392 simple_namespace_part-1.0.0/src/simple_namespace/part/__init__.py
1393 ");
1394 assert_snapshot!(build1.wheel_contents.join("\n"), @"
1395 simple_namespace/
1396 simple_namespace/part/
1397 simple_namespace/part/__init__.py
1398 simple_namespace_part-1.0.0.dist-info/
1399 simple_namespace_part-1.0.0.dist-info/METADATA
1400 simple_namespace_part-1.0.0.dist-info/RECORD
1401 simple_namespace_part-1.0.0.dist-info/WHEEL
1402 ");
1403
1404 let pyproject_toml = indoc! {r#"
1406 [project]
1407 name = "simple-namespace-part"
1408 version = "1.0.0"
1409
1410 [tool.uv.build-backend]
1411 module-name = "simple_namespace.part"
1412 namespace = true
1413
1414 [build-system]
1415 requires = ["uv_build>=0.5.15,<0.6.0"]
1416 build-backend = "uv_build"
1417 "#
1418 };
1419 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1420
1421 let build2 = build(src.path(), dist.path(), Preview::default()).unwrap();
1422 assert_eq!(build1, build2);
1423 }
1424
1425 #[test]
1427 fn complex_namespace_package() {
1428 let src = TempDir::new().unwrap();
1429 let pyproject_toml = indoc! {r#"
1430 [project]
1431 name = "complex-namespace"
1432 version = "1.0.0"
1433
1434 [tool.uv.build-backend]
1435 namespace = true
1436
1437 [build-system]
1438 requires = ["uv_build>=0.5.15,<0.6.0"]
1439 build-backend = "uv_build"
1440 "#
1441 };
1442 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1443 fs_err::create_dir_all(
1444 src.path()
1445 .join("src")
1446 .join("complex_namespace")
1447 .join("part_a"),
1448 )
1449 .unwrap();
1450 File::create(
1451 src.path()
1452 .join("src")
1453 .join("complex_namespace")
1454 .join("part_a")
1455 .join("__init__.py"),
1456 )
1457 .unwrap();
1458 fs_err::create_dir_all(
1459 src.path()
1460 .join("src")
1461 .join("complex_namespace")
1462 .join("part_b"),
1463 )
1464 .unwrap();
1465 File::create(
1466 src.path()
1467 .join("src")
1468 .join("complex_namespace")
1469 .join("part_b")
1470 .join("__init__.py"),
1471 )
1472 .unwrap();
1473
1474 let dist = TempDir::new().unwrap();
1475 let build1 = build(src.path(), dist.path(), Preview::default()).unwrap();
1476 assert_snapshot!(build1.wheel_contents.join("\n"), @"
1477 complex_namespace-1.0.0.dist-info/
1478 complex_namespace-1.0.0.dist-info/METADATA
1479 complex_namespace-1.0.0.dist-info/RECORD
1480 complex_namespace-1.0.0.dist-info/WHEEL
1481 complex_namespace/
1482 complex_namespace/part_a/
1483 complex_namespace/part_a/__init__.py
1484 complex_namespace/part_b/
1485 complex_namespace/part_b/__init__.py
1486 ");
1487
1488 let pyproject_toml = indoc! {r#"
1490 [project]
1491 name = "complex-namespace"
1492 version = "1.0.0"
1493
1494 [tool.uv.build-backend]
1495 module-name = "complex_namespace"
1496 namespace = true
1497
1498 [build-system]
1499 requires = ["uv_build>=0.5.15,<0.6.0"]
1500 build-backend = "uv_build"
1501 "#
1502 };
1503 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1504
1505 let build2 = build(src.path(), dist.path(), Preview::default()).unwrap();
1506 assert_eq!(build1, build2);
1507 }
1508
1509 #[test]
1511 fn stubs_namespace() {
1512 let src = TempDir::new().unwrap();
1513 let pyproject_toml = indoc! {r#"
1514 [project]
1515 name = "cloud.db.schema-stubs"
1516 version = "1.0.0"
1517
1518 [tool.uv.build-backend]
1519 module-name = "cloud-stubs.db.schema"
1520
1521 [build-system]
1522 requires = ["uv_build>=0.5.15,<0.6.0"]
1523 build-backend = "uv_build"
1524 "#
1525 };
1526 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1527 fs_err::create_dir_all(
1528 src.path()
1529 .join("src")
1530 .join("cloud-stubs")
1531 .join("db")
1532 .join("schema"),
1533 )
1534 .unwrap();
1535 File::create(
1536 src.path()
1537 .join("src")
1538 .join("cloud-stubs")
1539 .join("db")
1540 .join("schema")
1541 .join("__init__.pyi"),
1542 )
1543 .unwrap();
1544
1545 let dist = TempDir::new().unwrap();
1546 let build = build(src.path(), dist.path(), Preview::default()).unwrap();
1547 assert_snapshot!(build.wheel_contents.join("\n"), @"
1548 cloud-stubs/
1549 cloud-stubs/db/
1550 cloud-stubs/db/schema/
1551 cloud-stubs/db/schema/__init__.pyi
1552 cloud_db_schema_stubs-1.0.0.dist-info/
1553 cloud_db_schema_stubs-1.0.0.dist-info/METADATA
1554 cloud_db_schema_stubs-1.0.0.dist-info/RECORD
1555 cloud_db_schema_stubs-1.0.0.dist-info/WHEEL
1556 ");
1557 }
1558
1559 #[test]
1561 fn multiple_module_names() {
1562 let src = TempDir::new().unwrap();
1563 let pyproject_toml = indoc! {r#"
1564 [project]
1565 name = "simple-namespace-part"
1566 version = "1.0.0"
1567
1568 [tool.uv.build-backend]
1569 module-name = ["foo", "simple_namespace.part_a", "simple_namespace.part_b"]
1570
1571 [build-system]
1572 requires = ["uv_build>=0.5.15,<0.6.0"]
1573 build-backend = "uv_build"
1574 "#
1575 };
1576 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1577 fs_err::create_dir_all(src.path().join("src").join("foo")).unwrap();
1578 fs_err::create_dir_all(
1579 src.path()
1580 .join("src")
1581 .join("simple_namespace")
1582 .join("part_a"),
1583 )
1584 .unwrap();
1585 fs_err::create_dir_all(
1586 src.path()
1587 .join("src")
1588 .join("simple_namespace")
1589 .join("part_b"),
1590 )
1591 .unwrap();
1592
1593 assert_snapshot!(
1598 build_err(src.path()),
1599 @"Expected a Python module at: [TEMP_PATH]/src/foo/__init__.py"
1600 );
1601
1602 File::create(src.path().join("src").join("foo").join("__init__.py")).unwrap();
1604
1605 assert_snapshot!(
1607 build_err(src.path()),
1608 @"Expected a Python module at: [TEMP_PATH]/src/simple_namespace/part_a/__init__.py"
1609 );
1610
1611 File::create(
1613 src.path()
1614 .join("src")
1615 .join("simple_namespace")
1616 .join("part_a")
1617 .join("__init__.py"),
1618 )
1619 .unwrap();
1620 File::create(
1621 src.path()
1622 .join("src")
1623 .join("simple_namespace")
1624 .join("part_b")
1625 .join("__init__.py"),
1626 )
1627 .unwrap();
1628
1629 let bogus_init_py = src
1631 .path()
1632 .join("src")
1633 .join("simple_namespace")
1634 .join("__init__.py");
1635 File::create(&bogus_init_py).unwrap();
1636 assert_snapshot!(
1637 build_err(src.path()),
1638 @"For namespace packages, `__init__.py[i]` is not allowed in parent directory: [TEMP_PATH]/src/simple_namespace"
1639 );
1640 fs_err::remove_file(bogus_init_py).unwrap();
1641
1642 let dist = TempDir::new().unwrap();
1643 let build = build(src.path(), dist.path(), Preview::default()).unwrap();
1644 assert_snapshot!(build.source_dist_contents.join("\n"), @"
1645 simple_namespace_part-1.0.0/
1646 simple_namespace_part-1.0.0/PKG-INFO
1647 simple_namespace_part-1.0.0/pyproject.toml
1648 simple_namespace_part-1.0.0/src
1649 simple_namespace_part-1.0.0/src/foo
1650 simple_namespace_part-1.0.0/src/foo/__init__.py
1651 simple_namespace_part-1.0.0/src/simple_namespace
1652 simple_namespace_part-1.0.0/src/simple_namespace/part_a
1653 simple_namespace_part-1.0.0/src/simple_namespace/part_a/__init__.py
1654 simple_namespace_part-1.0.0/src/simple_namespace/part_b
1655 simple_namespace_part-1.0.0/src/simple_namespace/part_b/__init__.py
1656 ");
1657 assert_snapshot!(build.wheel_contents.join("\n"), @"
1658 foo/
1659 foo/__init__.py
1660 simple_namespace/
1661 simple_namespace/part_a/
1662 simple_namespace/part_a/__init__.py
1663 simple_namespace/part_b/
1664 simple_namespace/part_b/__init__.py
1665 simple_namespace_part-1.0.0.dist-info/
1666 simple_namespace_part-1.0.0.dist-info/METADATA
1667 simple_namespace_part-1.0.0.dist-info/RECORD
1668 simple_namespace_part-1.0.0.dist-info/WHEEL
1669 ");
1670 }
1671
1672 #[test]
1675 fn test_prune_redundant_modules() {
1676 fn check(input: &[&str], expect: &[&str]) {
1677 let input = input.iter().map(|s| (*s).to_string()).collect();
1678 let expect: Vec<_> = expect.iter().map(|s| (*s).to_string()).collect();
1679 assert_eq!(prune_redundant_modules(input), expect);
1680 }
1681
1682 check(&[], &[]);
1684 check(&["foo"], &["foo"]);
1685 check(&["foo", "bar"], &["bar", "foo"]);
1686
1687 check(&["foo", "foo.bar"], &["foo"]);
1689 check(&["foo.bar", "foo"], &["foo"]);
1690 check(
1691 &["foo.bar.a", "foo.bar.b", "foo.bar", "foo", "foo.bar.a.c"],
1692 &["foo"],
1693 );
1694 check(
1695 &["bar.one", "bar.two", "baz", "bar", "baz.one"],
1696 &["bar", "baz"],
1697 );
1698
1699 check(&["foo", "foobar"], &["foo", "foobar"]);
1701 check(
1702 &["foo", "foobar", "foo.bar", "foobar.baz"],
1703 &["foo", "foobar"],
1704 );
1705 check(&["foo.bar", "foo.baz"], &["foo.bar", "foo.baz"]);
1706 check(&["foo", "foo", "foo.bar", "foo.bar"], &["foo"]);
1707
1708 check(
1710 &[
1711 "foo.inner",
1712 "foo.inner.deeper",
1713 "foo",
1714 "bar",
1715 "bar.sub",
1716 "bar.sub.deep",
1717 "foobar",
1718 "baz.baz.bar",
1719 "baz.baz",
1720 "qux",
1721 ],
1722 &["bar", "baz.baz", "foo", "foobar", "qux"],
1723 );
1724 }
1725
1726 #[test]
1728 fn duplicate_module_names() {
1729 let src = TempDir::new().unwrap();
1730 let pyproject_toml = indoc! {r#"
1731 [project]
1732 name = "duplicate"
1733 version = "1.0.0"
1734
1735 [tool.uv.build-backend]
1736 module-name = ["foo", "foo", "bar.baz", "bar.baz.submodule"]
1737
1738 [build-system]
1739 requires = ["uv_build>=0.5.15,<0.6.0"]
1740 build-backend = "uv_build"
1741 "#
1742 };
1743 fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
1744 fs_err::create_dir_all(src.path().join("src").join("foo")).unwrap();
1745 File::create(src.path().join("src").join("foo").join("__init__.py")).unwrap();
1746 fs_err::create_dir_all(src.path().join("src").join("bar").join("baz")).unwrap();
1747 File::create(
1748 src.path()
1749 .join("src")
1750 .join("bar")
1751 .join("baz")
1752 .join("__init__.py"),
1753 )
1754 .unwrap();
1755
1756 let dist = TempDir::new().unwrap();
1757 let build = build(src.path(), dist.path(), Preview::default()).unwrap();
1758 assert_snapshot!(build.source_dist_contents.join("\n"), @"
1759 duplicate-1.0.0/
1760 duplicate-1.0.0/PKG-INFO
1761 duplicate-1.0.0/pyproject.toml
1762 duplicate-1.0.0/src
1763 duplicate-1.0.0/src/bar
1764 duplicate-1.0.0/src/bar/baz
1765 duplicate-1.0.0/src/bar/baz/__init__.py
1766 duplicate-1.0.0/src/foo
1767 duplicate-1.0.0/src/foo/__init__.py
1768 ");
1769 assert_snapshot!(build.wheel_contents.join("\n"), @"
1770 bar/
1771 bar/baz/
1772 bar/baz/__init__.py
1773 duplicate-1.0.0.dist-info/
1774 duplicate-1.0.0.dist-info/METADATA
1775 duplicate-1.0.0.dist-info/RECORD
1776 duplicate-1.0.0.dist-info/WHEEL
1777 foo/
1778 foo/__init__.py
1779 ");
1780 }
1781
1782 #[test]
1784 fn metadata_json_preview() {
1785 let src = TempDir::new().unwrap();
1786 fs_err::write(
1787 src.path().join("pyproject.toml"),
1788 indoc! {r#"
1789 [project]
1790 name = "metadata-json-preview"
1791 version = "1.0.0"
1792
1793 [build-system]
1794 requires = ["uv_build>=0.5.15,<0.6.0"]
1795 build-backend = "uv_build"
1796 "#
1797 },
1798 )
1799 .unwrap();
1800 fs_err::create_dir_all(src.path().join("src").join("metadata_json_preview")).unwrap();
1801 File::create(
1802 src.path()
1803 .join("src")
1804 .join("metadata_json_preview")
1805 .join("__init__.py"),
1806 )
1807 .unwrap();
1808
1809 let dist = TempDir::new().unwrap();
1810 let build = build(
1811 src.path(),
1812 dist.path(),
1813 Preview::new(&[PreviewFeature::MetadataJson]),
1814 )
1815 .unwrap();
1816
1817 assert_snapshot!(build.wheel_contents.join("\n"), @"
1818 metadata_json_preview-1.0.0.dist-info/
1819 metadata_json_preview-1.0.0.dist-info/METADATA
1820 metadata_json_preview-1.0.0.dist-info/METADATA.json
1821 metadata_json_preview-1.0.0.dist-info/RECORD
1822 metadata_json_preview-1.0.0.dist-info/WHEEL
1823 metadata_json_preview-1.0.0.dist-info/WHEEL.json
1824 metadata_json_preview/
1825 metadata_json_preview/__init__.py
1826 ");
1827 }
1828}