#!/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 : 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:-}")" >&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 && 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 ` 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; 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 <