#!/bin/bash # # makerepropkg - rebuild a package to see if it is reproducible # # Copyright (c) 2019 by Eli Schwartz <eschwartz@archlinux.org> # # SPDX-License-Identifier: GPL-3.0-or-later m4_include(lib/common.sh) m4_include(lib/archroot.sh) source /usr/share/makepkg/util/config.sh source /usr/share/makepkg/util/message.sh declare -A buildinfo declare -a buildenv buildopts installed installpkgs archiveurl='https://archive.archlinux.org/packages' buildroot=/var/lib/archbuild/reproducible diffoscope=0 chroot=$USER [[ -n ${SUDO_USER:-} ]] && chroot=$SUDO_USER [[ -z "$chroot" || $chroot = root ]] && chroot=copy parse_buildinfo() { local line var val while read -r line; do var="${line%% = *}" val="${line#* = }" case ${var} in buildenv) buildenv+=("${val}") ;; options) buildopts+=("${val}") ;; installed) installed+=("${val}") ;; *) buildinfo["${var}"]="${val}" ;; esac done } get_pkgfile() { local cdir=${cache_dirs[0]} local pkgfilebase=${1} local mode=${2} local pkgname=${pkgfilebase%-*-*-*} local pkgfile ext # try without downloading if [[ ${mode} != localonly ]] && get_pkgfile "${pkgfilebase}" localonly; then return 0 fi for ext in .zst .xz ''; do pkgfile=${pkgfilebase}.pkg.tar${ext} for c in "${cache_dirs[@]}"; do if [[ -f ${c}/${pkgfile} ]]; then cdir=${c} break fi done for f in "${pkgfile}" "${pkgfile}.sig"; do if [[ ! -f "${cdir}/${f}" ]]; then if [[ ${mode} = localonly ]]; then continue 2 fi msg2 "retrieving '%s'..." "${f}" >&2 curl -Llf -# -o "${cdir}/${f}" "${archiveurl}/${pkgname:0:1}/${pkgname}/${f}" || continue 2 fi done printf '%s\n' "file://${cdir}/${pkgfile}" return 0 done return 1 } get_makepkg_conf() { local fname=${1} local arch="${2}" local makepkg_conf="${3}" if ! buildtool_file=$(get_pkgfile "${fname}"); then error "failed to retrieve ${fname}" return 1 fi msg2 "using makepkg.conf from ${fname}" bsdtar xOqf "${buildtool_file/file:\/\//}" "usr/share/devtools/makepkg-${arch}.conf" > "${makepkg_conf}" return 0 } usage() { cat << __EOF__ usage: ${BASH_SOURCE[0]##*/} [options] <package_file> Run this script in a PKGBUILD dir to build a package inside a clean chroot while attempting to reproduce it. The package file will be used to derive metadata needed for reproducing the package, including the .PKGINFO as well as the buildinfo. For more details see https://reproducible-builds.org/ OPTIONS -d Run diffoscope if the package is unreproducible -c <dir> Set pacman cache -M <file> Location of a makepkg config file -l <chroot> The directory name to use as the chroot namespace Useful for maintaining multiple copies Default: $chroot -h Show this usage message __EOF__ } while getopts 'dM:c:l:h' arg; do case "$arg" in d) diffoscope=1 ;; M) archroot_args+=(-M "$OPTARG") ;; c) cache_dirs+=("$OPTARG") ;; l) chroot="$OPTARG" ;; h) usage; exit 0 ;; *|?) usage; exit 1 ;; esac done shift $((OPTIND - 1)) check_root [[ -f PKGBUILD ]] || { error "No PKGBUILD in current directory."; exit 1; } # without arguments, get list of packages from PKGBUILD if [[ -z $1 ]]; then mapfile -t pkgnames < <(source PKGBUILD; pacman -Sddp --print-format '%r/%n' "${pkgname[@]}") wait $! || { error "No package file specified and failed to retrieve package names from './PKGBUILD'." plain "Try '${BASH_SOURCE[0]##*/} -h' for more information." >&2 exit 1 } msg "Reproducing all pkgnames listed in ./PKGBUILD" set -- "${pkgnames[@]}" fi # check each package to see if it's a file, and if not, try to download it # using pacman -Sw, and get the filename from there splitpkgs=() for p in "$@"; do if [[ -f ${p} ]]; then splitpkgs+=("${p}") else pkgfile_remote=$(pacman -Sddp "${p}" 2>/dev/null) || { error "package name '%s' not in repos" "${p}"; exit 1; } pkgfile=${pkgfile_remote#file://} if [[ ! -f ${pkgfile} ]]; then msg "Downloading package '%s' into pacman's cache" "${pkgfile}" sudo pacman -Swdd --noconfirm --logfile /dev/null "${p}" || exit 1 pkgfile_remote=$(pacman -Sddp "${p}" 2>/dev/null) pkgfile="${pkgfile_remote#file://}" fi splitpkgs+=("${pkgfile}") fi done for f in "${splitpkgs[@]}"; do if ! bsdtar -tqf "${f}" .BUILDINFO >/dev/null 2>&1; then error "file is not a valid pacman package: '%s'" "${f}" exit 1 fi done if (( ${#cache_dirs[@]} == 0 )); then mapfile -t cache_dirs < <(pacman-conf CacheDir) fi ORIG_HOME=${HOME} IFS=: read -r _ _ _ _ _ HOME _ < <(getent passwd "${SUDO_USER:-$USER}") load_makepkg_config HOME=${ORIG_HOME} [[ -d ${SRCDEST} ]] || SRCDEST=${PWD} parse_buildinfo < <(bsdtar -xOqf "${splitpkgs[0]}" .BUILDINFO) export SOURCE_DATE_EPOCH="${buildinfo[builddate]}" PACKAGER="${buildinfo[packager]}" BUILDDIR="${buildinfo[builddir]}" BUILDTOOL="${buildinfo[buildtool]}" BUILDTOOLVER="${buildinfo[buildtoolver]}" PKGEXT=${splitpkgs[0]#${splitpkgs[0]%.pkg.tar*}} # nuke and restore reproducible testenv namespace="$buildroot/$chroot" lock 9 "${namespace}.lock" "Locking chroot namespace '%s'" "${namespace}" for copy in "${namespace}"/*/; do [[ -d ${copy} ]] || continue subvolume_delete_recursive "${copy}" done rm -rf --one-file-system "${namespace}" (umask 0022; mkdir -p "${namespace}") for fname in "${installed[@]}"; do if ! allpkgfiles+=("$(get_pkgfile "${fname}")"); then error "failed to retrieve ${fname}" exit 1 fi done trap 'rm -rf $TEMPDIR' EXIT INT TERM QUIT TEMPDIR=$(mktemp -d --tmpdir makerepropkg.XXXXXXXXXX) makepkg_conf="${TEMPDIR}/makepkg.conf" # anything before buildtool support is pinned to the last none buildtool aware release if [[ -z "${BUILDTOOL}" ]]; then get_makepkg_conf "devtools-20210202-3-any" "${CARCH}" "${makepkg_conf}" || exit 1 # prefere to assume devtools up until matching makepkg version so repository packages remain reproducible elif [[ "${BUILDTOOL}" = makepkg ]] && (( $(vercmp "${BUILDTOOLVER}" 6.0.1) <= 0 )); then get_makepkg_conf "devtools-20210202-3-any" "${CARCH}" "${makepkg_conf}" || exit 1 # all devtools builds elif [[ "${BUILDTOOL}" = devtools ]] && get_makepkg_conf "${BUILDTOOL}-${BUILDTOOLVER}" "${CARCH}" "${makepkg_conf}"; then true # fallback to current makepkg.conf else warning "Unknown buildtool (${BUILDTOOL}-${BUILDTOOLVER}), using fallback" makepkg_conf=@pkgdatadir@/makepkg-${CARCH}.conf fi printf '%s\n' "${allpkgfiles[@]}" | mkarchroot -M "${makepkg_conf}" -U "${archroot_args[@]}" "${namespace}/root" - || exit 1 # use makechrootpkg to prep the build directory makechrootpkg -r "${namespace}" -l build -- --packagelist || exit 1 # set detected makepkg.conf options { for var in PACKAGER BUILDDIR BUILDTOOL BUILDTOOLVER PKGEXT; do printf '%s=%s\n' "${var}" "${!var@Q}" done printf 'OPTIONS=(%s)\n' "${buildopts[*]@Q}" printf 'BUILDENV=(%s)\n' "${buildenv[*]@Q}" } >> "${namespace}/build"/etc/makepkg.conf install -d -o "${SUDO_UID:-$UID}" -g "$(id -g "${SUDO_UID:-$UID}")" "${namespace}/build/${BUILDDIR}" # kick off the build arch-nspawn "${namespace}/build" \ --bind="${PWD}:/startdir" \ --bind="${SRCDEST}:/srcdest" \ /chrootbuild -C --noconfirm --log --holdver --skipinteg ret=$? if (( ${ret} == 0 )); then msg2 "built succeeded! built packages can be found in ${namespace}/build/pkgdest" msg "comparing artifacts..." for pkgfile in "${splitpkgs[@]}"; do comparefiles=("${pkgfile}" "${namespace}/build/pkgdest/${pkgfile##*/}") if cmp -s "${comparefiles[@]}"; then msg2 "Package '%s' successfully reproduced!" "${pkgfile}" else ret=1 warning "Package '%s' is not reproducible. :(" "${pkgfile}" sha256sum "${comparefiles[@]}" if (( diffoscope )); then diffoscope "${comparefiles[@]}" fi fi done fi # return failure from chrootbuild, or the reproducibility status exit ${ret}