From f6a2e9370b733687c4b1f2eb81b09ec2989845a0 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 8 Jun 2026 13:56:06 +0200 Subject: [PATCH] feat(minerva): wire real host harness adapter Refs #687 --- docs/export/minerva.md | 26 +++- .../ROOT/pages/how-to/minerva-export.adoc | 27 +++- .../api/skainet-compile-minerva.api | 4 + .../skainet-compile-minerva/build.gradle.kts | 20 +++ .../minerva/MinervaHostVerificationModels.kt | 4 + .../minerva/MinervaJvmCompilerAndPackager.kt | 139 +++++++++++++++++- .../examples/MinervaTinyMlpExportSample.kt | 24 ++- .../MinervaJvmCompilerAndPackagerTest.kt | 55 ++++++- .../MinervaTinyMlpExportSampleTest.kt | 10 +- 9 files changed, 298 insertions(+), 11 deletions(-) diff --git a/docs/export/minerva.md b/docs/export/minerva.md index d344df0e..6563ed05 100644 --- a/docs/export/minerva.md +++ b/docs/export/minerva.md @@ -15,6 +15,10 @@ export MINERVA_CALIBRATION_NPZ=/secure/project/calibration.npz export MINERVA_KEY_FILE=/secure/project/device.key export MINERVA_RUN_CMAKE=true export MINERVA_RUN_CTEST=true +export MINERVA_HOST_ADAPTER_SOURCE=/secure/project/minerva_runtime_adapter.c +export MINERVA_HOST_INCLUDE_DIRS=/opt/libminerva/include +export MINERVA_HOST_LIBRARY_DIRS=/opt/libminerva/lib +export MINERVA_HOST_LIBRARIES=minerva ``` Do not commit real device keys. `include/secrets.example.h` contains placeholders only. @@ -65,6 +69,7 @@ build/minerva/TinySecureMlp/ host/ CMakeLists.txt main.c + runtime_adapter.example.c reference-input.txt reference-output.txt observed-output.txt # optional output from a real host run @@ -76,17 +81,30 @@ build/minerva/TinySecureMlp/ Host verification checks package structure, generated weight files, `model.npz` integrity, placeholder secret hygiene, and SKaiNET reference fixture generation. The packager writes deterministic `host/reference-input.txt` and `host/reference-output.txt` files and records them in `manifest.json`. A real host run can write comma- or whitespace-separated float outputs to `host/observed-output.txt` for zero-config parity comparison. +The generated host harness has a stable adapter ABI: + +```c +int minerva_run_inference(const float *input, int input_count, float *output, int output_count); +``` + +Copy `host/runtime_adapter.example.c` outside the generated bundle, wire that function to the pinned libminerva runtime, then point CMake at the adapter source. This keeps SKaiNET from hard-coding unverified libminerva runtime entry point names. + Add these metadata keys to opt into CMake, CTest, and parity comparison with a custom host output file: ```kotlin metadata = mapOf( MinervaHostVerificationMetadata.RUN_CMAKE_BUILD to "true", MinervaHostVerificationMetadata.RUN_CTEST to "true", - MinervaHostVerificationMetadata.HOST_OUTPUT_PATH to "host-output.txt" + MinervaHostVerificationMetadata.HOST_OUTPUT_PATH to "host-output.txt", + MinervaHostVerificationMetadata.HOST_ADAPTER_SOURCE to "/secure/project/minerva_runtime_adapter.c", + MinervaHostVerificationMetadata.HOST_INCLUDE_DIRS to "/opt/libminerva/include", + MinervaHostVerificationMetadata.HOST_LIBRARY_DIRS to "/opt/libminerva/lib", + MinervaHostVerificationMetadata.HOST_LIBRARIES to "minerva" ) ``` `HOST_OUTPUT_PATH` is optional when the host run writes `host/observed-output.txt`. +The include, library directory, and library values are passed to CMake as semicolon-separated lists, matching CMake list syntax. CI recipe: @@ -97,7 +115,11 @@ CI recipe: -Pminerva.runtimeRoot="$MINERVA_RUNTIME_ROOT" \ -Pminerva.compilerScript="$MINERVA_COMPILER_SCRIPT" \ -Pminerva.calibrationNpz="$MINERVA_CALIBRATION_NPZ" \ - -Pminerva.keyFile="$MINERVA_KEY_FILE" + -Pminerva.keyFile="$MINERVA_KEY_FILE" \ + -Pminerva.hostVerification.hostAdapterSource="$MINERVA_HOST_ADAPTER_SOURCE" \ + -Pminerva.hostVerification.hostIncludeDirs="$MINERVA_HOST_INCLUDE_DIRS" \ + -Pminerva.hostVerification.hostLibraryDirs="$MINERVA_HOST_LIBRARY_DIRS" \ + -Pminerva.hostVerification.hostLibraries="$MINERVA_HOST_LIBRARIES" ``` `minervaHostVerification` is skipped by default. When enabled, it runs `jvmTest` and `runMinervaTinyMlpSample` with CMake and CTest host verification enabled unless `-Pminerva.hostVerification.runCmakeBuild=false` or `-Pminerva.hostVerification.runCTest=false` is set. diff --git a/docs/modules/ROOT/pages/how-to/minerva-export.adoc b/docs/modules/ROOT/pages/how-to/minerva-export.adoc index 0909e035..619b469b 100644 --- a/docs/modules/ROOT/pages/how-to/minerva-export.adoc +++ b/docs/modules/ROOT/pages/how-to/minerva-export.adoc @@ -51,6 +51,10 @@ export MINERVA_CALIBRATION_NPZ=/secure/project/calibration.npz export MINERVA_KEY_FILE=/secure/project/device.key export MINERVA_RUN_CMAKE=true export MINERVA_RUN_CTEST=true +export MINERVA_HOST_ADAPTER_SOURCE=/secure/project/minerva_runtime_adapter.c +export MINERVA_HOST_INCLUDE_DIRS=/opt/libminerva/include +export MINERVA_HOST_LIBRARY_DIRS=/opt/libminerva/lib +export MINERVA_HOST_LIBRARIES=minerva ---- `MINERVA_KEY_FILE` and the generated `include/secrets.example.h` are placeholders for integration. Do not commit real device keys or derived secrets. @@ -128,6 +132,7 @@ build/minerva/TinySecureMlp/ host/ CMakeLists.txt main.c + runtime_adapter.example.c reference-input.txt reference-output.txt observed-output.txt # optional output from a real host run @@ -141,6 +146,15 @@ The manifest records the target, quantization, libminerva root, compiler command Host verification always checks the package structure, generated weight files, `model.npz` integrity, and placeholder secret hygiene. It also writes and validates deterministic `host/reference-input.txt` and `host/reference-output.txt` fixtures. +The generated host harness has a stable adapter ABI: + +[source,c] +---- +int minerva_run_inference(const float *input, int input_count, float *output, int output_count); +---- + +Copy `host/runtime_adapter.example.c` outside the generated bundle, wire that function to the pinned libminerva runtime, then point CMake at the adapter source. This keeps SKaiNET from hard-coding unverified libminerva runtime entry point names. + Use these metadata keys to opt into external host checks: [source,kotlin] @@ -148,13 +162,18 @@ Use these metadata keys to opt into external host checks: metadata = mapOf( MinervaHostVerificationMetadata.RUN_CMAKE_BUILD to "true", MinervaHostVerificationMetadata.RUN_CTEST to "true", - MinervaHostVerificationMetadata.HOST_OUTPUT_PATH to "host-output.txt" + MinervaHostVerificationMetadata.HOST_OUTPUT_PATH to "host-output.txt", + MinervaHostVerificationMetadata.HOST_ADAPTER_SOURCE to "/secure/project/minerva_runtime_adapter.c", + MinervaHostVerificationMetadata.HOST_INCLUDE_DIRS to "/opt/libminerva/include", + MinervaHostVerificationMetadata.HOST_LIBRARY_DIRS to "/opt/libminerva/lib", + MinervaHostVerificationMetadata.HOST_LIBRARIES to "minerva" ) ---- `RUN_CMAKE_BUILD` configures and builds `host/CMakeLists.txt`. `RUN_CTEST` runs the packaged CTest smoke test. `HOST_OUTPUT_PATH` lets a real host run write comma- or whitespace-separated float outputs that are compared with the SKaiNET reference output using `hostVerificationTolerance`. `HOST_OUTPUT_PATH` is optional when the host run writes the default `host/observed-output.txt` file. +The include, library directory, and library values are passed to CMake as semicolon-separated lists, matching CMake list syntax. Local CI recipe: @@ -166,7 +185,11 @@ Local CI recipe: -Pminerva.runtimeRoot="$MINERVA_RUNTIME_ROOT" \ -Pminerva.compilerScript="$MINERVA_COMPILER_SCRIPT" \ -Pminerva.calibrationNpz="$MINERVA_CALIBRATION_NPZ" \ - -Pminerva.keyFile="$MINERVA_KEY_FILE" + -Pminerva.keyFile="$MINERVA_KEY_FILE" \ + -Pminerva.hostVerification.hostAdapterSource="$MINERVA_HOST_ADAPTER_SOURCE" \ + -Pminerva.hostVerification.hostIncludeDirs="$MINERVA_HOST_INCLUDE_DIRS" \ + -Pminerva.hostVerification.hostLibraryDirs="$MINERVA_HOST_LIBRARY_DIRS" \ + -Pminerva.hostVerification.hostLibraries="$MINERVA_HOST_LIBRARIES" ---- The Gradle `minervaHostVerification` task is gated. It only runs when `minerva.hostVerification.enabled=true`, `minerva.runtimeRoot`, and `minerva.compilerScript` are present. When enabled, it runs `jvmTest` and `runMinervaTinyMlpSample` with `MINERVA_RUN_CMAKE=true` and `MINERVA_RUN_CTEST=true` by default. Override `-Pminerva.hostVerification.runCmakeBuild=false` or `-Pminerva.hostVerification.runCTest=false` only for runtime bring-up. diff --git a/skainet-compile/skainet-compile-minerva/api/skainet-compile-minerva.api b/skainet-compile/skainet-compile-minerva/api/skainet-compile-minerva.api index 5a3cb7b2..71bd6474 100644 --- a/skainet-compile/skainet-compile-minerva/api/skainet-compile-minerva.api +++ b/skainet-compile/skainet-compile-minerva/api/skainet-compile-minerva.api @@ -388,6 +388,10 @@ public final class sk/ainet/compile/minerva/MinervaHostVerification { public final class sk/ainet/compile/minerva/MinervaHostVerificationMetadata { public static final field CMAKE_EXECUTABLE Ljava/lang/String; public static final field CTEST_EXECUTABLE Ljava/lang/String; + public static final field HOST_ADAPTER_SOURCE Ljava/lang/String; + public static final field HOST_INCLUDE_DIRS Ljava/lang/String; + public static final field HOST_LIBRARIES Ljava/lang/String; + public static final field HOST_LIBRARY_DIRS Ljava/lang/String; public static final field HOST_OUTPUT_PATH Ljava/lang/String; public static final field INSTANCE Lsk/ainet/compile/minerva/MinervaHostVerificationMetadata; public static final field RUN_CMAKE_BUILD Ljava/lang/String; diff --git a/skainet-compile/skainet-compile-minerva/build.gradle.kts b/skainet-compile/skainet-compile-minerva/build.gradle.kts index 6b22084d..b5d5e4e8 100644 --- a/skainet-compile/skainet-compile-minerva/build.gradle.kts +++ b/skainet-compile/skainet-compile-minerva/build.gradle.kts @@ -39,6 +39,10 @@ val minervaCalibrationNpz = providers.gradleProperty("minerva.calibrationNpz") val minervaRunCmakeBuild = providers.gradleProperty("minerva.hostVerification.runCmakeBuild") val minervaRunCTest = providers.gradleProperty("minerva.hostVerification.runCTest") val minervaHostOutputPath = providers.gradleProperty("minerva.hostVerification.hostOutputPath") +val minervaHostAdapterSource = providers.gradleProperty("minerva.hostVerification.hostAdapterSource") +val minervaHostIncludeDirs = providers.gradleProperty("minerva.hostVerification.hostIncludeDirs") +val minervaHostLibraryDirs = providers.gradleProperty("minerva.hostVerification.hostLibraryDirs") +val minervaHostLibraries = providers.gradleProperty("minerva.hostVerification.hostLibraries") val minervaRunCmakeBuildForSample = minervaRunCmakeBuild .orElse(providers.environmentVariable("MINERVA_RUN_CMAKE")) .orElse(minervaHostVerificationEnabled.map { it.toString() }) @@ -64,6 +68,10 @@ tasks.register("minervaHostVerification") { inputs.property("minerva.hostVerification.runCmakeBuild", minervaRunCmakeBuildForSample) inputs.property("minerva.hostVerification.runCTest", minervaRunCTestForSample) inputs.property("minerva.hostVerification.hostOutputPath", minervaHostOutputPath.orElse("")) + inputs.property("minerva.hostVerification.hostAdapterSource", minervaHostAdapterSource.orElse("")) + inputs.property("minerva.hostVerification.hostIncludeDirs", minervaHostIncludeDirs.orElse("")) + inputs.property("minerva.hostVerification.hostLibraryDirs", minervaHostLibraryDirs.orElse("")) + inputs.property("minerva.hostVerification.hostLibraries", minervaHostLibraries.orElse("")) } tasks.register("runMinervaTinyMlpSample") { @@ -101,4 +109,16 @@ tasks.register("runMinervaTinyMlpSample") { minervaHostOutputPath.orElse(providers.environmentVariable("MINERVA_HOST_OUTPUT_PATH")).orNull?.let { environment("MINERVA_HOST_OUTPUT_PATH", it) } + minervaHostAdapterSource.orElse(providers.environmentVariable("MINERVA_HOST_ADAPTER_SOURCE")).orNull?.let { + environment("MINERVA_HOST_ADAPTER_SOURCE", it) + } + minervaHostIncludeDirs.orElse(providers.environmentVariable("MINERVA_HOST_INCLUDE_DIRS")).orNull?.let { + environment("MINERVA_HOST_INCLUDE_DIRS", it) + } + minervaHostLibraryDirs.orElse(providers.environmentVariable("MINERVA_HOST_LIBRARY_DIRS")).orNull?.let { + environment("MINERVA_HOST_LIBRARY_DIRS", it) + } + minervaHostLibraries.orElse(providers.environmentVariable("MINERVA_HOST_LIBRARIES")).orNull?.let { + environment("MINERVA_HOST_LIBRARIES", it) + } } diff --git a/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaHostVerificationModels.kt b/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaHostVerificationModels.kt index 09c38318..2ff69047 100644 --- a/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaHostVerificationModels.kt +++ b/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaHostVerificationModels.kt @@ -14,6 +14,10 @@ public object MinervaHostVerificationMetadata { public const val CMAKE_EXECUTABLE: String = "minerva.hostVerification.cmakeExecutable" public const val CTEST_EXECUTABLE: String = "minerva.hostVerification.ctestExecutable" public const val HOST_OUTPUT_PATH: String = "minerva.hostVerification.hostOutputPath" + public const val HOST_ADAPTER_SOURCE: String = "minerva.hostVerification.hostAdapterSource" + public const val HOST_INCLUDE_DIRS: String = "minerva.hostVerification.hostIncludeDirs" + public const val HOST_LIBRARY_DIRS: String = "minerva.hostVerification.hostLibraryDirs" + public const val HOST_LIBRARIES: String = "minerva.hostVerification.hostLibraries" } /** diff --git a/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackager.kt b/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackager.kt index 2ce1161e..47834638 100644 --- a/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackager.kt +++ b/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackager.kt @@ -393,10 +393,13 @@ public class JvmMinervaProjectPackager @kotlin.jvm.JvmOverloads constructor( if (options.generateHostHarness) { val hostCmake = hostDir.resolve("CMakeLists.txt") val hostMain = hostDir.resolve("main.c") + val hostAdapterExample = hostDir.resolve("runtime_adapter.example.c") Files.writeString(hostCmake, hostCmake(options)) Files.writeString(hostMain, hostMain(request)) + Files.writeString(hostAdapterExample, hostRuntimeAdapterExample()) generatedPaths.add(hostCmake) generatedPaths.add(hostMain) + generatedPaths.add(hostAdapterExample) } if (options.generateFirmwareExample) { val firmwareMain = firmwareDir.resolve("main.c") @@ -544,16 +547,48 @@ public class JvmMinervaProjectPackager @kotlin.jvm.JvmOverloads constructor( } private fun hostCmake(options: MinervaExportOptions): String { + val runtimeRoot = cmakeString(options.runtimeRoot ?: "") + val adapterSource = cmakeString(options.metadata[MinervaHostVerificationMetadata.HOST_ADAPTER_SOURCE] ?: "") + val includeDirs = cmakeString(options.metadata[MinervaHostVerificationMetadata.HOST_INCLUDE_DIRS] ?: "") + val libraryDirs = cmakeString(options.metadata[MinervaHostVerificationMetadata.HOST_LIBRARY_DIRS] ?: "") + val libraries = cmakeString(options.metadata[MinervaHostVerificationMetadata.HOST_LIBRARIES] ?: "") return """ |cmake_minimum_required(VERSION 3.20) |project(${options.projectName}_host C) | - |add_executable(${options.projectName}_host main.c ../generated/weights.c) + |set(MINERVA_RUNTIME_ROOT "$runtimeRoot" CACHE PATH "libminerva checkout or install root") + |set(MINERVA_HOST_ADAPTER_SOURCE "$adapterSource" CACHE FILEPATH "C source file implementing minerva_run_inference") + |set(MINERVA_HOST_INCLUDE_DIRS "$includeDirs" CACHE STRING "Additional semicolon-separated Minerva host include directories") + |set(MINERVA_HOST_LIBRARY_DIRS "$libraryDirs" CACHE STRING "Additional semicolon-separated Minerva host library directories") + |set(MINERVA_HOST_LIBRARIES "$libraries" CACHE STRING "Additional semicolon-separated Minerva host libraries") + | + |set(MINERVA_HOST_SOURCES main.c ../generated/weights.c) + |if(MINERVA_HOST_ADAPTER_SOURCE) + | list(APPEND MINERVA_HOST_SOURCES "${'$'}{MINERVA_HOST_ADAPTER_SOURCE}") + |endif() + | + |add_executable(${options.projectName}_host ${'$'}{MINERVA_HOST_SOURCES}) |target_include_directories(${options.projectName}_host PRIVATE ../include) + |if(MINERVA_RUNTIME_ROOT) + | target_include_directories(${options.projectName}_host PRIVATE "${'$'}{MINERVA_RUNTIME_ROOT}/include") + |endif() + |if(MINERVA_HOST_INCLUDE_DIRS) + | target_include_directories(${options.projectName}_host PRIVATE ${'$'}{MINERVA_HOST_INCLUDE_DIRS}) + |endif() + |if(MINERVA_HOST_LIBRARY_DIRS) + | target_link_directories(${options.projectName}_host PRIVATE ${'$'}{MINERVA_HOST_LIBRARY_DIRS}) + |endif() + |if(MINERVA_HOST_LIBRARIES) + | target_link_libraries(${options.projectName}_host PRIVATE ${'$'}{MINERVA_HOST_LIBRARIES}) + |endif() + |if(MINERVA_HOST_ADAPTER_SOURCE) + | target_compile_definitions(${options.projectName}_host PRIVATE MINERVA_HOST_RUNTIME_ENABLED=1) + |endif() | |include(CTest) |if(BUILD_TESTING) | add_test(NAME minerva_host_smoke COMMAND ${options.projectName}_host) + | set_tests_properties(minerva_host_smoke PROPERTIES WORKING_DIRECTORY "${'$'}{CMAKE_CURRENT_SOURCE_DIR}") |endif() | """.trimMargin() @@ -568,22 +603,83 @@ public class JvmMinervaProjectPackager @kotlin.jvm.JvmOverloads constructor( |#include |#include "weights.h" | + |#define MINERVA_OBSERVED_OUTPUT_PATH "$HOST_OBSERVED_OUTPUT_FILE" + | + |#ifdef MINERVA_HOST_RUNTIME_ENABLED + |extern int minerva_run_inference(const float *input, int input_count, float *output, int output_count); + | + |static int write_observed_output(const float *output, int output_count) { + | FILE *file = fopen(MINERVA_OBSERVED_OUTPUT_PATH, "w"); + | if (file == NULL) { + | perror("failed to open " MINERVA_OBSERVED_OUTPUT_PATH); + | return 1; + | } + | for (int i = 0; i < output_count; ++i) { + | if (fprintf(file, "%s%.9g", i == 0 ? "" : "\n", (double)output[i]) < 0) { + | fclose(file); + | return 1; + | } + | } + | if (fprintf(file, "\n") < 0) { + | fclose(file); + | return 1; + | } + | return fclose(file) == 0 ? 0 : 1; + |} + |#endif + | |int main(void) { | float input[$inputCount] = {${cFloatInitializer(referenceInput)}}; | float output[$outputCount] = {0}; | - | /* Link this harness with libminerva and call the runtime inference entry point here. */ + |#ifdef MINERVA_HOST_RUNTIME_ENABLED + | int status = minerva_run_inference(input, $inputCount, output, $outputCount); + | if (status != 0) { + | fprintf(stderr, "minerva_run_inference failed with status %d\n", status); + | return status; + | } + | if (write_observed_output(output, $outputCount) != 0) { + | fprintf(stderr, "failed to write " MINERVA_OBSERVED_OUTPUT_PATH "\n"); + | return 2; + | } + | puts("Minerva host harness wrote " MINERVA_OBSERVED_OUTPUT_PATH "."); + |#else | (void)input; | (void)output; | puts("Minerva host harness packaged successfully."); | puts("Reference input: $HOST_REFERENCE_INPUT_FILE"); | puts("Write observed output to: $HOST_OBSERVED_OUTPUT_FILE"); + |#endif | return 0; |} | """.trimMargin() } + private fun hostRuntimeAdapterExample(): String { + return """ + |/* + | * Copy this file outside the generated bundle and wire it to your pinned + | * libminerva runtime. Then configure CMake with: + | * + | * -DMINERVA_HOST_ADAPTER_SOURCE=/path/to/minerva_runtime_adapter.c + | * + | * The generated host harness calls this stable shim so SKaiNET does not + | * need to hard-code libminerva runtime entry point names. + | */ + |#include "weights.h" + | + |int minerva_run_inference(const float *input, int input_count, float *output, int output_count) { + | (void)input; + | (void)input_count; + | (void)output; + | (void)output_count; + | return 1; + |} + | + """.trimMargin() + } + private fun firmwareMain(request: MinervaProjectPackageRequest): String { val inputCount = request.intermediate.input.elementCount val outputCount = request.intermediate.output.elementCount @@ -623,6 +719,12 @@ public class JvmMinervaProjectPackager @kotlin.jvm.JvmOverloads constructor( return values.joinToString(separator = ", ") { "${it}f" } } + private fun cmakeString(value: String): String { + return value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + } + private fun jsonString(value: String): String { val escaped = buildString { value.forEach { char -> @@ -884,6 +986,7 @@ public class JvmMinervaHostVerifier @kotlin.jvm.JvmOverloads constructor( if (request.options.generateHostHarness) { add(projectDir.resolve("host/CMakeLists.txt")) add(projectDir.resolve("host/main.c")) + add(projectDir.resolve("host/runtime_adapter.example.c")) } if (request.options.generateFirmwareExample) { add(projectDir.resolve("firmware/main.c")) @@ -955,7 +1058,7 @@ public class JvmMinervaHostVerifier @kotlin.jvm.JvmOverloads constructor( Files.createDirectories(buildDir) val cmake = options.metadata[MinervaHostVerificationMetadata.CMAKE_EXECUTABLE] ?: "cmake" val configure = runExternalCommand( - command = listOf(cmake, "-S", hostDir.toString(), "-B", buildDir.toString(), "-DBUILD_TESTING=ON"), + command = cmakeConfigureCommand(cmake, hostDir, buildDir, options), workingDir = projectDir, logPath = buildDir.resolve("cmake-configure.log"), context = context @@ -989,6 +1092,36 @@ public class JvmMinervaHostVerifier @kotlin.jvm.JvmOverloads constructor( return null } + private fun cmakeConfigureCommand( + cmake: String, + hostDir: Path, + buildDir: Path, + options: MinervaExportOptions + ): List { + val command = mutableListOf( + cmake, + "-S", + hostDir.toString(), + "-B", + buildDir.toString(), + "-DBUILD_TESTING=ON" + ) + options.runtimeRoot?.let { command += "-DMINERVA_RUNTIME_ROOT=${Paths.get(it).toAbsolutePath().normalize()}" } + options.metadata[MinervaHostVerificationMetadata.HOST_ADAPTER_SOURCE]?.let { + command += "-DMINERVA_HOST_ADAPTER_SOURCE=${Paths.get(it).toAbsolutePath().normalize()}" + } + options.metadata[MinervaHostVerificationMetadata.HOST_INCLUDE_DIRS]?.let { + command += "-DMINERVA_HOST_INCLUDE_DIRS=$it" + } + options.metadata[MinervaHostVerificationMetadata.HOST_LIBRARY_DIRS]?.let { + command += "-DMINERVA_HOST_LIBRARY_DIRS=$it" + } + options.metadata[MinervaHostVerificationMetadata.HOST_LIBRARIES]?.let { + command += "-DMINERVA_HOST_LIBRARIES=$it" + } + return command + } + private fun runCTest( projectDir: Path, options: MinervaExportOptions, diff --git a/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/examples/MinervaTinyMlpExportSample.kt b/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/examples/MinervaTinyMlpExportSample.kt index c62a55df..b0ddfd12 100644 --- a/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/examples/MinervaTinyMlpExportSample.kt +++ b/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/examples/MinervaTinyMlpExportSample.kt @@ -35,7 +35,11 @@ internal object MinervaTinyMlpExportSample { calibrationNpz = envPath(env, "MINERVA_CALIBRATION_NPZ"), runCmakeBuild = envFlag(env, "MINERVA_RUN_CMAKE"), runCTest = envFlag(env, "MINERVA_RUN_CTEST"), - hostOutputPath = envPath(env, "MINERVA_HOST_OUTPUT_PATH") + hostOutputPath = envPath(env, "MINERVA_HOST_OUTPUT_PATH"), + hostAdapterSource = envPath(env, "MINERVA_HOST_ADAPTER_SOURCE"), + hostIncludeDirs = envPath(env, "MINERVA_HOST_INCLUDE_DIRS"), + hostLibraryDirs = envPath(env, "MINERVA_HOST_LIBRARY_DIRS"), + hostLibraries = envPath(env, "MINERVA_HOST_LIBRARIES") ) val result = MinervaExportFacade().exportGraph(tinyMlpGraph(), options) @@ -124,7 +128,11 @@ internal object MinervaTinyMlpExportSample { calibrationNpz: String? = null, runCmakeBuild: Boolean = false, runCTest: Boolean = false, - hostOutputPath: String? = null + hostOutputPath: String? = null, + hostAdapterSource: String? = null, + hostIncludeDirs: String? = null, + hostLibraryDirs: String? = null, + hostLibraries: String? = null ): MinervaExportOptions { val metadata = mutableMapOf("sample" to "minerva-tiny-mlp") if (runCmakeBuild) { @@ -136,6 +144,18 @@ internal object MinervaTinyMlpExportSample { hostOutputPath?.let { metadata[MinervaHostVerificationMetadata.HOST_OUTPUT_PATH] = it } + hostAdapterSource?.let { + metadata[MinervaHostVerificationMetadata.HOST_ADAPTER_SOURCE] = it + } + hostIncludeDirs?.let { + metadata[MinervaHostVerificationMetadata.HOST_INCLUDE_DIRS] = it + } + hostLibraryDirs?.let { + metadata[MinervaHostVerificationMetadata.HOST_LIBRARY_DIRS] = it + } + hostLibraries?.let { + metadata[MinervaHostVerificationMetadata.HOST_LIBRARIES] = it + } return MinervaExportOptions( outputDir = outputDir, projectName = projectName, diff --git a/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackagerTest.kt b/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackagerTest.kt index 42fc6187..3c47ad13 100644 --- a/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackagerTest.kt +++ b/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackagerTest.kt @@ -123,11 +123,19 @@ class MinervaJvmCompilerAndPackagerTest { assertTrue(Files.isRegularFile(projectDir.resolve("host/reference-input.txt"))) assertTrue(Files.isRegularFile(projectDir.resolve("host/reference-output.txt"))) assertTrue(Files.isRegularFile(projectDir.resolve("host/main.c"))) + assertTrue(Files.isRegularFile(projectDir.resolve("host/runtime_adapter.example.c"))) assertTrue(Files.isRegularFile(projectDir.resolve("firmware/main.c"))) - assertTrue(Files.readString(projectDir.resolve("host/CMakeLists.txt")).contains("add_test")) + val hostCmake = Files.readString(projectDir.resolve("host/CMakeLists.txt")) + assertTrue(hostCmake.contains("MINERVA_HOST_ADAPTER_SOURCE")) + assertTrue(hostCmake.contains("target_link_libraries")) + assertTrue(hostCmake.contains("set_tests_properties")) val hostMain = Files.readString(projectDir.resolve("host/main.c")) assertTrue(hostMain.contains("0.25f, 0.5f, 0.75f, 1.0f")) + assertTrue(hostMain.contains("minerva_run_inference")) assertTrue(hostMain.contains("observed-output.txt")) + val adapterExample = Files.readString(projectDir.resolve("host/runtime_adapter.example.c")) + assertTrue(adapterExample.contains("minerva_run_inference")) + assertTrue(adapterExample.contains("return 1")) val secretsExample = Files.readString(projectDir.resolve("include/secrets.example.h")) assertTrue(secretsExample.contains("replace-with-device-key")) assertFalse(secretsExample.contains("REAL_SECRET_KEY_MATERIAL")) @@ -141,6 +149,7 @@ class MinervaJvmCompilerAndPackagerTest { assertTrue(bundle.generatedFiles.contains("include/secrets.example.h")) assertTrue(bundle.generatedFiles.contains("host/reference-input.txt")) assertTrue(bundle.generatedFiles.contains("host/reference-output.txt")) + assertTrue(bundle.generatedFiles.contains("host/runtime_adapter.example.c")) assertTrue(context.artifacts.any { it.role == GraphExportArtifactRole.PROJECT_DIRECTORY }) assertTrue(context.artifacts.any { it.role == GraphExportArtifactRole.TEST_REPORT }) assertTrue(context.diagnostics.any { it.code == "minerva.packaging.completed" }) @@ -160,6 +169,50 @@ class MinervaJvmCompilerAndPackagerTest { assertTrue(fixture.context.diagnostics.any { it.code == "minerva.host_verification.passed" }) } + @Test + fun hostVerifierPassesAdapterConfigurationToCMake() { + val fixture = packagedProject(tempDir("host-verify-cmake-adapter"), "AdapterCmakeVerify") + val toolDir = tempDir("fake-cmake") + val fakeCmake = toolDir.resolve("cmake") + Files.writeString( + fakeCmake, + """ + |#!/bin/sh + |exit 0 + | + """.trimMargin() + ) + assertTrue(fakeCmake.toFile().setExecutable(true)) + val runtimeRoot = toolDir.resolve("runtime") + val adapterSource = toolDir.resolve("minerva_adapter.c") + Files.createDirectories(runtimeRoot) + Files.writeString(adapterSource, "int minerva_run_inference(void) { return 0; }\n") + val request = fixture.request.copy( + options = fixture.request.options.copy( + runtimeRoot = runtimeRoot.toString(), + metadata = fixture.request.options.metadata + mapOf( + MinervaHostVerificationMetadata.RUN_CMAKE_BUILD to "true", + MinervaHostVerificationMetadata.CMAKE_EXECUTABLE to fakeCmake.toString(), + MinervaHostVerificationMetadata.HOST_ADAPTER_SOURCE to adapterSource.toString(), + MinervaHostVerificationMetadata.HOST_INCLUDE_DIRS to "/opt/libminerva/include;/project/include", + MinervaHostVerificationMetadata.HOST_LIBRARY_DIRS to "/opt/libminerva/lib", + MinervaHostVerificationMetadata.HOST_LIBRARIES to "minerva;crypto" + ) + ) + ) + + val verification = JvmMinervaHostVerifier().verify(request, fixture.context) + + assertEquals(MinervaHostVerificationStatus.PASSED, verification.status) + assertEquals(MinervaHostVerificationStatus.PASSED, verification.hostBuildStatus) + val log = Files.readString(fixture.projectDir.resolve("host/build/cmake-configure.log")) + assertTrue(log.contains("-DMINERVA_RUNTIME_ROOT=${runtimeRoot.toAbsolutePath().normalize()}")) + assertTrue(log.contains("-DMINERVA_HOST_ADAPTER_SOURCE=${adapterSource.toAbsolutePath().normalize()}")) + assertTrue(log.contains("-DMINERVA_HOST_INCLUDE_DIRS=/opt/libminerva/include;/project/include")) + assertTrue(log.contains("-DMINERVA_HOST_LIBRARY_DIRS=/opt/libminerva/lib")) + assertTrue(log.contains("-DMINERVA_HOST_LIBRARIES=minerva;crypto")) + } + @Test fun hostVerifierComparesConfiguredHostOutput() { val fixture = packagedProject(tempDir("host-verify-output"), "OutputVerify") diff --git a/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/examples/MinervaTinyMlpExportSampleTest.kt b/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/examples/MinervaTinyMlpExportSampleTest.kt index 1c058444..970bdb86 100644 --- a/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/examples/MinervaTinyMlpExportSampleTest.kt +++ b/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/examples/MinervaTinyMlpExportSampleTest.kt @@ -47,12 +47,20 @@ class MinervaTinyMlpExportSampleTest { compilerScript = "/opt/libminerva/tools/compile_model.py", runCmakeBuild = true, runCTest = true, - hostOutputPath = "host-output.txt" + hostOutputPath = "host-output.txt", + hostAdapterSource = "/project/minerva_adapter.c", + hostIncludeDirs = "/opt/libminerva/include", + hostLibraryDirs = "/opt/libminerva/lib", + hostLibraries = "minerva" ) assertEquals("minerva-tiny-mlp", options.metadata["sample"]) assertEquals("true", options.metadata[MinervaHostVerificationMetadata.RUN_CMAKE_BUILD]) assertEquals("true", options.metadata[MinervaHostVerificationMetadata.RUN_CTEST]) assertEquals("host-output.txt", options.metadata[MinervaHostVerificationMetadata.HOST_OUTPUT_PATH]) + assertEquals("/project/minerva_adapter.c", options.metadata[MinervaHostVerificationMetadata.HOST_ADAPTER_SOURCE]) + assertEquals("/opt/libminerva/include", options.metadata[MinervaHostVerificationMetadata.HOST_INCLUDE_DIRS]) + assertEquals("/opt/libminerva/lib", options.metadata[MinervaHostVerificationMetadata.HOST_LIBRARY_DIRS]) + assertEquals("minerva", options.metadata[MinervaHostVerificationMetadata.HOST_LIBRARIES]) } }