Scientific Computing

CMake version recommendations and install

Downloading the latest release of CMake is usually easy. Admin / sudo is not required.

  • Linux: snap install cmake
  • macOS: brew install cmake
  • Windows: winget install Kitware.CMake
  • PyPI CMake package: python -m pip install cmake

For platforms where CMake binaries aren’t easily available, build CMake using scripts/build_cmake.cmake.

To see the merge requests for a certain release, use a URL like: https://gitlab.kitware.com/cmake/cmake/-/merge_requests?milestone_title=4.3.0&scope=all&state=all

Another CMake changelog summary we like.

CMake 4.5 additions include:

CMake 4.4 additions include:

CMake 4.3 additions include:

  • Common Package Specification (CPS) officially supported. This provides a solution for the long-standing program of build system-agnostic JSON package configuration files, which can be generated by CMake’s install(EXPORT) and consumable by other build systems.
  • cmake --version=json-v1 tells details of the libraries vendored into CMake, which is useful for debugging and reporting issues with CMake’s bundled dependencies. This is complementary to cmake -E capabilities which reports specific capabilities of the CMake.
  • CMake command line and file(ARCHIVE_CREATE) commands can specify compression level and algorithm in more detail
  • numerous generator string expressions were added

CMake 4.2 additions include:

  • Visual Studio 18 2026 generator added
  • cmake_language(TRACE) enables tracing just selected nestable portions of CMake script, which is important for debugging CMake projects due to the large volume of trace output.
  • Ability to force shorter paths on project builds, important for large or complex project on Windows where the 260 character path limit is a problem for some tools. This is done via CMAKE_INTERMEDIATE_DIR_STRATEGY which is a CMake environment variable as well as a CMake command-line option.
  • FASTBuild generator has impressive benchmarks. FASTBuild supports more modern platforms including Windows, versus ccache or distcc.
  • ExternalProject added options to modify environment variables at each step by CONFIGURE_ENVIRONMENT_MODIFICATION, BUILD_ENVIRONMENT_MODIFICATION, INSTALL_ENVIRONMENT_MODIFICATION, TEST_ENVIRONMENT_MODIFICATION
  • Target property SPDX_LICENSE

CMake 4.1 additions include:

  • project() added COMPAT_VERSION that propagates to subdirectories and can be queried for the top-level COMPAT_VERSION.

CMake 4.0 additions include:

CMake 3.31 additions include:

  • CMake warns if cmake_minimum_required() is < 3.10.
  • TLS ≥ 1.2 is required by default for internet operations e.g. file(DOWNLOAD), ExternalProject, FetchContent, and similar.
  • file(ARCHIVE_CREATE) gained a long-needed WORKING_DIRECTORY parameter that is essentially necessary to avoid machine-specific paths being embedded in the archive.
  • CMAKE_LINK_LIBRARIES_STRATEGY allows specifying a strategy for ordering target direct link dependencies.

CMake 3.30 additions include:

  • C++26 support.
  • CMAKE_TLS_VERIFY environment variable was added to set TLS verification (true, false).
  • defaults CMAKE_TLS_VERIFY to on, where previously it was off.
  • CTest can use the --test-dir argument with --preset, which avoids needing to be locked into a specific build directory.

CMake 3.29 additions include:

  • cmake_language(EXIT code) to exit CMake script mode with a specific return code. This is useful when using CMake as a platform-agnostic scripting language instead of shell script.
  • Environment variable CMAKE_INSTALL_PREFIX is used to set the default install prefix across projects–it can be overridden as typical by cmake -DCMAKE_INSTALL_PREFIX= option.
  • Target property TEST_LAUNCHER allows specifying a test launcher. For MPI program this allows deduplicating or making more programmatic test runner scripts.
  • Linker information variables including CMAKE__COMPILER_LINKER_ID have been added to allow programmatic logic like setting target_link_options() based on the particular linker.
  • ctest --parallel without a number or 0 will use unbounded test run parallelism.

CMake 3.28 additions include:

  • changes PATH behavior for Windows find_{library,path,file}() to no longer search PATH. This may break some projects that rely on PATH for finding libraries. MSYS2-distributed CMake is patched to include PATH like earlier CMake, which can be confusing for CI etc. not using MSYS CMake with that patch. Windows CI/user may need to specify CMAKE_PREFIX_PATH like

    cmake -DCMAKE_PREFIX_PATH=$Env:SYSTEMDRIVE/msys64/ucrt64/lib -B build
  • Support for C++20 modules is considerably improved and most users will want at least CMake 3.28 to make C++ modules usable.

  • Generator expressions $<IF> $<AND> $<OR> now short circuit.

  • Test properties now have a DIRECTORY parameter, useful for setting test parameters from the project’s top level CMakeLists.txt.

  • CMake 3.28.4 fixed a long-standing bug in Ninja Fortran targets that use include statements.

CMake 3.27 additions include:

ℹ️ Note

Fortran + Ninja was broken for OBJECT libraries in CMake 3.27.0..3.27.8 and fixed in 3.27.9.


Older CMake changelog

Detect CMake non-target directives

CMake directives can be target-specific - generally starting with target*() like target_compile_options() versus non-target-specific like add_compile_options() or include_directories(). It’s usually recommended to use target-specific directives whenever possible, as they provide better scope control over individual targets, avoiding applying directives in unexpected scopes.

CMake provides a warning flag -Wnon-target-directive to detect such directives. Sometimes they may be truly unavoidable or in a third-party library out of one’s scope.

cmake -Werror=non-target-directive -B build

or to only warn without treating it as an error:

cmake -Wnon-target-directive -B build

Terminal escape inter-program communications

Terminal escape sequences are often used to control the appearance and behavior of terminal emulators. Beyond visual effects, they can also facilitate inter-program communications by signaling events or passing information between programs running in the terminal. The OSC 7501 protocol of escape sequences allows programs to communicate their status to the terminal, which can then relay this information to other programs or scripts that are monitoring the terminal output. A prime use of this is for AI agents, but could also be used for coordinating other types of programs, such as build systems, monitoring tools, or custom terminal-based workflows. Check out the author’s article linked above for a detailed explanation and examples of how OSC 7501 can be used for inter-program communications in terminal environments.

Get directory of Bash script

Most shells don’t have a straightforward way to get the directory of the script being executed. There are one-line commands to get the script directory in each shell:

Shell one-line command
Bash sdir=$(dirname $(realpath ${BASH_SOURCE[0]}))
Zsh sdir=${${(%):-%x}:A:h}

Execution of scripts without a shebang goes to the system “/bin/sh”, which may be in POSIX mode and so may not support Bash- or Zsh-specific features.

With HPC batch systems, the above likely does not work as intended since the HPC batch scheduler generally copies the batch script to a temporary directory. Each HPC batch system has a way of indicating from where the batch script was originally submitted - which can be distinct from the script’s original directory. The technique is to be mindful of what directory one is submitting the HPC batch job from.

Batch system environment variable indicating submission directory
Slurm SLURM_SUBMIT_DIR
PBS PBS_O_WORKDIR
LSF LS_SUBCWD

Using “~” or “$HOME” within the batch script may not point to the user home directory due to the possibility of LSF account mapping, a different automount prefix, or a container environment. The HPC generally has a networked drive available with environment variables pointing to the intended directories for data and working storage. Use the documentation for the intended HPC batch system to determine the appropriate environment variables for data, scratch, and working directories. These environment variables would be used inside the HPC batch script.

Pull request scheduling AI skill

William Martin of GitHub has written an agent skill that pays down technical debt in GitHub’s command line interface cli/cli by opening a managed stream of pull requests. The skill and William’s commentary are in pull request 14095, merged in August 2026. The interesting parts are the set of constraints that keep the loop from outrunning the human who has to read the diff.

Martin aims to clear a backlog of roughly 1900 findings. A single cleanup pull request would be unreviewable. The skill is the alternative: one verified gap per run, opened ready for review, and then the run stops. The goal is a change so small that reviewing it takes about two minutes.

A stalled review halts production rather than letting work pile up behind it, so tick frequency does not matter: merging one a day yields one a day. An unattended run may try up to three targets. The first attempt that validates becomes the pull request. Failure notes from the others ride along so the next run does not rediscover them. A run that lands nothing is silent - the absence of a pull request is the signal. A human handles review comments, and the one-open-pull-request cap means the loop waits.

Steering goes through a committed file .experiments/tech-debt-burndown/memory.md. A human owns the Current focus section. The run may append rejected targets and failed attempts below it with a 150-line budget, and must re-check any factual claim it finds there. The pull request is opened ready for human review.

Pip install says Microsoft Visual C++ is required

On Windows if installing a Python package that requires a native build the following error may result if Visual Studio Build Tools isn’t installed:

error Microsoft Visual C++ is required

Check PyPI for a prebuilt wheel compatible with your Python version and Windows architecture to avoid the need for a local build.

Otherwise, it will be necessary to build the package from source by installing Microsoft’s Build Tools for Visual Studio. In the installer, select Desktop development with C++ and include the MSVC C++ compiler and Windows SDK components for the target architecture.

Use a toolset supported by the targeted Python release and package; match the compiler architecture to the Python interpreter. Check the Python interpreter architecture like:

#!/usr/bin/env python3
import struct
import platform

print(f"Architecture: {platform.machine()}")
print(f"Pointer size: {struct.calcsize('P') * 8} bits")

Python documentation for Windows extension builds and the package’s own build instructions are the references for version-specific requirements.

C or C++ extension modules are defined in pyproject.toml by the project.

Environment variables maximum size

Unix-like shells and Windows both limit how much data a new process can receive. Exceeding the limit produces Argument list too long (E2BIG) on POSIX, or a truncated / rejected command line on Windows.

ARG_MAX is the combined size of the argument vector and the environment block that exec / CreateProcess will accept.

getconf ARG_MAX

or from Python:

import os
print(os.sysconf("SC_ARG_MAX"))

POSIX defines ARG_MAX as the maximum length of the arguments to the exec family, including environment data. That means:

  • every argv[i] string (plus its terminating NUL)
  • every NAME=value environment string (plus NUL)
  • implementation-specific accounting for pointer arrays, null terminators, and alignment

On Linux, the executable path is also included in the E2BIG check.

On Linux, GNU xargs reports how much of the ARG_MAX budget the current environment already uses:

true | xargs --show-limits

POSIX’s guaranteed minimum is only _POSIX_ARG_MAX = 4096 bytes, which could be a consideration on micro embedded systems. Real systems ARG_MAX values are usually much larger on modern systems. POSIX does not define a portable way to calculate the exact remaining room because implementations may account for pointers, null terminators, and alignment differently.

Linux

Two independent limits apply.

ARG_MAX: overall combined size of the argument vector and environment block.

  • at most 1/4 of the current soft RLIMIT_STACK
  • at most 3/4 of the kernel’s _STK_LIM (8 MiB) → 6 MiB ceiling
  • at least 32 pages (128 KiB floor, since Linux 2.6.25)

Default stack is 8 MiB, so getconf ARG_MAX is typically 2097152 (2 MiB). Raising ulimit -s raises the reported value up to the 6 MiB cap; dropping the stack limit drops ARG_MAX toward 128 KiB.

<linux/limits.h> ARG_MAX header value is a compile-time minimum, not the runtime limit. Instead, use getconf or sysconf(_SC_ARG_MAX).

MAX_ARG_STRLEN: This is the limit constraining export of a huge variable.

A single argument or environment variable (NAME=value\0) cannot exceed MAX_ARG_STRLEN. Exceeding MAX_ARG_STRLEN results in execve returning E2BIG, even when the total is well under ARG_MAX. Raising MAX_ARG_STRLEN requires rebuilding the kernel.

macOS

Darwin does not have Linux’s per-string MAX_ARG_STRLEN cap. One argument or one environment variable can consume almost the entire ARG_MAX.

Typical values:

source typical value
getconf ARG_MAX 1048576 (1MiB)
sysctl kern.argmax 1048576 (1MiB)

kern.argmax is not tunable at runtime.

The budget still includes the environment. A 128 KiB PATH can contribute to exec failures when combined with the rest of the environment and the command arguments, even though 1 MiB looks ample on paper.

Shell builtins (echo, printf) are not subject to ARG_MAX. External binaries are.

Windows

Windows splits the problem across several APIs. There is no getconf ARG_MAX.

Layer Limit
CreateProcessW command line 32,767 characters including the terminating NUL
CreateProcessA environment block 32,767 characters
Unicode environment block (Vista+) no documented hard cap; 256 KiB+ works from Win32
Documented max one user-defined env var 32,767 characters
cmd.exe command line 8191 characters
cmd.exe inherited env var it will honor 8191 characters
set in cmd.exe 8191 characters
setx 1024 characters when assigning a value

So:

  • A program calling CreateProcessW can pass a ~32k character command line and a large Unicode environment.
  • Anything that goes through cmd.exe or a .bat file dies at 8191.
  • PowerShell and the Win32 APIs can set a single variable closer to the 32,767 character documented max; cmd cannot.

The environment block format is:

NAME=value\0NAME=value\0\0

Names cannot contain =.

How the two limits interact

Setting a huge variable in the current shell often succeeds. The failure happens at the next exec:

# Linux: this assignment may work in bash
export HUGE=$(python3 -c "print('x'*200000)")
# this exec fails with E2BIG because 200000 > MAX_ARG_STRLEN
/bin/true

On macOS the same pattern fails when len("HUGE=")+200000 plus the rest of the environment exceeds ARG_MAX.

On Windows, cmd.exe will not even accept a set line longer than 8191 characters, while CreateProcessW with an explicit environment block can go much further.

Prove the Linux per-string cap

import os, subprocess

for n in (131069, 131070, 131071):
    env = os.environ.copy()
    env["H"] = "x" * n
    try:
        subprocess.check_call(["/bin/true"], env=env)
        print(n, "ok")
    except OSError as e:
        print(n, e)

On a 4 KiB page Linux kernel, 131069 typically succeeds and 131070 fails because H= plus the value and the terminating NUL must fit within MAX_ARG_STRLEN.

Workarounds

  • Do not put large payloads in the environment or argv. Use a file, pipe, socket, or response file (@args.txt).
  • xargs / find -exec … {} + chunk argv to stay under the total.
  • On Linux, ulimit -s only helps the total, not MAX_ARG_STRLEN.
  • On Windows, call CreateProcessW directly (or PowerShell / Python subprocess with env=) instead of going through cmd.exe.
  • Keep PATH short; it is the environment variable that most often eats the budget.

Summary

Platform Total argv + env One environment variable
Linux (≥ 2.6.25) ~1/4 of stack, 128 KiB–6 MiB (often 2 MiB) 128 KiB (MAX_ARG_STRLEN)
macOS 1 MiB (kern.argmax) same as total
Windows Win32 32,767-character command line; large Unicode environment block 32,767 characters
Windows cmd.exe 8191 characters 8191 characters

The 128 KiB figure often quoted for Unix is the Linux per-string cap (MAX_ARG_STRLEN), not the total, and not a universal Unix number.

Run macOS Terminal command with Rosetta

On macOS, Terminal commands can be specified to run as a Rosetta CPU arch translated binary using the arch command. If supported by the macOS version, for Apple Silicon CPUs Rosetta can be installed with the following command:

softwareupdate --install-rosetta

Then run an x86_64 command like:

arch -x86_64 <command> <command options>

To run cmake as an x86_64 binary on an Apple Silicon Mac:

arch -x86_64 cmake -B build

Check the architecture of the current shell process:

arch -x86_64 uname -m

Universal or x86_64 binaries required

When using

arch -x86_64 <command>

to run a program, the program must be a universal binary or an x86_64 binary. If not, the error message will be similar to:

arch: posix_spawnp: <command>: Bad CPU type in executable

For programs like CMake that are designed for cross-compiling, command CMake to build for x86_64 by setting the CMAKE_OSX_ARCHITECTURES variable to x86_64:

cmake -B build -DCMAKE_OSX_ARCHITECTURES=x86_64

cmake --build build

Detect if macOS app is Rosetta or Native

It’s desirable to run the native version of the app rather than the Rosetta CPU arch translated binary. Determine if an app is native, Universal, or other architecture with lipo:

lipo -archs /path/to/program

lipo doesn’t search $PATH, so you need to provide the full path to the program, which can be done like:

lipo -archs "$(command -v program_name)"

For example

lipo -archs "$(command -v cmake)"

A symptom of running an x86_64 app on an Apple Silicon CPU without Rosetta is like

arch -x86_64 /usr/bin/true

arch: posix_spawnp: /usr/bin/true: Bad CPU type in executable

Or

❯ cmake-3.13.5/CMake.app/Contents/bin/cmake

zsh: bad CPU type in executable: cmake-3.13.5/CMake.app/Contents/bin/cmake

Fortran terminal ISO standard units

iso_fortran_env is respected by modern Fortran compilers, as implied by its name. iso_fortran_env is part of the Fortran 2003 specification and popular Fortran compilers added it by calendar year 2010 generally.

Why use iso_fortran_env terminal I/O: legacy programs written before Fortran 2003 often write to terminal with:

write(*,*) 'The value of X is ',x

or

write(6,*) 'The value of X is ',x

This is a problem when trying to debug with text output to terminal, especially where someone has used “6” for file I/O unit by mistake. Or, if trying to write to a file with an uninitialized unit number, stderr gets redirected to file fort.0.

For operating systems including macOS, Windows, and Linux the standard terminal units correspond like:

File descriptor Fortran unit number Standard unit Gfortran env var
0 5 input_unit (stdin) GFORTRAN_STDIN_UNIT
1 6 output_unit (stdout) GFORTRAN_STDOUT_UNIT
2 0 error_unit (stderr) GFORTRAN_STDERR_UNIT

Legacy code making use of open(unit=) number in this range can cause unexpected behavior. Please use open(newunit=...) to avoid conflicts with the standard terminal units. Of note, GFortran can use environment variables to change the default unit numbers for standard input, output, and error as in the table above.

Example

Print repeatedly to the same line, and combine prompt text on the same line with input.

iso_fortran_env terminal I/O: example prints to stdout, then stderr and finally asks for user input with a prompt on the same line.

program myterm
use iso_fortran_env
implicit none (type, external)
character(1000) :: usertxt  ! 1000 is an arbitrarily large number
integer :: ios

! could also just use print *,'printed to stdout'
write(output_unit,*) 'Printed to stdout'

write(error_unit,*) 'printed to stderr'

! prompt with caret on same line as input, here using a greater than sign >
write(output_unit,'(A)',advance='no') ' >'
flush(output_unit)

read(input_unit,"(A)", iostat=ios) usertxt
! trap Ctrl-D EOF on Unix-like systems to avoid crashing program
if (ios/=0) backspace(input_unit)  ! ctrl D gobble

write(output_unit,*) usertxt

end program

If stderr goes to file fort.0

If stderr from error_unit gets written to a file fort.0 instead of being printed to screen, this is an indication of open(u)ing a file without first setting a value for u, which might default to 0.

Unless needing to persist a file opening between calls of a function/subroutine, normally open a file with newunit.

program myfile
use iso_fortran_env
implicit none (type, external)

integer :: u, ios
character(1000) :: fn ! 1000 is an arbitrarily large number
character(1000) :: dat

print *, "please input file to open"

read(input_unit, '(a)', iostat=ios) fn
if(is_iostat_end(ios)) stop

! open file, using better to ask forgiveness than permission principle
! status='old' means generate error if file doesn't exist
open(newunit=u,file=fn, status='old',action='read',iostat=ios)
if (ios /= 0) then
    write(error_unit,*) 'could not open file ',trim(fn)
    error stop 'file IO error'
endif

read(u,'(A)') dat
print *,'first two lines of ',trim(fn),' are:'
print *,trim(dat)
read(u,'(A)') dat
print *,trim(dat)

close(u)  ! implicitly closed at end of program, but as good practice...
end program

Related: