main.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. use anyhow::{Context, Result, bail};
  2. use cache::relative_to_absolute;
  3. use clap::{Parser, Subcommand};
  4. use std::collections::HashMap;
  5. // use std::env;
  6. use std::env;
  7. use std::fs::{File, create_dir_all, read_dir, remove_dir_all, set_permissions};
  8. use std::io::{BufRead, BufReader};
  9. use std::path::PathBuf;
  10. use std::process::Command;
  11. use std::time::Duration;
  12. mod cache;
  13. #[derive(Parser)]
  14. #[command(
  15. about = "Go Updater",
  16. long_about = "Go Updater
  17. This checks https://go.dev/dl for newer versions of go.
  18. It depends upon go being in the path, and optionally GOPATH being set.
  19. This can't update a package manager installed version of go (permissions)."
  20. )]
  21. struct Cli {
  22. /*
  23. /// Cache directory path
  24. #[arg(short, long, default_value = "cache")]
  25. cache: PathBuf,
  26. */
  27. #[command(subcommand)]
  28. command: Option<Commands>,
  29. }
  30. #[derive(Subcommand)]
  31. enum Commands {
  32. /// Update go
  33. Update {},
  34. /// Display information
  35. Info {},
  36. }
  37. /// Query `go env`
  38. ///
  39. /// This is a good example of parsing output line by line.
  40. #[must_use]
  41. fn get_go_env() -> Result<HashMap<String, String>> {
  42. let output = Command::new("go").arg("env").output()?;
  43. if output.status.success() {
  44. let mut result = HashMap::<String, String>::new();
  45. let output_str = std::str::from_utf8(output.stdout.as_slice())?;
  46. for mut line in output_str.split("\n") {
  47. line = line.trim();
  48. if let Some(parts) = line.split_once("=") {
  49. let value = parts.1.trim_matches('\'');
  50. result.insert(parts.0.to_string(), value.to_string());
  51. }
  52. }
  53. /*
  54. for mut line in String::from_utf8_lossy(output.stdout.as_slice()).split("\n") {
  55. }
  56. */
  57. Ok(result)
  58. } else {
  59. bail!("Failed to query `go env`");
  60. }
  61. }
  62. fn extract_tarball(tarball: &str, target: &str) -> Result<()> {
  63. println!("Extract {} to {}", tarball, target);
  64. let output = Command::new("tar")
  65. // Extract, gzipped, from file
  66. .arg("-xzf")
  67. .arg(tarball)
  68. // archive contains go directory. Strip that out.
  69. .arg("--strip-components=1")
  70. // Set target to extract to.
  71. .arg("-C")
  72. .arg(target)
  73. .output()?;
  74. if output.status.success() {
  75. return Ok(());
  76. }
  77. bail!("Extract {} failed.", tarball);
  78. }
  79. /*
  80. /// Query `go version`
  81. #[must_use]
  82. #[allow(dead_code)]
  83. #[deprecated = "Use get_go_env"]
  84. fn find_go_version() -> Result<String> {
  85. let output = Command::new("go").arg("version").output()?;
  86. if output.status.success() {
  87. // Ok! We have something!
  88. return Ok(String::from_utf8_lossy(output.stdout.as_slice()).to_string());
  89. }
  90. bail!("Failed to query go version.");
  91. }
  92. */
  93. /*
  94. /// Locate the go binary
  95. ///
  96. /// This is redundant, it should be located via GO_PATH.
  97. #[allow(dead_code)]
  98. #[must_use]
  99. fn find_go() -> Result<String> {
  100. let output = Command::new("which").arg("go").output()?;
  101. if output.status.success() {
  102. return Ok(String::from_utf8_lossy(output.stdout.as_slice()).to_string());
  103. }
  104. bail!("Failed to locate go binary.");
  105. }
  106. */
  107. const GO_URL: &str = "https://go.dev/dl/";
  108. // static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
  109. // I've changed the parser, so scrapy isn't needed.
  110. // I'll revisit this when I get further with html tokeparser.
  111. /*
  112. CSS Selector elements of interest: (a href="" part).
  113. href are relative...
  114. div[class="downloadWrapper"] > a[class="download downloadBox"]
  115. <a class="download downloadBox" href="/dl/go1.24.0.windows-amd64.msi">
  116. <a class="download downloadBox" href="/dl/go1.24.0.linux-amd64.tar.gz">
  117. table[class="downloadtable"] > tr[class=" "] > td[class="filename"] > a[class="download"]
  118. Or possibly,
  119. table[class="downloadtable"] > tr[class=" "]
  120. or tr[class="highlight "] ?
  121. and grab the section of td's. class=filename has a href, last has SHA256.
  122. <a class="download" href="/dl/go1.24.0.src.tar.gz">go1.24.0.src.tar.gz</a>
  123. <a class="download" href="/dl/go1.24.0.linux-amd64.tar.gz">go1.24.0.linux-amd64.tar.gz</a>
  124. <a class="download" href="/dl/go1.24.0.linux-arm64.tar.gz">go1.24.0.linux-arm64.tar.gz</a>
  125. <a class="download" href="/dl/go1.24.0.windows-amd64.zip">go1.24.0.windows-amd64.zip</a>
  126. <a class="download" href="/dl/go1.24.0.windows-amd64.msi">go1.24.0.windows-amd64.msi</a>
  127. */
  128. // URL: https://go.dev/dl/go1.24.1.linux-amd64.tar.gz
  129. /// Get go version from download link.
  130. ///
  131. /// Given https://go.dev/dl/go1.24.1.linux-amd64.tar.gz
  132. /// return "1.24.1" version part.
  133. #[must_use]
  134. fn version_from_url(url: &str, arch: &str) -> Option<String> {
  135. if let Some(parts) = url.split_once(arch) {
  136. if let Some(part) = parts.0.rsplit_once("/go") {
  137. let part = part.1.trim_matches('.');
  138. return Some(part.to_string());
  139. }
  140. }
  141. None
  142. }
  143. /// Return just the href="<return just this part>".
  144. #[must_use]
  145. fn just_href(link: &str) -> Result<String> {
  146. let parts = link.split_once("href=\"").unwrap_or(("", "")).1;
  147. let href = parts.split_once("\"").unwrap_or(("", "")).0;
  148. if !href.is_empty() {
  149. return Ok(href.to_string());
  150. }
  151. bail!("Unable to locate href");
  152. }
  153. /// Find a href link for given os and arch (architecture) in a file.
  154. ///
  155. /// Look for a line with <a class="download" href="
  156. #[must_use]
  157. fn find_arch_link(os_arch: &str, fp: &File) -> Result<String> {
  158. let reader = BufReader::new(fp);
  159. for line in reader.lines() {
  160. if let Ok(line) = line {
  161. if line.contains("a class=\"download\"") {
  162. if line.contains(os_arch) {
  163. // Return just the href part.
  164. return just_href(&line);
  165. }
  166. }
  167. }
  168. }
  169. bail!("Unable to locate OS architecture download link");
  170. }
  171. /*
  172. // This is terrible, since it has all of the HTML in a string.
  173. /// find_link for given arch (architecture)
  174. ///
  175. /// Look for <a class="download" href=""
  176. #[must_use]
  177. #[deprecated = "Use find_arch_link don't store the HTML in memory."]
  178. fn find_link(arch: &str) -> Result<String> {
  179. // , Box<dyn Error>> {
  180. let fp = File::open(GO_FILE)?;
  181. let reader = BufReader::new(fp);
  182. for line in reader.lines() {
  183. if let Ok(line) = line {
  184. if line.contains("a class=\"download\"") {
  185. if line.contains(arch) {
  186. // Return just the href part.
  187. return just_href(&line);
  188. }
  189. }
  190. }
  191. }
  192. bail!("Unable to locate architecture download link");
  193. }
  194. */
  195. /// Get value from given HashMap<String,String>
  196. ///
  197. /// This gives us a String, not a &String, and not an Option.
  198. /// Returns empty String if key not found.
  199. fn hashget(hash: &HashMap<String, String>, key: &str) -> String {
  200. if let Some(value) = hash.get(key) {
  201. return value.clone();
  202. }
  203. String::new()
  204. }
  205. fn recursive_remove_readonly(path: &str) -> Result<bool> {
  206. let mut result: bool = false;
  207. for entry in read_dir(path)? {
  208. let fsentry = entry?;
  209. if let Ok(meta) = fsentry.metadata() {
  210. if meta.is_dir() {
  211. if let Ok(r) =
  212. recursive_remove_readonly(&fsentry.path().to_string_lossy().to_string())
  213. {
  214. if r {
  215. result = true;
  216. }
  217. }
  218. }
  219. if meta.permissions().readonly() {
  220. // println!("Fixing: {}", fsentry.path().display());
  221. let mut read = meta.permissions();
  222. read.set_readonly(false);
  223. // Ok, this is marked readonly, change it.
  224. set_permissions(fsentry.path(), read)?;
  225. result = true;
  226. }
  227. }
  228. }
  229. Ok(result)
  230. }
  231. fn main() -> Result<()> {
  232. let cli = Cli::parse();
  233. // Construct cache_dir from HOME/~
  234. let home = env::var("HOME")?;
  235. let cache_dir = format!("{home}/.cache/go-up");
  236. /*
  237. GOROOT, defaults to $HOME/go
  238. This seems to be managed by go now. So running ```go env``` would
  239. have/show GOROOT being set by that instance of go.
  240. GOPATH is another story. It's where go stores things it downloads from
  241. the web, and most certainly shouldn't be $HOME/go != GOROOT.
  242. I want to be a bit looser on requiring GOROOT later on.
  243. */
  244. /*
  245. // Get go environment
  246. // The old way of doing this. Using `go env` now.
  247. let go_path = env::var("GOPATH").unwrap_or(String::new());
  248. let go_root = env::var("GOROOT").unwrap_or(String::new());
  249. */
  250. let go_env = get_go_env().context("Calling `go env` failed.")?;
  251. let mut go_version = hashget(&go_env, "GOVERSION");
  252. if go_version.starts_with("go") {
  253. go_version.remove(0);
  254. go_version.remove(0);
  255. }
  256. let go_arch = hashget(&go_env, "GOARCH");
  257. let go_os = hashget(&go_env, "GOOS");
  258. let go_path = hashget(&go_env, "GOPATH");
  259. let go_root = hashget(&go_env, "GOROOT");
  260. // Construct OS-ARCH value.
  261. let go_os_arch = format!("{go_os}-{go_arch}");
  262. // Initialize the cache.
  263. let cache = cache::Cache::new(PathBuf::from(cache_dir), None)?;
  264. let ex = cache.expire(Duration::from_secs(7 * 60 * 60 * 24))?;
  265. if ex {
  266. println!("Expired files from cache.");
  267. }
  268. match &cli.command {
  269. Some(Commands::Update {}) => {
  270. let status = cache.fetch(GO_URL)?;
  271. // Check to see if file already exists AND check version against go's version.
  272. // Since the go.dev site doesn't allow caching or knowing when it changes,
  273. // we always end up in Status::Fetched arm.
  274. let filename = status.download_path();
  275. // Make this function the same whether or not we have a cache hit.
  276. let fp = File::open(filename)?;
  277. let link = find_arch_link(&go_os_arch, &fp);
  278. if let Ok(relative) = link {
  279. // Download link for arch located. Make absolute URL.
  280. let latest_version = version_from_url(&relative, &go_os_arch);
  281. if let Some(latest) = latest_version {
  282. println!("Version: {} [have {}]", latest, go_version);
  283. if go_version != latest {
  284. println!("Downloading newer version...");
  285. let abs = relative_to_absolute(GO_URL, &relative).unwrap();
  286. let latest_status = cache.fetch(&abs)?;
  287. let update = latest_status.download_path();
  288. println!("Clearing GOROOT {}", go_root);
  289. // GOROOT might contain GOPATH files
  290. // Fix permissions before attempting to remove.
  291. recursive_remove_readonly(&go_path)?;
  292. remove_dir_all(&go_root)?;
  293. create_dir_all(&go_root)?;
  294. if !go_path.is_empty() {
  295. if go_path != go_root {
  296. println!("Clearing GOPATH {}", go_path);
  297. // This fails with permissions error
  298. recursive_remove_readonly(&go_path)?;
  299. remove_dir_all(&go_path)?;
  300. create_dir_all(&go_path)?;
  301. }
  302. }
  303. println!("Extracting...");
  304. extract_tarball(&update.to_string_lossy().to_string(), &go_root)?;
  305. // Extract go.tar.gz into go_root
  306. // Ok, we have the update! Now what?
  307. // Verify new GO version!
  308. let new_go_env = get_go_env().context("Calling `go env` with new go.")?;
  309. let mut new_go_version = hashget(&new_go_env, "GOVERSION");
  310. if new_go_version.starts_with("go") {
  311. new_go_version.remove(0);
  312. new_go_version.remove(0);
  313. }
  314. println!("Updated {} to {}", go_version, new_go_version);
  315. /*
  316. Clear GOROOT.
  317. mkdir_all(GOROOT).
  318. If GOPATH exists
  319. IF GOPATH != GOROOT, clear it.
  320. mkdir_all(GOPATH)
  321. Untarball go into GOROOT.
  322. Check/Verify go version?
  323. */
  324. // println!("Latest: {:?}", latest_status);
  325. } else {
  326. println!("You're already good to GO.");
  327. }
  328. } else {
  329. println!("Finding version failed: [{}]", relative);
  330. }
  331. } else {
  332. bail!("Unable to locate download link");
  333. }
  334. /*
  335. match status {
  336. cache::Status::Fetched(filename) |
  337. cache::Status::Cached(filename) => {
  338. // Make this function the same whether or not we have a cache hit.
  339. let fp = File::open(filename)?;
  340. let link = find_arch_link(&go_os_arch, &fp);
  341. if let Ok(relative) = link {
  342. // Download link for arch located. Make absolute URL.
  343. let abs = relative_to_absolute(GO_URL, &relative).unwrap();
  344. println!("URL: {}", abs);
  345. let latest_version = version_from_url(&abs, &go_os_arch);
  346. if let Some(latest) = latest_version {
  347. println!("Version: {} [have {}]", latest, go_version);
  348. if go_version != latest {
  349. println!("Downloading newer version...");
  350. let latest_status = cache.fetch(&abs);
  351. println!("Latest: {:?}", latest_status);
  352. } else {
  353. println!("You're already good to GO.");
  354. }
  355. } else {
  356. println!("Finding version failed: [{}]", abs);
  357. }
  358. } else {
  359. bail!("Unable to locate download link");
  360. }
  361. }
  362. _ => {
  363. println!("Status = {:?}", status);
  364. }
  365. }
  366. */
  367. }
  368. Some(Commands::Info {}) => {
  369. println!("GOPATH {}", go_path);
  370. println!("GOROOT {}", go_root);
  371. println!("go ver {}", go_version);
  372. println!("go_arch {}", go_arch);
  373. println!("go os {}", go_os);
  374. println!("go os arch: {}", go_os_arch);
  375. }
  376. None => {
  377. // Display help.
  378. let _show_help: Cli = Cli::parse_from(["", "--help"]);
  379. }
  380. }
  381. Ok(())
  382. }