diff --git a/.github/instructions/docker-build.instructions.md b/.github/instructions/docker-build.instructions.md new file mode 100644 index 000000000..e10bf5857 --- /dev/null +++ b/.github/instructions/docker-build.instructions.md @@ -0,0 +1,184 @@ +--- +description: "Use when building, testing, or validating C++ changes. Covers how to compile the project inside the Docker build container using Makefile targets." +applyTo: "src/**/*.cpp,src/**/*.hpp,src/**/CMakeLists.txt,docker/**,Makefile" +--- + +# Building and Testing with the Docker Build Container + +All compilation and testing happens inside the Docker container. +Use `make` targets from the project root to build, test, and validate changes. The Makefile handles all Docker invocation. + +Real builds are still made on native host machines directly. The docker builds are used for testing and validation, not for production releases. The Docker image is a convenient, consistent environment for all developers to run builds and tests without needing to set up Qt, Conan, or the OpenStudio SDK on their local machines. + +--- + +## Prerequisites + +Docker must be running. On a fresh checkout, build the image once: + +```bash +make image +``` + +This is slow (~20 min) because it bakes Qt 6.11.0 and the OpenStudio SDK into +the image. Re-run only when `docker/Dockerfile` changes. + +--- + +## Typical workflow + +### First time after checkout (or after `make clean`) + +```bash +make configure # Conan install + cmake configure +make build # Compile +make test # Run all CTest tests +``` + +### Incremental build after editing source files + +```bash +make build # Recompiles only changed translation units (ccache accelerated) +make test # Re-runs tests +``` + +### After changing `conanfile.py` or `CMakeLists.txt` + +```bash +make configure # Re-run conan + cmake before building +make build +make test +``` + +--- + +## All available targets + +| Target | When to use | +|--------|-------------| +| `make image` | Rebuild the Docker image (Dockerfile changed) | +| `make configure` | After checkout, after `conanfile.py`/`CMakeLists.txt` changes, or after `make clean` | +| `make build` | After any source file edit | +| `make test` | Validate correctness; starts Xvfb at `:99`, runs `ctest -j4 --test-timeout 120 --exclude-regex GithubRelease` | +| `make cppcheck` | Static analysis; output written to `build/cppcheck-results.txt` | +| `make run-app` | Launch the compiled OpenStudioApp GUI (requires WSLg/Linux/macOS+XQuartz) | +| `make check-build` | Bash shell with all volumes mounted — inspect build artifacts, run commands against your actual source and build output | +| `make attach` | `/bin/sh` with **no volumes mounted** — inspect the image itself (e.g. verify Qt at `/opt/Qt`, check `/opt/openstudio-sdk`) | +| `make clean` | Wipe the build volume (keeps Conan + ccache volumes); alias for `build-clean` | +| `make build-clean` | Destroy and recreate the build volume (empty slate) | +| `make image-clean` | Remove the Docker image | +| `make volumes-clean` | Destroy all named volumes — build, Conan, and ccache (forces complete rebuild) | + +--- + +## Running make commands on Windows + +All `make` targets require a Linux-compatible shell because they invoke Docker +with bash process substitution and Unix-style paths. On Windows, always run +`make` commands inside **WSL** (Windows Subsystem for Linux), not in +PowerShell or Command Prompt. + +When the user is on Windows and asks you to run a `make` target, use a WSL +shell. For example: + +```powershell +wsl make configure +wsl make build +wsl make test +``` + +Or open a persistent WSL session and run the commands there. Never run `make` targets directly in a PowerShell or `cmd.exe` terminal on Windows. + +`make build` and `make test` can run for many minutes. Follow these rules to avoid getting stuck not knowing whether a command finished. + +**Always capture the exit code explicitly** — append `; echo "EXIT: $?"` so the final line of output is unambiguous even when stdout is truncated: + +```bash +wsl bash -c 'cd /mnt/c/repos/osapp3 && make build; echo "EXIT: $?"' +``` + +**Never pipe Docker commands** — piping (e.g. `| tail -50`, `| grep`) causes SIGPIPE (exit 141) when the consumer exits before Docker finishes. Redirect stderr into stdout with `2>&1` instead: + +```bash +wsl bash -c 'cd /mnt/c/repos/osapp3 && make build 2>&1; echo "EXIT: $?"' +``` + +**Completion markers to look for in output:** + +| Command | Success indicator | +|---------|-------------------| +| `make configure` | `Install finished successfully` + `conan install exit code: 0` | +| `make build` | `cmake --build` exits and `EXIT: 0` is printed | +| `make test` | `100% tests passed` or `EXIT: 0` printed after ctest | + +**If output is truncated** and the completion marker is not visible, call `get_terminal_output` again — do not assume the command is still running until at least 5 minutes have elapsed without new output. + +--- + +## Validating a refactoring step + +After every logical change (e.g. one item from `developer/doc/refactoring-ideas.md`): + +1. **Determine which targets to run** based on what changed: + - Source files only (`.cpp`/`.hpp`) → `make build && make test` + - Added or removed source files from a `CMakeLists.txt` → `make configure && make build && make test` + - Changed `conanfile.py`, `CMakeLists.txt` contents, or any `*.cmake` file → `make configure && make build && make test` + +2. **`make build` must exit 0 with zero warnings.** The project builds with `-Werror`; any compiler warning is a build failure. Fix all warnings before proceeding. + +3. **`make test` must exit 0 with no regressions.** All tests that passed before the change must still pass. The following tests are known baseline failures in the Docker environment and are **not** regressions: + + | Test | Reason | + |------|--------| + | `ModelEditorFixture.MorePath_Conversions` | Tests Windows-style backslash path conversion; always fails on Linux | + | `OpenStudioLibFixture.AnalyticsHelperSecrets` | Requires analytics API secrets injected by CI; always empty in local builds | + + `GithubRelease*` tests are excluded entirely by the Makefile (`--exclude-regex GithubRelease`) and are not part of the baseline. If `make test` fails, read `build/Testing/Temporary/LastTest.log` to identify which test failed and why before attempting a fix. + +4. **Optionally run `make cppcheck`** after any non-trivial structural change (new class, moved logic, changed ownership patterns). Review `build/cppcheck-results.txt` for new issues introduced by the change. + +Do not proceed to the next refactoring step until steps 2 and 3 succeed. + +--- + +## Named Docker volumes + +Three named volumes persist data between runs: + +| Volume | Mounted at | Contents | +|--------|-----------|----------| +| `osapp-build` | `/workspace/build` | CMake build tree, compiled objects, Ninja database | +| `osapp-conan-cache` | `/conan-cache` | Downloaded Conan packages (`CONAN_HOME`) | +| `osapp-ccache` | `/ccache` | ccache object cache (`CCACHE_DIR`) | + +The build volume shadows the host `build/` directory — build output never lands on the host filesystem directly. Use `make check-build` to inspect volume contents interactively. `make clean` / `make build-clean` wipes only `osapp-build`; Conan and ccache volumes are preserved. `make volumes-clean` destroys all three. + +--- + +## Reading build output + +Build artifacts and logs land in `build/` (git-ignored). Key paths inside the container (mapped to the same path on the host): + +| Path | Contents | +|------|----------| +| `build/compile_commands.json` | Compilation database (used by cppcheck and clangd) | +| `build/Testing/Temporary/LastTest.log` | CTest output from the most recent run | +| `build/cppcheck-results.txt` | Static analysis output from `make cppcheck` | + +--- + +## Troubleshooting + +**`make configure` fails with a Conan error about a missing package** +Run `make volumes-clean` then `make configure` again to rebuild the Conan cache from scratch. + +**`make image` fails to pull `ubuntu:22.04`** +Docker Desktop cannot reach `docker.io`. Restart Docker Desktop or check proxy/VPN settings. Run `docker pull ubuntu:22.04` to confirm connectivity before retrying. + +**Tests fail due to a missing display** +The container includes `xvfb`; `make test` starts a dedicated Xvfb process at `DISPLAY=:99` before invoking ctest, then kills it afterward. If running ctest manually inside `make check-build`, start Xvfb first: +```bash +Xvfb :99 -screen 0 1280x1024x24 -ac & +export DISPLAY=:99 +cd build && ctest -j4 --output-on-failure --test-timeout 120 --exclude-regex GithubRelease +``` diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml new file mode 100644 index 000000000..cfecbc573 --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -0,0 +1,96 @@ +# Docker-based CI for pull requests. +# +# Builds the project inside the osapp-build Docker image (Ubuntu 22.04 with +# Qt 6.11.0, OpenStudio SDK 3.11.0, and Conan 2 pre-installed) and runs +# CTest. The Docker image layers are cached via the GitHub Actions cache +# backend so the slow Qt/SDK bake step is only repeated when the Dockerfile +# changes. +# +# Runs on: pull_request to master or develop (non-draft only) + +name: Docker CI + +on: + pull_request: + branches: [ master, develop ] + types: [ opened, reopened, synchronize, ready_for_review ] + +jobs: + docker-build-test: + name: Build & Test (Docker / Ubuntu 22.04) + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + # BuildKit is required for the cache-from/cache-to directives below. + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Build the image and push layers into the GitHub Actions cache. + # The cache key is tied to the Dockerfile so the slow layers (Qt, SDK) + # are only rebuilt when the Dockerfile itself changes. + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: docker/ + load: true + tags: osapp-build:latest + cache-from: type=gha,scope=osapp-build-${{ hashFiles('docker/Dockerfile') }} + cache-to: type=gha,scope=osapp-build-${{ hashFiles('docker/Dockerfile') }},mode=max + + # Optional: cache Conan packages between runs to avoid re-building + # dependencies from source on every PR push. + - name: Cache Conan packages + uses: actions/cache@v4 + with: + path: ~/.conan2-ci-cache + key: conan-docker-${{ hashFiles('conanfile.py') }} + restore-keys: | + conan-docker- + + # Create named Docker volumes (idempotent; volumes persist for this job). + - name: Create cache volumes + run: make volumes + + # Inject the host-side Conan package cache into the named volume so that + # previously downloaded packages don't need to be rebuilt. + - name: Populate Conan volume from cache + run: | + if [ -d "$HOME/.conan2-ci-cache" ] && [ "$(ls -A $HOME/.conan2-ci-cache)" ]; then + docker run --rm \ + -v osapp-conan-cache:/conan-cache \ + -v "$HOME/.conan2-ci-cache:/host-cache:ro" \ + ubuntu:22.04 \ + bash -c "cp -a /host-cache/. /conan-cache/ 2>/dev/null || true" + fi + + - name: Configure (Conan install + CMake configure) + run: make configure + + - name: Build + run: make build + + - name: Test + run: make test + + # Persist Conan packages back to the host cache directory for the next run. + - name: Save Conan packages to host cache + if: always() + run: | + mkdir -p "$HOME/.conan2-ci-cache" + docker run --rm \ + -v osapp-conan-cache:/conan-cache:ro \ + -v "$HOME/.conan2-ci-cache:/host-cache" \ + ubuntu:22.04 \ + bash -c "cp -a /conan-cache/. /host-cache/ 2>/dev/null || true" + + - name: ccache stats + if: always() + run: | + docker run --rm \ + -v osapp-ccache:/ccache \ + -e CCACHE_DIR=/ccache \ + osapp-build:latest \ + ccache --show-stats || true diff --git a/CMakeLists.txt b/CMakeLists.txt index d34f5b643..d536578e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -384,7 +384,7 @@ if(UNIX) #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fPIC -fno-strict-aliasing -Winvalid-pch -Wnon-virtual-dtor") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -fno-strict-aliasing -Winvalid-pch") # Treat all warnings as errors - #set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror") if(APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-overloaded-virtual -ftemplate-depth=1024") diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..feb0da1ca --- /dev/null +++ b/Makefile @@ -0,0 +1,295 @@ +# ============================================================================= +# OpenStudio Application — Docker-based build system +# +# Prerequisites: Docker (with BuildKit enabled) + GNU make (or WSL/Git Bash +# on Windows). +# +# Quick start: +# make image # Build the Docker image (once, ~20 min) +# make configure # Install Conan deps + CMake configure +# make build # Compile +# make test # Run CTest +# make check-build # Drop into an interactive container shell +# ============================================================================= + +# --------------------------------------------------------------------------- +# Git Bash/MSYS2: Prevent unwanted path conversion for Docker +# If running in Git Bash/MSYS2, export MSYS_NO_PATHCONV=1 to avoid issues with +# Docker volume and working directory paths. This is a no-op in other shells. +ifeq ($(shell uname -s | grep -iE 'mingw|msys|cygwin'),) +else +export MSYS_NO_PATHCONV=1 +endif + +IMAGE := osapp-build +TAG := latest +BUILD_DIR := build + +# Named volumes – persist Conan packages, ccache, and build artifacts between runs. +# The build volume is mounted at /workspace/build inside the container, shadowing +# any host build/ directory. This gives Linux-native filesystem performance for +# incremental Ninja builds and avoids file-ownership noise on Windows hosts. +CONAN_VOL := osapp-conan-cache +CCACHE_VOL := osapp-ccache +BUILD_VOL := osapp-build + +# Qt install dir inside the image (matches Dockerfile ENV). +QT_INSTALL_DIR := /opt/Qt/6.11.0/gcc_64 + +# --------------------------------------------------------------------------- +# Base docker run command (non-interactive, workspace mounted as /workspace). +# Runs as root so build artifacts have consistent ownership. +# The build volume is mounted over /workspace/build so the host never sees +# raw build output; use 'make check-build' to inspect artifacts interactively. +# --------------------------------------------------------------------------- +DOCKER_RUN := docker run --rm \ + -v "$(CURDIR):/workspace" \ + -v "$(BUILD_VOL):/workspace/build" \ + -v "$(CONAN_VOL):/conan-cache" \ + -v "$(CCACHE_VOL):/ccache" \ + -e CONAN_HOME=/conan-cache \ + -e CCACHE_DIR=/ccache \ + -e QT_INSTALL_DIR=$(QT_INSTALL_DIR) \ + -w /workspace \ + $(IMAGE):$(TAG) + +.PHONY: all image volumes configure build test cppcheck run-app check-build attach \ + clean image-clean volumes-clean build-clean help + +all: help + +# --------------------------------------------------------------------------- +# image — Build the Docker image (slow; only needed when Dockerfile changes). +# --------------------------------------------------------------------------- +image: + docker build -t $(IMAGE):$(TAG) docker/ + +# --------------------------------------------------------------------------- +# volumes — Ensure all named volumes exist. +# --------------------------------------------------------------------------- +volumes: + docker volume inspect $(CONAN_VOL) > /dev/null 2>&1 || docker volume create $(CONAN_VOL) + docker volume inspect $(CCACHE_VOL) > /dev/null 2>&1 || docker volume create $(CCACHE_VOL) + docker volume inspect $(BUILD_VOL) > /dev/null 2>&1 || docker volume create $(BUILD_VOL) + +# --------------------------------------------------------------------------- +# configure — Bootstrap Conan, symlink SDK, run conan install + cmake. +# Re-run whenever conanfile.py or CMakeLists.txt changes. +# --------------------------------------------------------------------------- +configure: volumes + $(DOCKER_RUN) bash /workspace/docker/configure.sh + +# --------------------------------------------------------------------------- +# build — Compile (uses Ninja + ccache; incremental). +# --------------------------------------------------------------------------- +build: volumes + $(DOCKER_RUN) cmake --build --preset conan-release -j + +# --------------------------------------------------------------------------- +# test — Run CTest inside the build directory. +# Xvfb is started manually so all parallel test processes share the +# same display (xvfb-run only wraps a single process, which is +# insufficient when ctest spawns many children in parallel). +# +# Per-test timeout notes: +# All tests have TIMEOUT 660 set in CTestTestfile.cmake. +# ctest --timeout only sets the *default* for tests that have no +# timeout property, so it does not override 660. +# cmake 3.27+ ctest --test-timeout DOES override per-test timeouts. +# We use --test-timeout 120 so no single test can block the suite. +# +# Excluded tests: +# GithubRelease* — make live HTTP calls to the GitHub releases API +# which hang or fail in a network-sandboxed container. +# --------------------------------------------------------------------------- +test: build + $(DOCKER_RUN) bash -c "\ + Xvfb :99 -screen 0 1280x1024x24 -ac &\ + XVFB_PID=$$! ;\ + export DISPLAY=:99 ;\ + export QT_QPA_PLATFORM=xcb ;\ + sleep 1 ;\ + cd build && ctest -j4 \ + --output-on-failure \ + --test-timeout 120 \ + --exclude-regex 'GithubRelease' \ + ; CTEST_EXIT=$$? ;\ + kill $$XVFB_PID 2>/dev/null || true ;\ + exit $$CTEST_EXIT" + +# --------------------------------------------------------------------------- +# cppcheck — Static analysis (matches CI cppcheck.yml flags). +# Requires build/ to exist for compile_commands.json. +# Output written to build/cppcheck-results.txt. +# --------------------------------------------------------------------------- +cppcheck: build + $(DOCKER_RUN) bash -c \ + "cppcheck \ + --std=c++20 \ + --suppress=useStlAlgorithm \ + --inline-suppr \ + --inconclusive \ + --enable=all \ + --library=qt \ + --project=build/compile_commands.json \ + 2>&1 | tee build/cppcheck-results.txt" + +# --------------------------------------------------------------------------- +# run-app — Launch the compiled OpenStudioApp with GUI forwarded to the host. +# +# Automatically detects the host platform: +# +# WSL2/WSLg (Windows 11): +# Uses WSLg's X11 socket (/tmp/.X11-unix) and Wayland runtime +# (/mnt/wslg). No extra software needed. +# Override display: make run-app DISPLAY=:1 +# +# Linux (native): +# Mounts /tmp/.X11-unix and uses the host DISPLAY. Runs xhost +local:docker +# first to grant the container access. +# Override display: make run-app DISPLAY=:1 +# +# macOS: +# Requires XQuartz (https://xquartz.org). Start XQuartz and run: +# xhost + 127.0.0.1 +# Then: make run-app +# DISPLAY is set to host.docker.internal:0 automatically. +# +# If on native Linux and GPU passthrough is unavailable (--device /dev/dri +# fails), add: make run-app LIBGL_ALWAYS_SOFTWARE=1 +# --------------------------------------------------------------------------- +APP_BIN := /workspace/build/Products/OpenStudioApp + +# Detect host OS. 'uname -s' is available on Linux, macOS, and WSL. +# On native Windows (no WSL) this will be empty — unsupported directly. +_UNAME := $(shell uname -s 2>/dev/null) + +# Detect WSL2: /mnt/wslg exists only inside the WSL2 VM. +_IS_WSL := $(shell test -d /mnt/wslg && echo 1 || echo 0) + +ifeq ($(_UNAME),Darwin) + # macOS: XQuartz listens on host.docker.internal:0 + DISPLAY ?= host.docker.internal:0 + _X11_MOUNTS := + _WSLG_MOUNTS := + _DRI_DEVICE := + _DISPLAY_ENV := -e DISPLAY=$(DISPLAY) +else ifeq ($(_IS_WSL),1) + # WSL2 + WSLg: GPU is handled by the WSLg Wayland compositor via /dev/dxg; + # no --device /dev/dri needed (that device doesn't exist in the WSL2 VM). + DISPLAY ?= :0 + _X11_MOUNTS := -v /tmp/.X11-unix:/tmp/.X11-unix + _WSLG_MOUNTS := -v /mnt/wslg:/mnt/wslg \ + -e WAYLAND_DISPLAY=wayland-0 \ + -e XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir \ + -e PULSE_SERVER=/mnt/wslg/PulseServer + _DRI_DEVICE := + _DISPLAY_ENV := -e DISPLAY=$(DISPLAY) +else + # Native Linux + DISPLAY ?= $(shell echo $$DISPLAY) + _X11_MOUNTS := -v /tmp/.X11-unix:/tmp/.X11-unix + _WSLG_MOUNTS := + _DRI_DEVICE := --device /dev/dri + _DISPLAY_ENV := -e DISPLAY=$(DISPLAY) +endif + +run-app: volumes +ifeq ($(_UNAME),Linux) +ifneq ($(_IS_WSL),1) + xhost +local:docker 2>/dev/null || true +endif +endif + docker run --rm -it \ + -v "$(CURDIR):/workspace" \ + -v "$(BUILD_VOL):/workspace/build" \ + -v "$(CONAN_VOL):/conan-cache" \ + -v "$(CCACHE_VOL):/ccache" \ + $(_X11_MOUNTS) \ + $(_WSLG_MOUNTS) \ + $(_DISPLAY_ENV) \ + $(_DRI_DEVICE) \ + -e CONAN_HOME=/conan-cache \ + -e CCACHE_DIR=/ccache \ + -e QT_INSTALL_DIR=$(QT_INSTALL_DIR) \ + -e QT_QPA_PLATFORM=xcb \ + -w /workspace \ + $(IMAGE):$(TAG) \ + $(APP_BIN) + +# --------------------------------------------------------------------------- +# check-build — Interactive bash shell inside the container (all volumes mounted). +# --------------------------------------------------------------------------- +check-build: volumes + docker run --rm -it \ + -v "$(CURDIR):/workspace" \ + -v "$(BUILD_VOL):/workspace/build" \ + -v "$(CONAN_VOL):/conan-cache" \ + -v "$(CCACHE_VOL):/ccache" \ + -e CONAN_HOME=/conan-cache \ + -e CCACHE_DIR=/ccache \ + -e QT_INSTALL_DIR=$(QT_INSTALL_DIR) \ + -w /workspace \ + $(IMAGE):$(TAG) bash + +# --------------------------------------------------------------------------- +# attach — /bin/sh inside the image with NO volume mounts. +# Use this to debug the image itself (inspect /opt/Qt, /opt/openstudio-sdk, etc.) +# --------------------------------------------------------------------------- +attach: + docker run --rm -it $(IMAGE):$(TAG) /bin/sh + +# --------------------------------------------------------------------------- +# clean — Wipe the build volume (keeps Conan, ccache, and source). +# Equivalent to the old 'rm -rf build/'. +# Also removes any stale host-side build/ directory if present. +# --------------------------------------------------------------------------- +clean: build-clean + +# --------------------------------------------------------------------------- +# build-clean — Destroy and recreate the build volume (empty slate). +# --------------------------------------------------------------------------- +build-clean: + docker volume rm $(BUILD_VOL) || true + docker volume create $(BUILD_VOL) + rm -rf $(BUILD_DIR) + +# --------------------------------------------------------------------------- +# image-clean — Remove the Docker image. +# --------------------------------------------------------------------------- +image-clean: + docker rmi $(IMAGE):$(TAG) || true + +# --------------------------------------------------------------------------- +# volumes-clean — Destroy all named volumes (forces complete rebuild). +# --------------------------------------------------------------------------- +volumes-clean: + docker volume rm $(BUILD_VOL) $(CONAN_VOL) $(CCACHE_VOL) || true + rm -rf $(BUILD_DIR) + +# --------------------------------------------------------------------------- +# help — List all targets. +# --------------------------------------------------------------------------- +help: + @echo "" + @echo "Available targets:" + @echo " image Build the Docker image (run once after Dockerfile changes)" + @echo " configure Bootstrap Conan + run cmake configure" + @echo " build Compile the project (incremental)" + @echo " test Run CTest" + @echo " cppcheck Static analysis (output -> build/cppcheck-results.txt)" + @echo " run-app Launch OpenStudioApp GUI (WSLg/Linux/macOS+XQuartz)" + @echo " check-build Interactive bash shell inside the build container" + @echo " attach /bin/sh in the image with no mounts (debug image contents)" + @echo " clean Wipe the build volume (equivalent to rm -rf build/)" + @echo " build-clean Alias for clean" + @echo " image-clean Remove the Docker image" + @echo " volumes-clean Destroy all named volumes (build + Conan + ccache)" + @echo "" + @echo "Typical first-time workflow:" + @echo " make image && make configure && make build && make test" + @echo "" + @echo "Note: build artifacts live in the '$(BUILD_VOL)' Docker volume." + @echo " Use 'make check-build' to inspect them interactively." + @echo " 'make clean' wipes the build volume; Conan/ccache are preserved." + @echo "" diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..412f9b3fa --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,51 @@ +FROM ubuntu:22.04 + +ARG DEBIAN_FRONTEND=noninteractive + +# ── Versions (change here when bumping) ───────────────────────────────────── +ARG QT_VERSION=6.11.0 +ARG QT_ARCH=linux_gcc_64 +# ───────────────────────────────────────────────────────────────────────────── + +# Layer 1: system packages +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential git curl wget ninja-build ccache \ + cmake python3 python3-pip python3-dev \ + mesa-common-dev libglu1-mesa-dev xvfb \ + libxkbcommon-x11-dev libgl1-mesa-dev chrpath \ + libxcb-icccm4 libxcb-keysyms1 libxcb-xkb1 libxcb-randr0 \ + libxcb-shape0 libxkbcommon-x11-0 libxcb-cursor0 \ + patchelf cppcheck ca-certificates lsb-release libglib2.0-0 \ + libfontconfig1-dev libdbus-1-dev \ + libasound2-dev libnss3-dev libnspr4-dev libxdamage-dev libxcomposite-dev libxrandr-dev libxtst-dev \ + && rm -rf /var/lib/apt/lists/* + +# Layer 2: Python tools + Qt 6.11.0 +# Use the PyPI release of aqtinstall (not git master) — the git+https +# workaround in app_build.yml is only required for Windows Qt 6.11.0 due to +# a directory-structure change (github.com/miurahr/aqtinstall/issues/1007). +# Linux is unaffected, so the stable PyPI release works here. +RUN pip3 install --no-cache-dir setuptools --upgrade \ + && pip3 install --no-cache-dir 'conan>2' \ + && pip3 install --no-cache-dir aqtinstall \ + && aqt install-qt \ + --outputdir /opt/Qt \ + linux desktop ${QT_VERSION} ${QT_ARCH} \ + -m qtwebchannel qtwebengine qtwebview qtpositioning qtcharts \ + && rm -rf ~/.cache/pip + +# NOTE: The OpenStudio SDK is NOT baked into the image. +# It is downloaded once by configure.sh (make configure) into the host-side +# build/ directory, which is mounted at /workspace/build at runtime. +# FindOpenStudioSDK.cmake finds it there automatically. + +# Layer 3: Newer CMake +# Ubuntu 22.04 ships cmake 3.22, but Conan 2 generates CMakePresets.json +# version 4 which requires cmake >= 3.23. Install via pip (small layer, +# keeps the Qt layer above cached). +RUN pip3 install --no-cache-dir cmake + +ENV QT_INSTALL_DIR=/opt/Qt/${QT_VERSION}/gcc_64 +ENV PATH="/opt/Qt/${QT_VERSION}/gcc_64/bin:${PATH}" + +WORKDIR /workspace diff --git a/docker/configure.sh b/docker/configure.sh new file mode 100644 index 000000000..581df16f1 --- /dev/null +++ b/docker/configure.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# configure.sh — runs inside the build container. +# Called by: make configure +# Purpose: 1. Download the OpenStudio SDK into build/ if not already present +# 2. Bootstrap Conan home (first run only) +# 3. Run `conan install` to fetch/build dependencies and generate +# the CMake toolchain + CMakeUserPresets.json. +# 4. Run `cmake --preset conan-release` to configure the project. +set -euo pipefail + +mkdir -p /workspace/build + +# ── Verbose debug header ────────────────────────────────────────────────────── +echo "================================================================" +echo " configure.sh — $(date -u '+%Y-%m-%d %H:%M:%S UTC')" +echo " Host: $(uname -a)" +echo " User: $(id)" +echo " Workdir: $(pwd)" +echo " CONAN_HOME: ${CONAN_HOME:-}" +echo " CCACHE_DIR: ${CCACHE_DIR:-}" +echo " QT_INSTALL_DIR: ${QT_INSTALL_DIR:-}" +echo " PATH: ${PATH}" +echo "================================================================" + +echo "--- Tool versions ---" +echo " bash: $(bash --version | head -1)" +echo " cmake: $(cmake --version | head -1)" +echo " conan: $(conan --version 2>&1 | head -1)" +echo " ninja: $(ninja --version 2>&1 || echo 'not found')" +echo " ccache: $(ccache --version 2>&1 | head -1 || echo 'not found')" +echo " curl: $(curl --version | head -1)" +echo "---------------------" + +# ── SDK paths (must match FindOpenStudioSDK.cmake) ─────────────────────────── +SDK_VERSION="3.11.0" +SDK_SHA="+241b8abb4d" +SDK_PLATFORM="Ubuntu-22.04-x86_64" # matches FindOpenStudioSDK.cmake: ${LSB_RELEASE_ID_SHORT}-${LSB_RELEASE_VERSION_SHORT}-${ARCH} +SDK_BASENAME="OpenStudio-${SDK_VERSION}${SDK_SHA}-${SDK_PLATFORM}" +SDK_DIR="build/OpenStudio-${SDK_VERSION}" # created in workspace +SDK_DEST="${SDK_DIR}/${SDK_BASENAME}" # where CMake looks +SDK_URL="https://github.com/NREL/OpenStudio/releases/download/v${SDK_VERSION}/${SDK_BASENAME}.tar.gz" + +echo " SDK_DEST: ${SDK_DEST}" +echo " SDK_URL: ${SDK_URL}" + +# ── Qt ─────────────────────────────────────────────────────────────────────── +QT_INSTALL_DIR="${QT_INSTALL_DIR:-/opt/Qt/6.11.0/linux_gcc_64}" +echo " Qt: ${QT_INSTALL_DIR}" +if [ -d "${QT_INSTALL_DIR}" ]; then + echo " Qt dir exists: OK" +else + echo " WARNING: Qt dir not found at ${QT_INSTALL_DIR}" +fi + +echo "==> [1/4] Checking OpenStudio SDK ..." +if [ -d "${SDK_DEST}" ]; then + echo " SDK already present at ${SDK_DEST}" + echo " SDK contents (top-level):" + ls -lah "${SDK_DEST}" || true +else + echo " SDK not found — downloading ..." + mkdir -p "${SDK_DIR}" + echo " Downloading ${SDK_URL} ..." + curl -fSL --retry 5 --retry-delay 10 --retry-connrefused \ + --progress-bar \ + "${SDK_URL}" -o "${SDK_DIR}/${SDK_BASENAME}.tar.gz" + echo " Download complete. Archive size: $(du -sh "${SDK_DIR}/${SDK_BASENAME}.tar.gz" | cut -f1)" + echo " Extracting ..." + tar xzf "${SDK_DIR}/${SDK_BASENAME}.tar.gz" -C "${SDK_DIR}" --verbose 2>&1 | tail -5 + echo " SDK extracted to ${SDK_DEST}" +fi + +# ── Conan first-run bootstrap ───────────────────────────────────────────────── +echo "==> [2/4] Bootstrapping Conan ..." +CONAN_HOME="${CONAN_HOME:-${HOME}/.conan2}" +echo " CONAN_HOME resolved to: ${CONAN_HOME}" +if [ ! -f "${CONAN_HOME}/profiles/default" ]; then + echo " No default profile found — running 'conan profile detect' ..." + conan profile detect --force + # Enforce C++20 and Release build type in the default profile + sed -i 's/cppstd=.*$/cppstd=20/' "${CONAN_HOME}/profiles/default" + sed -i 's/build_type=.*$/build_type=Release/' "${CONAN_HOME}/profiles/default" + echo " Profile after edits:" + cat "${CONAN_HOME}/profiles/default" + # NREL custom remote (hosts ruby/3.2.2 and other project packages). + # --insecure disables TLS certificate verification — needed while + # conan.openstudio.net has an expired certificate. Remove --insecure once + # the certificate is renewed. + conan remote add --force --insecure nrel-v2 \ + https://conan.openstudio.net/artifactory/api/conan/conan-v2 + echo " Conan profile created." +else + echo " Conan profile already exists:" + cat "${CONAN_HOME}/profiles/default" +fi + +# ── Ensure nrel-v2 remote is registered and insecure ───────────────────────── +# conan.openstudio.net currently has an expired TLS certificate; --insecure +# disables cert verification so the build is not blocked. Remove the +# --insecure flag (and this comment) once the certificate is renewed. +echo " Checking for nrel-v2 remote ..." +if conan remote list 2>/dev/null | grep -q 'nrel-v2'; then + echo " nrel-v2 remote found — ensuring it is enabled and insecure ..." + conan remote enable nrel-v2 + conan remote update nrel-v2 \ + --url https://conan.openstudio.net/artifactory/api/conan/conan-v2 \ + --insecure +else + echo " nrel-v2 remote not registered — adding with --insecure ..." + conan remote add --insecure nrel-v2 \ + https://conan.openstudio.net/artifactory/api/conan/conan-v2 +fi +echo " Active Conan remotes:" +conan remote list + +# Always (re)write global.conf so stale values from prior runs are corrected. +mkdir -p "${CONAN_HOME}" +echo " Writing ${CONAN_HOME}/global.conf ..." +{ + echo "core:non_interactive = True" + echo "core.download:parallel = 4" + echo "core.sources:download_cache = ${CONAN_HOME}/.conan-download-cache" +} > "${CONAN_HOME}/global.conf" +echo " global.conf written:" +cat "${CONAN_HOME}/global.conf" + +# ── ccache setup ────────────────────────────────────────────────────────────── +if command -v ccache &>/dev/null; then + echo " ccache found — configuring ..." + ccache --max-size=500M + ccache --set-config=compression=true + echo " ccache stats before build:" + ccache --show-stats +else + echo " WARNING: ccache not found — builds will not be cached" +fi + +# ── Conan install ───────────────────────────────────────────────────────────── +echo "==> [3/4] Running conan install ..." +echo " conanfile.py: $(head -5 conanfile.py 2>/dev/null || echo 'not found')" +conan install . \ + --output-folder=./build \ + --build=missing \ + -c tools.cmake.cmaketoolchain:generator=Ninja \ + -s compiler.cppstd=20 \ + -s build_type=Release +echo " conan install exit code: $?" +echo " build/ contents after conan install:" +ls -lah build/ | grep -v "^total" || true + +# ── CMake configure ─────────────────────────────────────────────────────────── +echo "==> [4/4] Running cmake configure ..." +echo " Preset: conan-release" +echo " CMakeUserPresets.json:" +cat CMakeUserPresets.json 2>/dev/null || echo " (not found)" +cmake --preset conan-release \ + -DQT_INSTALL_DIR:PATH="${QT_INSTALL_DIR}" \ + -DBUILD_DOCUMENTATION:BOOL=OFF \ + -DBUILD_PACKAGE:BOOL=OFF \ + -DBUILD_TESTING:BOOL=ON \ + -DBUILD_BENCHMARK:BOOL=ON \ + --log-level=STATUS +echo " cmake configure exit code: $?" +echo " CMakeCache.txt key values:" +grep -E "^(CMAKE_BUILD_TYPE|CMAKE_CXX_COMPILER|CMAKE_MAKE_PROGRAM|QT_INSTALL_DIR|BUILD_TESTING|BUILD_BENCHMARK)" \ + build/CMakeCache.txt 2>/dev/null | sort || echo " (CMakeCache.txt not found)" + +echo "" +echo "================================================================" +echo "Configure complete. Run 'make build' to compile." +echo "================================================================"