diff --git a/.ci/build-wasm.sh b/.ci/build-wasm.sh new file mode 100755 index 0000000..c668dbc --- /dev/null +++ b/.ci/build-wasm.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Build WebAssembly version and prepare deployment artifacts +# Usage: .ci/build-wasm.sh [build|deploy-prep] + +set -euo pipefail + +source "$(dirname "$0")/common.sh" + +MODE="${1:-build}" + +case "$MODE" in +build) + # Bootstrap Kconfig tools first + make defconfig + + # Apply WASM configuration using proper Kconfig flow + python3 tools/kconfig/defconfig.py --kconfig configs/Kconfig configs/wasm_defconfig + python3 tools/kconfig/genconfig.py --header-path src/iui_config.h configs/Kconfig + + # Build with full Emscripten toolchain + CC=emcc AR=emar RANLIB=emranlib make $PARALLEL + print_success "WebAssembly build complete" + ;; +deploy-prep) + # Prepare deployment artifacts + mkdir -p deploy + cp assets/web/index.html deploy/ + cp assets/web/iui-wasm.js deploy/ + + # Copy generated files (may be in assets/web or root) + if [ -f assets/web/libiui_example.js ]; then + cp assets/web/libiui_example.js deploy/ + cp assets/web/libiui_example.wasm deploy/ + elif [ -f libiui_example.js ]; then + cp libiui_example.js deploy/ + cp libiui_example.wasm deploy/ + else + print_error "WASM artifacts not found - build may have failed" + exit 1 + fi + + ls -la deploy/ + print_success "Deployment artifacts prepared" + ;; +*) + print_error "Unknown mode: $MODE" + echo "Usage: $0 [build|deploy-prep]" + exit 1 + ;; +esac diff --git a/.ci/common.sh b/.ci/common.sh old mode 100755 new mode 100644 diff --git a/.ci/install-deps.sh b/.ci/install-deps.sh new file mode 100755 index 0000000..b2aba96 --- /dev/null +++ b/.ci/install-deps.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# Install build dependencies for CI +# Usage: .ci/install-deps.sh [sdl2|headless|format] + +set -euo pipefail + +source "$(dirname "$0")/common.sh" + +MODE="${1:-sdl2}" + +case "$MODE" in +sdl2) + if [ "$OS_TYPE" = "Linux" ]; then + sudo apt-get update -q=2 + sudo apt-get install -y -q=2 --no-install-recommends libsdl2-dev python3 + else + brew install sdl2 python3 + fi + ;; +headless) + if [ "$OS_TYPE" = "Linux" ]; then + sudo apt-get update -q=2 + sudo apt-get install -y -q=2 --no-install-recommends python3 + else + brew install python3 + fi + ;; +format) + if [ "$OS_TYPE" != "Linux" ]; then + print_error "Formatting tools only supported on Linux" + exit 1 + fi + sudo apt-get update -q=2 + sudo apt-get install -y -q=2 --no-install-recommends shfmt python3-pip gnupg ca-certificates lsb-release + + # Install clang-format-20 from LLVM repository + # LLVM signing key fingerprint: 6084F3CF814B57C1CF12EFD515CF4D18AF4F7421 + LLVM_KEYRING=/usr/share/keyrings/llvm-archive-keyring.gpg + LLVM_KEY_FP="6084F3CF814B57C1CF12EFD515CF4D18AF4F7421" + TMPKEY=$(mktemp) + download_to_file https://apt.llvm.org/llvm-snapshot.gpg.key "$TMPKEY" + ACTUAL_FP=$(gpg --with-fingerprint --with-colons "$TMPKEY" 2>/dev/null | grep fpr | head -1 | cut -d: -f10) + if [ "$ACTUAL_FP" != "$LLVM_KEY_FP" ]; then + print_error "LLVM key fingerprint mismatch!" + print_error "Expected: $LLVM_KEY_FP" + print_error "Got: $ACTUAL_FP" + rm -f "$TMPKEY" + exit 1 + fi + sudo gpg --dearmor -o "$LLVM_KEYRING" <"$TMPKEY" + rm -f "$TMPKEY" + + CODENAME=$(lsb_release -cs) + echo "deb [signed-by=${LLVM_KEYRING}] https://apt.llvm.org/${CODENAME}/ llvm-toolchain-${CODENAME}-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list + sudo apt-get update -q=2 + sudo apt-get install -y -q=2 --no-install-recommends clang-format-20 + + # Install Python formatter (version-pinned for reproducibility) + pip3 install --break-system-packages --only-binary=:all: black==25.1.0 + ;; +*) + print_error "Unknown mode: $MODE" + echo "Usage: $0 [sdl2|headless|format]" + exit 1 + ;; +esac + +print_success "Dependencies installed for mode: $MODE" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6b5708c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# Top-level EditorConfig file +root = true + +# Shell script settings - use tabs (shfmt default) +[*.sh] +indent_style = tab +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9343213..edffb68 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,23 +54,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Install formatting tools - run: | - source .ci/common.sh - - # Install base tools - sudo apt-get update -q=2 - sudo apt-get install -q=2 --no-install-recommends shfmt python3-pip gnupg ca-certificates - - # Install clang-format-20 from LLVM repository with proper keyring - LLVM_KEYRING=/usr/share/keyrings/llvm-archive-keyring.gpg - download_to_stdout https://apt.llvm.org/llvm-snapshot.gpg.key | sudo gpg --dearmor -o "$LLVM_KEYRING" - CODENAME=$(lsb_release -cs) - echo "deb [signed-by=${LLVM_KEYRING}] https://apt.llvm.org/${CODENAME}/ llvm-toolchain-${CODENAME}-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list - sudo apt-get update -q=2 - sudo apt-get install -q=2 --no-install-recommends clang-format-20 - - # Install Python formatter - pip3 install --break-system-packages black==25.1.0 + run: .ci/install-deps.sh format - name: Check newline at end of files run: .ci/check-newline.sh - name: Check code formatting @@ -85,67 +69,35 @@ jobs: fail-fast: false matrix: os: [ubuntu-24.04, macos-latest] - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Install dependencies (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get update -q=2 - sudo apt-get install -q=2 --no-install-recommends libsdl2-dev python3 - - - name: Install dependencies (macOS) - if: runner.os == 'macOS' - run: | - brew install sdl2 python3 - - - name: Set parallel jobs variable + - uses: actions/checkout@v6 + - name: Install dependencies + run: .ci/install-deps.sh sdl2 + - name: Build and test run: | source .ci/common.sh - echo "PARALLEL=$PARALLEL" >> "$GITHUB_ENV" - - - name: Configure and build - run: | make defconfig make $PARALLEL - - - name: Run unit tests - run: make check + make check headless-tests: needs: [detect-code-related-file-changes, unit-tests] if: needs.detect-code-related-file-changes.outputs.has_code_related_changes == 'true' timeout-minutes: 30 runs-on: ubuntu-24.04 - steps: - - name: Checkout repository - uses: actions/checkout@v6 - + - uses: actions/checkout@v6 - name: Install dependencies - run: | - sudo apt-get update -q=2 - sudo apt-get install -q=2 --no-install-recommends python3 - - - name: Set parallel jobs variable + run: .ci/install-deps.sh headless + - name: Run headless tests run: | source .ci/common.sh - echo "PARALLEL=$PARALLEL" >> "$GITHUB_ENV" - - - name: Configure and run headless tests - run: | - # Generate .config file (required by Makefile) make defconfig - # Uses test library built with -DIUI_MD3_RUNTIME_VALIDATION make check-headless $PARALLEL - - name: Save screenshots on failure if: failure() run: python3 scripts/headless-test.py --lib .build/test/libiui.a -s continue-on-error: true - - name: Upload test artifacts if: failure() uses: actions/upload-artifact@v6 @@ -159,25 +111,15 @@ jobs: if: needs.detect-code-related-file-changes.outputs.has_code_related_changes == 'true' timeout-minutes: 30 runs-on: ubuntu-24.04 - steps: - - name: Checkout repository - uses: actions/checkout@v6 - + - uses: actions/checkout@v6 - name: Install dependencies - run: | - sudo apt-get update -q=2 - sudo apt-get install -q=2 --no-install-recommends libsdl2-dev python3 - - - name: Set parallel jobs variable - run: | - source .ci/common.sh - echo "PARALLEL=$PARALLEL" >> "$GITHUB_ENV" - - - name: Configure and build with ASan + run: .ci/install-deps.sh sdl2 + - name: Build and test with ASan run: | make defconfig - make check SANITIZERS=1 + echo "CONFIG_SANITIZERS=y" >> .config + make check build-matrix: needs: [detect-code-related-file-changes] @@ -194,33 +136,68 @@ jobs: modules: "CONFIG_MODULE_BASIC=y" - config: "full" modules: "CONFIG_MODULE_BASIC=y CONFIG_MODULE_INPUT=y CONFIG_MODULE_CONTAINER=y" - steps: - - name: Checkout repository - uses: actions/checkout@v6 - + - uses: actions/checkout@v6 - name: Install dependencies - run: | - sudo apt-get update -q=2 - sudo apt-get install -q=2 --no-install-recommends libsdl2-dev python3 - - - name: Set parallel jobs variable + run: .ci/install-deps.sh sdl2 + - name: Build ${{ matrix.config }} run: | source .ci/common.sh - echo "PARALLEL=$PARALLEL" >> "$GITHUB_ENV" - - - name: Configure ${{ matrix.config }} build - run: | make defconfig - # Apply module configuration - for mod in ${{ matrix.modules }}; do - echo "$mod" >> .config - done - - - name: Build library - run: make libiui.a $PARALLEL - - - name: Check binary size - run: | + for mod in ${{ matrix.modules }}; do echo "$mod" >> .config; done + make libiui.a $PARALLEL size libiui.a || true ls -lh libiui.a + + wasm-build: + needs: [detect-code-related-file-changes] + if: needs.detect-code-related-file-changes.outputs.has_code_related_changes == 'true' + timeout-minutes: 30 + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - name: Cache Emscripten + uses: actions/cache@v5 + with: + path: emsdk-cache + key: emsdk-4.0.3-${{ runner.os }} + - name: Setup Emscripten + uses: mymindstorm/setup-emsdk@v14 + with: + version: 4.0.3 + actions-cache-folder: emsdk-cache + - name: Build WebAssembly + run: .ci/build-wasm.sh build + - name: Prepare deployment + run: .ci/build-wasm.sh deploy-prep + - name: Upload WASM artifacts + uses: actions/upload-artifact@v6 + with: + name: wasm-build + path: deploy/ + retention-days: 7 + + deploy-pages: + needs: [wasm-build] + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + timeout-minutes: 10 + runs-on: ubuntu-24.04 + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/download-artifact@v6 + with: + name: wasm-build + path: deploy/ + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: deploy/ + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/Makefile b/Makefile index 891dc43..1b3c9f6 100644 --- a/Makefile +++ b/Makefile @@ -272,9 +272,14 @@ distclean: clean wasm-install: @if [ "$(CC_IS_EMCC)" = "1" ]; then \ echo "Installing WebAssembly files to assets/web/..."; \ + if [ ! -f libiui_example ] || [ ! -f libiui_example.wasm ]; then \ + echo "Error: WASM build artifacts not found"; \ + echo "Expected: libiui_example and libiui_example.wasm"; \ + exit 1; \ + fi; \ mkdir -p assets/web; \ - cp -f libiui_example assets/web/libiui_example.js 2>/dev/null || true; \ - cp -f libiui_example.wasm assets/web/ 2>/dev/null || true; \ + cp -f libiui_example assets/web/libiui_example.js; \ + cp -f libiui_example.wasm assets/web/; \ echo ""; \ echo "WebAssembly build installed to assets/web/"; \ echo "To test, run:"; \ @@ -285,8 +290,10 @@ wasm-install: echo "Build with: make"; \ fi -# Override all target to add post-build hook for Emscripten +# Add wasm-install as post-build step for Emscripten +# wasm-install depends on the actual build targets to avoid race conditions ifeq ($(CC_IS_EMCC), 1) +wasm-install: $(target-y) all: wasm-install endif diff --git a/configs/wasm_defconfig b/configs/wasm_defconfig new file mode 100644 index 0000000..a15a8d7 --- /dev/null +++ b/configs/wasm_defconfig @@ -0,0 +1,5 @@ +# WebAssembly (Emscripten) build configuration +# Only WASM-specific overrides; other options inherited from Kconfig defaults + +CONFIG_PORT_WASM=y +CONFIG_OPTIMIZE_SIZE=y diff --git a/scripts/headless-test.py b/scripts/headless-test.py index 478ee28..c83b750 100755 --- a/scripts/headless-test.py +++ b/scripts/headless-test.py @@ -35,6 +35,23 @@ GOLDEN_DIR = PROJECT_ROOT / "tests" / "golden" LIB_PATH = PROJECT_ROOT / "libiui.a" # Default, can be overridden via --lib + +def get_sanitizer_flags(): + """Read .config and return sanitizer flags if enabled.""" + config_path = PROJECT_ROOT / ".config" + if not config_path.exists(): + return [] + try: + content = config_path.read_text() + if "CONFIG_SANITIZERS=y" in content: + return ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] + except (IOError, OSError): + pass + return [] + + +SANITIZER_FLAGS = get_sanitizer_flags() + # ============================================================================= # Shared Memory Constants (must match headless-shm.h) # ============================================================================= @@ -555,8 +572,10 @@ def run_test(name, screenshot=False, verbose=False): f"-I{PROJECT_ROOT}/include", f"-I{PROJECT_ROOT}/src", f"-I{PROJECT_ROOT}", + *SANITIZER_FLAGS, str(LIB_PATH), "-lm", + *SANITIZER_FLAGS, ], check=True, capture_output=True, @@ -718,8 +737,10 @@ def run_md3_tests(verbose=False): f"-I{PROJECT_ROOT}/include", f"-I{PROJECT_ROOT}/src", f"-I{PROJECT_ROOT}", + *SANITIZER_FLAGS, str(LIB_PATH), "-lm", + *SANITIZER_FLAGS, ], check=True, capture_output=True, @@ -904,8 +925,10 @@ def run_md3_runtime_tests(verbose=False): f"-I{test_build_dir}", # For iui_config.h from test build f"-I{PROJECT_ROOT}/src", f"-I{PROJECT_ROOT}", + *SANITIZER_FLAGS, str(LIB_PATH), "-lm", + *SANITIZER_FLAGS, ], check=True, capture_output=True, @@ -1266,8 +1289,10 @@ def run_shm_test(verbose: bool = False) -> tuple[bool, dict]: f"-I{PROJECT_ROOT}", f"-I{PROJECT_ROOT}/include", f"-I{PROJECT_ROOT}/src", + *SANITIZER_FLAGS, str(LIB_PATH), "-lm", + *SANITIZER_FLAGS, ], check=True, capture_output=True,