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
3 changes: 2 additions & 1 deletion .github/ISSUE_TEMPLATE/step_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ assignees: ''
* Which exact commands should Topgrade run?
* Does it have a `--dry-run` option? i.e., print what should be done and exit
* Does it need the user to confirm the execution? And does it provide a `--yes`
option to skip this?
option to skip this step?
* Can Topgrade extract the components that it updated to use in the summary?

## More information

Expand Down
2 changes: 2 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ If you are an AI agent acting autonomously, please state so.
- [ ] *Optional:* The `--dry-run` option works with this step
- [ ] *Optional:* The `--yes` option works with this step if it is supported by
the underlying command
- [ ] *Optional:* This step extracts and returns its updated components
- [ ] Maintainer: update tracking issue #1918

<!-- If you developed a feature or a bug fix for someone else and you do not have the
means to test it, please tag this person here. -->
81 changes: 76 additions & 5 deletions src/runner.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use color_eyre::eyre::{Result, WrapErr};
use rust_i18n::t;
use std::borrow::Cow;
use std::fmt::Debug;
use std::fmt::{Debug, Display};
use std::io;
use tracing::debug;

Expand All @@ -12,7 +12,7 @@ use crate::step::Step;
use crate::terminal::{ShouldRetry, print_error, print_warning, should_retry};

pub enum StepResult {
Success,
Success(Option<UpdatedComponents>),
Failure,
Ignored,
SkippedMissingSudo,
Expand All @@ -24,7 +24,7 @@ impl StepResult {
use StepResult::*;

match self {
Success | Ignored | Skipped(_) | SkippedMissingSudo => false,
Success(_) | Ignored | Skipped(_) | SkippedMissingSudo => false,
Failure => true,
}
}
Expand All @@ -38,6 +38,61 @@ enum RetryDecision {

type Report<'a> = Vec<(Cow<'a, str>, StepResult)>;

pub struct UpdatedComponents(Vec<UpdatedComponent>);

impl UpdatedComponents {
pub fn new(updated: Vec<UpdatedComponent>) -> Self {
Self(updated)
}
}

impl Display for UpdatedComponents {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0.as_slice() {
[] => write!(f, "No updates found"),
components => {
writeln!(f, "Updated:")?;
let updates = components
.iter()
.map(|c| format!("- {c}"))
.collect::<Vec<_>>()
.join("\n");
write!(f, "{updates}")?;
Ok(())
}
}
}
}

pub struct UpdatedComponent {
name: String,
from_version: Option<String>,
to_version: Option<String>,
}

impl UpdatedComponent {
pub fn new(name: String, from_version: Option<String>, to_version: Option<String>) -> Self {
Self {
name,
from_version,
to_version,
}
}
}

impl Display for UpdatedComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (&self.from_version, &self.to_version) {
(None, None) => write!(f, "{}", self.name),
(None, Some(to_version)) => write!(f, "{} to {}", self.name, to_version),
(Some(from_version), None) => write!(f, "{} from {}", self.name, from_version),
(Some(from_version), Some(to_version)) => {
write!(f, "{} from {} to {}", self.name, from_version, to_version)
}
}
}
}

pub struct Runner<'a> {
ctx: &'a ExecutionContext<'a>,
report: Report<'a>,
Expand Down Expand Up @@ -78,6 +133,22 @@ impl<'a> Runner<'a> {
where
K: Into<Cow<'a, str>> + Debug,
F: Fn() -> Result<()>,
{
self._execute(step, key, || func().map(|()| None))
}

pub fn execute_with_updated<K, F>(&mut self, step: Step, key: K, func: F) -> Result<()>
where
K: Into<Cow<'a, str>> + Debug,
F: Fn() -> Result<Vec<UpdatedComponent>>,
{
self._execute(step, key, || func().map(Some))
}

fn _execute<K, F>(&mut self, step: Step, key: K, func: F) -> Result<()>
where
K: Into<Cow<'a, str>> + Debug,
F: Fn() -> Result<Option<Vec<UpdatedComponent>>>,
{
if !self.ctx.config().should_run(step) {
return Ok(());
Expand All @@ -101,8 +172,8 @@ impl<'a> Runner<'a> {

loop {
match func() {
Ok(()) => {
self.push_result(key, StepResult::Success);
Ok(updated) => {
self.push_result(key, StepResult::Success(updated.map(UpdatedComponents::new)));
break;
}
Err(e) if e.downcast_ref::<DryRun>().is_some() => break,
Expand Down
2 changes: 1 addition & 1 deletion src/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ impl Step {
runner.execute(*self, "tpack", || tmux::run_tpack(ctx))?
}
Typst => runner.execute(*self, "Typst", || generic::run_typst(ctx))?,
Uv => runner.execute(*self, "uv", || generic::run_uv(ctx))?,
Uv => runner.execute_with_updated(*self, "uv", || generic::run_uv(ctx))?,
Vagrant => {
if ctx.config().should_run(Vagrant)
&& let Ok(boxes) = vagrant::collect_boxes(ctx)
Expand Down
59 changes: 47 additions & 12 deletions src/steps/generic.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use color_eyre::eyre::{OptionExt, eyre};
use color_eyre::eyre::{Context, OptionExt, Result, eyre};
use jetbrains_toolbox_updater::{FindError, find_jetbrains_toolbox, update_jetbrains_toolbox};
use regex::bytes::Regex;
use regex::Regex;
Comment thread
GideonBear marked this conversation as resolved.
use rust_i18n::t;
use semver::Version;
use serde::Deserialize;
Expand All @@ -20,6 +18,7 @@
use crate::execution_context::ExecutionContext;
use crate::executor::ExecutorOutput;
use crate::output_changed_message;
use crate::runner::UpdatedComponent;
use crate::step::Step;
use crate::sudo::SudoExecuteOpts;
use crate::terminal::{print_separator, shell};
Expand Down Expand Up @@ -1470,7 +1469,7 @@
let lensfun_update_data = require("lensfun-update-data")?;

print_separator("Lensfun's database update");

if ctx.config().lensfun_use_sudo() {
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, &lensfun_update_data)?
Expand All @@ -1494,8 +1493,8 @@
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;

static SHEBANG_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^#![ \t]*([^ \t\n]+)(?:[ \t]+([^\n]+)?)?").unwrap());
static SHEBANG_REGEX: LazyLock<regex::bytes::Regex> =
LazyLock::new(|| regex::bytes::Regex::new(r"^#![ \t]*([^ \t\n]+)(?:[ \t]+([^\n]+)?)?").unwrap());

let script = fs::read(poetry)?;
if let Some(c) = SHEBANG_REGEX.captures(&script) {
Expand All @@ -1514,8 +1513,9 @@

use std::str;

static SHEBANG_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"^#![ \t]*(?:"([^"\n]+)"|([^" \t\n]+))(?:[ \t]+([^\n]+)?)?"#).unwrap());
static SHEBANG_REGEX: LazyLock<regex::bytes::Regex> = LazyLock::new(|| {
regex::bytes::Regex::new(r#"^#![ \t]*(?:"([^"\n]+)"|([^" \t\n]+))(?:[ \t]+([^\n]+)?)?"#).unwrap()
});

let data = fs::read(poetry)?;

Expand Down Expand Up @@ -1589,10 +1589,12 @@
ctx.execute(&poetry).args(["self", "update"]).status_checked()
}

pub fn run_uv(ctx: &ExecutionContext) -> Result<()> {
pub fn run_uv(ctx: &ExecutionContext) -> Result<Vec<UpdatedComponent>> {
Comment thread
GideonBear marked this conversation as resolved.
let uv_exec = require("uv")?;
print_separator("uv");

let mut updated = vec![];

// Run `uv self update` if the `uv` binary is built with the `self-update`
// cargo feature enabled.
//
Expand Down Expand Up @@ -1630,6 +1632,8 @@
let version =
Version::parse(version_str).wrap_err_with(|| output_changed_message!("uv --version", "Invalid version"))?;

let mut self_output = None;

if version < Version::new(0, 4, 25) {
// For uv before version 0.4.25 (exclusive), the `self` sub-command only
// exists under the `self-update` feature, we run `uv self --help` to check
Expand All @@ -1642,7 +1646,12 @@
.is_ok();

if self_update_feature_enabled {
ctx.execute(&uv_exec).args(["self", "update"]).status_checked()?;
let output = ctx.execute(&uv_exec).args(["self", "update"]).output_checked()?;

std::io::stdout().write_all(&output.stdout)?;
std::io::stderr().write_all(&output.stderr)?;

self_output = Some(output);
}
} else {
// After 0.4.25 (inclusive), running `uv self` succeeds regardless of the
Expand Down Expand Up @@ -1677,7 +1686,7 @@
ExecutorOutput::Wet(wet) => wet,
ExecutorOutput::Dry => return Err(DryRun().into()),
};
let stderr = std::str::from_utf8(&output.stderr).expect("output should be UTF-8 encoded");
let stderr = std::str::from_utf8(&output.stderr).wrap_err("Output should be valid UTF-8")?;

if ERROR_MSGS.iter().any(|&n| stderr.contains(n)) {
// Feature `self-update` is disabled, nothing to do.
Expand All @@ -1691,10 +1700,36 @@
if !output.status.success() {
return Err(eyre!("uv self update failed"));
}

self_output = Some(output);
}
};

// Extract if the self-update happened

static UV_SELF_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"success: (?:(?:You're on the latest version of uv \((?:v[\.0-9]+)\))|(?:Upgraded uv from (v[\.0-9]+) to (v[\.0-9]+)!))"
).expect("Uv self-update output regex always compiles")
});

if let Some(output) = self_output {
let captures = UV_SELF_REGEX
.captures(std::str::from_utf8(&output.stderr).wrap_err("Output should be valid UTF-8")?)
.ok_or_else(|| eyre!(output_changed_message!("uv self update", "regex did not match")))?;
match (captures.get(1), captures.get(2)) {
(None, None) => (),
(Some(from_version), Some(to_version)) => updated.push(UpdatedComponent::new(
"(self-update) uv".to_string(),
Some(from_version.as_str().to_string()),
Some(to_version.as_str().to_string()),
)),
_ => unreachable!("Regex should match none or both groups"),
}
}

// Update the installed tools
// TODO: include this in `updated`
Comment thread Fixed

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
ctx.execute(&uv_exec)
.args(["tool", "upgrade", "--all"])
.status_checked()?;
Expand All @@ -1709,7 +1744,7 @@
ctx.execute(&uv_exec).args(["cache", "prune"]).status_checked()?;
}

Ok(())
Ok(updated)
}

/// Involve `zvm upgrade` to update ZVM
Expand Down
10 changes: 9 additions & 1 deletion src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,15 @@ impl Terminal {
"{}: {}\n",
key,
match result {
StepResult::Success => format!("{}", style(t!("OK")).bold().green()),
StepResult::Success(updated) => {
let mut s = format!("{}", style(t!("OK")).bold().green());
// Only add the ": No updates found" or ": Updated:" when this step
// supports extracting updated components
if let Some(updated) = updated {
s.push_str(&format!(": {updated}"));
}
s
}
StepResult::Failure => format!("{}", style(t!("FAILED")).bold().red()),
StepResult::Ignored => format!("{}", style(t!("IGNORED")).bold().yellow()),
StepResult::SkippedMissingSudo => format!(
Expand Down
Loading