armbian-build/tools/repository/repo.sh
Igor Pecovnik 63fe441107
fix: always publish main component even if utils/desktop are empty
This fixes the case where repositories like debs-beta only have packages
in the main/common component (e.g., sid with only kernel packages).
Previously, the merge command would skip publishing if both utils and
desktop repos were empty, resulting in an incomplete repository.

Now we always publish at minimum the main/common component, ensuring all
distributions with any packages get properly published.
2026-01-11 09:30:49 +01:00

1405 lines
54 KiB
Bash
Executable File

#!/usr/bin/env bash
# Global variables
DRY_RUN=false # Full dry-run: don't make any repository changes
KEEP_SOURCES=false # Keep source packages when adding to repo (don't delete)
SINGLE_RELEASE="" # Process only a single release (for GitHub Actions parallel workflow)
FORCE_ADD=false # Force re-adding packages even if they already exist in repo
FORCE_PUBLISH=true # Force publishing even when no packages to add
# Logging function - uses syslog, view logs with: journalctl -t repo-management -f
# Arguments:
# $* - Message to log
log() {
logger -t repo-management "$*"
}
# Execute aptly command and check for errors
# Exits with status 1 if the command fails (unless in dry-run mode)
# Arguments:
# $* - Aptly command to execute (without 'aptly' prefix)
run_aptly() {
if [[ "$DRY_RUN" == true ]]; then
log "[DRY-RUN] Would execute: aptly $*"
return 0
fi
if ! aptly "$@"; then
local exit_code=$?
log "ERROR: aptly $* failed with exit code $exit_code"
exit 1
fi
}
# Drop published repositories that are no longer supported
# Identifies and removes published repositories for releases that are no longer
# in config/distributions/*/support (excluding 'eos')
# Arguments:
# $1 - "all" to drop all published repositories, otherwise drops only unsupported ones
drop_unsupported_releases() {
local supported_releases=()
local published_repos=()
local repos_to_drop=()
# Determine which releases should be kept
if [[ "$1" == "all" ]]; then
log "Cleanup: dropping all published repositories"
supported_releases=()
else
log "Cleanup: dropping unsupported releases"
supported_releases=($(grep -rw config/distributions/*/support | cut -d"/" -f3))
fi
# Get currently published repositories
published_repos=($(aptly publish list -config="${CONFIG}" --raw | sed "s/. //g"))
# Find repos to drop (published but not supported)
for repo in "${published_repos[@]}"; do
local should_keep=false
for supported in "${supported_releases[@]}"; do
[[ "$repo" == "$supported" ]] && { should_keep=true; break; }
done
[[ "$should_keep" == false ]] && repos_to_drop+=("$repo")
done
# Drop the identified repositories
for repo in "${repos_to_drop[@]}"; do
run_aptly publish drop -config="${CONFIG}" "${repo}"
done
}
# Display contents of all repositories
# Shows packages in the common repository and release-specific repositories (utils, desktop)
# In single-release mode, shows content from isolated database
# Otherwise, shows content from main database and any existing isolated databases
# Uses global DISTROS array for iteration, or discovers repos automatically if DISTROS is empty
showall() {
echo "Displaying common repository contents"
aptly repo show -with-packages -config="${CONFIG}" common 2>/dev/null | tail -n +7
# If DISTROS array is empty, discover repos from the database
local releases_to_show=("${DISTROS[@]}")
if [[ ${#DISTROS[@]} -eq 0 ]]; then
# First, discover releases from isolated databases
local all_repos=()
if [[ -d "$output" ]]; then
for isolated_dir in "$output"/aptly-isolated-*; do
if [[ -d "$isolated_dir" ]]; then
local release_name=$(basename "$isolated_dir" | sed 's/aptly-isolated-//')
all_repos+=("$release_name")
fi
done
fi
# Also get repos from main database (for non-isolated repos)
local main_repos
main_repos=($(aptly repo list -config="${CONFIG}" -raw 2>/dev/null | awk '{print $NF}' | grep -E '^.+-(utils|desktop)$' | sed 's/-(utils|desktop)$//' | sort -u))
# Merge and deduplicate
all_repos+=("${main_repos[@]}")
releases_to_show=($(echo "${all_repos[@]}" | tr ' ' '\n' | sort -u))
fi
for release in "${releases_to_show[@]}"; do
# In single-release mode, only show that specific release from the isolated database
if [[ -n "$SINGLE_RELEASE" ]]; then
if [[ "$release" != "$SINGLE_RELEASE" ]]; then
continue
fi
fi
# Check if there's an isolated database for this release
local isolated_db="${output}/aptly-isolated-${release}"
local show_config="$CONFIG"
if [[ -d "$isolated_db" ]]; then
# Create temporary config for the isolated database
local temp_config
temp_config="$(mktemp)"
sed 's|"rootDir": ".*"|"rootDir": "'$isolated_db'"|g' tools/repository/aptly.conf > "$temp_config"
show_config="$temp_config"
fi
# Show utils repo if it exists
if aptly repo show -config="${show_config}" "${release}-utils" &>/dev/null; then
echo "Displaying repository contents for $release-utils"
aptly repo show -with-packages -config="${show_config}" "${release}-utils" | tail -n +7
fi
# Show desktop repo if it exists
if aptly repo show -config="${show_config}" "${release}-desktop" &>/dev/null; then
echo "Displaying repository contents for $release-desktop"
aptly repo show -with-packages -config="${show_config}" "${release}-desktop" | tail -n +7
fi
# Clean up temp config if we created one
if [[ -n "$temp_config" && -f "$temp_config" ]]; then
rm -f "$temp_config"
fi
done
}
# Add packages to an aptly repository component
# Processes .deb files from a source directory, optionally repacking BSP packages
# to pin kernel versions, then adds them to the specified repository
# Arguments:
# $1 - Repository component name (e.g., "common", "jammy-utils")
# $2 - Subdirectory path relative to input folder (e.g., "", "/extra/jammy-utils")
# $3 - Description (unused, for documentation only)
# $4 - Base input folder containing packages
adding_packages() {
local component="$1"
local subdir="$2"
local input_folder="$4"
local package_dir="${input_folder}${subdir}"
# Check if any .deb files exist in the directory
if ! find "$package_dir" -maxdepth 1 -type f -name "*.deb" 2> /dev/null | grep -q .; then
return 0
fi
# Get list of packages already in repo for deduplication check
# Use associative array for O(1) lookup performance
local -A repo_packages_map
if [[ "$FORCE_ADD" != true ]]; then
log "Building package list from $component for deduplication check..."
# Read aptly output line by line and parse properly
# aptly output format: " name_version_arch" (has leading spaces)
while IFS= read -r line; do
[[ -z "$line" ]] && continue
# Trim leading/trailing whitespace from line
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
[[ -z "$line" ]] && continue
# aptly repo show -with-packages outputs packages as: name_version_arch
# Split by underscore to get name, version, arch
# But version can contain underscores (e.g., 25.11.0-trunk.502), so we need to be careful
# Format: name_version_arch where arch is last field, version is everything before arch after name
local name version arch
# Get architecture (last field after last underscore)
arch="${line##*_}"
# Remove architecture from line to get name_version
local temp="${line%_*}"
# Get package name (first field before first underscore)
name="${temp%%_*}"
# Get version (everything between first and last underscore)
version="${temp#*_}"
[[ -z "$name" || -z "$version" || -z "$arch" ]] && continue
repo_packages_map["${name}|${version}|${arch}"]=1
done < <(aptly repo show -with-packages -config="${CONFIG}" "$component" 2>/dev/null | tail -n +7)
log "Built lookup map with ${#repo_packages_map[@]} unique packages in $component"
fi
# Process each .deb file
for deb_file in "${package_dir}"/*.deb; do
# Get package info using dpkg-deb -f to get reliable format
# Single call to get all fields at once (faster than 3 separate calls)
local deb_info deb_name deb_version deb_arch
deb_info=$(dpkg-deb -f "$deb_file" Package Version Architecture 2>/dev/null)
deb_name=$(echo "$deb_info" | sed -n '1s/Package: //p')
deb_version=$(echo "$deb_info" | sed -n '2s/Version: //p')
deb_arch=$(echo "$deb_info" | sed -n '3s/Architecture: //p')
# Create full identifier using pipe as separator (won't appear in package names)
local deb_key="${deb_name}|${deb_version}|${deb_arch}"
local deb_display="${deb_name}_${deb_version}_${deb_arch}"
log "Checking package: $deb_display"
# Skip if exact package (name+version+arch) already exists in repo (unless FORCE_ADD is true)
if [[ "$FORCE_ADD" != true && -n "${repo_packages_map[$deb_key]}" ]]; then
echo "[-] SKIP: $deb_display"
log "SKIP: $deb_display already in $component"
continue
fi
# Repack BSP packages if last-known-good kernel map exists
# This prevents upgrading to kernels that may break the board
if [[ -f userpatches/last-known-good.map ]]; then
local package_name
package_name=$(dpkg-deb -W "$deb_file" | awk '{ print $1 }')
# Read kernel pinning mappings from file
while IFS='|' read -r board branch linux_family last_kernel; do
if [[ "${package_name}" == "armbian-bsp-cli-${board}-${branch}" ]]; then
echo "Setting last kernel upgrade for $board to linux-image-$branch-$board=${last_kernel}"
# Extract, modify control file, and repackage
local tempdir
tempdir=$(mktemp -d)
dpkg-deb -R "$deb_file" "$tempdir"
sed -i '/^Replaces:/ s/$/, linux-image-'$branch'-'$linux_family' (>> '$last_kernel'), linux-dtb-'$branch'-'$linux_family' (>> '$last_kernel')/' "$tempdir/DEBIAN/control"
dpkg-deb -b "$tempdir" "${deb_file}" >/dev/null
rm -rf "$tempdir"
fi
done < userpatches/last-known-good-kernel-pkg.map
fi
# Determine whether to remove source files after adding to repo
# KEEP_SOURCES mode preserves source packages
# DRY_RUN mode also preserves sources (and skips all repo modifications)
# SINGLE_RELEASE mode preserves sources so parallel workers don't delete files needed by other workers
local remove_flag="-remove-files"
if [[ "$KEEP_SOURCES" == true ]] || [[ "$DRY_RUN" == true ]] || [[ -n "$SINGLE_RELEASE" ]]; then
remove_flag=""
fi
# Add package to repository
log "Adding $deb_name to $component"
run_aptly repo add $remove_flag -force-replace -config="${CONFIG}" "${component}" "${deb_file}"
done
}
# Build the common (main) repository component
# Creates/updates the common repository that contains packages shared across all releases
# Should be run once before processing individual releases in parallel
# Arguments:
# $1 - Input folder containing packages
# $2 - Output folder for published repository
# $3 - GPG password for signing (currently unused, signing is done separately)
update_main() {
local input_folder="$1"
local output_folder="$2"
local gpg_password="$3"
log "Building common (main) component"
# Create common repo if it doesn't exist
if [[ -z $(aptly repo list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep common) ]]; then
run_aptly repo create -config="${CONFIG}" -distribution="common" -component="main" -comment="Armbian common packages" "common" | logger -t repo-management >/dev/null
fi
# Add packages from main folder
adding_packages "common" "" "main" "$input_folder"
# Drop old snapshot if it exists and is not published
if [[ -n $(aptly snapshot list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep "common") ]]; then
# Check if snapshot is published
if ! aptly publish list -config="${CONFIG}" 2>/dev/null | grep -q "common"; then
run_aptly -config="${CONFIG}" snapshot drop common | logger -t repo-management >/dev/null
else
log "WARNING: common snapshot is published, cannot drop. Packages added to repo but snapshot not updated."
log "Run 'update' command to update all releases with new packages."
return 0
fi
fi
# Create new snapshot if it doesn't exist or was dropped
if [[ -z $(aptly snapshot list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep "common") ]]; then
run_aptly -config="${CONFIG}" snapshot create common from repo common | logger -t repo-management >/dev/null
else
log "common snapshot already exists, skipping creation"
fi
log "Common component built successfully"
}
# Process a single release distribution
# Creates/updates release-specific repositories (utils, desktop), publishes them,
# and signs the Release files. Can be run in parallel for different releases.
# Arguments:
# $1 - Release name (e.g., "jammy", "noble")
# $2 - Input folder containing packages
# $3 - Output folder for published repository
# $4 - GPG password for signing
process_release() {
local release="$1"
local input_folder="$2"
local output_folder="$3"
local gpg_password="$4"
log "Processing release: $release"
# In isolated mode (SINGLE_RELEASE), workers do NOT build common repo
# Common component is built separately by 'update-main' command and merged later
# This avoids duplicate work when running in parallel
if [[ -n "$SINGLE_RELEASE" ]]; then
log "Isolated mode: skipping common repo (will be merged by 'merge' command)"
fi
# Create release-specific repositories if they don't exist
if [[ -z $(aptly repo list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep "${release}-utils") ]]; then
run_aptly repo create -config="${CONFIG}" -component="${release}-utils" -distribution="${release}" -comment="Armbian ${release}-utils repository" "${release}-utils" | logger -t repo-management >/dev/null
fi
if [[ -z $(aptly repo list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep "${release}-desktop") ]]; then
run_aptly repo create -config="${CONFIG}" -component="${release}-desktop" -distribution="${release}" -comment="Armbian ${release}-desktop repository" "${release}-desktop" | logger -t repo-management >/dev/null
fi
# Add packages ONLY from release-specific extra folders
adding_packages "${release}-utils" "/extra/${release}-utils" "release utils" "$input_folder"
adding_packages "${release}-desktop" "/extra/${release}-desktop" "release desktop" "$input_folder"
# Run db cleanup before publishing to remove unreferenced packages
# This helps avoid "file already exists and is different" errors
log "Running database cleanup before publishing"
run_aptly db cleanup -config="${CONFIG}"
# Check if we have any packages to publish
# Get package counts in each repo
local utils_count=$(aptly repo show -config="${CONFIG}" "${release}-utils" 2>/dev/null | grep "Number of packages" | awk '{print $4}' || echo "0")
local desktop_count=$(aptly repo show -config="${CONFIG}" "${release}-desktop" 2>/dev/null | grep "Number of packages" | awk '{print $4}' || echo "0")
log "Package counts for $release: utils=$utils_count, desktop=$desktop_count"
# Always publish - even if no release-specific packages, we still need to publish common/main
# Check if this release was previously published for logging
if [[ "$utils_count" -eq 0 && "$desktop_count" -eq 0 && "$FORCE_PUBLISH" != true ]]; then
if ! aptly publish list -config="${CONFIG}" 2>/dev/null | grep -q "^\[${release}\]"; then
log "No release-specific packages for $release. Publishing common/main component only."
else
log "No new packages but $release was previously published. Will publish with common only."
fi
fi
if [[ "$FORCE_PUBLISH" == true ]]; then
log "Force publish enabled: will publish even with no packages"
fi
# Drop old snapshots if we have new packages to add OR if FORCE_PUBLISH is enabled
# This ensures fresh snapshots are created for force-publish scenarios
if [[ "$utils_count" -gt 0 || "$FORCE_PUBLISH" == true ]]; then
if [[ -n $(aptly snapshot list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep "${release}-utils") ]]; then
log "Dropping existing ${release}-utils snapshot"
run_aptly -config="${CONFIG}" snapshot drop ${release}-utils | logger -t repo-management 2>/dev/null
fi
fi
if [[ "$desktop_count" -gt 0 || "$FORCE_PUBLISH" == true ]]; then
if [[ -n $(aptly snapshot list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep "${release}-desktop") ]]; then
log "Dropping existing ${release}-desktop snapshot"
run_aptly -config="${CONFIG}" snapshot drop ${release}-desktop | logger -t repo-management 2>/dev/null
fi
fi
# Create snapshots only for repos that have packages
# OR when FORCE_PUBLISH is enabled (then we publish whatever exists in the DB)
# In isolated mode, do NOT include common snapshot - it will be merged later
local components_to_publish=()
local snapshots_to_publish=()
# Only add common/main component if NOT in isolated mode
if [[ -z "$SINGLE_RELEASE" ]]; then
components_to_publish=("main")
snapshots_to_publish=("common")
fi
if [[ "$utils_count" -gt 0 || "$FORCE_PUBLISH" == true ]]; then
# Only create snapshot if repo has packages, or if force-publishing
if [[ "$utils_count" -gt 0 ]]; then
run_aptly -config="${CONFIG}" snapshot create ${release}-utils from repo ${release}-utils | logger -t repo-management >/dev/null
components_to_publish+=("${release}-utils")
snapshots_to_publish+=("${release}-utils")
elif [[ "$FORCE_PUBLISH" == true ]]; then
log "Force publish: checking for existing ${release}-utils snapshot in DB"
# Try to use existing snapshot if it exists
if [[ -n $(aptly snapshot list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep "${release}-utils") ]]; then
components_to_publish+=("${release}-utils")
snapshots_to_publish+=("${release}-utils")
log "Using existing ${release}-utils snapshot"
else
# Create empty snapshot from empty repo
run_aptly -config="${CONFIG}" snapshot create ${release}-utils from repo ${release}-utils | logger -t repo-management >/dev/null
components_to_publish+=("${release}-utils")
snapshots_to_publish+=("${release}-utils")
log "Created empty ${release}-utils snapshot for force publish"
fi
fi
fi
if [[ "$desktop_count" -gt 0 || "$FORCE_PUBLISH" == true ]]; then
# Only create snapshot if repo has packages, or if force-publishing
if [[ "$desktop_count" -gt 0 ]]; then
run_aptly -config="${CONFIG}" snapshot create ${release}-desktop from repo ${release}-desktop | logger -t repo-management >/dev/null
components_to_publish+=("${release}-desktop")
snapshots_to_publish+=("${release}-desktop")
elif [[ "$FORCE_PUBLISH" == true ]]; then
log "Force publish: checking for existing ${release}-desktop snapshot in DB"
# Try to use existing snapshot if it exists
if [[ -n $(aptly snapshot list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep "${release}-desktop") ]]; then
components_to_publish+=("${release}-desktop")
snapshots_to_publish+=("${release}-desktop")
log "Using existing ${release}-desktop snapshot"
else
# Create empty snapshot from empty repo
run_aptly -config="${CONFIG}" snapshot create ${release}-desktop from repo ${release}-desktop | logger -t repo-management >/dev/null
components_to_publish+=("${release}-desktop")
snapshots_to_publish+=("${release}-desktop")
log "Created empty ${release}-desktop snapshot for force publish"
fi
fi
fi
log "Publishing $release with components: ${components_to_publish[*]}"
# Determine publish directory based on mode
local publish_dir="$output_folder"
if [[ -n "$SINGLE_RELEASE" ]]; then
publish_dir="$IsolatedRootDir"
fi
# In isolated mode, do NOT publish - only create repos and snapshots
# The merge command will handle all publishing with common component included
if [[ -n "$SINGLE_RELEASE" ]]; then
log "Isolated mode: skipping publishing (merge command will publish with common component)"
log "Created repos and snapshots for $release in isolated database"
return 0
fi
# Publish - include common snapshot for main component
log "Publishing $release"
# Drop existing publish for this release if it exists to avoid "file already exists" errors
if aptly publish list -config="${CONFIG}" 2>/dev/null | grep -q "^\[${release}\]"; then
log "Dropping existing publish for $release"
run_aptly publish drop -config="${CONFIG}" "${release}"
fi
# Build publish command with only components that have packages
local component_list=$(IFS=,; echo "${components_to_publish[*]}")
local snapshot_list="${snapshots_to_publish[*]}"
log "Publishing with components: $component_list"
log "Publishing with snapshots: $snapshot_list"
# Skip publishing if no components to publish (shouldn't happen, but safety check)
if [[ ${#components_to_publish[@]} -eq 0 ]]; then
log "WARNING: No components to publish for $release"
return 0
fi
run_aptly publish \
-skip-signing \
-skip-contents \
-architectures="armhf,arm64,amd64,riscv64,i386,loong64,all" \
-passphrase="${gpg_password}" \
-origin="Armbian" \
-label="Armbian" \
-config="${CONFIG}" \
-component="$component_list" \
-distribution="${release}" snapshot $snapshot_list
# Sign Release files for this release
# This includes:
# 1. Top-level Release file (dists/{release}/Release)
# 2. Component-level Release files (dists/{release}/{component}/Release)
# Only sign in non-isolated mode (isolated mode is signed by merge command)
log "Starting signing process for $release"
# Use shared output location for signing
local release_pub_dir="${output_folder}/public/dists/${release}"
# Get GPG keys from environment or use defaults
# Use BOTH keys for signing, just like the signing() function does
local gpg_keys=()
if [[ -n "$GPG_KEY" ]]; then
gpg_keys=("$GPG_KEY")
else
gpg_keys=("DF00FAF1C577104B50BF1D0093D6889F9F0E78D5" "8CFA83D13EB2181EEF5843E41EB30FAF236099FE")
fi
local gpg_params=("--yes" "--armor")
local keys_found=0
# Add all available keys to GPG parameters
for gpg_key in "${gpg_keys[@]}"; do
# Try to find the actual key in the keyring
local actual_key=""
if gpg --list-secret-keys "$gpg_key" >/dev/null 2>&1; then
actual_key="$gpg_key"
else
# Try to find by email or partial match
actual_key=$(gpg --list-secret-keys --keyid-format LONG 2>/dev/null | grep -B1 "$gpg_key" | grep "sec" | awk '{print $2}' | cut -d'/' -f2 || echo "")
fi
if [[ -n "$actual_key" ]]; then
gpg_params+=("-u" "$actual_key")
log "Adding GPG key for signing: $actual_key (requested: $gpg_key)"
((keys_found++))
else
log "WARNING: GPG key $gpg_key not found in keyring"
fi
done
if [[ $keys_found -eq 0 ]]; then
log "ERROR: No GPG keys found in keyring"
log "Available keys:"
gpg --list-secret-keys --keyid-format LONG 2>&1 | logger -t repo-management
return 1
fi
log "Using $keys_found GPG key(s) for signing"
# First, create component-level Release files by copying from binary-amd64 Release
# This is needed because aptly only creates Release files in binary-* subdirs
for component in main ${release}-utils ${release}-desktop; do
local component_dir="${release_pub_dir}/${component}"
if [[ -d "$component_dir" ]]; then
# Use the binary-amd64 Release file as the component Release file
local source_release="${component_dir}/binary-amd64/Release"
local target_release="${component_dir}/Release"
if [[ -f "$source_release" && ! -f "$target_release" ]]; then
log "Creating component Release file: ${target_release}"
cp "$source_release" "$target_release" 2>&1 | logger -t repo-management
fi
fi
done
# Now sign all Release files (both top-level and component-level)
# Find all Release files except those in binary-* subdirectories
find "${release_pub_dir}" -type f -name "Release" | while read -r release_file; do
# Skip binary-* subdirectories
if [[ "$release_file" =~ /binary-[^/]+/Release$ ]]; then
continue
fi
log "Signing: ${release_file}"
local sign_dir="$(dirname "$release_file")"
if gpg "${gpg_params[@]}" --clear-sign -o "${sign_dir}/InRelease" "$release_file" 2>&1 | logger -t repo-management >/dev/null; then
gpg "${gpg_params[@]}" --detach-sign -o "${sign_dir}/Release.gpg" "$release_file" 2>&1 | logger -t repo-management >/dev/null
log "Successfully signed: ${release_file}"
else
log "ERROR: Failed to sign: ${release_file}"
fi
done
log "Completed processing release: $release"
}
# Publish repositories for all configured releases
# Builds common component, processes each release, and finalizes the repository
# Arguments:
# $1 - Input folder containing packages
# $2 - Output folder for published repository
# $3 - Command name (unused, for compatibility)
# $4 - GPG password for signing
# $5 - Comma-separated list of releases (unused, determined from config)
publishing() {
# Only build common repo if NOT in single-release mode
# In single-release mode, common should be built separately with 'update-main' command
if [[ -z "$SINGLE_RELEASE" ]]; then
# This repository contains packages that are the same in all releases
if [[ -z $(aptly repo list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep common) ]]; then
run_aptly repo create -config="${CONFIG}" -distribution="common" -component="main" -comment="Armbian common packages" "common" | logger -t repo-management >/dev/null
fi
# Add packages from main folder
adding_packages "common" "" "main" "$1"
# Create snapshot
if [[ -n $(aptly snapshot list -config="${CONFIG}" -raw | awk '{print $(NF)}' | grep "common") ]]; then
run_aptly -config="${CONFIG}" snapshot drop common | logger -t repo-management >/dev/null
fi
run_aptly -config="${CONFIG}" snapshot create common from repo common | logger -t repo-management >/dev/null
else
# Single-release mode: common component should be built separately with 'update-main'
# and will be merged during the 'merge' command
log "Single-release mode: skipping common component (will be merged later)"
log "Common component should be built with: ./repo.sh -c update-main"
fi
# Get all distributions or use single release if specified
local distributions=()
if [[ -n "$SINGLE_RELEASE" ]]; then
distributions=("$SINGLE_RELEASE")
log "Single release mode: processing only $SINGLE_RELEASE"
else
distributions=($(grep -rw config/distributions/*/support -ve '' | cut -d"/" -f3))
fi
# Process releases sequentially
if [[ -n "$SINGLE_RELEASE" ]]; then
log "Processing single release: ${distributions[0]}"
else
log "Processing ${#distributions[@]} releases sequentially"
fi
for release in "${distributions[@]}"; do
process_release "$release" "$1" "$2" "$4"
done
# Cleanup database
run_aptly db cleanup -config="${CONFIG}"
# Copy GPG key to repository
mkdir -p "${2}"/public/
# Remove existing key file if it exists to avoid permission issues
rm -f "${2}"/public/armbian.key
cp config/armbian.key "${2}"/public/
# Write repository sync control file
date +%s > ${2}/public/control
# Display repository contents
showall
}
# Sign repository Release files using GPG
# Creates InRelease and Release.gpg signature files for component-level Release files
# Arguments:
# $1 - Output folder path containing published repository
# $@ - GPG key IDs to use for signing
signing() {
local output_folder="$1"
shift
local gpg_keys=("$@")
if [[ ${#gpg_keys[@]} -eq 0 ]]; then
echo "No GPG keys provided for signing." >&2
return 1
fi
# Build GPG parameters with available keys
local gpg_params=("--yes" "--armor")
for key in "${gpg_keys[@]}"; do
# Try to find the actual key in the keyring
local actual_key=""
if gpg --list-secret-keys "$key" >/dev/null 2>&1; then
actual_key="$key"
else
# Try to find by email or partial match
actual_key=$(gpg --list-secret-keys --keyid-format LONG 2>/dev/null | grep -B1 "$key" | grep "sec" | awk '{print $2}' | cut -d'/' -f2 || echo "")
if [[ -z "$actual_key" ]]; then
echo "Warning: GPG key $key not found on this system." >&2
continue
fi
fi
gpg_params+=("-u" "$actual_key")
echo "Using GPG key: $actual_key (requested: $key)" >&2
done
# Sign top-level Release files for each distribution
# Sign: dists/{release}/Release
# Skip: dists/{release}/{component}/binary-*/Release (subdirs, not needed)
find "$output_folder/public/dists" -maxdepth 2 -type f -name Release | while read -r release_file; do
# Skip if file is in a subdirectory (component or binary subdir)
# Only sign top-level dists/{release}/Release files
local rel_path="${release_file#$output_folder/public/dists/}"
# Count slashes - should have exactly 1 for top-level: {release}/Release
local slash_count=$(echo "$rel_path" | tr -cd '/' | wc -c)
if [[ $slash_count -eq 1 ]]; then
local distro_path
distro_path="$(dirname "$release_file")"
echo "Signing release at: $distro_path" | logger -t repo-management
gpg "${gpg_params[@]}" --clear-sign -o "$distro_path/InRelease" "$release_file"
gpg "${gpg_params[@]}" --detach-sign -o "$distro_path/Release.gpg" "$release_file"
fi
done
}
# Finalize repository after parallel GitHub Actions workers have built individual releases
# Combines the common/main component (built by update-main) with release-specific
# components (built by parallel workers) into the final repository structure
# Arguments:
# $1 - Base input folder (contains package sources, for consistency)
# $2 - Output folder containing combined repository
merge_repos() {
local input_folder="$1"
local output_folder="$2"
log "Merge mode: combining common component with release-specific components"
# We need to use the main database to properly merge components
# The main DB should have the common snapshot from update-main
# Create a temp config pointing to the main DB (not isolated)
local main_db_config
main_db_config="$(mktemp)"
sed 's|"rootDir": ".*"|"rootDir": "'$output_folder'"|g' tools/repository/aptly.conf > "$main_db_config"
# Check if common snapshot exists in main DB
local common_exists=false
if [[ -n $(aptly -config="$main_db_config" snapshot list -raw 2>/dev/null | awk '{print $(NF)}' | grep "common") ]]; then
common_exists=true
log "Found common snapshot in main database"
fi
# Get all releases that need to be merged
# These are releases that workers built in isolated DBs
local releases=()
# Discover releases from isolated databases directory
if [[ -d "$output_folder" ]]; then
for isolated_dir in "$output_folder"/aptly-isolated-*; do
if [[ -d "$isolated_dir" ]]; then
local release=$(basename "$isolated_dir" | sed 's/aptly-isolated-//')
releases+=("$release")
fi
done
fi
# Also check if there are any published releases (from old workflow or sequential mode)
if [[ -d "$output_folder/public/dists" ]]; then
for release_dir in "$output_folder/public/dists"/*; do
if [[ -d "$release_dir" ]]; then
local release=$(basename "$release_dir")
# Skip common distribution
[[ "$release" == "common" ]] && continue
# Add if not already in list
if [[ ! " ${releases[@]} " =~ " ${release} " ]]; then
releases+=("$release")
fi
fi
done
fi
log "Found ${#releases[@]} release(s) to process: ${releases[*]:-none}"
# If there are no releases to process, this is a no-op (not an error)
# This can happen when the repository is empty or workers haven't run yet
if [[ ${#releases[@]} -eq 0 ]]; then
log "No releases to merge - nothing to do"
rm -f "$main_db_config"
return 0
fi
# If we have releases but no common snapshot, that's an error (incomplete workflow)
if [[ "$common_exists" == false ]]; then
log "ERROR: Common snapshot not found in main database"
log "Found ${#releases[@]} release(s) to merge but no common snapshot"
log "Run 'update-main' command first!"
rm -f "$main_db_config"
return 1
fi
# Import snapshots from isolated databases into main database
# This allows us to re-publish with common component included
for release in "${releases[@]}"; do
local isolated_db="${output_folder}/aptly-isolated-${release}"
if [[ -d "$isolated_db" ]]; then
log "Importing from isolated DB for $release"
# Create temp config for isolated DB
local isolated_config
isolated_config="$(mktemp)"
sed 's|"rootDir": ".*"|"rootDir": "'$isolated_db'"|g' tools/repository/aptly.conf > "$isolated_config"
# Import release-specific snapshots from isolated DB to main DB
# First, we need to import the repos, then create snapshots
# Check if utils repo exists in isolated DB
if aptly -config="$isolated_config" repo show "${release}-utils" &>/dev/null; then
log "Importing ${release}-utils from isolated DB"
# Create repo in main DB if it doesn't exist
if ! aptly -config="$main_db_config" repo show "${release}-utils" &>/dev/null; then
run_aptly -config="$main_db_config" repo create -component="${release}-utils" -distribution="${release}" -comment="Armbian ${release}-utils repository" "${release}-utils"
fi
# Export packages from isolated repo and import to main repo
# Get list of packages in isolated repo
local packages
packages=$(aptly -config="$isolated_config" repo show -with-packages "${release}-utils" 2>/dev/null | tail -n +7)
if [[ -n "$packages" ]]; then
log "Adding ${release}-utils packages to main database"
# Get list of packages already in main DB to avoid re-adding them
local main_db_packages
main_db_packages=$(aptly -config="$main_db_config" repo show -with-packages "${release}-utils" 2>/dev/null | tail -n +7 || echo "")
# Add packages from isolated DB's pool to main repo
# We need to find the .deb files in the isolated pool and add them
local isolated_pool="${isolated_db}/pool"
if [[ -d "$isolated_pool" ]]; then
# Find all .deb files for this release in the isolated pool
# IMPORTANT: Only add packages that are actually in this repo, not all packages in pool!
find "$isolated_pool" -name "*.deb" -type f | while read -r deb_file; do
# Get package info to check if it belongs to this repo
local deb_name deb_version deb_arch
deb_info=$(dpkg-deb -f "$deb_file" Package Version Architecture 2>/dev/null)
deb_name=$(echo "$deb_info" | sed -n '1s/Package: //p')
deb_version=$(echo "$deb_info" | sed -n '2s/Version: //p')
deb_arch=$(echo "$deb_info" | sed -n '3s/Architecture: //p')
local deb_key="${deb_name}_${deb_version}_${deb_arch}"
# Check if this package is in the utils repo (isolated)
# aptly output has leading spaces, so grep without anchors
if echo "$packages" | grep -qw "${deb_key}"; then
# Check if package already exists in main DB repo to avoid conflicts
if echo "$main_db_packages" | grep -qw "${deb_key}"; then
# Package already in main DB, skip it
continue
fi
run_aptly -config="$main_db_config" repo add -force-replace "${release}-utils" "$deb_file"
fi
done
fi
fi
fi
# Same for desktop repo
if aptly -config="$isolated_config" repo show "${release}-desktop" &>/dev/null; then
log "Importing ${release}-desktop from isolated DB"
# Create repo in main DB if it doesn't exist
if ! aptly -config="$main_db_config" repo show "${release}-desktop" &>/dev/null; then
run_aptly -config="$main_db_config" repo create -component="${release}-desktop" -distribution="${release}" -comment="Armbian ${release}-desktop repository" "${release}-desktop"
fi
# Export packages from isolated repo and import to main repo
local packages
packages=$(aptly -config="$isolated_config" repo show -with-packages "${release}-desktop" 2>/dev/null | tail -n +7)
if [[ -n "$packages" ]]; then
log "Adding ${release}-desktop packages to main database"
# Get list of packages already in main DB to avoid re-adding them
local main_db_packages
main_db_packages=$(aptly -config="$main_db_config" repo show -with-packages "${release}-desktop" 2>/dev/null | tail -n +7 || echo "")
local isolated_pool="${isolated_db}/pool"
if [[ -d "$isolated_pool" ]]; then
find "$isolated_pool" -name "*.deb" -type f | while read -r deb_file; do
# Get package info to check if it belongs to this repo
local deb_name deb_version deb_arch
deb_info=$(dpkg-deb -f "$deb_file" Package Version Architecture 2>/dev/null)
deb_name=$(echo "$deb_info" | sed -n '1s/Package: //p')
deb_version=$(echo "$deb_info" | sed -n '2s/Version: //p')
deb_arch=$(echo "$deb_info" | sed -n '3s/Architecture: //p')
local deb_key="${deb_name}_${deb_version}_${deb_arch}"
# Check if this package is in the desktop repo (isolated)
# aptly output has leading spaces, so grep without anchors
if echo "$packages" | grep -qw "${deb_key}"; then
# Check if package already exists in main DB repo to avoid conflicts
if echo "$main_db_packages" | grep -qw "${deb_key}"; then
# Package already in main DB, skip it
continue
fi
run_aptly -config="$main_db_config" repo add -force-replace "${release}-desktop" "$deb_file"
fi
done
fi
fi
fi
rm -f "$isolated_config"
else
log "No isolated DB found for $release (repos may already be in main DB)"
# Repos may already exist in main DB from sequential mode
fi
done
# Now re-publish all releases with common component included
log "Re-publishing releases with common component..."
# First, drop ALL existing publishes for the releases we're about to publish
# This prevents "prefix/distribution already used" errors
log "Current publish list:"
aptly -config="$main_db_config" publish list 2>&1 | logger -t repo-management
for release in "${releases[@]}"; do
log "Checking for existing publishes for $release"
# Try to match various formats that aptly might use
# Formats seen: P.* ./bookworm, [bookworm], etc.
if aptly -config="$main_db_config" publish list 2>/dev/null | grep -E "(\\[${release}\\]|\\.\\/${release})" >/dev/null; then
log "Pre-drop: Removing existing publish for $release"
# Use aptly directly (not run_aptly) to avoid exit on failure
aptly -config="$main_db_config" publish drop "${release}" 2>/dev/null || true
# Also try with prefix if the above didn't work
aptly -config="$main_db_config" publish drop "./${release}" 2>/dev/null || true
else
log "No existing publish found for $release"
fi
done
# Clean up published pool once before all publishes to avoid "file already exists and is different" errors
# This happens when packages are rebuilt with same version but different content
# IMPORTANT: Do this ONCE before publishing all releases, not per-release
local published_pool="${output_folder}/public/pool"
if [[ -d "$published_pool" ]]; then
log "Removing published pool to avoid conflicts..."
rm -rf "${published_pool:?}"/*
log "Pool cleanup complete"
fi
for release in "${releases[@]}"; do
log "Publishing $release with common component..."
# Determine which components to publish
local components_to_publish=("main")
local snapshots_to_publish=("common")
# Check if utils repo has packages
local utils_has_packages=false
if aptly -config="$main_db_config" repo show "${release}-utils" &>/dev/null; then
local utils_count=$(aptly -config="$main_db_config" repo show "${release}-utils" 2>/dev/null | grep "Number of packages" | awk '{print $4}' || echo "0")
log "Utils repo has $utils_count packages"
if [[ "$utils_count" -gt 0 ]]; then
utils_has_packages=true
# Drop old snapshot if exists
if [[ -n $(aptly -config="$main_db_config" snapshot list -raw | awk '{print $(NF)}' | grep "${release}-utils") ]]; then
run_aptly -config="$main_db_config" snapshot drop "${release}-utils"
fi
# Create new snapshot
run_aptly -config="$main_db_config" snapshot create "${release}-utils" from repo "${release}-utils"
components_to_publish+=("${release}-utils")
snapshots_to_publish+=("${release}-utils")
fi
else
log "Utils repo does not exist in main DB"
fi
# Check if desktop repo has packages
local desktop_has_packages=false
if aptly -config="$main_db_config" repo show "${release}-desktop" &>/dev/null; then
local desktop_count=$(aptly -config="$main_db_config" repo show "${release}-desktop" 2>/dev/null | grep "Number of packages" | awk '{print $4}' || echo "0")
log "Desktop repo has $desktop_count packages"
if [[ "$desktop_count" -gt 0 ]]; then
desktop_has_packages=true
# Drop old snapshot if exists
if [[ -n $(aptly -config="$main_db_config" snapshot list -raw | awk '{print $(NF)}' | grep "${release}-desktop") ]]; then
run_aptly -config="$main_db_config" snapshot drop "${release}-desktop"
fi
# Create new snapshot
run_aptly -config="$main_db_config" snapshot create "${release}-desktop" from repo "${release}-desktop"
components_to_publish+=("${release}-desktop")
snapshots_to_publish+=("${release}-desktop")
fi
else
log "Desktop repo does not exist in main DB"
fi
# Always publish - at minimum, the main/common component is included
# This handles cases where a release only has main packages (no utils/desktop)
log "Publishing $release with components: ${components_to_publish[*]}"
# Build publish command
local component_list=$(IFS=,; echo "${components_to_publish[*]}")
local snapshot_list="${snapshots_to_publish[*]}"
# Publish with common component included
log "Publishing $release with component list: $component_list"
if ! run_aptly publish \
-skip-signing \
-skip-contents \
-architectures="armhf,arm64,amd64,riscv64,i386,loong64,all" \
-passphrase="${password:-}" \
-origin="Armbian" \
-label="Armbian" \
-config="$main_db_config" \
-component="$component_list" \
-distribution="${release}" snapshot $snapshot_list; then
log "ERROR: Failed to publish $release"
# Try to provide more diagnostic information
aptly -config="$main_db_config" publish list 2>&1 | logger -t repo-management
return 1
fi
log "Successfully published $release"
done
# Cleanup temp config
rm -f "$main_db_config"
# Sign all Release files
log "Signing Release files..."
signing "$output_folder" "DF00FAF1C577104B50BF1D0093D6889F9F0E78D5" "8CFA83D13EB2181EEF5843E41EB30FAF236099FE"
# Copy GPG key to repository
mkdir -p "${output_folder}"/public/
rm -f "${output_folder}"/public/armbian.key
cp config/armbian.key "${output_folder}"/public/
log "Copied GPG key to repository"
# Write repository sync control file
date +%s > ${output_folder}/public/control
log "Updated repository control file"
# Display repository contents
showall
log "Merge complete - repository is ready"
}
# Main repository manipulation dispatcher
# Routes commands to appropriate repository management functions
# Arguments:
# $1 - Input folder containing packages
# $2 - Output folder for published repository
# $3 - Command to execute (update-main, serve, html, delete, show, unique, update, merge)
# $4 - GPG password for signing
# $5 - Comma-separated list of releases (used by some commands)
# $6 - List of packages to delete (used by delete command)
repo-manipulate() {
# Read comma-delimited distros into array
IFS=', ' read -r -a DISTROS <<< "$5"
case "$3" in
update-main)
# Build common (main) component - runs once before parallel workers
update_main "$1" "$2" "$4"
return 0
;;
serve)
# Serve the published repository
# Since aptly serve requires published repos in its database, and we use
# direct file publishing, we'll use Python's HTTP server instead
local serve_ip=$(ip -f inet addr | grep -Po 'inet \K[\d.]+' | grep -v 127.0.0.1 | head -1)
if [[ -z "$serve_ip" ]]; then
log "WARNING: No external IP found, using 0.0.0.0"
serve_ip="0.0.0.0"
fi
local serve_port="${SERVE_PORT:-8080}"
if [[ ! -d "$output/public" ]]; then
log "ERROR: No published repository found at $output/public"
log "Please run 'update' command first to create the repository"
return 1
fi
log "Starting HTTP server on ${serve_ip}:${serve_port}"
log "Serving from: $output/public"
log "Press Ctrl+C to stop"
log ""
log "Repository URL: http://${serve_ip}:${serve_port}"
log ""
# Change to public directory and start HTTP server
cd "$output/public" || return 1
if ! command -v python3 &> /dev/null; then
log "ERROR: python3 not found. Install python3 to use serve command."
return 1
fi
python3 -m http.server "${serve_port}" --bind "${serve_ip}"
return 0
;;
html)
cat tools/repository/header.html
for release in "${DISTROS[@]}"; do
echo "<thead><tr><td colspan=3><h2>$release</h2></tr><tr><th>Main</th><th>Utils</th><th>Desktop</th></tr></thead>"
echo "<tbody><tr><td width=33% valign=top>"
aptly repo show -with-packages -config="${CONFIG}" "${release}-utils" | tail -n +7 | sed 's/.*/&<br>/'
echo "</td><td width=33% valign=top>"
aptly repo show -with-packages -config="${CONFIG}" "${release}-desktop" | tail -n +7 | sed 's/.*/&<br>/'
echo "</td></tr></tbody>"
done
cat tools/repository/footer.html
return 0
;;
delete)
echo "Deleting $6 from common"
aptly -config="${CONFIG}" repo remove common "$6"
for release in "${DISTROS[@]}"; do
echo "Deleting $6 from $release-utils"
aptly -config="${CONFIG}" repo remove "${release}-utils" "$6"
echo "Deleting $6 from $release-desktop"
aptly -config="${CONFIG}" repo remove "${release}-desktop" "$6"
done
return 0
;;
show)
showall
return 0
;;
unique)
# which package should be removed from all repositories
IFS=$'\n'
while true; do
LIST=()
LIST+=($(aptly repo show -with-packages -config="${CONFIG}" common | tail -n +7))
for release in "${DISTROS[@]}"; do
LIST+=($(aptly repo show -with-packages -config="${CONFIG}" "${release}-utils" | tail -n +7))
LIST+=($(aptly repo show -with-packages -config="${CONFIG}" "${release}-desktop" | tail -n +7))
done
LIST=($(echo "${LIST[@]}" | tr ' ' '\n' | sort -u))
new_list=()
# create a human readable menu
for ((n = 0; n < $((${#LIST[@]})); n++)); do
new_list+=("${LIST[$n]}")
new_list+=("")
done
LIST=("${new_list[@]}")
LIST_LENGTH=$((${#LIST[@]} / 2))
exec 3>&1
TARGET_VERSION=$(dialog --cancel-label "Cancel" --backtitle "BACKTITLE" --no-collapse --title \
"Remove packages from repositories" --clear --menu "Delete" $((9 + LIST_LENGTH)) 82 65 "${LIST[@]}" 2>&1 1>&3)
exitstatus=$?
exec 3>&-
if [[ $exitstatus -eq 0 ]]; then
aptly repo remove -config="${CONFIG}" "common" "$TARGET_VERSION"
for release in "${DISTROS[@]}"; do
aptly repo remove -config="${CONFIG}" "${release}-utils" "$TARGET_VERSION"
aptly repo remove -config="${CONFIG}" "${release}-desktop" "$TARGET_VERSION"
done
else
return 1
fi
aptly db cleanup -config="${CONFIG}" > /dev/null 2>&1
# remove empty folders
find "$2/public" -type d -empty -delete
done
;;
update)
# remove old releases from publishing (only drops unsupported releases, not all)
drop_unsupported_releases ""
publishing "$1" "$2" "$3" "$4" "$5"
# Only use signing function for non-single-release mode
# In single-release mode, workers already signed their components
if [[ -z "$SINGLE_RELEASE" ]]; then
signing "$2" "DF00FAF1C577104B50BF1D0093D6889F9F0E78D5" "8CFA83D13EB2181EEF5843E41EB30FAF236099FE"
fi
;;
merge)
# Merge repositories from parallel per-release runs
# Workers have already signed their releases, just finalize
merge_repos "$1" "$2"
;;
*)
echo -e "Unknown command"
return 1
;;
esac
}
# defaults
input="output/debs-beta"
output="output/repository"
command="show"
if [[ -d "config/distributions" ]]; then
releases=$(grep -rw config/distributions/*/support 2>/dev/null | cut -d"/" -f3 | xargs | sed -e 's/ /,/g')
if [[ -z "$releases" ]]; then
log "WARNING: No releases found in config/distributions"
fi
else
log "WARNING: config/distributions directory not found"
releases=""
fi
help()
{
echo "Armbian wrapper for Aptly v1.0
(c) Igor Pecovnik, igor@armbian.com
License: (MIT) <https://mit-license.org/>
Usage: $0 [ -short | --long ]
-h --help displays this help
-i --input [input folder] input folder with packages
-o --output [output folder] output folder for repository
-p --password [GPG password] GPG password for signing
-r --repository [jammy,sid,bullseye,...] comma-separated list of releases
-l --list [\"Name (% linux*)|armbian-config\"] list of packages
-c --command command to execute
-R --single-release [name] process only a single release (for parallel GitHub Actions)
example: -R jammy or -R noble
[show] displays packages in each repository
[sign] sign repository
[html] displays packages in each repository in html form
[serve] serve repository - useful for local diagnostics
[unique] manually select which package should be removed from all repositories
[update] search for packages in input folder and create/update repository
[update-main] build common (main) component - run once before parallel workers
[merge] merge repositories from parallel per-release runs into final repo
[delete] delete package from -l LIST of packages
-d --dry-run perform a full trial run without making any repository changes
(implies --keep-sources, shows what would be done)
-k --keep-sources keep source packages when adding to repository
(generates real repo but doesn't delete input packages)
-F --force-add force re-adding all packages even if they already exist
(by default, skips packages that are already in the repo)
-P --force-publish force publishing even when there are no packages to add
(by default, skips publishing empty releases)
GitHub Actions parallel workflow example:
# Step 1: Build common (main) component once
./repo.sh -c update-main -i /shared/packages -o /shared/output
# Step 2: Workers build release-specific components in parallel (isolated DBs)
# Workers do NOT build common component - it's merged later
# Worker 1: ./repo.sh -c update -R jammy -k -i /shared/packages -o /shared/output
# Worker 2: ./repo.sh -c update -R noble -k -i /shared/packages -o /shared/output
# Worker 3: ./repo.sh -c update -R bookworm -k -i /shared/packages -o /shared/output
# Step 3: Final merge to combine common component with release-specific components
./repo.sh -c merge -i /shared/packages -o /shared/output
Note: Each worker uses isolated DB (aptly-isolated-<release>) to avoid locking.
Workers build ONLY release-specific components; common component is merged by 'merge' command.
This avoids duplicate work - common packages are added once, not by every worker.
"
exit 2
}
SHORT=i:,l:,o:,c:,p:,r:,h,d,k,R:,F:,P:
LONG=input:,list:,output:,command:,password:,releases:,help,dry-run,keep-sources,single-release:,force-add:,force-publish:
if ! OPTS=$(getopt -a -n repo --options $SHORT --longoptions $LONG -- "$@"); then
help
exit 1
fi
# Note: Logging now uses syslog/journalctl - view with: journalctl -t repo-management -f
VALID_ARGUMENTS=$# # Returns the count of arguments that are in short or long options
eval set -- "$OPTS"
while :
do
case "$1" in
-i | --input )
input="$2"
shift 2
;;
-o | --output )
output="$2"
shift 2
;;
-c | --command )
command="$2"
shift 2
;;
-p | --password )
password="$2"
shift 2
;;
-r | --releases )
releases="$2"
shift 2
;;
-l | --list )
list="$2"
shift 2
;;
-k | --keep-sources )
KEEP_SOURCES=true
shift
;;
-R | --single-release )
SINGLE_RELEASE="$2"
shift 2
;;
-F | --force-add )
FORCE_ADD=true
shift
;;
-P | --force-publish )
FORCE_PUBLISH=true
shift
;;
-d | --dry-run )
DRY_RUN=true
# Dry-run implies keep-sources
KEEP_SOURCES=true
shift
;;
-h | --help)
help
;;
--)
shift;
break
;;
*)
echo "Unexpected option: $1"
help
;;
esac
done
# redefine output folder in Aptly
# Use isolated database for single-release mode to avoid DB locking during parallel execution
# Use shared database for regular (non-parallel) mode
if [[ -n "$SINGLE_RELEASE" ]]; then
# Create isolated aptly directory for this release
IsolatedRootDir="${output}/aptly-isolated-${SINGLE_RELEASE}"
# Create the isolated directory if it doesn't exist
if ! mkdir -p "$IsolatedRootDir"; then
log "ERROR: mkdir $IsolatedRootDir: permission denied"
exit 1
fi
# Do NOT copy the shared database to isolated DB
# This prevents "key not found" errors when the copied DB references packages
# that don't exist in the isolated pool. Instead, each worker creates a fresh DB
# and builds the common component from packages in the shared input folder.
# Do NOT link the shared pool either - each isolated DB should have its own pool
# Packages will be copied to the isolated pool when they're added via 'aptly repo add'
# This prevents hard link issues and "no such file or directory" errors during publish
# Create temp config file
TempDir="$(mktemp -d || exit 1)"
# Create config with isolated rootDir
cat tools/repository/aptly.conf | \
sed 's|"rootDir": ".*"|"rootDir": "'$IsolatedRootDir'"|g' > "${TempDir}"/aptly.conf
CONFIG="${TempDir}/aptly.conf"
log "Using isolated aptly root for $SINGLE_RELEASE at: $IsolatedRootDir"
else
TempDir="$(mktemp -d || exit 1)"
sed 's|"rootDir": ".*"|"rootDir": "'$output'"|g' tools/repository/aptly.conf > "${TempDir}"/aptly.conf
CONFIG="${TempDir}/aptly.conf"
fi
# Display configuration status
echo "=========================================="
echo "Configuration Status:"
echo " DRY-RUN: $([ "$DRY_RUN" == true ] && echo 'ENABLED' || echo 'disabled')"
echo " KEEP-SOURCES: $([ "$KEEP_SOURCES" == true ] && echo 'ENABLED' || echo 'disabled')"
echo " FORCE-ADD: $([ "$FORCE_ADD" == true ] && echo 'ENABLED' || echo 'disabled')"
echo " FORCE-PUBLISH: $([ "$FORCE_PUBLISH" == true ] && echo 'ENABLED' || echo 'disabled')"
if [[ -n "$SINGLE_RELEASE" ]]; then
echo " SINGLE-RELEASE: ENABLED ($SINGLE_RELEASE)"
else
echo " SINGLE-RELEASE: disabled"
fi
echo "=========================================="
log "Configuration: DRY_RUN=$DRY_RUN, KEEP_SOURCES=$KEEP_SOURCES, FORCE_ADD=$FORCE_ADD, FORCE_PUBLISH=$FORCE_PUBLISH, SINGLE_RELEASE=$SINGLE_RELEASE"
if [[ "$DRY_RUN" == true ]]; then
echo "=========================================="
echo "DRY-RUN MODE ENABLED"
echo "No changes will be made to repository"
echo "Packages will NOT be deleted on add"
echo "=========================================="
elif [[ "$KEEP_SOURCES" == true ]]; then
echo "=========================================="
echo "KEEP-SOURCES MODE ENABLED"
echo "Repository will be generated normally"
echo "Source packages will NOT be deleted"
echo "=========================================="
fi
if [[ "$FORCE_ADD" == true ]]; then
echo "=========================================="
echo "FORCE-ADD MODE ENABLED"
echo "All packages will be re-added even if already in repo"
echo "=========================================="
fi
if [[ -n "$SINGLE_RELEASE" ]]; then
echo "=========================================="
echo "SINGLE RELEASE MODE"
echo "Processing only: $SINGLE_RELEASE"
echo "=========================================="
fi
# main
repo-manipulate "$input" "$output" "$command" "$password" "$releases" "$list"
RETURN=$?
exit $RETURN