summaryrefslogtreecommitdiff
path: root/src/makerepropkg.in
diff options
context:
space:
mode:
Diffstat (limited to 'src/makerepropkg.in')
-rw-r--r--src/makerepropkg.in270
1 files changed, 270 insertions, 0 deletions
diff --git a/src/makerepropkg.in b/src/makerepropkg.in
new file mode 100644
index 0000000..b271f25
--- /dev/null
+++ b/src/makerepropkg.in
@@ -0,0 +1,270 @@
+#!/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 makepkg_conf="${2}"
+ 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-x86_64.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" "${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" "${makepkg_conf}" || exit 1
+# all devtools builds
+elif [[ "${BUILDTOOL}" = devtools ]] && get_makepkg_conf "${BUILDTOOL}-${BUILDTOOLVER}" "${makepkg_conf}"; then
+ true
+# fallback to current makepkg.conf
+else
+ warning "Unknown buildtool (${BUILDTOOL}-${BUILDTOOLVER}), using fallback"
+ makepkg_conf=@pkgdatadir@/makepkg-x86_64.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}