123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- use anyhow::{Context, Result, bail};
- use cache::relative_to_absolute;
- use clap::{Parser, Subcommand};
- use std::collections::HashMap;
- // use std::env;
- use std::env;
- use std::fs::{File, create_dir_all, read_dir, remove_dir_all, set_permissions};
- use std::io::{BufRead, BufReader};
- use std::path::PathBuf;
- use std::process::Command;
- use std::time::Duration;
- mod cache;
- #[derive(Parser)]
- #[command(
- about = "Go Updater",
- long_about = "Go Updater
- This checks https://go.dev/dl for newer versions of go.
- It depends upon go being in the path, and optionally GOPATH being set.
- This can't update a package manager installed version of go (permissions)."
- )]
- struct Cli {
- /*
- /// Cache directory path
- #[arg(short, long, default_value = "cache")]
- cache: PathBuf,
- */
- #[command(subcommand)]
- command: Option<Commands>,
- }
- #[derive(Subcommand)]
- enum Commands {
- /// Update go
- Update {},
- /// Display information
- Info {},
- }
- /// Query `go env`
- ///
- /// This is a good example of parsing output line by line.
- #[must_use]
- fn get_go_env() -> Result<HashMap<String, String>> {
- let output = Command::new("go").arg("env").output()?;
- if output.status.success() {
- let mut result = HashMap::<String, String>::new();
- let output_str = std::str::from_utf8(output.stdout.as_slice())?;
- for mut line in output_str.split("\n") {
- line = line.trim();
- if let Some(parts) = line.split_once("=") {
- let value = parts.1.trim_matches('\'');
- result.insert(parts.0.to_string(), value.to_string());
- }
- }
- /*
- for mut line in String::from_utf8_lossy(output.stdout.as_slice()).split("\n") {
- }
- */
- Ok(result)
- } else {
- bail!("Failed to query `go env`");
- }
- }
- fn extract_tarball(tarball: &str, target: &str) -> Result<()> {
- println!("Extract {} to {}", tarball, target);
- let output = Command::new("tar")
- // Extract, gzipped, from file
- .arg("-xzf")
- .arg(tarball)
- // archive contains go directory. Strip that out.
- .arg("--strip-components=1")
- // Set target to extract to.
- .arg("-C")
- .arg(target)
- .output()?;
- if output.status.success() {
- return Ok(());
- }
- bail!("Extract {} failed.", tarball);
- }
- /*
- /// Query `go version`
- #[must_use]
- #[allow(dead_code)]
- #[deprecated = "Use get_go_env"]
- fn find_go_version() -> Result<String> {
- 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<String> {
- 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/";
- // static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
- // I've changed the parser, so scrapy isn't needed.
- // I'll revisit this when I get further with html tokeparser.
- /*
- CSS Selector elements of interest: (a href="" part).
- href are relative...
- div[class="downloadWrapper"] > a[class="download downloadBox"]
- <a class="download downloadBox" href="/dl/go1.24.0.windows-amd64.msi">
- <a class="download downloadBox" href="/dl/go1.24.0.linux-amd64.tar.gz">
- 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.
- <a class="download" href="/dl/go1.24.0.src.tar.gz">go1.24.0.src.tar.gz</a>
- <a class="download" href="/dl/go1.24.0.linux-amd64.tar.gz">go1.24.0.linux-amd64.tar.gz</a>
- <a class="download" href="/dl/go1.24.0.linux-arm64.tar.gz">go1.24.0.linux-arm64.tar.gz</a>
- <a class="download" href="/dl/go1.24.0.windows-amd64.zip">go1.24.0.windows-amd64.zip</a>
- <a class="download" href="/dl/go1.24.0.windows-amd64.msi">go1.24.0.windows-amd64.msi</a>
- */
- // URL: https://go.dev/dl/go1.24.1.linux-amd64.tar.gz
- /// Get go version from download link.
- ///
- /// Given https://go.dev/dl/go1.24.1.linux-amd64.tar.gz
- /// return "1.24.1" version part.
- #[must_use]
- fn version_from_url(url: &str, arch: &str) -> Option<String> {
- 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
- }
- /// Return just the href="<return just this part>".
- #[must_use]
- fn just_href(link: &str) -> Result<String> {
- 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 os and arch (architecture) in a file.
- ///
- /// Look for a line with <a class="download" href="
- #[must_use]
- fn find_arch_link(os_arch: &str, fp: &File) -> Result<String> {
- let reader = BufReader::new(fp);
- for line in reader.lines() {
- if let Ok(line) = line {
- if line.contains("a class=\"download\"") {
- if line.contains(os_arch) {
- // Return just the href part.
- return just_href(&line);
- }
- }
- }
- }
- bail!("Unable to locate OS architecture download link");
- }
- /*
- // This is terrible, since it has all of the HTML in a string.
- /// find_link for given arch (architecture)
- ///
- /// Look for <a class="download" href=""
- #[must_use]
- #[deprecated = "Use find_arch_link don't store the HTML in memory."]
- fn find_link(arch: &str) -> Result<String> {
- // , Box<dyn Error>> {
- 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");
- }
- */
- /// Get value from given HashMap<String,String>
- ///
- /// This gives us a String, not a &String, and not an Option.
- /// Returns empty String if key not found.
- fn hashget(hash: &HashMap<String, String>, key: &str) -> String {
- if let Some(value) = hash.get(key) {
- return value.clone();
- }
- String::new()
- }
- fn recursive_remove_readonly(path: &str) -> Result<bool> {
- let mut result: bool = false;
- for entry in read_dir(path)? {
- let fsentry = entry?;
- if let Ok(meta) = fsentry.metadata() {
- if meta.is_dir() {
- if let Ok(r) =
- recursive_remove_readonly(&fsentry.path().to_string_lossy().to_string())
- {
- if r {
- result = true;
- }
- }
- }
- if meta.permissions().readonly() {
- // println!("Fixing: {}", fsentry.path().display());
- let mut read = meta.permissions();
- read.set_readonly(false);
- // Ok, this is marked readonly, change it.
- set_permissions(fsentry.path(), read)?;
- result = true;
- }
- }
- }
- Ok(result)
- }
- fn main() -> Result<()> {
- let cli = Cli::parse();
- // Construct cache_dir from HOME/~
- let home = env::var("HOME")?;
- let cache_dir = format!("{home}/.cache/go-up");
- /*
- GOROOT, defaults to $HOME/go
- This seems to be managed by go now. So running ```go env``` would
- have/show GOROOT being set by that instance of go.
- GOPATH is another story. It's where go stores things it downloads from
- the web, and most certainly shouldn't be $HOME/go != GOROOT.
- I want to be a bit looser on requiring GOROOT later on.
- */
- /*
- // Get go environment
- // The old way of doing this. Using `go env` now.
- let go_path = env::var("GOPATH").unwrap_or(String::new());
- let go_root = env::var("GOROOT").unwrap_or(String::new());
- */
- let go_env = get_go_env().context("Calling `go env` failed.")?;
- let mut go_version = hashget(&go_env, "GOVERSION");
- if go_version.starts_with("go") {
- go_version.remove(0);
- go_version.remove(0);
- }
- let go_arch = hashget(&go_env, "GOARCH");
- let go_os = hashget(&go_env, "GOOS");
- let go_path = hashget(&go_env, "GOPATH");
- let go_root = hashget(&go_env, "GOROOT");
- // Construct OS-ARCH value.
- let go_os_arch = format!("{go_os}-{go_arch}");
- // Initialize the cache.
- let cache = cache::Cache::new(PathBuf::from(cache_dir), None)?;
- let ex = cache.expire(Duration::from_secs(7 * 60 * 60 * 24))?;
- if ex {
- println!("Expired files from cache.");
- }
- 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 changes,
- // we always end up in Status::Fetched arm.
- let filename = status.download_path();
- // Make this function the same whether or not we have a cache hit.
- let fp = File::open(filename)?;
- let link = find_arch_link(&go_os_arch, &fp);
- if let Ok(relative) = link {
- // Download link for arch located. Make absolute URL.
- let latest_version = version_from_url(&relative, &go_os_arch);
- if let Some(latest) = latest_version {
- println!("Version: {} [have {}]", latest, go_version);
- if go_version != latest {
- println!("Downloading newer version...");
- let abs = relative_to_absolute(GO_URL, &relative).unwrap();
- let latest_status = cache.fetch(&abs)?;
- let update = latest_status.download_path();
- println!("Clearing GOROOT {}", go_root);
- // GOROOT might contain GOPATH files
- // Fix permissions before attempting to remove.
- recursive_remove_readonly(&go_path)?;
- remove_dir_all(&go_root)?;
- create_dir_all(&go_root)?;
- if !go_path.is_empty() {
- if go_path != go_root {
- println!("Clearing GOPATH {}", go_path);
- // This fails with permissions error
- recursive_remove_readonly(&go_path)?;
- remove_dir_all(&go_path)?;
- create_dir_all(&go_path)?;
- }
- }
- println!("Extracting...");
- extract_tarball(&update.to_string_lossy().to_string(), &go_root)?;
- // Extract go.tar.gz into go_root
- // Ok, we have the update! Now what?
- // Verify new GO version!
- let new_go_env = get_go_env().context("Calling `go env` with new go.")?;
- let mut new_go_version = hashget(&new_go_env, "GOVERSION");
- if new_go_version.starts_with("go") {
- new_go_version.remove(0);
- new_go_version.remove(0);
- }
- println!("Updated {} to {}", go_version, new_go_version);
- /*
- Clear GOROOT.
- mkdir_all(GOROOT).
- If GOPATH exists
- IF GOPATH != GOROOT, clear it.
- mkdir_all(GOPATH)
- Untarball go into GOROOT.
- Check/Verify go version?
- */
- // println!("Latest: {:?}", latest_status);
- } else {
- println!("You're already good to GO.");
- }
- } else {
- println!("Finding version failed: [{}]", relative);
- }
- } else {
- bail!("Unable to locate download link");
- }
- /*
- match status {
- cache::Status::Fetched(filename) |
- cache::Status::Cached(filename) => {
- // Make this function the same whether or not we have a cache hit.
- let fp = File::open(filename)?;
- let link = find_arch_link(&go_os_arch, &fp);
- if let Ok(relative) = link {
- // Download link for arch located. Make absolute URL.
- let abs = relative_to_absolute(GO_URL, &relative).unwrap();
- println!("URL: {}", abs);
- let latest_version = version_from_url(&abs, &go_os_arch);
- if let Some(latest) = latest_version {
- println!("Version: {} [have {}]", latest, go_version);
- if go_version != latest {
- println!("Downloading newer version...");
- let latest_status = cache.fetch(&abs);
- println!("Latest: {:?}", latest_status);
- } else {
- println!("You're already good to GO.");
- }
- } else {
- println!("Finding version failed: [{}]", abs);
- }
- } else {
- bail!("Unable to locate download link");
- }
- }
- _ => {
- println!("Status = {:?}", status);
- }
- }
- */
- }
- Some(Commands::Info {}) => {
- println!("GOPATH {}", go_path);
- println!("GOROOT {}", go_root);
- println!("go ver {}", go_version);
- println!("go_arch {}", go_arch);
- println!("go os {}", go_os);
- println!("go os arch: {}", go_os_arch);
- }
- None => {
- // Display help.
- let _show_help: Cli = Cli::parse_from(["", "--help"]);
- }
- }
- Ok(())
- }
|