guix-codesign raw

   1  #!/usr/bin/env bash
   2  export LC_ALL=C
   3  set -e -o pipefail
   4  
   5  # Source the common prelude, which:
   6  #   1. Checks if we're at the top directory of the Limenka repository
   7  #   2. Defines a few common functions and variables
   8  #
   9  # shellcheck source=libexec/prelude.bash
  10  source "$(dirname "${BASH_SOURCE[0]}")/libexec/prelude.bash"
  11  
  12  
  13  ###################
  14  ## SANITY CHECKS ##
  15  ###################
  16  
  17  ################
  18  # Required non-builtin commands should be invocable
  19  ################
  20  
  21  check_tools cat mkdir git guix
  22  
  23  ################
  24  # Required env vars should be non-empty
  25  ################
  26  
  27  cmd_usage() {
  28      cat <<EOF
  29  Synopsis:
  30  
  31      env DETACHED_SIGS_REPO=<path/to/limenka-detached-sigs> \\
  32        ./contrib/guix/guix-codesign
  33  
  34  EOF
  35  }
  36  
  37  if [ -z "$DETACHED_SIGS_REPO" ]; then
  38      cmd_usage
  39      exit 1
  40  fi
  41  
  42  ################
  43  # GUIX_BUILD_OPTIONS should be empty
  44  ################
  45  #
  46  # GUIX_BUILD_OPTIONS is an environment variable recognized by guix commands that
  47  # can perform builds. This seems like what we want instead of
  48  # ADDITIONAL_GUIX_COMMON_FLAGS, but the value of GUIX_BUILD_OPTIONS is actually
  49  # _appended_ to normal command-line options. Meaning that they will take
  50  # precedence over the command-specific ADDITIONAL_GUIX_<CMD>_FLAGS.
  51  #
  52  # This seems like a poor user experience. Thus we check for GUIX_BUILD_OPTIONS's
  53  # existence here and direct users of this script to use our (more flexible)
  54  # custom environment variables.
  55  if [ -n "$GUIX_BUILD_OPTIONS" ]; then
  56  cat << EOF
  57  Error: Environment variable GUIX_BUILD_OPTIONS is not empty:
  58    '$GUIX_BUILD_OPTIONS'
  59  
  60  Unfortunately this script is incompatible with GUIX_BUILD_OPTIONS, please unset
  61  GUIX_BUILD_OPTIONS and use ADDITIONAL_GUIX_COMMON_FLAGS to set build options
  62  across guix commands or ADDITIONAL_GUIX_<CMD>_FLAGS to set build options for a
  63  specific guix command.
  64  
  65  See contrib/guix/README.md for more details.
  66  EOF
  67  exit 1
  68  fi
  69  
  70  ################
  71  # SOURCE_DATE_EPOCH should not unintentionally be set
  72  ################
  73  
  74  check_source_date_epoch
  75  
  76  ################
  77  # The codesignature git worktree should not be dirty
  78  ################
  79  
  80  if ! git -C "$DETACHED_SIGS_REPO" diff-index --quiet HEAD -- && [ -z "$FORCE_DIRTY_WORKTREE" ]; then
  81      cat << EOF
  82  ERR: The DETACHED CODESIGNATURE git worktree is dirty, which may lead to broken builds.
  83  
  84       Aborting...
  85  
  86  Hint: To make your git worktree clean, You may want to:
  87        1. Commit your changes,
  88        2. Stash your changes, or
  89        3. Set the 'FORCE_DIRTY_WORKTREE' environment variable if you insist on
  90           using a dirty worktree
  91  EOF
  92      exit 1
  93  fi
  94  
  95  ################
  96  # Build directories should not exist
  97  ################
  98  
  99  # Default to building for all supported HOSTs (overridable by environment)
 100  export HOSTS="${HOSTS:-x86_64-apple-darwin arm64-apple-darwin}"
 101  
 102  # Usage: distsrc_for_host HOST
 103  #
 104  #   HOST: The current platform triple we're building for
 105  #
 106  distsrc_for_host() {
 107      echo "${DISTSRC_BASE}/distsrc-${VERSION}-${1}-codesigned"
 108  }
 109  
 110  # Accumulate a list of build directories that already exist...
 111  hosts_distsrc_exists=""
 112  for host in $HOSTS; do
 113      if [ -e "$(distsrc_for_host "$host")" ]; then
 114          hosts_distsrc_exists+=" ${host}"
 115      fi
 116  done
 117  
 118  if [ -n "$hosts_distsrc_exists" ]; then
 119  # ...so that we can print them out nicely in an error message
 120  cat << EOF
 121  ERR: Build directories for this commit already exist for the following platform
 122       triples you're attempting to build, probably because of previous builds.
 123       Please remove, or otherwise deal with them prior to starting another build.
 124  
 125       Aborting...
 126  
 127  Hint: To blow everything away, you may want to use:
 128  
 129    $ ./contrib/guix/guix-clean
 130  
 131  Specifically, this will remove all files without an entry in the index,
 132  excluding the SDK directory, the depends download cache, the depends built
 133  packages cache, the garbage collector roots for Guix environments, and the
 134  output directory.
 135  EOF
 136  for host in $hosts_distsrc_exists; do
 137      echo "     ${host} '$(distsrc_for_host "$host")'"
 138  done
 139  exit 1
 140  else
 141      mkdir -p "$DISTSRC_BASE"
 142  fi
 143  
 144  
 145  ################
 146  # Codesigning tarballs SHOULD exist
 147  ################
 148  
 149  # Usage: outdir_for_host HOST SUFFIX
 150  #
 151  #   HOST: The current platform triple we're building for
 152  #
 153  outdir_for_host() {
 154      echo "${OUTDIR_BASE}/${1}${2:+-${2}}"
 155  }
 156  
 157  
 158  codesigning_tarball_for_host() {
 159      case "$1" in
 160          *mingw*)
 161              echo "$(outdir_for_host "$1")/${DISTNAME}-win64-codesigning.tar.gz"
 162              ;;
 163          *darwin*)
 164              echo "$(outdir_for_host "$1")/${DISTNAME}-${1}-codesigning.tar.gz"
 165              ;;
 166          *)
 167              exit 1
 168              ;;
 169      esac
 170  }
 171  
 172  # Accumulate a list of build directories that already exist...
 173  hosts_codesigning_tarball_missing=""
 174  for host in $HOSTS; do
 175      if [ ! -e "$(codesigning_tarball_for_host "$host")" ]; then
 176          hosts_codesigning_tarball_missing+=" ${host}"
 177      fi
 178  done
 179  
 180  if [ -n "$hosts_codesigning_tarball_missing" ]; then
 181      # ...so that we can print them out nicely in an error message
 182      cat << EOF
 183  ERR: Codesigning tarballs do not exist
 184  ...
 185  
 186  EOF
 187  for host in $hosts_codesigning_tarball_missing; do
 188      echo "     ${host} '$(codesigning_tarball_for_host "$host")'"
 189  done
 190  exit 1
 191  fi
 192  
 193  ################
 194  # Check that we can connect to the guix-daemon
 195  ################
 196  
 197  cat << EOF
 198  Checking that we can connect to the guix-daemon...
 199  
 200  Hint: If this hangs, you may want to try turning your guix-daemon off and on
 201        again.
 202  
 203  EOF
 204  if ! guix gc --list-failures > /dev/null; then
 205      cat << EOF
 206  
 207  ERR: Failed to connect to the guix-daemon, please ensure that one is running and
 208       reachable.
 209  EOF
 210      exit 1
 211  fi
 212  
 213  # Developer note: we could use `guix repl` for this check and run:
 214  #
 215  #     (import (guix store)) (close-connection (open-connection))
 216  #
 217  # However, the internal API is likely to change more than the CLI invocation
 218  
 219  
 220  #########
 221  # SETUP #
 222  #########
 223  
 224  # Determine the maximum number of jobs to run simultaneously (overridable by
 225  # environment)
 226  JOBS="${JOBS:-$(nproc)}"
 227  
 228  # Determine the reference time used for determinism (overridable by environment)
 229  SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -c log.showSignature=false log --format=%at -1)}"
 230  
 231  # Make sure an output directory exists for our builds
 232  OUTDIR_BASE="${OUTDIR_BASE:-${VERSION_BASE}/output}"
 233  mkdir -p "$OUTDIR_BASE"
 234  
 235  # Usage: profiledir_for_host HOST SUFFIX
 236  #
 237  #   HOST: The current platform triple we're building for
 238  #
 239  profiledir_for_host() {
 240      echo "${PROFILES_BASE}/${1}${2:+-${2}}"
 241  }
 242  
 243  #########
 244  # BUILD #
 245  #########
 246  
 247  # Function to be called when codesigning for host ${1} and the user interrupts
 248  # the codesign
 249  int_trap() {
 250  cat << EOF
 251  ** INT received while codesigning ${1}, you may want to clean up the relevant
 252     work directories (e.g. distsrc-*) before recodesigning
 253  
 254  Hint: To blow everything away, you may want to use:
 255  
 256    $ ./contrib/guix/guix-clean
 257  
 258  Specifically, this will remove all files without an entry in the index,
 259  excluding the SDK directory, the depends download cache, the depends built
 260  packages cache, the garbage collector roots for Guix environments, and the
 261  output directory.
 262  EOF
 263  }
 264  
 265  # Deterministically build Limenka
 266  # shellcheck disable=SC2153
 267  for host in $HOSTS; do
 268  
 269      # Display proper warning when the user interrupts the build
 270      trap 'int_trap ${host}' INT
 271  
 272      (
 273          # Required for 'contrib/guix/manifest.scm' to output the right manifest
 274          # for the particular $HOST we're building for
 275          export HOST="$host"
 276  
 277          # shellcheck disable=SC2030
 278  cat << EOF
 279  INFO: Codesigning ${VERSION:?not set} for platform triple ${HOST:?not set}:
 280        ...using reference timestamp: ${SOURCE_DATE_EPOCH:?not set}
 281        ...from worktree directory: '${PWD}'
 282            ...bind-mounted in container to: '/limenka'
 283        ...in build directory: '$(distsrc_for_host "$HOST")'
 284            ...bind-mounted in container to: '$(DISTSRC_BASE=/distsrc-base && distsrc_for_host "$HOST")'
 285        ...outputting in: '$(outdir_for_host "$HOST" codesigned)'
 286            ...bind-mounted in container to: '$(OUTDIR_BASE=/outdir-base && outdir_for_host "$HOST" codesigned)'
 287        ...using detached signatures in: '${DETACHED_SIGS_REPO:?not set}'
 288            ...bind-mounted in container to: '/detached-sigs'
 289  EOF
 290  
 291  
 292          # Run the build script 'contrib/guix/libexec/build.sh' in the build
 293          # container specified by 'contrib/guix/manifest.scm'.
 294          #
 295          # Explanation of `guix shell` flags:
 296          #
 297          #   --container        run command within an isolated container
 298          #
 299          #     Running in an isolated container minimizes build-time differences
 300          #     between machines and improves reproducibility
 301          #
 302          #   --pure             unset existing environment variables
 303          #
 304          #     Same rationale as --container
 305          #
 306          #   --no-cwd           do not share current working directory with an
 307          #                      isolated container
 308          #
 309          #     When --container is specified, the default behavior is to share
 310          #     the current working directory with the isolated container at the
 311          #     same exact path (e.g. mapping '/home/satoshi/limenka/' to
 312          #     '/home/satoshi/limenka/'). This means that the $PWD inside the
 313          #     container becomes a source of irreproducibility. --no-cwd disables
 314          #     this behaviour.
 315          #
 316          #   --share=SPEC       for containers, share writable host file system
 317          #                      according to SPEC
 318          #
 319          #   --share="$PWD"=/limenka
 320          #
 321          #                     maps our current working directory to /limenka
 322          #                     inside the isolated container, which we later cd
 323          #                     into.
 324          #
 325          #     While we don't want to map our current working directory to the
 326          #     same exact path (as this introduces irreproducibility), we do want
 327          #     it to be at a _fixed_ path _somewhere_ inside the isolated
 328          #     container so that we have something to build. '/limenka' was
 329          #     chosen arbitrarily.
 330          #
 331          #   ${SOURCES_PATH:+--share="$SOURCES_PATH"}
 332          #
 333          #                     make the downloaded depends sources path available
 334          #                     inside the isolated container
 335          #
 336          #     The isolated container has no network access as it's in a
 337          #     different network namespace from the main machine, so we have to
 338          #     make the downloaded depends sources available to it. The sources
 339          #     should have been downloaded prior to this invocation.
 340          #
 341          #  ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"}
 342          #
 343          #                     fetch substitute from SUBSTITUTE_URLS if they are
 344          #                     authorized
 345          #
 346          #    Depending on the user's security model, it may be desirable to use
 347          #    substitutes (pre-built packages) from servers that the user trusts.
 348          #    Please read the README.md in the same directory as this file for
 349          #    more information.
 350          #
 351          # shellcheck disable=SC2086,SC2031
 352          time-machine shell --manifest="${PWD}/contrib/guix/manifest.scm" \
 353                                   --container \
 354                                   --pure \
 355                                   --no-cwd \
 356                                   --share="$PWD"=/limenka \
 357                                   --share="$DISTSRC_BASE"=/distsrc-base \
 358                                   --share="$OUTDIR_BASE"=/outdir-base \
 359                                   --share="$DETACHED_SIGS_REPO"=/detached-sigs \
 360                                   --expose="$(git rev-parse --git-common-dir)" \
 361                                   --expose="$(git -C "$DETACHED_SIGS_REPO" rev-parse --git-common-dir)" \
 362                                   ${SOURCES_PATH:+--share="$SOURCES_PATH"} \
 363                                   --cores="$JOBS" \
 364                                   --keep-failed \
 365                                   --fallback \
 366                                   --link-profile \
 367                                   --root="$(profiledir_for_host "${HOST}" codesigned)" \
 368                                   ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \
 369                                   ${ADDITIONAL_GUIX_COMMON_FLAGS} ${ADDITIONAL_GUIX_ENVIRONMENT_FLAGS} \
 370                                   -- env HOST="$host" \
 371                                          DISTNAME="$DISTNAME" \
 372                                          JOBS="$JOBS" \
 373                                          SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:?unable to determine value}" \
 374                                          ${V:+V=1} \
 375                                          ${SOURCES_PATH:+SOURCES_PATH="$SOURCES_PATH"} \
 376                                          DISTSRC="$(DISTSRC_BASE=/distsrc-base && distsrc_for_host "$HOST")" \
 377                                          OUTDIR="$(OUTDIR_BASE=/outdir-base && outdir_for_host "$HOST" codesigned)" \
 378                                          DIST_ARCHIVE_BASE=/outdir-base/dist-archive \
 379                                          DETACHED_SIGS_REPO=/detached-sigs \
 380                                          CODESIGNING_TARBALL="$(OUTDIR_BASE=/outdir-base && codesigning_tarball_for_host "$HOST")" \
 381                                        bash -c "cd /limenka && bash contrib/guix/libexec/codesign.sh"
 382      )
 383  
 384  done
 385