Skip to content

Commit

Permalink
Publish: Support --index <name> (#9694)
Browse files Browse the repository at this point in the history
When publishing, we currently ask the user to set `--publish-url` to the
upload URL and `--check-url` to the simple index URL, or the equivalent
configuration keys. But that's redundant with the `[[tool.uv.index]]`
declaration. Instead, we extend `[[tool.uv.index]]` with a `publish-url`
entry and allow passing `uv publish --index <name>`.

`uv publish --index <name>` requires the `pyproject.toml` to be present
when publishing, unlike using `--publish-url ... --check-url ...` which
can be used e.g. in CI without a checkout step. `--index` also always
uses the check URL feature to aid upload consistency.

The documentation tries to explain both approaches together, which
overlap for the check URL feature.

Fixes #8864

---------

Co-authored-by: Zanie Blue <[email protected]>
  • Loading branch information
konstin and zanieb authored Dec 10, 2024
1 parent a090cf1 commit 321101d
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 30 deletions.
39 changes: 33 additions & 6 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5180,14 +5180,32 @@ pub struct PublishArgs {
#[arg(default_value = "dist/*")]
pub files: Vec<String>,

/// The URL of the upload endpoint (not the index URL).
/// The name of an index in the configuration to use for publishing.
///
/// Note that there are typically different URLs for index access (e.g., `https:://.../simple`)
/// and index upload.
/// The index must have a `publish-url` setting, for example:
///
/// Defaults to PyPI's publish URL (<https://upload.pypi.org/legacy/>).
#[arg(long, env = EnvVars::UV_PUBLISH_URL)]
pub publish_url: Option<Url>,
/// ```toml
/// [[tool.uv.index]]
/// name = "pypi"
/// url = "https://pypi.org/simple"
/// publish-url = "https://upload.pypi.org/legacy/"
/// ```
///
/// The index `url` will be used to check for existing files to skip duplicate uploads.
///
/// With these settings, the following two calls are equivalent:
///
/// ```
/// uv publish --index pypi
/// uv publish --publish-url https://upload.pypi.org/legacy/ --check-url https://pypi.org/simple
/// ```
#[arg(
long,
env = EnvVars::UV_PUBLISH_INDEX,
conflicts_with = "publish_url",
conflicts_with = "check_url"
)]
pub index: Option<String>,

/// The username for the upload.
#[arg(short, long, env = EnvVars::UV_PUBLISH_USERNAME)]
Expand Down Expand Up @@ -5227,6 +5245,15 @@ pub struct PublishArgs {
#[arg(long, value_enum, env = EnvVars::UV_KEYRING_PROVIDER)]
pub keyring_provider: Option<KeyringProviderType>,

/// The URL of the upload endpoint (not the index URL).
///
/// Note that there are typically different URLs for index access (e.g., `https:://.../simple`)
/// and index upload.
///
/// Defaults to PyPI's publish URL (<https://upload.pypi.org/legacy/>).
#[arg(long, env = EnvVars::UV_PUBLISH_URL)]
pub publish_url: Option<Url>,

/// Check an index URL for existing files to skip duplicate uploads.
///
/// This option allows retrying publishing that failed after only some, but not all files have
Expand Down
19 changes: 19 additions & 0 deletions crates/uv-distribution-types/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{IndexUrl, IndexUrlError};

#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct Index {
/// The name of the index.
///
Expand Down Expand Up @@ -67,6 +68,19 @@ pub struct Index {
// /// can point to either local or remote resources.
// #[serde(default)]
// pub r#type: IndexKind,
/// The URL of the upload endpoint.
///
/// When using `uv publish --index <name>`, this URL is used for publishing.
///
/// A configuration for the default index PyPI would look as follows:
///
/// ```toml
/// [[tool.uv.index]]
/// name = "pypi"
/// url = "https://pypi.org/simple"
/// publish-url = "https://upload.pypi.org/legacy/"
/// ```
pub publish_url: Option<Url>,
}

// #[derive(
Expand All @@ -90,6 +104,7 @@ impl Index {
explicit: false,
default: true,
origin: None,
publish_url: None,
}
}

Expand All @@ -101,6 +116,7 @@ impl Index {
explicit: false,
default: false,
origin: None,
publish_url: None,
}
}

Expand All @@ -112,6 +128,7 @@ impl Index {
explicit: false,
default: false,
origin: None,
publish_url: None,
}
}

Expand Down Expand Up @@ -166,6 +183,7 @@ impl FromStr for Index {
explicit: false,
default: false,
origin: None,
publish_url: None,
});
}
}
Expand All @@ -178,6 +196,7 @@ impl FromStr for Index {
explicit: false,
default: false,
origin: None,
publish_url: None,
})
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/uv-static/src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ impl EnvVars {
/// will use this token (with the username `__token__`) for publishing.
pub const UV_PUBLISH_TOKEN: &'static str = "UV_PUBLISH_TOKEN";

/// Equivalent to the `--index` command-line argument in `uv publish`. If
/// set, uv the index with this name in the configuration for publishing.
pub const UV_PUBLISH_INDEX: &'static str = "UV_PUBLISH_INDEX";

/// Equivalent to the `--username` command-line argument in `uv publish`. If
/// set, uv will use this username for publishing.
pub const UV_PUBLISH_USERNAME: &'static str = "UV_PUBLISH_USERNAME";
Expand Down
40 changes: 39 additions & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::process::ExitCode;
use std::sync::atomic::Ordering;

use anstream::eprintln;
use anyhow::{bail, Result};
use anyhow::{bail, Context, Result};
use clap::error::{ContextKind, ContextValue};
use clap::{CommandFactory, Parser};
use owo_colors::OwoColorize;
Expand Down Expand Up @@ -1210,8 +1210,46 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
trusted_publishing,
keyring_provider,
check_url,
index,
index_locations,
} = PublishSettings::resolve(args, filesystem);

let (publish_url, check_url) = if let Some(index_name) = index {
debug!("Publishing with index {index_name}");
let index = index_locations
.indexes()
.find(|index| {
index
.name
.as_ref()
.is_some_and(|name| name.as_ref() == index_name)
})
.with_context(|| {
let mut index_names: Vec<String> = index_locations
.indexes()
.filter_map(|index| index.name.as_ref())
.map(ToString::to_string)
.collect();
index_names.sort();
if index_names.is_empty() {
format!("No indexes were found, can't use index: `{index_name}`")
} else {
let index_names = index_names.join("`, `");
format!(
"Index not found: `{index_name}`. Found indexes: `{index_names}`"
)
}
})?;
let publish_url = index
.publish_url
.clone()
.with_context(|| format!("Index is missing a publish URL: `{index_name}`"))?;
let check_url = index.url.clone();
(publish_url, Some(check_url))
} else {
(publish_url, check_url)
};

commands::publish(
files,
publish_url,
Expand Down
21 changes: 20 additions & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2854,12 +2854,16 @@ pub(crate) struct PublishSettings {
pub(crate) files: Vec<String>,
pub(crate) username: Option<String>,
pub(crate) password: Option<String>,
pub(crate) index: Option<String>,

// Both CLI and configuration.
pub(crate) publish_url: Url,
pub(crate) trusted_publishing: TrustedPublishing,
pub(crate) keyring_provider: KeyringProviderType,
pub(crate) check_url: Option<IndexUrl>,

// Configuration only
pub(crate) index_locations: IndexLocations,
}

impl PublishSettings {
Expand All @@ -2877,7 +2881,11 @@ impl PublishSettings {
check_url,
} = publish;
let ResolverInstallerOptions {
keyring_provider, ..
keyring_provider,
index,
extra_index_url,
index_url,
..
} = top_level;

// Tokens are encoded in the same way as username/password
Expand All @@ -2903,6 +2911,17 @@ impl PublishSettings {
.combine(keyring_provider)
.unwrap_or_default(),
check_url: args.check_url.combine(check_url),
index: args.index,
index_locations: IndexLocations::new(
index
.into_iter()
.flatten()
.chain(extra_index_url.into_iter().flatten().map(Index::from))
.chain(index_url.into_iter().map(Index::from))
.collect(),
Vec::new(),
false,
),
}
}
}
Expand Down
72 changes: 71 additions & 1 deletion crates/uv/tests/it/publish.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::common::{uv_snapshot, venv_bin_path, TestContext};
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::{FileTouch, PathChild};
use assert_fs::fixture::{FileTouch, FileWriteStr, PathChild};
use indoc::indoc;
use std::env;
use std::env::current_dir;
use uv_static::EnvVars;

#[test]
Expand Down Expand Up @@ -324,3 +326,71 @@ fn check_keyring_behaviours() {
"###
);
}

#[test]
fn invalid_index() {
let context = TestContext::new("3.12");

let pyproject_toml = indoc! {r#"
[project]
name = "foo"
version = "0.1.0"
[[tool.uv.index]]
name = "foo"
url = "https://example.com"
[[tool.uv.index]]
name = "internal"
url = "https://internal.example.org"
"#};
context
.temp_dir
.child("pyproject.toml")
.write_str(pyproject_toml)
.unwrap();

let ok_wheel = current_dir()
.unwrap()
.join("../../scripts/links/ok-1.0.0-py3-none-any.whl");

// No such index
uv_snapshot!(context.filters(), context.publish()
.arg("-u")
.arg("__token__")
.arg("-p")
.arg("dummy")
.arg("--index")
.arg("bar")
.arg(&ok_wheel)
.current_dir(context.temp_dir.path()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv publish` is experimental and may change without warning
error: Index not found: `bar`. Found indexes: `foo`, `internal`
"###
);

// Index does not have a publish URL
uv_snapshot!(context.filters(), context.publish()
.arg("-u")
.arg("__token__")
.arg("-p")
.arg("dummy")
.arg("--index")
.arg("foo")
.arg(&ok_wheel)
.current_dir(context.temp_dir.path()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv publish` is experimental and may change without warning
error: Index is missing a publish URL: `foo`
"###
);
}
Loading

0 comments on commit 321101d

Please sign in to comment.