CMake added rules via add_custom_rule as a much more convenient way to define outputs vs inputs - easier to understand and use than the classic combination of “add_custom_command” and “add_custom_target”
CTest can run incrementally avoiding rerunning tests with unchanged source
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
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
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_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.
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 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.
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.
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.
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.
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.
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:
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:
importosprint(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.
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.
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.
Setting a huge variable in the current shell often succeeds.
The failure happens at the next exec:
# Linux: this assignment may work in bashexportHUGE=$(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.
importos, subprocessfor n in (131069, 131070, 131071):
env = os.environ.copy()
env["H"] = "x" * n
try:
subprocess.check_call(["/bin/true"], env=env)
print(n, "ok")
exceptOSErroras 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.
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:
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:
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
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.
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.
programmytermuseiso_fortran_envimplicitnone(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,*)usertxtendprogram
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.
programmyfileuseiso_fortran_envimplicitnone(type,external)integer::u,ioscharacter(1000)::fn! 1000 is an arbitrarily large number
character(1000)::datprint*,"please input file to open"read(input_unit,'(a)',iostat=ios)fnif(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)thenwrite(error_unit,*)'could not open file ',trim(fn)error stop'file IO error'endifread(u,'(A)')datprint*,'first two lines of ',trim(fn),' are:'print*,trim(dat)read(u,'(A)')datprint*,trim(dat)close(u)! implicitly closed at end of program, but as good practice...
endprogram