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
35 changes: 35 additions & 0 deletions .github/actions/setup-embed-deps/action.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/install-esp-qemu.sh
Original file line number Diff line number Diff line change
@@ -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}"
3 changes: 2 additions & 1 deletion .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -209,7 +211,6 @@ jobs:
run: |
llgo test ./...


hello:
continue-on-error: true
timeout-minutes: 30
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/targets.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

name: Targets

on:
Expand Down
31 changes: 29 additions & 2 deletions _demo/embed/test_esp32c3_startup.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
Expand All @@ -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)..."
Expand Down Expand Up @@ -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
81 changes: 53 additions & 28 deletions internal/crosscompile/compile/libc/libc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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))
}
})

Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading