use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use crate::codegen::{GenerateError, GeneratedModuleSet, Generator};
use crate::config::GenerationConfig;
use crate::schema::Schema;
use crate::xml::{ParseError, parse, parse_file};
#[derive(Debug, thiserror::Error)]
pub enum BuildError {
#[error(transparent)]
Parse(#[from] ParseError),
#[error(transparent)]
Generate(#[from] GenerateError),
#[error("OUT_DIR is not set (run from a Cargo build.rs script)")]
MissingOutDir,
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("schema generated no modules")]
Empty,
}
pub fn generate_to_out_dir(
schema_path: impl AsRef<Path>,
config: GenerationConfig,
) -> Result<GeneratedModuleSet, BuildError> {
generate_to_dir(schema_path, config, &out_dir()?)
}
pub fn generate_to_dir(
schema_path: impl AsRef<Path>,
config: GenerationConfig,
out_dir: impl AsRef<Path>,
) -> Result<GeneratedModuleSet, BuildError> {
let schema_path = schema_path.as_ref();
let out_dir = out_dir.as_ref();
fs::create_dir_all(out_dir)?;
let ir = parse_file(schema_path)?;
let modules = write_generated(Schema::from_ir(ir), config, out_dir)?;
println!("cargo::rerun-if-changed={}", schema_path.display());
let is_cargo_out = env::var_os("OUT_DIR")
.map(|od| out_dir.starts_with(Path::new(&od)))
.unwrap_or(false);
if !is_cargo_out {
println!(
"cargo::warning=ergo-sbe wrote {} module(s) under {} (open for go-to-definition)",
modules.modules().len(),
out_dir.display()
);
}
Ok(modules)
}
pub fn generate_str_to_out_dir(
schema_xml: &str,
config: GenerationConfig,
) -> Result<GeneratedModuleSet, BuildError> {
generate_str_to_dir(schema_xml, config, &out_dir()?)
}
pub fn generate_str_to_dir(
schema_xml: &str,
config: GenerationConfig,
out_dir: &Path,
) -> Result<GeneratedModuleSet, BuildError> {
let ir = parse(schema_xml)?;
write_generated(Schema::from_ir(ir), config, out_dir)
}
pub fn out_dir() -> Result<PathBuf, BuildError> {
env::var_os("OUT_DIR")
.map(PathBuf::from)
.ok_or(BuildError::MissingOutDir)
}
fn write_generated(
schema: Schema,
config: GenerationConfig,
out: &Path,
) -> Result<GeneratedModuleSet, BuildError> {
let modules = Generator::new(config).generate(&schema)?;
if modules.modules().len() == 0 {
return Err(BuildError::Empty);
}
for m in modules.modules() {
let dest = out.join(&m.path);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&dest, &m.source)?;
}
for w in modules.warnings() {
println!("cargo::warning={w}");
}
Ok(modules)
}
#[macro_export]
macro_rules! include_sbe {
($module:literal) => {
include!(concat!(env!("OUT_DIR"), "/", $module, ".rs"));
};
($module:ident) => {
include!(concat!(env!("OUT_DIR"), "/", stringify!($module), ".rs"));
};
}
#[macro_export]
macro_rules! sbe_mod {
($name:ident) => {
mod $name {
#![allow(
dead_code,
unused_imports,
unused_variables,
unused_mut,
unused_assignments,
unused_must_use,
unused_comparisons,
non_camel_case_types,
non_snake_case,
unexpected_cfgs,
clippy::all
)]
include!(concat!(env!("OUT_DIR"), "/", stringify!($name), ".rs"));
}
};
($vis:vis $name:ident) => {
$vis mod $name {
#![allow(
dead_code,
unused_imports,
unused_variables,
unused_mut,
unused_assignments,
unused_must_use,
unused_comparisons,
non_camel_case_types,
non_snake_case,
unexpected_cfgs,
clippy::all
)]
include!(concat!(env!("OUT_DIR"), "/", stringify!($name), ".rs"));
}
};
($name:ident = $file:literal) => {
mod $name {
#![allow(
dead_code,
unused_imports,
unused_variables,
unused_mut,
unused_assignments,
unused_must_use,
unused_comparisons,
non_camel_case_types,
non_snake_case,
unexpected_cfgs,
clippy::all
)]
include!(concat!(env!("OUT_DIR"), "/", $file, ".rs"));
}
};
($vis:vis $name:ident = $file:literal) => {
$vis mod $name {
#![allow(
dead_code,
unused_imports,
unused_variables,
unused_mut,
unused_assignments,
unused_must_use,
unused_comparisons,
non_camel_case_types,
non_snake_case,
unexpected_cfgs,
clippy::all
)]
include!(concat!(env!("OUT_DIR"), "/", $file, ".rs"));
}
};
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_schema() -> &'static str {
r#"<?xml version="1.0"?>
<messageSchema package="t" id="1" version="0" byteOrder="littleEndian">
<types>
<composite name="messageHeader">
<type name="blockLength" primitiveType="uint16"/>
<type name="templateId" primitiveType="uint16"/>
<type name="schemaId" primitiveType="uint16"/>
<type name="version" primitiveType="uint16"/>
</composite>
</types>
<message name="Ping" id="1">
<field name="seq" id="1" type="uint32" offset="0"/>
</message>
</messageSchema>"#
}
#[test]
fn generate_str_to_dir_writes_module() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile_dir()?;
let set = generate_str_to_dir(minimal_schema(), GenerationConfig::new("ping"), &dir)?;
assert_eq!(set.modules().len(), 1);
let path = dir.join("ping.rs");
assert!(path.is_file(), "expected {}", path.display());
let src = fs::read_to_string(&path)?;
assert!(src.contains("PingEncoder"), "{src}");
assert!(src.contains("PingDecoder"), "{src}");
let _ = fs::remove_dir_all(&dir);
Ok(())
}
fn tempfile_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
let dir = env::temp_dir().join(format!(
"ergo_sbe_build_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos()
));
fs::create_dir_all(&dir)?;
Ok(dir)
}
}