From 26a5afc4aeabde4a621f61d04bc5af796b2feacf Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:11:46 -0400 Subject: [PATCH 01/14] Add `alias` command --- README.md | 1 + src/bin/julialauncher.rs | 11 ++++++++ src/bin/juliaup.rs | 2 ++ src/cli.rs | 2 ++ src/command_alias.rs | 55 ++++++++++++++++++++++++++++++++++++++++ src/command_api.rs | 34 +++++++++++++++++++++++++ src/command_status.rs | 4 +++ src/command_update.rs | 11 ++++++++ src/config_file.rs | 4 +++ src/lib.rs | 1 + src/operations.rs | 14 ++++++++++ 11 files changed, 139 insertions(+) create mode 100644 src/command_alias.rs diff --git a/README.md b/README.md index 049347fc..1bafdb51 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 alias r release` configures the `r` channel to act as if you had requested the `release` channel. - `juliaup self update` installs the latest version, which is necessary if new releases reach the beta channel, etc. - `juliaup self uninstall` uninstalls Juliaup. Note that on some platforms this command is not available, in those situations one should use platform specific methods to uninstall Juliaup. - `juliaup override status` shows all configured directory overrides. diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index bd544167..d4fc952e 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -217,6 +217,17 @@ fn get_julia_path_from_channel( args.as_ref().map_or_else(Vec::new, |v| v.clone()), )) } + JuliaupConfigChannel::AliasedChannel { + channel: newchannel, + } => { + return get_julia_path_from_channel( + versions_db, + config_data, + newchannel, + juliaupconfig_path, + juliaup_channel_source, + ) + } JuliaupConfigChannel::SystemChannel { version } => { let path = &config_data .installed_versions.get(version) diff --git a/src/bin/juliaup.rs b/src/bin/juliaup.rs index bc2ab118..b82f9d0a 100644 --- a/src/bin/juliaup.rs +++ b/src/bin/juliaup.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use clap::Parser; use juliaup::cli::{ConfigSubCmd, Juliaup, OverrideSubCmd, SelfSubCmd}; +use juliaup::command_alias::run_command_alias; use juliaup::command_api::run_command_api; use juliaup::command_completions::run_command_completions; #[cfg(not(windows))] @@ -102,6 +103,7 @@ fn main() -> Result<()> { file, args, } => run_command_link(&channel, &file, &args, &paths), + Juliaup::Alias { alias, channel } => run_command_alias(&alias, &channel, &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 5d7bc76a..df2311d5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -18,6 +18,8 @@ pub enum Juliaup { file: String, args: Vec, }, + /// Link an existing juliaup channel to a custom channel name + Alias { alias: String, channel: String }, /// List all available channels #[clap(alias = "ls")] List {}, diff --git a/src/command_alias.rs b/src/command_alias.rs new file mode 100644 index 00000000..a8c2ecac --- /dev/null +++ b/src/command_alias.rs @@ -0,0 +1,55 @@ +use crate::config_file::JuliaupConfigChannel; +use crate::config_file::{load_mut_config_db, save_config_db}; +use crate::global_paths::GlobalPaths; +#[cfg(not(windows))] +use crate::operations::create_symlink; +use crate::operations::is_valid_channel; +use crate::versions_file::load_versions_db; +use anyhow::{bail, Context, Result}; + +pub fn run_command_alias(alias: &str, channel: &str, paths: &GlobalPaths) -> Result<()> { + println!("alias: {}, channel: {}", alias, channel); + let mut config_file = load_mut_config_db(paths) + .with_context(|| "`alias` command failed to load configuration data.")?; + + let versiondb_data = + load_versions_db(paths).with_context(|| "`alias` command failed to load versions db.")?; + + if config_file.data.installed_channels.contains_key(alias) { + bail!("Channel name `{}` is already used.", alias) + } + + if !config_file.data.installed_channels.contains_key(channel) { + eprintln!("WARNING: The channel `{}` does not currently exist. If this was a mistake, run `juliaup remove {}` and try again.", channel, alias); + } + + if is_valid_channel(&versiondb_data, &alias.to_string())? { + eprintln!("WARNING: The channel name `{}` is also a system channel. By creating an alias to this channel you are hiding this system channel.", alias); + } + + config_file.data.installed_channels.insert( + alias.to_string(), + JuliaupConfigChannel::AliasedChannel { + channel: channel.to_string(), + }, + ); + + #[cfg(not(windows))] + let create_symlinks = config_file.data.settings.create_channel_symlinks; + + save_config_db(&mut config_file) + .with_context(|| "`alias` command failed to save configuration db.")?; + + #[cfg(not(windows))] + if create_symlinks { + create_symlink( + &JuliaupConfigChannel::AliasedChannel { + channel: channel.to_string(), + }, + &format!("julia-{}", channel), + paths, + )?; + } + + Ok(()) +} diff --git a/src/command_api.rs b/src/command_api.rs index 98c81ef8..62c42925 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -109,6 +109,40 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { Err(_) => continue, } } + // TODO: fix + JuliaupConfigChannel::AliasedChannel { channel } => { + let mut new_args: Vec = Vec::new(); + + new_args.push("--version".to_string()); + + let res = std::process::Command::new(&command) + .args(&new_args) + .output(); + + match res { + Ok(output) => { + let expected_version_prefix = "julia version "; + + let trimmed_string = std::str::from_utf8(&output.stdout).unwrap().trim(); + + if !trimmed_string.starts_with(expected_version_prefix) { + continue; + } + + let version = + Version::parse(&trimmed_string[expected_version_prefix.len()..])?; + + JuliaupChannelInfo { + name: key.clone(), + file: version.to_string(), + args: Vec::new(), + version: version.to_string(), + arch: "".to_string(), + } + } + Err(_) => continue, + } + } JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => { JuliaupChannelInfo { name: key.clone(), diff --git a/src/command_status.rs b/src/command_status.rs index 14ec58df..a8944a30 100644 --- a/src/command_status.rs +++ b/src/command_status.rs @@ -86,6 +86,9 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { } format!("Linked to `{}`", combined_command) } + JuliaupConfigChannel::AliasedChannel { channel } => { + format!("Aliased to channel `{}`", channel) + } }, update: match i.1 { JuliaupConfigChannel::SystemChannel { version } => { @@ -104,6 +107,7 @@ pub fn run_command_status(paths: &GlobalPaths) -> Result<()> { command: _, args: _, } => "".to_string(), + JuliaupConfigChannel::AliasedChannel { channel: _ } => "".to_string(), JuliaupConfigChannel::DirectDownloadChannel { path: _, url: _, diff --git a/src/command_update.rs b/src/command_update.rs index ac0a3a48..c0427ea7 100644 --- a/src/command_update.rs +++ b/src/command_update.rs @@ -75,6 +75,17 @@ fn update_channel( ); } } + JuliaupConfigChannel::AliasedChannel { + channel: realchannel, + } => { + return update_channel( + config_db, + realchannel, + version_db, + ignore_non_updatable_channel, + paths, + ) + } JuliaupConfigChannel::DirectDownloadChannel { path, url, diff --git a/src/config_file.rs b/src/config_file.rs index 07f11d7b..041f65f3 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -51,6 +51,10 @@ pub enum JuliaupConfigChannel { #[serde(rename = "Args")] args: Option>, }, + AliasedChannel { + #[serde(rename = "Channel")] + channel: String, + }, } #[derive(Serialize, Deserialize, Clone, PartialEq)] diff --git a/src/lib.rs b/src/lib.rs index 92675479..23d33f9e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use anyhow::Context; pub mod cli; pub mod command_add; +pub mod command_alias; pub mod command_api; pub mod command_completions; pub mod command_config_backgroundselfupdate; diff --git a/src/operations.rs b/src/operations.rs index af7cfcb6..b9f5358b 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -759,6 +759,7 @@ pub fn garbage_collect_versions( command: _, args: _, } => true, + JuliaupConfigChannel::AliasedChannel { channel: _ } => true, JuliaupConfigChannel::DirectDownloadChannel { path: _, url: _, @@ -969,6 +970,19 @@ pub fn create_symlink( ) })?; } + JuliaupConfigChannel::AliasedChannel { + channel: newchannel, + } => { + let config_file = load_config_db(paths, None) + .with_context(|| "Configuration file loading failed while trying to create symlink for aliased channel.")?; + if config_file.data.installed_channels.contains_key(newchannel) { + return create_symlink( + config_file.data.installed_channels.get(newchannel).unwrap(), + symlink_name, + paths, + ); + } + } }; if updating.is_none() { From ad208f4a6ecb413dfefe0cae5579faa280d9bc27 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Tue, 5 Nov 2024 20:15:01 -0400 Subject: [PATCH 02/14] "Fix" `api` command --- src/command_api.rs | 228 ++++++++++++++++++++++----------------------- src/config_file.rs | 1 + 2 files changed, 114 insertions(+), 115 deletions(-) diff --git a/src/command_api.rs b/src/command_api.rs index 62c42925..3c144ced 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -1,5 +1,6 @@ use crate::config_file::load_config_db; use crate::config_file::JuliaupConfigChannel; +use crate::config_file::JuliaupReadonlyConfigFile; use crate::global_paths::GlobalPaths; use crate::utils::parse_versionstring; use anyhow::{bail, Context, Result}; @@ -29,137 +30,134 @@ pub struct JuliaupApiGetinfoReturn { pub other_versions: Vec, } -pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { - if command != "getconfig1" { - bail!("Wrong API command."); - } - - let mut ret_value = JuliaupApiGetinfoReturn { - default: None, - other_versions: Vec::new(), - }; - - let config_file = load_config_db(paths, None).with_context(|| { - "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) - .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) { - Some(channel) => JuliaupChannelInfo { - name: key.clone(), - file: paths.juliauphome - .join(&channel.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.to_string(), - arch: platform - }, - None => bail!("The channel '{}' is configured as a system channel, but no such channel exists in the versions database.", key) - } +fn get_channel_info( + name: &String, + channel: &JuliaupConfigChannel, + config_file: &JuliaupReadonlyConfigFile, + paths: &GlobalPaths, +) -> Result> { + match channel { + 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.clone()) { + Some(channel) => return Ok(Some(JuliaupChannelInfo { + name: name.clone(), + file: paths.juliauphome + .join(&channel.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.to_string(), + arch: platform + })), + None => bail!("The channel '{}' is configured as a system channel, but no such channel exists in the versions database.", name) } - JuliaupConfigChannel::LinkedChannel { command, args } => { - let mut new_args: Vec = Vec::new(); + } + JuliaupConfigChannel::LinkedChannel { command, args } => { + let mut new_args: Vec = Vec::new(); - for i in args.as_ref().unwrap() { - new_args.push(i.to_string()); - } + for i in args.as_ref().unwrap() { + new_args.push(i.to_string()); + } - new_args.push("--version".to_string()); + new_args.push("--version".to_string()); - let res = std::process::Command::new(&command) - .args(&new_args) - .output(); + let res = std::process::Command::new(&command) + .args(&new_args) + .output(); - match res { - Ok(output) => { - let expected_version_prefix = "julia version "; + match res { + Ok(output) => { + let expected_version_prefix = "julia version "; - let trimmed_string = std::str::from_utf8(&output.stdout).unwrap().trim(); + let trimmed_string = std::str::from_utf8(&output.stdout).unwrap().trim(); - if !trimmed_string.starts_with(expected_version_prefix) { - continue; - } + if !trimmed_string.starts_with(expected_version_prefix) { + return Ok(None); + } - let version = - Version::parse(&trimmed_string[expected_version_prefix.len()..])?; + let version = + Version::parse(&trimmed_string[expected_version_prefix.len()..])?; - JuliaupChannelInfo { - name: key.clone(), - file: command.clone(), - args: args.unwrap_or_default(), - version: version.to_string(), - arch: "".to_string(), - } - } - Err(_) => continue, + Ok(Some(JuliaupChannelInfo { + name: name.clone(), + file: command.clone(), + args: args.clone().unwrap_or_default(), + version: version.to_string(), + arch: "".to_string(), + })) } + Err(_) => return Ok(None), } - // TODO: fix - JuliaupConfigChannel::AliasedChannel { channel } => { - let mut new_args: Vec = Vec::new(); - - new_args.push("--version".to_string()); - - let res = std::process::Command::new(&command) - .args(&new_args) - .output(); + } + // TODO: fix + JuliaupConfigChannel::AliasedChannel { channel } => { + let real_channel_info = get_channel_info(name, config_file.data.installed_channels.get(channel).unwrap(), config_file, paths)?; + + match real_channel_info { + Some(info) => { + return Ok(Some(JuliaupChannelInfo { + name: name.clone(), + file: info.file, + args: info.args, + version: info.version, + arch: info.arch, + })) + } + None => return Ok(None), + } + } + JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => { + return Ok(Some(JuliaupChannelInfo { + name: name.clone(), + file: paths.juliauphome + .join(path) + .join("bin") + .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) + .normalize() + .with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")? + .into_path_buf() + .to_string_lossy() + .to_string(), + args: Vec::new(), + version: version.clone(), + arch: "".to_string(), + })) + } + } +} - match res { - Ok(output) => { - let expected_version_prefix = "julia version "; +pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> { + if command != "getconfig1" { + bail!("Wrong API command."); + } - let trimmed_string = std::str::from_utf8(&output.stdout).unwrap().trim(); + let mut ret_value = JuliaupApiGetinfoReturn { + default: None, + other_versions: Vec::new(), + }; - if !trimmed_string.starts_with(expected_version_prefix) { - continue; - } + let config_file = load_config_db(paths, None).with_context(|| { + "Failed to load configuration file while running the getconfig1 API command." + })?; - let version = - Version::parse(&trimmed_string[expected_version_prefix.len()..])?; + let other_conf = config_file.clone(); - JuliaupChannelInfo { - name: key.clone(), - file: version.to_string(), - args: Vec::new(), - version: version.to_string(), - arch: "".to_string(), - } - } - Err(_) => continue, - } - } - JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => { - JuliaupChannelInfo { - name: key.clone(), - file: paths.juliauphome - .join(path) - .join("bin") - .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) - .normalize() - .with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")? - .into_path_buf() - .to_string_lossy() - .to_string(), - args: Vec::new(), - version: version.clone(), - arch: "".to_string(), - } - } + for (key, value) in config_file.data.installed_channels { + let curr = match get_channel_info(&key, &value, &other_conf, paths)? { + Some(channel_info) => channel_info, + None => continue, }; match config_file.data.default { diff --git a/src/config_file.rs b/src/config_file.rs index 041f65f3..fb3325ae 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -140,6 +140,7 @@ pub struct JuliaupConfigFile { pub self_data: JuliaupSelfConfig, } +#[derive(Clone)] pub struct JuliaupReadonlyConfigFile { pub data: JuliaupConfig, #[cfg(feature = "selfupdate")] From 285385625faa0838f6a6647f17c71bbc0ffd1b4a Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Sun, 24 Nov 2024 18:58:54 -0400 Subject: [PATCH 03/14] Add test --- tests/command_alias.rs | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/command_alias.rs diff --git a/tests/command_alias.rs b/tests/command_alias.rs new file mode 100644 index 00000000..f3687059 --- /dev/null +++ b/tests/command_alias.rs @@ -0,0 +1,49 @@ +use assert_cmd::Command; + +#[test] +fn command_alias() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.6") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("julia") + .unwrap() + .arg("+1.10.6") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout("1.10.6"); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("alias") + .arg("testalias") + .arg("1.10.6") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("julia") + .unwrap() + .arg("+testalias") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout("1.10.6"); +} From 91a4b1f75c7ecf6232419964a0b32875b99ad713 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:30:54 -0400 Subject: [PATCH 04/14] Remove debug text --- src/command_alias.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/command_alias.rs b/src/command_alias.rs index a8c2ecac..685feb48 100644 --- a/src/command_alias.rs +++ b/src/command_alias.rs @@ -8,7 +8,6 @@ use crate::versions_file::load_versions_db; use anyhow::{bail, Context, Result}; pub fn run_command_alias(alias: &str, channel: &str, paths: &GlobalPaths) -> Result<()> { - println!("alias: {}, channel: {}", alias, channel); let mut config_file = load_mut_config_db(paths) .with_context(|| "`alias` command failed to load configuration data.")?; From 5306353d809b29ca5106b1bae2e226786ee10582 Mon Sep 17 00:00:00 2001 From: Christian Guinard <28689358+christiangnrd@users.noreply.github.com> Date: Wed, 28 May 2025 18:21:18 -0300 Subject: [PATCH 05/14] Test --- tests/command_add.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/command_add.rs b/tests/command_add.rs index 5b52f3b9..b4c454ad 100644 --- a/tests/command_add.rs +++ b/tests/command_add.rs @@ -25,6 +25,7 @@ fn command_add() { .success() .stdout(""); + #[cfg(not(target_os = "freebsd"))] Command::cargo_bin("juliaup") .unwrap() .arg("add") From 4fecee6660edc32f19e27fe2d51c12dad1b35dde Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 26 Aug 2025 16:18:06 -0400 Subject: [PATCH 06/14] add tests for identified issues --- tests/command_alias.rs | 177 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 172 insertions(+), 5 deletions(-) diff --git a/tests/command_alias.rs b/tests/command_alias.rs index f3687059..abba06f1 100644 --- a/tests/command_alias.rs +++ b/tests/command_alias.rs @@ -1,7 +1,80 @@ use assert_cmd::Command; +use predicates::str; #[test] -fn command_alias() { +fn command_alias_basic_functionality() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Create a basic alias pointing to an existing channel - just test the alias creation + Command::cargo_bin("juliaup") + .unwrap() + .arg("alias") + .arg("testalias") + .arg("release") // 'release' is always available in the version db + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + // Check that the alias appears in the list + Command::cargo_bin("juliaup") + .unwrap() + .arg("list") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(str::contains("testalias")); +} + +#[test] +fn command_alias_circular_reference_simple() { + let temp_dir = tempfile::tempdir().unwrap(); + let depot_path = temp_dir.path(); + + // Create two aliases that will form a circle + Command::cargo_bin("juliaup") + .unwrap() + .args(["alias", "alias1", "release"]) + .env("JULIAUP_DEPOT_PATH", depot_path) + .env("JULIA_DEPOT_PATH", depot_path) + .assert() + .success(); + + Command::cargo_bin("juliaup") + .unwrap() + .args(["alias", "alias2", "alias1"]) + .env("JULIAUP_DEPOT_PATH", depot_path) + .env("JULIA_DEPOT_PATH", depot_path) + .assert() + .success(); + + // Now manually modify the first alias to point to alias2, creating a cycle + // This bypasses the validation in the alias command + // Read the config file directly and modify it to create circular reference + let config_path = depot_path.join("config.toml"); + let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default(); + + // Replace the alias1 target to create circular reference + config_content = config_content.replace("alias1 = \"release\"", "alias1 = \"alias2\""); + std::fs::write(&config_path, config_content).unwrap(); + + // This demonstrates that circular references aren't properly handled - + // we expect this to fail gracefully rather than hang in infinite recursion + // Since we can't easily test infinite recursion without hanging the test, + // we'll verify that at least the symlink creation worked correctly first + Command::cargo_bin("juliaup") + .unwrap() + .args(["list"]) + .env("JULIAUP_DEPOT_PATH", depot_path) + .env("JULIA_DEPOT_PATH", depot_path) + .assert() + .success(); +} + +#[test] +fn command_alias_circular_reference() { let depot_dir = assert_fs::TempDir::new().unwrap(); Command::cargo_bin("juliaup") @@ -14,21 +87,62 @@ fn command_alias() { .success() .stdout(""); + // Create alias A -> B + Command::cargo_bin("juliaup") + .unwrap() + .arg("alias") + .arg("aliasA") + .arg("aliasB") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + // Create alias B -> A (circular reference) + Command::cargo_bin("juliaup") + .unwrap() + .arg("alias") + .arg("aliasB") + .arg("aliasA") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + // This should fail with circular reference error, not hang Command::cargo_bin("julia") .unwrap() - .arg("+1.10.6") + .arg("+aliasA") .arg("-e") .arg("print(VERSION)") .env("JULIA_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .timeout(std::time::Duration::from_secs(5)) + .assert() + .failure(); // This should fail gracefully, not timeout +} + +#[test] +fn command_alias_chaining() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.6") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() .success() - .stdout("1.10.6"); + .stdout(""); + // Create chain: aliasA -> aliasB -> 1.10.6 Command::cargo_bin("juliaup") .unwrap() .arg("alias") - .arg("testalias") + .arg("aliasB") .arg("1.10.6") .env("JULIA_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_DEPOT_PATH", depot_dir.path()) @@ -36,9 +150,21 @@ fn command_alias() { .success() .stdout(""); + Command::cargo_bin("juliaup") + .unwrap() + .arg("alias") + .arg("aliasA") + .arg("aliasB") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + // Should work through the chain Command::cargo_bin("julia") .unwrap() - .arg("+testalias") + .arg("+aliasA") .arg("-e") .arg("print(VERSION)") .env("JULIA_DEPOT_PATH", depot_dir.path()) @@ -47,3 +173,44 @@ fn command_alias() { .success() .stdout("1.10.6"); } + +#[cfg(not(windows))] +#[test] +fn command_alias_symlink_naming() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Add release channel first + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("release") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Create alias without symlinks enabled + Command::cargo_bin("juliaup") + .unwrap() + .arg("alias") + .arg("testalias") + .arg("release") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Verify the alias was created by checking it appears in the list + Command::cargo_bin("juliaup") + .unwrap() + .arg("list") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(str::contains("testalias")); + + // This test verifies that alias creation works. + // The actual symlink naming fix is tested by the code logic: + // In command_alias.rs line 47, we use alias_name instead of target_channel for symlinks +} From b08988b389c64bd5467b2e884a8a11e02b8fde13 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 26 Aug 2025 17:04:42 -0400 Subject: [PATCH 07/14] bugfix --- src/command_alias.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command_alias.rs b/src/command_alias.rs index 685feb48..d46ea422 100644 --- a/src/command_alias.rs +++ b/src/command_alias.rs @@ -45,7 +45,7 @@ pub fn run_command_alias(alias: &str, channel: &str, paths: &GlobalPaths) -> Res &JuliaupConfigChannel::AliasedChannel { channel: channel.to_string(), }, - &format!("julia-{}", channel), + &format!("julia-{}", alias), paths, )?; } From faee75fa50376a3275608afd8ad39d638025267d Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 26 Aug 2025 17:04:57 -0400 Subject: [PATCH 08/14] show aliases in `list` --- src/command_list.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/command_list.rs b/src/command_list.rs index 1a39e9ea..8016e332 100644 --- a/src/command_list.rs +++ b/src/command_list.rs @@ -1,5 +1,6 @@ use crate::operations::{channel_to_name, get_channel_variations}; use crate::{global_paths::GlobalPaths, versions_file::load_versions_db}; +use crate::config_file::{load_config_db, JuliaupConfigChannel}; use anyhow::{Context, Result}; use cli_table::{ format::{Border, HorizontalLine, Separator}, @@ -20,6 +21,9 @@ pub fn run_command_list(paths: &GlobalPaths) -> Result<()> { let versiondb_data = load_versions_db(paths).with_context(|| "`list` command failed to load versions db.")?; + let config_data = load_config_db(paths, None) + .with_context(|| "`list` command failed to load configuration data.")?; + let non_db_channels: Vec = (get_channel_variations("nightly")?) .into_iter() .chain(get_channel_variations("x.y-nightly")?) @@ -36,6 +40,23 @@ pub fn run_command_list(paths: &GlobalPaths) -> Result<()> { }) .collect(); + // Add user-created aliases + let alias_rows: Vec = config_data + .data + .installed_channels + .iter() + .filter_map(|(alias_name, channel_config)| { + if let JuliaupConfigChannel::AliasedChannel { channel } = channel_config { + Some(ChannelRow { + name: alias_name.clone(), + version: format!("alias -> {}", channel), + }) + } else { + None + } + }) + .collect(); + let rows_in_table: Vec<_> = versiondb_data .available_channels .iter() @@ -47,6 +68,7 @@ pub fn run_command_list(paths: &GlobalPaths) -> Result<()> { }) .sorted_by(|a, b| cmp(&a.name, &b.name)) .chain(non_db_rows) + .chain(alias_rows) .collect(); print_stdout( From fa1288b8aa573e5a4267f45ed5917625b4e8f9ef Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 26 Aug 2025 17:05:14 -0400 Subject: [PATCH 09/14] add circular alias detection --- src/bin/julialauncher.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index d4fc952e..04c5c611 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -175,6 +175,29 @@ fn get_julia_path_from_channel( juliaupconfig_path: &Path, juliaup_channel_source: JuliaupChannelSource, ) -> Result<(PathBuf, Vec)> { + get_julia_path_from_channel_impl( + versions_db, + config_data, + channel, + juliaupconfig_path, + juliaup_channel_source, + &mut std::collections::HashSet::new(), + ) +} + +fn get_julia_path_from_channel_impl( + versions_db: &JuliaupVersionDB, + config_data: &JuliaupConfig, + channel: &str, + juliaupconfig_path: &Path, + juliaup_channel_source: JuliaupChannelSource, + visited: &mut std::collections::HashSet, +) -> Result<(PathBuf, Vec)> { + // Check for circular references + if visited.contains(channel) { + return Err(anyhow!("Circular alias detected: alias chain contains a cycle involving '{}'", channel)); + } + visited.insert(channel.to_string()); let channel_valid = is_valid_channel(versions_db, &channel.to_string())?; let channel_info = config_data .installed_channels @@ -220,12 +243,13 @@ fn get_julia_path_from_channel( JuliaupConfigChannel::AliasedChannel { channel: newchannel, } => { - return get_julia_path_from_channel( + return get_julia_path_from_channel_impl( versions_db, config_data, newchannel, juliaupconfig_path, juliaup_channel_source, + visited, ) } JuliaupConfigChannel::SystemChannel { version } => { From fd4b25ad67a5211e5f03a0be4096bb0ab1ae1505 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 26 Aug 2025 17:10:23 -0400 Subject: [PATCH 10/14] rustfmt --- src/bin/julialauncher.rs | 5 ++++- src/bin/juliaup.rs | 4 +++- src/cli.rs | 6 +++--- src/command_list.rs | 4 ++-- src/command_status.rs | 2 +- src/operations.rs | 4 ++-- tests/command_alias.rs | 4 ++-- tests/command_completions_test.rs | 6 +++++- 8 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 04c5c611..12b7b7d1 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -195,7 +195,10 @@ fn get_julia_path_from_channel_impl( ) -> Result<(PathBuf, Vec)> { // Check for circular references if visited.contains(channel) { - return Err(anyhow!("Circular alias detected: alias chain contains a cycle involving '{}'", channel)); + return Err(anyhow!( + "Circular alias detected: alias chain contains a cycle involving '{}'", + channel + )); } visited.insert(channel.to_string()); let channel_valid = is_valid_channel(versions_db, &channel.to_string())?; diff --git a/src/bin/juliaup.rs b/src/bin/juliaup.rs index cd56e2a4..28e9850e 100644 --- a/src/bin/juliaup.rs +++ b/src/bin/juliaup.rs @@ -150,6 +150,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 a1c80e42..d79d2297 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, @@ -64,9 +64,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_list.rs b/src/command_list.rs index 8016e332..ab3fedf2 100644 --- a/src/command_list.rs +++ b/src/command_list.rs @@ -1,13 +1,13 @@ +use crate::config_file::{load_config_db, JuliaupConfigChannel}; use crate::operations::{channel_to_name, get_channel_variations}; use crate::{global_paths::GlobalPaths, versions_file::load_versions_db}; -use crate::config_file::{load_config_db, JuliaupConfigChannel}; use anyhow::{Context, Result}; 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 717f1595..6ac05c39 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/operations.rs b/src/operations.rs index 320ffd5d..f6f570f8 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -1663,7 +1663,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; @@ -1734,7 +1734,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_alias.rs b/tests/command_alias.rs index abba06f1..8a2eeb05 100644 --- a/tests/command_alias.rs +++ b/tests/command_alias.rs @@ -10,7 +10,7 @@ fn command_alias_basic_functionality() { .unwrap() .arg("alias") .arg("testalias") - .arg("release") // 'release' is always available in the version db + .arg("release") // 'release' is always available in the version db .env("JULIA_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .assert() @@ -55,7 +55,7 @@ fn command_alias_circular_reference_simple() { // Read the config file directly and modify it to create circular reference let config_path = depot_path.join("config.toml"); let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default(); - + // Replace the alias1 target to create circular reference config_content = config_content.replace("alias1 = \"release\"", "alias1 = \"alias2\""); std::fs::write(&config_path, config_content).unwrap(); diff --git a/tests/command_completions_test.rs b/tests/command_completions_test.rs index a4d87651..60eb129f 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", + ], ); } From 517adfc06f8f5605491f784547ba190f8bf019f0 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 26 Aug 2025 20:05:02 -0400 Subject: [PATCH 11/14] Update command_alias.rs Co-authored-by: Miles Cranmer --- src/command_alias.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/command_alias.rs b/src/command_alias.rs index d46ea422..109b357f 100644 --- a/src/command_alias.rs +++ b/src/command_alias.rs @@ -33,21 +33,20 @@ pub fn run_command_alias(alias: &str, channel: &str, paths: &GlobalPaths) -> Res }, ); - #[cfg(not(windows))] - let create_symlinks = config_file.data.settings.create_channel_symlinks; - save_config_db(&mut config_file) .with_context(|| "`alias` command failed to save configuration db.")?; #[cfg(not(windows))] - if create_symlinks { - create_symlink( - &JuliaupConfigChannel::AliasedChannel { - channel: channel.to_string(), - }, - &format!("julia-{}", alias), - paths, - )?; + { + if config_file.data.settings.create_channel_symlinks { + create_symlink( + &JuliaupConfigChannel::AliasedChannel { + channel: channel.to_string(), + }, + &format!("julia-{}", alias), + paths, + )?; + } } Ok(()) From ed63238312240a094ba06585687c1301f52ce86c Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 26 Aug 2025 20:20:21 -0400 Subject: [PATCH 12/14] Update operations.rs Co-authored-by: Miles Cranmer --- src/operations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/operations.rs b/src/operations.rs index f6f570f8..970bbf55 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -975,9 +975,9 @@ pub fn create_symlink( } => { let config_file = load_config_db(paths, None) .with_context(|| "Configuration file loading failed while trying to create symlink for aliased channel.")?; - if config_file.data.installed_channels.contains_key(newchannel) { + if let Some(channel_config) = config_file.data.installed_channels.get(newchannel) { return create_symlink( - config_file.data.installed_channels.get(newchannel).unwrap(), + channel_config, symlink_name, paths, ); From 60093bc2692d95680634b4f498baa4adba418b99 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 26 Aug 2025 21:07:59 -0400 Subject: [PATCH 13/14] Apply suggestions from code review Co-authored-by: Miles Cranmer --- src/bin/julialauncher.rs | 5 ++--- src/command_alias.rs | 2 +- src/command_api.rs | 26 +++++++++++++++----------- src/command_list.rs | 15 ++++++--------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 12b7b7d1..1b04859e 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -194,14 +194,13 @@ fn get_julia_path_from_channel_impl( visited: &mut std::collections::HashSet, ) -> Result<(PathBuf, Vec)> { // Check for circular references - if visited.contains(channel) { + if !visited.insert(channel.to_string()) { return Err(anyhow!( "Circular alias detected: alias chain contains a cycle involving '{}'", channel )); } - visited.insert(channel.to_string()); - let channel_valid = is_valid_channel(versions_db, &channel.to_string())?; + let channel_valid = is_valid_channel(versions_db, channel)?; let channel_info = config_data .installed_channels .get(channel) diff --git a/src/command_alias.rs b/src/command_alias.rs index 109b357f..e86d00a3 100644 --- a/src/command_alias.rs +++ b/src/command_alias.rs @@ -22,7 +22,7 @@ pub fn run_command_alias(alias: &str, channel: &str, paths: &GlobalPaths) -> Res eprintln!("WARNING: The channel `{}` does not currently exist. If this was a mistake, run `juliaup remove {}` and try again.", channel, alias); } - if is_valid_channel(&versiondb_data, &alias.to_string())? { + if is_valid_channel(&versiondb_data, alias)? { eprintln!("WARNING: The channel name `{}` is also a system channel. By creating an alias to this channel you are hiding this system channel.", alias); } diff --git a/src/command_api.rs b/src/command_api.rs index 3c144ced..bc157afc 100644 --- a/src/command_api.rs +++ b/src/command_api.rs @@ -103,17 +103,21 @@ fn get_channel_info( } // TODO: fix JuliaupConfigChannel::AliasedChannel { channel } => { - let real_channel_info = get_channel_info(name, config_file.data.installed_channels.get(channel).unwrap(), config_file, paths)?; - - match real_channel_info { - Some(info) => { - return Ok(Some(JuliaupChannelInfo { - name: name.clone(), - file: info.file, - args: info.args, - version: info.version, - arch: info.arch, - })) + match config_file.data.installed_channels.get(channel) { + Some(target_channel) => { + let real_channel_info = get_channel_info(name, target_channel, config_file, paths)?; + match real_channel_info { + Some(info) => { + return Ok(Some(JuliaupChannelInfo { + name: name.clone(), + file: info.file, + args: info.args, + version: info.version, + arch: info.arch, + })) + } + None => return Ok(None), + } } None => return Ok(None), } diff --git a/src/command_list.rs b/src/command_list.rs index ab3fedf2..9b77d256 100644 --- a/src/command_list.rs +++ b/src/command_list.rs @@ -45,15 +45,12 @@ pub fn run_command_list(paths: &GlobalPaths) -> Result<()> { .data .installed_channels .iter() - .filter_map(|(alias_name, channel_config)| { - if let JuliaupConfigChannel::AliasedChannel { channel } = channel_config { - Some(ChannelRow { - name: alias_name.clone(), - version: format!("alias -> {}", channel), - }) - } else { - None - } + .filter_map(|(alias_name, channel_config)| match channel_config { + JuliaupConfigChannel::AliasedChannel { channel } => Some(ChannelRow { + name: alias_name.clone(), + version: format!("alias -> {}", channel), + }), + _ => None, }) .collect(); From 09604ca39f9548fdc7235b6c40cba2da41a1ffec Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 26 Aug 2025 21:50:42 -0400 Subject: [PATCH 14/14] reverts --- src/bin/julialauncher.rs | 2 +- src/command_alias.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 1b04859e..1aa183ac 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -200,7 +200,7 @@ fn get_julia_path_from_channel_impl( channel )); } - let channel_valid = is_valid_channel(versions_db, channel)?; + let channel_valid = is_valid_channel(versions_db, &channel.to_string())?; let channel_info = config_data .installed_channels .get(channel) diff --git a/src/command_alias.rs b/src/command_alias.rs index e86d00a3..109b357f 100644 --- a/src/command_alias.rs +++ b/src/command_alias.rs @@ -22,7 +22,7 @@ pub fn run_command_alias(alias: &str, channel: &str, paths: &GlobalPaths) -> Res eprintln!("WARNING: The channel `{}` does not currently exist. If this was a mistake, run `juliaup remove {}` and try again.", channel, alias); } - if is_valid_channel(&versiondb_data, alias)? { + if is_valid_channel(&versiondb_data, &alias.to_string())? { eprintln!("WARNING: The channel name `{}` is also a system channel. By creating an alias to this channel you are hiding this system channel.", alias); }