Skip to content

Instantly share code, notes, and snippets.

@suominen
Last active December 11, 2023 16:36
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 suominen/69b65ba8fd66cf488db7fe5c233d4415 to your computer and use it in GitHub Desktop.
Save suominen/69b65ba8fd66cf488db7fe5c233d4415 to your computer and use it in GitHub Desktop.
Manage a deb repository
#!/bin/sh
# @(#)$KimmoSuominen: .cfg/home/bin/share/debrepo,v 1.12 2023/12/11 16:35:53 kim Exp $
#
# Copyright (c) 2022-2023 Kimmo Suominen
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
set -eu
umask 077
PATH=/usr/pkg/bin:/bin:/usr/bin
export PATH
PROG="${0##*/}"
CONFFILE="${HOME}/.config/${PROG}/config"
die()
{
echo "${PROG}: ${@}" 1>&2
exit 1
}
generate_key()
{
gpg --batch --generate-key <<-EOF
%echo Generating a new GPG key
Key-Type: RSA
Key-Length: 4096
Name-Real: ${KEY_NAME}
Name-Email: ${KEY_EMAIL}
Expire-Date: 0
%no-protection
%commit
EOF
}
list_archs()
{
echo amd64 arm64
}
list_codenames()
{
local os
os="${1}"
case "${os}" in
debian) echo trixie bookworm bullseye buster;;
ubuntu) echo mantic lunar kinetic jammy focal bionic;;
esac
}
list_files()
{
local comp root
root="${1}"
comp="${2}"
find "${root}/${comp}" -type f -print \
| sed -e "s,^${root}/,,"
}
msg()
{
echo "${PROG}: ${@}" 1>&2
}
output_file_hashes()
{
local algo file hash root size
algo="${1}"
root="${2}"
while read file
do
hash="$("${algo}"sum "${root}/${file}" | cut -d' ' -f1)"
size="$(stat -c '%s' "${root}/${file}")"
printf ' %s %s %s\n' "${hash}" "${size}" "${file}"
done
}
output_release_files()
{
local algo comp files root
root="${1}"
comp="${2}"
files="$(list_files "${root}" "${comp}")"
for algo in md5 sha1 sha256 # sha512
do
case "${algo}" in
md5) echo 'MD5Sum:';;
sha1) echo 'SHA1:';;
sha256) echo 'SHA256:';;
sha512) echo 'SHA512:';;
esac
echo "${files}" \
| output_file_hashes "${algo}" "${root}"
done
}
output_release_head()
{
local codename os
os="${1}"
codename="${2}"
sed -e '/^ *$/d' <<-EOF
Origin: ${REPO_ORIGIN}
${REPO_LABEL:+Label: ${REPO_LABEL}}
Suite: ${REPO_SUITE}
Codename: ${codename}
Components: main
Architectures: $(list_archs "${os}")
${REPO_NOT_AUTOMATIC:+NotAutomatic: ${REPO_NOT_AUTOMATIC}}
${REPO_BUT_AUTOMATIC:+ButAutomaticUpgrades: ${REPO_BUT_AUTOMATIC}}
Date: ${DATE}
${REPO_DESCRIPTION:+Description: ${REPO_DESCRIPTION}}
EOF
}
rebuild_repo()
{
local arch bin codename comp dist os pool
os="${1}"
codename="${2}"
comp="main"
dist="dists/${codename}"
pool="pool/${codename}/${comp}"
if [ -d "${pool}" ]
then
for arch in $(list_archs "${os}" "${codename}")
do
bin="${dist}/${comp}/binary-${arch}"
mkdir -p "${bin}"
dpkg-scanpackages -m -a "${arch}" "${pool}" \
> "${bin}/.Packages.new"
if cmp -s "${bin}/.Packages.new" "${bin}/Packages"
then
rm -f "${bin}/.Packages.new"
else
rm -f "${bin}/Packages"*
mv "${bin}/.Packages.new" "${bin}/Packages"
xz --keep "${bin}/Packages"
fi
done
[ -f "${dist}/Release" ] || echo Empty > "${dist}/Release"
{
output_release_head "${os}" "${codename}"
output_release_files "${dist}" "${comp}"
} > "${dist}/.Release.new"
grep -v '^Date: ' "${dist}/.Release.new" \
> "${dist}/.Release.new.tmp"
grep -v '^Date: ' "${dist}/Release" \
> "${dist}/.Release.old.tmp"
if cmp -s "${dist}/.Release.new.tmp" "${dist}/.Release.old.tmp"
then
rm -f "${dist}/.Release."*
else
rm -f "${dist}/Release"* "${dist}/InRelease"
mv "${dist}/.Release.new" "${dist}/Release"
rm -f "${dist}/.Release."*
gpg --default-key "${KEY_NAME}" -abs \
< "${dist}/Release" \
> "${dist}/Release.gpg"
gpg --default-key "${KEY_NAME}" -abs --clearsign \
< "${dist}/Release" \
> "${dist}/InRelease"
fi
fi
}
release()
{
local codename os root
root="${1}"
umask 022
for os in debian ubuntu
do
for codename in $(list_codenames "${os}")
do
[ -d "${root}/${os}" ] || continue
( cd "${root}/${os}" && rebuild_repo "${os}" "${codename}" )
done
done
}
usage()
{
cat <<EOF
Usage: ${PROG} -r repo command
Options:
-h Show this usage
-r Select the repository to work on
Commands:
- dist Sync local repo to web server
- edit-key Edit GPG key (e.g. to change pass phrase)
- export-private-key Output secret GPG key in ASCII armor
- export-public-key Output public GPG key in ASCII armor
- export-public-key-bin Output public GPG key in GPG format
- generate-key Generate a new GPG key
- help Show this usage
- list-keys List GPG keys
- release Rebuild the repository metadata
- usage Show this usage
Default values can be set and changed in the configuration file.
Using configuration file: ${CONFFILE} (${CONFSTATUS}).
EOF
# 345678901234567890123456789012345678901234567890123456789012345678901234567890
}
DATE="$(date -Ru)"
YEAR="$(date +%Y)"
if [ -f "${CONFFILE}" ]
then
if [ -r "${CONFFILE}" ]
then
. "${CONFFILE}"
CONFSTATUS="found, readable"
else
CONFSTATUS="found, unreadable"
fi
else
CONFSTATUS="not found"
fi
# Don't allow setting these in config file
repo=
while getopts hr: opt
do
case "${opt}" in
h) usage; exit 0;;
r) repo="${OPTARG}";;
*) usage 1>&2; exit 1;;
esac
done
shift $((${OPTIND} - 1))
case "${repo}" in
'')
die 'Use -r to specify a repo'
;;
esac
if [ -f "${CONFFILE}-${repo}" ]
then
if [ -r "${CONFFILE}-${repo}" ]
then
. "${CONFFILE}-${repo}"
else
die "${CONFFILE}-${repo}: Permission denied"
fi
else
die "${CONFFILE}-${repo}: No such file"
fi
: ${GNUPGHOME}
: ${KEY_EMAIL}
: ${KEY_NAME}
: ${LOCAL_REPO}
: ${REMOTES}
: ${REPO_ORIGIN}
: ${REPO_SUITE}
export GNUPGHOME
GPG_TTY="$(tty)"
export GPG_TTY
gpgconf --kill gpg-agent
trap "gpgconf --kill gpg-agent" 0 1 2 15
case "${1:-usage}" in
dist)
if [ ! -d "${LOCAL_REPO}" ]
then
die "${LOCAL_REPO}: No such directory"
fi
for remote in ${REMOTES}
do
msg "Publishing to ${remote}"
case "${remote}" in
s3:*)
aws \
${AWS_PROFILE:+--profile="${AWS_PROFILE}"} \
s3 sync \
"${LOCAL_REPO}/" \
"${remote}/" \
--delete
eval aws --no-paginate \
${AWS_PROFILE:+--profile="'${AWS_PROFILE}'"} \
cloudfront create-invalidation \
--distribution-id "'${CF_DISTRIBUTION_ID}'" \
--paths "${CF_DISTRIBUTION_PATHS}"
;;
*)
rsync -crstOogp --delete \
--chmod Fgo-w,a+rX \
"${LOCAL_REPO}/" \
"${remote}/"
;;
esac
done
;;
edit-key)
gpg --edit-key "${KEY_NAME}"
;;
export-private-key)
gpg --armor --export-secret-keys "${KEY_NAME}"
;;
export-public-key)
gpg --armor --export "${KEY_NAME}"
;;
export-public-key-bin)
gpg --export "${KEY_NAME}"
;;
generate-key)
if [ -d "${GNUPGHOME}" ]
then
die "${GNUPGHOME} already exists!"
fi
mkdir -p "${GNUPGHOME}"
generate_key
;;
help)
usage
;;
list-keys)
gpg --batch --list-keys
;;
release)
release "${LOCAL_REPO}"
;;
usage)
usage
;;
*)
die "Unknown command: ${1}"
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment