aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dir-locals.el4
-rw-r--r--.github/workflows/main.yml2
-rw-r--r--.gitignore21
-rw-r--r--INSTALL33
-rw-r--r--Makefile69
-rw-r--r--README13
-rw-r--r--bin/cpt-readlink.c47
-rw-r--r--bin/cpt-stat.c41
-rw-r--r--clean.do5
-rw-r--r--config.mk25
-rw-r--r--config.rc22
-rwxr-xr-xcontrib/cpt-chbuild15
-rwxr-xr-xcontrib/cpt-owns2
-rw-r--r--default.do40
-rw-r--r--docs/cpt.org150
-rw-r--r--docs/cpt.texi143
-rw-r--r--docs/default.do33
-rw-r--r--install.do17
-rw-r--r--lib.rc60
-rw-r--r--man/cpt-contrib.110
-rw-r--r--src/Makefile4
-rwxr-xr-xsrc/cpt6
-rwxr-xr-xsrc/cpt-build2
-rwxr-xr-xsrc/cpt-checksum3
-rwxr-xr-xsrc/cpt-install2
-rw-r--r--src/cpt-lib.in (renamed from src/cpt-lib)189
-rwxr-xr-xsrc/cpt-list12
-rwxr-xr-xsrc/cpt-remove2
-rw-r--r--src/test.do7
-rwxr-xr-xtools/do446
-rwxr-xr-xtools/install.sh90
-rw-r--r--uninstall.do14
32 files changed, 1263 insertions, 266 deletions
diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644
index 0000000..81ec972
--- /dev/null
+++ b/.dir-locals.el
@@ -0,0 +1,4 @@
+;;; Directory Local Variables
+;;; For more information see (info "(emacs) Directory Variables")
+
+((sh-mode . ((flycheck-shellcheck-excluded-warnings . ("2119")))))
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 836d891..48b29d9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -10,4 +10,4 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Run tests.
- run: make test
+ run: ./tools/do test
diff --git a/.gitignore b/.gitignore
index 6a09127..570dc6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,5 @@
-### Binaries/Objects ###
-cpt-stat
-cpt-readlink
-getopt
-*.o
+# Built library
+cpt-lib
### Emacs ###
\#*\#
@@ -14,4 +11,16 @@ getopt
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
-[._]sw[a-p] \ No newline at end of file
+[._]sw[a-p]
+
+### Redo files
+/.redo
+/.do_built
+/.do_built.dir
+*.tmp
+*.did
+.dep*
+.target*
+
+### Texinfo ###
+*.info
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..2a80450
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,33 @@
+Installation Instructions
+=========================
+
+In order to install cpt, you either need a redo implementation on your system,
+or use the `do` script inside the `tools/` directory (which is slower but the
+source code isn't that big anyways).
+
+Paths and version information can be configured from the `config.rc` file or
+through environment variables.
+
+
+Installing from a git checkout
+------------------------------
+
+On the git checkout of the repository, the info page for the package manager
+doesn't exist. The build system looks for `makeinfo` in order to build the info
+page, but exits with success if it isn't on your system. That's why you
+shouldn't edit the MAKEINFO variable even if you don't have it on your system,
+it is optional.
+
+
+With redo
+---------
+
+ redo
+ DESTDIR= PREFIX=/usr/local redo install
+
+
+With minimal do
+---------------
+
+ ./tools/do
+ DESTDIR= PREFIX=/usr/local ./tools/do install
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 1f03f57..0000000
--- a/Makefile
+++ /dev/null
@@ -1,69 +0,0 @@
-# See LICENSE for copyright information
-include config.mk
-
-SRC = bin/cpt-readlink.c bin/cpt-stat.c
-OBJ = ${SRC:.c=.o}
-BIN = ${SRC:.c=}
-
-.SUFFIXES:
-.SUFFIXES: .o .c .org .texi .info
-
-.org.texi:
- ${EMACS} $< --batch -f org-texinfo-export-to-texinfo
-
-.texi.info:
- ${MAKEINFO} $< -o $@
-
-all: ${BIN}
-
-.c:
- ${CC} ${CFLAGS} ${LDFLAGS} -o $@ $< ${LIBS}
-
-clean:
- rm -f ${BIN} ${OBJ}
-
-test: ${BIN}
- bin/cpt-stat bin
- bin/cpt-stat Makefile
- bin/cpt-readlink /bin/sh
- ${MAKE} -C src test
-
-install-bin: ${BIN}
- for bin in ${BIN}; do \
- install -Dm755 $${bin} ${DESTDIR}${BINDIR}/$${bin##*/}; done
-
-install-src:
- for bin in src/cpt-*; do \
- install -Dm755 $${bin} ${DESTDIR}${BINDIR}/$${bin##*/}; done
-
-install-contrib:
- for bin in contrib/cpt-*; do \
- install -Dm755 $${bin} ${DESTDIR}${BINDIR}/$${bin##*/}; done
-
-install-contrib-static:
- mkdir -p ${DESTDIR}${BINDIR}
- for bin in contrib/cpt-*; do \
- sed '/\. cpt-lib/r src/cpt-lib' $${bin} | \
- sed '/\. cpt-lib/d' > ${DESTDIR}${BINDIR}/$${bin##*/}; \
- chmod 755 ${DESTDIR}${BINDIR}/$${bin##*/}; done
-
-install-src-static:
- mkdir -p ${DESTDIR}${BINDIR}
- for bin in src/cpt-*; do \
- sed '/\. cpt-lib/r src/cpt-lib' $${bin} | \
- sed '/\. cpt-lib/d' > ${DESTDIR}${BINDIR}/$${bin##*/}; \
- chmod 755 ${DESTDIR}${BINDIR}/$${bin##*/}; done
-
-install-doc:
- for man in man/*.1; do install -Dm644 $${man} ${DESTDIR}${MAN1}/$${man##*/}; done
-
-install: install-bin install-src install-contrib install-doc
-install-static: install-bin install-src-static install-contrib-static install-doc
-
-uninstall:
- for bin in ${BIN} src/cpt-* contrib/cpt-*; do \
- rm -f ${DESTDIR}${BINDIR}/$${bin##*/}; done
- for man in man/*; do rm -f ${DESTDIR}${MAN1}/$${man##*/}; done
-
-
-.PHONY: all install-bin install-src install-contrib install-doc install-src-static install-contrib-static install uninstall test clean
diff --git a/README b/README
index 0e83582..c1021ad 100644
--- a/README
+++ b/README
@@ -19,25 +19,22 @@ Dependencies
To build and use cpt, you need the following software.
-- C compiler [make]
+RUNTIME DEPENDS
- rsync
- curl
-- getopt [provided by cpt if not available]
- POSIX base utilities [coreutils, busybox, sbase, etc.]
- tar [GNU tar, busybox, toybox, libarchive, etc.]
-Build configuration can be done from the 'config.mk' file. If you have getopt
-on your system, add SYSTEM_GETOPT=1 option to your 'config.mk'.
+MAKE DEPENDS
+- redo (optional, the repository contains tools/do)
Directory Structure
--------------------------------------------------------------------------------
- / -- cpt, README, Makefile, LICENSE, CHANGELOG
- bin/ -- for C programs.
+ / -- README, LICENSE, CHANGELOG
contrib/ -- for Shell scripts that wrap around cpt.
- doc/ -- for documentation.
- getopt-ul -- for cpt provided util-linux standalone getopt(1).
+ docs/ -- for documentation.
man/ -- for manual pages.
src/ -- for the tools that make up the package manager.
diff --git a/bin/cpt-readlink.c b/bin/cpt-readlink.c
deleted file mode 100644
index e7cfe50..0000000
--- a/bin/cpt-readlink.c
+++ /dev/null
@@ -1,47 +0,0 @@
-/* cpt-readlink --- a utility replacement for readlink
- * See LICENSE for copyright information.
- *
- * This is basically a 'readlink -f' command.
- */
-#include <stdio.h>
-#include <stdlib.h>
-#include <libgen.h>
-#include <string.h>
-#include <limits.h>
-
-#define DIR_MAX PATH_MAX - NAME_MAX - 1
-
-
-char *realpath(const char *path, char *resolved_path);
-
-int
-main(int argc, char *argv[])
-{
-
- char buf[PATH_MAX];
-
- /* We are going to use these if the file doesn't exist, but we can still
- * use directories above the file. We are using dname and bname so that
- * they don't clash with the functions with the same name.
- */
- char dname[DIR_MAX]; /* directory name */
- char bname[NAME_MAX]; /* base name */
- sprintf(bname, "%s", (basename(argv[1])));
-
- if (argc != 2 || strcmp(argv[1], "--help") == 0) {
- fprintf(stderr, "usage: %s [file]\n", argv[0]);
- return 1;
- }
-
- if (!realpath(argv[1], buf)) {
-
- if (!realpath(dirname(argv[1]), dname)) {
- perror(argv[0]);
- return 1;
- }
- sprintf(buf, "%s/%s", dname, bname);
- }
-
- printf("%s\n", buf);
- return 0;
-}
diff --git a/bin/cpt-stat.c b/bin/cpt-stat.c
deleted file mode 100644
index 584c2df..0000000
--- a/bin/cpt-stat.c
+++ /dev/null
@@ -1,41 +0,0 @@
-/* cpt-stat --- a utility for getting the user name of file owner
- * See LICENSE for copyright information
- *
- * The reason this simple tool exists is because 'stat' is not
- * portable and ls is not exactly stable enough for scripting.
- * This program is for outputting the owner name, and that's it.
- */
-
-#include <pwd.h>
-#include <sys/stat.h>
-#include <stdio.h>
-#include <string.h>
-
-struct passwd *pw;
-struct stat sb;
-
-int
-main (int argc, char *argv[])
-{
- /* Exit if no or multiple arguments are given. */
- if (argc != 2 || strcmp(argv[1], "--help") == 0) {
- fprintf(stderr, "Usage: %s [pathname]\n", argv[0]);
- return 1;
- }
-
- /* Exit if file stat cannot be obtained. */
- if (lstat(argv[1], &sb) == -1) {
- perror(argv[0]);
- return 1;
- }
-
- /* Exit if name of the owner cannot be retrieved. */
- if (!getpwuid(sb.st_uid)) {
- return 1;
- }
-
- /* Print the user name of file owner. */
- pw = getpwuid(sb.st_uid);
- printf("%s\n", pw->pw_name);
- return 0;
-}
diff --git a/clean.do b/clean.do
new file mode 100644
index 0000000..57ab99f
--- /dev/null
+++ b/clean.do
@@ -0,0 +1,5 @@
+. ./config.rc
+redo src/clean
+redo_clean
+rm -f "cpt-$VERSION.tar.xz"
+find docs -name '*.info' -exec rm -f -- {} +
diff --git a/config.mk b/config.mk
deleted file mode 100644
index ba76118..0000000
--- a/config.mk
+++ /dev/null
@@ -1,25 +0,0 @@
-# See LICENSE for copyright information
-VERSION = 5.1.1
-
-# Paths
-PREFIX = /usr/local
-BINDIR = ${PREFIX}/bin
-SHAREDIR = ${PREFIX}/share
-DOCDIR = ${SHAREDIR}/doc
-CPTDOC = ${DOCDIR}/cpt
-MANPREFIX = ${SHAREDIR}/man
-MAN1 = ${MANPREFIX}/man1
-
-# Flags
-CFLAGS = -std=c99 -Wpedantic -Wall -Os
-CFLAGS += -D_XOPEN_SOURCE=700
-LDFLAGS = -s -static
-LIBS = -lc
-
-# C compiler and linker
-CC = cc
-LD = ${CC}
-
-# Documentation tools
-EMACS = emacs
-MAKEINFO = makeinfo
diff --git a/config.rc b/config.rc
new file mode 100644
index 0000000..963e71e
--- /dev/null
+++ b/config.rc
@@ -0,0 +1,22 @@
+# -*- mode: redo -*-
+# See LICENSE for copyright information
+
+# This loads some functions that are shared between redo files.
+# shellcheck source=./lib.rc
+. "${SRC_ROOT:=.}/lib.rc"
+
+setv VERSION = 5.1.1
+
+# Paths
+setv PREFIX = /usr/local
+setv BINDIR = "${PREFIX}/bin"
+setv SHAREDIR = "${PREFIX}/share"
+setv DOCDIR = "${SHAREDIR}/doc"
+setv CPTDOC = "${DOCDIR}/cpt"
+setv INFODIR = "${SHAREDIR}/info"
+setv MANPREFIX = "${SHAREDIR}/man"
+setv MAN1 = "${MANPREFIX}/man1"
+
+# Documentation tools
+setv EMACS = emacs
+setv MAKEINFO = makeinfo
diff --git a/contrib/cpt-chbuild b/contrib/cpt-chbuild
index 923a7fa..6177e67 100755
--- a/contrib/cpt-chbuild
+++ b/contrib/cpt-chbuild
@@ -18,7 +18,6 @@ die() {
case "$(uname -m)" in i*86) arch=i686; esac
url="https://dl.carbslinux.org/releases/${arch:-$(uname -m)}/carbs-rootfs.tar.xz"
-pid=$$
cd "$cac_dir"
@@ -44,20 +43,22 @@ sh256 carbs-rootfs.tar.xz | diff - carbs-rootfs.tar.xz.sum ||
( cd carbs-chroot; tar xf ../carbs-rootfs.tar.xz )
}
+create_cache empty
+
log "Creating temporary chroot"
-cp -a carbs-chroot "chroot-$pid"
+cp -a carbs-chroot "${chr_dir:=$tmp_dir/chroot}"
[ "$1" ] && {
log "Installing extra packages"
- CPT_ROOT=$PWD/chroot-$pid cpt-install "$@"
+ CPT_ROOT=$chr_dir cpt-install "$@"
}
-run_hook pre-chroot "" "$PWD/chroot-$pid"
+run_hook pre-chroot "" "$chr_dir"
log "Entering chroot"
if [ "$(id -u)" -eq 0 ]; then
- cpt-chroot "chroot-$pid"
- rm -rf "chroot-$pid"
+ cpt-chroot "$chr_dir"
+ rm -rf "$chr_dir"
else
- as_root sh -c "cpt-chroot chroot-$pid; rm -rf chroot-$pid"
+ as_root sh -c "cpt-chroot $chr_dir; rm -rf $chr_dir"
fi
diff --git a/contrib/cpt-owns b/contrib/cpt-owns
index 5f73674..494e0fd 100755
--- a/contrib/cpt-owns
+++ b/contrib/cpt-owns
@@ -17,7 +17,7 @@ esac
# Strip 'CPT_ROOT' from the file path if passed and
# follow symlinks.
file="${1#$CPT_ROOT}"
-dirname=$(cpt-readlink "$CPT_ROOT/${file%/*}")
+dirname=$(_readlinkf "$CPT_ROOT/${file%/*}")
file="$dirname/${file##*/}"
# Check if the file exists and exit if it is not.
diff --git a/default.do b/default.do
new file mode 100644
index 0000000..23cedf8
--- /dev/null
+++ b/default.do
@@ -0,0 +1,40 @@
+. ./config.rc
+
+case "$1" in
+ all) redo-ifchange src/cpt-lib docs/all ;;
+ dist)
+ redo clean
+ redo "cpt-$VERSION.tar.xz"
+ ;;
+ src/cpt-lib)
+ redo-ifchange "$1.in"
+ sed -e "s|@VERSION@|$VERSION|g" \
+ -e "s|@DOCSTRING@|Call functions from the library|g" < "$1.in" > "$3"
+ chmod +x "$3"
+ ;;
+ "cpt-$VERSION.tar.xz")
+ redo docs/cpt.info
+ rm -rf -- "cpt-$VERSION"
+ mkdir -p "cpt-$VERSION"
+ { git ls-tree -r HEAD --name-only && echo docs/cpt.info ;} |
+ while read -r file; do
+ [ "${file##*/*}" ] ||
+ mkdir -p "cpt-$VERSION/${file%/*}"
+ cp "$file" "cpt-$VERSION/$file"
+ done
+ tar cf "cpt-$VERSION.tar" "cpt-$VERSION"
+ xz -z "cpt-$VERSION.tar"
+ rm -rf -- "cpt-$VERSION"
+ mv "$1" "$3"
+ ;;
+ test)
+ redo src/test
+ ;;
+ src/clean)
+ rm -f src/cpt-lib
+ PHONY
+ ;;
+ *)
+ echo "Unknown target $1"
+ exit 99
+esac
diff --git a/docs/cpt.org b/docs/cpt.org
index bff8ac9..dfddc38 100644
--- a/docs/cpt.org
+++ b/docs/cpt.org
@@ -48,7 +48,9 @@ manual for *Carbs Packaging Tools*. For development logs see [[https://git.carbs
- [[#option-parsing][Option parsing]]
- [[#message-functions][Message functions]]
- [[#text-functions][Text functions]]
+ - [[#portability-functions][Portability functions]]
- [[#system-functions][System Functions]]
+ - [[#package-functions][Package Functions]]
* Copying
:PROPERTIES:
@@ -890,11 +892,11 @@ Following functions are used to manipulate, check, or interact with text.
given string. If the string is inside the list, it will return 0, otherwise 1.
#+begin_src sh
- # Usage
- contains "$LIST" foo
+# Usage
+contains "$LIST" foo
- contains "foo bar" foo # Returns 0
- contains "foo bar" baz # Returns 1
+contains "foo bar" foo # Returns 0
+contains "foo bar" baz # Returns 1
#+end_src
*** =regesc()=
@@ -906,7 +908,7 @@ given string. If the string is inside the list, it will return 0, otherwise 1.
in POSIX BRE. Those characters are, =$=, =.=, =*=, =[=, =\\=, and =^=.
#+begin_src sh
- regesc '^[$\' # Returns \^\[\$\\
+regesc '^[$\' # Returns \^\[\$\\
#+end_src
*** =pop()=
@@ -918,13 +920,75 @@ in POSIX BRE. Those characters are, =$=, =.=, =*=, =[=, =\\=, and =^=.
call. Word splitting is intentional when using this function.
#+begin_src sh
- # Usage
- pop foo from $LIST
+# Usage
+pop foo from $LIST
- pop foo from foo baz bar # Returns baz bar
+pop foo from foo baz bar # Returns baz bar
#+end_src
-** System Functions
+*** =sepchar()=
+:PROPERTIES:
+:DESCRIPTION: Separate characters from a string
+:END:
+
+This function can be used to separate characters from the given string without
+resorting to external resources.
+
+#+begin_src sh
+sepchar mystring
+# Prints:
+# m
+# y
+# s
+# t
+# r
+# i
+# n
+# g
+#+end_src
+
+** Portability functions
+:PROPERTIES:
+:DESCRIPTION: Functions to replace non-POSIX commands
+:END:
+
+These helper functions are used so that we don't depend on non-POSIX programs for
+certain functionality. They are prefixed with the =_= character.
+
+*** =_seq()=
+:PROPERTIES:
+:DESCRIPTION: 'seq(1)' but no newline
+:END:
+
+This function is similar to =seq(1)= except that it only takes a single argument
+and doesn't print any newlines. It is suitable to be used in =for= loops.
+
+#+begin_src sh
+_seq 5
+# Prints:
+# 1 2 3 4 5
+#+end_src
+
+*** =_stat()=
+:PROPERTIES:
+:DESCRIPTION: 'stat %U' replacement
+:END:
+
+This function imitates =stat %U=. =stat= isn't defined by POSIX, and this is
+also a GNU extension. This function returns the owner of a file. If the owner
+cannot be found, it will return =root=.
+
+*** =_readlinkf()=
+:PROPERTIES:
+:DESCRIPTION: 'readlink -f' replacement
+:END:
+
+This function was taken from [[https://github.com/ko1nksm/readlinkf][POSIX sh readlinkf library by Koichi Nakashima]].
+=readlink= is also not defined by POSIX, so this function uses =ls= to follow
+symbolic links until it reaches the actual file.
+
+** TODO System Functions
+- [ ] Add description
*** =as_root()=
:PROPERTIES:
:DESCRIPTION: Run a command as the root user
@@ -935,6 +999,68 @@ environment variable is set, it will call the following arguments as the root
user. It supports the following programs for privilege escalation with the
following order:
-1. =sudo=
-2. =doas=
-3. =su=
+1. =sls=
+2. =sudo=
+3. =doas=
+4. =su=
+
+** TODO Package Functions
+:PROPERTIES:
+:DESCRIPTION: Manipulate, or query anything related to packages
+:END:
+
+Obviously, package functions are the most important ones for =cpt-lib=, those
+are the ones you will use to build, to query, to manipulate, or to otherwise
+interact with packages.
+
+*** =pkg_owner()=
+:PROPERTIES:
+:DESCRIPTION: Check which package owns the given file
+:END:
+
+This function can be used to determine the owner of a package. The first
+argument is used for flags that will be passed to =grep=, and the second one is
+for the file query. Rest of the arguments can be used in order to specify the
+manifests to be used, but it is optional. =pkg_owner()= will search for all the
+installed packages if no other arguments are given.
+
+#+begin_src sh
+# Example
+pkg_owner -lFx /usr/bin/grep # Returns 'busybox'
+
+# An example call made by `pkg_fix_deps()` to figure out whether the built
+# package contains the file it depends.
+pkg_owner -l "/${dep#/}\$" "$PWD/manifest" >/dev/null && continue
+pkg_owner -l "/${dep#/}\$" "$@" ||:
+#+end_src
+
+*** =pkg_isbuilt()=
+:PROPERTIES:
+:DESCRIPTION: Check whether the given package is built
+:END:
+
+This function returns with success when the given package has a built tarball
+with the matching version and release strings from the repository.
+
+*** =pkg_lint()=
+:PROPERTIES:
+:DESCRIPTION: Check whether a package directory fits the standards
+:END:
+
+This function checks whether a given package fits the proper package
+specification. This function *does not return with failure, it exits outright*
+if it fails.
+
+*** TODO =pkg_find()=
+:PROPERTIES:
+:DESCRIPTION: Query package locations
+:END:
+
+=pkg_find()=
+
+*** TODO =pkg_gentree=
+:PROPERTIES:
+:DESCRIPTION: Generate a dependency tree for the given package
+:END:
+
+Keep in mind /etc/cpt-base
diff --git a/docs/cpt.texi b/docs/cpt.texi
index 9a9974f..7c1a95f 100644
--- a/docs/cpt.texi
+++ b/docs/cpt.texi
@@ -106,7 +106,9 @@ CPT Library
* Option parsing:: Easy way of parsing options with cpt-lib
* Message functions:: Communicate to users
* Text functions:: Manipulate or check text
+* Portability functions:: Functions to replace non-POSIX commands
* System Functions::
+* Package Functions:: Manipulate, or query anything related to packages
Option parsing
@@ -126,11 +128,26 @@ Text functions
* @samp{contains()}:: Check if a "string list" contains a word
* @samp{regesc()}:: Escape regular expression characters
* @samp{pop()}:: Remove an item from a string list
+* @samp{sepchar()}:: Separate characters from a string
+
+Portability functions
+
+* @samp{_seq()}:: 'seq(1)' but no newline
+* @samp{_stat()}:: 'stat %U' replacement
+* @samp{_readlinkf()}:: 'readlink -f' replacement
System Functions
* @samp{as_root()}:: Run a command as the root user
+Package Functions
+
+* @samp{pkg_owner()}:: Check which package owns the given file
+* @samp{pkg_isbuilt()}:: Check whether the given package is built
+* @samp{pkg_lint()}:: Check whether a package directory fits the standards
+* @samp{pkg_find()}:: Query package locations
+* @samp{pkg_gentree}:: Generate a dependency tree for the given package
+
@end detailmenu
@end menu
@@ -888,7 +905,9 @@ package manager library.
* Option parsing:: Easy way of parsing options with cpt-lib
* Message functions:: Communicate to users
* Text functions:: Manipulate or check text
+* Portability functions:: Functions to replace non-POSIX commands
* System Functions::
+* Package Functions:: Manipulate, or query anything related to packages
@end menu
@node Calling the library
@@ -1048,6 +1067,7 @@ Following functions are used to manipulate, check, or interact with text.
* @samp{contains()}:: Check if a "string list" contains a word
* @samp{regesc()}:: Escape regular expression characters
* @samp{pop()}:: Remove an item from a string list
+* @samp{sepchar()}:: Separate characters from a string
@end menu
@node @samp{contains()}
@@ -1087,8 +1107,70 @@ pop foo from $LIST
pop foo from foo baz bar # Returns baz bar
@end example
+@node @samp{sepchar()}
+@subsection @samp{sepchar()}
+
+This function can be used to separate characters from the given string without
+resorting to external resources.
+
+@example
+sepchar mystring
+# Prints:
+# m
+# y
+# s
+# t
+# r
+# i
+# n
+# g
+@end example
+
+@node Portability functions
+@section Portability functions
+
+These helper functions are used so that we don't depend on non-POSIX programs for
+certain functionality. They are prefixed with the @samp{_} character.
+
+@menu
+* @samp{_seq()}:: 'seq(1)' but no newline
+* @samp{_stat()}:: 'stat %U' replacement
+* @samp{_readlinkf()}:: 'readlink -f' replacement
+@end menu
+
+@node @samp{_seq()}
+@subsection @samp{_seq()}
+
+This function is similar to @samp{seq(1)} except that it only takes a single argument
+and doesn't print any newlines. It is suitable to be used in @samp{for} loops.
+
+@example
+_seq 5
+# Prints:
+# 1 2 3 4 5
+@end example
+
+@node @samp{_stat()}
+@subsection @samp{_stat()}
+
+This function imitates @samp{stat %U}. @samp{stat} isn't defined by POSIX, and this is
+also a GNU extension. This function returns the owner of a file. If the owner
+cannot be found, it will return @samp{root}.
+
+@node @samp{_readlinkf()}
+@subsection @samp{_readlinkf()}
+
+This function was taken from @uref{https://github.com/ko1nksm/readlinkf, POSIX sh readlinkf library by Koichi Nakashima}.
+@samp{readlink} is also not defined by POSIX, so this function uses @samp{ls} to follow
+symbolic links until it reaches the actual file.
+
@node System Functions
-@section System Functions
+@section @strong{TODO} System Functions
+
+@itemize
+@item
+Add description
+@end itemize
@menu
* @samp{as_root()}:: Run a command as the root user
@@ -1104,6 +1186,8 @@ following order:
@enumerate
@item
+@samp{sls}
+@item
@samp{sudo}
@item
@samp{doas}
@@ -1111,4 +1195,61 @@ following order:
@samp{su}
@end enumerate
+@node Package Functions
+@section @strong{TODO} Package Functions
+
+Obviously, package functions are the most important ones for @samp{cpt-lib}, those
+are the ones you will use to build, to query, to manipulate, or to otherwise
+interact with packages.
+
+@menu
+* @samp{pkg_owner()}:: Check which package owns the given file
+* @samp{pkg_isbuilt()}:: Check whether the given package is built
+* @samp{pkg_lint()}:: Check whether a package directory fits the standards
+* @samp{pkg_find()}:: Query package locations
+* @samp{pkg_gentree}:: Generate a dependency tree for the given package
+@end menu
+
+@node @samp{pkg_owner()}
+@subsection @samp{pkg_owner()}
+
+This function can be used to determine the owner of a package. The first
+argument is used for flags that will be passed to @samp{grep}, and the second one is
+for the file query. Rest of the arguments can be used in order to specify the
+manifests to be used, but it is optional. @samp{pkg_owner()} will search for all the
+installed packages if no other arguments are given.
+
+@example
+# Example
+pkg_owner -lFx /usr/bin/grep # Returns 'busybox'
+
+# An example call made by `pkg_fix_deps()` to figure out whether the built
+# package contains the file it depends.
+pkg_owner -l "/$@{dep#/@}\$" "$PWD/manifest" >/dev/null && continue
+pkg_owner -l "/$@{dep#/@}\$" "$@@" ||:
+@end example
+
+@node @samp{pkg_isbuilt()}
+@subsection @samp{pkg_isbuilt()}
+
+This function returns with success when the given package has a built tarball
+with the matching version and release strings from the repository.
+
+@node @samp{pkg_lint()}
+@subsection @samp{pkg_lint()}
+
+This function checks whether a given package fits the proper package
+specification. This function @strong{does not return with failure, it exits outright}
+if it fails.
+
+@node @samp{pkg_find()}
+@subsection @strong{TODO} @samp{pkg_find()}
+
+@samp{pkg_find()}
+
+@node @samp{pkg_gentree}
+@subsection @strong{TODO} @samp{pkg_gentree}
+
+Keep in mind /etc/cpt-base
+
@bye \ No newline at end of file
diff --git a/docs/default.do b/docs/default.do
new file mode 100644
index 0000000..fcf8802
--- /dev/null
+++ b/docs/default.do
@@ -0,0 +1,33 @@
+SRC_ROOT=..
+. ../config.rc
+
+# Extensionless name of file
+fn="${1%.*}"
+
+case "$1" in
+ all) redo info ;;
+ allclean) redo ../clean; rm -f cpt.texi ;;
+ info) redo-ifchange cpt.info cpt.texi cpt.org ;;
+ *.info)
+ # Don't bother if makeinfo doesn't exist on the system, exit with success.
+ if ! command -v $MAKEINFO >/dev/null; then
+ PHONY
+ exit 0
+ fi
+ redo-ifchange "$fn.texi"
+ $MAKEINFO "$fn.texi" -o "$3"
+ ;;
+ *.texi)
+ [ -f "$fn.org" ] || exit 0
+ redo-ifchange "$fn.org"
+ cp "$fn.org" "$3.org"
+ $EMACS "$3.org" --batch -f org-texinfo-export-to-texinfo
+ rm -f "$3.org"
+ mv "$3.texi" "$3"
+ ;;
+ *)
+ echo "Unknown target $1"
+ exit 99
+esac
+
+PHONY all info html
diff --git a/install.do b/install.do
new file mode 100644
index 0000000..dcab154
--- /dev/null
+++ b/install.do
@@ -0,0 +1,17 @@
+. ./config.rc
+redo all
+PHONY
+
+INSTALLSH=./tools/install.sh
+
+# Install executables.
+"$INSTALLSH" -Dm755 -t "$DESTDIR$BINDIR" $(getbin)
+
+# Install manual pages.
+"$INSTALLSH" -Dm644 -t "$DESTDIR$MAN1" man/*.1
+
+# Install the documentation info page.
+# We don't want to bother if the info page wasn't created, just exit without an
+# error.
+[ -f docs/cpt.info ] || exit 0
+"$INSTALLSH" -Dm644 docs/cpt.info "$DESTDIR$INFODIR/cpt.info"
diff --git a/lib.rc b/lib.rc
new file mode 100644
index 0000000..92dc216
--- /dev/null
+++ b/lib.rc
@@ -0,0 +1,60 @@
+# -*- mode: redo -*-
+# Helper functions
+target=$1 basename=$2 dest=$3
+
+# Make all targets dependent on the library and the config file
+redo-ifchange "$SRC_ROOT/lib.rc" "$SRC_ROOT/config.rc"
+
+setv() {
+ # Set variables if unset. Works similar to the Makefile syntax.
+ [ "$3" ] || {
+ printf '%s\n' "Faulty variable syntax" >&2
+ exit 1
+ }
+ var=$1; sym=$2; shift 2
+ case "$sym" in
+ \?=|=) eval "[ \"\$$var\" ]" || export "$var=$*" ;;
+ +=) eval "export \"$var=\$$var $*\""
+ esac
+}
+
+redo_clean() {
+ # Clean function for various redo implementations
+ [ -r .do_built ] && {
+ while read -r file; do
+ [ -d "$file" ] || rm -f "$file"
+ done < .do_built
+ }
+ find . -type f \( -name '*.tmp' -o -name '*.did' -o -name '.dep*' -o -name '.target*' \) \
+ -exec rm -f -- {} +
+ [ "$DO_BUILT" ] || find . -name '.do_built*' -exec rm -rf -- {} +
+ [ "$REDO_BASE" ] || find . -name .redo -type d -exec rm -rf -- {} +
+}
+
+targcheck() {
+ # Usage: targcheck [target...]
+ #
+ # Check if current target is one of the given arguments of this function.
+ # Returns 0 if target is one of the arguments, returns 1 if not.
+ case " $* " in *" $target "*) return 0; esac; return 1
+}
+
+PHONY() {
+ # Usage: PHONY [[target...]]
+ #
+ # Function that resembles the .PHONY: target on the classic 'make' system.
+ # You can either use it without an argument on a single target, or specify
+ # multiple targets.
+ if [ -z "$1" ] || targcheck "$@"; then
+ # shellcheck disable=2064
+ trap "rm -f $dest" EXIT INT
+ fi
+}
+
+getbin() {
+ # Function to get all executables
+ find src contrib \( -name cpt -o -name 'cpt-*' \) ! -name '*.in' ! -name '*.did'
+}
+
+# Phony targets
+PHONY all dist clean install uninstall test
diff --git a/man/cpt-contrib.1 b/man/cpt-contrib.1
index 8a34364..ffc5e54 100644
--- a/man/cpt-contrib.1
+++ b/man/cpt-contrib.1
@@ -88,11 +88,6 @@ can be used to check if personal packages are outdated.
<file>
Checks which package has installed the given file.
-.SH CPT-READLINK
-.B cpt-readlink
-<file>
-
-A 'readlink -f' replacement to be used inside the package manager.
.SH CPT-REPODEPENDS
.B cpt-repodepends
<pkg>
@@ -112,11 +107,6 @@ Prints the packages that depend on the given package. (Reverse dependencies)
<pkg>
Prints the given package's size, and its individual files.
-.SH CPT-STAT
-.B cpt-stat
-<file>
-
-Outputs the owner name of a file/directory
.SH CPT-WHICH
.B cpt-which
<pkg>
diff --git a/src/Makefile b/src/Makefile
deleted file mode 100644
index aae4e59..0000000
--- a/src/Makefile
+++ /dev/null
@@ -1,4 +0,0 @@
-test:
- shellcheck -x -f gcc ./cpt* ../contrib/*
-
-.PHONY: test
diff --git a/src/cpt b/src/cpt
index ce4c70f..ee4a328 100755
--- a/src/cpt
+++ b/src/cpt
@@ -16,11 +16,9 @@ case "$arg" in
done
for path; do
- # These are the files to be ignored.
- contains "lib readlink stat" "$path" && continue
-
printf "%b->%b %-${max}s " "$colorb" "$colre" "${path#*/cpt-}"
- sed -n 's/^# *//;2p' "$(command -v "cpt-$path")"
+ awk 'NR==2{if(/^# /){sub(/^# */,"");print}else print "";exit}' \
+ "$(command -v "cpt-$path")"
done | sort -uk1 >&2
exit
;;
diff --git a/src/cpt-build b/src/cpt-build
index d67f1a6..cb93949 100755
--- a/src/cpt-build
+++ b/src/cpt-build
@@ -10,7 +10,7 @@ parser_definition() {
if [ -f ./cpt-lib ]; then . ./cpt-lib; else . cpt-lib; fi
-[ "$1" ] || set -- "${PWD##*/}"; export CPT_PATH=${PWD%/*}:$CPT_PATH
+[ "$1" ] || { set -- "${PWD##*/}"; export CPT_PATH=${PWD%/*}:$CPT_PATH ;}
create_cache
diff --git a/src/cpt-checksum b/src/cpt-checksum
index 0a83ae0..cd1fa4e 100755
--- a/src/cpt-checksum
+++ b/src/cpt-checksum
@@ -22,8 +22,7 @@ for pkg; do
tee "$repo_dir/checksums"
else
log "$pkg" "Need permissions to generate checksums"
-
- user=$(cpt-stat "$repo_dir") as_root tee "$repo_dir/checksums"
+ user=$(_stat "$repo_dir") as_root tee "$repo_dir/checksums"
fi
}
diff --git a/src/cpt-install b/src/cpt-install
index b046e02..aab70ea 100755
--- a/src/cpt-install
+++ b/src/cpt-install
@@ -10,7 +10,7 @@ parser_definition() {
if [ -f ./cpt-lib ]; then . ./cpt-lib; else . cpt-lib; fi
-[ "$1" ] || set -- "${PWD##*/}"; export CPT_PATH=${PWD%/*}:$CPT_PATH
+[ "$1" ] || { set -- "${PWD##*/}"; export CPT_PATH=${PWD%/*}:$CPT_PATH ;}
[ -w "$CPT_ROOT/" ] || [ "$uid" = 0 ] || {
as_root "$0" "$@"
diff --git a/src/cpt-lib b/src/cpt-lib.in
index 6afd030..7728ae8 100644
--- a/src/cpt-lib
+++ b/src/cpt-lib.in
@@ -1,4 +1,5 @@
#!/bin/sh -ef
+# @DOCSTRING@
# shellcheck source=/dev/null
#
# This is the Carbs Packaging Toolchain written for Carbs Linux.
@@ -8,7 +9,7 @@
# Currently maintained by Cem Keylan.
version() {
- log "Carbs Packaging Tools" 5.1.1
+ log "Carbs Packaging Tools" @VERSION@
exit 0
}
@@ -44,6 +45,72 @@ trap_set() {
esac
}
+sepchar() (
+ # Seperate every character on the given string without resorting to external
+ # processes.
+ [ "$1" ] || return 0; str=$1; set --
+ while [ "$str" ]; do
+ str_tmp=$str
+ for i in $(_seq $(( ${#str} - 1 ))); do
+ str_tmp=${str_tmp%?}
+ done
+ set -- "$@" "$str_tmp"
+ str=${str#$str_tmp}
+ done
+ printf '%s\n' "$@"
+)
+
+_seq() (
+ # Pure shell counter meant to be used in 'for' loops.
+ i=0 buf=''
+ while [ "$(( i += 1 ))" -le "$1" ]; do
+ buf="$buf $i "
+ done
+ printf '%s' "$buf"
+)
+
+_stat() (
+ _user=; eval set -- "$(ls -ld "$1")"
+ id -u "${_user:=$3}" >/dev/null 2>&1 || _user=root
+ printf '%s' "$_user"
+)
+
+_readlinkf() (
+ # Public domain POSIX sh readlink function by Koichi Nakashima
+ [ "${1:-}" ] || return 1
+ max_symlinks=40
+ CDPATH='' # to avoid changing to an unexpected directory
+
+ target=$1
+ [ -e "${target%/}" ] || target=${1%"${1##*[!/]}"} # trim trailing slashes
+ [ -d "${target:-/}" ] && target="$target/"
+
+ cd -P . 2>/dev/null || return 1
+ while [ "$max_symlinks" -ge 0 ] && max_symlinks=$((max_symlinks - 1)); do
+ if [ ! "$target" = "${target%/*}" ]; then
+ case $target in
+ /*) cd -P "${target%/*}/" 2>/dev/null || break ;;
+ *) cd -P "./${target%/*}" 2>/dev/null || break ;;
+ esac
+ target=${target##*/}
+ fi
+
+ if [ ! -L "$target" ]; then
+ target="${PWD%/}${target:+/}${target}"
+ printf '%s\n' "${target:-/}"
+ return 0
+ fi
+
+ # `ls -dl` format: "%s %u %s %s %u %s %s -> %s\n",
+ # <file mode>, <number of links>, <owner name>, <group name>,
+ # <size>, <date and time>, <pathname of link>, <contents of link>
+ # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html
+ link=$(ls -dl -- "$target" 2>/dev/null) || break
+ target=${link#*" $target -> "}
+ done
+ return 1
+)
+
# This is the public domain getoptions shell library. It also forms a usage
# function.
# URL: https://github.com/ko1nksm/getoptions (v2.0.1)
@@ -346,7 +413,7 @@ as_root() {
"$@"
case ${su##*/} in
- sudo|doas) "$su" -u "$user" -- env "$@" ;;
+ sls|sudo|doas) "$su" -u "$user" -- env "$@" ;;
su) su -c "env $* <&3" "$user" 3<&0 </dev/tty ;;
*) die "Invalid CPT_SU value: $su" ;;
esac
@@ -706,16 +773,29 @@ pkg_depends() {
(pkg_list "$1" >/dev/null) && return
while read -r dep type || [ "$dep" ]; do
+ # Skip comments and empty lines.
+ [ "${dep##\#*}" ] || continue
# Skip test dependencies unless $CPT_TEST is set to 1.
- case $type in test) [ "$CPT_TEST" = 1 ] || continue; esac
+ #
+ # Skip make dependencies on the 'tree' operation for child packages
+ # or when the 'first-nomake' argument is given.
+ case $type in
+ test) [ "$CPT_TEST" = 1 ] || continue ;;
+ make) [ "$2" = tree ] && [ -z "${3#first-nomake}" ] && continue
+ esac
- # Recurse through the dependencies of the child packages.
- [ "${dep##\#*}" ] && pkg_depends "$dep"
+ # Recurse through the dependencies of the child packages. Forward
+ # the 'tree' operation.
+ if [ "$2" = tree ]; then
+ pkg_depends "$dep" tree
+ else
+ pkg_depends "$dep"
+ fi
done 2>/dev/null < "$(pkg_find "$1")/depends" ||:
# After child dependencies are added to the list,
# add the package which depends on them.
- [ "$2" = explicit ] || deps="$deps $1 "
+ [ "$2" = explicit ] || [ "$3" ] || deps="$deps $1 "
}
}
@@ -1019,6 +1099,11 @@ pkg_build() {
diff -U 3 "$pkg_build" .build.cpt > "$pkg_build.diff" &&
rm -f "$pkg_build.diff"
+ # We don't want the package manager to track 'dir' pages of the info
+ # directory. We don't want every single package to create their own dir
+ # files either.
+ rm -f "$pkg_dir/$pkg/usr/share/info/dir"
+
# We never ever want this. Let's end the endless conflicts
# and remove it.
find "$pkg_dir/$pkg" -name charset.alias -exec rm -f {} +
@@ -1145,14 +1230,11 @@ pkg_conflicts() {
# Use $CPT_ROOT in filename so that we follow its symlinks.
file=$CPT_ROOT/${file#/}
- # We will only follow the symlinks of the directories, so we
- # reserve the directory name in this 'dirname' value. cpt-readlink
- # functions in a similar fashion to 'readlink -f', it makes sure
- # every component except for the first one to be available on
- # the directory structure. If we cannot find it in the system,
- # we don't need to make this much more complex by trying so
- # hard to find it. Simply use the original directory name.
- dirname="$(cpt-readlink "${file%/*}" 2>/dev/null)" ||
+ # We will only follow the symlinks of the directories, so we reserve the
+ # directory name in this 'dirname' value. If we cannot find it in the
+ # system, we don't need to make this much more complex by trying so hard
+ # to find it. Simply use the original directory name.
+ dirname="$(_readlinkf "${file%/*}" 2>/dev/null)" ||
dirname="${file%/*}"
@@ -1577,8 +1659,7 @@ pkg_fetch() {
# ownership of files and directories in the rare
# case that the repository is owned by a 3rd user.
(
- user=$(cpt-stat "$PWD") || user=root
- id -u "$user" >/dev/null 2>&1 || user=root
+ user=$(_stat "$PWD")
[ "$user" = root ] ||
log "Dropping permissions to $user for pull"
@@ -1614,8 +1695,7 @@ pkg_fetch() {
# We are going to do the same operation as above, to
# find the owner of the repository.
(
- user=$(cpt-stat "$PWD") || user=root
- id -u "$user" >/dev/null 2>&1 || user=root
+ user=$(_stat "$PWD")
[ "$user" = root ] ||
log "Dropping permissions to $user for pull"
@@ -1647,8 +1727,7 @@ pkg_fetch() {
# Similar to the git update, we find the owner of
# the repository and spawn rsync as that user.
(
- user=$(cpt-stat "$PWD") || user=root
- id -u "$user" >/dev/null 2>&1 || user=root
+ user=$(_stat "$PWD")
[ "$user" = root ] ||
log "Dropping permissions to $user for pull"
@@ -1743,6 +1822,48 @@ pkg_updates(){
log "Updated all packages"
}
+pkg_get_base() (
+ # Print the packages defined in the /etc/cpt-base file.
+ # If an argument is given, it prints a space seperated list instead
+ # of a list seperated by newlines.
+
+ # cpt-base is an optional file, return with success if it doesn't exist.
+ [ -f "$CPT_ROOT/etc/cpt-base" ] || return 0
+ nonl=$1; set --
+
+ # Older versions of shellcheck warns us that the variable is changing on the
+ # subshell. That is our purpose here, thank you very much.
+ # shellcheck disable=SC2030
+ while read -r pkgname _; do
+ [ "${pkgname##\#*}" ] || continue
+ set -- "$@" "$pkgname"
+ done < "$CPT_ROOT/etc/cpt-base"
+ if [ "$nonl" ]; then printf '%s ' "$@"; else printf '%s\n' "$@"; fi
+)
+
+pkg_gentree() (
+ # Generate an ordered dependency tree of a package. Useful for testing
+ # whether the generated dependency tree is enough to actually building a
+ # given package. A second argument can be given as a combination of
+ # characters (similar to 'tar(1)' keys) which will be used as an option
+ # parser. See the documentation for more information on the keys.
+ deps='' reverse='' nonl='' make_deps=first
+ for op in $(sepchar "$2"); do
+ case "$op" in
+ b) deps="$(pkg_get_base nonl)" ;;
+ x) make_deps=first-nomake ;;
+ r) reverse=1 ;;
+ n) nonl=1 ;;
+ *) return 1
+ esac
+ done
+ pkg_depends "$1" tree "${make_deps:+first}"
+ eval set -- "$deps"
+ pkg_order "$@"
+ if [ "$reverse" ]; then eval set -- "$redro"; else eval set -- "$order"; fi
+ if [ "$nonl" ]; then printf '%s ' "$@"; else printf '%s\n' "$@"; fi
+)
+
pkg_clean() {
# Clean up on exit or error. This removes everything related
# to the build.
@@ -1763,10 +1884,14 @@ create_cache() {
#
# Create the required temporary directories and set the variables
# which point to them.
- mkdir -p "${CPT_TMPDIR:=$cac_dir/proc}" \
- "${mak_dir:=$CPT_TMPDIR/$pid/build}" \
- "${pkg_dir:=$CPT_TMPDIR/$pid/pkg}" \
- "${tar_dir:=$CPT_TMPDIR/$pid/export}"
+ mkdir -p "${tmp_dir:=${CPT_TMPDIR:=$cac_dir/proc}/$pid}"
+
+ # If an argument is given, skip the creation of other cache directories.
+ # This here makes shellcheck extremely angry, so I am globally disabling
+ # SC2119.
+ [ "$1" ] || mkdir -p "${mak_dir:=$tmp_dir/build}" \
+ "${pkg_dir:=$tmp_dir/pkg}" \
+ "${tar_dir:=$tmp_dir/export}"
}
@@ -1821,7 +1946,9 @@ create_cache() {
# Figure out which 'sudo' command to use based on the user's choice or
# what is available on the system.
- su=${CPT_SU:-$(command -v sudo || command -v doas)} || su=su
+ su=${CPT_SU:-$(command -v sls ||
+ command -v sudo ||
+ command -v doas)} || su=su
# Store the date and time of script invocation to be used as the name
# of the log files the package manager creates uring builds.
@@ -1872,3 +1999,15 @@ create_cache() {
fi
}
+
+# If the library is being called with its own name, run arguments.
+if [ "${0##*/}" = cpt-lib ]; then
+ pd() {
+ setup REST help:usage -- "usage: ${0##*/} [funcall...]"
+ global_options
+ }
+ eval "$(getoptions pd parse "$0")"
+ parse "$@"
+ eval set -- "$REST"
+ "$@"
+fi
diff --git a/src/cpt-list b/src/cpt-list
index a161abf..363e22f 100755
--- a/src/cpt-list
+++ b/src/cpt-list
@@ -5,11 +5,23 @@ parser_definition() {
setup REST help:usage -- "usage: ${0##*/} [-c] [pkg...]"
msg -- '' 'Options:'
flag CURRENT -c --current -- "Use the current directory as a package"
+ param PKG --check label:" --check PKG TRUE FALSE" -- \
+ "Check if PKG exists and return the string of TRUE if"\
+ "it exists, and the string of FALSE if it doesn't." \
+ "Useful for optional packaging."
global_options
}
if [ -f ./cpt-lib ]; then . ./cpt-lib; else . cpt-lib; fi
+if [ "$PKG" ]; then
+ if pkg_list "$PKG" >/dev/null 2>&1; then
+ printf %s "$1"
+ else
+ printf %s "$2"
+ fi
+else
[ "$CURRENT" ] && set -- "${PWD##*/}"
pkg_list "$@"
+fi
diff --git a/src/cpt-remove b/src/cpt-remove
index bcf5047..cce3739 100755
--- a/src/cpt-remove
+++ b/src/cpt-remove
@@ -10,7 +10,7 @@ parser_definition() {
if [ -f ./cpt-lib ]; then . ./cpt-lib; else . cpt-lib; fi
-[ "$1" ] || set -- "${PWD##*/}"; export CPT_PATH=${PWD%/*}:$CPT_PATH
+[ "$1" ] || { set -- "${PWD##*/}"; export CPT_PATH=${PWD%/*}:$CPT_PATH ;}
[ -w "$CPT_ROOT/" ] || [ "$uid" = 0 ] || {
as_root "$0" "$@"
diff --git a/src/test.do b/src/test.do
new file mode 100644
index 0000000..159cece
--- /dev/null
+++ b/src/test.do
@@ -0,0 +1,7 @@
+SRC_ROOT=..
+. ${SRC_ROOT}/config.rc
+
+redo-ifchange cpt-lib
+exec >&2
+find . ../contrib -name 'cpt-*' ! -name '*.*' -exec shellcheck -e 2119 -x -f gcc {} +
+PHONY
diff --git a/tools/do b/tools/do
new file mode 100755
index 0000000..f38a2a7
--- /dev/null
+++ b/tools/do
@@ -0,0 +1,446 @@
+#!/bin/sh
+#
+# A minimal alternative to djb redo that doesn't support incremental builds.
+# For the full version, visit http://github.com/apenwarr/redo
+#
+# The author disclaims copyright to this source file and hereby places it in
+# the public domain. (2010 12 14; updated 2019 02 24)
+#
+USAGE="
+usage: do [-d] [-x] [-v] [-c] <targets...>
+ -d print extra debug messages (mostly about dependency checks)
+ -v run .do files with 'set -v'
+ -x run .do files with 'set -x'
+ -c clean up all old targets before starting
+
+ Note: do is an implementation of redo that does *not* check dependencies.
+ It will never rebuild a target it has already built, unless you use -c.
+"
+
+# CDPATH apparently causes unexpected 'cd' output on some platforms.
+unset CDPATH
+
+# By default, no output coloring.
+green=""
+bold=""
+plain=""
+
+if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then
+ green="$(printf '\033[32m')"
+ bold="$(printf '\033[1m')"
+ plain="$(printf '\033[m')"
+fi
+
+# The 'seq' command is not available on all platforms.
+_seq() {
+ local x=0 max="$1"
+ while [ "$x" -lt "$max" ]; do
+ x=$((x + 1))
+ echo "$x"
+ done
+}
+
+# Split $1 into a dir part ($_dirsplit_dir) and base filename ($_dirsplit_base)
+_dirsplit() {
+ _dirsplit_base=${1##*/}
+ _dirsplit_dir=${1%$_dirsplit_base}
+}
+
+# Like /usr/bin/dirname, but avoids a fork and uses _dirsplit semantics.
+qdirname() (
+ _dirsplit "$1"
+ dir=${_dirsplit_dir%/}
+ echo "${dir:-.}"
+)
+
+_dirsplit "$0"
+REDO=$(cd "$(pwd -P)" &&
+ cd "${_dirsplit_dir:-.}" &&
+ echo "$PWD/$_dirsplit_base")
+export REDO
+_cmd=$_dirsplit_base
+
+DO_TOP=
+if [ -z "$DO_BUILT" ]; then
+ export _do_opt_debug=
+ export _do_opt_exec=
+ export _do_opt_verbose=
+ export _do_opt_clean=
+fi
+while getopts 'dxvcj:h?' _opt; do
+ case $_opt in
+ d) _do_opt_debug=1 ;;
+ x) _do_opt_exec=x ;;
+ v) _do_opt_verbose=v ;;
+ c) _do_opt_clean=1 ;;
+ j) ;; # silently ignore, for compat with real redo
+ \?|h|*) printf "%s" "$USAGE" >&2
+ exit 99
+ ;;
+ esac
+done
+shift "$((OPTIND - 1))"
+_debug() {
+ [ -z "$_do_opt_debug" ] || echo "$@" >&2
+}
+
+if [ -z "$DO_BUILT" -a "$_cmd" != "redo-whichdo" ]; then
+ DO_TOP=1
+ if [ "$#" -eq 0 ] && [ "$_cmd" = "do" -o "$_cmd" = "redo" ]; then
+ set all # only toplevel redo has a default target
+ fi
+ export DO_STARTDIR="$(pwd -P)"
+ # If starting /bin/pwd != $PWD, this will fix it.
+ # That can happen when $PWD contains symlinks that the shell is
+ # trying helpfully (but unsuccessfully) to hide from the user.
+ cd "$DO_STARTDIR" || exit 99
+ export DO_BUILT="$PWD/.do_built"
+ if [ -z "$_do_opt_clean" -a -e "$DO_BUILT" ]; then
+ echo "do: Incremental mode. Use -c for clean rebuild." >&2
+ fi
+ : >>"$DO_BUILT"
+ sort -u "$DO_BUILT" >"$DO_BUILT.new"
+ while read f; do
+ [ -n "$_do_opt_clean" ] && printf "%s\0%s.did\0" "$f" "$f"
+ printf "%s.did.tmp\0" "$f"
+ done <"$DO_BUILT.new" |
+ xargs -0 rm -f 2>/dev/null
+ mv "$DO_BUILT.new" "$DO_BUILT"
+ export DO_PATH="$DO_BUILT.dir"
+ export PATH="$DO_PATH:$PATH"
+ rm -rf "$DO_PATH"
+ mkdir "$DO_PATH"
+ for d in redo redo-ifchange redo-whichdo; do
+ ln -s "$REDO" "$DO_PATH/$d"
+ done
+ for d in redo-ifcreate redo-stamp redo-always redo-ood \
+ redo-targets redo-sources; do
+ echo "#!/bin/sh" >"$DO_PATH/$d"
+ chmod a+rx "$DO_PATH/$d"
+ done
+fi
+
+
+# Chop the "file" part off a /path/to/file pathname.
+# Note that if the filename already ends in a /, we just remove the slash.
+_updir()
+{
+ local v="${1%/*}"
+ [ "$v" != "$1" ] && echo "$v"
+ # else "empty" which means we went past the root
+}
+
+
+# Returns true if $1 starts with $2.
+_startswith()
+{
+ [ "${1#"$2"}" != "$1" ]
+}
+
+
+# Returns true if $1 ends with $2.
+_endswith()
+{
+ [ "${1%"$2"}" != "$1" ]
+}
+
+
+# Prints $1 if it's absolute, or $2/$1 if $1 is not absolute.
+_abspath()
+{
+ local here="$2" there="$1"
+ if _startswith "$1" "/"; then
+ echo "$1"
+ else
+ echo "$2/$1"
+ fi
+}
+
+
+# Prints $1 as a path relative to $PWD (not starting with /).
+# If it already doesn't start with a /, doesn't change the string.
+_relpath()
+{
+ local here="$2" there="$1" out= hadslash=
+ #echo "RP start '$there' hs='$hadslash'" >&2
+ _startswith "$there" "/" || { echo "$there" && return; }
+ [ "$there" != "/" ] && _endswith "$there" "/" && hadslash=/
+ here=${here%/}/
+ while [ -n "$here" ]; do
+ #echo "RP out='$out' here='$here' there='$there'" >&2
+ [ "${here%/}" = "${there%/}" ] && there= && break;
+ [ "${there#$here}" != "$there" ] && break
+ out=../$out
+ _dirsplit "${here%/}"
+ here=$_dirsplit_dir
+ done
+ there=${there#$here}
+ if [ -n "$there" ]; then
+ echo "$out${there%/}$hadslash"
+ else
+ echo "${out%/}$hadslash"
+ fi
+}
+
+
+# Prints a "normalized relative" path, with ".." resolved where possible.
+# For example, a/b/../c will be reduced to just a/c.
+_normpath()
+(
+ local path="$1" relto="$2" out= isabs=
+ #echo "NP start '$path'" >&2
+ if _startswith "$path" "/"; then
+ isabs=1
+ else
+ path="${relto%/}/$path"
+ fi
+ set -f
+ IFS=/
+ for d in ${path%/}; do
+ #echo "NP out='$out' d='$d'" >&2
+ if [ "$d" = ".." ]; then
+ out=$(_updir "${out%/}")/
+ else
+ out=$out$d/
+ fi
+ done
+ #echo "NP out='$out' (done)" >&2
+ out=${out%/}
+ if [ -n "$isabs" ]; then
+ echo "${out:-/}"
+ else
+ _relpath "${out:-/}" "$relto"
+ fi
+)
+
+
+# Prints a "real" path, with all symlinks resolved where possible.
+_realpath()
+{
+ local path="$1" relto="$2" isabs= rest=
+ if _startswith "$path" "/"; then
+ isabs=1
+ else
+ path="${relto%/}/$path"
+ fi
+ (
+ for d in $(_seq 100); do
+ #echo "Trying: $PWD--$path" >&2
+ if cd -P "$path" 2>/dev/null; then
+ # success
+ pwd=$(pwd -P)
+ #echo " chdir ok: $pwd--$rest" >&2
+ np=$(_normpath "${pwd%/}/$rest" "$relto")
+ if [ -n "$isabs" ]; then
+ echo "$np"
+ else
+ _relpath "$np" "$relto"
+ fi
+ break
+ fi
+ _dirsplit "${path%/}"
+ path=$_dirsplit_dir
+ rest="$_dirsplit_base/$rest"
+ done
+ )
+}
+
+
+# List the possible names for default*.do files in dir $1 matching the target
+# pattern in $2. We stop searching when we find the first one that exists.
+_find_dofiles_pwd()
+{
+ local dodir="$1" dofile="$2"
+ _startswith "$dofile" "default." || dofile=${dofile#*.}
+ while :; do
+ dofile=default.${dofile#default.*.}
+ echo "$dodir$dofile"
+ [ -e "$dodir$dofile" ] && return 0
+ [ "$dofile" = default.do ] && break
+ done
+ return 1
+}
+
+
+# List the possible names for default*.do files in $PWD matching the target
+# pattern in $1. We stop searching when we find the first name that works.
+# If there are no matches in $PWD, we'll search in .., and so on, to the root.
+_find_dofiles()
+{
+ local target="$1" dodir= dofile= newdir=
+ _debug "find_dofile: '$PWD' '$target'"
+ dofile="$target.do"
+ echo "$dofile"
+ [ -e "$dofile" ] && return 0
+
+ # Try default.*.do files, walking up the tree
+ _dirsplit "$dofile"
+ dodir=$_dirsplit_dir
+ dofile=$_dirsplit_base
+ [ -n "$dodir" ] && dodir=${dodir%/}/
+ [ -e "$dodir$dofile" ] && return 0
+ for i in $(_seq 100); do
+ [ -n "$dodir" ] && dodir=${dodir%/}/
+ #echo "_find_dofiles: '$dodir' '$dofile'" >&2
+ _find_dofiles_pwd "$dodir" "$dofile" && return 0
+ newdir=$(_realpath "${dodir}.." "$PWD")
+ [ "$newdir" = "$dodir" ] && break
+ dodir=$newdir
+ done
+ return 1
+}
+
+
+# Print the last .do file returned by _find_dofiles.
+# If that file exists, returns 0, else 1.
+_find_dofile()
+{
+ local files="$(_find_dofiles "$1")"
+ rv=$?
+ #echo "files='$files'" >&2
+ [ "$rv" -ne 0 ] && return $rv
+ echo "$files" | {
+ while read -r linex; do line=$linex; done
+ printf "%s\n" "$line"
+ }
+}
+
+
+# Actually run the given $dofile with the arguments in $@.
+# Note: you should always run this in a subshell.
+_run_dofile()
+{
+ export DO_DEPTH="$DO_DEPTH "
+ export REDO_TARGET="$PWD/$target"
+ local line1
+ set -e
+ read line1 <"$PWD/$dofile" || true
+ cmd=${line1#"#!/"}
+ if [ "$cmd" != "$line1" ]; then
+ set -$_do_opt_verbose$_do_opt_exec
+ exec /$cmd "$PWD/$dofile" "$@"
+ else
+ set -$_do_opt_verbose$_do_opt_exec
+ # If $dofile is empty, "." might not change $? at
+ # all, so we clear it first with ":".
+ :; . "$PWD/$dofile"
+ fi
+}
+
+
+# Find and run the right .do file, starting in dir $1, for target $2,
+# providing a temporary output file as $3. Renames the temp file to $2 when
+# done.
+_do()
+{
+ local dir="$1" target="$1$2" tmp="$1$2.redo.tmp" tdir=
+ local dopath= dodir= dofile= ext=
+ if [ "$_cmd" = "redo" ] ||
+ ( [ ! -e "$target" -o -d "$target" ] &&
+ [ ! -e "$target.did" ] ); then
+ printf '%sdo %s%s%s%s\n' \
+ "$green" "$DO_DEPTH" "$bold" "$target" "$plain" >&2
+ dopath=$(_find_dofile "$target")
+ if [ ! -e "$dopath" ]; then
+ echo "do: $target: no .do file ($PWD)" >&2
+ return 1
+ fi
+ _dirsplit "$dopath"
+ dodir=$_dirsplit_dir dofile=$_dirsplit_base
+ if _startswith "$dofile" "default."; then
+ ext=${dofile#default}
+ ext=${ext%.do}
+ else
+ ext=
+ fi
+ target=$PWD/$target
+ tmp=$PWD/$tmp
+ cd "$dodir" || return 99
+ target=$(_relpath "$target" "$PWD") || return 98
+ tmp=$(_relpath "$tmp" "$PWD") || return 97
+ base=${target%$ext}
+ tdir=$(qdirname "$target")
+ [ ! -e "$DO_BUILT" ] || [ ! -w "$tdir/." ] ||
+ : >>"$target.did.tmp"
+ # $qtmp is a temporary file used to capture stdout.
+ # Since it might be accidentally deleted as a .do file
+ # does its work, we create it, then open two fds to it,
+ # then immediately delete the name. We use one fd to
+ # redirect to stdout, and the other to read from after,
+ # because there's no way to fseek(fd, 0) in sh.
+ qtmp=$DO_PATH/do.$$.tmp
+ (
+ rm -f "$qtmp"
+ ( _run_dofile "$target" "$base" "$tmp" >&3 3>&- 4<&- )
+ rv=$?
+ if [ $rv != 0 ]; then
+ printf "do: %s%s\n" "$DO_DEPTH" \
+ "$target: got exit code $rv" >&2
+ rm -f "$tmp.tmp" "$tmp.tmp2" "$target.did"
+ return $rv
+ fi
+ echo "$PWD/$target" >>"$DO_BUILT"
+ if [ ! -e "$tmp" ]; then
+ # if $3 wasn't created, copy from stdout file
+ cat <&4 >$tmp
+ # if that's zero length too, forget it
+ [ -s "$tmp" ] || rm -f "$tmp"
+ fi
+ ) 3>$qtmp 4<$qtmp # can't use "|| return" here...
+ # ...because "|| return" would mess up "set -e" inside the ()
+ # on some shells. Running commands in "||" context, even
+ # deep inside, will stop "set -e" from functioning.
+ rv=$?
+ [ "$rv" = 0 ] || return "$rv"
+ mv "$tmp" "$target" 2>/dev/null
+ [ -e "$target.did.tmp" ] &&
+ mv "$target.did.tmp" "$target.did" ||
+ : >>"$target.did"
+ else
+ _debug "do $DO_DEPTH$target exists." >&2
+ fi
+}
+
+
+# Implementation of the "redo" command.
+_redo()
+{
+ local i startdir="$PWD" dir base
+ set +e
+ for i in "$@"; do
+ i=$(_abspath "$i" "$startdir")
+ (
+ cd "$DO_STARTDIR" || return 99
+ i=$(_realpath "$(_relpath "$i" "$PWD")" "$PWD")
+ _dirsplit "$i"
+ dir=$_dirsplit_dir base=$_dirsplit_base
+ _do "$dir" "$base"
+ )
+ [ "$?" = 0 ] || return 1
+ done
+}
+
+
+# Implementation of the "redo-whichdo" command.
+_whichdo()
+{
+ _find_dofiles "$1"
+}
+
+
+case $_cmd in
+ do|redo|redo-ifchange) _redo "$@" ;;
+ redo-whichdo) _whichdo "$1" ;;
+ do.test) ;;
+ *) printf "do: '%s': unexpected redo command" "$_cmd" >&2; exit 99 ;;
+esac
+[ "$?" = 0 ] || exit 1
+
+if [ -n "$DO_TOP" ]; then
+ if [ -n "$_do_opt_clean" ]; then
+ echo "do: Removing stamp files..." >&2
+ [ ! -e "$DO_BUILT" ] ||
+ while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" |
+ xargs -0 rm -f 2>/dev/null
+ fi
+fi
diff --git a/tools/install.sh b/tools/install.sh
new file mode 100755
index 0000000..f79cb3b
--- /dev/null
+++ b/tools/install.sh
@@ -0,0 +1,90 @@
+#!/bin/sh -e
+# Portable install version that supports -D -m and -t
+usage() {
+ printf '%s\n' "usage: $0 [-D] [-m mode] source dest" \
+ " or: $0 [-D] [-m mode] [-t dir] [source...]" >&2
+ exit 1
+}
+
+die() { printf '%s\n' "$@" >&2; exit 1;}
+
+mkdirp=''
+target=''
+mode=''
+REST=''
+parse() {
+ OPTIND=$(($#+1))
+ while OPTARG= && [ $# -gt 0 ]; do
+ case $1 in
+ -[tm]?*) OPTARG=$1; shift
+ eval 'set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}"' ${1+'"$@"'}
+ ;;
+ -[!-]?*) OPTARG=$1; shift
+ eval 'set -- "${OPTARG%"${OPTARG#??}"}" -"${OPTARG#??}"' ${1+'"$@"'}
+ OPTARG= ;;
+ esac
+ case $1 in
+ '-D')
+ [ "${OPTARG:-}" ] && OPTARG=${OPTARG#*\=} && set "noarg" "$1" && break
+ eval '[ ${OPTARG+x} ] &&:' && OPTARG='1' || OPTARG=''
+ mkdirp="$OPTARG"
+ ;;
+ '-t')
+ [ $# -le 1 ] && set "required" "$1" && break
+ OPTARG=$2
+ target="$OPTARG"
+ shift ;;
+ '-m')
+ [ $# -le 1 ] && set "required" "$1" && break
+ OPTARG=$2
+ mode="$OPTARG"
+ shift ;;
+ '-h'|'--help')
+ usage
+ exit 0 ;;
+ --)
+ shift
+ while [ $# -gt 0 ]; do
+ REST="${REST} \"\${$((OPTIND-$#))}\""
+ shift
+ done
+ break ;;
+ [-]?*) set "unknown" "$1"; break ;;
+ *)
+ REST="${REST} \"\${$((OPTIND-$#))}\""
+ esac
+ shift
+ done
+ [ $# -eq 0 ] && { OPTIND=1; unset OPTARG; return 0; }
+ case $1 in
+ unknown) set "Unrecognized option: $2" "$@" ;;
+ noarg) set "Does not allow an argument: $2" "$@" ;;
+ required) set "Requires an argument: $2" "$@" ;;
+ pattern:*) set "Does not match the pattern (${1#*:}): $2" "$@" ;;
+ notcmd) set "Not a command: $2" "$@" ;;
+ *) set "Validation error ($1): $2" "$@"
+ esac
+ echo "$1" >&2
+ exit 1
+}
+
+parse "$@" && eval set -- "$REST"
+
+if [ "$target" ]; then
+ [ "$mkdirp" ] || [ -d "$target" ] || die "$target doesn't exist"
+ mkdir -p "$target"
+ for arg; do
+ [ -d "$target/${arg##*/}" ] && die "$target/${arg##*/} is a directory"
+ cp "$arg" "$target/${arg##*/}"
+
+ # Most implementations set the mode to 0755 by default when -t is set.
+ chmod "${mode:=0755}" "$target/${arg##*/}"
+ done
+else
+ case "$2" in */*) [ "$mkdirp" ] || [ -d "${2%/*}" ] || die "${2%/*} doesn't exist"
+ mkdir -p "${2%/*}"
+ esac
+ [ -d "$2" ] && die "$2 is a directory"
+ cp "$1" "$2"
+ chmod "${mode:=0755}" "$2"
+fi
diff --git a/uninstall.do b/uninstall.do
new file mode 100644
index 0000000..3347874
--- /dev/null
+++ b/uninstall.do
@@ -0,0 +1,14 @@
+. ./config.rc
+
+# Remove executables.
+getbin | while read -r file; do
+ rm -f "${DESTDIR}${BINDIR}/${file##*/}"
+done
+
+# Remove manual pages.
+for man in man/*.1; do
+ rm -f "${DESTDIR}${MAN1}/${man##*/}"
+done
+
+# Remove the info page.
+rm -f "${DESTDIR}${INFODIR}/cpt.info"