forked from 1b-agent/opencadstudio-upstream
Adds Leader validation and annotation/dimension-style reference checks to ocs_plugin_api, maps 15 writable and 5 read-only fields in the adapter, and adds staged_python_leader_lifecycle_over_real_ipc covering C/R/E/D/U/I/V/P. The W gate stays open: acadrust's DXF writer omits the annotation handle and DXF reads drop annotation_offset; DWG R2010+ does not store text size or the hookline flag. Each gap is pinned by a canary assertion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1253 lines
45 KiB
Rust
1253 lines
45 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
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");
|
|
mod entity_coverage_schema {
|
|
include!("src/entity_coverage_types.rs");
|
|
}
|
|
use entity_coverage_schema::{
|
|
EntityCoverageCatalog, EntityKindCoverage, EntityScope, ModelAccess, PropertyCoverage,
|
|
};
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct EntityCoveragePolicy {
|
|
internal_kinds: Vec<String>,
|
|
opaque_kinds: Vec<String>,
|
|
editable: BTreeMap<String, Vec<String>>,
|
|
readable: BTreeMap<String, Vec<String>>,
|
|
aliases: BTreeMap<String, BTreeMap<String, String>>,
|
|
}
|
|
|
|
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={}",
|
|
workspace_cargo_lock_path().display()
|
|
);
|
|
println!("cargo:rerun-if-changed=entity_coverage_policy.json");
|
|
println!("cargo:rerun-if-changed=src/entity_coverage_types.rs");
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
// 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);
|
|
|
|
// Trace the complete record roots used by the editor and MCP API. Nested
|
|
// entity/object variants and their enums are added to the same registry.
|
|
type TraceFn = fn(&mut Tracer, &Samples);
|
|
let types: Vec<(&str, TraceFn)> = vec![
|
|
("EntityType", trace::<acadrust::EntityType>),
|
|
("ObjectType", trace::<acadrust::objects::ObjectType>),
|
|
(
|
|
"HeaderVariables",
|
|
trace::<acadrust::document::HeaderVariables>,
|
|
),
|
|
("SummaryInfo", trace::<acadrust::document::SummaryInfo>),
|
|
("LineType", trace::<acadrust::LineType>),
|
|
("TextStyle", trace::<acadrust::TextStyle>),
|
|
("BlockRecord", trace::<acadrust::BlockRecord>),
|
|
("DimStyle", trace::<acadrust::DimStyle>),
|
|
("AppId", trace::<acadrust::AppId>),
|
|
("View", trace::<acadrust::View>),
|
|
("VPort", trace::<acadrust::VPort>),
|
|
("Ucs", trace::<acadrust::Ucs>),
|
|
("VxTableRecord", trace::<acadrust::VxTableRecord>),
|
|
("DxfClass", trace::<acadrust::classes::DxfClass>),
|
|
(
|
|
"BlockVisibilityParameter",
|
|
trace::<acadrust::objects::BlockVisibilityParameter>,
|
|
),
|
|
("FieldDef", trace::<acadrust::document::FieldDef>),
|
|
(
|
|
"DgnLsDefinition",
|
|
trace::<acadrust::objects::DgnLsDefinition>,
|
|
),
|
|
("DgnLsComponent", trace::<acadrust::objects::DgnLsComponent>),
|
|
(
|
|
"NotificationCollection",
|
|
trace::<acadrust::notification::NotificationCollection>,
|
|
),
|
|
("Preview", trace::<acadrust::document::Preview>),
|
|
("Point", trace::<acadrust::Point>),
|
|
("Line", trace::<acadrust::Line>),
|
|
("Circle", trace::<acadrust::Circle>),
|
|
("Arc", trace::<acadrust::Arc>),
|
|
("Ellipse", trace::<acadrust::Ellipse>),
|
|
("Polyline", trace::<acadrust::Polyline>),
|
|
("Polyline2D", trace::<acadrust::entities::Polyline2D>),
|
|
("Polyline3D", trace::<acadrust::entities::Polyline3D>),
|
|
("LwPolyline", trace::<acadrust::LwPolyline>),
|
|
("MText", trace::<acadrust::entities::MText>),
|
|
("Spline", trace::<acadrust::Spline>),
|
|
("EntityCommon", trace::<acadrust::entities::EntityCommon>),
|
|
("Handle", trace::<acadrust::Handle>),
|
|
("Vector2", trace::<acadrust::Vector2>),
|
|
("Vector3", trace::<acadrust::Vector3>),
|
|
("Color", trace::<acadrust::Color>),
|
|
("Layer", trace::<acadrust::Layer>),
|
|
("XDataValue", trace::<acadrust::xdata::XDataValue>),
|
|
("XRecord", trace::<acadrust::objects::XRecord>),
|
|
("XRecordEntry", trace::<acadrust::objects::XRecordEntry>),
|
|
("XRecordValue", trace::<acadrust::objects::XRecordValue>),
|
|
(
|
|
"XRecordValueType",
|
|
trace::<acadrust::objects::XRecordValueType>,
|
|
),
|
|
("XRecordSection", trace::<acadrust::objects::XRecordSection>),
|
|
(
|
|
"DictionaryCloningFlags",
|
|
trace::<acadrust::objects::DictionaryCloningFlags>,
|
|
),
|
|
(
|
|
"KnownXRecordKind",
|
|
trace::<acadrust::objects::KnownXRecordKind>,
|
|
),
|
|
(
|
|
"ProxyObjectReference",
|
|
trace::<acadrust::objects::ProxyObjectReference>,
|
|
),
|
|
(
|
|
"ProxyReferenceKind",
|
|
trace::<acadrust::objects::ProxyReferenceKind>,
|
|
),
|
|
];
|
|
|
|
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
|
|
);
|
|
}
|
|
|
|
type TraceSimpleFn = fn(&mut Tracer);
|
|
let enums: Vec<(&str, TraceSimpleFn)> = vec![
|
|
(
|
|
"AssocAnnotationKind",
|
|
trace_simple::<acadrust::objects::AssocAnnotationKind>,
|
|
),
|
|
(
|
|
"AssocConstraintNodeData",
|
|
trace_simple::<acadrust::objects::AssocConstraintNodeData>,
|
|
),
|
|
(
|
|
"AssocEvalValue",
|
|
trace_simple::<acadrust::objects::AssocEvalValue>,
|
|
),
|
|
(
|
|
"AssocSubcurveKind",
|
|
trace_simple::<acadrust::objects::AssocSubcurveKind>,
|
|
),
|
|
(
|
|
"AssocSurfaceActionKind",
|
|
trace_simple::<acadrust::objects::AssocSurfaceActionKind>,
|
|
),
|
|
(
|
|
"AssocViewObjectActionParamKind",
|
|
trace_simple::<acadrust::objects::AssocViewObjectActionParamKind>,
|
|
),
|
|
(
|
|
"AcisVersion",
|
|
trace_simple::<acadrust::entities::AcisVersion>,
|
|
),
|
|
(
|
|
"AssociativeData",
|
|
trace_simple::<acadrust::objects::AssociativeData>,
|
|
),
|
|
(
|
|
"AttachmentPointType",
|
|
trace_simple::<acadrust::entities::AttachmentPointType>,
|
|
),
|
|
(
|
|
"BlockContentConnectionType",
|
|
trace_simple::<acadrust::entities::BlockContentConnectionType>,
|
|
),
|
|
(
|
|
"BlockEvalValue",
|
|
trace_simple::<acadrust::objects::BlockEvalValue>,
|
|
),
|
|
("BorderType", trace_simple::<acadrust::entities::BorderType>),
|
|
(
|
|
"BoundaryEdge",
|
|
trace_simple::<acadrust::entities::BoundaryEdge>,
|
|
),
|
|
(
|
|
"BreakFlowDirection",
|
|
trace_simple::<acadrust::entities::BreakFlowDirection>,
|
|
),
|
|
(
|
|
"CellAlignment",
|
|
trace_simple::<acadrust::objects::CellAlignment>,
|
|
),
|
|
(
|
|
"CellStyleType",
|
|
trace_simple::<acadrust::entities::CellStyleType>,
|
|
),
|
|
("CellType", trace_simple::<acadrust::entities::CellType>),
|
|
(
|
|
"CellValueType",
|
|
trace_simple::<acadrust::entities::CellValueType>,
|
|
),
|
|
(
|
|
"ClassObjectData",
|
|
trace_simple::<acadrust::objects::ClassObjectData>,
|
|
),
|
|
("ClipMode", trace_simple::<acadrust::entities::ClipMode>),
|
|
("ClipType", trace_simple::<acadrust::entities::ClipType>),
|
|
(
|
|
"CompoundEntry",
|
|
trace_simple::<acadrust::compound_file::CompoundEntry>,
|
|
),
|
|
(
|
|
"CompoundPropertyValue",
|
|
trace_simple::<acadrust::compound_file::CompoundPropertyValue>,
|
|
),
|
|
(
|
|
"CompoundStreamContent",
|
|
trace_simple::<acadrust::compound_file::CompoundStreamContent>,
|
|
),
|
|
(
|
|
"DataObjectData",
|
|
trace_simple::<acadrust::objects::DataObjectData>,
|
|
),
|
|
(
|
|
"DgnLineStyleData",
|
|
trace_simple::<acadrust::objects::DgnLineStyleData>,
|
|
),
|
|
(
|
|
"DgnLsComponentData",
|
|
trace_simple::<acadrust::objects::DgnLsComponentData>,
|
|
),
|
|
(
|
|
"DgnLsComponentType",
|
|
trace_simple::<acadrust::objects::DgnLsComponentType>,
|
|
),
|
|
(
|
|
"DgnLsPhaseMode",
|
|
trace_simple::<acadrust::objects::DgnLsPhaseMode>,
|
|
),
|
|
("DimSubtype", trace_simple::<acadrust::objects::DimSubtype>),
|
|
("Dimension", trace_simple::<acadrust::entities::Dimension>),
|
|
(
|
|
"DimensionType",
|
|
trace_simple::<acadrust::entities::DimensionType>,
|
|
),
|
|
(
|
|
"DynamicBlockData",
|
|
trace_simple::<acadrust::objects::DynamicBlockData>,
|
|
),
|
|
(
|
|
"EmbeddedEntity",
|
|
trace_simple::<acadrust::entities::EmbeddedEntity>,
|
|
),
|
|
(
|
|
"ExtendedEntityData",
|
|
trace_simple::<acadrust::entities::ExtendedEntityData>,
|
|
),
|
|
(
|
|
"FlowDirectionType",
|
|
trace_simple::<acadrust::entities::FlowDirectionType>,
|
|
),
|
|
(
|
|
"HatchPatternType",
|
|
trace_simple::<acadrust::entities::HatchPatternType>,
|
|
),
|
|
(
|
|
"HatchStyleType",
|
|
trace_simple::<acadrust::entities::HatchStyleType>,
|
|
),
|
|
(
|
|
"HelixConstraint",
|
|
trace_simple::<acadrust::entities::HelixConstraint>,
|
|
),
|
|
(
|
|
"HooklineDirection",
|
|
trace_simple::<acadrust::entities::HooklineDirection>,
|
|
),
|
|
(
|
|
"HorizontalAlignment",
|
|
trace_simple::<acadrust::entities::HorizontalAlignment>,
|
|
),
|
|
(
|
|
"LeaderContentType",
|
|
trace_simple::<acadrust::entities::LeaderContentType>,
|
|
),
|
|
(
|
|
"LeaderCreationType",
|
|
trace_simple::<acadrust::entities::LeaderCreationType>,
|
|
),
|
|
(
|
|
"LeaderDrawOrderType",
|
|
trace_simple::<acadrust::objects::LeaderDrawOrderType>,
|
|
),
|
|
(
|
|
"LeaderPathType",
|
|
trace_simple::<acadrust::entities::LeaderPathType>,
|
|
),
|
|
(
|
|
"LineTypeComplexContent",
|
|
trace_simple::<acadrust::tables::LineTypeComplexContent>,
|
|
),
|
|
(
|
|
"LegacyEntityData",
|
|
trace_simple::<acadrust::entities::LegacyEntityData>,
|
|
),
|
|
(
|
|
"MLineJustification",
|
|
trace_simple::<acadrust::entities::MLineJustification>,
|
|
),
|
|
("MTextFlag", trace_simple::<acadrust::entities::MTextFlag>),
|
|
(
|
|
"MaterialProceduralValue",
|
|
trace_simple::<acadrust::objects::MaterialProceduralValue>,
|
|
),
|
|
(
|
|
"MultiLeaderDrawOrderType",
|
|
trace_simple::<acadrust::objects::MultiLeaderDrawOrderType>,
|
|
),
|
|
(
|
|
"MultiLeaderPathType",
|
|
trace_simple::<acadrust::entities::MultiLeaderPathType>,
|
|
),
|
|
(
|
|
"ObjectContextKind",
|
|
trace_simple::<acadrust::objects::ObjectContextKind>,
|
|
),
|
|
(
|
|
"NotificationType",
|
|
trace_simple::<acadrust::notification::NotificationType>,
|
|
),
|
|
(
|
|
"OleFrameEnvelope",
|
|
trace_simple::<acadrust::entities::OleFrameEnvelope>,
|
|
),
|
|
(
|
|
"OleObjectType",
|
|
trace_simple::<acadrust::entities::OleObjectType>,
|
|
),
|
|
(
|
|
"PlotPaperUnits",
|
|
trace_simple::<acadrust::objects::PlotPaperUnits>,
|
|
),
|
|
(
|
|
"PlotRotation",
|
|
trace_simple::<acadrust::objects::PlotRotation>,
|
|
),
|
|
("PlotType", trace_simple::<acadrust::objects::PlotType>),
|
|
(
|
|
"PreviewFormat",
|
|
trace_simple::<acadrust::document::PreviewFormat>,
|
|
),
|
|
(
|
|
"PolyfaceSmoothType",
|
|
trace_simple::<acadrust::entities::PolyfaceSmoothType>,
|
|
),
|
|
(
|
|
"ProxyPayloadEncoding",
|
|
trace_simple::<acadrust::objects::ProxyPayloadEncoding>,
|
|
),
|
|
(
|
|
"ResolutionUnit",
|
|
trace_simple::<acadrust::objects::ResolutionUnit>,
|
|
),
|
|
("ScaledType", trace_simple::<acadrust::objects::ScaledType>),
|
|
(
|
|
"SemanticPropertyValue",
|
|
trace_simple::<acadrust::objects::SemanticPropertyValue>,
|
|
),
|
|
(
|
|
"ShadePlotMode",
|
|
trace_simple::<acadrust::objects::ShadePlotMode>,
|
|
),
|
|
(
|
|
"ShadePlotResolutionLevel",
|
|
trace_simple::<acadrust::objects::ShadePlotResolutionLevel>,
|
|
),
|
|
(
|
|
"SolidHistoryOperation",
|
|
trace_simple::<acadrust::objects::SolidHistoryOperation>,
|
|
),
|
|
(
|
|
"SurfaceData",
|
|
trace_simple::<acadrust::entities::SurfaceData>,
|
|
),
|
|
(
|
|
"SurfaceKind",
|
|
trace_simple::<acadrust::entities::SurfaceKind>,
|
|
),
|
|
(
|
|
"SurfaceSmoothType",
|
|
trace_simple::<acadrust::entities::SurfaceSmoothType>,
|
|
),
|
|
(
|
|
"TableBorderType",
|
|
trace_simple::<acadrust::objects::TableBorderType>,
|
|
),
|
|
(
|
|
"TableCellContentType",
|
|
trace_simple::<acadrust::entities::TableCellContentType>,
|
|
),
|
|
(
|
|
"TableFlowDirection",
|
|
trace_simple::<acadrust::objects::TableFlowDirection>,
|
|
),
|
|
(
|
|
"TextAlignmentType",
|
|
trace_simple::<acadrust::entities::TextAlignmentType>,
|
|
),
|
|
(
|
|
"TextAngleType",
|
|
trace_simple::<acadrust::entities::TextAngleType>,
|
|
),
|
|
(
|
|
"TextAttachmentDirectionType",
|
|
trace_simple::<acadrust::entities::TextAttachmentDirectionType>,
|
|
),
|
|
(
|
|
"TextAttachmentPointType",
|
|
trace_simple::<acadrust::entities::TextAttachmentPointType>,
|
|
),
|
|
(
|
|
"TextAttachmentType",
|
|
trace_simple::<acadrust::entities::TextAttachmentType>,
|
|
),
|
|
(
|
|
"TextHorizontalAlignment",
|
|
trace_simple::<acadrust::entities::TextHorizontalAlignment>,
|
|
),
|
|
(
|
|
"TextVerticalAlignment",
|
|
trace_simple::<acadrust::entities::TextVerticalAlignment>,
|
|
),
|
|
(
|
|
"UnderlayType",
|
|
trace_simple::<acadrust::entities::UnderlayType>,
|
|
),
|
|
(
|
|
"ValueUnitType",
|
|
trace_simple::<acadrust::entities::ValueUnitType>,
|
|
),
|
|
(
|
|
"VbaDirectoryValue",
|
|
trace_simple::<acadrust::vba::VbaDirectoryValue>,
|
|
),
|
|
(
|
|
"VerticalAlignment",
|
|
trace_simple::<acadrust::entities::VerticalAlignment>,
|
|
),
|
|
(
|
|
"ViewportRenderMode",
|
|
trace_simple::<acadrust::entities::ViewportRenderMode>,
|
|
),
|
|
(
|
|
"VisualStylePropertyValue",
|
|
trace_simple::<acadrust::objects::VisualStylePropertyValue>,
|
|
),
|
|
(
|
|
"ViewRepSketchGeometry",
|
|
trace_simple::<acadrust::objects::ViewRepSketchGeometry>,
|
|
),
|
|
(
|
|
"WipeoutClipMode",
|
|
trace_simple::<acadrust::entities::WipeoutClipMode>,
|
|
),
|
|
(
|
|
"WipeoutClipType",
|
|
trace_simple::<acadrust::entities::WipeoutClipType>,
|
|
),
|
|
("WireType", trace_simple::<acadrust::entities::WireType>),
|
|
];
|
|
for (name, trace) in enums {
|
|
trace(&mut tracer);
|
|
eprintln!("[ocs_plugin_api build] traced enum variants: {name}");
|
|
}
|
|
|
|
let traced = tracer
|
|
.registry()
|
|
.expect("type registry tracing failed; see stderr for individual errors");
|
|
let mut registry = map_to_custom_schema(&traced);
|
|
let mut section_style_tracer = Tracer::new(TracerConfig::default());
|
|
section_style_tracer
|
|
.trace_simple_type::<acadrust::entities::SectionViewStyle>()
|
|
.expect("section view style tracing failed");
|
|
let section_style_registry = section_style_tracer
|
|
.registry()
|
|
.expect("section view style registry failed");
|
|
let section_style = section_style_registry
|
|
.get("SectionViewStyle")
|
|
.expect("section view style registry entry");
|
|
registry.types.insert(
|
|
TypeId::new("EntitySectionViewStyle"),
|
|
map_container("EntitySectionViewStyle", section_style),
|
|
);
|
|
let json = serde_json::to_string_pretty(®istry).unwrap();
|
|
fs::write(out_dir.join("type_registry.json"), json).unwrap();
|
|
generate_entity_coverage(out_dir, ®istry);
|
|
}
|
|
|
|
fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
|
use std::collections::HashSet;
|
|
let policy: EntityCoveragePolicy =
|
|
serde_json::from_str(include_str!("entity_coverage_policy.json"))
|
|
.expect("valid entity coverage policy");
|
|
let variants = ®istry.types[&TypeId::new("EntityType")].variants;
|
|
let variant_names: HashSet<&str> = variants.iter().map(|v| v.name.as_str()).collect();
|
|
for name in policy
|
|
.internal_kinds
|
|
.iter()
|
|
.chain(&policy.opaque_kinds)
|
|
.chain(policy.editable.keys())
|
|
.chain(policy.readable.keys())
|
|
{
|
|
assert!(
|
|
variant_names.contains(name.as_str()),
|
|
"coverage policy names unknown kind: {name}"
|
|
);
|
|
}
|
|
for name in &policy.internal_kinds {
|
|
assert!(
|
|
!policy.opaque_kinds.contains(name) && !policy.editable.contains_key(name),
|
|
"internal kind has conflicting coverage: {name}"
|
|
);
|
|
}
|
|
for name in &policy.opaque_kinds {
|
|
assert!(
|
|
!policy.editable.contains_key(name),
|
|
"opaque kind is editable: {name}"
|
|
);
|
|
}
|
|
for name in policy.aliases.keys() {
|
|
assert!(
|
|
policy.editable.contains_key(name),
|
|
"aliases for non-editable kind: {name}"
|
|
);
|
|
}
|
|
|
|
let common = ®istry.types[&TypeId::new("EntityCommon")];
|
|
let mut entity_kinds = Vec::with_capacity(variants.len());
|
|
for variant in variants {
|
|
let kind = &variant.name;
|
|
let scope = if policy.internal_kinds.contains(kind) {
|
|
EntityScope::Internal
|
|
} else if policy.opaque_kinds.contains(kind) {
|
|
EntityScope::Opaque
|
|
} else {
|
|
EntityScope::Canvas
|
|
};
|
|
let editable = policy.editable.get(kind).cloned().unwrap_or_default();
|
|
let readable = policy.readable.get(kind).cloned().unwrap_or_default();
|
|
let mut unresolved: HashSet<String> = editable.iter().chain(&readable).cloned().collect();
|
|
assert_eq!(
|
|
unresolved.len(),
|
|
editable.len() + readable.len(),
|
|
"duplicate mapped property in {kind}"
|
|
);
|
|
let aliases = policy.aliases.get(kind);
|
|
let mut properties = vec![PropertyCoverage {
|
|
name: "kind".into(),
|
|
source_path: "<variant>".into(),
|
|
type_id: "String".into(),
|
|
optional: false,
|
|
is_sequence: false,
|
|
snapshot_readable: true,
|
|
model_access: ModelAccess::ReadOnly,
|
|
validation: "none".into(),
|
|
}];
|
|
for field in &common.fields {
|
|
let (name, access) = match field.name.as_str() {
|
|
"handle" => ("handle".to_owned(), ModelAccess::ReadOnly),
|
|
"owner_handle" => ("owner_handle".to_owned(), ModelAccess::ReadOnly),
|
|
"layer" if scope == EntityScope::Canvas => {
|
|
unresolved.remove("layer");
|
|
("layer".to_owned(), ModelAccess::ReadWrite)
|
|
}
|
|
"layer" => ("layer".to_owned(), ModelAccess::ReadOnly),
|
|
other => (format!("common.{other}"), ModelAccess::Unmapped),
|
|
};
|
|
properties.push(PropertyCoverage {
|
|
name,
|
|
source_path: format!("common.{}", field.name),
|
|
type_id: field.type_id.as_str().to_owned(),
|
|
optional: field.optional,
|
|
is_sequence: field.is_sequence,
|
|
snapshot_readable: true,
|
|
validation: if access == ModelAccess::ReadWrite && field.name == "layer" {
|
|
"transaction_nonempty"
|
|
} else if access == ModelAccess::ReadWrite {
|
|
"type_conversion_only"
|
|
} else {
|
|
"none"
|
|
}
|
|
.into(),
|
|
model_access: access,
|
|
});
|
|
}
|
|
let shape_type = variant
|
|
.fields
|
|
.first()
|
|
.expect("entity variant payload")
|
|
.type_id
|
|
.clone();
|
|
let shape = ®istry.types[&shape_type];
|
|
for field in &shape.fields {
|
|
if field.name == "common" {
|
|
continue;
|
|
}
|
|
let exposed = editable.iter().chain(&readable).find(|name| {
|
|
aliases
|
|
.and_then(|map| map.get(*name))
|
|
.map_or(name.as_str(), String::as_str)
|
|
== field.name
|
|
});
|
|
let (name, access) = match exposed {
|
|
Some(name) => {
|
|
unresolved.remove(name);
|
|
(
|
|
(*name).clone(),
|
|
if editable.contains(name) {
|
|
ModelAccess::ReadWrite
|
|
} else {
|
|
ModelAccess::ReadOnly
|
|
},
|
|
)
|
|
}
|
|
None => (field.name.clone(), ModelAccess::Unmapped),
|
|
};
|
|
let validation = if access != ModelAccess::ReadWrite {
|
|
"none"
|
|
} else if matches!(
|
|
(kind.as_str(), name.as_str()),
|
|
("Point", "location")
|
|
| ("Line", "start" | "end")
|
|
| ("Circle" | "Arc", "center" | "radius")
|
|
| ("Ray" | "XLine", "base_point" | "direction")
|
|
| (
|
|
"Solid",
|
|
"first_corner"
|
|
| "second_corner"
|
|
| "third_corner"
|
|
| "fourth_corner"
|
|
| "normal"
|
|
| "thickness"
|
|
)
|
|
| (
|
|
"Face3D",
|
|
"first_corner"
|
|
| "second_corner"
|
|
| "third_corner"
|
|
| "fourth_corner"
|
|
| "invisible_edges"
|
|
)
|
|
| (
|
|
"Insert",
|
|
"insert_point"
|
|
| "x_scale"
|
|
| "y_scale"
|
|
| "z_scale"
|
|
| "rotation"
|
|
| "normal"
|
|
| "column_count"
|
|
| "row_count"
|
|
| "column_spacing"
|
|
| "row_spacing"
|
|
)
|
|
| (
|
|
"Tolerance",
|
|
"insertion_point"
|
|
| "direction"
|
|
| "normal"
|
|
| "text"
|
|
| "dimension_style_name"
|
|
| "text_height"
|
|
| "dimension_gap"
|
|
)
|
|
| (
|
|
"Shape",
|
|
"insertion_point"
|
|
| "size"
|
|
| "shape_name"
|
|
| "shape_number"
|
|
| "rotation"
|
|
| "relative_x_scale"
|
|
| "oblique_angle"
|
|
| "normal"
|
|
| "thickness"
|
|
| "style_name"
|
|
)
|
|
| (
|
|
"AttributeDefinition",
|
|
"tag"
|
|
| "prompt"
|
|
| "default_value"
|
|
| "insertion_point"
|
|
| "alignment_point"
|
|
| "height"
|
|
| "rotation"
|
|
| "width_factor"
|
|
| "oblique_angle"
|
|
| "text_style"
|
|
| "text_generation_flags"
|
|
| "horizontal_alignment"
|
|
| "vertical_alignment"
|
|
| "flags"
|
|
| "field_length"
|
|
| "normal"
|
|
| "mtext_flag"
|
|
| "is_multiline"
|
|
| "line_count"
|
|
| "lock_position"
|
|
)
|
|
| (
|
|
"AttributeEntity",
|
|
"tag"
|
|
| "value"
|
|
| "insertion_point"
|
|
| "alignment_point"
|
|
| "height"
|
|
| "rotation"
|
|
| "width_factor"
|
|
| "oblique_angle"
|
|
| "text_style"
|
|
| "text_generation_flags"
|
|
| "horizontal_alignment"
|
|
| "vertical_alignment"
|
|
| "flags"
|
|
| "field_length"
|
|
| "normal"
|
|
| "mtext_flag"
|
|
| "is_multiline"
|
|
| "line_count"
|
|
| "lock_position"
|
|
)
|
|
| (
|
|
"Hatch",
|
|
"elevation"
|
|
| "normal"
|
|
| "is_solid"
|
|
| "pattern"
|
|
| "pattern_angle"
|
|
| "pattern_scale"
|
|
| "pattern_type"
|
|
| "is_double"
|
|
| "style"
|
|
| "is_associative"
|
|
| "pixel_size"
|
|
| "paths"
|
|
| "seed_points"
|
|
)
|
|
| (
|
|
"Leader",
|
|
"dimension_style"
|
|
| "arrow_enabled"
|
|
| "path_type"
|
|
| "creation_type"
|
|
| "hookline_direction"
|
|
| "hookline_enabled"
|
|
| "text_height"
|
|
| "text_width"
|
|
| "vertices"
|
|
| "annotation_handle"
|
|
| "normal"
|
|
| "horizontal_direction"
|
|
| "block_offset"
|
|
| "annotation_offset"
|
|
)
|
|
) {
|
|
"transaction_geometry"
|
|
} else {
|
|
"type_conversion_only"
|
|
};
|
|
properties.push(PropertyCoverage {
|
|
name,
|
|
source_path: field.name.clone(),
|
|
type_id: if exposed.is_some_and(|name| name == "closed") {
|
|
"bool".into()
|
|
} else {
|
|
field.type_id.as_str().to_owned()
|
|
},
|
|
optional: field.optional,
|
|
is_sequence: field.is_sequence,
|
|
snapshot_readable: true,
|
|
validation: validation.into(),
|
|
model_access: access,
|
|
});
|
|
}
|
|
assert!(
|
|
unresolved.is_empty(),
|
|
"{kind} has coverage keys absent from registry: {unresolved:?}"
|
|
);
|
|
entity_kinds.push(EntityKindCoverage {
|
|
kind: kind.clone(),
|
|
scope,
|
|
properties,
|
|
});
|
|
}
|
|
let catalog = EntityCoverageCatalog {
|
|
schema_version: 1,
|
|
entity_kinds,
|
|
};
|
|
fs::write(
|
|
out_dir.join("entity_coverage.json"),
|
|
serde_json::to_string_pretty(&catalog).unwrap(),
|
|
)
|
|
.expect("write entity coverage");
|
|
}
|
|
|
|
fn trace<T>(tracer: &mut Tracer, samples: &Samples)
|
|
where
|
|
T: serde::Serialize + DeserializeOwned,
|
|
{
|
|
let name = std::any::type_name::<T>();
|
|
if let Err(e) = tracer.trace_type::<T>(samples) {
|
|
eprintln!(
|
|
"[ocs_plugin_api build] warning: tracing {} failed: {}",
|
|
name, e
|
|
);
|
|
}
|
|
}
|
|
|
|
fn trace_simple<T>(tracer: &mut Tracer)
|
|
where
|
|
T: DeserializeOwned,
|
|
{
|
|
if let Err(error) = tracer.trace_simple_type::<T>() {
|
|
panic!(
|
|
"type registry enum tracing failed for {}: {error}",
|
|
std::any::type_name::<T>()
|
|
);
|
|
}
|
|
}
|
|
|
|
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
|
|
// need concrete serialized values.
|
|
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::Transparency::BY_LAYER);
|
|
let _ = tracer.trace_value(samples, &acadrust::Transparency::BY_BLOCK);
|
|
let _ = tracer.trace_value(samples, &acadrust::Transparency::OPAQUE);
|
|
|
|
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::entities::AttachmentPoint::TopLeft);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::AttachmentPoint::TopCenter);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::AttachmentPoint::TopRight);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::AttachmentPoint::MiddleLeft);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::AttachmentPoint::MiddleCenter);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::AttachmentPoint::MiddleRight);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::AttachmentPoint::BottomLeft);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::AttachmentPoint::BottomCenter);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::AttachmentPoint::BottomRight);
|
|
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::DrawingDirection::LeftToRight);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::DrawingDirection::TopToBottom);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::DrawingDirection::ByStyle);
|
|
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::LineSpacingStyle::AtLeast);
|
|
let _ = tracer.trace_value(samples, &acadrust::entities::LineSpacingStyle::Exactly);
|
|
|
|
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));
|
|
|
|
// XRecord value variants
|
|
let _ = tracer.trace_value(
|
|
samples,
|
|
&acadrust::objects::XRecordValue::String(String::new()),
|
|
);
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::XRecordValue::Double(0.0));
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::XRecordValue::Int16(0));
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::XRecordValue::Int32(0));
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::XRecordValue::Int64(0));
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::XRecordValue::Byte(0));
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::XRecordValue::Bool(false));
|
|
let _ = tracer.trace_value(
|
|
samples,
|
|
&acadrust::objects::XRecordValue::Handle(acadrust::Handle::default()),
|
|
);
|
|
let _ = tracer.trace_value(
|
|
samples,
|
|
&acadrust::objects::XRecordValue::Point3D(0.0, 0.0, 0.0),
|
|
);
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::XRecordValue::Chunk(Vec::new()));
|
|
|
|
// Proxy reference kinds pulled in by XRecord.object_references
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::ProxyReferenceKind::Undefined);
|
|
let _ = tracer.trace_value(
|
|
samples,
|
|
&acadrust::objects::ProxyReferenceKind::SoftOwnership,
|
|
);
|
|
let _ = tracer.trace_value(
|
|
samples,
|
|
&acadrust::objects::ProxyReferenceKind::HardOwnership,
|
|
);
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::ProxyReferenceKind::SoftPointer);
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::ProxyReferenceKind::HardPointer);
|
|
|
|
// KnownXRecordKind variants
|
|
let _ = tracer.trace_value(
|
|
samples,
|
|
&acadrust::objects::KnownXRecordKind::LayerViewportAlphaOverride,
|
|
);
|
|
let _ = tracer.trace_value(samples, &acadrust::objects::KnownXRecordKind::Unknown);
|
|
}
|
|
|
|
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<Format>) -> 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::<Vec<_>>()
|
|
.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<String> {
|
|
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::<Vec<_>>()
|
|
.join(",")
|
|
),
|
|
Format::Variable(_) => "Value".to_string(),
|
|
_ => "unknown".to_string(),
|
|
}
|
|
}
|
|
|
|
fn map_variant(discriminant: u32, named: &Named<VariantFormat>) -> 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 rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
|
|
let rustc_version = Command::new(rustc)
|
|
.arg("--version")
|
|
.output()
|
|
.ok()
|
|
.and_then(|o| {
|
|
if o.status.success() {
|
|
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
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()),
|
|
"rustc_version": rustc_version,
|
|
"api_version": 7,
|
|
"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")
|
|
}
|