Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4897,6 +4897,20 @@ pub struct ExportArgs {
#[arg(long, overrides_with("no_header"), hide = true)]
pub header: bool,

/// Include `--index-url` and `--extra-index-url` entries in the generated output file.
#[arg(long, overrides_with("no_emit_index_url"))]
pub emit_index_url: bool,

#[arg(long, overrides_with("emit_index_url"), hide = true)]
pub no_emit_index_url: bool,

/// Include `--find-links` entries in the generated output file.
#[arg(long, overrides_with("no_emit_find_links"))]
pub emit_find_links: bool,

#[arg(long, overrides_with("emit_find_links"), hide = true)]
pub no_emit_find_links: bool,

/// Export any non-editable dependencies, including the project and any workspace members, as
/// editable.
#[arg(long, overrides_with = "no_editable", hide = true)]
Expand Down
34 changes: 34 additions & 0 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ use anyhow::{Result, anyhow};
use clap::ValueEnum;
use itertools::Itertools;
use owo_colors::OwoColorize;
use rustc_hash::FxHashSet;

use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use uv_configuration::{
Concurrency, DependencyGroups, EditableMode, ExportFormat, ExtrasSpecification, InstallOptions,
};
use uv_distribution_types::Verbatim;
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_preview::Preview;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
Expand Down Expand Up @@ -71,6 +73,8 @@ pub(crate) async fn export(
frozen: Option<FrozenSource>,
include_annotations: bool,
include_header: bool,
include_index_url: bool,
include_find_links: bool,
script: Option<Pep723Script>,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
Expand Down Expand Up @@ -374,6 +378,36 @@ pub(crate) async fn export(
)?;
writeln!(writer, "{}", format!("# {}", cmd()).green())?;
}

let mut wrote_preamble = false;

// If necessary, include the `--index-url` and `--extra-index-url` locations.
if include_index_url {
if let Some(index) = settings.index_locations.default_index() {
writeln!(writer, "--index-url {}", index.url().verbatim())?;
wrote_preamble = true;
}
let mut seen = FxHashSet::default();
for extra_index in settings.index_locations.implicit_indexes() {
if seen.insert(extra_index.url()) {
writeln!(writer, "--extra-index-url {}", extra_index.url().verbatim())?;
wrote_preamble = true;
}
}
}

// If necessary, include the `--find-links` locations.
if include_find_links {
for flat_index in settings.index_locations.flat_indexes() {
writeln!(writer, "--find-links {}", flat_index.url().verbatim())?;
wrote_preamble = true;
}
}

if wrote_preamble {
writeln!(writer)?;
}

write!(writer, "{export}")?;
}
ExportFormat::PylockToml => {
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2519,6 +2519,8 @@ async fn run_project(
args.frozen,
args.include_annotations,
args.include_header,
args.include_index_url,
args.include_find_links,
script,
args.python,
args.install_mirrors,
Expand Down
10 changes: 10 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2301,6 +2301,8 @@ pub(crate) struct ExportSettings {
pub(crate) frozen: Option<FrozenSource>,
pub(crate) include_annotations: bool,
pub(crate) include_header: bool,
pub(crate) include_index_url: bool,
pub(crate) include_find_links: bool,
pub(crate) script: Option<PathBuf>,
pub(crate) python: Option<String>,
pub(crate) install_mirrors: PythonInstallMirrors,
Expand Down Expand Up @@ -2336,6 +2338,10 @@ impl ExportSettings {
no_annotate,
header,
no_header,
emit_index_url,
no_emit_index_url,
emit_find_links,
no_emit_find_links,
editable,
no_editable,
hashes,
Expand Down Expand Up @@ -2414,6 +2420,10 @@ impl ExportSettings {
frozen: resolve_frozen(frozen),
include_annotations: flag(annotate, no_annotate, "annotate").unwrap_or(true),
include_header: flag(header, no_header, "header").unwrap_or(true),
include_index_url: flag(emit_index_url, no_emit_index_url, "emit-index-url")
.unwrap_or(false),
include_find_links: flag(emit_find_links, no_emit_find_links, "emit-find-links")
.unwrap_or(false),
script,
python: python.and_then(Maybe::into_option),
refresh: Refresh::from(refresh),
Expand Down
105 changes: 105 additions & 0 deletions crates/uv/tests/it/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8947,3 +8947,108 @@ fn pylock_toml_filter_by_requires_python() -> Result<()> {

Ok(())
}

#[test]
fn requirements_txt_emit_index_url() -> Result<()> {
let context = uv_test::test_context!("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 = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#,
)?;

context.lock().assert().success();

uv_snapshot!(context.filters(), context.export().arg("--emit-index-url"), @r"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv export --cache-dir [CACHE_DIR] --emit-index-url
--index-url https://pypi.org/simple

-e .
anyio==3.7.0 \
--hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \
--hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0
# via project
idna==3.6 \
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
# via anyio
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
# via anyio

----- stderr -----
Resolved 4 packages in [TIME]
");

Ok(())
}

#[test]
fn requirements_txt_emit_extra_index_url() -> Result<()> {
let context = uv_test::test_context!("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 = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#,
)?;

context.lock().assert().success();

// Use `--frozen` and `--index` via CLI so that no network access to the extra index is
// required during the export.
uv_snapshot!(context.filters(), context.export()
.arg("--no-header")
.arg("--emit-index-url")
.arg("--frozen")
.arg("--index")
.arg("https://test.pypi.org/simple/"), @r"
success: true
exit_code: 0
----- stdout -----
--index-url https://pypi.org/simple
--extra-index-url https://test.pypi.org/simple/

-e .
anyio==3.7.0 \
--hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \
--hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0
# via project
idna==3.6 \
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
# via anyio
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
# via anyio

----- stderr -----
");

Ok(())
}
Loading