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