diff --git a/Cargo.toml b/Cargo.toml index 06a1df60..0bf0bd05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ path-absolutize = "3.1.0" human-sort = "0.2.2" [target.'cfg(windows)'.dependencies] -windows = { version = "0.52.0", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_System_Console", "Services_Store", "Foundation", "Foundation_Collections", "Web_Http", "Storage_Streams",] } +windows = { version = "0.52.0", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_System_Console", "Services_Store", "Foundation", "Foundation_Collections", "Web_Http", "Web_Http_Headers", "Storage_Streams",] } [target.'cfg(target_os = "macos")'.dependencies] reqwest = { version = "0.11.18", default-features = false, features = ["blocking", "native-tls", "socks"] } diff --git a/README.md b/README.md index 4fc6e709..8e64c298 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ The available system provided channels are: - `lts`: always points to the latest long term supported version. - `beta`: always points to the latest beta version if one exists. If a newer release candidate exists, it will point to that, and if there is neither a beta or rc candidate available it will point to the same version as the `release` channel. - `rc`: same as `beta`, but only starts with release candidate versions. +- `nightly`: always points to the latest build from the `master` branch in the Julia repository. - specific versions, e.g. `1.5.4`. - minor version channels, e.g. `1.5`. - major version channels, e.g. `1`. diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index d774f195..62c1ce17 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -208,7 +208,7 @@ fn get_julia_path_from_channel( check_channel_uptodate(channel, version, versions_db).with_context(|| { format!( - "The Julia launcher failed while checking whether the channe {} is up-to-date.", + "The Julia launcher failed while checking whether the channel {} is up-to-date.", channel ) })?; @@ -227,6 +227,39 @@ fn get_julia_path_from_channel( })?; return Ok((absolute_path.into_path_buf(), Vec::new())); } + JuliaupConfigChannel::DirectDownloadChannel { + path, + url: _, + local_etag, + server_etag, + version: _, + } => { + if local_etag != server_etag { + eprintln!( + "A new version of Julia for the `{}` channel is available. Run:", + channel + ); + eprintln!(); + eprintln!(" juliaup update"); + eprintln!(); + eprintln!("to install the latest Julia for the `{}` channel.", channel); + } + + let absolute_path = juliaupconfig_path + .parent() + .unwrap() + .join(path) + .join("bin") + .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) + .normalize() + .with_context(|| { + format!( + "Failed to normalize path for Julia binary, starting from `{}`.", + juliaupconfig_path.display() + ) + })?; + return Ok((absolute_path.into_path_buf(), Vec::new())); + } } } diff --git a/src/command_add.rs b/src/command_add.rs index 3ff18996..419cdedc 100644 --- a/src/command_add.rs +++ b/src/command_add.rs @@ -2,11 +2,15 @@ use crate::config_file::{load_mut_config_db, save_config_db, JuliaupConfigChanne use crate::global_paths::GlobalPaths; #[cfg(not(windows))] use crate::operations::create_symlink; -use crate::operations::{install_version, update_version_db}; +use crate::operations::{identify_nightly, install_nightly, install_version, update_version_db}; use crate::versions_file::load_versions_db; use anyhow::{anyhow, Context, Result}; pub fn run_command_add(channel: &str, paths: &GlobalPaths) -> Result<()> { + if channel == "nightly" || channel.starts_with("nightly~") { + return add_nightly(channel, paths); + } + update_version_db(paths).with_context(|| "Failed to update versions db.")?; let version_db = load_versions_db(paths).with_context(|| "`add` command failed to load versions db.")?; @@ -66,3 +70,37 @@ pub fn run_command_add(channel: &str, paths: &GlobalPaths) -> Result<()> { Ok(()) } + +fn add_nightly(channel: &str, paths: &GlobalPaths) -> Result<()> { + let mut config_file = load_mut_config_db(paths) + .with_context(|| "`add` command failed to load configuration data.")?; + + if config_file.data.installed_channels.contains_key(channel) { + eprintln!("'{}' is already installed.", &channel); + return Ok(()); + } + + let name = identify_nightly(&channel.to_string())?; + let config_channel = install_nightly(channel, &name, paths)?; + + config_file + .data + .installed_channels + .insert(channel.to_string(), config_channel.clone()); + + if config_file.data.default.is_none() { + config_file.data.default = Some(channel.to_string()); + } + + save_config_db(&mut config_file).with_context(|| { + format!( + "Failed to save configuration file from `add` command after '{channel}' was installed.", + ) + })?; + + #[cfg(not(windows))] + if config_file.data.settings.create_channel_symlinks { + create_symlink(&config_channel, &format!("julia-{}", channel), paths)?; + } + Ok(()) +} diff --git a/src/command_api.rs b/src/command_api.rs index 4a648508..22ca684f 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -109,6 +109,23 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { Err(_) => continue, } } + JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => { + JuliaupChannelInfo { + name: key.clone(), + file: paths.juliauphome + .join(path) + .join("bin") + .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) + .normalize() + .with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")? + .into_path_buf() + .to_string_lossy() + .to_string(), + args: Vec::new(), + version: version.clone(), + arch: "".to_string(), + } + } }; match config_file.data.default { diff --git a/src/command_list.rs b/src/command_list.rs index bac2aa24..30681b6d 100644 --- a/src/command_list.rs +++ b/src/command_list.rs @@ -1,3 +1,4 @@ +use crate::operations::{compatible_nightly_archs, identify_nightly}; use crate::{global_paths::GlobalPaths, versions_file::load_versions_db}; use anyhow::{Context, Result}; use cli_table::{ @@ -19,6 +20,24 @@ pub fn run_command_list(paths: &GlobalPaths) -> Result<()> { let versiondb_data = load_versions_db(paths).with_context(|| "`list` command failed to load versions db.")?; + let nightly_channels: Vec = std::iter::once("nightly".to_string()) + .chain( + compatible_nightly_archs()? + .into_iter() + .map(|arch| format!("nightly~{}", arch)), + ) + .collect(); + let nightly_rows: Vec = nightly_channels + .into_iter() + .map(|channel| { + let nightly_name = identify_nightly(&channel).expect("Failed to identify nightly"); + ChannelRow { + name: channel, + version: nightly_name, + } + }) + .collect(); + let rows_in_table: Vec<_> = versiondb_data .available_channels .iter() @@ -29,6 +48,7 @@ pub fn run_command_list(paths: &GlobalPaths) -> Result<()> { } }) .sorted_by(|a, b| compare(&a.name, &b.name)) + .chain(nightly_rows) .collect(); print_stdout( diff --git a/src/command_remove.rs b/src/command_remove.rs index bd490b5c..7abe52fe 100644 --- a/src/command_remove.rs +++ b/src/command_remove.rs @@ -1,7 +1,7 @@ #[cfg(not(windows))] use crate::operations::remove_symlink; use crate::{ - config_file::{load_mut_config_db, save_config_db}, + config_file::{load_mut_config_db, save_config_db, JuliaupConfigChannel}, global_paths::GlobalPaths, operations::garbage_collect_versions, }; @@ -39,6 +39,25 @@ pub fn run_command_remove(channel: &str, paths: &GlobalPaths) -> Result<()> { ); } + let x = config_file.data.installed_channels.get(channel).unwrap(); + + if let JuliaupConfigChannel::DirectDownloadChannel { + path, + url: _, + local_etag: _, + server_etag: _, + version: _, + } = x + { + let path_to_delete = paths.juliauphome.join(&path); + + let display = path_to_delete.display(); + + if std::fs::remove_dir_all(&path_to_delete).is_err() { + eprintln!("WARNING: Failed to delete {}. You can try to delete at a later point by running `juliaup gc`.", display) + } + }; + config_file.data.installed_channels.remove(channel); #[cfg(not(windows))] diff --git a/src/command_status.rs b/src/command_status.rs index 47ef3516..56caf1a4 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -52,6 +52,15 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { name: i.0.to_string(), version: match i.1 { JuliaupConfigChannel::SystemChannel { version } => version.clone(), + JuliaupConfigChannel::DirectDownloadChannel { + path: _, + url: _, + local_etag: _, + server_etag: _, + version, + } => { + format!("Development version {}", version) + } JuliaupConfigChannel::LinkedChannel { command, args } => { let mut combined_command = String::new(); @@ -95,6 +104,19 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { command: _, args: _, } => "".to_string(), + JuliaupConfigChannel::DirectDownloadChannel { + path: _, + url: _, + local_etag, + server_etag, + version: _, + } => { + if local_etag != server_etag { + "Update available".to_string() + } else { + "".to_string() + } + } }, } }) diff --git a/src/command_update.rs b/src/command_update.rs index 7c77d0ce..37081584 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -4,10 +4,11 @@ use crate::global_paths::GlobalPaths; use crate::jsonstructs_versionsdb::JuliaupVersionDB; #[cfg(not(windows))] use crate::operations::create_symlink; -use crate::operations::garbage_collect_versions; +use crate::operations::{garbage_collect_versions, install_from_url}; use crate::operations::{install_version, update_version_db}; use crate::versions_file::load_versions_db; use anyhow::{anyhow, bail, Context, Result}; +use std::path::PathBuf; fn update_channel( config_db: &mut JuliaupConfig, @@ -17,7 +18,7 @@ fn update_channel( paths: &GlobalPaths, ) -> Result<()> { let current_version = - config_db.installed_channels.get(channel).ok_or_else(|| anyhow!("Trying to get the installed version for a channel that does not exist in the config database."))?; + &config_db.installed_channels.get(channel).ok_or_else(|| anyhow!("Trying to get the installed version for a channel that does not exist in the config database."))?.clone(); match current_version { JuliaupConfigChannel::SystemChannel { version } => { @@ -71,6 +72,40 @@ fn update_channel( ); } } + JuliaupConfigChannel::DirectDownloadChannel { + path, + url, + local_etag, + server_etag, + version, + } => { + // We only do this so that we use `version` on both Windows and Linux to prevent a compiler warning/error + assert!(!version.is_empty()); + + if local_etag != server_etag { + let channel_data = + install_from_url(&url::Url::parse(url)?, &PathBuf::from(path), paths)?; + + config_db + .installed_channels + .insert(channel.clone(), channel_data); + + #[cfg(not(windows))] + if config_db.settings.create_channel_symlinks { + create_symlink( + &JuliaupConfigChannel::DirectDownloadChannel { + path: path.clone(), + url: url.clone(), + local_etag: local_etag.clone(), + server_etag: server_etag.clone(), + version: version.clone(), + }, + &channel, + paths, + )?; + } + } + } } Ok(()) diff --git a/src/config_file.rs b/src/config_file.rs index b7a2cca4..0928d9aa 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -29,6 +29,18 @@ pub struct JuliaupConfigVersion { #[derive(Serialize, Deserialize, Clone)] #[serde(untagged)] pub enum JuliaupConfigChannel { + DirectDownloadChannel { + #[serde(rename = "Path")] + path: String, + #[serde(rename = "Url")] + url: String, + #[serde(rename = "LocalETag")] + local_etag: String, + #[serde(rename = "ServerETag")] + server_etag: String, + #[serde(rename = "Version")] + version: String, + }, SystemChannel { #[serde(rename = "Version")] version: String, diff --git a/src/operations.rs b/src/operations.rs index 5d1ee118..8ff0e638 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -9,6 +9,7 @@ use crate::get_juliaup_target; use crate::global_paths::GlobalPaths; use crate::jsonstructs_versionsdb::JuliaupVersionDB; use crate::utils::get_bin_dir; +use crate::utils::get_julianightlies_base_url; use crate::utils::get_juliaserver_base_url; use anyhow::{anyhow, bail, Context, Result}; use bstr::ByteSlice; @@ -28,6 +29,8 @@ use std::{ path::{Component::Normal, Path, PathBuf}, }; use tar::Archive; +use tempfile::Builder; +use url::Url; fn unpack_sans_parent(mut archive: Archive, dst: P, levels_to_skip: usize) -> Result<()> where @@ -52,7 +55,8 @@ pub fn download_extract_sans_parent( url: &str, target_path: &Path, levels_to_skip: usize, -) -> Result<()> { +) -> Result { + log::debug!("Downloading from url `{}`.", url); let response = reqwest::blocking::get(url) .with_context(|| format!("Failed to download from url `{}`.", url))?; @@ -71,6 +75,14 @@ pub fn download_extract_sans_parent( .progress_chars("=> "), ); + let last_modified = response + .headers() + .get("etag") + .unwrap() + .to_str() + .unwrap() + .to_string(); + let response_with_pb = pb.wrap_read(response); let tar = GzDecoder::new(response_with_pb); @@ -78,7 +90,7 @@ pub fn download_extract_sans_parent( unpack_sans_parent(archive, target_path, levels_to_skip) .with_context(|| format!("Failed to extract downloaded file from url `{}`.", url))?; - Ok(()) + Ok(last_modified) } #[cfg(windows)] @@ -106,7 +118,9 @@ pub fn download_extract_sans_parent( url: &str, target_path: &Path, levels_to_skip: usize, -) -> Result<()> { +) -> Result { + use windows::core::HSTRING; + let http_client = windows::Web::Http::HttpClient::new().with_context(|| "Failed to create HttpClient.")?; @@ -123,6 +137,13 @@ pub fn download_extract_sans_parent( .EnsureSuccessStatusCode() .with_context(|| "HTTP download reported error status code.")?; + let last_modified = http_response + .Headers() + .unwrap() + .Lookup(&HSTRING::from("etag")) + .unwrap() + .to_string(); + let http_response_content = http_response .Content() .with_context(|| "Failed to obtain content from http response.")?; @@ -162,7 +183,7 @@ pub fn download_extract_sans_parent( unpack_sans_parent(archive, target_path, levels_to_skip) .with_context(|| format!("Failed to extract downloaded file from url `{}`.", url))?; - Ok(()) + Ok(last_modified) } #[cfg(not(windows))] @@ -333,6 +354,180 @@ pub fn install_version( Ok(()) } +// which nightly arch to default to when simply using the `nightly` channel +pub fn default_nightly_arch() -> Result { + if cfg!(target_arch = "aarch64") { + Ok("aarch64".to_string()) + } else if cfg!(target_arch = "x86_64") { + Ok("x64".to_string()) + } else if cfg!(target_arch = "x86") { + Ok("x86".to_string()) + } else { + bail!("Unsupported architecture for nightly channel.") + } +} + +// which nightly archs are compatible with the current system, for `juliaup list` purposes +pub fn compatible_nightly_archs() -> Result> { + if cfg!(target_os = "macos") { + if cfg!(target_arch = "x86_64") { + Ok(vec!["x64".to_string()]) + } else if cfg!(target_arch = "aarch64") { + // Rosetta 2 can execute x86_64 binaries + Ok(vec!["aarch64".to_string(), "x64".to_string()]) + } else { + bail!("Unsupported architecture for nightly channel on macOS.") + } + } else if cfg!(target_arch = "x86") { + Ok(vec!["x86".to_string()]) + } else if cfg!(target_arch = "x86_64") { + // x86_64 can execute x86 binaries + Ok(vec!["x86".to_string(), "x64".to_string()]) + } else if cfg!(target_arch = "aarch64") { + Ok(vec!["aarch64".to_string()]) + } else { + bail!("Unsupported architecture for nightly channel.") + } +} + +// Identify the unversioned name of a nightly (e.g., `latest-macos-x86_64`) for a channel +pub fn identify_nightly(channel: &String) -> Result { + let arch = if channel == "nightly" { + default_nightly_arch()? + } else { + let parts: Vec<&str> = channel.splitn(2, '~').collect(); + if parts.len() != 2 { + bail!("Invalid nightly channel name '{}'.", channel) + } + parts[1].to_string() + }; + + let name = { + #[cfg(target_os = "macos")] + if arch == "x64" { + "latest-macos-x86_64" + } else if arch == "aarch64" { + "latest-macos-aarch64" + } else { + bail!("Unsupported architecture for nightly channel on macOS.") + } + + #[cfg(target_os = "windows")] + if arch == "x64" { + "latest-win64" + } else if arch == "x86" { + "latest-win32" + } else { + bail!("Unsupported architecture for nightly channel on Windows.") + } + + #[cfg(target_os = "linux")] + if arch == "x64" { + "latest-linux-x86_64" + } else if arch == "x86" { + "latest-linux-i686" + } else if arch == "aarch64" { + "latest-linux-aarch64" + } else { + bail!("Unsupported architecture for nightly channel on Linux.") + } + }; + + Ok(name.to_string()) +} + +pub fn install_from_url( + url: &Url, + path: &PathBuf, + paths: &GlobalPaths, +) -> Result { + // Download and extract into a temporary directory + let temp_dir = Builder::new() + .prefix("julia-temp-") + .tempdir_in(&paths.juliauphome) + .expect("Failed to create temporary directory"); + + let download_result = download_extract_sans_parent(url.as_ref(), &temp_dir.path(), 1); + + let server_etag = match download_result { + Ok(last_updated) => last_updated, + Err(e) => { + std::fs::remove_dir_all(temp_dir.into_path())?; + bail!("Failed to download and extract nightly: {}", e); + } + }; + + // Query the actual version + let julia_path = temp_dir + .path() + .join("bin") + .join(format!("julia{}", std::env::consts::EXE_SUFFIX)); + let julia_process = std::process::Command::new(julia_path.clone()) + .arg("--startup-file=no") + .arg("-e") + .arg("print(VERSION)") + .output() + .with_context(|| { + format!( + "Failed to execute Julia binary at `{}`.", + julia_path.display() + ) + })?; + let julia_version = String::from_utf8(julia_process.stdout)?; + + // Move into the final location + let target_path = paths.juliauphome.join(&path); + if target_path.exists() { + std::fs::remove_dir_all(&target_path)?; + } + std::fs::rename(temp_dir.into_path(), &target_path)?; + + Ok(JuliaupConfigChannel::DirectDownloadChannel { + path: path.to_string_lossy().into_owned(), + url: url.to_string().to_owned(), // TODO Use proper URL + local_etag: server_etag.clone(), // TODO Use time stamp of HTTPS response + server_etag: server_etag, + version: julia_version, + }) +} + +pub fn install_nightly( + channel: &str, + name: &String, + paths: &GlobalPaths, +) -> Result { + // Determine the download URL + let download_url_base = get_julianightlies_base_url()?; + let download_url_path = match name.as_str() { + "latest-macos-x86_64" => Ok("bin/macos/x86_64/julia-latest-macos-x86_64.tar.gz"), + "latest-macos-aarch64" => Ok("bin/macos/aarch64/julia-latest-macos-aarch64.tar.gz"), + "latest-win64" => Ok("bin/winnt/x64/julia-latest-win64.tar.gz"), + "latest-win32" => Ok("bin/winnt/x86/julia-latest-win32.tar.gz"), + "latest-linux-x86_64" => Ok("bin/linux/x86_64/julia-latest-linux-x86_64.tar.gz"), + "latest-linux-i686" => Ok("bin/linux/i686/julia-latest-linux-i686.tar.gz"), + "latest-linux-aarch64" => Ok("bin/linux/aarch64/julia-latest-linux-aarch64.tar.gz"), + _ => Err(anyhow!("Unknown nightly.")), + }?; + let download_url = download_url_base.join(download_url_path).with_context(|| { + format!( + "Failed to construct a valid url from '{}' and '{}'.", + download_url_base, download_url_path + ) + })?; + + let child_target_foldername = format!("julia-{}", channel); + + let mut rel_path = PathBuf::new(); + rel_path.push("."); + rel_path.push(&child_target_foldername); + + eprintln!("{} Julia {}", style("Installing").green().bold(), name); + + let res = install_from_url(&download_url, &rel_path, paths)?; + + Ok(res) +} + pub fn garbage_collect_versions( config_data: &mut JuliaupConfig, paths: &GlobalPaths, @@ -345,6 +540,13 @@ pub fn garbage_collect_versions( command: _, args: _, } => true, + JuliaupConfigChannel::DirectDownloadChannel { + path: _, + url: _, + local_etag: _, + server_etag: _, + version: _, + } => true, }) { let path_to_delete = paths.juliauphome.join(&detail.path); let display = path_to_delete.display(); @@ -404,9 +606,33 @@ pub fn create_symlink( match channel { JuliaupConfigChannel::SystemChannel { version } => { - let child_target_fullname = format!("julia-{}", version); + let child_target_foldername = format!("julia-{}", version); + + let target_path = paths.juliauphome.join(&child_target_foldername); + + eprintln!( + "{} {} for Julia {}.", + style("Creating symlink").cyan().bold(), + symlink_name, + version + ); - let target_path = paths.juliauphome.join(&child_target_fullname); + std::os::unix::fs::symlink(target_path.join("bin").join("julia"), &symlink_path) + .with_context(|| { + format!( + "failed to create symlink `{}`.", + symlink_path.to_string_lossy() + ) + })?; + } + JuliaupConfigChannel::DirectDownloadChannel { + path, + url: _, + local_etag: _, + server_etag: _, + version, + } => { + let target_path = paths.juliauphome.join(path); eprintln!( "{} {} for Julia {}.", @@ -954,6 +1180,34 @@ pub fn update_version_db(paths: &GlobalPaths) -> Result<()> { let online_dbversion = download_juliaup_version(&dbversion_url.to_string()) .with_context(|| "Failed to download current version db version.")?; + let direct_download_etags = download_direct_download_etags(&mut config_file.data)?; + + for (channel, etag) in direct_download_etags { + let channel_data = config_file.data.installed_channels.get(&channel).unwrap(); + + match channel_data { + JuliaupConfigChannel::DirectDownloadChannel { + path, + url, + local_etag, + server_etag: _, + version, + } => { + config_file.data.installed_channels.insert( + channel, + JuliaupConfigChannel::DirectDownloadChannel { + path: path.clone(), + url: url.clone(), + local_etag: local_etag.clone(), + server_etag: etag, + version: version.clone(), + }, + ); + } + _ => {} + } + } + config_file.data.last_version_db_update = Some(chrono::Utc::now()); save_config_db(&mut config_file).with_context(|| "Failed to save configuration file.")?; @@ -1009,3 +1263,102 @@ pub fn update_version_db(paths: &GlobalPaths) -> Result<()> { Ok(()) } + +#[cfg(windows)] +fn download_direct_download_etags( + config_data: &mut JuliaupConfig, +) -> Result> { + use windows::core::HSTRING; + use windows::Web::Http::HttpMethod; + use windows::Web::Http::HttpRequestMessage; + + let http_client = + windows::Web::Http::HttpClient::new().with_context(|| "Failed to create HttpClient.")?; + + let requests: Vec<_> = config_data + .installed_channels + .iter() + .filter_map(|(channel_name, channel)| { + if let JuliaupConfigChannel::DirectDownloadChannel { + path: _, + url, + local_etag: _, + server_etag: _, + version: _, + } = channel + { + let request_uri = + windows::Foundation::Uri::CreateUri(&windows::core::HSTRING::from(url)) + .with_context(|| "Failed to convert url string to Uri.") + .unwrap(); + + let request = + HttpRequestMessage::Create(&HttpMethod::Head().unwrap(), &request_uri).unwrap(); + + let request = http_client.SendRequestAsync(&request).unwrap(); + + Some((channel_name, request)) + } else { + None + } + }) + .collect(); + + let requests: Vec<_> = requests + .into_iter() + .map(|(channel_name, request)| { + ( + channel_name.clone(), + request + .get() + .unwrap() + .Headers() + .unwrap() + .Lookup(&HSTRING::from("etag")) + .unwrap() + .to_string(), + ) + }) + .collect(); + + Ok(requests) +} + +#[cfg(not(windows))] +fn download_direct_download_etags( + config_data: &mut JuliaupConfig, +) -> Result> { + let client = reqwest::blocking::Client::new(); + + let requests: Vec<_> = config_data + .installed_channels + .iter() + .filter_map(|(channel_name, channel)| { + if let JuliaupConfigChannel::DirectDownloadChannel { + path: _, + url, + local_etag: _, + server_etag: _, + version: _, + } = channel + { + let etag = client + .head(url) + .send() + .unwrap() + .headers() + .get("etag") + .unwrap() + .to_str() + .unwrap() + .to_string(); + + Some((channel_name.clone(), etag)) + } else { + None + } + }) + .collect(); + + Ok(requests) +} diff --git a/src/utils.rs b/src/utils.rs index 5fc64115..b0977ea1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -24,6 +24,27 @@ pub fn get_juliaserver_base_url() -> Result { Ok(parsed_url) } +pub fn get_julianightlies_base_url() -> Result { + let base_url = if let Ok(val) = std::env::var("JULIAUP_NIGHTLY_SERVER") { + if val.ends_with('/') { + val + } else { + format!("{}/", val) + } + } else { + "https://julialangnightlies-s3.julialang.org".to_string() + }; + + let parsed_url = Url::parse(&base_url).with_context(|| { + format!( + "Failed to parse the value of JULIAUP_NIGHTLY_SERVER '{}' as a uri.", + base_url + ) + })?; + + Ok(parsed_url) +} + pub fn get_bin_dir() -> Result { let entry_sep = if std::env::consts::OS == "windows" { ';' diff --git a/tests/channel_selection.rs b/tests/channel_selection.rs index aabe1def..7af73a4a 100644 --- a/tests/channel_selection.rs +++ b/tests/channel_selection.rs @@ -44,7 +44,7 @@ fn channel_selection() { .success() .stdout(""); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -54,7 +54,7 @@ fn channel_selection() { .success() .stdout("1.6.7"); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("+1.8.5") .arg("-e") @@ -65,7 +65,7 @@ fn channel_selection() { .success() .stdout("1.8.5"); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -76,7 +76,7 @@ fn channel_selection() { .success() .stdout("1.7.3"); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("+1.8.5") .arg("-e") @@ -90,7 +90,7 @@ fn channel_selection() { // Now testing incorrect channels - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("+1.8.6") .arg("-e") @@ -101,7 +101,7 @@ fn channel_selection() { .failure() .stderr("ERROR: Invalid Juliaup channel `1.8.6`. Please run `juliaup list` to get a list of valid channels and versions.\n"); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -114,7 +114,7 @@ fn channel_selection() { "ERROR: Invalid Juliaup channel `1.7.4` in environment variable JULIAUP_CHANNEL. Please run `juliaup list` to get a list of valid channels and versions.\n", ); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("+1.8.6") .arg("-e") diff --git a/tests/command_add.rs b/tests/command_add.rs index b848b9bd..09c4e27d 100644 --- a/tests/command_add.rs +++ b/tests/command_add.rs @@ -1,4 +1,5 @@ use assert_cmd::Command; +use predicates::prelude::predicate; #[test] fn command_add() { @@ -14,7 +15,17 @@ fn command_add() { .success() .stdout(""); - Command::cargo_bin("julialauncher") + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("nightly") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("julia") .unwrap() .arg("+1.6.4") .arg("-e") @@ -24,4 +35,20 @@ fn command_add() { .assert() .success() .stdout("1.6.4"); + + Command::cargo_bin("julia") + .unwrap() + .arg("+nightly") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout( + predicate::str::is_match( + "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)-DEV\\.(0|[1-9]\\d*)", + ) + .unwrap(), + ); } diff --git a/tests/command_default.rs b/tests/command_default.rs index 7c32436d..285a4838 100644 --- a/tests/command_default.rs +++ b/tests/command_default.rs @@ -24,7 +24,7 @@ fn command_default() { .success() .stdout(""); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") diff --git a/tests/command_override_test.rs b/tests/command_override_test.rs index e3a2f2a0..22c1d948 100644 --- a/tests/command_override_test.rs +++ b/tests/command_override_test.rs @@ -72,7 +72,7 @@ fn command_override_cur_dir_test() { .success() .stdout(""); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -94,7 +94,7 @@ fn command_override_cur_dir_test() { .assert() .success(); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -115,7 +115,7 @@ fn command_override_cur_dir_test() { .assert() .success(); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -163,7 +163,7 @@ fn command_override_arg_test() { .success() .stdout(""); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -186,7 +186,7 @@ fn command_override_arg_test() { .assert() .success(); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -208,7 +208,7 @@ fn command_override_arg_test() { .assert() .success(); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -292,7 +292,7 @@ fn command_override_overlap_test() { .assert() .success(); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") @@ -303,7 +303,7 @@ fn command_override_overlap_test() { .success() .stdout("1.7.3"); - Command::cargo_bin("julialauncher") + Command::cargo_bin("julia") .unwrap() .arg("-e") .arg("print(VERSION)") diff --git a/tests/command_remove.rs b/tests/command_remove.rs index bbe6fabc..9b2c25d0 100644 --- a/tests/command_remove.rs +++ b/tests/command_remove.rs @@ -73,4 +73,42 @@ fn command_remove() { .assert() .success() .stdout(predicates::str::contains("1.6.4").and(predicates::str::contains("release").not())); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("nightly") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("status") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(predicates::str::contains("1.6.4").and(predicates::str::contains("-DEV"))); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("remove") + .arg("nightly") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("status") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(predicates::str::contains("1.6.4").and(predicates::str::contains("-DEV").not())); }