diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index e66c7a3d..ed4aa032 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -1,8 +1,11 @@ use anyhow::{anyhow, Context, Result}; use console::{style, Term}; +use dialoguer::Select; use is_terminal::IsTerminal; use itertools::Itertools; -use juliaup::config_file::{load_config_db, JuliaupConfig, JuliaupConfigChannel}; +use juliaup::config_file::{ + load_config_db, load_mut_config_db, save_config_db, JuliaupConfig, JuliaupConfigChannel, +}; use juliaup::global_paths::get_paths; use juliaup::jsonstructs_versionsdb::JuliaupVersionDB; use juliaup::operations::{is_pr_channel, is_valid_channel}; @@ -128,6 +131,164 @@ fn run_selfupdate(_config_file: &juliaup::config_file::JuliaupReadonlyConfigFile Ok(()) } +fn is_interactive() -> bool { + // First check if we have TTY access - this is a prerequisite for interactivity + if !std::io::stdin().is_terminal() || !std::io::stderr().is_terminal() { + return false; + } + + // Even with TTY available, check if Julia is being invoked in a non-interactive way + let args: Vec = std::env::args().collect(); + + // Skip the first argument (program name) and any channel specification (+channel) + let mut julia_args = args.iter().skip(1); + + // Skip channel specification if present + if let Some(first_arg) = julia_args.clone().next() { + if first_arg.starts_with('+') { + julia_args.next(); // consume the +channel argument + } + } + + // Check for non-interactive usage patterns + for arg in julia_args { + match arg.as_str() { + // Expression evaluation is non-interactive + "-e" | "--eval" => return false, + // Reading from stdin pipe is non-interactive + "-" => return false, + // Print options are typically non-interactive + "-p" | "--print" => return false, + // License/version display is non-interactive + "-L" | "--license" => return false, + "-v" | "--version" => return false, + // Help is non-interactive + "-h" | "--help" => return false, + // Check if this looks like a Julia file (ends with .jl) + filename if filename.ends_with(".jl") && !filename.starts_with('-') => { + return false; + } + // Any other non-flag argument that doesn't start with '-' could be a script + filename if !filename.starts_with('-') && !filename.is_empty() => { + // This could be a script file, check if it exists as a file + if std::path::Path::new(filename).exists() { + return false; + } + } + _ => {} // Continue checking other arguments + } + } + + true +} + +fn handle_auto_install_prompt( + channel: &str, + paths: &juliaup::global_paths::GlobalPaths, +) -> Result { + // Check if we're in interactive mode + if !is_interactive() { + // Non-interactive mode, don't auto-install + return Ok(false); + } + + // Use dialoguer for a consistent UI experience + let selection = Select::new() + .with_prompt(format!( + "{} The Juliaup channel '{}' is not installed. Would you like to install it?", + style("Question:").yellow().bold(), + channel + )) + .item("Yes (install this time only)") + .item("Yes and remember my choice (always auto-install)") + .item("No") + .default(0) // Default to "Yes" + .interact()?; + + match selection { + 0 => { + // Just install for this time + Ok(true) + } + 1 => { + // Install and remember the preference + set_auto_install_preference(true, paths)?; + Ok(true) + } + 2 => { + // Don't install + Ok(false) + } + _ => { + // Should not happen with dialoguer, but default to no + Ok(false) + } + } +} + +fn set_auto_install_preference( + auto_install: bool, + paths: &juliaup::global_paths::GlobalPaths, +) -> Result<()> { + let mut config_file = load_mut_config_db(paths) + .with_context(|| "Failed to load configuration for setting auto-install preference.")?; + + config_file.data.settings.auto_install_channels = Some(auto_install); + + save_config_db(&mut config_file) + .with_context(|| "Failed to save auto-install preference to configuration.")?; + + eprintln!( + "{} Auto-install preference set to '{}'.", + style("Info:").cyan().bold(), + auto_install + ); + + Ok(()) +} + +fn spawn_juliaup_add( + channel: &str, + _paths: &juliaup::global_paths::GlobalPaths, + is_automatic: bool, +) -> Result<()> { + if is_automatic { + eprintln!( + "{} Installing Julia {} automatically per juliaup settings...", + style("Info:").cyan().bold(), + channel + ); + } else { + eprintln!( + "{} Installing Julia {} as requested...", + style("Info:").cyan().bold(), + channel + ); + } + + let juliaup_path = get_juliaup_path().with_context(|| "Failed to obtain juliaup path.")?; + + let status = std::process::Command::new(juliaup_path) + .args(["add", channel]) + .status() + .with_context(|| format!("Failed to spawn juliaup to install channel '{}'", channel))?; + + if status.success() { + eprintln!( + "{} Successfully installed Julia {}.", + style("Info:").cyan().bold(), + channel + ); + Ok(()) + } else { + Err(anyhow!( + "Failed to install channel '{}'. juliaup add command failed with exit code: {:?}", + channel, + status.code() + )) + } +} + fn check_channel_uptodate( channel: &str, current_version: &str, @@ -170,49 +331,113 @@ fn get_julia_path_from_channel( channel: &str, juliaupconfig_path: &Path, juliaup_channel_source: JuliaupChannelSource, + paths: &juliaup::global_paths::GlobalPaths, ) -> Result<(PathBuf, Vec)> { let channel_valid = is_valid_channel(versions_db, &channel.to_string())?; - let channel_info = config_data - .installed_channels - .get(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) } - } else { - UserError { msg: format!("Invalid Juliaup channel `{}`. Please run `juliaup list` to get a list of valid channels and versions.", 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) } - } 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) } - } - }, - 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) } + + // First check if the channel is already installed + if let Some(channel_info) = config_data.installed_channels.get(channel) { + return get_julia_path_from_installed_channel( + versions_db, + config_data, + channel, + juliaupconfig_path, + channel_info, + ); + } + + // Handle auto-installation for command line channel selection + if let JuliaupChannelSource::CmdLine = juliaup_channel_source { + if channel_valid || is_pr_channel(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)? } else { - UserError { msg: format!("Invalid Juliaup channel `{}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.", channel) } + false } - }, - JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) } - })?; + } + }; - match channel_info { - JuliaupConfigChannel::LinkedChannel { command, args } => { - Ok(( - PathBuf::from(command), - args.as_ref().map_or_else(Vec::new, |v| v.clone()), - )) + 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)?; + + // 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) + { + return get_julia_path_from_installed_channel( + versions_db, + &updated_config_file.data, + channel, + juliaupconfig_path, + channel_info, + ); + } else { + return Err(anyhow!( + "Channel '{}' was installed but could not be found in configuration.", + channel + )); + } + } + // If we reach here, either installation failed or user declined } + } + + // Original error handling for non-command-line sources or invalid channels + 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) } + } else { + UserError { msg: format!("Invalid Juliaup channel `{}`. Please run `juliaup list` to get a list of valid channels and versions.", 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) } + } 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) } + } + }, + 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) } + } else { + UserError { msg: format!("Invalid Juliaup channel `{}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.", channel) } + } + }, + JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) } + }; + + Err(error.into()) +} + +fn get_julia_path_from_installed_channel( + versions_db: &JuliaupVersionDB, + config_data: &JuliaupConfig, + channel: &str, + juliaupconfig_path: &Path, + channel_info: &JuliaupConfigChannel, +) -> Result<(PathBuf, Vec)> { + match channel_info { + JuliaupConfigChannel::LinkedChannel { command, args } => Ok(( + PathBuf::from(command), + args.as_ref().map_or_else(Vec::new, |v| v.clone()), + )), JuliaupConfigChannel::SystemChannel { version } => { let path = &config_data .installed_versions.get(version) @@ -354,6 +579,7 @@ fn run_app() -> Result { &julia_channel_to_use, &paths.juliaupconfig, juliaup_channel_source, + &paths, ) .with_context(|| { format!( diff --git a/src/bin/juliaup.rs b/src/bin/juliaup.rs index 0b5ee6df..e2660461 100644 --- a/src/bin/juliaup.rs +++ b/src/bin/juliaup.rs @@ -3,6 +3,7 @@ use clap::Parser; use juliaup::cli::{ConfigSubCmd, Juliaup, OverrideSubCmd, SelfSubCmd}; use juliaup::command_api::run_command_api; use juliaup::command_completions::generate_completion_for_command; +use juliaup::command_config_autoinstall::run_command_config_autoinstall; #[cfg(not(windows))] use juliaup::command_config_symlinks::run_command_config_symlinks; use juliaup::command_config_versionsdbupdate::run_command_config_versionsdbupdate; @@ -123,6 +124,9 @@ fn main() -> Result<()> { ConfigSubCmd::VersionsDbUpdateInterval { value } => { run_command_config_versionsdbupdate(value, false, &paths) } + ConfigSubCmd::AutoInstallChannels { value } => { + run_command_config_autoinstall(value, false, &paths) + } }, Juliaup::Api { command } => run_command_api(&command, &paths), Juliaup::InitialSetupFromLauncher {} => run_command_initial_setup_from_launcher(&paths), @@ -148,6 +152,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 05de9415..03b560e9 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, @@ -62,9 +62,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 @@ -171,4 +171,13 @@ pub enum ConfigSubCmd { /// New value value: Option, }, + /// Whether to automatically install Julia channels requested from the command line. + /// When set to true, 'julia +channel' will automatically install missing channels. + /// When false, users will not be prompted and shown an error. + /// When default (unset), users will be prompted interactively or shown an error in non-interactive mode. + #[clap(name = "autoinstallchannels")] + AutoInstallChannels { + /// New value: true, false, or default + value: Option, + }, } diff --git a/src/command_config_autoinstall.rs b/src/command_config_autoinstall.rs new file mode 100644 index 00000000..9b2457c0 --- /dev/null +++ b/src/command_config_autoinstall.rs @@ -0,0 +1,71 @@ +use anyhow::{anyhow, Result}; + +pub fn run_command_config_autoinstall( + value: Option, + quiet: bool, + paths: &crate::global_paths::GlobalPaths, +) -> Result<()> { + use crate::config_file::{load_config_db, load_mut_config_db, save_config_db}; + use anyhow::Context; + + match value { + Some(value_str) => { + let mut config_file = load_mut_config_db(paths) + .with_context(|| "`config` command failed to load configuration data.")?; + + let mut value_changed = false; + let new_value = match value_str.to_lowercase().as_str() { + "true" => Some(true), + "false" => Some(false), + "default" => None, + _ => { + return Err(anyhow!( + "Invalid value '{}'. Valid values are: true, false, default (to unset the property)", + value_str + )) + } + }; + + if new_value != config_file.data.settings.auto_install_channels { + config_file.data.settings.auto_install_channels = new_value; + value_changed = true; + } + + save_config_db(&mut config_file) + .with_context(|| "Failed to save configuration file from `config` command.")?; + + if !quiet { + let display_value = new_value + .map(|b| b.to_string()) + .unwrap_or_else(|| "default (not set)".to_string()); + + if value_changed { + eprintln!("Property 'autoinstallchannels' set to '{}'", display_value); + } else { + eprintln!( + "Property 'autoinstallchannels' is already set to '{}'", + display_value + ); + } + } + } + None => { + let config_file = load_config_db(paths, None) + .with_context(|| "`config` command failed to load configuration data.")?; + + if !quiet { + eprintln!( + "Property 'autoinstallchannels' set to '{}'", + config_file + .data + .settings + .auto_install_channels + .map(|b| b.to_string()) + .unwrap_or_else(|| "default (not set)".to_string()) + ); + } + } + }; + + Ok(()) +} diff --git a/src/command_config_backgroundselfupdate.rs b/src/command_config_backgroundselfupdate.rs index 97f3277c..dc11bde7 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 7af5a574..422ff3c6 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_list.rs b/src/command_list.rs index 1a39e9ea..b1cdd040 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 07efd1b6..59f5f837 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/config_file.rs b/src/config_file.rs index 4b1f923e..ee72e658 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -67,6 +67,12 @@ pub struct JuliaupConfigSettings { skip_serializing_if = "is_default_versionsdb_update_interval" )] pub versionsdb_update_interval: i64, + #[serde( + rename = "AutoInstallChannels", + default, + skip_serializing_if = "Option::is_none" + )] + pub auto_install_channels: Option, } impl Default for JuliaupConfigSettings { @@ -74,6 +80,7 @@ impl Default for JuliaupConfigSettings { JuliaupConfigSettings { create_channel_symlinks: false, versionsdb_update_interval: default_versionsdb_update_interval(), + auto_install_channels: None, } } } @@ -204,6 +211,7 @@ pub fn load_config_db( settings: JuliaupConfigSettings { create_channel_symlinks: false, versionsdb_update_interval: default_versionsdb_update_interval(), + auto_install_channels: None, }, last_version_db_update: None, }, @@ -304,6 +312,7 @@ pub fn load_mut_config_db(paths: &GlobalPaths) -> Result { settings: JuliaupConfigSettings { create_channel_symlinks: false, versionsdb_update_interval: default_versionsdb_update_interval(), + auto_install_channels: None, }, last_version_db_update: None, }; diff --git a/src/lib.rs b/src/lib.rs index 92675479..93533458 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod cli; pub mod command_add; pub mod command_api; pub mod command_completions; +pub mod command_config_autoinstall; pub mod command_config_backgroundselfupdate; pub mod command_config_modifypath; pub mod command_config_startupselfupdate; diff --git a/src/operations.rs b/src/operations.rs index 2173b591..39bea850 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -175,7 +175,8 @@ pub fn download_extract_sans_parent( http_response .EnsureSuccessStatusCode() - .with_context(|| "HTTP download reported error status code.")?; + .with_context(|| format!("Failed to get etag from `{}`.\n\ + This is likely due to requesting a pull request that does not have a cached build available. You may have to build locally.", url))?; let last_modified = http_response .Headers() @@ -709,6 +710,7 @@ pub fn install_non_db_version( Ok("bin/macos/aarch64/julia-".to_owned() + &id + "-macos-aarch64.tar.gz") } "win64" => Ok("bin/windows/x86_64/julia-".to_owned() + &id + "-windows-x86_64.tar.gz"), + "win32" => Ok("bin/windows/x86/julia-".to_owned() + &id + "-windows-x86.tar.gz"), "linux-x86_64" => { Ok("bin/linux/x86_64/julia-".to_owned() + &id + "-linux-x86_64.tar.gz") } @@ -796,9 +798,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()); } @@ -1443,12 +1446,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, @@ -1509,7 +1513,7 @@ where eprintln!("{}", message); // Now wait for the function to complete - + rx.recv().unwrap() } Err(e) => panic!("Error receiving result: {:?}", e), @@ -1532,7 +1536,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; @@ -1603,7 +1607,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/channel_selection.rs b/tests/channel_selection.rs index ed375c9f..4f03fbd5 100644 --- a/tests/channel_selection.rs +++ b/tests/channel_selection.rs @@ -1,4 +1,5 @@ use assert_cmd::Command; +use predicates::str::contains; #[test] fn channel_selection() { @@ -127,6 +128,18 @@ fn channel_selection() { .stderr("ERROR: Invalid Juliaup channel `1.8.6`. Please run `juliaup list` to get a list of valid channels and versions.\n"); // https://github.com/JuliaLang/juliaup/issues/766 + // First enable auto-install in configuration + Command::cargo_bin("juliaup") + .unwrap() + .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() .arg("+1.8.2") @@ -136,23 +149,42 @@ fn channel_selection() { .env("JULIAUP_DEPOT_PATH", depot_dir.path()) .env("JULIAUP_CHANNEL", "1.7.4") .assert() - .failure() - .stderr("ERROR: `1.8.2` is not installed. Please run `juliaup add 1.8.2` to install channel or version.\n"); + .success() + .stdout("1.8.2") + .stderr(contains( + "Info: Installing Julia 1.8.2 automatically per juliaup settings...", + )); // https://github.com/JuliaLang/juliaup/issues/820 + // Command line channel selector should auto-install valid channels including nightly Command::cargo_bin("julia") .unwrap() .arg("+nightly") .arg("-e") - .arg("print(VERSION)") + .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() - .failure() - .stderr("ERROR: `nightly` is not installed. Please run `juliaup add nightly` to install channel or version.\n"); + .success() + .stdout("SUCCESS") + .stderr(contains( + "Info: Installing Julia nightly automatically per juliaup settings...", + )); // https://github.com/JuliaLang/juliaup/issues/995 + // Reset auto-install to false for this test + Command::cargo_bin("juliaup") + .unwrap() + .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() .arg("+pr1") @@ -163,5 +195,47 @@ fn channel_selection() { .env("JULIAUP_CHANNEL", "1.7.4") .assert() .failure() - .stderr("ERROR: `pr1` is not installed. Please run `juliaup add pr1` to install pull request channel if available.\n"); + .stderr(contains("`pr1` is not installed. Please run `juliaup add pr1` to install pull request channel if available.")); +} + +#[test] +fn auto_install_valid_channel() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // First set up a basic julia installation so juliaup is properly initialized + Command::cargo_bin("juliaup") + .unwrap() + .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() + .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() + .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") + .stderr(contains( + "Info: Installing Julia 1.10.10 automatically per juliaup settings...", + )); } 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", + ], ); }