1#!/bin/bash
2#
3# Copyright 2020, Data61, CSIRO (ABN 41 687 119 230)
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8# Note: This script uses bash for its execution, but not because it uses any
9# Bashisms; instead, it is to work around a POSIX-violating bug in signal
10# handling in dash, which is the /bin/sh on most Debian-based systems.  See
11# <https://bugs.debian.org/779416>, filed in 2015 and still not fixed (posh has
12# the same problem; ksh, mksh, yash, and zsh do not).
13
14# Note: This is _not_ a legitimate parser for CMakeLists!  It is quite crude.
15
16# TODO: Rewrite this in Python!
17
18set -eu
19
20SOURCE_ROOT=${0%/*}
21PROGNAME=${0##*/}
22REPO_DIR=.repo
23CMAKE_COMPILER_DEFAULT=gcc
24CMAKE_COMPILER=$CMAKE_COMPILER_DEFAULT
25CMAKELISTS="$SOURCE_ROOT"/CMakeLists.txt
26CMAKECACHE=CMakeCache.txt
27CMAKETOOLCHAIN="$SOURCE_ROOT"/kernel/"$CMAKE_COMPILER".cmake
28DO_CMAKE_INITIALIZE=
29EASY_KNOBS="$SOURCE_ROOT"/easy-settings.cmake
30CMAKE_ARGS=
31# Set to a non-null string (like "yes") to enable debugging output.
32DEBUG_MATCHER=
33MODE=invoke
34
35# We use the following exit status conventions:
36#   0: normal operation, successful, "true"
37#   1: expected failure, "false"
38#   2: usage error
39#   3: other error
40EXIT_STATUS=3
41
42# Set up terminal capabilities (for displaying in bold and colors).
43#
44# See terminfo(5) for a list of terminal capability strings.
45#
46# tput returns an empty string (and exits with a nonzero status) for
47# unsupported string capabilities, and -1 for unsupported integer
48# capablilities.
49BOLD=$(tput bold) || BOLD=
50NORMAL=$(tput sgr0) || NORMAL=
51NCOLORS=$(tput colors)
52# If the terminal doesn't support color at all, these will remain null.
53RED=
54GREEN=
55YELLOW=
56CYAN=
57
58# We want different foreground color numbers if we have a terminal capable of
59# more than 8, because generally the contrast is bad if we use the low-numbered
60# colors (bold helps, but only so much).  On terminals truly capable of only 8
61# colors, we have to rely on the implementation to provide good contrast.
62if [ -n "$NCOLORS" ]
63then
64    if [ $NCOLORS -gt 8 ]
65    then
66        RED=$(tput setaf 9)
67        GREEN=$(tput setaf 10)
68        YELLOW=$(tput setaf 11)
69        CYAN=$(tput setaf 14)
70    # This is an exact equality match on purpose.  tput will report -1 for a
71    # truly monochrome terminal and in that case we don't want to mess with
72    # the setaf capability at all.
73    elif [ $NCOLORS -eq 8 ]
74    then
75        RED=$(tput setaf 1)
76        GREEN=$(tput setaf 2)
77        YELLOW=$(tput setaf 3)
78        CYAN=$(tput setaf 6)
79    fi
80fi
81
82# Emit diagnostic message.
83# @params: a set of strings comprising a human-intelligible message
84#
85# Display the diagnostic message itself in bold.
86_print () {
87    echo "${PROGNAME:-(unknown program)}: $BOLD$*$NORMAL"
88}
89
90# Emit debugging message to standard error.
91# @params: a set of strings comprising a human-intelligible message
92debug () {
93    _print "${CYAN}debug: $*" >&2
94}
95
96# Emit informational message to standard error.
97notice () {
98    _print "${GREEN}notice: $*" >&2
99}
100
101# Emit warning message to standard error.
102warn () {
103    _print "${YELLOW}warning: $*" >&2
104}
105
106# Emit error message to standard error.
107fail () {
108    _print "${RED}error: $*" >&2
109}
110
111# Report unrecoverable error and terminate script.
112# @params: a set of strings comprising a human-intelligible message
113#
114# Note: $EXIT_STATUS, if set in the invoking scope, determines the exit status
115# used by this function.
116die () {
117    _print "${RED}fatal error: $*" >&2
118    exit ${EXIT_STATUS:-3}
119}
120
121# [debugging] Report how the input line was classified.
122# @params: a string describing the classification
123describe () {
124    test -n "$DEBUG_MATCHER" && debug "$CMAKE_LINENO: $*" || :
125}
126
127# Back up the CMake cache file, re-run CMake, and see if the new cache file
128# differs from the backup.  If it does, the configuration is not stable and we
129# will warn about it (see end of script).
130#
131# Returns 0 (success) if the files are the same; 1 if they differ; other if
132# trouble.
133is_configuration_stable () {
134    CMAKECACHE_BACKUP=$CMAKECACHE.griddle.bak
135    if ! [ -e $CMAKECACHE ]
136    then
137        die "CMake cache file \"$CMAKECACHE\" unexpectedly does not exist!"
138    fi
139
140    cp $CMAKECACHE $CMAKECACHE_BACKUP
141    # $CMAKE_ARGS is unquoted because because cmake needs shell word-splitting
142    # to be done on its parameters.  Furthermore, there should be no
143    # configuration variables with whitespace embedded in their flag names or
144    # values.  (Well, certainly not the _names_...)
145    cmake $CMAKE_ARGS . || die "cmake failed"
146    cmp -s $CMAKECACHE $CMAKECACHE_BACKUP
147    # `return` with no arguments returns the exit status of the last "simple
148    # command" executed, so don't insert anything between `cmp` and `return`.
149    return
150}
151
152# Break up Set directive and save interesting parts.
153# @params: one or more strings comprising a line from a CMake input file
154unpack_set () {
155    # TODO: Handle a last parameter of "FORCE".
156    MYLINE=$*
157    # Chop off directive.
158    MYLINE=${MYLINE#set(}
159    # Chop off trailing parenthesis.
160    MYLINE=${MYLINE%)}
161    # By turning off globbing and leaving $MYLINE unquoted, we largely get the
162    # word-splitting we want.
163    set -o noglob
164    set -- $MYLINE
165    set +o noglob
166    CONFIG_VAR=$1
167    shift
168    DEFAULT_VALUE=$1
169    shift
170
171    if [ "$1" = "CACHE" ]
172    then
173        CACHED="(cached)"
174    else
175        CACHED="(not cached)"
176    fi
177
178    shift
179    TYPE=$1
180    shift
181    DESCRIPTION=$*
182    # Chop off leading and trailing double quotes.
183    DESCRIPTION=${DESCRIPTION#\"}
184    DESCRIPTION=${DESCRIPTION%\"}
185}
186
187# Set the value of the variable named in $1 to the maximum of $2 and its current
188# value.
189# @params: $1: a variable name; $2: the potential new value
190update_field_width () {
191    VAR=$1
192    # We use eval so we can get the value of the indirectly-referenced variable
193    # in VAR.  E.g., if $VAR is "CV_WIDTH", we set $OLD_WIDTH to the value of
194    # $CV_WIDTH below.
195    eval OLD_WIDTH=\$$VAR
196    shift
197    VALUE=$*
198    NEW_WIDTH=${#VALUE}
199
200    if [ $NEW_WIDTH -gt $OLD_WIDTH ]
201    then
202        # We use eval to assign to the variable named in $VAR.
203        eval $VAR=$NEW_WIDTH
204    fi
205}
206
207# Perform sanity checks on the environment.
208
209# Is a repo dir present in the PWD?
210if [ -d "$REPO_DIR" ]
211then
212    die "run this tool from a build directory (e.g., \"mkdir build; cd build\")"
213fi
214
215# Guard against rookie mistake of running tool in some non-build subdirectory of
216# the repo checkout.
217THIS_DIR=${PWD##*/}
218
219if [ "$THIS_DIR" = kernel ] || [ "$THIS_DIR" = projects ] \
220    || [ "$THIS_DIR" = tools ]
221then
222    die "run this tool from a build directory (e.g., \"mkdir ../build;" \
223        " cd ../build\")"
224fi
225
226# Is a repo dir present in the PWD?
227if ! [ -d "$SOURCE_ROOT"/"$REPO_DIR" ]
228then
229    # We are completely in the wilderness.
230    die "cannot find \"$REPO_DIR\" in this directory or its parent;" \
231        "${NORMAL}you need to (1) initialise a repo with \"repo init -u" \
232        "\$GIT_CLONE_URL\", (2) \"repo sync\", (3) create a build directory" \
233        "(e.g., \"mkdir build\"), (4) change into that directory (e.g." \
234        "\"cd build\"), and (5) try to run this tool again."
235fi
236
237# Is an easy config file available?
238if ! [ -r "$EASY_KNOBS" ]
239then
240    # At this point we know we're probably in a build directory and there is a
241    # CMake lists file, but not an easy settings file.
242    die "\"$EASY_KNOBS\" does not exist or is not readable;" \
243        "${NORMAL}this project may not yet support \"$PROGNAME\""
244fi
245
246CMAKE_LINENO=0
247# Set up some variables to compute pleasant field widths.
248CV_WIDTH=0 # $CONFIG_VAR field with
249TY_WIDTH=0 # $TYPE field width
250DV_WIDTH=0 # $DEFAULT_VALUE field width
251
252while read -r LINE
253do
254    CMAKE_LINENO=$((CMAKE_LINENO + 1))
255
256    # Remove syntactically unimportant leading and trailing white space.
257    LINE=$(echo "$LINE" | sed -e 's/^\s\+//' -e 's/\s\+$//')
258
259    case "$LINE" in
260    ('#'*)
261        describe "comment line"
262        ;;
263    ('')
264        describe "blank line"
265        ;;
266    (set'('*)
267        describe "configuration variable: \"$LINE\""
268        unpack_set "$LINE"
269        update_field_width CV_WIDTH "$CONFIG_VAR"
270        update_field_width TY_WIDTH "$TYPE"
271        update_field_width DV_WIDTH "$DEFAULT_VALUE"
272        # Save the configuration variable name as an acceptable long option
273        # for getopt.
274
275        # If the configuration variable is of boolean type, its parameter is
276        # optional; getopt indicates that with a trailing double colon
277        # instead of a single one.
278        if [ "$TYPE" = BOOL ]
279        then
280            GETOPT_FLAGS=${GETOPT_FLAGS:+$GETOPT_FLAGS,}$CONFIG_VAR::
281        else
282            GETOPT_FLAGS=${GETOPT_FLAGS:+$GETOPT_FLAGS,}$CONFIG_VAR:
283        fi
284
285        # Use eval to interpolate $CONFIG_VAR into a shell variable.  For
286        # instance, the following line might expand to:
287        #   VAR_SIMULATION_TYPE=BOOL
288        eval "VAR_${CONFIG_VAR}_TYPE"="$TYPE"
289
290        # Pack information about the configuration variable (except for
291        # caching information) into a string to be decoded by show_usage().
292        #
293        # The "records" are separated by "@@" and the "fields" by "@:".
294        OPTIONS=${OPTIONS:+$OPTIONS@@}$CONFIG_VAR@:$TYPE@:$DEFAULT_VALUE@:$DESCRIPTION
295        OPTION_REPORT="${OPTION_REPORT:=}
296$CONFIG_VAR is type: $TYPE, default: $DEFAULT_VALUE, $CACHED; $DESCRIPTION"
297        ;;
298    (mark_as_advanced'('*)
299        describe "exporting external setting: \"$LINE\""
300        ;;
301    (*)
302        die "$EASY_KNOBS:$CMAKE_LINENO: I don't know how to handle \"$LINE\""
303        ;;
304    esac
305done < "$EASY_KNOBS"
306
307# Now that we've parsed the CMakefile, we know what options we can accept.
308#
309# Append a record separator to the end of $OPTIONS for ease of processing later.
310OPTIONS=${OPTIONS:-}@@
311
312# List supported target platforms.
313#
314# This function relies on the current working directory being the build
315# directory, but this is true by the time it is called.
316show_platform_help () {
317    # This is uglier than it should be because CMake insists on its input being
318    # seekable.  So we have to set up a temporary file, ensure we write to it
319    # only by appending, and make sure it gets cleaned up by setting up a signal
320    # handler.  Note also that CMake's message() writes to standard error.
321    #
322    # We quote $TEMP when dereferencing it because mktemp uses $TMPDIR, and the
323    # user might have set that to a whitespace-containing pathname.
324    #
325    # We give `rm` the `-f` option in the trap handler in the event we end up
326    # racing against the ordinary cleanup scenario.  Consider:
327    #   # Clean up the temporary file and deregister the signal handler.
328    #   rm "$TEMP"
329    #   <CTRL-C>
330    #   trap - HUP INT QUIT TERM
331    # When the user interrupts the script, the temporary file has been removed
332    # but the signal handler has not yet been deregistered.
333    #
334    # This function can be greatly simplified once JIRA SELFOUR-2369 is fixed.
335    TEMP=$(mktemp)
336
337    # In our trap handler, we have to (1) do our cleanup work; (2) clear the
338    # trap handler (restoring the default signal handler); and (3) commit
339    # suicide so that the shell knows we exited abnormally.  Unfortunately POSIX
340    # shell offers no way of knowing which signal we are handling, short of
341    # writing the trap handler multiple times (once for each signal); we choose
342    # INT as our final disposition somewhat arbitrarily.
343    #
344    # See <https://www.cons.org/cracauer/sigint.html> for a detailed exploration
345    # of this issue.
346    trap 'rm -f "$TEMP"; trap - HUP INT QUIT TERM; kill -s INT $$' \
347        HUP INT QUIT TERM
348
349    cat >> "$TEMP" <<EOF
350include(configs/seL4Config.cmake)
351
352foreach(val IN LISTS kernel_platforms)
353    message("\${val}")
354endforeach()
355EOF
356
357    (cd ../kernel && cmake -DCMAKE_TOOLCHAIN_FILE=ignore -P "$TEMP" 2>&1 \
358        | cut -d';' -f1)
359    # Clean up the temporary file and deregister the signal handler.
360    rm "$TEMP"
361    trap - HUP INT QUIT TERM
362    notice "not all seL4 projects (e.g., \"camkes\", \"sel4bench\") support" \
363        "all platforms"
364}
365
366# Display a usage message.
367show_usage () {
368    # Make sure our field widths are wide enough for our column headings.
369    update_field_width CV_WIDTH "Option"
370    update_field_width TY_WIDTH "Type"
371    update_field_width DV_WIDTH "Default"
372    # Furthermore make sure the field width for the configuration flag name
373    # itself is wide enough to accommodate the two option dashes we will add.
374    CV_WIDTH=$(( CV_WIDTH + 2 ))
375
376    cat <<EOF
377$PROGNAME: easy cooking with CMake
378
379$PROGNAME eases the setup of seL4-related builds by exposing only the most
380commonly-used configuration variables in the seL4 CMake infrastructure.  These
381differ between projects, but you can always discover them with:
382    $PROGNAME --help
383
384Usage:
385    $PROGNAME [--compiler={gcc|llvm}] [CMAKE-CONFIGURATION-VARIABLE] ...
386    $PROGNAME --help
387    $PROGNAME --platform-help
388
389Options:
390    --compiler={gcc|llvm}   Report "gcc" or "llvm" compiler suite to CMake.
391                            (default: $CMAKE_COMPILER_DEFAULT)
392    --help                  Display this message and exit.
393    --platform-help         List supported target platforms and exit.
394EOF
395
396    if [ -z "$OPTIONS" ]
397    then
398        cat <<EOF
399
400The file "$EASY_KNOBS" defines no basic configuration options for this project.
401EOF
402        return
403    fi
404
405    if [ -n "${GETOPT_FLAGS:+flags}" ]
406    then
407        echo
408        FORMAT_STRING="%${CV_WIDTH}s  %${TY_WIDTH}s  %${DV_WIDTH}s  %s\n"
409        printf "$FORMAT_STRING" "Option" "Type" "Default" "Description"
410        echo
411
412        while [ -n "$OPTIONS" ]
413        do
414            # Unpack and display the information condensed into $OPTIONS.
415            #
416            # The "records" are separated by "@@" and the "fields" by "@:".
417            #
418            # Break off one option record at a time for clarity.
419            RECORD=${OPTIONS%%@@*}
420            OPTIONS=${OPTIONS#*@@}
421
422            # We now have one record in $RECORD.  Extract the fields.
423            CONFIG_VAR=${RECORD%%@:*}
424            RECORD=${RECORD#*@:}
425            TYPE=${RECORD%%@:*}
426            RECORD=${RECORD#*@:}
427            DEFAULT_VALUE=${RECORD%%@:*}
428            RECORD=${RECORD#*@:}
429            DESCRIPTION=$RECORD
430
431            printf "$FORMAT_STRING" \
432                "--$CONFIG_VAR" "$TYPE" "$DEFAULT_VALUE" "$DESCRIPTION"
433        done
434    fi
435}
436
437# Check the option given against those extracted from the CMake file.
438# @params: the option name to look up
439# @return: 0 (true) if option recognized; 1 (false) otherwise
440validate_name () {
441    FLAG=$1
442
443    if echo "$GETOPT_FLAGS" | egrep -q "(^|.+:)?$FLAG(:.+|$)?"
444    then
445        return 0
446    else
447        return 1
448    fi
449}
450
451# Check the option parameter given against the declared type.
452# @params: an option name and its parameter
453# @return: 0 (true) if parameter type-checks; 1 (false) otherwise
454#
455# When returning 1, be certain to issue a `fail` diagnostic.
456validate_parameter () {
457    FLAG=$1
458    VALUE=$2
459
460    # Use eval to interpolate $FLAG into a shell variable which should have been
461    # defined in the big case statement above (when the CMake file was parsed).
462    #
463    # Calling validate_name() before this function should prevent any attempt at
464    # expanding an undefined variable name.
465    eval TYPE=\$VAR_${FLAG}_TYPE
466
467    case "$TYPE" in
468    (BOOL)
469        case "$VALUE" in
470        (ON|OFF)
471            ;;
472        (*)
473            fail "\"$FLAG\" only supports values of \"ON\" or \"OFF\""
474            return 1
475            ;;
476        esac
477        ;;
478    (STRING)
479        # No validation at present.
480        ;;
481    (*)
482        # This is a fatal error because it indicates a limitation of the script,
483        # not invalid user input.
484        die "unsupported configuration variable type \"$TYPE\" (\"$FLAG\")"
485        ;;
486    esac
487
488    return 0
489}
490
491getopt -T || GETOPT_STATUS=$?
492
493if [ $GETOPT_STATUS -ne 4 ]
494then
495    die "getopt from util-linux required"
496fi
497
498if ! ARGS=$(getopt -o '' \
499    --long "${GETOPT_FLAGS:+$GETOPT_FLAGS,}"compiler:,help,platform-help \
500    --name "$PROGNAME" -- "$@")
501then
502    show_usage >&2
503    exit 2
504fi
505
506eval set -- "$ARGS"
507unset ARGS
508
509HAD_ARGUMENT_PROBLEMS=
510
511while [ -n "${1:-}" ]
512do
513    case "$1" in
514    (--compiler)
515        if [ "$2" = gcc ] || [ "$2" = llvm ]
516        then
517            CMAKE_COMPILER="$2"
518            # We may be changing compilers; re-init CMake.
519            DO_CMAKE_INITIALIZE=yes
520        else
521            die "unrecognized compiler \"$2\"; expected \"gcc\" or \"llvm\""
522        fi
523
524        break
525        ;;
526    (--help)
527        MODE=help
528        shift
529        ;;
530    (--platform-help)
531        MODE=platform-help
532        shift
533        ;;
534    (--)
535        shift
536        break
537        ;;
538    (--*)
539        # Strip off the argument's leading dashes.
540        OPT=${1#--}
541        # Reset variables set by previous iterations.
542        FLAG=
543        VALUE=
544
545        if validate_name "$OPT"
546        then
547            MODE=invoke
548        else
549            # getopt should have caught this, but just in case...
550            fail "unrecognized configuration option \"$1\""
551            HAD_ARGUMENT_PROBLEMS=yes
552        fi
553
554        VALUE=$2
555
556        # Handle the option argument.
557        case "${VALUE:-}" in
558            # GNU getopt synthesises a single space as an option argument if one
559            # was not specified.
560            (" ")
561                fail "configuration option \"$FLAG\" must be given a value"
562                HAD_ARGUMENT_PROBLEMS=yes
563                ;;
564            (*)
565                if validate_parameter "$FLAG" "$VALUE"
566                then
567                    CMAKE_ARGS=$CMAKE_ARGS" -D$FLAG=$VALUE"
568                else
569                    # validate_parameter() should have issued an error message.
570                    HAD_ARGUMENT_PROBLEMS=yes
571                fi
572                ;;
573        esac
574
575        # Dispose of the option and option-argument pair.
576        shift 2
577
578        # XXX: temporary hack until SELFOUR-1648 is resolved -- GBR
579        if [ "$FLAG" = "PLATFORM" ]
580        then
581            case "$VALUE" in
582                (sabre)
583                    EXTRA_ARGS="-DAARCH32=1"
584                    ;;
585                (tk1)
586                    EXTRA_ARGS="-DAARCH32HF=1"
587                    ;;
588                (tx[12])
589                    EXTRA_ARGS="-DAARCH64=1"
590                    ;;
591            esac
592        fi
593        # XXX: end of hack -- GBR
594        ;;
595    (*)
596        die "internal error while processing options"
597        ;;
598    esac
599done
600
601if [ -n "$HAD_ARGUMENT_PROBLEMS" ]
602then
603    notice "try \"$PROGNAME --help\" for option usage"
604    exit 2
605fi
606
607if ! [ -e $CMAKECACHE ]
608then
609    # If the CMake cache file does not exist, call CMake with initialization
610    # flags.
611    DO_CMAKE_INITIALIZE=yes
612fi
613
614if [ $MODE = help ]
615then
616    show_usage
617elif [ $MODE = platform-help ]
618then
619    show_platform_help
620elif [ $MODE = invoke ]
621then
622    if [ -n "$DO_CMAKE_INITIALIZE" ]
623    then
624
625        if [ -e "$CMAKELISTS" ]
626        then
627            # Some of these variables are unquoted because because cmake needs shell
628            # word-splitting to be done on its parameters.  Furthermore, there
629            # should be no configuration variables with whitespace embedded in their
630            # flag names or values.  (Well, certainly not the _names_...)
631            # $SOURCE_ROOT, however, could be anywhere in the user's file system and
632            # its value may have embedded whitespace.
633            cmake \
634                -DCMAKE_TOOLCHAIN_FILE="$CMAKETOOLCHAIN" \
635                -G Ninja \
636                ${EXTRA_ARGS:-} \
637                $CMAKE_ARGS \
638                -C "$SOURCE_ROOT/settings.cmake" \
639                "$SOURCE_ROOT"
640        elif [ -e "$EASY_KNOBS" ]
641        then
642            # If we don't have a CMakeLists.txt in the top level project directory then
643            # assume we use the project's directory tied to easy-settings.cmake and resolve
644            # that to use as the CMake source directory.
645            REAL_EASY_KNOBS=$(realpath "$EASY_KNOBS")
646            PROJECT_DIR=${REAL_EASY_KNOBS%/*}
647            # Initialize CMake.
648            cmake -G Ninja ${EXTRA_ARGS:-} \
649                    $CMAKE_ARGS \
650                    -C "$PROJECT_DIR/settings.cmake" "$PROJECT_DIR"
651        else
652            # This case shouldn't be hit as if $SOURCE_ROOT/easy-settings.cmake doesn't
653            # exist then the script should have failed earlier.
654            die "impossible: \"$CMAKELISTS\" does not exist and \"$EASY_KNOBS\" does not either"
655        fi
656    fi
657
658    # If this tool is re-run over an existing build with new parameters, the
659    # CMake cache file may mutate.  Warn about it if it does, and re-run CMake
660    # until it stabilizes.
661    THREW_STABILITY_DIAGNOSTIC=
662
663    while ! is_configuration_stable
664    do
665        warn "configuration not stable; regenerating"
666        THREW_STABILITY_DIAGNOSTIC=yes
667    done
668
669    if [ -n "$THREW_STABILITY_DIAGNOSTIC" ]
670    then
671        notice "configuration is stable"
672    fi
673
674    rm $CMAKECACHE_BACKUP
675else
676    die "internal error; unrecognized operation mode \"$MODE\""
677fi
678