diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index d865481d5..8228b9a81 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -34,4 +34,4 @@ jobs: with: components: clippy - name: Run clippy - run: cargo clippy --all-targets --features ${{ matrix.features }} -- -D warnings + run: cargo clippy --workspace --features ${{ matrix.features }} -- -D warnings diff --git a/README.md b/README.md index 049347fc3..6979c8bf8 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Here are some of the things you can do with `juliaup`: - `juliaup add 1.6.1~x86` installs the 32 bit version of Julia 1.6.1 on your system. - `juliaup default 1.6~x86` configures the `julia` command to start the latest 1.6.x 32 bit version of Julia you have installed on your system. - `juliaup link dev ~/juliasrc/julia` configures the `dev` channel to use a binary that you provide that is located at `~/juliasrc/julia`. You can then use `dev` as if it was a system provided channel, i.e. make it the default or use it with the `+` version selector. You can use other names than `dev` and link as many versions into `juliaup` as you want. +- `juliaup link r +release` creates a channel alias `r` that points to the `release` channel. This allows you to use `julia +r` as a shortcut for `julia +release`. Channel aliases can point to any installed channel or system-provided channel. - `juliaup self update` installs the latest version, which is necessary if new releases reach the beta channel, etc. - `juliaup self uninstall` uninstalls Juliaup. Note that on some platforms this command is not available, in those situations one should use platform specific methods to uninstall Juliaup. - `juliaup override status` shows all configured directory overrides. diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index ed4aa0320..154ed2a3b 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use console::{style, Term}; use dialoguer::Select; use is_terminal::IsTerminal; @@ -333,29 +333,38 @@ fn get_julia_path_from_channel( juliaup_channel_source: JuliaupChannelSource, paths: &juliaup::global_paths::GlobalPaths, ) -> Result<(PathBuf, Vec)> { - let channel_valid = is_valid_channel(versions_db, &channel.to_string())?; + // First check if the channel is an alias and extract its args + let (resolved_channel, alias_args) = match config_data.installed_channels.get(channel) { + Some(JuliaupConfigChannel::AliasChannel { target, args }) => { + (target.to_string(), args.clone().unwrap_or_default()) + } + _ => (channel.to_string(), Vec::new()), + }; + + let channel_valid = is_valid_channel(versions_db, &resolved_channel)?; // First check if the channel is already installed - if let Some(channel_info) = config_data.installed_channels.get(channel) { + if let Some(channel_info) = config_data.installed_channels.get(&resolved_channel) { return get_julia_path_from_installed_channel( versions_db, config_data, - channel, + &resolved_channel, juliaupconfig_path, channel_info, + alias_args.clone(), ); } // Handle auto-installation for command line channel selection if let JuliaupChannelSource::CmdLine = juliaup_channel_source { - if channel_valid || is_pr_channel(channel) { + if channel_valid || is_pr_channel(&resolved_channel) { // Check the user's auto-install preference let should_auto_install = match config_data.settings.auto_install_channels { Some(auto_install) => auto_install, // User has explicitly set a preference None => { // User hasn't set a preference - prompt in interactive mode, default to false in non-interactive if is_interactive() { - handle_auto_install_prompt(channel, paths)? + handle_auto_install_prompt(&resolved_channel, paths)? } else { false } @@ -365,25 +374,29 @@ fn get_julia_path_from_channel( if should_auto_install { // Install the channel using juliaup let is_automatic = config_data.settings.auto_install_channels == Some(true); - spawn_juliaup_add(channel, paths, is_automatic)?; + spawn_juliaup_add(&resolved_channel, paths, is_automatic)?; // Reload the config to get the newly installed channel let updated_config_file = load_config_db(paths, None) .with_context(|| "Failed to reload configuration after installing channel.")?; - if let Some(channel_info) = updated_config_file.data.installed_channels.get(channel) - { + let updated_channel_info = updated_config_file + .data + .installed_channels + .get(&resolved_channel); + + if let Some(channel_info) = updated_channel_info { return get_julia_path_from_installed_channel( versions_db, &updated_config_file.data, - channel, + &resolved_channel, juliaupconfig_path, channel_info, + alias_args, ); } else { return Err(anyhow!( - "Channel '{}' was installed but could not be found in configuration.", - channel + "Channel '{resolved_channel}' was installed but could not be found in configuration." )); } } @@ -395,32 +408,32 @@ fn get_julia_path_from_channel( let error = match juliaup_channel_source { JuliaupChannelSource::CmdLine => { if channel_valid { - UserError { msg: format!("`{}` is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) } - } else if is_pr_channel(channel) { - UserError { msg: format!("`{}` is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) } + UserError { msg: format!("`{resolved_channel}` is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") } + } else if is_pr_channel(&resolved_channel) { + UserError { msg: format!("`{resolved_channel}` is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") } } else { - UserError { msg: format!("Invalid Juliaup channel `{}`. Please run `juliaup list` to get a list of valid channels and versions.", channel) } + UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}`. Please run `juliaup list` to get a list of valid channels and versions.") } } }, JuliaupChannelSource::EnvVar=> { if channel_valid { - UserError { msg: format!("`{}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) } - } else if is_pr_channel(channel) { - UserError { msg: format!("`{}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) } + UserError { msg: format!("`{resolved_channel}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") } + } else if is_pr_channel(&resolved_channel) { + UserError { msg: format!("`{resolved_channel}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") } } else { - UserError { msg: format!("Invalid Juliaup channel `{}` from environment variable JULIAUP_CHANNEL. Please run `juliaup list` to get a list of valid channels and versions.", channel) } + UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}` from environment variable JULIAUP_CHANNEL. Please run `juliaup list` to get a list of valid channels and versions.") } } }, JuliaupChannelSource::Override=> { if channel_valid { - UserError { msg: format!("`{}` from directory override is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) } - } else if is_pr_channel(channel){ - UserError { msg: format!("`{}` from directory override is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) } + UserError { msg: format!("`{resolved_channel}` from directory override is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") } + } else if is_pr_channel(&resolved_channel) { + UserError { msg: format!("`{resolved_channel}` from directory override is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") } } else { - UserError { msg: format!("Invalid Juliaup channel `{}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.", channel) } + UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.") } } }, - JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) } + JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{resolved_channel}` is not installed.") } }; Err(error.into()) @@ -432,22 +445,24 @@ fn get_julia_path_from_installed_channel( channel: &str, juliaupconfig_path: &Path, channel_info: &JuliaupConfigChannel, + alias_args: Vec, ) -> Result<(PathBuf, Vec)> { match channel_info { - JuliaupConfigChannel::LinkedChannel { command, args } => Ok(( - PathBuf::from(command), - args.as_ref().map_or_else(Vec::new, |v| v.clone()), - )), + JuliaupConfigChannel::AliasChannel { .. } => { + bail!("Unexpected alias channel after resolution: {channel}"); + } + JuliaupConfigChannel::LinkedChannel { command, args } => { + let mut combined_args = alias_args; + combined_args.extend(args.as_ref().map_or_else(Vec::new, |v| v.clone())); + Ok((PathBuf::from(command), combined_args)) + } JuliaupConfigChannel::SystemChannel { version } => { let path = &config_data .installed_versions.get(version) - .ok_or_else(|| anyhow!("The juliaup configuration is in an inconsistent state, the channel {} is pointing to Julia version {}, which is not installed.", channel, version))?.path; + .ok_or_else(|| anyhow!("The juliaup configuration is in an inconsistent state, the channel {channel} is pointing to Julia version {version}, which is not installed."))?.path; check_channel_uptodate(channel, version, versions_db).with_context(|| { - format!( - "The Julia launcher failed while checking whether the channel {} is up-to-date.", - channel - ) + format!("The Julia launcher failed while checking whether the channel {channel} is up-to-date.") })?; let absolute_path = juliaupconfig_path .parent() @@ -462,7 +477,7 @@ fn get_julia_path_from_installed_channel( juliaupconfig_path.display() ) })?; - Ok((absolute_path.into_path_buf(), Vec::new())) + Ok((absolute_path.into_path_buf(), alias_args)) } JuliaupConfigChannel::DirectDownloadChannel { path, @@ -505,7 +520,7 @@ fn get_julia_path_from_installed_channel( juliaupconfig_path.display() ) })?; - Ok((absolute_path.into_path_buf(), Vec::new())) + Ok((absolute_path.into_path_buf(), alias_args)) } } } diff --git a/src/bin/juliaup.rs b/src/bin/juliaup.rs index e26604612..376809bfd 100644 --- a/src/bin/juliaup.rs +++ b/src/bin/juliaup.rs @@ -100,9 +100,9 @@ fn main() -> Result<()> { Juliaup::Gc { prune_linked } => run_command_gc(prune_linked, &paths), Juliaup::Link { channel, - file, + target, args, - } => run_command_link(&channel, &file, &args, &paths), + } => run_command_link(&channel, &target, &args, &paths), Juliaup::List {} => run_command_list(&paths), Juliaup::Config(subcmd) => match subcmd { #[cfg(not(windows))] diff --git a/src/cli.rs b/src/cli.rs index dfc74cbb2..2d14606f0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -25,10 +25,13 @@ pub enum Juliaup { Default { channel: String }, /// Add a specific Julia version or channel to your system. Access via `julia +{channel}` e.g. `julia +1.6` Add { channel: String }, - /// Link an existing Julia binary to a custom channel name + /// Link an existing Julia binary or channel to a custom channel name Link { + /// Name of the new channel to create channel: String, - file: String, + /// Path to Julia binary, or +{channel} to create an alias + target: String, + /// Additional arguments for the Julia binary args: Vec, }, /// List all available channels diff --git a/src/command_api.rs b/src/command_api.rs index 98c81ef85..e20440c8d 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -43,17 +43,32 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { "Failed to load configuration file while running the getconfig1 API command." })?; - for (key, value) in config_file.data.installed_channels { - let curr = match value { - JuliaupConfigChannel::SystemChannel { - version: fullversion, - } => { - let (platform, mut version) = parse_versionstring(&fullversion) + for (key, value) in &config_file.data.installed_channels { + let curr = match &value { + 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(), + } + } + JuliaupConfigChannel::SystemChannel { version: fullversion } => { + let (platform, mut version) = parse_versionstring(fullversion) .with_context(|| "Encountered invalid version string in the configuration file while running the getconfig1 API command.")?; version.build = semver::BuildMetadata::EMPTY; - match config_file.data.installed_versions.get(&fullversion) { + match config_file.data.installed_versions.get(fullversion) { Some(channel) => JuliaupChannelInfo { name: key.clone(), file: paths.juliauphome @@ -73,15 +88,10 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { } } JuliaupConfigChannel::LinkedChannel { command, args } => { - let mut new_args: Vec = Vec::new(); - - for i in args.as_ref().unwrap() { - new_args.push(i.to_string()); - } - + let mut new_args = args.clone().unwrap_or_default(); new_args.push("--version".to_string()); - let res = std::process::Command::new(&command) + let res = std::process::Command::new(command) .args(&new_args) .output(); @@ -101,36 +111,28 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { JuliaupChannelInfo { name: key.clone(), file: command.clone(), - args: args.unwrap_or_default(), + args: args.clone().unwrap_or_default(), version: version.to_string(), - arch: "".to_string(), + arch: String::new(), } } Err(_) => continue, } } - JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => { + JuliaupConfigChannel::AliasChannel { target, args } => { 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(), + file: format!("alias-to-{target}"), + args: args.clone().unwrap_or_default(), + version: format!("alias to {target}"), + arch: String::new(), } } }; match config_file.data.default { Some(ref default_value) => { - if &key == default_value { + if key == default_value { ret_value.default = Some(curr.clone()); } else { ret_value.other_versions.push(curr); @@ -146,7 +148,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { let j = serde_json::to_string(&ret_value)?; // Print, write to a file, or send to an HTTP server. - println!("{}", j); + println!("{j}"); Ok(()) } diff --git a/src/command_link.rs b/src/command_link.rs index 8f5820429..f281c0d05 100644 --- a/src/command_link.rs +++ b/src/command_link.rs @@ -12,7 +12,7 @@ use std::path::Path; pub fn run_command_link( channel: &str, - file: &str, + target: &str, args: &[String], paths: &GlobalPaths, ) -> Result<()> { @@ -27,24 +27,61 @@ pub fn run_command_link( } if is_valid_channel(&versiondb_data, &channel.to_string())? { - eprintln!("WARNING: The channel name `{}` is also a system channel. By linking your custom binary to this channel you are hiding this system channel.", channel); + eprintln!("WARNING: The channel name `{channel}` is also a system channel. By linking your custom binary to this channel you are hiding this system channel."); } - let absolute_file_path = Path::new(file) - .absolutize() - .with_context(|| format!("Failed to convert path `{}` to absolute path.", file))?; + // Check if this is a channel alias (starts with +) + if let Some(target_channel) = target.strip_prefix('+') { + // Validate that the target channel exists and is not an alias + if let Some(target_info) = config_file.data.installed_channels.get(target_channel) { + // Prevent alias-to-alias chains for simplicity and maintainability + if let JuliaupConfigChannel::AliasChannel { .. } = target_info { + bail!("Cannot create an alias to another alias `{}`. Please create an alias directly to the target channel instead.", target_channel); + } + } else if !is_valid_channel(&versiondb_data, &target_channel.to_string())? { + bail!("Target channel `{}` is not installed and is not a valid system channel. Please run `juliaup add {}` first or check `juliaup list` for available channels.", target_channel, target_channel); + } - if !is_valid_julia_path(&absolute_file_path.to_path_buf()) { - eprintln!("WARNING: There is no julia binary at {}. If this was a mistake, run `juliaup remove {}` and try again.", absolute_file_path.to_string_lossy(), channel); - } + config_file.data.installed_channels.insert( + channel.to_string(), + JuliaupConfigChannel::AliasChannel { + target: target_channel.to_string(), + args: if args.is_empty() { + None + } else { + Some(args.to_vec()) + }, + }, + ); + + if args.is_empty() { + eprintln!("Channel alias `{channel}` created, pointing to `{target_channel}`."); + } else { + eprintln!("Channel alias `{channel}` created, pointing to `{target_channel}` with args: {:?}.", args); + } + } else { + let absolute_file_path = Path::new(target) + .absolutize() + .with_context(|| format!("Failed to convert path `{target}` to absolute path."))?; - config_file.data.installed_channels.insert( - channel.to_string(), - JuliaupConfigChannel::LinkedChannel { - command: absolute_file_path.to_string_lossy().to_string(), - args: Some(args.to_vec()), - }, - ); + if !is_valid_julia_path(&absolute_file_path.to_path_buf()) { + eprintln!("WARNING: There is no julia binary at {}. If this was a mistake, run `juliaup remove {}` and try again.", absolute_file_path.to_string_lossy(), channel); + } + + config_file.data.installed_channels.insert( + channel.to_string(), + JuliaupConfigChannel::LinkedChannel { + command: absolute_file_path.to_string_lossy().to_string(), + args: Some(args.to_vec()), + }, + ); + + eprintln!( + "Channel `{}` linked to `{}`.", + channel, + absolute_file_path.to_string_lossy() + ); + } #[cfg(not(windows))] let create_symlinks = config_file.data.settings.create_channel_symlinks; @@ -53,13 +90,17 @@ pub fn run_command_link( .with_context(|| "`link` command failed to save configuration db.")?; #[cfg(not(windows))] - if create_symlinks { + if create_symlinks && !target.starts_with('+') { + let absolute_file_path = Path::new(target) + .absolutize() + .with_context(|| format!("Failed to convert path `{target}` to absolute path."))?; + create_symlink( &JuliaupConfigChannel::LinkedChannel { - command: file.to_string(), + command: absolute_file_path.to_string_lossy().to_string(), args: Some(args.to_vec()), }, - &format!("julia-{}", channel), + &format!("julia-{channel}"), paths, )?; } diff --git a/src/command_remove.rs b/src/command_remove.rs index 47407c21f..3a2a8b4db 100644 --- a/src/command_remove.rs +++ b/src/command_remove.rs @@ -13,7 +13,7 @@ pub fn run_command_remove(channel: &str, paths: &GlobalPaths) -> Result<()> { if !config_file.data.installed_channels.contains_key(channel) { bail!( - "'{}' cannot be removed because it is currently not installed.", + "'{}' cannot be removed because it is not currently installed. Please run `juliaup list` to see available channels.", channel ); } @@ -39,7 +39,17 @@ pub fn run_command_remove(channel: &str, paths: &GlobalPaths) -> Result<()> { ); } - let x = config_file.data.installed_channels.get(channel).unwrap(); + let channel_info = config_file.data.installed_channels.get(channel).unwrap(); + + // Determine what type of channel is being removed for better messaging + let channel_type = match channel_info { + JuliaupConfigChannel::DirectDownloadChannel { .. } => "channel".to_string(), + JuliaupConfigChannel::SystemChannel { .. } => "channel".to_string(), + JuliaupConfigChannel::LinkedChannel { .. } => "linked channel".to_string(), + JuliaupConfigChannel::AliasChannel { target, .. } => { + format!("alias (pointing to '{target}')") + } + }; if let JuliaupConfigChannel::DirectDownloadChannel { path, @@ -47,32 +57,31 @@ pub fn run_command_remove(channel: &str, paths: &GlobalPaths) -> Result<()> { local_etag: _, server_etag: _, version: _, - } = x + } = channel_info { 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) + eprintln!("WARNING: Failed to delete {display}. You can try to delete at a later point by running `juliaup gc`.") } }; config_file.data.installed_channels.remove(channel); #[cfg(not(windows))] - remove_symlink(&format!("julia-{}", channel))?; + remove_symlink(&format!("julia-{channel}"))?; garbage_collect_versions(false, &mut config_file.data, paths)?; save_config_db(&mut config_file).with_context(|| { format!( - "Failed to save configuration file from `remove` command after '{}' was installed.", - channel + "Failed to save configuration file from `remove` command after '{channel}' was removed." ) })?; - eprintln!("Julia '{}' successfully removed.", channel); + eprintln!("Julia {channel_type} '{channel}' successfully removed."); Ok(()) } diff --git a/src/command_status.rs b/src/command_status.rs index 59f5f837e..b60db3595 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -1,6 +1,7 @@ use crate::config_file::load_config_db; -use crate::config_file::JuliaupConfigChannel; +use crate::config_file::{JuliaupConfigChannel, JuliaupReadonlyConfigFile}; use crate::global_paths::GlobalPaths; +use crate::jsonstructs_versionsdb::JuliaupVersionDB; use crate::versions_file::load_versions_db; use anyhow::{Context, Result}; use cli_table::format::HorizontalLine; @@ -13,6 +14,95 @@ use cli_table::{ use itertools::Itertools; use numeric_sort::cmp; +fn format_linked_command(command: &str, args: &Option>) -> String { + let mut combined_command = String::new(); + + if command.contains(' ') { + combined_command.push('\"'); + combined_command.push_str(command); + combined_command.push('\"'); + } else { + combined_command.push_str(command); + } + + if let Some(args) = args { + for arg in args { + combined_command.push(' '); + if arg.contains(' ') { + combined_command.push('\"'); + combined_command.push_str(arg); + combined_command.push('\"'); + } else { + combined_command.push_str(arg); + } + } + } + + format!("Linked to `{combined_command}`") +} + +fn format_version(channel: &JuliaupConfigChannel) -> String { + match channel { + JuliaupConfigChannel::DirectDownloadChannel { version, .. } => { + format!("Development version {version}") + } + JuliaupConfigChannel::SystemChannel { version } => version.clone(), + JuliaupConfigChannel::LinkedChannel { command, args } => { + format_linked_command(command, args) + } + JuliaupConfigChannel::AliasChannel { target, args } => match args { + Some(args) if !args.is_empty() => { + format!("Alias to `{target}` with args: {:?}", args) + } + _ => format!("Alias to `{target}`"), + }, + } +} + +fn get_update_info( + channel_name: &str, + channel: &JuliaupConfigChannel, + config_file: &JuliaupReadonlyConfigFile, + versiondb_data: &JuliaupVersionDB, +) -> String { + match channel { + JuliaupConfigChannel::DirectDownloadChannel { + local_etag, + server_etag, + .. + } => (local_etag != server_etag).then(|| "Update available".to_string()), + JuliaupConfigChannel::SystemChannel { version } => { + match versiondb_data.available_channels.get(channel_name) { + Some(channel) if &channel.version != version => { + Some(format!("Update to {} available", channel.version)) + } + _ => None, + } + } + JuliaupConfigChannel::LinkedChannel { .. } => None, + JuliaupConfigChannel::AliasChannel { target, .. } => { + // Check if the target channel has updates available + match config_file.data.installed_channels.get(target) { + Some(JuliaupConfigChannel::DirectDownloadChannel { + local_etag, + server_etag, + .. + }) => (local_etag != server_etag).then(|| "Update available".to_string()), + Some(JuliaupConfigChannel::SystemChannel { version }) => { + match versiondb_data.available_channels.get(target) { + Some(channel) if channel.version != *version => { + Some(format!("Update to {} available", channel.version)) + } + _ => None, + } + } + _ => None, // Target channel doesn't exist or not updatable + } + } + } + .unwrap_or_default() +} + #[derive(Table)] struct ChannelRow { #[table(title = "Default", justify = "Justify::Right")] @@ -32,93 +122,19 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { let versiondb_data = load_versions_db(paths).with_context(|| "`status` command failed to load versions db.")?; - let rows_in_table: Vec<_> = config_file + let rows_in_table: Vec = config_file .data .installed_channels .iter() - .sorted_by(|a, b| cmp(&a.0.to_string(), &b.0.to_string())) - .map(|i| -> ChannelRow { - ChannelRow { - default: match config_file.data.default { - Some(ref default_value) => { - if i.0 == default_value { - "*" - } else { - "" - } - } - None => "", - }, - 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(); - - if command.contains(' ') { - combined_command.push('\"'); - combined_command.push_str(command); - combined_command.push('\"'); - } else { - combined_command.push_str(command); - } - - if let Some(args) = args { - for i in args { - combined_command.push(' '); - if i.contains(' ') { - combined_command.push('\"'); - combined_command.push_str(i); - combined_command.push('\"'); - } else { - combined_command.push_str(i); - } - } - } - format!("Linked to `{}`", combined_command) - } - }, - update: match i.1 { - JuliaupConfigChannel::SystemChannel { version } => { - match versiondb_data.available_channels.get(i.0) { - Some(channel) => { - if &channel.version != version { - format!("Update to {} available", channel.version) - } else { - "".to_string() - } - } - None => "".to_string(), - } - } - JuliaupConfigChannel::LinkedChannel { - 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() - } - } - }, - } + .sorted_by(|(channel_name_a, _), (channel_name_b, _)| cmp(channel_name_a, channel_name_b)) + .map(|(channel_name, channel)| ChannelRow { + default: match &config_file.data.default { + Some(ref default_value) if channel_name == default_value => "*", + _ => "", + }, + name: channel_name.to_string(), + version: format_version(channel), + update: get_update_info(channel_name, channel, &config_file, &versiondb_data), }) .collect(); diff --git a/src/command_update.rs b/src/command_update.rs index f72b780f8..adc9a7a06 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -11,6 +11,14 @@ use anyhow::{anyhow, bail, Context, Result}; use console::style; use std::path::PathBuf; +fn resolve_channel_alias(config_db: &JuliaupConfig, channel_name: &str) -> Result { + match config_db.installed_channels.get(channel_name) { + Some(JuliaupConfigChannel::AliasChannel { target, .. }) => Ok(target.to_string()), + Some(_) => Ok(channel_name.to_string()), + None => bail!("Channel '{}' not found", channel_name), + } +} + fn update_channel( config_db: &mut JuliaupConfig, channel: &String, @@ -22,6 +30,45 @@ fn update_channel( &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::DirectDownloadChannel { + path, + url, + local_etag, + server_etag, + version, + } => { + if local_etag != server_etag { + // We only do this so that we use `version` on both Windows and Linux to prevent a compiler warning/error + if version.is_empty() { + eprintln!( + "Channel {channel} version is empty, you may need to manually codesign this channel if you trust the contents of this pull request." + ); + } + eprintln!("{} channel {channel}", style("Updating").green().bold()); + + 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, + )?; + } + } + } JuliaupConfigChannel::SystemChannel { version } => { let should_version = version_db.available_channels.get(channel); @@ -64,10 +111,7 @@ fn update_channel( ); } } - JuliaupConfigChannel::LinkedChannel { - command: _, - args: _, - } => { + JuliaupConfigChannel::LinkedChannel { .. } => { if !ignore_non_updatable_channel { bail!( "Failed to update '{}' because it is a linked channel.", @@ -75,45 +119,8 @@ fn update_channel( ); } } - JuliaupConfigChannel::DirectDownloadChannel { - path, - url, - local_etag, - server_etag, - version, - } => { - if local_etag != server_etag { - // We only do this so that we use `version` on both Windows and Linux to prevent a compiler warning/error - if version.is_empty() { - eprintln!( - "Channel {} version is empty, you may need to manually codesign this channel if you trust the contents of this pull request.", - channel - ); - } - eprintln!("{} channel {}", style("Updating").green().bold(), channel); - - 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, - )?; - } - } + JuliaupConfigChannel::AliasChannel { .. } => { + unreachable!("Alias channels should be resolved before calling update_channel. Please submit a bug report."); } } @@ -131,7 +138,12 @@ pub fn run_command_update(channel: &Option, paths: &GlobalPaths) -> Resu match channel { None => { - for (k, _) in config_file.data.installed_channels.clone() { + for (k, v) in config_file.data.installed_channels.clone() { + // Skip alias channels - they don't need to be updated directly + // since they point to other channels that will be updated + if let JuliaupConfigChannel::AliasChannel { .. } = v { + continue; + } update_channel(&mut config_file.data, &k, &version_db, true, paths)?; } } @@ -143,7 +155,16 @@ pub fn run_command_update(channel: &Option, paths: &GlobalPaths) -> Resu ); } - update_channel(&mut config_file.data, channel, &version_db, false, paths)?; + // Resolve any aliases to get the actual target channel + let resolved_channel = resolve_channel_alias(&config_file.data, channel)?; + + update_channel( + &mut config_file.data, + &resolved_channel, + &version_db, + false, + paths, + )?; } }; diff --git a/src/config_file.rs b/src/config_file.rs index ee72e6580..480671977 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -51,6 +51,12 @@ pub enum JuliaupConfigChannel { #[serde(rename = "Args")] args: Option>, }, + AliasChannel { + #[serde(rename = "Target")] + target: String, + #[serde(rename = "Args")] + args: Option>, + }, } #[derive(Serialize, Deserialize, Clone, PartialEq)] diff --git a/src/operations.rs b/src/operations.rs index 39bea8505..9fd7f1b87 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -757,17 +757,7 @@ pub fn garbage_collect_versions( for (installed_version, detail) in &config_data.installed_versions { if config_data.installed_channels.iter().all(|j| match &j.1 { JuliaupConfigChannel::SystemChannel { version } => version != installed_version, - JuliaupConfigChannel::LinkedChannel { - command: _, - args: _, - } => true, - JuliaupConfigChannel::DirectDownloadChannel { - path: _, - url: _, - local_etag: _, - server_etag: _, - version: _, - } => true, + _ => true, }) { let path_to_delete = paths.juliauphome.join(&detail.path).canonicalize()?; let display = path_to_delete.display(); @@ -786,7 +776,10 @@ pub fn garbage_collect_versions( } if versions_to_uninstall.is_empty() { - eprintln!("Nothing to remove."); + eprintln!( + "{}: No unused Julia installations to clean up.", + style("GC").cyan().bold() + ); } else { for i in versions_to_uninstall { eprintln!("{} Julia {}", style("Removed").green().bold(), &i); @@ -844,6 +837,138 @@ pub fn remove_symlink(symlink_name: &String) -> Result<()> { Ok(()) } +#[cfg(not(windows))] +fn create_system_channel_symlink( + version: &str, + symlink_name: &str, + symlink_path: &Path, + paths: &GlobalPaths, + updating: &Option, +) -> Result<()> { + let child_target_foldername = format!("julia-{}", version); + let target_path = paths.juliauphome.join(&child_target_foldername); + + if let Some(ref prev_target) = updating { + eprintln!( + "{} symlink {} ( {} -> {} )", + style("Updating").cyan().bold(), + symlink_name, + prev_target.to_string_lossy(), + version + ); + } else { + eprintln!( + "{} {} for Julia {}.", + style("Creating symlink").cyan().bold(), + symlink_name, + version + ); + } + + 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() + ) + }, + ) +} + +#[cfg(not(windows))] +fn create_direct_download_symlink( + path: &str, + version: &str, + symlink_name: &str, + symlink_path: &Path, + paths: &GlobalPaths, + updating: &Option, +) -> Result<()> { + let target_path = paths.juliauphome.join(path); + + if let Some(ref prev_target) = updating { + eprintln!( + "{} symlink {} ( {} -> {} )", + style("Updating").cyan().bold(), + symlink_name, + prev_target.to_string_lossy(), + version + ); + } else { + eprintln!( + "{} {} for Julia {}.", + style("Creating symlink").cyan().bold(), + symlink_name, + version + ); + } + + 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() + ) + }, + ) +} + +#[cfg(not(windows))] +fn create_linked_channel_shim( + command: &str, + args: &Option>, + symlink_name: &str, + symlink_path: &Path, + updating: &Option, +) -> Result<()> { + let formatted_command = match args { + Some(x) => format!("{} {}", command, x.join(" ")), + None => command.to_string(), + }; + + if let Some(ref prev_target) = updating { + eprintln!( + "{} shim {} ( {} -> {} )", + style("Updating").cyan().bold(), + symlink_name, + prev_target.to_string_lossy(), + formatted_command + ); + } else { + eprintln!( + "{} {} for {}.", + style("Creating shim").cyan().bold(), + symlink_name, + formatted_command + ); + } + + std::fs::write( + symlink_path, + format!( + r#"#!/bin/sh +{} "$@" +"#, + formatted_command, + ), + ) + .with_context(|| { + format!( + "failed to create shim `{}`.", + symlink_path.to_string_lossy() + ) + })?; + + // set as executable + let perms = std::fs::Permissions::from_mode(0o755); + std::fs::set_permissions(symlink_path, perms).with_context(|| { + format!( + "failed to change permissions for shim `{}`.", + symlink_path.to_string_lossy() + ) + }) +} + #[cfg(not(windows))] pub fn create_symlink( channel: &JuliaupConfigChannel, @@ -854,122 +979,27 @@ pub fn create_symlink( .with_context(|| "Failed to retrieve binary directory while trying to create a symlink.")?; let symlink_path = symlink_folder.join(symlink_name); - let updating = _remove_symlink(&symlink_path)?; match channel { JuliaupConfigChannel::SystemChannel { version } => { - let child_target_foldername = format!("julia-{}", version); - let target_path = paths.juliauphome.join(&child_target_foldername); - - if let Some(ref prev_target) = updating { - eprintln!( - "{} symlink {} ( {} -> {} )", - style("Updating").cyan().bold(), - symlink_name, - prev_target.to_string_lossy(), - version - ); - } else { - eprintln!( - "{} {} for Julia {}.", - style("Creating symlink").cyan().bold(), - symlink_name, - version - ); - } - - 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() - ) - })?; + create_system_channel_symlink(version, symlink_name, &symlink_path, paths, &updating) } - JuliaupConfigChannel::DirectDownloadChannel { - path, - url: _, - local_etag: _, - server_etag: _, - version, - } => { - let target_path = paths.juliauphome.join(path); - - if let Some(ref prev_target) = updating { - eprintln!( - "{} symlink {} ( {} -> {} )", - style("Updating").cyan().bold(), - symlink_name, - prev_target.to_string_lossy(), - version - ); - } else { - eprintln!( - "{} {} for Julia {}.", - style("Creating symlink").cyan().bold(), - symlink_name, - version - ); - } - - 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::LinkedChannel { command, args } => { - let formatted_command = match args { - Some(x) => format!("{} {}", command, x.join(" ")), - None => command.clone(), - }; - - if let Some(ref prev_target) = updating { - eprintln!( - "{} shim {} ( {} -> {} )", - style("Updating").cyan().bold(), - symlink_name, - prev_target.to_string_lossy(), - formatted_command - ); - } else { - eprintln!( - "{} {} for {}.", - style("Creating shim").cyan().bold(), - symlink_name, - formatted_command - ); - } - - std::fs::write( + JuliaupConfigChannel::DirectDownloadChannel { path, version, .. } => { + create_direct_download_symlink( + path, + version, + symlink_name, &symlink_path, - format!( - r#"#!/bin/sh -{} "$@" -"#, - formatted_command, - ), + paths, + &updating, ) - .with_context(|| { - format!( - "failed to create shim `{}`.", - symlink_path.to_string_lossy() - ) - })?; - - // set as executable - let perms = std::fs::Permissions::from_mode(0o755); - std::fs::set_permissions(&symlink_path, perms).with_context(|| { - format!( - "failed to change permissions for shim `{}`.", - symlink_path.to_string_lossy() - ) - })?; } - }; + JuliaupConfigChannel::LinkedChannel { command, args } => { + create_linked_channel_shim(command, args, symlink_name, &symlink_path, &updating) + } + JuliaupConfigChannel::AliasChannel { .. } => Ok(()), // Aliases have their symlinks resolved at runtime + }?; if updating.is_none() { if let Ok(path) = std::env::var("PATH") { diff --git a/tests/channel_selection.rs b/tests/channel_selection.rs index 4f03fbd51..ab8361932 100644 --- a/tests/channel_selection.rs +++ b/tests/channel_selection.rs @@ -1,89 +1,67 @@ -use assert_cmd::Command; use predicates::str::contains; +mod utils; +use utils::TestEnv; + #[test] fn channel_selection() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.7.3") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.8.5") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("default") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout("1.6.7"); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+1.8.5") .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout("1.8.5"); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_CHANNEL", "1.7.3") .assert() .success() .stdout("1.7.3"); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+1.8.5") .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_CHANNEL", "1.7.3") .assert() .success() @@ -91,23 +69,17 @@ fn channel_selection() { // Now testing incorrect channels - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+1.8.6") .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .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("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_CHANNEL", "1.7.4") .assert() .failure() @@ -115,13 +87,10 @@ fn channel_selection() { "ERROR: Invalid Juliaup channel `1.7.4` from environment variable JULIAUP_CHANNEL. Please run `juliaup list` to get a list of valid channels and versions.\n", ); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+1.8.6") .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_CHANNEL", "1.7.4") .assert() .failure() @@ -129,24 +98,18 @@ fn channel_selection() { // https://github.com/JuliaLang/juliaup/issues/766 // First enable auto-install in configuration - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("config") .arg("autoinstallchannels") .arg("true") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Command line channel selector should auto-install valid channels - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+1.8.2") .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_CHANNEL", "1.7.4") .assert() .success() @@ -157,13 +120,10 @@ fn channel_selection() { // https://github.com/JuliaLang/juliaup/issues/820 // Command line channel selector should auto-install valid channels including nightly - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+nightly") .arg("-e") .arg("print(\"SUCCESS\")") // Use SUCCESS instead of VERSION since nightly version can vary - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_CHANNEL", "1.7.4") .assert() .success() @@ -174,24 +134,18 @@ fn channel_selection() { // https://github.com/JuliaLang/juliaup/issues/995 // Reset auto-install to false for this test - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("config") .arg("autoinstallchannels") .arg("false") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // PR channels that don't exist should not auto-install in non-interactive mode - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+pr1") .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_CHANNEL", "1.7.4") .assert() .failure() @@ -200,38 +154,29 @@ fn channel_selection() { #[test] fn auto_install_valid_channel() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // First set up a basic julia installation so juliaup is properly initialized - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.11") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); // Enable auto-install for this test - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("config") .arg("autoinstallchannels") .arg("true") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Now test auto-installing a valid but not installed channel via command line - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+1.10.10") .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout("1.10.10") diff --git a/tests/command_add.rs b/tests/command_add.rs index 5b52f3b9d..72541d751 100644 --- a/tests/command_add.rs +++ b/tests/command_add.rs @@ -1,58 +1,45 @@ -use assert_cmd::Command; use predicates::prelude::predicate; +mod utils; +use utils::TestEnv; + #[test] fn command_add() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.6.4") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .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() + env.juliaup() .arg("add") .arg("1.11-nightly") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+1.6.4") .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout("1.6.4"); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("+nightly") .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout( diff --git a/tests/command_default.rs b/tests/command_default.rs index 285a48389..80ac0cf6f 100644 --- a/tests/command_default.rs +++ b/tests/command_default.rs @@ -1,35 +1,27 @@ -use assert_cmd::Command; +mod utils; +use utils::TestEnv; #[test] fn command_default() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.6.0") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("default") .arg("1.6.0") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout("1.6.0"); diff --git a/tests/command_gc.rs b/tests/command_gc.rs index e60afad04..9a24d9c5f 100644 --- a/tests/command_gc.rs +++ b/tests/command_gc.rs @@ -1,95 +1,54 @@ -use assert_cmd::Command; use predicates::prelude::*; +mod utils; +use utils::TestEnv; + #[test] fn command_gc() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); - Command::cargo_bin("juliaup") - .unwrap() - .arg("add") - .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success(); + env.juliaup().arg("add").arg("1.6.7").assert().success(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("link") .arg("julib") .arg("julib") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("link") .arg("julic") .arg("julic") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("juliaup") - .unwrap() - .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success() - .stdout( - predicate::str::contains("\n") - .count(5) - .and(predicate::str::contains("julic")) - .and(predicate::str::contains("julib")), - ); + env.juliaup().arg("status").assert().success().stdout( + predicate::str::contains("\n") + .count(5) + .and(predicate::str::contains("julic")) + .and(predicate::str::contains("julib")), + ); - Command::cargo_bin("juliaup") - .unwrap() - .arg("gc") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success(); + env.juliaup().arg("gc").assert().success(); - Command::cargo_bin("juliaup") - .unwrap() - .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success() - .stdout( - predicate::str::contains("\n") - .count(5) - .and(predicate::str::contains("julic")) - .and(predicate::str::contains("julib")), - ); + env.juliaup().arg("status").assert().success().stdout( + predicate::str::contains("\n") + .count(5) + .and(predicate::str::contains("julic")) + .and(predicate::str::contains("julib")), + ); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("gc") .arg("--prune-linked") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("juliaup") - .unwrap() - .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success() - .stdout( - predicate::str::contains("\n") - .count(3) - .and(predicate::str::contains("julic").not()) - .and(predicate::str::contains("julib").not()), - ); + env.juliaup().arg("status").assert().success().stdout( + predicate::str::contains("\n") + .count(3) + .and(predicate::str::contains("julic").not()) + .and(predicate::str::contains("julib").not()), + ); } diff --git a/tests/command_link.rs b/tests/command_link.rs new file mode 100644 index 000000000..30c9eeaae --- /dev/null +++ b/tests/command_link.rs @@ -0,0 +1,310 @@ +use predicates::prelude::*; + +mod utils; +use utils::TestEnv; + +#[test] +fn command_link_binary() { + let env = TestEnv::new(); + + // First add a regular channel for testing + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Test linking to a binary file (existing functionality) + env.juliaup() + .arg("link") + .arg("custom") + .arg("/usr/bin/false") // Use a binary that exists but won't work as Julia + .assert() + .success(); + + // Verify the link shows up in status + env.juliaup() + .arg("status") + .assert() + .success() + .stdout(predicate::str::contains("custom").and(predicate::str::contains("Linked to"))); +} + +#[test] +fn command_link_alias() { + let env = TestEnv::new(); + + // First install a Julia version to create an alias to + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Create an alias to the installed version + env.juliaup() + .arg("link") + .arg("stable") + .arg("+1.10.10") + .assert() + .success() + .stderr(predicate::str::contains( + "Channel alias `stable` created, pointing to `1.10.10`.", + )); + + // Verify the alias shows up in status + env.juliaup().arg("status").assert().success().stdout( + predicate::str::contains("stable").and(predicate::str::contains("Alias to `1.10.10`")), + ); +} + +#[test] +fn command_link_alias_to_system_channel() { + let env = TestEnv::new(); + + // Test creating an alias to a system channel (release) + env.juliaup() + .arg("link") + .arg("r") + .arg("+release") + .assert() + .success() + .stderr(predicate::str::contains( + "Channel alias `r` created, pointing to `release`.", + )); + + // Verify the alias shows up in status + env.juliaup() + .arg("status") + .assert() + .success() + .stdout(predicate::str::contains("r").and(predicate::str::contains("Alias to `release`"))); +} + +#[test] +fn command_link_alias_invalid_target() { + let env = TestEnv::new(); + + // Test creating an alias to a non-existent channel + env.juliaup() + .arg("link") + .arg("broken") + .arg("+nonexistent") + .assert() + .failure() + .stderr(predicate::str::contains( + "Target channel `nonexistent` is not installed", + )); +} + +#[test] +fn command_link_alias_with_args_works() { + let env = TestEnv::new(); + + // Test that creating an alias with extra arguments works and shows them in the output + env.juliaup() + .arg("link") + .arg("alias_with_args") + .arg("+release") + .arg("--") + .arg("--some-arg") + .assert() + .success() + .stderr(predicate::str::contains("args: [\"--some-arg\"]")); +} + +#[test] +fn alias_with_args_passes_through() { + let env = TestEnv::new(); + + // First install a Julia version + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Create an alias with args that will be passed to Julia + env.juliaup() + .arg("link") + .arg("julia_with_threads") + .arg("+1.10.10") + .arg("--") + .arg("--threads=4") + .arg("--startup-file=no") + .assert() + .success() + .stderr(predicate::str::contains( + "args: [\"--threads=4\", \"--startup-file=no\"]", + )); + + // Test that the args are actually passed through when running Julia + // Julia with --threads=4 should report 4 threads + env.julia() + .arg("+julia_with_threads") + .arg("-e") + .arg("println(Threads.nthreads())") + .assert() + .success() + .stdout("4\n"); +} + +#[test] +fn command_link_duplicate_channel() { + let env = TestEnv::new(); + + // First add a regular channel + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Try to create an alias with the same name as an existing channel + env.juliaup() + .arg("link") + .arg("1.10.10") + .arg("+release") + .assert() + .failure() + .stderr(predicate::str::contains( + "Channel name `1.10.10` is already used", + )); +} + +#[test] +fn command_remove_alias() { + let env = TestEnv::new(); + + // Create an alias + env.juliaup() + .arg("link") + .arg("r") + .arg("+release") + .assert() + .success(); + + // Remove the alias + env.juliaup() + .arg("remove") + .arg("r") + .assert() + .success() + .stderr(predicate::str::contains( + "Julia alias (pointing to 'release') 'r' successfully removed.", + )); + + // Verify the alias is gone from status (check for empty list or no mention of the alias) + env.juliaup() + .arg("status") + .assert() + .success() + .stdout(predicate::str::contains("Alias to").not()); +} + +#[test] +fn command_remove_non_existent() { + let env = TestEnv::new(); + + // Try to remove a non-existent channel + env.juliaup() + .arg("remove") + .arg("nonexistent") + .assert() + .failure() + .stderr(predicate::str::contains("'nonexistent' cannot be removed because it is not currently installed. Please run `juliaup list` to see available channels.")); +} + +#[test] +fn alias_resolution_julia_launcher() { + let env = TestEnv::new(); + + // Add a channel first + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Create an alias to it + env.juliaup() + .arg("link") + .arg("stable") + .arg("+1.10.10") + .assert() + .success(); + + // Try to use the alias with julia +alias + env.julia() + .arg("+stable") + .arg("-e") + .arg("print(VERSION)") + .assert() + .success() + .stdout("1.10.10"); +} + +#[test] +fn alias_as_default() { + let env = TestEnv::new(); + + // Add a channel first + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Create an alias + env.juliaup() + .arg("link") + .arg("stable") + .arg("+1.10.10") + .assert() + .success(); + + // Set the alias as default + env.juliaup() + .arg("default") + .arg("stable") + .assert() + .success(); + + // Test that julia without + uses the alias + env.julia() + .arg("-e") + .arg("print(VERSION)") + .assert() + .success() + .stdout("1.10.10"); +} + +#[test] +fn alias_to_alias_prevented() { + let env = TestEnv::new(); + + // Add a channel first + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Create first alias + env.juliaup() + .arg("link") + .arg("stable") + .arg("+1.10.10") + .assert() + .success(); + + // Try to create alias to alias - should now fail + env.juliaup() + .arg("link") + .arg("prod") + .arg("+stable") + .assert() + .failure() + .stderr(predicate::str::contains( + "Cannot create an alias to another alias `stable`", + )); +} + +// The old alias_circular_reference_detection and alias_deep_chain_limit tests +// are no longer relevant since we now prevent alias-to-alias chains entirely + +#[test] +fn alias_update_resolves_target() { + let env = TestEnv::new(); + + // First install a Julia version to create an alias to + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Create an alias to the installed version + env.juliaup() + .arg("link") + .arg("r") + .arg("+1.10.10") + .assert() + .success(); + + // Update through the alias - should work and update the target + env.juliaup() + .arg("update") + .arg("r") + .assert() + .success() + .stderr(predicate::str::contains("Checking for new Julia versions")); +} diff --git a/tests/command_list_test.rs b/tests/command_list_test.rs index 275a796e5..7a661e838 100644 --- a/tests/command_list_test.rs +++ b/tests/command_list_test.rs @@ -1,18 +1,14 @@ -use assert_cmd::Command; use predicates::prelude::*; +mod utils; +use utils::TestEnv; + #[test] fn command_list() { - let depot_dir = tempfile::Builder::new() - .prefix("juliauptest") - .tempdir() - .unwrap(); + let env = TestEnv::new(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("list") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(predicate::str::starts_with(" Channel").and(predicate::str::contains("release"))) @@ -20,11 +16,8 @@ fn command_list() { .stdout(predicate::str::contains("x.y-nightly")) .stdout(predicate::str::contains("pr{number}")); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("ls") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(predicate::str::starts_with(" Channel").and(predicate::str::contains("release"))); diff --git a/tests/command_override_test.rs b/tests/command_override_test.rs index f2d5f3bc0..8d6bc1b55 100644 --- a/tests/command_override_test.rs +++ b/tests/command_override_test.rs @@ -1,37 +1,30 @@ -use assert_cmd::Command; use predicates::prelude::PredicateBooleanExt; use predicates::str::starts_with; +mod utils; +use utils::TestEnv; + #[test] fn command_override_status_test() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.8.5") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(" Path Channel \n---------------\n"); @@ -39,117 +32,87 @@ fn command_override_status_test() { #[test] fn command_override_cur_dir_test() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); let or_dir = assert_fs::TempDir::new().unwrap(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.8.5") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("default") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success() .stdout("1.6.7"); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("set") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success() .stdout("") .stderr(starts_with("Override set to '1.6.7'")); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("set") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success() .stdout("") .stderr(starts_with("Override already set to '1.6.7'")); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("set") .arg("1.8.5") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success() .stdout("") .stderr(starts_with("Override changed from '1.6.7' to '1.8.5'")); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success() .stdout("1.8.5"); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("unset") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success(); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success() @@ -158,91 +121,67 @@ fn command_override_cur_dir_test() { #[test] fn command_override_arg_test() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); let or_dir = assert_fs::TempDir::new().unwrap(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.8.5") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("default") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success() .stdout("1.6.7"); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("set") .arg("--path") .arg(or_dir.as_os_str()) .arg("1.8.5") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success() .stdout("1.8.5"); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("unset") .arg("--path") .arg(or_dir.as_os_str()) - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir) .assert() .success() @@ -251,93 +190,69 @@ fn command_override_arg_test() { #[test] fn command_override_overlap_test() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); let or_dir_parent = assert_fs::TempDir::new().unwrap(); let or_dir_child = or_dir_parent.join("child"); std::fs::create_dir_all(&or_dir_child).unwrap(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.7.3") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.8.5") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("default") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("set") .arg("--path") .arg(or_dir_parent.as_os_str()) .arg("1.7.3") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("set") .arg("--path") .arg(or_dir_child.as_os_str()) .arg("1.8.5") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir_parent) .assert() .success() .stdout("1.7.3"); - Command::cargo_bin("julia") - .unwrap() + env.julia() .arg("-e") .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .current_dir(&or_dir_child) .assert() .success() @@ -346,74 +261,56 @@ fn command_override_overlap_test() { #[test] fn command_override_delete_empty_test() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); let or_dir1 = assert_fs::TempDir::new().unwrap(); let or_dir2 = assert_fs::TempDir::new().unwrap(); let or_dir3 = assert_fs::TempDir::new().unwrap(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("set") .arg("--path") .arg(or_dir1.as_os_str()) .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("set") .arg("--path") .arg(or_dir2.as_os_str()) .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("set") .arg("--path") .arg(or_dir3.as_os_str()) .arg("1.6.7") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("unset") .arg("--nonexistent") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(predicates::ord::eq(" Path Channel \n---------------\n").not()); @@ -422,22 +319,16 @@ fn command_override_delete_empty_test() { std::fs::remove_dir(or_dir2).unwrap(); std::fs::remove_dir(or_dir3).unwrap(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("unset") .arg("--nonexistent") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("override") .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(" Path Channel \n---------------\n"); diff --git a/tests/command_remove.rs b/tests/command_remove.rs index 9b2c25d0e..ef1cf06a9 100644 --- a/tests/command_remove.rs +++ b/tests/command_remove.rs @@ -1,113 +1,79 @@ -use assert_cmd::Command; use predicates::boolean::PredicateBooleanExt; +mod utils; +use utils::TestEnv; + #[test] fn command_remove() { - let depot_dir = tempfile::Builder::new() - .prefix("juliauptest") - .tempdir() - .unwrap(); + let env = TestEnv::new(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .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").not()); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.6.4") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .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")); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("release") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .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("release"))); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("remove") .arg("release") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .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("release").not())); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .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() + env.juliaup() .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() + env.juliaup() .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() + env.juliaup() .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())); diff --git a/tests/command_status_test.rs b/tests/command_status_test.rs index 94c765d44..dcb6fa0a5 100644 --- a/tests/command_status_test.rs +++ b/tests/command_status_test.rs @@ -1,26 +1,18 @@ -use assert_cmd::Command; +mod utils; +use utils::TestEnv; #[test] fn command_status() { - let depot_dir = tempfile::Builder::new() - .prefix("juliauptest") - .tempdir() - .unwrap(); + let env = TestEnv::new(); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(" Default Channel Version Update \n-----------------------------------\n"); - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("st") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(" Default Channel Version Update \n-----------------------------------\n"); diff --git a/tests/command_update.rs b/tests/command_update.rs index 493029cc3..63869dc7f 100644 --- a/tests/command_update.rs +++ b/tests/command_update.rs @@ -1,27 +1,48 @@ -use assert_cmd::Command; +mod utils; +use utils::TestEnv; #[test] fn command_update() { - let depot_dir = tempfile::Builder::new() - .prefix("juliauptest") - .tempdir() - .unwrap(); - - Command::cargo_bin("juliaup") - .unwrap() - .arg("update") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + let env = TestEnv::new(); + + env.juliaup().arg("update").assert().success().stdout(""); + + env.juliaup().arg("up").assert().success().stdout(""); +} + +#[test] +fn command_update_alias_works() { + let env = TestEnv::new(); + + // First install a Julia version to create an alias to + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Create an alias to the installed version + env.juliaup() + .arg("link") + .arg("r") + .arg("+1.10.10") .assert() - .success() - .stdout(""); - - Command::cargo_bin("juliaup") - .unwrap() - .arg("up") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .success(); + + // Update the alias - should succeed and update the target + env.juliaup().arg("update").arg("r").assert().success(); +} + +#[test] +fn command_update_all_with_alias() { + let env = TestEnv::new(); + + // First install a Julia version to create an alias to + env.juliaup().arg("add").arg("1.10.10").assert().success(); + + // Create an alias to the installed version - this reproduces the original bug scenario + env.juliaup() + .arg("link") + .arg("r") + .arg("+1.10.10") .assert() - .success() - .stdout(""); + .success(); + + env.juliaup().arg("update").assert().success(); } diff --git a/tests/utils.rs b/tests/utils.rs new file mode 100644 index 000000000..811f90ec3 --- /dev/null +++ b/tests/utils.rs @@ -0,0 +1,36 @@ +use assert_cmd::Command; +use assert_fs::TempDir; + +/// A test environment that provides convenient methods for running juliaup and julia commands +/// with isolated depot directories. +pub struct TestEnv { + depot_dir: TempDir, +} + +#[allow(dead_code)] // May not be used in all test configurations +impl TestEnv { + /// Create a new test environment with an isolated temporary depot directory + pub fn new() -> Self { + Self { + depot_dir: TempDir::new().unwrap(), + } + } + + /// Get a Command for running juliaup with the test environment's depot paths + pub fn juliaup(&self) -> Command { + self.command("juliaup") + } + + /// Get a Command for running julia with the test environment's depot paths + pub fn julia(&self) -> Command { + self.command("julia") + } + + /// Get a Command for running any binary with the test environment's depot paths + pub fn command(&self, bin: &str) -> Command { + let mut cmd = Command::cargo_bin(bin).unwrap(); + cmd.env("JULIA_DEPOT_PATH", self.depot_dir.path()); + cmd.env("JULIAUP_DEPOT_PATH", self.depot_dir.path()); + cmd + } +}