diff --git a/README.md b/README.md index 2f99e60..19335d2 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,12 @@ make --version # GNU Make (optional: for local validation) dotnet run --project src/Generator -- --dbc examples/sample.dbc --out gen ``` -3) Build and run a basic test +3) (Optional) Generate a local test harness and build ```bash +# Create/upgrade an adaptive Makefile and copy a sample main.c +dotnet run --project src/Signal.CANdy.CLI -- -d examples/sample.dbc -o gen -t + make -C gen build ./gen/build/test_runner test_roundtrip ``` @@ -204,6 +207,28 @@ dotnet run --project src/Generator -- \ --emit-main false ``` +### signal-candy CLI (test harness helper) + +The CLI provides short flags and an optional harness mode that creates/updates an adaptive Makefile and copies a sample `main.c` for quick local builds. + +- `-d, --dbc `: input DBC (required) +- `-o, --out `: output directory (required) +- `-c, --config `: optional YAML config +- `-t, --harness`: write/upgrade `gen/Makefile` and ensure `gen/src/main.c` exists; idempotent +- `-v, --version`: print version +- `-h, --help`: usage + +Examples + +```bash +# Minimal codegen only +dotnet run --project src/Signal.CANdy.CLI -- -d examples/sample.dbc -o gen + +# With harness (writes Makefile + copies main.c), then build +dotnet run --project src/Signal.CANdy.CLI -- -d examples/sample.dbc -o gen -t +make -C gen build +``` + ### Using a config ```bash @@ -251,7 +276,7 @@ make -C gen build Notes - Codegen writes files under `gen/include` and `gen/src`. -- A sample test runner `gen/src/main.c` is emitted by copying `examples/main.c`. Do not ship or compile this in firmware; it exists only for local testing. +- A sample test runner `gen/src/main.c` is provided when using the CLI with `-t/--harness` (it copies `examples/main.c`). Do not ship or compile this in firmware; it exists only for local testing. ## Large-scale testing and stress suite @@ -355,10 +380,10 @@ void compare_state(int v) { ### Output layout and naming - gen/include/ - - sc_utils.h, sc_registry.h (prefix configurable via config: file_prefix) + - utils.h or utils.h, registry.h or registry.h (prefix configurable via config: file_prefix) - .h per message (snake_case filename) - gen/src/ - - sc_utils.c, sc_registry.c (prefix configurable) + - utils.c or utils.c, registry.c or registry.c (prefix configurable) - .c per message (snake_case filename) - main.c (test runner; exclude in firmware builds) @@ -473,6 +498,16 @@ extern "C" { The generated code remains pure C99, ensuring compatibility with both C and C++ projects without requiring changes to build systems or toolchains. +### Harness Makefile details + +When you run the CLI with `-t/--harness`, it creates or upgrades `gen/Makefile`: + +- Discovers `src/*.c` dynamically so it adapts to any generated filenames or prefixes. +- Avoids duplicate common sources (drops legacy unprefixed `utils.c`/`registry.c` when prefixed variants exist). +- Supports optional `../examples/stress_test.c` with `-DHAVE_STRESS`. +- Toolchain knobs: `CC ?= gcc`, `CFLAGS ?= -Wall -Wextra -std=c99`, `EXTRA_CFLAGS ?=`, `LDLIBS ?= -lm`. +- Idempotent and safe: if a non-harness Makefile exists, it is backed up to `Makefile.bak` before upgrading. + ## Platforms, compilers, and test environments Tested combos @@ -491,6 +526,13 @@ Windows notes (early guidance) - Linking: MSVC doesn’t need `-lm`; math functions (llround/llroundf) are in the CRT when including ``. - If you hit MSVC-specific C99 quirks, consider LLVM clang-cl or MinGW as a fallback. Please open an issue with compiler/version details so we can add CI coverage. +MinGW quick tip + +```pwsh +# Build with MinGW toolchain and extra flags via harness Makefile +make -C gen build CC=x86_64-w64-mingw32-gcc EXTRA_CFLAGS='-O2 -DNDEBUG' +``` + ### Quick checklist (firmware integration) - [ ] Do NOT compile `gen/src/main.c` (test runner only) diff --git a/infra/ci.yml b/infra/ci.yml index 2727194..95ee503 100644 --- a/infra/ci.yml +++ b/infra/ci.yml @@ -35,17 +35,15 @@ jobs: - name: Format check run: dotnet fantomas --check - - name: Build generated C code (Placeholder) + - name: Generate and build C code run: | - mkdir -p gen - # This step would typically involve running the F# generator to produce C code - # For now, we'll just create dummy C files to simulate the output - mkdir -p gen/include gen/src - echo '#ifndef DUMMY_H\n#define DUMMY_H\nvoid dummy_func();\n#endif' > gen/include/dummy.h - echo '#include "dummy.h"\nvoid dummy_func() { }' > gen/src/dummy.c + # Generate C code from sample DBC with harness + dotnet run --project src/Signal.CANdy.CLI -- -d examples/sample.dbc -o gen -t - # Build the dummy C code using make + # Build the generated C code make -C gen build - - name: Run C tests (Placeholder) - run: make -C gen test + - name: Run C tests + run: | + # Run basic test suite + ./gen/build/test_runner test_roundtrip diff --git a/src/Signal.CANdy.CLI/Program.fs b/src/Signal.CANdy.CLI/Program.fs index fb0c914..c6b00a2 100644 --- a/src/Signal.CANdy.CLI/Program.fs +++ b/src/Signal.CANdy.CLI/Program.fs @@ -3,14 +3,14 @@ open Signal.CANdy.Core open Signal.CANdy.Core.Errors module Cli = - type Parsed = { - DbcPath: string option - OutDir: string option - ConfigPath: string option - ShowVersion: bool - ShowHelp: bool - Unknown: string list - } + type Parsed = + { DbcPath: string option + OutDir: string option + ConfigPath: string option + ShowVersion: bool + ShowHelp: bool + WithHarness: bool + Unknown: string list } let empty: Parsed = { DbcPath = None @@ -18,6 +18,7 @@ module Cli = ConfigPath = None ShowVersion = false ShowHelp = false + WithHarness = false Unknown = [] } let usage () = @@ -25,16 +26,17 @@ module Cli = "Signal.CANdy.CLI — DBC → C code generator"; ""; "Usage:"; - " signal-candy --dbc --out [--config ]"; + " signal-candy -d -o [-c ] [-t]"; " signal-candy --version"; " signal-candy --help"; ""; "Options:"; - " --dbc Path to input DBC file (required)"; - " --out Output directory for generated C files (required)"; - " --config Optional YAML config (phys_type, range_check, dispatch, etc.)"; - " --version Print library version and exit"; - " --help, -h Show this help and exit" + " -d, --dbc Path to input DBC file (required)"; + " -o, --out Output directory for generated C files (required)"; + " -c, --config Optional YAML config (phys_type, range_check, dispatch, etc.)"; + " -t, --harness Generate test harness files (main.c + Makefile) if missing"; + " -v, --version Print library version and exit"; + " -h, --help Show this help and exit" ] let parse (argv: string array): Parsed = @@ -43,9 +45,15 @@ module Cli = else match argv.[i] with | "--dbc" when i + 1 < argv.Length -> loop (i + 2) { st with DbcPath = Some argv.[i + 1] } + | "-d" when i + 1 < argv.Length -> loop (i + 2) { st with DbcPath = Some argv.[i + 1] } | "--out" when i + 1 < argv.Length -> loop (i + 2) { st with OutDir = Some argv.[i + 1] } + | "-o" when i + 1 < argv.Length -> loop (i + 2) { st with OutDir = Some argv.[i + 1] } | "--config" when i + 1 < argv.Length -> loop (i + 2) { st with ConfigPath = Some argv.[i + 1] } + | "-c" when i + 1 < argv.Length -> loop (i + 2) { st with ConfigPath = Some argv.[i + 1] } + | "--harness" -> loop (i + 1) { st with WithHarness = true } + | "-t" -> loop (i + 1) { st with WithHarness = true } | "--version" -> loop (i + 1) { st with ShowVersion = true } + | "-v" -> loop (i + 1) { st with ShowVersion = true } | "--help" | "-h" -> loop (i + 1) { st with ShowHelp = true } | unk -> loop (i + 1) { st with Unknown = st.Unknown @ [ unk ] } loop 0 empty @@ -75,6 +83,202 @@ let main argv: int = | Ok files -> printfn "Code generation successful." printfn "Headers: %d, Sources: %d, Others: %d" (files.Headers.Length) (files.Sources.Length) (files.Others.Length) + // Optionally generate test harness if requested + if args.WithHarness then + try + let outDirFull = System.IO.Path.GetFullPath(outDir) + let srcDir = System.IO.Path.Combine(outDirFull, "src") + let includeDir = System.IO.Path.Combine(outDirFull, "include") + System.IO.Directory.CreateDirectory(srcDir) |> ignore + System.IO.Directory.CreateDirectory(includeDir) |> ignore + + // Try to copy examples/main.c if available and not already present + let mainDst = System.IO.Path.Combine(srcDir, "main.c") + if not (System.IO.File.Exists(mainDst)) then + let candidates = [ + System.IO.Path.Combine("examples", "main.c") + System.IO.Path.Combine(AppContext.BaseDirectory, "..", "..", "examples", "main.c") + ] + let found = candidates |> List.tryFind System.IO.File.Exists + match found with + | Some src -> System.IO.File.Copy(src, mainDst, true) + | None -> eprintfn "Warning: examples/main.c not found; skipping main.c copy." + + // Harmonize includes in harness sources to match available headers (handles prefixed utils/registry) + let chooseHeader (pattern: string) (fallback: string) = + try + let files = System.IO.Directory.GetFiles(includeDir, pattern) |> Array.toList + match files |> List.map System.IO.Path.GetFileName with + | [] -> fallback + | names -> + // Prefer prefixed variants like "test_utils.h" over generic "utils.h" + match names |> List.filter (fun n -> not (n.Equals(fallback, StringComparison.OrdinalIgnoreCase))) with + | pref::_ -> pref + | [] -> fallback + with _ -> fallback + + let utilsHeader = chooseHeader "*utils.h" "utils.h" + let registryHeader = chooseHeader "*registry.h" "registry.h" + + let tryRewriteIncludes (path: string) = + try + if System.IO.File.Exists(path) then + let text = System.IO.File.ReadAllText(path) + let replaced = + text + .Replace("\"sc_utils.h\"", $"\"{utilsHeader}\"") + .Replace("\"sc_registry.h\"", $"\"{registryHeader}\"") + // If a different prefixed header exists, normalize generic includes too + .Replace("\"utils.h\"", $"\"{utilsHeader}\"") + .Replace("\"registry.h\"", $"\"{registryHeader}\"") + if replaced <> text then System.IO.File.WriteAllText(path, replaced) + with ex -> eprintfn "Harness include rewrite warning for %s: %s" path ex.Message + + tryRewriteIncludes(mainDst) + // Also adapt fixed_test.c if present + let fixedTest = System.IO.Path.Combine(srcDir, "fixed_test.c") + tryRewriteIncludes(fixedTest) + + // Patch generated common sources to include the available headers + let patchCommonSource (namePattern: string) (expectedHeader: string) = + try + let files = System.IO.Directory.GetFiles(srcDir, namePattern) + for f in files do + let text = System.IO.File.ReadAllText(f) + let replaced = + text + .Replace("\"sc_" + expectedHeader + "\"", $"\"{expectedHeader}\"") + .Replace($"\"{expectedHeader}\"", $"\"{expectedHeader}\"") + .Replace("\"sc_utils.h\"", $"\"{utilsHeader}\"") + .Replace("\"sc_registry.h\"", $"\"{registryHeader}\"") + if replaced <> text then System.IO.File.WriteAllText(f, replaced) + with ex -> eprintfn "Harness common source rewrite warning: %s" ex.Message + + patchCommonSource "*utils.c" utilsHeader + patchCommonSource "*registry.c" registryHeader + + // Create or upgrade a Makefile to adapt to whatever files were generated + let mkPath = System.IO.Path.Combine(outDirFull, "Makefile") + let mk = """ +# Signal.CANdy harness Makefile v1 +# Auto-generated by Signal.CANdy.CLI when using --harness (-t). +# It discovers sources under ./src dynamically, so it should work regardless of generated filenames. +# If this file already existed, the CLI will back it up to Makefile.bak before upgrading. + +CC ?= gcc +CFLAGS ?= -Wall -Wextra -std=c99 +EXTRA_CFLAGS ?= +LDLIBS ?= -lm + +BUILD_DIR = build +SRC_DIR = src +INCLUDE_DIR = include + +# Discover all C sources under src and map to objects under build +SRCS := $(wildcard $(SRC_DIR)/*.c) +# Add stress test source if available +ifneq (,$(wildcard ../examples/stress_test.c)) +SRCS += ../examples/stress_test.c +CFLAGS += -DHAVE_STRESS +endif + +# If any prefixed common files exist, drop legacy unprefixed ones to avoid duplicates +ifneq (,$(filter-out $(SRC_DIR)/registry.c,$(wildcard $(SRC_DIR)/*registry.c))) +SRCS := $(filter-out $(SRC_DIR)/registry.c,$(SRCS)) +endif +ifneq (,$(filter-out $(SRC_DIR)/utils.c,$(wildcard $(SRC_DIR)/*utils.c))) +SRCS := $(filter-out $(SRC_DIR)/utils.c,$(SRCS)) +endif + +# If multiple prefixed variants exist (e.g., sc_utils.c and test_utils.c), +# keep only one deterministically (lexicographically first) to avoid duplicate symbols. +UTIL_SRCS := $(wildcard $(SRC_DIR)/*utils.c) +ifneq ($(strip $(UTIL_SRCS)),) +PRIMARY_UTIL := $(word 1,$(sort $(UTIL_SRCS))) +SRCS := $(filter-out $(filter-out $(PRIMARY_UTIL),$(UTIL_SRCS)),$(SRCS)) +endif + +REG_SRCS := $(wildcard $(SRC_DIR)/*registry.c) +ifneq ($(strip $(REG_SRCS)),) +PRIMARY_REG := $(word 1,$(sort $(REG_SRCS))) +SRCS := $(filter-out $(filter-out $(PRIMARY_REG),$(REG_SRCS)),$(SRCS)) +endif +OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS)) +OBJS := $(patsubst ../examples/%.c,$(BUILD_DIR)/%.o,$(OBJS)) + +TARGET = $(BUILD_DIR)/test_runner + +.PHONY: all build test clean + +all: build + +build: $(TARGET) + +# Generic build rule for any C file in src/ +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c + mkdir -p $(@D) + $(CC) $(CFLAGS) $(EXTRA_CFLAGS) -I$(INCLUDE_DIR) -c $< -o $@ + +# Rule for examples directory +$(BUILD_DIR)/%.o: ../examples/%.c + mkdir -p $(@D) + $(CC) $(CFLAGS) $(EXTRA_CFLAGS) -I$(INCLUDE_DIR) -c $< -o $@ + +# Link all objects into the test runner +$(TARGET): $(OBJS) + mkdir -p $(@D) + $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(OBJS) $(LDLIBS) -o $@ + +# Placeholder test target (adjust as needed) +test: + @echo "Running C tests... (Placeholder)" + @echo "No actual tests implemented yet. This is a placeholder." + # For example: ./$(TARGET) test_be_basic + +clean: + rm -rf $(BUILD_DIR) +""" + let writeMk () = System.IO.File.WriteAllText(mkPath, mk) + if System.IO.File.Exists(mkPath) then + try + let existing = System.IO.File.ReadAllText(mkPath) + if existing.Contains("Signal.CANdy harness Makefile v1") then + if existing <> mk then + writeMk() + printfn "Harness: Makefile updated to latest template." + else + printfn "Harness: Makefile already up-to-date." + else + let bak = mkPath + ".bak" + System.IO.File.Copy(mkPath, bak, true) + writeMk() + printfn "Harness: Makefile upgraded with backup at %s" bak + with ex -> + eprintfn "Harness Makefile upgrade warning: %s" ex.Message + else + writeMk() + printfn "Harness: Makefile created at %s" mkPath + + // Provide compatibility alias headers so legacy test sources that include sc_*.h keep building + let tryCreateAlias (pattern: string) (aliasName: string) = + try + let headers = System.IO.Directory.GetFiles(includeDir, pattern) |> Array.sort + if headers.Length > 0 then + let primary = System.IO.Path.GetFileName(headers.[0]) + let aliasPath = System.IO.Path.Combine(includeDir, aliasName) + if not (System.IO.File.Exists(aliasPath)) then + let content = sprintf "/* Auto-generated alias for harness compatibility */\n#include \"%s\"\n" primary + System.IO.File.WriteAllText(aliasPath, content) + printfn "Harness: Created alias header %s -> %s" aliasName primary + with ex -> + eprintfn "Harness alias header warning (%s): %s" aliasName ex.Message + + // sc_utils.h -> utils.h (if sc_utils.h missing) + tryCreateAlias "*utils.h" "sc_utils.h" + // sc_registry.h -> registry.h (if sc_registry.h missing) + tryCreateAlias "*registry.h" "sc_registry.h" + with ex -> + eprintfn "Harness generation warning: %s" ex.Message 0 | Error err -> let msg =