Initial commit
Test and release / Run Tests (push) Successful in 2m58s
Test and release / Build and Release (push) Successful in 4m36s

This commit is contained in:
Shy
2026-04-19 17:16:29 +02:00
commit 7e354c2e04
8 changed files with 358 additions and 0 deletions
+87
View File
@@ -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 }}
+4
View File
@@ -0,0 +1,4 @@
/target
run/
config.json
Cargo.lock
+17
View File
@@ -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"
+2
View File
@@ -0,0 +1,2 @@
# shy-launcher
Utility for launching apps for Linux.
+72
View File
@@ -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<String, String>,
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<String, String> {
&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<String>,
downloads: Vec<Download>
}
impl Config {
pub fn load(filepath: &str) -> Result<Config, Box<dyn Error>> {
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<String> {
&self.args
}
pub fn downloads(&self) -> &Vec<Download> {
&self.downloads
}
}
+139
View File
@@ -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<dyn Error>> {
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<Download>){
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<Response, Box<dyn Error>> {
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<RequestBuilder, Box<dyn Error>> {
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)
}
+2
View File
@@ -0,0 +1,2 @@
pub mod config;
pub mod downloader;
+35
View File
@@ -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<dyn Error>> {
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(())
}