I recently contributed to SwiftSyntax, a subproject of the Swift open-source project. Building Swift and its subprojects from scratch and then unit-testing them takes about three hours, and the Terminal command is both long and complicated. A build-and-test run can output more than a megabyte to Terminal. Some of this output is potentially useful for diagnosing build-or-test failures. As an iOS developer, I haven’t spent much time in Terminal, but, in the course of running long and long-running Terminal commands recently, I reacquainted myself with some Unix tricks that I developed in the late 90s while working primarily on AIX, which, like macOS, is a Unix. These tricks could potentially benefit anyone running Terminal commands that are long, that take a long time to complete, or that generate a lot of output.
Contributors to Swift and its subprojects invoke a Python script called
build-script in order “to build, test, and prepare binary distribution archives of Swift and related tools.”
build-script can take many arguments, but the following invocation is typical for building Swift and running its unit tests:
utils/build-script --skip-build-benchmarks --skip-ios --skip-watchos --skip-tvos --swift-darwin-supported-archs "x86_64" --cmake-c-launcher="$(which sccache)" --cmake-cxx-launcher="$(which sccache)" --release-debuginfo --test --infer
Although this command works, I call it a naïve command because it can be greatly improved, as demonstrated below.
Break It Up
The naïve command is long. So long, for example, that, as I write this blog post and build it using Jekyll, the command is three times wider than what macOS Safari can display without horizontal scrolling.
When I paste the command in Terminal, the command wraps in awkward places, right in the middle of
Some of the ten arguments are conceptually related to each other, but the naïve command gives no indication of these relations.
The solution to wrapping and loss of semantic value is to break up the command using
utils/build-script \ --skip-build-benchmarks --skip-ios --skip-watchos --skip-tvos --swift-darwin-supported-archs "x86_64" \ --cmake-c-launcher="$(which sccache)" --cmake-cxx-launcher="$(which sccache)" \ --release-debuginfo \ --test \ --infer
This improved command has five conceptual groups of arguments. The last three groups have one argument only and convey the meanings described in
build-script’s documentation. But the first two groups convey additional meaning. The first group means, “Skip the stuff not needed for this project: tvOS, watchOS, iOS, ARM, and the Swift Benchmark Suite.” The second group means, “Use sccache to ‘avoid compilation when possible, storing cached results … on local disk’.” Grouping arguments on this conceptual basis helps future human readers of the command understand the “skip stuff” and “use
sccache” intents that I had when I composed the command.
The broken-up command doesn’t wrap at all in Terminal and almost fits without horizontal scrolling in macOS Safari.
Save the Output
Another problem with the naïve command is that its 1.4 megabytes of output go to Terminal, which discards the output if Terminal becomes RAM-constrained, if I invoke the
clear command, or if I quit Terminal. This possible loss of output is unacceptable because the command may fail, in which case I need to examine the output for forensic analysis or to seek the assistance of the Swift cognoscenti.
build-script actually launches many sub-processes, and a failure in one of these may not even appear near the end of the output. If Terminal’s scroll buffer isn’t large enough to hold all the output, the failure can disappear into the æther.
utils/build-script \ # omitted for brevity > ~/Desktop/buildOutput.txt 2>&1
In this invocation, output goes to a file on my desktop. I like to store on my desktop files, including
build-script output files, that I intend to eventually delete so that their presence reminds me to delete them.
Play a Sound
build-script invocation takes so long, I don’t stare expectantly at Terminal while it executes. I do something else. A Wikipedia deep dive, for example. Did you know that a natural nuclear reactor spontaneously activated in what is now Gabon 1.7 billion years ago? One did. But I’m eager to continue development when
build-script finishes. Rather than periodically glance at Terminal, I listen for my MacBook’s fan. When it stops,
build-script is usually finished. But there is a more-reliable way to be informed when a long-running command finishes: have Terminal play a sound after completion of the command. Here is how to do that:
utils/build-script \ # omitted for brevity ; echo $'\a'
Although this approach to playing a sound after completion works for me, the reader should be aware of certain limitations described here.
Knowing how long an invocation like
build-script takes is useful. You can brag to friends that a clean build and test takes three hours. More importantly, certain optional arguments may or may not impact running time and, if an optional argument doesn’t affect running time, it’s a good candidate for omission from future invocations. Here is how to use Bash’s built-in
time command to time execution:
time utils/build-script \ # omitted for brevity
Here is the output:
real 172m51.465s user 1267m36.406s sys 28m0.306s
The first value is real-world elapsed time. Regarding
sys time, I lazily quote Wikipedia.
The total CPU time is the combination of the amount of time the CPU or CPUs spent performing some action for a program and the amount of time they spent performing system calls for the kernel on the program’s behalf. When a program loops through an array, it is accumulating user CPU time. Conversely, when a program executes a system call such as
fork, it is accumulating system CPU time.
The fact that
sys time is more than seven times longer than
real time implies that
build-script runs in a highly concurrent manner. 🙇♂️
time command in this example is built into Bash and is not a free-standing Unix utility. But
/usr/bin/time, a BSD utility, exists and produces differently formatted output. Here is the output from
/usr/bin/time ls run in the log-file folder for this website. Note the lack of concurrency implied by arithmetic.
7.85 real 5.29 user 1.27 sys
Here is the
build-script invocation with all of the improvements described above:
time utils/build-script \ --skip-build-benchmarks --skip-ios --skip-watchos --skip-tvos --swift-darwin-supported-archs "x86_64" \ --cmake-c-launcher="$(which sccache)" --cmake-cxx-launcher="$(which sccache)" \ --release-debuginfo \ --test \ --infer \ > ~/Desktop/buildOutput.txt 2>&1 \ ; echo $'\a'
I hope you find these four weird Unix tricks useful. Please let me know if you have any suggestions for further improving my