Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/actions/test-helloworld/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ runs:
func main() {
}
EOL
llgo build -v -target esp32-coreboard-v2 -o demo.out .
test -f demo.out.elf && echo "ESP32 cross-compilation test passed: demo.out.elf generated"
llgo build -v -target esp32-coreboard-v2 -o demo.elf .
test -f demo.elf && echo "ESP32 cross-compilation test passed: demo.elf generated"
exit $?
2 changes: 1 addition & 1 deletion .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ jobs:
echo "Testing cross-compilation wasm with $(go env GOVERSION)"

# Compile for wasm architecture
GOOS=wasip1 GOARCH=wasm llgo build -o hello -tags=nogc -v ./helloc
GOOS=wasip1 GOARCH=wasm llgo build -o hello.wasm -tags=nogc -v ./helloc

# Check file type
file hello.wasm
Expand Down
4 changes: 2 additions & 2 deletions _demo/embed/export/verify_export.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ echo "Building for embedded target..."

# Build for embedded target as executable
# Use llgo directly instead of llgo.sh to avoid go.mod version check
llgo build -o test-verify --target=esp32 .
llgo build -o test-verify.elf --target=esp32 .

echo "Checking exported symbols..."

Expand Down Expand Up @@ -44,7 +44,7 @@ echo ""

echo "Testing that non-embedded target rejects different export names..."
# Build without --target should fail with panic
if llgo build -o test-notarget . 2>&1 | grep -q 'export comment has wrong name "LPSPI2_IRQHandler"'; then
if llgo build -o test-notarget.elf . 2>&1 | grep -q 'export comment has wrong name "LPSPI2_IRQHandler"'; then
echo "✅ Correctly rejected different export name on non-embedded target"
else
echo "❌ Should have panicked with 'export comment has wrong name' error"
Expand Down
2 changes: 1 addition & 1 deletion _demo/embed/test_esp32c3_startup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ EOF

echo "==> Building for ESP32-C3 target (ELF + BIN)..."
cd "$TEMP_DIR"
llgo build -target=esp32c3 -o test -obin "$TEST_GO"
llgo build -target=esp32c3 -o test.elf -obin "$TEST_GO"

if [ ! -f "$TEST_ELF" ]; then
echo "✗ FAIL: Build failed, $TEST_ELF not found"
Expand Down
13 changes: 9 additions & 4 deletions _demo/go/export/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,18 +163,22 @@ if [[ "$run_build_mode_tests" == true ]]; then

# Test 1: c-shared mode
print_status "=== Test 1: Building with -buildmode c-shared ==="
if $LLGO_SCRIPT build -buildmode c-shared -o export .; then
if [[ "$OSTYPE" == "darwin"* ]]; then
SHARED_LIB="libexport.dylib"
else
SHARED_LIB="libexport.so"
fi

if $LLGO_SCRIPT build -buildmode c-shared -o "$SHARED_LIB" .; then
print_status "Build succeeded"

# Check generated files (different extensions on different platforms)
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
check_file "libexport.dylib" "Dynamic library (libexport.dylib)"
SHARED_LIB="libexport.dylib"
else
# Linux and others
check_file "libexport.so" "Dynamic library (libexport.so)"
SHARED_LIB="libexport.so"
fi

check_file "libexport.h" "C header (libexport.h)"
Expand Down Expand Up @@ -216,7 +220,7 @@ fi

# Test 2: c-archive mode
print_status "=== Test 2: Building with -buildmode c-archive ==="
if $LLGO_SCRIPT build -buildmode c-archive -o export .; then
if $LLGO_SCRIPT build -buildmode c-archive -o libexport.a .; then
print_status "Build succeeded"

# Check generated files
Expand Down Expand Up @@ -314,6 +318,7 @@ else
print_error "Go export demo execution failed"
print_error "Error output:"
cat /tmp/go_export_output.log | sed 's/^/ /'
build_failures=$((build_failures + 1))
fi

# Cleanup temporary file
Expand Down
2 changes: 1 addition & 1 deletion _demo/go/export/use/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ all: build-go $(TARGET)
# Build the Go library first
build-go:
@echo $(BUILD_MSG)
cd .. && ../../../dev/llgo.sh build -buildmode $(BUILDMODE) $(LLGOFLAGS) -o export .
cd .. && ../../../dev/llgo.sh build -buildmode $(BUILDMODE) $(LLGOFLAGS) -o $(notdir $(LIBRARY)) .

# Build the C executable
$(TARGET): $(SOURCES) $(LIBRARY) $(HEADER)
Expand Down
2 changes: 1 addition & 1 deletion doc/Embedded_Cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Flags

- `-o <file>` - Specify output file name
- `-o <file>` - Specify output file name (used as-is; no prefix or extension is added)
- `-target <platform>` - Specify target platform for cross-compilation
- `-obin` - Generate binary format output (requires `-target`)
- `-ohex` - Generate Intel HEX format output (requires `-target`)
Expand Down
8 changes: 6 additions & 2 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,8 +733,7 @@ func Build(inv Invocation) ([]Package, error) {

// Generate C headers for c-archive and c-shared modes before linking
if ctx.buildConf.BuildMode == BuildModeCArchive || ctx.buildConf.BuildMode == BuildModeCShared {
libname := strings.TrimSuffix(filepath.Base(outFmts.Out), conf.AppExt)
headerPath := filepath.Join(filepath.Dir(outFmts.Out), libname) + ".h"
libname, headerPath := cHeaderOutputPath(outFmts.Out)
pkgs := cHeaderPackages(allPkgs)
headerErr := header.GenHeaderFile(prog, pkgs, libname, headerPath, verbose)
if headerErr != nil {
Expand Down Expand Up @@ -801,6 +800,11 @@ func Build(inv Invocation) ([]Package, error) {
return allPkgs, nil
}

func cHeaderOutputPath(output string) (libname, headerPath string) {
libname = strings.TrimSuffix(filepath.Base(output), filepath.Ext(output))
return libname, filepath.Join(filepath.Dir(output), libname) + ".h"
}

// cHeaderPackages excludes the patched standard runtime implementation. Its
// //export callbacks are linker implementation details and may use internal C
// types that are deliberately not representable in a public generated header.
Expand Down
45 changes: 22 additions & 23 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -981,13 +981,33 @@ func TestRunPrintfWithStdioNobuf(t *testing.T) {
mockRun([]string{"../../cl/_testdata/printf"}, &Config{Mode: ModeRun})
}

func TestCHeaderOutputPath(t *testing.T) {
tests := []struct {
output string
wantLibname string
wantHeaderPath string
}{
{output: "libmylib.a", wantLibname: "libmylib", wantHeaderPath: "libmylib.h"},
{output: "build/custom.so", wantLibname: "custom", wantHeaderPath: "build/custom.h"},
{output: "/tmp/custom.dylib", wantLibname: "custom", wantHeaderPath: "/tmp/custom.h"},
}

for _, tt := range tests {
t.Run(tt.output, func(t *testing.T) {
libname, headerPath := cHeaderOutputPath(tt.output)
if libname != tt.wantLibname || headerPath != tt.wantHeaderPath {
t.Fatalf("cHeaderOutputPath(%q) = (%q, %q), want (%q, %q)", tt.output, libname, headerPath, tt.wantLibname, tt.wantHeaderPath)
}
})
}
}

func TestTestOutputFileLogic(t *testing.T) {
// Test output file path determination logic for test mode
tests := []struct {
name string
pkgName string
conf *Config
multiPkg bool
wantBase string
wantDir string
description string
Expand All @@ -996,34 +1016,14 @@ func TestTestOutputFileLogic(t *testing.T) {
name: "compile only without -o",
pkgName: "mypackage.test",
conf: &Config{Mode: ModeTest, CompileOnly: true},
multiPkg: false,
wantBase: "mypackage.test",
wantDir: ".",
description: "-c without -o: write pkg.test in current directory",
},
{
name: "with -o absolute file path",
pkgName: "mypackage",
conf: &Config{Mode: ModeTest, OutFile: "/tmp/mytest.test", AppExt: ".test"},
multiPkg: false,
wantBase: "mytest",
wantDir: "/tmp",
description: "-o with absolute file path: use specified file",
},
{
name: "with -o relative file path",
pkgName: "mypackage",
conf: &Config{Mode: ModeTest, OutFile: "my.test", AppExt: ".test"},
multiPkg: false,
wantBase: "my",
wantDir: ".",
description: "-o with relative file path: use specified file in current dir",
},
{
name: "with -o directory",
pkgName: "mypackage.test",
conf: &Config{Mode: ModeTest, OutFile: "/tmp/build/", AppExt: ".test"},
multiPkg: false,
wantBase: "mypackage.test",
wantDir: "/tmp/build/",
description: "-o with directory: write pkg.test in that directory",
Expand All @@ -1032,7 +1032,6 @@ func TestTestOutputFileLogic(t *testing.T) {
name: "default test mode",
pkgName: "mypackage",
conf: &Config{Mode: ModeTest, AppExt: ".test"},
multiPkg: false,
wantBase: "mypackage",
wantDir: "",
description: "default test mode: use temp file",
Expand All @@ -1041,7 +1040,7 @@ func TestTestOutputFileLogic(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
baseName, dir := determineBaseNameAndDir(tt.pkgName, tt.conf, tt.multiPkg)
baseName, dir := determineBaseNameAndDir(tt.pkgName, tt.conf)
if baseName != tt.wantBase {
t.Errorf("%s: got baseName=%q, want %q", tt.description, baseName, tt.wantBase)
}
Expand Down
57 changes: 26 additions & 31 deletions internal/build/outputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,36 +54,17 @@ func setOutFmt(conf *Config, formatName string) {
}
}

// buildOutFmts creates OutFmtDetails based on package, configuration and multi-package status
// determineBaseNameAndDir extracts the base name and directory from configuration
func determineBaseNameAndDir(pkgName string, conf *Config, multiPkg bool) (baseName, dir string) {
func determineBaseNameAndDir(pkgName string, conf *Config) (baseName, dir string) {
switch conf.Mode {
case ModeInstall:
return pkgName, conf.BinPath
case ModeBuild:
if !multiPkg && conf.OutFile != "" {
dir = filepath.Dir(conf.OutFile)
baseName = strings.TrimSuffix(filepath.Base(conf.OutFile), conf.AppExt)
if dir == "." {
dir = ""
}
return baseName, dir
}
return pkgName, ""
case ModeTest:
if conf.OutFile != "" {
// Handle -o flag for test mode
if strings.HasSuffix(conf.OutFile, "/") || isDir(conf.OutFile) {
// If OutFile ends in / or is a directory, write pkg.test in that directory
// pkgName for test packages already includes .test suffix
return pkgName, conf.OutFile
}
// Otherwise, use the specified file path
dir = filepath.Dir(conf.OutFile)
baseName = strings.TrimSuffix(filepath.Base(conf.OutFile), conf.AppExt)
// Don't convert "." to "" for test mode with explicit output
// This preserves the information that user specified an output file
return baseName, dir
// Explicit file paths are handled by buildOutFmts before reaching here.
return pkgName, conf.OutFile
}
if conf.CompileOnly {
// -c without -o: write pkg.test in current directory
Expand All @@ -103,6 +84,15 @@ func isDir(path string) bool {
return err == nil && info.IsDir()
}

// explicitOutFile reports whether the user controls the final output path verbatim.
func explicitOutFile(conf *Config, multiPkg bool) bool {
if conf.OutFile == "" || multiPkg {
return false
}
return conf.Mode == ModeBuild ||
(conf.Mode == ModeTest && !strings.HasSuffix(conf.OutFile, "/") && !isDir(conf.OutFile))
}

// applyPrefix applies build mode specific naming conventions
func applyPrefix(baseName string, buildMode BuildMode, target string, goos string) string {
// Determine the effective OS for naming conventions
Expand Down Expand Up @@ -140,7 +130,9 @@ func applyPrefix(baseName string, buildMode BuildMode, target string, goos strin

// buildOutputPath creates the final output path from baseName, dir and other parameters
func buildOutputPath(baseName, dir string, conf *Config, multiPkg bool, appExt string) (string, error) {
baseName = applyPrefix(baseName, conf.BuildMode, conf.Target, conf.Goos)
if conf.OutFile == "" {
baseName = applyPrefix(baseName, conf.BuildMode, conf.Target, conf.Goos)
}
Comment thread
visualfc marked this conversation as resolved.

if dir != "" {
// dir == "." means current directory (explicit user specification)
Expand All @@ -161,18 +153,21 @@ func buildOutputPath(baseName, dir string, conf *Config, multiPkg bool, appExt s
}
}

// buildOutFmts creates OutFmtDetails based on package, configuration and multi-package status.
func buildOutFmts(pkgName string, conf *Config, multiPkg bool, crossCompile *crosscompile.Export) (*OutFmtDetails, error) {
details := &OutFmtDetails{}

// Determine base name and directory
baseName, dir := determineBaseNameAndDir(pkgName, conf, multiPkg)

// Build output path
outputPath, err := buildOutputPath(baseName, dir, conf, multiPkg, conf.AppExt)
if err != nil {
return nil, err
if explicitOutFile(conf, multiPkg) {
// An explicit -o file name is authoritative: do not rewrite its prefix or extension.
details.Out = conf.OutFile
} else {
baseName, dir := determineBaseNameAndDir(pkgName, conf)
outputPath, err := buildOutputPath(baseName, dir, conf, multiPkg, conf.AppExt)
if err != nil {
return nil, err
}
details.Out = outputPath
}
details.Out = outputPath
if conf.PCLNMode == PCLNExternal {
details.PCLN = pclnSidecarPath(details.Out)
}
Expand Down
Loading
Loading