diff --git a/.github/actions/setup-embed-deps/action.yml b/.github/actions/setup-embed-deps/action.yml new file mode 100644 index 0000000000..82222efc80 --- /dev/null +++ b/.github/actions/setup-embed-deps/action.yml @@ -0,0 +1,35 @@ +name: "Setup Embedded Dependencies" +description: "Install dependencies required for embedded QEMU tests" + +runs: + using: "composite" + steps: + - name: Install macOS embedded dependencies + if: runner.os == 'macOS' + shell: bash + env: + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 + run: | + brew update + brew install sdl2 + + - name: Install Ubuntu embedded dependencies + if: runner.os == 'Linux' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y libsdl2-2.0-0 + + - name: Install ESP32 RISC-V QEMU + shell: bash + run: | + chmod +x .github/workflows/install-esp-qemu.sh + QEMU_DIR=".cache/qemu" + .github/workflows/install-esp-qemu.sh "$QEMU_DIR" + echo "${PWD}/${QEMU_DIR}/bin" >> $GITHUB_PATH + + - name: Verify ESP32 RISC-V QEMU installation + shell: bash + run: | + which qemu-system-riscv32 + qemu-system-riscv32 --version diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6379bddafa..7da03c78e9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -34,6 +34,9 @@ jobs: with: llvm-version: ${{matrix.llvm}} + - name: Install embedded dependencies + uses: ./.github/actions/setup-embed-deps + - name: Clang information run: | echo $PATH diff --git a/.github/workflows/install-esp-qemu.sh b/.github/workflows/install-esp-qemu.sh new file mode 100755 index 0000000000..e097626422 --- /dev/null +++ b/.github/workflows/install-esp-qemu.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -e + +# Installation directory (from argument or default) +INSTALL_DIR="${1:-.cache/qemu}" + +# Detect platform +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) + +# Map architecture names +case "$ARCH" in + x86_64|amd64) + ARCH="x86_64" + ;; + aarch64|arm64) + ARCH="aarch64" + ;; + *) + echo "Unsupported architecture: $ARCH" + exit 1 + ;; +esac + +# Map OS names +case "$OS" in + darwin) + PLATFORM="${ARCH}-apple-darwin" + ;; + linux) + PLATFORM="${ARCH}-linux-gnu" + ;; + *) + echo "Unsupported OS: $OS" + exit 1 + ;; +esac + +# Download URL +VERSION="esp_develop_9.2.2_20250817" +FILENAME="qemu-riscv32-softmmu-${VERSION}-${PLATFORM}.tar.xz" +URL="https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250817/${FILENAME}" + +echo "Detected platform: $PLATFORM" +echo "Installing to: ${INSTALL_DIR}" +echo "Downloading ESP32 RISC-V QEMU from: $URL" + +# Download and extract +mkdir -p "$INSTALL_DIR" +curl -fsSL "$URL" | tar -xJ -C "$INSTALL_DIR" --strip-components=1 + +# Verify installation +if [ ! -f "${INSTALL_DIR}/bin/qemu-system-riscv32" ]; then + echo "Error: qemu-system-riscv32 not found after extraction" + exit 1 +fi + +echo "ESP32 RISC-V QEMU installed successfully to: ${INSTALL_DIR}" diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 807135da1a..df8a017615 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -51,6 +51,8 @@ jobs: uses: ./.github/actions/setup-deps with: llvm-version: ${{matrix.llvm}} + - name: Install embedded dependencies + uses: ./.github/actions/setup-embed-deps - name: Download model artifact uses: actions/download-artifact@v7 with: @@ -209,7 +211,6 @@ jobs: run: | llgo test ./... - hello: continue-on-error: true timeout-minutes: 30 diff --git a/.github/workflows/targets.yml b/.github/workflows/targets.yml index 4a25cceefc..f93df13af8 100644 --- a/.github/workflows/targets.yml +++ b/.github/workflows/targets.yml @@ -1,4 +1,3 @@ - name: Targets on: diff --git a/_demo/embed/test_esp32c3_startup.sh b/_demo/embed/test_esp32c3_startup.sh index 5e81632aa6..b2a01aa430 100755 --- a/_demo/embed/test_esp32c3_startup.sh +++ b/_demo/embed/test_esp32c3_startup.sh @@ -1,6 +1,9 @@ #!/bin/bash # ESP32-C3 Startup and .init_array Regression Test # +# This script verifies ESP32-C3 compilation and runs the program in QEMU +# emulator to ensure basic functionality works correctly. +# # Verifies: # 1. _start uses newlib's __libc_init_array (not TinyGo's start.S) # 2. .init_array section is merged into .rodata section @@ -9,7 +12,9 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TEMP_DIR=$(mktemp -d) +# Create temp dir inside _demo/embed/ to use existing go.mod +TEMP_DIR="$SCRIPT_DIR/.test_tmp_$$" +mkdir -p "$TEMP_DIR" TEST_GO="$TEMP_DIR/main.go" TEST_ELF="test.elf" TEST_BIN="test.bin" @@ -32,7 +37,11 @@ echo "==> Creating minimal test program..." cat > "$TEST_GO" << 'EOF' package main -func main() {} +import "github.com/goplus/lib/c" + +func main() { + c.Printf(c.Str("Hello World\n")) +} EOF echo "==> Building for ESP32-C3 target (ELF + BIN)..." @@ -223,11 +232,29 @@ else exit 1 fi +echo "" +echo "=== Test 4: Verify QEMU output ===" + +# Ignore emulator boot logs and validate the last non-empty line. +RUN_OUT=$(llgo run -a -target=esp32c3-basic -emulator . 2>&1) +LAST_LINE=$(printf "%s\n" "$RUN_OUT" | tr -d '\r' | awk 'NF{line=$0} END{print line}') +if [ "$LAST_LINE" = "Hello World" ]; then + echo "✓ PASS: QEMU output ends with Hello World" +else + echo "✗ FAIL: QEMU output mismatch" + echo "Last line: $LAST_LINE" + echo "" + echo "Full output:" + echo "$RUN_OUT" + exit 1 +fi + echo "" echo "=== All Tests Passed ===" echo "✓ ESP32-C3 uses newlib startup (_start calls __libc_init_array)" echo "✓ .init_array merged into .rodata section" echo "✓ .rodata (including .init_array) included in BIN file" +echo "✓ QEMU output ends with Hello World" echo "✓ Constructor function pointers will be correctly flashed to ESP32-C3" exit 0 diff --git a/internal/crosscompile/compile/libc/libc_test.go b/internal/crosscompile/compile/libc/libc_test.go index a751fa35fc..8135a61c76 100644 --- a/internal/crosscompile/compile/libc/libc_test.go +++ b/internal/crosscompile/compile/libc/libc_test.go @@ -17,23 +17,23 @@ func TestGetNewlibESP32Config_LibConfig(t *testing.T) { t.Errorf("Expected Name '%s', got '%s'", expectedName, config.Name) } - expectedVersion := "esp-4.3.0_20250211-patch4" + expectedVersion := "esp-4.3.0_20250211-patch5" if config.Version != expectedVersion { t.Errorf("Expected Version '%s', got '%s'", expectedVersion, config.Version) } - expectedUrl := "https://github.com/goplus/newlib/archive/refs/tags/esp-4.3.0_20250211-patch4.tar.gz" + expectedUrl := "https://github.com/goplus/newlib/archive/refs/tags/esp-4.3.0_20250211-patch5.tar.gz" if config.Url != expectedUrl { t.Errorf("Expected Url '%s', got '%s'", expectedUrl, config.Url) } - expectedArchiveSrcDir := "newlib-esp-4.3.0_20250211-patch4" + expectedArchiveSrcDir := "newlib-esp-4.3.0_20250211-patch5" if config.ResourceSubDir != expectedArchiveSrcDir { t.Errorf("Expected ResourceSubDir '%s', got '%s'", expectedArchiveSrcDir, config.ResourceSubDir) } // Test String() method - expectedString := "newlib-esp32-esp-4.3.0_20250211-patch4" + expectedString := "newlib-esp32-esp-4.3.0_20250211-patch5" if config.String() != expectedString { t.Errorf("Expected String() '%s', got '%s'", expectedString, config.String()) } @@ -333,20 +333,19 @@ func TestGetNewlibESP32ConfigRISCV(t *testing.T) { } // Test Groups configuration - if len(config.Groups) != 3 { - t.Errorf("Expected 3 groups, got %d", len(config.Groups)) + if len(config.Groups) != 4 { + t.Errorf("Expected 4 groups, got %d", len(config.Groups)) } else { - // Group 0: libcrt0 + // Group 0: libsemihost group0 := config.Groups[0] - expectedOutput0 := "libcrt0-" + target + ".a" + expectedOutput0 := "libsemihost-" + target + ".a" if group0.OutputFileName != expectedOutput0 { t.Errorf("Group0 OutputFileName expected '%s', got '%s'", expectedOutput0, group0.OutputFileName) } // Check sample files in group0 sampleFiles0 := []string{ - filepath.Join(baseDir, "libgloss", "riscv", "esp", "esp_board.c"), - filepath.Join(baseDir, "libgloss", "riscv", "esp", "crt1-board.S"), + filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_exit.c"), } for _, sample := range sampleFiles0 { found := false @@ -361,17 +360,17 @@ func TestGetNewlibESP32ConfigRISCV(t *testing.T) { } } - // Group 1: libgloss + // Group 1: libcrt0 group1 := config.Groups[1] - expectedOutput1 := "libgloss-" + target + ".a" + expectedOutput1 := "libcrt0-" + target + ".a" if group1.OutputFileName != expectedOutput1 { t.Errorf("Group1 OutputFileName expected '%s', got '%s'", expectedOutput1, group1.OutputFileName) } // Check sample files in group1 sampleFiles1 := []string{ - filepath.Join(baseDir, "libgloss", "libnosys", "close.c"), - filepath.Join(baseDir, "libgloss", "libnosys", "sbrk.c"), + filepath.Join(baseDir, "libgloss", "riscv", "esp", "esp_board.c"), + filepath.Join(baseDir, "libgloss", "riscv", "esp", "crt1-board.S"), } for _, sample := range sampleFiles1 { found := false @@ -386,17 +385,17 @@ func TestGetNewlibESP32ConfigRISCV(t *testing.T) { } } - // Group 2: libc + // Group 2: libgloss group2 := config.Groups[2] - expectedOutput2 := "libc-" + target + ".a" + expectedOutput2 := "libgloss-" + target + ".a" if group2.OutputFileName != expectedOutput2 { t.Errorf("Group2 OutputFileName expected '%s', got '%s'", expectedOutput2, group2.OutputFileName) } // Check sample files in group2 sampleFiles2 := []string{ - filepath.Join(libcDir, "string", "memcpy.c"), - filepath.Join(libcDir, "stdlib", "malloc.c"), + filepath.Join(baseDir, "libgloss", "libnosys", "close.c"), + filepath.Join(baseDir, "libgloss", "libnosys", "sbrk.c"), } for _, sample := range sampleFiles2 { found := false @@ -411,24 +410,49 @@ func TestGetNewlibESP32ConfigRISCV(t *testing.T) { } } - // Test CFlags for group2 - expectedCFlagsGroup2 := []string{ + // Group 3: libc + group3 := config.Groups[3] + expectedOutput3 := "libc-" + target + ".a" + if group3.OutputFileName != expectedOutput3 { + t.Errorf("Group3 OutputFileName expected '%s', got '%s'", expectedOutput3, group3.OutputFileName) + } + + // Check sample files in group3 + sampleFiles3 := []string{ + filepath.Join(libcDir, "string", "memcpy.c"), + filepath.Join(libcDir, "stdlib", "malloc.c"), + } + for _, sample := range sampleFiles3 { + found := false + for _, file := range group3.Files { + if file == sample { + found = true + break + } + } + if !found { + t.Errorf("Expected file '%s' not found in group3 files", sample) + } + } + + // Test CFlags for group3 (libc) + expectedCFlagsGroup3 := []string{ "-DHAVE_CONFIG_H", "-D_LIBC", "-DHAVE_NANOSLEEP", "-D__NO_SYSCALLS__", // ... (other expected flags) } - for _, expectedFlag := range expectedCFlagsGroup2 { + for _, expectedFlag := range expectedCFlagsGroup3 { found := false - for _, flag := range group2.CFlags { + for _, flag := range group3.CFlags { if flag == expectedFlag { found = true break } } if !found { - t.Errorf("Expected flag '%s' not found in group2 CFlags", expectedFlag) + t.Errorf("Expected flag '%s' not found in group3 CFlags", expectedFlag) } } @@ -564,8 +588,8 @@ func TestEdgeCases(t *testing.T) { t.Run("EmptyTarget_RISCV", func(t *testing.T) { config := getNewlibESP32ConfigRISCV("/test/base", "") - // Check output file name formatting - expectedOutput := "libcrt0-.a" + // Check output file name formatting (first group is libsemihost) + expectedOutput := "libsemihost-.a" if config.Groups[0].OutputFileName != expectedOutput { t.Errorf("Expected OutputFileName '%s', got '%s'", expectedOutput, config.Groups[0].OutputFileName) } @@ -599,8 +623,8 @@ func TestGroupConfiguration(t *testing.T) { t.Run("RISCV_GroupCount", func(t *testing.T) { config := getNewlibESP32ConfigRISCV(baseDir, target) - if len(config.Groups) != 3 { - t.Errorf("Expected 3 groups for RISCV, got %d", len(config.Groups)) + if len(config.Groups) != 4 { + t.Errorf("Expected 4 groups for RISCV, got %d", len(config.Groups)) } }) @@ -614,6 +638,7 @@ func TestGroupConfiguration(t *testing.T) { t.Run("RISCV_GroupNames", func(t *testing.T) { config := getNewlibESP32ConfigRISCV(baseDir, target) expectedNames := []string{ + "libsemihost-" + target + ".a", "libcrt0-" + target + ".a", "libgloss-" + target + ".a", "libc-" + target + ".a", @@ -650,7 +675,7 @@ func TestCompilerFlags(t *testing.T) { t.Run("RISCV_CFlags", func(t *testing.T) { config := getNewlibESP32ConfigRISCV(baseDir, target) - group := config.Groups[2] // libc group + group := config.Groups[3] // libc group (index 3: libsemihost=0, libcrt0=1, libgloss=2, libc=3) requiredFlags := []string{ "-DHAVE_CONFIG_H", diff --git a/internal/crosscompile/compile/libc/newlibesp.go b/internal/crosscompile/compile/libc/newlibesp.go index 924cddeb82..4b6cbafc6e 100644 --- a/internal/crosscompile/compile/libc/newlibesp.go +++ b/internal/crosscompile/compile/libc/newlibesp.go @@ -29,10 +29,10 @@ func withDefaultCCFlags(ccflags []string) []string { // GetNewlibESP32Config returns the configuration for downloading and building newlib for ESP32 func GetNewlibESP32Config() compile.LibConfig { return compile.LibConfig{ - Url: "https://github.com/goplus/newlib/archive/refs/tags/esp-4.3.0_20250211-patch4.tar.gz", + Url: "https://github.com/goplus/newlib/archive/refs/tags/esp-4.3.0_20250211-patch5.tar.gz", Name: "newlib-esp32", - Version: "esp-4.3.0_20250211-patch4", - ResourceSubDir: "newlib-esp-4.3.0_20250211-patch4", + Version: "esp-4.3.0_20250211-patch5", + ResourceSubDir: "newlib-esp-4.3.0_20250211-patch5", } } @@ -48,6 +48,20 @@ func getNewlibESP32ConfigRISCV(baseDir, target string) compile.CompileConfig { return compile.CompileConfig{ ExportCFlags: libcIncludeDir, Groups: []compile.CompileGroup{ + { + OutputFileName: fmt.Sprintf("libsemihost-%s.a", target), + Files: []string{ + filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_exit.c"), + }, + CFlags: []string{ + "-DHAVE_CONFIG_H", + "-isystem" + filepath.Join(libcDir, "include"), + "-I" + filepath.Join(baseDir, "libgloss"), + "-I" + filepath.Join(baseDir, "libgloss", "riscv"), + }, + LDFlags: _libcLDFlags, + CCFlags: _libcCCFlags, + }, { OutputFileName: fmt.Sprintf("libcrt0-%s.a", target), Files: []string{ @@ -93,7 +107,6 @@ func getNewlibESP32ConfigRISCV(baseDir, target string) compile.CompileConfig { filepath.Join(baseDir, "libgloss", "libnosys", "wait.c"), filepath.Join(baseDir, "libgloss", "libnosys", "write.c"), filepath.Join(baseDir, "libgloss", "libnosys", "getentropy.c"), - filepath.Join(baseDir, "libgloss", "libnosys", "_exit.c"), filepath.Join(baseDir, "libgloss", "libnosys", "getreent.c"), filepath.Join(baseDir, "libgloss", "libnosys", "time.c"), filepath.Join(baseDir, "libgloss", "libnosys", "fcntl.c"), @@ -233,7 +246,6 @@ func getNewlibESP32ConfigRISCV(baseDir, target string) compile.CompileConfig { filepath.Join(baseDir, "libgloss", "riscv", "sys_close.c"), filepath.Join(baseDir, "libgloss", "riscv", "sys_conv_stat.c"), filepath.Join(baseDir, "libgloss", "riscv", "sys_execve.c"), - filepath.Join(baseDir, "libgloss", "riscv", "sys_exit.c"), filepath.Join(baseDir, "libgloss", "riscv", "sys_faccessat.c"), filepath.Join(baseDir, "libgloss", "riscv", "sys_fork.c"), filepath.Join(baseDir, "libgloss", "riscv", "sys_fstat.c"), @@ -260,21 +272,6 @@ func getNewlibESP32ConfigRISCV(baseDir, target string) compile.CompileConfig { filepath.Join(baseDir, "libgloss", "riscv", "sys_wait.c"), filepath.Join(baseDir, "libgloss", "riscv", "sys_write.c"), filepath.Join(baseDir, "libgloss", "riscv", "nanosleep.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_close.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_exit.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_fdtable.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_fstat.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_ftime.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_isatty.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_link.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_lseek.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_open.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_read.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_sbrk.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_stat.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_stat_common.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_unlink.c"), - filepath.Join(baseDir, "libgloss", "riscv", "semihost-sys_write.c"), }, CFlags: []string{ "-DHAVE_CONFIG_H", @@ -1112,7 +1109,6 @@ func getNewlibESP32ConfigXtensa(baseDir, target string) compile.CompileConfig { filepath.Join(baseDir, "libgloss", "libnosys", "wait.c"), filepath.Join(baseDir, "libgloss", "libnosys", "write.c"), filepath.Join(baseDir, "libgloss", "libnosys", "getentropy.c"), - filepath.Join(baseDir, "libgloss", "libnosys", "_exit.c"), filepath.Join(baseDir, "libgloss", "libnosys", "getreent.c"), filepath.Join(baseDir, "libgloss", "libnosys", "time.c"), filepath.Join(baseDir, "libgloss", "libnosys", "fcntl.c"), diff --git a/targets/esp32c3-basic.json b/targets/esp32c3-basic.json index 8ff963b191..9c2e0cb089 100644 --- a/targets/esp32c3-basic.json +++ b/targets/esp32c3-basic.json @@ -24,5 +24,5 @@ "gdb": [ "riscv32-esp-elf-gdb" ], - "emulator": "qemu-system-riscv32 -machine esp32c3 -nographic -drive file={img},if=mtd,format=raw -serial mon:stdio" + "emulator": "qemu-system-riscv32 -semihosting -machine esp32c3 -nographic -drive file={img},if=mtd,format=raw -serial mon:stdio" }