uv/crates/uv/tests/it/build_backend.rs

1120 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::common::{TestContext, uv_snapshot, venv_bin_path};
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::{FileTouch, FileWriteBin, FileWriteStr, PathChild, PathCreateDir};
use flate2::bufread::GzDecoder;
use fs_err::File;
use indoc::{formatdoc, indoc};
use std::env;
use std::io::BufReader;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
use uv_static::EnvVars;
const BUILT_BY_UV_TEST_SCRIPT: &str = indoc! {r#"
from built_by_uv import greet
from built_by_uv.arithmetic.circle import area
print(greet())
print(f"Area of a circle with r=2: {area(2)}")
"#};
/// Test that build backend works if we invoke it directly.
///
/// We can't test end-to-end here including the PEP 517 bridge code since we don't have a uv wheel.
#[test]
#[cfg(feature = "pypi")]
fn built_by_uv_direct_wheel() -> Result<()> {
let context = TestContext::new("3.12");
let built_by_uv = Path::new("../../scripts/packages/built-by-uv");
let temp_dir = TempDir::new()?;
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(temp_dir.path())
.current_dir(built_by_uv), @r###"
success: true
exit_code: 0
----- stdout -----
built_by_uv-0.1.0-py3-none-any.whl
----- stderr -----
"###);
context
.pip_install()
.arg(temp_dir.path().join("built_by_uv-0.1.0-py3-none-any.whl"))
.assert()
.success();
uv_snapshot!(context.python_command()
.arg("-c")
.arg(BUILT_BY_UV_TEST_SCRIPT), @r###"
success: true
exit_code: 0
----- stdout -----
Hello 👋
Area of a circle with r=2: 12.56636
----- stderr -----
"###);
uv_snapshot!(Command::new("say-hi")
.env(EnvVars::PATH, venv_bin_path(&context.venv)), @r###"
success: true
exit_code: 0
----- stdout -----
Hi from a script!
----- stderr -----
"###);
Ok(())
}
/// Test that source tree -> source dist -> wheel works.
///
/// We can't test end-to-end here including the PEP 517 bridge code since we don't have a uv wheel,
/// so we call the build backend directly.
#[test]
#[cfg(feature = "pypi")]
fn built_by_uv_direct() -> Result<()> {
let context = TestContext::new("3.12");
let built_by_uv = Path::new("../../scripts/packages/built-by-uv");
let sdist_dir = TempDir::new()?;
uv_snapshot!(context
.build_backend()
.arg("build-sdist")
.arg(sdist_dir.path())
.current_dir(built_by_uv), @r###"
success: true
exit_code: 0
----- stdout -----
built_by_uv-0.1.0.tar.gz
----- stderr -----
"###);
let sdist_tree = TempDir::new()?;
let sdist_reader = BufReader::new(File::open(
sdist_dir.path().join("built_by_uv-0.1.0.tar.gz"),
)?);
tar::Archive::new(GzDecoder::new(sdist_reader)).unpack(sdist_tree.path())?;
drop(sdist_dir);
let wheel_dir = TempDir::new()?;
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(wheel_dir.path())
.current_dir(sdist_tree.path().join("built_by_uv-0.1.0")), @r###"
success: true
exit_code: 0
----- stdout -----
built_by_uv-0.1.0-py3-none-any.whl
----- stderr -----
"###);
drop(sdist_tree);
context
.pip_install()
.arg(wheel_dir.path().join("built_by_uv-0.1.0-py3-none-any.whl"))
.assert()
.success();
drop(wheel_dir);
uv_snapshot!(context.python_command()
.arg("-c")
.arg(BUILT_BY_UV_TEST_SCRIPT), @r###"
success: true
exit_code: 0
----- stdout -----
Hello 👋
Area of a circle with r=2: 12.56636
----- stderr -----
"###);
Ok(())
}
/// Test that editables work.
///
/// We can't test end-to-end here including the PEP 517 bridge code since we don't have a uv wheel,
/// so we call the build backend directly.
#[test]
#[cfg(feature = "pypi")]
fn built_by_uv_editable() -> Result<()> {
let context = TestContext::new("3.12");
let built_by_uv = Path::new("../../scripts/packages/built-by-uv");
// Without the editable, pytest fails.
context.pip_install().arg("pytest").assert().success();
context
.python_command()
.arg("-m")
.arg("pytest")
.current_dir(built_by_uv)
.assert()
.failure();
// Build and install the editable. Normally, this should be one step with the editable never
// been seen, but we have to split it for the test.
let wheel_dir = TempDir::new()?;
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(wheel_dir.path())
.current_dir(built_by_uv), @r###"
success: true
exit_code: 0
----- stdout -----
built_by_uv-0.1.0-py3-none-any.whl
----- stderr -----
"###);
context
.pip_install()
.arg(wheel_dir.path().join("built_by_uv-0.1.0-py3-none-any.whl"))
.assert()
.success();
drop(wheel_dir);
// Now, pytest passes.
uv_snapshot!(context.python_command()
.arg("-m")
.arg("pytest")
// Avoid showing absolute paths and column dependent layout
.arg("--quiet")
.arg("--capture=no")
.current_dir(built_by_uv), @r###"
success: true
exit_code: 0
----- stdout -----
..
2 passed in [TIME]
----- stderr -----
"###);
Ok(())
}
#[cfg(all(unix, feature = "git"))]
#[test]
fn preserve_executable_bit() -> Result<()> {
use std::io::Write;
let context = TestContext::new("3.12");
let project_dir = context.temp_dir.path().join("preserve_executable_bit");
context
.init()
.arg("--lib")
.arg(&project_dir)
.assert()
.success();
fs_err::OpenOptions::new()
.write(true)
.append(true)
.open(project_dir.join("pyproject.toml"))?
.write_all(
indoc! {r#"
[tool.uv.build-backend.data]
scripts = "scripts"
"#}
.as_bytes(),
)?;
fs_err::create_dir(project_dir.join("scripts"))?;
fs_err::write(
project_dir.join("scripts").join("greet.sh"),
indoc! {r#"
echo "Hi from the shell"
"#},
)?;
context
.build_backend()
.arg("build-wheel")
.arg(context.temp_dir.path())
.current_dir(project_dir)
.assert()
.success();
let wheel = context
.temp_dir
.path()
.join("preserve_executable_bit-0.1.0-py3-none-any.whl");
context.pip_install().arg(wheel).assert().success();
uv_snapshot!(Command::new("greet.sh")
.env(EnvVars::PATH, venv_bin_path(&context.venv)), @r###"
success: true
exit_code: 0
----- stdout -----
Hi from the shell
----- stderr -----
"###);
Ok(())
}
/// Test `tool.uv.build-backend.module-name`.
///
/// We include only the module specified by `module-name`, ignoring the project name and all other
/// potential modules.
#[test]
fn rename_module() -> Result<()> {
let context = TestContext::new("3.12");
let temp_dir = TempDir::new()?;
context
.temp_dir
.child("pyproject.toml")
.write_str(indoc! {r#"
[project]
name = "foo"
version = "1.0.0"
[tool.uv.build-backend]
module-name = "bar"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
// This is the module we would usually include, but due to the renaming by `module-name` must
// ignore.
context
.temp_dir
.child("src/foo/__init__.py")
.write_str(r#"print("Hi from foo")"#)?;
// This module would be ignored from just `project.name`, but is selected due to the renaming.
context
.temp_dir
.child("src/bar/__init__.py")
.write_str(r#"print("Hi from bar")"#)?;
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(temp_dir.path()), @r###"
success: true
exit_code: 0
----- stdout -----
foo-1.0.0-py3-none-any.whl
----- stderr -----
"###);
context
.pip_install()
.arg(temp_dir.path().join("foo-1.0.0-py3-none-any.whl"))
.assert()
.success();
// Importing the module with the `module-name` name succeeds.
uv_snapshot!(context.python_command()
.arg("-c")
.arg("import bar"), @r###"
success: true
exit_code: 0
----- stdout -----
Hi from bar
----- stderr -----
"###);
// Importing the package name fails, it was overridden by `module-name`.
uv_snapshot!(context.python_command()
.arg("-c")
.arg("import foo"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'foo'
"###);
Ok(())
}
/// Test `tool.uv.build-backend.module-name` for editable builds.
#[test]
fn rename_module_editable_build() -> Result<()> {
let context = TestContext::new("3.12");
let temp_dir = TempDir::new()?;
context
.temp_dir
.child("pyproject.toml")
.write_str(indoc! {r#"
[project]
name = "foo"
version = "1.0.0"
[tool.uv.build-backend]
module-name = "bar"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
context
.temp_dir
.child("src/bar/__init__.py")
.write_str(r#"print("Hi from bar")"#)?;
uv_snapshot!(context
.build_backend()
.arg("build-editable")
.arg(temp_dir.path()), @r###"
success: true
exit_code: 0
----- stdout -----
foo-1.0.0-py3-none-any.whl
----- stderr -----
"###);
context
.pip_install()
.arg(temp_dir.path().join("foo-1.0.0-py3-none-any.whl"))
.assert()
.success();
// Importing the module with the `module-name` name succeeds.
uv_snapshot!(context.python_command()
.arg("-c")
.arg("import bar"), @r###"
success: true
exit_code: 0
----- stdout -----
Hi from bar
----- stderr -----
"###);
Ok(())
}
/// Check that the build succeeds even if the module name mismatches by case.
#[test]
fn build_module_name_normalization() -> Result<()> {
let context = TestContext::new("3.12");
let wheel_dir = context.temp_dir.path().join("dist");
fs_err::create_dir(&wheel_dir)?;
context
.temp_dir
.child("pyproject.toml")
.write_str(indoc! {r#"
[project]
name = "django-plugin"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-name = "Django_plugin"
"#})?;
fs_err::create_dir_all(context.temp_dir.join("src"))?;
// Error case 1: No matching module.
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(&wheel_dir), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Expected a Python module at: src/Django_plugin/__init__.py
");
fs_err::create_dir_all(context.temp_dir.join("src/Django_plugin"))?;
// Error case 2: A matching module, but no `__init__.py`.
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(&wheel_dir), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Expected a Python module at: src/Django_plugin/__init__.py
");
// Use `Django_plugin` instead of `django_plugin`
context
.temp_dir
.child("src/Django_plugin/__init__.py")
.write_str(r#"print("Hi from bar")"#)?;
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(&wheel_dir), @r"
success: true
exit_code: 0
----- stdout -----
django_plugin-1.0.0-py3-none-any.whl
----- stderr -----
");
context
.pip_install()
.arg("--no-index")
.arg("--find-links")
.arg(&wheel_dir)
.arg("django-plugin")
.assert()
.success();
uv_snapshot!(context.python_command()
.arg("-c")
.arg("import Django_plugin"), @r"
success: true
exit_code: 0
----- stdout -----
Hi from bar
----- stderr -----
");
// Former error case 3, now accepted: Multiple modules a matching name.
// Requires a case-sensitive filesystem.
#[cfg(target_os = "linux")]
{
context
.temp_dir
.child("src/django_plugin/__init__.py")
.write_str(r#"print("Hi from bar")"#)?;
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(&wheel_dir), @r"
success: true
exit_code: 0
----- stdout -----
django_plugin-1.0.0-py3-none-any.whl
----- stderr -----
");
}
Ok(())
}
#[test]
fn build_sdist_with_long_path() -> Result<()> {
let context = TestContext::new("3.12");
let temp_dir = TempDir::new()?;
context
.temp_dir
.child("pyproject.toml")
.write_str(indoc! {r#"
[project]
name = "foo"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
context
.temp_dir
.child("src/foo/__init__.py")
.write_str(r#"print("Hi from foo")"#)?;
let long_path = format!("src/foo/l{}ng/__init__.py", "o".repeat(100));
context
.temp_dir
.child(long_path)
.write_str(r#"print("Hi from foo")"#)?;
uv_snapshot!(context
.build_backend()
.arg("build-sdist")
.arg(temp_dir.path()), @r###"
success: true
exit_code: 0
----- stdout -----
foo-1.0.0.tar.gz
----- stderr -----
"###);
Ok(())
}
#[test]
fn sdist_error_without_module() -> Result<()> {
let context = TestContext::new("3.12");
let temp_dir = TempDir::new()?;
context
.temp_dir
.child("pyproject.toml")
.write_str(indoc! {r#"
[project]
name = "foo"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
uv_snapshot!(context
.build_backend()
.arg("build-sdist")
.arg(temp_dir.path()), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Expected a Python module at: src/foo/__init__.py
");
fs_err::create_dir(context.temp_dir.join("src"))?;
uv_snapshot!(context
.build_backend()
.arg("build-sdist")
.arg(temp_dir.path()), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Expected a Python module at: src/foo/__init__.py
");
Ok(())
}
#[test]
fn complex_namespace_packages() -> Result<()> {
let context = TestContext::new("3.12");
let dist = context.temp_dir.child("dist");
dist.create_dir_all()?;
let init_py_a = indoc! {"
def one():
return 1
"};
let init_py_b = indoc! {"
from complex_project.part_a import one
def two():
return one() + one()
"};
let projects = [
("complex-project", "part_a", init_py_a),
("complex-project", "part_b", init_py_b),
];
for (project_name, part_name, init_py) in projects {
let project = context
.temp_dir
.child(format!("{project_name}-{part_name}"));
let project_name_dist_info = project_name.replace('-', "_");
let pyproject_toml = formatdoc! {r#"
[project]
name = "{project_name}-{part_name}"
version = "1.0.0"
[tool.uv.build-backend]
module-name = "{project_name_dist_info}.{part_name}"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#
};
project.child("pyproject.toml").write_str(&pyproject_toml)?;
project
.child("src")
.child(project_name_dist_info)
.child(part_name)
.child("__init__.py")
.write_str(init_py)?;
context
.build()
.arg(project.path())
.arg("--out-dir")
.arg(dist.path())
.assert()
.success();
}
uv_snapshot!(
context.filters(),
context
.pip_install()
.arg("complex-project-part-a")
.arg("complex-project-part-b")
.arg("--offline")
.arg("--find-links")
.arg(dist.path()),
@r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ complex-project-part-a==1.0.0
+ complex-project-part-b==1.0.0
"
);
uv_snapshot!(context.python_command()
.arg("-c")
.arg("from complex_project.part_b import two; print(two())"),
@r"
success: true
exit_code: 0
----- stdout -----
2
----- stderr -----
"
);
// Test editable installs
uv_snapshot!(
context.filters(),
context
.pip_install()
.arg("-e")
.arg("complex-project-part_a")
.arg("-e")
.arg("complex-project-part_b")
.arg("--offline"),
@r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Uninstalled 2 packages in [TIME]
Installed 2 packages in [TIME]
- complex-project-part-a==1.0.0
+ complex-project-part-a==1.0.0 (from file://[TEMP_DIR]/complex-project-part_a)
- complex-project-part-b==1.0.0
+ complex-project-part-b==1.0.0 (from file://[TEMP_DIR]/complex-project-part_b)
"
);
uv_snapshot!(context.python_command()
.arg("-c")
.arg("from complex_project.part_b import two; print(two())"),
@r"
success: true
exit_code: 0
----- stdout -----
2
----- stderr -----
"
);
Ok(())
}
#[test]
fn license_glob_without_matches_errors() -> Result<()> {
let context = TestContext::new("3.12");
let project = context.temp_dir.child("missing-license");
context
.init()
.arg("--lib")
.arg(project.path())
.assert()
.success();
project
.child("LICENSE.txt")
.write_str("permissive license")?;
project.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "missing-license"
version = "1.0.0"
license-files = ["abc", "LICENSE.txt"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#
})?;
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(context.temp_dir.path())
.current_dir(project.path()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid pyproject.toml
Caused by: `project.license-files` glob `abc` did not match any files
"###);
Ok(())
}
#[test]
fn license_file_must_be_utf8() -> Result<()> {
let context = TestContext::new("3.12");
let project = context.temp_dir.child("license-utf8");
context
.init()
.arg("--lib")
.arg(project.path())
.assert()
.success();
project.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "license-utf8"
version = "1.0.0"
license-files = ["LICENSE.bin"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#
})?;
project.child("LICENSE.bin").write_binary(&[0xff])?;
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(context.temp_dir.path())
.current_dir(project.path()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Invalid pyproject.toml
Caused by: License file `LICENSE.bin` must be UTF-8 encoded
"###);
Ok(())
}
/// Test that a symlinked file (here: license) gets included.
#[test]
#[cfg(unix)]
fn symlinked_file() -> Result<()> {
let context = TestContext::new("3.12");
let project = context.temp_dir.child("project");
context
.init()
.arg("--lib")
.arg(project.path())
.assert()
.success();
project.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "project"
version = "1.0.0"
license-files = ["LICENSE"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#
})?;
let license_file = context.temp_dir.child("LICENSE");
let license_symlink = project.child("LICENSE");
let license_text = "Project license";
license_file.write_str(license_text)?;
fs_err::os::unix::fs::symlink(license_file.path(), license_symlink.path())?;
uv_snapshot!(context
.build_backend()
.arg("build-sdist")
.arg(context.temp_dir.path())
.current_dir(project.path()), @r"
success: true
exit_code: 0
----- stdout -----
project-1.0.0.tar.gz
----- stderr -----
");
uv_snapshot!(context
.build_backend()
.arg("build-wheel")
.arg(context.temp_dir.path())
.current_dir(project.path()), @r"
success: true
exit_code: 0
----- stdout -----
project-1.0.0-py3-none-any.whl
----- stderr -----
");
uv_snapshot!(context.filters(), context.pip_install().arg("project-1.0.0-py3-none-any.whl"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ project==1.0.0 (from file://[TEMP_DIR]/project-1.0.0-py3-none-any.whl)
");
// Check that we included the actual license text and not a broken symlink.
let installed_license = context
.site_packages()
.join("project-1.0.0.dist-info")
.join("licenses")
.join("LICENSE");
assert!(
fs_err::symlink_metadata(&installed_license)?
.file_type()
.is_file()
);
let license = fs_err::read_to_string(&installed_license)?;
assert_eq!(license, license_text);
Ok(())
}
/// Ignore invalid build backend settings when not building.
///
/// They may be from another `uv_build` version that has a different schema.
#[test]
fn invalid_build_backend_settings_are_ignored() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "built-by-uv"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv.build-backend]
# Error: `source-include` must be a list
source-include = "data/build-script.py"
[build-system]
requires = ["uv_build>=10000,<10001"]
build-backend = "uv_build"
"#})?;
// Since we are not building, this must pass without complaining about the error in
// `tool.uv.build-backend`.
uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
");
Ok(())
}
/// Error when there is a relative module root outside the project root, such as
/// `tool.uv.build-backend.module-root = ".."`.
#[test]
fn error_on_relative_module_root_outside_project_root() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv.build-backend]
module-root = ".."
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
context.temp_dir.child("__init__.py").touch()?;
uv_snapshot!(context.filters(), context.build(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Building source distribution (uv build backend)...
× Failed to build `[TEMP_DIR]/`
╰─▶ Module root must be inside the project: ..
");
uv_snapshot!(context.filters(), context.build().arg("--wheel"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Building wheel (uv build backend)...
× Failed to build `[TEMP_DIR]/`
╰─▶ Module root must be inside the project: ..
");
Ok(())
}
/// Error when there is a relative data directory outside the project root, such as
/// `tool.uv.build-backend.data.headers = "../headers"`.
#[test]
fn error_on_relative_data_dir_outside_project_root() -> Result<()> {
let context = TestContext::new("3.12");
let project = context.temp_dir.child("project");
project.create_dir_all()?;
let pyproject_toml = project.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv.build-backend.data]
headers = "../header"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
let project_module = project.child("src/project");
project_module.create_dir_all()?;
project_module.child("__init__.py").touch()?;
context.temp_dir.child("headers").create_dir_all()?;
uv_snapshot!(context.filters(), context.build().arg("project"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Building source distribution (uv build backend)...
× Failed to build `[TEMP_DIR]/project`
╰─▶ The path for the data directory headers must be inside the project: ../header
");
uv_snapshot!(context.filters(), context.build().arg("project").arg("--wheel"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Building wheel (uv build backend)...
× Failed to build `[TEMP_DIR]/project`
╰─▶ The path for the data directory headers must be inside the project: ../header
");
Ok(())
}
/// Show an explicit error when there is a venv in source tree.
#[test]
fn venv_in_source_tree() {
let context = TestContext::new("3.12");
context
.init()
.arg("--lib")
.arg("--name")
.arg("foo")
.assert()
.success();
context
.venv()
.arg(context.temp_dir.join("src").join("foo").join(".venv"))
.assert()
.success();
uv_snapshot!(context.filters(), context.build(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Building source distribution (uv build backend)...
× Failed to build `[TEMP_DIR]/`
╰─▶ Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: src/foo/.venv
");
uv_snapshot!(context.filters(), context.build().arg("--wheel"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Building wheel (uv build backend)...
× Failed to build `[TEMP_DIR]/`
╰─▶ Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: src/foo/.venv
");
}