Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1cf4afe
Expand `link` to handle aliases
IanButterworth Aug 27, 2025
cfda620
rustfmt
IanButterworth Aug 27, 2025
3420332
clippy fixes
IanButterworth Aug 27, 2025
98acced
make the noop GC operation clearer
IanButterworth Aug 27, 2025
1baab25
Update src/operations.rs
IanButterworth Aug 27, 2025
d5489bb
Merge branch 'main' into ib/alias_links
IanButterworth Sep 7, 2025
a358beb
address review
IanButterworth Sep 7, 2025
a286279
break match into functions
IanButterworth Sep 7, 2025
9a7c486
fmt
IanButterworth Sep 7, 2025
687d3f4
fixes
IanButterworth Sep 7, 2025
152d070
switch to `update: Option<String>`
IanButterworth Sep 7, 2025
49c65f3
Update command_status.rs
IanButterworth Sep 7, 2025
804d717
also show if aliases have updates
IanButterworth Sep 7, 2025
b368acb
Merge branch 'main' into ib/alias_links
IanButterworth Sep 7, 2025
4790091
fix "sketchy" issue
IanButterworth Sep 7, 2025
3626e22
Apply suggestions from code review
IanButterworth Sep 7, 2025
fa50887
undos
IanButterworth Sep 7, 2025
689c372
break out into function
IanButterworth Sep 7, 2025
f73b2db
test tidy etc.
IanButterworth Sep 7, 2025
e84c810
fmt
IanButterworth Sep 7, 2025
561e855
suggestions
IanButterworth Sep 7, 2025
b267e1c
Update src/command_status.rs
IanButterworth Sep 7, 2025
de4e361
fmt
IanButterworth Sep 7, 2025
3ae53b4
Create test/utils.rs and use it
IanButterworth Sep 7, 2025
1eddf53
Apply suggestions from code review
IanButterworth Sep 7, 2025
cfb8fe5
fmt
IanButterworth Sep 7, 2025
1b027ba
Apply suggestions from code review
IanButterworth Sep 7, 2025
8e01eea
move import to top
IanButterworth Sep 7, 2025
f6bf3a1
rm unused function
IanButterworth Sep 7, 2025
e53a14e
fmt
IanButterworth Sep 7, 2025
a8a8469
fix clippy errors
IanButterworth Sep 7, 2025
9c4cb25
clippy fix
IanButterworth Sep 7, 2025
9261f05
more clippy fixes
IanButterworth Sep 7, 2025
452bc2d
fix clippy setup
IanButterworth Sep 7, 2025
4052c42
mark utils as allowed dead code
IanButterworth Sep 7, 2025
5a9336f
dead code fix
IanButterworth Sep 7, 2025
4c7e409
Refactor tests to use TestEnv (#1241)
MilesCranmer Sep 7, 2025
e3b3eb4
tweak explanation
IanButterworth Sep 7, 2025
bd15d3d
add support for args on link aliases
IanButterworth Sep 7, 2025
1ef2b3c
turn bail into unreachable
IanButterworth Sep 7, 2025
a0e276a
make test stricter
IanButterworth Sep 7, 2025
5546829
fix test
IanButterworth Sep 7, 2025
133b97a
Fixes for alias feature (#1242)
MilesCranmer Sep 8, 2025
e13976f
Merge branch 'main' into ib/alias_links
IanButterworth Sep 10, 2025
7af0946
fix `juliaup update` when an alias exists
IanButterworth Sep 13, 2025
851833d
add test
IanButterworth Sep 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/clippy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
87 changes: 51 additions & 36 deletions src/bin/julialauncher.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use console::{style, Term};
use dialoguer::Select;
use is_terminal::IsTerminal;
Expand Down Expand Up @@ -333,29 +333,38 @@ fn get_julia_path_from_channel(
juliaup_channel_source: JuliaupChannelSource,
paths: &juliaup::global_paths::GlobalPaths,
) -> Result<(PathBuf, Vec<String>)> {
let channel_valid = is_valid_channel(versions_db, &channel.to_string())?;
// First check if the channel is an alias and extract its args
let (resolved_channel, alias_args) = match config_data.installed_channels.get(channel) {
Some(JuliaupConfigChannel::AliasChannel { target, args }) => {
(target.to_string(), args.clone().unwrap_or_default())
}
_ => (channel.to_string(), Vec::new()),
};

let channel_valid = is_valid_channel(versions_db, &resolved_channel)?;

// First check if the channel is already installed
if let Some(channel_info) = config_data.installed_channels.get(channel) {
if let Some(channel_info) = config_data.installed_channels.get(&resolved_channel) {
return get_julia_path_from_installed_channel(
versions_db,
config_data,
channel,
&resolved_channel,
juliaupconfig_path,
channel_info,
alias_args.clone(),
);
}

// Handle auto-installation for command line channel selection
if let JuliaupChannelSource::CmdLine = juliaup_channel_source {
if channel_valid || is_pr_channel(channel) {
if channel_valid || is_pr_channel(&resolved_channel) {
// Check the user's auto-install preference
let should_auto_install = match config_data.settings.auto_install_channels {
Some(auto_install) => auto_install, // User has explicitly set a preference
None => {
// User hasn't set a preference - prompt in interactive mode, default to false in non-interactive
if is_interactive() {
handle_auto_install_prompt(channel, paths)?
handle_auto_install_prompt(&resolved_channel, paths)?
} else {
false
}
Expand All @@ -365,25 +374,29 @@ fn get_julia_path_from_channel(
if should_auto_install {
// Install the channel using juliaup
let is_automatic = config_data.settings.auto_install_channels == Some(true);
spawn_juliaup_add(channel, paths, is_automatic)?;
spawn_juliaup_add(&resolved_channel, paths, is_automatic)?;

// Reload the config to get the newly installed channel
let updated_config_file = load_config_db(paths, None)
.with_context(|| "Failed to reload configuration after installing channel.")?;

if let Some(channel_info) = updated_config_file.data.installed_channels.get(channel)
{
let updated_channel_info = updated_config_file
.data
.installed_channels
.get(&resolved_channel);

if let Some(channel_info) = updated_channel_info {
return get_julia_path_from_installed_channel(
versions_db,
&updated_config_file.data,
channel,
&resolved_channel,
juliaupconfig_path,
channel_info,
alias_args,
);
} else {
return Err(anyhow!(
"Channel '{}' was installed but could not be found in configuration.",
channel
"Channel '{resolved_channel}' was installed but could not be found in configuration."
));
}
}
Expand All @@ -395,32 +408,32 @@ fn get_julia_path_from_channel(
let error = match juliaup_channel_source {
JuliaupChannelSource::CmdLine => {
if channel_valid {
UserError { msg: format!("`{}` is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) }
} else if is_pr_channel(channel) {
UserError { msg: format!("`{}` is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) }
UserError { msg: format!("`{resolved_channel}` is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
} else if is_pr_channel(&resolved_channel) {
UserError { msg: format!("`{resolved_channel}` is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") }
} else {
UserError { msg: format!("Invalid Juliaup channel `{}`. Please run `juliaup list` to get a list of valid channels and versions.", channel) }
UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}`. Please run `juliaup list` to get a list of valid channels and versions.") }
}
},
JuliaupChannelSource::EnvVar=> {
if channel_valid {
UserError { msg: format!("`{}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) }
} else if is_pr_channel(channel) {
UserError { msg: format!("`{}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) }
UserError { msg: format!("`{resolved_channel}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
} else if is_pr_channel(&resolved_channel) {
UserError { msg: format!("`{resolved_channel}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") }
} else {
UserError { msg: format!("Invalid Juliaup channel `{}` from environment variable JULIAUP_CHANNEL. Please run `juliaup list` to get a list of valid channels and versions.", channel) }
UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}` from environment variable JULIAUP_CHANNEL. Please run `juliaup list` to get a list of valid channels and versions.") }
}
},
JuliaupChannelSource::Override=> {
if channel_valid {
UserError { msg: format!("`{}` from directory override is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) }
} else if is_pr_channel(channel){
UserError { msg: format!("`{}` from directory override is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) }
UserError { msg: format!("`{resolved_channel}` from directory override is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
} else if is_pr_channel(&resolved_channel) {
UserError { msg: format!("`{resolved_channel}` from directory override is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") }
} else {
UserError { msg: format!("Invalid Juliaup channel `{}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.", channel) }
UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.") }
}
},
JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) }
JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{resolved_channel}` is not installed.") }
};

Err(error.into())
Expand All @@ -432,22 +445,24 @@ fn get_julia_path_from_installed_channel(
channel: &str,
juliaupconfig_path: &Path,
channel_info: &JuliaupConfigChannel,
alias_args: Vec<String>,
) -> Result<(PathBuf, Vec<String>)> {
match channel_info {
JuliaupConfigChannel::LinkedChannel { command, args } => Ok((
PathBuf::from(command),
args.as_ref().map_or_else(Vec::new, |v| v.clone()),
)),
JuliaupConfigChannel::AliasChannel { .. } => {
bail!("Unexpected alias channel after resolution: {channel}");
}
JuliaupConfigChannel::LinkedChannel { command, args } => {
let mut combined_args = alias_args;
combined_args.extend(args.as_ref().map_or_else(Vec::new, |v| v.clone()));
Ok((PathBuf::from(command), combined_args))
}
JuliaupConfigChannel::SystemChannel { version } => {
let path = &config_data
.installed_versions.get(version)
.ok_or_else(|| anyhow!("The juliaup configuration is in an inconsistent state, the channel {} is pointing to Julia version {}, which is not installed.", channel, version))?.path;
.ok_or_else(|| anyhow!("The juliaup configuration is in an inconsistent state, the channel {channel} is pointing to Julia version {version}, which is not installed."))?.path;

check_channel_uptodate(channel, version, versions_db).with_context(|| {
format!(
"The Julia launcher failed while checking whether the channel {} is up-to-date.",
channel
)
format!("The Julia launcher failed while checking whether the channel {channel} is up-to-date.")
})?;
let absolute_path = juliaupconfig_path
.parent()
Expand All @@ -462,7 +477,7 @@ fn get_julia_path_from_installed_channel(
juliaupconfig_path.display()
)
})?;
Ok((absolute_path.into_path_buf(), Vec::new()))
Ok((absolute_path.into_path_buf(), alias_args))
}
JuliaupConfigChannel::DirectDownloadChannel {
path,
Expand Down Expand Up @@ -505,7 +520,7 @@ fn get_julia_path_from_installed_channel(
juliaupconfig_path.display()
)
})?;
Ok((absolute_path.into_path_buf(), Vec::new()))
Ok((absolute_path.into_path_buf(), alias_args))
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/bin/juliaup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
7 changes: 5 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ pub enum Juliaup {
Default { channel: String },
/// Add a specific Julia version or channel to your system. Access via `julia +{channel}` e.g. `julia +1.6`
Add { channel: String },
/// Link an existing Julia binary to a custom channel name
/// Link an existing Julia binary or channel to a custom channel name
Link {
/// Name of the new channel to create
channel: String,
file: String,
/// Path to Julia binary, or +{channel} to create an alias
target: String,
/// Additional arguments for the Julia binary
args: Vec<String>,
},
/// List all available channels
Expand Down
64 changes: 33 additions & 31 deletions src/command_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,32 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> {
"Failed to load configuration file while running the getconfig1 API command."
})?;

for (key, value) in config_file.data.installed_channels {
let curr = match value {
JuliaupConfigChannel::SystemChannel {
version: fullversion,
} => {
let (platform, mut version) = parse_versionstring(&fullversion)
for (key, value) in &config_file.data.installed_channels {
let curr = match &value {
JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => {
JuliaupChannelInfo {
name: key.clone(),
file: paths.juliauphome
.join(path)
.join("bin")
.join(format!("julia{}", std::env::consts::EXE_SUFFIX))
.normalize()
.with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")?
.into_path_buf()
.to_string_lossy()
.to_string(),
args: Vec::new(),
version: version.clone(),
arch: "".to_string(),
}
}
JuliaupConfigChannel::SystemChannel { version: fullversion } => {
let (platform, mut version) = parse_versionstring(fullversion)
.with_context(|| "Encountered invalid version string in the configuration file while running the getconfig1 API command.")?;

version.build = semver::BuildMetadata::EMPTY;

match config_file.data.installed_versions.get(&fullversion) {
match config_file.data.installed_versions.get(fullversion) {
Some(channel) => JuliaupChannelInfo {
name: key.clone(),
file: paths.juliauphome
Expand All @@ -73,15 +88,10 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> {
}
}
JuliaupConfigChannel::LinkedChannel { command, args } => {
let mut new_args: Vec<String> = Vec::new();

for i in args.as_ref().unwrap() {
new_args.push(i.to_string());
}

let mut new_args = args.clone().unwrap_or_default();
new_args.push("--version".to_string());

let res = std::process::Command::new(&command)
let res = std::process::Command::new(command)
.args(&new_args)
.output();

Expand All @@ -101,36 +111,28 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> {
JuliaupChannelInfo {
name: key.clone(),
file: command.clone(),
args: args.unwrap_or_default(),
args: args.clone().unwrap_or_default(),
version: version.to_string(),
arch: "".to_string(),
arch: String::new(),
}
}
Err(_) => continue,
}
}
JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => {
JuliaupConfigChannel::AliasChannel { target, args } => {
JuliaupChannelInfo {
name: key.clone(),
file: paths.juliauphome
.join(path)
.join("bin")
.join(format!("julia{}", std::env::consts::EXE_SUFFIX))
.normalize()
.with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")?
.into_path_buf()
.to_string_lossy()
.to_string(),
args: Vec::new(),
version: version.clone(),
arch: "".to_string(),
file: format!("alias-to-{target}"),
args: args.clone().unwrap_or_default(),
version: format!("alias to {target}"),
arch: String::new(),
}
}
};

match config_file.data.default {
Some(ref default_value) => {
if &key == default_value {
if key == default_value {
ret_value.default = Some(curr.clone());
} else {
ret_value.other_versions.push(curr);
Expand All @@ -146,7 +148,7 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> {
let j = serde_json::to_string(&ret_value)?;

// Print, write to a file, or send to an HTTP server.
println!("{}", j);
println!("{j}");

Ok(())
}
Loading
Loading