~starkingdoms/starkingdoms

9a16ae19edf42104cf85c5f2570a0c05f1909db1 — core 2 years ago 0bb4cc7
make pretty
M spacetime_rs/src/cmd.rs => spacetime_rs/src/cmd.rs +5 -2
@@ 12,6 12,9 @@ pub fn enforce_commands() {

fn _enforce_command(cmd: &str) {
    if which(cmd).is_err() {
        eprintln!("[!] Unable to find required binary {}. Please install it to continue.", cmd);
        eprintln!(
            "[!] Unable to find required binary {}. Please install it to continue.",
            cmd
        );
    }
}
\ No newline at end of file
}

M spacetime_rs/src/commands/api.rs => spacetime_rs/src/commands/api.rs +15 -5
@@ 1,8 1,8 @@
use std::error::Error;
use std::path::PathBuf;
use crate::configure::create_writer;
use crate::configure::rust::configure_rust_target;
use crate::ninja::{exec, exec_ninja};
use std::error::Error;
use std::path::PathBuf;

pub fn build_api(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;


@@ 31,7 31,11 @@ pub fn run_api(args: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {

    exec_ninja(&root, vec!["api".to_string()])?;

    exec(root.join("target/debug/starkingdoms-api").to_str().unwrap(), &root, args)?;
    exec(
        root.join("target/debug/starkingdoms-api").to_str().unwrap(),
        &root,
        args,
    )?;

    Ok(())
}


@@ 43,7 47,13 @@ pub fn run_api_prod(args: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Erro

    exec_ninja(&root, vec!["api".to_string()])?;

    exec(root.join("target/release/starkingdoms-api").to_str().unwrap(), &root, args)?;
    exec(
        root.join("target/release/starkingdoms-api")
            .to_str()
            .unwrap(),
        &root,
        args,
    )?;

    Ok(())
}
\ No newline at end of file
}

M spacetime_rs/src/commands/assets.rs => spacetime_rs/src/commands/assets.rs +8 -5
@@ 1,9 1,9 @@
use std::error::Error;
use std::path::PathBuf;
use std::time::SystemTime;
use crate::configure::asset::configure_assets;
use crate::configure::create_writer;
use crate::ninja::exec_ninja;
use std::error::Error;
use std::path::PathBuf;
use std::time::SystemTime;

pub fn build_assets(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;


@@ 15,9 15,12 @@ pub fn build_assets(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>>
    let end = SystemTime::now();
    let duration = end.duration_since(start).unwrap();

    println!("[spacetime] configure completed in {} seconds", duration.as_secs_f32());
    println!(
        "[spacetime] configure completed in {} seconds",
        duration.as_secs_f32()
    );

    exec_ninja(&root, vec!["asset".to_string()])?;

    Ok(())
}
\ No newline at end of file
}

M spacetime_rs/src/commands/clean.rs => spacetime_rs/src/commands/clean.rs +2 -2
@@ 1,11 1,11 @@
use crate::ninja::exec;
use std::error::Error;
use std::fs;
use std::path::PathBuf;
use crate::ninja::exec;

pub fn clean(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    exec("cargo", &root, vec!["clean".to_string()])?;
    fs::remove_dir_all(root.join("assets").join("dist"))?;
    fs::remove_dir_all(root.join("assets").join("final"))?;
    Ok(())
}
\ No newline at end of file
}

M spacetime_rs/src/commands/client.rs => spacetime_rs/src/commands/client.rs +8 -4
@@ 1,8 1,8 @@
use std::error::Error;
use std::path::PathBuf;
use crate::configure::client::configure_client;
use crate::configure::create_writer;
use crate::ninja::{exec, exec_ninja};
use std::error::Error;
use std::path::PathBuf;

pub fn run_http(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;


@@ 12,7 12,11 @@ pub fn run_http(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    exec_ninja(&root, vec!["asset".to_string()])?;

    exec("yarn", &root.join("client"), vec![])?;
    exec("yarn", &root.join("client"), vec!["run".to_string(), "dev".to_string()])?;
    exec(
        "yarn",
        &root.join("client"),
        vec!["run".to_string(), "dev".to_string()],
    )?;

    Ok(())
}


@@ 25,4 29,4 @@ pub fn client_protobuf(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Erro
pub fn build_client_prod(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    exec("yarn", &root.join("client"), vec![])?;
    exec("yarn", &root.join("client"), vec!["build".to_string()])
}
\ No newline at end of file
}

M spacetime_rs/src/commands/docker.rs => spacetime_rs/src/commands/docker.rs +122 -29
@@ 1,10 1,10 @@
use std::error::Error;
use std::path::PathBuf;
use std::process::Command;
use crate::commands::api::build_api_prod;
use crate::commands::client::build_client_prod;
use crate::commands::server::build_server_prod;
use crate::ninja::exec;
use std::error::Error;
use std::path::PathBuf;
use std::process::Command;

fn _build(img: &str, channel: &str, root: &PathBuf) -> Result<(), Box<dyn Error>> {
    // compile the various thingies


@@ 16,35 16,128 @@ fn _build(img: &str, channel: &str, root: &PathBuf) -> Result<(), Box<dyn Error>
        build_client_prod(vec![], root.clone())?
    }

    let git_commit_id = String::from_utf8(Command::new("git").args(&["rev-parse", "--short", "HEAD"]).current_dir(root).output().unwrap().stdout).unwrap().replace('\n', "");
    exec("docker", root, vec![
        "buildx", "build", "-f",
        root.join(format!("{}.Dockerfile", img)).to_str().unwrap(),
        "-t", &format!("registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}", img, git_commit_id), root.to_str().unwrap()].iter().map(|u| u.to_string()).collect())?;
    exec("docker", root, vec![
        "buildx", "build", "-f",
        root.join(format!("{}.Dockerfile", img)).to_str().unwrap(),
        "-t", &format!("registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}", img, channel), root.to_str().unwrap()].iter().map(|u| u.to_string()).collect())?;
    let git_commit_id = String::from_utf8(
        Command::new("git")
            .args(["rev-parse", "--short", "HEAD"])
            .current_dir(root)
            .output()
            .unwrap()
            .stdout,
    )
    .unwrap()
    .replace('\n', "");
    exec(
        "docker",
        root,
        vec![
            "buildx",
            "build",
            "-f",
            root.join(format!("{}.Dockerfile", img)).to_str().unwrap(),
            "-t",
            &format!(
                "registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}",
                img, git_commit_id
            ),
            root.to_str().unwrap(),
        ]
        .iter()
        .map(|u| u.to_string())
        .collect(),
    )?;
    exec(
        "docker",
        root,
        vec![
            "buildx",
            "build",
            "-f",
            root.join(format!("{}.Dockerfile", img)).to_str().unwrap(),
            "-t",
            &format!(
                "registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}",
                img, channel
            ),
            root.to_str().unwrap(),
        ]
        .iter()
        .map(|u| u.to_string())
        .collect(),
    )?;

    exec(
        "docker",
        root,
        vec![
            "push",
            &format!(
                "registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}",
                img, git_commit_id
            ),
        ]
        .iter()
        .map(|u| u.to_string())
        .collect(),
    )?;
    exec(
        "docker",
        root,
        vec![
            "push",
            &format!(
                "registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}",
                img, channel
            ),
        ]
        .iter()
        .map(|u| u.to_string())
        .collect(),
    )?;

    exec("docker", root, vec![
        "push", &format!("registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}", img, git_commit_id)].iter().map(|u| u.to_string()).collect())?;
    exec("docker", root, vec![
        "push", &format!("registry.gitlab.com/starkingdoms.tk/starkingdoms.tk:{}-{}", img, channel)].iter().map(|u| u.to_string()).collect())?;
    
    Ok(())
}

pub fn build_docker_api(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { _build("api", "bleeding", &root) }
pub fn build_docker_server(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { _build("server", "bleeding", &root) }
pub fn build_docker_web(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { _build("web", "bleeding", &root) }
pub fn build_docker(_a: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { build_docker_api(_a.clone(), root.clone())?; build_docker_server(_a.clone(), root.clone())?; build_docker_web(_a, root.clone()) }
pub fn build_docker_api(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("api", "bleeding", &root)
}
pub fn build_docker_server(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("server", "bleeding", &root)
}
pub fn build_docker_web(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("web", "bleeding", &root)
}
pub fn build_docker(_a: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    build_docker_api(_a.clone(), root.clone())?;
    build_docker_server(_a.clone(), root.clone())?;
    build_docker_web(_a, root)
}

pub fn build_docker_api_beta(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { _build("api", "beta", &root) }
pub fn build_docker_server_beta(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { _build("server", "beta", &root) }
pub fn build_docker_web_beta(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { _build("web", "beta", &root) }
pub fn build_docker_beta(_a: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { build_docker_api_beta(_a.clone(), root.clone())?; build_docker_server_beta(_a.clone(), root.clone())?; build_docker_web_beta(_a, root.clone()) }
pub fn build_docker_api_beta(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("api", "beta", &root)
}
pub fn build_docker_server_beta(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("server", "beta", &root)
}
pub fn build_docker_web_beta(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("web", "beta", &root)
}
pub fn build_docker_beta(_a: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    build_docker_api_beta(_a.clone(), root.clone())?;
    build_docker_server_beta(_a.clone(), root.clone())?;
    build_docker_web_beta(_a, root)
}

pub fn build_docker_api_stable(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { _build("api", "stable", &root) }
pub fn build_docker_server_stable(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { _build("server", "stable", &root) }
pub fn build_docker_web_stable(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { _build("web", "stable", &root) }
pub fn build_docker_stable(_a: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> { build_docker_api_stable(_a.clone(), root.clone())?; build_docker_server_stable(_a.clone(), root.clone())?; build_docker_web_stable(_a, root.clone()) }
\ No newline at end of file
pub fn build_docker_api_stable(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("api", "stable", &root)
}
pub fn build_docker_server_stable(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("server", "stable", &root)
}
pub fn build_docker_web_stable(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    _build("web", "stable", &root)
}
pub fn build_docker_stable(_a: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    build_docker_api_stable(_a.clone(), root.clone())?;
    build_docker_server_stable(_a.clone(), root.clone())?;
    build_docker_web_stable(_a, root)
}

M spacetime_rs/src/commands/mod.rs => spacetime_rs/src/commands/mod.rs +3 -3
@@ 1,6 1,6 @@
pub mod client;
pub mod api;
pub mod assets;
pub mod clean;
pub mod api;
pub mod client;
pub mod docker;
pub mod server;
pub mod docker;
\ No newline at end of file

M spacetime_rs/src/commands/server.rs => spacetime_rs/src/commands/server.rs +17 -5
@@ 1,8 1,8 @@
use std::error::Error;
use std::path::PathBuf;
use crate::configure::create_writer;
use crate::configure::rust::configure_rust_target;
use crate::ninja::{exec, exec_ninja};
use std::error::Error;
use std::path::PathBuf;

pub fn build_server(_: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let mut config_file_writer = create_writer(&root)?;


@@ 31,7 31,13 @@ pub fn run_server(args: Vec<String>, root: PathBuf) -> Result<(), Box<dyn Error>

    exec_ninja(&root, vec!["server".to_string()])?;

    exec(root.join("target/debug/starkingdoms-server").to_str().unwrap(), &root, args)?;
    exec(
        root.join("target/debug/starkingdoms-server")
            .to_str()
            .unwrap(),
        &root,
        args,
    )?;

    Ok(())
}


@@ 43,7 49,13 @@ pub fn run_server_prod(args: Vec<String>, root: PathBuf) -> Result<(), Box<dyn E

    exec_ninja(&root, vec!["server".to_string()])?;

    exec(root.join("target/release/starkingdoms-server").to_str().unwrap(), &root, args)?;
    exec(
        root.join("target/release/starkingdoms-server")
            .to_str()
            .unwrap(),
        &root,
        args,
    )?;

    Ok(())
}
\ No newline at end of file
}

M spacetime_rs/src/config.rs => spacetime_rs/src/config.rs +1 -1
@@ 1,4 1,4 @@
pub const ASSET_DIR: &str = "assets/";
pub const ASSETS_DIST_SUBDIR: &str = "dist/";
pub const ASSETS_SRC_SUBDIR: &str = "src/";
pub const ASSETS_FINAL_SUBDIR: &str = "final/";
\ No newline at end of file
pub const ASSETS_FINAL_SUBDIR: &str = "final/";

M spacetime_rs/src/configure/asset.rs => spacetime_rs/src/configure/asset.rs +342 -35
@@ 1,12 1,12 @@
use crate::config::{ASSETS_DIST_SUBDIR, ASSETS_FINAL_SUBDIR, ASSETS_SRC_SUBDIR, ASSET_DIR};
use crate::ninja::NinjaWriter;
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::fs::File;
use std::path::PathBuf;
use crate::config::{ASSET_DIR, ASSETS_DIST_SUBDIR, ASSETS_FINAL_SUBDIR, ASSETS_SRC_SUBDIR};
use crate::ninja::NinjaWriter;
use std::path::{Path, PathBuf};

pub fn configure_assets(writer: &mut NinjaWriter<File>, root: &PathBuf) -> Result<(), Box<dyn Error>> {
pub fn configure_assets(writer: &mut NinjaWriter<File>, root: &Path) -> Result<(), Box<dyn Error>> {
    // scan for assets
    let asset_src_dir = root.join(ASSET_DIR).join(ASSETS_SRC_SUBDIR);



@@ 20,13 20,13 @@ pub fn configure_assets(writer: &mut NinjaWriter<File>, root: &PathBuf) -> Resul
        }
    }

    println!("[spacetime] asset scan: found {} assets", found_assets.len());
    println!(
        "[spacetime] asset scan: found {} assets",
        found_assets.len()
    );

    let default_asset_size = 512;
    let asset_overrides = HashMap::from([
        ("earth.ink.svg", 2048),
        ("moon.ink.svg", 2048)
    ]);
    let asset_overrides = HashMap::from([("earth.ink.svg", 2048), ("moon.ink.svg", 2048)]);

    // generate an inkscape rule for all required asset sizes
    let mut written_rules_for = vec![];


@@ 37,17 37,35 @@ pub fn configure_assets(writer: &mut NinjaWriter<File>, root: &PathBuf) -> Resul
        gen_inkscape_rule(*size, writer, &mut written_rules_for)?;
    }

    println!("[spacetime] generated {} image conversion rules", written_rules_for.len() * 3);
    println!(
        "[spacetime] generated {} image conversion rules",
        written_rules_for.len() * 3
    );

    let mut files_375 = vec![];
    let mut files_125 = vec![];
    let mut files_full = vec![];

    for asset in &found_assets {
        gen_convert_rule(asset, root, writer, &mut files_375, &mut files_full, &mut files_125, asset_size(asset.to_str().unwrap(), &asset_overrides, default_asset_size))?;
        gen_convert_rule(
            asset,
            root,
            writer,
            &mut files_375,
            &mut files_full,
            &mut files_125,
            asset_size(
                asset.to_str().unwrap(),
                &asset_overrides,
                default_asset_size,
            ),
        )?;
    }

    println!("[spacetime] generated {} image conversion steps", files_full.len() + files_125.len() + files_375.len());
    println!(
        "[spacetime] generated {} image conversion steps",
        files_full.len() + files_125.len() + files_375.len()
    );

    gen_packer_rule(root, writer, &files_375, &files_full, &files_125)?;



@@ 56,39 74,283 @@ pub fn configure_assets(writer: &mut NinjaWriter<File>, root: &PathBuf) -> Resul
    Ok(())
}

fn gen_packer_rule(root: &PathBuf, writer: &mut NinjaWriter<File>, files_375: &Vec<PathBuf>, files_full: &Vec<PathBuf>, files_125: &Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
    writer.rule("pack", &format!("cd {} && atlasify -m 4096,4096 -o $out $in && touch $out", root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).to_string_lossy()), None, None, None, Some("console"), None, None, None, None)?;
fn gen_packer_rule(
    root: &Path,
    writer: &mut NinjaWriter<File>,
    files_375: &[PathBuf],
    files_full: &[PathBuf],
    files_125: &[PathBuf],
) -> Result<(), Box<dyn Error>> {
    writer.rule(
        "pack",
        &format!(
            "cd {} && atlasify -m 4096,4096 -o $out $in && touch $out",
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .to_string_lossy()
        ),
        None,
        None,
        None,
        Some("console"),
        None,
        None,
        None,
        None,
    )?;

    writer.build(vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-full").to_str().unwrap().to_string()], "pack".to_string(), files_full.iter().map(|u| u.to_str().unwrap().to_string()).collect(), vec![], vec![], HashMap::new(), vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-full.json").to_str().unwrap().to_string(), root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-full.png").to_str().unwrap().to_string()], None, None)?;
    writer.build(vec!["asset-full".to_string()], "phony".to_string(), vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-full").to_str().unwrap().to_string()], vec![], vec![], HashMap::new(), vec![], None, None)?;
    writer.build(
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-full")
            .to_str()
            .unwrap()
            .to_string()],
        "pack".to_string(),
        files_full
            .iter()
            .map(|u| u.to_str().unwrap().to_string())
            .collect(),
        vec![],
        vec![],
        HashMap::new(),
        vec![
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-full.json")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-full.png")
                .to_str()
                .unwrap()
                .to_string(),
        ],
        None,
        None,
    )?;
    writer.build(
        vec!["asset-full".to_string()],
        "phony".to_string(),
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-full")
            .to_str()
            .unwrap()
            .to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    writer.build(vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-125").to_str().unwrap().to_string()], "pack".to_string(), files_125.iter().map(|u| u.to_str().unwrap().to_string()).collect(), vec![], vec![], HashMap::new(), vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-125.json").to_str().unwrap().to_string(), root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-125.png").to_str().unwrap().to_string()], None, None)?;
    writer.build(vec!["asset-125".to_string()], "phony".to_string(), vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-125").to_str().unwrap().to_string()], vec![], vec![], HashMap::new(), vec![], None, None)?;
    writer.build(
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-125")
            .to_str()
            .unwrap()
            .to_string()],
        "pack".to_string(),
        files_125
            .iter()
            .map(|u| u.to_str().unwrap().to_string())
            .collect(),
        vec![],
        vec![],
        HashMap::new(),
        vec![
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-125.json")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-125.png")
                .to_str()
                .unwrap()
                .to_string(),
        ],
        None,
        None,
    )?;
    writer.build(
        vec!["asset-125".to_string()],
        "phony".to_string(),
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-125")
            .to_str()
            .unwrap()
            .to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    writer.build(vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-375").to_str().unwrap().to_string()], "pack".to_string(), files_375.iter().map(|u| u.to_str().unwrap().to_string()).collect(), vec![], vec![], HashMap::new(), vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-375.json").to_str().unwrap().to_string(), root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-375.png").to_str().unwrap().to_string()], None, None)?;
    writer.build(vec!["asset-375".to_string()], "phony".to_string(), vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-375").to_str().unwrap().to_string()], vec![], vec![], HashMap::new(), vec![], None, None)?;

    writer.build(vec!["asset".to_string()], "phony".to_string(), vec![root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-375").to_str().unwrap().to_string(), root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-full").to_str().unwrap().to_string(), root.join(ASSET_DIR).join(ASSETS_DIST_SUBDIR).join("spritesheet-125").to_str().unwrap().to_string()], vec![], vec![], HashMap::new(), vec![], None, None)?;
    writer.build(
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-375")
            .to_str()
            .unwrap()
            .to_string()],
        "pack".to_string(),
        files_375
            .iter()
            .map(|u| u.to_str().unwrap().to_string())
            .collect(),
        vec![],
        vec![],
        HashMap::new(),
        vec![
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-375.json")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-375.png")
                .to_str()
                .unwrap()
                .to_string(),
        ],
        None,
        None,
    )?;
    writer.build(
        vec!["asset-375".to_string()],
        "phony".to_string(),
        vec![root
            .join(ASSET_DIR)
            .join(ASSETS_DIST_SUBDIR)
            .join("spritesheet-375")
            .to_str()
            .unwrap()
            .to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    writer.build(
        vec!["asset".to_string()],
        "phony".to_string(),
        vec![
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-375")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-full")
                .to_str()
                .unwrap()
                .to_string(),
            root.join(ASSET_DIR)
                .join(ASSETS_DIST_SUBDIR)
                .join("spritesheet-125")
                .to_str()
                .unwrap()
                .to_string(),
        ],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    Ok(())
}

fn gen_convert_rule(asset: &PathBuf, root: &PathBuf, writer: &mut NinjaWriter<File>, files_375: &mut Vec<PathBuf>, files_full: &mut Vec<PathBuf>, files_125: &mut Vec<PathBuf>, size: i32) -> Result<(), Box<dyn Error>> {
    let out_full = root.join(ASSET_DIR).join(ASSETS_FINAL_SUBDIR).join("full/").join(asset.file_stem().unwrap().to_str().unwrap().to_string() + ".png");
fn gen_convert_rule(
    asset: &Path,
    root: &Path,
    writer: &mut NinjaWriter<File>,
    files_375: &mut Vec<PathBuf>,
    files_full: &mut Vec<PathBuf>,
    files_125: &mut Vec<PathBuf>,
    size: i32,
) -> Result<(), Box<dyn Error>> {
    let out_full = root
        .join(ASSET_DIR)
        .join(ASSETS_FINAL_SUBDIR)
        .join("full/")
        .join(asset.file_stem().unwrap().to_str().unwrap().to_string() + ".png");
    files_full.push(out_full.clone());
    let rule_full = format!("inkscape_{}_px_full", size);
    writer.build(vec![out_full.to_str().unwrap().to_string()], rule_full, vec![asset.to_str().unwrap().to_string()], vec![], vec![], HashMap::new(), vec![], None, None)?;
    writer.build(
        vec![out_full.to_str().unwrap().to_string()],
        rule_full,
        vec![asset.to_str().unwrap().to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    let out_375 = root.join(ASSET_DIR).join(ASSETS_FINAL_SUBDIR).join("375/").join(asset.file_stem().unwrap().to_str().unwrap().to_string() + ".png");
    let out_375 = root
        .join(ASSET_DIR)
        .join(ASSETS_FINAL_SUBDIR)
        .join("375/")
        .join(asset.file_stem().unwrap().to_str().unwrap().to_string() + ".png");
    files_375.push(out_375.clone());
    let rule_375 = format!("inkscape_{}_px_375", size);
    writer.build(vec![out_375.to_str().unwrap().to_string()], rule_375, vec![asset.to_str().unwrap().to_string()], vec![], vec![], HashMap::new(), vec![], None, None)?;
    writer.build(
        vec![out_375.to_str().unwrap().to_string()],
        rule_375,
        vec![asset.to_str().unwrap().to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    let out_125 = root.join(ASSET_DIR).join(ASSETS_FINAL_SUBDIR).join("125/").join(asset.file_stem().unwrap().to_str().unwrap().to_string() + ".png");
    let out_125 = root
        .join(ASSET_DIR)
        .join(ASSETS_FINAL_SUBDIR)
        .join("125/")
        .join(asset.file_stem().unwrap().to_str().unwrap().to_string() + ".png");
    files_125.push(out_125.clone());
    let rule_125 = format!("inkscape_{}_px_125", size);
    writer.build(vec![out_125.to_str().unwrap().to_string()], rule_125, vec![asset.to_str().unwrap().to_string()], vec![], vec![], HashMap::new(), vec![], None, None)?;
    writer.build(
        vec![out_125.to_str().unwrap().to_string()],
        rule_125,
        vec![asset.to_str().unwrap().to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    Ok(())
}


@@ 97,16 359,61 @@ fn asset_size(asset: &str, overrides: &HashMap<&str, i32>, default: i32) -> i32 
    *overrides.get(asset).unwrap_or(&default)
}

fn gen_inkscape_rule(size: i32, writer: &mut NinjaWriter<File>, written: &mut Vec<i32>) -> Result<(), Box<dyn Error>> {
fn gen_inkscape_rule(
    size: i32,
    writer: &mut NinjaWriter<File>,
    written: &mut Vec<i32>,
) -> Result<(), Box<dyn Error>> {
    if written.contains(&size) {
        return Ok(())
        return Ok(());
    }

    writer.rule(&format!("inkscape_{}_px_full", size), &format!("inkscape -w {} -h {} $in -o $out", size, size), None, None, None, None, None, None, None, None)?;
    writer.rule(&format!("inkscape_{}_px_375", size), &format!("inkscape -w {} -h {} $in -o $out", (size as f64 * 0.375) as i32, (size as f64 * 0.375) as i32), None, None, None, None, None, None, None, None)?;
    writer.rule(&format!("inkscape_{}_px_125", size), &format!("inkscape -w {} -h {} $in -o $out", (size as f64 * 0.125) as i32, (size as f64 * 0.125) as i32), None, None, None, None, None, None, None, None)?;
    writer.rule(
        &format!("inkscape_{}_px_full", size),
        &format!("inkscape -w {} -h {} $in -o $out", size, size),
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
    )?;
    writer.rule(
        &format!("inkscape_{}_px_375", size),
        &format!(
            "inkscape -w {} -h {} $in -o $out",
            (size as f64 * 0.375) as i32,
            (size as f64 * 0.375) as i32
        ),
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
    )?;
    writer.rule(
        &format!("inkscape_{}_px_125", size),
        &format!(
            "inkscape -w {} -h {} $in -o $out",
            (size as f64 * 0.125) as i32,
            (size as f64 * 0.125) as i32
        ),
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
    )?;

    written.push(size);

    Ok(())
}
\ No newline at end of file
}

M spacetime_rs/src/configure/client.rs => spacetime_rs/src/configure/client.rs +5 -5
@@ 1,11 1,11 @@
use std::error::Error;
use std::fs::File;
use std::path::PathBuf;
use crate::configure::asset::configure_assets;
use crate::ninja::NinjaWriter;
use std::error::Error;
use std::fs::File;
use std::path::Path;

pub fn configure_client(writer: &mut NinjaWriter<File>, root: &PathBuf) -> Result<(), Box<dyn Error>> {
pub fn configure_client(writer: &mut NinjaWriter<File>, root: &Path) -> Result<(), Box<dyn Error>> {
    configure_assets(writer, root)?;

    Ok(())
}
\ No newline at end of file
}

M spacetime_rs/src/configure/mod.rs => spacetime_rs/src/configure/mod.rs +4 -4
@@ 1,15 1,15 @@
use crate::ninja::NinjaWriter;
use std::fs::File;
use std::io;
use std::path::PathBuf;
use crate::ninja::NinjaWriter;
use std::path::Path;

pub mod asset;
pub mod client;
pub mod rust;

pub fn create_writer(root: &PathBuf) -> Result<NinjaWriter<File>, io::Error> {
pub fn create_writer(root: &Path) -> Result<NinjaWriter<File>, io::Error> {
    let mut w = NinjaWriter::new(File::create(root.join("build.ninja"))?);
    w.comment("Generated by spacetime")?;
    w.comment("Do not edit manually")?;
    Ok(w)
}
\ No newline at end of file
}

M spacetime_rs/src/configure/rust.rs => spacetime_rs/src/configure/rust.rs +75 -8
@@ 13,24 13,91 @@ def gen_rules_for_api(root, env, writer, modules):
    writer.build(['api'], 'phony', [f'{root}/target/{out_dir}/starkingdoms-api'])
 */

use crate::ninja::NinjaWriter;
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use std::path::PathBuf;
use crate::ninja::NinjaWriter;
use std::path::Path;

pub fn configure_rust_target(rust_target: &str, rust_env: &str, writer: &mut NinjaWriter<File>, root: &PathBuf) -> Result<(), Box<dyn Error>> {
pub fn configure_rust_target(
    rust_target: &str,
    rust_env: &str,
    writer: &mut NinjaWriter<File>,
    root: &Path,
) -> Result<(), Box<dyn Error>> {
    let out_dir;
    if rust_env == "dev" {
        out_dir = "debug";
        writer.rule(&format!("cargo-{}", rust_target), &format!("cargo build --bin starkingdoms-{}", rust_target), None, Some(root.join("target/debug/").join(format!("starkingdoms-{}.d", rust_target)).to_str().unwrap()), None, Some("console"), None, None, None, None)?;
        writer.rule(
            &format!("cargo-{}", rust_target),
            &format!("cargo build --bin starkingdoms-{}", rust_target),
            None,
            Some(
                root.join("target/debug/")
                    .join(format!("starkingdoms-{}.d", rust_target))
                    .to_str()
                    .unwrap(),
            ),
            None,
            Some("console"),
            None,
            None,
            None,
            None,
        )?;
    } else {
        out_dir = "release";
        writer.rule(&format!("cargo-{}", rust_target), &format!("cargo build --bin starkingdoms-{} --release", rust_target), None, Some(root.join("target/release/").join(format!("starkingdoms-{}.d", rust_target)).to_str().unwrap()), None, Some("console"), None, None, None, None)?;
        writer.rule(
            &format!("cargo-{}", rust_target),
            &format!("cargo build --bin starkingdoms-{} --release", rust_target),
            None,
            Some(
                root.join("target/release/")
                    .join(format!("starkingdoms-{}.d", rust_target))
                    .to_str()
                    .unwrap(),
            ),
            None,
            Some("console"),
            None,
            None,
            None,
            None,
        )?;
    }

    writer.build(vec![root.join(format!("target/{}/", out_dir)).join(format!("starkingdoms-{}", rust_target)).to_str().unwrap().to_string()], format!("cargo-{}", rust_target), vec![root.join("server/Cargo.toml").to_str().unwrap().to_string()], vec![], vec![], HashMap::new(), vec![], None, None)?;
    writer.build(vec![rust_target.to_string()], "phony".to_string(), vec![root.join(format!("target/{}/", out_dir)).join(format!("starkingdoms-{}", rust_target)).to_str().unwrap().to_string()], vec![], vec![], HashMap::new(), vec![], None, None)?;
    writer.build(
        vec![root
            .join(format!("target/{}/", out_dir))
            .join(format!("starkingdoms-{}", rust_target))
            .to_str()
            .unwrap()
            .to_string()],
        format!("cargo-{}", rust_target),
        vec![root.join("server/Cargo.toml").to_str().unwrap().to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;
    writer.build(
        vec![rust_target.to_string()],
        "phony".to_string(),
        vec![root
            .join(format!("target/{}/", out_dir))
            .join(format!("starkingdoms-{}", rust_target))
            .to_str()
            .unwrap()
            .to_string()],
        vec![],
        vec![],
        HashMap::new(),
        vec![],
        None,
        None,
    )?;

    Ok(())
}
\ No newline at end of file
}

M spacetime_rs/src/main.rs => spacetime_rs/src/main.rs +178 -43
@@ 1,57 1,181 @@
use std::collections::HashMap;
use std::error::Error;
use std::io::{Write};
use std::path::{PathBuf};
use std::time::SystemTime;
use tabwriter::TabWriter;
use crate::cmd::enforce_commands;
use crate::commands::api::{build_api, build_api_prod, run_api, run_api_prod};
use crate::commands::assets::build_assets;
use crate::commands::clean::{clean};
use crate::commands::clean::clean;
use crate::commands::client::{build_client_prod, client_protobuf, run_http};
use crate::commands::docker::{build_docker, build_docker_api, build_docker_api_beta, build_docker_api_stable, build_docker_beta, build_docker_server, build_docker_server_beta, build_docker_server_stable, build_docker_stable, build_docker_web, build_docker_web_beta, build_docker_web_stable};
use crate::commands::docker::{
    build_docker, build_docker_api, build_docker_api_beta, build_docker_api_stable,
    build_docker_beta, build_docker_server, build_docker_server_beta, build_docker_server_stable,
    build_docker_stable, build_docker_web, build_docker_web_beta, build_docker_web_stable,
};
use crate::commands::server::{build_server, build_server_prod, run_server, run_server_prod};
use std::collections::HashMap;
use std::error::Error;
use std::io::Write;
use std::path::PathBuf;
use std::time::SystemTime;
use tabwriter::TabWriter;

pub mod commands;
pub mod ninja;
pub mod cmd;
pub mod configure;
pub mod commands;
pub mod config;
pub mod configure;
pub mod ninja;

fn main() {
    let mut bcm = BuildCommandManager::new();

    bcm.register("run_http", Box::new(run_http), "Compile the client and then run a development HTTP server", false);
    bcm.register("build_assets", Box::new(build_assets), "Compile the asset source files into spritesheets", false);
    bcm.register("client_protobuf", Box::new(client_protobuf), "Update the client protocol bindings", false);
    bcm.register("clean", Box::new(clean), "Remove all compilation artifacts", false);
    bcm.register(
        "run_http",
        Box::new(run_http),
        "Compile the client and then run a development HTTP server",
        false,
    );
    bcm.register(
        "build_assets",
        Box::new(build_assets),
        "Compile the asset source files into spritesheets",
        false,
    );
    bcm.register(
        "client_protobuf",
        Box::new(client_protobuf),
        "Update the client protocol bindings",
        false,
    );
    bcm.register(
        "clean",
        Box::new(clean),
        "Remove all compilation artifacts",
        false,
    );

    bcm.register("build_api", Box::new(build_api), "Compile the API server", false);
    bcm.register(
        "build_api",
        Box::new(build_api),
        "Compile the API server",
        false,
    );
    bcm.register("run_api", Box::new(run_api), "Run the API server", false);
    bcm.register("build_api_prod", Box::new(build_api_prod), "Compile the API server with optimizations", false);
    bcm.register("run_api_prod", Box::new(run_api_prod), "Run the API server with optimizations", false);
    bcm.register(
        "build_api_prod",
        Box::new(build_api_prod),
        "Compile the API server with optimizations",
        false,
    );
    bcm.register(
        "run_api_prod",
        Box::new(run_api_prod),
        "Run the API server with optimizations",
        false,
    );

    bcm.register("build_server", Box::new(build_server), "Compile the game server", false);
    bcm.register("run_server", Box::new(run_server), "Run the game server", false);
    bcm.register("build_server_prod", Box::new(build_server_prod), "Compile the game server with optimizations", false);
    bcm.register("run_server_prod", Box::new(run_server_prod), "Run the game server with optimizations", false);
    bcm.register(
        "build_server",
        Box::new(build_server),
        "Compile the game server",
        false,
    );
    bcm.register(
        "run_server",
        Box::new(run_server),
        "Run the game server",
        false,
    );
    bcm.register(
        "build_server_prod",
        Box::new(build_server_prod),
        "Compile the game server with optimizations",
        false,
    );
    bcm.register(
        "run_server_prod",
        Box::new(run_server_prod),
        "Run the game server with optimizations",
        false,
    );

    bcm.register("build_docker_beta", Box::new(build_docker_beta), "Build all three docker images for the beta channel", false);
    bcm.register("build_docker_api_beta", Box::new(build_docker_api_beta), "Build the API docker image for the beta channel", false);
    bcm.register("build_docker_server_beta", Box::new(build_docker_server_beta), "Build the main docker image for the beta channel", false);
    bcm.register("build_docker_web_beta", Box::new(build_docker_web_beta), "Build the webserver docker image for the beta channel", false);
    bcm.register(
        "build_docker_beta",
        Box::new(build_docker_beta),
        "Build all three docker images for the beta channel",
        false,
    );
    bcm.register(
        "build_docker_api_beta",
        Box::new(build_docker_api_beta),
        "Build the API docker image for the beta channel",
        false,
    );
    bcm.register(
        "build_docker_server_beta",
        Box::new(build_docker_server_beta),
        "Build the main docker image for the beta channel",
        false,
    );
    bcm.register(
        "build_docker_web_beta",
        Box::new(build_docker_web_beta),
        "Build the webserver docker image for the beta channel",
        false,
    );

    bcm.register("build_docker_stable", Box::new(build_docker_stable), "Build all three docker images for the stable channel", false);
    bcm.register("build_docker_api_stable", Box::new(build_docker_api_stable), "Build the API docker image for the stable channel", false);
    bcm.register("build_docker_server_stable", Box::new(build_docker_server_stable), "Build the main docker image for the stable channel", false);
    bcm.register("build_docker_web_stable", Box::new(build_docker_web_stable), "Build the webserver docker image for the stable channel", false);
    bcm.register(
        "build_docker_stable",
        Box::new(build_docker_stable),
        "Build all three docker images for the stable channel",
        false,
    );
    bcm.register(
        "build_docker_api_stable",
        Box::new(build_docker_api_stable),
        "Build the API docker image for the stable channel",
        false,
    );
    bcm.register(
        "build_docker_server_stable",
        Box::new(build_docker_server_stable),
        "Build the main docker image for the stable channel",
        false,
    );
    bcm.register(
        "build_docker_web_stable",
        Box::new(build_docker_web_stable),
        "Build the webserver docker image for the stable channel",
        false,
    );

    bcm.register("build_docker", Box::new(build_docker), "Build all three docker images for the bleeding channel", false);
    bcm.register("build_docker_api", Box::new(build_docker_api), "Build the API docker image for the bleeding channel", false);
    bcm.register("build_docker_server", Box::new(build_docker_server), "Build the main docker image for the bleeding channel", false);
    bcm.register("build_docker_web", Box::new(build_docker_web), "Build the webserver docker image for the bleeding channel", false);
    bcm.register(
        "build_docker",
        Box::new(build_docker),
        "Build all three docker images for the bleeding channel",
        false,
    );
    bcm.register(
        "build_docker_api",
        Box::new(build_docker_api),
        "Build the API docker image for the bleeding channel",
        false,
    );
    bcm.register(
        "build_docker_server",
        Box::new(build_docker_server),
        "Build the main docker image for the bleeding channel",
        false,
    );
    bcm.register(
        "build_docker_web",
        Box::new(build_docker_web),
        "Build the webserver docker image for the bleeding channel",
        false,
    );

    bcm.register("build_client_prod", Box::new(build_client_prod), "Build the production-ready client bundle", false);
    bcm.register(
        "build_client_prod",
        Box::new(build_client_prod),
        "Build the production-ready client bundle",
        false,
    );

    let start = SystemTime::now();



@@ 78,17 202,24 @@ fn main() {
type BuildCommandCallback = Box<dyn Fn(Vec<String>, PathBuf) -> Result<(), Box<dyn Error>>>;
#[derive(Default)]
pub struct BuildCommandManager {
    commands: HashMap<String, (BuildCommandCallback, String, bool)>
    commands: HashMap<String, (BuildCommandCallback, String, bool)>,
}
impl BuildCommandManager {
    pub fn new() -> Self {
        Self {
            commands: HashMap::new()
            commands: HashMap::new(),
        }
    }

    pub fn register(&mut self, name: &str, command: BuildCommandCallback, description: &str, hidden: bool) {
        self.commands.insert(name.to_string(), (command, description.to_string(), hidden));
    pub fn register(
        &mut self,
        name: &str,
        command: BuildCommandCallback,
        description: &str,
        hidden: bool,
    ) {
        self.commands
            .insert(name.to_string(), (command, description.to_string(), hidden));
    }

    pub fn help(&self) {


@@ 99,18 230,22 @@ impl BuildCommandManager {
        let mut tw = TabWriter::new(vec![]);

        for (target, (_, description, hidden)) in &self.commands {
            if *hidden { continue };
            if *hidden {
                continue;
            };

            writeln!(tw, "\t{}\t{}", target, description).unwrap();
        }

        std::io::stdout().write_all(&tw.into_inner().unwrap()).unwrap();
        std::io::stdout()
            .write_all(&tw.into_inner().unwrap())
            .unwrap();
    }

    pub fn exec(&self, args: Vec<String>) -> Result<(), Box<dyn Error>> {
        if args.len() < 2 || args[1] == "help" || args[1] == "h" {
            self.help();
            return Ok(())
            return Ok(());
        }

        enforce_commands();


@@ 125,4 260,4 @@ impl BuildCommandManager {
            Ok(())
        }
    }
}
\ No newline at end of file
}

M spacetime_rs/src/ninja.rs => spacetime_rs/src/ninja.rs +47 -12
@@ 1,22 1,22 @@
use std::collections::HashMap;
use std::error::Error;
use std::io::Write;
use std::io;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};

fn escape_path(word: &str) -> String {
    word.replace("$ ", "$$ ").replace(' ', "$ ").replace(':', "$:")
    word.replace("$ ", "$$ ")
        .replace(' ', "$ ")
        .replace(':', "$:")
}

pub struct NinjaWriter<T: Write> {
    output: T
    output: T,
}
impl<T: Write> NinjaWriter<T> {
    pub fn new(output: T) -> Self {
        Self {
            output
        }
        Self { output }
    }

    pub fn newline(&mut self) -> Result<(), io::Error> {


@@ 29,7 29,7 @@ impl<T: Write> NinjaWriter<T> {

    pub fn variable(&mut self, key: &str, value: &str, indent: usize) -> Result<(), io::Error> {
        if value.is_empty() {
            return Ok(())
            return Ok(());
        }
        writeln!(self.output, "{}{} = {}", self._indent(indent), key, value)
    }


@@ 39,7 39,20 @@ impl<T: Write> NinjaWriter<T> {
        self.variable("depth", &depth.to_string(), 1)
    }

    pub fn rule(&mut self, name: &str, command: &str, description: Option<&str>, depfile: Option<&str>, generator: Option<bool>, pool: Option<&str>, restat: Option<bool>, rspfile: Option<&str>, rspfile_content: Option<&str>, deps: Option<&str>) -> Result<(), io::Error> {
    #[allow(clippy::too_many_arguments)]
    pub fn rule(
        &mut self,
        name: &str,
        command: &str,
        description: Option<&str>,
        depfile: Option<&str>,
        generator: Option<bool>,
        pool: Option<&str>,
        restat: Option<bool>,
        rspfile: Option<&str>,
        rspfile_content: Option<&str>,
        deps: Option<&str>,
    ) -> Result<(), io::Error> {
        writeln!(self.output, "rule {}", name)?;
        self.variable("command", command, 1)?;
        if let Some(desc) = description {


@@ 74,7 87,19 @@ impl<T: Write> NinjaWriter<T> {
        Ok(())
    }

    pub fn build(&mut self, outputs: Vec<String>, rule: String, inputs: Vec<String>, mut implicit: Vec<String>, mut order_only: Vec<String>, variables: HashMap<String, String>, mut implicit_outputs: Vec<String>, pool: Option<String>, dyndep: Option<String>) -> Result<(), io::Error> {
    #[allow(clippy::too_many_arguments)]
    pub fn build(
        &mut self,
        outputs: Vec<String>,
        rule: String,
        inputs: Vec<String>,
        mut implicit: Vec<String>,
        mut order_only: Vec<String>,
        variables: HashMap<String, String>,
        mut implicit_outputs: Vec<String>,
        pool: Option<String>,
        dyndep: Option<String>,
    ) -> Result<(), io::Error> {
        let mut out_outputs: Vec<String> = outputs.iter().map(|u| escape_path(u)).collect();
        let mut all_inputs: Vec<String> = inputs.iter().map(|u| escape_path(u)).collect();



@@ 88,12 113,22 @@ impl<T: Write> NinjaWriter<T> {
        }
        if !implicit_outputs.is_empty() {
            out_outputs.push("|".to_string());
            out_outputs.append(&mut implicit_outputs.iter_mut().map(|u| escape_path(u)).collect());
            out_outputs.append(
                &mut implicit_outputs
                    .iter_mut()
                    .map(|u| escape_path(u))
                    .collect(),
            );
        }

        all_inputs.insert(0, rule);

        writeln!(self.output, "build {}: {}", out_outputs.join(" "), all_inputs.join(" "))?;
        writeln!(
            self.output,
            "build {}: {}",
            out_outputs.join(" "),
            all_inputs.join(" ")
        )?;

        if let Some(pool) = pool {
            self.variable("pool", &pool, 1)?;


@@ 156,4 191,4 @@ pub fn exec(cmd: &str, dir: &PathBuf, args: Vec<String>) -> Result<(), Box<dyn E
    } else {
        Err(format!("{} exited with error: {}", cmd, stat.code().unwrap()).into())
    }
}
\ No newline at end of file
}