#![deny(warnings)]
#![cfg_attr(feature = "cargo-clippy", deny(clippy))]
extern crate chrono;
extern crate dirs;
extern crate reqwest;
#[macro_use]
extern crate structopt;
extern crate termcolor;
use std::io::Write;
use std::path::PathBuf;
use structopt::{clap::AppSettings, StructOpt};
use termcolor::{Color, ColorChoice, ColorSpec, WriteColor};
#[derive(StructOpt)]
#[structopt(
raw(global_settings = "&[AppSettings::DisableHelpSubcommand,
AppSettings::InferSubcommands,
AppSettings::VersionlessSubcommands]")
)]
struct Args {
#[structopt(short = "v", long = "verbose")]
verbose: bool,
#[structopt(short = "q", long = "quiet")]
quiet: bool,
#[structopt(short = "n", long = "no-colors")]
no_colors: bool,
#[structopt(short = "d", long = "days", default_value = "30")]
days: usize,
#[structopt(short = "o", long = "offset", default_value = "0")]
offset: usize,
#[structopt(short = "b", long = "rustup-bin", default_value = "rustup", parse(try_from_str = "parse_path"))]
rustup_bin: PathBuf,
#[structopt(short = "r", long = "rustup-dir", default_value = "~/.rustup", parse(try_from_str = "parse_path"))]
rustup_dir: PathBuf,
#[structopt(short = "t", long = "toolchain", parse(try_from_str = "parse_toolchain"))]
toolchain: Option<(String, String)>,
#[structopt(short = "c", long = "components")]
components: Vec<String>,
#[structopt(short = "s", long = "skip-installed")]
skip_components: bool,
#[structopt(subcommand)]
command: Option<Cmd>
}
#[derive(Copy, Clone, PartialEq, Eq, StructOpt)]
enum Cmd {
#[structopt(name = "find")]
Find,
#[structopt(name = "install")]
Install,
#[structopt(name = "replace")]
Replace {
#[structopt(short = "k", long = "keep-previous")]
keep_old: bool
}
}
#[cfg_attr(feature = "cargo-clippy",
allow(cyclomatic_complexity))] #[cfg_attr(feature = "cargo-clippy",
allow(write_literal))] fn main() {
let Args {
command,
mut components,
days,
no_colors,
offset,
quiet,
rustup_bin,
mut rustup_dir,
skip_components,
toolchain,
verbose,
} = Args::from_args();
let colors = if no_colors { ColorChoice::Never } else { ColorChoice::Auto };
macro_rules! status {
(error, $( $arg: expr ),+ ) => (
status!(termcolor::StandardStream::stderr(colors), Color::Red, "[-] ", $($arg),+)
);
(warning, $( $arg: expr ),+ ) => (
status!(termcolor::StandardStream::stderr(colors), Color::Yellow, "[!] ", $($arg),+)
);
(info, $( $arg: expr ),+ ) => (
if verbose {
status!(termcolor::StandardStream::stdout(colors), Color::Blue, "[i] ", $($arg),+)
}
);
(success, $( $arg: expr ),+ ) => (
status!(termcolor::StandardStream::stdout(colors), Color::Green, "[+] ", $($arg),+)
);
( $out: expr, $color: expr, $( $arg: expr ),+ ) => (
if !quiet {
let mut out = $out;
let mut _has_color = true;
$(
if _has_color {
_has_color = false;
let _ = out.set_color(ColorSpec::new().set_fg(Some($color)));
} else {
_has_color = true;
let _ = out.reset();
}
let _ = write!(out, "{}", $arg);
);+
let _ = out.write(b"\n");
}
)
}
macro_rules! fail {
( $status: expr, $( $arg: expr ),+ ) => ({
status!(error, $( $arg ),+);
std::process::exit($status)
});
}
macro_rules! rustup {
(output, $( $arg: expr ),+ ) => ({
let result = rustup!( $( $arg ),+ );
if !result.status.success() {
if !quiet {
use std::fmt::Write;
let mut bin = rustup_bin.to_str().unwrap().to_string();
$(
let _ = write!(bin, " {}", $arg);
);+
eprintln!("Failed to execute \"{:#?}\":", bin);
std::io::stderr().write_all(&result.stderr).is_ok();
}
std::process::exit(1)
}
match String::from_utf8(result.stdout) {
Ok(output) => output,
Err(err) => fail!(2, "Failed to convert output of Rustup command: ", err, '.')
}
});
( $( $arg: expr ),+ ) => (
std::process::Command::new(&rustup_bin)
.args(&[ $( $arg ),+ ])
.output()
.expect("Failed to spawn rustup process.")
);
}
let (channel, target) = match toolchain {
Some(values) => values,
None => {
let output = rustup!(output, "toolchain", "list");
match output.lines().find(|line| line.ends_with(" (default)")) {
Some(line) => parse_toolchain(&line[..line.len() - 10]).unwrap(),
None => fail!(3, "Could not find default toolchain.")
}
}
};
let toolchain = format!("{}-{}", channel, target);
status!(info, "Channel: ", &channel, '.');
status!(info, "Target: ", &target, '.');
if !skip_components {
let output = rustup!(output, "component", "list", "--toolchain", &toolchain);
for line in output.lines() {
let component = if line.ends_with(" (default)") {
let line = &line[.. line.len() - 10];
if line.ends_with(&target) {
&line[.. line.len() - target.len() - 1]
} else {
line
}
} else if line.ends_with(" (installed)") {
let line = &line[.. line.len() - 12];
if line.ends_with(&target) {
&line[.. line.len() - target.len() - 1]
} else {
line
}
} else {
""
};
if !component.is_empty() && !component.starts_with("rust-src") && !component.starts_with("rust-std") {
components.push(component.to_string());
}
}
}
status!(info, "Required components: ", {
use std::fmt::Write;
let mut s = components[0].clone();
for component in &components[1..] {
let _ = write!(s, ", {}", component);
}
s
}, ".");
let mut date = chrono::Utc::now() - chrono::Duration::days(offset as i64 - 1);
let one_day = chrono::Duration::days(1);
let start_date = date;
let new_toolchain = 'main: loop {
date = date - one_day;
if start_date - date > chrono::Duration::days(days as _) {
fail!(5, "Could not find a match in the last ", days, " days.");
}
let date_str = date.format("%Y-%m-%d");
let url = format!("https://static.rust-lang.org/dist/{}/channel-rust-{}.toml",
date_str, channel);
match reqwest::get(&url) {
Ok(mut res) => {
let text = match res.text() {
Ok(text) => text,
Err(_) => {
status!(error, "Cannot get toolchain for ", date_str, ".");
continue 'main
}
};
let mut lines = text.lines();
let mut remaining_components = components.clone();
'match_components: loop {
if let Some(line) = lines.next() {
if !line.starts_with("[pkg.") || !line.ends_with(']') {
continue
}
if let Some(next_line) = lines.next() {
if next_line != "available = true" {
continue
}
} else {
continue 'main
}
let mut i = 0;
while i < remaining_components.len() {
let remove = {
let component = &remaining_components[i];
line[5..].starts_with(component) && (
(line.len() == 6 + component.len()) ||
(
line[5 + component.len() ..].starts_with(".target") &&
line[13 + component.len() ..].starts_with(&target)
)
)
};
if remove {
remaining_components.swap_remove(i);
if remaining_components.is_empty() {
break 'match_components
}
} else {
i += 1;
}
}
} else {
if !verbose {
continue 'main
}
if components.len() == remaining_components.len() {
status!(info, "No components were available in ", &date_str, ".");
} else if remaining_components.len() == 1 {
status!(info, "The following component was missing in ", &date_str, ": ", remaining_components[0], ".");
} else {
status!(info, "The following components were missing in ", &date_str, ":");
for component in &remaining_components {
status!(info, " - ", &component, ".");
}
}
continue 'main
}
}
break format!("{}-{}-{}", channel, date_str, target)
},
Err(_) => continue
}
};
let command = match command {
None | Some(Cmd::Find) => {
println!("{}", new_toolchain);
std::process::exit(0);
},
Some(command) => command
};
status!(success, "Found valid toolchain: ", &new_toolchain, ".");
status!(info, "Installing toolchain...");
let output = rustup!("toolchain", "install", &new_toolchain);
if !output.status.success() {
status!(error, "Could not install toolchain ", &new_toolchain, ":");
let _ = std::io::stdout().write_all(&output.stdout);
std::process::exit(6);
}
status!(success, "Installed toolchain ", &new_toolchain, ".");
if let Cmd::Replace { keep_old } = command {
status!(info, "Replacing previous toolchain ", &toolchain, "...");
rustup_dir.push("toolchains");
if keep_old {
let r = std::fs::rename(
rustup_dir.join(&toolchain),
rustup_dir.join(format!("{}-old", toolchain)));
if r.is_err() {
fail!(7, "Could not move previous toolchain ", &toolchain, " to new location.")
}
} else {
let r = std::fs::remove_dir_all(rustup_dir.join(&toolchain));
if r.is_err() {
fail!(8, "Could not remove previous toolchain ", &toolchain, ".");
}
}
let r = std::fs::rename(rustup_dir.join(&new_toolchain), rustup_dir.join(&toolchain));
if r.is_err() {
fail!(9, "Could not move toolchain ", &new_toolchain, " to new location ", &toolchain, ".");
}
rustup_dir.pop();
rustup_dir.push("update-hashes");
if keep_old {
let r = std::fs::rename(
rustup_dir.join(&toolchain),
rustup_dir.join(format!("{}-old", toolchain)));
if r.is_err() {
fail!(10, "Could not move previous hashes for toolchain ", &toolchain, " to new location.")
}
} else {
let r = std::fs::remove_dir_all(rustup_dir.join(&toolchain));
if r.is_err() {
fail!(11, "Could not remove previous hashes for toolchain ", &toolchain, ".");
}
}
let r = std::fs::rename(rustup_dir.join(&new_toolchain), rustup_dir.join(&toolchain));
if r.is_err() {
fail!(12, "Could not move hashes of toolchain ", &new_toolchain, " to new location ", &toolchain, ".");
}
status!(success, "Replaced previous toolchain ", &toolchain, " by ", &new_toolchain, ".");
}
}
fn parse_toolchain(toolchain: &str) -> Result<(String, String), &'static str> {
let hyphen = toolchain.find('-').ok_or("Invalid toolchain format.")?;
Ok((toolchain[..hyphen].to_string(), toolchain[hyphen+1..].to_string()))
}
fn parse_path(path: &str) -> Result<PathBuf, &'static str> {
if path.starts_with('~') {
let home = dirs::home_dir().ok_or("Unable to resolve path.")?;
Ok(home.join(&path[2..]))
} else {
Ok(PathBuf::from(path))
}
}