chore(ci): refactor build to be better

This commit is contained in:
omo50
2026-04-18 13:37:16 -06:00
parent 06ad38d237
commit 40ae945290

View File

@@ -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<String> = 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<Self> {
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<String> = env::args().collect();
let short_sha = args
.get(1)
.ok_or_else(|| anyhow!("usage: builder <short-sha>"))?
.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<HashSet<(String, String)>> {
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!