33import type { FmuArchiveOptions } from "@modelscript/core" ;
44import {
55 Context ,
6+ FMI2_FUNCTIONS_H ,
7+ FMI2_FUNCTION_TYPES_H ,
8+ FMI2_TYPES_PLATFORM_H ,
69 ModelicaDAE ,
710 ModelicaFlattener ,
811 ModelicaLinter ,
912 ModelicaSimulator ,
1013 buildFmuArchive ,
14+ createZip ,
1115 generateFmu ,
16+ generateFmuCSources ,
1217} from "@modelscript/core" ;
1318import Modelica from "@modelscript/tree-sitter-modelica" ;
19+ import { execSync } from "node:child_process" ;
1420import fs from "node:fs" ;
21+ import os from "node:os" ;
1522import path from "node:path" ;
1623import Parser , { type Range } from "tree-sitter" ;
1724import type { CommandModule } from "yargs" ;
@@ -88,6 +95,11 @@ export const ExportFmu: CommandModule<{}, ExportFmuArgs> = {
8895 description : "include C source files in the FMU archive" ,
8996 type : "boolean" ,
9097 default : true ,
98+ } )
99+ . option ( "compile" , {
100+ description : "compile C sources into a shared library using the system C compiler" ,
101+ type : "boolean" ,
102+ default : false ,
91103 } ) ;
92104 // eslint-disable-next-line @typescript-eslint/no-explicit-any
93105 } ) as any ,
@@ -214,9 +226,96 @@ export const ExportFmu: CommandModule<{}, ExportFmuArgs> = {
214226 const result = buildFmuArchive ( dae , archiveOptions , simulator ) ;
215227 const outputPath = args . output ?? `${ modelIdentifier } .fmu` ;
216228
217- fs . writeFileSync ( outputPath , result . archive ) ;
229+ // ── Optional compilation ──
230+ if ( args . compile ) {
231+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "msc-fmu-" ) ) ;
232+ try {
233+ // Generate C sources to temp dir
234+ const sources = generateFmuCSources ( dae , result . fmuResult , archiveOptions ) ;
235+ const srcDir = path . join ( tmpDir , "sources" ) ;
236+ fs . mkdirSync ( srcDir , { recursive : true } ) ;
237+ fs . writeFileSync ( path . join ( srcDir , `${ modelIdentifier } _model.h` ) , sources . modelH ) ;
238+ fs . writeFileSync ( path . join ( srcDir , `${ modelIdentifier } _model.c` ) , sources . modelC ) ;
239+ fs . writeFileSync ( path . join ( srcDir , "fmi2Functions.c" ) , sources . fmi2FunctionsC ) ;
240+
241+ // Write FMI headers from archive
242+ const encoder = new TextEncoder ( ) ;
243+ for ( const [ name , content ] of Object . entries ( FMI_HEADERS ) ) {
244+ fs . writeFileSync ( path . join ( srcDir , name ) , encoder . encode ( content ) ) ;
245+ }
246+
247+ // Detect platform
248+ const arch = os . arch ( ) === "x64" ? "64" : "32" ;
249+ const plat =
250+ process . platform === "win32"
251+ ? `win${ arch } `
252+ : process . platform === "darwin"
253+ ? `darwin${ arch } `
254+ : `linux${ arch } ` ;
255+ const ext = process . platform === "win32" ? ".dll" : process . platform === "darwin" ? ".dylib" : ".so" ;
256+
257+ // Compile
258+ const cc = process . env . CC ?? "gcc" ;
259+ const binDir = path . join ( tmpDir , "binaries" , plat ) ;
260+ fs . mkdirSync ( binDir , { recursive : true } ) ;
261+ const sharedLib = path . join ( binDir , `${ modelIdentifier } ${ ext } ` ) ;
262+
263+ const ccCmd = [
264+ cc ,
265+ "-shared" ,
266+ "-fPIC" ,
267+ "-O2" ,
268+ "-Wall" ,
269+ "-Wextra" ,
270+ `-I${ srcDir } ` ,
271+ path . join ( srcDir , `${ modelIdentifier } _model.c` ) ,
272+ path . join ( srcDir , "fmi2Functions.c" ) ,
273+ "-o" ,
274+ sharedLib ,
275+ "-lm" ,
276+ ] . join ( " " ) ;
277+
278+ console . log ( `Compiling with: ${ cc } ` ) ;
279+ try {
280+ execSync ( ccCmd , { stdio : "pipe" } ) ;
281+ console . log ( ` Compiled: binaries/${ plat } /${ modelIdentifier } ${ ext } ` ) ;
282+ } catch ( compileErr ) {
283+ const msg =
284+ compileErr instanceof Error && "stderr" in compileErr
285+ ? ( compileErr as { stderr : Buffer } ) . stderr . toString ( )
286+ : String ( compileErr ) ;
287+ console . error ( `Compilation failed:\n${ msg } ` ) ;
288+ fs . writeFileSync ( outputPath , result . archive ) ;
289+ return ;
290+ }
218291
219- const types = [ ] ;
292+ // Rebuild archive with the compiled binary
293+ const finalEntries = new Map < string , Uint8Array > ( ) ;
294+ // Copy all original entries by reconstructing from the result
295+ finalEntries . set ( "modelDescription.xml" , encoder . encode ( result . fmuResult . modelDescriptionXml ) ) ;
296+ if ( archiveOptions . includeSources !== false ) {
297+ finalEntries . set ( `sources/${ modelIdentifier } _model.h` , encoder . encode ( sources . modelH ) ) ;
298+ finalEntries . set ( `sources/${ modelIdentifier } _model.c` , encoder . encode ( sources . modelC ) ) ;
299+ finalEntries . set ( "sources/fmi2Functions.c" , encoder . encode ( sources . fmi2FunctionsC ) ) ;
300+ finalEntries . set ( "sources/CMakeLists.txt" , encoder . encode ( sources . cmakeLists ) ) ;
301+ for ( const [ name , content ] of Object . entries ( FMI_HEADERS ) ) {
302+ finalEntries . set ( `sources/${ name } ` , encoder . encode ( content ) ) ;
303+ }
304+ }
305+ if ( archiveOptions . includeModelJson !== false ) {
306+ finalEntries . set ( "resources/model.json" , encoder . encode ( JSON . stringify ( dae . toJSON , null , 2 ) ) ) ;
307+ }
308+ finalEntries . set ( `binaries/${ plat } /${ modelIdentifier } ${ ext } ` , fs . readFileSync ( sharedLib ) ) ;
309+
310+ fs . writeFileSync ( outputPath , createZip ( finalEntries ) ) ;
311+ } finally {
312+ fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
313+ }
314+ } else {
315+ fs . writeFileSync ( outputPath , result . archive ) ;
316+ }
317+
318+ const types : string [ ] = [ ] ;
220319 if ( fmuType . modelExchange ) types . push ( "Model Exchange" ) ;
221320 if ( fmuType . coSimulation ) types . push ( "Co-Simulation" ) ;
222321
@@ -225,10 +324,22 @@ export const ExportFmu: CommandModule<{}, ExportFmuArgs> = {
225324 console . log ( ` GUID: ${ result . fmuResult . guid } ` ) ;
226325 console . log ( ` Variables: ${ result . fmuResult . scalarVariables . length } ` ) ;
227326 console . log ( ` States: ${ result . fmuResult . modelStructure . derivatives . length } ` ) ;
228- console . log ( ` Files: ${ result . files . length } ` ) ;
229- for ( const f of result . files ) {
327+ const fileList = args . compile
328+ ? result . files . concat ( [
329+ `binaries/.../${ modelIdentifier } ${ process . platform === "win32" ? ".dll" : process . platform === "darwin" ? ".dylib" : ".so" } ` ,
330+ ] )
331+ : result . files ;
332+ console . log ( ` Files: ${ fileList . length } ` ) ;
333+ for ( const f of fileList ) {
230334 console . log ( ` ${ f } ` ) ;
231335 }
232336 }
233337 } ,
234338} ;
339+
340+ /** FMI 2.0 header file map for compilation. */
341+ const FMI_HEADERS : Record < string , string > = {
342+ "fmi2Functions.h" : FMI2_FUNCTIONS_H ,
343+ "fmi2TypesPlatform.h" : FMI2_TYPES_PLATFORM_H ,
344+ "fmi2FunctionTypes.h" : FMI2_FUNCTION_TYPES_H ,
345+ } ;
0 commit comments