#!/usr/bin/env bash # Copyright 2026 Mocktail Project Authors # SPDX-License-Identifier: Apache-2.0 set -Eeuo pipefail AnyLinuxDependencyDie() { printf '[anylinux-dependencies] error: %s\n' "$*" >&2 exit 1 } AnyLinuxDependencyRequireContainer() { # Never replace a developer's desktop / graphics packages on the host. [[ -f /.dockerenv || -f /run/.containerenv ]] || AnyLinuxDependencyDie 'installation requires a disposable container' [[ "$(id -u)" == 0 ]] || AnyLinuxDependencyDie 'installation requires container root' } AnyLinuxDependencySnapshot() { local manifest="$1" release="$2" host_arch="${3:-$(uname -m)}" local filename remainder names_json [[ "${host_arch}" == x86_64 || "${host_arch}" == aarch64 ]] || AnyLinuxDependencyDie 'unsupported AppImage architecture' local -a names=() local -A seen=() while read -r filename remainder || [[ -n "${filename}" ]]; do [[ -n "${filename}" && "${filename}" != \#* ]] || continue [[ "${filename}" =~ ^[a-z0-9-]+-mini-${host_arch}\.pkg\.tar\.(zst|xz)$ && -z "${remainder}" && -z "${seen[${filename}]:-}" ]] || AnyLinuxDependencyDie 'invalid or duplicate dependency manifest entry' seen["${filename}"]=1 names+=("${filename}") done <"${manifest}" (( ${#names[@]} > 0 )) || AnyLinuxDependencyDie 'empty dependency manifest' names_json="$(printf '%s\n' "${names[@]}" | jq --raw-input --slurp \ 'split("\n")[:-1]')" # continuous assets are replaced on upstream rebuilds. Resolve all required # names and GitHub-computed digests in one response, then download by asset ID # rather than by rolling filename. These are per-build integrity checks, not # immutable version pins. Never accept absent digests or compute our own trust. jq --exit-status --raw-output --argjson names "${names_json}" \ --arg base 'https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases/assets/' ' (if .tag_name != "continuous" or (.assets | type) != "array" then error("invalid continuous release metadata") else .assets end) as $assets | $names[] as $name | [$assets[] | select(.name == $name)] as $matches | if ($matches | length) != 1 then error("missing or duplicate dependency asset: " + $name) else $matches[0] end | if .state != "uploaded" or (.id | type) != "number" or .id <= 0 or .id != (.id | floor) or (.digest | type) != "string" then error("invalid dependency asset metadata: " + $name) elif (.digest | test("^sha256:[0-9a-f]{64}$") | not) or .url != ($base + (.id | tostring)) then error("invalid dependency digest or URL: " + $name) else [(.digest | ltrimstr("sha256:")), $name, .url] | @tsv end ' "${release}" || AnyLinuxDependencyDie 'cannot resolve verified dependency assets' } AnyLinuxDownloadDependencies() { local manifest="$1" downloads="$2" attempt digest filename url failed local -a curl_options=( --fail --silent --show-error --location --retry 3 --proto '=https' --proto-redir '=https' --connect-timeout 15 --max-time 180 --header 'X-GitHub-Api-Version: 2022-11-28' ) if [[ -n "${GH_TOKEN:-}" ]]; then curl_options+=(--header "Authorization: Bearer ${GH_TOKEN}") fi for attempt in 1 2 3; do curl "${curl_options[@]}" --header 'Accept: application/vnd.github+json' \ --output "${downloads}/release.json" \ https://api.github.com/repos/pkgforge-dev/archlinux-pkgs-debloated/releases/tags/continuous || AnyLinuxDependencyDie 'cannot fetch upstream release metadata' AnyLinuxDependencySnapshot "${manifest}" "${downloads}/release.json" \ >"${downloads}/dependencies.tsv" : >"${downloads}/SHA256SUMS" failed=false while IFS=$'\t' read -r digest filename url; do printf '[anylinux-dependencies] downloading %s (SHA256 %s)\n' \ "${filename}" "${digest}" >&2 if ! curl "${curl_options[@]}" --header 'Accept: application/octet-stream' \ --output "${downloads}/${filename}" "${url}"; then failed=true break fi printf '%s %s\n' "${digest}" "${filename}" >>"${downloads}/SHA256SUMS" done <"${downloads}/dependencies.tsv" if [[ "${failed}" == true ]]; then # An upstream upload can delete an ID between resolution and download. # Retry a complete snapshot, never mix old checksums with new filenames. printf '[anylinux-dependencies] download failed; refreshing snapshot (%s/3)\n' \ "${attempt}" >&2 continue fi (cd -- "${downloads}" && sha256sum --check --strict SHA256SUMS) || AnyLinuxDependencyDie 'dependency checksum verification failed' return 0 done AnyLinuxDependencyDie 'dependency downloads failed after 3 snapshots' } # Prints the package name and path of each archive that is older than the # installed package. AnyLinuxDependencyDowngrades() { local package name version installed for package in "$@"; do read -r name version < <(pacman -Qp -- "${package}") installed="$(pacman -Q -- "${name}" 2>/dev/null | cut -d ' ' -f 2)" || continue [[ -n "${installed}" ]] || continue if (( $(vercmp "${version}" "${installed}") < 0 )); then printf '%s\t%s\n' "${name}" "${package}" fi done } # Prints each pkg-config module whose requirements are not met. AnyLinuxDependencyBrokenModules() { local module while read -r module; do pkg-config --exists "${module}" 2>/dev/null || printf '%s\n' "${module}" done < <(pkg-config --list-package-names 2>/dev/null) } AnyLinuxDependenciesMain() ( AnyLinuxDependencyRequireContainer (( $# == 0 )) || AnyLinuxDependencyDie 'no arguments are supported' readonly ROOT="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" local host_arch="$(uname -m)" manifest_name case "${host_arch}" in x86_64) manifest_name=anylinux-dependencies.txt ;; aarch64) manifest_name=anylinux-dependencies-aarch64.txt ;; *) AnyLinuxDependencyDie "unsupported AppImage architecture: ${host_arch}" ;; esac readonly MANIFEST="${ROOT}/packaging/${manifest_name}" readonly DOWNLOADS="$(mktemp -d "${TMPDIR:-/tmp}/mocktail-debloated.XXXXXX")" trap 'rm -rf -- "${DOWNLOADS}"' EXIT AnyLinuxDownloadDependencies "${MANIFEST}" "${DOWNLOADS}" local digest filename url local -a packages=() while IFS=$'\t' read -r digest filename url; do packages+=("${DOWNLOADS}/${filename}") done <"${DOWNLOADS}/dependencies.tsv" local broken name package local -a current=() older=() older_names=() local -A is_older=() while IFS=$'\t' read -r name package; do older_names+=("${name}") is_older["${package}"]=1 done < <(AnyLinuxDependencyDowngrades "${packages[@]}") for package in "${packages[@]}"; do if [[ -n "${is_older[${package}]:-}" ]]; then older+=("${package}") else current+=("${package}") fi done (( ${#current[@]} == 0 )) || pacman -U --noconfirm --needed "${current[@]}" (( ${#older[@]} > 0 )) || return 0 # A reduced package can lag the distribution. If the older version leaves # another package's pkg-config requirements unmet, build with the # distribution's package for that library. Modules that the reduced # packages of the current version already leave unmet do not count. broken="$(AnyLinuxDependencyBrokenModules)" pacman -U --noconfirm --needed "${older[@]}" if [[ "$(AnyLinuxDependencyBrokenModules)" != "${broken}" ]]; then printf '[anylinux-dependencies] reduced packages are too old; restoring %s\n' \ "${older_names[*]}" >&2 pacman -S --noconfirm "${older_names[@]}" [[ "$(AnyLinuxDependencyBrokenModules)" == "${broken}" ]] || AnyLinuxDependencyDie 'reduced packages break pkg-config requirements' fi ) if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then AnyLinuxDependenciesMain "$@" fi