Skip to main content

synta_codegen/
c_cmake_codegen.rs

1//! CMake build-system file generator for synta-generated C code.
2//!
3//! Produces a `CMakeLists.txt` (full or fragment) that:
4//! - Defines each ASN.1 module as a CMake `STATIC`/`SHARED` library target.
5//! - Expresses inter-module dependencies via `target_link_libraries`.
6//! - Locates the synta library and sets the required C99 standard.
7
8use crate::ast::Module;
9use crate::naming::module_file_stem;
10use crate::naming::to_pascal_case;
11use std::fmt::Write;
12
13/// Configuration for CMake file generation.
14#[derive(Debug, Clone, Default)]
15pub struct CMakeConfig {
16    /// Path to the synta source tree (the directory that contains `include/`
17    /// and `target/release/`).  When `None` the generated file uses a
18    /// `SYNTA_ROOT` CMake cache variable that the user must supply on the
19    /// `cmake` command line.
20    pub synta_root: Option<String>,
21    /// Build the generated library as `SHARED` instead of `STATIC`.
22    pub shared_library: bool,
23}
24
25/// Generate a `CMakeLists.txt` for one or more ASN.1 modules.
26///
27/// `modules` is the full slice of parsed modules; `order` is the topological
28/// generation order returned by [`crate::import_graph::topological_order`]
29/// (dependencies-first).  Pass `&[0]` when there is only one module.
30pub fn generate_cmake(
31    modules: &[Module],
32    order: &[usize],
33    config: CMakeConfig,
34) -> Result<String, Box<dyn std::error::Error>> {
35    let mut out = String::new();
36
37    // ── Header ────────────────────────────────────────────────────────────────
38    let module_names: Vec<&str> = order.iter().map(|&i| modules[i].name.as_str()).collect();
39    let project_name = if modules.len() == 1 {
40        to_pascal_case(modules[0].name.as_str())
41    } else {
42        // Use the last module in topological order (the primary "root" module
43        // that imports all the others).
44        to_pascal_case(modules[*order.last().unwrap()].name.as_str())
45    };
46
47    writeln!(
48        out,
49        "# Generated from ASN.1 module{} {}",
50        if modules.len() == 1 { "" } else { "s" },
51        module_names.join(", ")
52    )?;
53    writeln!(out, "# DO NOT EDIT - auto-generated code")?;
54    writeln!(out)?;
55    writeln!(out, "cmake_minimum_required(VERSION 3.10)")?;
56    writeln!(out, "project({} C)", project_name)?;
57    writeln!(out)?;
58    writeln!(out, "set(CMAKE_C_STANDARD 99)")?;
59    writeln!(out, "set(CMAKE_C_STANDARD_REQUIRED ON)")?;
60    writeln!(out)?;
61    writeln!(out, "if(MSVC)")?;
62    writeln!(out, "    add_compile_options(/W4)")?;
63    writeln!(out, "else()")?;
64    writeln!(out, "    add_compile_options(-Wall -Wextra)")?;
65    writeln!(out, "endif()")?;
66    writeln!(out)?;
67
68    // ── Synta location ────────────────────────────────────────────────────────
69    writeln!(out, "# Locate the synta library.")?;
70    writeln!(
71        out,
72        "# If the parent project already defines a Synta::Synta imported target,"
73    )?;
74    writeln!(out, "# this block is skipped automatically.")?;
75    writeln!(out, "if(NOT TARGET Synta::Synta)")?;
76
77    if let Some(ref root) = config.synta_root {
78        writeln!(out, "    set(_synta_root \"{}\")", root)?;
79    } else {
80        writeln!(out, "    if(NOT DEFINED SYNTA_ROOT)")?;
81        writeln!(out, "        set(SYNTA_ROOT \"\" CACHE PATH")?;
82        writeln!(out, "            \"Root of the synta source tree (contains include/ and target/release/)\")")?;
83        writeln!(out, "    endif()")?;
84        writeln!(out, "    if(\"${{SYNTA_ROOT}}\" STREQUAL \"\")")?;
85        writeln!(out, "        message(FATAL_ERROR")?;
86        writeln!(
87            out,
88            "            \"SYNTA_ROOT is not set. Pass it on the cmake command line:\\n\""
89        )?;
90        writeln!(
91            out,
92            "            \"  cmake -DSYNTA_ROOT=/path/to/synta -S . -B build\")"
93        )?;
94        writeln!(out, "    endif()")?;
95        writeln!(out, "    set(_synta_root \"${{SYNTA_ROOT}}\")")?;
96    }
97
98    writeln!(out, "    find_library(SYNTA_LIBRARY")?;
99    writeln!(out, "        NAMES synta")?;
100    writeln!(out, "        PATHS \"${{_synta_root}}/target/release\"")?;
101    writeln!(out, "        NO_DEFAULT_PATH")?;
102    writeln!(out, "    )")?;
103    writeln!(out, "    if(NOT SYNTA_LIBRARY)")?;
104    writeln!(out, "        message(FATAL_ERROR")?;
105    writeln!(
106        out,
107        "            \"synta library not found under ${{_synta_root}}/target/release\\n\""
108    )?;
109    writeln!(out, "            \"Build it first: cd ${{_synta_root}} && cargo build --release --features ffi\")")?;
110    writeln!(out, "    endif()")?;
111    writeln!(out, "    add_library(Synta::Synta UNKNOWN IMPORTED)")?;
112    writeln!(out, "    set_target_properties(Synta::Synta PROPERTIES")?;
113    writeln!(out, "        IMPORTED_LOCATION \"${{SYNTA_LIBRARY}}\"")?;
114    writeln!(
115        out,
116        "        INTERFACE_INCLUDE_DIRECTORIES \"${{_synta_root}}/include\""
117    )?;
118    writeln!(out, "    )")?;
119    writeln!(out, "endif()")?;
120    writeln!(out)?;
121
122    // ── Platform link dependencies ─────────────────────────────────────────────
123    writeln!(out, "# Platform-specific libraries required by libcsynta.")?;
124    writeln!(out, "if(UNIX AND NOT APPLE)")?;
125    writeln!(out, "    set(_synta_platform_libs pthread dl m)")?;
126    writeln!(out, "elseif(APPLE)")?;
127    writeln!(out, "    set(_synta_platform_libs pthread)")?;
128    writeln!(out, "elseif(WIN32)")?;
129    writeln!(out, "    set(_synta_platform_libs ws2_32 userenv bcrypt)")?;
130    writeln!(out, "endif()")?;
131    writeln!(out)?;
132
133    // ── Library targets ───────────────────────────────────────────────────────
134    let lib_type = if config.shared_library {
135        "SHARED"
136    } else {
137        "STATIC"
138    };
139
140    // Build a map from module name to target name for dependency edges.
141    let target_for: std::collections::HashMap<&str, String> = modules
142        .iter()
143        .map(|m| (m.name.as_str(), to_pascal_case(&m.name)))
144        .collect();
145
146    for &idx in order {
147        let module = &modules[idx];
148        let stem = module_file_stem(&module.name);
149        let target = to_pascal_case(&module.name);
150
151        writeln!(out, "# ASN.1 module: {}", module.name)?;
152        writeln!(out, "add_library({} {}", target, lib_type)?;
153        writeln!(out, "    {}.c", stem)?;
154        writeln!(out, ")")?;
155        writeln!(out, "target_include_directories({} PUBLIC", target)?;
156        writeln!(out, "    $<BUILD_INTERFACE:${{CMAKE_CURRENT_LIST_DIR}}>")?;
157        writeln!(out, "    $<INSTALL_INTERFACE:include>")?;
158        writeln!(out, ")")?;
159
160        // Collect inter-module deps that are in the known set.
161        let module_deps: Vec<&str> = module
162            .imports
163            .iter()
164            .filter_map(|imp| target_for.get(imp.module_name.as_str()).map(|t| t.as_str()))
165            .collect();
166
167        write!(out, "target_link_libraries({} PUBLIC", target)?;
168        for dep in &module_deps {
169            write!(out, "\n    {}", dep)?;
170        }
171        writeln!(out, "\n    Synta::Synta")?;
172        writeln!(out, "    ${{_synta_platform_libs}}")?;
173        writeln!(out, ")")?;
174        writeln!(out)?;
175    }
176
177    Ok(out)
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::parse;
184
185    #[test]
186    fn test_single_module_cmake() {
187        let m = parse("MyModule DEFINITIONS ::= BEGIN Foo ::= INTEGER END").unwrap();
188        let out = generate_cmake(&[m], &[0], CMakeConfig::default()).unwrap();
189
190        assert!(out.contains("cmake_minimum_required(VERSION 3.10)"));
191        assert!(out.contains("project(MyModule C)"));
192        assert!(out.contains("CMAKE_C_STANDARD 99"));
193        assert!(out.contains("add_library(MyModule STATIC"));
194        assert!(out.contains("my_module.c"));
195        assert!(out.contains("Synta::Synta"));
196        assert!(out.contains("SYNTA_ROOT"));
197    }
198
199    #[test]
200    fn test_cmake_with_synta_root() {
201        let m = parse("Cert DEFINITIONS ::= BEGIN END").unwrap();
202        let config = CMakeConfig {
203            synta_root: Some("/opt/synta".to_string()),
204            shared_library: false,
205        };
206        let out = generate_cmake(&[m], &[0], config).unwrap();
207
208        assert!(out.contains("set(_synta_root \"/opt/synta\")"));
209        // SYNTA_ROOT cache variable block should not appear
210        assert!(!out.contains("CACHE PATH"));
211    }
212
213    #[test]
214    fn test_cmake_shared_library() {
215        let m = parse("Foo DEFINITIONS ::= BEGIN END").unwrap();
216        let config = CMakeConfig {
217            shared_library: true,
218            ..Default::default()
219        };
220        let out = generate_cmake(&[m], &[0], config).unwrap();
221        assert!(out.contains("add_library(Foo SHARED"));
222    }
223
224    #[test]
225    fn test_multi_module_cmake_deps() {
226        let a = parse("ModA DEFINITIONS ::= BEGIN IMPORTS X FROM ModB; END").unwrap();
227        let b = parse("ModB DEFINITIONS ::= BEGIN END").unwrap();
228        // topological order: ModB (1) before ModA (0)
229        let order = vec![1usize, 0usize];
230        let out = generate_cmake(&[a, b], &order, CMakeConfig::default()).unwrap();
231
232        // ModB target defined
233        assert!(out.contains("add_library(ModB STATIC"));
234        // ModA target defined
235        assert!(out.contains("add_library(ModA STATIC"));
236        // ModA links against ModB
237        assert!(out.contains("target_link_libraries(ModA PUBLIC\n    ModB"));
238    }
239}