diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index bd544167..90f3fb52 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -25,6 +25,18 @@ use windows::Win32::System::{ Threading::GetCurrentProcess, }; +/// Check if this binary is being run as 'julia' (the launcher) rather than 'juliaup' +fn is_julia_launcher() -> bool { + std::env::current_exe() + .ok() + .and_then(|path| { + path.file_stem() + .and_then(|stem| stem.to_str()) + .map(|name| name == "julia" || name == "julialauncher") + }) + .unwrap_or(false) +} + #[derive(thiserror::Error, Debug)] #[error("{msg}")] pub struct UserError { @@ -49,10 +61,27 @@ fn do_initial_setup(juliaupconfig_path: &Path) -> Result<()> { if !juliaupconfig_path.exists() { let juliaup_path = get_juliaup_path().with_context(|| "Failed to obtain juliaup path.")?; - std::process::Command::new(juliaup_path) + // Try to run initial setup, but don't fail Julia startup if it fails + // This ensures Julia can still run even without network connectivity + match std::process::Command::new(juliaup_path) .arg("46029ef5-0b73-4a71-bff3-d0d05de42aac") // This is our internal command to do the initial setup .status() - .with_context(|| "Failed to start juliaup for the initial setup.")?; + { + Ok(status) => { + if !status.success() { + // Don't show warnings when running as Julia launcher to avoid confusion + if !is_julia_launcher() { + eprintln!( + "Warning: Initial setup failed, but Julia will still attempt to run." + ); + } + } + } + Err(_) => { + // Silently ignore initial setup failures to avoid confusing Julia users + // The initial setup will be retried on next run + } + } } Ok(()) } @@ -337,20 +366,46 @@ fn run_app() -> Result { } } - let (julia_channel_to_use, juliaup_channel_source) = - if let Some(channel) = channel_from_cmd_line { - (channel, JuliaupChannelSource::CmdLine) - } else if let Ok(channel) = std::env::var("JULIAUP_CHANNEL") { - (channel, JuliaupChannelSource::EnvVar) - } else if let Ok(Some(channel)) = get_override_channel(&config_file) { - (channel, JuliaupChannelSource::Override) - } else if let Some(channel) = config_file.data.default.clone() { - (channel, JuliaupChannelSource::Default) + let (julia_channel_to_use, juliaup_channel_source) = if let Some(channel) = + channel_from_cmd_line + { + (channel, JuliaupChannelSource::CmdLine) + } else if let Ok(channel) = std::env::var("JULIAUP_CHANNEL") { + (channel, JuliaupChannelSource::EnvVar) + } else if let Ok(Some(channel)) = get_override_channel(&config_file) { + (channel, JuliaupChannelSource::Override) + } else if let Some(channel) = config_file.data.default.clone() { + (channel, JuliaupChannelSource::Default) + } else { + // Check if we have any installed channels at all + if config_file.data.installed_channels.is_empty() { + return Err(UserError { + msg: format!( + "No Julia versions are installed. This can happen if Julia installation failed due to network issues.\n\ + \n\ + To fix this, please run:\n\ + \n\ + juliaup add release\n\ + \n\ + when you have network connectivity to install the latest stable Julia version." + ) + }.into()); } else { - return Err(anyhow!( - "The Julia launcher failed to figure out which juliaup channel to use." - )); - }; + return Err(UserError { + msg: format!( + "No default Julia version is set. You have the following versions installed: {}\n\ + \n\ + To set a default version, please run:\n\ + \n\ + juliaup default CHANNEL\n\ + \n\ + where CHANNEL is one of: {}", + config_file.data.installed_channels.keys().map(|s| s.as_str()).collect::>().join(", "), + config_file.data.installed_channels.keys().map(|s| s.as_str()).collect::>().join(", ") + ) + }.into()); + } + }; let (julia_path, julia_args) = get_julia_path_from_channel( &versiondb_data, @@ -431,10 +486,11 @@ fn run_app() -> Result { ctrlc::set_handler(|| ()) .with_context(|| "Failed to set the Ctrl-C handler.")?; - run_versiondb_update(&config_file) - .with_context(|| "Failed to run version db update")?; + // Silently handle version db update failures to avoid confusing Julia users + let _ = run_versiondb_update(&config_file); - run_selfupdate(&config_file).with_context(|| "Failed to run selfupdate.")?; + // Silently handle selfupdate failures to avoid confusing Julia users + let _ = run_selfupdate(&config_file); } Err(_) => panic!("Could not double-fork"), } @@ -495,9 +551,11 @@ fn run_app() -> Result { ) }; - run_versiondb_update(&config_file).with_context(|| "Failed to run version db update")?; + // Silently handle version db update failures to avoid confusing Julia users + let _ = run_versiondb_update(&config_file); - run_selfupdate(&config_file).with_context(|| "Failed to run selfupdate.")?; + // Silently handle selfupdate failures to avoid confusing Julia users + let _ = run_selfupdate(&config_file); let status = child_process .wait() @@ -537,7 +595,15 @@ fn main() -> Result { return Ok(std::process::ExitCode::FAILURE); } else { - return Err(client_status.unwrap_err()); + // If we're running as the Julia launcher, don't fail on juliaup errors + // This ensures 'julia' command never fails due to juliaup issues + if is_julia_launcher() { + eprintln!("Julia startup encountered an issue: {}", err); + eprintln!("Julia installation may be incomplete. Please run 'juliaup add release' to fix this."); + return Ok(std::process::ExitCode::FAILURE); + } else { + return Err(client_status.unwrap_err()); + } } } } diff --git a/src/command_initial_setup_from_launcher.rs b/src/command_initial_setup_from_launcher.rs index d97e1327..f4fdeff4 100644 --- a/src/command_initial_setup_from_launcher.rs +++ b/src/command_initial_setup_from_launcher.rs @@ -1,9 +1,31 @@ -use crate::{command_add::run_command_add, global_paths::GlobalPaths}; +use crate::{ + command_add::run_command_add, + config_file::{load_mut_config_db, save_config_db}, + global_paths::GlobalPaths, +}; use anyhow::{Context, Result}; pub fn run_command_initial_setup_from_launcher(paths: &GlobalPaths) -> Result<()> { - run_command_add("release", paths) - .with_context(|| "Failed to run `run_command_add` from the `run_command_initial_setup_from_launcher` command.")?; + // Try to add the release channel normally + match run_command_add("release", paths) { + Ok(()) => { + // Success - everything is set up properly + Ok(()) + } + Err(_) => { + // Silently create a minimal config file so we don't keep trying to do initial setup + // This ensures Julia can start even if the initial installation fails + let mut config_file = load_mut_config_db(paths) + .with_context(|| "Failed to create minimal configuration file.")?; - Ok(()) + // Just save the config file - this creates the basic structure + save_config_db(&mut config_file) + .with_context(|| "Failed to save minimal configuration file.")?; + + // Note: We don't print warnings here as this is called from the Julia launcher + // and users expect Julia to "just work" without juliaup messages + + Ok(()) + } + } } 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/operations.rs b/src/operations.rs index af7cfcb6..f0da0413 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -1649,7 +1649,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; @@ -1720,7 +1720,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/julia_launcher_resilience_test.rs b/tests/julia_launcher_resilience_test.rs new file mode 100644 index 00000000..19365f8f --- /dev/null +++ b/tests/julia_launcher_resilience_test.rs @@ -0,0 +1,209 @@ +use assert_cmd::Command; +use assert_fs::prelude::*; +use predicates::prelude::*; +use std::path::Path; +use std::time::Duration; + +/// Test that `julia --version` fails gracefully when no Julia versions are installed +/// This simulates the scenario from issue #1204 after initial setup fails +#[test] +fn julia_version_with_no_versions_installed() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Create a minimal juliaup config directory but with no installed versions + let juliaup_dir = depot_dir.child("juliaup"); + juliaup_dir.create_dir_all().unwrap(); + + // Create a minimal juliaup.json with no installed channels + let config_content = r#"{ + "default": null, + "installed_channels": {}, + "installed_versions": {}, + "settings": { + "versionsdb_update_interval": 1440, + "startup_selfupdate_interval": null, + "auto_gc": true, + "create_channel_symlinks": true, + "modify_path": true + }, + "overrides": [], + "last_version_db_update": null + }"#; + + juliaup_dir + .child("juliaup.json") + .write_str(config_content) + .unwrap(); + + // When no versions are installed, `julia --version` should fail gracefully + // with a helpful error message, not crash due to juliaup issues + Command::cargo_bin("julia") + .unwrap() + .arg("--version") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .failure() + .stderr( + predicate::str::contains("Julia startup encountered an issue") + .or(predicate::str::contains("No Julia versions are installed")), + ) + .stderr(predicate::str::contains("juliaup add release")) + // Should NOT contain panic messages or stack traces + .stderr(predicate::str::contains("panic").not()) + .stderr(predicate::str::contains("thread 'main' panicked").not()); +} + +/// Test that `julia --version` works when there are corrupted juliaup files +/// This simulates scenarios where the version database or config files are corrupted +#[test] +fn julia_version_with_corrupted_config() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // First install a Julia version normally + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.8.5") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Corrupt the juliaup config file + let config_path = depot_dir.path().join("juliaup").join("juliaup.json"); + std::fs::write(&config_path, "{ invalid json }").unwrap(); + + // `julia --version` should handle this gracefully and not crash + Command::cargo_bin("julia") + .unwrap() + .arg("--version") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .failure() + .stderr(predicate::str::contains( + "Julia startup encountered an issue", + )) + .stderr(predicate::str::contains("juliaup add release")); +} + +/// Test that `julia` command handles network issues gracefully during initial setup +/// This directly tests the scenario from issue #1204 +#[test] +fn julia_with_network_unavailable_during_initial_setup() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // Ensure no Julia versions are installed + depot_dir + .child(Path::new("juliaup")) + .assert(predicate::path::missing()); + + // Try to run julia with invalid proxy to simulate network issues + // Use a short timeout to avoid hanging the test + let _result = Command::cargo_bin("julia") + .unwrap() + .arg("-e") + .arg("println(\"Hello\")") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .env("https_proxy", "http://invalid-proxy:9999") + .env("http_proxy", "http://invalid-proxy:9999") + .timeout(Duration::from_secs(15)) // Don't wait too long + .assert(); + + // The command should either: + // 1. Fail gracefully with a helpful error message, or + // 2. Succeed if it can fall back to bundled versions + // It should NOT panic or hang indefinitely + + // Check that output doesn't contain panic information + if let Ok(output) = std::process::Command::new("cargo") + .args(&["run", "--bin", "julia", "--", "-e", "println(\"Hello\")"]) + .current_dir("/Users/ian/Documents/GitHub/juliaup") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .env("https_proxy", "http://invalid-proxy:9999") + .env("http_proxy", "http://invalid-proxy:9999") + .output() + { + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("panic"), + "Julia launcher should not panic: {}", + stderr + ); + assert!( + !stderr.contains("thread 'main' panicked"), + "Julia launcher should not panic: {}", + stderr + ); + } +} + +/// Test that `julia` with a specific channel works even when default setup fails +#[test] +fn julia_with_channel_when_setup_fails() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // First install a specific version + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.8.5") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Corrupt the config to break default detection but leave versions intact + let config_path = depot_dir.path().join("juliaup").join("juliaup.json"); + let config_content = std::fs::read_to_string(&config_path).unwrap(); + let corrupted_config = config_content.replace("\"default\":", "\"corrupted_default\":"); + std::fs::write(&config_path, corrupted_config).unwrap(); + + // Using `+1.8.5` should still work even when default detection is broken + Command::cargo_bin("julia") + .unwrap() + .arg("+1.8.5") + .arg("--version") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(predicate::str::contains("julia version 1.8.5")); +} + +/// Test that environment variable JULIAUP_CHANNEL works when config is broken +#[test] +fn julia_with_env_channel_when_config_broken() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + // First install a specific version + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.8.5") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success(); + + // Instead of completely corrupting the config, just remove the default + // to simulate a broken default detection scenario + let config_path = depot_dir.path().join("juliaup").join("juliaup.json"); + let config_content = std::fs::read_to_string(&config_path).unwrap(); + let corrupted_config = config_content.replace("\"default\":", "\"corrupted_default\":"); + std::fs::write(&config_path, corrupted_config).unwrap(); + + // Using JULIAUP_CHANNEL should still work for specifying a valid channel + Command::cargo_bin("julia") + .unwrap() + .arg("--version") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_CHANNEL", "1.8.5") + .assert() + .success() + .stdout(predicate::str::contains("julia version 1.8.5")); +}