Skip to content

Commit ec5cf62

Browse files
authored
Sorcerer: Add CLI (#890)
1 parent dccb443 commit ec5cf62

5 files changed

Lines changed: 834 additions & 125 deletions

File tree

tools/sorcerer/README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,127 @@
11
# Sorcerer
2+
3+
Sorcerer is a suite of tools for visualizing and generating MicroZig register definitions. It
4+
consists of two components:
5+
6+
- **Sorcerer (GUI)**: A graphical application for browsing and editing register definitions
7+
- **sorcerer-cli**: A command-line tool for generating register code
8+
9+
Both tools work with MicroZig's register definition files (SVD, ATDF, Embassy formats) and use
10+
[regz](../regz/) to generate type-safe Zig code.
11+
12+
## Building
13+
14+
From the `tools/sorcerer/` directory:
15+
16+
```bash
17+
# Build both tools
18+
zig build
19+
```
20+
21+
## Sorcerer GUI
22+
23+
The GUI provides an interactive interface for:
24+
- Browsing all MicroZig register definitions
25+
- Viewing generated Zig code with syntax highlighting
26+
- Opening custom SVD/ATDF files
27+
- Searching chips, boards, and targets
28+
- Viewing patch files that modify register definitions
29+
- Statistics overview showing chip counts, formats, and patch coverage
30+
31+
### Running
32+
33+
```bash
34+
zig build run
35+
# or
36+
./zig-out/bin/sorcerer
37+
```
38+
39+
### Dependencies
40+
41+
The GUI requires additional dependencies:
42+
- DVUI (SDL3-based UI framework)
43+
- tree-sitter-zig (syntax highlighting)
44+
- serial (serial port communication)
45+
46+
## `sorcerer-cli`
47+
48+
A lightweight CLI alternative that generates register definitions without GUI dependencies.
49+
50+
### Usage
51+
52+
```
53+
sorcerer-cli <command> [options]
54+
55+
Commands:
56+
list List all available targets
57+
generate <chip> Generate register definitions for a chip
58+
59+
Options for 'list':
60+
--port <name> Filter by port name (e.g., rp2xxx, ch32v)
61+
--json Output in JSON format
62+
63+
Options for 'generate':
64+
-o, --output <dir> Output directory (default: ./zig-out)
65+
66+
General options:
67+
-h, --help Show help
68+
```
69+
70+
### Examples
71+
72+
```bash
73+
# List all available chips
74+
./zig-out/bin/sorcerer-cli list
75+
76+
# Output:
77+
# CHIP PORT BOARD
78+
# ------------------------ ------------------------ --------------------------------
79+
# RP2040 raspberrypi/rp2xxx Raspberry Pi Pico
80+
# RP2350 raspberrypi/rp2xxx Raspberry Pi Pico 2
81+
# STM32F103C8 stmicro/stm32 -
82+
# CH32V003 wch/ch32v -
83+
# ...
84+
85+
# Filter by port (substring matching)
86+
./zig-out/bin/sorcerer-cli list --port rp2xxx
87+
88+
# JSON output (for scripting)
89+
./zig-out/bin/sorcerer-cli list --json | jq '.[] | select(.port | contains("rp2xxx"))'
90+
91+
# Generate register definitions for a chip
92+
./zig-out/bin/sorcerer-cli generate RP2040 -o ./my-regs/
93+
94+
# This generates:
95+
# ./my-regs/RP2040.zig
96+
# ./my-regs/types.zig
97+
# ./my-regs/types/
98+
```
99+
100+
### Running via build system
101+
102+
```bash
103+
# Run CLI with arguments
104+
zig build run-cli -- list --port ch32v
105+
zig build run-cli -- generate CH32V003 -o /tmp/regs
106+
```
107+
108+
## Architecture
109+
110+
Both tools share the same underlying data:
111+
112+
1. **Build time**: `build.zig` collects all register definitions from MicroZig ports
113+
2. **Schema generation**: Schemas are embedded as Zig compile-time constants in a generated
114+
`register_schemas.zig` file
115+
3. **Both GUI and CLI**: Import the same `schemas` module - no runtime file loading or JSON parsing
116+
needed
117+
118+
## Register Definition Formats
119+
120+
Sorcerer supports the following formats via regz:
121+
122+
| Format | Extension | Description |
123+
|--------|-----------|-------------|
124+
| SVD | `.svd` | ARM CMSIS System View Description |
125+
| ATDF | `.atdf` | Microchip ATDF (AVR/SAM devices) |
126+
| Embassy | - | Embassy HAL register definitions |
127+
| TargetDB | - | TI TargetDB format |

tools/sorcerer/build.zig

Lines changed: 183 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,9 @@ pub fn build(b: *std.Build) void {
1414
const mb = MicroBuild.init(b, mz_dep) orelse return;
1515
const register_schemas = get_register_schemas(b, mb) catch @panic("OOM");
1616
const write_files = b.addWriteFiles();
17-
const register_schema = write_files.add("register_schemas.json", std.json.Stringify.valueAlloc(b.allocator, register_schemas, .{}) catch @panic("OOM"));
18-
const register_schema_install = b.addInstallFile(register_schema, "data/register_schemas.json");
19-
b.getInstallStep().dependOn(&register_schema_install.step);
2017

21-
const dvui_dep = b.dependency("dvui", .{
22-
.target = target,
23-
.optimize = optimize,
24-
});
18+
// Generate Zig file with embedded schemas (used by both CLI and GUI)
19+
const register_schema_zig = write_files.add("register_schemas.zig", generate_zig_schema_literal(b.allocator, register_schemas) catch @panic("OOM"));
2520

2621
const regz_dep = mz_dep.builder.dependency("tools/regz", .{
2722
.target = target,
@@ -32,13 +27,63 @@ pub fn build(b: *std.Build) void {
3227
.optimize = .ReleaseSafe,
3328
});
3429

30+
const regz_mod = regz_dep.module("regz");
31+
32+
// Shared module for RegisterSchemaUsage (used by both schemas_mod and cli_mod)
33+
const register_schema_usage_mod = b.createModule(.{
34+
.root_source_file = b.path("src/RegisterSchemaUsage.zig"),
35+
});
36+
37+
// Create schemas module from generated Zig file
38+
const schemas_mod = b.createModule(.{
39+
.root_source_file = register_schema_zig,
40+
.imports = &.{
41+
.{ .name = "RegisterSchemaUsage", .module = register_schema_usage_mod },
42+
},
43+
});
44+
45+
// ─────────────────────────────────────────────────────────────────────────
46+
// CLI executable
47+
// ─────────────────────────────────────────────────────────────────────────
48+
const cli_mod = b.createModule(.{
49+
.root_source_file = b.path("src/cli.zig"),
50+
.target = target,
51+
.optimize = optimize,
52+
.imports = &.{
53+
.{ .name = "regz", .module = regz_mod },
54+
.{ .name = "schemas", .module = schemas_mod },
55+
.{ .name = "RegisterSchemaUsage", .module = register_schema_usage_mod },
56+
},
57+
});
58+
59+
const cli_exe = b.addExecutable(.{
60+
.name = "sorcerer-cli",
61+
.root_module = cli_mod,
62+
});
63+
b.installArtifact(cli_exe);
64+
65+
const run_cli_cmd = b.addRunArtifact(cli_exe);
66+
run_cli_cmd.step.dependOn(b.getInstallStep());
67+
if (b.args) |args| {
68+
run_cli_cmd.addArgs(args);
69+
}
70+
const run_cli_step = b.step("run-cli", "Run the CLI tool");
71+
run_cli_step.dependOn(&run_cli_cmd.step);
72+
73+
// ─────────────────────────────────────────────────────────────────────────
74+
// GUI executable
75+
// ─────────────────────────────────────────────────────────────────────────
76+
const dvui_dep = b.dependency("dvui", .{
77+
.target = target,
78+
.optimize = optimize,
79+
});
80+
3581
const serial_dep = b.dependency("serial", .{
3682
.target = target,
3783
.optimize = optimize,
3884
});
3985

4086
const dvui_mod = dvui_dep.module("dvui_sdl3");
41-
const regz_mod = regz_dep.module("regz");
4287
const serial_mod = serial_dep.module("serial");
4388

4489
const exe_mod = b.createModule(.{
@@ -58,6 +103,14 @@ pub fn build(b: *std.Build) void {
58103
.name = "serial",
59104
.module = serial_mod,
60105
},
106+
.{
107+
.name = "schemas",
108+
.module = schemas_mod,
109+
},
110+
.{
111+
.name = "RegisterSchemaUsage",
112+
.module = register_schema_usage_mod,
113+
},
61114
},
62115
});
63116

@@ -111,7 +164,7 @@ pub fn build(b: *std.Build) void {
111164
run_cmd.addArgs(args);
112165
}
113166

114-
const run_step = b.step("run", "Run the app");
167+
const run_step = b.step("run", "Run the GUI app");
115168
run_step.dependOn(&run_cmd.step);
116169

117170
const exe_unit_tests = b.addTest(.{
@@ -368,9 +421,16 @@ fn get_register_schemas(b: *std.Build, mb: *MicroBuild) ![]const RegisterSchemaU
368421
}
369422

370423
if (t.board) |board| if (boards.getEntry(lazy_path)) |entry| {
371-
try entry.value_ptr.append(b.allocator, .{
372-
.name = board.name,
373-
});
424+
// Check if this board name already exists (deduplicate by name)
425+
const board_exists = for (entry.value_ptr.items) |existing_board| {
426+
if (std.mem.eql(u8, existing_board.name, board.name)) break true;
427+
} else false;
428+
429+
if (!board_exists) {
430+
try entry.value_ptr.append(b.allocator, .{
431+
.name = board.name,
432+
});
433+
}
374434
} else {
375435
var board_list: std.ArrayList(RegisterSchemaUsage.Board) = .{};
376436
try board_list.append(b.allocator, .{
@@ -418,3 +478,114 @@ fn get_port_name(path: []const u8) []const u8 {
418478

419479
unreachable;
420480
}
481+
482+
/// Generate a Zig source file containing the register schemas as compile-time constants.
483+
fn generate_zig_schema_literal(allocator: std.mem.Allocator, schemas: []const RegisterSchemaUsage) ![]const u8 {
484+
var buf: std.ArrayList(u8) = .{};
485+
const writer = buf.writer(allocator);
486+
487+
// Helper to normalize paths (convert backslashes to forward slashes for Windows compatibility)
488+
const normalize_path = struct {
489+
fn call(alloc: std.mem.Allocator, path: []const u8) ![]const u8 {
490+
const result = try alloc.alloc(u8, path.len);
491+
for (path, 0..) |c, i| {
492+
result[i] = if (c == '\\') '/' else c;
493+
}
494+
return result;
495+
}
496+
}.call;
497+
498+
try writer.writeAll(
499+
\\// Auto-generated file - do not edit manually.
500+
\\// Generated by tools/sorcerer/build.zig
501+
\\
502+
\\const RegisterSchemaUsage = @import("RegisterSchemaUsage");
503+
\\
504+
\\pub const schemas: []const RegisterSchemaUsage = &.{
505+
\\
506+
);
507+
508+
for (schemas) |schema| {
509+
try writer.writeAll(" .{\n");
510+
511+
// Format
512+
try writer.print(" .format = .{s},\n", .{@tagName(schema.format)});
513+
514+
// Chips
515+
try writer.writeAll(" .chips = &.{\n");
516+
for (schema.chips) |chip| {
517+
try writer.writeAll(" .{\n");
518+
try writer.print(" .name = \"{s}\",\n", .{chip.name});
519+
try writer.print(" .target_name = \"{s}\",\n", .{chip.target_name});
520+
// Patch files
521+
if (chip.patch_files.len > 0) {
522+
try writer.writeAll(" .patch_files = &.{\n");
523+
for (chip.patch_files) |patch_file| {
524+
switch (patch_file) {
525+
.src_path => |src| {
526+
const sub_path = try normalize_path(allocator, src.sub_path);
527+
const build_root = try normalize_path(allocator, src.build_root);
528+
try writer.writeAll(" .{ .src_path = .{\n");
529+
try writer.print(" .sub_path = \"{s}\",\n", .{sub_path});
530+
try writer.print(" .build_root = \"{s}\",\n", .{build_root});
531+
try writer.writeAll(" } },\n");
532+
},
533+
.dependency => |dep| {
534+
const sub_path = try normalize_path(allocator, dep.sub_path);
535+
const build_root = try normalize_path(allocator, dep.build_root);
536+
try writer.writeAll(" .{ .dependency = .{\n");
537+
try writer.print(" .sub_path = \"{s}\",\n", .{sub_path});
538+
try writer.print(" .build_root = \"{s}\",\n", .{build_root});
539+
try writer.print(" .dep_name = \"{s}\",\n", .{dep.dep_name});
540+
try writer.writeAll(" } },\n");
541+
},
542+
}
543+
}
544+
try writer.writeAll(" },\n");
545+
}
546+
try writer.writeAll(" },\n");
547+
}
548+
try writer.writeAll(" },\n");
549+
550+
// Boards
551+
try writer.writeAll(" .boards = &.{");
552+
for (schema.boards, 0..) |board, i| {
553+
if (i > 0) try writer.writeAll(", ");
554+
try writer.print(".{{ .name = \"{s}\" }}", .{board.name});
555+
}
556+
try writer.writeAll("},\n");
557+
558+
// Location
559+
try writer.writeAll(" .location = ");
560+
switch (schema.location) {
561+
.src_path => |src| {
562+
const sub_path = try normalize_path(allocator, src.sub_path);
563+
const build_root = try normalize_path(allocator, src.build_root);
564+
try writer.writeAll(".{ .src_path = .{\n");
565+
try writer.print(" .port_name = \"{s}\",\n", .{src.port_name});
566+
try writer.print(" .sub_path = \"{s}\",\n", .{sub_path});
567+
try writer.print(" .build_root = \"{s}\",\n", .{build_root});
568+
try writer.writeAll(" } },\n");
569+
},
570+
.dependency => |dep| {
571+
const sub_path = try normalize_path(allocator, dep.sub_path);
572+
const build_root = try normalize_path(allocator, dep.build_root);
573+
try writer.writeAll(".{ .dependency = .{\n");
574+
try writer.print(" .sub_path = \"{s}\",\n", .{sub_path});
575+
try writer.print(" .build_root = \"{s}\",\n", .{build_root});
576+
try writer.print(" .dep_name = \"{s}\",\n", .{dep.dep_name});
577+
try writer.print(" .port_name = \"{s}\",\n", .{dep.port_name});
578+
try writer.writeAll(" } },\n");
579+
},
580+
}
581+
582+
try writer.writeAll(" },\n");
583+
}
584+
585+
try writer.writeAll(
586+
\\};
587+
\\
588+
);
589+
590+
return buf.toOwnedSlice(allocator);
591+
}

tools/sorcerer/src/RegzWindow.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const Allocator = std.mem.Allocator;
119119

120120
const regz = @import("regz");
121121
const VirtualFilesystem = regz.VirtualFilesystem;
122-
const RegisterSchemaUsage = @import("RegisterSchemaUsage.zig");
122+
const RegisterSchemaUsage = @import("RegisterSchemaUsage");
123123
const diffz = @import("diffz");
124124

125125
const dvui = @import("dvui");

0 commit comments

Comments
 (0)