Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tools/devtool
1955 views
#!/usr/bin/env bash

# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

# Firecracker devtool
#
# Use this script to build and test Firecracker.
#
# TL;DR
# Make sure you have Docker installed and properly configured
# (http://docker.com). Then,
#   building: `./devtool build`
#     Then find the binaries under build/debug/
#   testing: `./devtool test`
#     Will run the entire test battery; will take several minutes to complete.
#   deep-dive: `./devtool shell`
#     Open a shell prompt inside the container. Then build or test (or do
#     anything, really) manually.
#
# Still TL;DR: have Docker; ./devtool build; ./devtool test; ./devtool help.
#
#
# Both building and testing are done inside a Docker container. Please make sure
# you have Docker up and running on your system (see http:/docker.com) and your
# user has permission to run Docker containers.
#
# The Firecracker sources dir will be bind-mounted inside the development
# container (under /firecracker) and any files generated by the build process
# will show up under the build/ dir.  This includes the final binaries, as well
# as any intermediate or cache files.
#
# By default, all devtool commands run the container transparently, removing
# it after the command completes. Any persisting files will be stored under
# build/.
# If, for any reason, you want to access the container directly, please use
# `devtool shell`. This will perform the initial setup (bind-mounting the
# sources dir, setting privileges) and will then drop into a BASH shell inside
# the container.
#
# Building:
#   Run `./devtool build`.
#   By default, the debug binaries are built and placed under build/debug/.
#   To build the release version, run `./devtool build --release` instead.
#   You can then find the binaries under build/release/.
#
# Testing:
#   Run `./devtool test`.
#   This will run the entire integration test battery. The testing system is
#   based on pytest (http://pytest.org).
#
# Opening a shell prompt inside the development container:
#   Run `./devtool shell`.
#
# Additional information:
#   Run `./devtool help`.
#
#
# TODO:
#   - Cache test binaries, preserving them across `./devtool test` invocations.
#     At the moment, Firecracker is rebuilt everytime a test is run.
#   - List tests by parsing the `pytest --collect-only` output.
#   - Implement test filtering with `./devtool test --filter <filter>`
#   - Find an easier way to run individual tests on existing binaries.
#   - Add a `./devtool run` command to set up and run Firecracker.
#   - Add a `./devtool diag` command to help with troubleshooting, by checking
#     the most common failure conditions.
#   - Look into caching the Cargo registry within the container and if that
#     would help with reproducible builds (in addition to pinning Cargo.lock)

# Development container image (without tag)
DEVCTR_IMAGE_NO_TAG="public.ecr.aws/firecracker/fcuvm"

# Development container tag
DEVCTR_IMAGE_TAG="v30"

# Development container image (name:tag)
# This should be updated whenever we upgrade the development container.
# (Yet another step on our way to reproducible builds.)
DEVCTR_IMAGE="${DEVCTR_IMAGE_NO_TAG}:${DEVCTR_IMAGE_TAG}"

# Naming things is hard
MY_NAME="Firecracker $(basename "$0")"

# Full path to the Firecracker tools dir on the host.
FC_TOOLS_DIR=$(cd "$(dirname "$0")" && pwd)

# Full path to the Firecracker sources dir on the host.
FC_ROOT_DIR=$(cd "${FC_TOOLS_DIR}/.." && pwd)

# Full path to the build dir on the host.
FC_BUILD_DIR="${FC_ROOT_DIR}/build"

# Full path to devctr dir on the host.
FC_DEVCTR_DIR="${FC_ROOT_DIR}/tools/devctr"

# Path to the linux kernel directory on the host.
KERNEL_DIR="${FC_ROOT_DIR}/.kernel"

# Full path to the cargo registry dir on the host. This appears on the host
# because we want to persist the cargo registry across container invocations.
# Otherwise, any rust crates from crates.io would be downloaded again each time
# we build or test.
CARGO_REGISTRY_DIR="${FC_BUILD_DIR}/cargo_registry"

# Full path to the cargo git registry on the host. This serves the same purpose
# as CARGO_REGISTRY_DIR, for crates downloaded from GitHub repos instead of
# crates.io.
CARGO_GIT_REGISTRY_DIR="${FC_BUILD_DIR}/cargo_git_registry"

# Full path to the cargo target dir on the host.
CARGO_TARGET_DIR="${FC_BUILD_DIR}/cargo_target"

# Full path to the seccompiler cargo target dir on the host.
CARGO_SECCOMPILER_TARGET_DIR="${FC_BUILD_DIR}/seccompiler"

# Full path to the Firecracker sources dir, as bind-mounted in the container.
CTR_FC_ROOT_DIR="/firecracker"

# Full path to the build dir, as bind-mounted in the container.
CTR_FC_BUILD_DIR="${CTR_FC_ROOT_DIR}/build"

# Full path to the cargo target dir, as bind-mounted in the container.
CTR_CARGO_TARGET_DIR="$CTR_FC_BUILD_DIR/cargo_target"

# Full path to the seccompiler cargo target dir, as bind-mounted in the container.
CTR_CARGO_SECCOMPILER_TARGET_DIR="$CTR_FC_BUILD_DIR/seccompiler"

# Full path to the microVM images cache dir
CTR_MICROVM_IMAGES_DIR="$CTR_FC_BUILD_DIR/img"

# Full path to the public key mapping on the guest
PUB_KEY_PATH=/root/.ssh/id_rsa.pub

# Full path to the private key mapping on the guest
PRIV_KEY_PATH=/root/.ssh/id_rsa

# Path to the linux kernel directory, as bind-mounted in the container.
CTR_KERNEL_DIR="${CTR_FC_ROOT_DIR}/.kernel"

# Global options received by $0
# These options are not command-specific, so we store them as global vars
OPT_UNATTENDED=false

# Get the target prefix to avoid repeated calls to uname -m
TARGET_PREFIX="$(uname -m)-unknown-linux-"

DEFAULT_TEST_SESSION_ROOT_PATH=/srv
DEFAULT_RAMDISK_PATH=/mnt/devtool-ramdisk


# Send a decorated message to stdout, followed by a new line
#
say() {
    [ -t 1 ] && [ -n "$TERM" ] \
        && echo "$(tput setaf 2)[$MY_NAME]$(tput sgr0) $*" \
        || echo "[$MY_NAME] $*"
}

# Send a decorated message to stdout, without a trailing new line
#
say_noln() {
    [ -t 1 ] && [ -n "$TERM" ] \
        && echo -n "$(tput setaf 2)[$MY_NAME]$(tput sgr0) $*" \
        || echo "[$MY_NAME] $*"
}

# Send a text message to stderr
#
say_err() {
    [ -t 2 ] && [ -n "$TERM" ] \
        && echo -e "$(tput setaf 1)[$MY_NAME] $*$(tput sgr0)" 1>&2 \
        || echo -e "[$MY_NAME] $*" 1>&2
}

# Send a warning-highlighted text to stdout
say_warn() {
    [ -t 1 ] && [ -n "$TERM" ] \
        && echo "$(tput setaf 3)[$MY_NAME] $*$(tput sgr0)" \
        || echo "[$MY_NAME] $*"
}

# Exit with an error message and (optional) code
# Usage: die [-c <error code>] <error message>
#
die() {
    code=1
    [[ "$1" = "-c" ]] && {
        code="$2"
        shift 2
    }
    say_err "$@"
    exit $code
}

# Exit with an error message if the last exit code is not 0
#
ok_or_die() {
    code=$?
    [[ $code -eq 0 ]] || die -c $code "$@"
}

# Check if Docker is available and exit if it's not.
# Upon returning from this call, the caller can be certain Docker is available.
#
ensure_docker() {
    NEWLINE=$'\n'
    output=$(which docker 2>&1)
    ok_or_die "Docker not found. Aborting." \
        "Please make sure you have Docker (http://docker.com) installed" \
        "and properly configured.${NEWLINE}" \
        "Error: $?, command output: ${output}"

    output=$(docker ps 2>&1)
    ok_or_die "Error accessing Docker. Please make sure the Docker daemon" \
        "is running and that you are part of the docker group.${NEWLINE}" \
        "Error: $?, command output: ${output}${NEWLINE}" \
        "For more information, see" \
        "https://docs.docker.com/install/linux/linux-postinstall/"
}

# Run a command and retry multiple times if it fails. Once it stops
# failing return to normal execution. If there are "retry count"
# failures, set the last error code.
# $1 - command
# $2 - retry count
# $3 - sleep interval between retries
retry_cmd() {
    command=$1
    retry_cnt=$2
    sleep_int=$3

    {
        $command
    } || {
        # Command failed, substract one from retry_cnt
        retry_cnt=$((retry_cnt - 1))

        # If retry_cnt is larger than 0, sleep and call again
        if [ "$retry_cnt" -gt 0 ]; then
            echo "$command failed, retrying..."
            sleep "$sleep_int"
            retry_cmd "$command" "$retry_cnt" "$sleep_int"
        fi
    }
}

# Attempt to download our Docker image. Exit if that fails.
# Upon returning from this call, the caller can be certain our Docker image is
# available on this system.
#
ensure_devctr() {

    # We depend on having Docker present.
    ensure_docker

    # Check if we have the container image available locally. Attempt to
    # download it, if we don't.
    [[ $(docker images -q "$DEVCTR_IMAGE" | wc -l) -gt 0 ]] || {
        say "About to pull docker image $DEVCTR_IMAGE"
        get_user_confirmation || die "Aborted."

        # Run docker pull 5 times in case it fails - sleep 3 seconds
        # between attempts
        retry_cmd "docker pull $DEVCTR_IMAGE" 5 3

        ok_or_die "Error pulling docker image. Aborting."
    }
}

# Check if /dev/kvm exists. Exit if it doesn't.
# Upon returning from this call, the caller can be certain /dev/kvm is
# available.
#
ensure_kvm() {
    [[ -c /dev/kvm ]] || die "/dev/kvm not found. Aborting."
}

# Make sure the build/ dirs are available. Exit if we can't create them.
# Upon returning from this call, the caller can be certain the build/ dirs exist.
#
ensure_build_dir() {
    for dir in "$FC_BUILD_DIR" "$CARGO_TARGET_DIR" \
               "$CARGO_REGISTRY_DIR" "$CARGO_GIT_REGISTRY_DIR"; do
        mkdir -p "$dir" || die "Error: cannot create dir $dir"
        [ -x "$dir" ] && [ -w "$dir" ] || \
            {
                say "Wrong permissions for $dir. Attempting to fix them ..."
                chmod +x+w "$dir"
            } || \
            die "Error: wrong permissions for $dir. Should be +x+w"
    done
}

# Makes sure that Firecracker release binaries were built.
# This is relevant in the context of stripping the already built release binaries.
ensure_release_binaries_exist() {
    target=$1
    profile=$2
    firecracker_bin_path="$CARGO_TARGET_DIR/$target/$profile/firecracker"
    jailer_bin_path="$CARGO_TARGET_DIR/$target/$profile/jailer"
    seccompiler_bin_path="$CARGO_SECCOMPILER_TARGET_DIR/$target/$profile/seccompiler-bin"

    # Both binaries must exist for the stripping to be successful.
    [ -f "$firecracker_bin_path" ] && [ -f "$jailer_bin_path" ] && [ -f "$seccompiler_bin_path" ] || \
    die "Missing release binaries. Needed files:\n" \
    "* $firecracker_bin_path\n" \
    "* $jailer_bin_path\n" \
    "* $seccompiler_bin_path\n" \
    "To build the binaries, run:\n\t$0 build --$profile"
}

# Fix build/ dir permissions after a privileged container run.
# Since the privileged container runs as root, any files it creates will be
# owned by root. This fixes that by recursively changing the ownership of build/
# to the current user.
#
cmd_fix_perms() {
    # Yes, running Docker to get elevated privileges, just to chown some files
    # is a dirty hack.
    run_devctr \
        -- \
        chown -R "$(id -u):$(id -g)" "$CTR_FC_BUILD_DIR"
}

# Builds the development container from its Dockerfile.
#
cmd_build_devctr() {
    arch=$(uname -m)
    docker_file_name="Dockerfile.$arch"
    should_update_python_packages=true
    build_args=""

    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")      { cmd_help; exit 1; } ;;
            "-n"|"--no-python-package-update")
                shift
                should_update_python_packages=false
                build_args="--build-arg POETRY_LOCK_PATH=tools/devctr/poetry.lock"
                ;;
            "--")               { shift; break;     } ;;
            *)
                die "Unknown argument: $1. Please use --help for help."
            ;;
        esac
        shift
    done

    docker build -t "$DEVCTR_IMAGE_NO_TAG" -f "$FC_DEVCTR_DIR/$docker_file_name" $build_args .

    if [ "$should_update_python_packages" = true ]; then
        update_python_package_version
    fi
}

# Prompt the user for confirmation before proceeding.
# Args:
#   $1  prompt text.
#       Default: Continue? (y/n)
#   $2  confirmation input.
#       Default: y
# Return:
#   exit code 0 for successful confirmation
#   exit code != 0 if the user declined
#
get_user_confirmation() {

    # Pass if running unattended
    [[ "$OPT_UNATTENDED" = true ]] && return 0

    # Fail if STDIN is not a terminal (there's no user to confirm anything)
    [[ -t 0 ]] || return 1

    # Otherwise, ask the user
    #
    msg=$([ -n "$1" ] && echo -n "$1" || echo -n "Continue? (y/n) ")
    yes=$([ -n "$2" ] && echo -n "$2" || echo -n "y")
    say_noln "$msg"
    read c && [ "$c" = "$yes" ] && return 0
    return 1
}

# Validate the user supplied version number.
# It must be composed of 3 groups of integers separated by dot.
#
validate_version() {
    declare version_regex="^([0-9]+.){2}[0-9]+$"
    version="$1"

    if [ -z "$version" ]; then
        die "Version cannot be empty."
    elif [[ ! "$version" =~ $version_regex ]]; then
        die "Invalid version number: $version (expected: \$Major.\$Minor.\$Build)."
    fi

}

# Validate the user supplied kernel version number.
# It must be composed of 2 groups of integers separated by dot, with an optional third group.
validate_kernel_version() {
    local version_regex="^([0-9]+.)[0-9]+(.[0-9]+)?$"
    version="$1"

    if [ -z "$version" ]; then
        die "Version cannot be empty."
    elif [[ ! "$version" =~ $version_regex ]]; then
        die "Invalid version number: $version (expected: \$Major.\$Minor.\$Patch(optional))."
    fi

}

# Compose the text for a new release tag using the information in the changelog,
# between the two specified releases.
# The following transformations are applied:
# * `-` is replaced with `*` for unnumbered lists.
# * section headers (`###`) are removed.
#
# Args:
#   $1  previous version.
#   $2  new version.
#
compose_tag_text() {
    declare prev_ver="$1"
    declare curr_ver="$2"
    declare changelog="$FC_ROOT_DIR/CHANGELOG.md"

    validate_version "$prev_ver"
    validate_version "$curr_ver"

    # Patterns for the sections in the changelog corresponding to the versions.
    pat_prev="^##\s\[$prev_ver\]"
    pat_curr="^##\s\[$curr_ver\]"

    # Extract the section enclosed between the 2 headers and strip off the first
    # 2 and last 2 lines (one is blank and one contains the header `## [A.B.C]`).
    # Then, replace `-` with `*` and remove section headers.
    sed "/$pat_curr/,/$pat_prev/!d" "$changelog" \
      | sed '1,2d;$d' \
      | sed "s/^-/*/g" \
      | sed "s/^###\s//g"
}

# Helper function to run the dev container.
# Usage: run_devctr <docker args> -- <container args>
# Example: run_devctr --privileged -- bash -c "echo 'hello world'"
run_devctr() {
    docker_args=()
    ctr_args=()
    docker_args_done=false
    while [[ $# -gt 0 ]]; do
        [[ "$1" = "--" ]] && {
            docker_args_done=true
            shift
            continue
        }
        [[ $docker_args_done = true ]] && ctr_args+=("$1") || docker_args+=("$1")
        shift
    done

    # If we're running in a terminal, pass the terminal to Docker and run
    # the container interactively
    [[ -t 0 ]] && docker_args+=("-i")
    [[ -t 1 ]] && docker_args+=("-t")

    # Try to pass these environments from host into container for network proxies
    proxies=(http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY)
    for i in "${proxies[@]}"; do
        if [[ ! -z ${!i} ]]; then
            docker_args+=("--env") && docker_args+=("$i=${!i}")
        fi
    done

    # Finally, run the dev container
    # Use 'z' on the --volume parameter for docker to automatically relabel the
    # content and allow sharing between containers.
    docker run "${docker_args[@]}" \
        --rm \
        --volume /dev:/dev \
        --volume "$FC_ROOT_DIR:$CTR_FC_ROOT_DIR:z" \
        --env OPT_LOCAL_IMAGES_PATH="$(dirname "$CTR_MICROVM_IMAGES_DIR")" \
        --env PYTHONDONTWRITEBYTECODE=1 \
        "$DEVCTR_IMAGE" "${ctr_args[@]}"
}

# Helper function to test that the argument provided is a valid path to a SSH key.
#
test_key() {
    ssh-keygen -lf "$1" &>/dev/null
    ret=$?
    [ $ret -ne 0 ] && die "$1 is not a valid key file."
}

# Tries to update any outdated python packages on the container,
# locks the new versions and copies back the ```poetry.lock```
# file to the host.
#
update_python_package_version() {
    say "Updating python packages..."

    # defined in Dockerfile
    poetry_dir_on_container="/tmp/poetry"
    lock_file_location_on_host="$FC_DEVCTR_DIR/poetry.lock"
    image_id=$(docker images -q "$DEVCTR_IMAGE_NO_TAG" | head -n 1)
    dummy_container_name=$(uuidgen)
    dummy_container_id=$(docker create -ti --name \
        "$dummy_container_name" \
        "$image_id" \
        bash)

    docker start "$dummy_container_id"
    cmd="cd "$poetry_dir_on_container"; poetry update"
    docker exec -ti "$dummy_container_id" /bin/bash -c "${cmd}"
    docker cp \
        "$dummy_container_id":/tmp/poetry/poetry.lock \
        "$lock_file_location_on_host"
    docker commit "$dummy_container_id" "$DEVCTR_IMAGE_NO_TAG"

    docker stop "$dummy_container_id"
    docker rm -f "$dummy_container_name"
}

# `$0 help`
# Show the detailed devtool usage information.
#
cmd_help() {
    echo ""
    echo "Firecracker $(basename $0)"
    echo "Usage: $(basename $0) [<args>] <command> [<command args>]"
    echo ""
    echo "Global arguments"
    echo "    -y, --unattended         Run unattended. Assume the user would always"
    echo "                             answer \"yes\" to any confirmation prompt."
    echo ""
    echo "Available commands:"
    echo ""
    echo "    build [--debug|--release] [-l|--libc musl|gnu] [-- [<cargo args>]]"
    echo "        Build the Firecracker binaries."
    echo "        Firecracker is built using the Rust build system (cargo). All arguments after --"
    echo "        will be passed through to cargo."
    echo "        --debug               Build the debug binaries. This is the default."
    echo "        --release             Build the release binaries."
    echo "        -l, --libc musl|gnu   Choose the libc flavor against which Firecracker will"
    echo "                              be linked. Default is musl."
    echo "        --ssh-keys            Provide the paths to the public and private SSH keys on the host"
    echo "                              (in this particular order) required for the git authentication."
    echo "                              It is mandatory that both keys are specified."
    echo ""
    echo "    build_devctr [--no-python-package-update]"
    echo "        Builds the development container from its Dockerfile."
    echo "        -n, --no-python-package-update  Do not update python packages."
    echo ""
    echo "    checkenv"
    echo "        Performs prerequisites checks needed to execute firecracker."
    echo ""
    echo "    ci"
    echo "        Run a continuous integration test run that executes the integration tests and"
    echo "        checks that the release process works."
    echo ""
    echo "    distclean"
    echo "        Clean up the build tree and remove the docker container."
    echo ""
    echo "    fix_perms"
    echo "        Fixes permissions when devtool dies in the middle of a privileged session."
    echo ""
    echo "    fmt"
    echo "        Auto-format all Rust source files, to match the Firecracker requirements."
    echo "        This should be used as the last step in every commit, to ensure that the"
    echo "        Rust style tests pass."
    echo ""
    echo "    generate_syscall_tables <version>"
    echo "        Generates the syscall tables for seccompiler, according to a given kernel version."
    echo "        Release candidate (rc) linux versions are not allowed."
    echo "        Outputs a rust file for each supported arch: src/seccompiler/src/syscall_table/{arch}.rs"
    echo "        Supported architectures: x86_64 and aarch64."
    echo ""
    echo "    install [-p|--path] [--debug|--release]"
    echo "      Install firecracker, jailer and seccomp binaries to /usr/local/bin or a given path."
    echo "      Only the musl linked binaries are supported."
    echo "        --path                Install binaries to a specified path."
    echo "        --debug               Install the debug binaries."
    echo "        --release             Install the release binaries. This is the default."
    echo ""
    echo "    help"
    echo "        Display this help message."
    echo ""
    echo "    prepare_release <version>"
    echo "        Prepare a new Firecracker release by updating the version number, crate "
    echo "        dependencies and credits."
    echo ""
    echo "    shell [--privileged]"
    echo "        Launch the development container and open an interactive BASH shell."
    echo "        -p, --privileged    Run the container as root, in privileged mode."
    echo "                            Running Firecracker via the jailer requires elevated"
    echo "                            privileges, though the build phase does not."
    echo ""
    echo "    tag <version>"
    echo "        Create a git tag for the specified version. The tag message will contain "
    echo "        the contents of CHANGELOG.md enclosed between the header corresponding to "
    echo "        the specified version and the one corresponding to the previous version."
    echo ""
    echo "    test [-- [<pytest args>]]"
    echo "        Run the Firecracker integration tests."
    echo "        The Firecracker testing system is based on pytest. All arguments after --"
    echo "        will be passed through to pytest."
    echo "        -c, --cpuset-cpus cpulist    Set a dedicated cpulist to be used by the tests."
    echo "        -m, --cpuset-mems memlist    Set a dedicated memlist to be used by the tests."
    echo "        -r, --ramdisk size[k|m|g]    Use a ramdisk of `size` MB for
                                               the entire test session (e.g
                                               stored artifacts, Firecracker
                                               binaries, logs/metrics FIFOs
                                               and test created device files)."
    echo ""
    echo "    strip"
    echo "        Strip debug symbols from the Firecracker release binaries."
    echo ""
}

# `$0 build` - build Firecracker
# Please see `$0 help` for more information.
#
cmd_build() {

    # By default, we'll build the debug binaries.
    profile="debug"
    libc="musl"

    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")  { cmd_help; exit 1;     } ;;
            "--debug")      { profile="debug";      } ;;
            "--release")    { profile="release";    } ;;
            "--ssh-keys")
                shift
                [[ -z "$1" ]] && \
                    die "Please provide the path to the public SSH key."
                [[ ! -f "$1" ]]  && die "The public key file does not exist: $1."
                test_key "$1"
                host_pub_key_path="$1"
                shift
                [[ -z "$1" ]] && \
                    die "Please provide the path to the private SSH key."
                [[ ! -f "$1" ]]  && die "The private key file does not exist: $1."
                test_key "$1"
                host_priv_key_path="$1"
                ;;
            "-l"|"--libc")
                shift
                [[ "$1" =~ ^(musl|gnu)$ ]] || \
                    die "Invalid libc: $1. Valid options are \"musl\" and \"gnu\"."
                libc="$1"
                ;;
            "--")           { shift; break;         } ;;
            *)
                die "Unknown argument: $1. Please use --help for help."
            ;;
        esac
        shift
    done

    target="$TARGET_PREFIX${libc}"

    # Check prerequisites
    ensure_devctr
    ensure_build_dir

    say "Starting build ($profile, $libc) ..."

    # Cargo uses the debug profile by default. If we're building the release
    # binaries, we need to pass an extra argument to cargo.
    cargo_args=("$@")

    # Add the default target if we did not get that argument in the build command.
    add_default_target=true
    for flag in "${@}"; do
        if [[ "$flag" == "--" ]]; then
            break
        elif [[ "$flag" == "--target" || "$flag" =~ --target=.* ]]; then
            add_default_target=false
        fi
    done

    if [ "$add_default_target" = true ]; then
        cargo_args+=(--target "$target")
    fi

    [ $profile = "release" ] && cargo_args+=("--release")

    # Map the public and private keys to the guest if they are specified.
    [ ! -z "$host_pub_key_path" ] && [ ! -z "$host_priv_key_path" ] &&
        extra_args="--volume $host_pub_key_path:$PUB_KEY_PATH:z \
                    --volume $host_priv_key_path:$PRIV_KEY_PATH:z"

    # Artificially trigger a re-run of the build script,
    # to make sure that `firecracker --version` reports the latest changes.
    touch "$FC_ROOT_DIR/build.rs"

    # Run the cargo build process inside the container.
    # We don't need any special privileges for the build phase, so we run the
    # container as the current user/group.

    # Build seccompiler-bin.
    run_devctr \
        --user "$(id -u):$(id -g)" \
        --workdir "$CTR_FC_ROOT_DIR" \
        ${extra_args} \
        -- \
        cargo build -p seccompiler --bin seccompiler-bin \
            --target-dir "$CTR_CARGO_SECCOMPILER_TARGET_DIR" \
            "${cargo_args[@]}"
    ret=$?

    [ $ret -ne 0 ] && return $ret

    # Build Firecracker.
    run_devctr \
        --user "$(id -u):$(id -g)" \
        --workdir "$CTR_FC_ROOT_DIR" \
        ${extra_args} \
        -- \
        cargo build \
            --target-dir "$CTR_CARGO_TARGET_DIR" \
            "${cargo_args[@]}"
    ret=$?

    [ $ret -ne 0 ] && return $ret

    # Build jailer only in case of musl for compatibility reasons.
    if [ "$libc" == "musl" ];then
        run_devctr \
            --user "$(id -u):$(id -g)" \
            --workdir "$CTR_FC_ROOT_DIR" \
            ${extra_args} \
            -- \
            cargo build -p jailer \
                --target-dir "$CTR_CARGO_TARGET_DIR" \
                "${cargo_args[@]}"

    fi

    ret=$?

    # If `cargo build` was successful, output a message.
    [ $ret -eq 0 ] && {
        cargo_bin_dir="$CARGO_TARGET_DIR/$target/$profile"
        seccompiler_bin_dir="$CARGO_SECCOMPILER_TARGET_DIR/$target/$profile"

        # Seccompiler has a different build folder, we need to output two
        # messages.
        say "Build successful."
        say "Firecracker and Jailer binaries placed under $cargo_bin_dir"
        say "Seccompiler-bin binary placed under $seccompiler_bin_dir"
    }

    return $ret
}

cmd_distclean() {
    # List of folders to remove.
    dirs=("build" "test_results")

    for dir in "${dirs[@]}"; do
        if [ -d "$dir" ]; then
            say "Removing $dir"
            rm -rf "$dir"
        fi
    done

    # Remove devctr if it exists
    if [ $(docker images -q "$DEVCTR_IMAGE" | wc -l) -eq "1" ]; then
        say "Removing $DEVCTR_IMAGE"
        docker rmi -f "$DEVCTR_IMAGE"
    fi
}

cmd_strip() {
    profile="release"
    target="$TARGET_PREFIX""musl"

    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")  { cmd_help; exit 1;     } ;;
            *)
                die "Unknown argument: $1. Please use --help for help."
            ;;
        esac
        shift
    done

    # Check prerequisites
    ensure_devctr
    ensure_build_dir
    ensure_release_binaries_exist $target $profile

    say "Starting stripping the debug symbols for $profile binaries built against $target target."
    strip_flags="--strip-debug"
    say "Strip flags: $strip_flags."

    run_devctr \
      --user "$(id -u):$(id -g)" \
      -- \
      strip $strip_flags\
        "$CTR_CARGO_TARGET_DIR/$target/$profile/firecracker" \
        "$CTR_CARGO_TARGET_DIR/$target/$profile/jailer" \
        "$CTR_CARGO_SECCOMPILER_TARGET_DIR/$target/$profile/seccompiler-bin"
    ret=$?

    [ $ret -eq 0 ] && {
        say "Stripping was successful."
        say "Stripped Firecracker and Jailer binaries placed under $CARGO_TARGET_DIR/$target/$profile."
        say "Stripped seccompiler-bin binary placed under $CARGO_SECCOMPILER_TARGET_DIR/$target/$profile."
    }

    return $ret
}

mount_ramdisk() {
    say "Using ramdisk ..."
    local ramdisk_size="$1"
    umount_ramdisk
    mkdir -p ${DEFAULT_RAMDISK_PATH} && \
    mount -t tmpfs -o size=${ramdisk_size} tmpfs ${DEFAULT_RAMDISK_PATH}
    ok_or_die "Failed to mount ramdisk to ${DEFAULT_RAMDISK_PATH}. Check the permission."
    mkdir -p ${DEFAULT_RAMDISK_PATH}/srv
    mkdir -p  ${DEFAULT_RAMDISK_PATH}/tmp
}

umount_ramdisk() {
    if [ ! -e "${DEFAULT_RAMDISK_PATH}" ]; then
        return 0
    fi
    if [ ! -d "${DEFAULT_RAMDISK_PATH}" ]; then
        die "${DEFAULT_RAMDISK_PATH} is not a directory."
    fi
    if [ ! -w "${DEFAULT_RAMDISK_PATH}" ]; then
        die "Failed to unmount ${DEFAULT_RAMDISK_PATH}. Check the permission."
    fi
    umount ${DEFAULT_RAMDISK_PATH} &>/dev/null
    rmdir ${DEFAULT_RAMDISK_PATH} &>/dev/null
}

# `$0 test` - run integration tests
# Please see `$0 help` for more information.
#
cmd_test() {

    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")      { cmd_help; exit 1; } ;;
            "-c"|"--cpuset-cpus")
                shift
                local cpuset_cpus="$1"
                ;;
            "-m"|"--cpuset-mems")
                shift
                local cpuset_mems="$1"
                ;;
            "-r"|"--ramdisk")
                shift
                local ramdisk_size="$1"
                local ramdisk=true
                ;;
            "--")               { shift; break;     } ;;
            *)
                die "Unknown argument: $1. Please use --help for help."
            ;;
        esac
        shift
    done

    # Check prerequisites.
    ensure_kvm
    ensure_devctr
    ensure_build_dir

    # If we got to here, we've got all we need to continue.
    say "$(date -u +'%F %H:%M:%S %Z')"
    say "Starting test run ..."

    if [[ $ramdisk = true ]]; then
        mount_ramdisk ${ramdisk_size}
        ramdisk_args="--volume ${DEFAULT_RAMDISK_PATH}:${DEFAULT_TEST_SESSION_ROOT_PATH}"
    fi

    # Testing (running Firecracker via the jailer) needs root access,
    # in order to set-up the Firecracker jail (manipulating cgroups, net
    # namespaces, etc).
    # We need to run a privileged container to get that kind of access.
    run_devctr \
        --privileged \
        --security-opt seccomp=unconfined \
        --ulimit core=0 \
        --ulimit nofile=4096:4096 \
        --workdir "$CTR_FC_ROOT_DIR/tests" \
        --cpuset-cpus="$cpuset_cpus" \
        --cpuset-mems="$cpuset_mems" \
        ${ramdisk_args} \
        -- \
        pytest "$@"

    ret=$?


    if [[ $ramdisk = true ]]; then
        umount_ramdisk
    fi

    # Running as root would have created some root-owned files under the build
    # dir. Let's fix that.
    cmd_fix_perms

    return $ret
}

# `$0 shell` - drop to a shell prompt inside the dev container
# Please see `$0 help` for more information.
#
cmd_shell() {

    # By default, we run the container as the current user.
    privileged=false

    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")          { cmd_help; exit 1; } ;;
            "-p"|"--privileged")    { privileged=true;  } ;;
            "-r"|"--ramdisk")
                  shift
                  local ramdisk_size="$1"
                  local ramdisk=true
                  ;;
              "--")               { shift; break;     } ;;
            *)
                die "Unknown argument: $1. Please use --help for help."
            ;;
        esac
        shift
    done

    # Make sure we have what we need to continue.
    ensure_devctr
    ensure_build_dir

    if [[ $ramdisk = true ]]; then
        mount_ramdisk ${ramdisk_size}
        ramdisk_args="--volume ${DEFAULT_RAMDISK_PATH}:${DEFAULT_TEST_SESSION_ROOT_PATH}"
    fi

    if [[ $privileged = true ]]; then
        # If requested, spin up a privileged container.
        #
        say "Dropping to a privileged shell prompt ..."
        say "Note: $FC_ROOT_DIR is bind-mounted under $CTR_FC_ROOT_DIR"
        say_warn "You are running as root; any files that get created under" \
            "$CTR_FC_ROOT_DIR will be owned by root."
        run_devctr \
            --privileged \
            --ulimit nofile=4096:4096 \
            --security-opt seccomp=unconfined \
            --workdir "$CTR_FC_ROOT_DIR" \
            ${ramdisk_args} \
            -- \
            bash
        ret=$?

        # Running as root may have created some root-owned files under the build
        # dir. Let's fix that.
        #
        cmd_fix_perms
    else
        say "Dropping to shell prompt as user $(whoami) ..."
        say "Note: $FC_ROOT_DIR is bind-mounted under $CTR_FC_ROOT_DIR"
        say_warn "You won't be able to run Firecracker via the jailer," \
            "but you can still build it."
        say "You can use \`$0 shell --privileged\` to get a root shell."

        [ -w /dev/kvm ] || \
            say_warn "WARNING: user $(whoami) doesn't have permission to" \
                "access /dev/kvm. You won't be able to run Firecracker."

        run_devctr \
            --user "$(id -u):$(id -g)" \
            --ulimit nofile=4096:4096 \
            --device=/dev/kvm:/dev/kvm \
            --workdir "$CTR_FC_ROOT_DIR" \
            --env PS1="$(whoami)@\h:\w\$ " \
            -- \
            bash --norc
        ret=$?
    fi

    if [[ $ramdisk = true ]]; then
        umount_ramdisk
    fi

    return $ret
}


# Auto-format all source code, to match the Firecracker requirements. For the
# moment, this is just a wrapper over `cargo fmt --all`
# Example: `devtool fmt`
#
cmd_fmt() {

    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")      { cmd_help; exit 1; } ;;
            *)
                die "Unknown argument: $1. Please use --help for help."
            ;;
        esac
        shift
    done

    ensure_devctr

    say "Applying rustfmt ..."
    run_devctr \
        --user "$(id -u):$(id -g)" \
        --workdir "$CTR_FC_ROOT_DIR" \
        -- \
        cargo fmt --all
}


# Prepare a Firecracker release by updating the version, changelog
# and credits.
# Example: `devtool release 0.42.0`
#
cmd_prepare_release() {

    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")      { cmd_help; exit 1;    } ;;
            *)                  { version="$1"; break; } ;;
        esac
        shift
    done

    validate_version "$version"

    # We'll be needing the dev container later on.
    ensure_devctr

    # The cargo registry dir needs to be there for `cargo update`.
    ensure_build_dir

    # Get current version from the swagger spec.
    swagger="$FC_ROOT_DIR/src/api_server/swagger/firecracker.yaml"
    curr_ver=$(grep "version: " "$swagger" | awk -F : '{print $2}' | tr -d ' ')

    say "Updating from $curr_ver to $version ..."
    get_user_confirmation || die "Aborted."

    # Update version in files.
    files_to_change=("$swagger"                                 \
                     "$FC_ROOT_DIR/src/firecracker/Cargo.toml"  \
                     "$FC_ROOT_DIR/src/jailer/Cargo.toml"       \
                     "$FC_ROOT_DIR/src/seccompiler/Cargo.toml")
    say "Updating source files:"
    for file in "${files_to_change[@]}"; do
        say "- $file"
        # Dirty hack to make this work on both macOS/BSD and Linux.
        sed -i="" "s/$curr_ver/$version/g" "$file"
        rm -f "${file}="
    done

    # Run `cargo check` to update firecracker and jailer versions in
    # `Cargo.lock`.
    say "Updating lockfile..."
    run_devctr \
        --user "$(id -u):$(id -g)" \
        --workdir "$CTR_FC_ROOT_DIR" \
        -- \
        cargo check
    ok_or_die "cargo check failed."

    # Update credits.
    say "Updating credits..."
    "$FC_TOOLS_DIR/update-credits.sh"

    # Update changelog.
    say "Updating changelog..."
    sed -i="" "s/\[Unreleased\]/\[$version\]/g" "$FC_ROOT_DIR/CHANGELOG.md"
    rm -f "$FC_ROOT_DIR/CHANGELOG.md="
}

# Create a tag for the specified release.
# The tag text will be composed from the changelog contents enclosed between the
# specified release number and the previous one.
# Args:
#   $1  release number.
# Example: `devtool tag 0.42.0`
#
cmd_tag() {

    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")      { cmd_help; exit 1;    } ;;
            *)                  { version="$1"; break; } ;;
        esac
        shift
    done

    validate_version "$version"

    declare pat_release="^## \[([0-9]+\.){2}[0-9]+\]"
    declare changelog="$FC_ROOT_DIR/CHANGELOG.md"

    grep -q "\[$version\]" "$changelog"
    ok_or_die "No changelog entry for release $version can be found."

    # We work with the assumption that the changelog has already been updated
    # and contains a header (and a corresponding section) for the new release.

    # Step 1: Get all release numbers.
    all_releases=($(grep -E "$pat_release" "$changelog"))

    # Step 2: Trim out headers (`##`).
    all_releases=(${all_releases[@]//##*})

    # Step 3: Walk the array until we come across the desired release number,
    # then pick up the next one. Since the latest releases are at the top of the
    # changelog, the next one in line will be the previous one chronologically.
    # The array now contains all the release numbers in the changelog, enclosed
    # in square brackets.
    found=
    for release in "${all_releases[@]}"; do
        if [ ! -z "$found" ]; then
            # Trim out square brackets.
            prev_version=$(echo "$release" | awk -F"[][]" "{print \$2}")
            break
        elif [ "$release" == "[$version]" ]; then
            found=1
        fi
    done

    # Create tag.
    tag_text=$(compose_tag_text "$prev_version" "$version")
    say "Preparing to create tag..."
    say "Tag: v$version"
    say "Tag text:"
    echo "$tag_text"
    say "Continue with tag creation?"
    get_user_confirmation || die "Tag not created."

    git tag -a "v$version" -m "$tag_text"
    ok_or_die "Tag v$version not created."
    say "Tag v$version created."
}

# Check if able to run firecracker.
# ../docs/getting-started.md#prerequisites

ensure_kvm_rw () {
    [[ -c /dev/kvm && -w /dev/kvm && -r /dev/kvm ]] || \
        say_err "FAILED: user $(whoami) doesn't have permission to" \
                "access /dev/kvm."
}

check_kernver () {
    KERN_MAJOR=4
    KERN_MINOR=14
    (uname -r | awk -v MAJOR=$KERN_MAJOR -v MINOR=$KERN_MINOR '{ split($0,kver,".");
    if( (kver[1] + (kver[2] / 100) ) <  MAJOR + (MINOR/100) )
    {
      exit 1;
    } }') ||
    say_err "FAILED: Kernel version must be >= $KERN_MAJOR.$KERN_MINOR"
}

# Check Production Host Setup
# ../docs/prod-host-setup.md

check_SMT () {
    (grep -q "^forceoff$\|^notsupported$" \
      /sys/devices/system/cpu/smt/control) ||
    say_warn "WARNING: Hyperthreading ENABLED."
}

check_KPTI () {
    (grep -q "^Mitigation: PTI$" \
      /sys/devices/system/cpu/vulnerabilities/meltdown) || \
    say_warn "WARNING: KPTI NOT SUPPORTED"
}

check_KSM () {
    (grep -q "^0$" /sys/kernel/mm/ksm/run) || \
    say_warn "WARNING: KSM ENABLED"
}

check_IBPB_IBRS () {
    (grep -q "^Mitigation: Full generic retpoline, IBPB, IBRS_FW$"\
      /sys/devices/system/cpu/vulnerabilities/spectre_v2) || \
    say_warn "WARNING: retpoline, IBPB, IBRS: DISABLED."
}

check_L1TF () {
    declare -a CONDITIONS=("Mitigation: PTE Inversion" "VMX: cache flushes")
    for cond in "${CONDITIONS[@]}";
    do (grep -q "$cond" /sys/devices/system/cpu/vulnerabilities/l1tf) ||
       say_warn "WARNING: $cond: DISABLED";
    done
}

check_swap () {
    (grep -q "swap.img" /proc/swaps ) && \
    say_warn "WARNING: SWAP enabled"
}

check_SSBD () {
    arch=$(uname -m)
    if [ "$arch" = "aarch64" ]; then
        local param="ssbd=force-on"
    elif [ "$arch" = "x86_64" ]; then
        local param="spec_store_bypass_disable=on"
    fi

    ssbd_sysfs_file="/sys/devices/system/cpu/vulnerabilities/spec_store_bypass"

    if [ -f "$ssbd_sysfs_file" ]; then
        (grep -q "^Vulnerable" $ssbd_sysfs_file) && \
        say_warn "WARNING: SSBD mitigation is either globally disabled or"\
            "system does not support mitigation via prctl or seccomp. Try"\
            "enabling it system-wide, using the \`${param}\` boot parameter."
    else
        say_warn "WARNING: SSBD mitigation not supported on this kernel."\
            "View the prod-host-setup.md for more details."
    fi
}

check_vm() {
    if [ $(dmesg | grep -c -i "hypervisor detected") -gt 0 ]; then
        say_warn "WARNING: you are running in a virtual machine." \
    "Firecracker is not well tested under nested virtualization."
    fi
}

cmd_checkenv() {
    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")      { cmd_help; exit 1; } ;;
            *)
                die "Unknown argument: $1. Please use --help for help."
        ;;
        esac
        shift
    done
    PROD_DOC="../docs/prod-host-setup.md"
    QUICKSTART="../docs/getting-started.md#prerequisites"
    say "Checking prerequisites for running Firecracker."
    say "Please check $QUICKSTART in case of any error."
    ensure_kvm_rw
    check_kernver
    check_vm
    say "Checking Host Security Configuration."
    say "Please check $PROD_DOC in case of any error."
    check_KSM
    check_IBPB_IBRS
    check_L1TF
    check_SMT
    check_swap
    check_SSBD
}

generate_syscall_table_x86_64() {
    path_to_rust_file="$FC_ROOT_DIR/src/seccompiler/src/syscall_table/x86_64.rs"

    echo "$header" > $path_to_rust_file

    # the table for x86_64 is nicely formatted here: linux/arch/x86/entry/syscalls/syscall_64.tbl
    cat linux/arch/x86/entry/syscalls/syscall_64.tbl | grep -v "^#" | grep -v -e '^$' |\
        awk '{print $2,$3,$1}' | grep -v "^x32" |\
        awk '{print "    map.insert(\""$2"\".to_string(), "$3");"}' | sort >> $path_to_rust_file

    echo "$footer" >> $path_to_rust_file

    say "Generated at: $path_to_rust_file"
}

generate_syscall_table_aarch64() {
    path_to_rust_file="$FC_ROOT_DIR/src/seccompiler/src/syscall_table/aarch64.rs"

    # filter for substituting `#define`s that point to other macros;
    # values taken from linux/include/uapi/asm-generic/unistd.h
    replace+='s/__NR3264_fadvise64/223/;'
    replace+='s/__NR3264_fcntl/25/;'
    replace+='s/__NR3264_fstatat/79/;'
    replace+='s/__NR3264_fstatfs/44/;'
    replace+='s/__NR3264_fstat/80/;'
    replace+='s/__NR3264_ftruncate/46/;'
    replace+='s/__NR3264_lseek/62/;'
    replace+='s/__NR3264_sendfile/71/;'
    replace+='s/__NR3264_statfs/43/;'
    replace+='s/__NR3264_truncate/45/;'
    replace+='s/__NR3264_mmap/222/;'

    echo "$header" > $path_to_rust_file

    # run the gcc command in the Docker container (to make sure that we have gcc installed)
    # the aarch64 syscall table is not located in a .tbl file, like x86; we run gcc's
    # pre-processor to extract the numeric constants from header files.
    run_devctr \
        --user "$(id -u):$(id -g)" \
        --workdir "$CTR_KERNEL_DIR" \
        -- \
            gcc -E -dM -D__ARCH_WANT_RENAMEAT -D__BITS_PER_LONG=64\
                linux/arch/arm64/include/uapi/asm/unistd.h | grep "#define __NR_" |\
                grep -v "__NR_syscalls" | grep -v "__NR_arch_specific_syscall" |\
                awk -F '__NR_' '{print $2}' | sed $replace |\
                awk '{ print "    map.insert(\""$1"\".to_string(), "$2");" }' |\
                sort >> $path_to_rust_file
    ret=$?

    [ $ret -ne 0 ] && return $ret

    echo "$footer" >> $path_to_rust_file

    say "Generated at: $path_to_rust_file"
}

cmd_generate_syscall_tables() {
    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help")      { cmd_help; exit 1;    } ;;
            *)                  { kernel_version="$1"; break; } ;;
        esac
        shift
    done

    validate_kernel_version "$kernel_version"

    kernel_major=v$(echo ${kernel_version} | cut -d . -f 1).x
    kernel_baseurl=https://www.kernel.org/pub/linux/kernel/${kernel_major}
    kernel_archive=linux-${kernel_version}.tar.xz

    ensure_devctr

    # Create the kernel clone directory
    rm -rf "$KERNEL_DIR"
    mkdir -p "$KERNEL_DIR" || die "Error: cannot create dir $dir"
        [ -x "$KERNEL_DIR" ] && [ -w "$dir" ] || \
            {
                chmod +x+w "$KERNEL_DIR"
            } || \
            die "Error: wrong permissions for $KERNEL_DIR. Should be +x+w"

    cd "$KERNEL_DIR"

    say "Fetching linux kernel..."

    # Get sha256 checksum.
    curl -fsSLO ${kernel_baseurl}/sha256sums.asc && \
    kernel_sha256=$(grep ${kernel_archive} sha256sums.asc | cut -d ' ' -f 1)
    # Get kernel archive.
    curl -fsSLO "$kernel_baseurl/$kernel_archive" && \
    # Verify checksum.
    echo "${kernel_sha256}  ${kernel_archive}" | sha256sum -c - && \
    # Decompress the kernel source.
    xz -d "${kernel_archive}" && \
    cat linux-${kernel_version}.tar | tar -x && mv linux-${kernel_version} linux

    ret=$?
    [ $ret -ne 0 ] && return $ret

    # rust file header
    read -r -d '' header << EOM
// Copyright $(date +"%Y") Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// This file is auto-generated by \`tools/devtool generate_syscall_tables\`.
// Do NOT manually edit!
// Generated at: $(date)
// Kernel version: $kernel_version

use std::collections::HashMap;

pub(crate) fn make_syscall_table(map: &mut HashMap<String, i64>) {
EOM

    # rust file footer
    read -r -d '' footer << EOM
}

EOM

    # generate syscall table for x86_64
    say "Generating table for x86_64..."
    generate_syscall_table_x86_64 $header $footer

    # generate syscall table for aarch64
    say "Generating table for aarch64..."
    generate_syscall_table_aarch64 $header $footer

    ret=$?
    [ $ret -ne 0 ] && return $ret
}

cmd_install() {
    # By default we install release/musl binaries.
    profile="release"
    target="$TARGET_PREFIX""musl"
    install_path="/usr/local/bin"

    # Parse any command line args.
    while [ $# -gt 0 ]; do
        case "$1" in
            "-h"|"--help") { cmd_help; exit 1; } ;;
            "-p"|"--path")
                shift;
                install_path=$1;
                ;;
            "--debug")      { profile="debug";      } ;;
            "--release")    { profile="release";    } ;;
            *)
                die "Unknown argument: $1. Please use --help for help."
            ;;
        esac
        shift
    done

    # Check that the binaries exist first
    ensure_release_binaries_exist $target $profile

    say "Installing firecracker in $install_path"
    install -m 755 "$CARGO_TARGET_DIR/$target/$profile/firecracker" "$install_path"

    say "Installing jailer in $install_path"
    install -m 755 "$CARGO_TARGET_DIR/$target/$profile/jailer" "$install_path"

    say "Installing seccomp in $install_path"
    install -m 755 "$CARGO_SECCOMPILER_TARGET_DIR/$target/$profile/seccompiler-bin" "$install_path"
}

main() {

    if [ $# = 0 ]; then
    die "No command provided. Please use \`$0 help\` for help."
    fi

    # Parse main command line args.
    #
    while [ $# -gt 0 ]; do
        case "$1" in
            -h|--help)              { cmd_help; exit 1;     } ;;
            -y|--unattended)        { OPT_UNATTENDED=true;  } ;;
            -*)
                die "Unknown arg: $1. Please use \`$0 help\` for help."
            ;;
            *)
                break
            ;;
        esac
        shift
    done

    # $1 is now a command name. Check if it is a valid command and, if so,
    # run it.
    #
    declare -f "cmd_$1" > /dev/null
    ok_or_die "Unknown command: $1. Please use \`$0 help\` for help."

    cmd=cmd_$1
    shift

    # $@ is now a list of command-specific args
    #
    $cmd "$@"
}

main "$@"