use std::{fs, io, path::PathBuf};
use anyhow::{anyhow, bail, Context, Result};
use clap::{Parser, Subcommand};
use crate::{
current::CurrentManager,
manifest::Manifest,
release::ReleaseService,
remote::RemoteClient,
spec::{Channel, InstallSpec, ReleaseId, VersionSpec},
RialoDirs,
};
#[derive(Parser, Debug)]
#[command(author, version, about = "Rialo release and toolchain manager", long_about = None)]
pub struct Cli {
#[arg(long = "home", env = "RIALO_HOME", value_name = "PATH")]
pub home_override: Option<PathBuf>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Install {
#[arg(value_name = "SPEC")]
spec: InstallSpec,
#[arg(long, help = "Install and make this release the current default")]
default: bool,
#[arg(long, help = "Skip installing the compatible Rust toolchain")]
no_toolchain: bool,
},
Use {
#[arg(value_name = "SPEC")]
spec: InstallSpec,
},
List,
Uninstall {
#[arg(value_name = "SPEC")]
spec: InstallSpec,
#[arg(long)]
force: bool,
},
Current,
Which {
#[arg(value_name = "BINARY")]
binary: String,
},
ListRemote,
#[command(subcommand)]
Toolchain(ToolchainCommands),
}
#[derive(Subcommand, Debug)]
pub enum ToolchainCommands {
Install {
name: String,
#[arg(long)]
version: Option<String>,
#[arg(long)]
from_source: bool,
},
List,
Validate {
name: String,
#[arg(long)]
version: Option<String>,
},
Uninstall {
name: String,
#[arg(long)]
version: Option<String>,
},
Build {
#[arg(long)]
version: Option<String>,
},
Upload {
name: String,
#[arg(long)]
version: Option<String>,
},
BuildAndUpload {
#[arg(long)]
version: Option<String>,
},
}
pub fn run(cli: Cli) -> Result<()> {
let dirs = RialoDirs::new(cli.home_override.as_ref())?;
dirs.ensure_layout()?;
let remote = RemoteClient::new()?;
let service = ReleaseService::new(dirs.clone(), remote);
match cli.command {
Commands::Install {
spec,
default,
no_toolchain,
} => {
let id = service.install(&spec, default)?;
println!("Installed {id}");
if !no_toolchain {
if let Some(tc_version) = service.load_manifest(&id)?.rust_toolchain_version() {
use rialo_build_lib::{RialoRustToolchain, Toolchain};
eprintln!("Installing Rust toolchain {}...", tc_version);
let toolchain = RialoRustToolchain::with_version(tc_version)?;
if let Err(e) = toolchain.install() {
eprintln!("⚠️ Failed to install Rust toolchain: {}", e);
eprintln!(" You can install it manually with:");
eprintln!(
" rialoman toolchain install rialo-rust --version {}",
tc_version
);
}
}
}
}
Commands::Use { spec } => {
let resolved = match spec.version {
VersionSpec::Explicit(v) => v.clone(),
VersionSpec::Latest => {
bail!("`use` requires an explicit version. Try running `rialoman install {}` first", spec.channel)
}
};
let id = ReleaseId::new(spec.channel, resolved);
service.use_existing(&id)?;
println!("Now using {id}");
}
Commands::List => list_installed(&dirs)?,
Commands::Uninstall { spec, force } => {
let resolved = match spec.version {
crate::spec::VersionSpec::Explicit(ref v) => v.clone(),
crate::spec::VersionSpec::Latest => {
bail!("`uninstall` requires an explicit version; install it first")
}
};
let id = ReleaseId::new(spec.channel, resolved);
service.uninstall(&id, force)?;
println!("Removed {id}");
}
Commands::Current => show_current(&dirs)?,
Commands::Which { binary } => which_binary(&dirs, &binary)?,
Commands::ListRemote => list_remote()?,
Commands::Toolchain(toolchain_cmd) => handle_toolchain_command(toolchain_cmd)?,
}
Ok(())
}
fn list_installed(dirs: &RialoDirs) -> Result<()> {
let current = CurrentManager::new(dirs.current_file().clone()).load()?;
let releases = match std::fs::read_dir(dirs.releases()) {
Ok(dir) => dir,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e.into()),
};
for entry in releases {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let channel = entry.file_name();
let channel_name = channel.to_string_lossy();
let is_known = Channel::KNOWN
.iter()
.any(|known| known.as_str() == channel_name.as_ref());
if !is_known {
eprintln!("warn: found unrecognized channel directory '{channel_name}'");
continue;
}
let version_entries = std::fs::read_dir(entry.path())?;
for version_entry in version_entries {
let version_entry = version_entry?;
if !version_entry.file_type()?.is_dir() {
continue;
}
let version = version_entry.file_name();
let version = version.to_string_lossy();
let is_current = current
.as_ref()
.is_some_and(|cur| cur.channel == channel_name && cur.version == version);
let marker = if is_current { " (current)" } else { "" };
println!("{channel_name}@{version}{marker}");
if is_current {
if let Err(e) = print_toolchain_status(dirs, &channel_name, &version) {
eprintln!(" ⚠️ Could not read toolchain status: {}", e);
}
}
}
}
Ok(())
}
fn show_current(dirs: &RialoDirs) -> Result<()> {
let current = CurrentManager::new(dirs.current_file().clone()).load()?;
match current {
Some(c) => println!("{}@{}", c.channel, c.version),
None => bail!("No active release"),
}
Ok(())
}
fn which_binary(dirs: &RialoDirs, binary: &str) -> Result<()> {
let current = CurrentManager::new(dirs.current_file().clone()).load()?;
let Some(cur) = current else {
bail!("No active release");
};
let path = dirs
.releases()
.join(&cur.channel)
.join(&cur.version)
.join("bin")
.join(binary);
if path.exists() {
println!("{}", path.display());
Ok(())
} else {
bail!("binary `{binary}` not found in current release")
}
}
fn list_remote() -> Result<()> {
let remote = RemoteClient::new()?;
for channel in crate::spec::Channel::KNOWN {
let spec = InstallSpec {
channel: *channel,
version: VersionSpec::Latest,
};
if let Ok(manifest) = remote.fetch_manifest(&spec) {
println!("{}@{}", manifest.channel, manifest.version);
}
}
Ok(())
}
fn handle_toolchain_command(command: ToolchainCommands) -> Result<()> {
match command {
ToolchainCommands::Install {
name,
version,
from_source,
} => {
install_rust_toolchain(&name, version.as_deref(), from_source)?;
}
ToolchainCommands::List => {
list_rust_toolchains()?;
}
ToolchainCommands::Validate { name, version } => {
validate_rust_toolchain(&name, version.as_deref())?;
}
ToolchainCommands::Uninstall { name, version } => {
uninstall_rust_toolchain(&name, version.as_deref())?;
}
ToolchainCommands::Build { version } => {
build_toolchain_from_source(version.as_deref())?;
}
ToolchainCommands::Upload { name, version } => {
upload_toolchain(&name, version.as_deref())?;
}
ToolchainCommands::BuildAndUpload { version } => {
build_and_upload_toolchain(version.as_deref())?;
}
}
Ok(())
}
fn install_rust_toolchain(name: &str, version: Option<&str>, from_source: bool) -> Result<()> {
use rialo_build_lib::{RialoRustToolchain, SourceBuildable, Toolchain};
if !matches!(name, "rialo-rust" | "rust") {
bail!("Unknown toolchain: {name}. Use 'rialo-rust' or 'rust' as the toolchain name",);
}
let manifest_version = get_current_toolchain_version().ok();
if let (Some(explicit), Some(expected)) = (version, manifest_version.as_ref()) {
if explicit != expected {
eprintln!("⚠️ Warning: Installing rialo-rust {explicit} but current release specifies {expected}");
eprintln!(" This will change what `cargo +rialo` points to.");
eprintln!(" To sync back to the release version, run:");
eprintln!(" rialoman toolchain install rialo-rust");
}
}
let toolchain = match version.or(manifest_version.as_deref()) {
Some(v) => RialoRustToolchain::with_version(v),
None => RialoRustToolchain::new(),
}?;
if !from_source {
toolchain.install()?;
} else {
println!("Building Rialo Rust toolchain from source...");
println!("This will take 30-60 minutes depending on your system.");
println!();
let config = toolchain.get_source_config()?;
toolchain.build_from_source(&config)?;
}
toolchain.validate()?;
Ok(())
}
fn list_rust_toolchains() -> Result<()> {
use rialo_build_lib::toolchain;
let toolchains = toolchain::list_installed_toolchains()?;
if toolchains.is_empty() {
println!("No Rust toolchains installed.");
println!(
"Install a toolchain with: rialoman toolchain install rialo-rust --version <VERSION>"
);
return Ok(());
}
println!("Installed Rust toolchains:\n");
let rialo_rust_versions: Vec<_> = toolchains
.into_iter()
.filter_map(|(name, version)| (name == "rialo-rust").then_some(version))
.collect();
if !rialo_rust_versions.is_empty() {
println!("rialo-rust:");
for version in rialo_rust_versions {
println!(" {version}");
}
}
Ok(())
}
fn validate_rust_toolchain(name: &str, version: Option<&str>) -> Result<()> {
use rialo_build_lib::{RialoRustToolchain, Toolchain};
match name {
"rialo-rust" | "rust" => {
let toolchain = if let Some(v) = version {
RialoRustToolchain::with_version(v)?
} else {
RialoRustToolchain::new()?
};
toolchain.validate()?;
}
_ => {
bail!(
"Unknown toolchain: {}. Use 'rialo-rust' or 'rust' as the toolchain name",
name
);
}
}
Ok(())
}
fn uninstall_rust_toolchain(name: &str, version: Option<&str>) -> Result<()> {
use rialo_build_lib::RialoRustToolchain;
match name {
"rialo-rust" | "rust" => {
let toolchain = if let Some(v) = version {
RialoRustToolchain::with_version(v)?
} else {
RialoRustToolchain::new()?
};
toolchain.uninstall()?;
}
_ => {
bail!(
"Unknown toolchain: {}. Use 'rialo-rust' or 'rust' as the toolchain name",
name
);
}
}
Ok(())
}
fn build_toolchain_from_source(version: Option<&str>) -> Result<()> {
use rialo_build_lib::{RialoRustToolchain, SourceBuildable, Toolchain};
println!("Building Rialo Rust toolchain from source...");
let toolchain = if let Some(v) = version {
RialoRustToolchain::with_version(v)?
} else {
RialoRustToolchain::new()?
};
println!("This will take 30-60 minutes depending on your system.");
println!();
let config = toolchain.get_source_config()?;
toolchain.build_from_source(&config)?;
toolchain.validate()?;
Ok(())
}
fn upload_toolchain(name: &str, version: Option<&str>) -> Result<()> {
use rialo_build_lib::{RialoRustToolchain, Toolchain};
validate_toolchain_name(name)?;
if !check_aws_credentials() {
bail!(
"AWS credentials not found. Please set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY \
environment variables, or configure AWS credentials via AWS CLI or config files."
);
}
println!("Uploading toolchain to S3...");
println!(
"Bucket: {}",
std::env::var("RIALO_TOOLCHAIN_S3_BUCKET")
.unwrap_or_else(|_| "rialo-artifacts".to_string())
);
println!();
match name {
"rialo-rust" | "rust" => {
let toolchain = if let Some(v) = version {
RialoRustToolchain::with_version(v)?
} else {
RialoRustToolchain::new()?
};
if !toolchain.is_installed()? {
bail!(
"Rialo Rust toolchain is not installed. Install it first with: \
rialoman toolchain install rialo-rust"
);
}
toolchain.upload_to_s3()?;
}
_ => unreachable!("Toolchain name already validated"),
}
println!();
println!("✅ Upload complete");
Ok(())
}
fn build_and_upload_toolchain(version: Option<&str>) -> Result<()> {
use rialo_build_lib::{RialoRustToolchain, SourceBuildable, Toolchain};
if !check_aws_credentials() {
bail!(
"AWS credentials required for upload. Please set AWS_ACCESS_KEY_ID and \
AWS_SECRET_ACCESS_KEY environment variables, or configure AWS credentials \
via AWS CLI or config files."
);
}
println!("Building Rialo Rust toolchain from source and uploading to S3...");
println!(
"Bucket: {}",
std::env::var("RIALO_TOOLCHAIN_S3_BUCKET")
.unwrap_or_else(|_| "rialo-artifacts".to_string())
);
println!();
println!("This will take 30-60 minutes depending on your system.");
println!();
let toolchain = if let Some(v) = version {
RialoRustToolchain::with_version(v)?
} else {
RialoRustToolchain::new()?
};
let config = toolchain.get_source_config()?;
toolchain.build_from_source(&config)?;
toolchain.validate()?;
println!();
println!("Build completed successfully. Now uploading to S3...");
println!();
toolchain.upload_to_s3()?;
println!();
println!("✅ Build and upload complete");
Ok(())
}
fn validate_toolchain_name(name: &str) -> Result<()> {
match name {
"rialo-rust" | "rust" => Ok(()),
_ => bail!(
"Unknown toolchain: {}. Use 'rialo-rust' or 'rust' as the toolchain name",
name
),
}
}
fn check_aws_credentials() -> bool {
std::env::var("AWS_ACCESS_KEY_ID").is_ok() && std::env::var("AWS_SECRET_ACCESS_KEY").is_ok()
}
fn get_current_toolchain_version() -> Result<String> {
let dirs = RialoDirs::new(None)?;
let current = CurrentManager::new(dirs.current_file().clone())
.load()?
.ok_or_else(|| {
anyhow!("No active release. Use --version to specify a toolchain version.")
})?;
let manifest_path = dirs
.releases()
.join(current.channel.as_str())
.join(¤t.version)
.join("manifest.json");
let manifest: Manifest = serde_json::from_str(
&fs::read_to_string(&manifest_path)
.with_context(|| format!("failed to read manifest at {}", manifest_path.display()))?,
)
.context("failed to parse manifest")?;
manifest
.rust_toolchain_version()
.map(String::from)
.ok_or_else(|| {
anyhow!(
"Release {}@{} does not specify a toolchain. Use --version to specify one.",
current.channel,
current.version
)
})
}
fn print_toolchain_status(dirs: &RialoDirs, channel: &str, version: &str) -> Result<()> {
use rialo_build_lib::toolchain::list_installed_toolchains;
let manifest_path = dirs
.releases()
.join(channel)
.join(version)
.join("manifest.json");
let contents = fs::read_to_string(&manifest_path)
.with_context(|| format!("failed to read manifest at {}", manifest_path.display()))?;
let manifest: Manifest = serde_json::from_str(&contents).context("failed to parse manifest")?;
let Some(tc_version) = manifest.rust_toolchain_version() else {
return Ok(());
};
let installed = list_installed_toolchains()
.map(|tc| tc.iter().any(|(n, v)| n == "rialo-rust" && v == tc_version))
.unwrap_or(false);
let (icon, suffix) = if installed {
("✓", "")
} else {
("○", " (not installed)")
};
println!(" {} rialo-rust {}{}", icon, tc_version, suffix);
Ok(())
}