diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..c3f20ad8 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,72 @@ +FROM ghcr.io/framework-r-d/phlex-dev:latest + +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=1000 +ARG SPACK_GID=2000 + +# Validate Python site-packages symlink +RUN <<'VALIDATE_PYTHON_SYMLINK' +set -euo pipefail +shopt -s nullglob + +SPACK_VIEW_LIB=/opt/spack-environments/phlex-ci/.spack-env/view/lib + +# Find all versioned Python site-packages directories +python_dirs=("$SPACK_VIEW_LIB"/python3.*) +count=${#python_dirs[@]} + +# Ensure exactly one versioned Python site-packages directory exists +if [ $count -eq 0 ]; then + echo "ERROR: No versioned Python site-packages directory found in $SPACK_VIEW_LIB" >&2 + exit 1 +elif [ $count -gt 1 ]; then + echo "ERROR: Multiple versioned Python site-packages directories found in $SPACK_VIEW_LIB:" >&2 + printf ' %s\n' "${python_dirs[@]}" >&2 + exit 1 +fi + +# Get the single Python directory and its basename +python_dir="${python_dirs[0]}" +python_basename=$(basename "$python_dir") + +# Create the symlink if it doesn't exist or update it if it's incorrect +python_link="$SPACK_VIEW_LIB/python" +if [ -L "$python_link" ]; then + current_target=$(readlink "$python_link") + if [ "$current_target" != "$python_basename" ]; then + echo "Updating $python_link -> $python_basename" + ln -sfn "$python_basename" "$python_link" + fi +elif [ -e "$python_link" ]; then + echo "ERROR: $python_link exists but is not a symlink" >&2 + exit 1 +else + echo "Creating $python_link -> $python_basename" + ln -sn "$python_basename" "$python_link" +fi + +# Verify the symlink is correct +if [ ! -L "$python_link" ]; then + echo "ERROR: Failed to create symlink at $python_link" >&2 + exit 1 +fi + +# Verify the symlink target is a valid directory +if [ ! -d "$python_link" ]; then + echo "ERROR: Symlink $python_link does not point to a valid directory" >&2 + exit 1 +fi + +echo "Python symlink validated: $python_link -> $(readlink "$python_link")" +VALIDATE_PYTHON_SYMLINK + +# Create the user and add to spack group +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID --create-home $USERNAME \ + && usermod -aG $SPACK_GID $USERNAME \ + && echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +# Setup entrypoint usage in bashrc +RUN echo '. /entrypoint.sh' >> /home/$USERNAME/.bashrc diff --git a/.devcontainer/cmake_wrapper.sh b/.devcontainer/cmake_wrapper.sh new file mode 100755 index 00000000..05d60472 --- /dev/null +++ b/.devcontainer/cmake_wrapper.sh @@ -0,0 +1,3 @@ +#!/bin/bash +. /entrypoint.sh +exec cmake "$@" diff --git a/.devcontainer/ctest_wrapper.sh b/.devcontainer/ctest_wrapper.sh new file mode 100755 index 00000000..dab827d5 --- /dev/null +++ b/.devcontainer/ctest_wrapper.sh @@ -0,0 +1,3 @@ +#!/bin/bash +. /entrypoint.sh +exec ctest "$@" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..0dd904e7 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,94 @@ +{ + "name": "Phlex CI Dev Container", + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--format=docker" + ] + }, + "workspaceFolder": "/workspaces/phlex", + "remoteUser": "vscode", + "containerEnv": { + "GH_CONFIG_DIR": "/home/vscode/.config/gh" + }, + "mounts": [ + "source=${env:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind,readonly,required=false" + ], + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "/bin/bash", + "args": [ + "-i" + ], + "icon": "terminal-bash" + } + }, + "terminal.integrated.shellIntegration.suggestEnablement": false, + "python.defaultInterpreterPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", + "python.analysis.extraPaths": [ + "${workspaceFolder}/build", + "/opt/spack-environments/phlex-ci/.spack-env/view/lib/root", + "/opt/spack-environments/phlex-ci/.spack-env/view/lib/python/site-packages" + ], + "cmake.cmakePath": "${workspaceFolder}/.devcontainer/cmake_wrapper.sh", + "cmake.ctestPath": "${workspaceFolder}/.devcontainer/ctest_wrapper.sh", + "cmake.generator": "Ninja", + "C_Cpp.default.cStandard": "c17", + "C_Cpp.default.cppStandard": "c++20", + "C_Cpp.default.intelliSenseMode": "linux-gcc-x64", + "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", + "python.languageServer": "Pylance", + "python.analysis.typeCheckingMode": "basic", + "python.analysis.diagnosticMode": "workspace" + }, + "extensions": [ + "charliermarsh.ruff", + "cheshirekow.cmake-format", + "chrisjsewell.myst-tml-syntax", + + "davidanson.vscode-markdownlint", + "donjayamanne.githistory", + "dotjoshjohnson.xml", + "eamodio.gitlens", + "github.copilot", + "github.copilot-chat", + "github.vscode-github-actions", + "github.vscode-pull-request-github", + "jebbs.plantuml", + "lextudio.iis", + "lextudio.restructuredtext", + "lextudio.restructuredtext-pack", + "lfs.vscode-emacs-friendly", + "links-req-tracer.links-requirement-tracer", + + "ms-python.debugpy", + "ms-python.mypy-type-checker", + "ms-python.pylint", + + "ms-python.vscode-pylance", + "ms-python.vscode-python-envs", + "ms-vscode.cmake-tools", + "ms-vscode.cpptools", + "ms-vscode.cpptools-extension-pack", + "ms-vscode.cpptools-themes", + "ms-vscode.hexeditor", + "ms-vscode.live-server", + "ms-vscode.makefile-tools", + "ms-vscode.vscode-websearchforcopilot", + "redhat.vscode-yaml", + + "shd101wyy.markdown-preview-enhanced", + "swyddfa.esbonio", + "trond-snekvik.simple-rst", + "twxs.cmake", + "vadimcn.vscode-lldb", + "wequick.coverage-gutters", + "xaver.clang-format" + ] + } + } +} diff --git a/ci/Dockerfile b/ci/Dockerfile index a96bd826..ebaf833e 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -3,8 +3,9 @@ # Multi-target Dockerfile for building the Phlex CI and development images. # Podman instructions for building tagged images with this file: -# $ podman build --target ci --tag phlex-ci: . -# $ podman build --target dev --tag phlex-dev: . +# +# $ 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 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: @@ -12,9 +13,12 @@ # where is the date (e.g. "2025-08-12"). -FROM gcc:15.2.0 AS base ARG parallelism=18 +FROM gcc:15.2.0 AS base +ARG parallelism + +ENV DEBIAN_FRONTEND=noninteractive ENV LANG=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8 ENV SPACK_USER_CONFIG_PATH=/dev/null @@ -72,8 +76,6 @@ COPY entrypoint.sh /entrypoint.sh RUN <<'CONFIGURE_SPACK_DEFAULTS' set -euo pipefail -. /spack/share/spack/setup-env.sh - SPACK_REPO_ROOT=/opt/spack-repos rm -rf "$SPACK_REPO_ROOT/fnal_art" "$SPACK_REPO_ROOT/phlex-spack-recipes" mkdir -p "$SPACK_REPO_ROOT" @@ -83,12 +85,11 @@ chgrp -R spack "$SPACK_REPO_ROOT" chmod -R g+rwX "$SPACK_REPO_ROOT" find "$SPACK_REPO_ROOT" -type d -exec chmod g+s {} + -spack --timestamp repo remove phlex >/dev/null 2>&1 || true -spack --timestamp repo remove fnal_art >/dev/null 2>&1 || true -spack --timestamp repo add --scope site /opt/spack-repos/phlex-spack-recipes/spack_repo/phlex -spack --timestamp repo add --scope site /opt/spack-repos/fnal_art/spack_repo/fnal_art -spack --timestamp repo set --scope site --destination /spack/var/spack/repos/builtin builtin +. /spack/share/spack/setup-env.sh +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 compiler find @@ -154,7 +155,7 @@ set -euo pipefail spack --timestamp install --fail-fast -j $parallelism -p 1 \ --no-add --only-concrete --no-check-signature \ - cmake lcov ninja py-cmake-format py-gcovr py-pip + cmake lcov ninja py-gcovr py-pip spack clean -dfs INSTALL_TOOLING_CORE @@ -223,19 +224,6 @@ if [ -d "/build-cache" ]; then fi EXPORT_BUILDCACHE -######################################################################## -# Install ancillary Python tools (ruff) outside Spack - -RUN <<'INSTALL_PYTHON_TOOLS' -set -euo pipefail - -# Use pip to install tooling that is not packaged in Spack -. /entrypoint.sh - -PYTHONDONTWRITEBYTECODE=1 pip --isolated --no-input --disable-pip-version-check --no-cache-dir install --prefix /usr/local ruff -rm -rf ~/.spack -INSTALL_PYTHON_TOOLS - ######################################################################## # Remove transient files used during build @@ -257,12 +245,32 @@ USER root # Dev image additions (tooling and dev user configuration) FROM base AS dev -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=1000 -ENV HOME=/home/$USERNAME +ARG parallelism ENV SPACK_GROUP=spack +######################################################################## +# Install developer tooling (py-cmake-format, ruff) + +RUN <<'INSTALL_DEV_TOOLING' +set -euo pipefail + +# Install developer tooling not needed for CI +. /entrypoint.sh + +# Install py-cmake-format via Spack +spack --timestamp install --fail-fast -j $parallelism -p 1 \ + --no-add --only-concrete --no-check-signature \ + py-cmake-format + +spack clean -dfs + +# 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 +rm -rf ~/.spack +INSTALL_DEV_TOOLING + ######################################################################## # Install additional system packages needed for developer tooling @@ -283,16 +291,7 @@ INSTALL_DEV_PACKAGES ######################################################################## # Create developer user and grant access to the shared spack group -RUN <<'CREATE_DEV_USER' -set -euo pipefail - -# Create developer user and grant access to the shared spack group -groupadd --gid $USER_GID $USERNAME -useradd --uid $USER_UID --gid $USER_GID --create-home $USERNAME -echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME -chmod 0440 /etc/sudoers.d/$USERNAME -usermod -aG $SPACK_GROUP $USERNAME -CREATE_DEV_USER +# User creation moved to .devcontainer/Dockerfile to support flexible runtime environments ######################################################################## # Prepare workspace directory writable by the developer group @@ -301,8 +300,7 @@ RUN <<'PREP_DEV_WORKSPACE' set -euo pipefail # Prepare workspace directory writable by the developer group -mkdir -p /workspaces/phlex -chown $USERNAME:$USERNAME /workspaces +mkdir -p /workspaces chgrp $SPACK_GROUP /workspaces chmod g+rwX /workspaces PREP_DEV_WORKSPACE @@ -343,7 +341,5 @@ apt-get clean rm -rf /var/lib/apt/lists/* INSTALL_GH_CLI -USER $USERNAME -WORKDIR /workspaces/phlex SHELL ["/bin/bash", "-c"] HEALTHCHECK CMD curl -f https://github.com || exit 1 diff --git a/phlex.code-workspace b/phlex.code-workspace new file mode 100644 index 00000000..ee7b48a4 --- /dev/null +++ b/phlex.code-workspace @@ -0,0 +1,103 @@ +{ + "folders": [ + { + "name": "Phlex", + "path": "." + } + ], + "settings": { + "files.associations": { + "*.yml": "yaml", + "*.yaml": "yaml" + }, + "files.exclude": { + "**/local/.*/**": true, + "**/local/*/": true + }, + "search.exclude": { + "**/local/.*/**": true, + "**/local/*/": true, + "**/build/_deps/**": true, + "**/build/CMakeFiles/**": true + }, + "files.watcherExclude": { + "**/local/.*/**": true, + "**/local/*/": true, + "**/build/_deps/**": true, + "**/build/CMakeFiles/**": true + }, + "cmake.sourceDirectory": "${workspaceFolder}", + "cmake.buildDirectory": "${workspaceFolder}/build", + "cmake.useCMakePresets": "always", + "cmake.generator": "Ninja", + "C_Cpp.default.cStandard": "c17", + "C_Cpp.default.cppStandard": "c++20", + "C_Cpp.default.intelliSenseMode": "linux-gcc-x64", + "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", + "C_Cpp.exclusionPolicy": "checkFolders", + "C_Cpp.files.exclude": { + "**/local/.*/**": true, + "**/local/*/": true, + "**/build/_deps/**": true, + "**/build/CMakeFiles/**": true + }, + "python.languageServer": "Pylance", + "python.analysis.typeCheckingMode": "basic", + "python.analysis.diagnosticMode": "workspace", + "python.analysis.exclude": [ + "**/local/.*/**", + "**/local/*/", + "**/build/_deps/**", + "**/build/CMakeFiles/**" + ], + "python.analysis.ignore": [ + "**/local/.*/**", + "**/local/*/", + "**/build/_deps/**", + "**/build/CMakeFiles/**" + ], + "flake8.enabled": false, + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.organizeImports.ruff": "explicit", + "source.fixAll.ruff": "explicit" + } + }, + "yaml.schemas": { + "https://json.schemastore.org/github-workflow.json": "./.github/workflows/*" + } + }, + "extensions": { + "recommendations": [ + "ms-vscode.cmake-tools", + "ms-vscode.cpptools", + "ms-vscode.cpptools-extension-pack", + "twxs.cmake", + "ms-vscode.vscode-json", + "redhat.vscode-yaml", + "charliermarsh.ruff", + "github.vscode-actions", + "github.copilot", + "github.copilot-chat", + "github.vscode-pull-request-github", + "ms-vscode.hexeditor" + ], + "unwantedRecommendations": [ + "reditorsupport.r", + "ms-vscode.r", + "ikuyadeu.r", + "ms-vscode.r-debugger", + "vscjava.vscode-java-pack", + "redhat.java", + "vscjava.vscode-java-debug", + "vscjava.vscode-java-test", + "vscjava.vscode-maven", + "vscjava.vscode-gradle", + "ms-python.python", + "ms-toolsai.jupyter", + "ms-python.flake8" + ] + } +}