diff --git a/Cargo.lock b/Cargo.lock index 674091dce..028184222 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5994,6 +5994,7 @@ dependencies = [ "uv-distribution-filename", "uv-distribution-types", "uv-extract", + "uv-flags", "uv-fs", "uv-git", "uv-git-types", diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index 190ea5dbe..84c7317e1 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -24,6 +24,7 @@ uv-configuration = { workspace = true } uv-distribution-filename = { workspace = true } uv-distribution-types = { workspace = true } uv-extract = { workspace = true } +uv-flags = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-git = { workspace = true } uv-git-types = { workspace = true } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 488d1f0a7..d934d2b9f 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -2503,7 +2503,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } else { BuildKind::Wheel }, - BuildOutput::Debug, + if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) { + BuildOutput::Quiet + } else { + BuildOutput::Debug + }, self.build_stack.cloned().unwrap_or_default(), ) .await @@ -2604,7 +2608,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source.as_dist(), source_strategy, build_kind, - BuildOutput::Debug, + if uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) { + BuildOutput::Quiet + } else { + BuildOutput::Debug + }, self.build_stack.cloned().unwrap_or_default(), ) .await diff --git a/crates/uv-flags/src/lib.rs b/crates/uv-flags/src/lib.rs index 3c58882e9..d896dfcfe 100644 --- a/crates/uv-flags/src/lib.rs +++ b/crates/uv-flags/src/lib.rs @@ -6,6 +6,7 @@ bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct EnvironmentFlags: u32 { const SKIP_WHEEL_FILENAME_CHECK = 1 << 0; + const HIDE_BUILD_OUTPUT = 1 << 1; } } diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 6b9b8e98f..c06e78232 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -585,6 +585,7 @@ pub struct Concurrency { #[derive(Debug, Clone)] pub struct EnvironmentOptions { pub skip_wheel_filename_check: Option, + pub hide_build_output: Option, pub python_install_bin: Option, pub python_install_registry: Option, pub install_mirrors: PythonInstallMirrors, @@ -613,6 +614,7 @@ impl EnvironmentOptions { skip_wheel_filename_check: parse_boolish_environment_variable( EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK, )?, + hide_build_output: parse_boolish_environment_variable(EnvVars::UV_HIDE_BUILD_OUTPUT)?, python_install_bin: parse_boolish_environment_variable(EnvVars::UV_PYTHON_INSTALL_BIN)?, python_install_registry: parse_boolish_environment_variable( EnvVars::UV_PYTHON_INSTALL_REGISTRY, @@ -775,6 +777,9 @@ impl From<&EnvironmentOptions> for EnvironmentFlags { if options.skip_wheel_filename_check == Some(true) { flags.insert(Self::SKIP_WHEEL_FILENAME_CHECK); } + if options.hide_build_output == Some(true) { + flags.insert(Self::HIDE_BUILD_OUTPUT); + } flags } } diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index c5d1e2fdd..354c3ec3a 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -1236,4 +1236,9 @@ impl EnvVars { /// around invalid artifacts in rare cases. #[attr_added_in("0.8.23")] pub const UV_SKIP_WHEEL_FILENAME_CHECK: &'static str = "UV_SKIP_WHEEL_FILENAME_CHECK"; + + /// Suppress output from the build backend when building source distributions, even in the event + /// of build failures. + #[attr_added_in("0.9.14")] + pub const UV_HIDE_BUILD_OUTPUT: &'static str = "UV_HIDE_BUILD_OUTPUT"; } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 1d2d4303c..eb4bd48d4 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -663,7 +663,7 @@ async fn build_package( let build_output = match printer { Printer::Default | Printer::NoProgress | Printer::Verbose => { - if build_logs { + if build_logs && !uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) { BuildOutput::Stderr } else { BuildOutput::Quiet diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index ee9ffa737..b1828e4e3 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -1180,6 +1180,102 @@ fn build_no_build_logs() -> Result<()> { Ok(()) } +/// Test that `UV_HIDE_BUILD_OUTPUT` suppresses build output. +#[test] +fn build_hide_build_output_env_var() -> Result<()> { + let context = TestContext::new("3.12"); + + let project = context.temp_dir.child("project"); + + let pyproject_toml = project.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#, + )?; + + project + .child("src") + .child("project") + .child("__init__.py") + .touch()?; + project.child("README").touch()?; + + uv_snapshot!(&context.filters(), context.build().arg("project").env(EnvVars::UV_HIDE_BUILD_OUTPUT, "1"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building source distribution... + Building wheel from source distribution... + Successfully built project/dist/project-0.1.0.tar.gz + Successfully built project/dist/project-0.1.0-py3-none-any.whl + "###); + + Ok(()) +} + +/// Test that `UV_HIDE_BUILD_OUTPUT` hides build output even on failure. +#[test] +fn build_hide_build_output_on_failure() -> Result<()> { + let context = TestContext::new("3.12"); + let filters = context + .filters() + .into_iter() + .chain([(r"\\\.", "")]) + .collect::>(); + + let project = context.temp_dir.child("project"); + + let pyproject_toml = project.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // Create a `setup.py` that prints an environment variable before failing. + project.child("setup.py").write_str(indoc! {r#" + import os + import sys + print("FOO=" + os.environ.get("FOO", "not-set"), file=sys.stderr) + sys.stderr.flush() + raise Exception("Build failed intentionally!") + "#})?; + + // With `UV_HIDE_BUILD_OUTPUT`, the output is hidden even on failure. + uv_snapshot!(&filters, context.build().arg("project").env(EnvVars::UV_HIDE_BUILD_OUTPUT, "1").env("FOO", "bar"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building source distribution... + × Failed to build `[TEMP_DIR]/project` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta.build_sdist` failed (exit status: 1) + hint: This usually indicates a problem with the package or the build environment. + "###); + + Ok(()) +} + #[test] fn build_tool_uv_sources() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/docs/reference/environment.md b/docs/reference/environment.md index ccc3826d1..7f911b077 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -150,6 +150,12 @@ Equivalent to the `--token` argument for self update. A GitHub token for authent Enables fetching files stored in Git LFS when installing a package from a Git repository. +### `UV_HIDE_BUILD_OUTPUT` +added in `0.9.14` + +Suppress output from the build backend when building source distributions, even in the event +of build failures. + ### `UV_HTTP_RETRIES` added in `0.7.21`