CTest
–interactive-debug-mode
is particularly useful on Windows to enable/disable debug dumps and error popup modal windows, especially for missing DLL errors.
By default this feature is enabled.
This default behavior to have interactive debug mode “on” can make problems with modal popups hanging on automated systems like CI or agentic workflows where user interaction is not desired or possible.
To avoid this, one can explicitly disable interactive debug mode by option
ctest --interactive-debug-mode=0
An even better approach is to make CMakePresets.json explicitly set the interactive debug mode for CTest invocation with preset or workflow with "interactiveDebugging": false thats inherited for all CTest preset execution like:
In agentic instruction files like
CMAKE.instructions.md
or similar, consider adding a line like:
CTest on Windows should be run with option --interactive-debug-mode=0 to prevent modal popups from interrupting automated workflows.
NOTE: CTest environment variable
CTEST_INTERACTIVE_DEBUG_MODE
is read-only and has no effect on setting the interactive debug mode. The mode must be controlled via the --interactive-debug-mode command-line option when invoking CTest.
Visual Studio executables built in Debug mode by default pop up
modal
debug windows if an unhandled exception occurs.
This can be can be undesirable for reasons including:
users having to close nuisance windows manually.
CI runs hang if an exception occurs and the modal window appears.
AI agents or automated tools may hang with unexpected modal windows.
remotely operated systems: modal windows can be difficult to close if the modal window is accidentally off-screen.
Debug popups for exception handling can be disabled by C++ runtime library functions provided by Visual Studio described below.
NOTE: missing DLL modal popups are
fixed by the calling program
such as CTest instead of this C++ method below.
Adding a few lines of code to the C++ program works around this issue by redirecting the error text to stderr console and not popping up the modal window.
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 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: