aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Dong <contact@carldong.me>2021-02-04 19:09:48 -0500
committerCarl Dong <contact@carldong.me>2021-05-13 15:41:56 -0400
commit38eb91eb0616ed6dbe34c23e11588d130fef07f8 (patch)
tree869218b5fc6ba22433d22398d7be8e79d0ccf6f5
parentbac2690e6f683fcedb883fe1d32f3c33c628a141 (diff)
downloadbitcoin-38eb91eb0616ed6dbe34c23e11588d130fef07f8.tar.xz
guix: Add codesigning functionality
-rwxr-xr-xcontrib/guix/guix-build10
-rwxr-xr-xcontrib/guix/guix-codesign392
-rwxr-xr-xcontrib/guix/libexec/codesign.sh103
3 files changed, 500 insertions, 5 deletions
diff --git a/contrib/guix/guix-build b/contrib/guix/guix-build
index 5b3c20b234..69c244a6fa 100755
--- a/contrib/guix/guix-build
+++ b/contrib/guix/guix-build
@@ -267,20 +267,20 @@ for host in $HOSTS; do
make -C "${PWD}/depends" -j"$JOBS" download-"$(host_to_commonname "$host")" ${V:+V=1} ${SOURCES_PATH:+SOURCES_PATH="$SOURCES_PATH"}
done
-# Usage: outdir_for_host HOST
+# Usage: outdir_for_host HOST SUFFIX
#
# HOST: The current platform triple we're building for
#
outdir_for_host() {
- echo "${OUTDIR_BASE}/${1}"
+ echo "${OUTDIR_BASE}/${1}${2:+-${2}}"
}
-# Usage: profiledir_for_host HOST COMMAND
+# Usage: profiledir_for_host HOST SUFFIX
#
# HOST: The current platform triple we're building for
#
profiledir_for_host() {
- echo "${PROFILES_BASE}/${2}-${1}"
+ echo "${PROFILES_BASE}/${1}${2:+-${2}}"
}
@@ -412,7 +412,7 @@ EOF
--keep-failed \
--fallback \
--link-profile \
- --root="$(profiledir_for_host "${HOST}" build)" \
+ --root="$(profiledir_for_host "${HOST}")" \
${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \
${ADDITIONAL_GUIX_COMMON_FLAGS} ${ADDITIONAL_GUIX_ENVIRONMENT_FLAGS} \
-- env HOST="$host" \
diff --git a/contrib/guix/guix-codesign b/contrib/guix/guix-codesign
new file mode 100755
index 0000000000..62853dbf4d
--- /dev/null
+++ b/contrib/guix/guix-codesign
@@ -0,0 +1,392 @@
+#!/usr/bin/env bash
+export LC_ALL=C
+set -e -o pipefail
+
+# Source the common prelude, which:
+# 1. Checks if we're at the top directory of the Bitcoin Core repository
+# 2. Defines a few common functions and variables
+#
+# shellcheck source=libexec/prelude.bash
+source "$(dirname "${BASH_SOURCE[0]}")/libexec/prelude.bash"
+
+
+###################
+## SANITY CHECKS ##
+###################
+
+################
+# Required non-builtin commands should be invocable
+################
+
+check_tools cat mkdir git guix
+
+################
+# Required env vars should be non-empty
+################
+
+cmd_usage() {
+ cat <<EOF
+Synopsis:
+
+ env DETACHED_SIGS_REPO=<path/to/bitcoin-detached-sigs> \\
+ ./contrib/guix/guix-codesign
+
+EOF
+}
+
+if [ -z "$DETACHED_SIGS_REPO" ]; then
+ cmd_usage
+ exit 1
+fi
+
+################
+# GUIX_BUILD_OPTIONS should be empty
+################
+#
+# GUIX_BUILD_OPTIONS is an environment variable recognized by guix commands that
+# can perform builds. This seems like what we want instead of
+# ADDITIONAL_GUIX_COMMON_FLAGS, but the value of GUIX_BUILD_OPTIONS is actually
+# _appended_ to normal command-line options. Meaning that they will take
+# precedence over the command-specific ADDITIONAL_GUIX_<CMD>_FLAGS.
+#
+# This seems like a poor user experience. Thus we check for GUIX_BUILD_OPTIONS's
+# existence here and direct users of this script to use our (more flexible)
+# custom environment variables.
+if [ -n "$GUIX_BUILD_OPTIONS" ]; then
+cat << EOF
+Error: Environment variable GUIX_BUILD_OPTIONS is not empty:
+ '$GUIX_BUILD_OPTIONS'
+
+Unfortunately this script is incompatible with GUIX_BUILD_OPTIONS, please unset
+GUIX_BUILD_OPTIONS and use ADDITIONAL_GUIX_COMMON_FLAGS to set build options
+across guix commands or ADDITIONAL_GUIX_<CMD>_FLAGS to set build options for a
+specific guix command.
+
+See contrib/guix/README.md for more details.
+EOF
+exit 1
+fi
+
+################
+# The codesignature git worktree should not be dirty
+################
+
+if ! git -C "$DETACHED_SIGS_REPO" diff-index --quiet HEAD -- && [ -z "$FORCE_DIRTY_WORKTREE" ]; then
+ cat << EOF
+ERR: The DETACHED CODESIGNATURE git worktree is dirty, which may lead to broken builds.
+
+ Aborting...
+
+Hint: To make your git worktree clean, You may want to:
+ 1. Commit your changes,
+ 2. Stash your changes, or
+ 3. Set the 'FORCE_DIRTY_WORKTREE' environment variable if you insist on
+ using a dirty worktree
+EOF
+ exit 1
+fi
+
+################
+# Build directories should not exist
+################
+
+# Default to building for all supported HOSTs (overridable by environment)
+export HOSTS="${HOSTS:-x86_64-w64-mingw32 x86_64-apple-darwin18}"
+
+# Usage: distsrc_for_host HOST
+#
+# HOST: The current platform triple we're building for
+#
+distsrc_for_host() {
+ echo "${DISTSRC_BASE}/distsrc-${VERSION}-${1}-codesigned"
+}
+
+# Accumulate a list of build directories that already exist...
+hosts_distsrc_exists=""
+for host in $HOSTS; do
+ if [ -e "$(distsrc_for_host "$host")" ]; then
+ hosts_distsrc_exists+=" ${host}"
+ fi
+done
+
+if [ -n "$hosts_distsrc_exists" ]; then
+# ...so that we can print them out nicely in an error message
+cat << EOF
+ERR: Build directories for this commit already exist for the following platform
+ triples you're attempting to build, probably because of previous builds.
+ Please remove, or otherwise deal with them prior to starting another build.
+
+ Aborting...
+
+Hint: To blow everything away, you may want to use:
+
+ $ ./contrib/guix/guix-clean
+
+Specifically, this will remove all files without an entry in the index,
+excluding the SDK directory, the depends download cache, the depends built
+packages cache, the garbage collector roots for Guix environments, and the
+output directory.
+EOF
+for host in $hosts_distsrc_exists; do
+ echo " ${host} '$(distsrc_for_host "$host")'"
+done
+exit 1
+else
+ mkdir -p "$DISTSRC_BASE"
+fi
+
+
+################
+# Unsigned tarballs SHOULD exist
+################
+
+# Usage: outdir_for_host HOST SUFFIX
+#
+# HOST: The current platform triple we're building for
+#
+outdir_for_host() {
+ echo "${OUTDIR_BASE}/${1}${2:+-${2}}"
+}
+
+
+unsigned_tarball_for_host() {
+ case "$1" in
+ *mingw*)
+ echo "$(outdir_for_host "$1")/${DISTNAME}-win-unsigned.tar.gz"
+ ;;
+ *darwin*)
+ echo "$(outdir_for_host "$1")/${DISTNAME}-osx-unsigned.tar.gz"
+ ;;
+ *)
+ exit 1
+ ;;
+ esac
+}
+
+# Accumulate a list of build directories that already exist...
+hosts_unsigned_tarball_missing=""
+for host in $HOSTS; do
+ if [ ! -e "$(unsigned_tarball_for_host "$host")" ]; then
+ hosts_unsigned_tarball_missing+=" ${host}"
+ fi
+done
+
+if [ -n "$hosts_unsigned_tarball_missing" ]; then
+ # ...so that we can print them out nicely in an error message
+ cat << EOF
+ERR: Unsigned tarballs do not exist
+...
+
+EOF
+for host in $hosts_unsigned_tarball_missing; do
+ echo " ${host} '$(unsigned_tarball_for_host "$host")'"
+done
+exit 1
+fi
+
+################
+# Check that we can connect to the guix-daemon
+################
+
+cat << EOF
+Checking that we can connect to the guix-daemon...
+
+Hint: If this hangs, you may want to try turning your guix-daemon off and on
+ again.
+
+EOF
+if ! guix gc --list-failures > /dev/null; then
+ cat << EOF
+
+ERR: Failed to connect to the guix-daemon, please ensure that one is running and
+ reachable.
+EOF
+ exit 1
+fi
+
+# Developer note: we could use `guix repl` for this check and run:
+#
+# (import (guix store)) (close-connection (open-connection))
+#
+# However, the internal API is likely to change more than the CLI invocation
+
+
+#########
+# SETUP #
+#########
+
+# Determine the maximum number of jobs to run simultaneously (overridable by
+# environment)
+JOBS="${JOBS:-$(nproc)}"
+
+# Determine the reference time used for determinism (overridable by environment)
+SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git log --format=%at -1)}"
+
+# Execute "$@" in a pinned, possibly older version of Guix, for reproducibility
+# across time.
+time-machine() {
+ # shellcheck disable=SC2086
+ guix time-machine --url=https://github.com/dongcarl/guix.git \
+ --commit=490e39ff303f4f6873a04bfb8253755bdae1b29c \
+ --cores="$JOBS" \
+ --keep-failed \
+ --fallback \
+ ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \
+ ${ADDITIONAL_GUIX_COMMON_FLAGS} ${ADDITIONAL_GUIX_TIMEMACHINE_FLAGS} \
+ -- "$@"
+}
+
+# Make sure an output directory exists for our builds
+OUTDIR_BASE="${OUTDIR_BASE:-${VERSION_BASE}/output}"
+mkdir -p "$OUTDIR_BASE"
+
+# Usage: profiledir_for_host HOST SUFFIX
+#
+# HOST: The current platform triple we're building for
+#
+profiledir_for_host() {
+ echo "${PROFILES_BASE}/${1}${2:+-${2}}"
+}
+
+#########
+# BUILD #
+#########
+
+# Function to be called when codesigning for host ${1} and the user interrupts
+# the codesign
+int_trap() {
+cat << EOF
+** INT received while codesigning ${1}, you may want to clean up the relevant
+ work directories (e.g. distsrc-*) before recodesigning
+
+Hint: To blow everything away, you may want to use:
+
+ $ ./contrib/guix/guix-clean
+
+Specifically, this will remove all files without an entry in the index,
+excluding the SDK directory, the depends download cache, the depends built
+packages cache, the garbage collector roots for Guix environments, and the
+output directory.
+EOF
+}
+
+# Deterministically build Bitcoin Core
+# shellcheck disable=SC2153
+for host in $HOSTS; do
+
+ # Display proper warning when the user interrupts the build
+ trap 'int_trap ${host}' INT
+
+ (
+ # Required for 'contrib/guix/manifest.scm' to output the right manifest
+ # for the particular $HOST we're building for
+ export HOST="$host"
+
+ # shellcheck disable=SC2030
+cat << EOF
+INFO: Codesigning ${VERSION:?not set} for platform triple ${HOST:?not set}:
+ ...using reference timestamp: ${SOURCE_DATE_EPOCH:?not set}
+ ...from worktree directory: '${PWD}'
+ ...bind-mounted in container to: '/bitcoin'
+ ...in build directory: '$(distsrc_for_host "$HOST")'
+ ...bind-mounted in container to: '$(DISTSRC_BASE=/distsrc-base && distsrc_for_host "$HOST")'
+ ...outputting in: '$(outdir_for_host "$HOST" codesigned)'
+ ...bind-mounted in container to: '$(OUTDIR_BASE=/outdir-base && outdir_for_host "$HOST" codesigned)'
+ ...using detached signatures in: '${DETACHED_SIGS_REPO:?not set}'
+ ...bind-mounted in container to: '/detached-sigs'
+EOF
+
+
+ # Run the build script 'contrib/guix/libexec/build.sh' in the build
+ # container specified by 'contrib/guix/manifest.scm'.
+ #
+ # Explanation of `guix environment` flags:
+ #
+ # --container run command within an isolated container
+ #
+ # Running in an isolated container minimizes build-time differences
+ # between machines and improves reproducibility
+ #
+ # --pure unset existing environment variables
+ #
+ # Same rationale as --container
+ #
+ # --no-cwd do not share current working directory with an
+ # isolated container
+ #
+ # When --container is specified, the default behavior is to share
+ # the current working directory with the isolated container at the
+ # same exact path (e.g. mapping '/home/satoshi/bitcoin/' to
+ # '/home/satoshi/bitcoin/'). This means that the $PWD inside the
+ # container becomes a source of irreproducibility. --no-cwd disables
+ # this behaviour.
+ #
+ # --share=SPEC for containers, share writable host file system
+ # according to SPEC
+ #
+ # --share="$PWD"=/bitcoin
+ #
+ # maps our current working directory to /bitcoin
+ # inside the isolated container, which we later cd
+ # into.
+ #
+ # While we don't want to map our current working directory to the
+ # same exact path (as this introduces irreproducibility), we do want
+ # it to be at a _fixed_ path _somewhere_ inside the isolated
+ # container so that we have something to build. '/bitcoin' was
+ # chosen arbitrarily.
+ #
+ # ${SOURCES_PATH:+--share="$SOURCES_PATH"}
+ #
+ # make the downloaded depends sources path available
+ # inside the isolated container
+ #
+ # The isolated container has no network access as it's in a
+ # different network namespace from the main machine, so we have to
+ # make the downloaded depends sources available to it. The sources
+ # should have been downloaded prior to this invocation.
+ #
+ # ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"}
+ #
+ # fetch substitute from SUBSTITUTE_URLS if they are
+ # authorized
+ #
+ # Depending on the user's security model, it may be desirable to use
+ # substitutes (pre-built packages) from servers that the user trusts.
+ # Please read the README.md in the same directory as this file for
+ # more information.
+ #
+ # shellcheck disable=SC2086,SC2031
+ time-machine environment --manifest="${PWD}/contrib/guix/manifest.scm" \
+ --container \
+ --pure \
+ --no-cwd \
+ --share="$PWD"=/bitcoin \
+ --share="$DISTSRC_BASE"=/distsrc-base \
+ --share="$OUTDIR_BASE"=/outdir-base \
+ --share="$DETACHED_SIGS_REPO"=/detached-sigs \
+ --expose="$(git rev-parse --git-common-dir)" \
+ --expose="$(git -C "$DETACHED_SIGS_REPO" rev-parse --git-common-dir)" \
+ ${SOURCES_PATH:+--share="$SOURCES_PATH"} \
+ --cores="$JOBS" \
+ --keep-failed \
+ --fallback \
+ --link-profile \
+ --root="$(profiledir_for_host "${HOST}" codesigned)" \
+ ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \
+ ${ADDITIONAL_GUIX_COMMON_FLAGS} ${ADDITIONAL_GUIX_ENVIRONMENT_FLAGS} \
+ -- env HOST="$host" \
+ DISTNAME="$DISTNAME" \
+ JOBS="$JOBS" \
+ SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:?unable to determine value}" \
+ ${V:+V=1} \
+ ${SOURCES_PATH:+SOURCES_PATH="$SOURCES_PATH"} \
+ DISTSRC="$(DISTSRC_BASE=/distsrc-base && distsrc_for_host "$HOST")" \
+ OUTDIR="$(OUTDIR_BASE=/outdir-base && outdir_for_host "$HOST" codesigned)" \
+ DIST_ARCHIVE_BASE=/outdir-base/dist-archive \
+ DETACHED_SIGS_REPO=/detached-sigs \
+ UNSIGNED_TARBALL="$(OUTDIR_BASE=/outdir-base && unsigned_tarball_for_host "$HOST")" \
+ bash -c "cd /bitcoin && bash contrib/guix/libexec/codesign.sh"
+ )
+
+done
diff --git a/contrib/guix/libexec/codesign.sh b/contrib/guix/libexec/codesign.sh
new file mode 100755
index 0000000000..46b42a5712
--- /dev/null
+++ b/contrib/guix/libexec/codesign.sh
@@ -0,0 +1,103 @@
+#!/usr/bin/env bash
+export LC_ALL=C
+set -e -o pipefail
+export TZ=UTC
+
+# Although Guix _does_ set umask when building its own packages (in our case,
+# this is all packages in manifest.scm), it does not set it for `guix
+# environment`. It does make sense for at least `guix environment --container`
+# to set umask, so if that change gets merged upstream and we bump the
+# time-machine to a commit which includes the aforementioned change, we can
+# remove this line.
+#
+# This line should be placed before any commands which creates files.
+umask 0022
+
+if [ -n "$V" ]; then
+ # Print both unexpanded (-v) and expanded (-x) forms of commands as they are
+ # read from this file.
+ set -vx
+ # Set VERBOSE for CMake-based builds
+ export VERBOSE="$V"
+fi
+
+# Check that required environment variables are set
+cat << EOF
+Required environment variables as seen inside the container:
+ UNSIGNED_TARBALL: ${UNSIGNED_TARBALL:?not set}
+ DETACHED_SIGS_REPO: ${DETACHED_SIGS_REPO:?not set}
+ DIST_ARCHIVE_BASE: ${DIST_ARCHIVE_BASE:?not set}
+ DISTNAME: ${DISTNAME:?not set}
+ HOST: ${HOST:?not set}
+ SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH:?not set}
+ DISTSRC: ${DISTSRC:?not set}
+ OUTDIR: ${OUTDIR:?not set}
+EOF
+
+ACTUAL_OUTDIR="${OUTDIR}"
+OUTDIR="${DISTSRC}/output"
+
+git_head_version() {
+ local recent_tag
+ if recent_tag="$(git -C "$1" describe --exact-match HEAD 2> /dev/null)"; then
+ echo "${recent_tag#v}"
+ else
+ git -C "$1" rev-parse --short=12 HEAD
+ fi
+}
+
+CODESIGNATURE_GIT_ARCHIVE="${DIST_ARCHIVE_BASE}/${DISTNAME}-codesignatures-$(git_head_version "$DETACHED_SIGS_REPO").tar.gz"
+
+# Create the codesignature tarball if not already there
+if [ ! -e "$CODESIGNATURE_GIT_ARCHIVE" ]; then
+ mkdir -p "$(dirname "$CODESIGNATURE_GIT_ARCHIVE")"
+ git -C "$DETACHED_SIGS_REPO" archive --output="$CODESIGNATURE_GIT_ARCHIVE" HEAD
+fi
+
+mkdir -p "$OUTDIR"
+cat << EOF > "$OUTDIR"/inputs.SHA256SUMS
+$(sha256sum "$UNSIGNED_TARBALL" | cut -d' ' -f1) inputs/$(basename "$UNSIGNED_TARBALL")
+$(sha256sum "$CODESIGNATURE_GIT_ARCHIVE" | cut -d' ' -f1) inputs/$(basename "$CODESIGNATURE_GIT_ARCHIVE")
+EOF
+
+mkdir -p "$DISTSRC"
+(
+ cd "$DISTSRC"
+
+ tar -xf "$UNSIGNED_TARBALL"
+
+ mkdir -p codesignatures
+ tar -C codesignatures -xf "$CODESIGNATURE_GIT_ARCHIVE"
+
+ case "$HOST" in
+ *mingw*)
+ find "$PWD" -name "*-unsigned.exe" | while read -r infile; do
+ infile_base="$(basename "$infile")"
+
+ # Codesigned *-unsigned.exe and output to OUTDIR
+ osslsigncode attach-signature \
+ -in "$infile" \
+ -out "${OUTDIR}/${infile_base/-unsigned}" \
+ -sigin codesignatures/win/"$infile_base".pem
+ done
+ ;;
+ *darwin*)
+ # Apply detached codesignatures to dist/ (in-place)
+ signapple apply dist/Bitcoin-Qt.app codesignatures/osx/dist
+
+ # Make an uncompressed DMG from dist/
+ xorrisofs -D -l -V "$(< osx_volname)" -no-pad -r -dir-mode 0755 \
+ -o uncompressed.dmg \
+ dist \
+ -- -volume_date all_file_dates ="$SOURCE_DATE_EPOCH"
+
+ # Compress uncompressed.dmg and output to OUTDIR
+ ./dmg dmg uncompressed.dmg "${OUTDIR}/${DISTNAME}-osx-signed.dmg"
+ ;;
+ *)
+ exit 1
+ ;;
+ esac
+) # $DISTSRC
+
+mv --no-target-directory "$OUTDIR" "$ACTUAL_OUTDIR"