Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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 <path>`: input DBC (required)
- `-o, --out <dir>`: output directory (required)
- `-c, --config <path>`: 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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 <prefix>utils.h, registry.h or <prefix>registry.h (prefix configurable via config: file_prefix)
- <message>.h per message (snake_case filename)
- gen/src/
- sc_utils.c, sc_registry.c (prefix configurable)
- utils.c or <prefix>utils.c, registry.c or <prefix>registry.c (prefix configurable)
- <message>.c per message (snake_case filename)
- main.c (test runner; exclude in firmware builds)

Expand Down Expand Up @@ -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
Expand All @@ -491,6 +526,13 @@ Windows notes (early guidance)
- Linking: MSVC doesn’t need `-lm`; math functions (llround/llroundf) are in the CRT when including `<math.h>`.
- 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)
Expand Down
18 changes: 8 additions & 10 deletions infra/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
232 changes: 218 additions & 14 deletions src/Signal.CANdy.CLI/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,40 @@ 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
OutDir = None
ConfigPath = None
ShowVersion = false
ShowHelp = false
WithHarness = false
Unknown = [] }

let usage () =
String.concat "\n" [
"Signal.CANdy.CLI β€” DBC β†’ C code generator";
"";
"Usage:";
" signal-candy --dbc <file.dbc> --out <out_dir> [--config <config.yaml>]";
" signal-candy -d <file.dbc> -o <out_dir> [-c <config.yaml>] [-t]";
" signal-candy --version";
" signal-candy --help";
"";
"Options:";
" --dbc <path> Path to input DBC file (required)";
" --out <dir> Output directory for generated C files (required)";
" --config <path> 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> Path to input DBC file (required)";
" -o, --out <dir> Output directory for generated C files (required)";
" -c, --config <path> 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 =
Expand All @@ -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
Expand Down Expand Up @@ -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 -> <prefix>utils.h (if sc_utils.h missing)
tryCreateAlias "*utils.h" "sc_utils.h"
// sc_registry.h -> <prefix>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 =
Expand Down