Rust babyyyyyyy
This commit is contained in:
parent
f37463570d
commit
3be5d99000
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "school-computer-toolkit"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
miette = { version = "4.7", features = [ "fancy" ] }
|
||||
ramhorns = "0.14"
|
||||
reqwest = { version = "0.11.10", features = [ "json" ] }
|
||||
serde = { version = "1", features = [ "derive" ] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1.19", features = [ "fs", "macros", "rt" ] }
|
||||
url = "2.2"
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
@echo on
|
||||
|
||||
for /f %%i in ('cd') do set _RunPath=%%i
|
||||
|
||||
pushd C:\SWTools
|
||||
|
||||
IF EXIST .\python\python.exe (
|
||||
echo "Python already installed."
|
||||
) ELSE (
|
||||
IF EXIST .\python.zip (
|
||||
) ELSE (
|
||||
curl "https://www.python.org/ftp/python/3.10.5/python-3.10.5-embed-amd64.zip" --output .\python.zip
|
||||
)
|
||||
|
||||
IF EXIST .\python (
|
||||
del .\python
|
||||
)
|
||||
|
||||
tar -xf .\python.zip
|
||||
)
|
||||
|
||||
.\python\python.exe %_RunPath%\main.py _RunPath
|
|
@ -1,27 +0,0 @@
|
|||
$run_path = Get-Location
|
||||
|
||||
$swtools = C:/SWTools
|
||||
|
||||
Push-Location $swtools
|
||||
|
||||
if (Get-Command "./python/python.exe" -ErrorAction SilentlyContinue) {
|
||||
echo "Python already installed."
|
||||
} else {
|
||||
if (Get-Command "./python.zip" -ErrorAction SilentlyContinue) {
|
||||
} else {
|
||||
curl -Uri "https://www.python.org/ftp/python/3.10.5/python-3.10.5-embed-amd64.zip" -OutFile ./python.zip
|
||||
}
|
||||
|
||||
if (Test-Path -Path "./python") {
|
||||
Remove-Item -Recurse -ErrorAction:Stop "./python"
|
||||
}
|
||||
|
||||
mkdir python
|
||||
Push-Location python
|
||||
tar -xf ../python.zip
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Pop-Location
|
||||
|
||||
$swtools/python/python.exe main.py $run_path
|
5
main.py
5
main.py
|
@ -1,5 +0,0 @@
|
|||
import sys
|
||||
|
||||
run_path = sys.argv[1]
|
||||
|
||||
println(f"Run from {run_path}.")
|
|
@ -0,0 +1,251 @@
|
|||
use std::borrow::Cow;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::Context;
|
||||
|
||||
/// A sequential pipeline of [`Step`]s.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Pipeline<'a> {
|
||||
name: &'a str,
|
||||
steps: Vec<Step<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Pipeline<'a> {
|
||||
#[inline(always)]
|
||||
pub fn new(name: &'a str, steps: impl Into<Vec<Step<'a>>>) -> Self {
|
||||
Self { name, steps: steps.into() }
|
||||
}
|
||||
|
||||
pub async fn invoke(&self, ctx: &Context) -> Result<()> {
|
||||
println!("Invoking {}...", self.name);
|
||||
|
||||
for step in self.steps.iter() {
|
||||
step.invoke(ctx).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Step<'a> {
|
||||
DownloadFile {
|
||||
/// Remote resourcee to download from.
|
||||
res: RemoteResource<'a>,
|
||||
|
||||
/// Path to save the downloaded file to.
|
||||
file: Cow<'a, Path>,
|
||||
},
|
||||
|
||||
/// Extracts the file using the `tar` binary that ships with Windows.
|
||||
ExtractFile {
|
||||
file: Cow<'a, Path>,
|
||||
dest: Cow<'a, Path>,
|
||||
},
|
||||
|
||||
ExecuteCommand {
|
||||
/// Path to the executable file.
|
||||
file: Cow<'a, Path>,
|
||||
|
||||
args: &'a [&'a str],
|
||||
},
|
||||
|
||||
CreateShortcut {
|
||||
/// Target of the shortcut (i.e. what is points to).
|
||||
target: ShortcutTarget<'a>,
|
||||
|
||||
/// Optional icon of the shortcut.
|
||||
icon: Option<Cow<'a, Path>>,
|
||||
|
||||
/// Path of the created shortcut file.
|
||||
file: Cow<'a, Path>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> Step<'a> {
|
||||
pub async fn invoke(&self, ctx: &Context) -> Result<()> {
|
||||
match self {
|
||||
Self::DownloadFile { res, file } => {
|
||||
println!("Downloading file {file} from {res:?}...", file = file.to_str().unwrap_or("<NON UTF-8>"));
|
||||
const FETCH_FILE_ERROR_MSG: &'static str = "Fetching the remote resource failed.";
|
||||
const WRITE_FILE_ERROR_MSG: &'static str =
|
||||
"Writing the remote resource to disk failed.";
|
||||
let url = match res {
|
||||
RemoteResource::Url(url) => url::Url::parse(url)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Invalid url for download step.")?,
|
||||
RemoteResource::GitHubArtifact { repo, pattern } => {
|
||||
const FETCH_META_ERROR_MSG: &'static str =
|
||||
"Fetching the latest release metadata from GitHub failed.";
|
||||
let url = url::Url::parse(&format!(
|
||||
"https://api.github.com/repos/{repo}/releases/latest"
|
||||
))
|
||||
.into_diagnostic()
|
||||
.wrap_err("Invalid GitHub repo for download step.")?;
|
||||
let mut release: GitHubRelease = ctx.reqwest.get(url)
|
||||
.send()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err(FETCH_META_ERROR_MSG)?
|
||||
.json()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err(FETCH_META_ERROR_MSG)?;
|
||||
let pattern = ramhorns::Template::new(*pattern)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Invalid pattern for artifact matching in download step.")?;
|
||||
release.meta.tag_name_strip_prefix = release
|
||||
.meta
|
||||
.tag_name
|
||||
.strip_prefix('v')
|
||||
.unwrap_or(&release.meta.tag_name);
|
||||
let asset_name = pattern.render(&release.meta);
|
||||
let artifact = release.assets.into_iter().filter(move |asset| asset.name == asset_name).next().ok_or_else(|| miette!("No artifact of the latest release matched the pattern in download step."))?;
|
||||
url::Url::parse(&artifact.browser_download_url)
|
||||
.into_diagnostic()
|
||||
.wrap_err(
|
||||
"Invalid url returned by GitHub for latest release artifact.",
|
||||
)?
|
||||
}
|
||||
};
|
||||
let mut resp = ctx.reqwest.get(url)
|
||||
.send()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err(FETCH_FILE_ERROR_MSG)?;
|
||||
let _content_length = resp.content_length();
|
||||
mkdir_all(file.parent().ok_or_else(|| miette!("Destination file for download step has no parent."))?).await?;
|
||||
let mut writer = tokio::io::BufWriter::new(
|
||||
tokio::fs::File::create(file)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err(WRITE_FILE_ERROR_MSG)?,
|
||||
);
|
||||
while let Some(mut chunk) = resp
|
||||
.chunk()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err(FETCH_FILE_ERROR_MSG)?
|
||||
{
|
||||
writer
|
||||
.write_all_buf(&mut chunk)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err(WRITE_FILE_ERROR_MSG)?;
|
||||
}
|
||||
writer
|
||||
.flush()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err(WRITE_FILE_ERROR_MSG)?;
|
||||
}
|
||||
Self::ExtractFile { file, dest } => {
|
||||
mkdir_all(dest).await?;
|
||||
todo!();
|
||||
}
|
||||
Self::ExecuteCommand { file, args } => {
|
||||
todo!();
|
||||
}
|
||||
Self::CreateShortcut { target, icon, file } => {
|
||||
mkdir_all(file.parent().ok_or_else(|| miette!("Destination file for shortcut step has no parent."))?).await?;
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
println!("-> Done.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RemoteResource<'a> {
|
||||
// I'm not using url::Url here because it makes it impossible to make this const.
|
||||
Url(&'a str),
|
||||
|
||||
GitHubArtifact {
|
||||
/// In the form `org/repo`
|
||||
repo: &'a str,
|
||||
|
||||
/// Artifact name pattern.
|
||||
///
|
||||
/// Any of the fields in [`GitHubReleaseMeta`] may be injected via handlebar syntax:
|
||||
/// `{{field_name}}`.
|
||||
/// with `v` prefix stripped if present.
|
||||
pattern: &'a str,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ShortcutTarget<'a> {
|
||||
/// An executable shortcut.
|
||||
Executable {
|
||||
/// The executable the shortcut should open.
|
||||
file: Cow<'a, Path>,
|
||||
|
||||
/// Arguments to the executable.
|
||||
arg: &'a [&'a str],
|
||||
},
|
||||
|
||||
/// A file or folder shortcut. Please use [`Self::Executable`] for shortcuts to binaries.
|
||||
Path { path: Cow<'a, Path> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct GitHubRelease<'a> {
|
||||
#[serde(flatten)]
|
||||
meta: GitHubReleaseMeta<'a>,
|
||||
//reactions: todo!(),
|
||||
assets: Vec<GitHubReleaseAsset>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ramhorns::Content)]
|
||||
struct GitHubReleaseMeta<'a> {
|
||||
url: String,
|
||||
assets_url: String,
|
||||
upload_url: String,
|
||||
html_url: String,
|
||||
id: u64,
|
||||
//author: todo!(),
|
||||
node_id: String,
|
||||
tag_name: String,
|
||||
#[serde(skip_deserializing, default = "empty_str")]
|
||||
tag_name_strip_prefix: &'a str,
|
||||
target_commitish: String,
|
||||
name: String,
|
||||
draft: bool,
|
||||
prerelease: bool,
|
||||
created_at: String,
|
||||
published_at: String,
|
||||
tarball_url: String,
|
||||
zipball_url: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct GitHubReleaseAsset {
|
||||
url: String,
|
||||
id: u64,
|
||||
node_id: String,
|
||||
name: String,
|
||||
label: Option<String>,
|
||||
//uploader: todo!(),
|
||||
content_type: String,
|
||||
state: String,
|
||||
size: u64,
|
||||
download_count: u64,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
const fn empty_str<'a>() -> &'a str {
|
||||
""
|
||||
}
|
||||
|
||||
async fn mkdir_all(path: impl AsRef<Path>) -> Result<()> {
|
||||
tokio::fs::DirBuilder::new().mode(0o775).recursive(true).create(path).await.into_diagnostic().wrap_err("Creating directory and any missing parents failed.")
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
#[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, Step};
|
||||
|
||||
const SWTOOLS_PATH: &'static str = "C:\\SWTools";
|
||||
|
||||
pub fn swtools_path() -> &'static Path {
|
||||
Path::new(SWTOOLS_PATH)
|
||||
}
|
||||
|
||||
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 utilities = [Pipeline::new(
|
||||
"Install Notepad++",
|
||||
vec![
|
||||
Step::DownloadFile {
|
||||
file: swtools_path()
|
||||
.join("temp")
|
||||
.join("notepad-plus-plus.zip")
|
||||
.into(),
|
||||
res: RemoteResource::GitHubArtifact {
|
||||
repo: "notepad-plus-plus/notepad-plus-plus",
|
||||
pattern: "npp.{{tag_name_strip_prefix}}.portable.x64.zip",
|
||||
},
|
||||
},
|
||||
],
|
||||
)];
|
||||
|
||||
let ctx = Context {
|
||||
reqwest: reqwest::Client::builder().build().into_diagnostic().wrap_err("Could not initialize HTTP client.")?,
|
||||
};
|
||||
|
||||
for utility in utilities {
|
||||
utility.invoke(&ctx).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue