#!/bin/sh -e # # This is a simple package manager written in POSIX 'sh' for # KISS Linux utlizing the core unix utilites where needed. # # The script runs with 'set -e' enabled. It will exit on any # non-zero return code. This ensures that no function continues # if it fails at any point. # # Keep in mind that this involves extra code in the case where # an error is optional or required. # # Where possible the package manager should "error first". # Check things first, die is necessary and continue if all is well. # # The code below conforms to shellcheck's rules. However, some # lint errors *are* disabled as they relate to unexpected # behavior (which we do expect). # # KISS is available under the MIT license. # # - Dylan Araps. die() { # Print a message and exit with '1' (error). printf '\033[31m!>\033[m %s\n' "$@" >&2 exit 1 } log() { # Print a message with a colorful arrow to distinguish # from other output. printf '\033[32m=>\033[m %s\n' "$@" } pkg_lint() { # Check that each mandatory file in the package entry exists. log "[$1]: Checking repository files..." pkg_location=$(pkg_search "$1") cd "$pkg_location" || die "'$pkg_location' not accessible" [ -f sources ] || die "Sources file not found." [ -x build ] || die "Build file not found or not executable." [ -s licenses ] || die "License file not found or empty." [ -s version ] || die "Version file not found or empty." # Ensure that the release field in the version file is set # to something. read -r _ rel < version [ "$rel" ] || die "Release field not found in version file." # Unset this variable so it isn't used again on a failed # source. There's no 'local' keyword in POSIX sh. rel= } pkg_search() { # Figure out which repository a package belongs to by # searching for directories matching the package name # in $KISS_PATH/*. [ "$KISS_PATH" ] || \ die "\$KISS_PATH needs to be set." \ "Example: KISS_PATH=/packages/core:/packages/extra:/packages/xorg" \ "Repositories will be searched in the configured order." \ "The variable should work just like \$PATH." # Disable globbing with 'set -f' to ensure that the unquoted # variable doesn't expand into anything nasty. # shellcheck disable=2086,2046 { set -f set -- "$1" $(IFS=:; find $KISS_PATH -maxdepth 1 -name "$1") set +f } # A package may also not be found due to a repository not being # readable by the current user. Either way, we need to die here. [ -z "$2" ] && die "Package '$1' not in any repository." printf '%s\n' "$2" } pkg_list() { # List installed packages. As the format is files and # diectories, this just involves a simple for loop and # file read. # Changing directories is similar to storing the full # full path in a variable, only there is no variable as # you can access children relatively. cd "$KISS_ROOT/var/db/kiss" || \ die "KISS database doesn't exist or is inaccessible." # Optional arguments can be passed to check for specific # packages. If no arguments are passed, list all. As we # loop over '$@', if there aren't any arguments we can # just set the directory contents to the argument list. [ "$1" ] || set -- * # Loop over each version file and warn if one doesn't exist. # Also warn if a package is missing its version file. for pkg; do [ -d "$pkg" ] || { log "Package '$pkg' is not installed." return 1 } [ -f "$pkg/version" ] || { log "Warning: Package '$pkg' has no version file." return } read -r version release < "$pkg/version" && printf '%s\n' "${pkg%/*} $version-$release" done } pkg_sources() { # Download any remote package sources. log "[$1]: Downloading sources..." # Store each downloaded source in named after the package it # belongs to. This avoid conflicts between two packages having a # source of the same name. mkdir -p "$src_dir/$1" && cd "$src_dir/$1" # Find the package's repository files. This needs to keep # happening as we can't store this data in any kind of data # structure. repo_dir=$(pkg_search "$1") while read -r src _; do case $src in # Git repository. git:*) git clone "${src##git:}" "$mak_dir" ;; # Remote source. *://*) [ -f "${src##*/}" ] && { log "[$1]: Found cached source '${src##*/}'." continue } wget "$src" || { rm -f "${src##*/}" die "[$1]: Failed to download $src." } ;; # Local files (Any source that is non-remote is assumed to be local). *) [ -f "$repo_dir/$src" ] || die "[$1]: No local file '$src'." log "[$1]: Found local file '$src'." ;; esac done < "$repo_dir/sources" } pkg_extract() { # Extract all source archives to the build diectory and copy over # any local repository files. log "[$1]: Extracting sources..." # Store each downloaded source in named after the package it # belongs to. This avoid conflicts between two packages having a # source of the same name. mkdir -p "$mak_dir/$1" && cd "$mak_dir/$1" # Find the package's repository files. This needs to keep # happening as we can't store this data in any kind of data # structure. repo_dir=$(pkg_search "$1") while read -r src dest; do mkdir -p "./$dest" case $src in # Do nothing as git repository was downloaded to the build # diectory directly. git:*) ;; # Only 'tar' archives are currently supported for extaction. # Any other filetypes are simply copied to '$mak_dir' which # allows you to extract them manually. *://*.tar*|*://*.tgz) tar xf "$src_dir/$1/${src##*/}" -C "./$dest" \ --strip-components 1 \ || die "[$1]: Couldn't extract ${src##*/}." ;; # Local files (Any source that is non-remote is assumed to be local). *) [ -f "$repo_dir/$src" ] || die "[$1]: Local file $src not found." cp -f "$repo_dir/$src" "./$dest" ;; esac done < "$repo_dir/sources" } pkg_depends() { # Resolve all dependencies and install them in the right order. # Find the package's repository files. This needs to keep # happening as we can't store this data in any kind of data # structure. repo_dir=$(pkg_search "$1") # This does a depth-first search. The deepest dependencies are # listed first and then the parents in reverse order. if pkg_list "$1" >/dev/null; then # If a package is already installed but 'pkg_depends' was # given an argument, add it to the list anyway. [ "$2" ] && missing_deps="$missing_deps $1 " else case $missing_deps in # Dependency is already in list, skip it. *" $1 "*) ;; *) [ -f "$repo_dir/depends" ] && while read -r dep _; do pkg_depends "$dep" ||: done < "$repo_dir/depends" missing_deps="$missing_deps $1 " ;; esac fi } pkg_build() { # Build packages and turn them into packaged tarballs. This function # also checks checksums, downloads sources and ensure all dependencies # are installed. # Resolve dependencies and generate a list. # Send 'force' to 'pkg_depends' to always include the explicitly # requested packages. log "Resolving dependencies..." for pkg; do pkg_depends "$pkg" force; done # Disable globbing with 'set -f' to ensure that the unquoted # variable doesn't expand into anything nasty. # shellcheck disable=2086,2046 { # Set the resolved dependency list as the function's arguments. set -f set -- $missing_deps set +f } log "Installing: $*." for pkg; do pkg_lint "$pkg"; done for pkg; do # Find the package's repository files. This needs to keep # happening as we can't store this data in any kind of data # structure. repo_dir=$(pkg_search "$pkg") # Ensure that checksums exist prior to building the package. [ -f "$repo_dir/checksums" ] || { log "[$pkg]: Checksums are missing." # Instead of dying above, log it to the terminal. Also define a # variable so we *can* die after all checksum files have been # checked. no_checkums="$no_checkums$pkg " } done # Die here as packages without checksums were found above. [ "$no_checkums" ] && die "Run '$kiss checksum ${no_checkums% }' to generate checksums." for pkg; do pkg_sources "$pkg"; done for pkg; do # Find the package's repository files. This needs to keep # happening as we can't store this data in any kind of data # structure. repo_dir=$(pkg_search "$pkg") # Generate a second set of checksums to compare against the # repositorie's checksums for the package. pkg_checksums .checksums "$pkg" # Compare the checksums using 'cmp'. cmp -s "$repo_dir/.checksums" "$repo_dir/checksums" || { log "[$pkg]: Checksum mismatch." # Instead of dying above, log it to the terminal. Also define a # variable so we *can* die after all checksum files have been # checked. mismatch="$mismatch$pkg " } # The second set of checksums use a temporary file, we need to # delete it. rm -f "$repo_dir/.checksums" done # Die here as packages with differing checksums were found above. [ "$mismatch" ] && die "Checksum mismatch with: ${mismatch% }" log "Verified all checksums." for pkg; do pkg_extract "$pkg"; done log "Extracted all sources." log "Building packages..." for pkg; do # Find the package's repository files. This needs to keep # happening as we can't store this data in any kind of data # structure. repo_dir=$(pkg_search "$pkg") # Install built packages to a directory under the package name # to avod collisions with other packages. mkdir -p "$pkg_dir/$pkg/var/db/$kiss" # Move to the build directory and call the build script. (cd "$mak_dir/$pkg"; "$repo_dir/build" "$pkg_dir/$pkg") || die "[$pkg]: Build failed." # Copy the repository files to the package directory. # This acts as the database entry. cp -Rf "$repo_dir" "$pkg_dir/$pkg/var/db/$kiss" log "[$pkg]: Sucessfully built package." # Create the manifest file early and make it empty. # This ensure that the manifest is added to the manifest... : > "$pkg_dir/$pkg/var/db/$kiss/$pkg/manifest" done log "Stripping packages..." for pkg; do # Find the package's repository files. This needs to keep # happening as we can't store this data in any kind of data # structure. repo_dir=$(pkg_search "$pkg") # Package has stripping disabled, stop here. [ -f "$repo_dir/nostrip" ] && continue log "[$pkg]: Stripping binaries and libraries..." find "$pkg_dir/$pkg" -type f | while read -r binary; do case $(file -bi "$binary") in application/x-sharedlib*|application/x-pie-executable*) strip_opts=--strip-unneeded ;; application/x-archive*) strip_opts=--strip-debug ;; application/x-executable*) strip_opts=--strip-all ;; *) continue ;; esac strip "$strip_opts" "$binary" 2>/dev/null done done log "Stripped all binaries and libraries." } pkg_checksums() { # Generate checksums for packages. # This also downloads any remote sources. checksum_file=$1 shift for pkg; do # Find the package's repository files. This needs to keep # happening as we can't store this data in any kind of data # structure. repo_dir=$(pkg_search "$pkg") while read -r src _; do case $src in # Git repository. # Skip checksums on git repositories. git:*) ;; *) # File is local to the package and is stored in the # repository. [ -f "$repo_dir/$src" ] && src_path=$repo_dir/${src%/*} # File is remote and was downloaded. [ -f "$src_dir/$pkg/${src##*/}" ] && src_path=$src_dir/$pkg # Die here if source for some reason, doesn't exist. [ "$src_path" ] || die "[$pkg]: Couldn't find source '$src'." # An easy way to get 'sha256sum' to print with the basenames # of files is to 'cd' to the file's directory beforehand. (cd "$src_path" && sha256sum "${src##*/}") || die "[$pkg]: Failed to generate checksums." # Unset this variable so it isn't used again on a failed # source. There's no 'local' keyword in POSIX sh. src_path= ;; esac done < "$repo_dir/sources" > "$repo_dir/$checksum_file" log "[$pkg]: Generated/Verified checksums." done } setup_caching() { # Setup the host machine for the package manager. Create any # directories which need to exist and set variables for easy # access to them. # Main cache directory (~/.cache/kiss/) typically. mkdir -p "${cac_dir:=${XDG_CACHE_HOME:=$HOME/.cache}/$kiss}" || die "Couldn't create cache directory ($cac_dir)." # Build directory. mkdir -p "${mak_dir:=$cac_dir/build-$pid}" || die "Couldn't create build directory ($mak_dir)." # Package directory. mkdir -p "${pkg_dir:=$cac_dir/pkg-$pid}" || die "Couldn't create package directory ($pkg_dir)." # Tar directory. mkdir -p "${tar_dir:=$cac_dir/extract-$pid}" || die "Couldn't create tar directory ($tar_dir)." # Source directory. mkdir -p "${src_dir:=$cac_dir/sources}" || die "Couldn't create source directory ($src_dir)." # Binary directory. mkdir -p "${bin_dir:=$cac_dir/bin}" || die "Couldn't create binary directory ($bin_dir)." } pkg_clean() { # Clean up on exit or error. This removes everything related # to the build. # Remove temporary directories. rm -rf -- "$mak_dir" "$pkg_dir" "$tar_dir" } root_check() { # Ensure that the user has write permissions to '$KISS_ROOT'. # When this variable is empty, a value of '/' is assumed. [ -w "$KISS_ROOT/" ] || \ die "No write permissions to '${KISS_ROOT:-/}'." \ "You may need to run '$kiss' as root." } args() { # Parse script arguments manually. POSIX 'sh' has no 'getopts' # or equivalent built in. This is rather easy to do in our case # since the first argument is always an "action" and the arguments # that follow are all package names. # Actions can be abbreviated to their first letter. This saves # keystrokes once you memorize themand it also has the side-effect # of "correcting" spelling mistakes assuming the first letter is # right. case $1 in # Build the list of packages. b*) shift [ "$1" ] || die "'kiss build' requires an argument." pkg_build "$@" ;; # Generate checksums for packages. c*) shift [ "$1" ] || die "'kiss checksum' requires an argument." for pkg; do pkg_lint "$pkg"; done for pkg; do pkg_sources "$pkg"; done pkg_checksums checksums "$@" ;; # Install packages. i*) shift [ "$1" ] || die "'kiss install' requires an argument." root_check ;; # Remove packages. r*) shift [ "$1" ] || die "'kiss remove' requires an argument." root_check ;; # List installed packages. l*) shift pkg_list "$@" ;; # Print version and exit. v*) log "$kiss 0.1.10" ;; # Catch all invalid arguments as well as # any help related flags (-h, --help, help). *) log "$kiss [b|c|i|l|r|u] [pkg]" \ "build: Build a package." \ "checksum: Generate checksums." \ "install: Install a package (Runs build if needed)." \ "list: List packages." \ "remove: Remove a package." \ "update: Check for updates." ;; esac } main() { # Store the script name in a variable and use it everywhere # in place of 'kiss'. This allows the script name to be changed # easily. kiss=${0##*/} # The PID of the current shell process is used to isolate directories # to each specific KISS instance. This allows multiple package manager # instances to be run at once. Store the value in another variable so # that it doesn't change beneath us. pid=$$ # Catch errors and ensure that build files and directories are cleaned # up before we die. This occurs on 'Ctrl+C' as well as sucess and error. trap pkg_clean EXIT INT setup_caching args "$@" } main "$@"