Some checks failed
deploy preview to netlify / build (push) Has been cancelled
Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
417 lines
12 KiB
Rust
417 lines
12 KiB
Rust
use std::{
|
|
ffi::OsString,
|
|
process::{Command, ExitStatus},
|
|
};
|
|
|
|
use clap::{builder::TypedValueParser, Parser, Subcommand};
|
|
use strum::VariantNames;
|
|
|
|
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_GENERATE_TYPE: &[&str] = &["enums", "instructions", "database"];
|
|
const DEFAULT_GENERATE: &[&str] = &["enums"];
|
|
fn parse_generate_modules(s: &str) -> Result<String, String> {
|
|
if !VALID_GENERATE_TYPE.contains(&s) {
|
|
let valid_str = VALID_GENERATE_TYPE.join(", ");
|
|
return Err(format!(
|
|
"{s} is not a valid generate module. One of: {valid_str}"
|
|
));
|
|
}
|
|
Ok(s.to_string())
|
|
}
|
|
|
|
#[derive(
|
|
Debug, Clone, PartialEq, strum::EnumString, strum::Display, strum::VariantNames, strum::AsRefStr,
|
|
)]
|
|
enum VersionBumpType {
|
|
#[strum(serialize = "patch")]
|
|
Patch,
|
|
#[strum(serialize = "minor")]
|
|
Minor,
|
|
#[strum(serialize = "major")]
|
|
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(VersionBumpType::VARIANTS).map(|s| s.parse::<VersionBumpType>().unwrap()))]
|
|
bump: VersionBumpType,
|
|
},
|
|
/// Update changelog
|
|
Changelog {},
|
|
/// Generate modules and databases from extracted stationeers data
|
|
Generate {
|
|
#[arg(long, short = 'm', value_delimiter = ',', default_values = DEFAULT_GENERATE, value_parser = parse_generate_modules)]
|
|
modules: Vec<String>,
|
|
#[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>,
|
|
},
|
|
/// Update changelog and tag release, optionally bump the version at the same time
|
|
Tag {
|
|
#[arg(value_parser = clap::builder::PossibleValuesParser::new(VersionBumpType::VARIANTS).map(|s| s.parse::<VersionBumpType>().unwrap()))]
|
|
bump: Option<VersionBumpType>,
|
|
},
|
|
}
|
|
|
|
#[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 {} => {
|
|
start_server(&args, &workspace)?;
|
|
}
|
|
Task::Deploy {} => {
|
|
deploy_web(&args, &workspace)?;
|
|
}
|
|
Task::Version { bump } => {
|
|
bump_version(&args, &workspace, bump)?;
|
|
}
|
|
Task::Changelog {} => {
|
|
update_changelog(&workspace)?;
|
|
}
|
|
Task::Generate { modules, path } => {
|
|
generate_data(&workspace, modules, path)?;
|
|
}
|
|
Task::Tag { bump } => {
|
|
tag_release(&args, &workspace, bump.as_ref())?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn build_command<S, A, I, P>(program: S, args: I, working_dir: P) -> Command
|
|
where
|
|
S: AsRef<std::ffi::OsStr>,
|
|
A: AsRef<std::ffi::OsStr>,
|
|
I: IntoIterator<Item = A>,
|
|
P: AsRef<std::path::Path>,
|
|
{
|
|
let mut cmd = Command::new(program);
|
|
cmd.current_dir(working_dir);
|
|
cmd.args(args);
|
|
cmd
|
|
}
|
|
|
|
fn run_command<S, A, I, P>(
|
|
program: S,
|
|
args: I,
|
|
working_dir: P,
|
|
) -> Result<(ExitStatus, Command), Error>
|
|
where
|
|
S: AsRef<std::ffi::OsStr>,
|
|
A: AsRef<std::ffi::OsStr>,
|
|
I: IntoIterator<Item = A>,
|
|
P: AsRef<std::path::Path>,
|
|
{
|
|
let mut cmd = build_command(program, args, working_dir);
|
|
let status = cmd
|
|
.status()
|
|
.map_err(|e| Error::Command(format!("{}", cmd.get_program().to_string_lossy()), e))?;
|
|
Ok((status, cmd))
|
|
}
|
|
|
|
fn start_server(args: &Args, workspace: &std::path::Path) -> Result<(), Error> {
|
|
pnpm_install(&args, &workspace)?;
|
|
eprintln!("Starting server");
|
|
run_command(&args.manager, ["run", "start"], &workspace.join("www"))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn deploy_web(args: &Args, workspace: &std::path::Path) -> Result<(), Error> {
|
|
pnpm_install(&args, &workspace)?;
|
|
eprintln!("Production Build");
|
|
run_command(&args.manager, ["run", "build"], &workspace.join("www"))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn bump_version(
|
|
args: &Args,
|
|
workspace: &std::path::Path,
|
|
bump: &VersionBumpType,
|
|
) -> Result<(), Error> {
|
|
run_command(
|
|
"cargo",
|
|
["set-version", "--bump", bump.as_ref()],
|
|
&workspace,
|
|
)?;
|
|
run_command(
|
|
&args.manager,
|
|
["version", bump.as_ref()],
|
|
&workspace.join("www"),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn update_changelog(workspace: &std::path::Path) -> Result<(), Error> {
|
|
run_command(
|
|
"git-changelog",
|
|
[
|
|
"-io",
|
|
"CHANGELOG.md",
|
|
"-t",
|
|
"path:CHANGELOG.md.jinja",
|
|
"-c",
|
|
"conventional",
|
|
"--sections",
|
|
"chore,feat,fix,perf,revert",
|
|
"--bump",
|
|
VERSION.unwrap_or("auto"),
|
|
"--parse-refs",
|
|
"--trailers",
|
|
],
|
|
&workspace,
|
|
)?;
|
|
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 cmd_args: [std::ffi::OsString; 3] = [
|
|
"build".into(),
|
|
if release {
|
|
"--release".into()
|
|
} else {
|
|
"--dev".into()
|
|
},
|
|
package.into(),
|
|
];
|
|
let (status, cmd) = run_command(&args.wasm_pack, [&cmd_args, rest].concat(), workspace)?;
|
|
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 (status, _) = run_command(&args.manager, ["install"], &workspace.join("www"))?;
|
|
Ok(status)
|
|
}
|
|
|
|
fn generate_data(
|
|
workspace: &std::path::Path,
|
|
modules: &Vec<String>,
|
|
path: &Option<OsString>,
|
|
) -> color_eyre::Result<()> {
|
|
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.to_path_buf();
|
|
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,
|
|
&modules.iter().map(String::as_str).collect::<Vec<_>>(),
|
|
)?;
|
|
} else {
|
|
return Err(Error::BadStationeersPath(path).into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn tag_release(
|
|
args: &Args,
|
|
workspace: &std::path::Path,
|
|
bump: Option<&VersionBumpType>,
|
|
) -> Result<(), Error> {
|
|
let mut version = semver::Version::parse(VERSION.expect("package version to be set"))
|
|
.expect("package version to parse");
|
|
if let Some(bump) = bump {
|
|
eprintln!("Bumping {} version", bump.as_ref());
|
|
bump_version(args, workspace, bump)?;
|
|
match bump {
|
|
&VersionBumpType::Major => {
|
|
version.major += 1;
|
|
version.minor = 0;
|
|
version.patch = 0;
|
|
}
|
|
&VersionBumpType::Minor => {
|
|
version.minor += 1;
|
|
version.patch = 0;
|
|
}
|
|
&VersionBumpType::Patch => {
|
|
version.patch += 1;
|
|
}
|
|
}
|
|
}
|
|
update_changelog(workspace)?;
|
|
let tag_ver = format!("v{}", version);
|
|
run_command("git", ["add", "."], &workspace)?;
|
|
run_command(
|
|
"git",
|
|
["commit", "-m", &format!("tagging version {}", &tag_ver)],
|
|
&workspace,
|
|
)?;
|
|
run_command(
|
|
"git",
|
|
[
|
|
"tag",
|
|
"-f",
|
|
"-s",
|
|
&tag_ver,
|
|
"-m",
|
|
&format!("Version {}", version),
|
|
],
|
|
&workspace,
|
|
)?;
|
|
Ok(())
|
|
}
|