Skip to content

Instantly share code, notes, and snippets.

@smoser
Last active December 1, 2020 21:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save smoser/87dc8fe5da16d585cd548ef85ff2daf5 to your computer and use it in GitHub Desktop.
Save smoser/87dc8fe5da16d585cd548ef85ff2daf5 to your computer and use it in GitHub Desktop.
rpm build in lxd

build rpm in lxd container

This should support building either fedora or centos git repos.

It supports downloading sources from both project's lookaside caches.

Example:

$ git clone https://git.centos.org/rpms/lvm2.git
$ cd lvm2.git
$ ./atx-rpm-build

That will

  • get centos 7 lxd (need to add some '--image' arg).
  • get source from lookaside
  • build source in container as 'build' user

TODO:

  • allow for '-ba' or '-bs' flags (or other) to rpmbuild
  • delete container by default and give flag to keep.

Notes

  • make-snapshot: if you want to make a snapshot of a conatiner use atx-rpm-build make-snapshot and then you can start from a better formed container.
#!/bin/bash
# https://gist.github.com/87dc8fe5da16d585cd548ef85ff2daf5.git
set -o pipefail
CR='
'
MYNAME="atx-rpm-build"
DEFAULT_IMAGE="images:centos/7"
DEFAULT_REMOTE="local"
SNAPSHOT_PREFIX="atx-build/"
VERBOSITY=${VERBOSITY:-0}
TEMP_D=""
error() { echo "$@" 1>&2; }
fail() { local r=$?; [ $r -eq 0 ] && r=1; failrc "$r" "$@"; }
failrc() { local r=$1; shift; [ $# -eq 0 ] || error "$@"; exit $r; }
Usage() {
cat <<EOF
Usage: ${0##*/} [ options ] [package-dir] [-- rpmbuild args]
build package inside lxc container. requires ctool in path.
package-dir defaults to '.' if there is a SPECS and SOURCES dir in .
If no args are given, '-ba' (build all) is used.
options:
-v | --verbose increase verbosity
-R | --remote use lxc remote (default $DEFAULT_REMOTE)
-i | --image X use lxc image X (default $DEFAULT_IMAGE)
--name N name the container N - default is random
subcmd:
make-snapshot [--remote=remote] [-v] [image] [snapshot-name]
This will start a container on 'remote' of 'image',
and update and stop it. Then it will publish it to
remote:snapshot-name
if 'image' not given use $DEFAULT_IMAGE
if 'snapshot-name' not provided, use
$SNAPSHOT_PREFIX<image-name>
after running 'make-snapshot' subsequent runs will by
default use the published snapshot.
EOF
}
bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; }
cleanup() {
[ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
}
debug() {
local level=${1}; shift;
[ "${level}" -gt "${VERBOSITY}" ] && return
error "${@}"
}
inside() {
local n="$1"
shift
debug 1 "[$n] running: $*"
ctool execute "--container=$n" "--user=root" -- "$@"
}
has_cmd() {
command -v "$1" >/dev/null 2>&1
}
install_deps() {
# install sudo and generic build-deps
local cmd="" extra=""
if has_cmd dnf; then
cmd="dnf"
extra="subscription-manager"
else
cmd="yum"
extra="yum-utils"
fi
local vid="" epelurl=""
vid=$( . /etc/os-release ; echo "$VERSION_ID" ) && {
epelurl="https://dl.fedoraproject.org/pub/epel/epel-release-latest-$vid.noarch.rpm"
}
debug 1 "installing sudo spectool git $extra $epelurl"
$cmd install --assumeyes sudo spectool git \
$extra ${epelurl:+"${epelurl}"} || {
error "Failed to install deps."
return 1
}
if [ "$vid" = "8" ]; then
dnf config-manager --set-enabled PowerTools || {
error "Failed to enable power tools."
return 1
}
fi
$cmd groupinstall --assumeyes "Development Tools" || {
error "Failed to install Development Tools"
return 1
}
return 0
}
disable_extras() {
# https://bugs.centos.org/view.php?id=15615
# there is no repodata at http://vault.centos.org/centos/7/extras/Source/
# so yum-builddep would fail.
# so we disable the 'extra-sources' repo.
local f="/etc/yum.repos.d/CentOS-Sources.repo"
if ! grep -q "^\[extras-source\]" "$f"; then
return 0
fi
mv "$f" "$f.dist" || return
oifs="$IFS"
IFS="$CR"
# very hackily comment out the '[extras-source]' stanza
while read line; do
case "$line" in
\[extras-source\])
echo "#$line"
while read line; do
echo "#$line"
[ "$line" = "" ] && break
done
;;
*) echo "$line";;
esac
done < "$f.dist" >"$f"
}
install_pkg_deps() {
local d="$1"
specdir_info "$d" || return 1
local topd="$_RET_TOPD" spec="$_RET_SPEC"
if has_cmd dnf; then
set -- dnf builddep
else
set -- yum-builddep
disable_extras
fi
set -- sudo "$@" --define "_topdir $topd" --assumeyes "$spec"
debug 1 "installing build dependencies with: $*"
"$@"
}
one_file() {
local found=""
for f in "$@"; do
[ -f "$f" ] || continue
[ -n "$found" ] && return 1
found="$f"
done
_RET="$found"
[ -n "$found" ]
}
dosum() {
local file="$1" summer="$2" expected="$3" out="" found=""
case ${summer%[Ss][Uu][Mm]} in
[Mm][Dd]5) summer=md5sum;;
[Ss][Hh][Aa]256) summer=sha256sum;;
[Ss][Hh][Aa]512) summer=sha512sum;;
[Ss][Hh][Aa]1) summer=sha1sum;;
esac
out=$("$summer" "$file") || return 1
found=${out% *}
_RET="$found"
[ $# -eq 2 ] && return 0
[ "$expected" = "$found" ]
}
download() {
local url="$1" fname="$2" hashname="$3" sum="$4" tmp=""
if [ ! -f "$fname" ]; then
local dir=$(dirname $fname) tmp="$fname.tmp"
[ -d "$dir" ] || mkdir -p "$dir" || return 1
debug 1 "downloading $url -> $fname"
curl --fail "$url" > "$tmp" || {
error "failed download of $url -> $tmp"
rm "$tmp"
return 1
}
fi
if [ -z "$sum" ]; then
debug 1 "no expected sum for $fname"
[ -z "$tmp" ] || mv "$tmp" "$fname"
return
fi
if ! dosum "${tmp:-${fname}}" "$hashname" "$sum"; then
error "$fname had incorrect $hashname: expected $sum found $_RET"
[ -z "$tmp" ] || rm "$tmp"
return 1
fi
found=${_RET}
[ -z "$tmp" ] || mv "$tmp" "$fname" || return 1
debug 1 "$fname had $hashname $sum"
return 0
}
get_fedora_sources() {
# packages are in <surl>/<pkgname>/<filename>/<lowercase-hashname>/<hash>/<filename>
# sources looks like:
# SHA512 (LVM2.2.03.09.tgz) = <hash>
local pkg="$1" sources="$2" out_d="$3"
local surl="https://src.fedoraproject.org/repo/pkgs"
local _ hashname fname hash url="" n="0"
while read hashname fname _ hash; do
n=$((n+1))
fname="${fname#[(]}"
fname="${fname%[)]}"
hashname=${hashname,,} # lower case
url="$surl/$pkg/$fname/$hashname/$hash/$fname"
debug 1 "source $fname from $url [$hash]"
download "$url" "$out_d/$fname" "$hashname" "$hash" || return 1
done <"$sources"
if [ $n -eq 0 ]; then
debug 0 "empty sources file? $sources"
fi
return 0
}
guess_current_centos_branch() {
# generate a list of all branches containing current HEAD
local remotename="" out="" remote=""
if [ -n "$CENTOS_BRANCH" ]; then
unset _RET
_RET="$CENTOS_BRANCH"
return
fi
if [ -n "${CENTOS_REMOTENAME}" ]; then
remotename="${CENTOS_REMOTENAME}"
else
out=$(git remote -v) || {
error "Failed to run 'git remote -v'"
return 1
}
remotename=$(echo "$out" |
awk '$2 ~ /centos.org/ { print $1; exit(0); }')
[ -n "$remotename" ] || {
error "did not find any remotes that look like centos"
error "set CENTOS_REMOTENAME"
error "Guessing branches: c7 c8"
_RET="c7 c8"
return 0
}
fi
local brname mb=""
unset _RET; _RET=""
for brname in c7 c8; do
if mb=$(git merge-base HEAD "$remotename/$brname" 2>/dev/null); then
_RET="$brname $_RET"
fi
done
_RET=${_RET% }
[ -n "$_RET" ] || {
error "Failed to find any branches on $remotename that look good."
error "Guessing branches: c7 c8"
_RET="c7 c8"
return 0
}
return 0
}
get_centos_sources() {
# centos lookaside looks like
# https://git.centos.org/sources/<pkg>/<branchname>/<hash>
# $ cat .lvm2.metadata
# 7a3834ca1ddaa7c4edc3863f18ec604f45722c65 SOURCES/LVM2.2.02.186.tgz
# dd96613e238f342641b5be8977ee8598662e8ab9 SOURCES/boom-0.9.tar.gz
local pkg="$1" mdfile="$2" out_d="$3" branch="$4" fbranch=""
local surl="https://git.centos.org/sources"
local _ hashname fpath fname url="" n=0
[ -f "$mdfile" ] || {
error "$mdfile: not a file"
return 1
}
if [ -n "$branch" ]; then
branches=( $branch )
else
guess_current_centos_branch || {
error "Failed to determine centos branch"
return 1
}
local branches=""
branches=( $_RET )
fi
while read hash fpath; do
n=$((n+1))
fname=${fpath##*/}
case ${#hash} in
128) hashname="sha512";;
64) hashname="sha256";;
40) hashname="sha1sum";;
32) hashname="md5sum";;
*) error "unknown hash $hash (len=${#hash})" \
"for $fpath in $sources";
return 1;;
esac
fbranch=""
for branch in "${branches[@]}"; do
url="$surl/$pkg/$branch/$hash"
debug 1 "source $fname from $url [$hash]"
download "$url" "$out_d/$fname" "$hashname" "$hash" &&
fbranch="$branch" && break
done
[ -n "$fbranch" ] || {
debug 1 "Failed to get source for $pkg $hash from any of ${branches[*]}"
return 1
}
done <"$mdfile"
if [ $n -eq 0 ]; then
debug 0 "empty metadata file file? $mdfile"
fi
return 0
}
get_sources() {
local d="$1" pkg=""
specdir_info "$d" || return 1
local sourced="$_RET_SOURCES" spec="$_RET_SPEC" topd="$_RET_TOPD"
if [ -f "$topd/sources" ]; then
# fedora style
pkg=$(awk '$1 == "Name:" { print $2; exit(0); }' "$spec")
[ -n "$pkg" ] || {
error "could not get pkg from $spec";
return 1;
}
get_fedora_sources "$pkg" "$topd/sources" "$sourced" || {
error "failed to download fedora sources."
return 1
}
elif one_file "$topd"/.*.metadata; then
# centos style (.lvm2.metadata)
local mdfile="$_RET"
pkg=${_RET##*/}
pkg=${pkg%.metadata}
pkg=${pkg#.}
echo "pkg=$pkg mdfile=$mdfile sourced=$sourced"
get_centos_sources "$pkg" "$mdfile" "$sourced" || {
error "failed to get centos source"
return 1
}
else
spectool --get-files "--directory=$sourced" "$spec" || {
error "Failed to get sources with spectool"
return 1
}
fi
}
specdir_info() {
local d="$1" specd="" f="" topd=""
[ -z "$d" ] && d="."
topd=$(realpath "$d") || {
error "failed to get realpath for $d"
return 1
}
if [ -d "$topd/SPECS" ]; then
specd="$topd/SPECS"
else
specd="$topd"
fi
one_file "$specd"/*.spec || {
error "did not find exactly one spec in $specd" "$specd"/*.spec
return 1
}
_RET_TOPD="$topd"
_RET_SPEC="$_RET"
_RET_SPECD="$specd"
[ -d "$topd/SOURCES" ] && _RET_SOURCES="$topd/SOURCES" ||
_RET_SOURCES="$topd"
}
build() {
[ $# -eq 0 ] && set -- .
local d="$1"
specdir_info "$d" || return 1
shift
if [ $# -eq 0 ]; then
set -- -ba
fi
local sourced="$_RET_SOURCES" topd="$_RET_TOPD"
local spec="$_RET_SPEC" specd="$_RET_SPECD"
if [ "$specd" = "$topd" ]; then
ln -snf . SPECS || {
error "failed to link SPECS to . in fedora style build"
return 1
}
fi
if [ "$sourced" = "$topd" ]; then
ln -snf . SOURCES || {
error "failed to link SOURCES to . in fedora style build"
return 1
}
fi
debug 0 "building: rpmbuild --define \"_topdir \$PWD\" $* $spec"
set -- rpmbuild --define "_topdir $PWD" "$@" "$spec"
"$@" | tee build.log
}
wait_for_boot() {
local tests="0"
while :; do
is_system_up "$tests" && return 0
[ $tests -gt 20 ] && return 1
echo -n .
sleep 1
tests=$((tests+1))
done
}
is_system_up() {
local s="" num="$1"
s=$(systemctl is-system-running 2>&1);
_RET="$? $s"
case "$s" in
initializing|starting) return 1;;
*[Ff]ailed*connect*bus*)
# warn if not the first run.
[ "$num" -lt 5 ] ||
error "Failed to connect to systemd bus [${_RET%% *}]";
return 1;;
esac
s=$(systemctl is-active network.service 2>&1)
_RET="$? $s"
case "$s" in
initializing|starting|activating) return 1;;
esac
return 0
}
get_image_name() {
local remote="$1" image="$2"
[ -n "$image" ] && {
_RET="$image"
return 0
}
# check to see if a snapshot of default exists.
local def="${DEFAULT_IMAGE}" snapname="" out=""
# this ends up looking like atx-build/centos/7
snapname=${SNAPSHOT_PREFIX}${def##*:}
_RET="$def"
out=$(lxc image show "$remote:$snapname" 2>&1) && {
debug 1 "using snapshot $remote:$snapname of $def"
_RET="$remote:$snapname"
}
return 0
}
start_container(){
# start a container, wait
local out="" remote="$1" name="$2" image="$3" install_deps="${3:-true}"
get_image_name "$remote" "$image" || {
error "failed to get default image"
return 1
}
image="$_RET"
local rname="$remote:$name"
debug 1 "lxc init $image $rname"
out=$(lxc init "$image" "$rname") || {
error "failed: lxc init $image $rname"
return 1
}
if [ -z "$name" ]; then
name=$(echo "$out" | sed -n 's/.*Instance name is: \([-a-z]*\).*/\1/p')
[ -n "$name" ] || {
error "failed to start container from '$image'"
error "init output looked like '$out'"
return 1
}
rname="$remote:$name"
fi
debug 0 "container $rname started from $image"
# push self inside
lxc file push "$0" "$rname/usr/bin/$MYNAME" || {
error "Failed to insert $0 into $name"
return 1
}
lxc start "$rname" || {
error "failed to start container '$name'"
return 1
}
inside "$rname" "$MYNAME" wait-for-boot || {
error "Failed to wait for $name to boot"
return 1
}
if [ "$install_deps" != "false" ]; then
# install deps
inside "$rname" "$MYNAME" install-deps || {
error "Failed to install deps in $name."
return 1
}
fi
_RET="$name"
}
main() {
local short_opts="hi:r:v"
local long_opts="help,name:,remote:,image:,verbose"
local getopt_out=""
getopt_out=$(getopt --name "${0##*/}" \
--options "${short_opts}" --long "${long_opts}" -- "$@") &&
eval set -- "${getopt_out}" ||
{ bad_Usage; return; }
local cur="" next="" image="" buildu="build"
local name="" remote="local" rname=""
while [ $# -ne 0 ]; do
cur="$1"; next="$2";
case "$cur" in
-h|--help) Usage ; exit 0;;
-i|--image) image="$next"; shift;;
-R|--remote) remote="$next"; shift;;
-v|--verbose) VERBOSITY=$((VERBOSITY+1));;
--name) name="$next"; shift;;
--) shift; break;;
esac
shift;
done
[ $# -eq 0 ] && set -- .
assert_ctool || fail
local pkg_d="$1" topd=""
shift
local build_args=""
build_args=( "$@" )
specdir_info "${pkg_d}" || return 1
spec=${_RET_SPEC}
topd=${_RET_TOPD}
debug 0 "Building $spec in $topd"
start_container "$remote" "$name" "$image" || {
error "Failed to start container from $image on $remote"
return 1
}
name="$_RET"
rname="$remote:$name"
# if we can get sources externally... just try
local gotsource=false
"$0" get-sources "$topd" && gotsource=true ||
debug 0 "failed to get sources outside. will try again inside."
# add the user ('build'). it gets sudo access also.
ctool add-user "--container=$rname" "$buildu" || {
error "Failed to add user $buildu"
return 1
}
# copy source in from topd
local bname="${topd##*/}"
debug 1 "copying source to $rname"
tar -C "$topd/.." -cf - "${bname}/" |
ctool execute "--container=$rname" "--user=$buildu" tar -xf - || {
error "Failed to copy source into $name from $topd"
return
}
local ctopd="/home/$buildu/$bname"
# install per-package build-deps
inside "$rname" "$MYNAME" install-pkg-deps "$ctopd" || {
error "Failed to install package deps"
return 1
}
local msg="to enter: ctool execute --container=$rname"
msg="$msg --user=$buildu --dir=$ctopd"
if [ "$gotsource" != "true" ]; then
# get source files
ctool execute "--container=$rname" "--user=$buildu" -- \
"$MYNAME" get-sources "$ctopd" || {
error "Failed to get sources inside $name"
error "$msg"
return 1
}
fi
ctool execute "--container=$rname" "--user=$buildu" "--dir=$ctopd" -- \
"$MYNAME" build . "${build_args[@]}" || {
error "Failed to build inside $name as $buildu in $ctopd"
error "$msg"
return 1
}
ctool execute "--container=$rname" "--user=$buildu" "--dir=$ctopd" -- \
"$MYNAME" "copy-out" | tar -xf - || {
error "Failed to copy build info out."
return 1
}
debug 0 "$msg"
}
copy_out() {
local d="$1"
specdir_info "$d" || return 1
local sourced="$_RET_SOURCES" spec="$_RET_SPEC" topd="$_RET_TOPD"
local dirs="" f=""
dirs=()
for f in "$topd/"{SRPMS,RPMS,build.log}; do
[ -e "$f" ] && dirs[${#dirs[@]}]="${f#$topd/}"
done
if [ "${#dirs[@]}" -eq 0 ]; then
error "No build ouput found in $topd"
return 1
fi
tar -C "$topd" -cf - "${dirs[@]}"
}
make_snapshot() {
local short_opts="hR:v"
local long_opts="help,remote:,verbose"
local getopt_out=""
getopt_out=$(getopt --name "${0##*/}" \
--options "${short_opts}" --long "${long_opts}" -- "$@") &&
eval set -- "${getopt_out}" ||
{ bad_Usage; return; }
local cur="" next="" buildu="build"
local name="" remote="local"
while [ $# -ne 0 ]; do
cur="$1"; next="$2";
case "$cur" in
-h|--help) Usage ; exit 0;;
-R|--remote) remote=$next; shift;;
-v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
--) shift; break;;
esac
shift;
done
[ $# -lt 2 ] || {
bad_Usage "got $# args, expected 0 or 1 ($*)"
return 1
}
local image="${1:-${DEFAULT_IMAGE}}" snapname="$2"
local out="" rname=""
[ -z "$snapname" ] && snapname="${SNAPSHOT_PREFIX}${image#*:}"
assert_ctool || fail
debug 0 "creating snapshot of $image -> $remote:$snapname"
if out=$(lxc image show "$remote:$snapname" 2>&1); then
error "image $remote:snapname exists, remove it first:"
error " lxc image alias delete $remote:$snapname"
return 1
fi
start_container "$remote" "" "$image" || {
error "Starting container failed (image=$image)"
return 1
}
name="$_RET"
rname="$remote:$name"
lxc stop "$rname" || {
error "Failed to stop $name"
return 1
}
debug 1 "publishing $rname -> $remote as $snapname"
lxc publish "$rname" "$remote:" "--alias=$snapname" || {
error "failed: lxc publish $name --alias=$snapname"
return 1
}
debug 0 "Published updated image of $image as $remote:$snapname"
lxc delete --force "$rname"
}
assert_ctool() {
command -v ctool >/dev/null || {
local url="https://github.com/canonical/uss-tableflip/blob/master/doc/ctool.md"
error "no ctool. Get it. its useful."
error "$url"
return 1
}
}
case "$1" in
copy-out|\
install-deps|install-pkg-deps|get-sources|wait-for-boot|build|make-snapshot)
fn=${1//-/_}
shift
"$fn" "$@"
;;
*) main "$@";;
esac
# vi: ts=4 expandtab
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment