From 81b330c30e1abd5f1e25f378ef7c4bdb35566221 Mon Sep 17 00:00:00 2001 From: yafox Date: Wed, 25 Nov 2020 06:29:21 +0000 Subject: [PATCH] initial commit. --- .gitignore | 4 + LICENSE | 20 +++ README | 206 ++++++++++++++++++++++++ lib/built.sh | 9 ++ lib/clearopaques.sh | 14 ++ lib/dependencies.sh | 33 ++++ lib/guessver.sh | 21 +++ lib/log.sh | 3 + lib/lowerlayers.sh | 36 +++++ lib/mounts.sh | 13 ++ lib/pkg.sh | 100 ++++++++++++ lib/reverse.sh | 3 + lib/versionformat.sh | 7 + lib/versionpasses.sh | 34 ++++ lix.sh | 363 +++++++++++++++++++++++++++++++++++++++++++ makefile | 26 ++++ sloc.sh | 12 ++ 17 files changed, 904 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README create mode 100644 lib/built.sh create mode 100644 lib/clearopaques.sh create mode 100644 lib/dependencies.sh create mode 100644 lib/guessver.sh create mode 100644 lib/log.sh create mode 100644 lib/lowerlayers.sh create mode 100644 lib/mounts.sh create mode 100644 lib/pkg.sh create mode 100644 lib/reverse.sh create mode 100644 lib/versionformat.sh create mode 100755 lib/versionpasses.sh create mode 100755 lix.sh create mode 100644 makefile create mode 100755 sloc.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74b4813 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +lower +system +built-with +build-conf diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd8c239 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright 2020 "yafox" + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README b/README new file mode 100644 index 0000000..fed6a9a --- /dev/null +++ b/README @@ -0,0 +1,206 @@ +lix +=== + +lix is a source-based package manager written entirely in POSIX-compliant shell +script. it was developed to serve as the core component of lix os. + +it uses chroots and package-and-version-specific overlayfs layers to avoid +package conflicts during the build process. aside from anything in the "system" +bootstrap directory, the only tools and libraries available to a package are +those it explicitly lists as a dependency. all package files are kept in +separate directories and soft-linked in to the system root. this makes it easy +to see what packages have supplied each file in one's system root. + +lix is composed of the following small shell script utilities, all of which can +be used independently of lix and each other (with the exception of `how`, which +uses `vercmp` to allow defining instructions for ranges of package versions and +`shsort` to quicksort them): + +1) `lmr` can merge one directory into another using soft or hard links. +2) `lyr` provides directories and commands for managing overlayfs overlays. +3) `src` pulls and verifies source code and provides default version numbers. +4) `how` provides scripts for patching, configuring, building, and installing. +5) `chin` sets up common system paths and chroots in to a target directory. +6) `vercmp` allows comparison of version strings in package-specific formats. +7) `shsort` is a POSIX shell quicksort implementation which takes a user + supplied comparison command. + +because of its small size and modularity, lix and its utilities can provide a +foundation for building a simple source-based distribution of one's own. the +lix os project serves as an example of how one might do this. + +## manual intervention + +lix favors an interactive approach to problem solving. patching, configuring, +building, and installing are all separate commands. the results of commands +executed on a package up to any given point may be examined via: + +- mounting the package's chroot contents using `lix mount ` +- obtaining a shell into the package's chroot using `lix do sh ` +- examining the package's layers at `lyr upperdir lix//` + +## dependency handling + +lix does not solve dependency graphs. dependency graph resolution is a +non-trivial problem, and chrooted builds make solving it largely unnecessary +anyway. each package can have whatever dependencies it needs without causing +conflicts with other packages. if conflicts between packages arise, they can be +quickly resolved by swapping around softlinks using `lix up` and `lix down`. + +`lix` records the versions of each dependency used when building a package in +the `built-with` directory in LIXROOT after each successful build. this makes +it possible to determine which packages need to be rebuilt when changes are made +to a dependency. + +lix also does not recursively build or install dependencies. only code the user +has explicitly asked for should ever run on a user's machine. however, assuming +package dependency lists are exhaustive and kept sorted by their height in the +(acyclic) dependency graph in a similar way to the linux kernel's module +dependencies list, implementing this behavior should require little more than a +loop and some list deduplication. + +## dynamic dependencies + +any line in a package's list of dependencies that matches the regex +"^\$[A-Za-z0-9]+$" will get evaluated before being parsed. this allows some +dependencies to be provided via environment variables. this functionality is +provided in order to support the user's ability to choose a text editor during +the configuration of certain packages. by convention, lix packages use the +"EDITORDEP" variable to reference the package providing the command named in the +"EDITOR" variable. because of its general nature, this technique can also be +extended to provide build flexibility as needed. + +for example, the line: + + $EDITORDEP + +in combination with the command: + + export EDITORDEP="elvis > 0" + +results in the line: + + elvis > 0 + +which selects the most recent version of the elvis package, assuming its version +number is greater than zero. + +## --bootstrap + +every package chroot needs, at minimum, a userland of some kind. (e.g., a shell +and utilities like `ls` and `cat`.) this userland should be included in the +dependencies of the `how` packages one has chosen. however, when bootstrapping +a lix system, no dependencies have been compiled yet and compilation must depend +on the host system's userland. the `--bootstrap` option allows one to specify a +directory to use as the lowest layer in lix's overlayfs chroot. this directory +must be constructed; `--bootstrap=/` will NOT work because overlayfs does not +allow mounting overlays inside of lower directories. `lmr` may come in handy +here. `lix-os-utilities` also provides a script for constructing such a +directory, `mkbootstrap.sh`. + +## the `build-conf` directory + +when a package's build chroot is being constructed, versions for the target +package and each of its dependencies must be chosen somehow. if a version for +the target package is not passed on the command line, the `build-conf` directory +is examined for a file with the same name as the target package. this file +should contain a whitespace delimited list of package names and version numbers, +with the first line containing the name of the target package and the version to +build by default when no version is specified on the command line. + +if there is no such file, the versions are taken from the `defaults.sh` file in +the `src` package roots for the target package and its dependencies. + +each version specified is validated against the dependency constraints supplied +by `how`'s `deps` command. + +if no version can be determined for a dependency, or if the dependency's layer +does not exist yet, a warning is emitted but the build attempts to continue. +this is because there is no guarantee that the dependency has not simply been +included via the `system` directory. during a bootstrap build, this will be the +case more often than not. in this situation, the dependency's name is still +written to a `built-with` file, but no version number is included. + +## the `built-with` directory + +every time a package is built successfully, a tab delimited list of the +dependencies included in its build chroot and their versions is written to the +`built-with` directory using the pattern `-` for the filename. + +dependencies whose versions could not be determined (likely because they were +included via the `system` directory) or whose layers don't exist yet are listed +without a version number. + +## layers + +in the process of building and installing a package, the following `lyr` layers +are created: + +1. lix///src + used as the top layer for /mnt/src in the package chroot. overlaid on top + of the package's source code. holds all changes that would have been made + to the package's source code directory during the patching, configuring, and + building process. + +2. lix///build + bind mounted to /mnt/build in the package chroot. not a true layer as there + is nothing to overlay. gives packages a place to build out-of-source without + having to mess up the package's root file system. + +3. lix///how + provided as the upper layer for the package's `how` instructions. this layer + gets read-only bind mounted to /mnt/how in the package chroot. mostly exists + to give `how` a place to mount its layers. (see lib/ch.sh for details.) + +4. lix///fs + contains the root filesystem for the package. this is the "upper directory" + in the overlayfs for the package's chroot. gets used as a "lower directory" + in the overlayfs mounts of packages that depend on it. gets `lmr`ed in to the + system root directory by `lix up`. + +## usage + +lix [] + +commands: + pl, pull + acquire and patch package source code. + + cf, config, configure + configure the package. + + mk, make + build the package. + + in, inst, install + install the package to its overlay. + + un, uninst, uninstall + remove the package overlay. + + ad, add + pull, configure, make, and install the package to its overlay. + + rm, remove + delete the package version's overlay, source code, and signature. + + up + symlink the package overlay into the system root. + + dn, down + remove the package symlinks from the system root. + + do + chroot into the package filesystem overlay and run . + + mt, mount + mounts the package filesystem overlay at the given path. + + um, umount + unmounts the package filesystem overlay at the given path. + + vr, ver, version + print the version inferred for the given package. + + dp, deps, dependencies + list package dependencies and their inferred versions. diff --git a/lib/built.sh b/lib/built.sh new file mode 100644 index 0000000..425aab8 --- /dev/null +++ b/lib/built.sh @@ -0,0 +1,9 @@ +#!/bin/sh -e + +. "$LIXROOT/lib/versionformat.sh" + +built() { + ls "$(lyr upperdir lix/$1)" | while read ver; do + [ "ls $(lyr upperdir lix/$1/$ver/fs/)" = "" ] || echo "$ver" + done | shsort -r "vercmp -f $(versionformat "$1")" 2> /dev/null +} diff --git a/lib/clearopaques.sh b/lib/clearopaques.sh new file mode 100644 index 0000000..4f2496b --- /dev/null +++ b/lib/clearopaques.sh @@ -0,0 +1,14 @@ + +#!/bin/sh -e + +clearopaques() { + # opaque directories are created when a layer creates a directory that does + # not exist in one of its currently mounted lower layers. preserving the + # layer's illusion of being the progenitor of a directory when mounted with + # different lower layers is incompatible with lix's operation, and blocking + # another layer's files in general is unsupported behavior as far as lix is + # concerned. this strips all directories of the "opaque" attribute. + + find "$1" -type d -exec setfattr -x trusted.overlay.opaque {} \; 2>/dev/null \ + || true # find returns nonzero if no 'opaque' files were found. +} diff --git a/lib/dependencies.sh b/lib/dependencies.sh new file mode 100644 index 0000000..2ccc385 --- /dev/null +++ b/lib/dependencies.sh @@ -0,0 +1,33 @@ +#!/bin/sh -e + +. "$LIXROOT/lib/built.sh" +. "$LIXROOT/lib/reverse.sh" +. "$LIXROOT/lib/versionpasses.sh" + +dependencies() { # + seen="" + deps="$(echo "$1" | while read line; do + eval "printf '%s\n' "\"$line\""" + done)" + + echo "$deps" | while read line; do + [ "$line" ] || continue; + + # eval inline expressions and convert all whitespace to single spaces. + line="$(echo "$line" | tr -s '[:space:]' ' ')" + dep="$(echo "$line" | cut -d' ' -f1)" + depreq="$(echo "$line" | cut -d' ' -f2-)" + depver="$(built "$dep" | reverse | while read ver; do + [ "$ver" ] || continue; + if [ "$(versionpasses "$dep" "$ver" "$depreq")" = "yes" ]; then + echo "$ver" + break + fi + done)" + + resolved="$(printf '%s\t%s\n' "$dep" "$depver")" + echo "$seen" | grep -q "^$resolved\$" || echo "$resolved" + + seen="$resolved\n$seen" + done +} diff --git a/lib/guessver.sh b/lib/guessver.sh new file mode 100644 index 0000000..44bf995 --- /dev/null +++ b/lib/guessver.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +. "$LIXROOT/lib/versionformat.sh" + +guessver() { + pkg="$1" + tgt="${2:-$pkg}" + + if [ -f "$LIXROOT/build-conf/$pkg" ]; then + awk '$1 == "'$tgt'" { print $2 }' "$LIXROOT/build-conf/$tgt" + return 0 + fi + + printf '%s\n%s\n%s' \ + "$(version=''; eval "$(src -d $tgt 2> /dev/null)"; echo "$version")" \ + "$(ls "$HOWROOT/pkg/$tgt" 2>/dev/null)" \ + "$(built "$tgt" 2> /dev/null)" \ + | grep -vE '^default$|^$' \ + | shsort -r "vercmp -f $(versionformat "$tgt")" 2> /dev/null \ + | tail -n1 +} diff --git a/lib/log.sh b/lib/log.sh new file mode 100644 index 0000000..e9bb6c6 --- /dev/null +++ b/lib/log.sh @@ -0,0 +1,3 @@ +#!/bin/sh -e + + diff --git a/lib/lowerlayers.sh b/lib/lowerlayers.sh new file mode 100644 index 0000000..6106e2c --- /dev/null +++ b/lib/lowerlayers.sh @@ -0,0 +1,36 @@ +#!/bin/sh -e + +#. "$LIXROOT/libs/reverse.sh" + +# errors out if a dependency was not resolved or if the LAYERMAX is exceeded. +lowerlayers() { # [] + layers="$2" + layercount="0" + maxlayers=${LAYERMAX:-100} + + layers="$(echo "$1" | reverse | while read line; do + [ "$line" ] || continue; + + dep="$(echo "$line" | awk '{ print $1 }')" + ver="$(echo "$line" | awk '{ print $2 }')" + dir="$(lyr upperdir lix/$dep/$ver/fs)" + + # only warn on missing dependencies if a bootstrap directory was + # provided. it's possible the dependency is in the bootstrap. + [ "$2" ] && logfn="wrn possible" || logfn="err" + + [ "$ver" ] && [ -d "$dir" ] \ + && printf "$dir:" \ + || $logfn "missing dependency: $dep $ver" + + layercount="$(expr $layercount + 1)" + [ "$layercount" -le "$maxlayers" ] \ + || err "overlayfs can't handle more than $maxlayers lower directories!" + + done)$layers" + + [ "$?" -eq 0 ] || exit $? # error codes weren't bubbling up... :/ + + # strip possible trailing colon and echo. + echo "${layers%:}" +} diff --git a/lib/mounts.sh b/lib/mounts.sh new file mode 100644 index 0000000..624ba74 --- /dev/null +++ b/lib/mounts.sh @@ -0,0 +1,13 @@ +#!/bin/sh -e + +. "$LIXROOT/lib/reverse.sh" + +registermount() { + MOUNTS="$1\n$MOUNTS" +} + +unmountall() { + echo "$MOUNTS" | while read path; do + umount "$path" + done +} diff --git a/lib/pkg.sh b/lib/pkg.sh new file mode 100644 index 0000000..57b9c9f --- /dev/null +++ b/lib/pkg.sh @@ -0,0 +1,100 @@ +#!/bin/sh -e + +. "$LIXROOT/lib/built.sh" +. "$LIXROOT/lib/dependencies.sh" +. "$LIXROOT/lib/clearopaques.sh" +. "$LIXROOT/lib/lowerlayers.sh" +. "$LIXROOT/lib/versionformat.sh" + +export PKGHOW="" +export PKGDEPS="" + +# sets up the package chroot, runs the given command in it, then tears it down. +pkgdo() { + if [ -z "$FAKEROOT" ]; then + FAKEROOT="$(mktemp -d)" + CLEANUPFAKEROOT="yes" + + log "loading $name version $version overlay" + + mountpkg "$FAKEROOT" "$2" \ + || err "failed to mount $name $version overlay." + fi + + chin "$FAKEROOT" "cd /mnt/src; $1" +} + +mountpkghow() { + # mount the 'how' overlay for the given package and version if not mounted. + PKGHOW="$(lyr mountdir "lix/$name/$version/how")" + if ! (mountpoint -q "$PKGHOW"); then + lyr mk "lix/$name/$version/how" + lyr up "$(how "$name" "$version")" "lix/$name/$version/how" > /dev/null + fi +} + +loadpkgdeps() { + [ "$PKGHOW" ] || mountpkghow + + if [ -z "$PKGDEPS" ]; then + [ -f "$PKGHOW/deps" ] \ + || err "could not find a 'deps' file in the package's 'how' overlay!" + + PKGDEPS="$(dependencies "$(cat "$PKGHOW/deps")")" + fi +} + +# sets up the package chroot overlay. optionally takes a path at which the +# overlay should be created. +mountpkg() { + loadpkgdeps + + tgt="$1" + pkglower="$(lowerlayers "$PKGDEPS" "$BSLAYER")" + + # make and mount package root filesystem overlay + lyr mk "lix/$name/$version/fs" + + fs="$(lyr up "$pkglower" "lix/$name/$version/fs")" + + [ -n "$fs" ] || err "could not bring up chroot layer for $name $version." + + # bind mount overlay and override $fs value for remainder of function if a + # target path was specified. + mount -o bind "$fs" "$tgt" + + # bind the how overlay to /mnt/how in the chroot. + mkdir -p "$tgt/mnt/how" + mount -o bind "$PKGHOW" "$tgt/mnt/how" + + # mount source code overlay and bind to /mnt/src in the chroot. + lyr mk "lix/$name/$version/src" + srcmnt="$(lyr up "$SRCREPO/$name/$version" "lix/$name/$version/src")" + + mkdir -p "$tgt/mnt/src" + mount -o bind "$srcmnt" "$tgt/mnt/src" + + # create build layer and bind to /mnt/build in the chroot. + # this layer exists to support packages that need an out-of-source build. + # doesn't really *need* to be a layer, but it's a handy place to stash any + # writes we make to it. + lyr mk "lix/$name/$version/build" + mkdir -p "$tgt/mnt/build" + mount -o bind "$(lyr upperdir "lix/$name/$version/build")" "$tgt/mnt/build" +} + +unmountpkg() { + _dn() { mountpoint -q "$1" && umount "$1"; }; + _dnandrm() { _dn "$1" && rmdir "$1" 2> /dev/null; } + _dnandrm "$1/mnt/build" || true + _dnandrm "$1/mnt/src" || true + _dnandrm "$1/mnt/how" || true + _dn "$1" || true + + lyr dn "lix/$name/$version/src" || true + lyr dn "lix/$name/$version/fs" || true + _dn "$PKGHOW" + + rmdir "$(lyr upperdir lix/$name/$version)/fs/mnt" || true + clearopaques "$(lyr upperdir lix/$name/$version)/fs" || true +} diff --git a/lib/reverse.sh b/lib/reverse.sh new file mode 100644 index 0000000..9407c6b --- /dev/null +++ b/lib/reverse.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +alias reverse="sed '1!x;H;1h;\$!d;g'" diff --git a/lib/versionformat.sh b/lib/versionformat.sh new file mode 100644 index 0000000..6bee94f --- /dev/null +++ b/lib/versionformat.sh @@ -0,0 +1,7 @@ +#!/bin/sh -e + +versionformat() { + (vercmp formats | grep -q "^$1\$") \ + && echo "$1" \ + || echo "default" +} diff --git a/lib/versionpasses.sh b/lib/versionpasses.sh new file mode 100755 index 0000000..653db07 --- /dev/null +++ b/lib/versionpasses.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +. "$LIXROOT/lib/versionformat.sh" + +versionpasses() { # + + index() { echo "$1" | tr -s '[:space:]' ' ' | cut -d' ' -f$2; } + + format="$(versionformat "$1")" + version="$2" + constraints="$3" + value="yes" + + while [ "$constraints" != "" ]; do + + # handle 'or' operator + if [ "$(index "$constraints" 1)" = "||" ]; then + constraints="$(index "$constraints" 2-)" + + [ "$value" = "yes" ] \ + && break \ + || { value="yes" && continue; } + fi + + # handle implicit 'and' operator + if [ "$value" = "yes" ]; then + value="$(vercmp -f "$format" "$version $(index "$constraints" -2)")" + fi + + constraints="$(index "$constraints" 3-)" + done + + echo "$value" +} diff --git a/lix.sh b/lix.sh new file mode 100755 index 0000000..5daf313 --- /dev/null +++ b/lix.sh @@ -0,0 +1,363 @@ +#!/bin/sh -e + +[ -n "$2" ] || { \ +echo " +usage: $(basename $0) [--bootstrap=] [] + +options: + --bootstrap= + use as the lowest layer in the overlay stack. + +commands: + pl, pull + acquire and patch package source code. + + cf, config, configure + configure the package. + + mk, make + build the package. + + in, inst, install + install the package to its overlay. + + un, uninst, uninstall + remove the package overlay. + + ad, add + pull, configure, make, and install the package to its overlay. + + rm, remove + delete the package version's overlay, source code, and signature. + + up [additional lmr flags] + symlink the package into the system root with lmr, passing along flags. + + dn, down + remove the package symlinks from the system root with lmr. + + do + chroot into the package filesystem overlay and run . + + mt, mount + mounts the package filesystem overlay at the given path. + + um, umount + unmounts the package filesystem overlay at the given path. + + vr, ver, version + print the default version for the given package. + + bl, built + list all built versions for the given package. + + dp, deps, dependencies + list package dependencies and their inferred versions. +" && exit 0; } + +# if this is running on an interactive terminal (as opposed to in a cron job, +# for example), if tput is installed, and if tput knows some color codes, then +# set the color values. +if [ -t 1 ] && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then + _clr="$(tput sgr0)" + _blu="$(tput setaf 6)" + _ylw="$(tput setaf 3)" + _red="$(tput setaf 1)" +fi + +log() { echo "$_blu[LOG]$_clr $@"; } +wrn() { echo "$_ylw[WRN]$_clr $@" >&2; } +err() { echo "$_red[ERR]$_clr $@" >&2; exit 1; } + +export LIXPREFIX="${LIXPREFIX:-${0%/bin/lix}}" +export LIXROOT="$(dirname "$(readlink -f "$0")")" +export HOWROOT="$(dirname "$(readlink -f "$(which how)")")" +export SRCREPO="${SRCREPO:-$LIXPREFIX/var/src}" +export SRCROOT="${SRCROOT:-${LIXROOT%/lix}/src}" + +export cmd="$1" +export OP="$cmd" +export FAKEROOT="" +export CLEANUPFAKEROOT="" +export SUCCESS="" + +# these are lowercase because they will be used in 'how' scripts. +export name +export version + +[ -d "$SRCROOT" ] || err "$SRCROOT is not a directory!" +[ -d "$SRCREPO" ] || err "$SRCREPO is not a directory!" +[ -d "$LIXROOT" ] || err "$LIXROOT is not a directory!" + +. "$LIXROOT/lib/pkg.sh" +. "$LIXROOT/lib/built.sh" +. "$LIXROOT/lib/dependencies.sh" +. "$LIXROOT/lib/lowerlayers.sh" +. "$LIXROOT/lib/guessver.sh" + +# process command options +if [ "${cmd#--bootstrap}" != "$cmd" ]; then + if [ "$cmd" = "--bootstrap" ]; then + shift + export BSLAYER="$1" + else + export BSLAYER="$(echo "$cmd" | cut -d'=' -f2)" + fi + [ -d "$BSLAYER" ] || err "$BSLAYER is not a directory!" + + shift + cmd="$1" +fi + +case "$cmd" in + do) + docmd="$2" + shift + ;; + + mt|mnt|mount|um|umt|umnt|umount) + FAKEROOT="$2" + shift + ;; + + up) + while [ "${2#-}" != "$2" ]; do + upflags="$upflags $2" + shift + done + ;; + + sh|shell) + # maybe this should be its own utility. + [ "$1" ] || err "missing 'layer name' argument!" + [ -e "$2" ] || err "no such file as '$2'." + + [ "$1" = "${1#lix/}" ] \ + || wrn "you are using the 'lix' namespace! this can be dangerous..." + + layers="$(lowerlayers "$(dependencies "$(cat "$2")")" "$BSLAYER")" + lyr mk "$1" + + chin "$(lyr up "$layers" "$1")" "${3:-sh}" + exit 0 + ;; +esac + +# get required positional arguments +name="$2" + +[ -n "$3" ] \ +&& version="$3" \ +|| version="$(guessver "$name")" + +[ -n "$version" ] || err "could not determine version for '$name'." + +cleanup() { + [ -z "$PKGHOW" ] || umount "$PKGHOW" || true + [ -z "$FAKEROOT" ] || unmountpkg "$FAKEROOT" || true + [ -z "$CLEANUPFAKEROOT" ] || rmdir "$FAKEROOT" || true + [ -n "$SUCCESS" ] || err "$name $version '$OP' failed" +} +trap cleanup EXIT INT HUP + +beginmsg() { + OP="$1" + log "starting '$OP' for $name $version" +} + +successmsg() { log "completed '$OP' for $name $version"; } + +expectsrc() { + [ -d "$(lyr upperdir lix/$name/$version/src)" ] \ + || err "'src' layer missing! did you run 'lix pull $name $version' first?" +} + +pullcmd() { + beginmsg "pull" + + # if `how` has specific instructions for this package but there is no + # `src` package, then this must be a "bundle" package or something. + # just create an empty source directory and call it a day. (but please + # consider finding a more elegant way to handle bundles. there must + # be one, but it will probably require a rewrite. design problems + # usually arise out of fundamentally flawed models. with the right + # model, a _clean_ solution becomes trivial.) + if { how $name $version | grep -q ':' ; } \ + && [ ! -d "$SRCROOT/pkg/$name" ] + then + wrn "no 'src' entry. assuming this is a 'sourceless' package..." + mkdir -p "$SRCREPO/$name/$version" # ew... + elif [ -e "$SRCREPO/$name/$version" ]; then + wrn "$SRCREPO/$name/$version already exists. skipping remote pull." + else + src "$name" "$version"; + fi + + if [ -d "$(lyr upperdir lix/$name/$version/src)" ]; then + wrn "source code upper layer already exists! skipping patch.sh." + else + loadpkgdeps + + log "looking for a patch.sh from 'how' to run in $name's overlay." + pkgdo ". ../how/env.sh; ../how/patch.sh" + fi + + successmsg +} + +confcmd() { + beginmsg "configure" + + pkgdo ". ../how/env.sh; ../how/conf.sh" + + successmsg +} + +makecmd() { + beginmsg "make" + + pkgdo ". ../how/env.sh; ../how/make.sh" + + # compile "built-with" list + mkdir -p -- "$LIXROOT/built-with/${name}" + echo "$deps" > "$LIXROOT/built-with/${name}/$version" + + successmsg +} + +instcmd() { + beginmsg "install" + + upperdir="$(lyr upperdir lix/$name/$version)" + + # clean out previous installation and make backup + if [ -d "$upperdir/fs" ]; then + log "making backup of package's existing 'fs' layer..." + + fsbak="$upperdir/bak/$(basename "$(mktemp -u)")" + mkdir -p "$fsbak" + cp -a "$upperdir/fs/." "$fsbak/" + fi + + # install. restore backup on failure. delete backup on success. + if { pkgdo ". ../how/env.sh; ../how/inst.sh"; }; then + log "removing backup of package's previous 'fs' layer..." + rm -fr "$fsbak" + rmdir "$(dirname "$fsbak")" 2> /dev/null || true + else + log "restoring backup of package's previous 'fs' layer..." + rm -fr "$upperdir/fs" + mv "$fsbak" "$upperdir/fs" + rmdir "$(dirname "$fsbak")" 2> /dev/null || true + exit 1 + fi + + successmsg +} + +# commands can be invoked with their full name, with a two-letter short form +# composed of the first letter and the last consonant (e.g., 'mk' for 'make'), +# and with an assortment of one-off abbreviations that just make sense for each +# command. e.g., 'rm' for 'remove'. + +case "$cmd" in + pl|pull) pullcmd ;; + + cf|cr|cg|conf|config|configure) + expectsrc + confcmd + ;; + + mk|make) + expectsrc + makecmd + ;; + + in|il|inst|install) + expectsrc + instcmd + ;; + + ad|add) + pullcmd + + expectsrc + confcmd + makecmd + instcmd + ;; + + cl|cn|clean) + rm -fr "$(lyr upperdir lix/$name/$version)/src" 2> /dev/null || true + rm -fr "$(lyr upperdir lix/$name/$version)/build" 2> /dev/null || true + ;; + + rm|rv|remove) lyr rm "lix/$name/$version" ;; + + pg|purge) + rm -fr $SRCREPO/$name/$version + lyr rm "lix/$name/$version" + ;; + + up) lmr -s $upflags "$(lyr upperdir lix/$name/$version/fs)" / ;; + + dn|down) lmr -s -u "$(lyr upperdir lix/$name/$version/fs)" / ;; + + do) + expectsrc + beginmsg "do" + pkgdo ". ../how/env.sh; $docmd" + successmsg + ;; + + mt|mnt|mount) + beginmsg "mount" + [ -d "$mntpth" ] || err "no such directory: $mntpth" + + expectsrc + + mountpkg "$mntpth" >/dev/null \ + || err "failed to mount $name $version overlay." + + successmsg + ;; + + um|ut|umt|umnt|umount|unmount) + unmountpkg "$mntpth" \ + && log "unmounted $name $version overlay at $mntpth" \ + || err "error unmounting $name $version overlay at $mntpth" + ;; + + vr|vn|ver|version) echo "$version" ;; + + bl|built) built "$name" ;; + + dp|deps|dependencies) + loadpkgdeps + + cols=0 + cols="$(echo "$PKGDEPS" | while read line; do + newcols="$(echo "$line" | awk '{ print $1 }' | wc -c)" + [ "$newcols" -ge "$cols" ] || continue + cols="$newcols" + echo "$cols" + done | tail -n1)" + + # get a list of dependencies with resolved version numbers. if no + # version number could be resolved, the "version" column will be empty + # and a warning will be printed. + echo "$PKGDEPS" | while read line; do + [ "$line" ] || continue; + dep="$(echo "$line" | awk '{ print $1 }')" + depver="$(echo "$line" | awk '{ print $2 }')" + [ "$depver" ] || wrn "could not resolve dependency '$dep'!" + + # pad-space each dependency name so the versions align in a column. + printf "%-${cols}s %s\n" "$dep" "$depver" + done + ;; + + *) err "unrecognized command '$cmd'";; +esac + +SUCCESS="yes" diff --git a/makefile b/makefile new file mode 100644 index 0000000..903f857 --- /dev/null +++ b/makefile @@ -0,0 +1,26 @@ +SRCDIR := $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +PREFIX ?= /usr +DESTDIR ?= $(PREFIX)/bin +LIXROOT ?= $(PREFIX)/share/lix + +.PHONY: default install uninstall + +default: $(SRCDIR)built-with $(SRCDIR)build-conf + +$(SRCDIR)built-with: + mkdir -p $(SRCDIR)built-with + +$(SRCDIR)build-conf: + mkdir -p $(SRCDIR)build-conf + +$(LIXROOT): + ln -sf $(SRCDIR) $(LIXROOT) + +$(DESTDIR)/lix: + ln -sf $(LIXROOT)/lix.sh $(DESTDIR)/lix + +install: $(LIXROOT) $(DESTDIR)/lix + +uninstall: + rm $(DESTDIR)/lix + rm $(LIXROOT) diff --git a/sloc.sh b/sloc.sh new file mode 100755 index 0000000..663a03b --- /dev/null +++ b/sloc.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# find all *.sh files not under `pkg` that are not symbolic links, strip all +# trailing whitespace, then all leading whitespace, then all lines starting +# with '#', then all empty lines. then count the remaining lines. + +find . -name "*.sh" ! -type l \ +| xargs sed 's/[[:space:]]*$//g; s/^[[:space:]]*//g; s/^#.*$//g; /^$/d' \ +| wc -l - \ +| cut -d' ' -f1 + +# note that this script's ELOC is also included in the count.