mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Merge 51bd377d94
into f609e1ddaf
This commit is contained in:
commit
c85c31d735
6 changed files with 277 additions and 138 deletions
|
@ -58,6 +58,15 @@ pub enum ListFormat {
|
|||
Json,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, clap::ValueEnum)]
|
||||
pub enum SelfUpdateFormat {
|
||||
/// Output plain text messages.
|
||||
#[default]
|
||||
Text,
|
||||
/// Output result as JSON.
|
||||
Json,
|
||||
}
|
||||
|
||||
fn extra_name_with_clap_error(arg: &str) -> Result<ExtraName> {
|
||||
ExtraName::from_str(arg).map_err(|_err| {
|
||||
anyhow!(
|
||||
|
@ -651,6 +660,10 @@ pub struct SelfUpdateArgs {
|
|||
/// Run without performing the update.
|
||||
#[arg(long)]
|
||||
pub dry_run: bool,
|
||||
|
||||
/// The format in which the result would be displayed.
|
||||
#[arg(long, value_enum, default_value_t = SelfUpdateFormat::default())]
|
||||
pub output_format: SelfUpdateFormat,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use std::fmt::Write;
|
||||
use std::fmt::{Display, Write};
|
||||
|
||||
use anyhow::Result;
|
||||
use axoupdater::{AxoUpdater, AxoupdateError, UpdateRequest};
|
||||
use axoupdater::{AxoUpdater, AxoupdateError, UpdateRequest, Version};
|
||||
use owo_colors::OwoColorize;
|
||||
use serde::{Serialize, Serializer};
|
||||
use tracing::debug;
|
||||
|
||||
use uv_cli::SelfUpdateFormat;
|
||||
use uv_client::WrappedReqwestError;
|
||||
use uv_fs::Simplified;
|
||||
|
||||
|
@ -12,27 +14,204 @@ use crate::commands::ExitStatus;
|
|||
use crate::printer::Printer;
|
||||
use crate::settings::NetworkSettings;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
struct VersionWrapper(Version);
|
||||
|
||||
impl Display for VersionWrapper {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Version> for VersionWrapper {
|
||||
fn from(value: Version) -> VersionWrapper {
|
||||
VersionWrapper(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for VersionWrapper {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "result", rename_all = "kebab-case")]
|
||||
enum SelfUpdateOutput {
|
||||
Offline,
|
||||
ExternallyInstalled,
|
||||
MultipleInstallations {
|
||||
current: String,
|
||||
other: String,
|
||||
},
|
||||
#[serde(rename = "github-rate-limit-exceeded")]
|
||||
GitHubRateLimitExceeded,
|
||||
OnLatest {
|
||||
version: String,
|
||||
#[serde(skip_serializing)]
|
||||
dry_run: bool,
|
||||
},
|
||||
WouldUpdate {
|
||||
from: String,
|
||||
to: String,
|
||||
},
|
||||
Updated {
|
||||
from: Option<VersionWrapper>,
|
||||
to: VersionWrapper,
|
||||
tag: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl SelfUpdateOutput {
|
||||
fn exit_status(&self) -> ExitStatus {
|
||||
match self {
|
||||
Self::Offline => ExitStatus::Failure,
|
||||
Self::ExternallyInstalled => ExitStatus::Error,
|
||||
Self::MultipleInstallations { .. } => ExitStatus::Error,
|
||||
Self::GitHubRateLimitExceeded => ExitStatus::Error,
|
||||
Self::WouldUpdate { .. } => ExitStatus::Success,
|
||||
Self::Updated { .. } => ExitStatus::Success,
|
||||
Self::OnLatest { .. } => ExitStatus::Success,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to update the uv binary.
|
||||
pub(crate) async fn self_update(
|
||||
version: Option<String>,
|
||||
token: Option<String>,
|
||||
dry_run: bool,
|
||||
output_format: SelfUpdateFormat,
|
||||
printer: Printer,
|
||||
network_settings: NetworkSettings,
|
||||
) -> Result<ExitStatus> {
|
||||
if network_settings.connectivity.is_offline() {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format_args!(
|
||||
concat!(
|
||||
"{}{} Self-update is not possible because network connectivity is disabled (i.e., with `--offline`)"
|
||||
),
|
||||
"error".red().bold(),
|
||||
":".bold()
|
||||
let output = self_update_impl(
|
||||
version,
|
||||
token,
|
||||
dry_run,
|
||||
output_format,
|
||||
printer,
|
||||
network_settings,
|
||||
)
|
||||
.await?;
|
||||
let exit_status = output.exit_status();
|
||||
|
||||
if matches!(output_format, SelfUpdateFormat::Json) {
|
||||
writeln!(printer.stdout(), "{}", serde_json::to_string(&output)?)?;
|
||||
return Ok(exit_status);
|
||||
}
|
||||
|
||||
let message = match output {
|
||||
SelfUpdateOutput::Offline => format!(
|
||||
concat!(
|
||||
"{}{} Self-update is not possible because network connectivity is disabled (i.e., with `--offline`)"
|
||||
),
|
||||
"error".red().bold(),
|
||||
":".bold()
|
||||
),
|
||||
SelfUpdateOutput::ExternallyInstalled => format!(
|
||||
concat!(
|
||||
"{}{} Self-update is only available for uv binaries installed via the standalone installation scripts.",
|
||||
"\n",
|
||||
"\n",
|
||||
"If you installed uv with pip, brew, or another package manager, update uv with `pip install --upgrade`, `brew upgrade`, or similar."
|
||||
),
|
||||
"error".red().bold(),
|
||||
":".bold()
|
||||
),
|
||||
SelfUpdateOutput::MultipleInstallations { current, other } => format!(
|
||||
concat!(
|
||||
"{}{} Self-update is only available for uv binaries installed via the standalone installation scripts.",
|
||||
"\n",
|
||||
"\n",
|
||||
"The current executable is at `{}` but the standalone installer was used to install uv to `{}`. Are multiple copies of uv installed?"
|
||||
),
|
||||
"error".red().bold(),
|
||||
":".bold(),
|
||||
current.bold().cyan(),
|
||||
other.bold().cyan()
|
||||
),
|
||||
SelfUpdateOutput::GitHubRateLimitExceeded => format!(
|
||||
"{}{} GitHub API rate limit exceeded. Please provide a GitHub token via the {} option.",
|
||||
"error".red().bold(),
|
||||
":".bold(),
|
||||
"`--token`".green().bold()
|
||||
),
|
||||
|
||||
SelfUpdateOutput::OnLatest { version, dry_run } => {
|
||||
if dry_run {
|
||||
format!(
|
||||
"You're on the latest version of uv ({})",
|
||||
format!("v{version}").bold().white()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}{} You're on the latest version of uv ({})",
|
||||
"success".green().bold(),
|
||||
":".bold(),
|
||||
format!("v{version}").bold().cyan()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SelfUpdateOutput::WouldUpdate { from, to } => {
|
||||
let to = if to == "latest" {
|
||||
"the latest version".to_string()
|
||||
} else {
|
||||
format!("v{to}")
|
||||
};
|
||||
|
||||
format!(
|
||||
"Would update uv from {} to {}",
|
||||
format!("v{from}").bold().white(),
|
||||
to.bold().white(),
|
||||
)
|
||||
)?;
|
||||
return Ok(ExitStatus::Failure);
|
||||
}
|
||||
|
||||
SelfUpdateOutput::Updated { from, to, tag } => {
|
||||
let direction = if from.as_ref().is_some_and(|from| *from > to) {
|
||||
"Downgraded"
|
||||
} else {
|
||||
"Upgraded"
|
||||
};
|
||||
|
||||
let version_information = if let Some(from) = from {
|
||||
format!(
|
||||
"from {} to {}",
|
||||
format!("v{from}").bold().cyan(),
|
||||
format!("v{to}").bold().cyan(),
|
||||
)
|
||||
} else {
|
||||
format!("to {}", format!("v{to}").bold().cyan())
|
||||
};
|
||||
|
||||
format!(
|
||||
"{}{} {direction} uv {}! {}",
|
||||
"success".green().bold(),
|
||||
":".bold(),
|
||||
version_information,
|
||||
format!("https://github.com/astral-sh/uv/releases/tag/{tag}").cyan()
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
writeln!(printer.stderr(), "{message}")?;
|
||||
Ok(exit_status)
|
||||
}
|
||||
|
||||
async fn self_update_impl(
|
||||
version: Option<String>,
|
||||
token: Option<String>,
|
||||
dry_run: bool,
|
||||
output_format: SelfUpdateFormat,
|
||||
printer: Printer,
|
||||
network_settings: NetworkSettings,
|
||||
) -> Result<SelfUpdateOutput> {
|
||||
if network_settings.connectivity.is_offline() {
|
||||
return Ok(SelfUpdateOutput::Offline);
|
||||
}
|
||||
|
||||
let mut updater = AxoUpdater::new_for("uv");
|
||||
|
@ -46,21 +225,7 @@ pub(crate) async fn self_update(
|
|||
// uv was likely installed via a package manager.
|
||||
let Ok(updater) = updater.load_receipt() else {
|
||||
debug!("No receipt found; assuming uv was installed via a package manager");
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format_args!(
|
||||
concat!(
|
||||
"{}{} Self-update is only available for uv binaries installed via the standalone installation scripts.",
|
||||
"\n",
|
||||
"\n",
|
||||
"If you installed uv with pip, brew, or another package manager, update uv with `pip install --upgrade`, `brew upgrade`, or similar."
|
||||
),
|
||||
"error".red().bold(),
|
||||
":".bold()
|
||||
)
|
||||
)?;
|
||||
return Ok(ExitStatus::Error);
|
||||
return Ok(SelfUpdateOutput::ExternallyInstalled);
|
||||
};
|
||||
|
||||
// If we know what our version is, ignore whatever the receipt thinks it is!
|
||||
|
@ -78,35 +243,24 @@ pub(crate) async fn self_update(
|
|||
let current_exe = std::env::current_exe()?;
|
||||
let receipt_prefix = updater.install_prefix_root()?;
|
||||
|
||||
return Ok(SelfUpdateOutput::MultipleInstallations {
|
||||
current: current_exe.simplified_display().to_string(),
|
||||
other: receipt_prefix.simplified_display().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if matches!(output_format, SelfUpdateFormat::Text) {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format_args!(
|
||||
concat!(
|
||||
"{}{} Self-update is only available for uv binaries installed via the standalone installation scripts.",
|
||||
"\n",
|
||||
"\n",
|
||||
"The current executable is at `{}` but the standalone installer was used to install uv to `{}`. Are multiple copies of uv installed?"
|
||||
),
|
||||
"error".red().bold(),
|
||||
":".bold(),
|
||||
current_exe.simplified_display().bold().cyan(),
|
||||
receipt_prefix.simplified_display().bold().cyan()
|
||||
"{}{} Checking for updates...",
|
||||
"info".cyan().bold(),
|
||||
":".bold()
|
||||
)
|
||||
)?;
|
||||
return Ok(ExitStatus::Error);
|
||||
}
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format_args!(
|
||||
"{}{} Checking for updates...",
|
||||
"info".cyan().bold(),
|
||||
":".bold()
|
||||
)
|
||||
)?;
|
||||
|
||||
let update_request = if let Some(version) = version {
|
||||
UpdateRequest::SpecificTag(version)
|
||||
} else {
|
||||
|
@ -118,108 +272,49 @@ pub(crate) async fn self_update(
|
|||
if dry_run {
|
||||
// TODO(charlie): `updater.fetch_release` isn't public, so we can't say what the latest
|
||||
// version is.
|
||||
if updater.is_update_needed().await? {
|
||||
return if updater.is_update_needed().await? {
|
||||
let version = match update_request {
|
||||
UpdateRequest::Latest | UpdateRequest::LatestMaybePrerelease => {
|
||||
"the latest version".to_string()
|
||||
"latest".to_string()
|
||||
}
|
||||
UpdateRequest::SpecificTag(version) | UpdateRequest::SpecificVersion(version) => {
|
||||
format!("v{version}")
|
||||
version
|
||||
}
|
||||
};
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Would update uv from {} to {}",
|
||||
format!("v{}", env!("CARGO_PKG_VERSION")).bold().white(),
|
||||
version.bold().white(),
|
||||
)?;
|
||||
|
||||
Ok(SelfUpdateOutput::WouldUpdate {
|
||||
from: env!("CARGO_PKG_VERSION").to_string(),
|
||||
to: version,
|
||||
})
|
||||
} else {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format_args!(
|
||||
"You're on the latest version of uv ({})",
|
||||
format!("v{}", env!("CARGO_PKG_VERSION")).bold().white()
|
||||
)
|
||||
)?;
|
||||
}
|
||||
return Ok(ExitStatus::Success);
|
||||
Ok(SelfUpdateOutput::OnLatest {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
dry_run: true,
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// Run the updater. This involves a network request, since we need to determine the latest
|
||||
// available version of uv.
|
||||
match updater.run().await {
|
||||
Ok(Some(result)) => {
|
||||
let direction = if result
|
||||
.old_version
|
||||
.as_ref()
|
||||
.is_some_and(|old_version| *old_version > result.new_version)
|
||||
{
|
||||
"Downgraded"
|
||||
} else {
|
||||
"Upgraded"
|
||||
};
|
||||
|
||||
let version_information = if let Some(old_version) = result.old_version {
|
||||
format!(
|
||||
"from {} to {}",
|
||||
format!("v{old_version}").bold().cyan(),
|
||||
format!("v{}", result.new_version).bold().cyan(),
|
||||
)
|
||||
} else {
|
||||
format!("to {}", format!("v{}", result.new_version).bold().cyan())
|
||||
};
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format_args!(
|
||||
"{}{} {direction} uv {}! {}",
|
||||
"success".green().bold(),
|
||||
":".bold(),
|
||||
version_information,
|
||||
format!(
|
||||
"https://github.com/astral-sh/uv/releases/tag/{}",
|
||||
result.new_version_tag
|
||||
)
|
||||
.cyan()
|
||||
)
|
||||
)?;
|
||||
}
|
||||
Ok(None) => {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format_args!(
|
||||
"{}{} You're on the latest version of uv ({})",
|
||||
"success".green().bold(),
|
||||
":".bold(),
|
||||
format!("v{}", env!("CARGO_PKG_VERSION")).bold().cyan()
|
||||
)
|
||||
)?;
|
||||
}
|
||||
Err(err) => {
|
||||
return if let AxoupdateError::Reqwest(err) = err {
|
||||
Ok(Some(result)) => Ok(SelfUpdateOutput::Updated {
|
||||
from: result.old_version.map(VersionWrapper),
|
||||
to: result.new_version.into(),
|
||||
tag: result.new_version_tag,
|
||||
}),
|
||||
Ok(None) => Ok(SelfUpdateOutput::OnLatest {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
dry_run: false,
|
||||
}),
|
||||
Err(err) => match err {
|
||||
AxoupdateError::Reqwest(err) => {
|
||||
if err.status() == Some(http::StatusCode::FORBIDDEN) && token.is_none() {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format_args!(
|
||||
"{}{} GitHub API rate limit exceeded. Please provide a GitHub token via the {} option.",
|
||||
"error".red().bold(),
|
||||
":".bold(),
|
||||
"`--token`".green().bold()
|
||||
)
|
||||
)?;
|
||||
Ok(ExitStatus::Error)
|
||||
Ok(SelfUpdateOutput::GitHubRateLimitExceeded)
|
||||
} else {
|
||||
Err(WrappedReqwestError::from(err).into())
|
||||
}
|
||||
} else {
|
||||
Err(err.into())
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => Err(err.into()),
|
||||
},
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
|
|
@ -1076,12 +1076,14 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
target_version,
|
||||
token,
|
||||
dry_run,
|
||||
output_format,
|
||||
}),
|
||||
}) => {
|
||||
commands::self_update(
|
||||
target_version,
|
||||
token,
|
||||
dry_run,
|
||||
output_format,
|
||||
printer,
|
||||
globals.network_settings,
|
||||
)
|
||||
|
|
|
@ -400,6 +400,15 @@ impl TestContext {
|
|||
self
|
||||
}
|
||||
|
||||
/// Add a filter that filters out version fields in `self version`'s JSON output.
|
||||
pub fn with_filtered_version_fields(mut self) -> Self {
|
||||
self.filters.push((
|
||||
r#""(version|from|to)":"[^"]+""#.to_string(),
|
||||
r#""$1":"<version>""#.to_string(),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
/// Clear filters on `TestContext`.
|
||||
pub fn clear_filters(mut self) -> Self {
|
||||
self.filters.clear();
|
||||
|
|
|
@ -57,3 +57,18 @@ fn test_self_update_offline_error() {
|
|||
error: Self-update is not possible because network connectivity is disabled (i.e., with `--offline`)
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_self_update_offline_json() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
uv_snapshot!(context.self_update().arg("--offline").arg("--output-format=json"),
|
||||
@r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{"result":"offline"}
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
}
|
||||
|
|
|
@ -5226,7 +5226,12 @@ uv self update [OPTIONS] [TARGET_VERSION]
|
|||
<p>May also be set with the <code>UV_NO_PROGRESS</code> environment variable.</p></dd><dt id="uv-self-update--no-python-downloads"><a href="#uv-self-update--no-python-downloads"><code>--no-python-downloads</code></a></dt><dd><p>Disable automatic downloads of Python.</p>
|
||||
</dd><dt id="uv-self-update--offline"><a href="#uv-self-update--offline"><code>--offline</code></a></dt><dd><p>Disable network access.</p>
|
||||
<p>When disabled, uv will only use locally cached data and locally available files.</p>
|
||||
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-self-update--project"><a href="#uv-self-update--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
|
||||
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-self-update--output-format"><a href="#uv-self-update--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>The format in which the result would be displayed</p>
|
||||
<p>[default: text]</p><p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>text</code>: Output plain text messages</li>
|
||||
<li><code>json</code>: Output result as JSON</li>
|
||||
</ul></dd><dt id="uv-self-update--project"><a href="#uv-self-update--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
|
||||
<p>All <code>pyproject.toml</code>, <code>uv.toml</code>, and <code>.python-version</code> files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (<code>.venv</code>).</p>
|
||||
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
|
||||
<p>See <code>--directory</code> to change the working directory entirely.</p>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue