From d885e6fb357e8a1404c479875f20ed4d8ff34886 Mon Sep 17 00:00:00 2001 From: TEC Date: Sat, 5 Oct 2024 12:22:23 +0800 Subject: [PATCH 1/7] Launch the manifest-specified Julia version When running `julia` with no extra arguments, and no explicit version, it is best to match the manifest version. This is done by implemented a limited form of the Julia executable's argument parsing and load path interpreting to determine the appropriate project to inspect, and then some light ad-hoc parsing of the manifest. We can then search the installed versions for a matching minor version, and run that. --- Cargo.lock | 11 +++ Cargo.toml | 1 + src/bin/julialauncher.rs | 197 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e7297bde..a37f9e18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1031,6 +1031,7 @@ dependencies = [ "tar", "tempfile", "thiserror", + "toml 0.8.19", "url", "windows", "winres", @@ -1974,6 +1975,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", + "winnow", ] [[package]] @@ -2446,6 +2448,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "winres" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index 65c0a9a5..e72f5373 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ is-terminal = "0.4" path-absolutize = "3.1.0" human-sort = "0.2.2" regex = "1.10" +toml = "0.8.19" [target.'cfg(windows)'.dependencies] windows = { version = "0.58.0", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects", "Win32_System_Console", "Win32_System_Threading", "Services_Store", "Foundation", "Foundation_Collections", "Web_Http", "Web_Http_Headers", "Storage_Streams", "Management_Deployment"] } diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 1c40c0f0..937f5258 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -13,12 +13,14 @@ use nix::{ unistd::{fork, ForkResult}, }; use normpath::PathExt; +use semver::Version; #[cfg(not(windows))] use std::os::unix::process::CommandExt; #[cfg(windows)] use std::os::windows::io::{AsRawHandle, RawHandle}; use std::path::Path; use std::path::PathBuf; +use toml::Value; #[cfg(windows)] use windows::Win32::System::{ JobObjects::{AssignProcessToJobObject, SetInformationJobObject}, @@ -161,10 +163,12 @@ fn check_channel_uptodate( Ok(()) } +#[derive(PartialEq, Eq)] enum JuliaupChannelSource { CmdLine, EnvVar, Override, + Manifest, Default, } @@ -176,6 +180,12 @@ fn get_julia_path_from_channel( juliaup_channel_source: JuliaupChannelSource, ) -> Result<(PathBuf, Vec)> { let channel_valid = is_valid_channel(versions_db, &channel.to_string())?; + if juliaup_channel_source == JuliaupChannelSource::Manifest { + let path = + get_julia_path_for_version(config_data, juliaupconfig_path, &Version::parse(channel)?)?; + return Ok((path, Vec::new())); + } + let channel_info = config_data .installed_channels .get(channel) @@ -208,6 +218,8 @@ fn get_julia_path_from_channel( } }.into(), JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) } + JuliaupChannelSource::Manifest => unreachable!(), + JuliaupChannelSource::Default => anyhow!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) })?; match channel_info { @@ -308,6 +320,189 @@ fn get_override_channel( } } +fn get_program_file(args: &Vec) -> Option<(usize, &String)> { + let mut program_file: Option<(usize, &String)> = None; + let no_arg_short_switches = ['v', 'h', 'i', 'q']; + let no_arg_long_switches = [ + "--version", + "--help", + "--help-hidden", + "--interactive", + "--quiet", + // Hidden options + "--lisp", + "--image-codegen", + "--rr-detach", + "--strip-metadata", + "--strip-ir", + "--permalloc-pkgimg", + "--heap-size-hint", + "--trim", + ]; + let mut skip_next = false; + for (i, arg) in args.iter().skip(1).enumerate() { + if skip_next { + skip_next = false; + } else if arg == "--" { + if i + 1 < args.len() { + program_file = Some((i + 1, args.get(i + 1).unwrap())); + } + break; + } else if arg.starts_with("--") { + if !no_arg_long_switches.contains(&arg.as_str()) && !arg.contains('=') { + skip_next = true; + } + } else if arg.starts_with("-") { + let arg: Vec = arg.chars().skip(1).collect(); + if arg.iter().all(|&c| no_arg_short_switches.contains(&c)) { + continue; + } + for (j, &c) in arg.iter().enumerate() { + if no_arg_short_switches.contains(&c) { + continue; + } else if j < arg.len() - 1 { + break; + } else { + // `j == arg.len() - 1` + skip_next = true; + } + } + } else { + program_file = Some((i, arg)); + break; + } + } + return program_file; +} + +fn get_project(args: &Vec) -> Option { + let program_file = get_program_file(args); + let recognised_proj_flags: [&str; 4] = ["--project", "--projec", "--proje", "--proj"]; + let mut project_arg: Option = None; + for arg in args + .iter() + .take(program_file.map_or(args.len(), |(i, _)| i)) + { + if arg.starts_with("--proj") { + let mut parts = arg.splitn(2, '='); + if recognised_proj_flags.contains(&parts.next().unwrap_or("")) { + project_arg = Some(parts.next().unwrap_or("@.").to_string()); + } + } + } + let project = if project_arg.is_some() { + project_arg.unwrap() + } else if let Ok(val) = std::env::var("JULIA_PROJECT") { + val + } else { + return None; + }; + if project == "@" { + return None; + } else if project == "@." || project == "" { + let mut path = PathBuf::from(std::env::current_dir().unwrap()); + while !path.join("Project.toml").exists() && !path.join("JuliaProject.toml").exists() { + if !path.pop() { + return None; + } + } + return Some(path); + } else if project == "@script" { + if let Some((_, file)) = program_file { + let mut path = PathBuf::from(file); + path.pop(); + while !path.join("Project.toml").exists() && !path.join("JuliaProject.toml").exists() { + if !path.pop() { + return None; + } + } + return Some(path); + } else { + return None; + } + } else if project.starts_with('@') { + let depot = match std::env::var("JULIA_DEPOT_PATH") { + Ok(val) => match val.split(':').next() { + Some(p) => PathBuf::from(p), + None => dirs::home_dir().unwrap().join(".julia"), + }, + _ => dirs::home_dir().unwrap().join(".julia"), + }; + let path = depot.join("environments").join(&project[1..]); + if path.exists() { + return Some(path); + } else { + return None; + } + } else { + return Some(PathBuf::from(project)); + } +} + +fn julia_version_from_manifest(path: PathBuf) -> Option { + let manifest = if path.join("JuliaManifest.toml").exists() { + path.join("JuliaManifest.toml") + } else if path.join("Manifest.toml").exists() { + path.join("Manifest.toml") + } else { + return None; + }; + let content = std::fs::read_to_string(manifest) + .ok()? + .parse::() + .ok()?; + if let Some(manifest_format) = content.get("manifest_format") { + if manifest_format.as_str()?.starts_with("2.") { + if let Some(julia_version) = content.get("julia_version") { + return julia_version.as_str().and_then(|v| Version::parse(v).ok()); + } + } + } + return None; +} + +fn get_julia_path_for_version( + config_data: &JuliaupConfig, + juliaupconfig_path: &Path, + version: &Version, +) -> Result { + let mut best_match: Option<(&String, Version)> = None; + for (installed_version_str, path) in &config_data.installed_versions { + if let Ok(installed_semver) = Version::parse(installed_version_str) { + if installed_semver.major != version.major || installed_semver.minor != version.minor { + continue; + } + if let Some((_, ref best_version)) = best_match { + if installed_semver > *best_version { + best_match = Some((&path.path, installed_semver)); + } + } else { + best_match = Some((&path.path, installed_semver)); + } + } + } + if let Some((path, _)) = best_match { + let absolute_path = juliaupconfig_path + .parent() + .unwrap() + .join(path) + .join("bin") + .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) + .normalize() + .with_context(|| { + format!( + "Failed to normalize path for Julia binary, starting from `{}`.", + juliaupconfig_path.display() + ) + })?; + return Ok(absolute_path.into_path_buf()); + } else { + return Err(anyhow!( + "No installed version of Julia matches the requested version." + )); + } +} + fn run_app() -> Result { if std::io::stdout().is_terminal() { // Set console title @@ -344,6 +539,8 @@ fn run_app() -> Result { (channel, JuliaupChannelSource::EnvVar) } else if let Ok(Some(channel)) = get_override_channel(&config_file) { (channel, JuliaupChannelSource::Override) + } else if let Some(version) = get_project(&args).and_then(julia_version_from_manifest) { + (version.to_string(), JuliaupChannelSource::Manifest) } else if let Some(channel) = config_file.data.default.clone() { (channel, JuliaupChannelSource::Default) } else { From 3a2c51ac2f3b36fa524a6f4a69a6c069e863cdb7 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Sat, 5 Oct 2024 17:36:04 -0700 Subject: [PATCH 2/7] Add featuremanifestsupport config option --- src/bin/juliaup.rs | 4 ++ src/cli.rs | 6 +++ src/command_config_featuremanifestsupport.rs | 50 ++++++++++++++++++++ src/config_file.rs | 9 ++++ src/lib.rs | 1 + 5 files changed, 70 insertions(+) create mode 100644 src/command_config_featuremanifestsupport.rs diff --git a/src/bin/juliaup.rs b/src/bin/juliaup.rs index 33835acf..7439e136 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::run_command_completions; +use juliaup::command_config_featuremanifestsupport::run_command_config_featuremanifestsupport; #[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::FeatureManifestSupport { value } => { + run_command_config_featuremanifestsupport(value, false, &paths) + } }, Juliaup::Api { command } => run_command_api(&command, &paths), Juliaup::InitialSetupFromLauncher {} => run_command_initial_setup_from_launcher(&paths), diff --git a/src/cli.rs b/src/cli.rs index 5d7bc76a..69af596f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -157,4 +157,10 @@ pub enum ConfigSubCmd { /// New value value: Option, }, + /// Enable Julia version selection from manifests + #[clap(name = "featuremanifestsupport")] + FeatureManifestSupport { + /// New value + value: Option, + }, } diff --git a/src/command_config_featuremanifestsupport.rs b/src/command_config_featuremanifestsupport.rs new file mode 100644 index 00000000..f32036c4 --- /dev/null +++ b/src/command_config_featuremanifestsupport.rs @@ -0,0 +1,50 @@ +use crate::config_file::{load_config_db, load_mut_config_db, save_config_db}; +use anyhow::{Context, Result}; + +pub fn run_command_config_featuremanifestsupport( + value: Option, + quiet: bool, + paths: &crate::global_paths::GlobalPaths, +) -> Result<()> { + match value { + Some(value) => { + let mut config_file = load_mut_config_db(paths) + .with_context(|| "`config` command failed to load configuration data.")?; + + let mut value_changed = false; + + if value != config_file.data.settings.feature_manifest_support { + config_file.data.settings.feature_manifest_support = value; + + value_changed = true; + } + + save_config_db(&mut config_file) + .with_context(|| "Failed to save configuration file from `config` command.")?; + + if !quiet { + if value_changed { + eprintln!("Property 'featuremanifestsupport' set to '{}'", value); + } else { + eprintln!( + "Property 'featuremanifestsupport' is already set to '{}'", + value + ); + } + } + } + None => { + let config_file = load_config_db(paths) + .with_context(|| "`config` command failed to load configuration data.")?; + + if !quiet { + eprintln!( + "Property 'featuremanifestsupport' set to '{}'", + config_file.data.settings.feature_manifest_support + ); + } + } + }; + + Ok(()) +} diff --git a/src/config_file.rs b/src/config_file.rs index 07f11d7b..c907e230 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 = "FeatureManifestSupport", + default, + skip_serializing_if = "is_default" + )] + pub feature_manifest_support: bool, } impl Default for JuliaupConfigSettings { @@ -74,6 +80,7 @@ impl Default for JuliaupConfigSettings { JuliaupConfigSettings { create_channel_symlinks: false, versionsdb_update_interval: default_versionsdb_update_interval(), + feature_manifest_support: false, } } } @@ -203,6 +210,7 @@ pub fn load_config_db( settings: JuliaupConfigSettings { create_channel_symlinks: false, versionsdb_update_interval: default_versionsdb_update_interval(), + feature_manifest_support: false, }, last_version_db_update: None, }, @@ -301,6 +309,7 @@ pub fn load_mut_config_db(paths: &GlobalPaths) -> Result { settings: JuliaupConfigSettings { create_channel_symlinks: false, versionsdb_update_interval: default_versionsdb_update_interval(), + feature_manifest_support: false, }, last_version_db_update: None, }; diff --git a/src/lib.rs b/src/lib.rs index 92675479..f8ea1caf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod command_add; pub mod command_api; pub mod command_completions; pub mod command_config_backgroundselfupdate; +pub mod command_config_featuremanifestsupport; pub mod command_config_modifypath; pub mod command_config_startupselfupdate; pub mod command_config_symlinks; From 2eb8d067df7ad05b1c5dea959a8bf51b07cd91a5 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Thu, 10 Oct 2024 14:25:49 -0700 Subject: [PATCH 3/7] Launch exact Julia version from manifest --- src/bin/julialauncher.rs | 290 ++++++++++++++++++--------------------- 1 file changed, 133 insertions(+), 157 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 937f5258..0b0dcfc4 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -165,33 +165,56 @@ fn check_channel_uptodate( #[derive(PartialEq, Eq)] enum JuliaupChannelSource { - CmdLine, - EnvVar, - Override, - Manifest, - Default, + CmdLine { channel: String }, + EnvVar { channel: String }, + Override { channel: String }, + Manifest { version: String }, + Default { channel: String }, } fn get_julia_path_from_channel( versions_db: &JuliaupVersionDB, config_data: &JuliaupConfig, - channel: &str, + launch_parameters: &JuliaupChannelSource, juliaupconfig_path: &Path, - juliaup_channel_source: JuliaupChannelSource, ) -> Result<(PathBuf, Vec)> { - let channel_valid = is_valid_channel(versions_db, &channel.to_string())?; - if juliaup_channel_source == JuliaupChannelSource::Manifest { - let path = - get_julia_path_for_version(config_data, juliaupconfig_path, &Version::parse(channel)?)?; - return Ok((path, Vec::new())); - } + if let JuliaupChannelSource::Manifest { version } = launch_parameters { + let version_string = versions_db.available_channels.get(version) + .ok_or_else(|| anyhow!("The project you are trying to launch uses Julia {}, but no such Julia version exists. Please make sure you are using a valid Julia manifest file.", version))?; + + let version_config = config_data.installed_versions.get(&version_string.version) + .ok_or_else(|| anyhow!("The project you are trying to launch uses Julia {}, but you do not have that version installed. You can install it by running `juliaup add {}`.", version, version))?; + + let absolute_path = juliaupconfig_path + .parent() + .unwrap() // unwrap OK because there should always be a parent + .join(&version_config.path) + .join("bin") + .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) + .normalize() + .with_context(|| { + format!( + "Failed to normalize path for Julia binary, starting from `{}`.", + juliaupconfig_path.display() + ) + })?; - let channel_info = config_data + return Ok((absolute_path.into_path_buf(), Vec::new())); + } else { + let channel = match launch_parameters { + JuliaupChannelSource::CmdLine { channel } => channel, + JuliaupChannelSource::Default { channel } => channel, + JuliaupChannelSource::EnvVar { channel } => channel, + JuliaupChannelSource::Override { channel } => channel, + _ => unreachable!(), + }; + + let channel_info = config_data .installed_channels .get(channel) - .ok_or_else(|| match juliaup_channel_source { - JuliaupChannelSource::CmdLine => { - if channel_valid { + .ok_or_else(|| match launch_parameters { + JuliaupChannelSource::CmdLine {..} => { + if versions_db.available_channels.contains_key(channel) { UserError { msg: format!("`{}` is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) } } else if is_pr_channel(&channel.to_string()) { UserError { msg: format!("`{}` is not installed. Please run `juliaup add {}` to install pull request channel if available.", channel, channel) } @@ -199,104 +222,104 @@ fn get_julia_path_from_channel( UserError { msg: format!("Invalid Juliaup channel `{}`. Please run `juliaup list` to get a list of valid channels and versions.", channel) } } }.into(), - 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) } + JuliaupChannelSource::EnvVar {..} => { + if versions_db.available_channels.contains_key(channel) { + UserError { msg: format!("`{}` for environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) } } else if is_pr_channel(&channel.to_string()) { 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) } } }.into(), - 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) } + JuliaupChannelSource::Override {..} => { + if versions_db.available_channels.contains_key(channel) { + UserError { msg: format!("`{}` for directory override is not installed. Please run `juliaup add {}` to install channel or version.", channel, channel) } } else if is_pr_channel(&channel.to_string()){ 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) } } }.into(), - JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) } - JuliaupChannelSource::Manifest => unreachable!(), - JuliaupChannelSource::Default => anyhow!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) + JuliaupChannelSource::Manifest {..} => unreachable!(), + JuliaupChannelSource::Default {..} => anyhow!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) })?; - match channel_info { - JuliaupConfigChannel::LinkedChannel { command, args } => { - return Ok(( - PathBuf::from(command), - args.as_ref().map_or_else(Vec::new, |v| v.clone()), - )) - } - JuliaupConfigChannel::SystemChannel { version } => { - let path = &config_data + match channel_info { + JuliaupConfigChannel::LinkedChannel { command, args } => { + return 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) .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; - check_channel_uptodate(channel, version, versions_db).with_context(|| { + check_channel_uptodate(channel, version, versions_db).with_context(|| { format!( "The Julia launcher failed while checking whether the channel {} is up-to-date.", channel ) })?; - let absolute_path = juliaupconfig_path - .parent() - .unwrap() // unwrap OK because there should always be a parent - .join(path) - .join("bin") - .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) - .normalize() - .with_context(|| { - format!( - "Failed to normalize path for Julia binary, starting from `{}`.", - juliaupconfig_path.display() - ) - })?; - return Ok((absolute_path.into_path_buf(), Vec::new())); - } - JuliaupConfigChannel::DirectDownloadChannel { - path, - url: _, - local_etag, - server_etag, - version: _, - } => { - if local_etag != server_etag { - if channel.starts_with("nightly") { - // Nightly is updateable several times per day so this message will show - // more often than not unless folks update a couple of times a day. - // Also, folks using nightly are typically more experienced and need - // less detailed prompting - eprintln!( - "A new `nightly` version is available. Install with `juliaup update`." - ); - } else { - eprintln!( - "A new version of Julia for the `{}` channel is available. Run:", - channel - ); - eprintln!(); - eprintln!(" juliaup update"); - eprintln!(); - eprintln!("to install the latest Julia for the `{}` channel.", channel); - } + let absolute_path = juliaupconfig_path + .parent() + .unwrap() // unwrap OK because there should always be a parent + .join(path) + .join("bin") + .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) + .normalize() + .with_context(|| { + format!( + "Failed to normalize path for Julia binary, starting from `{}`.", + juliaupconfig_path.display() + ) + })?; + return Ok((absolute_path.into_path_buf(), Vec::new())); } + JuliaupConfigChannel::DirectDownloadChannel { + path, + url: _, + local_etag, + server_etag, + version: _, + } => { + if local_etag != server_etag { + if channel.starts_with("nightly") { + // Nightly is updateable several times per day so this message will show + // more often than not unless folks update a couple of times a day. + // Also, folks using nightly are typically more experienced and need + // less detailed prompting + eprintln!( + "A new `nightly` version is available. Install with `juliaup update`." + ); + } else { + eprintln!( + "A new version of Julia for the `{}` channel is available. Run:", + channel + ); + eprintln!(); + eprintln!(" juliaup update"); + eprintln!(); + eprintln!("to install the latest Julia for the `{}` channel.", channel); + } + } - let absolute_path = juliaupconfig_path - .parent() - .unwrap() - .join(path) - .join("bin") - .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) - .normalize() - .with_context(|| { - format!( - "Failed to normalize path for Julia binary, starting from `{}`.", - juliaupconfig_path.display() - ) - })?; - return Ok((absolute_path.into_path_buf(), Vec::new())); + let absolute_path = juliaupconfig_path + .parent() + .unwrap() + .join(path) + .join("bin") + .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) + .normalize() + .with_context(|| { + format!( + "Failed to normalize path for Julia binary, starting from `{}`.", + juliaupconfig_path.display() + ) + })?; + return Ok((absolute_path.into_path_buf(), Vec::new())); + } } } } @@ -461,48 +484,6 @@ fn julia_version_from_manifest(path: PathBuf) -> Option { return None; } -fn get_julia_path_for_version( - config_data: &JuliaupConfig, - juliaupconfig_path: &Path, - version: &Version, -) -> Result { - let mut best_match: Option<(&String, Version)> = None; - for (installed_version_str, path) in &config_data.installed_versions { - if let Ok(installed_semver) = Version::parse(installed_version_str) { - if installed_semver.major != version.major || installed_semver.minor != version.minor { - continue; - } - if let Some((_, ref best_version)) = best_match { - if installed_semver > *best_version { - best_match = Some((&path.path, installed_semver)); - } - } else { - best_match = Some((&path.path, installed_semver)); - } - } - } - if let Some((path, _)) = best_match { - let absolute_path = juliaupconfig_path - .parent() - .unwrap() - .join(path) - .join("bin") - .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) - .normalize() - .with_context(|| { - format!( - "Failed to normalize path for Julia binary, starting from `{}`.", - juliaupconfig_path.display() - ) - })?; - return Ok(absolute_path.into_path_buf()); - } else { - return Err(anyhow!( - "No installed version of Julia matches the requested version." - )); - } -} - fn run_app() -> Result { if std::io::stdout().is_terminal() { // Set console title @@ -532,36 +513,31 @@ 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(version) = get_project(&args).and_then(julia_version_from_manifest) { - (version.to_string(), JuliaupChannelSource::Manifest) - } else if let Some(channel) = config_file.data.default.clone() { - (channel, JuliaupChannelSource::Default) - } else { - return Err(anyhow!( - "The Julia launcher failed to figure out which juliaup channel to use." - )); - }; + let julia_launch_config = if let Some(channel) = channel_from_cmd_line { + JuliaupChannelSource::CmdLine { channel: channel } + } else if let Ok(channel) = std::env::var("JULIAUP_CHANNEL") { + JuliaupChannelSource::EnvVar { channel: channel } + } else if let Ok(Some(channel)) = get_override_channel(&config_file) { + JuliaupChannelSource::Override { channel: channel } + } else if let Some(version) = get_project(&args).and_then(julia_version_from_manifest) { + JuliaupChannelSource::Manifest { + version: version.to_string(), + } + } else if let Some(channel) = config_file.data.default.clone() { + JuliaupChannelSource::Default { channel: channel } + } else { + return Err(anyhow!( + "The Julia launcher failed to figure out which juliaup channel to use." + )); + }; let (julia_path, julia_args) = get_julia_path_from_channel( &versiondb_data, &config_file.data, - &julia_channel_to_use, + &julia_launch_config, &paths.juliaupconfig, - juliaup_channel_source, ) - .with_context(|| { - format!( - "The Julia launcher failed to determine the command for the `{}` channel.", - julia_channel_to_use - ) - })?; + .with_context(|| "The Julia launcher failed to determine the Julia version to launch.")?; let mut new_args: Vec = Vec::new(); From 343a4243e6458a1afaad221af6fe58cc655e581f Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Thu, 10 Oct 2024 14:39:19 -0700 Subject: [PATCH 4/7] Use config setting for manifest launch --- src/bin/julialauncher.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 0b0dcfc4..1f828038 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -398,7 +398,11 @@ fn get_program_file(args: &Vec) -> Option<(usize, &String)> { return program_file; } -fn get_project(args: &Vec) -> Option { +fn get_project(args: &Vec, config: &JuliaupConfig) -> Option { + if !config.settings.feature_manifest_support { + return None + } + let program_file = get_program_file(args); let recognised_proj_flags: [&str; 4] = ["--project", "--projec", "--proje", "--proj"]; let mut project_arg: Option = None; @@ -519,7 +523,7 @@ fn run_app() -> Result { JuliaupChannelSource::EnvVar { channel: channel } } else if let Ok(Some(channel)) = get_override_channel(&config_file) { JuliaupChannelSource::Override { channel: channel } - } else if let Some(version) = get_project(&args).and_then(julia_version_from_manifest) { + } else if let Some(version) = get_project(&args, &config_file.data).and_then(julia_version_from_manifest) { JuliaupChannelSource::Manifest { version: version.to_string(), } From e5c245a2337546bbc6794ed802fcad3dc2b6e1b8 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Fri, 11 Oct 2024 14:03:34 -0700 Subject: [PATCH 5/7] Use correct way to show error message when launching from manifest --- src/bin/julialauncher.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 1f828038..afe09dae 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -180,10 +180,10 @@ fn get_julia_path_from_channel( ) -> Result<(PathBuf, Vec)> { if let JuliaupChannelSource::Manifest { version } = launch_parameters { let version_string = versions_db.available_channels.get(version) - .ok_or_else(|| anyhow!("The project you are trying to launch uses Julia {}, but no such Julia version exists. Please make sure you are using a valid Julia manifest file.", version))?; + .ok_or_else(|| UserError {msg: format!("The project you are trying to launch uses Julia {}, but no such Julia version exists. Please make sure you are using a valid Julia manifest file.", version) } )?; let version_config = config_data.installed_versions.get(&version_string.version) - .ok_or_else(|| anyhow!("The project you are trying to launch uses Julia {}, but you do not have that version installed. You can install it by running `juliaup add {}`.", version, version))?; + .ok_or_else(|| UserError {msg: format!("The project you are trying to launch uses Julia {}, but you do not have that version installed. You can install it by running `juliaup add {}`.", version, version) } )?; let absolute_path = juliaupconfig_path .parent() From 2efd1fda976377be85da48bb6ae5f3e430bb51ee Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Sat, 26 Oct 2024 14:43:00 -0700 Subject: [PATCH 6/7] Adjust to call signature change --- src/command_config_featuremanifestsupport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command_config_featuremanifestsupport.rs b/src/command_config_featuremanifestsupport.rs index f32036c4..02f76112 100644 --- a/src/command_config_featuremanifestsupport.rs +++ b/src/command_config_featuremanifestsupport.rs @@ -34,7 +34,7 @@ pub fn run_command_config_featuremanifestsupport( } } None => { - let config_file = load_config_db(paths) + let config_file = load_config_db(paths, None) .with_context(|| "`config` command failed to load configuration data.")?; if !quiet { From 103e5c12b0f2a5b9c6950e07aecda9b4c4226a99 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Sat, 26 Oct 2024 15:20:47 -0700 Subject: [PATCH 7/7] Test the channel selection from manifest file --- tests/channel_selection.rs | 201 +++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) diff --git a/tests/channel_selection.rs b/tests/channel_selection.rs index ed375c9f..8485f0af 100644 --- a/tests/channel_selection.rs +++ b/tests/channel_selection.rs @@ -165,3 +165,204 @@ fn channel_selection() { .failure() .stderr("ERROR: `pr1` is not installed. Please run `juliaup add pr1` to install pull request channel if available.\n"); } + +#[test] +fn manifest_version_selection() { + let depot_dir = assert_fs::TempDir::new().unwrap(); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.6.2") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.10.1") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("add") + .arg("1.11.1") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("juliaup") + .unwrap() + .arg("default") + .arg("1.11.1") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("julia") + .unwrap() + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout("1.11.1"); + + let proj1_dir = assert_fs::TempDir::new().unwrap(); + + // We are adding and then removing a package here to force generation of an actual Project.toml + Command::cargo_bin("julia") + .unwrap() + .arg("+1.10.1") + .arg("-e") + .arg("using Pkg; Pkg.activate(\".\"); Pkg.add(\"StringBuilders\"); Pkg.rm(\"StringBuilders\"); print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&proj1_dir) + .assert() + .success() + .stdout("1.10.1"); + + // First we try this with the feature disabled + Command::cargo_bin("julia") + .unwrap() + .arg("--project=.") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&proj1_dir) + .assert() + .success() + .stdout("1.11.1"); + + // Now we enable the feature + Command::cargo_bin("juliaup") + .unwrap() + .arg("config") + .arg("featuremanifestsupport") + .arg("true") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .assert() + .success() + .stdout(""); + + Command::cargo_bin("julia") + .unwrap() + .arg("--project=.") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&proj1_dir) + .assert() + .success() + .stdout("1.10.1"); + + // TODO This currently fails, but it shouldn't + Command::cargo_bin("julia") + .unwrap() + .arg("--project") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&proj1_dir) + .assert() + .success() + .stdout("1.10.1"); + + Command::cargo_bin("julia") + .unwrap() + .arg("--project=@.") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&proj1_dir) + .assert() + .success() + .stdout("1.10.1"); + + let sub_dir1 = &proj1_dir.path().join("subdir1"); + std::fs::create_dir(&sub_dir1).unwrap(); + + Command::cargo_bin("julia") + .unwrap() + .arg("--project=.") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&sub_dir1) + .assert() + .success() + .stdout("1.11.1"); + + // TODO This currently fails, but it shouldn't + Command::cargo_bin("julia") + .unwrap() + .arg("--project") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&sub_dir1) + .assert() + .success() + .stdout("1.10.1"); + + Command::cargo_bin("julia") + .unwrap() + .arg("--project=@.") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&sub_dir1) + .assert() + .success() + .stdout("1.10.1"); + + // Now we try with a Julia version that generates schema v1 manifests + let proj2_dir = assert_fs::TempDir::new().unwrap(); + + // We are adding and then removing a package here to force generation of an actual Project.toml + Command::cargo_bin("julia") + .unwrap() + .arg("+1.6.2") + .arg("-e") + .arg("using Pkg; Pkg.activate(\".\"); Pkg.add(\"StringBuilders\"); Pkg.rm(\"StringBuilders\"); print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&proj2_dir) + .assert() + .success() + .stdout("1.6.2"); + + // It shouldn't pick up the version from the manifest, as it isn't stored in the manifest + Command::cargo_bin("julia") + .unwrap() + .arg("--project=.") + .arg("-e") + .arg("print(VERSION)") + .env("JULIA_DEPOT_PATH", depot_dir.path()) + .env("JULIAUP_DEPOT_PATH", depot_dir.path()) + .current_dir(&proj2_dir) + .assert() + .success() + .stdout("1.11.1"); +}