summaryrefslogtreecommitdiff
path: root/cli/tools/upgrade.rs
diff options
context:
space:
mode:
authorBartek IwaƄczuk <biwanczuk@gmail.com>2024-08-09 10:44:21 +0100
committerGitHub <noreply@github.com>2024-08-09 09:44:21 +0000
commitc9f626e2512d52fdc354e490b179eed7200b394b (patch)
treecc8e52db954022b44deabfba8e3be6a9a2b8624f /cli/tools/upgrade.rs
parentc21f42c825b19d7309d883513d21f62395996f78 (diff)
refactor(upgrade): cleanup pass (#24954)
No functional changes, just factoring out some helpers to make it easier to update and test.
Diffstat (limited to 'cli/tools/upgrade.rs')
-rw-r--r--cli/tools/upgrade.rs495
1 files changed, 311 insertions, 184 deletions
diff --git a/cli/tools/upgrade.rs b/cli/tools/upgrade.rs
index 830f108e6..fe5c565f0 100644
--- a/cli/tools/upgrade.rs
+++ b/cli/tools/upgrade.rs
@@ -18,6 +18,7 @@ use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_core::unsync::spawn;
+use deno_core::url::Url;
use deno_semver::Version;
use once_cell::sync::Lazy;
use std::borrow::Cow;
@@ -32,6 +33,7 @@ use std::sync::Arc;
use std::time::Duration;
const RELEASE_URL: &str = "https://github.com/denoland/deno/releases";
+const CANARY_URL: &str = "https://dl.deno.land/canary";
pub static ARCHIVE_NAME: Lazy<String> =
Lazy::new(|| format!("deno-{}.zip", env!("TARGET")));
@@ -236,22 +238,24 @@ fn get_minor_version(version: &str) -> &str {
}
fn print_release_notes(current_version: &str, new_version: &str) {
- if get_minor_version(current_version) != get_minor_version(new_version) {
- log::info!(
- "Release notes:\n\n {}\n",
- colors::bold(format!(
- "https://github.com/denoland/deno/releases/tag/v{}",
- &new_version,
- ))
- );
- log::info!(
- "Blog post:\n\n {}\n",
- colors::bold(format!(
- "https://deno.com/blog/v{}",
- get_minor_version(new_version)
- ))
- );
+ if get_minor_version(current_version) == get_minor_version(new_version) {
+ return;
}
+
+ log::info!(
+ "Release notes:\n\n {}\n",
+ colors::bold(format!(
+ "https://github.com/denoland/deno/releases/tag/v{}",
+ &new_version,
+ ))
+ );
+ log::info!(
+ "Blog post:\n\n {}\n",
+ colors::bold(format!(
+ "https://deno.com/blog/v{}",
+ get_minor_version(new_version)
+ ))
+ );
}
pub fn upgrade_check_enabled() -> bool {
@@ -411,7 +415,8 @@ pub async fn upgrade(
upgrade_flags: UpgradeFlags,
) -> Result<(), AnyError> {
let factory = CliFactory::from_flags(flags);
- let client = factory.http_client_provider().get_or_create()?;
+ let http_client_provider = factory.http_client_provider();
+ let client = http_client_provider.get_or_create()?;
let current_exe_path = std::env::current_exe()?;
let full_path_output_flag = match &upgrade_flags.output {
Some(output) => Some(
@@ -424,131 +429,43 @@ pub async fn upgrade(
let output_exe_path =
full_path_output_flag.as_ref().unwrap_or(&current_exe_path);
- let permissions = if let Ok(metadata) = fs::metadata(output_exe_path) {
- let permissions = metadata.permissions();
- if permissions.readonly() {
- bail!(
- "You do not have write permission to {}",
- output_exe_path.display()
- );
- }
- #[cfg(unix)]
- if std::os::unix::fs::MetadataExt::uid(&metadata) == 0
- && !nix::unistd::Uid::effective().is_root()
- {
- bail!(concat!(
- "You don't have write permission to {} because it's owned by root.\n",
- "Consider updating deno through your package manager if its installed from it.\n",
- "Otherwise run `deno upgrade` as root.",
- ), output_exe_path.display());
- }
- permissions
- } else {
- fs::metadata(&current_exe_path)?.permissions()
- };
-
- let install_version = match upgrade_flags.version {
- Some(passed_version) => {
- let re_hash = lazy_regex::regex!("^[0-9a-f]{40}$");
- let passed_version = passed_version
- .strip_prefix('v')
- .unwrap_or(&passed_version)
- .to_string();
+ let permissions = set_exe_permissions(&current_exe_path, output_exe_path)?;
- if upgrade_flags.canary && !re_hash.is_match(&passed_version) {
- bail!("Invalid commit hash passed");
- } else if !upgrade_flags.canary
- && Version::parse_standard(&passed_version).is_err()
- {
- bail!("Invalid version passed");
- }
+ let force_selection_of_new_version =
+ upgrade_flags.force || full_path_output_flag.is_some();
- let current_is_passed = if upgrade_flags.canary {
- crate::version::GIT_COMMIT_HASH == passed_version
- } else if !crate::version::is_canary() {
- crate::version::deno() == passed_version
- } else {
- false
- };
+ let requested_version =
+ RequestedVersion::from_upgrade_flags(upgrade_flags.clone())?;
- if !upgrade_flags.force
- && full_path_output_flag.is_none()
- && current_is_passed
- {
- log::info!("Version {} is already installed", crate::version::deno());
- return Ok(());
- }
-
- passed_version
- }
- None => {
- let release_channel = if upgrade_flags.canary {
- log::info!("{}", colors::gray("Looking up latest canary version"));
- ReleaseChannel::Canary
- } else {
- log::info!("{}", colors::gray("Looking up latest version"));
- ReleaseChannel::Stable
- };
-
- let latest_version = fetch_latest_version(
- &client,
- release_channel,
- UpgradeCheckKind::Execution,
+ let maybe_install_version = match requested_version {
+ RequestedVersion::Latest(channel) => {
+ find_latest_version_to_upgrade(
+ http_client_provider.clone(),
+ channel,
+ force_selection_of_new_version,
)
- .await?;
-
- let current_is_most_recent = if upgrade_flags.canary {
- let latest_hash = &latest_version;
- crate::version::GIT_COMMIT_HASH == latest_hash
- } else if !crate::version::is_canary() {
- let current = Version::parse_standard(crate::version::deno()).unwrap();
- let latest = Version::parse_standard(&latest_version).unwrap();
- current >= latest
- } else {
- false
- };
-
- if !upgrade_flags.force
- && full_path_output_flag.is_none()
- && current_is_most_recent
- {
- log::info!(
- "{}",
- colors::green(format!(
- "\nLocal deno version {} is the most recent release\n",
- if upgrade_flags.canary {
- crate::version::GIT_COMMIT_HASH
- } else {
- crate::version::deno()
- }
- ))
- );
- return Ok(());
- } else {
- log::info!(
- "{}",
- colors::bold(format!("\nFound latest version {}\n", latest_version))
- );
- latest_version
- }
+ .await?
+ }
+ RequestedVersion::SpecificVersion(channel, version) => {
+ select_specific_version_for_upgrade(
+ channel,
+ version,
+ force_selection_of_new_version,
+ )?
}
};
- let download_url = if upgrade_flags.canary {
- format!(
- "https://dl.deno.land/canary/{}/{}",
- install_version, *ARCHIVE_NAME
- )
- } else {
- format!(
- "{}/download/v{}/{}",
- RELEASE_URL, install_version, *ARCHIVE_NAME
- )
+ let Some(install_version) = maybe_install_version else {
+ return Ok(());
};
- let archive_data = download_package(&client, &download_url)
- .await
- .with_context(|| format!("Failed downloading {download_url}. The version you requested may not have been built for the current architecture."))?;
+ let download_url = get_download_url(&install_version, upgrade_flags.canary)?;
+ log::info!("{}", colors::gray(format!("Downloading {}", &download_url)));
+ let Some(archive_data) = download_package(&client, download_url).await?
+ else {
+ log::error!("Download could not be found, aborting");
+ std::process::exit(1)
+ };
log::info!(
"{}",
@@ -572,50 +489,180 @@ pub async fn upgrade(
if !upgrade_flags.canary {
print_release_notes(version::deno(), &install_version);
}
+ drop(temp_dir);
+ return Ok(());
+ }
+
+ let output_exe_path =
+ full_path_output_flag.as_ref().unwrap_or(&current_exe_path);
+ let output_result = if *output_exe_path == current_exe_path {
+ replace_exe(&new_exe_path, output_exe_path)
} else {
- let output_exe_path =
- full_path_output_flag.as_ref().unwrap_or(&current_exe_path);
- let output_result = if *output_exe_path == current_exe_path {
- replace_exe(&new_exe_path, output_exe_path)
+ fs::rename(&new_exe_path, output_exe_path)
+ .or_else(|_| fs::copy(&new_exe_path, output_exe_path).map(|_| ()))
+ };
+ check_windows_access_denied_error(output_result, output_exe_path)?;
+
+ log::info!(
+ "{}",
+ colors::green(format!(
+ "\nUpgraded successfully to Deno v{}\n",
+ install_version
+ ))
+ );
+ if !upgrade_flags.canary {
+ print_release_notes(version::deno(), &install_version);
+ }
+
+ drop(temp_dir); // delete the temp dir
+ Ok(())
+}
+
+enum RequestedVersion {
+ Latest(ReleaseChannel),
+ SpecificVersion(ReleaseChannel, String),
+}
+
+impl RequestedVersion {
+ fn from_upgrade_flags(upgrade_flags: UpgradeFlags) -> Result<Self, AnyError> {
+ let is_canary = upgrade_flags.canary;
+
+ let Some(passed_version) = upgrade_flags.version else {
+ let channel = if is_canary {
+ ReleaseChannel::Canary
+ } else {
+ ReleaseChannel::Stable
+ };
+ return Ok(Self::Latest(channel));
+ };
+
+ let re_hash = lazy_regex::regex!("^[0-9a-f]{40}$");
+ let passed_version = passed_version
+ .strip_prefix('v')
+ .unwrap_or(&passed_version)
+ .to_string();
+
+ let (channel, passed_version) = if is_canary {
+ if !re_hash.is_match(&passed_version) {
+ bail!("Invalid commit hash passed");
+ }
+ (ReleaseChannel::Canary, passed_version)
} else {
- fs::rename(&new_exe_path, output_exe_path)
- .or_else(|_| fs::copy(&new_exe_path, output_exe_path).map(|_| ()))
+ if Version::parse_standard(&passed_version).is_err() {
+ bail!("Invalid version passed");
+ };
+ (ReleaseChannel::Stable, passed_version)
};
- if let Err(err) = output_result {
- const WIN_ERROR_ACCESS_DENIED: i32 = 5;
- if cfg!(windows) && err.raw_os_error() == Some(WIN_ERROR_ACCESS_DENIED) {
- return Err(err).with_context(|| {
- format!(
- concat!(
- "Could not replace the deno executable. This may be because an ",
- "existing deno process is running. Please ensure there are no ",
- "running deno processes (ex. Stop-Process -Name deno ; deno {}), ",
- "close any editors before upgrading, and ensure you have ",
- "sufficient permission to '{}'."
- ),
- // skip the first argument, which is the executable path
- std::env::args().skip(1).collect::<Vec<_>>().join(" "),
- output_exe_path.display(),
- )
- });
+
+ Ok(RequestedVersion::SpecificVersion(channel, passed_version))
+ }
+}
+
+fn select_specific_version_for_upgrade(
+ release_channel: ReleaseChannel,
+ version: String,
+ force: bool,
+) -> Result<Option<String>, AnyError> {
+ match release_channel {
+ ReleaseChannel::Stable => {
+ let current_is_passed = if !version::is_canary() {
+ version::deno() == version
} else {
- return Err(err.into());
+ false
+ };
+
+ if !force && current_is_passed {
+ log::info!("Version {} is already installed", version::deno());
+ return Ok(None);
+ }
+
+ Ok(Some(version))
+ }
+ ReleaseChannel::Canary => {
+ let current_is_passed = version::GIT_COMMIT_HASH == version;
+ if !force && current_is_passed {
+ log::info!("Version {} is already installed", version::deno());
+ return Ok(None);
+ }
+
+ Ok(Some(version))
+ }
+ // TODO(bartlomieju)
+ ReleaseChannel::Rc => unreachable!(),
+ // TODO(bartlomieju)
+ ReleaseChannel::Lts => unreachable!(),
+ }
+}
+
+async fn find_latest_version_to_upgrade(
+ http_client_provider: Arc<HttpClientProvider>,
+ release_channel: ReleaseChannel,
+ force: bool,
+) -> Result<Option<String>, AnyError> {
+ log::info!(
+ "{}",
+ colors::gray(&format!("Looking up {} version", release_channel.name()))
+ );
+
+ let client = http_client_provider.get_or_create()?;
+ let latest_version =
+ fetch_latest_version(&client, release_channel, UpgradeCheckKind::Execution)
+ .await?;
+
+ let (maybe_newer_latest_version, current_version) = match release_channel {
+ ReleaseChannel::Stable => {
+ let current_version = version::deno();
+ let current_is_most_recent = if !version::is_canary() {
+ let current = Version::parse_standard(current_version).unwrap();
+ let latest = Version::parse_standard(&latest_version).unwrap();
+ current >= latest
+ } else {
+ false
+ };
+
+ if !force && current_is_most_recent {
+ (None, current_version)
+ } else {
+ (Some(latest_version), current_version)
}
}
+ ReleaseChannel::Canary => {
+ let current_version = version::GIT_COMMIT_HASH;
+ let current_is_most_recent = current_version == latest_version;
+
+ if !force && current_is_most_recent {
+ (None, current_version)
+ } else {
+ (Some(latest_version), current_version)
+ }
+ }
+ // TODO(bartlomieju)
+ ReleaseChannel::Rc => unreachable!(),
+ // TODO(bartlomieju)
+ ReleaseChannel::Lts => unreachable!(),
+ };
+
+ log::info!("");
+ if let Some(newer_latest_version) = maybe_newer_latest_version.as_ref() {
log::info!(
"{}",
- colors::green(format!(
- "\nUpgraded successfully to Deno v{}\n",
- install_version
- ))
+ color_print::cformat!(
+ "<g>Found latest version {}</>",
+ newer_latest_version
+ )
+ );
+ } else {
+ log::info!(
+ "{}",
+ color_print::cformat!(
+ "<g>Local deno version {} is the most recent release</>",
+ current_version
+ )
);
- if !upgrade_flags.canary {
- print_release_notes(version::deno(), &install_version);
- }
}
+ log::info!("");
- drop(temp_dir); // delete the temp dir
- Ok(())
+ Ok(maybe_newer_latest_version)
}
#[derive(Debug, Clone, Copy)]
@@ -635,6 +682,17 @@ enum ReleaseChannel {
Rc,
}
+impl ReleaseChannel {
+ fn name(&self) -> &str {
+ match self {
+ Self::Stable => "latest",
+ Self::Canary => "canary",
+ Self::Rc => "release candidate",
+ Self::Lts => "LTS (long term support)",
+ }
+ }
+}
+
async fn fetch_latest_version(
client: &HttpClient,
release_channel: ReleaseChannel,
@@ -685,27 +743,34 @@ fn base_upgrade_url() -> Cow<'static, str> {
}
}
+fn get_download_url(version: &str, is_canary: bool) -> Result<Url, AnyError> {
+ let download_url = if is_canary {
+ format!("{}/{}/{}", CANARY_URL, version, *ARCHIVE_NAME)
+ } else {
+ format!("{}/download/v{}/{}", RELEASE_URL, version, *ARCHIVE_NAME)
+ };
+
+ Url::parse(&download_url).with_context(|| {
+ format!(
+ "Failed to parse URL to download new release: {}",
+ download_url
+ )
+ })
+}
+
async fn download_package(
client: &HttpClient,
- download_url: &str,
-) -> Result<Vec<u8>, AnyError> {
- log::info!("{}", colors::gray(format!("Downloading {}", &download_url)));
- let maybe_bytes = {
- let progress_bar = ProgressBar::new(ProgressBarStyle::DownloadBars);
- // provide an empty string here in order to prefer the downloading
- // text above which will stay alive after the progress bars are complete
- let progress = progress_bar.update("");
- client
- .download_with_progress(download_url.parse()?, None, &progress)
- .await?
- };
- match maybe_bytes {
- Some(bytes) => Ok(bytes),
- None => {
- log::error!("Download could not be found, aborting");
- std::process::exit(1)
- }
- }
+ download_url: Url,
+) -> Result<Option<Vec<u8>>, AnyError> {
+ let progress_bar = ProgressBar::new(ProgressBarStyle::DownloadBars);
+ // provide an empty string here in order to prefer the downloading
+ // text above which will stay alive after the progress bars are complete
+ let progress = progress_bar.update("");
+ let maybe_bytes = client
+ .download_with_progress(download_url.clone(), None, &progress)
+ .await
+ .with_context(|| format!("Failed downloading {download_url}. The version you requested may not have been built for the current architecture."))?;
+ Ok(maybe_bytes)
}
fn replace_exe(from: &Path, to: &Path) -> Result<(), std::io::Error> {
@@ -722,6 +787,68 @@ fn replace_exe(from: &Path, to: &Path) -> Result<(), std::io::Error> {
Ok(())
}
+fn check_windows_access_denied_error(
+ output_result: Result<(), std::io::Error>,
+ output_exe_path: &Path,
+) -> Result<(), AnyError> {
+ let Err(err) = output_result else {
+ return Ok(());
+ };
+
+ if !cfg!(windows) {
+ return Err(err.into());
+ }
+
+ const WIN_ERROR_ACCESS_DENIED: i32 = 5;
+ if err.raw_os_error() != Some(WIN_ERROR_ACCESS_DENIED) {
+ return Err(err.into());
+ };
+
+ Err(err).with_context(|| {
+ format!(
+ concat!(
+ "Could not replace the deno executable. This may be because an ",
+ "existing deno process is running. Please ensure there are no ",
+ "running deno processes (ex. Stop-Process -Name deno ; deno {}), ",
+ "close any editors before upgrading, and ensure you have ",
+ "sufficient permission to '{}'."
+ ),
+ // skip the first argument, which is the executable path
+ std::env::args().skip(1).collect::<Vec<_>>().join(" "),
+ output_exe_path.display(),
+ )
+ })
+}
+
+fn set_exe_permissions(
+ current_exe_path: &Path,
+ output_exe_path: &Path,
+) -> Result<std::fs::Permissions, AnyError> {
+ let Ok(metadata) = fs::metadata(output_exe_path) else {
+ let metadata = fs::metadata(current_exe_path)?;
+ return Ok(metadata.permissions());
+ };
+
+ let permissions = metadata.permissions();
+ if permissions.readonly() {
+ bail!(
+ "You do not have write permission to {}",
+ output_exe_path.display()
+ );
+ }
+ #[cfg(unix)]
+ if std::os::unix::fs::MetadataExt::uid(&metadata) == 0
+ && !nix::unistd::Uid::effective().is_root()
+ {
+ bail!(concat!(
+ "You don't have write permission to {} because it's owned by root.\n",
+ "Consider updating deno through your package manager if its installed from it.\n",
+ "Otherwise run `deno upgrade` as root.",
+ ), output_exe_path.display());
+ }
+ Ok(permissions)
+}
+
fn check_exe(exe_path: &Path) -> Result<(), AnyError> {
let output = Command::new(exe_path)
.arg("-V")