Skip to content

Commit 3858cb7

Browse files
committed
feat(cli): add --compile flag and cmake build system to fmu export
1 parent 1ec8b6b commit 3858cb7

3 files changed

Lines changed: 184 additions & 10 deletions

File tree

packages/cli/src/commands/export-fmu.ts

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@
33
import type { FmuArchiveOptions } from "@modelscript/core";
44
import {
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";
1318
import Modelica from "@modelscript/tree-sitter-modelica";
19+
import { execSync } from "node:child_process";
1420
import fs from "node:fs";
21+
import os from "node:os";
1522
import path from "node:path";
1623
import Parser, { type Range } from "tree-sitter";
1724
import 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+
};

packages/core/src/compiler/modelica/fmu-archive.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export function buildFmuArchive(
7575
files.set("sources/fmi2Functions.h", encoder.encode(FMI2_FUNCTIONS_H));
7676
files.set("sources/fmi2TypesPlatform.h", encoder.encode(FMI2_TYPES_PLATFORM_H));
7777
files.set("sources/fmi2FunctionTypes.h", encoder.encode(FMI2_FUNCTION_TYPES_H));
78+
79+
// CMake build system
80+
files.set("sources/CMakeLists.txt", encoder.encode(sources.cmakeLists));
7881
}
7982

8083
// ── model.json (serialized DAE for JS runtime) ──
@@ -95,7 +98,7 @@ export function buildFmuArchive(
9598

9699
// ── ZIP file builder (pure TypeScript, no external deps beyond pako) ──
97100

98-
function createZip(files: Map<string, Uint8Array>): Uint8Array {
101+
export function createZip(files: Map<string, Uint8Array>): Uint8Array {
99102
const centralDirectory: Uint8Array[] = [];
100103
const localFiles: Uint8Array[] = [];
101104
let offset = 0;
@@ -201,7 +204,7 @@ function crc32(data: Uint8Array): number {
201204

202205
// ── FMI 2.0 standard header files (minimal, self-contained) ──
203206

204-
const FMI2_TYPES_PLATFORM_H = `/* FMI 2.0 Type Platform — auto-included by ModelScript */
207+
export const FMI2_TYPES_PLATFORM_H = `/* FMI 2.0 Type Platform — auto-included by ModelScript */
205208
#ifndef fmi2TypesPlatform_h
206209
#define fmi2TypesPlatform_h
207210
@@ -253,7 +256,7 @@ typedef struct {
253256
#endif
254257
`;
255258

256-
const FMI2_FUNCTION_TYPES_H = `/* FMI 2.0 Function Types — auto-included by ModelScript */
259+
export const FMI2_FUNCTION_TYPES_H = `/* FMI 2.0 Function Types — auto-included by ModelScript */
257260
#ifndef fmi2FunctionTypes_h
258261
#define fmi2FunctionTypes_h
259262
@@ -276,7 +279,7 @@ typedef struct {
276279
#endif
277280
`;
278281

279-
const FMI2_FUNCTIONS_H = `/* FMI 2.0 Functions — auto-included by ModelScript */
282+
export const FMI2_FUNCTIONS_H = `/* FMI 2.0 Functions — auto-included by ModelScript */
280283
#ifndef fmi2Functions_h
281284
#define fmi2Functions_h
282285

packages/core/src/compiler/modelica/fmu-codegen.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export interface FmuCSourceFiles {
3737
modelC: string;
3838
/** fmi2Functions.c — FMI 2.0 API wrapper. */
3939
fmi2FunctionsC: string;
40+
/** CMakeLists.txt — build system for compiling the FMU shared library. */
41+
cmakeLists: string;
4042
}
4143

4244
/**
@@ -57,7 +59,10 @@ export function generateFmuCSources(dae: ModelicaDAE, fmuResult: FmuResult, opti
5759
// ── fmi2Functions.c ──
5860
const fmi2FunctionsC = generateFmi2FunctionsC(id, nVars, nStates, fmuResult);
5961

60-
return { modelH, modelC, fmi2FunctionsC };
62+
// ── CMakeLists.txt ──
63+
const cmakeLists = generateCMakeLists(id);
64+
65+
return { modelH, modelC, fmi2FunctionsC, cmakeLists };
6166
}
6267

6368
// ── Expression → C transpiler ──
@@ -685,7 +690,62 @@ function generateFmi2FunctionsC(id: string, nVars: number, nStates: number, resu
685690
return lines.join("\n");
686691
}
687692

688-
// ── Helper: extract der(x) name from expression ──
693+
// ── CMakeLists.txt generator ──
694+
695+
function generateCMakeLists(id: string): string {
696+
return `# Auto-generated by ModelScript — CMake build for FMU shared library
697+
cmake_minimum_required(VERSION 3.10)
698+
project(${id} C)
699+
700+
set(CMAKE_C_STANDARD 99)
701+
702+
# FMI platform identifier
703+
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
704+
set(FMI_PLATFORM_BITS "64")
705+
else()
706+
set(FMI_PLATFORM_BITS "32")
707+
endif()
708+
709+
if(WIN32)
710+
set(FMI_PLATFORM "win\${FMI_PLATFORM_BITS}")
711+
elseif(APPLE)
712+
set(FMI_PLATFORM "darwin\${FMI_PLATFORM_BITS}")
713+
else()
714+
set(FMI_PLATFORM "linux\${FMI_PLATFORM_BITS}")
715+
endif()
716+
717+
# Build shared library
718+
add_library(${id} SHARED
719+
${id}_model.c
720+
fmi2Functions.c
721+
)
722+
723+
target_include_directories(${id} PRIVATE \${CMAKE_CURRENT_SOURCE_DIR})
724+
725+
# Export FMI symbols, hide everything else
726+
set_target_properties(${id} PROPERTIES
727+
PREFIX ""
728+
C_VISIBILITY_PRESET hidden
729+
POSITION_INDEPENDENT_CODE ON
730+
)
731+
732+
if(MSVC)
733+
target_compile_definitions(${id} PRIVATE FMI2_FUNCTION_PREFIX=)
734+
else()
735+
target_compile_options(${id} PRIVATE -Wall -Wextra -O2)
736+
endif()
737+
738+
# Install into FMU-standard binaries/<platform>/ directory
739+
install(TARGETS ${id}
740+
LIBRARY DESTINATION binaries/\${FMI_PLATFORM}
741+
RUNTIME DESTINATION binaries/\${FMI_PLATFORM}
742+
)
743+
744+
message(STATUS "FMI platform: \${FMI_PLATFORM}")
745+
message(STATUS "Build with: cmake -B build && cmake --build build")
746+
message(STATUS "Library will be: build/${id}\${CMAKE_SHARED_LIBRARY_SUFFIX}")
747+
`;
748+
}
689749

690750
function extractDerName(expr: unknown): string | null {
691751
if (expr && typeof expr === "object" && "functionName" in expr && "args" in expr) {

0 commit comments

Comments
 (0)