From 1cf4afe4f5bbdc7c3a0e2f517cb2be6bf62266f3 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Wed, 27 Aug 2025 10:51:46 -0400 Subject: [PATCH 01/43] Expand `link` to handle aliases --- README.md | 1 + build.rs | 5 +- src/bin/julialauncher.rs | 68 ++++-- src/cli.rs | 12 +- src/command_api.rs | 48 +++- src/command_link.rs | 60 +++-- src/command_remove.rs | 23 +- src/command_status.rs | 8 +- src/command_update.rs | 37 ++- src/config_file.rs | 4 + src/operations.rs | 7 +- tests/command_link.rs | 507 +++++++++++++++++++++++++++++++++++++++ tests/command_update.rs | 39 +++ 13 files changed, 750 insertions(+), 69 deletions(-) create mode 100644 tests/command_link.rs 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/build.rs b/build.rs index a6c908389..81ef1bb2e 100644 --- a/build.rs +++ b/build.rs @@ -21,7 +21,7 @@ fn main() -> Result<()> { let db_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()) .join("versiondb") - .join(format!("versiondb-{}.json", target_platform)); + .join(format!("versiondb-{target_platform}.json")); let version_db_path = out_path.join("versionsdb.json"); std::fs::copy(&db_path, &version_db_path).unwrap(); @@ -35,8 +35,7 @@ fn main() -> Result<()> { std::fs::write( &bundled_version_path, format!( - "pub const BUNDLED_JULIA_VERSION: &str = {}; pub const BUNDLED_DB_VERSION: &str = {};", - bundled_version_as_string, bundled_dbversion_as_string + "pub const BUNDLED_JULIA_VERSION: &str = {bundled_version_as_string}; pub const BUNDLED_DB_VERSION: &str = {bundled_dbversion_as_string};" ), ) .unwrap(); diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index e66c7a3da..e786732bf 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 is_terminal::IsTerminal; use itertools::Itertools; @@ -164,6 +164,23 @@ enum JuliaupChannelSource { Default, } +fn resolve_channel_alias( + config_data: &JuliaupConfig, + channel: &str, + max_depth: usize, +) -> Result { + if max_depth == 0 { + bail!("Channel alias chain too deep for `{}`. There might be a circular reference.", channel); + } + + match config_data.installed_channels.get(channel) { + Some(JuliaupConfigChannel::AliasChannel { target }) => { + resolve_channel_alias(config_data, target, max_depth - 1) + } + _ => Ok(channel.to_string()), + } +} + fn get_julia_path_from_channel( versions_db: &JuliaupVersionDB, config_data: &JuliaupConfig, @@ -171,42 +188,49 @@ fn get_julia_path_from_channel( juliaupconfig_path: &Path, juliaup_channel_source: JuliaupChannelSource, ) -> Result<(PathBuf, Vec)> { - let channel_valid = is_valid_channel(versions_db, &channel.to_string())?; + // First resolve any aliases + let resolved_channel = resolve_channel_alias(config_data, channel, 10)?; + + let channel_valid = is_valid_channel(versions_db, &resolved_channel)?; let channel_info = config_data .installed_channels - .get(channel) + .get(&resolved_channel) .ok_or_else(|| 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!("`{}` is not installed. Please run `juliaup add {}` to install channel or version.", resolved_channel, resolved_channel) } + } else if is_pr_channel(&resolved_channel) { + UserError { msg: format!("`{}` is not installed. Please run `juliaup add {}` to install pull request channel if available.", resolved_channel, resolved_channel) } } 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 `{}`. Please run `juliaup list` to get a list of valid channels and versions.", resolved_channel) } } }, 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!("`{}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install channel or version.", resolved_channel, resolved_channel) } + } else if is_pr_channel(&resolved_channel) { + UserError { msg: format!("`{}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install pull request channel if available.", resolved_channel, resolved_channel) } } 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 `{}` from environment variable JULIAUP_CHANNEL. Please run `juliaup list` to get a list of valid channels and versions.", resolved_channel) } } }, 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!("`{}` from directory override is not installed. Please run `juliaup add {}` to install channel or version.", resolved_channel, resolved_channel) } + } else if is_pr_channel(&resolved_channel) { + UserError { msg: format!("`{}` from directory override is not installed. Please run `juliaup add {}` to install pull request channel if available.", resolved_channel, resolved_channel) } } 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 `{}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.", resolved_channel) } } }, - 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 `{}` is not installed.", resolved_channel) } })?; match channel_info { + JuliaupConfigChannel::AliasChannel { target: _ } => { + // This should not happen after alias resolution, but just in case + bail!("Unexpected alias channel after resolution: {}", resolved_channel); + } JuliaupConfigChannel::LinkedChannel { command, args } => { Ok(( PathBuf::from(command), @@ -216,12 +240,12 @@ fn get_julia_path_from_channel( 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 {} is pointing to Julia version {}, which is not installed.", resolved_channel, version))?.path; - check_channel_uptodate(channel, version, versions_db).with_context(|| { + check_channel_uptodate(&resolved_channel, version, versions_db).with_context(|| { format!( "The Julia launcher failed while checking whether the channel {} is up-to-date.", - channel + resolved_channel ) })?; let absolute_path = juliaupconfig_path @@ -247,7 +271,7 @@ fn get_julia_path_from_channel( version: _, } => { if local_etag != server_etag { - if channel.starts_with("nightly") { + if resolved_channel.starts_with("nightly") { // Nightly is updateable several times per day so this message will show // more often than not unless folks update a couple of times a day. // Also, folks using nightly are typically more experienced and need @@ -258,12 +282,12 @@ fn get_julia_path_from_channel( } else { eprintln!( "A new version of Julia for the `{}` channel is available. Run:", - channel + resolved_channel ); eprintln!(); eprintln!(" juliaup update"); eprintln!(); - eprintln!("to install the latest Julia for the `{}` channel.", channel); + eprintln!("to install the latest Julia for the `{}` channel.", resolved_channel); } } diff --git a/src/cli.rs b/src/cli.rs index 05de9415b..989d80def 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,7 +4,7 @@ use clap::{Parser, ValueEnum}; #[derive(Clone, ValueEnum)] pub enum CompletionShell { Bash, - Elvish, + Elvish, Fish, Nushell, PowerShell, @@ -23,10 +23,14 @@ 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 to a custom channel name, or create a channel alias Link { + /// Name of the new channel to create channel: String, + /// Path to Julia binary, or +CHANNEL to create an alias + #[clap(help = "Path to Julia binary, or +CHANNEL to create an alias (e.g. +release)", value_name = "TARGET")] file: String, + /// Additional arguments for the Julia binary (not used for aliases) args: Vec, }, /// List all available channels @@ -62,9 +66,9 @@ pub enum Juliaup { #[clap(subcommand, name = "self")] SelfSubCmd(SelfSubCmd), /// Generate tab-completion scripts for your shell - Completions { + Completions { #[arg(value_enum, value_name = "SHELL")] - shell: CompletionShell + shell: CompletionShell }, // This is used for the cron jobs that we create. By using this UUID for the command // We can identify the cron jobs that were created by juliaup for uninstall purposes diff --git a/src/command_api.rs b/src/command_api.rs index 98c81ef85..21b655954 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -43,17 +43,41 @@ 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::AliasChannel { target } => { + // For aliases, we need to resolve to the target and get its info + // Skip if the target doesn't exist to avoid infinite recursion + if let Some(target_channel) = config_file.data.installed_channels.get(target) { + match target_channel { + JuliaupConfigChannel::AliasChannel { .. } => { + // Avoid infinite recursion for alias-to-alias + continue; + } + _ => { + // Recursively get info for the target + // For now, just indicate it's an alias + JuliaupChannelInfo { + name: key.clone(), + file: format!("alias-to-{}", target), + args: Vec::new(), + version: format!("alias to {}", target), + arch: "".to_string(), + } + } + } + } else { + // Target doesn't exist, skip this alias + continue; + } + } + 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 @@ -75,13 +99,13 @@ 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() { + for i in args.as_ref().unwrap_or(&Vec::new()) { new_args.push(i.to_string()); } 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,7 +125,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { JuliaupChannelInfo { name: key.clone(), file: command.clone(), - args: args.unwrap_or_default(), + args: args.as_ref().unwrap_or(&Vec::new()).clone(), version: version.to_string(), arch: "".to_string(), } @@ -130,7 +154,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { 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 +170,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..fb6127146 100644 --- a/src/command_link.rs +++ b/src/command_link.rs @@ -27,24 +27,51 @@ 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) = file.strip_prefix('+') { + // Remove the + prefix - 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); - } + // Validate that the target channel exists or is valid + if !config_file.data.installed_channels.contains_key(target_channel) + && !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 !args.is_empty() { + bail!("Arguments are not supported when creating channel aliases. Remove the extra arguments: {:?}", args); + } + + config_file.data.installed_channels.insert( + channel.to_string(), + JuliaupConfigChannel::AliasChannel { + target: target_channel.to_string(), + }, + ); + + eprintln!("Channel alias `{channel}` created, pointing to `{target_channel}`."); + } else { + // Original behavior for linking to binary files + let absolute_file_path = Path::new(file) + .absolutize() + .with_context(|| format!("Failed to convert path `{file}` 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 +80,14 @@ pub fn run_command_link( .with_context(|| "`link` command failed to save configuration db.")?; #[cfg(not(windows))] - if create_symlinks { + if create_symlinks && !file.starts_with('+') { + // Only create symlinks for binary links, not channel aliases create_symlink( &JuliaupConfigChannel::LinkedChannel { command: file.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..b0e615480 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,15 @@ 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::AliasChannel { target } => format!("alias (pointing to '{target}')"), + JuliaupConfigChannel::LinkedChannel { .. } => "linked channel".to_string(), + JuliaupConfigChannel::SystemChannel { .. } => "channel".to_string(), + JuliaupConfigChannel::DirectDownloadChannel { .. } => "channel".to_string(), + }; if let JuliaupConfigChannel::DirectDownloadChannel { path, @@ -47,32 +55,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 07efd1b6a..b166b27c2 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -59,7 +59,7 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { server_etag: _, version, } => { - format!("Development version {}", version) + format!("Development version {version}") } JuliaupConfigChannel::LinkedChannel { command, args } => { let mut combined_command = String::new(); @@ -84,7 +84,10 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { } } } - format!("Linked to `{}`", combined_command) + format!("Linked to `{combined_command}`") + } + JuliaupConfigChannel::AliasChannel { target } => { + format!("Alias to `{target}`") } }, update: match i.1 { @@ -104,6 +107,7 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { command: _, args: _, } => "".to_string(), + JuliaupConfigChannel::AliasChannel { target: _ } => "".to_string(), JuliaupConfigChannel::DirectDownloadChannel { path: _, url: _, diff --git a/src/command_update.rs b/src/command_update.rs index f72b780f8..6ca34c1c9 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -11,6 +11,30 @@ use anyhow::{anyhow, bail, Context, Result}; use console::style; use std::path::PathBuf; +fn resolve_channel_alias(config_db: &JuliaupConfig, channel_name: &str) -> Result { + let mut current_channel = channel_name; + let mut visited = std::collections::HashSet::new(); + + loop { + if visited.contains(current_channel) { + bail!("Channel alias chain contains a cycle: {}", channel_name); + } + visited.insert(current_channel.to_string()); + + if visited.len() > 10 { + bail!("Channel alias chain too deep: {}", channel_name); + } + + match config_db.installed_channels.get(current_channel) { + Some(JuliaupConfigChannel::AliasChannel { target }) => { + current_channel = target; + } + Some(_) => return Ok(current_channel.to_string()), + None => bail!("Channel '{}' not found", current_channel), + } + } +} + fn update_channel( config_db: &mut JuliaupConfig, channel: &String, @@ -75,6 +99,13 @@ fn update_channel( ); } } + JuliaupConfigChannel::AliasChannel { target: _ } => { + // This should never happen since we resolve aliases before calling this function + bail!( + "Internal error: Tried to update an alias channel '{}' directly.", + channel + ); + } JuliaupConfigChannel::DirectDownloadChannel { path, url, @@ -143,7 +174,11 @@ 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) + .with_context(|| format!("Failed to resolve channel '{}'", 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 4b1f923ee..506ae163d 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -51,6 +51,10 @@ pub enum JuliaupConfigChannel { #[serde(rename = "Args")] args: Option>, }, + AliasChannel { + #[serde(rename = "Target")] + target: String, + }, } #[derive(Serialize, Deserialize, Clone, PartialEq)] diff --git a/src/operations.rs b/src/operations.rs index 2173b591d..00a93b262 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -759,6 +759,7 @@ pub fn garbage_collect_versions( command: _, args: _, } => true, + JuliaupConfigChannel::AliasChannel { target: _ } => true, JuliaupConfigChannel::DirectDownloadChannel { path: _, url: _, @@ -855,6 +856,10 @@ pub fn create_symlink( let updating = _remove_symlink(&symlink_path)?; match channel { + JuliaupConfigChannel::AliasChannel { target: _ } => { + // Aliases don't create symlinks directly, they are resolved at runtime + return Ok(()); + } JuliaupConfigChannel::SystemChannel { version } => { let child_target_foldername = format!("julia-{}", version); let target_path = paths.juliauphome.join(&child_target_foldername); @@ -1509,7 +1514,7 @@ where eprintln!("{}", message); // Now wait for the function to complete - + rx.recv().unwrap() } Err(e) => panic!("Error receiving result: {:?}", e), diff --git a/tests/command_link.rs b/tests/command_link.rs new file mode 100644 index 000000000..89351a4a6 --- /dev/null +++ b/tests/command_link.rs @@ -0,0 +1,507 @@ +use assert_cmd::Command; +use predicates::prelude::*; + +#[test] +fn command_link_binary() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // First add a regular channel for testing + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Test linking to a binary file (existing functionality) + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("custom") + .arg("/usr/bin/false") // Use a binary that exists but won't work as Julia + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Verify the link shows up in status + 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("custom").and(predicate::str::contains("Linked to"))); +} + +#[test] +fn command_link_alias() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // First install a Julia version to create an alias to + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create an alias to the installed version + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("stable") + .arg("+1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stderr(predicate::str::contains("Channel alias `stable` created, pointing to `1.10.10`.")); + + // Verify the alias shows up in status + 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("stable").and(predicate::str::contains("Alias to `1.10.10`")) + ); +} + +#[test] +fn command_link_alias_to_system_channel() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Test creating an alias to a system channel (release) + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("r") + .arg("+release") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stderr(predicate::str::contains("Channel alias `r` created, pointing to `release`.")); + + // Verify the alias shows up in status + 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("r").and(predicate::str::contains("Alias to `release`"))); +} + +#[test] +fn command_link_alias_invalid_target() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Test creating an alias to a non-existent channel + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("broken") + .arg("+nonexistent") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .failure() + .stderr(predicate::str::contains("Target channel `nonexistent` is not installed")); +} + +#[test] +fn command_link_alias_with_args_fails() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Test that creating an alias with extra arguments fails (the argument parser should reject this) + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("alias_with_args") + .arg("+release") + .arg("--some-arg") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .failure() + .stderr(predicate::str::contains("unexpected argument")); +} + +#[test] +fn command_link_duplicate_channel() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // First add a regular channel + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Try to create an alias with the same name as an existing channel + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("1.10.10") + .arg("+release") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .failure() + .stderr(predicate::str::contains("Channel name `1.10.10` is already used")); +} + +#[test] +fn command_remove_alias() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Create an alias + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("r") + .arg("+release") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Remove the alias + Command::cargo_bin("juliaup") + .unwrap() + .arg("remove") + .arg("r") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .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) + 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("Alias to").not()); +} + +#[test] +fn command_remove_non_existent() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Try to remove a non-existent channel + Command::cargo_bin("juliaup") + .unwrap() + .arg("remove") + .arg("nonexistent") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .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 depot_dir = assert_fs::TempDir::new().unwrap(); + + // Add a channel first + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create an alias to it + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("stable") + .arg("+1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Try to use the alias with julia +alias + Command::cargo_bin("julia") + .unwrap() + .arg("+stable") + .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"); +} + +#[test] +fn alias_as_default() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Add a channel first + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create an alias + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("stable") + .arg("+1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Set the alias as default + Command::cargo_bin("juliaup") + .unwrap() + .arg("default") + .arg("stable") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Test that julia without + uses the alias + Command::cargo_bin("julia") + .unwrap() + .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"); +} + +#[test] +fn alias_chain() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Add a channel first + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create first alias + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("stable") + .arg("+1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create alias to alias + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("prod") + .arg("+stable") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Test that the chained alias works + Command::cargo_bin("julia") + .unwrap() + .arg("+prod") + .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"); + + // Verify both aliases show up in status + 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("stable") + .and(predicate::str::contains("Alias to `1.10.10`")) + .and(predicate::str::contains("prod")) + .and(predicate::str::contains("Alias to `stable`")) + ); +} + +#[test] +fn alias_circular_reference_detection() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Create first alias + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("a") + .arg("+release") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create second alias pointing to first + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("b") + .arg("+a") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Try to create circular reference - should work for creation + Command::cargo_bin("juliaup") + .unwrap() + .arg("remove") + .arg("a") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("a") + .arg("+b") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // But using the circular alias should fail + Command::cargo_bin("julia") + .unwrap() + .arg("+a") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .failure() + .stderr(predicate::str::contains("Channel alias chain too deep")); +} + +#[test] +fn alias_deep_chain_limit() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Create a very deep chain of aliases to test the depth limit + let alias_names = ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10", "a11", "a12"]; + + // Start with a system channel + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg(alias_names[0]) + .arg("+release") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create a chain of aliases + for i in 1..alias_names.len() { + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg(alias_names[i]) + .arg(&format!("+{}", alias_names[i-1])) + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + } + + // Using the deep alias should fail due to depth limit + Command::cargo_bin("julia") + .unwrap() + .arg(&format!("+{}", alias_names[alias_names.len()-1])) + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .failure() + .stderr(predicate::str::contains("Channel alias chain too deep")); +} + +#[test] +fn alias_update_resolves_target() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // First install a Julia version to create an alias to + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create an alias to the installed version + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("r") + .arg("+1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Update through the alias - should work and update the target + Command::cargo_bin("juliaup") + .unwrap() + .arg("update") + .arg("r") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); +} diff --git a/tests/command_update.rs b/tests/command_update.rs index 493029cc3..74d2f6e0c 100644 --- a/tests/command_update.rs +++ b/tests/command_update.rs @@ -25,3 +25,42 @@ fn command_update() { .success() .stdout(""); } + +#[test] +fn command_update_alias_works() { + let depot_dir = tempfile::Builder::new() + .prefix("juliauptest") + .tempdir() + .unwrap(); + + // First install a Julia version to create an alias to + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create an alias to the installed version + Command::cargo_bin("juliaup") + .unwrap() + .arg("link") + .arg("r") + .arg("+1.10.10") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Update the alias - should succeed and update the target + Command::cargo_bin("juliaup") + .unwrap() + .arg("update") + .arg("r") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); +} From cfda6200cde08cd888b4251a37abb5957262f711 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Wed, 27 Aug 2025 11:21:47 -0400 Subject: [PATCH 02/43] rustfmt --- src/bin/julialauncher.rs | 25 ++++++++++------ src/bin/juliaup.rs | 4 ++- src/cli.rs | 7 +++-- src/command_config_backgroundselfupdate.rs | 5 +++- src/command_config_startupselfupdate.rs | 5 +++- src/command_link.rs | 14 +++++++-- src/command_list.rs | 2 +- src/command_status.rs | 2 +- src/command_update.rs | 8 ++++- src/operations.rs | 24 ++++++++------- tests/command_completions_test.rs | 6 +++- tests/command_link.rs | 34 +++++++++++++++------- 12 files changed, 93 insertions(+), 43 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index e786732bf..e4a222a79 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -170,7 +170,10 @@ fn resolve_channel_alias( max_depth: usize, ) -> Result { if max_depth == 0 { - bail!("Channel alias chain too deep for `{}`. There might be a circular reference.", channel); + bail!( + "Channel alias chain too deep for `{}`. There might be a circular reference.", + channel + ); } match config_data.installed_channels.get(channel) { @@ -229,14 +232,15 @@ fn get_julia_path_from_channel( match channel_info { JuliaupConfigChannel::AliasChannel { target: _ } => { // This should not happen after alias resolution, but just in case - bail!("Unexpected alias channel after resolution: {}", resolved_channel); - } - JuliaupConfigChannel::LinkedChannel { command, args } => { - Ok(( - PathBuf::from(command), - args.as_ref().map_or_else(Vec::new, |v| v.clone()), - )) + bail!( + "Unexpected alias channel after resolution: {}", + resolved_channel + ); } + JuliaupConfigChannel::LinkedChannel { command, args } => Ok(( + PathBuf::from(command), + args.as_ref().map_or_else(Vec::new, |v| v.clone()), + )), JuliaupConfigChannel::SystemChannel { version } => { let path = &config_data .installed_versions.get(version) @@ -287,7 +291,10 @@ fn get_julia_path_from_channel( eprintln!(); eprintln!(" juliaup update"); eprintln!(); - eprintln!("to install the latest Julia for the `{}` channel.", resolved_channel); + eprintln!( + "to install the latest Julia for the `{}` channel.", + resolved_channel + ); } } diff --git a/src/bin/juliaup.rs b/src/bin/juliaup.rs index 0b5ee6df7..555822313 100644 --- a/src/bin/juliaup.rs +++ b/src/bin/juliaup.rs @@ -148,6 +148,8 @@ fn main() -> Result<()> { #[cfg(not(feature = "selfupdate"))] SelfSubCmd::Uninstall {} => run_command_selfuninstall_unavailable(), }, - Juliaup::Completions { shell } => generate_completion_for_command::(shell, "juliaup"), + Juliaup::Completions { shell } => { + generate_completion_for_command::(shell, "juliaup") + } } } diff --git a/src/cli.rs b/src/cli.rs index 989d80def..a120b14a9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -28,7 +28,10 @@ pub enum Juliaup { /// Name of the new channel to create channel: String, /// Path to Julia binary, or +CHANNEL to create an alias - #[clap(help = "Path to Julia binary, or +CHANNEL to create an alias (e.g. +release)", value_name = "TARGET")] + #[clap( + help = "Path to Julia binary, or +CHANNEL to create an alias (e.g. +release)", + value_name = "TARGET" + )] file: String, /// Additional arguments for the Julia binary (not used for aliases) args: Vec, @@ -68,7 +71,7 @@ pub enum Juliaup { /// Generate tab-completion scripts for your shell Completions { #[arg(value_enum, value_name = "SHELL")] - shell: CompletionShell + shell: CompletionShell, }, // This is used for the cron jobs that we create. By using this UUID for the command // We can identify the cron jobs that were created by juliaup for uninstall purposes diff --git a/src/command_config_backgroundselfupdate.rs b/src/command_config_backgroundselfupdate.rs index 97f3277c8..dc11bde7d 100644 --- a/src/command_config_backgroundselfupdate.rs +++ b/src/command_config_backgroundselfupdate.rs @@ -63,7 +63,10 @@ pub fn run_command_config_backgroundselfupdate( if !quiet { eprintln!( "Property 'backgroundselfupdateinterval' set to '{}'", - config_file.self_data.background_selfupdate_interval.unwrap_or(0) + config_file + .self_data + .background_selfupdate_interval + .unwrap_or(0) ); } } diff --git a/src/command_config_startupselfupdate.rs b/src/command_config_startupselfupdate.rs index 7af5a5745..422ff3c6d 100644 --- a/src/command_config_startupselfupdate.rs +++ b/src/command_config_startupselfupdate.rs @@ -53,7 +53,10 @@ pub fn run_command_config_startupselfupdate( if !quiet { eprintln!( "Property 'startupselfupdateinterval' set to '{}'", - config_file.self_data.startup_selfupdate_interval.unwrap_or(0) + config_file + .self_data + .startup_selfupdate_interval + .unwrap_or(0) ); } } diff --git a/src/command_link.rs b/src/command_link.rs index fb6127146..6729adfb6 100644 --- a/src/command_link.rs +++ b/src/command_link.rs @@ -35,8 +35,12 @@ pub fn run_command_link( // Remove the + prefix // Validate that the target channel exists or is valid - if !config_file.data.installed_channels.contains_key(target_channel) - && !is_valid_channel(&versiondb_data, &target_channel.to_string())? { + if !config_file + .data + .installed_channels + .contains_key(target_channel) + && !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); } @@ -70,7 +74,11 @@ pub fn run_command_link( }, ); - eprintln!("Channel `{}` linked to `{}`.", channel, absolute_file_path.to_string_lossy()); + eprintln!( + "Channel `{}` linked to `{}`.", + channel, + absolute_file_path.to_string_lossy() + ); } #[cfg(not(windows))] diff --git a/src/command_list.rs b/src/command_list.rs index 1a39e9ea4..b1cdd0402 100644 --- a/src/command_list.rs +++ b/src/command_list.rs @@ -5,8 +5,8 @@ use cli_table::{ format::{Border, HorizontalLine, Separator}, print_stdout, ColorChoice, Table, WithTitle, }; -use numeric_sort::cmp; use itertools::Itertools; +use numeric_sort::cmp; #[derive(Table)] struct ChannelRow { diff --git a/src/command_status.rs b/src/command_status.rs index b166b27c2..2babc0d13 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -10,8 +10,8 @@ use cli_table::{ format::{Border, Justify}, print_stdout, Table, WithTitle, }; -use numeric_sort::cmp; use itertools::Itertools; +use numeric_sort::cmp; #[derive(Table)] struct ChannelRow { diff --git a/src/command_update.rs b/src/command_update.rs index 6ca34c1c9..e34fd82b4 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -178,7 +178,13 @@ pub fn run_command_update(channel: &Option, paths: &GlobalPaths) -> Resu let resolved_channel = resolve_channel_alias(&config_file.data, channel) .with_context(|| format!("Failed to resolve channel '{}'", channel))?; - update_channel(&mut config_file.data, &resolved_channel, &version_db, false, paths)?; + update_channel( + &mut config_file.data, + &resolved_channel, + &version_db, + false, + paths, + )?; } }; diff --git a/src/operations.rs b/src/operations.rs index 00a93b262..0f2927b66 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -797,9 +797,10 @@ pub fn garbage_collect_versions( let mut channels_to_uninstall: Vec = Vec::new(); for (installed_channel, detail) in &config_data.installed_channels { if let JuliaupConfigChannel::LinkedChannel { - command: cmd, - args: _, - } = &detail { + command: cmd, + args: _, + } = &detail + { if !is_valid_julia_path(&PathBuf::from(cmd)) { channels_to_uninstall.push(installed_channel.clone()); } @@ -1448,12 +1449,13 @@ pub fn update_version_db(channel: &Option, paths: &GlobalPaths) -> Resul .unwrap(); if let JuliaupConfigChannel::DirectDownloadChannel { - path, - url, - local_etag, - server_etag: _, - version, - } = channel_data { + path, + url, + local_etag, + server_etag: _, + version, + } = channel_data + { if let Some(etag) = etag { new_config_file.data.installed_channels.insert( channel, @@ -1537,7 +1539,7 @@ fn download_direct_download_etags( let mut requests = Vec::new(); for (channel_name, installed_channel) in &config_data.installed_channels { - if let Some(chan) = channel{ + if let Some(chan) = channel { // TODO: convert to an if-let chain once stabilized https://github.com/rust-lang/rust/pull/132833 if chan != channel_name { continue; @@ -1608,7 +1610,7 @@ fn download_direct_download_etags( let mut requests = Vec::new(); for (channel_name, installed_channel) in &config_data.installed_channels { - if let Some(chan) = channel{ + if let Some(chan) = channel { // TODO: convert to an if-let chain once stabilized https://github.com/rust-lang/rust/pull/132833 if chan != channel_name { continue; diff --git a/tests/command_completions_test.rs b/tests/command_completions_test.rs index a4d876519..60eb129f5 100644 --- a/tests/command_completions_test.rs +++ b/tests/command_completions_test.rs @@ -50,6 +50,10 @@ fn completions_elvish() { fn completions_nushell() { test_shell_completion( "nushell", - &["module completions", "export extern juliaup", "export use completions"], + &[ + "module completions", + "export extern juliaup", + "export use completions", + ], ); } diff --git a/tests/command_link.rs b/tests/command_link.rs index 89351a4a6..e2ebd690c 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -41,7 +41,7 @@ fn command_link_binary() { fn command_link_alias() { let depot_dir = assert_fs::TempDir::new().unwrap(); - // First install a Julia version to create an alias to + // First install a Julia version to create an alias to Command::cargo_bin("juliaup") .unwrap() .arg("add") @@ -61,7 +61,9 @@ fn command_link_alias() { .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() - .stderr(predicate::str::contains("Channel alias `stable` created, pointing to `1.10.10`.")); + .stderr(predicate::str::contains( + "Channel alias `stable` created, pointing to `1.10.10`.", + )); // Verify the alias shows up in status Command::cargo_bin("juliaup") @@ -72,7 +74,7 @@ fn command_link_alias() { .assert() .success() .stdout( - predicate::str::contains("stable").and(predicate::str::contains("Alias to `1.10.10`")) + predicate::str::contains("stable").and(predicate::str::contains("Alias to `1.10.10`")), ); } @@ -90,7 +92,9 @@ fn command_link_alias_to_system_channel() { .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() - .stderr(predicate::str::contains("Channel alias `r` created, pointing to `release`.")); + .stderr(predicate::str::contains( + "Channel alias `r` created, pointing to `release`.", + )); // Verify the alias shows up in status Command::cargo_bin("juliaup") @@ -117,7 +121,9 @@ fn command_link_alias_invalid_target() { .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .failure() - .stderr(predicate::str::contains("Target channel `nonexistent` is not installed")); + .stderr(predicate::str::contains( + "Target channel `nonexistent` is not installed", + )); } #[test] @@ -162,7 +168,9 @@ fn command_link_duplicate_channel() { .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .failure() - .stderr(predicate::str::contains("Channel name `1.10.10` is already used")); + .stderr(predicate::str::contains( + "Channel name `1.10.10` is already used", + )); } #[test] @@ -189,7 +197,9 @@ fn command_remove_alias() { .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() - .stderr(predicate::str::contains("Julia alias (pointing to 'release') 'r' successfully removed.")); + .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) Command::cargo_bin("juliaup") @@ -363,7 +373,7 @@ fn alias_chain() { predicate::str::contains("stable") .and(predicate::str::contains("Alias to `1.10.10`")) .and(predicate::str::contains("prod")) - .and(predicate::str::contains("Alias to `stable`")) + .and(predicate::str::contains("Alias to `stable`")), ); } @@ -431,7 +441,9 @@ fn alias_deep_chain_limit() { let depot_dir = assert_fs::TempDir::new().unwrap(); // Create a very deep chain of aliases to test the depth limit - let alias_names = ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10", "a11", "a12"]; + let alias_names = [ + "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10", "a11", "a12", + ]; // Start with a system channel Command::cargo_bin("juliaup") @@ -450,7 +462,7 @@ fn alias_deep_chain_limit() { .unwrap() .arg("link") .arg(alias_names[i]) - .arg(&format!("+{}", alias_names[i-1])) + .arg(&format!("+{}", alias_names[i - 1])) .env("JULIA_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() @@ -460,7 +472,7 @@ fn alias_deep_chain_limit() { // Using the deep alias should fail due to depth limit Command::cargo_bin("julia") .unwrap() - .arg(&format!("+{}", alias_names[alias_names.len()-1])) + .arg(&format!("+{}", alias_names[alias_names.len() - 1])) .arg("-e") .arg("print(VERSION)") .env("JULIA_DEPOT_PATH", depot_dir.path()) From 34203324abf44cd743a95257d49843e629d5ba36 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Wed, 27 Aug 2025 11:24:01 -0400 Subject: [PATCH 03/43] clippy fixes --- tests/command_link.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/command_link.rs b/tests/command_link.rs index e2ebd690c..bc376adcb 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -462,7 +462,7 @@ fn alias_deep_chain_limit() { .unwrap() .arg("link") .arg(alias_names[i]) - .arg(&format!("+{}", alias_names[i - 1])) + .arg(format!("+{}", alias_names[i - 1])) .env("JULIA_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() @@ -472,7 +472,7 @@ fn alias_deep_chain_limit() { // Using the deep alias should fail due to depth limit Command::cargo_bin("julia") .unwrap() - .arg(&format!("+{}", alias_names[alias_names.len() - 1])) + .arg(format!("+{}", alias_names[alias_names.len() - 1])) .arg("-e") .arg("print(VERSION)") .env("JULIA_DEPOT_PATH", depot_dir.path()) From 98accedece0a56da8b0d3838a47c3f383b8a4f7c Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Wed, 27 Aug 2025 11:56:44 -0400 Subject: [PATCH 04/43] make the noop GC operation clearer --- src/operations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/operations.rs b/src/operations.rs index 0f2927b66..211099973 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -785,7 +785,7 @@ 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); From 1baab2534e6f961bb2af4d24e355c08db684767c Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Wed, 27 Aug 2025 11:59:07 -0400 Subject: [PATCH 05/43] Update src/operations.rs Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/operations.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/operations.rs b/src/operations.rs index 211099973..d8e953bad 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -785,7 +785,10 @@ pub fn garbage_collect_versions( } if versions_to_uninstall.is_empty() { - eprintln!("{} No unused Julia installations to clean up.", style("GC").cyan().bold()); + 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); From a358beb453d630604c659d1a3e536258b3c31b93 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 11:25:38 -0400 Subject: [PATCH 06/43] address review --- build.rs | 5 +- src/bin/julialauncher.rs | 61 ++++------ src/bin/juliaup.rs | 4 +- src/cli.rs | 11 +- src/command_api.rs | 39 ++----- src/command_link.rs | 28 +++-- src/command_update.rs | 40 ++----- src/operations.rs | 2 +- tests/command_link.rs | 239 +++++++-------------------------------- tests/command_update.rs | 32 ++---- 10 files changed, 113 insertions(+), 348 deletions(-) diff --git a/build.rs b/build.rs index 81ef1bb2e..a6c908389 100644 --- a/build.rs +++ b/build.rs @@ -21,7 +21,7 @@ fn main() -> Result<()> { let db_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()) .join("versiondb") - .join(format!("versiondb-{target_platform}.json")); + .join(format!("versiondb-{}.json", target_platform)); let version_db_path = out_path.join("versionsdb.json"); std::fs::copy(&db_path, &version_db_path).unwrap(); @@ -35,7 +35,8 @@ fn main() -> Result<()> { std::fs::write( &bundled_version_path, format!( - "pub const BUNDLED_JULIA_VERSION: &str = {bundled_version_as_string}; pub const BUNDLED_DB_VERSION: &str = {bundled_dbversion_as_string};" + "pub const BUNDLED_JULIA_VERSION: &str = {}; pub const BUNDLED_DB_VERSION: &str = {};", + bundled_version_as_string, bundled_dbversion_as_string ), ) .unwrap(); diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index fdd6fc696..66774781c 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -325,22 +325,9 @@ enum JuliaupChannelSource { Default, } -fn resolve_channel_alias( - config_data: &JuliaupConfig, - channel: &str, - max_depth: usize, -) -> Result { - if max_depth == 0 { - bail!( - "Channel alias chain too deep for `{}`. There might be a circular reference.", - channel - ); - } - +fn resolve_channel_alias(config_data: &JuliaupConfig, channel: &str) -> Result { match config_data.installed_channels.get(channel) { - Some(JuliaupConfigChannel::AliasChannel { target }) => { - resolve_channel_alias(config_data, target, max_depth - 1) - } + Some(JuliaupConfigChannel::AliasChannel { target }) => Ok(target.to_string()), _ => Ok(channel.to_string()), } } @@ -354,7 +341,7 @@ fn get_julia_path_from_channel( paths: &juliaup::global_paths::GlobalPaths, ) -> Result<(PathBuf, Vec)> { // First resolve any aliases - let resolved_channel = resolve_channel_alias(config_data, channel, 10)?; + let resolved_channel = resolve_channel_alias(config_data, channel)?; let channel_valid = is_valid_channel(versions_db, &resolved_channel)?; @@ -394,11 +381,12 @@ fn get_julia_path_from_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 + let updated_channel_info = updated_config_file .data .installed_channels - .get(&resolved_channel) - { + .get(&resolved_channel); + + if let Some(channel_info) = updated_channel_info { return get_julia_path_from_installed_channel( versions_db, &updated_config_file.data, @@ -408,8 +396,7 @@ fn get_julia_path_from_channel( ); } else { return Err(anyhow!( - "Channel '{}' was installed but could not be found in configuration.", - resolved_channel + "Channel '{resolved_channel}' was installed but could not be found in configuration." )); } } @@ -421,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.", resolved_channel, resolved_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!("`{}` is not installed. Please run `juliaup add {}` to install pull request channel if available.", resolved_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.", resolved_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.", resolved_channel, resolved_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!("`{}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install pull request channel if available.", resolved_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.", resolved_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.", resolved_channel, resolved_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!("`{}` from directory override is not installed. Please run `juliaup add {}` to install pull request channel if available.", resolved_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.", resolved_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.", resolved_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()) @@ -460,9 +447,8 @@ fn get_julia_path_from_installed_channel( channel_info: &JuliaupConfigChannel, ) -> Result<(PathBuf, Vec)> { match channel_info { - JuliaupConfigChannel::AliasChannel { target: _ } => { - // This should not happen after alias resolution, but just in case - bail!("Unexpected alias channel after resolution: {}", channel); + JuliaupConfigChannel::AliasChannel { .. } => { + bail!("Unexpected alias channel after resolution: {channel}"); } JuliaupConfigChannel::LinkedChannel { command, args } => Ok(( PathBuf::from(command), @@ -471,13 +457,10 @@ fn get_julia_path_from_installed_channel( 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() 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 8036792cc..5cf5342b9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -23,16 +23,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, or create a channel alias + /// Link an existing Julia binary or channel to a custom channel name Link { /// Name of the new channel to create channel: String, - /// Path to Julia binary, or +CHANNEL to create an alias - #[clap( - help = "Path to Julia binary, or +CHANNEL to create an alias (e.g. +release)", - value_name = "TARGET" - )] - file: String, + /// Path to Julia binary, or +CHANNEL to create an alias (e.g. +release) + #[clap(value_name = "TARGET")] + target: String, /// Additional arguments for the Julia binary (not used for aliases) args: Vec, }, diff --git a/src/command_api.rs b/src/command_api.rs index 21b655954..db970e836 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -46,29 +46,13 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { for (key, value) in &config_file.data.installed_channels { let curr = match &value { JuliaupConfigChannel::AliasChannel { target } => { - // For aliases, we need to resolve to the target and get its info - // Skip if the target doesn't exist to avoid infinite recursion - if let Some(target_channel) = config_file.data.installed_channels.get(target) { - match target_channel { - JuliaupConfigChannel::AliasChannel { .. } => { - // Avoid infinite recursion for alias-to-alias - continue; - } - _ => { - // Recursively get info for the target - // For now, just indicate it's an alias - JuliaupChannelInfo { - name: key.clone(), - file: format!("alias-to-{}", target), - args: Vec::new(), - version: format!("alias to {}", target), - arch: "".to_string(), - } - } - } - } else { - // Target doesn't exist, skip this alias - continue; + // Since we no longer support alias-to-alias chains, this is simpler + JuliaupChannelInfo { + name: key.clone(), + file: format!("alias-to-{target}"), + args: Vec::new(), + version: format!("alias to {target}"), + arch: String::new(), } } JuliaupConfigChannel::SystemChannel { version: fullversion } => { @@ -97,12 +81,7 @@ 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_or(&Vec::new()) { - new_args.push(i.to_string()); - } - + let mut new_args = args.as_ref().unwrap_or(&Vec::new()).clone(); new_args.push("--version".to_string()); let res = std::process::Command::new(command) @@ -127,7 +106,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { file: command.clone(), args: args.as_ref().unwrap_or(&Vec::new()).clone(), version: version.to_string(), - arch: "".to_string(), + arch: String::new(), } } Err(_) => continue, diff --git a/src/command_link.rs b/src/command_link.rs index 6729adfb6..4934fe6a3 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<()> { @@ -31,16 +31,14 @@ pub fn run_command_link( } // Check if this is a channel alias (starts with +) - if let Some(target_channel) = file.strip_prefix('+') { - // Remove the + prefix - - // Validate that the target channel exists or is valid - if !config_file - .data - .installed_channels - .contains_key(target_channel) - && !is_valid_channel(&versiondb_data, &target_channel.to_string())? - { + 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); } @@ -58,9 +56,9 @@ pub fn run_command_link( eprintln!("Channel alias `{channel}` created, pointing to `{target_channel}`."); } else { // Original behavior for linking to binary files - let absolute_file_path = Path::new(file) + let absolute_file_path = Path::new(target) .absolutize() - .with_context(|| format!("Failed to convert path `{file}` to absolute path."))?; + .with_context(|| format!("Failed to convert path `{target}` to absolute path."))?; 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); @@ -88,11 +86,11 @@ pub fn run_command_link( .with_context(|| "`link` command failed to save configuration db.")?; #[cfg(not(windows))] - if create_symlinks && !file.starts_with('+') { + if create_symlinks && !target.starts_with('+') { // Only create symlinks for binary links, not channel aliases create_symlink( &JuliaupConfigChannel::LinkedChannel { - command: file.to_string(), + command: target.to_string(), args: Some(args.to_vec()), }, &format!("julia-{channel}"), diff --git a/src/command_update.rs b/src/command_update.rs index e34fd82b4..51480068b 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -12,26 +12,10 @@ use console::style; use std::path::PathBuf; fn resolve_channel_alias(config_db: &JuliaupConfig, channel_name: &str) -> Result { - let mut current_channel = channel_name; - let mut visited = std::collections::HashSet::new(); - - loop { - if visited.contains(current_channel) { - bail!("Channel alias chain contains a cycle: {}", channel_name); - } - visited.insert(current_channel.to_string()); - - if visited.len() > 10 { - bail!("Channel alias chain too deep: {}", channel_name); - } - - match config_db.installed_channels.get(current_channel) { - Some(JuliaupConfigChannel::AliasChannel { target }) => { - current_channel = target; - } - Some(_) => return Ok(current_channel.to_string()), - None => bail!("Channel '{}' not found", current_channel), - } + 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), } } @@ -99,12 +83,8 @@ fn update_channel( ); } } - JuliaupConfigChannel::AliasChannel { target: _ } => { - // This should never happen since we resolve aliases before calling this function - bail!( - "Internal error: Tried to update an alias channel '{}' directly.", - channel - ); + JuliaupConfigChannel::AliasChannel { .. } => { + bail!("Internal error: Tried to update an alias channel '{channel}' directly."); } JuliaupConfigChannel::DirectDownloadChannel { path, @@ -117,11 +97,10 @@ fn update_channel( // 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 + "Channel {channel} version is empty, you may need to manually codesign this channel if you trust the contents of this pull request." ); } - eprintln!("{} channel {}", style("Updating").green().bold(), channel); + eprintln!("{} channel {channel}", style("Updating").green().bold()); let channel_data = install_from_url(&url::Url::parse(url)?, &PathBuf::from(path), paths)?; @@ -175,8 +154,7 @@ pub fn run_command_update(channel: &Option, paths: &GlobalPaths) -> Resu } // Resolve any aliases to get the actual target channel - let resolved_channel = resolve_channel_alias(&config_file.data, channel) - .with_context(|| format!("Failed to resolve channel '{}'", channel))?; + let resolved_channel = resolve_channel_alias(&config_file.data, channel)?; update_channel( &mut config_file.data, diff --git a/src/operations.rs b/src/operations.rs index 66cf283ae..7fb44f63b 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -788,7 +788,7 @@ pub fn garbage_collect_versions( if versions_to_uninstall.is_empty() { eprintln!( - "{} No unused Julia installations to clean up.", + "{}: No unused Julia installations to clean up.", style("GC").cyan().bold() ); } else { diff --git a/tests/command_link.rs b/tests/command_link.rs index bc376adcb..c231f4ff6 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -1,37 +1,42 @@ use assert_cmd::Command; use predicates::prelude::*; +fn juliaup_command(depot_dir: &assert_fs::TempDir) -> Command { + let mut cmd = Command::cargo_bin("juliaup").unwrap(); + cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()); + cmd +} + +fn julia_command(depot_dir: &assert_fs::TempDir) -> Command { + let mut cmd = Command::cargo_bin("julia").unwrap(); + cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()); + cmd +} + #[test] fn command_link_binary() { let depot_dir = assert_fs::TempDir::new().unwrap(); // First add a regular channel for testing - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("add") .arg("1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Test linking to a binary file (existing functionality) - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("link") .arg("custom") .arg("/usr/bin/false") // Use a binary that exists but won't work as Julia - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Verify the link shows up in status - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(predicate::str::contains("custom").and(predicate::str::contains("Linked to"))); @@ -42,23 +47,17 @@ fn command_link_alias() { let depot_dir = assert_fs::TempDir::new().unwrap(); // First install a Julia version to create an alias to - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("add") .arg("1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Create an alias to the installed version - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("link") .arg("stable") .arg("+1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stderr(predicate::str::contains( @@ -66,11 +65,8 @@ fn command_link_alias() { )); // Verify the alias shows up in status - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout( @@ -83,13 +79,10 @@ fn command_link_alias_to_system_channel() { let depot_dir = assert_fs::TempDir::new().unwrap(); // Test creating an alias to a system channel (release) - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("link") .arg("r") .arg("+release") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stderr(predicate::str::contains( @@ -97,11 +90,8 @@ fn command_link_alias_to_system_channel() { )); // Verify the alias shows up in status - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("status") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(predicate::str::contains("r").and(predicate::str::contains("Alias to `release`"))); @@ -112,13 +102,10 @@ fn command_link_alias_invalid_target() { let depot_dir = assert_fs::TempDir::new().unwrap(); // Test creating an alias to a non-existent channel - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("link") .arg("broken") .arg("+nonexistent") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .failure() .stderr(predicate::str::contains( @@ -254,13 +241,10 @@ fn alias_resolution_julia_launcher() { .success(); // Try to use the alias with julia +alias - Command::cargo_bin("julia") - .unwrap() + julia_command(&depot_dir) .arg("+stable") .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"); @@ -302,218 +286,71 @@ fn alias_as_default() { .success(); // Test that julia without + uses the alias - Command::cargo_bin("julia") - .unwrap() + julia_command(&depot_dir) .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"); } #[test] -fn alias_chain() { +fn alias_to_alias_prevented() { let depot_dir = assert_fs::TempDir::new().unwrap(); // Add a channel first - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("add") .arg("1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Create first alias - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("link") .arg("stable") .arg("+1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); - // Create alias to alias - Command::cargo_bin("juliaup") - .unwrap() + // Try to create alias to alias - should now fail + juliaup_command(&depot_dir) .arg("link") .arg("prod") .arg("+stable") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success(); - - // Test that the chained alias works - Command::cargo_bin("julia") - .unwrap() - .arg("+prod") - .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"); - - // Verify both aliases show up in status - 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("stable") - .and(predicate::str::contains("Alias to `1.10.10`")) - .and(predicate::str::contains("prod")) - .and(predicate::str::contains("Alias to `stable`")), - ); -} - -#[test] -fn alias_circular_reference_detection() { - let depot_dir = assert_fs::TempDir::new().unwrap(); - - // Create first alias - Command::cargo_bin("juliaup") - .unwrap() - .arg("link") - .arg("a") - .arg("+release") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success(); - - // Create second alias pointing to first - Command::cargo_bin("juliaup") - .unwrap() - .arg("link") - .arg("b") - .arg("+a") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success(); - - // Try to create circular reference - should work for creation - Command::cargo_bin("juliaup") - .unwrap() - .arg("remove") - .arg("a") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success(); - - Command::cargo_bin("juliaup") - .unwrap() - .arg("link") - .arg("a") - .arg("+b") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success(); - - // But using the circular alias should fail - Command::cargo_bin("julia") - .unwrap() - .arg("+a") - .arg("-e") - .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .failure() - .stderr(predicate::str::contains("Channel alias chain too deep")); + .stderr(predicate::str::contains( + "Cannot create an alias to another alias `stable`", + )); } -#[test] -fn alias_deep_chain_limit() { - let depot_dir = assert_fs::TempDir::new().unwrap(); - - // Create a very deep chain of aliases to test the depth limit - let alias_names = [ - "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10", "a11", "a12", - ]; - - // Start with a system channel - Command::cargo_bin("juliaup") - .unwrap() - .arg("link") - .arg(alias_names[0]) - .arg("+release") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success(); - - // Create a chain of aliases - for i in 1..alias_names.len() { - Command::cargo_bin("juliaup") - .unwrap() - .arg("link") - .arg(alias_names[i]) - .arg(format!("+{}", alias_names[i - 1])) - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .success(); - } - - // Using the deep alias should fail due to depth limit - Command::cargo_bin("julia") - .unwrap() - .arg(format!("+{}", alias_names[alias_names.len() - 1])) - .arg("-e") - .arg("print(VERSION)") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) - .assert() - .failure() - .stderr(predicate::str::contains("Channel alias chain too deep")); -} +// 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 depot_dir = assert_fs::TempDir::new().unwrap(); // First install a Julia version to create an alias to - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("add") .arg("1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Create an alias to the installed version - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("link") .arg("r") .arg("+1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Update through the alias - should work and update the target - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("update") .arg("r") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); } diff --git a/tests/command_update.rs b/tests/command_update.rs index 74d2f6e0c..6ebb6fa28 100644 --- a/tests/command_update.rs +++ b/tests/command_update.rs @@ -1,5 +1,12 @@ use assert_cmd::Command; +fn juliaup_command(depot_dir: &tempfile::TempDir) -> Command { + let mut cmd = Command::cargo_bin("juliaup").unwrap(); + cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()); + cmd +} + #[test] fn command_update() { let depot_dir = tempfile::Builder::new() @@ -7,20 +14,14 @@ fn command_update() { .tempdir() .unwrap(); - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("update") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("up") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stdout(""); @@ -34,33 +35,24 @@ fn command_update_alias_works() { .unwrap(); // First install a Julia version to create an alias to - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("add") .arg("1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Create an alias to the installed version - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("link") .arg("r") .arg("+1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Update the alias - should succeed and update the target - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .arg("update") .arg("r") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); } From a2862794ed526be04d71001763ec25527e29fde5 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 11:30:39 -0400 Subject: [PATCH 07/43] break match into functions --- src/operations.rs | 240 ++++++++++++++++++++++++++-------------------- 1 file changed, 137 insertions(+), 103 deletions(-) diff --git a/src/operations.rs b/src/operations.rs index 7fb44f63b..92811e32c 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -848,6 +848,138 @@ pub fn remove_symlink(symlink_name: &String) -> Result<()> { Ok(()) } +fn create_alias_symlink() -> Result<()> { + // Aliases don't create symlinks directly, they are resolved at runtime + Ok(()) +} + +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() + ) + }) +} + +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() + ) + }) +} + +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, @@ -858,42 +990,14 @@ 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::AliasChannel { target: _ } => { - // Aliases don't create symlinks directly, they are resolved at runtime - return Ok(()); + create_alias_symlink() } 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, @@ -902,82 +1006,12 @@ pub fn create_symlink( 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() - ) - })?; + create_direct_download_symlink(path, version, symlink_name, &symlink_path, paths, &updating) } 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( - &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() - ) - })?; + create_linked_channel_shim(command, args, symlink_name, &symlink_path, &updating) } - }; + }?; if updating.is_none() { if let Ok(path) = std::env::var("PATH") { From 9a7c4864ac5f9d3bc2d6f3331aa619ba4622e95f Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 11:33:05 -0400 Subject: [PATCH 08/43] fmt --- src/operations.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/operations.rs b/src/operations.rs index 92811e32c..6d834f30f 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -880,13 +880,14 @@ fn create_system_channel_symlink( ); } - std::os::unix::fs::symlink(target_path.join("bin").join("julia"), symlink_path) - .with_context(|| { + 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() ) - }) + }, + ) } fn create_direct_download_symlink( @@ -916,13 +917,14 @@ fn create_direct_download_symlink( ); } - std::os::unix::fs::symlink(target_path.join("bin").join("julia"), symlink_path) - .with_context(|| { + 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() ) - }) + }, + ) } fn create_linked_channel_shim( @@ -993,9 +995,7 @@ pub fn create_symlink( let updating = _remove_symlink(&symlink_path)?; match channel { - JuliaupConfigChannel::AliasChannel { target: _ } => { - create_alias_symlink() - } + JuliaupConfigChannel::AliasChannel { target: _ } => create_alias_symlink(), JuliaupConfigChannel::SystemChannel { version } => { create_system_channel_symlink(version, symlink_name, &symlink_path, paths, &updating) } @@ -1005,9 +1005,14 @@ pub fn create_symlink( local_etag: _, server_etag: _, version, - } => { - create_direct_download_symlink(path, version, symlink_name, &symlink_path, paths, &updating) - } + } => create_direct_download_symlink( + path, + version, + symlink_name, + &symlink_path, + paths, + &updating, + ), JuliaupConfigChannel::LinkedChannel { command, args } => { create_linked_channel_shim(command, args, symlink_name, &symlink_path, &updating) } From 687d3f4c08d03520ffb7249de2483209bb2aeb08 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 11:37:31 -0400 Subject: [PATCH 09/43] fixes --- src/command_api.rs | 4 ++-- src/operations.rs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/command_api.rs b/src/command_api.rs index db970e836..66d115d22 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -81,7 +81,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { } } JuliaupConfigChannel::LinkedChannel { command, args } => { - let mut new_args = args.as_ref().unwrap_or(&Vec::new()).clone(); + let mut new_args = args.as_deref().unwrap_or_default().to_vec(); new_args.push("--version".to_string()); let res = std::process::Command::new(command) @@ -104,7 +104,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { JuliaupChannelInfo { name: key.clone(), file: command.clone(), - args: args.as_ref().unwrap_or(&Vec::new()).clone(), + args: args.as_deref().unwrap_or_default().to_vec(), version: version.to_string(), arch: String::new(), } diff --git a/src/operations.rs b/src/operations.rs index 6d834f30f..ee13e8187 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -848,11 +848,13 @@ pub fn remove_symlink(symlink_name: &String) -> Result<()> { Ok(()) } +#[cfg(not(windows))] fn create_alias_symlink() -> Result<()> { // Aliases don't create symlinks directly, they are resolved at runtime Ok(()) } +#[cfg(not(windows))] fn create_system_channel_symlink( version: &str, symlink_name: &str, @@ -890,6 +892,7 @@ fn create_system_channel_symlink( ) } +#[cfg(not(windows))] fn create_direct_download_symlink( path: &str, version: &str, @@ -927,6 +930,7 @@ fn create_direct_download_symlink( ) } +#[cfg(not(windows))] fn create_linked_channel_shim( command: &str, args: &Option>, From 152d070f63e43a8578f69d53e893b00be8a7f997 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 11:41:11 -0400 Subject: [PATCH 10/43] switch to `update: Option` --- src/command_status.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index 2babc0d13..61a4b55e2 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -95,19 +95,19 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { match versiondb_data.available_channels.get(i.0) { Some(channel) => { if &channel.version != version { - format!("Update to {} available", channel.version) + Some(format!("Update to {} available", channel.version)) } else { - "".to_string() + None } } - None => "".to_string(), + None => None, } } JuliaupConfigChannel::LinkedChannel { command: _, args: _, - } => "".to_string(), - JuliaupConfigChannel::AliasChannel { target: _ } => "".to_string(), + } => None, + JuliaupConfigChannel::AliasChannel { target: _ } => None, JuliaupConfigChannel::DirectDownloadChannel { path: _, url: _, @@ -116,9 +116,9 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { version: _, } => { if local_etag != server_etag { - "Update available".to_string() + Some("Update available".to_string()) } else { - "".to_string() + None } } }, From 49c65f38d3fb32584829880260b374c5926184c4 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 11:43:09 -0400 Subject: [PATCH 11/43] Update command_status.rs --- src/command_status.rs | 57 +++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index 61a4b55e2..c393200e2 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -90,37 +90,40 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { format!("Alias to `{target}`") } }, - update: match i.1 { - JuliaupConfigChannel::SystemChannel { version } => { - match versiondb_data.available_channels.get(i.0) { - Some(channel) => { - if &channel.version != version { - Some(format!("Update to {} available", channel.version)) - } else { - None + update: { + let update_option = match i.1 { + JuliaupConfigChannel::SystemChannel { version } => { + match versiondb_data.available_channels.get(i.0) { + Some(channel) => { + if &channel.version != version { + Some(format!("Update to {} available", channel.version)) + } else { + None + } } + None => None, } - None => None, } - } - JuliaupConfigChannel::LinkedChannel { - command: _, - args: _, - } => None, - JuliaupConfigChannel::AliasChannel { target: _ } => None, - JuliaupConfigChannel::DirectDownloadChannel { - path: _, - url: _, - local_etag, - server_etag, - version: _, - } => { - if local_etag != server_etag { - Some("Update available".to_string()) - } else { - None + JuliaupConfigChannel::LinkedChannel { + command: _, + args: _, + } => None, + JuliaupConfigChannel::AliasChannel { target: _ } => None, + JuliaupConfigChannel::DirectDownloadChannel { + path: _, + url: _, + local_etag, + server_etag, + version: _, + } => { + if local_etag != server_etag { + Some("Update available".to_string()) + } else { + None + } } - } + }; + update_option.unwrap_or_default() }, } }) From 804d717624a6288289f6de96b9b0197381ade66a Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 11:45:28 -0400 Subject: [PATCH 12/43] also show if aliases have updates --- src/command_status.rs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/command_status.rs b/src/command_status.rs index c393200e2..2f9754894 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -108,7 +108,44 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { command: _, args: _, } => None, - JuliaupConfigChannel::AliasChannel { target: _ } => None, + JuliaupConfigChannel::AliasChannel { target } => { + // Check if the target channel has updates available + match config_file.data.installed_channels.get(target) { + Some(target_channel) => match target_channel { + JuliaupConfigChannel::SystemChannel { version } => { + match versiondb_data.available_channels.get(target) { + Some(channel) => { + if &channel.version != version { + Some(format!( + "Update to {} available", + channel.version + )) + } else { + None + } + } + None => None, + } + } + JuliaupConfigChannel::DirectDownloadChannel { + path: _, + url: _, + local_etag, + server_etag, + version: _, + } => { + if local_etag != server_etag { + Some("Update available".to_string()) + } else { + None + } + } + // LinkedChannels and nested aliases don't have updates + _ => None, + }, + None => None, // Target channel doesn't exist + } + } JuliaupConfigChannel::DirectDownloadChannel { path: _, url: _, From 47900912c2b1724fc0643e1e73e39774b126a710 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 11:51:43 -0400 Subject: [PATCH 13/43] fix "sketchy" issue --- src/command_link.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/command_link.rs b/src/command_link.rs index 4934fe6a3..2a50d45ba 100644 --- a/src/command_link.rs +++ b/src/command_link.rs @@ -67,7 +67,7 @@ pub fn run_command_link( config_file.data.installed_channels.insert( channel.to_string(), JuliaupConfigChannel::LinkedChannel { - command: absolute_file_path.to_string_lossy().to_string(), + command: absolute_file_path.to_string_lossy().into_owned(), args: Some(args.to_vec()), }, ); @@ -88,9 +88,14 @@ pub fn run_command_link( #[cfg(not(windows))] if create_symlinks && !target.starts_with('+') { // Only create symlinks for binary links, not channel aliases + // We need to recreate the absolute path since it goes out of scope + let absolute_file_path = Path::new(target) + .absolutize() + .with_context(|| format!("Failed to convert path `{target}` to absolute path."))?; + create_symlink( &JuliaupConfigChannel::LinkedChannel { - command: target.to_string(), + command: absolute_file_path.to_string_lossy().into_owned(), args: Some(args.to_vec()), }, &format!("julia-{channel}"), From 3626e2255db292c0cb27f66ef899362a1ab6cf59 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 12:42:53 -0400 Subject: [PATCH 14/43] Apply suggestions from code review Co-authored-by: Miles Cranmer --- src/cli.rs | 1 - src/command_api.rs | 5 ++--- src/command_link.rs | 11 +++-------- tests/command_link.rs | 1 + 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 3174d35ef..3e3e126aa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -30,7 +30,6 @@ pub enum Juliaup { /// Name of the new channel to create channel: String, /// Path to Julia binary, or +CHANNEL to create an alias (e.g. +release) - #[clap(value_name = "TARGET")] target: String, /// Additional arguments for the Julia binary (not used for aliases) args: Vec, diff --git a/src/command_api.rs b/src/command_api.rs index 66d115d22..3025b731d 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -46,7 +46,6 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { for (key, value) in &config_file.data.installed_channels { let curr = match &value { JuliaupConfigChannel::AliasChannel { target } => { - // Since we no longer support alias-to-alias chains, this is simpler JuliaupChannelInfo { name: key.clone(), file: format!("alias-to-{target}"), @@ -81,7 +80,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { } } JuliaupConfigChannel::LinkedChannel { command, args } => { - let mut new_args = args.as_deref().unwrap_or_default().to_vec(); + let mut new_args = args.clone().unwrap_or_default(); new_args.push("--version".to_string()); let res = std::process::Command::new(command) @@ -104,7 +103,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { JuliaupChannelInfo { name: key.clone(), file: command.clone(), - args: args.as_deref().unwrap_or_default().to_vec(), + args: args.clone().unwrap_or_default(), version: version.to_string(), arch: String::new(), } diff --git a/src/command_link.rs b/src/command_link.rs index 2a50d45ba..673a387c0 100644 --- a/src/command_link.rs +++ b/src/command_link.rs @@ -55,9 +55,7 @@ pub fn run_command_link( eprintln!("Channel alias `{channel}` created, pointing to `{target_channel}`."); } else { - // Original behavior for linking to binary files - let absolute_file_path = Path::new(target) - .absolutize() + let absolute_file_path = std::fs::canonicalize(target) .with_context(|| format!("Failed to convert path `{target}` to absolute path."))?; if !is_valid_julia_path(&absolute_file_path.to_path_buf()) { @@ -67,7 +65,7 @@ pub fn run_command_link( config_file.data.installed_channels.insert( channel.to_string(), JuliaupConfigChannel::LinkedChannel { - command: absolute_file_path.to_string_lossy().into_owned(), + command: absolute_file_path.to_string_lossy().to_string(), args: Some(args.to_vec()), }, ); @@ -87,10 +85,7 @@ pub fn run_command_link( #[cfg(not(windows))] if create_symlinks && !target.starts_with('+') { - // Only create symlinks for binary links, not channel aliases - // We need to recreate the absolute path since it goes out of scope - let absolute_file_path = Path::new(target) - .absolutize() + let absolute_file_path = std::fs::canonicalize(target) .with_context(|| format!("Failed to convert path `{target}` to absolute path."))?; create_symlink( diff --git a/tests/command_link.rs b/tests/command_link.rs index c231f4ff6..cfff747f8 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -1,4 +1,5 @@ use assert_cmd::Command; +use assert_fs::TempDir; use predicates::prelude::*; fn juliaup_command(depot_dir: &assert_fs::TempDir) -> Command { From fa508872c366921546d6a477180b71cd12064ce7 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 13:03:44 -0400 Subject: [PATCH 15/43] undos --- src/command_link.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/command_link.rs b/src/command_link.rs index 673a387c0..1ec5b8c52 100644 --- a/src/command_link.rs +++ b/src/command_link.rs @@ -55,7 +55,8 @@ pub fn run_command_link( eprintln!("Channel alias `{channel}` created, pointing to `{target_channel}`."); } else { - let absolute_file_path = std::fs::canonicalize(target) + let absolute_file_path = Path::new(target) + .absolutize() .with_context(|| format!("Failed to convert path `{target}` to absolute path."))?; if !is_valid_julia_path(&absolute_file_path.to_path_buf()) { @@ -85,12 +86,13 @@ pub fn run_command_link( #[cfg(not(windows))] if create_symlinks && !target.starts_with('+') { - let absolute_file_path = std::fs::canonicalize(target) + let absolute_file_path = Path::new(target) + .absolutize() .with_context(|| format!("Failed to convert path `{target}` to absolute path."))?; create_symlink( &JuliaupConfigChannel::LinkedChannel { - command: absolute_file_path.to_string_lossy().into_owned(), + command: absolute_file_path.to_string_lossy().to_string(), args: Some(args.to_vec()), }, &format!("julia-{channel}"), From 689c372475cb27749790a3820accf16e1ff11c6b Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 13:05:50 -0400 Subject: [PATCH 16/43] break out into function --- src/command_status.rs | 77 +++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index 2f9754894..85c48d620 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -13,6 +13,46 @@ use cli_table::{ use itertools::Itertools; use numeric_sort::cmp; +fn get_alias_update_info( + target: &str, + config_file: &crate::config_file::JuliaupReadonlyConfigFile, + versiondb_data: &crate::jsonstructs_versionsdb::JuliaupVersionDB, +) -> Option { + // Check if the target channel has updates available + match config_file.data.installed_channels.get(target) { + Some(target_channel) => match target_channel { + JuliaupConfigChannel::SystemChannel { version } => { + match versiondb_data.available_channels.get(target) { + Some(channel) => { + if channel.version != *version { + Some(format!("Update to {} available", channel.version)) + } else { + None + } + } + None => None, + } + } + JuliaupConfigChannel::DirectDownloadChannel { + path: _, + url: _, + local_etag, + server_etag, + version: _, + } => { + if local_etag != server_etag { + Some("Update available".to_string()) + } else { + None + } + } + // LinkedChannels and nested aliases don't have updates + _ => None, + }, + None => None, // Target channel doesn't exist + } +} + #[derive(Table)] struct ChannelRow { #[table(title = "Default", justify = "Justify::Right")] @@ -109,42 +149,7 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { args: _, } => None, JuliaupConfigChannel::AliasChannel { target } => { - // Check if the target channel has updates available - match config_file.data.installed_channels.get(target) { - Some(target_channel) => match target_channel { - JuliaupConfigChannel::SystemChannel { version } => { - match versiondb_data.available_channels.get(target) { - Some(channel) => { - if &channel.version != version { - Some(format!( - "Update to {} available", - channel.version - )) - } else { - None - } - } - None => None, - } - } - JuliaupConfigChannel::DirectDownloadChannel { - path: _, - url: _, - local_etag, - server_etag, - version: _, - } => { - if local_etag != server_etag { - Some("Update available".to_string()) - } else { - None - } - } - // LinkedChannels and nested aliases don't have updates - _ => None, - }, - None => None, // Target channel doesn't exist - } + get_alias_update_info(target, &config_file, &versiondb_data) } JuliaupConfigChannel::DirectDownloadChannel { path: _, From f73b2dbb8e7719492b2e95a04b51816c3713c078 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 13:22:59 -0400 Subject: [PATCH 17/43] test tidy etc. --- tests/command_link.rs | 151 ++++++++++++++++++------------------------ 1 file changed, 64 insertions(+), 87 deletions(-) diff --git a/tests/command_link.rs b/tests/command_link.rs index cfff747f8..208bc8c03 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -2,33 +2,46 @@ use assert_cmd::Command; use assert_fs::TempDir; use predicates::prelude::*; -fn juliaup_command(depot_dir: &assert_fs::TempDir) -> Command { - let mut cmd = Command::cargo_bin("juliaup").unwrap(); - cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()); - cmd +struct TestEnv { + depot_dir: TempDir, } -fn julia_command(depot_dir: &assert_fs::TempDir) -> Command { - let mut cmd = Command::cargo_bin("julia").unwrap(); - cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()); - cmd +impl TestEnv { + fn new() -> Self { + Self { + depot_dir: TempDir::new().unwrap(), + } + } + + fn juliaup(&self) -> Command { + self.command("juliaup") + } + + fn julia(&self) -> Command { + self.command("julia") + } + + 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 + } } #[test] fn command_link_binary() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // First add a regular channel for testing - juliaup_command(&depot_dir) + env.juliaup() .arg("add") .arg("1.10.10") .assert() .success(); // Test linking to a binary file (existing functionality) - juliaup_command(&depot_dir) + env.juliaup() .arg("link") .arg("custom") .arg("/usr/bin/false") // Use a binary that exists but won't work as Julia @@ -36,7 +49,7 @@ fn command_link_binary() { .success(); // Verify the link shows up in status - juliaup_command(&depot_dir) + env.juliaup() .arg("status") .assert() .success() @@ -45,17 +58,17 @@ fn command_link_binary() { #[test] fn command_link_alias() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // First install a Julia version to create an alias to - juliaup_command(&depot_dir) + env.juliaup() .arg("add") .arg("1.10.10") .assert() .success(); // Create an alias to the installed version - juliaup_command(&depot_dir) + env.juliaup() .arg("link") .arg("stable") .arg("+1.10.10") @@ -66,7 +79,7 @@ fn command_link_alias() { )); // Verify the alias shows up in status - juliaup_command(&depot_dir) + env.juliaup() .arg("status") .assert() .success() @@ -77,10 +90,10 @@ fn command_link_alias() { #[test] fn command_link_alias_to_system_channel() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // Test creating an alias to a system channel (release) - juliaup_command(&depot_dir) + env.juliaup() .arg("link") .arg("r") .arg("+release") @@ -91,7 +104,7 @@ fn command_link_alias_to_system_channel() { )); // Verify the alias shows up in status - juliaup_command(&depot_dir) + env.juliaup() .arg("status") .assert() .success() @@ -100,10 +113,10 @@ fn command_link_alias_to_system_channel() { #[test] fn command_link_alias_invalid_target() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // Test creating an alias to a non-existent channel - juliaup_command(&depot_dir) + env.juliaup() .arg("link") .arg("broken") .arg("+nonexistent") @@ -116,17 +129,14 @@ fn command_link_alias_invalid_target() { #[test] fn command_link_alias_with_args_fails() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // Test that creating an alias with extra arguments fails (the argument parser should reject this) - Command::cargo_bin("juliaup") - .unwrap() + env.command("juliaup") .arg("link") .arg("alias_with_args") .arg("+release") .arg("--some-arg") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .failure() .stderr(predicate::str::contains("unexpected argument")); @@ -134,26 +144,20 @@ fn command_link_alias_with_args_fails() { #[test] fn command_link_duplicate_channel() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // First add a regular channel - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Try to create an alias with the same name as an existing channel - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("link") .arg("1.10.10") .arg("+release") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .failure() .stderr(predicate::str::contains( @@ -163,26 +167,20 @@ fn command_link_duplicate_channel() { #[test] fn command_remove_alias() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // Create an alias - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("link") .arg("r") .arg("+release") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Remove the alias - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("remove") .arg("r") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() .stderr(predicate::str::contains( @@ -190,11 +188,8 @@ fn command_remove_alias() { )); // Verify the alias is gone from status (check for empty list or no mention of the alias) - 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(predicate::str::contains("Alias to").not()); @@ -202,15 +197,12 @@ fn command_remove_alias() { #[test] fn command_remove_non_existent() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // Try to remove a non-existent channel - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("remove") .arg("nonexistent") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .failure() .stderr(predicate::str::contains("'nonexistent' cannot be removed because it is not currently installed. Please run `juliaup list` to see available channels.")); @@ -218,31 +210,25 @@ fn command_remove_non_existent() { #[test] fn alias_resolution_julia_launcher() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // Add a channel first - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Create an alias to it - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("link") .arg("stable") .arg("+1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Try to use the alias with julia +alias - julia_command(&depot_dir) + env.julia() .arg("+stable") .arg("-e") .arg("print(VERSION)") @@ -253,41 +239,32 @@ fn alias_resolution_julia_launcher() { #[test] fn alias_as_default() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // Add a channel first - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("add") .arg("1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Create an alias - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("link") .arg("stable") .arg("+1.10.10") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Set the alias as default - Command::cargo_bin("juliaup") - .unwrap() + env.juliaup() .arg("default") .arg("stable") - .env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success(); // Test that julia without + uses the alias - julia_command(&depot_dir) + env.julia() .arg("-e") .arg("print(VERSION)") .assert() @@ -297,17 +274,17 @@ fn alias_as_default() { #[test] fn alias_to_alias_prevented() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // Add a channel first - juliaup_command(&depot_dir) + env.juliaup() .arg("add") .arg("1.10.10") .assert() .success(); // Create first alias - juliaup_command(&depot_dir) + env.juliaup() .arg("link") .arg("stable") .arg("+1.10.10") @@ -315,7 +292,7 @@ fn alias_to_alias_prevented() { .success(); // Try to create alias to alias - should now fail - juliaup_command(&depot_dir) + env.juliaup() .arg("link") .arg("prod") .arg("+stable") @@ -331,17 +308,17 @@ fn alias_to_alias_prevented() { #[test] fn alias_update_resolves_target() { - let depot_dir = assert_fs::TempDir::new().unwrap(); + let env = TestEnv::new(); // First install a Julia version to create an alias to - juliaup_command(&depot_dir) + env.juliaup() .arg("add") .arg("1.10.10") .assert() .success(); // Create an alias to the installed version - juliaup_command(&depot_dir) + env.juliaup() .arg("link") .arg("r") .arg("+1.10.10") @@ -349,7 +326,7 @@ fn alias_update_resolves_target() { .success(); // Update through the alias - should work and update the target - juliaup_command(&depot_dir) + env.juliaup() .arg("update") .arg("r") .assert() From e84c810de7acc4f7e2d8ceb1aa5d549028b1fb78 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 13:23:16 -0400 Subject: [PATCH 18/43] fmt --- tests/command_link.rs | 58 ++++++++----------------------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/tests/command_link.rs b/tests/command_link.rs index 208bc8c03..a4a00397c 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -34,11 +34,7 @@ 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(); + env.juliaup().arg("add").arg("1.10.10").assert().success(); // Test linking to a binary file (existing functionality) env.juliaup() @@ -61,11 +57,7 @@ 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(); + env.juliaup().arg("add").arg("1.10.10").assert().success(); // Create an alias to the installed version env.juliaup() @@ -79,13 +71,9 @@ fn command_link_alias() { )); // 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`")), - ); + env.juliaup().arg("status").assert().success().stdout( + predicate::str::contains("stable").and(predicate::str::contains("Alias to `1.10.10`")), + ); } #[test] @@ -147,11 +135,7 @@ fn command_link_duplicate_channel() { let env = TestEnv::new(); // First add a regular channel - env.juliaup() - .arg("add") - .arg("1.10.10") - .assert() - .success(); + 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() @@ -213,11 +197,7 @@ fn alias_resolution_julia_launcher() { let env = TestEnv::new(); // Add a channel first - env.juliaup() - .arg("add") - .arg("1.10.10") - .assert() - .success(); + env.juliaup().arg("add").arg("1.10.10").assert().success(); // Create an alias to it env.juliaup() @@ -242,11 +222,7 @@ fn alias_as_default() { let env = TestEnv::new(); // Add a channel first - env.juliaup() - .arg("add") - .arg("1.10.10") - .assert() - .success(); + env.juliaup().arg("add").arg("1.10.10").assert().success(); // Create an alias env.juliaup() @@ -277,11 +253,7 @@ fn alias_to_alias_prevented() { let env = TestEnv::new(); // Add a channel first - env.juliaup() - .arg("add") - .arg("1.10.10") - .assert() - .success(); + env.juliaup().arg("add").arg("1.10.10").assert().success(); // Create first alias env.juliaup() @@ -311,11 +283,7 @@ 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(); + env.juliaup().arg("add").arg("1.10.10").assert().success(); // Create an alias to the installed version env.juliaup() @@ -326,9 +294,5 @@ fn alias_update_resolves_target() { .success(); // Update through the alias - should work and update the target - env.juliaup() - .arg("update") - .arg("r") - .assert() - .success(); + env.juliaup().arg("update").arg("r").assert().success(); } From 561e855a6a3b750b7b0f749936fcba1f41c278b5 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:06:27 -0400 Subject: [PATCH 19/43] suggestions --- src/command_status.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index 85c48d620..143cff645 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -1,5 +1,5 @@ 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::versions_file::load_versions_db; use anyhow::{Context, Result}; @@ -15,7 +15,7 @@ use numeric_sort::cmp; fn get_alias_update_info( target: &str, - config_file: &crate::config_file::JuliaupReadonlyConfigFile, + config_file: &JuliaupReadonlyConfigFile, versiondb_data: &crate::jsonstructs_versionsdb::JuliaupVersionDB, ) -> Option { // Check if the target channel has updates available @@ -23,14 +23,10 @@ fn get_alias_update_info( Some(target_channel) => match target_channel { JuliaupConfigChannel::SystemChannel { version } => { match versiondb_data.available_channels.get(target) { - Some(channel) => { - if channel.version != *version { - Some(format!("Update to {} available", channel.version)) - } else { - None - } + Some(channel) if channel.version != *version => { + Some(format!("Update to {} available", channel.version)) } - None => None, + _ => None, } } JuliaupConfigChannel::DirectDownloadChannel { From b267e1c4eff9696799c8e1e4fc88c5ee9a469bcf Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:09:02 -0400 Subject: [PATCH 20/43] Update src/command_status.rs Co-authored-by: Miles Cranmer --- src/command_status.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index 143cff645..deed59de8 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -36,11 +36,7 @@ fn get_alias_update_info( server_etag, version: _, } => { - if local_etag != server_etag { - Some("Update available".to_string()) - } else { - None - } + (local_etag != server_etag).then(|| "Update available".to_string()) } // LinkedChannels and nested aliases don't have updates _ => None, From de4e361b4a3795c2ed6f81b296ad197e0c31a6d8 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:10:32 -0400 Subject: [PATCH 21/43] fmt --- src/command_status.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index deed59de8..f43f1822c 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -35,9 +35,7 @@ fn get_alias_update_info( local_etag, server_etag, version: _, - } => { - (local_etag != server_etag).then(|| "Update available".to_string()) - } + } => (local_etag != server_etag).then(|| "Update available".to_string()), // LinkedChannels and nested aliases don't have updates _ => None, }, From 3ae53b4b55cc3c64ef6e561bec67fdd5e9627f9f Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:18:53 -0400 Subject: [PATCH 22/43] Create test/utils.rs and use it --- tests/command_add.rs | 30 ++++++------------ tests/command_default.rs | 20 +++++------- tests/command_link.rs | 28 ++--------------- tests/command_remove.rs | 15 ++++----- tests/command_update.rs | 8 ++--- tests/utils.rs | 67 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+), 75 deletions(-) create mode 100644 tests/utils.rs diff --git a/tests/command_add.rs b/tests/command_add.rs index 5b52f3b9d..3850f6d8b 100644 --- a/tests/command_add.rs +++ b/tests/command_add.rs @@ -1,58 +1,46 @@ 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..90139bd49 100644 --- a/tests/command_default.rs +++ b/tests/command_default.rs @@ -1,35 +1,29 @@ 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_link.rs b/tests/command_link.rs index a4a00397c..a2aed45f1 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -2,32 +2,8 @@ use assert_cmd::Command; use assert_fs::TempDir; use predicates::prelude::*; -struct TestEnv { - depot_dir: TempDir, -} - -impl TestEnv { - fn new() -> Self { - Self { - depot_dir: TempDir::new().unwrap(), - } - } - - fn juliaup(&self) -> Command { - self.command("juliaup") - } - - fn julia(&self) -> Command { - self.command("julia") - } - - 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 - } -} +mod utils; +use utils::TestEnv; #[test] fn command_link_binary() { diff --git a/tests/command_remove.rs b/tests/command_remove.rs index 9b2c25d0e..48e2e6a0b 100644 --- a/tests/command_remove.rs +++ b/tests/command_remove.rs @@ -1,6 +1,9 @@ use assert_cmd::Command; use predicates::boolean::PredicateBooleanExt; +mod utils; +use utils::juliaup_command_tempfile as juliaup_command; + #[test] fn command_remove() { let depot_dir = tempfile::Builder::new() @@ -8,26 +11,20 @@ fn command_remove() { .tempdir() .unwrap(); - Command::cargo_bin("juliaup") - .unwrap() + juliaup_command(&depot_dir) .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() + juliaup_command(&depot_dir) .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") + juliaup_command(&depot_dir) .unwrap() .arg("status") .env("JULIA_DEPOT_PATH", depot_dir.path()) diff --git a/tests/command_update.rs b/tests/command_update.rs index 6ebb6fa28..2922a6e4e 100644 --- a/tests/command_update.rs +++ b/tests/command_update.rs @@ -1,11 +1,7 @@ use assert_cmd::Command; -fn juliaup_command(depot_dir: &tempfile::TempDir) -> Command { - let mut cmd = Command::cargo_bin("juliaup").unwrap(); - cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()); - cmd -} +mod utils; +use utils::juliaup_command_tempfile as juliaup_command; #[test] fn command_update() { diff --git a/tests/utils.rs b/tests/utils.rs new file mode 100644 index 000000000..14798593e --- /dev/null +++ b/tests/utils.rs @@ -0,0 +1,67 @@ +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, +} + +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 + } + + /// Get the path to the depot directory for this test environment + pub fn depot_path(&self) -> &std::path::Path { + self.depot_dir.path() + } +} + +/// Legacy function for backward compatibility with existing tests. +/// Consider using TestEnv instead for new tests. +pub fn juliaup_command(depot_dir: &TempDir) -> Command { + let mut cmd = Command::cargo_bin("juliaup").unwrap(); + cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()); + cmd +} + +/// Legacy function for backward compatibility with existing tests. +/// Consider using TestEnv instead for new tests. +pub fn julia_command(depot_dir: &TempDir) -> Command { + let mut cmd = Command::cargo_bin("julia").unwrap(); + cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()); + cmd +} + +/// Legacy function for backward compatibility with existing tests using tempfile::TempDir. +/// Consider using TestEnv instead for new tests. +pub fn juliaup_command_tempfile(depot_dir: &tempfile::TempDir) -> Command { + let mut cmd = Command::cargo_bin("juliaup").unwrap(); + cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()); + cmd +} From 1eddf53baf7607bb0be073b7f732ae07523ea2ea Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:20:58 -0400 Subject: [PATCH 23/43] Apply suggestions from code review Co-authored-by: Miles Cranmer --- src/command_status.rs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index f43f1822c..f984a80d8 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -124,14 +124,9 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { let update_option = match i.1 { JuliaupConfigChannel::SystemChannel { version } => { match versiondb_data.available_channels.get(i.0) { - Some(channel) => { - if &channel.version != version { - Some(format!("Update to {} available", channel.version)) - } else { - None - } - } - None => None, + Some(channel) if &channel.version != version => + Some(format!("Update to {} available", channel.version)), + _ => None, } } JuliaupConfigChannel::LinkedChannel { @@ -148,11 +143,7 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { server_etag, version: _, } => { - if local_etag != server_etag { - Some("Update available".to_string()) - } else { - None - } + (local_etag != server_etag).then(|| "Update available".to_string()) } }; update_option.unwrap_or_default() From cfb8fe57198dfd0a2dc3f690cd4417fce1d28dd5 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:21:24 -0400 Subject: [PATCH 24/43] fmt --- src/command_status.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index f984a80d8..df8b3a9db 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -124,8 +124,9 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { let update_option = match i.1 { JuliaupConfigChannel::SystemChannel { version } => { match versiondb_data.available_channels.get(i.0) { - Some(channel) if &channel.version != version => - Some(format!("Update to {} available", channel.version)), + Some(channel) if &channel.version != version => { + Some(format!("Update to {} available", channel.version)) + } _ => None, } } @@ -142,9 +143,7 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { local_etag, server_etag, version: _, - } => { - (local_etag != server_etag).then(|| "Update available".to_string()) - } + } => (local_etag != server_etag).then(|| "Update available".to_string()), }; update_option.unwrap_or_default() }, From 1b027ba26adfaf4063a122cb5195aedb6f23880d Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:51:21 -0400 Subject: [PATCH 25/43] Apply suggestions from code review Co-authored-by: Miles Cranmer --- src/command_status.rs | 43 ++++++++++++------------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index df8b3a9db..ff5de317a 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -20,26 +20,15 @@ fn get_alias_update_info( ) -> Option { // Check if the target channel has updates available match config_file.data.installed_channels.get(target) { - Some(target_channel) => match target_channel { - JuliaupConfigChannel::SystemChannel { version } => { - match versiondb_data.available_channels.get(target) { - Some(channel) if channel.version != *version => { - Some(format!("Update to {} available", channel.version)) - } - _ => None, - } - } - JuliaupConfigChannel::DirectDownloadChannel { - path: _, - url: _, - local_etag, - server_etag, - version: _, - } => (local_etag != server_etag).then(|| "Update available".to_string()), - // LinkedChannels and nested aliases don't have updates - _ => None, - }, - None => None, // Target channel doesn't exist + 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, + }, + Some(JuliaupConfigChannel::DirectDownloadChannel { local_etag, server_etag, .. }) => + (local_etag != server_etag).then(|| "Update available".to_string()), + _ => None, // Target channel doesn't exist or not updatable } } @@ -130,20 +119,12 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { _ => None, } } - JuliaupConfigChannel::LinkedChannel { - command: _, - args: _, - } => None, + JuliaupConfigChannel::LinkedChannel { .. } => None, JuliaupConfigChannel::AliasChannel { target } => { get_alias_update_info(target, &config_file, &versiondb_data) } - JuliaupConfigChannel::DirectDownloadChannel { - path: _, - url: _, - local_etag, - server_etag, - version: _, - } => (local_etag != server_etag).then(|| "Update available".to_string()), + JuliaupConfigChannel::DirectDownloadChannel { local_etag, server_etag, .. } => + (local_etag != server_etag).then(|| "Update available".to_string()), }; update_option.unwrap_or_default() }, From 8e01eea9a0b1be738f348990eaef7b21c5c9283f Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:24:13 -0400 Subject: [PATCH 26/43] move import to top --- src/command_status.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/command_status.rs b/src/command_status.rs index ff5de317a..dd84fe90e 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, 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; @@ -16,7 +17,7 @@ use numeric_sort::cmp; fn get_alias_update_info( target: &str, config_file: &JuliaupReadonlyConfigFile, - versiondb_data: &crate::jsonstructs_versionsdb::JuliaupVersionDB, + versiondb_data: &JuliaupVersionDB, ) -> Option { // Check if the target channel has updates available match config_file.data.installed_channels.get(target) { From f6bf3a1705295464e91d64da19b1cc6588a551cf Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:50:20 -0400 Subject: [PATCH 27/43] rm unused function --- src/operations.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/operations.rs b/src/operations.rs index ee13e8187..c5b7b4485 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -848,12 +848,6 @@ pub fn remove_symlink(symlink_name: &String) -> Result<()> { Ok(()) } -#[cfg(not(windows))] -fn create_alias_symlink() -> Result<()> { - // Aliases don't create symlinks directly, they are resolved at runtime - Ok(()) -} - #[cfg(not(windows))] fn create_system_channel_symlink( version: &str, @@ -999,7 +993,10 @@ pub fn create_symlink( let updating = _remove_symlink(&symlink_path)?; match channel { - JuliaupConfigChannel::AliasChannel { target: _ } => create_alias_symlink(), + JuliaupConfigChannel::AliasChannel { target: _ } => { + // Aliases don't create symlinks directly, they are resolved at runtime + Ok(()) + } JuliaupConfigChannel::SystemChannel { version } => { create_system_channel_symlink(version, symlink_name, &symlink_path, paths, &updating) } From e53a14e327211dc773c2d23514f309d83338a33f Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:52:46 -0400 Subject: [PATCH 28/43] fmt --- src/command_status.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/command_status.rs b/src/command_status.rs index dd84fe90e..7013e11d1 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -21,15 +21,20 @@ fn get_alias_update_info( ) -> Option { // Check if the target channel has updates available match config_file.data.installed_channels.get(target) { - Some(JuliaupConfigChannel::SystemChannel { version }) => + Some(JuliaupConfigChannel::SystemChannel { version }) => { match versiondb_data.available_channels.get(target) { - Some(channel) if channel.version != *version => - Some(format!("Update to {} available", channel.version)), + Some(channel) if channel.version != *version => { + Some(format!("Update to {} available", channel.version)) + } _ => None, - }, - Some(JuliaupConfigChannel::DirectDownloadChannel { local_etag, server_etag, .. }) => - (local_etag != server_etag).then(|| "Update available".to_string()), - _ => None, // Target channel doesn't exist or not updatable + } + } + Some(JuliaupConfigChannel::DirectDownloadChannel { + local_etag, + server_etag, + .. + }) => (local_etag != server_etag).then(|| "Update available".to_string()), + _ => None, // Target channel doesn't exist or not updatable } } @@ -124,8 +129,11 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { JuliaupConfigChannel::AliasChannel { target } => { get_alias_update_info(target, &config_file, &versiondb_data) } - JuliaupConfigChannel::DirectDownloadChannel { local_etag, server_etag, .. } => - (local_etag != server_etag).then(|| "Update available".to_string()), + JuliaupConfigChannel::DirectDownloadChannel { + local_etag, + server_etag, + .. + } => (local_etag != server_etag).then(|| "Update available".to_string()), }; update_option.unwrap_or_default() }, From a8a8469d7f0bb86a4950049ac2cb7dbb73e79685 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:55:54 -0400 Subject: [PATCH 29/43] fix clippy errors --- tests/command_default.rs | 2 -- tests/command_link.rs | 2 -- tests/command_remove.rs | 1 - tests/command_update.rs | 2 -- tests/utils.rs | 18 ------------------ 5 files changed, 25 deletions(-) diff --git a/tests/command_default.rs b/tests/command_default.rs index 90139bd49..80ac0cf6f 100644 --- a/tests/command_default.rs +++ b/tests/command_default.rs @@ -1,5 +1,3 @@ -use assert_cmd::Command; - mod utils; use utils::TestEnv; diff --git a/tests/command_link.rs b/tests/command_link.rs index a2aed45f1..315ea4b86 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -1,5 +1,3 @@ -use assert_cmd::Command; -use assert_fs::TempDir; use predicates::prelude::*; mod utils; diff --git a/tests/command_remove.rs b/tests/command_remove.rs index 48e2e6a0b..93625d239 100644 --- a/tests/command_remove.rs +++ b/tests/command_remove.rs @@ -25,7 +25,6 @@ fn command_remove() { .stdout(""); juliaup_command(&depot_dir) - .unwrap() .arg("status") .env("JULIA_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_DEPOT_PATH", depot_dir.path()) diff --git a/tests/command_update.rs b/tests/command_update.rs index 2922a6e4e..b94faf99b 100644 --- a/tests/command_update.rs +++ b/tests/command_update.rs @@ -1,5 +1,3 @@ -use assert_cmd::Command; - mod utils; use utils::juliaup_command_tempfile as juliaup_command; diff --git a/tests/utils.rs b/tests/utils.rs index 14798593e..61b4b6670 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -39,24 +39,6 @@ impl TestEnv { } } -/// Legacy function for backward compatibility with existing tests. -/// Consider using TestEnv instead for new tests. -pub fn juliaup_command(depot_dir: &TempDir) -> Command { - let mut cmd = Command::cargo_bin("juliaup").unwrap(); - cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()); - cmd -} - -/// Legacy function for backward compatibility with existing tests. -/// Consider using TestEnv instead for new tests. -pub fn julia_command(depot_dir: &TempDir) -> Command { - let mut cmd = Command::cargo_bin("julia").unwrap(); - cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()); - cmd -} - /// Legacy function for backward compatibility with existing tests using tempfile::TempDir. /// Consider using TestEnv instead for new tests. pub fn juliaup_command_tempfile(depot_dir: &tempfile::TempDir) -> Command { From 9c4cb25882dbdaa83129111481f9115edff562a0 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 14:57:57 -0400 Subject: [PATCH 30/43] clippy fix --- tests/utils.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/utils.rs b/tests/utils.rs index 61b4b6670..4ca4357a9 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -7,6 +7,12 @@ pub struct TestEnv { depot_dir: TempDir, } +impl Default for TestEnv { + fn default() -> Self { + Self::new() + } +} + impl TestEnv { /// Create a new test environment with an isolated temporary depot directory pub fn new() -> Self { From 9261f05e6481c9513b551b95bb29194724543443 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 15:01:43 -0400 Subject: [PATCH 31/43] more clippy fixes --- tests/command_add.rs | 1 - tests/utils.rs | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/command_add.rs b/tests/command_add.rs index 3850f6d8b..72541d751 100644 --- a/tests/command_add.rs +++ b/tests/command_add.rs @@ -1,4 +1,3 @@ -use assert_cmd::Command; use predicates::prelude::predicate; mod utils; diff --git a/tests/utils.rs b/tests/utils.rs index 4ca4357a9..1eb67d890 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -3,6 +3,7 @@ use assert_fs::TempDir; /// A test environment that provides convenient methods for running juliaup and julia commands /// with isolated depot directories. +#[allow(dead_code)] pub struct TestEnv { depot_dir: TempDir, } @@ -40,6 +41,7 @@ impl TestEnv { } /// Get the path to the depot directory for this test environment + #[allow(dead_code)] pub fn depot_path(&self) -> &std::path::Path { self.depot_dir.path() } @@ -47,6 +49,7 @@ impl TestEnv { /// Legacy function for backward compatibility with existing tests using tempfile::TempDir. /// Consider using TestEnv instead for new tests. +#[allow(dead_code)] pub fn juliaup_command_tempfile(depot_dir: &tempfile::TempDir) -> Command { let mut cmd = Command::cargo_bin("juliaup").unwrap(); cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) From 452bc2dd8d2e1297b241d3ceae1a6878f85d2756 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 15:06:57 -0400 Subject: [PATCH 32/43] fix clippy setup --- .github/workflows/clippy.yml | 2 +- tests/utils.rs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) 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/tests/utils.rs b/tests/utils.rs index 1eb67d890..4ca4357a9 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -3,7 +3,6 @@ use assert_fs::TempDir; /// A test environment that provides convenient methods for running juliaup and julia commands /// with isolated depot directories. -#[allow(dead_code)] pub struct TestEnv { depot_dir: TempDir, } @@ -41,7 +40,6 @@ impl TestEnv { } /// Get the path to the depot directory for this test environment - #[allow(dead_code)] pub fn depot_path(&self) -> &std::path::Path { self.depot_dir.path() } @@ -49,7 +47,6 @@ impl TestEnv { /// Legacy function for backward compatibility with existing tests using tempfile::TempDir. /// Consider using TestEnv instead for new tests. -#[allow(dead_code)] pub fn juliaup_command_tempfile(depot_dir: &tempfile::TempDir) -> Command { let mut cmd = Command::cargo_bin("juliaup").unwrap(); cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) From 4052c4241f4681278e72d3480ae5295a537c205b Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 15:10:01 -0400 Subject: [PATCH 33/43] mark utils as allowed dead code --- tests/utils.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/utils.rs b/tests/utils.rs index 4ca4357a9..3ace4d7e3 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -40,6 +40,7 @@ impl TestEnv { } /// Get the path to the depot directory for this test environment + #[allow(dead_code)] // May not be used in all test configurations pub fn depot_path(&self) -> &std::path::Path { self.depot_dir.path() } @@ -47,6 +48,7 @@ impl TestEnv { /// Legacy function for backward compatibility with existing tests using tempfile::TempDir. /// Consider using TestEnv instead for new tests. +#[allow(dead_code)] // May not be used in all test configurations pub fn juliaup_command_tempfile(depot_dir: &tempfile::TempDir) -> Command { let mut cmd = Command::cargo_bin("juliaup").unwrap(); cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) From 5a9336f2068d9ca1a190121f2dfc481a35942053 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 15:15:05 -0400 Subject: [PATCH 34/43] dead code fix --- tests/utils.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/utils.rs b/tests/utils.rs index 3ace4d7e3..dfa6a8379 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -13,6 +13,7 @@ impl Default for TestEnv { } } +#[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 { From 4c7e4099876e98c52b0910d8b057e6547c828017 Mon Sep 17 00:00:00 2001 From: Miles Cranmer Date: Sun, 7 Sep 2025 21:33:30 +0100 Subject: [PATCH 35/43] Refactor tests to use TestEnv (#1241) --- tests/channel_selection.rs | 103 ++++------------- tests/command_gc.rs | 95 +++++----------- tests/command_list_test.rs | 19 +--- tests/command_override_test.rs | 199 ++++++++------------------------- tests/command_remove.rs | 56 +++------- tests/command_status_test.rs | 18 +-- tests/command_update.rs | 38 ++----- tests/utils.rs | 22 ---- 8 files changed, 128 insertions(+), 422 deletions(-) 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_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_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 93625d239..ef1cf06a9 100644 --- a/tests/command_remove.rs +++ b/tests/command_remove.rs @@ -1,109 +1,79 @@ -use assert_cmd::Command; use predicates::boolean::PredicateBooleanExt; mod utils; -use utils::juliaup_command_tempfile as juliaup_command; +use utils::TestEnv; #[test] fn command_remove() { - let depot_dir = tempfile::Builder::new() - .prefix("juliauptest") - .tempdir() - .unwrap(); + let env = TestEnv::new(); - juliaup_command(&depot_dir) + env.juliaup() .arg("status") .assert() .success() .stdout(predicates::str::contains("1.6.4").not()); - juliaup_command(&depot_dir) + env.juliaup() .arg("add") .arg("1.6.4") .assert() .success() .stdout(""); - juliaup_command(&depot_dir) + 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 b94faf99b..7ab630b39 100644 --- a/tests/command_update.rs +++ b/tests/command_update.rs @@ -1,42 +1,24 @@ mod utils; -use utils::juliaup_command_tempfile as juliaup_command; +use utils::TestEnv; #[test] fn command_update() { - let depot_dir = tempfile::Builder::new() - .prefix("juliauptest") - .tempdir() - .unwrap(); + let env = TestEnv::new(); - juliaup_command(&depot_dir) - .arg("update") - .assert() - .success() - .stdout(""); + env.juliaup().arg("update").assert().success().stdout(""); - juliaup_command(&depot_dir) - .arg("up") - .assert() - .success() - .stdout(""); + env.juliaup().arg("up").assert().success().stdout(""); } #[test] fn command_update_alias_works() { - let depot_dir = tempfile::Builder::new() - .prefix("juliauptest") - .tempdir() - .unwrap(); + let env = TestEnv::new(); // First install a Julia version to create an alias to - juliaup_command(&depot_dir) - .arg("add") - .arg("1.10.10") - .assert() - .success(); + env.juliaup().arg("add").arg("1.10.10").assert().success(); // Create an alias to the installed version - juliaup_command(&depot_dir) + env.juliaup() .arg("link") .arg("r") .arg("+1.10.10") @@ -44,9 +26,5 @@ fn command_update_alias_works() { .success(); // Update the alias - should succeed and update the target - juliaup_command(&depot_dir) - .arg("update") - .arg("r") - .assert() - .success(); + env.juliaup().arg("update").arg("r").assert().success(); } diff --git a/tests/utils.rs b/tests/utils.rs index dfa6a8379..811f90ec3 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -7,12 +7,6 @@ pub struct TestEnv { depot_dir: TempDir, } -impl Default for TestEnv { - fn default() -> Self { - Self::new() - } -} - #[allow(dead_code)] // May not be used in all test configurations impl TestEnv { /// Create a new test environment with an isolated temporary depot directory @@ -39,20 +33,4 @@ impl TestEnv { cmd.env("JULIAUP_DEPOT_PATH", self.depot_dir.path()); cmd } - - /// Get the path to the depot directory for this test environment - #[allow(dead_code)] // May not be used in all test configurations - pub fn depot_path(&self) -> &std::path::Path { - self.depot_dir.path() - } -} - -/// Legacy function for backward compatibility with existing tests using tempfile::TempDir. -/// Consider using TestEnv instead for new tests. -#[allow(dead_code)] // May not be used in all test configurations -pub fn juliaup_command_tempfile(depot_dir: &tempfile::TempDir) -> Command { - let mut cmd = Command::cargo_bin("juliaup").unwrap(); - cmd.env("JULIA_DEPOT_PATH", depot_dir.path()) - .env("JULIAUP_DEPOT_PATH", depot_dir.path()); - cmd } From e3b3eb4f10f3f2384fbb812abd936f2acc223f3d Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 17:33:02 -0400 Subject: [PATCH 36/43] tweak explanation --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 3e3e126aa..fc092e72f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -29,7 +29,7 @@ pub enum Juliaup { Link { /// Name of the new channel to create channel: String, - /// Path to Julia binary, or +CHANNEL to create an alias (e.g. +release) + /// Path to Julia binary, or +{channel} to create an alias target: String, /// Additional arguments for the Julia binary (not used for aliases) args: Vec, From bd15d3d07dbcd48a0207f232ae96f3d4a4e6f5fb Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 17:38:45 -0400 Subject: [PATCH 37/43] add support for args on link aliases --- src/bin/julialauncher.rs | 2 +- src/cli.rs | 2 +- src/command_api.rs | 2 +- src/command_link.rs | 15 ++++++++++----- src/command_remove.rs | 4 +++- src/command_status.rs | 11 +++++++---- src/command_update.rs | 2 +- src/config_file.rs | 2 ++ src/operations.rs | 4 ++-- tests/command_link.rs | 11 ++++++----- 10 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 66774781c..4b183ae1e 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -327,7 +327,7 @@ enum JuliaupChannelSource { fn resolve_channel_alias(config_data: &JuliaupConfig, channel: &str) -> Result { match config_data.installed_channels.get(channel) { - Some(JuliaupConfigChannel::AliasChannel { target }) => Ok(target.to_string()), + Some(JuliaupConfigChannel::AliasChannel { target, args: _ }) => Ok(target.to_string()), _ => Ok(channel.to_string()), } } diff --git a/src/cli.rs b/src/cli.rs index fc092e72f..2d14606f0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -31,7 +31,7 @@ pub enum Juliaup { channel: String, /// Path to Julia binary, or +{channel} to create an alias target: String, - /// Additional arguments for the Julia binary (not used for aliases) + /// 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 3025b731d..474ff943b 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -45,7 +45,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { for (key, value) in &config_file.data.installed_channels { let curr = match &value { - JuliaupConfigChannel::AliasChannel { target } => { + JuliaupConfigChannel::AliasChannel { target, args: _ } => { JuliaupChannelInfo { name: key.clone(), file: format!("alias-to-{target}"), diff --git a/src/command_link.rs b/src/command_link.rs index 1ec5b8c52..f281c0d05 100644 --- a/src/command_link.rs +++ b/src/command_link.rs @@ -42,18 +42,23 @@ pub fn run_command_link( 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 !args.is_empty() { - bail!("Arguments are not supported when creating channel aliases. Remove the extra arguments: {:?}", args); - } - 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()) + }, }, ); - eprintln!("Channel alias `{channel}` created, pointing to `{target_channel}`."); + 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() diff --git a/src/command_remove.rs b/src/command_remove.rs index b0e615480..13f336783 100644 --- a/src/command_remove.rs +++ b/src/command_remove.rs @@ -43,7 +43,9 @@ pub fn run_command_remove(channel: &str, paths: &GlobalPaths) -> Result<()> { // Determine what type of channel is being removed for better messaging let channel_type = match channel_info { - JuliaupConfigChannel::AliasChannel { target } => format!("alias (pointing to '{target}')"), + JuliaupConfigChannel::AliasChannel { target, args: _ } => { + format!("alias (pointing to '{target}')") + } JuliaupConfigChannel::LinkedChannel { .. } => "linked channel".to_string(), JuliaupConfigChannel::SystemChannel { .. } => "channel".to_string(), JuliaupConfigChannel::DirectDownloadChannel { .. } => "channel".to_string(), diff --git a/src/command_status.rs b/src/command_status.rs index 7013e11d1..b3b20d9b0 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -111,9 +111,12 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { } format!("Linked to `{combined_command}`") } - JuliaupConfigChannel::AliasChannel { target } => { - format!("Alias to `{target}`") - } + JuliaupConfigChannel::AliasChannel { target, args } => match args { + Some(args) if !args.is_empty() => { + format!("Alias to `{target}` with args: {:?}", args) + } + _ => format!("Alias to `{target}`"), + }, }, update: { let update_option = match i.1 { @@ -126,7 +129,7 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { } } JuliaupConfigChannel::LinkedChannel { .. } => None, - JuliaupConfigChannel::AliasChannel { target } => { + JuliaupConfigChannel::AliasChannel { target, args: _ } => { get_alias_update_info(target, &config_file, &versiondb_data) } JuliaupConfigChannel::DirectDownloadChannel { diff --git a/src/command_update.rs b/src/command_update.rs index 51480068b..867a5aac3 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -13,7 +13,7 @@ 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(JuliaupConfigChannel::AliasChannel { target, args: _ }) => Ok(target.to_string()), Some(_) => Ok(channel_name.to_string()), None => bail!("Channel '{}' not found", channel_name), } diff --git a/src/config_file.rs b/src/config_file.rs index 6058e1d58..6052bb402 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -54,6 +54,8 @@ pub enum JuliaupConfigChannel { AliasChannel { #[serde(rename = "Target")] target: String, + #[serde(rename = "Args", skip_serializing_if = "Option::is_none")] + args: Option>, }, } diff --git a/src/operations.rs b/src/operations.rs index c5b7b4485..801b63e0f 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -761,7 +761,7 @@ pub fn garbage_collect_versions( command: _, args: _, } => true, - JuliaupConfigChannel::AliasChannel { target: _ } => true, + JuliaupConfigChannel::AliasChannel { target: _, args: _ } => true, JuliaupConfigChannel::DirectDownloadChannel { path: _, url: _, @@ -993,7 +993,7 @@ pub fn create_symlink( let updating = _remove_symlink(&symlink_path)?; match channel { - JuliaupConfigChannel::AliasChannel { target: _ } => { + JuliaupConfigChannel::AliasChannel { target: _, args: _ } => { // Aliases don't create symlinks directly, they are resolved at runtime Ok(()) } diff --git a/tests/command_link.rs b/tests/command_link.rs index 315ea4b86..1c1ec766c 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -90,18 +90,19 @@ fn command_link_alias_invalid_target() { } #[test] -fn command_link_alias_with_args_fails() { +fn command_link_alias_with_args_works() { let env = TestEnv::new(); - // Test that creating an alias with extra arguments fails (the argument parser should reject this) - env.command("juliaup") + // 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() - .failure() - .stderr(predicate::str::contains("unexpected argument")); + .success() + .stderr(predicate::str::contains("args: [\"--some-arg\"]")); } #[test] From 1ef2b3c47ff80513b30295d21c57c0de1053b29f Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 17:43:05 -0400 Subject: [PATCH 38/43] turn bail into unreachable --- src/command_update.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command_update.rs b/src/command_update.rs index 867a5aac3..a6bc19733 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -84,7 +84,7 @@ fn update_channel( } } JuliaupConfigChannel::AliasChannel { .. } => { - bail!("Internal error: Tried to update an alias channel '{channel}' directly."); + unreachable!("Alias channels should be resolved before calling update_channel. This is a programming error."); } JuliaupConfigChannel::DirectDownloadChannel { path, From a0e276a79d6dc4153aea9b57fce48a4ebdc65500 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 17:44:14 -0400 Subject: [PATCH 39/43] make test stricter --- tests/command_link.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/command_link.rs b/tests/command_link.rs index 1c1ec766c..2820160f8 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -269,5 +269,12 @@ fn alias_update_resolves_target() { .success(); // Update through the alias - should work and update the target - env.juliaup().arg("update").arg("r").assert().success(); + env.juliaup() + .arg("update") + .arg("r") + .assert() + .success() + .stdout(predicate::str::contains("1.10.10").or( + predicate::str::contains("already up to date").or(predicate::str::contains("Updating")), + )); } From 55468297d78a83c3a8ea88bded7740765d76a2e1 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 7 Sep 2025 17:52:17 -0400 Subject: [PATCH 40/43] fix test --- tests/command_link.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/command_link.rs b/tests/command_link.rs index 2820160f8..ed6964be4 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -274,7 +274,5 @@ fn alias_update_resolves_target() { .arg("r") .assert() .success() - .stdout(predicate::str::contains("1.10.10").or( - predicate::str::contains("already up to date").or(predicate::str::contains("Updating")), - )); + .stderr(predicate::str::contains("Checking for new Julia versions")); } From 133b97a32af25e117ded3e5c1cfe68dc501c4f53 Mon Sep 17 00:00:00 2001 From: Miles Cranmer Date: Mon, 8 Sep 2025 12:24:10 +0100 Subject: [PATCH 41/43] Fixes for alias feature (#1242) --- src/bin/julialauncher.rs | 32 +++---- src/command_api.rs | 34 ++++---- src/command_remove.rs | 8 +- src/command_status.rs | 184 +++++++++++++++++++-------------------- src/command_update.rs | 87 +++++++++--------- src/config_file.rs | 2 +- src/operations.rs | 42 +++------ tests/command_link.rs | 32 +++++++ 8 files changed, 214 insertions(+), 207 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 4b183ae1e..154ed2a3b 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -325,13 +325,6 @@ enum JuliaupChannelSource { Default, } -fn resolve_channel_alias(config_data: &JuliaupConfig, channel: &str) -> Result { - match config_data.installed_channels.get(channel) { - Some(JuliaupConfigChannel::AliasChannel { target, args: _ }) => Ok(target.to_string()), - _ => Ok(channel.to_string()), - } -} - fn get_julia_path_from_channel( versions_db: &JuliaupVersionDB, config_data: &JuliaupConfig, @@ -340,8 +333,13 @@ fn get_julia_path_from_channel( juliaup_channel_source: JuliaupChannelSource, paths: &juliaup::global_paths::GlobalPaths, ) -> Result<(PathBuf, Vec)> { - // First resolve any aliases - let resolved_channel = resolve_channel_alias(config_data, channel)?; + // 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)?; @@ -353,6 +351,7 @@ fn get_julia_path_from_channel( &resolved_channel, juliaupconfig_path, channel_info, + alias_args.clone(), ); } @@ -393,6 +392,7 @@ fn get_julia_path_from_channel( &resolved_channel, juliaupconfig_path, channel_info, + alias_args, ); } else { return Err(anyhow!( @@ -445,15 +445,17 @@ 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::AliasChannel { .. } => { bail!("Unexpected alias channel after resolution: {channel}"); } - JuliaupConfigChannel::LinkedChannel { command, args } => Ok(( - PathBuf::from(command), - args.as_ref().map_or_else(Vec::new, |v| v.clone()), - )), + 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) @@ -475,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, @@ -518,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/command_api.rs b/src/command_api.rs index 474ff943b..e20440c8d 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -45,13 +45,21 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { for (key, value) in &config_file.data.installed_channels { let curr = match &value { - JuliaupConfigChannel::AliasChannel { target, args: _ } => { + JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => { JuliaupChannelInfo { name: key.clone(), - file: format!("alias-to-{target}"), + 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: format!("alias to {target}"), - arch: String::new(), + version: version.clone(), + arch: "".to_string(), } } JuliaupConfigChannel::SystemChannel { version: fullversion } => { @@ -111,21 +119,13 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { 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(), } } }; diff --git a/src/command_remove.rs b/src/command_remove.rs index 13f336783..3a2a8b4db 100644 --- a/src/command_remove.rs +++ b/src/command_remove.rs @@ -43,12 +43,12 @@ pub fn run_command_remove(channel: &str, paths: &GlobalPaths) -> Result<()> { // Determine what type of channel is being removed for better messaging let channel_type = match channel_info { - JuliaupConfigChannel::AliasChannel { target, args: _ } => { + JuliaupConfigChannel::DirectDownloadChannel { .. } => "channel".to_string(), + JuliaupConfigChannel::SystemChannel { .. } => "channel".to_string(), + JuliaupConfigChannel::LinkedChannel { .. } => "linked channel".to_string(), + JuliaupConfigChannel::AliasChannel { target, .. } => { format!("alias (pointing to '{target}')") } - JuliaupConfigChannel::LinkedChannel { .. } => "linked channel".to_string(), - JuliaupConfigChannel::SystemChannel { .. } => "channel".to_string(), - JuliaupConfigChannel::DirectDownloadChannel { .. } => "channel".to_string(), }; if let JuliaupConfigChannel::DirectDownloadChannel { diff --git a/src/command_status.rs b/src/command_status.rs index b3b20d9b0..b60db3595 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -14,28 +14,93 @@ use cli_table::{ use itertools::Itertools; use numeric_sort::cmp; -fn get_alias_update_info( - target: &str, +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, -) -> Option { - // Check if the target channel has updates available - match config_file.data.installed_channels.get(target) { - Some(JuliaupConfigChannel::SystemChannel { version }) => { - match versiondb_data.available_channels.get(target) { - Some(channel) if channel.version != *version => { +) -> 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, } } - Some(JuliaupConfigChannel::DirectDownloadChannel { - local_etag, - server_etag, - .. - }) => (local_etag != server_etag).then(|| "Update available".to_string()), - _ => None, // Target channel doesn't exist or not updatable + 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)] @@ -57,90 +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}`") - } - JuliaupConfigChannel::AliasChannel { target, args } => match args { - Some(args) if !args.is_empty() => { - format!("Alias to `{target}` with args: {:?}", args) - } - _ => format!("Alias to `{target}`"), - }, - }, - update: { - let update_option = match i.1 { - JuliaupConfigChannel::SystemChannel { version } => { - match versiondb_data.available_channels.get(i.0) { - Some(channel) if &channel.version != version => { - Some(format!("Update to {} available", channel.version)) - } - _ => None, - } - } - JuliaupConfigChannel::LinkedChannel { .. } => None, - JuliaupConfigChannel::AliasChannel { target, args: _ } => { - get_alias_update_info(target, &config_file, &versiondb_data) - } - JuliaupConfigChannel::DirectDownloadChannel { - local_etag, - server_etag, - .. - } => (local_etag != server_etag).then(|| "Update available".to_string()), - }; - update_option.unwrap_or_default() - }, - } + .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 a6bc19733..95b695252 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -13,7 +13,7 @@ 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, args: _ }) => Ok(target.to_string()), + Some(JuliaupConfigChannel::AliasChannel { target, .. }) => Ok(target.to_string()), Some(_) => Ok(channel_name.to_string()), None => bail!("Channel '{}' not found", channel_name), } @@ -30,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); @@ -72,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.", @@ -84,46 +120,7 @@ fn update_channel( } } JuliaupConfigChannel::AliasChannel { .. } => { - unreachable!("Alias channels should be resolved before calling update_channel. This is a programming error."); - } - 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, - )?; - } - } + unreachable!("Alias channels should be resolved before calling update_channel. Please submit a bug report."); } } diff --git a/src/config_file.rs b/src/config_file.rs index 6052bb402..480671977 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -54,7 +54,7 @@ pub enum JuliaupConfigChannel { AliasChannel { #[serde(rename = "Target")] target: String, - #[serde(rename = "Args", skip_serializing_if = "Option::is_none")] + #[serde(rename = "Args")] args: Option>, }, } diff --git a/src/operations.rs b/src/operations.rs index 801b63e0f..9fd7f1b87 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -757,18 +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::AliasChannel { target: _, 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(); @@ -993,30 +982,23 @@ pub fn create_symlink( let updating = _remove_symlink(&symlink_path)?; match channel { - JuliaupConfigChannel::AliasChannel { target: _, args: _ } => { - // Aliases don't create symlinks directly, they are resolved at runtime - Ok(()) - } JuliaupConfigChannel::SystemChannel { version } => { create_system_channel_symlink(version, symlink_name, &symlink_path, paths, &updating) } - JuliaupConfigChannel::DirectDownloadChannel { - path, - url: _, - local_etag: _, - server_etag: _, - version, - } => create_direct_download_symlink( - path, - version, - symlink_name, - &symlink_path, - paths, - &updating, - ), + JuliaupConfigChannel::DirectDownloadChannel { path, version, .. } => { + create_direct_download_symlink( + path, + version, + symlink_name, + &symlink_path, + paths, + &updating, + ) + } 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() { diff --git a/tests/command_link.rs b/tests/command_link.rs index ed6964be4..30c9eeaae 100644 --- a/tests/command_link.rs +++ b/tests/command_link.rs @@ -105,6 +105,38 @@ fn command_link_alias_with_args_works() { .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(); From 7af0946c9db169bea9b25c8ca79528979c631e53 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 13 Sep 2025 08:19:32 -0400 Subject: [PATCH 42/43] fix `juliaup update` when an alias exists --- src/command_update.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/command_update.rs b/src/command_update.rs index 95b695252..adc9a7a06 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -138,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)?; } } From 851833ddc1dc9ec4f20a52ac61e39664f019d2cf Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sat, 13 Sep 2025 08:19:41 -0400 Subject: [PATCH 43/43] add test --- tests/command_update.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/command_update.rs b/tests/command_update.rs index 7ab630b39..63869dc7f 100644 --- a/tests/command_update.rs +++ b/tests/command_update.rs @@ -28,3 +28,21 @@ fn command_update_alias_works() { // 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(); + + env.juliaup().arg("update").assert().success(); +}