#![feature(maybe_uninit_slice)] #![feature(maybe_uninit_uninit_array)] #[macro_use] extern crate miette; #[macro_use] extern crate serde; mod install; use std::path::{Path, PathBuf}; use miette::{IntoDiagnostic, Result, WrapErr}; use install::{Pipeline, RemoteResource, ShortcutTarget, Step}; const SWTOOLS_PATH: &'static str = "C:\\SWTools"; const USER_AGENT_STR: &'static str = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"; pub fn swtools_path() -> &'static Path { Path::new(SWTOOLS_PATH) } macro_rules! swtools_path { ($( $path:literal )/+) => { Path::new(concat!("C:\\SWTools" $(, "\\", $path )+)) }; } pub fn desktop_path() -> Option { dirs::desktop_dir() .filter(|dir| dir.exists()) .or_else(|| Some(Path::new("H:\\Profile\\Desktop").to_owned())) .filter(|dir| dir.exists()) .or_else(|| dirs::home_dir().map(|dir| dir.join("Desktop"))) } #[derive(Clone)] pub struct Context { pub reqwest: reqwest::Client, } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { println!("Bootstrapping..."); if cfg!(not(windows)) { bail!("Your platform is not supported."); } if !swtools_path().exists() { bail!("Could not find or access {}", SWTOOLS_PATH); } let desktop_path = desktop_path() .ok_or_else(|| miette!("Could not find your desktop directory."))?; let nppp_zip = swtools_path!("temp" / "notepad-plus-plus.zip"); let nppp_dir = swtools_path!("notepad-plus-plus"); let arduino_zip = swtools_path!("temp" / "arduino.zip"); let arduino_dir = swtools_path!("arduino"); let jdk_19_zip = swtools_path!("temp" / "jdk-19.zip"); let jdk_19_dir = swtools_path!("jdk-19"); let epp_zip = swtools_path!("temp" / "explorer-plus-plus.zip"); let epp_dir = swtools_path!("explorer-plus-plus"); let minecraft_dir = swtools_path!("minecraft"); //let psiphon_dir = swtools_path!("psiphon"); let psiphon_bin = swtools_path!("psiphon" / "psiphon3.exe"); let rustup_init = swtools_path!("temp" / "rustup-init.exe"); let deno_zip = swtools_path!("temp" / "deno.zip"); let deno_dir = swtools_path!("deno"); //let deno_exe = swtools_path!("deno" / "deno.exe"); let nppp_pl = [ Step::DownloadFile { file: nppp_zip.into(), res: RemoteResource::GitHubArtifact { repo: "notepad-plus-plus/notepad-plus-plus", pattern: "npp.{{tag_name_strip_prefix}}.portable.x64.zip", }, }, Step::ExtractFile { file: nppp_zip.into(), dest: nppp_dir.into(), }, Step::CreateShortcut { target: ShortcutTarget::Executable { file: nppp_dir.join("notepad++.exe").into(), args: "", }, file: desktop_path.join("Notepad++.lnk").into(), }, ]; let epp_pl = [ Step::DownloadFile { file: epp_zip.into(), res: RemoteResource::Url( "https://explorerplusplus.com/software/explorer++_1.3.5_x64.zip", ), }, Step::ExtractFile { file: epp_zip.into(), dest: epp_dir.into(), }, Step::CreateShortcut { target: ShortcutTarget::Executable { file: epp_dir.join("Explorer++.exe").into(), args: "", }, file: desktop_path.join("Explorer++.lnk").into(), }, ]; let psiphon_pl = [ Step::DownloadFile { file: psiphon_bin.into(), res: RemoteResource::Url("https://s3.amazonaws.com/f58p-mqce-k1yj/psiphon3.exe"), }, Step::CreateShortcut { target: ShortcutTarget::Executable { file: psiphon_bin.into(), args: "", }, file: desktop_path.join("Psiphon3.lnk").into(), }, ]; let minecraft_pl = [ Step::DownloadFile { file: minecraft_dir.join("minecraft.exe").into(), res: RemoteResource::Url("https://launcher.mojang.com/download/Minecraft.exe"), }, Step::CreateShortcut { target: ShortcutTarget::Executable { file: minecraft_dir.join("minecraft.exe").into(), args: "", }, file: desktop_path.join("Minecraft.lnk").into(), }, ]; let c_drive_lnk_pl = [Step::CreateShortcut { target: ShortcutTarget::Path { path: Path::new("C:\\").into(), }, file: desktop_path.join("OSDisk (C).lnk").into(), }]; let pwsh_pl = [Step::CreateShortcut { target: ShortcutTarget::Executable { file: Path::new("C:\\WINDOWS\\system32\\cmd.exe").into(), args: "/C powershell", }, file: desktop_path.join("PowerShell.lnk").into(), }]; let arduino_pl = [ Step::DownloadFile { file: arduino_zip.into(), res: RemoteResource::GitHubArtifact { repo: "arduino/arduino-ide", pattern: "arduino-ide_{{tag_name}}_Windows_64bit.zip", }, }, Step::ExtractFile { file: arduino_zip.into(), dest: arduino_dir.into(), }, Step::CreateShortcut { target: ShortcutTarget::Executable { file: arduino_dir.join("Arduino IDE.exe").into(), args: "", }, file: desktop_path.join("Arduino IDE.lnk").into(), }, // see https://docs.arduino.cc/software/ide-v1/tutorials/PortableIDE // TODO: assess if this is actually necessary //Step::CreateDirectory { // target: arduino_dir.join("portable").into(), // parents: false, //} ]; let jdk_19_pl = [ Step::DownloadFile { file: jdk_19_zip.into(), // TODO: expand GitHubArtifact templating support and replace the hardcoded // url with that. //res: RemoteResource::GitHubArtifact { // repo: "adoptium/temurin19-binaries", // pattern: "OpenJDK19U-jdk_x64_windows_hotspot_19.0.1_10.zip arduino-ide_{{tag_name}}_Windows_64bit.zip", //}, res: RemoteResource::Url( "https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.1%2B10/OpenJDK19U-jdk_x64_windows_hotspot_19.0.1_10.zip", ), }, Step::ExtractFile { file: jdk_19_zip.into(), dest: jdk_19_dir.into(), }, Step::AppendPath(jdk_19_dir.join("bin").into()), ]; let rust_pl = [ Step::DownloadFile { file: rustup_init.into(), res: RemoteResource::Url( "https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe", ), }, Step::ExecuteCommand { file: rustup_init.into(), args: &[ "--default-host", "x86_64-pc-windows-gnu", "--default-toolchain", "nightly", "--profile", "default", ], }, ]; let deno_pl = [ Step::CreateDirectory { target: deno_dir.into(), parents: true, }, Step::DownloadFile { file: deno_zip.into(), res: RemoteResource::Url( "https://github.com/denoland/deno/releases/latest/download/deno-x86_64-pc-windows-msvc.zip" ), }, Step::ExtractFile { file: deno_zip.into(), dest: deno_dir.into(), }, Step::AppendPath(deno_dir.into()), ]; let pipelines = [ Pipeline::new("Install Notepad++", nppp_pl.as_slice()), Pipeline::new("Install Explorer++", epp_pl.as_slice()), Pipeline::new("Install Psiphon VPN", psiphon_pl.as_slice()), Pipeline::new("Install Minecraft (Java Edition)", minecraft_pl.as_slice()), Pipeline::new("Create C:\\ Shortcut", c_drive_lnk_pl.as_slice()), Pipeline::new("Create PowerShell Shortcut", pwsh_pl.as_slice()), Pipeline::new("Install Arduino IDE v2", arduino_pl.as_slice()), Pipeline::new("Install Java (19/temurin)", jdk_19_pl.as_slice()), Pipeline::new("Install Rust (nightly)", rust_pl.as_slice()), // TODO: add an option to install from src Pipeline::new("Install Deno (pre-compiled)", deno_pl.as_slice()), ]; let mut args = std::env::args_os(); let _ = args.next(); if let Some(arg) = args.next() { match arg.to_str() { Some("list") => { println!("Pipelines:"); for pipeline in pipelines { let name = pipeline.name.unwrap_or("Unnamed"); println!("\t{name}"); } return Ok(()); } Some("run") => {} Some(cmd) => { println!("Unrecognized command: {cmd}"); } None => { println!("Unrecognized non-utf8 command"); } } } let ctx = Context { reqwest: reqwest::Client::builder() .user_agent(USER_AGENT_STR) .build() .into_diagnostic() .wrap_err("Could not initialize HTTP client.")?, }; let ctx = &ctx; let (_, results) = async_scoped::Scope::<'_, _, async_scoped::Tokio>::scope_and_block(move |scope| { pipelines .into_iter() .for_each(|pipeline| scope.spawn(async move { pipeline.invoke(ctx).await })) }); let results = results.into_iter().map( |result| match result.into_diagnostic().wrap_err("Task error.") { Ok(Ok(t)) => Ok(t), Ok(Err(e)) => Err(e), Err(e) => Err(e), }, ); let mut had_error = false; for result in results { if let Err(e) = result { had_error = true; eprintln!("{e:?}"); } } if had_error { Err(miette!("One or more errors in pipelines.")) } else { Ok(()) } }