#!/bin/bash # makerepropkg - rebuild a package to see if it is reproducible # # Copyright (c) 2019 by Eli Schwartz # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # 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 chroot=testenv diffoscope=0 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 } usage() { cat << __EOF__ usage: ${BASH_SOURCE[0]##*/} [options] 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 Set pacman cache -M Location of a makepkg config file -h Show this usage message __EOF__ } while getopts 'dM:c:h' arg; do case "$arg" in d) diffoscope=1 ;; M) archroot_args+=(-M "$OPTARG") ;; c) cache_dirs+=("$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]}" PKGEXT=${splitpkgs[0]#${splitpkgs[0]%.pkg.tar*}} # nuke and restore reproducible testenv for copy in "${buildroot}"/*/; do [[ -d ${copy} ]] || continue subvolume_delete_recursive "${copy}" done rm -rf --one-file-system "${buildroot}" (umask 0022; mkdir -p "${buildroot}") for fname in "${installed[@]}"; do if ! allpkgfiles+=("$(get_pkgfile "${fname}")"); then error "failed to retrieve ${fname}" exit 1 fi done printf '%s\n' "${allpkgfiles[@]}" | mkarchroot -M @pkgdatadir@/makepkg-x86_64.conf -U "${archroot_args[@]}" "${buildroot}"/root - || exit 1 # use makechrootpkg to prep the build directory makechrootpkg -r "${buildroot}" -l "${chroot}" -- --packagelist || exit 1 # set detected makepkg.conf options { for var in PACKAGER BUILDDIR PKGEXT; do printf '%s=%s\n' "${var}" "${!var@Q}" done printf 'OPTIONS=(%s)\n' "${buildopts[*]@Q}" printf 'BUILDENV=(%s)\n' "${buildenv[*]@Q}" } >> "${buildroot}/${chroot}"/etc/makepkg.conf >> "${buildroot}/${chroot}"/etc/makepkg.conf install -d -o "${SUDO_UID:-$UID}" -g "$(id -g "${SUDO_UID:-$UID}")" "${buildroot}/${chroot}/${BUILDDIR}" # kick off the build arch-nspawn "${buildroot}/${chroot}" \ --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 ${buildroot}/${chroot}/pkgdest" msg "comparing artifacts..." for pkgfile in "${splitpkgs[@]}"; do comparefiles=("${pkgfile}" "${buildroot}/${chroot}/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}