mirror of
https://github.com/Nostalgica-Reverie/Content-Monorepo.git
synced 2026-05-09 00:24:15 +00:00
chore(ci): refactor build to be better
This commit is contained in:
@@ -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!
|
||||
Reference in New Issue
Block a user