use anyhow::{anyhow, bail, Context, Result};
use console::{style, Term};
use dialoguer::Select;
use is_terminal::IsTerminal;
use itertools::Itertools;
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};
use juliaup::utils::{print_juliaup_style, resolve_julia_binary_path, JuliaupMessageType};
use juliaup::version_selection::get_auto_channel;
use juliaup::versions_file::load_versions_db;
#[cfg(not(windows))]
use nix::{
sys::wait::{waitpid, WaitStatus},
unistd::{fork, ForkResult},
};
use normpath::PathExt;
#[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;
#[cfg(windows)]
use windows::Win32::System::{
JobObjects::{AssignProcessToJobObject, SetInformationJobObject},
Threading::GetCurrentProcess,
};
#[derive(thiserror::Error, Debug)]
#[error("{msg}")]
pub struct UserError {
msg: String,
}
fn get_juliaup_path() -> Result<PathBuf> {
let my_own_path = std::env::current_exe()
.with_context(|| "std::env::current_exe() did not find its own path.")?
.canonicalize()
.with_context(|| "Failed to canonicalize the path to the Julia launcher.")?;
let juliaup_path = my_own_path
.parent()
.unwrap() .join(format!("juliaup{}", std::env::consts::EXE_SUFFIX));
Ok(juliaup_path)
}
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)
.arg("46029ef5-0b73-4a71-bff3-d0d05de42aac") .status()
.with_context(|| "Failed to start juliaup for the initial setup.")?;
}
Ok(())
}
fn run_versiondb_update(
config_file: &juliaup::config_file::JuliaupReadonlyConfigFile,
) -> Result<()> {
use chrono::Utc;
use std::process::Stdio;
let versiondb_update_interval = config_file.data.settings.versionsdb_update_interval;
if versiondb_update_interval > 0 {
let should_run =
if let Some(last_versiondb_update) = config_file.data.last_version_db_update {
let update_time =
last_versiondb_update + chrono::Duration::minutes(versiondb_update_interval);
Utc::now() >= update_time
} else {
true
};
if should_run {
let juliaup_path =
get_juliaup_path().with_context(|| "Failed to obtain juliaup path.")?;
std::process::Command::new(juliaup_path)
.args(["0cf1528f-0b15-46b1-9ac9-e5bf5ccccbcf"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null())
.spawn()
.with_context(|| "Failed to start juliaup for version db update.")?;
};
}
Ok(())
}
#[cfg(feature = "selfupdate")]
fn run_selfupdate(config_file: &juliaup::config_file::JuliaupReadonlyConfigFile) -> Result<()> {
use chrono::Utc;
use std::process::Stdio;
if let Some(val) = config_file.self_data.startup_selfupdate_interval {
let should_run = if let Some(last_selfupdate) = config_file.self_data.last_selfupdate {
let update_time = last_selfupdate + chrono::Duration::minutes(val);
Utc::now() >= update_time
} else {
true
};
if should_run {
let juliaup_path =
get_juliaup_path().with_context(|| "Failed to obtain juliaup path.")?;
std::process::Command::new(juliaup_path)
.args(["self", "update"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null())
.spawn()
.with_context(|| "Failed to start juliaup for self update.")?;
};
}
Ok(())
}
#[cfg(not(feature = "selfupdate"))]
fn run_selfupdate(_config_file: &juliaup::config_file::JuliaupReadonlyConfigFile) -> Result<()> {
Ok(())
}
fn is_interactive() -> bool {
if !std::io::stdin().is_terminal() || !std::io::stderr().is_terminal() {
return false;
}
let args: Vec<String> = std::env::args().collect();
let mut julia_args = args.iter().skip(1);
if let Some(first_arg) = julia_args.clone().next() {
if first_arg.starts_with('+') {
julia_args.next(); }
}
for arg in julia_args {
match arg.as_str() {
"-e" | "--eval" | "-E" | "--print" => return false,
"-" => return false,
"-v" | "--version" => return false,
"-h" | "--help" | "--help-hidden" => return false,
filename if filename.ends_with(".jl") && !filename.starts_with('-') => {
return false;
}
filename if !filename.starts_with('-') && !filename.is_empty() => {
if std::path::Path::new(filename).exists() {
return false;
}
}
_ => {} }
}
true
}
fn handle_auto_install_prompt(
channel: &str,
paths: &juliaup::global_paths::GlobalPaths,
) -> Result<bool> {
if !is_interactive() {
return Ok(false);
}
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) .interact()?;
match selection {
0 => {
Ok(true)
}
1 => {
set_auto_install_preference(true, paths)?;
Ok(true)
}
2 => {
Ok(false)
}
_ => {
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.")?;
print_juliaup_style(
"Configure",
&format!("Auto-install preference set to '{}'.", auto_install),
JuliaupMessageType::Success,
);
Ok(())
}
fn spawn_juliaup_add(
channel: &str,
_paths: &juliaup::global_paths::GlobalPaths,
is_automatic: bool,
) -> Result<()> {
if is_automatic {
print_juliaup_style(
"Installing",
&format!("Julia {} automatically per juliaup settings", channel),
JuliaupMessageType::Progress,
);
} else {
print_juliaup_style(
"Installing",
&format!("Julia {} as requested", channel),
JuliaupMessageType::Progress,
);
}
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() {
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,
versions_db: &JuliaupVersionDB,
) -> Result<()> {
let latest_version = &versions_db
.available_channels
.get(channel)
.ok_or_else(|| UserError {
msg: format!(
"The channel `{}` does not exist in the versions database.",
channel
),
})?
.version;
if latest_version != current_version {
print_juliaup_style(
"Info",
&format!(
"The latest version of Julia in the `{}` channel is {}. You currently have `{}` installed. Run:",
channel, latest_version, current_version
),
JuliaupMessageType::Progress,
);
eprintln!();
eprintln!(" juliaup update");
eprintln!();
eprintln!(
"in your terminal shell to install Julia {} and update the `{}` channel to that version.",
latest_version, channel
);
}
Ok(())
}
fn is_nightly_channel(channel: &str) -> bool {
use regex::Regex;
let nightly_re =
Regex::new(r"^((?:nightly|latest)|(\d+\.\d+)-(?:nightly|latest))(~|$)").unwrap();
nightly_re.is_match(channel)
}
#[derive(Debug)]
enum JuliaupChannelSource {
CmdLine,
EnvVar,
Override,
Auto,
Default,
}
fn get_julia_path_from_channel(
versions_db: &JuliaupVersionDB,
config_data: &JuliaupConfig,
channel: &str,
juliaupconfig_path: &Path,
juliaup_channel_source: JuliaupChannelSource,
paths: &juliaup::global_paths::GlobalPaths,
) -> Result<(PathBuf, Vec<String>)> {
let (resolved_channel, alias_args) = match config_data.installed_channels.get(channel) {
Some(JuliaupConfigChannel::AliasChannel { target, args }) => {
(target.to_string(), args.clone().unwrap_or_default())
}
_ => (channel.to_string(), Vec::new()),
};
let channel_valid = is_valid_channel(versions_db, &resolved_channel)?;
if let Some(channel_info) = config_data.installed_channels.get(&resolved_channel) {
return get_julia_path_from_installed_channel(
versions_db,
config_data,
&resolved_channel,
juliaupconfig_path,
channel_info,
alias_args.clone(),
);
}
if matches!(
juliaup_channel_source,
JuliaupChannelSource::CmdLine | JuliaupChannelSource::Auto
) && (channel_valid
|| is_pr_channel(&resolved_channel)
|| is_nightly_channel(&resolved_channel))
{
let should_auto_install = match config_data.settings.auto_install_channels {
Some(auto_install) => auto_install, None => {
if is_interactive() {
handle_auto_install_prompt(&resolved_channel, paths)?
} else {
false
}
}
};
if should_auto_install {
let is_automatic = config_data.settings.auto_install_channels == Some(true);
spawn_juliaup_add(&resolved_channel, paths, is_automatic)?;
let updated_config_file = load_config_db(paths, None)
.with_context(|| "Failed to reload configuration after installing channel.")?;
let updated_channel_info = updated_config_file
.data
.installed_channels
.get(&resolved_channel);
if let Some(channel_info) = updated_channel_info {
return get_julia_path_from_installed_channel(
versions_db,
&updated_config_file.data,
&resolved_channel,
juliaupconfig_path,
channel_info,
alias_args,
);
} else {
return Err(anyhow!(
"Channel '{resolved_channel}' was installed but could not be found in configuration."
));
}
}
}
let error = match juliaup_channel_source {
JuliaupChannelSource::CmdLine => {
if channel_valid {
UserError { msg: format!("`{resolved_channel}` is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
} else if is_pr_channel(&resolved_channel) {
UserError { msg: format!("`{resolved_channel}` is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") }
} else if is_nightly_channel(&resolved_channel) {
UserError { msg: format!("`{resolved_channel}` is not installed. Please run `juliaup add {resolved_channel}` to install nightly channel.") }
} else {
UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}`. Please run `juliaup list` to get a list of valid channels and versions.") }
}
},
JuliaupChannelSource::EnvVar=> {
if channel_valid {
UserError { msg: format!("`{resolved_channel}` from environment variable JULIAUP_CHANNEL is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
} else if is_pr_channel(&resolved_channel) {
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.") }
} else {
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.") }
}
},
JuliaupChannelSource::Override=> {
if channel_valid {
UserError { msg: format!("`{resolved_channel}` from directory override is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
} else if is_pr_channel(&resolved_channel) {
UserError { msg: format!("`{resolved_channel}` from directory override is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") }
} else {
UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}` from directory override. Please run `juliaup list` to get a list of valid channels and versions.") }
}
},
JuliaupChannelSource::Auto => {
if channel_valid {
UserError { msg: format!("`{resolved_channel}` resolved from project manifest is not installed. Please run `juliaup add {resolved_channel}` to install channel or version.") }
} else if is_pr_channel(&resolved_channel) {
UserError { msg: format!("`{resolved_channel}` resolved from project manifest is not installed. Please run `juliaup add {resolved_channel}` to install pull request channel if available.") }
} else if is_nightly_channel(&resolved_channel) {
UserError { msg: format!("`{resolved_channel}` resolved from project manifest is not installed. Please run `juliaup add {resolved_channel}` to install nightly channel.") }
} else {
UserError { msg: format!("Invalid Juliaup channel `{resolved_channel}` resolved from project manifest. Please run `juliaup list` to get a list of valid channels and versions.") }
}
},
JuliaupChannelSource::Default => UserError {msg: format!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{resolved_channel}` is not installed.") }
};
Err(error.into())
}
fn get_julia_path_from_installed_channel(
versions_db: &JuliaupVersionDB,
config_data: &JuliaupConfig,
channel: &str,
juliaupconfig_path: &Path,
channel_info: &JuliaupConfigChannel,
alias_args: Vec<String>,
) -> Result<(PathBuf, Vec<String>)> {
match channel_info {
JuliaupConfigChannel::AliasChannel { .. } => {
bail!("Unexpected alias channel after resolution: {channel}");
}
JuliaupConfigChannel::LinkedChannel { command, args } => {
let mut combined_args = alias_args;
combined_args.extend(args.as_ref().map_or_else(Vec::new, |v| v.clone()));
Ok((PathBuf::from(command), combined_args))
}
JuliaupConfigChannel::SystemChannel { version } => {
let version_info = config_data
.installed_versions.get(version)
.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."))?;
if is_interactive() {
check_channel_uptodate(channel, version, versions_db).with_context(|| {
format!("The Julia launcher failed while checking whether the channel {channel} is up-to-date.")
})?;
}
let config_dir = juliaupconfig_path.parent().unwrap();
let absolute_path = if let Some(ref binary_path) = version_info.binary_path {
config_dir.join(binary_path)
} else {
let base_path = config_dir.join(&version_info.path);
resolve_julia_binary_path(&base_path)?
}
.normalize()
.with_context(|| {
format!(
"Failed to normalize path for Julia binary, starting from `{}`.",
juliaupconfig_path.display()
)
})?;
Ok((absolute_path.into_path_buf(), alias_args))
}
JuliaupConfigChannel::DirectDownloadChannel {
path,
url: _,
local_etag,
server_etag,
version: _,
binary_path,
} => {
if local_etag != server_etag && is_interactive() {
if channel.starts_with("nightly") {
print_juliaup_style(
"Info",
"A new `nightly` version is available. Install with `juliaup update`.",
JuliaupMessageType::Progress,
);
} else {
print_juliaup_style(
"Info",
&format!(
"A new version of Julia for the `{}` channel is available. Run:",
channel
),
JuliaupMessageType::Progress,
);
eprintln!();
eprintln!(" juliaup update");
eprintln!();
eprintln!("to install the latest Julia for the `{}` channel.", channel);
}
}
let config_dir = juliaupconfig_path.parent().unwrap();
let absolute_path = if let Some(ref bp) = binary_path {
config_dir.join(bp)
} else {
let base_path = config_dir.join(path);
resolve_julia_binary_path(&base_path)?
}
.normalize()
.with_context(|| {
format!(
"Failed to normalize path for Julia binary, starting from `{}`.",
juliaupconfig_path.display()
)
})?;
Ok((absolute_path.into_path_buf(), alias_args))
}
}
}
fn get_override_channel(
config_file: &juliaup::config_file::JuliaupReadonlyConfigFile,
) -> Result<Option<String>> {
let curr_dir = std::env::current_dir()?.canonicalize()?;
let juliaup_override = config_file
.data
.overrides
.iter()
.filter(|i| curr_dir.starts_with(&i.path))
.sorted_by_key(|i| i.path.len())
.next_back();
match juliaup_override {
Some(val) => Ok(Some(val.channel.clone())),
None => Ok(None),
}
}
fn run_app() -> Result<i32> {
if std::io::stdout().is_terminal() {
let term = Term::stdout();
term.set_title("Julia");
}
let paths = get_paths().with_context(|| "Trying to load all global paths.")?;
do_initial_setup(&paths.juliaupconfig)
.with_context(|| "The Julia launcher failed to run the initial setup steps.")?;
let config_file = load_config_db(&paths, None)
.with_context(|| "The Julia launcher failed to load a configuration file.")?;
let versiondb_data = load_versions_db(&paths)
.with_context(|| "The Julia launcher failed to load a versions db.")?;
let mut channel_from_cmd_line: Option<String> = None;
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
let first_arg = &args[1];
if let Some(stripped) = first_arg.strip_prefix('+') {
channel_from_cmd_line = Some(stripped.to_string());
}
}
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 Ok(Some(channel)) = get_auto_channel(
&args,
&versiondb_data,
config_file.data.settings.manifest_version_detect,
) {
(channel, JuliaupChannelSource::Auto)
} 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_path, julia_args) = get_julia_path_from_channel(
&versiondb_data,
&config_file.data,
&julia_channel_to_use,
&paths.juliaupconfig,
juliaup_channel_source,
&paths,
)
.with_context(|| {
format!(
"The Julia launcher failed to determine the command for the `{}` channel.",
julia_channel_to_use
)
})?;
let mut new_args: Vec<String> = Vec::new();
for i in julia_args {
new_args.push(i);
}
for (i, v) in args.iter().skip(1).enumerate() {
if i > 0 || !v.starts_with('+') {
new_args.push(v.clone());
}
}
#[cfg(not(windows))]
match unsafe { fork() } {
Ok(ForkResult::Parent { child, .. }) => {
match waitpid(child, None) {
Ok(WaitStatus::Exited(_, code)) => {
if code != 0 {
panic!("Could not fork (child process exited with code: {})", code)
}
}
Ok(_) => {
panic!("Could not fork (child process did not exit normally)");
}
Err(e) => {
panic!("Could not fork (error waiting for child process, {})", e);
}
}
let _ = std::process::Command::new(&julia_path)
.args(&new_args)
.exec();
panic!(
"Could not launch Julia. Verify that there is a valid Julia binary at \"{}\".",
julia_path.to_string_lossy()
)
}
Ok(ForkResult::Child) => {
match unsafe { fork() } {
Ok(ForkResult::Parent { child: _, .. }) => {
}
Ok(ForkResult::Child) => {
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")?;
run_selfupdate(&config_file).with_context(|| "Failed to run selfupdate.")?;
}
Err(_) => panic!("Could not double-fork"),
}
Ok(0)
}
Err(_) => panic!("Could not fork"),
}
#[cfg(windows)]
{
ctrlc::set_handler(|| ()).with_context(|| "Failed to set the Ctrl-C handler.")?;
let mut job_attr: windows::Win32::Security::SECURITY_ATTRIBUTES =
windows::Win32::Security::SECURITY_ATTRIBUTES::default();
let mut job_info: windows::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION =
windows::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
job_attr.bInheritHandle = false.into();
job_info.BasicLimitInformation.LimitFlags =
windows::Win32::System::JobObjects::JOB_OBJECT_LIMIT_BREAKAWAY_OK
| windows::Win32::System::JobObjects::JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
| windows::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
let job_handle = unsafe {
windows::Win32::System::JobObjects::CreateJobObjectW(
Some(&job_attr),
windows::core::PCWSTR::null(),
)
}?;
unsafe {
SetInformationJobObject(
job_handle,
windows::Win32::System::JobObjects::JobObjectExtendedLimitInformation,
&job_info as *const _ as *const std::os::raw::c_void,
std::mem::size_of_val(&job_info) as u32,
)
}?;
unsafe { AssignProcessToJobObject(job_handle, GetCurrentProcess()) }?;
let mut child_process = std::process::Command::new(julia_path)
.args(&new_args)
.spawn()
.with_context(|| "The Julia launcher failed to start Julia.")?;
let _ = unsafe {
AssignProcessToJobObject(
job_handle,
std::mem::transmute::<RawHandle, windows::Win32::Foundation::HANDLE>(
child_process.as_raw_handle(),
),
)
};
run_versiondb_update(&config_file).with_context(|| "Failed to run version db update")?;
run_selfupdate(&config_file).with_context(|| "Failed to run selfupdate.")?;
let status = child_process
.wait()
.with_context(|| "Failed to wait for Julia process to finish.")?;
let code = match status.code() {
Some(code) => code,
None => {
anyhow::bail!("There is no exit code, that should not be possible on Windows.");
}
};
Ok(code)
}
}
fn main() -> Result<std::process::ExitCode> {
let client_status: std::prelude::v1::Result<i32, anyhow::Error>;
{
human_panic::setup_panic!(human_panic::Metadata::new(
"Juliaup launcher",
env!("CARGO_PKG_VERSION")
)
.support("https://github.com/JuliaLang/juliaup"));
let env = env_logger::Env::new()
.filter("JULIAUP_LOG")
.write_style("JULIAUP_LOG_STYLE");
env_logger::init_from_env(env);
client_status = run_app();
if let Err(err) = &client_status {
if let Some(e) = err.downcast_ref::<UserError>() {
eprintln!("{} {}", style("ERROR:").red().bold(), e.msg);
return Ok(std::process::ExitCode::FAILURE);
} else {
return Err(client_status.unwrap_err());
}
}
}
std::process::exit(client_status?);
}