commit 7e354c2e04216166bb2c1c8cae83b65f7b0d6e48 Author: Shiewk Date: Sun Apr 19 17:16:29 2026 +0200 Initial commit diff --git a/.gitea/workflows/test-release.yml b/.gitea/workflows/test-release.yml new file mode 100644 index 0000000..b5f66a3 --- /dev/null +++ b/.gitea/workflows/test-release.yml @@ -0,0 +1,87 @@ +name: Test and release + +on: + push: + branches: + - '**' + tags: + - 'v*' + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Run cargo test + run: cargo test --verbose + + build-and-release: + name: Build and Release + needs: test + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + permissions: + releases: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate Changelog + id: changelog + run: | + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + + if [ -z "$PREVIOUS_TAG" ]; then + echo "No previous tag found. Generating log from beginning of history." + git log --pretty=format:"* %s (%h)" | tee -a changelog.md + else + echo "Generating changes since $PREVIOUS_TAG" + git log "$PREVIOUS_TAG..HEAD" --pretty=format:"* %s (%h)" | tee -a changelog.md + fi + + echo "CHANGELOG_FILE=changelog.md" >> $GITHUB_ENV + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: "x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu" + + - name: Install AArch64 linker (for Arm64 cross-compilation) + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Build Release Binaries + env: + # Tell Cargo to use the AArch64 linker when building for Arm64 + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + run: | + cargo build --release --target x86_64-unknown-linux-gnu + cargo build --release --target aarch64-unknown-linux-gnu + + - name: Prepare binaries for release + run: | + BIN_NAME="shy-launcher" + + mv target/x86_64-unknown-linux-gnu/release/$BIN_NAME $BIN_NAME-x86_64-unknown-linux-gnu + mv target/aarch64-unknown-linux-gnu/release/$BIN_NAME $BIN_NAME-aarch64-unknown-linux-gnu + + - name: Create / Update Release and Upload Assets + uses: softprops/action-gh-release@v2 + with: + body_path: ${{ env.CHANGELOG_FILE }} + files: | + shy-launcher-aarch64-unknown-linux-gnu + shy-launcher-x86_64-unknown-linux-gnu + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..054b1b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +run/ +config.json +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e41db88 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "shy-launcher" +version = "1.0.0" +edition = "2024" + +[profile.release] +lto = "fat" +strip = "symbols" + +[dependencies] +tokio = { version = "1.52.1", features = ["full"] } +reqwest = { version = "0.13", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +libc = "0.2" +sha2 = "0.11.0" +bytes = "1.11.0" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a67d60 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# shy-launcher +Utility for launching apps for Linux. \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..cf8cabc --- /dev/null +++ b/src/config.rs @@ -0,0 +1,72 @@ +use std::{collections::HashMap, error::Error, fs::File}; + +use serde::{Deserialize, Serialize}; +use serde_json::Deserializer; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Download { + method: String, + url: String, + headers: HashMap, + filename: String, + permissions: u32 +} + +impl Download { + + pub fn method(&self) -> &String { + &self.method + } + + pub fn url(&self) -> &String { + &self.url + } + + pub fn headers(&self) -> &HashMap { + &self.headers + } + + pub fn filename(&self) -> &String { + &self.filename + } + + pub fn permissions(&self) -> u32 { + self.permissions % 10 + + ((self.permissions / 10) % 10) * 8 + + ((self.permissions / 100) % 10) * 64 + } + +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Config { + name: String, + run: String, + args: Vec, + downloads: Vec +} + +impl Config { + pub fn load(filepath: &str) -> Result> { + let file = File::open(filepath)?; + let mut deserializer = Deserializer::from_reader(file); + + Ok(Config::deserialize(&mut deserializer)?) + } + + pub fn name(&self) -> &String { + &self.name + } + + pub fn run(&self) -> &String { + &self.run + } + + pub fn args(&self) -> &Vec { + &self.args + } + + pub fn downloads(&self) -> &Vec { + &self.downloads + } +} diff --git a/src/downloader.rs b/src/downloader.rs new file mode 100644 index 0000000..d33cab9 --- /dev/null +++ b/src/downloader.rs @@ -0,0 +1,139 @@ +use std::{error::Error, fs::{self, File}, io::Read, os::unix::fs::PermissionsExt, path::Path, time::Duration}; + +use bytes::Bytes; +use reqwest::{Client, Method, RequestBuilder, Response, redirect::{Action, Attempt, Policy}}; +use sha2::{Digest, Sha256}; + +use crate::config::Download; + +fn get_file_hash(path: &Path) -> Result<[u8; 32], std::io::Error> { + let mut file = File::open(path)?; + let mut digest = Sha256::new(); + let mut buf= [0x00u8; 4096]; + loop { + let amt = file.read(&mut buf)?; + if amt == 0 { break; } + digest.update(&buf[..amt]); + } + Ok(digest.finalize().0) +} + +fn handle_downloaded_file(download: &Download, bytes: Bytes) -> Result<(), Box> { + let path = Path::new(download.filename()); + let digest_download = Sha256::digest(&bytes).0; + let mut should_update = true; + println!("Hash of downloaded file is {:?}", digest_download); + + if fs::exists(path)? { + let digest_file = get_file_hash(path)?; + println!("Hash of existing file is {:?}", digest_file); + if digest_file == digest_download { + println!("Hashes match, not updating."); + should_update = false; + } + } + + if should_update { + println!("Updating file on disk..."); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(download.filename(), &bytes)?; + } + + let meta = fs::metadata(path)?; + let current_perms = meta.permissions().mode() % 512; + + if current_perms != download.permissions() { + println!("Updating file perms {:o} to {:o}", current_perms, download.permissions()); + fs::set_permissions(path, PermissionsExt::from_mode(download.permissions()))?; + } + + Ok(()) +} + +fn handle_redirect(att: Attempt) -> Action { + if att.previous().len() < 5 { + println!("{} [redirect]", att.url()); + att.follow() + } else { + att.stop() + } +} + +pub async fn download(downloads: &Vec){ + let client: Client = Client::builder() + .timeout(Duration::from_secs(10)) + .redirect(Policy::custom(handle_redirect)) + .build() + .expect("Should build correctly"); + + let dl_count = downloads.len(); + + for (i, download) in downloads.iter().enumerate() { + println!("---------- [{}/{}] ----------", i+1, dl_count); + if let Ok(response) = make_request(&client, &download).await { + println!("Successfully fetched: {}", response.status()); + if response.status().as_u16() > 299 { + eprintln!("Status code not successful, skipping file."); + } else { + let bytes = response.bytes().await; + match bytes { + Err(e) => { + eprintln!("Getting the bytes did not succeed: {}", e); + }, + Ok(bytes) => { + if let Err(e) = handle_downloaded_file(download, bytes) { + eprintln!("Could not process download: {}", e); + }; + } + }; + } + } // ignored if err, error printed to stderr anyway + } +} + +async fn make_request(client: &Client, download: &Download) -> Result> { + let mut tries: u8 = 0; + loop { + tries += 1; + println!("{} [attempt {}/{}...]", download.url(), tries, 5); + match prepare_request(&client, &download) { + Ok(request) => { + let response = request.send().await; + match response { + Err(e) => { + eprintln!("Failed: {}", e); + if tries >= 5 { + return Err(Box::new(e)); + } else { + continue; + } + }, + Ok(response) => { + return Ok(response); + } + } + }, + Err(e) => { + eprintln!("Failed to build request, not downloading: {}", e); + return Err(e); + } + } + } +} + +fn prepare_request(client: &Client, download: &Download) -> Result> { + + let method = Method::from_bytes(download.method().as_bytes())?; + + let mut builder = client.request(method, download.url()); + + builder = builder.header("User-Agent", "shy-launcher"); + + for (k, v) in download.headers() { + builder = builder.header(k, v); + } + + Ok(builder) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..db00238 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod downloader; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2ae911c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,35 @@ +use std::{error::Error, process::Command}; + +use shy_launcher::{config::Config, downloader}; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { + println!("Starting app launcher..."); + let config = Config::load("config.json")?; + + println!("Starting download..."); + downloader::download(config.downloads()).await; + + println!("Freeing memory..."); + if unsafe { libc::malloc_trim(0) } == 1 { + println!("Freed up some memory."); + } + + println!("Launching app..."); + println!(); + println!("--------------------------"); + println!(); + + let mut child = Command::new(config.run()) + .args(config.args()) + .spawn()?; + + let exit = child.wait()?; + + println!(); + println!("--------------------------"); + println!(); + println!("Child exited: {exit}"); + + Ok(()) +} \ No newline at end of file