Skip to content

Commit 1a8243f

Browse files
Expand link to handle aliases. DRY tests mechanics. (#1237)
1 parent fd558ef commit 1a8243f

23 files changed

+1017
-781
lines changed

.github/workflows/clippy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ jobs:
3434
with:
3535
components: clippy
3636
- name: Run clippy
37-
run: cargo clippy --all-targets --features ${{ matrix.features }} -- -D warnings
37+
run: cargo clippy --workspace --features ${{ matrix.features }} -- -D warnings

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Here are some of the things you can do with `juliaup`:
116116
- `juliaup add 1.6.1~x86` installs the 32 bit version of Julia 1.6.1 on your system.
117117
- `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.
118118
- `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.
119+
- `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.
119120
- `juliaup self update` installs the latest version, which is necessary if new releases reach the beta channel, etc.
120121
- `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.
121122
- `juliaup override status` shows all configured directory overrides.

src/bin/julialauncher.rs

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::{anyhow, Context, Result};
1+
use anyhow::{anyhow, bail, Context, Result};
22
use console::{style, Term};
33
use dialoguer::Select;
44
use is_terminal::IsTerminal;
@@ -333,29 +333,38 @@ fn get_julia_path_from_channel(
333333
juliaup_channel_source: JuliaupChannelSource,
334334
paths: &juliaup::global_paths::GlobalPaths,
335335
) -> Result<(PathBuf, Vec<String>)> {
336-
let channel_valid = is_valid_channel(versions_db, &channel.to_string())?;
336+
// First check if the channel is an alias and extract its args
337+
let (resolved_channel, alias_args) = match config_data.installed_channels.get(channel) {
338+
Some(JuliaupConfigChannel::AliasChannel { target, args }) => {
339+
(target.to_string(), args.clone().unwrap_or_default())
340+
}
341+
_ => (channel.to_string(), Vec::new()),
342+
};
343+
344+
let channel_valid = is_valid_channel(versions_db, &resolved_channel)?;
337345

338346
// First check if the channel is already installed
339-
if let Some(channel_info) = config_data.installed_channels.get(channel) {
347+
if let Some(channel_info) = config_data.installed_channels.get(&resolved_channel) {
340348
return get_julia_path_from_installed_channel(
341349
versions_db,
342350
config_data,
343-
channel,
351+
&resolved_channel,
344352
juliaupconfig_path,
345353
channel_info,
354+
alias_args.clone(),
346355
);
347356
}
348357

349358
// Handle auto-installation for command line channel selection
350359
if let JuliaupChannelSource::CmdLine = juliaup_channel_source {
351-
if channel_valid || is_pr_channel(channel) {
360+
if channel_valid || is_pr_channel(&resolved_channel) {
352361
// Check the user's auto-install preference
353362
let should_auto_install = match config_data.settings.auto_install_channels {
354363
Some(auto_install) => auto_install, // User has explicitly set a preference
355364
None => {
356365
// User hasn't set a preference - prompt in interactive mode, default to false in non-interactive
357366
if is_interactive() {
358-
handle_auto_install_prompt(channel, paths)?
367+
handle_auto_install_prompt(&resolved_channel, paths)?
359368
} else {
360369
false
361370
}
@@ -365,25 +374,29 @@ fn get_julia_path_from_channel(
365374
if should_auto_install {
366375
// Install the channel using juliaup
367376
let is_automatic = config_data.settings.auto_install_channels == Some(true);
368-
spawn_juliaup_add(channel, paths, is_automatic)?;
377+
spawn_juliaup_add(&resolved_channel, paths, is_automatic)?;
369378

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

374-
if let Some(channel_info) = updated_config_file.data.installed_channels.get(channel)
375-
{
383+
let updated_channel_info = updated_config_file
384+
.data
385+
.installed_channels
386+
.get(&resolved_channel);
387+
388+
if let Some(channel_info) = updated_channel_info {
376389
return get_julia_path_from_installed_channel(
377390
versions_db,
378391
&updated_config_file.data,
379-
channel,
392+
&resolved_channel,
380393
juliaupconfig_path,
381394
channel_info,
395+
alias_args,
382396
);
383397
} else {
384398
return Err(anyhow!(
385-
"Channel '{}' was installed but could not be found in configuration.",
386-
channel
399+
"Channel '{resolved_channel}' was installed but could not be found in configuration."
387400
));
388401
}
389402
}
@@ -395,32 +408,32 @@ fn get_julia_path_from_channel(
395408
let error = match juliaup_channel_source {
396409
JuliaupChannelSource::CmdLine => {
397410
if channel_valid {
398-
UserError { msg: format!("`{}` is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) }
399-
} else if is_pr_channel(channel) {
400-
UserError { msg: format!("`{}` is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) }
411+
UserError { msg: format!("`{resolved_channel}` is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
412+
} else if is_pr_channel(&resolved_channel) {
413+
UserError { msg: format!("`{resolved_channel}` is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") }
401414
} else {
402-
UserError { msg: format!("Invalid Juliaup channel `{}`. Please run `juliaup list` to get a list of valid channels and versions.", channel) }
415+
UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}`. Please run `juliaup list` to get a list of valid channels and versions.") }
403416
}
404417
},
405418
JuliaupChannelSource::EnvVar=> {
406419
if channel_valid {
407-
UserError { msg: format!("`{}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) }
408-
} else if is_pr_channel(channel) {
409-
UserError { msg: format!("`{}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) }
420+
UserError { msg: format!("`{resolved_channel}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
421+
} else if is_pr_channel(&resolved_channel) {
422+
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.") }
410423
} else {
411-
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) }
424+
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.") }
412425
}
413426
},
414427
JuliaupChannelSource::Override=> {
415428
if channel_valid {
416-
UserError { msg: format!("`{}` from directory override is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) }
417-
} else if is_pr_channel(channel){
418-
UserError { msg: format!("`{}` from directory override is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) }
429+
UserError { msg: format!("`{resolved_channel}` from directory override is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
430+
} else if is_pr_channel(&resolved_channel) {
431+
UserError { msg: format!("`{resolved_channel}` from directory override is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") }
419432
} else {
420-
UserError { msg: format!("Invalid Juliaup channel `{}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.", channel) }
433+
UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.") }
421434
}
422435
},
423-
JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) }
436+
JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{resolved_channel}` is not installed.") }
424437
};
425438

426439
Err(error.into())
@@ -432,22 +445,24 @@ fn get_julia_path_from_installed_channel(
432445
channel: &str,
433446
juliaupconfig_path: &Path,
434447
channel_info: &JuliaupConfigChannel,
448+
alias_args: Vec<String>,
435449
) -> Result<(PathBuf, Vec<String>)> {
436450
match channel_info {
437-
JuliaupConfigChannel::LinkedChannel { command, args } => Ok((
438-
PathBuf::from(command),
439-
args.as_ref().map_or_else(Vec::new, |v| v.clone()),
440-
)),
451+
JuliaupConfigChannel::AliasChannel { .. } => {
452+
bail!("Unexpected alias channel after resolution: {channel}");
453+
}
454+
JuliaupConfigChannel::LinkedChannel { command, args } => {
455+
let mut combined_args = alias_args;
456+
combined_args.extend(args.as_ref().map_or_else(Vec::new, |v| v.clone()));
457+
Ok((PathBuf::from(command), combined_args))
458+
}
441459
JuliaupConfigChannel::SystemChannel { version } => {
442460
let path = &config_data
443461
.installed_versions.get(version)
444-
.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;
462+
.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;
445463

446464
check_channel_uptodate(channel, version, versions_db).with_context(|| {
447-
format!(
448-
"The Julia launcher failed while checking whether the channel {} is up-to-date.",
449-
channel
450-
)
465+
format!("The Julia launcher failed while checking whether the channel {channel} is up-to-date.")
451466
})?;
452467
let absolute_path = juliaupconfig_path
453468
.parent()
@@ -462,7 +477,7 @@ fn get_julia_path_from_installed_channel(
462477
juliaupconfig_path.display()
463478
)
464479
})?;
465-
Ok((absolute_path.into_path_buf(), Vec::new()))
480+
Ok((absolute_path.into_path_buf(), alias_args))
466481
}
467482
JuliaupConfigChannel::DirectDownloadChannel {
468483
path,
@@ -505,7 +520,7 @@ fn get_julia_path_from_installed_channel(
505520
juliaupconfig_path.display()
506521
)
507522
})?;
508-
Ok((absolute_path.into_path_buf(), Vec::new()))
523+
Ok((absolute_path.into_path_buf(), alias_args))
509524
}
510525
}
511526
}

src/bin/juliaup.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ fn main() -> Result<()> {
100100
Juliaup::Gc { prune_linked } => run_command_gc(prune_linked, &paths),
101101
Juliaup::Link {
102102
channel,
103-
file,
103+
target,
104104
args,
105-
} => run_command_link(&channel, &file, &args, &paths),
105+
} => run_command_link(&channel, &target, &args, &paths),
106106
Juliaup::List {} => run_command_list(&paths),
107107
Juliaup::Config(subcmd) => match subcmd {
108108
#[cfg(not(windows))]

src/cli.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ pub enum Juliaup {
2525
Default { channel: String },
2626
/// Add a specific Julia version or channel to your system. Access via `julia +{channel}` e.g. `julia +1.6`
2727
Add { channel: String },
28-
/// Link an existing Julia binary to a custom channel name
28+
/// Link an existing Julia binary or channel to a custom channel name
2929
Link {
30+
/// Name of the new channel to create
3031
channel: String,
31-
file: String,
32+
/// Path to Julia binary, or +{channel} to create an alias
33+
target: String,
34+
/// Additional arguments for the Julia binary
3235
args: Vec<String>,
3336
},
3437
/// List all available channels

src/command_api.rs

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,32 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> {
4343
"Failed to load configuration file while running the getconfig1 API command."
4444
})?;
4545

46-
for (key, value) in config_file.data.installed_channels {
47-
let curr = match value {
48-
JuliaupConfigChannel::SystemChannel {
49-
version: fullversion,
50-
} => {
51-
let (platform, mut version) = parse_versionstring(&fullversion)
46+
for (key, value) in &config_file.data.installed_channels {
47+
let curr = match &value {
48+
JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => {
49+
JuliaupChannelInfo {
50+
name: key.clone(),
51+
file: paths.juliauphome
52+
.join(path)
53+
.join("bin")
54+
.join(format!("julia{}", std::env::consts::EXE_SUFFIX))
55+
.normalize()
56+
.with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")?
57+
.into_path_buf()
58+
.to_string_lossy()
59+
.to_string(),
60+
args: Vec::new(),
61+
version: version.clone(),
62+
arch: "".to_string(),
63+
}
64+
}
65+
JuliaupConfigChannel::SystemChannel { version: fullversion } => {
66+
let (platform, mut version) = parse_versionstring(fullversion)
5267
.with_context(|| "Encountered invalid version string in the configuration file while running the getconfig1 API command.")?;
5368

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

56-
match config_file.data.installed_versions.get(&fullversion) {
71+
match config_file.data.installed_versions.get(fullversion) {
5772
Some(channel) => JuliaupChannelInfo {
5873
name: key.clone(),
5974
file: paths.juliauphome
@@ -73,15 +88,10 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> {
7388
}
7489
}
7590
JuliaupConfigChannel::LinkedChannel { command, args } => {
76-
let mut new_args: Vec<String> = Vec::new();
77-
78-
for i in args.as_ref().unwrap() {
79-
new_args.push(i.to_string());
80-
}
81-
91+
let mut new_args = args.clone().unwrap_or_default();
8292
new_args.push("--version".to_string());
8393

84-
let res = std::process::Command::new(&command)
94+
let res = std::process::Command::new(command)
8595
.args(&new_args)
8696
.output();
8797

@@ -101,36 +111,28 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> {
101111
JuliaupChannelInfo {
102112
name: key.clone(),
103113
file: command.clone(),
104-
args: args.unwrap_or_default(),
114+
args: args.clone().unwrap_or_default(),
105115
version: version.to_string(),
106-
arch: "".to_string(),
116+
arch: String::new(),
107117
}
108118
}
109119
Err(_) => continue,
110120
}
111121
}
112-
JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => {
122+
JuliaupConfigChannel::AliasChannel { target, args } => {
113123
JuliaupChannelInfo {
114124
name: key.clone(),
115-
file: paths.juliauphome
116-
.join(path)
117-
.join("bin")
118-
.join(format!("julia{}", std::env::consts::EXE_SUFFIX))
119-
.normalize()
120-
.with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")?
121-
.into_path_buf()
122-
.to_string_lossy()
123-
.to_string(),
124-
args: Vec::new(),
125-
version: version.clone(),
126-
arch: "".to_string(),
125+
file: format!("alias-to-{target}"),
126+
args: args.clone().unwrap_or_default(),
127+
version: format!("alias to {target}"),
128+
arch: String::new(),
127129
}
128130
}
129131
};
130132

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

148150
// Print, write to a file, or send to an HTTP server.
149-
println!("{}", j);
151+
println!("{j}");
150152

151153
Ok(())
152154
}

0 commit comments

Comments
 (0)