266 lines
6.4 KiB
Bash
Executable file
266 lines
6.4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: scripts/build-apt-repo.sh \
|
|
--repo-dir PATH \
|
|
--deb PATH \
|
|
--distribution NAME \
|
|
--component NAME \
|
|
--architecture NAME \
|
|
--origin TEXT \
|
|
--label TEXT \
|
|
--description TEXT \
|
|
--signing-key KEY_ID \
|
|
[--gpg-passphrase PASS] \
|
|
[--repo-url URL]
|
|
|
|
Builds and signs a simple APT repository tree rooted at PATH.
|
|
Repeated --deb arguments are allowed.
|
|
USAGE
|
|
}
|
|
|
|
require_tool() {
|
|
local tool="$1"
|
|
if ! command -v "$tool" >/dev/null 2>&1; then
|
|
echo "Required tool not found: $tool" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
repo_dir=""
|
|
distribution="stable"
|
|
component="main"
|
|
architecture="amd64"
|
|
origin="GitComet"
|
|
label="GitComet"
|
|
description="GitComet APT repository"
|
|
signing_key=""
|
|
gpg_passphrase=""
|
|
repo_url=""
|
|
declare -a deb_files=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--repo-dir)
|
|
repo_dir="${2:-}"
|
|
shift 2
|
|
;;
|
|
--deb)
|
|
deb_files+=("${2:-}")
|
|
shift 2
|
|
;;
|
|
--distribution)
|
|
distribution="${2:-}"
|
|
shift 2
|
|
;;
|
|
--component)
|
|
component="${2:-}"
|
|
shift 2
|
|
;;
|
|
--architecture)
|
|
architecture="${2:-}"
|
|
shift 2
|
|
;;
|
|
--origin)
|
|
origin="${2:-}"
|
|
shift 2
|
|
;;
|
|
--label)
|
|
label="${2:-}"
|
|
shift 2
|
|
;;
|
|
--description)
|
|
description="${2:-}"
|
|
shift 2
|
|
;;
|
|
--signing-key)
|
|
signing_key="${2:-}"
|
|
shift 2
|
|
;;
|
|
--gpg-passphrase)
|
|
gpg_passphrase="${2:-}"
|
|
shift 2
|
|
;;
|
|
--repo-url)
|
|
repo_url="${2:-}"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown arg: $1" >&2
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$repo_dir" || -z "$signing_key" || ${#deb_files[@]} -eq 0 ]]; then
|
|
echo "--repo-dir, at least one --deb, and --signing-key are required." >&2
|
|
usage
|
|
exit 2
|
|
fi
|
|
|
|
if ! [[ "$distribution" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
|
|
echo "Invalid --distribution '$distribution'." >&2
|
|
exit 2
|
|
fi
|
|
|
|
if ! [[ "$component" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
|
|
echo "Invalid --component '$component'." >&2
|
|
exit 2
|
|
fi
|
|
|
|
if ! [[ "$architecture" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
|
|
echo "Invalid --architecture '$architecture'." >&2
|
|
exit 2
|
|
fi
|
|
|
|
require_tool dpkg-deb
|
|
require_tool dpkg-scanpackages
|
|
require_tool gpg
|
|
require_tool gzip
|
|
require_tool md5sum
|
|
require_tool sha256sum
|
|
require_tool sha512sum
|
|
|
|
mkdir -p "$repo_dir"
|
|
repo_dir="$(cd "$repo_dir" && pwd)"
|
|
repo_url="${repo_url%/}"
|
|
|
|
for deb in "${deb_files[@]}"; do
|
|
if [[ ! -f "$deb" ]]; then
|
|
echo "Debian package not found: $deb" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
for deb in "${deb_files[@]}"; do
|
|
package_name="$(dpkg-deb -f "$deb" Package)"
|
|
package_arch="$(dpkg-deb -f "$deb" Architecture)"
|
|
|
|
if [[ "$package_arch" != "$architecture" && "$package_arch" != "all" ]]; then
|
|
echo "Package '$deb' has architecture '$package_arch', expected '$architecture' or 'all'." >&2
|
|
exit 1
|
|
fi
|
|
|
|
package_letter="$(printf '%s' "$package_name" | cut -c1 | tr '[:upper:]' '[:lower:]')"
|
|
if [[ -z "$package_letter" || ! "$package_letter" =~ ^[a-z0-9]$ ]]; then
|
|
package_letter="misc"
|
|
fi
|
|
|
|
pool_dir="${repo_dir}/pool/${component}/${package_letter}/${package_name}"
|
|
mkdir -p "$pool_dir"
|
|
install -m644 "$deb" "${pool_dir}/$(basename "$deb")"
|
|
done
|
|
|
|
dist_dir="${repo_dir}/dists/${distribution}"
|
|
binary_dir="${dist_dir}/${component}/binary-${architecture}"
|
|
|
|
rm -rf "$dist_dir"
|
|
mkdir -p "$binary_dir"
|
|
|
|
(
|
|
cd "$repo_dir"
|
|
dpkg-scanpackages --multiversion pool /dev/null > "${binary_dir#${repo_dir}/}/Packages"
|
|
)
|
|
|
|
if ! grep -q '^Package:' "${binary_dir}/Packages"; then
|
|
echo "Generated Packages index is empty." >&2
|
|
exit 1
|
|
fi
|
|
|
|
gzip -9 -n -c "${binary_dir}/Packages" > "${binary_dir}/Packages.gz"
|
|
|
|
release_file="${dist_dir}/Release"
|
|
|
|
write_release_checksums() {
|
|
local algo_name="$1"
|
|
local sum_cmd="$2"
|
|
|
|
echo "${algo_name}:"
|
|
while IFS= read -r rel_path; do
|
|
local full_path="${dist_dir}/${rel_path}"
|
|
local checksum
|
|
local size
|
|
checksum="$($sum_cmd "$full_path" | awk '{print $1}')"
|
|
size="$(wc -c < "$full_path" | tr -d '[:space:]')"
|
|
printf " %s %16s %s\n" "$checksum" "$size" "$rel_path"
|
|
done < <(
|
|
cd "$dist_dir"
|
|
find . -type f \
|
|
! -name 'InRelease' \
|
|
! -name 'Release' \
|
|
! -name 'Release.gpg' \
|
|
-printf '%P\n' \
|
|
| LC_ALL=C sort
|
|
)
|
|
}
|
|
|
|
# `Release` is parsed as a single deb822 paragraph. Blank lines would split
|
|
# the checksum stanzas into separate paragraphs and make APT ignore them.
|
|
{
|
|
echo "Origin: ${origin}"
|
|
echo "Label: ${label}"
|
|
echo "Suite: ${distribution}"
|
|
echo "Codename: ${distribution}"
|
|
echo "Date: $(LC_ALL=C date -Ru)"
|
|
echo "Architectures: ${architecture}"
|
|
echo "Components: ${component}"
|
|
echo "Description: ${description}"
|
|
write_release_checksums "MD5Sum" md5sum
|
|
write_release_checksums "SHA256" sha256sum
|
|
write_release_checksums "SHA512" sha512sum
|
|
} > "$release_file"
|
|
|
|
run_gpg() {
|
|
local -a args
|
|
args=(--batch --yes --pinentry-mode loopback --local-user "$signing_key")
|
|
|
|
if [[ -n "$gpg_passphrase" ]]; then
|
|
gpg "${args[@]}" --passphrase-fd 0 "$@" <<<"$gpg_passphrase"
|
|
else
|
|
gpg "${args[@]}" "$@"
|
|
fi
|
|
}
|
|
|
|
run_gpg --armor --detach-sign --output "${dist_dir}/Release.gpg" "$release_file"
|
|
run_gpg --clearsign --output "${dist_dir}/InRelease" "$release_file"
|
|
|
|
gpg --batch --yes --export-options export-minimal --output "${repo_dir}/gitcomet-archive-keyring.gpg" --export "$signing_key"
|
|
gpg --batch --yes --armor --export-options export-minimal --output "${repo_dir}/gitcomet-archive-keyring.asc" --export "$signing_key"
|
|
|
|
if [[ -n "$repo_url" ]]; then
|
|
cat > "${repo_dir}/gitcomet.sources" <<EOF
|
|
Types: deb
|
|
URIs: ${repo_url}
|
|
Suites: ${distribution}
|
|
Components: ${component}
|
|
Architectures: ${architecture}
|
|
Signed-By: /usr/share/keyrings/gitcomet-archive-keyring.gpg
|
|
EOF
|
|
|
|
cat > "${repo_dir}/gitcomet.list" <<EOF
|
|
deb [arch=${architecture} signed-by=/usr/share/keyrings/gitcomet-archive-keyring.gpg] ${repo_url} ${distribution} ${component}
|
|
EOF
|
|
|
|
cat > "${repo_dir}/README.txt" <<EOF
|
|
GitComet APT repository
|
|
|
|
Install:
|
|
curl -fsSL ${repo_url}/gitcomet-archive-keyring.gpg | sudo tee /usr/share/keyrings/gitcomet-archive-keyring.gpg >/dev/null
|
|
curl -fsSL ${repo_url}/gitcomet.sources | sudo tee /etc/apt/sources.list.d/gitcomet.sources >/dev/null
|
|
sudo apt-get update
|
|
sudo apt-get install gitcomet
|
|
EOF
|
|
fi
|
|
|
|
echo "Built APT repository:"
|
|
echo " ${repo_dir}"
|
|
echo " ${binary_dir}/Packages"
|
|
echo " ${dist_dir}/InRelease"
|
|
echo " ${repo_dir}/gitcomet-archive-keyring.gpg"
|