Skip to content

Commit 943c9da

Browse files
committed
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.
1 parent 463b45b commit 943c9da

File tree

1 file changed

+203
-12
lines changed

1 file changed

+203
-12
lines changed

src/bin/julialauncher.rs

+203-12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use nix::{
1212
unistd::{fork, ForkResult},
1313
};
1414
use normpath::PathExt;
15+
use semver::Version;
16+
use std::io::BufRead;
1517
#[cfg(not(windows))]
1618
use std::os::unix::process::CommandExt;
1719
#[cfg(windows)]
@@ -300,6 +302,181 @@ fn get_override_channel(
300302
}
301303
}
302304

305+
fn get_project(args: &Vec<String>) -> Option<PathBuf> {
306+
let mut project_arg: Option<String> = None;
307+
for (_, arg) in args.iter().enumerate() {
308+
if arg.starts_with("--project=") {
309+
project_arg = Some(arg["--project=".len()..].to_string());
310+
// Note: You'd think this might work, but it's not actually supported.
311+
// } else if arg == "--project" && i + 1 < args.len() {
312+
// project_arg = Some(args[i + 1].clone());
313+
} else if arg == "--" {
314+
break;
315+
}
316+
}
317+
let project = if project_arg.is_some() {
318+
project_arg.unwrap()
319+
} else if let Ok(val) = std::env::var("JULIA_PROJECT") {
320+
val
321+
} else {
322+
return None;
323+
};
324+
if project == "@" {
325+
return None;
326+
} else if project == "@." || project == "" {
327+
let mut path = PathBuf::from(std::env::current_dir().unwrap());
328+
while !path.join("Project.toml").exists() && !path.join("JuliaProject.toml").exists() {
329+
if !path.pop() {
330+
return None;
331+
}
332+
}
333+
return Some(path);
334+
} else if project == "@script" {
335+
let mut program_file: Option<String> = None;
336+
let no_arg_short_switches = vec!['v', 'h', 'i', 'q'];
337+
let no_arg_long_switches = vec![
338+
"--version",
339+
"--help",
340+
"--help-hidden",
341+
"--interactive",
342+
"--quiet",
343+
// Hidden options
344+
"--lisp",
345+
"--image-codegen",
346+
"--rr-detach",
347+
"--strip-metadata",
348+
"--strip-ir",
349+
"--permalloc-pkgimg",
350+
"--heap-size-hint",
351+
"--trim",
352+
];
353+
// `args` represents [switches...] -- [programfile] [programargs...]
354+
// We want to find the first non-switch argument or the first argument after `--`
355+
let mut skip_next = false;
356+
for (i, arg) in args.iter().skip(1).enumerate() {
357+
if skip_next {
358+
skip_next = false;
359+
} else if arg == "--" {
360+
program_file = args.get(i + 1).cloned();
361+
break;
362+
} else if arg.starts_with("--") {
363+
if !no_arg_long_switches.contains(&arg.as_str()) && !arg.contains('=') {
364+
skip_next = true;
365+
}
366+
} else if arg.starts_with("-") {
367+
let arg: Vec<char> = arg.chars().skip(1).collect();
368+
if arg.iter().all(|&c| no_arg_short_switches.contains(&c)) {
369+
continue;
370+
}
371+
for (j, &c) in arg.iter().enumerate() {
372+
if no_arg_short_switches.contains(&c) {
373+
continue;
374+
} else if j < arg.len() - 1 {
375+
break;
376+
} else {
377+
// `j == arg.len() - 1`
378+
skip_next = true;
379+
}
380+
}
381+
} else {
382+
program_file = Some(arg.clone());
383+
break;
384+
}
385+
}
386+
if let Some(program_file) = program_file {
387+
let mut path = PathBuf::from(program_file);
388+
path.pop();
389+
while !path.join("Project.toml").exists() && !path.join("JuliaProject.toml").exists() {
390+
if !path.pop() {
391+
return None;
392+
}
393+
}
394+
return Some(path);
395+
} else {
396+
return None;
397+
}
398+
} else if project.starts_with('@') {
399+
let depot = match std::env::var("JULIA_DEPOT_PATH") {
400+
Ok(val) => match val.split(':').next() {
401+
Some(p) => PathBuf::from(p),
402+
None => dirs::home_dir().unwrap().join(".julia"),
403+
},
404+
_ => dirs::home_dir().unwrap().join(".julia"),
405+
};
406+
let path = depot.join("environments").join(&project[1..]);
407+
if path.exists() {
408+
return Some(path);
409+
} else {
410+
return None;
411+
}
412+
} else {
413+
return Some(PathBuf::from(project));
414+
}
415+
}
416+
417+
fn julia_version_from_manifest(path: PathBuf) -> Option<Version> {
418+
let manifest = if path.join("Manifest.toml").exists() {
419+
path.join("Manifest.toml")
420+
} else if path.join("JuliaManifest.toml").exists() {
421+
path.join("JuliaManifest.toml")
422+
} else {
423+
return None;
424+
};
425+
let file = std::fs::File::open(manifest).ok()?;
426+
let reader = std::io::BufReader::new(file);
427+
// This is a somewhat bootleg way to parse the manifest,
428+
// but since we know the format it should be fine.
429+
for line in reader.lines() {
430+
let line = line.ok()?;
431+
if line.starts_with("julia_version = ") {
432+
return Version::parse(line["julia_version = ".len()..].trim_matches('"')).ok();
433+
}
434+
}
435+
return None;
436+
}
437+
438+
fn get_julia_path_for_version(
439+
config_data: &JuliaupConfig,
440+
juliaupconfig_path: &Path,
441+
version: &Version,
442+
) -> Result<PathBuf> {
443+
let mut best_match: Option<(&String, Version)> = None;
444+
for (installed_version_str, path) in &config_data.installed_versions {
445+
if let Ok(installed_semver) = Version::parse(installed_version_str) {
446+
if installed_semver.major != version.major || installed_semver.minor != version.minor {
447+
continue;
448+
}
449+
if let Some((_, ref best_version)) = best_match {
450+
if installed_semver > *best_version {
451+
best_match = Some((&path.path, installed_semver));
452+
}
453+
} else {
454+
best_match = Some((&path.path, installed_semver));
455+
}
456+
}
457+
}
458+
if let Some((path, _)) = best_match {
459+
let absolute_path = juliaupconfig_path
460+
.parent()
461+
.unwrap()
462+
.join(path)
463+
.join("bin")
464+
.join(format!("julia{}", std::env::consts::EXE_SUFFIX))
465+
.normalize()
466+
.with_context(|| {
467+
format!(
468+
"Failed to normalize path for Julia binary, starting from `{}`.",
469+
juliaupconfig_path.display()
470+
)
471+
})?;
472+
return Ok(absolute_path.into_path_buf());
473+
} else {
474+
return Err(anyhow!(
475+
"No installed version of Julia matches the requested version."
476+
));
477+
}
478+
}
479+
303480
fn run_app() -> Result<i32> {
304481
if std::io::stdout().is_terminal() {
305482
// Set console title
@@ -329,6 +506,16 @@ fn run_app() -> Result<i32> {
329506
}
330507
}
331508

509+
let manifest_derived_julia_path = if channel_from_cmd_line.is_none() {
510+
get_project(&args)
511+
.and_then(julia_version_from_manifest)
512+
.and_then(|ver| {
513+
get_julia_path_for_version(&config_file.data, &paths.juliaupconfig, &ver).ok()
514+
})
515+
} else {
516+
None
517+
};
518+
332519
let (julia_channel_to_use, juliaup_channel_source) =
333520
if let Some(channel) = channel_from_cmd_line {
334521
(channel, JuliaupChannelSource::CmdLine)
@@ -344,19 +531,23 @@ fn run_app() -> Result<i32> {
344531
));
345532
};
346533

347-
let (julia_path, julia_args) = get_julia_path_from_channel(
348-
&versiondb_data,
349-
&config_file.data,
350-
&julia_channel_to_use,
351-
&paths.juliaupconfig,
352-
juliaup_channel_source,
353-
)
354-
.with_context(|| {
355-
format!(
356-
"The Julia launcher failed to determine the command for the `{}` channel.",
357-
julia_channel_to_use
534+
let (julia_path, julia_args) = if let Some(path) = manifest_derived_julia_path {
535+
(path, Vec::new())
536+
} else {
537+
get_julia_path_from_channel(
538+
&versiondb_data,
539+
&config_file.data,
540+
&julia_channel_to_use,
541+
&paths.juliaupconfig,
542+
juliaup_channel_source,
358543
)
359-
})?;
544+
.with_context(|| {
545+
format!(
546+
"The Julia launcher failed to determine the command for the `{}` channel.",
547+
julia_channel_to_use
548+
)
549+
})?
550+
};
360551

361552
let mut new_args: Vec<String> = Vec::new();
362553

0 commit comments

Comments
 (0)