use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::process::Command;
use yog_cli::manifest::YogToml;
use yog_cli::{bench, content};
use super::prepare::{Prepared, prepare};
use yog_ids::platform::{self, Platform};
use super::targets::{
Builder, filter_benign_warnings, install_targets, installed_targets, lib_filename,
selected_targets,
};
pub(super) fn check() -> Result<(), String> {
let Prepared {
root, build_dir, ..
} = prepare()?;
let status = Command::new("cargo")
.current_dir(&build_dir)
.env("CARGO_TARGET_DIR", root.join("target"))
.arg("check")
.status()
.map_err(|e| format!("failed to run cargo: {e}"))?;
if !status.success() {
return Err(format!("cargo check failed ({status})"));
}
eprintln!("==> check OK");
Ok(())
}
pub(super) fn run_benches(args: &[String]) -> Result<(), String> {
let Prepared {
root, build_dir, ..
} = prepare()?;
if bench::discover(&root)?.is_empty() {
return Err(format!(
"no benchmarks: add {}/<name>.rs with #[yog_api::bench] functions",
bench::BENCH_DIR
));
}
let status = Command::new("cargo")
.current_dir(&build_dir)
.env("CARGO_TARGET_DIR", root.join("target"))
.args(["bench", "--benches", "--"])
.args(args)
.status()
.map_err(|e| format!("failed to run cargo: {e}"))?;
if !status.success() {
return Err(format!("cargo bench failed ({status})"));
}
Ok(())
}
pub(super) fn designer_lib() -> Result<(), String> {
let Prepared {
root,
meta,
build_dir,
..
} = prepare()?;
let target_dir = root.join("target").join("designer");
let out = Command::new("cargo")
.current_dir(&build_dir)
.env("CARGO_TARGET_DIR", &target_dir)
.args(["build", "--lib", "--color", "never"])
.stdout(std::process::Stdio::null())
.output()
.map_err(|e| format!("failed to run cargo: {e}"))?;
eprint!(
"{}",
filter_benign_warnings(&String::from_utf8_lossy(&out.stderr))
);
if !out.status.success() {
return Err(format!("cargo build failed ({})", out.status));
}
let lib = target_dir
.join("debug")
.join(lib_filename(&meta.id, std::env::consts::OS));
if !lib.is_file() {
return Err(format!("built, but {} is missing", lib.display()));
}
println!("{}", lib.display());
Ok(())
}
pub(super) fn build(debugging_symbols: bool, dev: bool) -> Result<(YogToml, PathBuf), String> {
let Prepared {
root,
meta,
build_dir,
resources,
content,
} = prepare()?;
let bundled = build_natives(&root, &meta, &build_dir, debugging_symbols)?;
let new_lock = build_dir.join("Cargo.lock");
if new_lock.exists() {
std::fs::copy(&new_lock, root.join(yog_ids::file::LOCK)).map_err(|e| e.to_string())?;
}
let assets = assets(&root, resources, content, dev)?;
let artifacts = root.join(ARTIFACTS_DIR);
std::fs::create_dir_all(&artifacts).map_err(|e| e.to_string())?;
let out = artifacts.join(yog_ids::Format::Mod.file(&meta.id));
package(&out, &meta, &bundled, &assets)?;
let tags: Vec<&str> = bundled.iter().map(|(t, _)| t.as_str()).collect();
eprintln!("==> packaged {} [{}]", out.display(), tags.join(", "));
Ok((meta, out))
}
pub(super) const ARTIFACTS_DIR: &str = "artifacts";
fn wanted_targets() -> Vec<&'static Platform> {
let selected = selected_targets();
platform::ALL
.iter()
.filter(|t| {
selected
.as_ref()
.is_none_or(|sel| sel.iter().any(|s| s == t.tag || s == t.triple))
})
.collect()
}
fn build_natives(
root: &Path,
meta: &YogToml,
build_dir: &Path,
debugging_symbols: bool,
) -> Result<Vec<(String, PathBuf)>, String> {
let builder = Builder::detect();
eprintln!(
"==> building {} {} with `cargo {}`",
meta.id,
meta.version,
builder.subcmd()
);
let wanted = wanted_targets();
install_targets(&wanted);
let installed = installed_targets();
let mut bundled: Vec<(String, PathBuf)> = Vec::new();
let mut failed = 0;
for t in wanted {
if !installed.iter().any(|s| s == t.triple) {
eprintln!(
" skip {} (rustup target not installed; run: yog setup)",
t.tag
);
continue;
}
if builder
.build(build_dir, t.triple, root, debugging_symbols)
.is_err()
{
failed += 1;
eprintln!(" skip {} (build failed)", t.tag);
continue;
}
let lib = lib_filename(&meta.id, t.os);
let path = root
.join(TARGET_DIR)
.join(t.triple)
.join(RELEASE_DIR)
.join(&lib);
if path.exists() {
eprintln!(" built {}", t.tag);
bundled.push((t.tag.to_string(), path));
} else {
eprintln!(" skip {} (built but output not found: {})", t.tag, lib);
}
}
if bundled.is_empty() {
return Err(if failed > 0 {
"no platform built — the build failed (errors above)".into()
} else {
"no platform built — run `yog setup` to install cross-compilation tools".into()
});
}
Ok(bundled)
}
const TARGET_DIR: &str = "target";
const RELEASE_DIR: &str = "release";
fn assets(
root: &Path,
resources: yog_cli::yres::Compiled,
mut content: content::Compiled,
dev: bool,
) -> Result<Vec<(String, Vec<u8>)>, String> {
if resources.files > 0 {
eprintln!(
" compiled {} .yres resource file(s) into {} asset file(s)",
resources.files,
resources.outputs.len()
);
}
if content.blocks + content.items + content.recipes + content.books > 0 {
eprintln!(
" compiled {} block(s), {} item(s), {} recipe(s) and {} book(s) from {}/",
content.blocks,
content.items,
content.recipes,
content.books,
content::CONTENT_DIR
);
}
let mut compiled = resources.outputs;
compiled.retain(|(path, _)| !content.packed.contains(path));
let registrations = std::mem::take(&mut content.registrations);
content::merge_outputs(root, &mut compiled, content)?;
if dev {
compiled.push((
yog_registry::REGISTRATIONS_FILE.to_string(),
registrations.into_bytes(),
));
}
let assets = gather_assets(root, compiled);
if !assets.is_empty() {
eprintln!(" bundled {} asset file(s)", assets.len());
}
Ok(assets)
}
pub(super) fn gather_assets(
root: &Path,
compiled: Vec<(String, Vec<u8>)>,
) -> Vec<(String, Vec<u8>)> {
let mut out = yog_cli::content::OutputSet::default();
let mut stack: Vec<PathBuf> = ["assets", "data"]
.iter()
.map(|d| root.join(d))
.filter(|p| p.is_dir())
.collect();
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if let Ok(rel) = path.strip_prefix(root) {
let key = yog_cli::slash_path(rel);
if let Ok(bytes) = std::fs::read(&path) {
out.put(key, bytes);
}
}
}
}
for (key, bytes) in compiled {
out.put(key, bytes);
}
out.into_vec()
}
pub(super) fn package(
out: &Path,
meta: &YogToml,
bundled: &[(String, PathBuf)],
assets: &[(String, Vec<u8>)],
) -> Result<(), String> {
let file = std::fs::File::create(out).map_err(|e| e.to_string())?;
let mut zip = zip::ZipWriter::new(file);
let opts = zip::write::FileOptions::<()>::default()
.compression_method(zip::CompressionMethod::Deflated);
for (tag, native) in bundled {
let lib_name = native
.file_name()
.and_then(|n| n.to_str())
.ok_or("bad native filename")?;
let entry = format!("{}/{tag}/{lib_name}", yog_ids::archive::NATIVES);
let bytes = std::fs::read(native).map_err(|e| e.to_string())?;
zip.start_file(&entry, opts).map_err(|e| e.to_string())?;
zip.write_all(&bytes).map_err(|e| e.to_string())?;
}
for (entry, bytes) in assets {
zip.start_file(entry, opts).map_err(|e| e.to_string())?;
zip.write_all(bytes).map_err(|e| e.to_string())?;
}
let built = yog_formats::manifest::Built {
id: meta.id.clone(),
name: meta.name.clone(),
version: meta.version.clone(),
description: meta.description.clone(),
authors: meta.authors.clone(),
license: meta.license.clone(),
platforms: bundled.iter().map(|(t, _)| t.clone()).collect(),
name_key: meta.name_key.clone(),
description_key: meta.description_key.clone(),
icon: meta.icon.clone(),
dependencies: meta
.mod_dependencies
.iter()
.map(|(id, _)| id.clone())
.collect(),
};
zip.start_file(yog_ids::archive::MANIFEST, opts)
.map_err(|e| e.to_string())?;
zip.write_all(&yog_data::to_bytes(&built))
.map_err(|e| e.to_string())?;
zip.finish().map_err(|e| e.to_string())?;
Ok(())
}