diff --git a/contrib/guix/guix-attest b/contrib/guix/guix-attest index 1f07a205..e9a0b817 100755 --- a/contrib/guix/guix-attest +++ b/contrib/guix/guix-attest @@ -221,8 +221,8 @@ mkdir -p "$outsigdir" # not needed if there are no $codesigned_fragments cat "${sha256sum_fragments[@]}" \ | sort -u \ - | sort -k2 \ | basenameify_SHA256SUMS \ + | sort -k2 \ > "$temp_all" if [ -e all.SHA256SUMS ]; then # The SHA256SUMS already exists, make sure it's exactly what we diff --git a/contrib/guix/guix-codesign b/contrib/guix/guix-codesign new file mode 100755 index 00000000..ad08df09 --- /dev/null +++ b/contrib/guix/guix-codesign @@ -0,0 +1,327 @@ +#!/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 Feather Wallet 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 < \\ + ./contrib/guix/guix-codesign + +EOF +} + +if [ -z "$GUIX_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__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__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 "$GUIX_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-w64-mingw32.installer}" + +# Usage: distsrc_for_host HOST +# +# HOST: The current platform triple we're building for +# +distsrc_for_host() { + echo "${DISTSRC_BASE}/build/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 files 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}}" +} + +# Usage: logdir_for_host HOST SUFFIX +# +# HOST: The current platform triple we're building for +# +logdir_for_host() { + echo "${LOGDIR_BASE}/${1}${2:+-${2}}" +} + +unsigned_file_for_host() { + case "$1" in + *mingw32.installer) + echo "$(outdir_for_host "$1")/FeatherWalletSetup-${VERSION}-unsigned.exe" + ;; + *mingw32*) + echo "$(outdir_for_host "$1")/${DISTNAME}-unsigned.exe" + ;; + *) + exit 1 + ;; + esac +} + +# Accumulate a list of build directories that already exist... +hosts_file_tarball_missing="" +for host in $HOSTS; do + if [ ! -e "$(unsigned_file_for_host "$host")" ]; then + hosts_file_tarball_missing+=" ${host}" + fi +done + +if [ -n "$hosts_file_tarball_missing" ]; then + # ...so that we can print them out nicely in an error message + cat << EOF +ERR: Unsigned files do not exist +... + +EOF +for host in $hosts_file_tarball_missing; do + echo " ${host} '$(unsigned_file_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 -c log.showSignature=false log --format=%at -1)}" + +# 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 +} + +# 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: '/feather' + ...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: '${GUIX_SIGS_REPO:?not set}' + ...bind-mounted in container to: '/detached-sigs' +EOF + + # shellcheck disable=SC2086,SC2031 + time-machine shell --manifest="${PWD}/contrib/guix/manifest.scm" \ + --container \ + --pure \ + --no-cwd \ + --share="$PWD"=/feather \ + --share="$DISTSRC_BASE"=/distsrc-base \ + --share="$OUTDIR_BASE"=/outdir-base \ + --share="$LOGDIR_BASE"=/logdir-base \ + --share="$GUIX_SIGS_REPO"=/guix-sigs \ + --expose="$(git rev-parse --git-common-dir)" \ + --expose="$(git -C "$GUIX_SIGS_REPO" rev-parse --git-common-dir)" \ + ${SOURCES_PATH:+--share="$SOURCES_PATH"} \ + --cores="$JOBS" \ + --keep-failed \ + --fallback \ + --link-profile \ + --user="user" \ + --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" \ + VERSION="$VERSION" \ + 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)" \ + LOGDIR="$(LOGDIR_BASE=/logdir-base && logdir_for_host "$HOST" codesigned)" \ + GUIX_SIGS_REPO=/guix-sigs \ + UNSIGNED_FILE="$(OUTDIR_BASE=/outdir-base && unsigned_file_for_host "$HOST")" \ + bash -c "cd /feather && 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 00000000..33fd9b78 --- /dev/null +++ b/contrib/guix/libexec/codesign.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Copyright (c) 2021-2022 The Bitcoin Core developers +# Copyright (c) 2024-2024 The Monero Project +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +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 +# shell`. It does make sense for at least `guix shell --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_FILE: ${UNSIGNED_FILE:?not set} + GUIX_SIGS_REPO: ${GUIX_SIGS_REPO:?not set} + DISTNAME: ${DISTNAME:?not set} + VERSION: ${VERSION:?not set} + HOST: ${HOST:?not set} + SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH:?not set} + DISTSRC: ${DISTSRC:?not set} + OUTDIR: ${OUTDIR:?not set} + LOGDIR: ${LOGDIR:?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 +} + +mkdir -p "$OUTDIR" + +mkdir -p "$DISTSRC" +( + cd "$DISTSRC" + + case "$HOST" in + *mingw32*) + infile_base="$(basename "$UNSIGNED_FILE")" + outfile_base="${infile_base/-unsigned}" + + # Codesigned *-unsigned.exe and output to OUTDIR + osslsigncode attach-signature \ + -in "$UNSIGNED_FILE" \ + -out "${OUTDIR}/$outfile_base" \ + -CAfile "$GUIX_ENVIRONMENT/etc/ssl/certs/ca-certificates.crt" \ + -sigin /guix-sigs/codesignatures/"${VERSION}"/"$outfile_base".pem + ;; + *) + exit 1 + ;; + esac +) # $DISTSRC + + +( + cd "$OUTDIR" + + case "$HOST" in + *mingw32.installer) + find . -print0 \ + | xargs -0r touch --no-dereference --date="@${SOURCE_DATE_EPOCH}" + find . \ + | sort \ + | zip -X@ "${OUTDIR}/${DISTNAME}-win-installer.zip" \ + || ( rm -f "${OUTDIR}/${DISTNAME}-win-installer.zip" && exit 1 ) + ;; + *mingw32*) + find . -print0 \ + | xargs -0r touch --no-dereference --date="@${SOURCE_DATE_EPOCH}" + find . \ + | sort \ + | zip -X@ "${OUTDIR}/${DISTNAME}-win.zip" \ + || ( rm -f "${OUTDIR}/${DISTNAME}-win.zip" && exit 1 ) + ;; + esac +) + +rm -rf "$ACTUAL_OUTDIR" +mv --no-target-directory "$OUTDIR" "$ACTUAL_OUTDIR" \ + || ( rm -rf "$ACTUAL_OUTDIR" && exit 1 ) + +( + cd /outdir-base + mkdir -p "$LOGDIR"/codesigned + { + find "$ACTUAL_OUTDIR" -type f + } | xargs realpath --relative-base="$PWD" \ + | xargs sha256sum \ + | sort -k2 \ + | sponge "$LOGDIR"/SHA256SUMS.part +) diff --git a/contrib/guix/manifest.scm b/contrib/guix/manifest.scm index 403591cf..50100f89 100644 --- a/contrib/guix/manifest.scm +++ b/contrib/guix/manifest.scm @@ -263,7 +263,7 @@ chain for " target " development.")) (define osslsigncode (package (name "osslsigncode") - (version "2.5") + (version "2.9") (source (origin (method git-fetch) (uri (git-reference @@ -271,9 +271,9 @@ chain for " target " development.")) (commit version))) (sha256 (base32 - "1j47vwq4caxfv0xw68kw5yh00qcpbd56d7rq6c483ma3y7s96yyz")))) + "160dwjzpwaxism6r7ryn7dgfq78rk3nkbg9m2kwg512hhn20blqh")))) (build-system cmake-build-system) - (inputs (list openssl)) + (inputs (list openssl zlib)) (home-page "https://github.com/mtrojnar/osslsigncode") (synopsis "Authenticode signing and timestamping tool") (description "osslsigncode is a small tool that implements part of the @@ -336,7 +336,7 @@ thus should be able to compile on most platforms where these exist.") xcb-util-wm ) (let ((target (getenv "HOST"))) - (cond ((string-suffix? "-mingw32" target) + (cond ((string-contains target "-mingw32") ;; Windows (list gcc-toolchain-12