diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2024-08-09 10:44:21 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-09 09:44:21 +0000 |
commit | c9f626e2512d52fdc354e490b179eed7200b394b (patch) | |
tree | cc8e52db954022b44deabfba8e3be6a9a2b8624f /cli/tools/upgrade.rs | |
parent | c21f42c825b19d7309d883513d21f62395996f78 (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.rs | 495 |
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(¤t_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(¤t_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(¤t_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(¤t_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(¤t_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") |