use crate::ast::Module;
use crate::naming::module_file_stem;
use crate::naming::to_pascal_case;
use std::fmt::Write;
#[derive(Debug, Clone, Default)]
pub struct CMakeConfig {
pub synta_root: Option<String>,
pub shared_library: bool,
}
pub fn generate_cmake(
modules: &[Module],
order: &[usize],
config: CMakeConfig,
) -> Result<String, Box<dyn std::error::Error>> {
let mut out = String::new();
let module_names: Vec<&str> = order.iter().map(|&i| modules[i].name.as_str()).collect();
let project_name = if modules.len() == 1 {
to_pascal_case(modules[0].name.as_str())
} else {
to_pascal_case(modules[*order.last().unwrap()].name.as_str())
};
writeln!(
out,
"# Generated from ASN.1 module{} {}",
if modules.len() == 1 { "" } else { "s" },
module_names.join(", ")
)?;
writeln!(out, "# DO NOT EDIT - auto-generated code")?;
writeln!(out)?;
writeln!(out, "cmake_minimum_required(VERSION 3.10)")?;
writeln!(out, "project({} C)", project_name)?;
writeln!(out)?;
writeln!(out, "set(CMAKE_C_STANDARD 99)")?;
writeln!(out, "set(CMAKE_C_STANDARD_REQUIRED ON)")?;
writeln!(out)?;
writeln!(out, "if(MSVC)")?;
writeln!(out, " add_compile_options(/W4)")?;
writeln!(out, "else()")?;
writeln!(out, " add_compile_options(-Wall -Wextra)")?;
writeln!(out, "endif()")?;
writeln!(out)?;
writeln!(out, "# Locate the synta library.")?;
writeln!(
out,
"# If the parent project already defines a Synta::Synta imported target,"
)?;
writeln!(out, "# this block is skipped automatically.")?;
writeln!(out, "if(NOT TARGET Synta::Synta)")?;
if let Some(ref root) = config.synta_root {
writeln!(out, " set(_synta_root \"{}\")", root)?;
} else {
writeln!(out, " if(NOT DEFINED SYNTA_ROOT)")?;
writeln!(out, " set(SYNTA_ROOT \"\" CACHE PATH")?;
writeln!(out, " \"Root of the synta source tree (contains include/ and target/release/)\")")?;
writeln!(out, " endif()")?;
writeln!(out, " if(\"${{SYNTA_ROOT}}\" STREQUAL \"\")")?;
writeln!(out, " message(FATAL_ERROR")?;
writeln!(
out,
" \"SYNTA_ROOT is not set. Pass it on the cmake command line:\\n\""
)?;
writeln!(
out,
" \" cmake -DSYNTA_ROOT=/path/to/synta -S . -B build\")"
)?;
writeln!(out, " endif()")?;
writeln!(out, " set(_synta_root \"${{SYNTA_ROOT}}\")")?;
}
writeln!(out, " find_library(SYNTA_LIBRARY")?;
writeln!(out, " NAMES synta")?;
writeln!(out, " PATHS \"${{_synta_root}}/target/release\"")?;
writeln!(out, " NO_DEFAULT_PATH")?;
writeln!(out, " )")?;
writeln!(out, " if(NOT SYNTA_LIBRARY)")?;
writeln!(out, " message(FATAL_ERROR")?;
writeln!(
out,
" \"synta library not found under ${{_synta_root}}/target/release\\n\""
)?;
writeln!(out, " \"Build it first: cd ${{_synta_root}} && cargo build --release --features ffi\")")?;
writeln!(out, " endif()")?;
writeln!(out, " add_library(Synta::Synta UNKNOWN IMPORTED)")?;
writeln!(out, " set_target_properties(Synta::Synta PROPERTIES")?;
writeln!(out, " IMPORTED_LOCATION \"${{SYNTA_LIBRARY}}\"")?;
writeln!(
out,
" INTERFACE_INCLUDE_DIRECTORIES \"${{_synta_root}}/include\""
)?;
writeln!(out, " )")?;
writeln!(out, "endif()")?;
writeln!(out)?;
writeln!(out, "# Platform-specific libraries required by libcsynta.")?;
writeln!(out, "if(UNIX AND NOT APPLE)")?;
writeln!(out, " set(_synta_platform_libs pthread dl m)")?;
writeln!(out, "elseif(APPLE)")?;
writeln!(out, " set(_synta_platform_libs pthread)")?;
writeln!(out, "elseif(WIN32)")?;
writeln!(out, " set(_synta_platform_libs ws2_32 userenv bcrypt)")?;
writeln!(out, "endif()")?;
writeln!(out)?;
let lib_type = if config.shared_library {
"SHARED"
} else {
"STATIC"
};
let target_for: std::collections::HashMap<&str, String> = modules
.iter()
.map(|m| (m.name.as_str(), to_pascal_case(&m.name)))
.collect();
for &idx in order {
let module = &modules[idx];
let stem = module_file_stem(&module.name);
let target = to_pascal_case(&module.name);
writeln!(out, "# ASN.1 module: {}", module.name)?;
writeln!(out, "add_library({} {}", target, lib_type)?;
writeln!(out, " {}.c", stem)?;
writeln!(out, ")")?;
writeln!(out, "target_include_directories({} PUBLIC", target)?;
writeln!(out, " $<BUILD_INTERFACE:${{CMAKE_CURRENT_LIST_DIR}}>")?;
writeln!(out, " $<INSTALL_INTERFACE:include>")?;
writeln!(out, ")")?;
let module_deps: Vec<&str> = module
.imports
.iter()
.filter_map(|imp| target_for.get(imp.module_name.as_str()).map(|t| t.as_str()))
.collect();
write!(out, "target_link_libraries({} PUBLIC", target)?;
for dep in &module_deps {
write!(out, "\n {}", dep)?;
}
writeln!(out, "\n Synta::Synta")?;
writeln!(out, " ${{_synta_platform_libs}}")?;
writeln!(out, ")")?;
writeln!(out)?;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
#[test]
fn test_single_module_cmake() {
let m = parse("MyModule DEFINITIONS ::= BEGIN Foo ::= INTEGER END").unwrap();
let out = generate_cmake(&[m], &[0], CMakeConfig::default()).unwrap();
assert!(out.contains("cmake_minimum_required(VERSION 3.10)"));
assert!(out.contains("project(MyModule C)"));
assert!(out.contains("CMAKE_C_STANDARD 99"));
assert!(out.contains("add_library(MyModule STATIC"));
assert!(out.contains("my_module.c"));
assert!(out.contains("Synta::Synta"));
assert!(out.contains("SYNTA_ROOT"));
}
#[test]
fn test_cmake_with_synta_root() {
let m = parse("Cert DEFINITIONS ::= BEGIN END").unwrap();
let config = CMakeConfig {
synta_root: Some("/opt/synta".to_string()),
shared_library: false,
};
let out = generate_cmake(&[m], &[0], config).unwrap();
assert!(out.contains("set(_synta_root \"/opt/synta\")"));
assert!(!out.contains("CACHE PATH"));
}
#[test]
fn test_cmake_shared_library() {
let m = parse("Foo DEFINITIONS ::= BEGIN END").unwrap();
let config = CMakeConfig {
shared_library: true,
..Default::default()
};
let out = generate_cmake(&[m], &[0], config).unwrap();
assert!(out.contains("add_library(Foo SHARED"));
}
#[test]
fn test_multi_module_cmake_deps() {
let a = parse("ModA DEFINITIONS ::= BEGIN IMPORTS X FROM ModB; END").unwrap();
let b = parse("ModB DEFINITIONS ::= BEGIN END").unwrap();
let order = vec![1usize, 0usize];
let out = generate_cmake(&[a, b], &order, CMakeConfig::default()).unwrap();
assert!(out.contains("add_library(ModB STATIC"));
assert!(out.contains("add_library(ModA STATIC"));
assert!(out.contains("target_link_libraries(ModA PUBLIC\n ModB"));
}
}