dependency_collecter_for_lfs_packageuser_system git · main
git clone https://git.christianimmanuel.de/linux-from-scratch/dependency_collecter_for_lfs_packageuser_system.gitwget https://git.christianimmanuel.de/linux-from-scratch/dependency_collecter_for_lfs_packageuser_system/archive/dependency_collecter_for_lfs_packageuser_system.tar.gzdepscollector raw
#!/usr/bin/env bash
set -euo pipefail
DB_PATH_DEFAULT="/var/lib/depscollector/depscollector.db"
db_path="$DB_PATH_DEFAULT"
update=0
verbose=0
declare -a bin_dirs=()
die() { echo "Error: $*" >&2; exit 1; }
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"; }
log() { (( verbose )) && echo "$*" >&2 || true; }
sql_escape() { local s=${1//\'/\'\'}; printf "%s" "$s"; }
usage() {
cat <<EOF
depscollector - scan binaries and store ELF NEEDED->library relationships in SQLite
Usage:
depscollector [-u] [-v] [-d DB] [-b DIR]...
Options:
-u Update mode: remove stale DB entries, then add only new binaries
-v Verbose logging
-d DB SQLite database path (default: $DB_PATH_DEFAULT)
-b DIR Directory to scan for binaries (can be used multiple times)
-h Help
Behavior:
If no -b DIR is given, it reuses directories from the 'sources' table;
if still empty, it falls back to /usr/bin.
EOF
}
# --------------------- sqlite single-connection ---------------------
# We run a persistent sqlite3 process and talk to it via stdin/stdout.
# For SELECTs, we append a unique marker row and read until we see it.
declare -a SQLCOPROC=()
sql_start() {
mkdir -p "$(dirname "$db_path")"
coproc SQLCOPROC { sqlite3 "$db_path" -batch; }
# Configure session
sql_send ".bail on"
sql_send ".echo off"
sql_send ".headers off"
sql_send ".mode list"
sql_send ".separator $'\t'"
sql_send "PRAGMA foreign_keys = ON;"
}
sql_stop() {
# Politely ask sqlite to quit; ignore errors during shutdown.
{ sql_send ".quit"; } 2>/dev/null || true
{ wait "${SQLCOPROC_PID:-0}"; } 2>/dev/null || true
}
sql_send() {
# Send exactly one line/statement (sqlite3 accepts multi-stmt lines too).
printf '%s\n' "$1" >&"${SQLCOPROC[1]}"
}
sql_exec() {
# Execute SQL that produces no output (or output we don't care about).
# We still add a marker SELECT to ensure sqlite processes it before we continue.
local sql="$1"
local mark="__EXEC_DONE__${RANDOM}${RANDOM}__"
sql_send "$sql"
sql_send "SELECT '$mark';"
local line
while IFS= read -r line <&"${SQLCOPROC[0]}"; do
[[ "$line" == "$mark" ]] && break
done
}
sql_query_lines() {
# Print result lines of a SELECT to stdout.
local sql="$1"
local mark="__END__${RANDOM}${RANDOM}__"
sql_send "$sql"
sql_send "SELECT '$mark';"
local line
while IFS= read -r line <&"${SQLCOPROC[0]}"; do
[[ "$line" == "$mark" ]] && break
printf '%s\n' "$line"
done
}
# --------------------- DB init ---------------------
init_db() {
sql_exec "
CREATE TABLE IF NOT EXISTS sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dir TEXT UNIQUE NOT NULL
);"
sql_exec "
CREATE TABLE IF NOT EXISTS binaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
owner_user TEXT,
owner_group TEXT,
source_id INTEGER NOT NULL,
FOREIGN KEY (source_id) REFERENCES sources(id)
);"
sql_exec "
CREATE TABLE IF NOT EXISTS libraries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
owner_user TEXT,
owner_group TEXT
);"
sql_exec "
CREATE TABLE IF NOT EXISTS binary_dependencies (
binary_id INTEGER NOT NULL,
library_id INTEGER NOT NULL,
PRIMARY KEY (binary_id, library_id),
FOREIGN KEY (binary_id) REFERENCES binaries(id) ON DELETE CASCADE,
FOREIGN KEY (library_id) REFERENCES libraries(id) ON DELETE CASCADE
);"
}
# --------------------- scan helpers ---------------------
get_owner() { stat -c '%U:%G' "$1"; }
is_elf() { file -b "$1" 2>/dev/null | grep -q 'ELF'; }
collect_bins_from_dirs() {
local -a all=()
local dir
for dir in "${bin_dirs[@]}"; do
[[ -d "$dir" ]] || { log "Skipping non-dir: $dir"; continue; }
while IFS= read -r -d '' f; do
all+=("$f")
done < <(find "$dir" -type f -perm -111 -print0 2>/dev/null)
done
printf "%s\n" "${all[@]}" | sort -u
}
# One-time ldconfig cache: libname -> full path
declare -A LDCACHE=()
build_ldcache() {
local name path
while IFS= read -r line; do
# Example line:
# libz.so.1 (libc6,x86-64) => /lib/libz.so.1
name="$(awk '{print $1}' <<<"$line")"
path="$(awk '{print $NF}' <<<"$line")"
[[ -n "$name" && -n "$path" ]] || continue
# Keep first hit
[[ -n "${LDCACHE[$name]+x}" ]] || LDCACHE["$name"]="$path"
done < <(ldconfig -p 2>/dev/null || true)
}
find_library_path() {
local lib="$1"
printf '%s' "${LDCACHE[$lib]:-}"
}
# --------------------- DB operations ---------------------
insert_binary() {
local path="$1"
local name owner_user owner_group source_dir
name="$(basename "$path")"
IFS=: read -r owner_user owner_group < <(get_owner "$path")
source_dir="$(dirname "$path")"
local p n ou og sd
p="$(sql_escape "$path")"
n="$(sql_escape "$name")"
ou="$(sql_escape "$owner_user")"
og="$(sql_escape "$owner_group")"
sd="$(sql_escape "$source_dir")"
sql_exec "INSERT OR IGNORE INTO sources (dir) VALUES ('$sd');"
sql_exec "
INSERT OR IGNORE INTO binaries (path, name, owner_user, owner_group, source_id)
VALUES ('$p', '$n', '$ou', '$og', (SELECT id FROM sources WHERE dir = '$sd'));
"
}
insert_library() {
local lib_path="$1"
[[ -e "$lib_path" ]] || return 0
local name owner_user owner_group
name="$(basename "$lib_path")"
IFS=: read -r owner_user owner_group < <(get_owner "$lib_path")
local p n ou og
p="$(sql_escape "$lib_path")"
n="$(sql_escape "$name")"
ou="$(sql_escape "$owner_user")"
og="$(sql_escape "$owner_group")"
sql_exec "
INSERT OR IGNORE INTO libraries (path, name, owner_user, owner_group)
VALUES ('$p', '$n', '$ou', '$og');
"
}
link_binary_to_library() {
local binary_path="$1"
local lib_path="$2"
local bp lp
bp="$(sql_escape "$binary_path")"
lp="$(sql_escape "$lib_path")"
sql_exec "
INSERT OR IGNORE INTO binary_dependencies (binary_id, library_id)
SELECT b.id, l.id
FROM binaries b
JOIN libraries l
WHERE b.path = '$bp' AND l.path = '$lp';
"
}
process_binary() {
local binary="$1"
insert_binary "$binary"
is_elf "$binary" || return 0
local libs lib lib_path
libs="$(readelf -d "$binary" 2>/dev/null | awk '/NEEDED/ { gsub(/
$$
|
$$
/, "", $NF); print $NF }' || true)"
while IFS= read -r lib; do
[[ -n "$lib" ]] || continue
lib_path="$(find_library_path "$lib" || true)"
if [[ -n "$lib_path" ]]; then
insert_library "$lib_path"
link_binary_to_library "$binary" "$lib_path"
fi
done <<<"$libs"
}
remove_stale_entries() {
mapfile -t current_bins < <(collect_bins_from_dirs)
mapfile -t db_bins < <(sql_query_lines "SELECT path FROM binaries ORDER BY path;")
local tmp_current tmp_db
tmp_current="$(mktemp)"
tmp_db="$(mktemp)"
printf "%s\n" "${current_bins[@]}" | sort >"$tmp_current"
printf "%s\n" "${db_bins[@]}" | sort >"$tmp_db"
while IFS= read -r stale_bin; do
[[ -n "$stale_bin" ]] || continue
local s; s="$(sql_escape "$stale_bin")"
sql_exec "DELETE FROM binaries WHERE path = '$s';"
done < <(comm -23 "$tmp_db" "$tmp_current")
rm -f "$tmp_current" "$tmp_db"
sql_exec "
DELETE FROM libraries
WHERE id NOT IN (SELECT library_id FROM binary_dependencies);
"
}
# --------------------- main ---------------------
main() {
need_cmd sqlite3
need_cmd find
need_cmd stat
need_cmd file
need_cmd readelf
need_cmd ldconfig
need_cmd awk
need_cmd sort
need_cmd comm
while [[ $# -gt 0 ]]; do
case "$1" in
-u) update=1; shift ;;
-v) verbose=1; shift ;;
-d) [[ $# -ge 2 ]] || die "-d requires DB"; db_path="$2"; shift 2 ;;
-b) [[ $# -ge 2 ]] || die "-b requires DIR"; bin_dirs+=("$2"); shift 2 ;;
-h) usage; exit 0 ;;
*) die "Invalid option: $1 (try -h)" ;;
esac
done
sql_start
trap sql_stop EXIT
init_db
build_ldcache
if [[ ${#bin_dirs[@]} -eq 0 ]]; then
mapfile -t bin_dirs < <(sql_query_lines "SELECT DISTINCT dir FROM sources ORDER BY dir;")
fi
if [[ ${#bin_dirs[@]} -eq 0 ]]; then
bin_dirs=("/usr/bin")
fi
log "DB: $db_path"
log "Dirs: ${bin_dirs[*]}"
sql_exec "BEGIN;"
if (( update )); then
remove_stale_entries
mapfile -t all_bins < <(collect_bins_from_dirs)
mapfile -t tracked < <(sql_query_lines "SELECT path FROM binaries ORDER BY path;")
local tmp_all tmp_tracked
tmp_all="$(mktemp)"
tmp_tracked="$(mktemp)"
printf "%s\n" "${all_bins[@]}" | sort >"$tmp_all"
printf "%s\n" "${tracked[@]}" | sort >"$tmp_tracked"
while IFS= read -r new_bin; do
[[ -n "$new_bin" ]] || continue
process_binary "$new_bin"
done < <(comm -13 "$tmp_tracked" "$tmp_all")
rm -f "$tmp_all" "$tmp_tracked"
else
while IFS= read -r bin; do
[[ -n "$bin" ]] || continue
process_binary "$bin"
done < <(collect_bins_from_dirs)
fi
sql_exec "COMMIT;"
}
main "$@"