diff --git a/.github/instructions/docker-build.instructions.md b/.github/instructions/docker-build.instructions.md new file mode 100644 index 000000000..8ce877ed3 --- /dev/null +++ b/.github/instructions/docker-build.instructions.md @@ -0,0 +1,314 @@ +--- +description: "Use when building, compiling, testing, or validating C++ changes; fixing build errors or compilation failures; running make configure, make build, make test, or cppcheck; working with CMake, Conan, Docker, or Makefile; or refactoring C++ code. Covers how to compile and test the project inside the Docker build container." +applyTo: "src/**/*.cpp,src/**/*.hpp,src/**/CMakeLists.txt,docker/**,Makefile" +--- + +# Building and Testing with the Docker Build Container + +Production release builds are made on native CI machines. The Docker container is used for local development validation — it provides a consistent environment without needing to install Qt, Conan, or the OpenStudio SDK locally. + +When using the Docker container builds, use `make` targets from the project root to build, test, and validate changes. The Makefile handles all further Docker invocations. + +--- + +## 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) | + +--- + +## Agent Instructions + +The sections below contain behavioral rules for the agent. Follow "Platform-specific command execution" to choose the right syntax, then refer to "Common build validation rules" and "Validating a refactoring step" for testing requirements. All remaining sections are reference documentation. + +--- + +## Platform-specific command execution + +**Always determine the user's OS before generating `make` commands.** Look for these indicators: + +| OS | Indicators | +|----|-----------| +| **Windows** | PowerShell prompts, Windows paths like `C:\`, explicit mention of Windows | +| **Linux** | Unix paths like `/home/`, bash prompts, explicit mention of Linux/Ubuntu/Debian/Fedora | +| **macOS** | Unix paths like `/Users/`, explicit mention of macOS/Mac | + +**If the OS is unknown**, ask the user before generating commands. + +--- + +## Common build validation rules + +`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** by appending `; echo "EXIT: $?"` (bash/Linux/macOS) or ``; echo 'EXIT: '`$?`` (PowerShell/Windows) so the final line of output is unambiguous even when stdout is truncated. + +**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. + +**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 immediately. Continue polling every 30 seconds. Only conclude the command is still running (and stop polling) if no new output has appeared for 5 consecutive minutes. + +--- + +## 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. + +### Command syntax + +Wrap all `make` commands in `wsl bash -lc`. Set `PROJECT_ROOT` to the WSL path before running commands: + +```powershell +$env:PROJECT_ROOT = '/mnt/c/repos/osapp' +wsl bash -lc "cd $env:PROJECT_ROOT && make configure" +wsl bash -lc "cd $env:PROJECT_ROOT && make build" +wsl bash -lc "cd $env:PROJECT_ROOT && make test" +``` + +**Path conversion:** Windows path `c:\repos\osapp` maps to `/mnt/c/repos/osapp` in WSL. To convert any Windows path to WSL, replace the drive letter and colon with `/mnt/` and flip backslashes to forward slashes (e.g. `D:\projects\osapp` → `/mnt/d/projects/osapp`). + +**Windows command template** combining exit-code capture and stderr redirect: + +```powershell +$env:PROJECT_ROOT = '/mnt/c/repos/osapp' +wsl bash -lc "cd $env:PROJECT_ROOT && make 2>&1; echo 'EXIT: '`$?" +``` + +Alternatively, open a persistent WSL session and run commands directly there. Never run `make` targets in PowerShell or `cmd.exe`. + +### Prerequisites: Install Ubuntu 22.04 in WSL + +This project's local Windows workflow should match the Linux CI environment (`ubuntu-22.04`). + +1. In PowerShell, install Ubuntu 22.04 in WSL: + ```powershell + wsl --install -d Ubuntu-22.04 + ``` +1. Confirm it is installed: + ```powershell + wsl -l -v + ``` +1. Set Ubuntu-22.04 as the default distro: + ```powershell + wsl --set-default Ubuntu-22.04 + ``` +1. Start Docker Desktop on Windows. +1. In Docker Desktop, enable WSL integration for Ubuntu-22.04: + - Open **Settings > Resources > WSL Integration**. + - Turn on **Enable integration with additional distros**. + - Enable **Ubuntu-22.04**. +1. In PowerShell, verify Docker Desktop is reachable from WSL: + ```powershell + wsl bash -lc 'docker version' + ``` +1. In PowerShell, run a container pull test from WSL: + ```powershell + wsl bash -lc 'docker pull ubuntu:22.04' + ``` +1. Open the distro: + ```powershell + wsl + ``` +1. In Ubuntu, update package indexes: + ```bash + sudo apt update + ``` +1. Install Make: + ```bash + sudo apt install -y make + ``` +1. Verify installation: + ```bash + make --version + ``` +1. Verify Docker access in Ubuntu: + ```bash + docker info + ``` + +If `docker info` fails with a permission error on `/var/run/docker.sock`, run: + +```bash +sudo usermod -aG docker $USER +newgrp docker +docker info +``` + +--- + +## Running make commands on Linux/macOS + +On native Linux or macOS, run `make` targets directly in your terminal without any WSL wrapper. + +### Command syntax + +Navigate to the project root and run commands directly: + +```bash +cd /path/to/osapp +make configure +make build +make test +``` + +**Command template with exit code capture:** + +```bash +cd /path/to/osapp && make ; echo "EXIT: $?" +``` + +### Prerequisites + +**Linux (Ubuntu/Debian):** +- Docker must be installed and running +- Install `make`: `sudo apt install make` +- Add user to docker group: `sudo usermod -aG docker $USER && newgrp docker` + +**macOS:** +- Docker Desktop must be installed and running +- `make` is pre-installed via Xcode Command Line Tools + +**macOS GUI testing (optional):** + +To use `make run-app` on macOS: +1. Install XQuartz: `brew install --cask xquartz` +2. Open XQuartz preferences → enable "Allow connections from network clients" +3. Restart XQuartz +4. Set `DISPLAY=host.docker.internal:0` before running `make run-app` + +--- + +## 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` + + If changes fall into multiple categories, use the highest-priority category that applies, where priority order is: **(1)** `conanfile.py`/`CMakeLists.txt`/`*.cmake` changes → **(2)** `CMakeLists.txt` source additions/removals → **(3)** source-only. When in doubt, run `make configure && make build && make test`. + +1. **`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. + +1. **`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. + + If a test fails that is not in the known-baseline table and does not appear related to your change, re-run `make test` once to rule out flakiness. If it fails again, check `git log` on that test file. If the test was already failing on `main` before your change, document it and continue; otherwise treat it as a regression to fix. + +1. **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..d6ddec3e4 --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -0,0 +1,52 @@ +# Docker-based CI for pull requests. +# +# Builds the project inside the osapp-build Docker image 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 + cache-to: type=gha,scope=osapp-build,mode=max + + - name: Configure (Conan install + CMake configure) + run: make configure + + - name: Build + run: make build + + - name: Test + run: make test + + - name: CppCheck + run: make cppcheck diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..0f442fb74 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,62 @@ +# OpenStudio Application Development + +## Project Overview + +Cross-platform (Windows, Mac, Linux) graphical interface for OpenStudio energy models. Built on the [OpenStudio SDK](https://github.com/NREL/OpenStudio) using Qt 6.11.0 and C++20. + +## Quick Start + +See [BUILDING.md](BUILDING.md) for comprehensive build instructions. + +**Docker-based local builds** (for validation, not production): +```bash +# First time setup +make image && make configure && make build && make test + +# After source file edits +make build && make test + +# After CMakeLists.txt or conanfile.py changes +make configure && make build && make test +``` + +**Windows users:** Run `make` commands via WSL (Ubuntu 22.04), not PowerShell. See [docker-build instructions](.github/instructions/docker-build.instructions.md) for setup details. + +**Production builds** use native conan + cmake (see [BUILDING.md](BUILDING.md)). + +## Code Standards + +- **C++20** standard, compile with `-Werror` (warnings are errors) +- **Qt 6.11.0** for all GUI components +- **Unit tests required** with 90%+ coverage (see [CONTRIBUTING.md](CONTRIBUTING.md)) +- Run static analysis: `make cppcheck` + +## Testing + +**Known baseline failures** in Docker environment (expected, not regressions): +- `ModelEditorFixture.MorePath_Conversions` — Windows path test always fails on Linux +- `OpenStudioLibFixture.AnalyticsHelperSecrets` — Requires CI-injected secrets + +All other tests must pass. Zero warnings, zero regressions. + +## Architecture + +- **src/model_editor** — Core model editing functionality and Qt UI components +- **src/openstudio_lib** — Application library layer, main UI screens +- **src/openstudio_app** — Qt application entry point and lifecycle +- **src/shared_gui_components** — Reusable widgets and workflow controllers + +Supports OpenStudio Measures (Ruby/Python scripts for model transformations). + +## Key Conventions + +- Docker builds are for **local validation only**; CI uses native builds on ubuntu-22.04, macos-12, and windows-2022 +- Build artifacts live in a Docker volume (`osapp-build`), not on the host filesystem — use `make check-build` to inspect +- On Windows, Makefile targets require WSL bash; they will not work in PowerShell or cmd.exe + +## Resources + +- [Documentation](https://openstudiocoalition.org/) +- [Contributing Guidelines](CONTRIBUTING.md) +- [Code of Conduct](CODE_OF_CONDUCT.md) +- [Building Instructions](BUILDING.md) 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..75f5d22cb --- /dev/null +++ b/Makefile @@ -0,0 +1,329 @@ +# ============================================================================= +# 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 + +# Extra commands after `make attach` are treated as a command to run in the image. +# Example: make attach env +ATTACH_ARGS := $(filter-out attach,$(MAKECMDGOALS)) +CHECK_BUILD_ARGS := $(filter-out check-build,$(MAKECMDGOALS)) + +# When `attach` or `check-build` is invoked with extra commands (eg: +# `make attach env` or `make check-build env`), prevent make from treating +# those commands as unknown targets. +ifneq (,$(filter attach,$(MAKECMDGOALS))) +$(foreach goal,$(ATTACH_ARGS),$(eval .PHONY: $(goal))) +$(foreach goal,$(ATTACH_ARGS),$(eval $(goal): ; @:)) +endif +ifneq (,$(filter check-build,$(MAKECMDGOALS))) +$(foreach goal,$(CHECK_BUILD_ARGS),$(eval .PHONY: $(goal))) +$(foreach goal,$(CHECK_BUILD_ARGS),$(eval $(goal): ; @:)) +endif + +# --------------------------------------------------------------------------- +# 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-docker -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 + @if [ -n "$(strip $(CHECK_BUILD_ARGS))" ]; then \ + docker run --rm -i \ + -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) /bin/bash -lc "$(CHECK_BUILD_ARGS)" ; \ + else \ + 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) /bin/bash ; \ + fi + +# --------------------------------------------------------------------------- +# attach - /bin/bash inside the image with NO volume mounts. +# Use this to debug the image itself (inspect /opt/Qt, /opt/openstudio-sdk, etc.) +# --------------------------------------------------------------------------- +attach: + @if [ -n "$(strip $(ATTACH_ARGS))" ]; then \ + docker run --rm -i $(IMAGE):$(TAG) /bin/bash -lc "$(ATTACH_ARGS)" ; \ + else \ + docker run --rm -it $(IMAGE):$(TAG) /bin/bash ; \ + fi + +# --------------------------------------------------------------------------- +# 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 Mounted build shell; run command with: make check-build env" + @echo " attach Image-only /bin/bash (no mounts); run command with: make attach env" + @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..d0a964648 --- /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..7b5987986 --- /dev/null +++ b/docker/configure.sh @@ -0,0 +1,201 @@ +#!/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-docker` 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). + conan remote add --force 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 ------------------------------------- +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 ..." + conan remote enable nrel-v2 + conan remote update nrel-v2 \ + --url https://conan.openstudio.net/artifactory/api/conan/conan-v2 +else + echo " nrel-v2 remote not registered - adding ..." + conan remote add 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 + +DOCKER_PRESET_NAME="conan-docker" +echo " Renaming generated preset to: ${DOCKER_PRESET_NAME}" +python3 - <<'PY' +import json +from pathlib import Path + +preset_path = Path("build/CMakePresets.json") +if not preset_path.exists(): + raise SystemExit("ERROR: build/CMakePresets.json not found after conan install") + +with preset_path.open("r", encoding="utf-8") as f: + data = json.load(f) + +renames = { + "conan-release": "conan-docker", +} + +for section in ("configurePresets", "buildPresets", "testPresets", "packagePresets", "workflowPresets"): + for entry in data.get(section, []): + if isinstance(entry, dict): + for key in ("name", "configurePreset", "inherits"): + value = entry.get(key) + if isinstance(value, str) and value in renames: + entry[key] = renames[value] + elif isinstance(value, list): + entry[key] = [renames.get(v, v) if isinstance(v, str) else v for v in value] + +with preset_path.open("w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") +PY +echo " Updated build/CMakePresets.json preset names:" +grep -n '"name"\|"configurePreset"' build/CMakePresets.json || true + +# -- CMake configure ---------------------------------------------------------- +echo "==> [4/4] Running cmake configure ..." +echo " Preset: ${DOCKER_PRESET_NAME}" +echo " CMakeUserPresets.json:" +cat CMakeUserPresets.json 2>/dev/null || echo " (not found)" +cmake --preset "${DOCKER_PRESET_NAME}" \ + -DQT_INSTALL_DIR:PATH="${QT_INSTALL_DIR}" \ + -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=ON \ + -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)" +grep -E "^CMAKE_EXPORT_COMPILE_COMMANDS" \ + build/CMakeCache.txt 2>/dev/null | sort || true + +echo "" +echo "================================================================" +echo "Configure complete. Run 'make build' to compile." +echo "================================================================"