From 06568da140b0e1758051fba064ab82e7a65e67a5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 21 Aug 2025 18:51:54 +0800 Subject: [PATCH] expand template vars in targets config --- internal/crosscompile/crosscompile.go | 89 ++++++++++++++- internal/crosscompile/crosscompile_test.go | 123 +++++++++++++++++++++ 2 files changed, 209 insertions(+), 3 deletions(-) diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index fea42d77..f81ccc7c 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -44,6 +44,83 @@ func cacheDir() string { return filepath.Join(env.LLGoCacheDir(), "crosscompile") } +// expandEnv expands template variables in a string +// Supports variables like {port}, {hex}, {bin}, {root}, {tmpDir}, etc. +// Special case: {} expands to the first available file variable (hex, bin, img, zip) +func expandEnv(template string, envs map[string]string) string { + return expandEnvWithDefault(template, envs) +} + +// expandEnvWithDefault expands template variables with optional default for {} +func expandEnvWithDefault(template string, envs map[string]string, defaultValue ...string) string { + if template == "" { + return "" + } + + result := template + + // Handle special case of {} - use provided default or first available file variable + if strings.Contains(result, "{}") { + defaultVal := "" + if len(defaultValue) > 0 && defaultValue[0] != "" { + defaultVal = defaultValue[0] + } else { + // Priority order: hex, bin, img, zip + for _, key := range []string{"hex", "bin", "img", "zip"} { + if value, exists := envs[key]; exists && value != "" { + defaultVal = value + break + } + } + } + result = strings.ReplaceAll(result, "{}", defaultVal) + } + + // Replace named variables + for key, value := range envs { + if key != "" { // Skip empty key used for {} default + result = strings.ReplaceAll(result, "{"+key+"}", value) + } + } + return result +} + +// expandEnvSlice expands template variables in a slice of strings +func expandEnvSlice(templates []string, envs map[string]string) []string { + return expandEnvSliceWithDefault(templates, envs) +} + +// expandEnvSliceWithDefault expands template variables in a slice with optional default for {} +func expandEnvSliceWithDefault(templates []string, envs map[string]string, defaultValue ...string) []string { + if len(templates) == 0 { + return templates + } + + result := make([]string, len(templates)) + for i, template := range templates { + result[i] = expandEnvWithDefault(template, envs, defaultValue...) + } + return result +} + +// buildEnvMap creates a map of template variables for the current context +func buildEnvMap(llgoRoot string) map[string]string { + envs := make(map[string]string) + + // Basic paths + envs["root"] = llgoRoot + envs["tmpDir"] = os.TempDir() + + // These will typically be set by calling code when actual values are known + // envs["port"] = "" // Serial port (e.g., "/dev/ttyUSB0", "COM3") + // envs["hex"] = "" // Path to hex file + // envs["bin"] = "" // Path to binary file + // envs["img"] = "" // Path to image file + // envs["zip"] = "" // Path to zip file + + return envs +} + // getMacOSSysroot returns the macOS SDK path using xcrun func getMacOSSysroot() (string, error) { cmd := exec.Command("xcrun", "--sdk", "macosx", "--show-sdk-path") @@ -347,6 +424,9 @@ func useTarget(targetName string) (export Export, err error) { export.GOOS = config.GOOS export.GOARCH = config.GOARCH + // Build environment map for template variable expansion + envs := buildEnvMap(env.LLGoROOT()) + // Convert LLVMTarget, CPU, Features to CCFLAGS/LDFLAGS var ccflags []string var ldflags []string @@ -361,7 +441,9 @@ func useTarget(targetName string) (export Export, err error) { cflags = append(cflags, "--target="+config.LLVMTarget) ccflags = append(ccflags, "--target="+config.LLVMTarget) } - cflags = append(cflags, config.CFlags...) + // Expand template variables in cflags + expandedCFlags := expandEnvSlice(config.CFlags, envs) + cflags = append(cflags, expandedCFlags...) // Inspired by tinygo cpu := config.CPU @@ -394,10 +476,11 @@ func useTarget(targetName string) (export Export, err error) { } ldflags = append(ldflags, "-L", env.LLGoROOT()) // search targets/*.ld - // Combine with config flags + // Combine with config flags and expand template variables export.CFLAGS = cflags export.CCFLAGS = ccflags - export.LDFLAGS = append(ldflags, config.LDFlags...) + expandedLDFlags := expandEnvSlice(config.LDFlags, envs) + export.LDFLAGS = append(ldflags, expandedLDFlags...) return export, nil } diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index ce755516..72b4839f 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -301,3 +301,126 @@ func TestUseWithTarget(t *testing.T) { t.Error("Expected LDFLAGS to be set for native build") } } + +func TestExpandEnv(t *testing.T) { + envs := map[string]string{ + "port": "/dev/ttyUSB0", + "hex": "firmware.hex", + "bin": "firmware.bin", + "root": "/usr/local/llgo", + } + + tests := []struct { + template string + expected string + }{ + { + "avrdude -c arduino -p atmega328p -P {port} -U flash:w:{hex}:i", + "avrdude -c arduino -p atmega328p -P /dev/ttyUSB0 -U flash:w:firmware.hex:i", + }, + { + "simavr -m atmega328p -f 16000000 {}", + "simavr -m atmega328p -f 16000000 firmware.hex", // {} expands to hex (first priority) + }, + { + "-I{root}/lib/CMSIS/CMSIS/Include", + "-I/usr/local/llgo/lib/CMSIS/CMSIS/Include", + }, + { + "no variables here", + "no variables here", + }, + { + "", + "", + }, + } + + for _, test := range tests { + result := expandEnv(test.template, envs) + if result != test.expected { + t.Errorf("expandEnv(%q) = %q, want %q", test.template, result, test.expected) + } + } +} + +func TestExpandEnvSlice(t *testing.T) { + envs := map[string]string{ + "root": "/usr/local/llgo", + "port": "/dev/ttyUSB0", + } + + input := []string{ + "-I{root}/include", + "-DPORT={port}", + "static-flag", + } + + expected := []string{ + "-I/usr/local/llgo/include", + "-DPORT=/dev/ttyUSB0", + "static-flag", + } + + result := expandEnvSlice(input, envs) + + if len(result) != len(expected) { + t.Fatalf("expandEnvSlice length mismatch: got %d, want %d", len(result), len(expected)) + } + + for i, exp := range expected { + if result[i] != exp { + t.Errorf("expandEnvSlice[%d] = %q, want %q", i, result[i], exp) + } + } +} + +func TestExpandEnvWithDefault(t *testing.T) { + envs := map[string]string{ + "port": "/dev/ttyUSB0", + "hex": "firmware.hex", + "bin": "firmware.bin", + "img": "image.img", + } + + tests := []struct { + template string + defaultValue string + expected string + }{ + { + "simavr {}", + "", // No default - should use hex (priority) + "simavr firmware.hex", + }, + { + "simavr {}", + "custom.elf", // Explicit default + "simavr custom.elf", + }, + { + "qemu -kernel {}", + "vmlinux", // Custom kernel + "qemu -kernel vmlinux", + }, + { + "no braces here", + "ignored", + "no braces here", + }, + } + + for i, test := range tests { + var result string + if test.defaultValue == "" { + result = expandEnvWithDefault(test.template, envs) + } else { + result = expandEnvWithDefault(test.template, envs, test.defaultValue) + } + + if result != test.expected { + t.Errorf("Test %d: expandEnvWithDefault(%q, envs, %q) = %q, want %q", + i, test.template, test.defaultValue, result, test.expected) + } + } +}