diff --git a/.github/workflows/cpp-ci-serial-programs-base.yml b/.github/workflows/cpp-ci-serial-programs-base.yml index baead1c8d4..8ad73a7b73 100644 --- a/.github/workflows/cpp-ci-serial-programs-base.yml +++ b/.github/workflows/cpp-ci-serial-programs-base.yml @@ -30,17 +30,32 @@ jobs: - name: Set variables shell: bash run: | - if [[ "${{inputs.compiler}}" == "clang" ]]; then - if [[ "${{inputs.os}}" == windows* ]]; then - CMAKE_ADDITIONAL_FLAGS="-T ClangCL" - elif [[ "${{inputs.os}}" == ubuntu* ]]; then - CMAKE_ADDITIONAL_FLAGS="-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=RelWithDebInfo" + BUILD_PARALLEL=10 + if [[ "${{inputs.os}}" == windows* ]]; then + # Build with Ninja: ccache can't wrap the compiler under the Visual Studio generator. + # "Ninja Multi-Config" keeps Visual Studio's output layout (bin/RelWithDebInfo/). + # ccache also can't cache /FAs listings, so turn them off. + # CI needs no debug info, so keep RelWithDebInfo's optimization but leave out its + # debug info: with policy CMP0141, an empty CMAKE_MSVC_DEBUG_INFORMATION_FORMAT + # drops /Zi, and linking without /debug writes no PDB. Release builds made outside + # CI keep their debug info. + echo "CMAKE_GENERATOR=Ninja Multi-Config" >> $GITHUB_ENV + CMAKE_ADDITIONAL_FLAGS="-DCMAKE_POLICY_DEFAULT_CMP0141=NEW -DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT= -DCMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO=/INCREMENTAL:NO -DPA_ASSEMBLY_LISTINGS=OFF" + if [[ "${{inputs.compiler}}" == "clang" ]]; then + CMAKE_ADDITIONAL_FLAGS="$CMAKE_ADDITIONAL_FLAGS -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl" + else + CMAKE_ADDITIONAL_FLAGS="$CMAKE_ADDITIONAL_FLAGS -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl" fi + # Visual Studio ran about one compile per core (/MP). Keep Ninja near that instead of + # 10 at once, so memory use stays close to what the runner handled before. + BUILD_PARALLEL=$(( $(nproc) + 2 )) + elif [[ "${{inputs.compiler}}" == "clang" && "${{inputs.os}}" == ubuntu* ]]; then + # RelWithDebInfo's -O2 without its -g: CI doesn't use the debug info. + CMAKE_ADDITIONAL_FLAGS="-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=RelWithDebInfo '-DCMAKE_C_FLAGS_RELWITHDEBINFO=-O2 -DNDEBUG' '-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O2 -DNDEBUG'" fi - if [[ "${{inputs.os}}" != windows* ]]; then - CMAKE_ADDITIONAL_FLAGS="$CMAKE_ADDITIONAL_FLAGS -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" - fi + CMAKE_ADDITIONAL_FLAGS="$CMAKE_ADDITIONAL_FLAGS -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" echo "CMAKE_ADDITIONAL_FLAGS=$CMAKE_ADDITIONAL_FLAGS" >> $GITHUB_ENV + echo "BUILD_PARALLEL=$BUILD_PARALLEL" >> $GITHUB_ENV if [[ "${{inputs.os}}" == windows* ]]; then UPLOAD_FOLDER="Arduino-Source/SerialPrograms/bin/RelWithDebInfo" @@ -125,26 +140,70 @@ jobs: # CMake quietly builds without Discord integration when it can't find DPP, so check here. pkg-config --exact-version=10.1.5 dpp + # Ninja needs the MSVC build environment (cl and link on PATH, INCLUDE, LIB, ...), which the + # Visual Studio generator set up by itself. Run vcvars64.bat from the newest Visual Studio, + # the one that generator used, and pass every variable it adds or changes to later steps. + - name: Set up MSVC environment (Windows) + if: startsWith(inputs.os, 'windows') + shell: pwsh + run: | + $vs = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + $vcvars = cmd /c "`"$vs\VC\Auxiliary\Build\vcvars64.bat`" >nul && set" + if ($LASTEXITCODE -ne 0) { + throw "vcvars64.bat failed with exit code $LASTEXITCODE" + } + foreach ($line in $vcvars) { + # Skip anything that isn't a NAME=value line. + if ($line -notmatch '^([^=]+)=(.*)$') { + continue + } + $name = $Matches[1] + $value = $Matches[2] + if ($name -eq 'Path' -and '${{inputs.compiler}}' -eq 'clang') { + # Find Visual Studio's own clang-cl, the one its ClangCL toolset used, before any + # other LLVM install on the runner. + $value = "$vs\VC\Tools\Llvm\x64\bin;$value" + } + if ([Environment]::GetEnvironmentVariable($name) -ne $value) { + "$name=$value" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + [Environment]::SetEnvironmentVariable($name, $value) + } + } + if (-not (Get-Command cl -ErrorAction SilentlyContinue)) { + throw "cl.exe isn't on PATH after vcvars64.bat" + } + Get-Command cl, link, clang-cl, ninja, cmake -ErrorAction SilentlyContinue | Format-Table Name, Version, Source -AutoSize + # Keep compiler outputs between runs, so a build only recompiles what a change affects. # Only pushes to main save the cache: every PR restores main's latest one, and PR runs don't # fill the repo's 10 GB cache quota with entries that no other run can read. - # Not used on Windows: ccache can't cache MSVC builds that write debug info to a PDB (/Zi). + # max-size is only a cap: evict-old-files drops everything this build didn't use, so each + # saved cache is about one build's worth. + # On Windows, install the action's own ccache: the runner's Strawberry Perl puts an older + # ccache (4.9.1) on PATH, which the default install: detect would use instead. - name: Set up ccache - if: ${{!startsWith(inputs.os, 'windows')}} uses: hendrikmuhs/ccache-action@v1.2.24 with: key: ${{inputs.os}}-${{inputs.compiler}} - max-size: 2G + max-size: 5G evict-old-files: job save: ${{github.ref == 'refs/heads/main'}} + install: ${{startsWith(inputs.os, 'windows') && 'binary' || 'detect'}} - name: Generate binaries + env: + # Depend mode: on a cache miss, ccache hashes the include files the compiler reports + # (-MD or /showIncludes) instead of running the preprocessor a second time, which the + # ccache manual says makes a miss much cheaper. In the first runs with ccache, where + # every file missed, compiling took 10-40% longer than before on Linux and macOS, and + # about twice as long on Windows. + CCACHE_DEPEND: 1 run: | cd Arduino-Source/SerialPrograms mkdir bin cd bin cmake .. -DQT_MAJOR:STRING=6 ${{env.CMAKE_ADDITIONAL_FLAGS}} - cmake --build . --config RelWithDebInfo --parallel 10 + cmake --build . --config RelWithDebInfo --parallel ${{env.BUILD_PARALLEL}} - name: Prepare upload build if: inputs.upload-build @@ -155,12 +214,16 @@ jobs: echo "https://github.com/${{github.repository_owner}}/Packages/commit/$(git -C Arduino-Source/Packages rev-parse HEAD)" } > ${{env.UPLOAD_FOLDER}}/version.txt + # Leave out the static libraries, which only the build itself uses. With debug info, + # SerialProgramsLib.lib alone was over 1 GB uncompressed. - name: Upload Build uses: actions/upload-artifact@v7 if: inputs.upload-build with: name: Serial Programs (os=${{inputs.os}} - compiler=${{inputs.compiler}}) - path: ${{env.UPLOAD_FOLDER}} + path: | + ${{env.UPLOAD_FOLDER}} + !${{env.UPLOAD_FOLDER}}/*.lib - name: Upload Executable Only uses: actions/upload-artifact@v7 diff --git a/SerialPrograms/CMakeLists.txt b/SerialPrograms/CMakeLists.txt index 774d2ae41c..c36d2f86f5 100644 --- a/SerialPrograms/CMakeLists.txt +++ b/SerialPrograms/CMakeLists.txt @@ -68,6 +68,12 @@ option(COMMAND_LINE_EXE "Build the optional executable" ON) # searched for. option(PA_CORE_ONLY "Build only the GUI-free core (basic microcontroller functions, no Qt, OpenCV, etc.)" OFF) +# With MSVC or clang-cl, write an assembly listing (/FAs) of every source file to +# Assembly/ in the build folder, to check the code the compiler generates, e.g. for +# the SIMD kernels. CI turns this off: ccache can't cache a compile that writes a +# listing, so every file would be recompiled on every run. +option(PA_ASSEMBLY_LISTINGS "Write an assembly listing of every source file to Assembly/ (MSVC and clang-cl only)" ON) + if(PA_CORE_ONLY) message(STATUS "PA_CORE_ONLY: building only the GUI-free core") set(CMAKE_AUTOMOC OFF) @@ -388,7 +394,15 @@ if (WIN32) PA_SOCIAL_SDK ) - target_compile_options(SerialProgramsLib PRIVATE /FAs /FaAssembly/ /MP /W4 /WX /external:anglebrackets /external:W0 /utf-8) + if(PA_ASSEMBLY_LISTINGS) + target_compile_options(SerialProgramsLib PRIVATE /FAs /FaAssembly/) + endif() + target_compile_options(SerialProgramsLib PRIVATE /W4 /WX /external:W0 /utf-8) + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # cl only: clang-cl ignores these, and /WX turns its unused-argument warning into an + # error. (Visual Studio's ClangCL toolset never passed them on to clang-cl.) + target_compile_options(SerialProgramsLib PRIVATE /MP /external:anglebrackets) + endif() target_compile_options(SerialProgramsLib PRIVATE /wd5054) # Deprecated enum arithemtic target_compile_options(SerialProgramsLib PRIVATE /wd4505) # unreferenced local function has been removed @@ -397,7 +411,9 @@ if (WIN32) set(ARCH_FLAGS_17_Skylake /arch:AVX512) set(ARCH_FLAGS_19_IceLake /arch:AVX512) - if(CMAKE_GENERATOR_TOOLSET MATCHES "ClangCL") + # clang-cl, whether from Visual Studio's ClangCL toolset (-T ClangCL) or from another + # generator such as Ninja, where CMAKE_GENERATOR_TOOLSET is empty. + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_options(SerialProgramsLib PRIVATE -Wno-unused-function) target_compile_options(SerialProgramsLib PRIVATE -march=nehalem) set(ARCH_FLAGS_09_Nehalem -march=nehalem) diff --git a/SerialPrograms/cmake/CoreLib.cmake b/SerialPrograms/cmake/CoreLib.cmake index 0494b954f9..e076d91edb 100644 --- a/SerialPrograms/cmake/CoreLib.cmake +++ b/SerialPrograms/cmake/CoreLib.cmake @@ -59,13 +59,21 @@ add_library(CoreLib STATIC ${CORE_LIBRARY_SOURCES}) pa_apply_gui_free_target_properties(CoreLib) if (WIN32) - # /FaAssembly/CoreLib/ writes assembly listings to Assembly/CoreLib/ in the build folder. - file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/Assembly/CoreLib) - target_compile_options(CoreLib PRIVATE /FAs /FaAssembly/CoreLib/ /MP /W4 /WX /external:anglebrackets /external:W0 /utf-8) + if(PA_ASSEMBLY_LISTINGS) # option defined in CMakeLists.txt + # /FaAssembly/CoreLib/ writes assembly listings to Assembly/CoreLib/ in the build folder. + file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/Assembly/CoreLib) + target_compile_options(CoreLib PRIVATE /FAs /FaAssembly/CoreLib/) + endif() + target_compile_options(CoreLib PRIVATE /W4 /WX /external:W0 /utf-8) + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # cl only: clang-cl ignores these, and /WX turns its unused-argument warning into an error. + target_compile_options(CoreLib PRIVATE /MP /external:anglebrackets) + endif() target_compile_options(CoreLib PRIVATE /wd5054) # Deprecated enum arithemtic target_compile_options(CoreLib PRIVATE /wd4505) # unreferenced local function has been removed - if(CMAKE_GENERATOR_TOOLSET MATCHES "ClangCL") + # clang-cl, from Visual Studio's ClangCL toolset or from another generator such as Ninja. + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") target_compile_options(CoreLib PRIVATE -Wno-unused-function) target_compile_options(CoreLib PRIVATE -march=nehalem) endif()