Initial commit
This commit is contained in:
@@ -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 }}
|
||||
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
run/
|
||||
config.json
|
||||
Cargo.lock
|
||||
+17
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod config;
|
||||
pub mod downloader;
|
||||
+35
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user