use anyhow::{bail, Result}; use cache::relative_to_absolute; use clap::{Parser, Subcommand}; use std::env; use std::fs::File; use std::path::PathBuf; use std::io::{BufRead, BufReader}; // , Write, stdout}; use std::process::Command; mod cache; // see reqwest/web-o/src/cache.rs for example cache // It restores reqwest::header::HeaderMap // (which allows duplicates... and ignores case on keys) #[derive(Parser)] #[command(about = "Go updater")] struct Cli { /// Cache directory path #[arg(default_value = "cache")] cache: PathBuf, #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Commands { /// Update go Update {}, /// Display information Info {}, } /// Query `go version` #[must_use] fn find_go_version() -> Result { let output = Command::new("go").arg("version").output()?; if output.status.success() { // Ok! We have something! return Ok(String::from_utf8_lossy(output.stdout.as_slice()).to_string()); } bail!("Failed to query go version."); } /// Locate the go binary /// /// This is redundant, it should be located via GO_PATH. #[allow(dead_code)] #[must_use] fn find_go() -> Result { let output = Command::new("which").arg("go").output()?; if output.status.success() { return Ok(String::from_utf8_lossy(output.stdout.as_slice()).to_string()); } bail!("Failed to locate go binary."); } const GO_URL: &str = "https://go.dev/dl/"; const GO_FILE: &str = "go-dl.html"; // 2 MB download of html... // static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); /* CSS Selector elements of interest: (a href="" part). href are relative... div[class="downloadWrapper"] > a[class="download downloadBox"] table[class="downloadtable"] > tr[class=" "] > td[class="filename"] > a[class="download"] Or possibly, table[class="downloadtable"] > tr[class=" "] or tr[class="highlight "] ? and grab the section of td's. class=filename has a href, last has SHA256. go1.24.0.src.tar.gz go1.24.0.linux-amd64.tar.gz go1.24.0.linux-arm64.tar.gz go1.24.0.windows-amd64.zip go1.24.0.windows-amd64.msi */ /* fn download_and_save(url: &str, filename: &str) -> Result<()> { let client = reqwest::blocking::Client::builder() .user_agent(APP_USER_AGENT) .build()?; print!("Downloading: {url} "); let _ = stdout().flush(); let mut resp = client.get(url).send().context("Failed get")?; if resp.status().is_success() { let mut file = File::create(filename).with_context(|| format!("Creating file {filename} failed."))?; resp.copy_to(&mut file)?; } else { bail!("Status Code: {:?}", resp.status()); } println!("OK"); Ok(()) } */ // URL: https://go.dev/dl/go1.24.1.linux-amd64.tar.gz #[must_use] /// Get go version from download URL. fn version_from_url(url: &str, arch: &str) -> Option { if let Some(parts) = url.split_once(arch) { if let Some(part) = parts.0.rsplit_once("/go") { let part = part.1.trim_matches('.'); return Some(part.to_string()); } } None } #[must_use] /// Get go version from `go version` output. fn version_from_go(text: &str) -> Option { let parts : Vec<&str> = text.split(' ').collect(); if parts.len() == 4 { return Some(parts[2].to_string().replace("go", "")) } None } /// Return just the href="". #[must_use] fn just_href(link: &str) -> Result { let parts = link.split_once("href=\"").unwrap_or(("","")).1; let href = parts.split_once("\"").unwrap_or(("", "")).0; if !href.is_empty() { return Ok(href.to_string()); } bail!("Unable to locate href"); } /// Find a href link for given arch (architecture) /// /// Look for Result { // , Box> { let fp = File::open(GO_FILE)?; let reader = BufReader::new(fp); for line in reader.lines() { if let Ok(line) = line { if line.contains("a class=\"download\"") { if line.contains(arch) { // Return just the href part. return just_href(&line); } } } } bail!("Unable to locate architecture download link"); } fn main() -> Result<()> { let cli = Cli::parse(); // Get go environment let go_path = env::var("GOPATH").unwrap_or(String::new()); let go_root = env::var("GOROOT").unwrap_or(String::new()); let go_version: String; if let Ok(version) = find_go_version() { go_version = version.as_str().trim().to_string(); } else { panic!("I wasn't able to locate go. I need `go version` to know what arch to dl."); } let version = version_from_go(&go_version).unwrap(); // Since I have GO_PATH, I really don't need to do `where go`... // $GOROOT/bin/go let go_where: String; if let Ok(location) = find_go() { go_where = location.as_str().trim().to_string(); } else { panic!("I wasn't able to locate the go binary."); } // Get arch (from `go version` output) let parts = go_version.split(" "); let mut arch = parts.last().unwrap().to_string(); arch = arch.replace("/", "-"); /* println!("GO_PATH {}", go_path); println!("GO_ROOT {}", go_root); println!("version: {}", go_version); println!("where: {}", go_where); println!("arch: {}", arch); */ let cache = cache::Cache::new(cli.cache, None)?; // println!("Result: {:?}", get_go_downloads().await ); // Get go version and path match &cli.command { Some(Commands::Update {}) => { let status = cache.fetch(GO_URL); // Check to see if file already exists AND // Check version against go's version. // Since the go.dev site doesn't allow caching or knowing when it changed... match status { cache::Status::Fetched(fp) => { let link = find_arch_link(&arch, &fp); if let Ok(relative) = link { let abs = relative_to_absolute(GO_URL, &relative).unwrap(); println!("URL: {}", abs); let latest_version = version_from_url(&abs, &arch); if let Some(latest) = latest_version { println!("Version: {} [have {}]", latest, version); if version != latest { let latest_status = cache.fetch(&abs); println!("Latest: {:?}", latest_status); } } else { println!("Finding version failed for string: [{}]", abs); } } }, cache::Status::Cached(fp) => { println!("(from cache)"); // I wish I could see this. let link = find_arch_link(&arch, &fp); if let Ok(relative) = link { let abs = relative_to_absolute(GO_URL, &relative).unwrap(); println!("URL: {}", abs); } } _ => { println!("Status = {:?}", status); } } } Some(Commands::Info {}) => { println!("GO_PATH {}", go_path); println!("GO_ROOT {}", go_root); println!("go ver: {}", go_version); println!("version: {}", version); println!("where: {}", go_where); println!("arch: {}", arch); } None => { // Display help. let _show_help: Cli = Cli::parse_from(["--help"]); } } Ok(()) }