/
Leprekon
/
packwrap
Обзор
Документация
Войти
/
Leprekon
/
packwrap
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
lib/api.sh
1 367 строк
46 KB
Sergei Chernov
Initial Import
14 июл 2026, 23:07
14 июл 2026, 23:07
b9ecd64
Код
Авторство
О чём код?
# packwrap/lib/api.sh — Core library for type loading and repository management # Sourced by bin/packwrap PACKWRAP_REPOS_BASE="${PACKWRAP_REPOS_BASE:-$HOME/packwrap/repos}" PACKWRAP_PROJECTS_BASE="${PACKWRAP_PROJECTS_BASE:-$HOME/packwrap/projects}" PACKWRAP_BUILD_BASE="${PACKWRAP_BUILD_BASE:-$HOME/packwrap/build}" PACKWRAP_CACHE_BASE="${PACKWRAP_CACHE_BASE:-$HOME/.cache/packwrap}" # ---- Global config ---- # Config files are shell-sourced; can set any PACKWRAP_* variable, # GENTOO_OVERLAY, etc. Environment variables take precedence. # Use PACKWRAP_CONFIG env var to override the entire config path. # Otherwise system config loads first, then user config may override. if [ -n "${PACKWRAP_CONFIG:-}" ]; then [ -f "$PACKWRAP_CONFIG" ] && . "$PACKWRAP_CONFIG" else [ -f "/etc/packwrap/packwrap.conf" ] && . "/etc/packwrap/packwrap.conf" _pw_cfg="${XDG_CONFIG_HOME:-$HOME/.config}/packwrap/packwrap.conf" [ -f "$_pw_cfg" ] && . "$_pw_cfg" unset _pw_cfg fi # ---- Login-user fallback for data directories ---- # When running as root (sudo, su), $HOME points to /root and the default # paths below won't match the original user's existing packwrap data. # Check logname and fall back to that user's home. if [ -z "${PACKWRAP_PROJECTS_BASE##*packwrap/projects*}" ] && [ ! -d "$PACKWRAP_PROJECTS_BASE" ]; then _pw_login="$(logname 2>/dev/null)" && [ -n "$_pw_login" ] && [ "$_pw_login" != "root" ] && [ -d "/home/$_pw_login/packwrap/projects" ] && { PACKWRAP_PROJECTS_BASE="/home/$_pw_login/packwrap/projects" PACKWRAP_BUILD_BASE="/home/$_pw_login/packwrap/build" PACKWRAP_REPOS_BASE="/home/$_pw_login/packwrap/repos" } unset _pw_login fi # ---- Logging ---- _log() { echo "$@" >&2; } _die() { _log "Error: $*"; exit 1; } # ---- Architecture mapping ---- # Normalise user-supplied arch string to canonical form _arch_normalize() { local a="$1" case "$a" in x86_64|amd64) echo "x86_64" ;; aarch64|arm64) echo "aarch64" ;; armv7*|armhf) echo "armv7hl" ;; i[3-6]86) echo "i386" ;; mips|mipsel|mips64|mips64el) echo "$a" ;; noarch) echo "noarch" ;; riscv64) echo "riscv64" ;; *) echo "$a" ;; esac } _arch_to_ebuild() { _arch_to_deb "$@"; } _arch_to_rpm() { local a="$1" case "$a" in x86_64|amd64) echo "x86_64" ;; aarch64|arm64) echo "aarch64" ;; armv7*|armhf) echo "armv7hl" ;; i[3-6]86) echo "i386" ;; mips|mipsel|mips64|mips64el) echo "$a" ;; noarch) echo "noarch" ;; riscv64) echo "riscv64" ;; *) echo "$a" ;; esac } _arch_to_deb() { local a="$1" case "$a" in x86_64|amd64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; armv7*|armhf) echo "armhf" ;; i[3-6]86) echo "i386" ;; mips|mipsel|mips64|mips64el) echo "$a" ;; noarch) echo "all" ;; riscv64) echo "riscv64" ;; *) echo "$a" ;; esac } # ---- Version normalization ---- # Normalize a raw version string for a specific package type # Handles Gentoo-style pre-release suffixes (_alpha, _beta, _rc, _pre, _p) _normalize_version_for_type() { local ver="$1" type="$2" case "$type" in ebuild) echo "$ver" ;; rpm) # Strip Gentoo pre-release suffix; RPM uses Release for that if [[ "$ver" =~ ^([0-9].*)_(alpha|beta|rc|pre|p)([0-9]*)$ ]]; then echo "${BASH_REMATCH[1]}" else echo "$ver" fi ;; deb) # Debian forbids underscore; use tilde (sorts before base) echo "$ver" | sed 's/_/~/g' ;; *) echo "$ver" ;; esac } # Extract pre-release suffix for types that need it (e.g. RPM Release tag) # Returns empty string if no suffix detected _normalize_prerel() { local ver="$1" type="$2" case "$type" in rpm) if [[ "$ver" =~ ^[0-9].*_(alpha|beta|rc|pre|p)([0-9]*)$ ]]; then echo "${BASH_REMATCH[1]}${BASH_REMATCH[2]}" fi ;; esac } # Build a Release string for RPM from a version string _normalize_rpm_release() { local ver="$1" local prerel prerel="$(_normalize_prerel "$ver" rpm)" if [ -n "$prerel" ]; then echo "0.1.${prerel}%{?dist}" else echo "1%{?dist}" fi } # ---- Dependency normalization ---- # Normalize a raw package name to generic (RPM-style) form. _normalize_dep_name() { local name="$1" case "$name" in xz-utils|liblzma) echo "xz" ;; ncurses-bin|libncurses) echo "ncurses" ;; 7zip|7z|p7zip-full) echo "p7zip" ;; *) echo "$name" ;; esac } # Strip leading operator, category prefix, and version suffix from a Gentoo dep atom. # Input: >=app-arch/bzip2-1.0.8 or ~app-arch/bzip2-1.0.8 # Output: bzip2 _strip_ebuild_atom() { local atom="$1" # Strip leading operators case "$atom" in \>=*|\<=*|\>*|\<*) ;; esac atom="${atom#>=}"; atom="${atom#<=}"; atom="${atom#\~}"; atom="${atom#=}" atom="${atom#>}"; atom="${atom#<}"; atom="${atom#\!}" # Strip leading |, ||, ? atom="${atom#||}"; atom="${atom#|}"; atom="${atom#\?}" # Strip category (everything up to and including /) atom="${atom#*/}" # Strip slot/subslot :slot atom="${atom%%:*}" # Strip version suffix — first -DIGIT to end atom="$(echo "$atom" | sed 's/-[0-9].*$//')" echo "$atom" } # Extract raw dependency names from ebuild files _extract_ebuild_deps() { local proj_path="$1" local all="" local f atom raw pkgs while IFS= read -r -d '' f; do # Join continuation lines, extract content of DEPEND/RDEPEND/BDEPEND blocks raw="$(sed -z 's/\\\n//g' "$f" 2>/dev/null \ | grep -zoE '(R|B)?DEPEND\+?="[^"]*"' 2>/dev/null \ | tr '\0' '\n' || true)" [ -z "$raw" ] && continue # Extract all words containing / (Gentoo category/package format) pkgs="$(echo "$raw" | grep -oE '\S+/\S+' || true)" [ -z "$pkgs" ] && continue while IFS= read -r atom; do [ -z "$atom" ] && continue atom="$(_strip_ebuild_atom "$atom")" [ -n "$atom" ] && all="$all $atom" done < <(echo "$pkgs") done < <(find "$proj_path" -name '*.ebuild' -print0 2>/dev/null) [ -n "$all" ] && echo "$all" | tr ' ' '\n' | sed '/^$/d' | sort -u } # Extract raw dependency names from spec files _extract_spec_deps() { local proj_path="$1" local all="" line word local -a spec_files=() while IFS= read -r -d '' f; do spec_files+=("$f") done < <(find "$proj_path" -maxdepth 1 -name '*.spec' -print0 2>/dev/null) [ ${#spec_files[@]} -eq 0 ] && return local data data="$(cat "${spec_files[@]}" 2>/dev/null)" # Join continuation lines (backslash at end) data="$(echo "$data" | sed ':a;/\\$/N;s/\\\n//;ta')" # Extract Requires: and BuildRequires: lines local req_lines req_lines="$(echo "$data" | grep -iE '^(Requires|BuildRequires):' 2>/dev/null || true)" [ -z "$req_lines" ] && return while IFS= read -r line; do line="${line#*:}" # Split on commas local part pkg IFS=',' read -ra parts <<< "$line" for part in "${parts[@]}"; do # Take the first word as package name (strip version constraints) read -r pkg _ <<< "$part" || true [ -z "$pkg" ] && continue # Skip ${...} variable references [[ "$pkg" == *'${'* ]] && continue all="$all $pkg" done done < <(echo "$req_lines") [ -n "$all" ] && echo "$all" | tr ' ' '\n' | sed '/^$/d' | sort -u } # Extract raw dependency names from debian/control _extract_control_deps() { local proj_path="$1" local control="$proj_path/debian/control" [ -f "$control" ] || return local data section line part all="" data="$(cat "$control" 2>/dev/null)" # Collapse continuation lines (those ending with comma) data="$(echo "$data" | sed ':a;/,$/{N;s/,\n/, /;ba}')" # Extract Depends: and Build-Depends: from source and binary sections local dep_lines dep_lines="$(echo "$data" | grep -E '^(Depends|Build-Depends):' 2>/dev/null || true)" [ -z "$dep_lines" ] && return while IFS= read -r line; do line="${line#*:}" local part pkg IFS=',' read -ra parts <<< "$line" for part in "${parts[@]}"; do # Take the first word as package name (strip version constraints) read -r pkg _ <<< "$part" || true [ -z "$pkg" ] && continue # Skip ${...} and built-in substitutions [[ "$pkg" == *'${'* ]] && continue [[ "$pkg" == *'shlibs:'* ]] && continue [[ "$pkg" == *'misc:'* ]] && continue all="$all $pkg" done done < <(echo "$dep_lines") [ -n "$all" ] && echo "$all" | tr ' ' '\n' | sed '/^$/d' | sort -u } # ---- Metadata discovery ---- # Scan a project directory and produce a metadata file for populate scripts. # Returns the path to the temp file. _discover_metadata() { local proj_path="$1" local meta_file meta_file="$(mktemp /tmp/packwrap-meta-XXXXXX)" # Defaults { echo "PKG_DESCRIPTION=''" echo "PKG_HOMEPAGE=''" echo "PKG_LICENSE=''" echo "PKG_MAINTAINER=''" echo "PKG_CHANGELOG=''" echo "PKG_BUILD_SYSTEM=''" echo "PKG_BINARIES=''" echo "PKG_DEPENDS=''" } > "$meta_file" # --- DESCRIPTION from README --- local readme for readme in "$proj_path"/README{,.md,.rst,.txt}; do [ -f "$readme" ] || continue local desc desc="$(sed -n '/^[[:space:]]*$/{ :a;n;/^[[:space:]]*$/!{p;q} };/^[#=]/!p' "$readme" 2>/dev/null | head -3 | tr '\n' ' ' | sed 's/[[:space:]]\{2,\}/ /g; s/^ *//; s/ *$//')" if [ -n "$desc" ]; then local escaped printf -v escaped "%q" "$desc" echo "PKG_DESCRIPTION=${escaped}" >> "$meta_file" fi break done # --- HOMEPAGE from README URLs or git remote --- local url="" if [ -d "$proj_path/.git" ]; then url="$(git -C "$proj_path" remote get-url origin 2>/dev/null || true)" if [ -n "$url" ]; then # Convert git@ to https url="${url#git@}" url="${url/://}" url="${url%.git}" fi fi if [ -z "$url" ] && [ -f "$proj_path/README.md" ]; then url="$(grep -Eo 'https?://[^")>[:space:]]+' "$proj_path/README.md" 2>/dev/null | head -1 || true)" fi if [ -n "$url" ]; then local escaped printf -v escaped "%q" "$url" echo "PKG_HOMEPAGE=${escaped}" >> "$meta_file" fi # --- LICENSE from LICENSE / COPYING --- local license_file for license_file in "$proj_path"/LICENSE{,.md} "$proj_path"/COPYING{,.md} "$proj_path"/COPYRIGHT; do [ -f "$license_file" ] || continue local lic lic="$(head -20 "$license_file" 2>/dev/null)" # Detect license type if echo "$lic" | grep -qi "GNU GENERAL PUBLIC LICENSE"; then if echo "$lic" | grep -qi "Version 3"; then echo "PKG_LICENSE='GPL-3.0-or-later'" >> "$meta_file" elif echo "$lic" | grep -qi "Version 2"; then echo "PKG_LICENSE='GPL-2.0-or-later'" >> "$meta_file" else echo "PKG_LICENSE='GPL'" >> "$meta_file" fi elif echo "$lic" | grep -qi "MIT License\|Permission is hereby granted"; then echo "PKG_LICENSE='MIT'" >> "$meta_file" elif echo "$lic" | grep -qi "Apache License"; then if echo "$lic" | grep -qi "Version 2"; then echo "PKG_LICENSE='Apache-2.0'" >> "$meta_file" else echo "PKG_LICENSE='Apache'" >> "$meta_file" fi elif echo "$lic" | grep -qi "BSD 2\|Redistributions.*source.*must retain"; then echo "PKG_LICENSE='BSD'" >> "$meta_file" elif echo "$lic" | grep -qi "Mozilla Public License\|MPL"; then echo "PKG_LICENSE='MPL-2.0'" >> "$meta_file" elif echo "$lic" | grep -qi "ISC License"; then echo "PKG_LICENSE='ISC'" >> "$meta_file" elif echo "$lic" | grep -qi "Boost Software License"; then echo "PKG_LICENSE='BSL-1.0'" >> "$meta_file" elif echo "$lic" | grep -qi "The Unlicense\|Unlicense"; then echo "PKG_LICENSE='Unlicense'" >> "$meta_file" elif echo "$lic" | grep -qi "CC0\|Creative Commons"; then echo "PKG_LICENSE='CC0-1.0'" >> "$meta_file" else # Try to extract SPDX identifier local spdx spdx="$(grep -Eo 'SPDX-License-Identifier:\s*\S+' "$license_file" 2>/dev/null | grep -Eo '\S+$' || true)" if [ -n "$spdx" ]; then local escaped printf -v escaped "%q" "$spdx" echo "PKG_LICENSE=${escaped}" >> "$meta_file" fi fi break done # If still empty, try copyright header in source files if [ -z "$(grep '^PKG_LICENSE=' "$meta_file" | cut -d= -f2)" ] && [ -d "$proj_path/src" ]; then local src_lic src_lic="$(head -30 "$proj_path/src"/*.sh "$proj_path/src"/*.py "$proj_path/src"/*.c 2>/dev/null | grep -Eo 'SPDX-License-Identifier:\s*\S+' | grep -Eo '\S+$' | head -1 || true)" if [ -n "$src_lic" ]; then local escaped printf -v escaped "%q" "$src_lic" echo "PKG_LICENSE=${escaped}" >> "$meta_file" fi fi # --- MAINTAINER from AUTHORS / git config / whoami --- if [ -f "$proj_path/AUTHORS" ]; then local author author="$(head -1 "$proj_path/AUTHORS" 2>/dev/null | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" if [ -n "$author" ]; then local escaped printf -v escaped "%q" "$author" echo "PKG_MAINTAINER=${escaped}" >> "$meta_file" fi elif [ -d "$proj_path/.git" ]; then local author author="$(git -C "$proj_path" log -1 --format='%an <%ae>' 2>/dev/null || true)" if [ -n "$author" ]; then local escaped printf -v escaped "%q" "$author" echo "PKG_MAINTAINER=${escaped}" >> "$meta_file" fi fi if [ -z "$(grep '^PKG_MAINTAINER=' "$meta_file" | cut -d= -f2-)" ]; then local fallback fallback="$(whoami) <$(whoami)@$(hostname)>" local escaped printf -v escaped "%q" "$fallback" echo "PKG_MAINTAINER=${escaped}" >> "$meta_file" fi # --- CHANGELOG from git log or CHANGELOG.md --- if [ -f "$proj_path/CHANGELOG.md" ]; then local cl cl="$(sed -n '/^## /{p;:a;n;/^## /q;/./p;ba}' "$proj_path/CHANGELOG.md" 2>/dev/null | head -10)" if [ -n "$cl" ]; then local escaped printf -v escaped "%q" "$cl" echo "PKG_CHANGELOG=${escaped}" >> "$meta_file" fi elif [ -d "$proj_path/.git" ]; then local cl cl="$(git -C "$proj_path" log --oneline -10 2>/dev/null || true)" if [ -n "$cl" ]; then local escaped printf -v escaped "%q" "$cl" echo "PKG_CHANGELOG=${escaped}" >> "$meta_file" fi fi # --- BUILD SYSTEM detection --- local bsys="none" for f in CMakeLists.txt Cargo.toml package.json configure configure.ac configure.in \ meson.build setup.py pyproject.toml Makefile makefile GNUmakefile; do [ -f "$proj_path/$f" ] || continue case "$f" in CMakeLists.txt) bsys="cmake" ;; Cargo.toml) bsys="cargo" ;; package.json) bsys="npm" ;; configure|configure.ac|configure.in) bsys="autotools" ;; meson.build) bsys="meson" ;; setup.py|pyproject.toml) bsys="python" ;; Makefile|makefile|GNUmakefile) bsys="make" ;; esac break done echo "PKG_BUILD_SYSTEM='${bsys}'" >> "$meta_file" # --- BINARIES (scripts/executables) --- local bins="" for d in "$proj_path"/src "$proj_path"/bin "$proj_path"/scripts; do [ -d "$d" ] || continue local f for f in "$d"/*; do [ -f "$f" ] || continue local rel="${f#$proj_path/}" # Detect scripts by shebang if head -1 "$f" 2>/dev/null | grep -qE '^#!.*/(bash|sh|python|perl|ruby|lua)'; then bins="$bins $rel" fi done done if [ -n "$bins" ]; then local escaped printf -v escaped "%q" "${bins# }" echo "PKG_BINARIES=${escaped}" >> "$meta_file" fi # --- DEPENDS from packaging files (ebuild/spec/control) --- # Try cross-type dependency extraction first local pkg_depends="" local tmp dep norm tmp="$(_extract_ebuild_deps "$proj_path")" [ -n "$tmp" ] && pkg_depends="${pkg_depends:+$pkg_depends }$tmp" tmp="$(_extract_spec_deps "$proj_path")" [ -n "$tmp" ] && pkg_depends="${pkg_depends:+$pkg_depends }$tmp" tmp="$(_extract_control_deps "$proj_path")" [ -n "$tmp" ] && pkg_depends="${pkg_depends:+$pkg_depends }$tmp" if [ -n "$pkg_depends" ]; then # Normalize all deps to generic names local normalized="" for dep in $pkg_depends; do norm="$(_normalize_dep_name "$dep")" [ -n "$norm" ] && normalized="$normalized $norm" done pkg_depends="$(echo "$normalized" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" fi # --- DEPENDS fallback: scan scripts for command calls --- if [ -z "$pkg_depends" ] && grep -q 'PKG_BINARIES=' "$meta_file" 2>/dev/null; then local deps="" local pat for pat in 'git ' 'curl ' 'wget ' 'jq ' 'sed ' 'awk ' 'grep ' 'sort ' 'find ' 'tar ' 'gzip ' 'bzip2 ' 'xz ' 'unzip ' 'rsync ' 'docker ' 'make ' 'python ' 'perl ' 'ruby ' 'node ' 'npm ' 'cmake ' 'pkg-config ' 'cat ' 'mkdir ' 'cp ' 'mv ' 'rm ' 'chmod ' 'ln ' 'mount ' 'umount '; do if grep -rq "\b${pat% }" "$proj_path/src" "$proj_path/bin" "$proj_path/scripts" 2>/dev/null; then deps="$deps ${pat% }" fi done if [ -n "$deps" ]; then pkg_depends="$(echo "${deps# }" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" fi fi # Write discovered depends if [ -n "$pkg_depends" ]; then local escaped printf -v escaped "%q" "$pkg_depends" echo "PKG_DEPENDS=${escaped}" >> "$meta_file" fi echo "$meta_file" } # ---- Hooks ---- # Source and run a user-defined hook from project's hooks.conf # Hook functions receive all remaining arguments. _run_hook() { local hook_name="$1"; shift local hooks_file="$base_dir/hooks.conf" [ -f "$hooks_file" ] || return 0 . "$hooks_file" declare -F "$hook_name" > /dev/null 2>&1 || return 0 "$hook_name" "$@" } # ---- Type management ---- # Resolve type script path _type_script() { local type="$1" echo "$PACKWRAP_ROOT/types/$type/repo" } # Load a type script and validate required fields _type_load() { local type="$1" local script script="$(_type_script "$type")" [ -f "$script" ] || _die "Unknown type: $type (not found: $script)" # Reset type vars before sourcing TYPE="" TYPE_DESC="" . "$script" [ -n "$TYPE" ] || _die "Type script $script does not set TYPE" } # ---- Repository management ---- # Check if a repository name is already registered _repo_exists() { local type="$1" name="$2" [ -e "$PACKWRAP_REPOS_BASE/$type/$name" ] } # Get the default repository for a type _repo_default() { local type="$1" local default="$PACKWRAP_REPOS_BASE/$type/default" if [ -L "$default" ]; then readlink "$default" elif [ -d "$default" ]; then echo "default" else echo "" fi } # Mark a repository as default for its type _repo_set_default() { local type="$1" name="$2" local dir="$PACKWRAP_REPOS_BASE/$type" mkdir -p "$dir" ln -sf "$name" "$dir/default" } # ---- repo create ---- cmd_repo_create() { [ $# -ge 2 ] || _die "Usage: packwrap repo create type:<type> <name> [--path <path>]" local type="" name="" path="" # Parse type:name local arg="$1"; shift if [[ "$arg" == type:* ]]; then type="${arg#type:}" else _die "Expected type:<type>, got: $arg" fi name="$1"; shift [ -n "$name" ] || _die "Repository name is required" # Parse optional --path while [ $# -gt 0 ]; do case "$1" in --path) path="$2"; shift 2 ;; *) _die "Unknown option: $1" ;; esac done # Load type _type_load "$type" # Resolve repo path local repo_dir in_place=0 if [ -n "$path" ]; then repo_dir="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")" || repo_dir="$path" else repo_dir="$PACKWRAP_REPOS_BASE/$type/$name" in_place=1 fi # Check for conflicts _repo_exists "$type" "$name" && _die "Repository '$name' for type '$type' already exists" # Create the repository directory and run type init echo "Creating $type repository '$name' at $repo_dir ..." mkdir -p "$repo_dir" init "$repo_dir" echo " Repository structure created" # Register: symlink if --path given, otherwise dir is already in-place if [ "$in_place" -eq 0 ]; then local link_dir="$PACKWRAP_REPOS_BASE/$type" mkdir -p "$link_dir" ln -sf "$repo_dir" "$link_dir/$name" echo " Registered as $link_dir/$name" fi # Mark default if first repository for this type if [ -z "$(_repo_default "$type")" ]; then _repo_set_default "$type" "$name" echo " Set as default for type '$type'" fi echo "Done." } # ---- Project init ---- # Try to load a type's proj script (optional, for detect_versions) _try_load_proj_script() { local type="$1" local script="$PACKWRAP_ROOT/types/$type/proj" if [ -f "$script" ]; then . "$script" return 0 fi return 1 } cmd_init() { local name="" version="" types_arg="" arch="" local force=false declare -A cat_args while [ $# -gt 0 ]; do case "$1" in --version) version="$2"; shift 2 ;; --types) types_arg="$2"; shift 2 ;; --arch) arch="$2"; shift 2 ;; --force|-f) force=true; shift ;; --cat-*) local ct="${1#--cat-}" cat_args["$ct"]="$2" shift 2 ;; *) [ -z "$name" ] && { name="$1"; shift; continue; } _die "Unexpected argument: $1" ;; esac done [ -z "$name" ] && name="$(basename "$(pwd)")" [ -z "$name" ] && _die "Could not determine project name" local base_dir="$PACKWRAP_PROJECTS_BASE/$name" if [ -d "$base_dir" ]; then if $force; then rm -rf "$base_dir" else echo -n "Project '$name' already exists. Overwrite? [y/N] " >&2 IFS= read -r yn case "$yn" in y|Y|yes|YES) rm -rf "$base_dir" ;; *) _die "Aborted by user" ;; esac fi fi # ---- Discover project metadata ---- local proj_path="$(pwd)" local disc_meta_file disc_meta_file="$(_discover_metadata "$proj_path")" # ---- Resolve type list ---- local -a all_types=() for d in "$PACKWRAP_ROOT"/types/*/; do all_types+=("$(basename "$d")") done [ ${#all_types[@]} -eq 0 ] && _die "No types available" local -a types=() declare -A type_versions local -a types_with_ver=() local has_explicit_types=false if [ -n "$types_arg" ]; then IFS=',' read -ra types <<< "$types_arg" has_explicit_types=true fi # ---- Auto-detect versions ---- local -a all_versions=() local user_provided_version=false if [ -n "$version" ]; then user_provided_version=true fi if ! $user_provided_version; then local -a detect_targets=() if $has_explicit_types; then detect_targets=("${types[@]}") else detect_targets=("${all_types[@]}") fi for type in "${detect_targets[@]}"; do local tv tv=$(PROJ_PATH="$(pwd)" bash -c ". '$PACKWRAP_ROOT/types/$type/proj' 2>/dev/null; detect_versions" 2>/dev/null || true) if [ -n "$tv" ]; then type_versions[$type]="$tv" types_with_ver+=("$type") fi done local all_v="" for type in "${types_with_ver[@]}"; do all_v="$all_v"$'\n'"${type_versions[$type]}" done while IFS= read -r v; do [ -n "$v" ] && all_versions+=("$v") done < <(echo "$all_v" | sort -u) fi # Decide which types get directories, and prompt for version if needed if $has_explicit_types; then # User asked for specific types — create them all, populate will generate stubs true # types already set elif [ ${#types_with_ver[@]} -gt 0 ]; then types=("${types_with_ver[@]}") else types=("${all_types[@]}") fi [ ${#types[@]} -eq 0 ] && _die "No types selected" if ! $user_provided_version && [ ${#all_versions[@]} -eq 0 ]; then echo -n "Version (or leave empty to skip): " >&2 IFS= read -r version [ -z "$version" ] && _die "Version is required" user_provided_version=true fi local -a versions=() if $user_provided_version; then versions=("$version") else versions=("${all_versions[@]}") fi # ---- Create project structure ---- mkdir -p "$base_dir" for ver in "${versions[@]}"; do local ver_dir="$base_dir/$ver" echo "Creating $name/$ver ..." for type in "${types[@]}"; do local do_create=true # With auto-detected types, only create if version matches if ! $has_explicit_types && [ ${#types_with_ver[@]} -gt 0 ]; then if ! echo "${type_versions[$type]}" | grep -Fxq "$ver" 2>/dev/null; then do_create=false fi fi if $do_create; then local type_dir="$ver_dir/$type" mkdir -p "$type_dir" # Normalize version for this type local norm_ver norm_ver="$(_normalize_version_for_type "$ver" "$type")" # Run populate in a subshell PROJ_PATH="$(pwd)" \ TYPE_DIR="$type_dir" \ VERSION="$ver" \ NORM_VERSION="$norm_ver" \ PROJECT_NAME="$name" \ META_FILE="$disc_meta_file" \ bash -c ". '$PACKWRAP_ROOT/types/$type/proj' 2>/dev/null; populate" 2>/dev/null || true fi done ln -sf "$(pwd)" "$ver_dir/src" done # Set current symlink to last version ln -sfn "${versions[-1]}" "$base_dir/current" # ---- Collect categories and write META ---- local default_cat meta_file="$base_dir/META" { echo "# Project metadata -- sourced by packwrap" echo "# Auto-generated by packwrap init. Edit as needed." echo "" } > "$meta_file" for type in "${types[@]}"; do local cat_val if [ -n "${cat_args[$type]:-}" ]; then cat_val="${cat_args[$type]}" else case "$type" in ebuild) default_cat="app-misc" ;; rpm) default_cat="Development/Tools" ;; deb) default_cat="misc" ;; *) default_cat="misc" ;; esac echo -n " Category for $type (default: $default_cat): " >&2 IFS= read -r user_input if [ -z "$user_input" ]; then cat_val="$default_cat" echo " *** WARNING: Using default category '$default_cat' for $type." >&2 echo " *** Set the correct category in $meta_file before building." >&2 else cat_val="$user_input" fi fi local var="CATEGORY_$(echo "$type" | tr '[:lower:]' '[:upper:]')" echo "${var}=\"${cat_val}\"" >> "$meta_file" done # Write architecture [ -z "$arch" ] && arch="$(uname -m)" arch="$(_arch_normalize "$arch")" echo "ARCH=\"${arch}\"" >> "$meta_file" # Clean up discovery metadata rm -f "$disc_meta_file" _run_hook hook_post_init "$name" "$version" "${types[@]}" echo "Done." } # ---- repo install (copy build artifacts into local repo) ---- cmd_repo_install() { local name="" while [ $# -gt 0 ]; do case "$1" in *) [ -z "$name" ] && { name="$1"; shift; continue; } _die "Unexpected argument: $1" ;; esac done [ -z "$name" ] && name="$(basename "$(pwd)")" [ -z "$name" ] && _die "Could not determine project name" local base_dir="$PACKWRAP_PROJECTS_BASE/$name" [ -d "$base_dir" ] || _die "Project '$name' not found at $base_dir" local meta_file="$base_dir/META" [ -f "$meta_file" ] || _die "No META file in $base_dir" . "$meta_file" # Get current version local version ver_dir version="$(readlink "$base_dir/current")" || _die "Broken current symlink" if [[ "$version" == /* ]]; then ver_dir="$version" else ver_dir="$base_dir/$version" fi [ -d "$ver_dir" ] || _die "Version directory $ver_dir not found" for type_dir in "$ver_dir"/*/; do [ -d "$type_dir" ] || continue local type type="$(basename "$type_dir")" [ "$type" = "src" ] && continue # Resolve repo local default_name default_name="$(_repo_default "$type")" if [ -z "$default_name" ]; then echo " *** No default repository for type '$type', skipping" >&2 continue fi local repo_path="$PACKWRAP_REPOS_BASE/$type/$default_name" if [ -L "$repo_path" ]; then repo_path="$(readlink -f "$repo_path")" fi [ -d "$repo_path" ] || { echo " *** Repository path $repo_path not found" >&2; continue; } # Get category for this type local cat_var="CATEGORY_$(echo "$type" | tr '[:lower:]' '[:upper:]')" local category="${!cat_var:-misc}" # Convert arch for this type local type_arch case "$type" in ebuild) type_arch="$(_arch_to_ebuild "$ARCH")" ;; rpm) type_arch="$(_arch_to_rpm "$ARCH")" ;; deb) type_arch="$(_arch_to_deb "$ARCH")" ;; *) type_arch="$ARCH" ;; esac local build_dir="$PACKWRAP_BUILD_BASE/$name/$version/$type" echo "Installing $type artifacts for $name/$version ($type_arch) ..." if ( . "$PACKWRAP_ROOT/types/$type/repo" install "$repo_path" "$category" "$name" "$version" "$type_dir" "$ver_dir/src" "$type_arch" "$build_dir" ); then _run_hook hook_post_install "$type" "$name" "$version" "$repo_path" "$category" "$type_arch" fi done echo "Done." } # ---- system install ---- # Install the project on the host system (dpkg -i / rpm -ivh / emerge). cmd_install() { local name="" version="" overlay="" while [ $# -gt 0 ]; do case "$1" in --version) version="$2"; shift 2 ;; --overlay) overlay="$2"; shift 2 ;; *) [ -z "$name" ] && { name="$1"; shift; continue; } _die "Unexpected argument: $1" ;; esac done [ -z "$name" ] && name="$(basename "$(pwd)")" [ -z "$name" ] && _die "Could not determine project name" # Parse optional category and version from name: # cat/pkg-1.0 → category=cat, name=pkg, version=1.0 # pkg-1.0 → name=pkg, version=1.0 # cat/pkg → category=cat, name=pkg local category="" if [[ "$name" =~ ^(.+)/(.+)$ ]]; then category="${BASH_REMATCH[1]}" name="${BASH_REMATCH[2]}" fi if [ -z "$version" ] && [[ "$name" =~ ^(.+)-([0-9].*)$ ]]; then name="${BASH_REMATCH[1]}" version="${BASH_REMATCH[2]}" fi local base_dir="$PACKWRAP_PROJECTS_BASE/$name" [ -d "$base_dir" ] || _die "Project '$name' not found at $base_dir" local meta_file="$base_dir/META" [ -f "$meta_file" ] || _die "No META file in $base_dir" # Resolve Gentoo overlay: CLI > env > META > config > default local pre_meta_overlay="${GENTOO_OVERLAY:-}" local env_overlay env_overlay=$(env | sed -n 's/^GENTOO_OVERLAY=//p' | head -1) . "$meta_file" local gentoo_overlay gentoo_overlay="${overlay:-${env_overlay:-${GENTOO_OVERLAY:-${pre_meta_overlay:-/var/db/repos/local}}}}" # Verify optional category matches META if [ -n "$category" ]; then local match=false for var in CATEGORY_EBUILD CATEGORY_RPM CATEGORY_DEB; do [ "${!var:-}" = "$category" ] && { match=true; break; } done $match || _die "Category '$category' does not match any CATEGORY_* in META" fi # Resolve version if [ -z "$version" ]; then version="$(readlink "$base_dir/current")" || _die "Broken current symlink" fi local ver_dir if [[ "$version" == /* ]]; then ver_dir="$version" else ver_dir="$base_dir/$version" fi [ -d "$ver_dir" ] || _die "Version directory $ver_dir not found" local type_any_ok=false for type_dir in "$ver_dir"/*/; do [ -d "$type_dir" ] || continue local type type="$(basename "$type_dir")" [ "$type" = "src" ] && continue # Get category for this type local cat_var="CATEGORY_$(echo "$type" | tr '[:lower:]' '[:upper:]')" local type_category="${!cat_var:-misc}" # Convert arch for this type local type_arch case "$type" in ebuild) type_arch="$(_arch_to_ebuild "$ARCH")" ;; rpm) type_arch="$(_arch_to_rpm "$ARCH")" ;; deb) type_arch="$(_arch_to_deb "$ARCH")" ;; *) type_arch="$ARCH" ;; esac local build_dir="$PACKWRAP_BUILD_BASE/$name/$version/$type" [ -d "$build_dir" ] || { echo " *** No build artifacts for $type at $build_dir" >&2; continue; } echo "Installing $name/$version ($type, $type_arch) on host system ..." case "$type" in deb) local deb_files=("$build_dir"/*.deb) if [ ${#deb_files[@]} -eq 0 ] || [ ! -f "${deb_files[0]}" ]; then echo " *** No .deb files in $build_dir" >&2 continue fi dpkg -i "${deb_files[@]}" || { echo " *** dpkg failed. Try: apt install -f" >&2 continue } echo " deb: installed" type_any_ok=true ;; rpm) local rpm_files=("$build_dir"/*.rpm) if [ ${#rpm_files[@]} -eq 0 ] || [ ! -f "${rpm_files[0]}" ]; then echo " *** No .rpm files in $build_dir" >&2 continue fi rpm -ivh "${rpm_files[@]}" || { echo " *** rpm install failed" >&2 continue } echo " rpm: installed" type_any_ok=true ;; ebuild) # Find ebuild file (type_dir first, fall back to build_dir) local ebuild_file="" for f in "$type_dir"/*.ebuild; do [ -f "$f" ] && { ebuild_file="$f"; break; } done if [ -z "$ebuild_file" ]; then for f in "$build_dir"/*.ebuild; do [ -f "$f" ] && { ebuild_file="$f"; break; } done fi if [ -z "$ebuild_file" ]; then echo " *** No ebuild file found for $name-$version" >&2 continue fi local pkg_dir="$gentoo_overlay/$type_category/$name" mkdir -p "$pkg_dir" cp "$ebuild_file" "$pkg_dir/" echo " ebuild: copied to $pkg_dir/" # Copy distfiles to overlay (portage picks them up) mkdir -p "$gentoo_overlay/distfiles" local f for f in "$build_dir"/*.tar.* "$build_dir"/*.tgz \ "$build_dir"/*.tbz2 "$build_dir"/*.zip; do [ -f "$f" ] || continue cp "$f" "$gentoo_overlay/distfiles/" echo " distfile: copied $(basename "$f")" done ebuild "$pkg_dir/"*.ebuild manifest || { echo " *** ebuild manifest failed" >&2 continue } # Expose source dir for ebuilds that reference LOCAL_SRC_DIR local _pw_src _pw_src="$(readlink -f "$ver_dir/src" 2>/dev/null || echo "$ver_dir/src")" if [[ "$version" =~ _(alpha|beta|pre|rc|p) ]]; then LOCAL_SRC_DIR="$_pw_src" ACCEPT_KEYWORDS="**" emerge -1 "$name" || { echo " *** emerge failed" >&2 continue } else LOCAL_SRC_DIR="$_pw_src" emerge -1 "$name" || { echo " *** emerge failed" >&2 continue } fi echo " ebuild: installed" type_any_ok=true ;; *) echo " *** Unknown type '$type', skipping" >&2 ;; esac _run_hook hook_post_install "$type" "$name" "$version" "$gentoo_overlay" "$type_category" "$type_arch" done $type_any_ok || _die "No types were installed" echo "Done." } # ---- build ---- cmd_build() { local name="" stage="all" while [ $# -gt 0 ]; do case "$1" in --stage) stage="$2"; shift 2 ;; *) [ -z "$name" ] && { name="$1"; shift; continue; } _die "Unexpected argument: $1" ;; esac done [ -z "$name" ] && name="$(basename "$(pwd)")" [ -z "$name" ] && _die "Could not determine project name" local base_dir="$PACKWRAP_PROJECTS_BASE/$name" [ -d "$base_dir" ] || _die "Project '$name' not found at $base_dir" local meta_file="$base_dir/META" [ -f "$meta_file" ] || _die "No META file in $base_dir" . "$meta_file" local version ver_dir version="$(readlink "$base_dir/current")" || _die "Broken current symlink" if [[ "$version" == /* ]]; then ver_dir="$version" else ver_dir="$base_dir/$version" fi [ -d "$ver_dir" ] || _die "Version directory $ver_dir not found" local do_build=false do_install=false case "$stage" in build) do_build=true ;; install) do_install=true ;; all) do_build=true; do_install=true ;; *) _die "Unknown stage: $stage (use build|install|all)" ;; esac local src_dir="$ver_dir/src" [ -d "$src_dir" ] || _die "Source directory $src_dir not found" for type_dir in "$ver_dir"/*/; do [ -d "$type_dir" ] || continue local type type="$(basename "$type_dir")" [ "$type" = "src" ] && continue local build_script="$PACKWRAP_ROOT/types/$type/build" [ -f "$build_script" ] || { echo " *** No build script for type '$type'" >&2; continue; } local type_arch case "$type" in ebuild) type_arch="$(_arch_to_ebuild "$ARCH")" ;; rpm) type_arch="$(_arch_to_rpm "$ARCH")" ;; deb) type_arch="$(_arch_to_deb "$ARCH")" ;; *) type_arch="$ARCH" ;; esac if $do_build; then local output_dir="$PACKWRAP_BUILD_BASE/$name/$version/$type" local work_dir="$PACKWRAP_CACHE_BASE/$type/$name/$version" mkdir -p "$output_dir" "$work_dir" echo "Building $type for $name/$version ($type_arch) ..." if ! ( . "$build_script" build "$name" "$version" "$src_dir" "$type_dir" "$output_dir" "$type_arch" "$work_dir" ); then echo " *** Build for $type failed" >&2 else _run_hook hook_post_build "$type" "$name" "$version" "$output_dir" "$type_arch" fi fi if $do_install; then cmd_repo_install "$name" fi done echo "Done." } # ---- add version ---- # Create a new version by copying packaging files from an existing version cmd_add_version() { local name="" new_version="" from_ver="" src_path="" while [ $# -gt 0 ]; do case "$1" in --version) new_version="$2"; shift 2 ;; --from) from_ver="$2"; shift 2 ;; --src) src_path="$2"; shift 2 ;; *) [ -z "$name" ] && { name="$1"; shift; continue; } _die "Unexpected argument: $1" ;; esac done [ -z "$name" ] && name="$(basename "$(pwd)")" [ -z "$name" ] && _die "Could not determine project name" [ -z "$new_version" ] && _die "Version argument is required (use --version <ver>)" local base_dir="$PACKWRAP_PROJECTS_BASE/$name" [ -d "$base_dir" ] || _die "Project '$name' not found at $base_dir" [ -z "$from_ver" ] && from_ver="$(readlink "$base_dir/current")" || true [ -z "$from_ver" ] && _die "Could not determine source version (--from or current symlink)" local from_dir="$base_dir/$from_ver" [ -d "$from_dir" ] || _die "Source version '$from_ver' not found at $from_dir" local new_dir="$base_dir/$new_version" [ -d "$new_dir" ] && _die "Version '$new_version' already exists" echo "Adding version $new_version (copied from $from_ver) ..." mkdir -p "$new_dir" for item in "$from_dir"/*/; do [ -d "$item" ] || continue local type type="$(basename "$item")" [ "$type" = "src" ] && continue cp -r "$item" "$new_dir/$type" case "$type" in ebuild) local f for f in "$new_dir/$type"/*.ebuild; do [ -f "$f" ] || continue local fname fname="$(basename "$f")" if [[ "$fname" == "${name}-${from_ver}.ebuild" ]]; then local new_fname="${name}-${new_version}.ebuild" mv "$f" "$(dirname "$f")/$new_fname" echo " ebuild: $fname -> $new_fname" fi done ;; rpm) local spec_files=("$new_dir/$type"/*.spec) if [ ${#spec_files[@]} -gt 0 ] && [ -f "${spec_files[0]}" ]; then local spec="${spec_files[0]}" local norm_ver release norm_ver="$(_normalize_version_for_type "$new_version" rpm)" release="$(_normalize_rpm_release "$new_version")" sed -i "s/^Version:\(\s*\).*/Version:\1${norm_ver}/" "$spec" sed -i "s/^Release:\(\s*\).*/Release:\1${release}/" "$spec" echo " rpm: updated Version/Release in $(basename "$spec")" fi ;; deb) local changelog="$new_dir/$type/debian/changelog" if [ -f "$changelog" ]; then local norm_ver norm_ver="$(_normalize_version_for_type "$new_version" deb)" local maintainer maintainer="$(grep -E '^ -- ' "$changelog" | head -1 | sed 's/^ -- //; s/ [A-Z][a-z].*$//' || true)" [ -z "$maintainer" ] && maintainer="${USER:-root} <${USER:-root}@$(hostname)>" local date_rv date_rv="$(date -R 2>/dev/null || date -u +'%a, %d %b %Y %H:%M:%S +0000')" local entry entry=$(printf '%s (%s-1) unstable; urgency=medium\n\n * New version %s\n\n -- %s %s\n\n' \ "$name" "$norm_ver" "$new_version" "$maintainer" "$date_rv") { printf '%s' "$entry" cat "$changelog" } > "${changelog}.new" && mv "${changelog}.new" "$changelog" echo " deb: added changelog entry for ${norm_ver}-1" fi ;; esac done local actual_src actual_src="${src_path:-$(readlink -f "$from_dir/src" 2>/dev/null || echo "$(pwd)")}" ln -sfn "$actual_src" "$new_dir/src" ln -sfn "$new_version" "$base_dir/current" _run_hook hook_post_init "$name" "$new_version" "$from_ver" echo "Done. Version $new_version is now current." } # ---- switch version ---- # Switch the current symlink to an existing version cmd_switch() { local name="" version="" while [ $# -gt 0 ]; do case "$1" in *) [ -z "$version" ] && { version="$1"; shift; continue; } [ -z "$name" ] && { name="$1"; shift; continue; } _die "Unexpected argument: $1" ;; esac done [ -z "$version" ] && _die "Usage: packwrap switch <version> [<name>]" [ -z "$name" ] && name="$(basename "$(pwd)")" [ -z "$name" ] && _die "Could not determine project name" local base_dir="$PACKWRAP_PROJECTS_BASE/$name" [ -d "$base_dir" ] || _die "Project '$name' not found at $base_dir" local ver_dir="$base_dir/$version" [ -d "$ver_dir" ] || _die "Version '$version' not found in project '$name'" local old_ver old_ver="$(readlink "$base_dir/current")" || old_ver="" [ "$old_ver" = "$version" ] && { echo "Already on version $version"; return 0; } ln -sfn "$version" "$base_dir/current" echo "Switched from ${old_ver:-<none>} to $version" }