Nimbin2git.christianimmanuel.de / Linux From Scratch / dependency_collecter_for_lfs_packageuser_system / depscollector

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.gz
depscollector 8.7 KB · 350 lines 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 "$@"