diff --git a/.forgejo/workflows/auto-update.yml b/.forgejo/workflows/auto-update.yml index 168a3d823..a93336e23 100644 --- a/.forgejo/workflows/auto-update.yml +++ b/.forgejo/workflows/auto-update.yml @@ -11,56 +11,68 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 - with: + with: token: ${{ secrets.FORGEJO_TOKEN }} fetch-depth: 1 + filter: blob:none sparse-checkout: | modpacks - src/actions + src/actions/updater - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 'stable' - cache: true - - - name: Cache Packwiz Binaries - id: cache-tooling - uses: actions/cache@v4 - with: - path: $HOME/go/bin - key: tooling-${{ runner.os }}-packwiz - - - name: Install Tooling - if: steps.cache-tooling.outputs.cache-hit != 'true' - run: | - mkdir -p $HOME/go/bin - go install github.com/packwiz/packwiz@latest - go install github.com/Merith-TK/packwiz-wrapper/cmd/pw@main - - - name: Add Path - run: echo "$HOME/go/bin" >> $GITHUB_PATH - - - name: Cache Updater + - name: Cache Updater Binary id: cache-updater uses: actions/cache@v4 with: path: ./updater-bin - key: updater-v3-${{ runner.os }}-${{ hashFiles('src/actions/updater/**') }} + key: updater-v4-${{ runner.os }}-${{ hashFiles('src/actions/updater/**/*.rs', 'src/actions/updater/Cargo.toml', 'src/actions/updater/Cargo.lock') }} - - name: Rust Cache + - name: Install Rust if: steps.cache-updater.outputs.cache-hit != 'true' - uses: Swatinem/rust-cache@v2 + uses: https://github.com/dtolnay/rust-toolchain@stable + + - name: Rust Cache (Compiler Internals) + if: steps.cache-updater.outputs.cache-hit != 'true' + uses: https://github.com/Swatinem/rust-cache@v2 with: workspaces: "src/actions/updater -> target" - name: Build Updater if: steps.cache-updater.outputs.cache-hit != 'true' run: | - cargo build --release --manifest-path src/actions/updater/Cargo.toml + cargo build --release --manifest-path src/actions/updater/Cargo.toml --bin updater mkdir -p ./updater-bin cp src/actions/updater/target/release/updater ./updater-bin/updater + - name: Cache Packwiz Binary + id: cache-go + uses: actions/cache@v4 + with: + path: ~/go/bin + key: go-bin-packwiz-v1-${{ runner.os }} + + - name: Setup Go + if: steps.cache-go.outputs.cache-hit != 'true' + uses: https://github.com/actions/setup-go@v5 + with: + go-version: 'stable' + cache: true + + - name: Install Packwiz + if: steps.cache-go.outputs.cache-hit != 'true' + run: go install github.com/packwiz/packwiz@latest + + - name: Add Go bin to PATH + run: echo "$HOME/go/bin" >> $GITHUB_PATH + + + - name: Cache Packwiz Downloads + uses: actions/cache@v4 + with: + path: ~/.cache/packwiz + key: packwiz-cache-${{ runner.os }}-${{ github.run_id }} + restore-keys: | + packwiz-cache-${{ runner.os }}- + - name: Run Updater id: rust-update continue-on-error: true @@ -68,10 +80,10 @@ jobs: chmod +x ./updater-bin/updater ./updater-bin/updater - - name: Run Shell Updater + - name: Run Shell Fallback if: steps.rust-update.outcome == 'failure' run: | - echo "Rust Updater failed. Falling back to Shell..." + echo "Rust updater failed, falling back to shell..." chmod +x ./modpacks/update-refresh.sh ./modpacks/update-refresh.sh diff --git a/modpacks/update-refresh.sh b/modpacks/update-refresh.sh index f7c09d965..6e421e169 100644 --- a/modpacks/update-refresh.sh +++ b/modpacks/update-refresh.sh @@ -1,8 +1,41 @@ #!/bin/bash -echo Updating -(cd ./modpacks/simply && pw batch update -a -y && pw batch refresh -y) & -(cd ./modpacks/rc-plus && pw batch update -a -y && pw batch refresh -y) & -(cd ./modpacks/2k && pw batch update -a -y && pw batch refresh -y) & -(cd ./modpacks/rekindled && pw batch update -a -y && pw batch refresh -y) & -wait -echo Done \ No newline at end of file +set -eu + +PACKS=("simply" "rc-plus" "2k" "rekindled") + +echo "Updating..." + +pids=() +for pack in "${PACKS[@]}"; do + pack_dir="modpacks/$pack" + if [ ! -d "$pack_dir" ]; then + echo "warning: $pack_dir missing, skipping" + continue + fi + + for subdir in "$pack_dir"/*-mr "$pack_dir"/*-cf; do + [ -d "$subdir" ] || continue + ( + echo "[$subdir] updating" + if (cd "$subdir" && packwiz update -a -y); then + echo "[$subdir] ok" + else + echo "[$subdir] FAIL" >&2 + exit 1 + fi + ) & + pids+=($!) + done +done + +fail=0 +for pid in "${pids[@]}"; do + wait "$pid" || fail=$((fail + 1)) +done + +if [ "$fail" -gt 0 ]; then + echo "$fail subdir(s) failed" >&2 + exit 1 +fi + +echo "Done" diff --git a/src/actions/updater/Cargo.toml b/src/actions/updater/Cargo.toml index f14fb11a8..5e23b8813 100644 --- a/src/actions/updater/Cargo.toml +++ b/src/actions/updater/Cargo.toml @@ -3,6 +3,9 @@ name = "updater" version = "26.4.0" edition = "2024" +[dependencies] +anyhow = "1.0" + [[bin]] name = "updater" -path = "updater.rs" \ No newline at end of file +path = "updater.rs" diff --git a/src/actions/updater/updater.rs b/src/actions/updater/updater.rs index d9e3c25b5..5499bf742 100644 --- a/src/actions/updater/updater.rs +++ b/src/actions/updater/updater.rs @@ -1,87 +1,121 @@ -use std::process::Command; -use std::sync::{Arc, Mutex}; -use std::thread; +use anyhow::{bail, Context, Result}; +use std::{ + fs, + path::PathBuf, + process::Command, + sync::{Arc, Mutex}, + thread, +}; -fn main() { - let modpacks = vec!["simply", "rc-plus", "2k", "rekindled"]; - let max_concurrent = 4; - - let modpacks_queue = Arc::new(Mutex::new(modpacks.into_iter())); - let errors = Arc::new(Mutex::new(Vec::new())); - let mut workers = vec![]; +const PACKS: &[&str] = &["simply", "rc-plus", "2k", "rekindled"]; +const MAX_CONCURRENT: usize = 8; - println!("starting throttled parallel updates ({} at a time)...", max_concurrent); +fn main() -> Result<()> { + let mut jobs: Vec = Vec::new(); + for pack in PACKS { + let pack_dir = PathBuf::from("modpacks").join(pack); + if !pack_dir.exists() { + eprintln!("warning: pack directory missing: {}", pack_dir.display()); + continue; + } - for i in 0..max_concurrent { - let queue_clone = Arc::clone(&modpacks_queue); - let err_clone = Arc::clone(&errors); + let entries = fs::read_dir(&pack_dir) + .with_context(|| format!("failed to read {}", pack_dir.display()))?; - let handle = thread::spawn(move || { + for entry in entries { + let entry = entry.context("failed to read directory entry")?; + let path = entry.path(); + if !path.is_dir() { + continue; + } + let Some(name) = path.file_name().and_then(|s| s.to_str()) else { + continue; + }; + if name.ends_with("-mr") || name.ends_with("-cf") { + jobs.push(path); + } + } + } + + if jobs.is_empty() { + println!("no packs to update."); + return Ok(()); + } + + println!( + "queued {} subdir(s) across {} pack(s), running up to {} in parallel", + jobs.len(), + PACKS.len(), + MAX_CONCURRENT, + ); + + let jobs = Arc::new(Mutex::new(jobs.into_iter())); + let failures: Arc>> = Arc::new(Mutex::new(Vec::new())); + + let mut handles = Vec::new(); + for worker_id in 0..MAX_CONCURRENT { + let jobs = Arc::clone(&jobs); + let failures = Arc::clone(&failures); + + handles.push(thread::spawn(move || { loop { - let pack = { - let mut queue = queue_clone.lock().unwrap(); - queue.next() - }; + let job = { jobs.lock().unwrap().next() }; + let Some(path) = job else { break }; - let pack = match pack { - Some(p) => p, - None => break, - }; + let label = path.display().to_string(); + println!("[W{worker_id}] updating {label}"); - let path = format!("modpacks/{}", pack); - println!("[Worker {}] starting: {}", i, pack); - - if !std::path::Path::new(&path).exists() { - let mut e = err_clone.lock().unwrap(); - e.push(format!("directory missing: {}", path)); - continue; - } - - let refresh = Command::new("pw") - .args(["batch", "refresh", "-y"]) + let output = Command::new("packwiz") + .args(["update", "-a", "-y"]) .current_dir(&path) - .status(); + .output(); - let update = Command::new("pw") - .args(["batch", "update", "-a", "-y"]) - .current_dir(&path) - .status(); - - let failed = match (refresh, update) { - (Ok(s1), Ok(s2)) => !s1.success() || !s2.success(), - (Err(e), _) => { - println!("[Worker {}] refresh failed for {}: {}", i, pack, e); - true - }, - (_, Err(e)) => { - println!("[Worker {}] update failed for {}: {}", i, pack, e); - true + match output { + Ok(o) if o.status.success() => { + println!("[W{worker_id}] ok: {label}"); + } + Ok(o) => { + let stderr = String::from_utf8_lossy(&o.stderr).into_owned(); + let stdout = String::from_utf8_lossy(&o.stdout).into_owned(); + eprintln!("[W{worker_id}] FAIL {label} (exit {})", o.status); + if !stdout.is_empty() { + eprintln!(" stdout:\n{}", indent(&stdout, " ")); + } + if !stderr.is_empty() { + eprintln!(" stderr:\n{}", indent(&stderr, " ")); + } + let reason = if !stderr.is_empty() { stderr } else { stdout }; + failures.lock().unwrap().push((path, reason)); + } + Err(e) => { + eprintln!("[W{worker_id}] FAIL {label}: could not launch packwiz: {e}"); + failures.lock().unwrap().push((path, e.to_string())); } - }; - - if failed { - let mut e = err_clone.lock().unwrap(); - e.push(format!("failed: {}", pack)); - } else { - println!("[Worker {}] done: {}", i, pack); } } - }); - workers.push(handle); + })); } - for handle in workers { - handle.join().unwrap(); + for h in handles { + h.join().expect("worker thread panicked"); } - let final_errors = errors.lock().unwrap(); - if !final_errors.is_empty() { - eprintln!("\nsummary of failures"); - for err in final_errors.iter() { - eprintln!("{}", err); - } - std::process::exit(1); - } else { + let failures = failures.lock().unwrap(); + if failures.is_empty() { println!("\nall updates finished successfully."); + Ok(()) + } else { + eprintln!("\n{} subdir(s) failed:", failures.len()); + for (path, _reason) in failures.iter() { + eprintln!(" - {}", path.display()); + } + bail!("{} update(s) failed", failures.len()) } -} \ No newline at end of file +} + +fn indent(s: &str, prefix: &str) -> String { + s.lines() + .map(|l| format!("{prefix}{l}")) + .collect::>() + .join("\n") +}