#!/bin/sh
# port-pac -- a compiled port as a Nervous package (.pac), for `npkg install -f` or the store.
#
#   port-pac [-n] [-y] [-o folder for the .pac] [--setup] <stage> [folder to make it in]
#   port-pac [-n] [-y] [-o folder for the .pac] <package folder>
#   port-pac --describe <stage>
#
# From a stage -- the port's work/stage after `make stage`, holding the compiled usr/local/... -- it
# makes the package folder, named as the package will be, in the current folder (or the one given).
# It is laid out as Forge Studio's Package Builder lays out a package, and opens there:
#
#   <Package>-<Version>-<Architecture>/
#       control           the package's metadata, filled in from the port where it can be: check it
#       files/            the stage's usr/local (and a driver's boot/modules), copied in as it is
#       assets/16x16/     icons of 16, 32 and 48 pixels: title bars and menus
#       assets/32x32/     icons of 32, 64 and 96 pixels: Start menu, desktop and taskbar
#       assets/256x256/   the 256-pixel logo: Package Installer and the store
#       assets/           anything else the package installs, to /usr/local/share/<Package>/
#       libmap.txt        (optional, written by hand) libraries the programs open by name while
#                         running, which ldd cannot see: "library<tab>path" points them at a
#                         path (libGL.so<tab>/usr/lib/nervous/libGL.so.1); a library's name alone
#                         bundles it from /usr/local/lib with the rest (libsoftokn3.so)
#
# and waits for "yes" once the images are in their folders -- any file names: they are told apart by
# their size in pixels and renamed as npkg names icons (<Package>.png, -2x and -3x) -- then builds
# <Package>-<Version>-<Architecture>.pac beside the folder (or in the -o folder). --setup only makes
# the package folder, and --describe only prints what the port says the package is, as control
# lines: Forge Studio does those, and asks for the images in its own window.
#
# Given a package folder made before (or a stage whose folder is there already), it checks that
# every part of it is there, makes what is missing, and builds; it asks first only when something
# still has to be put in. -y never asks, for scripts. A folder made by an older port-pac, with
# control.txt and assets/data/, is moved to this layout.
#
# Libraries the programs need from other ports (anything under /usr/local) go into the package, in
# /usr/local/lib/<Package>/deps, and /usr/local/etc/libmap.d/<Package>.conf points its own files,
# and nothing else, at them: a Nervous system has none of those ports. The ones Nervous carries
# itself in /usr/lib/nervous (Mesa, FFmpeg, libdrm, expat...) are mapped to those instead. With -n
# none are bundled: they are listed, with the ports they come from, for a package that names its
# dependencies in Depends. Libraries a program opens itself (dlopen) cannot be seen: try the
# package once installed. A missing icon size is made from the largest image when ImageMagick,
# GraphicsMagick or rsvg-convert is installed.
#
# Needs npkg, readelf, ldd and od -- all on a Nervous system.
set -eu

me=port-pac
say() { echo "$me: $*"; }
die() { echo "$me: $*" >&2; exit 1; }

usage() {
    sed -n '4,5p' "$0" | sed 's/^# *//' >&2
    exit 2
}

bundle=yes
ask=yes
setup_only=no
describe=no
output=""
while [ $# -gt 0 ]; do
    case $1 in
    -n) bundle=no ;;
    -y) ask=no ;;
    -o)
        [ $# -ge 2 ] || usage
        output=$2
        shift
        ;;
    --setup) setup_only=yes ;;
    --describe) describe=yes ;;
    -*) usage ;;
    *) break ;;
    esac
    shift
done
[ $# -ge 1 ] || usage
for tool in npkg readelf ldd od; do
    command -v "$tool" >/dev/null || die "needs $tool"
done

# --- helpers ---------------------------------------------------------------------------------------

# The port a stage folder belongs to: <port>/work/stage.
port_of_stage() {
    case $1 in
    */work/stage | */work-*/stage) dirname "$(dirname "$1")" ;;
    esac
}

# The ports tree a port is in, for `make -V`: PORTSDIR, the tree around it, a ports/ beside it.
ports_tree_for() {
    for candidate in "${PORTSDIR:-}" "$1/../.." "$1/../ports" "$HOME/Desktop/ports" /usr/ports; do
        if [ -n "$candidate" ] && [ -f "$candidate/Mk/bsd.port.mk" ]; then
            (cd "$candidate" && pwd)
            return
        fi
    done
}

port=""
ports_tree=""
# One variable of the port, or nothing.
port_var() {
    [ -n "$port" ] || return 0
    PORTSDIR=$ports_tree make -C "$port" -V "$1" 2>/dev/null || true
}

# A port's version as npkg writes versions: no epoch, and a port revision as -N.
npkg_version() {
    echo "$1" | sed -e 's/,[0-9]*$//' -e 's/_/-/g'
}

# A field of a control file, or nothing.
field() { # file key
    [ -f "$1" ] || return 0
    awk -v key="$2" '
        /^[ \t]*#/ { next }
        {
            colon = index($0, ":")
            if (!colon) next
            name = substr($0, 1, colon - 1); gsub(/^[ \t]+|[ \t]+$/, "", name)
            if (tolower(name) != tolower(key)) next
            value = substr($0, colon + 1); gsub(/^[ \t]+|[ \t]+$/, "", value)
            print value; exit
        }' "$1"
}

have_tty() {
    (exec < /dev/tty) 2>/dev/null
}

ask_value() { # question default
    have_tty || { echo "$2"; return; }
    printf '%s%s: ' "$1" "${2:+ [$2]}" > /dev/tty
    read -r answer < /dev/tty || answer=""
    echo "${answer:-$2}"
}

# "width height" of a PNG, read from its header; nothing for anything else.
png_size() {
    [ "$(od -An -tx1 -N8 "$1" 2>/dev/null | tr -d ' \n')" = 89504e470d0a1a0a ] || return 0
    set -- $(od -An -tu1 -j16 -N8 "$1")
    echo $(( ($1 << 24) + ($2 << 16) + ($3 << 8) + $4 )) $(( ($5 << 24) + ($6 << 16) + ($7 << 8) + $8 ))
}

is_elf() {
    [ "$(od -An -c -N4 "$1" 2>/dev/null | tr -d ' \n')" = '177ELF' ]
}

# Whether anything compiled is in a tree.
has_elf() {
    find "$1" -type f \( -perm -u+x -o -name '*.so' -o -name '*.so.*' -o -name '*.ko' \) | while read -r object; do
        if is_elf "$object"; then
            echo yes
            break
        fi
    done
}

# Copy a stage's install tree -- usr/local, and a driver's boot/modules -- into files/.
copy_stage() { # stage files
    for top in usr/local boot/modules; do
        [ -d "$1/$top" ] || continue
        mkdir -p "$2/$(dirname "$top")"
        cp -Rp "$1/$top" "$2/$top"
    done
    for entry in $(cd "$1" && ls -A); do
        case $entry in
        usr)
            for sub in $(cd "$1/usr" && ls -A); do
                [ "$sub" = local ] || say "left out usr/$sub: a package installs only under /usr/local"
            done
            ;;
        boot)
            for sub in $(cd "$1/boot" && ls -A); do
                [ "$sub" = modules ] || say "left out boot/$sub: only a driver's /boot/modules is installed"
            done
            ;;
        *) say "left out $entry: a package installs only under /usr/local" ;;
        esac
    done
}

# The publisher Forge Studio fills in where a control file leaves it empty: its PackageBuilder
# [Defaults] when they are set, otherwise Trajectory.
publisher_default() { # key fallback
    ini=${XDG_CONFIG_HOME:-$HOME/.config}/PackageBuilder.ini
    value=""
    [ -f "$ini" ] && value=$(awk -F= -v key="$1" '
        /^\[/ { section = $0; next }
        section == "[Defaults]" && $1 == key { print substr($0, index($0, "=") + 1); exit }' "$ini")
    echo "${value:-$2}"
}

write_control() { # file name version [stage]
    executable=""
    if [ -d "$folder/files/usr/local/bin" ]; then
        if [ -f "$folder/files/usr/local/bin/$2" ]; then
            executable=/usr/local/bin/$2
        else
            executable=$(cd "$folder/files/usr/local/bin" && ls | head -1)
            [ -n "$executable" ] && executable=/usr/local/bin/$executable
        fi
    fi
    homepage=$(port_var WWW | awk '{ print $1 }')
    # Where the files came from: Forge Studio builds a folder with this through port-pac.
    stage_line="# Made by port-pac"
    [ -z "${4:-}" ] || stage_line="# Stage: $4"
    cat > "$1" <<EOF
# $2's metadata. "Field: value", one a line; # starts a comment.
$stage_line
Package: $2
Version: $3
# app (has windows: needs Executable and a 32x32 icon), tool, library, driver, service or data
Type: app
Description: $(port_var COMMENT)
Maintainer: $(publisher_default Maintainer "Trajectory <support@trajectory.co.ug>")
License: $(port_var LICENSE)
Homepage: ${homepage:-$(publisher_default Homepage https://trajectory.co.ug)}
# What the Start menu runs, from files/: /usr/local/bin/...
Executable: $executable
StartMenuName: $(echo "$2" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2) }')
# Accessories, Applications, Development, Education, Games, Graphics, Internet, Multimedia,
# Office, Settings, System or Utilities
Category: Applications
# What it opens, comma-separated extensions (txt,md), for "Open with"
#FileTypes:
# Other packages it needs, comma-separated package names
#Depends:
EOF
}

# The images in one of the size folders: "pixels path", square PNGs only.
images_in() {
    for image in "$1"/*.png "$1"/*.PNG; do
        [ -f "$image" ] || continue
        size=$(png_size "$image")
        if [ -z "$size" ]; then
            say "$(basename "$image") in $(basename "$1")/ is not a PNG; left out" >&2
            continue
        fi
        set -- $size
        if [ "$1" -ne "$2" ]; then
            say "$(basename "$image") in $(basename "$(dirname "$image")")/ is $1x$2, not square; left out" >&2
            continue
        fi
        echo "$1 $image"
    done
}

# Whether there is an image of a size, in any of the size folders.
has_icon() { # pixels
    for size_folder in 16x16 32x32 256x256; do
        images_in "$folder/assets/$size_folder" 2>/dev/null
    done | awk -v px="$1" '$1 == px { found = 1 } END { exit !found }'
}

# What the package folder still lacks, a line each; nothing when it can be built.
missing_parts() {
    if [ ! -d "$folder/files" ] || [ -z "$(ls -A "$folder/files" 2>/dev/null)" ]; then
        echo "files/ is empty: the compiled usr/local/... goes there (or run port-pac with the stage)"
    fi
    control=$folder/control
    for key in Package Version Type; do
        [ -n "$(field "$control" "$key")" ] || echo "control has no $key"
    done
    if [ "$(field "$control" Type)" = app ]; then
        executable=$(field "$control" Executable)
        if [ -z "$executable" ]; then
            echo "control has no Executable, which an app needs"
        # A link counts (Firefox's /usr/local/bin/firefox): what it points at is where it is
        # installed, not here, and the build makes it a file.
        elif [ ! -f "$folder/files$executable" ] && [ ! -L "$folder/files$executable" ]; then
            echo "Executable $executable is not in files/"
        fi
        # One can be made from a bigger image when there is something to make it with.
        if ! has_icon 32; then
            if [ -z "$resizer" ] || { ! has_icon 256 && ! has_icon 64 && ! has_icon 96 && ! has_icon 48; }; then
                echo "no 32-pixel icon in assets/32x32/ (an app needs one)"
            fi
        fi
    fi
}

resizer=""
for tool in magick convert gm rsvg-convert; do
    if command -v "$tool" >/dev/null; then
        resizer=$tool
        break
    fi
done
resize() { # source pixels destination
    case $resizer in
    magick) magick "$1" -background none -resize "$2x$2" -gravity center -extent "$2x$2" "PNG32:$3" ;;
    convert) convert "$1" -background none -resize "$2x$2" -gravity center -extent "$2x$2" "PNG32:$3" ;;
    gm) gm convert "$1" -background none -resize "$2x$2" -gravity center -extent "$2x$2" "PNG32:$3" ;;
    rsvg-convert) case $1 in *.svg) rsvg-convert -w "$2" -h "$2" -o "$3" "$1" ;; *) return 1 ;; esac ;;
    *) return 1 ;;
    esac
}

# --- the package folder ----------------------------------------------------------------------------
source=$(cd "$1" && pwd) || die "no folder $1"
fresh=no
if [ -d "$source/usr/local" ] || [ -d "$source/boot/modules" ]; then
    stage=$source
    port=$(port_of_stage "$stage")
    if [ -n "$port" ] && [ -f "$port/Makefile" ]; then
        ports_tree=$(ports_tree_for "$port")
        [ -n "$ports_tree" ] || { say "no ports tree found for $port (set PORTSDIR)"; port=""; }
    else
        port=""
    fi
    name=$(port_var PKGBASE)
    version=$(npkg_version "$(port_var PKGVERSION)")
    arch=all
    [ -n "$(has_elf "$stage")" ] && arch=x86_64
    if [ "$describe" = yes ]; then
        echo "Package: $name"
        echo "Version: $version"
        echo "Architecture: $arch"
        echo "Description: $(port_var COMMENT)"
        echo "License: $(port_var LICENSE)"
        echo "Homepage: $(port_var WWW | awk '{ print $1 }')"
        exit 0
    fi
    [ -n "$name" ] || name=$(ask_value "Package name (a-z 0-9 + . -)" "")
    [ -n "$version" ] || version=$(ask_value "Version" "")
    [ -n "$name" ] && [ -n "$version" ] || die "a package needs a name and a version"
    parent=$(mkdir -p "${2:-.}" && cd "${2:-.}" && pwd)
    folder=$parent/$name-$version-$arch
    if [ -d "$folder" ]; then
        say "$folder is there already: checking it"
        if [ ! -d "$folder/files" ] || [ -z "$(ls -A "$folder/files" 2>/dev/null)" ]; then
            mkdir -p "$folder/files"
            copy_stage "$stage" "$folder/files"
            say "copied the stage into files/"
        else
            say "files/ kept as it is (empty it to copy the stage in again)"
        fi
    else
        mkdir -p "$folder/files"
        copy_stage "$stage" "$folder/files"
        fresh=yes
        say "made $folder, with the stage in files/"
    fi
    [ -f "$folder/control.txt" ] && [ ! -f "$folder/control" ] && mv "$folder/control.txt" "$folder/control"
    [ -f "$folder/control" ] || write_control "$folder/control" "$name" "$version" "$stage"
elif [ -f "$source/control" ] || [ -f "$source/control.txt" ] || [ -d "$source/files" ] || [ -d "$source/assets" ]; then
    folder=$source
    parent=$(dirname "$folder")
    [ -f "$folder/control.txt" ] && [ ! -f "$folder/control" ] && mv "$folder/control.txt" "$folder/control"
    if [ ! -f "$folder/control" ]; then
        base=$(basename "$folder")
        rest=${base%-*}
        write_control "$folder/control" "${rest%-*}" "${rest##*-}"
        say "made control; check it"
        fresh=yes
    fi
else
    die "$source is neither a stage (no usr/local in it) nor a package folder (no control, files/ or assets/)"
fi

for part in files assets/16x16 assets/32x32 assets/256x256; do
    if [ ! -d "$folder/$part" ]; then
        mkdir -p "$folder/$part"
        [ "$fresh" = yes ] || say "made the missing $part/"
    fi
done
# The older layout's assets/data/: npkg installs what is in assets/ itself to /usr/local/share/<Package>/.
if [ -d "$folder/assets/data" ]; then
    (cd "$folder/assets/data" && find . -mindepth 1 -maxdepth 1 -exec mv {} .. \;)
    rmdir "$folder/assets/data" 2>/dev/null || true
fi

if [ "$setup_only" = yes ]; then
    say "folder $folder"
    exit 0
fi

# Wait for the images (and control) until there is nothing left that the build needs.
while :; do
    missing=$(missing_parts)
    if [ "$fresh" = no ] && [ -z "$missing" ]; then
        break
    fi
    if [ "$ask" = no ] || ! have_tty; then
        [ -z "$missing" ] && break
        echo "$missing" | sed "s/^/$me: /" >&2
        die "the package folder is not ready: $folder"
    fi
    cat > /dev/tty <<EOF

Package folder: $folder (Forge Studio's Package Builder opens it too)
  control          the package's metadata: check it
  assets/16x16/    16, 32 and 48 pixel icons
  assets/32x32/    32, 64 and 96 pixel icons
  assets/256x256/  the 256-pixel logo
  assets/          anything else for /usr/local/share/$(field "$folder/control" Package)/
EOF
    if [ -n "$missing" ]; then
        echo "Still needed:" > /dev/tty
        echo "$missing" | sed 's/^/  - /' > /dev/tty
    fi
    printf 'Put the images in their folders, then type yes to build (no to stop): ' > /dev/tty
    read -r answer < /dev/tty || answer=no
    case $answer in
    [Yy] | [Yy][Ee][Ss]) fresh=no ;;
    [Nn] | [Nn][Oo])
        say "stopped; run port-pac $folder to build it when it is ready"
        exit 0
        ;;
    esac
done

control_in=$folder/control
name=$(field "$control_in" Package)
version=$(field "$control_in" Version)
work=$(mktemp -d "${TMPDIR:-/tmp}/$me.XXXXXX")
trap 'rm -rf "$work"' EXIT INT TERM

# --- icons: named as npkg names them, in the folder itself -----------------------------------------
# Each size folder's images, whatever they are called, by their size in pixels: <Package>.png, -2x
# and -3x. One from another size folder is copied; a size there is none of is made from the logo or
# the largest image when there is something to make it with. An image no size wants is moved to
# unused-icons/, so the folder stays one that npkg and Forge Studio accept as it is.
for size_folder in 16x16 32x32 256x256; do
    for image in "$folder/assets/$size_folder"/*; do
        [ -f "$image" ] || continue
        mv "$image" "$folder/assets/$size_folder/unplaced--$(basename "$image")"
    done
done
: > "$work/all-images"
for size_folder in 16x16 32x32 256x256; do
    images_in "$folder/assets/$size_folder" > "$work/images-$size_folder"
    cat "$work/images-$size_folder" >> "$work/all-images"
done
svg=$(ls "$folder"/assets/*/*.svg 2>/dev/null | head -1 || true)
largest=$(sort -n "$work/all-images" | tail -1 | cut -d' ' -f2-)
# Kept aside: the image itself is about to move into its slot.
[ -z "$largest" ] || { cp "$largest" "$work/largest.png"; largest=$work/largest.png; }
# The first image of a size in a list of "pixels path" lines that is still where the list says.
first_there() { # pixels list
    [ -f "$2" ] || return 0
    awk -v px="$1" '$1 == px { print substr($0, length($1) + 2) }' "$2" | while read -r image; do
        if [ -f "$image" ]; then
            echo "$image"
            break
        fi
    done
}
: > "$work/placed"
made=0
missing=""
for slot in "16x16 $name.png 16" "16x16 $name-2x.png 32" "16x16 $name-3x.png 48" \
    "32x32 $name.png 32" "32x32 $name-2x.png 64" "32x32 $name-3x.png 96" "256x256 $name.png 256"; do
    set -- $slot
    target=$folder/assets/$1/$2
    own=$(first_there "$3" "$work/images-$1")
    other=$(first_there "$3" "$work/all-images")
    placed=$(first_there "$3" "$work/placed")
    if [ -n "$own" ]; then
        mv "$own" "$target"
    elif [ -n "$other" ]; then
        cp "$other" "$target"
    elif [ -n "$placed" ]; then
        cp "$placed" "$target"
    elif [ -n "$resizer" ] && [ -n "$svg" ] && resize "$svg" "$3" "$target" 2>/dev/null; then
        made=$((made + 1))
    elif [ -n "$resizer" ] && [ -n "$largest" ] && resize "$largest" "$3" "$target" 2>/dev/null; then
        made=$((made + 1))
    fi
    if [ -f "$target" ]; then
        echo "$3 $target" >> "$work/placed"
    else
        case $2 in
        *-2x.png | *-3x.png) ;;
        *) missing="$missing $3" ;;
        esac
    fi
done
if [ "$made" -gt 0 ]; then
    say "$made icon sizes made with $resizer"
fi
if [ -n "$missing" ]; then
    say "no${missing} pixel icon (put a PNG of that size in its folder, or install ImageMagick to make it from a bigger one)"
fi
for size_folder in 16x16 32x32 256x256; do
    for image in "$folder/assets/$size_folder"/unplaced--*; do
        [ -f "$image" ] || continue
        mkdir -p "$folder/unused-icons/$size_folder"
        mv "$image" "$folder/unused-icons/$size_folder/${image##*/unplaced--}"
        say "$size_folder/${image##*/unplaced--} is no size an icon there needs; moved to unused-icons/"
    done
    rmdir "$folder/assets/$size_folder" 2>/dev/null || true
done

# --- the build copy: npkg's own layout --------------------------------------------------------------
pac=$work/$name
files=$pac/files
mkdir -p "$pac"
cp -Rp "$folder/files" "$files"
say "$name $version from $folder"

# --- symbolic links: npkg packages none ---------------------------------------------------------------
# Where a link ends inside the package, following links on the way; nothing when it leaves it.
link_end() {
    path=$1
    hops=0
    while [ -L "$path" ] && [ "$hops" -lt 8 ]; do
        target=$(readlink "$path")
        case $target in
        /*) path=$files$target ;;
        *) path=$(dirname "$path")/$target ;;
        esac
        hops=$((hops + 1))
    done
    if [ -e "$path" ] && [ ! -L "$path" ]; then
        echo "$path"
    fi
}
# A link to a program becomes a script that runs it where it is installed (a copy would look for its
# libraries beside itself: Firefox's /usr/local/bin/firefox); a link to anything else becomes a copy
# of it. A folder copied in may hold links of its own, hence the passes.
replaced=0
for pass in 1 2 3; do
    links=$(find "$files" -type l | wc -l)
    [ "$links" -gt 0 ] || break
    find "$files" -type l | while read -r link; do
        end=$(link_end "$link")
        target=$(readlink "$link")
        rm -f "$link"
        if [ -z "$end" ]; then
            say "left out ${link#$files}: a link to $target, which the package does not have"
        elif [ -d "$end" ]; then
            cp -Rp "$end" "$link"
        elif [ -x "$end" ] && case $end in *.so | *.so.*) false ;; *) true ;; esac; then
            printf '#!/bin/sh\nexec %s "$@"\n' "${end#$files}" > "$link"
            chmod 755 "$link"
        else
            cp -p "$end" "$link"
        fi
    done
    replaced=$((replaced + links))
done
if [ "$replaced" -gt 0 ]; then
    say "$replaced symbolic links made into files (a program's into a script that runs it)"
fi

# --- libraries from other ports -------------------------------------------------------------------
elves=$work/elves
find "$files" -type f \( -perm -u+x -o -name '*.so' -o -name '*.so.*' \) | while read -r object; do
    if is_elf "$object"; then
        echo "$object"
    fi
done > "$elves"

# The package's own libraries resolve to its own copies, and are never bundled twice.
own_dirs=$(find "$files" \( -type f -o -type l \) -name '*.so*' | xargs -r -n1 dirname | sort -u | tr '\n' ':')
find "$files" \( -type f -o -type l \) -name '*.so*' | xargs -r -n1 basename | sort -u > "$work/own"

# What the objects given need from /usr/local that the package does not hold: "name path", or
# "name -" for one that is not found.
needs() {
    for object in "$@"; do
        LD_LIBRARY_PATH=${own_dirs}${LD_LIBRARY_PATH:-} ldd "$object" 2>/dev/null || true
    done | awk -v files="$files/" -v ownfile="$work/own" '
        BEGIN { while ((getline line < ownfile) > 0) own[line] }
        $2 == "=>" && $3 == "not" { print $1, "-"; next }
        $2 == "=>" && $3 ~ /^\/usr\/local\// && index($3, files) != 1 && !($1 in own) { print $1, $3 }' | sort -u
}

: > "$work/needs"
# Libraries a program opens by name itself, named alone in libmap.txt (NSS's libsoftokn3.so beside
# libnss3, say): found under /usr/local/lib and bundled with the rest, with what they need.
if [ -f "$folder/libmap.txt" ]; then
    grep -v '^[[:space:]]*#' "$folder/libmap.txt" | awk 'NF == 1 { print $1 }' | while read -r lib; do
        path=$(find /usr/local/lib \( -type f -o -type l \) -name "$lib" 2>/dev/null | head -1)
        if [ -n "$path" ]; then
            echo "$lib $path"
        else
            say "libmap.txt names $lib, which is not under /usr/local/lib here" >&2
        fi
    done >> "$work/needs"
fi
if [ -s "$elves" ] || [ -s "$work/needs" ]; then
    [ -s "$elves" ] && needs $(cat "$elves") >> "$work/needs"
    # Until nothing new turns up: a bundled library's own dependencies.
    while :; do
        before=$(wc -l < "$work/needs")
        needs $(awk '$2 != "-" { print $2 }' "$work/needs") >> "$work/needs"
        sort -u "$work/needs" -o "$work/needs"
        [ "$(wc -l < "$work/needs")" -eq "$before" ] && break
    done
    awk '$2 == "-" { print $1 }' "$work/needs" | sort -u > "$work/unfound"
    if [ -s "$work/unfound" ]; then
        say "not found on this machine, so not bundled (install the ports that have them and run again):"
        sed 's/^/    /' "$work/unfound"
    fi
    awk '$2 != "-"' "$work/needs" > "$work/needs.tmp"
    mv "$work/needs.tmp" "$work/needs"
fi

: > "$work/map"
if [ -s "$work/needs" ]; then
    deps_prefix=/usr/local/lib/$name/deps
    : > "$work/depends"
    bundled=0
    mapped=0
    while read -r lib path; do
        if [ -f "/usr/lib/nervous/$lib" ]; then
            printf '%s\t%s\n' "$lib" "/usr/lib/nervous/$lib" >> "$work/map"
            mapped=$((mapped + 1))
        elif [ "$bundle" = no ]; then
            from=$(npkg which -q "$path" 2>/dev/null || true)
            echo "    $lib  ($path${from:+, from $from})" >> "$work/depends"
        else
            file=$(basename "$path")
            mkdir -p "$files$deps_prefix"
            [ -f "$files$deps_prefix/$file" ] || cp -L "$path" "$files$deps_prefix/$file"
            printf '%s\t%s\n' "$lib" "$deps_prefix/$file" >> "$work/map"
            bundled=$((bundled + 1))
        fi
    done < "$work/needs"
    if [ -s "$work/depends" ]; then
        say "needs these from other ports (name each port's package in Depends):"
        cat "$work/depends"
    fi
    if [ "$bundled" -gt 0 ]; then
        say "$bundled libraries bundled in $deps_prefix"
    fi
    if [ "$mapped" -gt 0 ]; then
        say "$mapped libraries mapped to Nervous's own in /usr/lib/nervous"
    fi
fi

# Libraries a program opens by name itself, which ldd cannot see: "name<tab>path" lines of the
# folder's libmap.txt (Firefox's libGL.so to /usr/lib/nervous/libGL.so.1, say).
if [ -f "$folder/libmap.txt" ]; then
    grep -v '^[[:space:]]*#' "$folder/libmap.txt" | awk 'NF == 2 { printf "%s\t%s\n", $1, $2 }' >> "$work/map"
fi

if [ -s "$work/map" ]; then
    # rtld picks a section by the object that asks for a library, not by the program that runs:
    # Firefox's programs ask for nothing bundled, its libxul.so asks for libffi.so.8. So every object
    # of the package's own is pointed at the copies -- its folder in /usr/local/lib, bundled libraries
    # and all, as one section; each object elsewhere (a program in /usr/local/bin) by its path.
    mkdir -p "$files/usr/local/etc/libmap.d"
    conf=$files/usr/local/etc/libmap.d/$name.conf
    own_dir=/usr/local/lib/$name/
    {
        echo "# Where $name's programs and libraries find the libraries they were built against (port-pac)."
        echo "# Only its own files are pointed there; everything else keeps the system's."
        if [ -d "$files$own_dir" ]; then
            echo "[$own_dir]"
            cat "$work/map"
        fi
        while read -r object; do
            path=${object#$files}
            case $path in
            "$own_dir"*) continue ;;
            esac
            echo "[$path]"
            cat "$work/map"
        done < "$elves"
    } > "$conf"
    if ! grep -q '^\[' "$conf"; then
        rm -f "$conf"
        say "libraries to point at, but nothing of the package's own to point at them"
    fi
fi

# --- control ---------------------------------------------------------------------------------------
control=$pac/control
grep -v '^[[:space:]]*#' "$control_in" | grep -v '^[[:space:]]*$' |
    awk '{ value = substr($0, index($0, ":") + 1); gsub(/^[ \t]+|[ \t]+$/, "", value); if (index($0, ":") && value != "") print }' > "$control"
if [ -z "$(field "$control_in" Architecture)" ]; then
    if [ -s "$elves" ]; then
        echo "Architecture: x86_64" >> "$control"
    else
        echo "Architecture: all" >> "$control"
    fi
fi

# --- assets: the icons placed above, and the rest for /usr/local/share/<Package>/ ---------------------
if [ -d "$folder/assets" ]; then
    cp -Rp "$folder/assets" "$pac/assets"
fi

# --- build -----------------------------------------------------------------------------------------
chmod -R go-w "$pac"
npkg check "$pac" || die "npkg check found problems (above); fix them in $folder and run port-pac $folder again"
output=${output:-$parent}
mkdir -p "$output"
npkg build "$pac" -o "$output"
ls -l "$output/$name-$version-"*.pac
