diff --git a/.github/workflows/05-Build_Apps.yml b/.github/workflows/05-Build_Apps.yml new file mode 100644 index 0000000..133eacf --- /dev/null +++ b/.github/workflows/05-Build_Apps.yml @@ -0,0 +1,117 @@ +name: 05-Build_Apps +run-name: ${{ github.actor }} started a workflow run + +on: + workflow_dispatch: + inputs: + sdk_version: + description: Selected SDK + required: true + type: choice + options: + - v2024.12.1 + - v2025.6.0 + - v2025.6.1 + - v2025.6.2 + - v2025.6.3 + - v2025.12.0 + - v2025.12.1 + - v2025.12.2 + - v2025.12.3 + - v2026.6.0 + selected_examples: + description: Selected examples, insert projects slcp file names comma-separated; empty selects all + type: string + selected_boards: + description: Selected boards, e.g brd4180a comma-separated; empty selects all + type: string + +jobs: + build_apps: + env: + APPS_OUTPUT_DIR: /tmp/output + name: Build applications + runs-on: ubuntu-24.04 + steps: + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: corretto + java-version: 21 + + - name: Setup SLT + uses: SiliconLabsSoftware/action-setup-slt@main + with: + install-conan: true + use-unstable: false + + - name: Install SDK and build tools + run: | + set -euo pipefail + + SDK_VERSION="${{ inputs.sdk_version }}" + SDK_VERSION="${SDK_VERSION#v}" + + slt install "simplicity-sdk/${SDK_VERSION}@silabs" + slt install slc-cli + slt install gcc-arm-none-eabi + slt install cmake + slt install ninja + slt install commander + + SDK_DIR="$(slt where "simplicity-sdk/${SDK_VERSION}")" + SLC_DIR="$(slt where slc-cli)" + ARM_GCC_DIR="$(slt where gcc-arm-none-eabi)" + CMAKE_DIR="$(slt where cmake)" + NINJA_DIR="$(slt where ninja)" + COMMANDER_DIR="$(slt where commander)" + + { + echo "SDK_DIR=$SDK_DIR" + echo "ARM_GCC_DIR=$ARM_GCC_DIR" + } >> "$GITHUB_ENV" + + printf '%s\n' \ + "$SLC_DIR" \ + "$ARM_GCC_DIR/bin" \ + "$CMAKE_DIR/bin" \ + "$NINJA_DIR" \ + "$COMMANDER_DIR" \ + "$COMMANDER_DIR/bin" >> "$GITHUB_PATH" + - name: Verify SLT installation + run: | + set -euo pipefail + slt --version + slc --version + arm-none-eabi-gcc --version + cmake --version + ninja --version + slt where commander + test -d "$SDK_DIR" + + - name: Configure SLC + run: | + set -euo pipefail + slc signature trust --sdk="$SDK_DIR" + slc configuration --sdk="$SDK_DIR" --gcc-toolchain="$ARM_GCC_DIR" + + - name: Check out repository + uses: actions/checkout@v6 + + - name: Build applications + run: | + set -euo pipefail + + mkdir -p "$APPS_OUTPUT_DIR" + + python3 build_sample_app.py \ + . \ + "${{ inputs.selected_examples }}" \ + "${{ inputs.selected_boards }}" \ + "$APPS_OUTPUT_DIR" + - name: Upload build artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: binaries & logs + path: ${{ env.APPS_OUTPUT_DIR }} diff --git a/build_sample_app.py b/build_sample_app.py new file mode 100644 index 0000000..898c8d3 --- /dev/null +++ b/build_sample_app.py @@ -0,0 +1,186 @@ +import subprocess +import sys +import xml.etree.ElementTree as ET +from pathlib import Path +from shutil import copytree + + +def parse_xml( + xml_file: Path, + selected_examples: list[str], + selected_boards: list[str], +) -> dict[Path, list[str]]: + """Return each selected sample application and its target boards.""" + sample_apps: dict[Path, list[str]] = {} + + for descriptor in ET.parse(xml_file).getroot().findall("descriptors"): + if selected_examples and descriptor.get("name") not in selected_examples: + continue + + properties = { + item.get("key"): item.get("value", "") + for item in descriptor.findall("properties") + } + project_path = properties.get("projectFilePaths") + if not project_path: + continue + + compatible_boards = [ + board + for board in properties.get("boardCompatibility", "").split() + if any(character.isdigit() for character in board) + ] + sample_apps[Path(project_path)] = selected_boards or compatible_boards + + return sample_apps + + +def generate_project(slcp_path: Path, project_path: Path, board: str) -> None: + """Generate one board-specific project with SLC.""" + print(f"Generating {project_path.name}...") + subprocess.run( + [ + "slc", + "generate", + "-p", + str(slcp_path), + "--new-project", + "-d", + str(project_path), + f"-name={project_path.name}", + "--output-type", + "cmake", + "--with", + board, + ], + capture_output=True, + text=True, + check=True, + ) + + +def compile_project(cmake_path: Path) -> None: + """Configure and build one generated project.""" + print(f"Building {cmake_path.parent.name}...") + subprocess.run( + ["cmake", "--workflow", "--preset", "project"], + cwd=cmake_path, + capture_output=True, + text=True, + check=True, + ) + + +def require_directory(path: Path, description: str) -> Path: + """Return an expected directory or raise a descriptive error.""" + if not path.is_dir(): + raise FileNotFoundError(f"{description} not found: {path}") + return path + + +def write_process_error( + error: subprocess.CalledProcessError, + log_path: Path, +) -> None: + """Write both captured process streams to a diagnostic log.""" + log_path.parent.mkdir(parents=True, exist_ok=True) + log_path.write_text( + f"STDOUT:\n{error.stdout or ''}\n\nSTDERR:\n{error.stderr or ''}", + encoding="utf-8", + ) + + +def write_error(error: OSError, log_path: Path) -> None: + """Write a non-process filesystem or layout error.""" + log_path.parent.mkdir(parents=True, exist_ok=True) + log_path.write_text(str(error), encoding="utf-8") + + +def build_apps( + sample_apps: dict[Path, list[str]], + sample_apps_root: Path, + output_dir: Path, +) -> None: + """Generate and build all selected projects, logging failures and continuing.""" + generated_root = Path("tmp") / "successful_generations" + + for app_path, boards in sample_apps.items(): + slcp_path = sample_apps_root / app_path + app_name = app_path.stem + + for board in boards: + project_name = f"{app_name}_{board}" + project_path = generated_root / app_name / project_name + + try: + generate_project(slcp_path, project_path, board) + except subprocess.CalledProcessError as error: + log_path = ( + output_dir + / "failed_generations" + / app_name + / project_name + / "generation_error.log" + ) + write_process_error(error, log_path) + print(f"Failed to generate {project_name}; details: {log_path}") + continue + + try: + cmake_path = require_directory( + project_path / "cmake_gcc", + "Generated CMake directory", + ) + compile_project(cmake_path) + build_output = require_directory( + cmake_path / "build" / "base", + "Build output directory", + ) + destination = output_dir / "successful_builds" / app_name / project_name + copytree(build_output, destination, dirs_exist_ok=True) + except subprocess.CalledProcessError as error: + log_path = ( + output_dir + / "failed_builds" + / app_name + / project_name + / "build_error.log" + ) + write_process_error(error, log_path) + print(f"Failed to build {project_name}; details: {log_path}") + except OSError as error: + log_path = ( + output_dir + / "failed_builds" + / app_name + / project_name + / "artifact_error.log" + ) + write_error(error, log_path) + print(f"Failed to collect artifacts for {project_name}; details: {log_path}") + + +def parse_selection(value: str) -> list[str]: + """Parse a comma-separated workflow input.""" + return [item.strip() for item in value.split(",") if item.strip()] + + +def main() -> None: + sample_apps_root = Path(sys.argv[1]) + selected_examples = parse_selection(sys.argv[2]) + selected_boards = parse_selection(sys.argv[3]) + output_dir = Path(sys.argv[4]) + + print("Parsing the template file...") + sample_apps = parse_xml( + sample_apps_root / "templates.xml", + selected_examples, + selected_boards, + ) + print(f"Selected {len(sample_apps)} sample application(s).") + print("Building apps...") + build_apps(sample_apps, sample_apps_root, output_dir) + + +if __name__ == "__main__": + main()