use crate::config_file::get_read_lock;
use crate::config_file::load_config_db;
use crate::config_file::load_mut_config_db;
use crate::config_file::save_config_db;
use crate::config_file::JuliaupConfig;
use crate::config_file::JuliaupConfigChannel;
use crate::config_file::JuliaupConfigVersion;
use crate::get_bundled_dbversion;
use crate::get_bundled_julia_version;
use crate::get_juliaup_target;
use crate::global_paths::GlobalPaths;
use crate::jsonstructs_versionsdb::JuliaupVersionDB;
use crate::utils::check_server_supports_nightlies;
use crate::utils::get_bin_dir;
use crate::utils::get_julianightlies_base_url;
use crate::utils::get_juliaserver_base_url;
use crate::utils::is_valid_julia_path;
use crate::utils::retry_rename;
use crate::utils::{print_juliaup_style, JuliaupMessageType};
use anyhow::{anyhow, bail, Context, Error, Result};
use bstr::ByteSlice;
use bstr::ByteVec;
use console::style;
#[cfg(not(target_os = "freebsd"))]
use flate2::read::GzDecoder;
use indicatif::{ProgressBar, ProgressStyle};
use indoc::formatdoc;
use regex::Regex;
use semver::Version;
#[cfg(not(windows))]
use std::os::unix::fs::PermissionsExt;
#[cfg(not(target_os = "freebsd"))]
use std::path::Component::Normal;
use std::{
io::{BufReader, Read, Seek, Write},
path::{Path, PathBuf},
};
#[cfg(not(target_os = "freebsd"))]
use tar::Archive;
use tempfile::Builder;
use tempfile::TempPath;
use url::Url;
const DOWNLOADING_PREFIX: &str = " Downloading";
#[cfg(not(windows))]
fn http_client() -> Result<reqwest::blocking::Client> {
let user_agent = format!("juliaup/{}", env!("CARGO_PKG_VERSION"));
reqwest::blocking::Client::builder()
.user_agent(user_agent)
.build()
.with_context(|| "Failed to create HTTP client")
}
#[cfg(windows)]
fn http_client() -> Result<windows::Web::Http::HttpClient> {
use windows::core::HSTRING;
let http_client =
windows::Web::Http::HttpClient::new().with_context(|| "Failed to create HttpClient.")?;
let user_agent = format!("juliaup/{}", env!("CARGO_PKG_VERSION"));
http_client
.DefaultRequestHeaders()
.with_context(|| "Failed to get default request headers.")?
.UserAgent()
.with_context(|| "Failed to get User-Agent header collection.")?
.TryParseAdd(&HSTRING::from(&user_agent))
.with_context(|| "Failed to set User-Agent header.")?;
Ok(http_client)
}
#[cfg(not(target_os = "freebsd"))]
fn unpack_sans_parent<R, P>(src: R, dst: P, levels_to_skip: usize) -> Result<()>
where
R: Read,
P: AsRef<Path>,
{
let tar = GzDecoder::new(src);
let mut archive = Archive::new(tar);
for entry in archive.entries()? {
let mut entry = entry?;
let path: PathBuf = entry
.path()?
.components()
.skip(levels_to_skip) .filter(|c| matches!(c, Normal(_))) .collect();
entry.unpack(dst.as_ref().join(path))?;
}
Ok(())
}
#[cfg(target_os = "freebsd")]
fn unpack_sans_parent<R, P>(mut src: R, dst: P, levels_to_skip: usize) -> Result<()>
where
R: Read,
P: AsRef<Path>,
{
std::fs::create_dir_all(dst.as_ref())?;
let mut tar = std::process::Command::new("tar")
.arg("-C")
.arg(dst.as_ref())
.arg("-x")
.arg("-z")
.arg(format!("--strip-components={}", levels_to_skip))
.arg("-f")
.arg("-")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.spawn()
.expect("Failed to spawn `tar` process");
let mut stdin = tar
.stdin
.take()
.expect("Failed to get stdin for `tar` process");
std::io::copy(&mut src, &mut stdin)?;
Ok(())
}
fn format_progress_bar(state: &indicatif::ProgressState, w: &mut dyn std::fmt::Write) {
use console::Style;
let width = 25; let pos = state.pos();
let len = state.len().unwrap_or(pos);
let perc = if len > 0 {
(pos as f64 / len as f64) * 100.0
} else {
0.0
};
let max_progress_width = width;
let n_filled = ((max_progress_width as f64 * perc / 100.0).floor() as usize).min(width);
let partial_filled = (max_progress_width as f64 * perc / 100.0) - n_filled as f64;
let cyan = Style::new().cyan();
let dim = Style::new().black().bright();
let filled_str = "━".repeat(n_filled);
let _ = write!(w, "{}", cyan.apply_to(filled_str));
if n_filled < width {
let n_left = width - n_filled - 1;
if partial_filled > 0.5 {
let _ = write!(w, "{}", cyan.apply_to("╸"));
} else {
let _ = write!(w, "{}", dim.apply_to("╺"));
}
let empty_str = "━".repeat(n_left);
let _ = write!(w, "{}", dim.apply_to(empty_str));
}
}
fn bar_style() -> ProgressStyle {
ProgressStyle::default_bar()
.template("{prefix:.cyan.bold} {bar} {bytes}/{total_bytes} eta: {eta}")
.unwrap()
.with_key("bar", format_progress_bar)
}
#[cfg(target_os = "macos")]
fn show_install_progress(message: &str) {
eprint!(
"\r\x1b[2K{} {}",
style(" Installing").cyan().bold(),
message
);
let _ = std::io::stderr().flush();
}
#[cfg(target_os = "macos")]
pub fn download_extract_dmg(url: &str, target_path: &Path) -> Result<String> {
use std::fs::File;
use std::io::Write;
struct DmgMountGuard {
mount_point: String,
armed: bool,
}
impl DmgMountGuard {
fn new(mount_point: String) -> Self {
Self {
mount_point,
armed: true,
}
}
fn detach(&mut self) {
if !self.armed {
return;
}
match std::process::Command::new("hdiutil")
.args(["detach", "-force", &self.mount_point])
.output()
{
Ok(output) if !output.status.success() => {
log::warn!(
"Failed to unmount DMG at {}: {}",
self.mount_point,
String::from_utf8_lossy(&output.stderr)
);
}
Err(e) => log::warn!("Failed to execute hdiutil detach: {}", e),
_ => {}
}
self.armed = false;
}
}
impl Drop for DmgMountGuard {
fn drop(&mut self) {
self.detach();
}
}
log::debug!("Downloading DMG from url `{}`.", url);
let response = http_client()?
.get(url)
.send()
.with_context(|| format!("Failed to download from url `{}`.", url))?;
if !response.status().is_success() {
bail!("DMG not found at URL (status: {})", response.status());
}
let pb = match response.content_length() {
Some(len) => ProgressBar::new(len),
None => ProgressBar::new_spinner(),
};
pb.set_prefix(DOWNLOADING_PREFIX);
pb.set_style(bar_style());
let etag = response
.headers()
.get("etag")
.ok_or_else(|| anyhow!("Failed to get etag from `{}`", url))?
.to_str()?
.to_string();
let temp_dmg = Builder::new().prefix("julia-").suffix(".dmg").tempfile()?;
let mut dmg_file = File::create(temp_dmg.path())?;
std::io::copy(&mut pb.wrap_read(response), &mut dmg_file)?;
dmg_file.flush()?;
drop(dmg_file);
pb.finish_and_clear();
if std::env::var_os("JULIAUP_TEST_DMG_FAIL").is_some() {
bail!("Simulated DMG install failure for tests.");
}
show_install_progress("Mounting installer...");
let mount_output = std::process::Command::new("hdiutil")
.args(["attach", "-nobrowse", "-plist"])
.arg(temp_dmg.path())
.output()?;
if !mount_output.status.success() {
bail!(
"Failed to mount DMG: {}",
String::from_utf8_lossy(&mount_output.stderr)
);
}
let mount_output_str = String::from_utf8_lossy(&mount_output.stdout);
let mount_point = mount_output_str
.lines()
.enumerate()
.find_map(|(i, line)| {
if line.trim() == "<key>mount-point</key>" {
mount_output_str.lines().nth(i + 1).and_then(|next_line| {
let trimmed = next_line.trim();
trimmed
.strip_prefix("<string>")
.and_then(|s| s.strip_suffix("</string>"))
.map(|s| s.to_string())
})
} else {
None
}
})
.ok_or_else(|| {
anyhow!(
"Failed to parse mount point from hdiutil output. Output:\n{}",
mount_output_str
)
})?;
log::debug!("Mounted DMG at: {}", mount_point);
let mut mount_guard = DmgMountGuard::new(mount_point.clone());
show_install_progress("Copying application...");
let mount_path = Path::new(&mount_point);
let app_bundle = std::fs::read_dir(mount_path)?
.filter_map(|e| e.ok())
.find(|e| e.file_name().to_str().is_some_and(|n| n.ends_with(".app")))
.ok_or_else(|| anyhow!("No .app bundle found in DMG."))?;
std::fs::create_dir_all(target_path)?;
let copy_status = std::process::Command::new("cp")
.args(["-R"])
.arg(app_bundle.path())
.arg(target_path)
.status()?;
if !copy_status.success() {
bail!("Failed to copy .app bundle from DMG.");
}
show_install_progress("Unmounting installer...");
mount_guard.detach();
eprint!("\r\x1b[2K");
let _ = std::io::stderr().flush();
Ok(etag)
}
#[cfg(target_os = "macos")]
fn strip_quarantine_attribute(path: &Path) {
let _ = std::process::Command::new("xattr")
.args(["-dr", "com.apple.quarantine"])
.arg(path)
.output();
}
#[cfg(target_os = "macos")]
fn try_download_dmg_with_fallback(url: &url::Url, target_path: &Path) -> Result<(String, bool)> {
let dmg_url = if url.as_str().ends_with(".tar.gz") {
url.as_str().replace(".tar.gz", ".dmg")
} else {
url.to_string()
};
if let Ok(dmg_url) = url::Url::parse(&dmg_url) {
if let Ok(etag) = download_extract_dmg(dmg_url.as_ref(), target_path) {
strip_quarantine_attribute(target_path);
return Ok((etag, true));
}
}
let etag = download_extract_sans_parent(url.as_ref(), target_path, 1)?;
strip_quarantine_attribute(target_path);
Ok((etag, false))
}
#[cfg(not(windows))]
pub fn download_extract_sans_parent(
url: &str,
target_path: &Path,
levels_to_skip: usize,
) -> Result<String> {
log::debug!("Downloading from url `{}`.", url);
let response = http_client()?
.get(url)
.send()
.with_context(|| format!("Failed to download from url `{}`.", url))?;
let content_length = response.content_length();
let pb = match content_length {
Some(content_length) => ProgressBar::new(content_length),
None => ProgressBar::new_spinner(),
};
pb.set_prefix(DOWNLOADING_PREFIX);
pb.set_style(bar_style());
let last_modified = response
.headers()
.get("etag")
.map(|etag| etag.to_str().unwrap_or("").to_string())
.unwrap_or_default();
let response_with_pb = pb.wrap_read(response);
unpack_sans_parent(response_with_pb, target_path, levels_to_skip)
.with_context(|| format!("Failed to extract downloaded file from url `{}`.", url))?;
Ok(last_modified)
}
#[cfg(windows)]
struct DataReaderWrap(windows::Storage::Streams::DataReader);
#[cfg(windows)]
impl std::io::Read for DataReaderWrap {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let mut bytes =
self.0
.LoadAsync(buf.len() as u32)
.map_err(|e| std::io::Error::from_raw_os_error(e.code().0))?
.join()
.map_err(|e| std::io::Error::from_raw_os_error(e.code().0))? as usize;
bytes = bytes.min(buf.len());
self.0
.ReadBytes(&mut buf[0..bytes])
.map_err(|e| std::io::Error::from_raw_os_error(e.code().0))
.map(|_| bytes)
}
}
#[cfg(windows)]
pub fn download_extract_sans_parent(
url: &str,
target_path: &Path,
levels_to_skip: usize,
) -> Result<String> {
use windows::core::HSTRING;
let http_client = http_client()?;
let request_uri = windows::Foundation::Uri::CreateUri(&HSTRING::from(url))
.with_context(|| "Failed to convert url string to Uri.")?;
let http_response = http_client
.GetAsync(&request_uri)
.with_context(|| "Failed to initiate download.")?
.join()
.with_context(|| "Failed to complete async download operation.")?;
http_response
.EnsureSuccessStatusCode()
.with_context(|| format!("Failed to download from `{}`.", url))?;
let last_modified = http_response
.Headers()
.ok()
.and_then(|headers| headers.Lookup(&HSTRING::from("etag")).ok())
.map(|etag| etag.to_string())
.unwrap_or_default();
let http_response_content = http_response
.Content()
.with_context(|| "Failed to obtain content from http response.")?;
let response_stream = http_response_content
.ReadAsInputStreamAsync()
.with_context(|| "Failed to initiate get input stream from response")?
.join()
.with_context(|| "Failed to obtain input stream from http response")?;
let reader = windows::Storage::Streams::DataReader::CreateDataReader(&response_stream)
.with_context(|| "Failed to create DataReader.")?;
reader
.SetInputStreamOptions(windows::Storage::Streams::InputStreamOptions::ReadAhead)
.with_context(|| "Failed to set input stream options.")?;
let mut content_length: u64 = 0;
let pb = if http_response_content.TryComputeLength(&mut content_length)? {
ProgressBar::new(content_length)
} else {
ProgressBar::new_spinner()
};
pb.set_prefix(DOWNLOADING_PREFIX);
pb.set_style(bar_style());
let response_with_pb = pb.wrap_read(DataReaderWrap(reader));
unpack_sans_parent(response_with_pb, target_path, levels_to_skip)
.with_context(|| format!("Failed to extract downloaded file from url `{}`.", url))?;
Ok(last_modified)
}
#[cfg(not(windows))]
pub fn download_juliaup_version(url: &str) -> Result<Version> {
let response = http_client()?
.get(url)
.send()
.with_context(|| format!("Failed to download from url `{}`.", url))?;
let status = response.status();
if !status.is_success() {
let status_code = status.as_u16();
let hint = match status_code {
404 => "Resource not found",
503 => "Service temporarily unavailable, please try again later",
_ => "Unexpected server error",
};
anyhow::bail!(
"Failed to download from `{}`: HTTP {} - {}",
url,
status,
hint
);
}
let response_text = response.text()?;
let trimmed_response = response_text.trim();
let version = Version::parse(trimmed_response).with_context(|| {
format!(
"`download_juliaup_version` failed to parse `{}` as a valid semversion.",
trimmed_response
)
})?;
Ok(version)
}
#[cfg(not(windows))]
pub fn download_versiondb(url: &str, path: &Path) -> Result<()> {
let mut response = http_client()?
.get(url)
.send()
.with_context(|| format!("Failed to download from url `{}`.", url))?;
let status = response.status();
if !status.is_success() {
let status_code = status.as_u16();
let hint = match status_code {
404 => "Resource not found",
503 => "Service temporarily unavailable, please try again later",
_ => "Unexpected server error",
};
anyhow::bail!(
"Failed to download version database from `{}`: HTTP {} - {}",
url,
status,
hint
);
}
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.with_context(|| format!("Failed to open or create version db file at {:?}", path))?;
let mut buf: Vec<u8> = vec![];
response.copy_to(&mut buf)?;
file.write_all(buf.as_slice())
.with_context(|| "Failed to write content into version db file.")?;
Ok(())
}
#[cfg(windows)]
pub fn download_juliaup_version(url: &str) -> Result<Version> {
let http_client = http_client()?;
let request_uri = windows::Foundation::Uri::CreateUri(&windows::core::HSTRING::from(url))
.with_context(|| "Failed to convert url string to Uri.")?;
let async_op = http_client
.GetStringAsync(&request_uri)
.with_context(|| "Failed on http_client.GetStringAsync")?;
let response = async_op
.join()
.with_context(|| "Failed on http_client.GetStringAsync.get")?
.to_string();
let trimmed_response = response.trim();
let version = Version::parse(trimmed_response).with_context(|| {
format!(
"`download_juliaup_version` failed to parse `{}` as a valid semversion.",
trimmed_response
)
})?;
Ok(version)
}
#[cfg(windows)]
pub fn download_versiondb(url: &str, path: &Path) -> Result<()> {
let http_client = http_client()?;
let request_uri = windows::Foundation::Uri::CreateUri(&windows::core::HSTRING::from(url))
.with_context(|| "Failed to convert url string to Uri.")?;
let response = http_client
.GetStringAsync(&request_uri)
.with_context(|| "Failed to download version db step 1.")?
.join()
.with_context(|| "Failed to download version db step 2.")?
.to_string();
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.with_context(|| format!("Failed to open or create version db file at {:?}", path))?;
file.write_all(response.as_bytes())
.with_context(|| "Failed to write content into version db file.")?;
Ok(())
}
fn compute_relative_binary_path(
target_path: &Path,
rel_prefix: &Path,
juliauphome: &Path,
) -> Option<String> {
crate::utils::resolve_julia_binary_path(target_path)
.ok()
.map(|p| {
p.strip_prefix(target_path)
.map(|suffix| rel_prefix.join(suffix).to_string_lossy().into_owned())
.unwrap_or_else(|_| {
p.strip_prefix(juliauphome)
.map(|rel| PathBuf::from(".").join(rel).to_string_lossy().into_owned())
.unwrap_or_else(|_| {
rel_prefix
.join("bin")
.join("julia")
.to_string_lossy()
.into_owned()
})
})
})
}
pub fn install_version(
fullversion: &String,
config_data: &mut JuliaupConfig,
version_db: &JuliaupVersionDB,
paths: &GlobalPaths,
) -> Result<()> {
if config_data.installed_versions.contains_key(fullversion) {
return Ok(());
}
let full_version_string_of_bundled_version = get_bundled_julia_version();
let my_own_path = std::env::current_exe()?;
let path_of_bundled_version = my_own_path
.parent()
.unwrap() .join("BundledJulia");
let child_target_foldername = format!("julia-{}", fullversion);
let target_path = paths.juliauphome.join(&child_target_foldername);
std::fs::create_dir_all(target_path.parent().unwrap())?;
if fullversion == full_version_string_of_bundled_version && path_of_bundled_version.exists() {
let mut options = fs_extra::dir::CopyOptions::new();
options.overwrite = true;
options.content_only = true;
fs_extra::dir::copy(path_of_bundled_version, &target_path, &options)?;
} else {
let juliaupserver_base =
get_juliaserver_base_url().with_context(|| "Failed to get Juliaup server base URL.")?;
let download_url_path = &version_db
.available_versions
.get(fullversion)
.ok_or_else(|| {
anyhow!(
"Failed to find download url in versions db for '{}'.",
fullversion
)
})?
.url_path;
let download_url = juliaupserver_base
.join(download_url_path)
.with_context(|| {
format!(
"Failed to construct a valid url from '{}' and '{}'.",
juliaupserver_base, download_url_path
)
})?;
print_juliaup_style(
"Installing",
&format!("Julia {}", fullversion),
JuliaupMessageType::Progress,
);
#[cfg(target_os = "macos")]
let used_dmg = {
let (_, used_dmg) = try_download_dmg_with_fallback(&download_url, &target_path)?;
used_dmg
};
#[cfg(target_os = "macos")]
if !used_dmg {
let needs_notarization_check = semver::Version::parse(fullversion)
.ok()
.zip(semver::Version::parse("1.11.0-rc1").ok())
.is_some_and(|(v, threshold)| v > threshold);
if needs_notarization_check {
let julia_path = crate::utils::resolve_julia_binary_path(&target_path)?;
check_stdlib_notarization(&julia_path);
}
}
#[cfg(not(target_os = "macos"))]
{
download_extract_sans_parent(download_url.as_ref(), &target_path, 1)?;
}
}
let mut rel_path = PathBuf::new();
rel_path.push(".");
rel_path.push(&child_target_foldername);
let binary_path = compute_relative_binary_path(&target_path, &rel_path, &paths.juliauphome);
config_data.installed_versions.insert(
fullversion.clone(),
JuliaupConfigVersion {
path: rel_path.to_string_lossy().into_owned(),
binary_path,
},
);
Ok(())
}
pub fn default_arch() -> Result<String> {
if cfg!(target_arch = "aarch64") {
Ok("aarch64".to_string())
} else if cfg!(target_arch = "x86_64") {
Ok("x64".to_string())
} else if cfg!(target_arch = "x86") {
Ok("x86".to_string())
} else {
bail!("Unsupported architecture for nightly channel.")
}
}
pub fn compatible_archs() -> Result<Vec<String>> {
if cfg!(target_os = "macos") {
if cfg!(target_arch = "x86_64") {
Ok(vec!["x64".to_string()])
} else if cfg!(target_arch = "aarch64") {
Ok(vec!["aarch64".to_string(), "x64".to_string()])
} else {
bail!("Unsupported architecture for nightly channel on macOS.")
}
} else if cfg!(target_arch = "x86") {
Ok(vec!["x86".to_string()])
} else if cfg!(target_arch = "x86_64") {
if cfg!(target_os = "freebsd") {
Ok(vec!["x64".to_string()])
} else {
Ok(vec!["x86".to_string(), "x64".to_string()])
}
} else if cfg!(target_arch = "aarch64") {
Ok(vec!["aarch64".to_string()])
} else {
bail!("Unsupported architecture for nightly channel.")
}
}
pub fn get_channel_variations(channel: &str) -> Result<Vec<String>> {
let archs = compatible_archs()?;
let channels: Vec<String> = std::iter::once(channel.to_string())
.chain(
archs
.into_iter()
.map(|arch| format!("{}~{}", channel, arch)),
)
.collect();
Ok(channels)
}
pub fn is_valid_channel(versions_db: &JuliaupVersionDB, channel: &String) -> Result<bool> {
let regular = versions_db.has_channel(channel);
let nightly_chans = get_channel_variations("nightly")?;
let nightly = nightly_chans.contains(channel);
Ok(regular || nightly)
}
pub fn is_pr_channel(channel: &str) -> bool {
Regex::new(r"^(pr\d+)(~|$)").unwrap().is_match(channel)
}
fn parse_nightly_channel_or_id(channel: &str) -> Option<String> {
let nightly_re =
Regex::new(r"^((?:nightly|latest)|latest|(\d+\.\d+)-(?:nightly|latest))").unwrap();
let caps = nightly_re.captures(channel)?;
if let Some(xy_match) = caps.get(2) {
Some(xy_match.as_str().to_string())
} else {
Some("".to_string())
}
}
pub fn channel_to_name(channel: &str) -> Result<String> {
let mut parts = channel.splitn(2, '~');
let channel = parts.next().expect("Failed to parse channel name.");
let version = if let Some(version_prefix) = parse_nightly_channel_or_id(channel) {
if version_prefix.is_empty() {
"latest".to_string()
} else {
format!("{}-latest", version_prefix)
}
} else {
channel.to_string()
};
let arch = match parts.next() {
Some(arch) => arch.to_string(),
None => default_arch()?,
};
let os_arch_suffix = {
#[cfg(target_os = "macos")]
if arch == "x64" {
"macos-x86_64"
} else if arch == "aarch64" {
"macos-aarch64"
} else {
bail!("Unsupported architecture for nightly channel on macOS.")
}
#[cfg(target_os = "windows")]
if arch == "x64" {
"win64"
} else if arch == "x86" {
"win32"
} else {
bail!("Unsupported architecture for nightly channel on Windows.")
}
#[cfg(target_os = "linux")]
if arch == "x64" {
"linux-x86_64"
} else if arch == "x86" {
"linux-i686"
} else if arch == "aarch64" {
"linux-aarch64"
} else {
bail!("Unsupported architecture for nightly channel on Linux.")
}
#[cfg(target_os = "freebsd")]
if arch == "x64" {
"freebsd-x86_64"
} else {
bail!("Unsupported architecture for nightly channel on FreeBSD.")
}
};
Ok(version.to_string() + "-" + os_arch_suffix)
}
fn query_julia_version(julia_path: &Path) -> Result<String> {
let output = std::process::Command::new(julia_path)
.arg("--startup-file=no")
.arg("-e")
.arg("print(VERSION)")
.output()
.with_context(|| {
format!(
"Failed to execute Julia binary at `{}`.",
julia_path.display()
)
})?;
Ok(String::from_utf8(output.stdout)?)
}
pub fn install_from_url(
url: &Url,
path: &PathBuf,
#[cfg_attr(not(target_os = "macos"), allow(unused))] is_pr: bool,
paths: &GlobalPaths,
) -> Result<(crate::config_file::JuliaupConfigChannel, bool)> {
if !check_server_supports_nightlies()
.context("Failed to check if nightly server supports etag headers")?
{
bail!(
"The configured nightly server does not support etag headers, which are required for nightly and PR channels.\n\
Nightly and PR channels cannot be installed from this server."
);
}
let temp_dir = Builder::new()
.prefix("julia-temp-")
.tempdir_in(&paths.juliauphome)
.expect("Failed to create temporary directory");
#[cfg(target_os = "macos")]
let (server_etag, used_dmg) = try_download_dmg_with_fallback(url, temp_dir.path())?;
#[cfg(not(target_os = "macos"))]
let (server_etag, used_dmg) = {
let download_result = download_extract_sans_parent(url.as_ref(), temp_dir.path(), 1);
match download_result {
Ok(last_updated) => (last_updated, false),
Err(e) => {
std::fs::remove_dir_all(temp_dir.path())?;
bail!("Failed to download and extract pr or nightly: {}", e);
}
}
};
#[cfg(target_os = "macos")]
let did_codesign = if is_pr && !used_dmg {
prompt_and_codesign_pr_build(temp_dir.path())?
} else {
true };
let julia_path = crate::utils::resolve_julia_binary_path(temp_dir.path())
.with_context(|| "Failed to resolve Julia binary path after extraction.")?;
#[cfg(target_os = "macos")]
let julia_version = if did_codesign {
let version = query_julia_version(&julia_path)?;
if !used_dmg {
check_stdlib_notarization(&julia_path);
}
version
} else {
String::new()
};
#[cfg(not(target_os = "macos"))]
let julia_version = query_julia_version(&julia_path)?;
let target_path = paths.juliauphome.join(path);
if target_path.exists() {
std::fs::remove_dir_all(&target_path)?;
}
retry_rename(&temp_dir.keep(), &target_path)?;
let binary_path = compute_relative_binary_path(&target_path, path, &paths.juliauphome);
Ok((
JuliaupConfigChannel::DirectDownloadChannel {
path: path.to_string_lossy().into_owned(),
url: url.to_string().to_owned(), local_etag: server_etag.clone(), server_etag,
version: julia_version,
binary_path,
},
used_dmg,
))
}
pub fn install_non_db_version(
channel: &str,
name: &String,
paths: &GlobalPaths,
) -> Result<(crate::config_file::JuliaupConfigChannel, bool)> {
if !check_server_supports_nightlies()
.context("Failed to check if nightly server supports etag headers")?
{
bail!(
"The configured nightly server does not support etag headers, which are required for nightly and PR channels.\n\
Nightly and PR channels cannot be installed from this server."
);
}
let download_url_base = get_julianightlies_base_url()?;
let mut parts = name.splitn(2, '-');
let mut id = parts
.next()
.expect("Failed to parse channel name.")
.to_string();
let mut arch = parts.next().expect("Failed to parse channel name.");
if arch.starts_with("latest") {
let mut parts = arch.splitn(2, '-');
let nightly = parts.next().expect("Failed to parse channel name.");
id.push('-');
id.push_str(nightly);
arch = parts.next().expect("Failed to parse channel name.");
}
let nightly_version = parse_nightly_channel_or_id(&id);
let download_url_path = if let Some(nightly_version) = nightly_version {
let nightly_folder = if nightly_version.is_empty() {
"".to_string() } else {
format!("/{}", nightly_version) };
match arch {
"macos-x86_64" => Ok(format!(
"bin/macos/x86_64{}/julia-latest-macos-x86_64.tar.gz",
nightly_folder
)),
"macos-aarch64" => Ok(format!(
"bin/macos/aarch64{}/julia-latest-macos-aarch64.tar.gz",
nightly_folder
)),
"win64" => Ok(format!(
"bin/winnt/x64{}/julia-latest-win64.tar.gz",
nightly_folder
)),
"win32" => Ok(format!(
"bin/winnt/x86{}/julia-latest-win32.tar.gz",
nightly_folder
)),
"linux-x86_64" => Ok(format!(
"bin/linux/x86_64{}/julia-latest-linux-x86_64.tar.gz",
nightly_folder
)),
"linux-i686" => Ok(format!(
"bin/linux/i686{}/julia-latest-linux-i686.tar.gz",
nightly_folder
)),
"linux-aarch64" => Ok(format!(
"bin/linux/aarch64{}/julia-latest-linux-aarch64.tar.gz",
nightly_folder
)),
"freebsd-x86_64" => Ok(format!(
"bin/freebsd/x86_64{}/julia-latest-freebsd-x86_64.tar.gz",
nightly_folder
)),
_ => Err(anyhow!("Unknown nightly.")),
}
} else if id.starts_with("pr") {
match arch {
"macos-x86_64" => {
Ok("bin/macos/x86_64/julia-".to_owned() + &id + "-macos-x86_64.tar.gz")
}
"macos-aarch64" => {
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")
}
"linux-aarch64" => {
Ok("bin/linux/aarch64/julia-".to_owned() + &id + "-linux-aarch64.tar.gz")
}
"freebsd-x86_64" => {
Ok("bin/freebsd/x86_64/julia-".to_owned() + &id + "-freebsd-x86_64.tar.gz")
}
_ => Err(anyhow!("Unknown pr.")),
}
} else {
Err(anyhow!("Unknown non-db channel."))
}?;
let download_url = download_url_base
.join(download_url_path.as_str())
.with_context(|| {
format!(
"Failed to construct a valid url from '{}' and '{}'.",
download_url_base, download_url_path
)
})?;
let child_target_foldername = format!("julia-{}", channel);
let mut rel_path = PathBuf::new();
rel_path.push(".");
rel_path.push(&child_target_foldername);
print_juliaup_style(
"Installing",
&format!("Julia {}", name),
JuliaupMessageType::Progress,
);
let (channel_data, used_dmg) =
install_from_url(&download_url, &rel_path, is_pr_channel(channel), paths)?;
Ok((channel_data, used_dmg))
}
pub fn garbage_collect_versions(
prune_linked: bool,
config_data: &mut JuliaupConfig,
paths: &GlobalPaths,
) -> Result<()> {
let mut versions_to_uninstall: Vec<String> = Vec::new();
for (installed_version, detail) in &config_data.installed_versions {
if config_data.installed_channels.iter().all(|j| match &j.1 {
JuliaupConfigChannel::SystemChannel { version } => version != installed_version,
_ => true,
}) {
let path_to_delete = paths.juliauphome.join(&detail.path).canonicalize()?;
let display = path_to_delete.display();
match std::fs::remove_dir_all(&path_to_delete) {
Ok(_) => versions_to_uninstall.push(installed_version.clone()),
Err(_) => print_juliaup_style(
"WARNING",
&format!(
"Failed to delete {}. \
Make sure to close any old julia version still running.\n\
You can try to delete at a later point by running `juliaup gc`.",
display
),
JuliaupMessageType::Warning,
),
}
}
}
if versions_to_uninstall.is_empty() {
print_juliaup_style(
"Tidyup",
"No unused Julia installations to clean up.",
JuliaupMessageType::Success,
);
} else {
for i in versions_to_uninstall {
print_juliaup_style(
"Tidyup",
&format!("Removed Julia {}", &i),
JuliaupMessageType::Success,
);
config_data.installed_versions.remove(&i);
}
}
if prune_linked {
let mut channels_to_uninstall: Vec<String> = Vec::new();
for (installed_channel, detail) in &config_data.installed_channels {
if let JuliaupConfigChannel::LinkedChannel {
command: cmd,
args: _,
} = &detail
{
if !is_valid_julia_path(&PathBuf::from(cmd)) {
channels_to_uninstall.push(installed_channel.clone());
}
}
}
for channel in channels_to_uninstall {
remove_symlink(&format!("julia-{}", &channel))?;
config_data.installed_channels.remove(&channel);
}
}
Ok(())
}
fn _remove_symlink(symlink_path: &Path) -> Result<Option<PathBuf>> {
std::fs::create_dir_all(symlink_path.parent().unwrap())?;
if symlink_path.exists() {
let prev_target = std::fs::read_link(symlink_path)?;
std::fs::remove_file(symlink_path)?;
return Ok(Some(prev_target));
}
Ok(None)
}
pub fn remove_symlink(symlink_name: &String) -> Result<()> {
let symlink_path = get_bin_dir()
.with_context(|| "Failed to retrieve binary directory while trying to remove a symlink.")?
.join(symlink_name);
print_juliaup_style(
"Deleting",
&format!("symlink {}.", symlink_name),
JuliaupMessageType::Progress,
);
_remove_symlink(&symlink_path)?;
Ok(())
}
#[cfg(not(windows))]
fn create_system_channel_symlink(
version: &str,
symlink_name: &str,
symlink_path: &Path,
paths: &GlobalPaths,
updating: &Option<PathBuf>,
) -> Result<()> {
let child_target_foldername = format!("julia-{}", version);
let target_path = paths.juliauphome.join(&child_target_foldername);
if let Some(ref prev_target) = updating {
print_juliaup_style(
"Updating",
&format!(
"symlink {} ( {} -> {} )",
symlink_name,
prev_target.to_string_lossy(),
version
),
JuliaupMessageType::Progress,
);
} else {
print_juliaup_style(
"Creating",
&format!("symlink {} for Julia {}", symlink_name, version),
JuliaupMessageType::Progress,
);
}
let binary_path = crate::utils::resolve_julia_binary_path(&target_path)?;
std::os::unix::fs::symlink(binary_path, symlink_path).with_context(|| {
format!(
"failed to create symlink `{}`.",
symlink_path.to_string_lossy()
)
})
}
#[cfg(not(windows))]
fn create_direct_download_symlink(
path: &str,
version: &str,
symlink_name: &str,
symlink_path: &Path,
paths: &GlobalPaths,
updating: &Option<PathBuf>,
) -> Result<()> {
let target_path = paths.juliauphome.join(path);
if let Some(ref prev_target) = updating {
print_juliaup_style(
"Updating",
&format!(
"symlink {} ( {} -> {} )",
symlink_name,
prev_target.to_string_lossy(),
version
),
JuliaupMessageType::Progress,
);
} else {
print_juliaup_style(
"Creating",
&format!("symlink {} for Julia {}", symlink_name, version),
JuliaupMessageType::Progress,
);
}
let binary_path = crate::utils::resolve_julia_binary_path(&target_path)?;
std::os::unix::fs::symlink(binary_path, symlink_path).with_context(|| {
format!(
"failed to create symlink `{}`.",
symlink_path.to_string_lossy()
)
})
}
#[cfg(not(windows))]
fn create_linked_channel_shim(
command: &str,
args: &Option<Vec<String>>,
symlink_name: &str,
symlink_path: &Path,
updating: &Option<PathBuf>,
) -> Result<()> {
let formatted_command = match args {
Some(x) => format!("{} {}", command, x.join(" ")),
None => command.to_string(),
};
if let Some(ref prev_target) = updating {
print_juliaup_style(
"Updating",
&format!(
"shim {} ( {} -> {} )",
symlink_name,
prev_target.to_string_lossy(),
formatted_command
),
JuliaupMessageType::Progress,
);
} else {
print_juliaup_style(
"Creating",
&format!("shim {} for {}", symlink_name, formatted_command),
JuliaupMessageType::Progress,
);
}
std::fs::write(
symlink_path,
format!(
r#"#!/bin/sh
{} "$@"
"#,
formatted_command,
),
)
.with_context(|| {
format!(
"failed to create shim `{}`.",
symlink_path.to_string_lossy()
)
})?;
let perms = std::fs::Permissions::from_mode(0o755);
std::fs::set_permissions(symlink_path, perms).with_context(|| {
format!(
"failed to change permissions for shim `{}`.",
symlink_path.to_string_lossy()
)
})
}
#[cfg(not(windows))]
pub fn create_symlink(
channel: &JuliaupConfigChannel,
symlink_name: &String,
paths: &GlobalPaths,
) -> Result<()> {
let symlink_folder = get_bin_dir()
.with_context(|| "Failed to retrieve binary directory while trying to create a symlink.")?;
let symlink_path = symlink_folder.join(symlink_name);
let updating = _remove_symlink(&symlink_path)?;
match channel {
JuliaupConfigChannel::SystemChannel { version } => {
create_system_channel_symlink(version, symlink_name, &symlink_path, paths, &updating)
}
JuliaupConfigChannel::DirectDownloadChannel { path, version, .. } => {
create_direct_download_symlink(
path,
version,
symlink_name,
&symlink_path,
paths,
&updating,
)
}
JuliaupConfigChannel::LinkedChannel { command, args } => {
create_linked_channel_shim(command, args, symlink_name, &symlink_path, &updating)
}
JuliaupConfigChannel::AliasChannel { .. } => Ok(()), }?;
if updating.is_none() {
if let Ok(path) = std::env::var("PATH") {
if !path.split(':').any(|p| Path::new(p) == symlink_folder) {
eprintln!(
"Symlink {} added in {}. Add this directory to the system PATH to make the command available in your shell.",
&symlink_name, symlink_folder.display(),
);
}
}
}
Ok(())
}
#[cfg(windows)]
pub fn create_symlink(_: &JuliaupConfigChannel, _: &String, _paths: &GlobalPaths) -> Result<()> {
Ok(())
}
#[cfg(feature = "selfupdate")]
pub fn install_background_selfupdate(interval: i64) -> Result<()> {
use itertools::Itertools;
use std::process::Stdio;
let own_exe_path = std::env::current_exe()
.with_context(|| "Could not determine the path of the running exe.")?;
let my_own_path = own_exe_path.to_str().unwrap();
match std::env::var("WSL_DISTRO_NAME") {
Ok(val) => {
std::process::Command::new("schtasks.exe")
.args([
"/create",
"/sc",
"minute",
"/mo",
&interval.to_string(),
"/tn",
&format!("Juliaup self update for WSL {} distribution", val),
"/f",
"/it",
"/tr",
&format!("wsl --distribution {} {} self update", val, my_own_path),
])
.output()
.with_context(|| "Failed to create new Windows task for juliaup.")?;
}
Err(_e) => {
let output = std::process::Command::new("crontab")
.args(["-l"])
.output()
.with_context(|| "Failed to retrieve crontab configuration.")?;
let new_crontab_content = String::from_utf8(output.stdout)?
.lines()
.filter(|x| !x.contains("4c79c12db1d34bbbab1f6c6f838f423f"))
.chain([
&format!(
"*/{} * * * * {} 4c79c12db1d34bbbab1f6c6f838f423f",
interval, my_own_path
),
"",
])
.join("\n");
let mut child = std::process::Command::new("crontab")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let mut child_stdin = child.stdin.take().unwrap();
child_stdin.write_all(new_crontab_content.as_bytes())?;
drop(child_stdin);
child.wait_with_output()?;
}
};
Ok(())
}
#[cfg(feature = "selfupdate")]
pub fn uninstall_background_selfupdate() -> Result<()> {
use itertools::Itertools;
use std::process::Stdio;
match std::env::var("WSL_DISTRO_NAME") {
Ok(val) => {
std::process::Command::new("schtasks.exe")
.args([
"/delete",
"/tn",
&format!("Juliaup self update for WSL {} distribution", val),
"/f",
])
.output()
.with_context(|| "Failed to remove Windows task for juliaup.")?;
}
Err(_e) => {
let output = std::process::Command::new("crontab")
.args(["-l"])
.output()
.with_context(|| "Failed to remove cron task.")?;
let new_crontab_content = String::from_utf8(output.stdout)?
.lines()
.filter(|x| !x.contains("4c79c12db1d34bbbab1f6c6f838f423f"))
.chain([""])
.join("\n");
let mut child = std::process::Command::new("crontab")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let mut child_stdin = child.stdin.take().unwrap();
child_stdin.write_all(new_crontab_content.as_bytes())?;
drop(child_stdin);
child.wait_with_output()?;
}
};
Ok(())
}
const S_MARKER: &[u8] = b"# >>> juliaup initialize >>>";
const E_MARKER: &[u8] = b"# <<< juliaup initialize <<<";
const HEADER: &[u8] = b"\n\n# !! Contents within this block are managed by juliaup !!\n\n";
fn get_shell_script_juliaup_content(bin_path: &Path, path: &Path) -> Result<Vec<u8>> {
let mut result: Vec<u8> = Vec::new();
let bin_path_str = match bin_path.to_str() {
Some(s) => s,
None => bail!("Could not create UTF-8 string from passed-in binary application path. Currently only valid UTF-8 paths are supported"),
};
result.extend_from_slice(S_MARKER);
result.extend_from_slice(HEADER);
if path.file_name().unwrap() == ".zshrc" {
append_zsh_content(&mut result, bin_path_str);
} else {
append_sh_content(&mut result, bin_path_str);
}
result.extend_from_slice(b"\n");
result.extend_from_slice(E_MARKER);
Ok(result)
}
fn append_zsh_content(buf: &mut Vec<u8>, path_str: &str) {
let content = formatdoc!(
"
path=('{}' $path)
export PATH
",
path_str
);
buf.extend_from_slice(content.as_bytes());
}
fn append_sh_content(buf: &mut Vec<u8>, path_str: &str) {
let content = formatdoc!(
"
case \":$PATH:\" in
*:{0}:*)
;;
*)
export PATH={0}${{PATH:+:${{PATH}}}}
;;
esac
",
path_str
);
buf.extend_from_slice(content.as_bytes());
}
fn match_markers(buffer: &[u8]) -> Result<Option<(usize, usize)>> {
let start_marker = buffer.find(S_MARKER);
let end_marker = buffer.find(E_MARKER);
let (start_marker, end_marker) = match (start_marker, end_marker) {
(Some(sidx), Some(eidx)) => {
if sidx != buffer.rfind(S_MARKER).unwrap() || eidx != buffer.rfind(E_MARKER).unwrap() {
bail!("Found multiple startup script sections from juliaup.");
}
(sidx, eidx)
}
(None, None) => {
return Ok(None);
}
(_, None) => {
bail!("Found an opening marker but no end marker of juliaup section.");
}
(None, _) => {
bail!("Found an opening marker but no end marker of juliaup section.");
}
};
Ok(Some((start_marker, end_marker + E_MARKER.len())))
}
fn add_path_to_specific_file(bin_path: &Path, path: &Path) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.with_context(|| format!("Failed to open file {}.", path.display()))?;
let mut buffer: Vec<u8> = Vec::new();
file.read_to_end(&mut buffer)
.with_context(|| format!("Failed to read data from file {}.", path.display()))?;
let existing_code_pos = match_markers(&buffer).with_context(|| {
format!(
"Error occured while searching juliaup shell startup script section in {}",
path.display()
)
})?;
let new_content = get_shell_script_juliaup_content(bin_path, path).with_context(|| {
format!(
"Error occured while generating juliaup shell startup script section for {}",
path.display()
)
})?;
match existing_code_pos {
Some(pos) => {
buffer.replace_range(pos.0..pos.1, &new_content);
}
None => {
buffer.extend_from_slice(b"\n");
buffer.extend_from_slice(&new_content);
buffer.extend_from_slice(b"\n");
}
};
file.rewind().unwrap();
file.set_len(0).unwrap();
file.write_all(&buffer).unwrap();
file.sync_all().unwrap();
Ok(())
}
fn remove_path_from_specific_file(path: &Path) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.with_context(|| format!("Failed to open file: {}", path.display()))?;
let mut buffer: Vec<u8> = Vec::new();
file.read_to_end(&mut buffer)?;
let existing_code_pos = match_markers(&buffer).with_context(|| {
format!(
"Error occured while searching juliaup shell startup script section in {}",
path.display()
)
})?;
if let Some(pos) = existing_code_pos {
buffer.replace_range(pos.0..pos.1, "");
file.rewind().unwrap();
file.set_len(0).unwrap();
file.write_all(&buffer).unwrap();
file.sync_all().unwrap();
}
Ok(())
}
pub fn find_shell_scripts_to_be_modified(add_case: bool) -> Result<Vec<PathBuf>> {
let home_dir = dirs::home_dir().unwrap();
let paths_to_test: Vec<PathBuf> = vec![
home_dir.join(".bashrc"),
home_dir.join(".profile"),
home_dir.join(".bash_profile"),
home_dir.join(".bash_login"),
home_dir.join(".zshrc"),
];
let result = paths_to_test
.iter()
.filter(
|p| {
p.exists()
|| (add_case
&& p.file_name().unwrap() == ".zshrc"
&& std::env::consts::OS == "macos")
}, )
.cloned()
.collect();
Ok(result)
}
pub fn add_binfolder_to_path_in_shell_scripts(bin_path: &Path) -> Result<()> {
let paths = find_shell_scripts_to_be_modified(true)?;
paths.into_iter().for_each(|p| {
add_path_to_specific_file(bin_path, &p).unwrap();
});
Ok(())
}
pub fn remove_binfolder_from_path_in_shell_scripts() -> Result<()> {
let paths = find_shell_scripts_to_be_modified(false)?;
paths.into_iter().for_each(|p| {
remove_path_from_specific_file(&p).unwrap();
});
Ok(())
}
pub fn update_version_db(channel: &Option<String>, paths: &GlobalPaths) -> Result<()> {
print_juliaup_style(
"Checking",
"for new Julia versions",
JuliaupMessageType::Progress,
);
let file_lock = get_read_lock(paths)?;
let mut temp_versiondb_download_path: Option<TempPath> = None;
let mut delete_old_version_db: bool = false;
let old_config_file = load_config_db(paths, Some(&file_lock)).with_context(|| {
"`run_command_update_version_db` command failed to load configuration db."
})?;
let local_dbversion = match std::fs::OpenOptions::new()
.read(true)
.open(&paths.versiondb)
{
Ok(file) => {
let reader = BufReader::new(&file);
if let Ok(versiondb) =
serde_json::from_reader::<BufReader<&std::fs::File>, JuliaupVersionDB>(reader)
{
semver::Version::parse(&versiondb.version).ok()
} else {
None
}
}
Err(_) => None,
};
{
let (_, res) = file_lock.data_unlock();
res.with_context(|| "Failed to unlock configuration file.")?;
}
#[cfg(feature = "selfupdate")]
let juliaup_channel = match &old_config_file.self_data.juliaup_channel {
Some(juliaup_channel) => juliaup_channel.to_string(),
None => "release".to_string(),
};
#[cfg(not(feature = "selfupdate"))]
let juliaup_channel = "release".to_string();
let juliaupserver_base =
get_juliaserver_base_url().with_context(|| "Failed to get Juliaup server base URL.")?;
let dbversion_url_path = match juliaup_channel.as_str() {
"release" => "juliaup/RELEASECHANNELDBVERSION",
"releasepreview" => "juliaup/RELEASEPREVIEWCHANNELDBVERSION",
"dev" => "juliaup/DEVCHANNELDBVERSION",
_ => bail!(
"Juliaup is configured to a channel named '{}' that does not exist.",
&juliaup_channel
),
};
let dbversion_url = juliaupserver_base
.join(dbversion_url_path)
.with_context(|| {
format!(
"Failed to construct a valid url from '{}' and '{}'.",
juliaupserver_base, dbversion_url_path
)
})?;
let online_dbversion = download_juliaup_version(dbversion_url.as_ref())
.with_context(|| "Failed to download current version db version.")?;
let bundled_dbversion = get_bundled_dbversion()
.with_context(|| "Failed to determine the bundled version db version.")?;
if online_dbversion > bundled_dbversion {
if local_dbversion.is_none() || online_dbversion > local_dbversion.unwrap() {
let onlineversiondburl = juliaupserver_base
.join(&format!(
"juliaup/versiondb/versiondb-{}-{}.json",
online_dbversion,
get_juliaup_target()
))
.with_context(|| "Failed to construct URL for version db download.")?;
let temp_path = tempfile::NamedTempFile::new_in(paths.versiondb.parent().unwrap())
.unwrap()
.into_temp_path();
download_versiondb(onlineversiondburl.as_ref(), &temp_path).with_context(|| {
format!(
"Failed to download new version db from {}.",
onlineversiondburl
)
})?;
temp_versiondb_download_path = Some(temp_path);
}
} else if local_dbversion.is_some() {
delete_old_version_db = true;
}
let direct_download_etags = download_direct_download_etags(channel, &old_config_file.data)?;
let mut new_config_file = load_mut_config_db(paths).with_context(|| {
"`run_command_update_version_db` command failed to load configuration db."
})?;
if new_config_file.data != old_config_file.data {
if let Some(temp_versiondb_download_path) = temp_versiondb_download_path {
let _ = std::fs::remove_file(temp_versiondb_download_path);
}
return Ok(());
}
for (channel, etag) in direct_download_etags {
let channel_data = new_config_file
.data
.installed_channels
.get(&channel)
.unwrap();
if let JuliaupConfigChannel::DirectDownloadChannel {
path,
url,
local_etag,
server_etag: _,
version,
binary_path,
} = channel_data
{
if let Some(etag) = etag {
new_config_file.data.installed_channels.insert(
channel,
JuliaupConfigChannel::DirectDownloadChannel {
path: path.clone(),
url: url.clone(),
local_etag: local_etag.clone(),
server_etag: etag,
version: version.clone(),
binary_path: binary_path.clone(),
},
);
} else {
print_juliaup_style(
"Failed",
&format!(
"to update {}. This can happen if a build is no longer available.",
channel
),
JuliaupMessageType::Error,
);
}
}
}
new_config_file.data.last_version_db_update = Some(chrono::Utc::now());
if let Some(temp_versiondb_download_path) = temp_versiondb_download_path {
retry_rename(&temp_versiondb_download_path, &paths.versiondb)?;
} else if delete_old_version_db {
let _ = std::fs::remove_file(&paths.versiondb);
}
save_config_db(&mut new_config_file).with_context(|| "Failed to save configuration file.")?;
Ok(())
}
fn run_with_slow_message<F, R>(func: F, timeout_secs: u64, message: &str) -> Result<R, Error>
where
F: FnOnce() -> Result<R, Error> + Send + 'static,
R: Send + 'static,
{
use std::sync::mpsc::channel;
use std::thread;
use std::time::Duration;
let (tx, rx) = channel();
thread::spawn(move || {
let result = func();
tx.send(result).unwrap();
});
match rx.recv_timeout(Duration::from_secs(timeout_secs)) {
Ok(result) => result,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
eprintln!("{}", message);
rx.recv().unwrap()
}
Err(e) => panic!("Error receiving result: {:?}", e),
}
}
#[cfg(windows)]
fn download_direct_download_etags(
channel: &Option<String>,
config_data: &JuliaupConfig,
) -> Result<Vec<(String, Option<String>)>> {
use windows::core::HSTRING;
use windows::Foundation::Uri;
use windows::Web::Http::HttpMethod;
use windows::Web::Http::HttpRequestMessage;
let server_supports_etag = check_server_supports_nightlies().unwrap_or(false);
let http_client = http_client()?;
let mut requests = Vec::new();
for (channel_name, installed_channel) in &config_data.installed_channels {
if let Some(chan) = channel {
if chan != channel_name {
continue;
}
}
if let JuliaupConfigChannel::DirectDownloadChannel { url, .. } = installed_channel {
if !server_supports_etag {
requests.push((channel_name.clone(), None));
continue;
}
let http_client = http_client.clone();
let url_clone = url.clone();
let channel_name_clone = channel_name.clone();
let message = format!(
"{} for new version on channel '{}' is taking a while... This can be slow due to server caching",
style(" Checking").cyan().bold(),
channel_name
);
let etag = run_with_slow_message(
move || {
let request_uri = Uri::CreateUri(&HSTRING::from(&url_clone))
.with_context(|| format!("Failed to create URI from {}", &url_clone))?;
let request = HttpRequestMessage::Create(&HttpMethod::Head()?, &request_uri)
.with_context(|| "Failed to create HttpRequestMessage.")?;
let async_op = http_client
.SendRequestAsync(&request)
.map_err(|e| anyhow!("Failed to send request: {:?}", e))?;
let response = async_op
.join()
.map_err(|e| anyhow!("Failed to get response: {:?}", e))?;
if response.IsSuccessStatusCode()? {
let etag = response
.Headers()
.ok()
.and_then(|headers| headers.Lookup(&HSTRING::from("ETag")).ok())
.map(|s| s.to_string());
Ok::<Option<String>, anyhow::Error>(etag)
} else {
Ok::<Option<String>, anyhow::Error>(None)
}
},
3, &message,
)?;
requests.push((channel_name_clone, etag));
}
}
Ok(requests)
}
#[cfg(not(windows))]
fn download_direct_download_etags(
channel: &Option<String>,
config_data: &JuliaupConfig,
) -> Result<Vec<(String, Option<String>)>> {
use std::sync::Arc;
let server_supports_etag = check_server_supports_nightlies().unwrap_or(false);
let client = Arc::new(http_client()?);
let mut requests = Vec::new();
for (channel_name, installed_channel) in &config_data.installed_channels {
if let Some(chan) = channel {
if chan != channel_name {
continue;
}
}
if let JuliaupConfigChannel::DirectDownloadChannel { url, .. } = installed_channel {
if !server_supports_etag {
requests.push((channel_name.clone(), None));
continue;
}
let client = Arc::clone(&client);
let url_clone = url.clone();
let channel_name_clone = channel_name.clone();
let message = format!(
"{} for new version on channel '{}' is taking a while... This can be slow due to server caching",
style(" Checking").cyan().bold(),
channel_name
);
let etag = run_with_slow_message(
move || {
let response = client.head(&url_clone).send().with_context(|| {
format!("Failed to send HEAD request to {}", &url_clone)
})?;
if response.status().is_success() {
let etag = response
.headers()
.get("etag")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
Ok::<Option<String>, anyhow::Error>(etag)
} else {
Ok::<Option<String>, anyhow::Error>(None)
}
},
3, &message,
)?;
requests.push((channel_name_clone, etag));
}
}
Ok(requests)
}
#[cfg(target_os = "macos")]
fn prompt_and_codesign_pr_build(dir: &Path) -> Result<bool> {
use std::io::{self, Write};
eprintln!("\nWARNING: PR builds are not code-signed for macOS.");
eprintln!(" The Julia binary will fail to run unless you codesign it locally.");
eprint!("\nWould you like to automatically codesign this PR build now? [Y/n]: ");
io::stderr().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim().to_lowercase();
if input == "n" || input == "no" {
eprintln!("\nSkipping codesigning. You can manually codesign later with:");
eprintln!(
" find {} -type f -perm +111 -exec codesign --force --sign - {{}} \\;",
dir.display()
);
return Ok(false);
}
eprintln!("\nCodesigning all Mach-O binaries...");
let mut signed_count = 0u32;
let mut dirs_to_visit = vec![dir.to_path_buf()];
while let Some(current) = dirs_to_visit.pop() {
let entries = match std::fs::read_dir(¤t) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
if !path.to_string_lossy().ends_with(".dSYM")
&& !path.ends_with("share/julia/compiled")
{
dirs_to_visit.push(path);
}
continue;
}
if !path.is_file() {
continue;
}
let is_executable = entry
.metadata()
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false);
let is_dylib = path.extension().map(|e| e == "dylib").unwrap_or(false);
if is_executable || is_dylib {
codesign_file(&path)?;
signed_count += 1;
}
}
}
eprintln!("\u{2713} Codesigning completed successfully ({signed_count} files).");
Ok(true)
}
#[cfg(target_os = "macos")]
fn codesign_file(path: &std::path::Path) -> Result<()> {
use std::process::Command;
let status = Command::new("codesign")
.args(["--force", "--sign", "-"])
.arg(path)
.status()
.with_context(|| format!("Failed to execute codesign on {}", path.display()))?;
if !status.success() {
return Err(anyhow!("Failed to codesign {}", path.display()));
}
Ok(())
}
#[cfg(target_os = "macos")]
fn check_stdlib_notarization(julia_path: &std::path::Path) {
eprint!("\nChecking standard library notarization");
let _ = std::io::stdout().flush();
match std::process::Command::new(julia_path)
.env("JULIA_LOAD_PATH", "@stdlib")
.arg("--startup-file=no")
.arg("-e")
.arg("foreach(p -> begin print(stderr, '.'); @eval(import $(Symbol(p))) end, filter!(x -> isfile(joinpath(Sys.STDLIB, x, \"src\", \"$(x).jl\")), readdir(Sys.STDLIB)))")
.status()
{
Ok(exit_status) => {
if exit_status.success() {
eprintln!("done.")
} else {
eprintln!("failed with {}.", exit_status);
}
}
Err(e) => {
eprintln!("failed to execute Julia binary.");
eprintln!("Error: {}", e);
if e.raw_os_error() == Some(86) {
eprintln!("This may indicate an architecture mismatch (e.g., trying to run an Intel binary on Apple Silicon or vice versa).");
}
eprintln!("Check was skipped.");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "macos")]
use std::sync::{Mutex, OnceLock};
#[cfg(target_os = "macos")]
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
}
#[cfg(target_os = "macos")]
struct EnvVarGuard {
key: &'static str,
original: Option<String>,
}
#[cfg(target_os = "macos")]
impl EnvVarGuard {
fn set(key: &'static str, value: &std::path::Path) -> Self {
let original = std::env::var(key).ok();
std::env::set_var(key, value);
Self { key, original }
}
}
#[cfg(target_os = "macos")]
impl Drop for EnvVarGuard {
fn drop(&mut self) {
if let Some(ref value) = self.original {
std::env::set_var(self.key, value);
} else {
std::env::remove_var(self.key);
}
}
}
#[test]
fn match_markers_none_without_markers() {
let inp: &[u8] = b"Some input\n";
let res = match_markers(inp);
assert!(res.is_ok());
let res = res.unwrap();
assert!(res.is_none());
}
#[test]
fn match_markers_returns_correct_indices() {
let mut inp: Vec<u8> = Vec::new();
let start_bytes = b"Some random bytes.";
let middle_bytes = b"More bytes.";
let end_bytes = b"Final bytes.";
inp.extend_from_slice(start_bytes);
inp.extend_from_slice(S_MARKER);
inp.extend_from_slice(middle_bytes);
inp.extend_from_slice(E_MARKER);
inp.extend_from_slice(end_bytes);
let res = match_markers(&inp);
assert!(res.is_ok());
let res = res.unwrap();
assert!(res.is_some());
let (sidx, eidx) = res.unwrap();
assert_eq!(sidx, start_bytes.len());
let expected_eidx =
start_bytes.len() + S_MARKER.len() + middle_bytes.len() + E_MARKER.len();
assert_eq!(eidx, expected_eidx);
}
#[test]
fn match_markers_returns_err_without_start() {
let mut inp: Vec<u8> = Vec::new();
let start_bytes = b"Some random bytes.";
let middle_bytes = b"More bytes.";
let end_bytes = b"Final bytes.";
inp.extend_from_slice(start_bytes);
inp.extend_from_slice(middle_bytes);
inp.extend_from_slice(E_MARKER);
inp.extend_from_slice(end_bytes);
let res = match_markers(&inp);
assert!(res.is_err());
}
#[test]
fn match_markers_returns_err_without_end() {
let mut inp: Vec<u8> = Vec::new();
let start_bytes = b"Some random bytes.";
let middle_bytes = b"More bytes.";
let end_bytes = b"Final bytes.";
inp.extend_from_slice(start_bytes);
inp.extend_from_slice(S_MARKER);
inp.extend_from_slice(middle_bytes);
inp.extend_from_slice(end_bytes);
let res = match_markers(&inp);
assert!(res.is_err());
}
#[test]
fn match_markers_returns_err_with_multiple_start() {
let mut inp: Vec<u8> = Vec::new();
let start_bytes = b"Some random bytes.";
let middle_bytes = b"More bytes.";
let end_bytes = b"Final bytes.";
inp.extend_from_slice(S_MARKER);
inp.extend_from_slice(start_bytes);
inp.extend_from_slice(S_MARKER);
inp.extend_from_slice(middle_bytes);
inp.extend_from_slice(E_MARKER);
inp.extend_from_slice(end_bytes);
let res = match_markers(&inp);
assert!(res.is_err());
}
#[test]
fn match_markers_returns_err_with_multiple_end() {
let mut inp: Vec<u8> = Vec::new();
let start_bytes = b"Some random bytes.";
let middle_bytes = b"More bytes.";
let end_bytes = b"Final bytes.";
inp.extend_from_slice(start_bytes);
inp.extend_from_slice(S_MARKER);
inp.extend_from_slice(middle_bytes);
inp.extend_from_slice(E_MARKER);
inp.extend_from_slice(end_bytes);
inp.extend_from_slice(E_MARKER);
let res = match_markers(&inp);
assert!(res.is_err());
}
#[cfg(target_os = "macos")]
#[test]
fn symlink_uses_app_bundle_for_system_channel() -> Result<()> {
let _guard = env_lock();
let depot_dir = tempfile::TempDir::new()?;
let _env_guard = EnvVarGuard::set("JULIAUP_DEPOT_PATH", depot_dir.path());
let paths = crate::global_paths::get_paths()?;
let version = "1.2.3";
let target_path = paths.juliauphome.join(format!("julia-{}", version));
let julia_bin = target_path.join("Julia-1.2.app/Contents/Resources/julia/bin/julia");
std::fs::create_dir_all(julia_bin.parent().unwrap())?;
std::fs::write(&julia_bin, b"")?;
let symlink_dir = tempfile::TempDir::new()?;
let symlink_path = symlink_dir.path().join("julia-1.2");
create_system_channel_symlink(version, "julia-1.2", &symlink_path, &paths, &None)?;
let link_target = std::fs::read_link(&symlink_path)?;
assert_eq!(link_target, julia_bin);
Ok(())
}
#[cfg(target_os = "macos")]
#[test]
fn symlink_uses_app_bundle_for_direct_download_channel() -> Result<()> {
let _guard = env_lock();
let depot_dir = tempfile::TempDir::new()?;
let _env_guard = EnvVarGuard::set("JULIAUP_DEPOT_PATH", depot_dir.path());
let paths = crate::global_paths::get_paths()?;
let version = "1.2.3";
let path = "julia-pr123";
let target_path = paths.juliauphome.join(path);
let julia_bin = target_path.join("Julia-1.2.app/Contents/Resources/julia/bin/julia");
std::fs::create_dir_all(julia_bin.parent().unwrap())?;
std::fs::write(&julia_bin, b"")?;
let symlink_dir = tempfile::TempDir::new()?;
let symlink_path = symlink_dir.path().join("julia-pr123");
create_direct_download_symlink(path, version, "julia-pr123", &symlink_path, &paths, &None)?;
let link_target = std::fs::read_link(&symlink_path)?;
assert_eq!(link_target, julia_bin);
Ok(())
}
#[cfg(target_os = "macos")]
#[test]
fn dmg_failure_falls_back_to_tarball() -> Result<()> {
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
use tar::Builder;
let _guard = env_lock();
let _env_guard = EnvVarGuard::set("JULIAUP_TEST_DMG_FAIL", std::path::Path::new("1"));
let tarball_bytes = {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
{
let mut builder = Builder::new(&mut encoder);
let mut dir_header = tar::Header::new_gnu();
dir_header.set_entry_type(tar::EntryType::Directory);
dir_header.set_mode(0o755);
dir_header.set_size(0);
dir_header.set_cksum();
builder.append_data(&mut dir_header, "julia-1.2.3/bin", std::io::empty())?;
let data = b"#!/bin/sh\n";
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o755);
header.set_cksum();
builder.append_data(&mut header, "julia-1.2.3/bin/julia", &data[..])?;
builder.finish()?;
}
encoder.finish()?
};
let dmg_bytes = b"not-a-real-dmg".to_vec();
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
let handle = thread::spawn(move || {
for _ in 0..2 {
if let Ok((mut stream, _)) = listener.accept() {
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).unwrap_or(0);
let request = String::from_utf8_lossy(&buf[..n]);
let path = request
.lines()
.next()
.and_then(|line| line.split_whitespace().nth(1))
.unwrap_or("/");
let (body, etag) = if path.ends_with(".dmg") {
(dmg_bytes.as_slice(), "\"dmg-etag\"")
} else {
(tarball_bytes.as_slice(), "\"tar-etag\"")
};
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nETag: {}\r\nConnection: close\r\n\r\n",
body.len(),
etag
);
let _ = stream.write_all(response.as_bytes());
let _ = stream.write_all(body);
}
}
});
let url = url::Url::parse(&format!("http://{}/julia.tar.gz", addr))?;
let target_dir = tempfile::TempDir::new()?;
let (etag, used_dmg) = try_download_dmg_with_fallback(&url, target_dir.path())?;
assert!(!used_dmg);
assert_eq!(etag, "\"tar-etag\"");
assert!(target_dir.path().join("bin/julia").exists());
let _ = handle.join();
Ok(())
}
}