use std::collections::BTreeMap; use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::time::SystemTime; use cargo_lock::Lockfile; use serde::de::DeserializeOwned; use serde_reflection::{ ContainerFormat, Format, Named, Registry, Samples, Tracer, TracerConfig, VariantFormat, }; // Include the stable schema types so the same definitions are used at build // time and at runtime. The file is self-contained and only depends on serde. include!("src/type_registry_types.rs"); fn main() { let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); generate_type_registry(&out_dir); generate_version_info(&out_dir); println!("cargo:rerun-if-changed=Cargo.lock"); } // ════════════════════════════════════════════════════════════════════════════ // Type registry // ════════════════════════════════════════════════════════════════════════════ fn generate_type_registry(out_dir: &Path) { let mut tracer = Tracer::new(TracerConfig::default()); let mut samples = Samples::new(); add_enum_samples(&mut tracer, &mut samples); // Curated allow-list of acadrust types. We intentionally exclude // EntityType here to avoid pulling in all 41+ entity variants and any // custom-serializer edge cases. type TraceFn = fn(&mut Tracer, &Samples); let types: Vec<(&str, TraceFn)> = vec![ ("Point", trace::), ("Line", trace::), ("Circle", trace::), ("Arc", trace::), ("Ellipse", trace::), ("Polyline", trace::), ("Polyline2D", trace::), ("Polyline3D", trace::), ("LwPolyline", trace::), ("Spline", trace::), ("EntityCommon", trace::), ("Handle", trace::), ("Vector2", trace::), ("Vector3", trace::), ("Color", trace::), ("Layer", trace::), ("XDataValue", trace::), ]; for (name, f) in types { f(&mut tracer, &samples); // serde-reflection accumulates named types as it traces; we only need // to ensure the seed types are recorded even if a nested trace fails. eprintln!("[ocs_plugin_api build] traced type registry entry: {}", name); } let traced = tracer .registry() .expect("type registry tracing failed; see stderr for individual errors"); let registry = map_to_custom_schema(&traced); let json = serde_json::to_string_pretty(®istry).unwrap(); fs::write(out_dir.join("type_registry.json"), json).unwrap(); } fn trace(tracer: &mut Tracer, samples: &Samples) where T: serde::Serialize + DeserializeOwned, { let name = std::any::type_name::(); if let Err(e) = tracer.trace_type::(samples) { eprintln!("[ocs_plugin_api build] warning: tracing {} failed: {}", name, e); } } fn add_enum_samples(tracer: &mut Tracer, samples: &mut Samples) { // serde-reflection needs at least one sample value per enum variant in // order to reconstruct the full schema. Provide samples for the enums that // appear inside the allow-list types below. let _ = tracer.trace_value(samples, &acadrust::LineWeight::ByLayer); let _ = tracer.trace_value(samples, &acadrust::LineWeight::ByBlock); let _ = tracer.trace_value(samples, &acadrust::LineWeight::Default); let _ = tracer.trace_value(samples, &acadrust::LineWeight::Value(0)); let _ = tracer.trace_value(samples, &acadrust::entities::SmoothSurfaceType::None); let _ = tracer.trace_value(samples, &acadrust::entities::SmoothSurfaceType::QuadraticBSpline); let _ = tracer.trace_value(samples, &acadrust::entities::SmoothSurfaceType::CubicBSpline); let _ = tracer.trace_value(samples, &acadrust::entities::SmoothSurfaceType::Bezier); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::String(String::new())); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::ControlString(String::new())); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::LayerName(String::new())); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::BinaryData(Vec::new())); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::Handle(acadrust::Handle::default())); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::Point3D(acadrust::Vector3::default())); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::Position3D(acadrust::Vector3::default())); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::Displacement3D(acadrust::Vector3::default())); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::Direction3D(acadrust::Vector3::default())); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::Real(0.0)); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::Distance(0.0)); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::ScaleFactor(0.0)); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::Integer16(0)); let _ = tracer.trace_value(samples, &acadrust::xdata::XDataValue::Integer32(0)); } fn map_to_custom_schema(traced: &Registry) -> TypeRegistry { let mut types = BTreeMap::new(); for (name, format) in traced.iter() { let info = map_container(name, format); types.insert(TypeId(name.clone()), info); } TypeRegistry { types } } fn map_container(name: &str, format: &ContainerFormat) -> TypeInfo { match format { ContainerFormat::Struct(fields) => TypeInfo { name: TypeId(name.to_string()), kind: TypeKind::Struct, fields: fields.iter().map(map_field).collect(), variants: vec![], methods: vec![], doc: None, }, ContainerFormat::Enum(variants) => TypeInfo { name: TypeId(name.to_string()), kind: TypeKind::Enum, fields: vec![], variants: variants .iter() .map(|(idx, v)| map_variant(*idx, v)) .collect(), methods: vec![], doc: None, }, ContainerFormat::NewTypeStruct(format) => TypeInfo { name: TypeId(name.to_string()), kind: TypeKind::Newtype, fields: vec![FieldInfo { name: "0".to_string(), ..map_format_field(format) }], variants: vec![], methods: vec![], doc: None, }, ContainerFormat::TupleStruct(formats) => TypeInfo { name: TypeId(name.to_string()), kind: TypeKind::Tuple, fields: formats .iter() .enumerate() .map(|(i, f)| FieldInfo { name: i.to_string(), ..map_format_field(f) }) .collect(), variants: vec![], methods: vec![], doc: None, }, ContainerFormat::UnitStruct => TypeInfo { name: TypeId(name.to_string()), kind: TypeKind::Unit, fields: vec![], variants: vec![], methods: vec![], doc: None, }, } } fn map_field(named: &Named) -> FieldInfo { let mut field = map_format_field(&named.value); field.name = named.name.clone(); field } fn map_format_field(format: &Format) -> FieldInfo { if let Some(name) = primitive_format_name(format) { return FieldInfo { name: String::new(), type_id: TypeId(name), optional: false, is_sequence: false, }; } match format { Format::TypeName(name) => FieldInfo { name: String::new(), type_id: TypeId(name.clone()), optional: false, is_sequence: false, }, Format::Option(inner) => { let mut field = map_format_field(inner); field.optional = true; field } Format::Seq(inner) => { let mut field = map_format_field(inner); field.is_sequence = true; field } Format::TupleArray { content, size } => { let mut field = map_format_field(content); field.type_id = TypeId(format!("[{}; {}]", type_id_of_format(content), size)); field.is_sequence = true; field } Format::Map { key, value } => FieldInfo { name: String::new(), type_id: TypeId(format!( "Map<{}, {}>", type_id_of_format(key), type_id_of_format(value) )), optional: false, is_sequence: false, }, Format::Tuple(formats) => FieldInfo { name: String::new(), type_id: TypeId(format!( "({})", formats .iter() .map(type_id_of_format) .collect::>() .join(",") )), optional: false, is_sequence: false, }, Format::Variable(_) => FieldInfo { name: String::new(), type_id: TypeId("Value".to_string()), optional: false, is_sequence: false, }, _ => FieldInfo { name: String::new(), type_id: TypeId("unknown".to_string()), optional: false, is_sequence: false, }, } } fn primitive_format_name(format: &Format) -> Option { let name = match format { Format::Unit => "()", Format::Bool => "bool", Format::I8 => "i8", Format::I16 => "i16", Format::I32 => "i32", Format::I64 => "i64", Format::I128 => "i128", Format::U8 => "u8", Format::U16 => "u16", Format::U32 => "u32", Format::U64 => "u64", Format::U128 => "u128", Format::F32 => "f32", Format::F64 => "f64", Format::Char => "char", Format::Str => "String", Format::Bytes => "bytes", _ => return None, }; Some(name.to_string()) } fn type_id_of_format(format: &Format) -> String { if let Some(name) = primitive_format_name(format) { return name; } match format { Format::TypeName(name) => name.clone(), Format::Option(inner) => format!("Option<{}>", type_id_of_format(inner)), Format::Seq(inner) => format!("Vec<{}>", type_id_of_format(inner)), Format::TupleArray { content, size } => { format!("[{}; {}]", type_id_of_format(content), size) } Format::Map { key, value } => format!( "Map<{}, {}>", type_id_of_format(key), type_id_of_format(value) ), Format::Tuple(formats) => format!( "({})", formats .iter() .map(type_id_of_format) .collect::>() .join(",") ), Format::Variable(_) => "Value".to_string(), _ => "unknown".to_string(), } } fn map_variant(discriminant: u32, named: &Named) -> EnumVariantInfo { let fields = match &named.value { VariantFormat::Unit => vec![], VariantFormat::Variable(_) => vec![], VariantFormat::NewType(format) => vec![FieldInfo { name: "0".to_string(), ..map_format_field(format) }], VariantFormat::Tuple(formats) => formats .iter() .enumerate() .map(|(i, f)| FieldInfo { name: i.to_string(), ..map_format_field(f) }) .collect(), VariantFormat::Struct(fields) => fields.iter().map(map_field).collect(), }; EnumVariantInfo { name: named.name.clone(), discriminant, fields, } } // ════════════════════════════════════════════════════════════════════════════ // Version info // ════════════════════════════════════════════════════════════════════════════ fn generate_version_info(out_dir: &Path) { let lock_path = workspace_cargo_lock_path(); let lockfile = Lockfile::load(&lock_path).expect("load Cargo.lock"); // Use Cargo.lock's mtime as the build timestamp so the embedded JSON stays // stable across normal incremental builds and only changes when dependencies // are updated. Stored as Unix seconds to avoid an extra date-formatting // dependency in the build script. let build_timestamp = fs::metadata(&lock_path) .and_then(|m| m.modified()) .and_then(|t| { t.duration_since(SystemTime::UNIX_EPOCH) .map(|d| d.as_secs()) .map_err(std::io::Error::other) }) .unwrap_or(0); let ocs = lockfile .packages .iter() .find(|p| p.name.as_str() == "OpenCADStudio") .expect("OpenCADStudio package in Cargo.lock"); let acadrust = lockfile .packages .iter() .find(|p| p.name.as_str() == "acadrust") .expect("acadrust package in Cargo.lock"); let info = serde_json::json!({ "ocs_version": ocs.version.to_string(), "ocs_plugin_api_version": env!("CARGO_PKG_VERSION"), "acadrust_version": acadrust.version.to_string(), "acadrust_source": acadrust.source.as_ref().map(|s| s.to_string()), "api_version": 4, "api_version_min_supported": 2, "build_timestamp": build_timestamp, }); fs::write(out_dir.join("version_info.json"), info.to_string()).unwrap(); } fn workspace_cargo_lock_path() -> PathBuf { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); // CARGO_MANIFEST_DIR is crates/ocs_plugin_api; walk up to the workspace root. manifest_dir .parent() .and_then(|p| p.parent()) .map(|p| p.join("Cargo.lock")) .expect("Cargo.lock in workspace root") }