Fix #5631 - Can't build epmodel on MSVC (LNK1189) - Split openstudiolib.dll into 4 separate DLLs on MSVC #766
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Test (Ubuntu 22.04) | |
| on: | |
| push: | |
| branches: [ incremental_build_debug ] | |
| pull_request: | |
| branches: [ develop, os-4.0-dev ] | |
| workflow_dispatch: | |
| inputs: | |
| aggressive_clean: | |
| description: 'Aggressive cleaning (remove all conan packages and build artifacts)' | |
| required: false | |
| type: boolean | |
| default: false | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'Pull Request - Ready for CI') | |
| runs-on: linux-openstudio-2 | |
| permissions: | |
| contents: read | |
| issues: read | |
| checks: write | |
| pull-requests: write | |
| actions: read | |
| env: | |
| MAX_SAFE_THREADS: $(( ($(nproc) * 90 + 50) / 100 )) | |
| CMAKE_CXX_COMPILER_LAUNCHER: ccache | |
| MAKEFLAGS: "-j$(( ($(nproc) * 90 + 50) / 100 ))" | |
| NODE_TLS_REJECT_UNAUTHORIZED: 0 | |
| DOCKER_ROOT: /github/home | |
| OPENSTUDIO_DOCKER_VOLUME: /github/home/Ubuntu | |
| OPENSTUDIO_SOURCE_NAME: OpenStudio | |
| OPENSTUDIO_BUILD_NAME: OS-build | |
| EXCLUDED_TESTS: "BCLFixture.RemoteBCL_BCLSearchResult" | |
| container: # Define the Docker container for the job. All subsequent steps run inside it. | |
| image: nrel/openstudio-cmake-tools:jammy-main | |
| options: --privileged -u root -e "LANG=en_US.UTF-8" # These options are passed to the 'docker run' command internally | |
| volumes: # envs don't work in volume definition for containers | |
| - "/srv/data/jenkins/docker-volumes/conan-data/.conan2:/github/home/.conan2" # Conan cache | |
| - "/srv/data/jenkins/docker-volumes/ubuntu-2204:/github/home/Ubuntu" | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Prepare workspace | |
| run: | | |
| cd ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }} | |
| # make safe repository | |
| git config --global --add safe.directory "*" | |
| - name: Aggressive clean | |
| if: inputs.aggressive_clean | |
| run: | | |
| BUILD_DIR="${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}" | |
| echo "Aggressive clean: removing entire build directory" | |
| rm -rf "$BUILD_DIR" | |
| echo "Aggressive clean: removing all conan packages to ensure clean state" | |
| # Force remove all to ensure rebuild with correct ABI | |
| conan remove "*" -c || true | |
| CONAN_PROFILE_DEFAULT="${{ env.DOCKER_ROOT }}/.conan2/profiles/default" | |
| if [ -f "$CONAN_PROFILE_DEFAULT" ]; then | |
| echo "Aggressive clean: removing conan profile to ensure correct detection and rebuild" | |
| conan profile show | |
| rm -Rf "$CONAN_PROFILE_DEFAULT" | |
| fi | |
| - name: Remove old artifacts | |
| run: | | |
| BUILD_DIR="${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}" | |
| if [ -d "$BUILD_DIR" ]; then | |
| echo "Cleaning up old artifacts in $BUILD_DIR" | |
| find "$BUILD_DIR" -maxdepth 1 -name "*.deb" -delete | |
| rm -rf "$BUILD_DIR/_CPack_Packages" | |
| fi | |
| - name: Cache ccache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ env.DOCKER_ROOT }}/.ccache | |
| key: ${{ runner.os }}-ccache-${{ github.sha }} | |
| restore-keys: | | |
| ${{ runner.os }}-ccache- | |
| - name: Configure ccache | |
| run: | | |
| ccache --max-size=2G | |
| ccache --set-config=compression=true | |
| ccache --set-config=compression_level=1 | |
| ccache --show-stats | |
| - name: Git Setup | |
| run: | | |
| # Set up git and fetch PR head, then detect conan profile and install dependencies | |
| cd ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }} | |
| git config --global --add safe.directory "*" && \ | |
| git config user.email "cicommercialbuilding@gmail.com" && \ | |
| git config user.name "ci-commercialbuildings" && \ | |
| git fetch origin && \ | |
| git fetch origin +refs/pull/*/head:refs/remotes/origin/pr/* && \ | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| git checkout origin/pr/${{ github.event.pull_request.number }} | |
| else | |
| git checkout ${{ github.ref_name }} | |
| fi | |
| - name: Install conan dependencies | |
| working-directory: ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }} | |
| run: | | |
| conan remote add conancenter --force https://center2.conan.io | |
| conan remote update conancenter --insecure | |
| conan remote add nrel-v2 --index 0 --force https://conan.openstudio.net/artifactory/api/conan/conan-v2 | |
| conan remote update nrel-v2 --insecure | |
| CONAN_PROFILE_DEFAULT="${{ env.DOCKER_ROOT }}/.conan2/profiles/default" | |
| if [ ! -f "$CONAN_PROFILE_DEFAULT" ]; then | |
| conan profile detect | |
| fi | |
| sed -i 's/cppstd=.*$/cppstd=20/g' $CONAN_PROFILE_DEFAULT | |
| conan profile show | |
| conan install . --output-folder=${{ env.OPENSTUDIO_BUILD_NAME }} --build=missing -c tools.cmake.cmaketoolchain:generator=Ninja -s compiler.cppstd=20 -s build_type=Release | |
| - name: Locate Ruby | |
| run: | | |
| ruby_path=$(command -v ruby) | |
| echo "SYSTEM_RUBY_PATH=$ruby_path" >> $GITHUB_ENV | |
| # wrap cmake with ccache using a flag or environment variable | |
| - name: Configure with CMake | |
| working-directory: ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }} | |
| run: | | |
| cmake --preset conan-release \ | |
| -DSYSTEM_RUBY_EXECUTABLE="$SYSTEM_RUBY_PATH" | |
| - name: Verify build state | |
| working-directory: ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }} | |
| run: | | |
| if [ -f "build.ninja" ]; then | |
| echo "Ninja build file found - checking what needs to be built" | |
| ninja -n -j 1 package | head -20 || true | |
| else | |
| echo "No build.ninja found - full reconfiguration will be needed" | |
| fi | |
| - name: Build with Ninja | |
| working-directory: ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }} | |
| run: | | |
| . ./conanbuild.sh | |
| ninja -j ${{ env.MAX_SAFE_THREADS }} package | |
| - name: Run CTests with enhanced error handling | |
| working-directory: ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }} | |
| run: | | |
| set +e | |
| mkdir -p Testing/run{1,2,3} | |
| begin_group() { echo -e "::group::\033[93m$1\033[0m"; } | |
| begin_group "Run 1 - Initial with all tests" | |
| ctest -j ${{ env.MAX_SAFE_THREADS }} --no-compress-output --output-junit Testing/run1/results.xml -E "${{ env.EXCLUDED_TESTS }}" 2>&1 | tee /tmp/run1.log | |
| RESULT1=${PIPESTATUS[0]} | |
| echo "::endgroup::" | |
| if [ $RESULT1 -ne 0 ]; then | |
| begin_group "Run 2 - Retrying failed tests" | |
| echo "Retrying failed tests..." | |
| ctest -j ${{ env.MAX_SAFE_THREADS }} --rerun-failed --no-compress-output --output-junit Testing/run2/results.xml -E "${{ env.EXCLUDED_TESTS }}" 2>&1 | tee /tmp/run2.log | |
| RESULT2=${PIPESTATUS[0]} | |
| echo "::endgroup::" | |
| if [ $RESULT2 -ne 0 ]; then | |
| # Final attempt with verbose output for failing tests | |
| begin_group "Run 3 - Final attempt with verbose output..." | |
| ctest -j ${{ env.MAX_SAFE_THREADS }} --rerun-failed --no-compress-output --output-junit Testing/run3/results.xml -E "${{ env.EXCLUDED_TESTS }}" --output-on-failure 2>&1 | tee /tmp/run3.log | |
| RESULT3=${PIPESTATUS[0]} | |
| echo "::endgroup::" | |
| else | |
| RESULT3=0 | |
| echo "Tests passed on retry" | |
| fi | |
| else | |
| echo "All tests passed" | |
| RESULT2=0 | |
| RESULT3=0 | |
| fi | |
| echo "Test Results: Run1=$RESULT1 Run2=$RESULT2 Run3=$RESULT3" | |
| if [ $RESULT1 -eq 0 ] || [ $RESULT2 -eq 0 ] || [ $RESULT3 -eq 0 ]; then | |
| exit 0 | |
| else | |
| exit 1 | |
| fi | |
| - name: Test Summary | |
| uses: test-summary/action@v2 | |
| with: | |
| paths: "${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/Testing/run*/results.xml" # Path to your JUnit output file | |
| output: "${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/Testing/test-summary.md" | |
| if: always() | |
| - name: Upload test summary | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-summary | |
| path: ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/Testing/test-summary.md | |
| if: always() | |
| - name: Generate test results dashboard | |
| if: always() | |
| run: | | |
| mkdir -p ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/Testing/dashboard | |
| # Create comprehensive test dashboard | |
| cat > ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/Testing/dashboard/test-dashboard.md << 'EOF' | |
| # 🧪 Test Results Dashboard | |
| ## Summary | |
| EOF | |
| # Process JUnit XML files and extract test information | |
| python3 << 'PYTHON_EOF' | |
| import xml.etree.ElementTree as ET | |
| import os | |
| import glob | |
| from datetime import datetime | |
| build_dir = "${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}" | |
| dashboard_file = f"{build_dir}/Testing/dashboard/test-dashboard.md" | |
| # Find all JUnit XML files | |
| xml_files = glob.glob(f"{build_dir}/Testing/run*/results.xml") | |
| total_tests = 0 | |
| total_failures = 0 | |
| total_errors = 0 | |
| total_skipped = 0 | |
| failed_tests = [] | |
| # Parse XML files | |
| for xml_file in xml_files: | |
| if os.path.exists(xml_file): | |
| try: | |
| tree = ET.parse(xml_file) | |
| root = tree.getroot() | |
| # Handle different JUnit XML formats | |
| if root.tag == 'testsuites': | |
| testsuites = root.findall('testsuite') | |
| else: | |
| testsuites = [root] | |
| for testsuite in testsuites: | |
| suite_name = testsuite.get('name', 'Unknown') | |
| tests = int(testsuite.get('tests', 0)) | |
| failures = int(testsuite.get('failures', 0)) | |
| errors = int(testsuite.get('errors', 0)) | |
| skipped = int(testsuite.get('skipped', 0)) | |
| total_tests += tests | |
| total_failures += failures | |
| total_errors += errors | |
| total_skipped += skipped | |
| # Get failed test details | |
| for testcase in testsuite.findall('testcase'): | |
| test_name = testcase.get('name', 'Unknown') | |
| classname = testcase.get('classname', suite_name) | |
| failure = testcase.find('failure') | |
| error = testcase.find('error') | |
| if failure is not None or error is not None: | |
| failure_info = failure if failure is not None else error | |
| message = failure_info.get('message', 'No message') | |
| details = failure_info.text or 'No details available' | |
| failed_tests.append({ | |
| 'suite': suite_name, | |
| 'class': classname, | |
| 'name': test_name, | |
| 'message': message, | |
| 'details': details, | |
| 'run': os.path.basename(os.path.dirname(xml_file)) | |
| }) | |
| except Exception as e: | |
| print(f"Error parsing {xml_file}: {e}") | |
| # Generate dashboard content | |
| with open(dashboard_file, 'a') as f: | |
| # Summary section | |
| success_rate = ((total_tests - total_failures - total_errors) / total_tests * 100) if total_tests > 0 else 0 | |
| f.write(f"| Metric | Value |\n") | |
| f.write(f"|--------|-------|\n") | |
| f.write(f"| **Total Tests** | {total_tests} |\n") | |
| f.write(f"| **Passed** | {total_tests - total_failures - total_errors} |\n") | |
| f.write(f"| **Failed** | {total_failures} |\n") | |
| f.write(f"| **Errors** | {total_errors} |\n") | |
| f.write(f"| **Skipped** | {total_skipped} |\n") | |
| f.write(f"| **Success Rate** | {success_rate:.1f}% |\n") | |
| f.write(f"| **Generated** | {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')} |\n\n") | |
| if success_rate >= 100: | |
| f.write("## ✅ All Tests Passed!\n\n") | |
| elif success_rate >= 95: | |
| f.write("## ⚠️ Minor Issues Detected\n\n") | |
| else: | |
| f.write("## ❌ Significant Test Failures\n\n") | |
| # Failed tests section | |
| if failed_tests: | |
| f.write(f"## 🔍 Failed Tests ({len(failed_tests)} failures)\n\n") | |
| # Group by test suite | |
| suites = {} | |
| for test in failed_tests: | |
| suite = test['suite'] | |
| if suite not in suites: | |
| suites[suite] = [] | |
| suites[suite].append(test) | |
| for suite_name, suite_tests in suites.items(): | |
| f.write(f"### {suite_name} ({len(suite_tests)} failures)\n\n") | |
| for test in suite_tests: | |
| f.write(f"<details>\n") | |
| f.write(f"<summary><strong>{test['class']}.{test['name']}</strong> ({test['run']})</summary>\n\n") | |
| f.write(f"**Error Message:**\n") | |
| f.write(f"```\n{test['message']}\n```\n\n") | |
| f.write(f"**Full Details:**\n") | |
| f.write(f"```\n{test['details']}\n```\n\n") | |
| f.write(f"</details>\n\n") | |
| # Test run information | |
| f.write("## 📊 Test Run Information\n\n") | |
| f.write("| Run | XML File | Status |\n") | |
| f.write("|-----|----------|--------|\n") | |
| for i, xml_file in enumerate(xml_files, 1): | |
| status = "✅ Found" if os.path.exists(xml_file) else "❌ Missing" | |
| run_name = os.path.basename(os.path.dirname(xml_file)) | |
| f.write(f"| {run_name} | `{os.path.basename(xml_file)}` | {status} |\n") | |
| if not xml_files: | |
| f.write("| - | No XML files found | ❌ Missing |\n") | |
| print(f"Dashboard generated with {total_tests} tests, {total_failures + total_errors} failures") | |
| PYTHON_EOF | |
| - name: Publish test results to PR | |
| if: always() && github.event_name == 'pull_request' | |
| uses: marocchino/sticky-pull-request-comment@v2 | |
| with: | |
| header: test-results | |
| path: ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/Testing/dashboard/test-dashboard.md | |
| - name: Upload comprehensive test results | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results-dashboard | |
| path: | | |
| ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/Testing/dashboard/ | |
| ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/Testing/run*/ | |
| if: always() | |
| - name: Upload build artifacts with metadata | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ubuntu-2204-${{ github.head_ref || github.ref_name }}-${{ github.sha }} | |
| path: | | |
| ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/*.deb | |
| ${{ env.OPENSTUDIO_DOCKER_VOLUME }}/${{ env.OPENSTUDIO_SOURCE_NAME }}/${{ env.OPENSTUDIO_BUILD_NAME }}/_CPack_Packages/Linux/TGZ/*.tar.gz | |
| retention-days: 30 | |
| if: always() |