Skip to content
Closed
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Here are some of the things you can do with `juliaup`:
- `juliaup add 1.6.1~x86` installs the 32 bit version of Julia 1.6.1 on your system.
- `juliaup default 1.6~x86` configures the `julia` command to start the latest 1.6.x 32 bit version of Julia you have installed on your system.
- `juliaup link dev ~/juliasrc/julia` configures the `dev` channel to use a binary that you provide that is located at `~/juliasrc/julia`. You can then use `dev` as if it was a system provided channel, i.e. make it the default or use it with the `+` version selector. You can use other names than `dev` and link as many versions into `juliaup` as you want.
- `juliaup alias r release` configures the `r` channel to act as if you had requested the `release` channel.
- `juliaup self update` installs the latest version, which is necessary if new releases reach the beta channel, etc.
- `juliaup self uninstall` uninstalls Juliaup. Note that on some platforms this command is not available, in those situations one should use platform specific methods to uninstall Juliaup.
- `juliaup override status` shows all configured directory overrides.
Expand Down
38 changes: 38 additions & 0 deletions src/bin/julialauncher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,32 @@ fn get_julia_path_from_channel(
juliaupconfig_path: &Path,
juliaup_channel_source: JuliaupChannelSource,
) -> Result<(PathBuf, Vec<String>)> {
get_julia_path_from_channel_impl(
versions_db,
config_data,
channel,
juliaupconfig_path,
juliaup_channel_source,
&mut std::collections::HashSet::new(),
)
}

fn get_julia_path_from_channel_impl(
versions_db: &JuliaupVersionDB,
config_data: &JuliaupConfig,
channel: &str,
juliaupconfig_path: &Path,
juliaup_channel_source: JuliaupChannelSource,
visited: &mut std::collections::HashSet<String>,
) -> Result<(PathBuf, Vec<String>)> {
// Check for circular references
if visited.contains(channel) {
return Err(anyhow!(
"Circular alias detected: alias chain contains a cycle involving '{}'",
channel
));
}
visited.insert(channel.to_string());
let channel_valid = is_valid_channel(versions_db, &channel.to_string())?;
let channel_info = config_data
.installed_channels
Expand Down Expand Up @@ -217,6 +243,18 @@ fn get_julia_path_from_channel(
args.as_ref().map_or_else(Vec::new, |v| v.clone()),
))
}
JuliaupConfigChannel::AliasedChannel {
channel: newchannel,
} => {
return get_julia_path_from_channel_impl(
versions_db,
config_data,
newchannel,
juliaupconfig_path,
juliaup_channel_source,
visited,
)
}
JuliaupConfigChannel::SystemChannel { version } => {
let path = &config_data
.installed_versions.get(version)
Expand Down
6 changes: 5 additions & 1 deletion src/bin/juliaup.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::{Context, Result};
use clap::Parser;
use juliaup::cli::{ConfigSubCmd, Juliaup, OverrideSubCmd, SelfSubCmd};
use juliaup::command_alias::run_command_alias;
use juliaup::command_api::run_command_api;
use juliaup::command_completions::generate_completion_for_command;
#[cfg(not(windows))]
Expand Down Expand Up @@ -102,6 +103,7 @@ fn main() -> Result<()> {
file,
args,
} => run_command_link(&channel, &file, &args, &paths),
Juliaup::Alias { alias, channel } => run_command_alias(&alias, &channel, &paths),
Juliaup::List {} => run_command_list(&paths),
Juliaup::Config(subcmd) => match subcmd {
#[cfg(not(windows))]
Expand Down Expand Up @@ -148,6 +150,8 @@ fn main() -> Result<()> {
#[cfg(not(feature = "selfupdate"))]
SelfSubCmd::Uninstall {} => run_command_selfuninstall_unavailable(),
},
Juliaup::Completions { shell } => generate_completion_for_command::<Juliaup>(shell, "juliaup"),
Juliaup::Completions { shell } => {
generate_completion_for_command::<Juliaup>(shell, "juliaup")
}
}
}
8 changes: 5 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use clap::{Parser, ValueEnum};
#[derive(Clone, ValueEnum)]
pub enum CompletionShell {
Bash,
Elvish,
Elvish,
Fish,
Nushell,
PowerShell,
Expand All @@ -29,6 +29,8 @@ pub enum Juliaup {
file: String,
args: Vec<String>,
},
/// Link an existing juliaup channel to a custom channel name
Alias { alias: String, channel: String },
/// List all available channels
#[clap(alias = "ls")]
List {},
Expand Down Expand Up @@ -62,9 +64,9 @@ pub enum Juliaup {
#[clap(subcommand, name = "self")]
SelfSubCmd(SelfSubCmd),
/// Generate tab-completion scripts for your shell
Completions {
Completions {
#[arg(value_enum, value_name = "SHELL")]
shell: CompletionShell
shell: CompletionShell,
},
// This is used for the cron jobs that we create. By using this UUID for the command
// We can identify the cron jobs that were created by juliaup for uninstall purposes
Expand Down
53 changes: 53 additions & 0 deletions src/command_alias.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::config_file::JuliaupConfigChannel;
use crate::config_file::{load_mut_config_db, save_config_db};
use crate::global_paths::GlobalPaths;
#[cfg(not(windows))]
use crate::operations::create_symlink;
use crate::operations::is_valid_channel;
use crate::versions_file::load_versions_db;
use anyhow::{bail, Context, Result};

pub fn run_command_alias(alias: &str, channel: &str, paths: &GlobalPaths) -> Result<()> {
let mut config_file = load_mut_config_db(paths)
.with_context(|| "`alias` command failed to load configuration data.")?;

let versiondb_data =
load_versions_db(paths).with_context(|| "`alias` command failed to load versions db.")?;

if config_file.data.installed_channels.contains_key(alias) {
bail!("Channel name `{}` is already used.", alias)
}

if !config_file.data.installed_channels.contains_key(channel) {
eprintln!("WARNING: The channel `{}` does not currently exist. If this was a mistake, run `juliaup remove {}` and try again.", channel, alias);
}

if is_valid_channel(&versiondb_data, &alias.to_string())? {
eprintln!("WARNING: The channel name `{}` is also a system channel. By creating an alias to this channel you are hiding this system channel.", alias);
}

config_file.data.installed_channels.insert(
alias.to_string(),
JuliaupConfigChannel::AliasedChannel {
channel: channel.to_string(),
},
);

save_config_db(&mut config_file)
.with_context(|| "`alias` command failed to save configuration db.")?;

#[cfg(not(windows))]
{
if config_file.data.settings.create_channel_symlinks {
create_symlink(
&JuliaupConfigChannel::AliasedChannel {
channel: channel.to_string(),
},
&format!("julia-{}", alias),
paths,
)?;
}
}

Ok(())
}
196 changes: 114 additions & 82 deletions src/command_api.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::config_file::load_config_db;
use crate::config_file::JuliaupConfigChannel;
use crate::config_file::JuliaupReadonlyConfigFile;
use crate::global_paths::GlobalPaths;
use crate::utils::parse_versionstring;
use anyhow::{bail, Context, Result};
Expand Down Expand Up @@ -29,6 +30,114 @@ pub struct JuliaupApiGetinfoReturn {
pub other_versions: Vec<JuliaupChannelInfo>,
}

fn get_channel_info(
name: &String,
channel: &JuliaupConfigChannel,
config_file: &JuliaupReadonlyConfigFile,
paths: &GlobalPaths,
) -> Result<Option<JuliaupChannelInfo>> {
match channel {
JuliaupConfigChannel::SystemChannel {
version: fullversion,
} => {
let (platform, mut version) = parse_versionstring(&fullversion)
.with_context(|| "Encountered invalid version string in the configuration file while running the getconfig1 API command.")?;

version.build = semver::BuildMetadata::EMPTY;

match config_file.data.installed_versions.get(&fullversion.clone()) {
Some(channel) => return Ok(Some(JuliaupChannelInfo {
name: name.clone(),
file: paths.juliauphome
.join(&channel.path)
.join("bin")
.join(format!("julia{}", std::env::consts::EXE_SUFFIX))
.normalize()
.with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")?
.into_path_buf()
.to_string_lossy()
.to_string(),
args: Vec::new(),
version: version.to_string(),
arch: platform
})),
None => bail!("The channel '{}' is configured as a system channel, but no such channel exists in the versions database.", name)
}
}
JuliaupConfigChannel::LinkedChannel { command, args } => {
let mut new_args: Vec<String> = Vec::new();

for i in args.as_ref().unwrap() {
new_args.push(i.to_string());
}

new_args.push("--version".to_string());

let res = std::process::Command::new(&command)
.args(&new_args)
.output();

match res {
Ok(output) => {
let expected_version_prefix = "julia version ";

let trimmed_string = std::str::from_utf8(&output.stdout).unwrap().trim();

if !trimmed_string.starts_with(expected_version_prefix) {
return Ok(None);
}

let version =
Version::parse(&trimmed_string[expected_version_prefix.len()..])?;

Ok(Some(JuliaupChannelInfo {
name: name.clone(),
file: command.clone(),
args: args.clone().unwrap_or_default(),
version: version.to_string(),
arch: "".to_string(),
}))
}
Err(_) => return Ok(None),
}
}
// TODO: fix
JuliaupConfigChannel::AliasedChannel { channel } => {
let real_channel_info = get_channel_info(name, config_file.data.installed_channels.get(channel).unwrap(), config_file, paths)?;

match real_channel_info {
Some(info) => {
return Ok(Some(JuliaupChannelInfo {
name: name.clone(),
file: info.file,
args: info.args,
version: info.version,
arch: info.arch,
}))
}
None => return Ok(None),
}
}
JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => {
return Ok(Some(JuliaupChannelInfo {
name: name.clone(),
file: paths.juliauphome
.join(path)
.join("bin")
.join(format!("julia{}", std::env::consts::EXE_SUFFIX))
.normalize()
.with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")?
.into_path_buf()
.to_string_lossy()
.to_string(),
args: Vec::new(),
version: version.clone(),
arch: "".to_string(),
}))
}
}
}

pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> {
if command != "getconfig1" {
bail!("Wrong API command.");
Expand All @@ -43,89 +152,12 @@ pub fn run_command_api(command: &str, paths: &GlobalPaths) -> Result<()> {
"Failed to load configuration file while running the getconfig1 API command."
})?;

for (key, value) in config_file.data.installed_channels {
let curr = match value {
JuliaupConfigChannel::SystemChannel {
version: fullversion,
} => {
let (platform, mut version) = parse_versionstring(&fullversion)
.with_context(|| "Encountered invalid version string in the configuration file while running the getconfig1 API command.")?;

version.build = semver::BuildMetadata::EMPTY;

match config_file.data.installed_versions.get(&fullversion) {
Some(channel) => JuliaupChannelInfo {
name: key.clone(),
file: paths.juliauphome
.join(&channel.path)
.join("bin")
.join(format!("julia{}", std::env::consts::EXE_SUFFIX))
.normalize()
.with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")?
.into_path_buf()
.to_string_lossy()
.to_string(),
args: Vec::new(),
version: version.to_string(),
arch: platform
},
None => bail!("The channel '{}' is configured as a system channel, but no such channel exists in the versions database.", key)
}
}
JuliaupConfigChannel::LinkedChannel { command, args } => {
let mut new_args: Vec<String> = Vec::new();

for i in args.as_ref().unwrap() {
new_args.push(i.to_string());
}
let other_conf = config_file.clone();

new_args.push("--version".to_string());

let res = std::process::Command::new(&command)
.args(&new_args)
.output();

match res {
Ok(output) => {
let expected_version_prefix = "julia version ";

let trimmed_string = std::str::from_utf8(&output.stdout).unwrap().trim();

if !trimmed_string.starts_with(expected_version_prefix) {
continue;
}

let version =
Version::parse(&trimmed_string[expected_version_prefix.len()..])?;

JuliaupChannelInfo {
name: key.clone(),
file: command.clone(),
args: args.unwrap_or_default(),
version: version.to_string(),
arch: "".to_string(),
}
}
Err(_) => continue,
}
}
JuliaupConfigChannel::DirectDownloadChannel { path, url: _, local_etag: _, server_etag: _, version } => {
JuliaupChannelInfo {
name: key.clone(),
file: paths.juliauphome
.join(path)
.join("bin")
.join(format!("julia{}", std::env::consts::EXE_SUFFIX))
.normalize()
.with_context(|| "Normalizing the path for an entry from the config file failed while running the getconfig1 API command.")?
.into_path_buf()
.to_string_lossy()
.to_string(),
args: Vec::new(),
version: version.clone(),
arch: "".to_string(),
}
}
for (key, value) in config_file.data.installed_channels {
let curr = match get_channel_info(&key, &value, &other_conf, paths)? {
Some(channel_info) => channel_info,
None => continue,
};

match config_file.data.default {
Expand Down
Loading