diff --git a/src/actions/builder/builder.rs b/src/actions/builder/builder.rs index b570b04a5..df64a039e 100644 --- a/src/actions/builder/builder.rs +++ b/src/actions/builder/builder.rs @@ -1,168 +1,224 @@ -// this was written in 60m -use std::{env, fs::{self, File}, io::{Write, Read}, path::Path, process::{Command, exit}}; -use std::collections::HashSet; +use anyhow::{anyhow, bail, Context, Result}; +use std::{ + collections::HashSet, + env, + fs::{self, File}, + io, + path::{Path, PathBuf}, + process::Command, +}; use walkdir::WalkDir; use zip::write::SimpleFileOptions; -fn main() { - let args: Vec = env::args().collect(); - let short_sha = args.get(1).map(|s| s.as_str()).unwrap_or("unknown"); +#[derive(Debug, Clone, Copy)] +enum Platform { + Modrinth, + Curseforge, +} +impl Platform { + fn from_suffix(s: &str) -> Option { + match s { + "mr" => Some(Platform::Modrinth), + "cf" => Some(Platform::Curseforge), + _ => None, + } + } + + fn short(self) -> &'static str { + match self { + Platform::Modrinth => "mr", + Platform::Curseforge => "cf", + } + } + + fn cli(self) -> &'static str { + match self { + Platform::Modrinth => "modrinth", + Platform::Curseforge => "curseforge", + } + } + + fn ext(self) -> &'static str { + match self { + Platform::Modrinth => "mrpack", + Platform::Curseforge => "zip", + } + } +} + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); + let short_sha = args + .get(1) + .ok_or_else(|| anyhow!("usage: builder "))? + .clone(); + + let repo_root = env::current_dir().context("failed to get current directory")?; + let artifacts_dir = repo_root.join("artifacts"); + fs::create_dir_all(&artifacts_dir) + .with_context(|| format!("failed to create {}", artifacts_dir.display()))?; + + let changed = detect_changed_targets().context("failed to detect changed targets")?; + + if changed.is_empty() { + println!("no packs detected in git diff."); + return Ok(()); + } + + for (category, pack_id) in &changed { + match category.as_str() { + "modpacks" => build_modpack(pack_id, &short_sha, &artifacts_dir) + .with_context(|| format!("modpack '{pack_id}' failed"))?, + "datapacks" => build_datapack(pack_id, &short_sha, &artifacts_dir) + .with_context(|| format!("datapack '{pack_id}' failed"))?, + other => println!("category '{other}' does not require a build."), + } + } + + println!("all builds completed successfully."); + Ok(()) +} + +fn detect_changed_targets() -> Result> { println!("detecting changed files..."); let output = Command::new("git") .args(["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"]) .output() - .expect("Failed to get git diff"); + .context("failed to invoke git")?; + + if !output.status.success() { + bail!( + "git diff-tree failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } let stdout = String::from_utf8_lossy(&output.stdout); - - let mut changed_targets = HashSet::new(); + let mut targets = HashSet::new(); for line in stdout.lines() { - if line.starts_with("external/") || line.starts_with(".actions/") || line.is_empty() { + if line.is_empty() || line.starts_with("external/") || line.starts_with(".actions/") { continue; } - - let parts: Vec<&str> = line.split('/').collect(); - if parts.len() >= 2 { - changed_targets.insert((parts[0], parts[1])); + let mut parts = line.splitn(3, '/'); + if let (Some(cat), Some(pack)) = (parts.next(), parts.next()) { + targets.insert((cat.to_string(), pack.to_string())); } } - if changed_targets.is_empty() { - println!(no packs detected in git diff."); - return; - } - - let _ = fs::create_dir_all("artifacts"); - let mut all_success = true; - - for (category, pack_id) in changed_targets { - match category { - "modpacks" => { - if !build_modpack(pack_id, short_sha) { all_success = false; } - }, - "resourcepacks" => build_resource_pack(pack_id, short_sha), - "datapacks" => build_datapack(pack_id, short_sha), - _ => println!("category '{}' does not require a build.", category), - } - } - - if !all_success { - eprintln!("one or more builds failed."); - exit(1); - } else { - println!("all builds completed successfully."); - } + Ok(targets) } -fn build_modpack(pack_id: &str, sha: &str) -> bool { - println!("building modpack: {}", pack_id); - let base_path = format!("modpacks/{}", pack_id); +fn build_modpack(pack_id: &str, sha: &str, artifacts_dir: &Path) -> Result<()> { + println!("building modpack: {pack_id}"); + let pack_dir = PathBuf::from("modpacks").join(pack_id); + + let manifest_path = pack_dir.join("manifest.json"); + let manifest: serde_json::Value = { + let file = File::open(&manifest_path) + .with_context(|| format!("failed to open {}", manifest_path.display()))?; + serde_json::from_reader(file) + .with_context(|| format!("invalid JSON in {}", manifest_path.display()))? + }; + let p_ver = manifest["version"] + .as_str() + .ok_or_else(|| anyhow!("missing 'version' in {}", manifest_path.display()))?; + let mut built_something = false; - for entry in WalkDir::new(&base_path).into_iter().filter_map(|e| e.ok()) { - if entry.file_name() == "manifest.json" { - let manifest_path = entry.path(); - let p_dir = manifest_path.parent().unwrap(); + let entries = fs::read_dir(&pack_dir) + .with_context(|| format!("failed to read {}", pack_dir.display()))?; - let file = match File::open(manifest_path) { - Ok(f) => f, - Err(e) => { - eprintln!("hailed to open {:?}: {}", manifest_path, e); - continue; - } - }; - - let json: serde_json::Value = match serde_json::from_reader(file) { - Ok(v) => v, - Err(e) => { - eprintln!("invalid JSON in {:?}: {}", manifest_path, e); - continue; - } - }; - - let mc_ver = json["mc_version"].as_str().unwrap_or("1.20.1"); - let p_ver = json["version"].as_str().unwrap_or("1.0.0"); - - for p in ["mr", "cf"] { - let target_subdir = format!("{}-{}", mc_ver, p); - let target_path = p_dir.join(&target_subdir); - - if target_path.exists() { - built_something = true; - let ext = if p == "mr" { "mrpack" } else { "zip" }; - let platform = if p == "mr" { "modrinth" } else { "curseforge" }; - let output_name = format!("{}-{}-{}-{}-{}.{}", pack_id, mc_ver, p, p_ver, sha, ext); - - let _ = Command::new("packwiz").args(["refresh", "-y"]).current_dir(&target_path).status(); - - let export = Command::new("packwiz") - .args([platform, "export", "--output", &format!("../../../artifacts/{}", output_name)]) - .current_dir(&target_path) - .status(); - - match export { - Ok(s) if !s.success() => { - eprintln!("packwiz export failed for {}", target_subdir); - return false; - }, - Err(e) => { - eprintln!("failed to launch packwiz: {}", e); - return false; - }, - _ => println!("exported {}", output_name), - } - } - } - } - } - - if !built_something { - println!("found no valid targets (mr/cf) or manifest for {}", pack_id); - } - - true -} - -fn build_resource_pack(pack_id: &str, sha: &str) { - println!("building resource pack: {}", pack_id); - let src = format!("resourcepacks/{}", pack_id); - let dest = format!("artifacts/{}-{}.zip", pack_id, sha); - - let status = Command::new("packsquash") - .args(["packsquash.toml", "--pack-directory", &src, "--output-file-path", &dest]) - .status() - .expect("Failed to execute PackSquash"); - - if !status.success() { exit(1); } -} - -fn build_datapack(pack_id: &str, sha: &str) { - println!("zipping datapack: {}", pack_id); - let src = format!("datapacks/{}", pack_id); - let dest = format!("artifacts/{}-{}.zip", pack_id, sha); - if let Err(e) = zip_dir(&src, &dest) { - eprintln!("❌ Failed to zip datapack {}: {}", pack_id, e); - exit(1); - } -} - -fn zip_dir(src: &str, dest: &str) -> zip::result::ZipResult<()> { - let file = File::create(dest)?; - let mut zip = zip::ZipWriter::new(file); - let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); - - for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) { + for entry in entries { + let entry = entry.context("failed to read directory entry")?; let path = entry.path(); - let name = path.strip_prefix(Path::new(src)).unwrap(); + if !path.is_dir() { + continue; + } + + let Some(dir_name) = path.file_name().and_then(|s| s.to_str()) else { + continue; + }; + + let Some((mc_ver, suffix)) = dir_name.rsplit_once('-') else { + continue; + }; + let Some(platform) = Platform::from_suffix(suffix) else { + continue; + }; + + built_something = true; + let output_name = format!( + "{}-{}-{}-{}-{}.{}", + pack_id, + mc_ver, + platform.short(), + p_ver, + sha, + platform.ext() + ); + let output_path = artifacts_dir.join(&output_name); + + let refresh = Command::new("packwiz") + .args(["refresh", "-y"]) + .current_dir(&path) + .status() + .context("failed to invoke packwiz refresh")?; + if !refresh.success() { + bail!("packwiz refresh failed in {}", path.display()); + } + + let export = Command::new("packwiz") + .args([ + platform.cli(), + "export", + "--output", + output_path + .to_str() + .ok_or_else(|| anyhow!("non-UTF8 output path"))?, + ]) + .current_dir(&path) + .status() + .context("failed to invoke packwiz export")?; + if !export.success() { + bail!("packwiz export failed for {dir_name}"); + } + + println!("exported {output_name}"); + } + + if !built_something { + bail!("no valid version dirs (expected '{{mc_ver}}-mr' or '{{mc_ver}}-cf') for {pack_id}"); + } + Ok(()) +} + +fn build_datapack(pack_id: &str, sha: &str, artifacts_dir: &Path) -> Result<()> { + println!("zipping datapack: {pack_id}"); + let src = PathBuf::from("datapacks").join(pack_id); + let dest = artifacts_dir.join(format!("{pack_id}-{sha}.zip")); + zip_dir(&src, &dest).with_context(|| format!("failed to zip {}", src.display())) +} + +fn zip_dir(src: &Path, dest: &Path) -> Result<()> { + let file = File::create(dest) + .with_context(|| format!("failed to create {}", dest.display()))?; + let mut zip = zip::ZipWriter::new(file); + let options = + SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); + + for entry in WalkDir::new(src) { + let entry = entry.context("walkdir error")?; + let path = entry.path(); + let name = path.strip_prefix(src).context("strip_prefix failed")?; if path.is_file() { zip.start_file(name.to_string_lossy(), options)?; - let mut f = File::open(path)?; - let mut buffer = Vec::new(); - f.read_to_end(&mut buffer)?; - zip.write_all(&buffer)?; + let mut f = File::open(path) + .with_context(|| format!("failed to open {}", path.display()))?; + io::copy(&mut f, &mut zip) + .with_context(|| format!("failed to write {} to zip", path.display()))?; } else if !name.as_os_str().is_empty() { zip.add_directory(name.to_string_lossy(), options)?; } @@ -170,4 +226,3 @@ fn zip_dir(src: &str, dest: &str) -> zip::result::ZipResult<()> { zip.finish()?; Ok(()) } -// say wallahi bro make this shit work! \ No newline at end of file