diff --git a/.actrc b/.actrc index 30c87d92..32abd23c 100644 --- a/.actrc +++ b/.actrc @@ -1,8 +1,19 @@ -# act configuration for local workflow testing on the host +# act configuration for local workflow testing # See: https://nektosact.com/usage/index.html # -# NOTE: act cannot run inside the devcontainer due to nested container limitations. -# Use this configuration when running act on the host machine. +# This configuration works both on the host and inside the phlex-dev +# devcontainer, provided the Podman socket is reachable. +# +# Host prerequisite (rootless Podman): +# Ensure the Podman API socket is active: +# $ systemctl --user enable --now podman.socket +# Then set DOCKER_HOST so act (and the Docker SDK it uses) can find it: +# $ export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/podman/podman.sock +# Add the export to your shell profile to make it permanent. +# +# Inside the devcontainer: +# DOCKER_HOST is set automatically via devcontainer.json; no extra +# steps are required. # Default image for jobs without explicit container # Medium = Ubuntu with common build tools diff --git a/.devcontainer/codespace.code-workspace b/.devcontainer/codespace.code-workspace index 087ca591..ad184f1d 100644 --- a/.devcontainer/codespace.code-workspace +++ b/.devcontainer/codespace.code-workspace @@ -58,6 +58,7 @@ "**/build/CMakeFiles/**": true }, "python.languageServer": "Pylance", + "python.defaultInterpreterPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", "python.analysis.typeCheckingMode": "basic", "python.analysis.diagnosticMode": "workspace", "python.analysis.exclude": [ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b59e1c3c..c7c51c2e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,12 +9,20 @@ "workspaceFolder": "/workspaces/phlex", "remoteUser": "vscode", "containerEnv": { - "GH_CONFIG_DIR": "/home/vscode/.config/gh" + "GH_CONFIG_DIR": "/home/vscode/.config/gh", + "DOCKER_HOST": "unix:///run/podman/podman.sock" }, "mounts": [ - "source=${env:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind,readonly" + "source=${localWorkspaceFolder}/../phlex-design,target=/workspaces/phlex-design,type=bind", + "source=${localWorkspaceFolder}/../phlex-examples,target=/workspaces/phlex-examples,type=bind", + "source=${localWorkspaceFolder}/../phlex-coding-guidelines,target=/workspaces/phlex-coding-guidelines,type=bind", + "source=${localWorkspaceFolder}/../phlex-spack-recipes,target=/workspaces/phlex-spack-recipes,type=bind", + "source=${localEnv:XDG_RUNTIME_DIR}/podman/podman.sock,target=/run/podman/podman.sock,type=bind", + "source=${localEnv:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind,readonly" ], + "initializeCommand": "bash .devcontainer/ensure-repos.sh", "onCreateCommand": "bash .devcontainer/setup-repos.sh /workspaces", + "postCreateCommand": "prek install", "customizations": { "vscode": { "settings": { @@ -59,7 +67,6 @@ "github.vscode-github-actions", "github.vscode-pull-request-github", "jebbs.plantuml", - "lextudio.iis", "lextudio.restructuredtext", "lextudio.restructuredtext-pack", "lfs.vscode-emacs-friendly", diff --git a/.devcontainer/ensure-repos.sh b/.devcontainer/ensure-repos.sh new file mode 100755 index 00000000..6f1d0846 --- /dev/null +++ b/.devcontainer/ensure-repos.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Ensure auxiliary repositories exist on the host as siblings of the phlex +# repository. Called by devcontainer.json initializeCommand before the +# container is created, so that bind mounts always have a valid source path. +# +# If a repository is already present its existing content is left untouched, +# preserving local branches and uncommitted changes. If a clone fails the +# directory is still created so the mount succeeds, and setup-repos.sh can +# retry the clone from inside the container. + +# Do not use set -e: clone failures are handled explicitly and must not abort +# the whole script, as that would prevent the container from starting. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PARENT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +clone_if_absent() { + local repo=$1 + local dest="${PARENT_DIR}/${repo}" + + if [ -e "${dest}/.git" ]; then + echo "Repository already present: ${dest}" + return 0 + fi + + if [ -d "${dest}" ] && [ -n "$(ls -A "${dest}" 2>/dev/null)" ]; then + echo "WARNING: ${dest} exists and is non-empty but not a git repository; skipping" >&2 + return 0 + fi + + # Create the directory so the bind mount has a valid source even if the + # clone fails. + mkdir -p "${dest}" + + echo "Cloning Framework-R-D/${repo} into ${dest} ..." + local max_tries=3 current_try=0 + while ! git clone --depth 1 "https://github.com/Framework-R-D/${repo}.git" "${dest}"; do + (( ++current_try )) + echo "Attempt ${current_try}/${max_tries} to clone ${repo} from GitHub FAILED" >&2 + if (( current_try >= max_tries )); then + echo "WARNING: unable to clone ${repo} to ${dest}; an empty directory was created" >&2 + return 0 + fi + sleep 5 + done +} + +clone_if_absent phlex-design +clone_if_absent phlex-examples +clone_if_absent phlex-coding-guidelines +clone_if_absent phlex-spack-recipes diff --git a/.github/actions/generate-build-matrix/generate_matrix.py b/.github/actions/generate-build-matrix/generate_matrix.py index eb44d4cc..4b8db62a 100644 --- a/.github/actions/generate-build-matrix/generate_matrix.py +++ b/.github/actions/generate-build-matrix/generate_matrix.py @@ -7,10 +7,16 @@ def get_default_combinations(event_name, all_combinations): """Gets the default build combinations based on the GitHub event type.""" - if event_name in ("push", "pull_request", "pull_request_target", "workflow_dispatch"): + if event_name in ( + "push", + "pull_request", + "pull_request_target", + "issue_comment", + "workflow_dispatch", + ): return ["gcc/none"] - elif event_name == "issue_comment": - return ["gcc/none", "clang/none"] + elif event_name == "schedule": + return ["gcc/perfetto"] else: # Default to a minimal safe configuration for unknown events return ["gcc/none"] @@ -23,10 +29,12 @@ def main(): "gcc/asan", "gcc/tsan", "gcc/valgrind", + "gcc/perfetto", "clang/none", "clang/asan", "clang/tsan", "clang/valgrind", + "clang/perfetto", ] user_input = os.getenv("USER_INPUT", "") comment_body = os.getenv("COMMENT_BODY", "") diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 52d70d43..3d6b4b86 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -25,8 +25,16 @@ automatically alongside the primary repository: - `/workspaces/phlex-coding-guidelines` — coding guidelines for contributors - `/workspaces/phlex-spack-recipes` — Spack recipes for Phlex and dependencies -Use the multi-root workspace file `.devcontainer/codespace.code-workspace` to -open all repositories in a single VS Code window. +Open `.devcontainer/codespace.code-workspace` to get a multi-root VS Code +window with all repositories visible. In VS Code: **File → Open Workspace from +File**, then select that file. From the terminal: + +```bash +code /workspaces/phlex/.devcontainer/codespace.code-workspace +``` + +Git hooks are installed automatically when the devcontainer is first created +(`postCreateCommand` runs `prek install`). No manual setup is required. ### Development Workflow @@ -317,16 +325,94 @@ All Markdown files must strictly follow these markdownlint rules: ### Local GitHub Actions Testing (`act`) - **Tool**: Use `act` to run GitHub Actions workflows locally. -- **Configuration**: Ensure `.actrc` exists in the workspace root with the following content to use a compatible runner image: - - ```text - -P ubuntu-latest=catthehacker/ubuntu:act-latest - ``` - +- **Configuration**: `.actrc` at the repository root contains the full `act` + configuration — do not overwrite or replace it. It sets the runner image, + container architecture, and artifact server path. +- **Daemon socket**: + - **Inside the devcontainer**: `DOCKER_HOST` is set automatically and the + Podman socket is mounted at `/run/podman/podman.sock`. No extra setup is + needed. + - **On the host**: The rootless Podman socket must be active and + `DOCKER_HOST` must point to it before invoking `act`: + + ```bash + systemctl --user enable --now podman.socket + export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/podman/podman.sock + ``` + + Add the `export` to your shell profile to make it permanent. - **Usage**: - List jobs: `act -l` - Run specific job: `act -j ` (e.g., `act -j python-check`) - Run specific event: `act pull_request` - **Troubleshooting**: - - **Docker Socket**: `act` requires access to the Docker socket. In dev containers, this may require specific mount configurations or permissions. - - **Artifacts**: `act` creates a `phlex-src` directory (or similar) for checkout. Ensure this is cleaned up or ignored by tools like `mypy`. + - **Artifacts**: `act` creates a `phlex-src` directory (or similar) for + checkout. Ensure this is cleaned up or ignored by tools like `mypy`. + +### Pre-commit Hooks (`prek` / `pre-commit`) + +The repository ships a `.pre-commit-config.yaml` that runs the full suite of +formatters and linters used by CI: trailing-whitespace/EOF fixers, ruff +(Python), clang-format (C++), gersemi (CMake), jsonnet-format/lint, prettier +(YAML), and markdownlint. `prek` — a Rust-based drop-in replacement for +`pre-commit` — is installed in `phlex-dev` and reads this config directly. +Individual developers on the host may have `pre-commit` instead; the two tools +are CLI-compatible for all commands used here. + +When invoking these tools, always detect which is available first: + +```bash +PREKCOMMAND=$(command -v prek || command -v pre-commit || echo "") +if [ -z "$PREKCOMMAND" ]; then + echo "Neither prek nor pre-commit found." >&2 +fi +``` + +Then substitute `$PREKCOMMAND` for the tool name in the commands below. + +- **Install git hooks** (run once per clone; done automatically in the + devcontainer via `postCreateCommand`): + + ```bash + $PREKCOMMAND install + ``` + + After this, hooks fire automatically on `git commit`. + +- **Run all hooks against all files** (recommended before opening a PR): + + ```bash + $PREKCOMMAND run --all-files + ``` + +- **Run hooks against only changed files** (equivalent to what fires on commit): + + ```bash + $PREKCOMMAND run + ``` + +- **Run hooks against a specific file or directory**: + + ```bash + $PREKCOMMAND run --files path/to/file + $PREKCOMMAND run --directory src/ + ``` + +- **Run hooks against the diff introduced by the last commit**: + + ```bash + $PREKCOMMAND run --last-commit + ``` + +- **Update hook revisions** to the latest upstream tags: + + ```bash + $PREKCOMMAND auto-update + ``` + +- **Availability**: `prek` is installed in `phlex-dev`. It is not installed in + `phlex-ci`. Host developers may have `pre-commit` instead; fall back to + invoking the individual formatters directly if neither is available. +- **Relationship to CI**: The hooks in `.pre-commit-config.yaml` mirror the + checks run by CI workflows. Running `$PREKCOMMAND run --all-files` before + pushing is the most reliable way to avoid CI formatting failures. diff --git a/.github/workflows/cmake-build.yaml b/.github/workflows/cmake-build.yaml index 5741ad3a..4acfeb73 100644 --- a/.github/workflows/cmake-build.yaml +++ b/.github/workflows/cmake-build.yaml @@ -7,6 +7,8 @@ run-name: "${{ github.actor }} building and testing ${{ github.repository }}" types: [created] push: branches: [main, develop] + schedule: + - cron: "7 18 * * 6" workflow_dispatch: inputs: ref: @@ -24,6 +26,16 @@ run-name: "${{ github.actor }} building and testing ${{ github.repository }}" Default (if empty): Run `gcc/none` required: false default: "" + perfetto-heap-profile: + description: "Enable heap profiling for Perfetto runs" + required: false + type: boolean + default: false + perfetto-cpu-profile: + description: "Enable CPU profiling for Perfetto runs" + required: false + type: boolean + default: true workflow_call: inputs: checkout-path: @@ -72,7 +84,7 @@ jobs: setup: if: > github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'push' || - github.event_name == 'workflow_call' || ( + github.event_name == 'schedule' || github.event_name == 'workflow_call' || ( github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(fromJSON('["OWNER", "COLLABORATOR", "MEMBER"]'), github.event.comment.author_association) && @@ -140,6 +152,7 @@ jobs: container: image: ghcr.io/framework-r-d/phlex-ci:latest + options: --cap-add=SYS_PTRACE steps: - name: Check out code @@ -175,6 +188,7 @@ jobs: extra-options: | ${{ matrix.sanitizer == 'asan' && format('-D{0}_ENABLE_ASAN=ON', steps.repo_name.outputs.name) || '' }} ${{ matrix.sanitizer == 'tsan' && format('-D{0}_ENABLE_TSAN=ON', steps.repo_name.outputs.name) || '' }} + ${{ matrix.sanitizer == 'perfetto' && format('-D{0}_ENABLE_PERFETTO=ON', steps.repo_name.outputs.name) || '' }} - name: Build id: build @@ -183,7 +197,7 @@ jobs: build-path: ${{ needs.setup.outputs.build_path }} - name: Run tests - if: matrix.sanitizer != 'valgrind' + if: matrix.sanitizer != 'valgrind' && matrix.sanitizer != 'perfetto' working-directory: ${{ needs.setup.outputs.build_path }} run: | . /entrypoint.sh @@ -215,6 +229,62 @@ jobs: echo "⚠️ Valgrind tests failed, but the workflow will continue." fi + - name: Run Perfetto profiling + if: matrix.sanitizer == 'perfetto' + working-directory: ${{ needs.setup.outputs.build_path }} + env: + PERFETTO_HEAP_PROFILE: ${{ github.event.inputs.perfetto-heap-profile || 'false' }} + PERFETTO_CPU_PROFILE: ${{ github.event.inputs.perfetto-cpu-profile || true }} + run: | + . /entrypoint.sh + + echo "➡️ Running tests with Perfetto profiling..." + + # Set perf_event_paranoid for CPU profiling + if [ "$PERFETTO_CPU_PROFILE" = "true" ]; then + echo "Configuring perf_event_paranoid for CPU profiling" + echo -1 | tee /proc/sys/kernel/perf_event_paranoid 2>/dev/null || echo "Warning: Could not set perf_event_paranoid" + fi + + # Configure profiling based on environment + TRACEBOX_ARGS="" + if [ "$PERFETTO_HEAP_PROFILE" = "true" ]; then + echo "Enabling heap profiling" + TRACEBOX_ARGS="$TRACEBOX_ARGS --app '*' --heapprofd" + fi + if [ "$PERFETTO_CPU_PROFILE" = "true" ]; then + echo "Enabling CPU profiling" + TRACEBOX_ARGS="$TRACEBOX_ARGS --cpu-freq --cpu-idle --cpu-sched" + fi + + # Run tests with or without tracebox wrapper + TEST_RESULT=0 + if [ -n "$TRACEBOX_ARGS" ]; then + echo "::group::Running ctest with tracebox" + tracebox $TRACEBOX_ARGS -o "perfetto-trace.pftrace" -- ctest --progress --output-on-failure -j "$(nproc)" || TEST_RESULT=$? + else + echo "::group::Running ctest with Perfetto SDK tracing" + ctest --progress --output-on-failure -j "$(nproc)" || TEST_RESULT=$? + fi + + echo "::endgroup::" + if [ "${TEST_RESULT:-0}" -eq 0 ]; then + echo "✅ Perfetto profiling completed." + else + echo "::error:: Perfetto profiling failed." + exit 1 + fi + + - name: Upload Perfetto traces + if: matrix.sanitizer == 'perfetto' + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: perfetto-traces-${{ matrix.compiler }} + path: | + ${{ needs.setup.outputs.build_path }}/**/*.pftrace + retention-days: 30 + if-no-files-found: warn + cmake-build-skipped: needs: [setup] if: > diff --git a/.github/workflows/cmake-format-check.yaml b/.github/workflows/cmake-format-check.yaml index fe6ae076..3564985b 100644 --- a/.github/workflows/cmake-format-check.yaml +++ b/.github/workflows/cmake-format-check.yaml @@ -82,13 +82,11 @@ jobs: repository: ${{ needs.setup.outputs.repo }} persist-credentials: false - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.x" + - name: Install uv + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - name: Install gersemi - run: pip install gersemi + run: uv tool install gersemi - name: Check CMake formatting id: lint diff --git a/.github/workflows/cmake-format-fix.yaml b/.github/workflows/cmake-format-fix.yaml index 2bafb972..6b0630eb 100644 --- a/.github/workflows/cmake-format-fix.yaml +++ b/.github/workflows/cmake-format-fix.yaml @@ -99,13 +99,11 @@ jobs: repository: ${{ needs.setup.outputs.repo }} token: ${{ secrets.WORKFLOW_PAT }} - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.x" + - name: Install uv + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - name: Install gersemi - run: pip install gersemi + run: uv tool install gersemi - name: Apply CMake formatting working-directory: ${{ needs.setup.outputs.checkout_path }} diff --git a/.github/workflows/python-check.yaml b/.github/workflows/python-check.yaml index 0cdd5b2f..ad24722d 100644 --- a/.github/workflows/python-check.yaml +++ b/.github/workflows/python-check.yaml @@ -85,14 +85,11 @@ jobs: repository: ${{ needs.setup.outputs.repo }} persist-credentials: false - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.x" + - name: Install uv + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - name: Install Python dependencies - run: | - pip install ruff mypy + run: uv tool install ruff && uv tool install mypy - name: Run ruff check id: ruff diff --git a/.github/workflows/python-fix.yaml b/.github/workflows/python-fix.yaml index d8928744..dbb93395 100644 --- a/.github/workflows/python-fix.yaml +++ b/.github/workflows/python-fix.yaml @@ -98,14 +98,11 @@ jobs: repository: ${{ needs.setup.outputs.repo }} token: ${{ secrets.WORKFLOW_PAT }} - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.x" + - name: Install uv + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - name: Install Python dependencies - run: | - pip install ruff + run: uv tool install ruff - name: Run ruff format and fix working-directory: ${{ needs.setup.outputs.checkout_path }} diff --git a/.gitignore b/.gitignore index 931fc492..23bce779 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /_deps/ /build-*/ /build/ +/out/ /phlex-build/ /phlex-src/ CMakeFiles/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f0a53b9..998d9a80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,14 @@ include(Modules/private/CreateCoverageTargets.cmake) option(ENABLE_TSAN "Enable Thread Sanitizer" OFF) option(ENABLE_ASAN "Enable Address Sanitizer" OFF) +include(CMakeDependentOption) +cmake_dependent_option( + ENABLE_PERFETTO + "Enable Perfetto profiling" + OFF + "CMAKE_SYSTEM_NAME STREQUAL Linux" + OFF +) option(PHLEX_USE_FORM "Enable experimental integration with FORM" OFF) option(ENABLE_COVERAGE "Enable code coverage instrumentation" OFF) option(ENABLE_BUILD_PROFILING "Enable monitoring of compile and link operations" OFF) @@ -196,6 +204,12 @@ if(ENABLE_ASAN) endif() endif() +# Configure Perfetto profiling if enabled +if(ENABLE_PERFETTO) + message(STATUS "Enabling Perfetto profiling") + find_package(Perfetto REQUIRED) +endif() + # Configure code coverage if enabled if(ENABLE_COVERAGE) # Check if the compiler supports code coverage diff --git a/Modules/FindPerfetto.cmake b/Modules/FindPerfetto.cmake new file mode 100644 index 00000000..0b2d4dae --- /dev/null +++ b/Modules/FindPerfetto.cmake @@ -0,0 +1,31 @@ +# FindPerfetto.cmake +# Finds the Perfetto SDK (single-header implementation) + +include(FindPackageHandleStandardArgs) + +find_path( + Perfetto_INCLUDE_DIR + NAMES perfetto.h + PATHS /opt/perfetto /usr/local/include /usr/include + DOC "Perfetto SDK header location" +) + +find_file( + Perfetto_SOURCE + NAMES perfetto.cc + PATHS /opt/perfetto /usr/local/include /usr/include + DOC "Perfetto SDK implementation file" +) + +find_package_handle_standard_args(Perfetto REQUIRED_VARS Perfetto_INCLUDE_DIR Perfetto_SOURCE) + +if(Perfetto_FOUND AND NOT TARGET Perfetto::Perfetto) + find_package(Threads REQUIRED) + add_library(Perfetto_impl STATIC "${Perfetto_SOURCE}") + target_include_directories(Perfetto_impl PUBLIC "${Perfetto_INCLUDE_DIR}") + target_compile_definitions(Perfetto_impl PUBLIC PERFETTO_ENABLE_TRACING=1) + target_link_libraries(Perfetto_impl PUBLIC Threads::Threads) + add_library(Perfetto::Perfetto ALIAS Perfetto_impl) +endif() + +mark_as_advanced(Perfetto_INCLUDE_DIR Perfetto_SOURCE) diff --git a/ci/Dockerfile b/ci/Dockerfile index 3b6087e7..fa5d4e46 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -4,14 +4,43 @@ # Podman instructions for building tagged images with this file: # -# $ podman build --format docker [-v :/build-cache] --target ci --tag phlex-ci: . -# $ podman build --format docker [-v :/build-cache] --target dev --tag phlex-dev: . +# $ podman build --format docker \ +# [-v :/build-cache] \ +# [-v :/gpg-signing] \ +# --target ci --tag phlex-ci: . +# $ podman build --format docker \ +# [-v :/build-cache] \ +# [-v :/gpg-signing] \ +# --target dev --tag phlex-dev: . # $ podman login ghcr.io --username -p # $ podman push phlex-ci: ghcr.io/framework-r-d/phlex-ci: # $ podman push phlex-dev: ghcr.io/framework-r-d/phlex-dev: # ... and optionally push either image as "latest". - +# # where is the date (e.g. "2025-08-12"). +# +# Binary package signing (optional): +# If -v :/gpg-signing is supplied together with +# -v :/build-cache, every .asc file found in +# is imported into Spack's keyring and the first +# available private key is used to sign the generated binary packages. +# To export a suitable armored key file from an existing keyring: +# +# $ gpg --armor --export-secret-keys > signing-key.asc +# +# Running act inside a phlex-dev container: +# act uses the Docker SDK to communicate with a container daemon; it +# does not require a Docker/Podman CLI inside the image. Mount the +# host's rootless Podman socket and advertise it via DOCKER_HOST: +# +# $ podman run --rm -it \ +# -v ${XDG_RUNTIME_DIR}/podman/podman.sock:/run/podman/podman.sock \ +# -e DOCKER_HOST=unix:///run/podman/podman.sock \ +# phlex-dev: +# +# The devcontainer.json already configures both the mount and the env +# var automatically when using VS Code / GitHub Codespaces. +# Host prerequisite: systemctl --user enable --now podman.socket ARG parallelism=18 @@ -105,10 +134,11 @@ RUN <<'CONFIGURE_SPACK_DEFAULTS' set -euo pipefail SPACK_REPO_ROOT=/opt/spack-repos -rm -rf "$SPACK_REPO_ROOT/fnal_art" "$SPACK_REPO_ROOT/phlex-spack-recipes" +rm -rf "$SPACK_REPO_ROOT/fnal_art" "$SPACK_REPO_ROOT/phlex-spack-recipes" "$SPACK_REPO_ROOT/spack-packages" mkdir -p "$SPACK_REPO_ROOT" git clone --depth=1 https://github.com/FNALssi/fnal_art.git "$SPACK_REPO_ROOT/fnal_art" git clone --depth=1 https://github.com/Framework-R-D/phlex-spack-recipes.git "$SPACK_REPO_ROOT/phlex-spack-recipes" +git clone --depth=1 https://github.com/spack/spack-packages.git "$SPACK_REPO_ROOT/spack-packages" chgrp -R spack "$SPACK_REPO_ROOT" chmod -R g+rwX "$SPACK_REPO_ROOT" find "$SPACK_REPO_ROOT" -type d -exec chmod g+s {} + @@ -117,7 +147,7 @@ find "$SPACK_REPO_ROOT" -type d -exec chmod g+s {} + spack --timestamp repo add --scope site $SPACK_REPO_ROOT/phlex-spack-recipes/spack_repo/phlex spack --timestamp repo add --scope site $SPACK_REPO_ROOT/fnal_art/spack_repo/fnal_art -spack --timestamp repo set --scope site --destination $SPACK_REPO_ROOT builtin +spack --timestamp repo add --scope site $SPACK_REPO_ROOT/spack-packages/repos/spack_repo/builtin spack --timestamp compiler find @@ -181,7 +211,6 @@ set -euo pipefail mkdir -p "$(dirname "$PHLEX_SPACK_ENV")" rm -rf "$PHLEX_SPACK_ENV" -spack versions -s llvm | grep -qEe '^[[:space:]]+21\.1\.4$' || spack checksum -ab llvm 21.1.4 spack --timestamp env create -d "$PHLEX_SPACK_ENV" /tmp/spack.yaml spack --timestamp env activate -d "$PHLEX_SPACK_ENV" spack --timestamp concretize @@ -201,7 +230,7 @@ set -euo pipefail spack --timestamp install --fail-fast -j $parallelism -p 1 \ --no-add --only-concrete --no-check-signature \ - cmake lcov ninja py-gcovr py-pip + cmake lcov ninja py-gcovr spack clean -dfs INSTALL_TOOLING_CORE @@ -253,19 +282,64 @@ spack --timestamp install --fail-fast -j $parallelism -p 1 \ spack clean -dfs INSTALL_PHLEX_DEPENDENCIES +######################################################################## +# Verify Python was built with SSL support + +RUN <<'VERIFY_PYTHON_SSL' +set -euo pipefail + +# Confirm that the Spack-built Python provides a working ssl module, +# which requires that Python was compiled against the expected SSL library. +. /entrypoint.sh + +python -c " +import ssl +ctx = ssl.create_default_context() +print('Python SSL OK:', ssl.OPENSSL_VERSION) +" || { + echo "ERROR: Spack Python does not provide a working ssl module." >&2 + echo "Check that Python was built with OpenSSL support (openssl~shared is a common culprit)." >&2 + exit 1 +} +VERIFY_PYTHON_SSL + ######################################################################## # Publish buildcache artifacts if a cache volume is mounted RUN <<'EXPORT_BUILDCACHE' set -euo pipefail -# Export buildcache entries when /build-cache is provided as a volume +# Export buildcache entries when /build-cache is provided as a volume. +# If /gpg-signing is also mounted, import any *.asc keys found there +# and use the first available private key to sign the packages. . /entrypoint.sh if [ -d "/build-cache" ]; then - spack --timestamp buildcache create -u --rebuild-index \ - /build-cache $(spack find --no-groups -LI | \ - sed -Ene 's&^\[\+\] +([^[:space:]]+).*$&/\1&p') + SPACK_SPECS=$(spack find --no-groups -LI | \ + sed -Ene 's&^\[\+\] +([^[:space:]]+).*$&/\1&p') + + SIGN_KEY= + if [ -d "/gpg-signing" ]; then + SPACK_GPG_HOME=$(spack location --spack-root)/opt/spack/gpg + mkdir -p "$SPACK_GPG_HOME" + chmod 700 "$SPACK_GPG_HOME" + for keyfile in /gpg-signing/*.asc; do + [ -f "$keyfile" ] || continue + gpg --homedir "$SPACK_GPG_HOME" --batch --import "$keyfile" + done + SIGN_KEY=$(gpg --homedir "$SPACK_GPG_HOME" \ + --list-secret-keys --with-colons 2>/dev/null \ + | awk -F: '/^fpr:/{print $10; exit}') + fi + + if [ -n "$SIGN_KEY" ]; then + spack --timestamp buildcache push --key "$SIGN_KEY" --rebuild-index \ + /build-cache $SPACK_SPECS + else + spack --timestamp buildcache push -u --rebuild-index \ + /build-cache $SPACK_SPECS + fi + spack mirror rm phlex-ci-local >/dev/null 2>&1 || true fi EXPORT_BUILDCACHE @@ -280,6 +354,36 @@ set -euo pipefail rm -f /tmp/spack.yaml CLEAN_TEMP_FILES +######################################################################## +# Install Perfetto SDK for profiling + +RUN <<'INSTALL_PERFETTO' +set -euo pipefail + +# Install Perfetto SDK and tools +apt-get update +apt-get install -y --no-install-recommends \ + wget +apt-get clean +rm -rf /var/lib/apt/lists/* + +mkdir -p /opt/perfetto +cd /opt/perfetto +PERFETTO_VERSION=v51.0 +wget -O perfetto.h https://raw.githubusercontent.com/google/perfetto/${PERFETTO_VERSION}/sdk/perfetto.h +wget -O perfetto.cc https://raw.githubusercontent.com/google/perfetto/${PERFETTO_VERSION}/sdk/perfetto.cc +chmod 644 perfetto.h perfetto.cc + +# Install tracebox for system-wide profiling. +# Note: get.perfetto.dev/tracebox serves the latest binary with no versioned +# or checksum-verifiable download currently offered by the Perfetto project. +# Revisit when the project provides versioned tracebox releases with integrity +# metadata. +wget -O tracebox https://get.perfetto.dev/tracebox +chmod +x tracebox +mv tracebox /usr/local/bin/ +INSTALL_PERFETTO + ######################################################################## # Finalize CI image stage @@ -295,7 +399,7 @@ ARG parallelism ENV SPACK_GROUP=spack ######################################################################## -# Install developer tooling (gersemi, ruff) +# Install developer tooling (gersemi, prek, ruff) RUN <<'INSTALL_DEV_TOOLING' set -euo pipefail @@ -303,10 +407,18 @@ set -euo pipefail # Install developer tooling not needed for CI . /entrypoint.sh -# Install ruff and gersemi via pip -PYTHONDONTWRITEBYTECODE=1 \ - pip --isolated --no-input --disable-pip-version-check --no-cache-dir install \ - --prefix /usr/local ruff gersemi +# Install uv for Python tooling management +curl -LsSf https://astral.sh/uv/install.sh | \ + env INSTALLER_NO_MODIFY_PATH=1 UV_INSTALL_DIR=/usr/local/bin sh + +# Install ruff, gersemi, and prek as isolated tools via uv +export UV_TOOL_BIN_DIR=/usr/local/bin +export UV_TOOL_DIR=/usr/local/share/uv/tools +uv tool install ruff +uv tool install gersemi +uv tool install prek + +uv cache clean rm -rf ~/.spack INSTALL_DEV_TOOLING diff --git a/ci/spack.yaml b/ci/spack.yaml index e14f367c..fa999a59 100644 --- a/ci/spack.yaml +++ b/ci/spack.yaml @@ -6,10 +6,9 @@ spack: - lcov - ninja - py-gcovr - - py-pip # Needed temporarily for ruff installation - py-pytest-cov # Needed for Python coverage reports - | - llvm@21.1.4: +zstd +llvm_dylib +link_llvm_dylib ~lldb targets=x86 + llvm@22.1.1 +zstd +llvm_dylib +link_llvm_dylib ~lldb targets=x86 view: true diff --git a/phlex/app/CMakeLists.txt b/phlex/app/CMakeLists.txt index 3c0a57af..3f1fd76c 100644 --- a/phlex/app/CMakeLists.txt +++ b/phlex/app/CMakeLists.txt @@ -33,4 +33,8 @@ cet_make_exec( jsonnet::lib ) +if(ENABLE_PERFETTO) + target_link_libraries(phlex PRIVATE Perfetto::Perfetto) +endif() + set_target_properties(phlex PROPERTIES INSTALL_RPATH "$ORIGIN/../lib")