Files
ic10emu/xtask/src/main.rs
2024-05-09 13:58:28 -07:00

280 lines
9.7 KiB
Rust

use std::process::{Command, ExitStatus};
use clap::{Parser, Subcommand};
mod enums;
mod generate;
mod stationpedia;
/// Helper program to start ic10emu and website.
///
/// Can be invoked as `cargo xtask <command>`
#[derive(Debug, Parser)]
#[command(bin_name = "cargo xtask")]
struct Args {
/// Package manager to use
#[arg(long, global = true, default_value = "pnpm")]
manager: String,
/// wasm-pack executable
#[arg(long, global = true, default_value = "wasm-pack")]
wasm_pack: String,
#[command(subcommand)]
task: Task,
}
const PACKAGES: &[&str] = &["ic10lsp_wasm", "ic10emu_wasm"];
const VALID_VERSION_TYPE: &[&str] = &["patch", "minor", "major"];
#[derive(Debug, Subcommand)]
enum Task {
/// Build the packages
Build {
/// Build in release mode
#[arg(long)]
release: bool,
/// Packages to build
#[arg(long, short = 'p', default_values = PACKAGES)]
packages: Vec<String>,
/// Additional arguments to pass to wasm-pack, use another `--` to pass to cargo build
#[arg(last = true, default_values = ["--","-q"])]
// #[arg(last = true)]
rest: Vec<std::ffi::OsString>,
},
/// Start the server
///
/// This does not build the packages, use `build` first
Start {},
/// Runs production page under 'www/dist', Run `build` first.
Deploy {},
/// bump the cargo.toml and package,json versions
Version {
#[arg(default_value = "patch", value_parser = clap::builder::PossibleValuesParser::new(VALID_VERSION_TYPE))]
version: String,
},
/// update changelog
Changelog {},
Generate {
#[arg()]
/// Path to Stationeers installation. Used to locate "Stationpedia.json" and "Enums.json"
/// generated by https://github.com/Ryex/StationeersStationpediaExtractor
/// Otherwise looks for both files in `<workspace>` or `<workspace>/data`.
/// Can also point directly at a folder containing the two files.
path: Option<std::ffi::OsString>,
},
}
#[derive(thiserror::Error)]
enum Error {
#[error("building package {0} failed. Command: `{1}` Status code {2}")]
BuildFailed(String, String, std::process::ExitStatus),
#[error("failed to run command `{0}`")]
Command(String, #[source] std::io::Error),
#[error("can not find `Stationpedia.json` and/or `Enums.json` at `{0}`")]
BadStationeersPath(std::path::PathBuf),
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::error::Error;
use std::fmt::*;
write!(f, "Error: {}", self)?;
let mut err: &dyn Error = self;
while let Some(cause) = err.source() {
write!(f, "\nCaused by: ")?;
Display::fmt(&cause, f)?;
err = cause;
}
Ok(())
}
}
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let args = Args::parse();
let workspace = {
let out = Command::new("cargo")
.arg("metadata")
.arg("--no-deps")
.arg("--format-version=1")
.output()
.map_err(|e| Error::Command("cargo metadata".to_string(), e))?;
let s = std::str::from_utf8(&out.stdout).unwrap();
let Some((_, s)) = s.split_once(r#"workspace_root":""#) else {
panic!("couldn't find workspace root");
};
let Some((path, _)) = s.split_once("\",") else {
panic!("couldn't find workspace root");
};
std::path::PathBuf::from(path)
};
match &args.task {
Task::Build {
release,
packages,
rest,
} => {
build(&args, packages, *release, &workspace, rest)?;
}
Task::Start {} => {
pnpm_install(&args, &workspace)?;
eprintln!("Starting server");
let mut cmd = Command::new(&args.manager);
cmd.current_dir(&workspace.join("www"));
cmd.args(["run", "start"]).status().map_err(|e| {
Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e)
})?;
}
Task::Deploy {} => {
pnpm_install(&args, &workspace)?;
eprintln!("Production Build");
let mut cmd = Command::new(&args.manager);
cmd.current_dir(&workspace.join("www"));
cmd.args(["run", "build"]).status().map_err(|e| {
Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e)
})?;
}
Task::Version { version } => {
let mut cmd = Command::new("cargo");
cmd.current_dir(&workspace);
cmd.args(["set-version", "--bump", &version])
.status()
.map_err(|e| {
Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e)
})?;
let mut cmd = Command::new(&args.manager);
cmd.current_dir(&workspace.join("www"));
cmd.args(["version", &version]).status().map_err(|e| {
Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e)
})?;
}
Task::Changelog {} => {
let mut cmd = Command::new("git-changelog");
cmd.current_dir(&workspace);
cmd.args([
"-io",
"CHANGELOG.md",
"-t",
"path:CHANGELOG.md.jinja",
"-c",
"conventional",
"--bump",
VERSION.unwrap_or("auto"),
"--parse-refs",
"--trailers",
])
.status()
.map_err(|e| Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e))?;
}
Task::Generate { path } => {
let path = match path {
Some(path) => {
let mut path = std::path::PathBuf::from(path);
if path.exists()
&& path
.parent()
.and_then(|p| p.file_name())
.is_some_and(|p| p == "Stationeers")
&& path.file_name().is_some_and(|name| {
(std::env::consts::OS == "windows" && name == "rocketstation.exe")
|| (name == "rocketstation")
|| (name == "rocketstation_Data")
})
{
path = path.parent().unwrap().to_path_buf();
}
if path.is_dir()
&& path.file_name().is_some_and(|name| name == "Stationeers")
&& path.join("Stationpedia").join("Stationpedia.json").exists()
{
path = path.join("Stationpedia");
}
if path.is_file()
&& path
.file_name()
.is_some_and(|name| name == "Stationpedia.json")
{
path = path.parent().unwrap().to_path_buf();
}
path
}
None => {
let mut path = workspace.clone();
if path.join("data").join("Stationpedia.json").exists()
&& path.join("data").join("Enums.json").exists()
{
path = path.join("data")
}
path
}
};
if path.is_dir()
&& path.join("Stationpedia.json").exists()
&& path.join("Enums.json").exists()
{
generate::generate(&path, &workspace)?;
} else {
return Err(Error::BadStationeersPath(path).into());
}
}
}
Ok(())
}
fn build<P: AsRef<std::ffi::OsStr> + std::fmt::Debug + std::fmt::Display>(
args: &Args,
packages: &[P],
release: bool,
workspace: &std::path::Path,
rest: &[std::ffi::OsString],
) -> Result<(), Error> {
if packages.is_empty() {
panic!("no package(s) specified")
}
eprintln!("Building packages: {:?}, release: {}", packages, release);
for package in packages {
eprintln!("Building package: {}", package);
eprintln!(
"Running command: {} build {} {} {}",
&args.wasm_pack,
if release { "--release" } else { "--dev" },
package,
rest.join(std::ffi::OsStr::new(" ")).to_string_lossy(),
);
let mut cmd = Command::new(&args.wasm_pack);
cmd.current_dir(workspace);
cmd.arg("build");
if release {
cmd.arg("--release");
} else {
cmd.arg("--dev");
}
cmd.arg(package);
cmd.args(rest);
let status = cmd
.status()
.map_err(|e| Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e))?;
if status.success() {
eprintln!("{} built successfully", package);
} else {
return Err(Error::BuildFailed(
package.to_string(),
format!("{cmd:?}"),
status,
));
}
}
Ok(())
}
fn pnpm_install(args: &Args, workspace: &std::path::Path) -> Result<ExitStatus, Error> {
eprintln!("Running `pnpm install`");
let mut cmd = Command::new(&args.manager);
cmd.current_dir(&workspace.join("www"));
cmd.args(["install"])
.status()
.map_err(|e| Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e))
}