Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publish: Support --index <name> #9694

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
39 changes: 33 additions & 6 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5119,14 +5119,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 @@ -5166,6 +5184,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";
konstin marked this conversation as resolved.
Show resolved Hide resolved

/// 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 @@ -1203,8 +1203,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 @@ -2829,12 +2829,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 @@ -2852,7 +2856,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 @@ -2878,6 +2886,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
Loading