mirror of https://github.com/astral-sh/uv
Add `--output-file` to `uv export` (#7109)
## Summary Closes https://github.com/astral-sh/uv/issues/7058.
This commit is contained in:
parent
3d57b6e795
commit
d0f9016eda
|
|
@ -2966,6 +2966,10 @@ pub struct ExportArgs {
|
||||||
#[arg(long, overrides_with("hashes"))]
|
#[arg(long, overrides_with("hashes"))]
|
||||||
pub no_hashes: bool,
|
pub no_hashes: bool,
|
||||||
|
|
||||||
|
/// Write the compiled requirements to the given `requirements.txt` file.
|
||||||
|
#[arg(long, short)]
|
||||||
|
pub output_file: Option<PathBuf>,
|
||||||
|
|
||||||
/// Assert that the `uv.lock` will remain unchanged.
|
/// Assert that the `uv.lock` will remain unchanged.
|
||||||
///
|
///
|
||||||
/// Requires that the lockfile is up-to-date. If the lockfile is missing or
|
/// Requires that the lockfile is up-to-date. If the lockfile is missing or
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
use std::time::Duration;
|
use anstream::AutoStream;
|
||||||
use std::{fmt::Display, fmt::Write, process::ExitCode};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::io::stdout;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::{fmt::Display, fmt::Write, process::ExitCode};
|
||||||
|
|
||||||
pub(crate) use build::build;
|
pub(crate) use build::build;
|
||||||
pub(crate) use cache_clean::cache_clean;
|
pub(crate) use cache_clean::cache_clean;
|
||||||
|
|
@ -198,3 +201,54 @@ pub(crate) struct SharedState {
|
||||||
/// The downloaded distributions.
|
/// The downloaded distributions.
|
||||||
pub(crate) in_flight: InFlight,
|
pub(crate) in_flight: InFlight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A multicasting writer that writes to both the standard output and an output file, if present.
|
||||||
|
#[allow(clippy::disallowed_types)]
|
||||||
|
struct OutputWriter<'a> {
|
||||||
|
stdout: Option<AutoStream<std::io::Stdout>>,
|
||||||
|
output_file: Option<&'a Path>,
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_types)]
|
||||||
|
impl<'a> OutputWriter<'a> {
|
||||||
|
/// Create a new output writer.
|
||||||
|
fn new(include_stdout: bool, output_file: Option<&'a Path>) -> Self {
|
||||||
|
let stdout = include_stdout.then(|| AutoStream::<std::io::Stdout>::auto(stdout()));
|
||||||
|
Self {
|
||||||
|
stdout,
|
||||||
|
output_file,
|
||||||
|
buffer: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the given arguments to both standard output and the output buffer, if present.
|
||||||
|
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
// Write to the buffer.
|
||||||
|
if self.output_file.is_some() {
|
||||||
|
self.buffer.write_fmt(args)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to standard output.
|
||||||
|
if let Some(stdout) = &mut self.stdout {
|
||||||
|
write!(stdout, "{args}")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit the buffer to the output file.
|
||||||
|
async fn commit(self) -> std::io::Result<()> {
|
||||||
|
if let Some(output_file) = self.output_file {
|
||||||
|
// If the output file is an existing symlink, write to the destination instead.
|
||||||
|
let output_file = fs_err::read_link(output_file)
|
||||||
|
.map(Cow::Owned)
|
||||||
|
.unwrap_or(Cow::Borrowed(output_file));
|
||||||
|
let stream = anstream::adapter::strip_bytes(&self.buffer).into_vec();
|
||||||
|
uv_fs::write_atomic(output_file, &stream).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io::stdout;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anstream::{eprint, AutoStream};
|
use anstream::eprint;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
|
|
@ -43,7 +41,7 @@ use uv_warnings::warn_user;
|
||||||
|
|
||||||
use crate::commands::pip::loggers::DefaultResolveLogger;
|
use crate::commands::pip::loggers::DefaultResolveLogger;
|
||||||
use crate::commands::pip::{operations, resolution_environment};
|
use crate::commands::pip::{operations, resolution_environment};
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::{ExitStatus, OutputWriter};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
||||||
/// Resolve a set of requirements into a set of pinned versions.
|
/// Resolve a set of requirements into a set of pinned versions.
|
||||||
|
|
@ -621,54 +619,3 @@ fn cmd(
|
||||||
.join(" ");
|
.join(" ");
|
||||||
format!("uv {args}")
|
format!("uv {args}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A multicasting writer that writes to both the standard output and an output file, if present.
|
|
||||||
#[allow(clippy::disallowed_types)]
|
|
||||||
struct OutputWriter<'a> {
|
|
||||||
stdout: Option<AutoStream<std::io::Stdout>>,
|
|
||||||
output_file: Option<&'a Path>,
|
|
||||||
buffer: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_types)]
|
|
||||||
impl<'a> OutputWriter<'a> {
|
|
||||||
/// Create a new output writer.
|
|
||||||
fn new(include_stdout: bool, output_file: Option<&'a Path>) -> Self {
|
|
||||||
let stdout = include_stdout.then(|| AutoStream::<std::io::Stdout>::auto(stdout()));
|
|
||||||
Self {
|
|
||||||
stdout,
|
|
||||||
output_file,
|
|
||||||
buffer: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write the given arguments to both standard output and the output buffer, if present.
|
|
||||||
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
// Write to the buffer.
|
|
||||||
if self.output_file.is_some() {
|
|
||||||
self.buffer.write_fmt(args)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to standard output.
|
|
||||||
if let Some(stdout) = &mut self.stdout {
|
|
||||||
write!(stdout, "{args}")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Commit the buffer to the output file.
|
|
||||||
async fn commit(self) -> std::io::Result<()> {
|
|
||||||
if let Some(output_file) = self.output_file {
|
|
||||||
// If the output file is an existing symlink, write to the destination instead.
|
|
||||||
let output_file = fs_err::read_link(output_file)
|
|
||||||
.map(Cow::Owned)
|
|
||||||
.unwrap_or(Cow::Borrowed(output_file));
|
|
||||||
let stream = anstream::adapter::strip_bytes(&self.buffer).into_vec();
|
|
||||||
uv_fs::write_atomic(output_file, &stream).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
|
|
@ -13,7 +14,7 @@ use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}
|
||||||
use crate::commands::pip::loggers::DefaultResolveLogger;
|
use crate::commands::pip::loggers::DefaultResolveLogger;
|
||||||
use crate::commands::project::lock::do_safe_lock;
|
use crate::commands::project::lock::do_safe_lock;
|
||||||
use crate::commands::project::{FoundInterpreter, ProjectError};
|
use crate::commands::project::{FoundInterpreter, ProjectError};
|
||||||
use crate::commands::{pip, ExitStatus};
|
use crate::commands::{pip, ExitStatus, OutputWriter};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
use crate::settings::ResolverSettings;
|
use crate::settings::ResolverSettings;
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ pub(crate) async fn export(
|
||||||
format: ExportFormat,
|
format: ExportFormat,
|
||||||
package: Option<PackageName>,
|
package: Option<PackageName>,
|
||||||
hashes: bool,
|
hashes: bool,
|
||||||
|
output_file: Option<PathBuf>,
|
||||||
extras: ExtrasSpecification,
|
extras: ExtrasSpecification,
|
||||||
dev: bool,
|
dev: bool,
|
||||||
locked: bool,
|
locked: bool,
|
||||||
|
|
@ -34,6 +36,7 @@ pub(crate) async fn export(
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
|
quiet: bool,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
|
|
@ -110,6 +113,9 @@ pub(crate) async fn export(
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Write the resolved dependencies to the output channel.
|
||||||
|
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());
|
||||||
|
|
||||||
// Generate the export.
|
// Generate the export.
|
||||||
match format {
|
match format {
|
||||||
ExportFormat::RequirementsTxt => {
|
ExportFormat::RequirementsTxt => {
|
||||||
|
|
@ -120,13 +126,16 @@ pub(crate) async fn export(
|
||||||
&dev,
|
&dev,
|
||||||
hashes,
|
hashes,
|
||||||
)?;
|
)?;
|
||||||
anstream::println!(
|
writeln!(
|
||||||
|
writer,
|
||||||
"{}",
|
"{}",
|
||||||
"# This file was autogenerated via `uv export`.".green()
|
"# This file was autogenerated via `uv export`.".green()
|
||||||
);
|
)?;
|
||||||
anstream::print!("{export}");
|
write!(writer, "{export}")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writer.commit().await?;
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1323,6 +1323,7 @@ async fn run_project(
|
||||||
args.format,
|
args.format,
|
||||||
args.package,
|
args.package,
|
||||||
args.hashes,
|
args.hashes,
|
||||||
|
args.output_file,
|
||||||
args.extras,
|
args.extras,
|
||||||
args.dev,
|
args.dev,
|
||||||
args.locked,
|
args.locked,
|
||||||
|
|
@ -1334,6 +1335,7 @@ async fn run_project(
|
||||||
globals.connectivity,
|
globals.connectivity,
|
||||||
globals.concurrency,
|
globals.concurrency,
|
||||||
globals.native_tls,
|
globals.native_tls,
|
||||||
|
globals.quiet,
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -957,6 +957,7 @@ pub(crate) struct ExportSettings {
|
||||||
pub(crate) extras: ExtrasSpecification,
|
pub(crate) extras: ExtrasSpecification,
|
||||||
pub(crate) dev: bool,
|
pub(crate) dev: bool,
|
||||||
pub(crate) hashes: bool,
|
pub(crate) hashes: bool,
|
||||||
|
pub(crate) output_file: Option<PathBuf>,
|
||||||
pub(crate) locked: bool,
|
pub(crate) locked: bool,
|
||||||
pub(crate) frozen: bool,
|
pub(crate) frozen: bool,
|
||||||
pub(crate) python: Option<String>,
|
pub(crate) python: Option<String>,
|
||||||
|
|
@ -978,6 +979,7 @@ impl ExportSettings {
|
||||||
no_dev,
|
no_dev,
|
||||||
hashes,
|
hashes,
|
||||||
no_hashes,
|
no_hashes,
|
||||||
|
output_file,
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
resolver,
|
resolver,
|
||||||
|
|
@ -995,6 +997,7 @@ impl ExportSettings {
|
||||||
),
|
),
|
||||||
dev: flag(dev, no_dev).unwrap_or(true),
|
dev: flag(dev, no_dev).unwrap_or(true),
|
||||||
hashes: flag(hashes, no_hashes).unwrap_or(true),
|
hashes: flag(hashes, no_hashes).unwrap_or(true),
|
||||||
|
output_file,
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
python,
|
python,
|
||||||
|
|
|
||||||
|
|
@ -636,3 +636,62 @@ fn no_hashes() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn output_file() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let pyproject_toml = context.temp_dir.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 = ["setuptools>=42"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
context.lock().assert().success();
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.export().arg("--output-file").arg("requirements.txt"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
# This file was autogenerated via `uv export`.
|
||||||
|
-e .
|
||||||
|
anyio==3.7.0 \
|
||||||
|
--hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \
|
||||||
|
--hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0
|
||||||
|
idna==3.6 \
|
||||||
|
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||||||
|
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||||||
|
sniffio==1.3.1 \
|
||||||
|
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \
|
||||||
|
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 4 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let contents = fs_err::read_to_string(context.temp_dir.child("requirements.txt"))?;
|
||||||
|
insta::assert_snapshot!(contents, @r###"
|
||||||
|
# This file was autogenerated via `uv export`.
|
||||||
|
-e .
|
||||||
|
anyio==3.7.0 \
|
||||||
|
--hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \
|
||||||
|
--hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0
|
||||||
|
idna==3.6 \
|
||||||
|
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||||||
|
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||||||
|
sniffio==1.3.1 \
|
||||||
|
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \
|
||||||
|
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1873,6 +1873,8 @@ uv export [OPTIONS]
|
||||||
|
|
||||||
<p>When disabled, uv will only use locally cached data and locally available files.</p>
|
<p>When disabled, uv will only use locally cached data and locally available files.</p>
|
||||||
|
|
||||||
|
</dd><dt><code>--output-file</code>, <code>-o</code> <i>output-file</i></dt><dd><p>Write the compiled requirements to the given <code>requirements.txt</code> file</p>
|
||||||
|
|
||||||
</dd><dt><code>--package</code> <i>package</i></dt><dd><p>Export the dependencies for a specific package in the workspace.</p>
|
</dd><dt><code>--package</code> <i>package</i></dt><dd><p>Export the dependencies for a specific package in the workspace.</p>
|
||||||
|
|
||||||
<p>If the workspace member does not exist, uv will exit with an error.</p>
|
<p>If the workspace member does not exist, uv will exit with an error.</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue