mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
refactor(upgrade): cleanup pass (#24954)
No functional changes, just factoring out some helpers to make it easier to update and test.
This commit is contained in:
parent
c21f42c825
commit
c9f626e251
1 changed files with 317 additions and 190 deletions
|
@ -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 permissions = set_exe_permissions(¤t_exe_path, output_exe_path)?;
|
||||
|
||||
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 force_selection_of_new_version =
|
||||
upgrade_flags.force || full_path_output_flag.is_some();
|
||||
|
||||
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 requested_version =
|
||||
RequestedVersion::from_upgrade_flags(upgrade_flags.clone())?;
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
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,52 +489,182 @@ 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)
|
||||
} else {
|
||||
fs::rename(&new_exe_path, output_exe_path)
|
||||
.or_else(|_| fs::copy(&new_exe_path, output_exe_path).map(|_| ()))
|
||||
};
|
||||
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(),
|
||||
)
|
||||
});
|
||||
} else {
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
log::info!(
|
||||
"{}",
|
||||
colors::green(format!(
|
||||
"\nUpgraded successfully to Deno v{}\n",
|
||||
install_version
|
||||
))
|
||||
);
|
||||
if !upgrade_flags.canary {
|
||||
print_release_notes(version::deno(), &install_version);
|
||||
}
|
||||
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 {
|
||||
if Version::parse_standard(&passed_version).is_err() {
|
||||
bail!("Invalid version passed");
|
||||
};
|
||||
(ReleaseChannel::Stable, passed_version)
|
||||
};
|
||||
|
||||
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 {
|
||||
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!(
|
||||
"{}",
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
log::info!("");
|
||||
|
||||
Ok(maybe_newer_latest_version)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ReleaseChannel {
|
||||
/// Stable version, eg. 1.45.4, 2.0.0, 2.1.0
|
||||
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue