Packagemanager-LFS-PackageUser-System git · main
Package-user based package manager for LFS/BLFS
Shell 63.7% Markdown 35.8%git clone https://git.christianimmanuel.de/linux-from-scratch/Packagemanager-LFS-PackageUser-System.gitwget https://git.christianimmanuel.de/linux-from-scratch/Packagemanager-LFS-PackageUser-System/archive/Packagemanager-LFS-PackageUser-System.tar.gzlfs-phases raw
#!/bin/bash
# lfs-phases -- the shared phase runner sourced by every generated install
# script. The script holds what is specific to the package: its variables at
# the top and its commands in build_pkg / install_pkg / configure_pkg /
# test_pkg. Everything the same for every package lives here.
#
# A script ends with:
# . "${PKGUSR_LIB:-lfs-phases}" && pkgusr_run "$@"
#
# Variables a script may set (all optional unless noted):
# name_version binutils-2.45 (required)
# pkg_glob how to find the tarball in the sources store (LFS scripts)
# pkg the tarball's exact filename (BLFS scripts)
# link where to download it when it is not on disk
# md5_sum checksum of $pkg
# additional_links=(...) further files to fetch beside it
# is_git=1 git_ref=... clone $link instead of fetching a tarball
# pkg_unpack=no fetch but do not extract: the page's own commands do that
# pkgusr_stage cross | chroot | (unset = the booted system)
#
# Functions a script may define:
# prepare_pkg runs in unpack, before the tarball is fetched
# build_pkg configure + compile (required)
# install_pkg the step that writes into system directories (required)
# configure_pkg post-install configuration
# test_pkg the test suite; runs only when PKGUSR_TESTS=1
# unpack_pkg replaces the built-in unpack entirely (rare)
#
# Environment the runner passes (all optional):
# PKGUSR_LIB, LFS_BUILD_ROOT, LFS_SOURCES_DIR, PKGUSR_TESTS (=1),
# PM_SKIP_MD5 (=1), PKGUSR_PHASE (internal: this process runs one phase)
#
# Every function and variable here carries a pkgusr_ / _pp_ prefix, because
# scripts are also SOURCED by lfs-helper to read their metadata, and this file
# comes along -- it must not shadow anything in the sourcing shell.
PKGUSR_LIB_VERSION="1.14.371"
_pp_build_id() {
local f="${BASH_SOURCE[0]}"
[ -r "$f" ] && md5sum "$f" 2>/dev/null | cut -c1-7
}
# ------------------------------------------------------------- dev source ----
# packagemanager dev <pkg> <checkout>: one line per package account in
# /etc/pkgusr/dev-sources.conf. Matched by the account running this, or by
# the package name when a root run has no account.
_pp_dev_source() {
local f="${PKGUSR_DEV_SOURCES:-/etc/pkgusr/dev-sources.conf}" me
[ -r "$f" ] || return 0
me="$(id -un 2>/dev/null)"
awk -v a="$me" -v n="${name:-}" '
{ sub(/#.*/, ""); sub(/[ \t]+<-.*/, "") }
NF >= 2 && ($1 == a || (n != "" && $1 == n)) {
$1 = ""; sub(/^ +/, ""); print; exit }' "$f"
}
# ----------------------------------------------------------------- output ----
_pp_say() { echo "$*"; }
_pp_note() { echo "note: $*" >&2; }
_pp_fail() { echo "!! $*" >&2; }
# ---------------------------------------------------------------- layout ----
# Where the sources are and where this package builds. Decided per process,
# from the environment the runner passed; nothing here is exported, because
# BUILD_ROOT is a name packages use (libvpx's Makefile has `BUILD_ROOT?=.`).
_pp_layout() {
# THE PACKAGE'S NAME, NOT THE FILE'S.
#
# The same script is run under several filenames by design -- the book
# copy (install_util-macros-1.20.2), the staged copy (install_util-macros)
# and the account's copy (install_p_util-macros) -- so a marker named
# after $0 was written by unpack under one name and looked for by install
# under another:
# !! no unpacked source in /usr/src/pkgusr/p_util-macros/src
# while the source sat right there. name_version is the package, and it
# is the same in every copy of the script.
_pp_step="${name_version:-${0##*/}}"; _pp_step="${_pp_step%.sh}"
if [ "${pkgusr_stage:-}" = "cross" ]; then
# chapters 5-6, as the lfs user on the host: the tree is $LFS
if [ -z "${LFS:-}" ]; then
_pp_fail '$LFS is not set -- refusing to guess. export LFS=/mnt/lfs (see book 2.6) and re-run.'
exit 2
fi
export LFS
if [ -z "${LFS_TGT:-}" ]; then
LFS_TGT="$(uname -m)-lfs-linux-gnu"; export LFS_TGT
_pp_note "\$LFS_TGT was unset -- defaulting to $LFS_TGT (book 4.4)"
fi
case ":$PATH:" in
*":$LFS/tools/bin:"*) ;;
*) PATH="$LFS/tools/bin:$PATH"; export PATH
_pp_note "added $LFS/tools/bin to \$PATH (book 4.4)" ;;
esac
elif [ "${pkgusr_stage:-}" = "chroot" ]; then
# chapters 7-10, inside the chroot: the tree is /
export LFS="${LFS:-/}"
fi
if [ -z "${MAKEFLAGS:-}" ]; then
export MAKEFLAGS="-j$(nproc)"
fi
if [ -n "${LFS_SOURCES_DIR:-}" ]; then SOURCES_DIR="$LFS_SOURCES_DIR"
elif [ -n "${SOURCES_DIR:-}" ]; then :
elif [ "${pkgusr_stage:-}" = "cross" ]; then SOURCES_DIR="$LFS/sources"
else SOURCES_DIR="/sources"; fi
if [ -n "${LFS_BUILD_ROOT:-}" ]; then BUILD_ROOT="$LFS_BUILD_ROOT"
elif [ -n "${BUILD_ROOT:-}" ]; then :
elif [ "${pkgusr_stage:-}" = "cross" ]; then BUILD_ROOT="$LFS/build"
elif [ -d "$HOME/src" ]; then BUILD_ROOT="$HOME/src"
elif [ "${pkgusr_stage:-}" = "chroot" ]; then BUILD_ROOT="/build"
else BUILD_ROOT="$PWD"; fi
_pp_dir_marker="$BUILD_ROOT/.pkgusr-$_pp_step.dir"
_pp_cwd_marker="$BUILD_ROOT/.pkgusr-$_pp_step.cwd"
_pp_built_marker="$BUILD_ROOT/.pkgusr-$_pp_step.built"
}
# A systemd book on a SysV system: the book's commands call systemctl and its
# family, and a missing command aborts the phase. Shim each one that is
# absent; on a systemd system nothing changes.
_pp_systemd_shims() {
local c
for c in systemctl systemd-run systemd-tmpfiles systemd-sysusers \
systemd-machine-id-setup systemd-hwdb loginctl busctl journalctl \
timedatectl hostnamectl localectl udevadm; do
command -v "$c" >/dev/null 2>&1 && continue
eval "$c() { echo \"# skipped (no $c on this system): $c \$*\"; }"
done
}
# --------------------------------------------------------------- sources ----
# Make BUILD_ROOT look like the sources directory to anything that walks up
# out of an unpacked tree: the LFS book writes
# tar -xf ../tcl8.6.16-html.tar.gz --strip-components=1
# from INSIDE the unpacked directory. Every downloaded file is linked, not a
# known list -- a list would be wrong the first time a new book adds one.
_pp_link_sources() {
[ -d "$SOURCES_DIR" ] || return 0
local f b
for b in "$BUILD_ROOT"/*; do
[ -L "$b" ] && [ ! -e "$b" ] && rm -f "$b"
done
for f in "$SOURCES_DIR"/*; do
[ -f "$f" ] || continue
b="$BUILD_ROOT/${f##*/}"
# never shadow real content: an unpacked tree wins over a link
[ -e "$b" ] && [ ! -L "$b" ] && continue
ln -sfn "$f" "$b" 2>/dev/null || true
done
}
# Put one file into BUILD_ROOT from wherever it already is on disk. Prints
# nothing; returns 1 when it is nowhere.
_pp_stage_file() {
local f="$1" d
[ -f "$BUILD_ROOT/$f" ] && return 0
for d in "$HOME" "$SOURCES_DIR" /sources; do
[ -f "$d/$f" ] || continue
ln -sfn "$d/$f" "$BUILD_ROOT/$f" 2>/dev/null \
|| cp -p "$d/$f" "$BUILD_ROOT/$f" 2>/dev/null \
|| { cat "$d/$f" > "$BUILD_ROOT/$f" || rm -f "$BUILD_ROOT/$f"; }
[ -f "$BUILD_ROOT/$f" ] && return 0
done
return 1
}
# Pick the tarball a glob names in the sources store. Two traps: patterns
# that match nothing must not swallow the one that did, and several packages
# ship docs beside the source (tcl8.6.16-html.tar.gz sorts before
# tcl8.6.16-src.tar.gz) -- prefer an explicit -src archive, never a doc one.
_pp_pick_glob() {
local _c
cd "$SOURCES_DIR" 2>/dev/null || return 1
for _c in $pkg_glob; do
[ -e "$_c" ] || continue
case "$_c" in *-src.*|*-source.*) echo "$_c"; return 0 ;; esac
done
for _c in $pkg_glob; do
[ -e "$_c" ] || continue
case "$_c" in
*-html.*|*-doc.*|*-docs.*|*-man.*|*-manual.*|*-tests.*) continue ;;
*-doc-*|*-docs-*|*-html-*|*-man-pages-*|*-tests-*) continue ;;
esac
echo "$_c"; return 0
done
return 1
}
# The GNU ftpmirror answers 502 more often than not, and a dead mirror asked
# three times is not a retry -- so the fallbacks are tried before giving up.
_pp_mirrors_for() {
local u="$1" f="${1##*/}" rest
printf '%s\n' "$u"
case "$u" in
*ftpmirror.gnu.org/*|*ftp.gnu.org/*)
rest="${u#*gnu.org/}"; rest="${rest#gnu/}"
printf '%s\n' "https://ftp.gnu.org/gnu/$rest" \
"https://mirrors.kernel.org/gnu/$rest" \
"https://mirror.csclub.uwaterloo.ca/gnu/$rest" ;;
esac
# the LFS mirror of last resort keeps most BLFS sources by filename
[ -n "${pkgusr_book_mirror:-}" ] && printf '%s\n' "${pkgusr_book_mirror%/}/$f"
return 0
}
_pp_fetch() {
# SAVE IT AS THE NAME THE SCRIPT ASKS FOR.
#
# A GitHub archive URL ends in the TAG, not the project:
# .../cisco/openh264/archive/refs/tags/v2.6.0.tar.gz
# so wget wrote v2.6.0.tar.gz, the unpack looked for the declared
# pkg=openh264-2.6.0.tar.gz, and the run said "no
# openh264-2.6.0.tar.gz ... and nowhere to fetch it from" one line under
# "'v2.6.0.tar.gz' saved [60302243]". Both statements were true.
#
# `pkg` is what every later phase uses, and two projects can both
# publish a v2.6.0.tar.gz, so the file has to land under that name.
local url="$1" try u out="${2:-${pkg:-}}"
for try in 1 2 3; do
while IFS= read -r u; do
[ -n "$u" ] || continue
[ "$u" = "$url" ] || _pp_say "trying mirror: $u"
if [ -n "$out" ]; then
wget -4 -c -O "$out" "$u" && return 0
# -O leaves an empty file behind on a 404, and -c would then
# "resume" it for ever
[ -s "$out" ] || rm -f "$out"
else
wget -4 -c "$u" && return 0
fi
done <<< "$(_pp_mirrors_for "$url" | awk '!seen[$0]++')"
[ "$try" = 3 ] && break
_pp_say "every mirror failed (try $try/3) -- retrying in $((try * 15))s"
sleep $((try * 15))
done
return 1
}
_pp_check_md5() {
[ -n "${md5_sum:-}" ] || return 0
grep -wq "$md5_sum" <<< "$(md5sum "$pkg")" && return 0
if [ "${PM_SKIP_MD5:-0}" = 1 ]; then
_pp_fail "md5sum does NOT match, and PM_SKIP_MD5=1 -- continuing anyway"
return 0
fi
_pp_fail "md5sum check failed for $pkg"
echo " expected $md5_sum" >&2
echo " got $(set -- $(md5sum "$pkg" 2>/dev/null); echo "${1:-<no file>}")" >&2
echo " To accept a mismatch on purpose: PM_SKIP_MD5=1 $0 unpack" >&2
return 1
}
# ---------------------------------------------------------------- unpack ----
_pp_unpack() {
mkdir -p "$BUILD_ROOT" 2>/dev/null
cd "$BUILD_ROOT" || { _pp_fail "no build scratch at $BUILD_ROOT and cannot create it"; return 1; }
rm -f "$_pp_dir_marker" "$_pp_cwd_marker"
case "${pkgusr_stage:-}" in cross|chroot) _pp_link_sources ;; esac
if declare -F prepare_pkg >/dev/null; then
prepare_pkg
# WHERE THE PAGE'S OWN COMMANDS LEFT OFF.
#
# Xorg Libraries writes lib-7.md5 in the build root, then `mkdir lib
# && cd lib` and downloads into it -- and the build loop reads
# `../lib-7.md5`, which only resolves from INSIDE lib. Returning to
# the build root here threw that away, so the build started one level
# too high and stopped on
# the list this build loops over is missing or empty: ../lib-7.md5
# after downloading all thirty-two tarballs successfully.
_pp_prepared_in="$PWD"
cd "$BUILD_ROOT" || return 1
fi
if [ "${is_git:-}" = 1 ]; then
pkg_dir="$name_version"
# GIT MUST NEVER ASK A QUESTION IN A BUILD.
#
# A URL that is not there gets a 401 from GitHub (it will not admit
# a repository is missing), so git asks for a username and the build
# ends with
# fatal: could not read Username for 'https://github.com'
# ...which reads like a credentials problem on a public repository
# and, on a terminal, would simply hang. Refuse the prompt and say
# what it means.
export GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=/bin/true GIT_SSH_COMMAND="ssh -oBatchMode=yes"
# A DEV SOURCE: a program developed on this machine is built from the
# developer's own checkout (packagemanager dev), not from url=. The
# checkout belongs to someone else, which git refuses to read
# ("detected dubious ownership") unless THIS account's own config
# names it -- `-c safe.directory=` does not reach the process that
# reads the other repository (git clears it), and the entry must be
# the .git path, not the work tree. --no-local: nothing is
# hardlinked across the two owners.
_pp_dev="$(_pp_dev_source)"
if [ -n "$_pp_dev" ]; then
link="$_pp_dev"
_pp_say "# dev source: $link -- its committed state, not the working tree"
fi
_pp_gc=(); _pp_cl=()
case "$link" in
/*|file://*)
_pp_cl=(--no-local)
_pp_p="${link#file://}"
for _pp_sd in "$_pp_p" "$_pp_p/.git"; do
git config --global --get-all safe.directory 2>/dev/null \
| grep -qxF "$_pp_sd" \
|| git config --global --add safe.directory "$_pp_sd"
done ;;
esac
if [ -d "$pkg_dir/.git" ]; then
# the clone follows whatever link= says NOW -- a dev source set or
# dropped since it was made must not keep fetching the old place
if [ "$(git -C "$pkg_dir" remote get-url origin 2>/dev/null)" != "$link" ]; then
_pp_say "# origin: $(git -C "$pkg_dir" remote get-url origin 2>/dev/null) -> $link"
git -C "$pkg_dir" remote set-url origin "$link"
fi
git "${_pp_gc[@]}" -C "$pkg_dir" fetch --all --tags --prune
else
rm -rf "$pkg_dir"
# git_submodules="0" (stack: submodules=no): the project's
# vendored copies are not needed here, so they are not cloned
_pp_rs=(--recurse-submodules)
[ "${git_submodules:-1}" = 0 ] && _pp_rs=()
if ! git "${_pp_gc[@]}" clone "${_pp_cl[@]}" "${_pp_rs[@]}" "$link" "$pkg_dir"; then
_pp_fail "could not clone $link"
echo " git was refused, and was not allowed to ask for a" >&2
echo " password (this is a build). On a public URL that" >&2
echo " almost always means the PATH IS WRONG: GitHub answers" >&2
echo " 401 for a repository that does not exist rather than" >&2
echo " admitting it is missing." >&2
echo " Check it, from here, before editing anything else:" >&2
echo " git ls-remote $link | head -3" >&2
echo " Then fix url= in the stack entry." >&2
return 1
fi
fi
# THE PACKAGE'S CLONE IS A BUILD TREE, NOT A WORKSPACE.
#
# swas's clone had edits to Makefile, README.md, config.example and
# swas.c -- made by hand, before there was a checkout of its own to
# work in -- and the checkout of the moved branch refused:
# error: Your local changes to the following files would be
# overwritten by checkout
# Every update would stop there. They may be real work, so they are
# SAVED as a patch in the package's home before the reset, never
# thrown away. Untracked files (build output) are left alone.
if [ -n "$(git -C "$pkg_dir" status --porcelain --untracked-files=no 2>/dev/null)" ]; then
_pp_patch="$HOME/${name:-$pkg_dir}-local-changes-$(date +%Y%m%d-%H%M%S).patch"
if git -C "$pkg_dir" diff HEAD > "$_pp_patch" 2>/dev/null && [ -s "$_pp_patch" ]; then
_pp_say "# the clone had uncommitted edits ($(git -C "$pkg_dir" diff HEAD --name-only | tr '\n' ' '))"
_pp_say "# saved: $_pp_patch"
_pp_say "# (keep them: cd <your checkout> && git apply $_pp_patch)"
git -C "$pkg_dir" reset -q --hard || { _pp_fail "could not reset $pkg_dir"; return 1; }
else
_pp_fail "the clone has uncommitted edits and they could not be saved to $_pp_patch -- not discarding them"
return 1
fi
fi
# ...AND FILES GIT DOES NOT TRACK. The browser's clone also held a
# hand-made HANDOVER.md; a later commit added a tracked one, and
# error: The following untracked working tree files would be
# overwritten by checkout: HANDOVER.md
# stopped the update the same way. Untracked, NOT ignored files are
# either hand-made or build output the project forgot to ignore --
# both go into a tar beside the patch, then the clone is cleaned.
# Ignored files (the project's own build output) stay.
_pp_untr="$(git -C "$pkg_dir" ls-files --others --exclude-standard 2>/dev/null)"
if [ -n "$_pp_untr" ]; then
_pp_tar="$HOME/${name:-$pkg_dir}-untracked-$(date +%Y%m%d-%H%M%S).tar"
if printf '%s\n' "$_pp_untr" | tar -C "$pkg_dir" -cf "$_pp_tar" -T - 2>/dev/null; then
_pp_say "# the clone had files git does not track ($(printf '%s' "$_pp_untr" | head -n 5 | tr '\n' ' ')$([ "$(printf '%s\n' "$_pp_untr" | wc -l)" -gt 5 ] && echo '...'))"
_pp_say "# saved: $_pp_tar (look: tar tf $_pp_tar)"
git -C "$pkg_dir" clean -fdq || { _pp_fail "could not clean $pkg_dir"; return 1; }
else
_pp_fail "the clone has untracked files and they could not be saved to $_pp_tar -- not removing them"
return 1
fi
fi
# A REF THAT DOES NOT EXIST IS A TYPO IN THE STACK, NOT A BUILD ERROR.
#
# git says only
# error: pathspec '1.0.0' did not match any file(s) known to git
# which reads like a build failure and sends people to the log. The
# repository is right here and knows what it does have; show it, and
# say where the wrong value came from.
# A BRANCH IS NOT A POINT IN TIME, AND `checkout` DOES NOT MOVE IT.
#
# On an existing clone the fetch above brings the new commits in, and
# `git checkout master` then says "Already on 'master'" and stays
# exactly where it was. So a package pinned to a branch rebuilt the
# SAME commit for ever: `update <pkg>` fetched, compiled and installed
# a version it already had, and said it had updated it. A tag or a
# commit is a point in time and must NOT move -- those are checked out
# detached, which is what they are.
_pp_ref="${git_ref:-}"
if [ -z "$_pp_ref" ]; then
# no ref: whatever the remote calls its default branch
_pp_ref="$(git -C "$pkg_dir" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)"
_pp_ref="${_pp_ref#origin/}"
[ -n "$_pp_ref" ] && _pp_say "# no ref given -- following origin's default branch ($_pp_ref)"
fi
if [ -n "$_pp_ref" ] \
&& git -C "$pkg_dir" show-ref --verify --quiet "refs/remotes/origin/$_pp_ref"; then
_pp_before="$(git -C "$pkg_dir" rev-parse --short HEAD 2>/dev/null)"
if ! git -C "$pkg_dir" checkout -B "$_pp_ref" "origin/$_pp_ref"; then
_pp_fail "could not put $pkg_dir on branch $_pp_ref"
return 1
fi
_pp_after="$(git -C "$pkg_dir" rev-parse --short HEAD 2>/dev/null)"
if [ -n "$_pp_before" ] && [ "$_pp_before" != "$_pp_after" ]; then
_pp_say "# $_pp_ref: $_pp_before -> $_pp_after ($(git -C "$pkg_dir" rev-list --count "$_pp_before..$_pp_after" 2>/dev/null) new commit(s))"
else
_pp_say "# $_pp_ref is already at $_pp_after"
fi
[ "${git_submodules:-1}" = 0 ] || git -C "$pkg_dir" submodule update --init --recursive
echo "$pkg_dir" > "$_pp_dir_marker"
_pp_say "checked out into $BUILD_ROOT/$pkg_dir"
return 0
fi
if [ -n "${git_ref:-}" ] && ! git -C "$pkg_dir" checkout --detach "$git_ref"; then
_pp_fail "'$git_ref' is not a tag, branch or commit in $link"
echo " The stack entry (or this script) asks for a ref the" >&2
echo " repository does not have. Recent tags:" >&2
# AN EMPTY LIST UNDER A HEADING IS A RIDDLE. A fork with no
# tags printed "Recent tags:" and then nothing, which reads like
# the command failed rather than "there are none -- use a
# branch".
_pp_tags="$(git -C "$pkg_dir" tag --sort=-v:refname 2>/dev/null | head -8)"
if [ -n "$_pp_tags" ]; then
printf '%s\n' "$_pp_tags" | sed 's/^/ /' >&2
else
echo " (none at all -- this repository is not tagged," >&2
echo " so a branch is the only thing to ask for)" >&2
fi
echo " Branches:" >&2
# HEAD and the bare remote name are not branches anyone can ask
# for: the list printed `origin` above `origin/main`.
git -C "$pkg_dir" branch -r --format='%(refname:short)' 2>/dev/null \
| grep -vE '^[^/]+$|/HEAD$' | sed 's|^[^/]*/||' \
| head -5 | sed 's/^/ /' >&2
echo " Fix ref= in the stack entry for '${name:-$name_version}'," >&2
echo " or set git_ref= in this script, then run it again." >&2
return 1
fi
[ "${git_submodules:-1}" = 0 ] || git -C "$pkg_dir" submodule update --init --recursive
echo "$pkg_dir" > "$_pp_dir_marker"
_pp_say "checked out into $BUILD_ROOT/$pkg_dir"
return 0
fi
# the tarball: by exact name, by glob in the store, or downloaded
if [ -z "${pkg:-}" ] && [ -n "${pkg_glob:-}" ]; then
pkg="$(_pp_pick_glob)" || pkg=""
cd "$BUILD_ROOT" || return 1
[ -n "$pkg" ] || { _pp_fail "no $pkg_glob in $SOURCES_DIR -- run: lfs build-system get-sources --run"; return 1; }
fi
if [ -z "${pkg:-}" ]; then
# a meta page: it builds a list of packages and its own commands fetch
# them. Remember where they left off -- Xorg Libraries does
# `mkdir lib && cd lib` and its build loop reads ../lib-7.md5 from there.
_pp_say "no single tarball -- this page builds a list of packages; its own commands fetch them"
printf '%s\n' "${_pp_prepared_in:-$PWD}" > "$_pp_cwd_marker" 2>/dev/null || true
[ -n "${_pp_prepared_in:-}" ] && [ "$_pp_prepared_in" != "$PWD" ] \
&& _pp_say "# the build resumes in $_pp_prepared_in"
return 0
fi
_pp_stage_file "$pkg" || { [ -n "${link:-}" ] && _pp_fetch "$link" "$pkg"; }
if [ ! -f "$pkg" ]; then
_pp_fail "no $pkg in $BUILD_ROOT and nowhere to fetch it from"
# SAY WHAT DID ARRIVE. This fired one line under a successful
# download and read as "the fetch failed", when the fetch had
# worked and only the NAME differed.
local _got
_got="$(ls -1t *.tar.* *.tgz *.zip 2>/dev/null | head -3)"
if [ -n "$_got" ]; then
echo " what IS here, newest first:" >&2
printf ' %s\n' $_got >&2
echo " if one of those is it, the entry's pkg= does not match the" >&2
echo " name it arrived under -- a GitHub archive URL ends in the" >&2
echo " TAG, not the project." >&2
fi
return 1
fi
_pp_check_md5 || return 1
local li f
# EACH EXTRA UNDER ITS OWN NAME. _pp_fetch saves to its second
# argument, else to $pkg -- and this call passed none, so LLVM's patch
# was fetched with `wget -c -O llvm-project-22.1.8.src.tar.xz`. The
# tarball was bigger, wget answered 416 "already fully retrieved", and
# the build stopped on
# patch: **** Can't open patch file ../llvm-22.1.8-upstream_fix-1.patch
# (An extra LARGER than the tarball would have been appended onto it.)
for li in "${additional_links[@]}"; do
f="${li##*/}"
_pp_stage_file "$f" || _pp_fetch "$li" "$f" || return 1
[ -s "$f" ] || { _pp_fail "fetched $li, and there is no $f"; return 1; }
done
if [ "${pkg_unpack:-yes}" = "no" ]; then
_pp_say "fetched $pkg (the page's own commands unpack it)"
printf '%s\n' "${_pp_prepared_in:-$PWD}" > "$_pp_cwd_marker" 2>/dev/null || true
return 0
fi
# Extract into a scratch dir, then move the ONE top-level directory to
# its name -- never `mv tmp/* .`, which nests it inside an existing
# same-named dir. --no-same-owner: as root, tar would restore the uids
# recorded in the archive.
rm -rf .pkgusr-tmp && mkdir .pkgusr-tmp && cd .pkgusr-tmp || return 1
case "$pkg" in
*.zip) unzip -q ../"$pkg" ;;
*) tar --no-same-owner -xf ../"$pkg" ;;
esac || { cd ..; rm -rf .pkgusr-tmp; return 1; }
local n; n="$(ls -A | wc -l)"
if [ "$n" = 1 ] && [ -d "$(ls -A)" ]; then
pkg_dir="$(ls -A)"
cd .. && rm -rf "$pkg_dir" && mv ".pkgusr-tmp/$pkg_dir" "$pkg_dir"
else
pkg_dir="$name_version"
cd .. && rm -rf "$pkg_dir" && mv .pkgusr-tmp "$pkg_dir"
fi
rm -rf .pkgusr-tmp
echo "$pkg_dir" > "$_pp_dir_marker"
_pp_say "unpacked into $BUILD_ROOT/$pkg_dir"
}
# ---------------------------------------------------------------- enter ----
# The unpacked source directory, for build/install/configure/test.
_pp_enter_source() {
cd "$BUILD_ROOT" || return 1
local d="" pkgdir=""
[ -f "$_pp_dir_marker" ] && pkgdir="$(cat "$_pp_dir_marker" 2>/dev/null)"
if [ -n "$pkgdir" ] && [ -d "$pkgdir" ]; then
cd "$pkgdir" || return 1
return 0
fi
# No marker, but the tree may still be here: one written by an older
# version of this runner, or by a copy of the script under another name.
# Look before giving up -- "run unpack" is the wrong answer when the
# unpacked source is sitting in front of us, and for a big package it
# throws away a compile that succeeded.
if [ -n "${name_version:-}" ] && [ -d "$name_version" ]; then
_pp_say "# found $BUILD_ROOT/$name_version (no marker) -- using it"
echo "$name_version" > "$_pp_dir_marker" 2>/dev/null || true
cd "$name_version" || return 1
return 0
fi
if [ -n "${pkg:-}${pkg_glob:-}" ]; then
local _only="" _c _n=0
for _c in */; do
[ -d "$_c" ] || continue
_n=$((_n + 1)); _only="${_c%/}"
done
if [ "$_n" = 1 ]; then
_pp_say "# found $BUILD_ROOT/$_only (no marker) -- using it"
echo "$_only" > "$_pp_dir_marker" 2>/dev/null || true
cd "$_only" || return 1
return 0
fi
fi
if [ "${pkg_unpack:-yes}" = "no" ] \
|| { [ -z "${pkg:-}" ] && [ -z "${pkg_glob:-}" ] && [ "${is_git:-}" != 1 ]; }; then
# a meta page has nothing to enter; resume where its commands left off
[ -f "$_pp_cwd_marker" ] && d="$(cat "$_pp_cwd_marker" 2>/dev/null)"
[ -n "$d" ] && [ -d "$d" ] && cd "$d"
return 0
fi
_pp_fail "no unpacked source in $BUILD_ROOT -- run: $0 unpack"
return 1
}
# Resume in the SAME subdir the build finished in (pkg_dir/build for meson,
# an out-of-tree build/ for glibc) -- the book's install commands assume it.
# A build that died part-way may have left no record: fall back to the usual
# out-of-tree layouts.
_pp_enter_build() {
_pp_enter_source || return 1
local d
if [ -f "$_pp_cwd_marker" ]; then
d="$(cat "$_pp_cwd_marker" 2>/dev/null)"
[ -n "$d" ] && [ -d "$d" ] && { cd "$d"; return 0; }
fi
[ -e build.ninja ] || [ -e Makefile ] && return 0
for d in build builddir _build obj; do
if [ -d "$d" ] && { [ -f "$d/Makefile" ] || [ -f "$d/build.ninja" ] \
|| [ -f "$d/config.status" ]; }; then
_pp_say "# resuming in ./$d (no build marker was recorded)"
cd "$d"; return 0
fi
done
return 0
}
# ----------------------------------------------------------------- phases ----
# Each phase runs as its OWN process: bash disables `set -e` inside any
# function reached from a tested context (an && chain, an || handler), and
# that suppression is inherited by subshells -- so a failing make would be
# silently ignored. A separate process has its own -e state that nothing
# upstream can suppress. The process is this script again, with
# PKGUSR_PHASE naming the one phase to run, and stdin closed so a book command
# that reads stdin (Expect's PTY check) cannot swallow anything.
_pp_child() {
PKGUSR_PHASE="$1" bash "$_pp_self" </dev/null
}
_pp_phase_unpack() {
if declare -F unpack_pkg >/dev/null; then unpack_pkg; else _pp_unpack; fi
}
_pp_phase_build() {
_pp_enter_source || return 1
# remove a stale CONFIGURED build dir so re-running "build" works, but
# never a source dir that merely happens to be named build/: only the one
# the last build attempt recorded, or one holding build-system artifacts
local d here; here="$PWD"
if [ -f "$_pp_cwd_marker" ]; then
d="$(cat "$_pp_cwd_marker" 2>/dev/null)"
case "$d" in "$here"/?*) [ -d "$d" ] && rm -rf "$d" ;; esac
fi
for d in build builddir _build; do
if [ -d "$d" ] && { [ -f "$d/build.ninja" ] || [ -f "$d/CMakeCache.txt" ] \
|| [ -d "$d/meson-private" ] || [ -f "$d/config.status" ]; }; then
rm -rf "$d"
fi
done
# Record where the build ends -- even when it fails part-way, so install
# and configure can still find it. A failure that never left the source
# dir keeps the previous record: that is where the build subdir is.
#
# And record SEPARATELY that the build actually finished. Without that,
# running a later phase on a source tree whose build never completed dies
# inside the package instead of here:
# make: *** No rule to make target 'install'. Stop.
# -- which says nothing about the real problem, that there is no Makefile
# because configure never ran.
rm -f "$_pp_built_marker" 2>/dev/null
_pp_here="$PWD"
trap '_pp_build_exit' EXIT
build_pkg
}
_pp_build_exit() {
local rc=$?
if [ "$rc" = 0 ] || [ "$PWD" != "$_pp_here" ]; then
pwd > "$_pp_cwd_marker" 2>/dev/null || true
fi
[ "$rc" = 0 ] && { : > "$_pp_built_marker" 2>/dev/null || true; }
return 0
}
# Say it BEFORE the package's own tools fail in their own words. A warning,
# not a refusal: a tree built by an older runner has no record either, and
# refusing there would be wrong.
_pp_warn_unbuilt() {
[ -f "$_pp_built_marker" ] && return 0
[ -f "$_pp_cwd_marker" ] && return 0
_pp_fail "nothing here recorded a finished build phase for $name_version."
echo " If the next lines are 'No rule to make target' or a missing" >&2
echo " Makefile, the build never completed and no later phase can" >&2
echo " stand in for it. Run it first:" >&2
echo " $0 build" >&2
return 0
}
# DOCUMENTATION THAT WAS NEVER BUILT IS NOT A FAILED INSTALL.
#
# parted's page ends with the book's doc block:
# install -v -m644 doc/{FAT,API,parted.{html,txt,pdf,ps,dvi}} \
# /usr/share/doc/parted-3.7
# parted.pdf, parted.ps, parted.dvi and API are built only when texlive is
# installed, and texlive is in avoid.stack. So `install` stopped on
# install: cannot stat 'doc/parted.pdf': No such file or directory
# after the package itself -- 97 files, every binary and library -- was
# already on disk. A missing PDF is not a failed install.
#
# Only for a destination under share/doc, only after the command has been
# tried EXACTLY as the book wrote it, and only for sources that are not
# there: everything else is passed through to /usr/bin/install untouched.
# The function is defined inside this phase, which runs in a child process
# of its own, so nothing outside it ever sees an `install` that is not the
# real one.
_pp_install_docs() {
local last="" a; for a in "$@"; do last="$a"; done
case "$last" in
*/share/doc/*|*/share/doc|*/share/gtk-doc/*) ;;
*) command install "$@"; return $? ;;
esac
command install "$@" 2>/dev/null && return 0
local rc=$?
local keep=() prev="" i=0 n=$# missing="" have=0
for a in "$@"; do
i=$((i + 1))
if [ "$i" -eq "$n" ]; then keep+=("$a"); continue; fi
case "$prev" in
-m|-o|-g|-t|--mode|--owner|--group|--target-directory)
keep+=("$a"); prev=""; continue ;;
esac
case "$a" in -*) keep+=("$a"); prev="$a"; continue ;; esac
prev=""
if [ -e "$a" ] || [ -L "$a" ]; then
keep+=("$a"); have=$((have + 1))
else
missing="$missing $a"
fi
done
# nothing missing -> the failure is a real one, and it is the book's
# command that must be reported, not ours
if [ -z "$missing" ]; then
command install "$@"
return $?
fi
if [ "$have" -gt 0 ]; then
command install "${keep[@]}" || { command install "$@"; return $?; }
fi
_pp_note "not installed, because this build did not produce them:$missing"
_pp_note " they are documentation (the book's doc block builds them"
_pp_note " only with texlive and friends); the package itself is in."
return 0
}
_pp_phase_install() {
_pp_enter_build || return 1
_pp_warn_unbuilt
install() { _pp_install_docs "$@"; }
install_pkg
}
_pp_phase_configure() {
declare -F configure_pkg >/dev/null || return 0
_pp_enter_build || return 1
configure_pkg
}
_pp_phase_test() {
_pp_enter_build || return 1
if declare -F test_pkg >/dev/null; then
test_pkg
elif [ -f build.ninja ]; then meson test || ninja test
elif [ -f Makefile ]; then make check || make test
else _pp_say "no recognized test target -- define test_pkg() for this package"
fi
}
_pp_run_tests() {
case "${PKGUSR_TESTS:-${LFS_RUN_TESTS:-${PM_TEST:-0}}}" in 1) return 0 ;; esac
return 1
}
# The books expect some tests to fail, so the result is reported, not fatal:
# read the summary and judge for yourself.
_pp_test_step() {
local rc=0
_pp_child test || rc=$?
if [ $rc -ne 0 ]; then
_pp_fail "test suite exited $rc -- check the summary above."
echo " Some failures are normal (the book lists the usual suspects);" >&2
echo " this does NOT stop the install." >&2
fi
return 0
}
_pp_phase_fail() {
echo "!! phase '$1' FAILED (exit $2)" >&2
exit "$2"
}
_pp_usage() {
cat <<EOF
usage: $0 {all|unpack|build|test|install|configure|update}
all full run (default); set PKGUSR_TESTS=1 to also run tests
unpack find or fetch the tarball and extract it
build configure + compile (needs unpack)
test run the test suite (needs build)
install install step only -- re-run after fixing a failure, without recompiling
configure post-install configuration
update unpack + build + install, no reconfigure (keeps your config)
EOF
}
# The entry point a script calls last. Does nothing when the script was only
# SOURCED (lfs-helper reads the variables that way).
pkgusr_run() {
if [ "${#BASH_SOURCE[@]}" -lt 2 ]; then
# piped on stdin or bash -c: each phase re-runs the script as a file,
# so it must BE a file
_pp_fail "run an install script by path (bash install_x all), not on stdin"
exit 2
fi
[ "${BASH_SOURCE[1]}" = "$0" ] || return 0 # sourced: metadata only
[ -n "${name_version:-}" ] || { _pp_fail "$0 sets no name_version"; exit 2; }
_pp_self="$(readlink -f "$0")"
_pp_layout
_pp_systemd_shims
if [ -n "${PKGUSR_PHASE:-}" ]; then
# the child process: one phase, at top level, under set -e
set -e
"_pp_phase_$PKGUSR_PHASE"
exit $?
fi
local phase="${1:-all}" p rc
case "$phase" in
all|update)
for p in unpack build; do
_pp_child "$p" || _pp_phase_fail "$p" $?
done
_pp_run_tests && _pp_test_step
_pp_child install || _pp_phase_fail install $?
[ "$phase" = all ] && { _pp_child configure || _pp_phase_fail configure $?; }
;;
unpack|build|install|configure)
_pp_child "$phase" || _pp_phase_fail "$phase" $? ;;
test)
_pp_test_step ;;
*) _pp_usage; exit 2 ;;
esac
return 0
}
# Run directly, this file only identifies itself.
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
case "${1:-}" in
--version) echo "lfs-phases $PKGUSR_LIB_VERSION (build $(_pp_build_id))" ;;
*) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' ;;
esac
exit 0
fi