CMake compiler feature detect

For nearly all CMake projects we choose to detect compiler features rather than make brittle, difficult to maintain nested if/elseif based on compiler vendor and version. There are still cases where if statements to work around known compiler bugs are necessary, but where we can, we use compiler feature checks instead.

An example of this is in our project demoing C11 features. The template is done is two steps

Feature checking

Suppose a project requires C11 variable length arrays. The CMakeLists.txt would look like:

include(CheckCSourceCompiles)

set(CMAKE_C_STANDARD 11)  # so that check_c_source_compiles works

check_c_source_compiles("int main(void){for (int i = 1; i < 5; i++){int a[i];} return 0;}" c11vla)

if(NOT c11vla)
 # fatal error or disable feature using C11 VLA
endif()

add_executable(c11demo demo.c)
target_compile_features(c11demo PRIVATE c_std_11)  # best practice to set per target

CI examples

Continuous Integration services run user-defined self-checks on code on each “git push”. GitHub Actions is often the best place to start with CI. CI is one part of the DevOps lifecycle.

CIs with quota

These services have quotas even for public Git repos:

CIs without quota

At the time of this writing, these CI didn’t have usage quotas for public repos. They are constrainted by the number of concurrent jobs.

Examples

Python + Fortran

Python compiling 40 year old Fortran code: Lowtran

C + HDF5 + Cmake

IPv6 C with HDF5 writing and Cmake: PythonCSockets

C++ + Cmake

C++ and Cmake: Zakharov

IDL / GDL

IDL or GDL GNU Data Language: McIntoshPlot

Pascal

Pascal with CMake

Microsoft Surface lifecycle

Microsoft Surface computers are generally a good device to consider for school, work and personal computing. Microsoft has published a firmware update lifecycle schedule for all Surface devices. While one would assume after four years or so of firmware updates the bugs would mostly be worked out, there is the possibility of an unpatched firmware vulnerability as has happened across OEMs.

IPython no confirm on exit

IPython “confirm on exit” is not always desired. To disable confirm on exit, from Terminal (not in Python) type:

ipython profile create

This will print out the locate of the IPython config file, which is like “~/.ipython/profile_default/ipython_config.py”. Open this file in a text editor, and uncomment the line:

c.TerminalInteractiveShell.confirm_exit = False

Upon restarting IPython, you’ll find that the usual Ctrl D immediately exits without confirmation.

Fortran MPI on Windows

There are several methods available to get MPI on Windows, including:

  • Microsoft MPI (MS-MPI) with MSYS2 or MinGW: open-source, discussed below
  • Intel oneAPI MPI
  • Cygwin libopenmpi-devel
  • Windows Subsystem for Linux libopenmpi-dev

MPI + MSYS2

This procedure gives MPI via MSYS2 GCC / Gfortran compilers on Windows.

  1. Download and install Microsoft MS-MPI

  2. To make mpiexec.exe available, add to user PATH: C:\Program Files\Microsoft MPI\Bin – this is needed even when using MSYS2.

  3. Install MSYS2 MS-MPI library, from the MSYS2 Terminal

    pacman -S mingw-w64-x86_64-msmpi
    
  4. compile, creating “mpi.mod” for Fortran use. Do NOT link with ar or MPI may only run with a single image.

    cd C:/msys64/mingw64/
    
    gfortran include/mpi.f90 -c -fno-range-check -fallow-invalid-boz
    

MPI + MinGW

While we recommennd MSYS2, it’s also possible to use MS-MPI with MinGW. We will put the resulting artifacts under c:/lib/mpi

mkdir c:/lib/mpi/

Copy-Item -Path "C:/Program Files (x86)/Microsoft SDKs/MPI/Include" -Destination "c:/lib/mpi/" -Recurse

cd c:/lib/mpi

you may need to edit “include/mpi.f90” to have INCLUDE 'mpifptr.h'

gfortran include/mpi.f90 -c -fno-range-check -fallow-invalid-boz

Do NOT link with ar or MPI may only run with a single image.

Notes

OpenMPI is not currently available for native Windows.


If you don’t compile the mpi.f90 as above, errors may result like:

use mpi

Fatal Error: Cannot open module file 'mpi.mod' for reading at (1): No such file or directory

IntelMPI clash with MinGW MS-MPI

Upon installing or updating Intel oneAPI compilers and libraries on Windows, you may experience CMake failing to find MPI for MinGW. This happens because Intel compilers put Intel MPI on the system PATH. Generally we remove Intel MPI from the system PATH and use the Intel Compiler shell instead, which provides all the needed directories. This is because we don’t typically use Intel MPI with Visual Studio, and if we did, we’d make a shell script for that.

Matlab with Azure CI

Matlab CI services include Azure Pipelines. Matlab Azure Pipelines CI is useful to automatically unit test Matlab code on each “git push” as with other coding languages. We use an “azure-pipelines.yml” file in the Git repo to control CI behavior. A top-level file named like “TestAll.m” is also needed to control CI run.

Matlab azure-pipelines.yml

This azure-pipelines.yml file will test a Matlab project on Azure Pipelines CI at no cost. It can be useful to specify a specific Matlab version to ensure a baseline Matlab version compatibility. For example, specify the older Matlab version the project must support, as developers typically use newer Matlab versions and may inadvertently use Matlab functions only available in newer Matlab versions than some users or project requirements specify.

pool:
  vmImage: ubuntu-latest
steps:
  - task: InstallMATLAB@0
    inputs:
      release: R2020a
  - task: RunMATLABCommand@0
    inputs:
      command: TestAll
  - task: PublishTestResults@2
    condition: succeededOrFailed()
    inputs:
      testResultsFiles: test-results/results.xml
  - task: PublishCodeCoverageResults@1
    inputs:
      codeCoverageTool: Cobertura
      summaryFileLocation: code-coverage/coverage.xml

A file at the top level with name like TestAll.m is also necessary, with contents like:

% this script is run on Azure (or local)
import matlab.unittest.TestRunner
import matlab.unittest.Verbosity
import matlab.unittest.plugins.CodeCoveragePlugin
import matlab.unittest.plugins.XMLPlugin
import matlab.unittest.plugins.codecoverage.CoberturaFormat

name = "MyProject";

suite = testsuite(name);
runner = TestRunner.withTextOutput('OutputDetail', Verbosity.Detailed);

mkdir('code-coverage');
mkdir('test-results');

runner.addPlugin(XMLPlugin.producingJUnitFormat('test-results/results.xml'));
runner.addPlugin(CodeCoveragePlugin.forPackage(name, 'Producing', CoberturaFormat('code-coverage/coverage.xml')));


results = runner.run(suite);
assert(~isempty(results), "no tests found")

assertSuccess(results)

Examples

We have several projects using Azure Pipelines with Matlab. It’s often useful to start from a known-working example as below:

GitHub Actions quick start

GitHub Actions is among the best no-cost CI and is relatively easy to use. Here are a couple example GitHub Actions templates we use across many projects. Each workflow is stored in the project under directory “.github/workflows”. For clarity, usually we use a filename like “.github/workflows/ci.yml”.

Pure Python

This template tests on Linux, MacOS and Windows. It waits to see if Linux succeeds before running MacOS and Windows, to save resources.

name: ci

on:
  push:
    paths:
      - "**.py"
      - .github/workflows/ci.yml


jobs:

  linux:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-python@v2
      with:
        python-version: '3.x'

    - run: pip install .
    - run: pip install flake8 mypy pytest

    - run: flake8
    - run: mypy

    - run: pytest


  integration:
    needs: linux
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [windows-latest, macos-latest]
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-python@v2
      with:
        python-version: '3.x'

    - run: pip install .
    - run: pip install pytest

    - run: pytest

Fortran + Python

This uses the default GCC / Gfortran 9. You would instead apt install gfortran and other packages for ABI compatibility if you need say libhdf5-dev. Note how we version lock Python so that when a new Python release arrives, we don’t get false CI failures due to Python packages not being immediately ready for the new Python release.

name: ci

on:
  push:
    paths:
    - "**.py"
    - .github/workflows/ci.yml

jobs:

  linux:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-python@v2
      with:
        python-version: '3.8'

    - run: pip install .
    - run: pip install mypy flake8 pytest

    - run: flake8
    - run: mypy

    - run: pytest
      env:
        FC: gfortran-9

  macos:
    needs: linux
    runs-on: macos-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-python@v2
      with:
        python-version: '3.8'

    - run: pip install .
    - run: pip install pytest

    - run: pytest
      env:
        CC: gcc-9
        FC: gfortran-9


  windows:
    needs: linux
    runs-on: windows-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-python@v2
      with:
        python-version: '3.8'

    - run: pip install .
    - run: pip install pytest

    - run: pytest

Modern Fortran test frameworks

Regardless of programming language, we feel that unit test frameworks should be lightweight so that it’s not burdensome to setup and maintain, so that end users can easily run the unit and integration tests themselves as part of a typical end-user install. Two Fortran unit test frameworks meeting these principles are Zofu and Vegetables.

A third option is to write small test programs and execute them using CMake or Meson. This method can also be an appropriate choice where the program uses complex library interactions that may not mix well with a generic test framework. Although it would perhaps be more robust to use a formal test framework, developer time limitations can also be a concern.

Zofu

Zofu uses CMake or Meson. Zofu is typically installed and then “found” by the main project’s CMake or Meson scripts. Then, meson test or ctest is used to execute the numerous small test programs generated in Zofu.

Vegetables

The Vegetables Fortran test framework requires fpm as other prerequisites are auto-downloaded and installed via fpm. If the Fortran project doesn’t use fpm, you might need to consider Zofu instead.