diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile | 49 | ||||
-rw-r--r-- | README.md | 28 | ||||
-rw-r--r-- | asp.in | 246 | ||||
-rw-r--r-- | man/asp.1.txt | 92 | ||||
-rw-r--r-- | package.inc.sh | 148 | ||||
-rw-r--r-- | remote.inc.sh | 50 |
7 files changed, 615 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e930cc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +asp +asp.1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..18f6786 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +PACKAGE_NAME = asp +VER=0 + +PREFIX = /usr/local + +BINPROGS = \ + asp + +MANPAGES = \ + man/asp.1 + +INCLUDES = \ + package.inc.sh \ + remote.inc.sh + +all: $(BINPROGS) $(MANPAGES) + +V_GEN = $(_v_GEN_$(V)) +_v_GEN_ = $(_v_GEN_0) +_v_GEN_0 = @echo " GEN " $@; + +edit = $(V_GEN) m4 -P $@.in >$@ && chmod go-w,+x $@ + +%: %.in $(INCLUDES) + $(edit) + +doc: $(MANPAGES) +man/%: man/%.txt Makefile + a2x -d manpage \ + -f manpage \ + -a manversion=$(VERSION) \ + -a manmanual="$(PACKAGE_NAME) manual" $< + +clean: + $(RM) $(BINPROGS) $(MANPAGES) + +install: all + install -dm755 $(DESTDIR)$(PREFIX)/bin + install -m755 $(BINPROGS) $(DESTDIR)$(PREFIX)/bin + +uninstall: + for f in $(BINPROGS); do $(RM) $(DESTDIR)$(PREFIX)/bin/$$f; done + $(RM) $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_archinstallscripts + +dist: + git archive --format=tar --prefix=$(PACKAGE_NAME)-$(VER)/ v$(VER) | gzip -9 > $(PACKAGE_NAME)-$(VER).tar.gz + gpg --detach-sign --use-agent $(PACKAGE_NAME)-$(VER).tar.gz + +.PHONY: all clean install uninstall dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..262e58b --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# asp + +`asp` is a tool to manage the build source files used to create Arch Linux +packages. It attempts to replace the `abs` tool, offering more up to date +sources (via the svntogit repositories) and uses a sparse checkout model to +conserve diskspace. This probably won't be interesting to users who want a +full checkout (for whatever reason that may be). + +# Setup + +None! Though, it should be noted that the **ASPROOT** environment variable +will control where `asp` keeps its locally tracked packages. By default, this +is `$HOME/asp`. + +# Examples + +Get the source files for some packages: + +~~~ +asp export pacman testing/systemd extra/pkgfile +~~~ + +List the repositories a package has been pushed to: + +~~~ +asp list-repos pacman +~~~ + @@ -0,0 +1,246 @@ +#!/bin/bash + +ARCH_GIT_REPOS=(packages community) + +OPT_ARCH=$(uname -m) +OPT_FORCE=0 +: ${ASPROOT:=$HOME/asp} + +m4_include(remote.inc.sh) +m4_include(package.inc.sh) + +log_meta() { + printf "$1 $2\n" "${@:3}" +} + +log_error() { + log_meta 'error:' "$@" >&2 +} + +log_fatal() { + log_error "$@" + exit 1 +} + +log_warning() { + log_meta 'warning:' "$@" >&2 +} + +log_info() { + log_meta '==>' "$@" +} + +map() { + local map_r=0 + for _ in "${@:2}"; do + "$1" "$_" || (( $# > 255 ? map_r=1 : ++r )) + done + return $map_r +} + +usage() { + cat<<EOF +asp [OPTIONS...] {COMMAND} ... + +Manage build sources for Arch packages. + +Options: + -a ARCH Specify an architecture other than the host's + -f Allow files to be overwritten + -h Show this help + -V Show package version + +Commands: + difflog NAME Show revision history with diffs + export NAME... Export packages + gc Cleanup and optimize the local repository + disk-usage Show amount of disk used by locally tracked packages + help Show this help + list-all List all known packages + list-arches NAME... List architectures for packages + list-local List tracked packages + list-repos NAME... List repos for packages + log NAME Show revision history + shortlog NAME Show revision history in short form + update [NAME...] Update packages (update all tracked if none specified) +EOF +} + +version() { + printf 'asp v0\n' +} + +update() { + local r + + for r in "${ARCH_GIT_REPOS[@]}"; do + log_info "updating remote '%s'" "$r" + remote_update "$r" + done +} + +update_packages() { + local refspecs=() remote pkgname + declare -A refspec_map + + for pkgname; do + package_init -n "$pkgname" remote || return 1 + refspec_map["$remote"]+=" packages/$pkgname" + done + + for remote in "${!refspec_map[@]}"; do + read -ra refspecs <<<"${refspec_map["$remote"]}" + remote_update_refs "$remote" "${refspecs[@]}" + done +} + +initialize() { + local remote + + if [[ -d .git && OPT_FORCE -eq 0 ]]; then + log_fatal 'refusing to overwrite existing repo in %s' "$ASPROOT" + fi + + git init --bare || return 1 + + for remote in "${ARCH_GIT_REPOS[@]}"; do + rm -rf "$remote" + git remote add "$remote" git://projects.archlinux.org/svntogit/"$remote".git + done + + touch .asp +} + +dump_packages() { + local remote refspecs dumpfn + + case $1 in + all) + dumpfn=remote_get_all_refs + ;; + local) + dumpfn=remote_get_tracked_refs + ;; + esac + + for remote in "${ARCH_GIT_REPOS[@]}"; do + "$dumpfn" "$remote" refspecs + if [[ $refspecs ]]; then + printf '%s\n' "${refspecs[@]/#packages/"$remote"}" + fi + done +} + +list_local() { + dump_packages 'local' +} + +list_all() { + dump_packages 'all' +} + +shortlog() { + package_log "$@" "$FUNCNAME" +} + +log() { + package_log "$@" "$FUNCNAME" +} + +difflog() { + package_log "$@" "$FUNCNAME" +} + +gc() { + git prune + git gc +} + +disk_usage() { + local usage + read usage _ < <(du -sh "$ASPROOT/objects") + + log_info 'Using %s on disk.' "$usage" +} + +umask 0022 +startdir=$PWD +cd "$ASPROOT" || log_fatal "ASPROOT ($ASPROOT) does not exist!" +[[ -f .asp ]] || initialize + +while getopts ':a:fhV' flag; do + case $flag in + a) + OPT_ARCH=$OPTARG + ;; + f) + OPT_FORCE=1 + ;; + h) + usage + exit 0 + ;; + V) + version + exit 0 + ;; + \?) + log_fatal "invalid option -- '%s'" "$OPTARG" + ;; + :) + log_fatal "option '-%s' requires an argument" "$OPTARG" + ;; + esac +done +shift $(( OPTIND - 1 )) + +action=$1 +shift + +case $action in + update) + if (( $# == 0 )); then + update + else + update_packages "$@" + fi + ;; + list-repos) + map package_get_repos "$@" + ;; + list-arches) + map package_get_arches "$@" + ;; + list-all) + list_all + ;; + list-local) + list_local + ;; + export) + map package_export "$@" + ;; + shortlog) + shortlog "$1" + ;; + log) + log "$1" + ;; + difflog) + difflog "$1" + ;; + disk-usage) + disk_usage + ;; + gc) + gc + ;; + help) + usage + exit 0 + ;; + *) + log_fatal 'unknown action: %s' "$action" + ;; +esac + diff --git a/man/asp.1.txt b/man/asp.1.txt new file mode 100644 index 0000000..3a90780 --- /dev/null +++ b/man/asp.1.txt @@ -0,0 +1,92 @@ +///// +vim:set ts=4 sw=4 syntax=asciidoc noet: +///// +asp(1) +====== + +Name +---- +asp - Manage Arch Linux build sources + +Synopsis +-------- +asp [options] command [targets...] + +Description +----------- +Manage the version-controlled sources for the build scripts used to create Arch +Linux packages. This program provides a thin wrapper over the svntogit +repositories hosted at http://projects.archlinux.org. It aims to provide a +replacement for abs which favors a sparse checkout. + +Commands +-------- +The following commands are understood: + +*difflog*:: + Show the full revision history of the target, with file diffs. + +*export*:: + Dump the build source files for each target into a directory of the + target's name in $PWD. Targets can be specified simply as 'package' to + check out the source files at HEAD, or in 'repository/package' format + to checkout the source files which were used to push the 'package' which + exists in 'repository'. + +*gc*:: + Perform housekeeping procedures on the local repo, optimizing and + compacting the repo to free disk space. + +*disk-usage*:: + Report the approximate disk usage for locally tracked packages. + +*help*:: + Display the command line usage and exit. + +*list-all*:: + List all known packages in the repositories. + +*list-arches*:: + List the architectures the given targets are available for. + +*list-local*:: + List all packages which are tracked locally. + +*list-repos*:: + List the repositories the given targets exist in. + +*log*:: + Show the revision history of the target. + +*shortlog*:: + Show a condensed revision history of the target. + +*update*:: + For each target, if the package is not known to the local repository, + attempt to track it. If the package is tracked, update the package + to the newest version. If no targets are provided, all locally known + packages will be updated. + +Options +------- +*-a* 'architecture':: + When relevant, specify an architecture other than that of the current host. + +*-f*:: + Allow files to be overwritten. + +*-h*:: + Print a short help text and exit. + +*-V*:: + Print a short version string and exit. + +Environment +----------- +*ASPROOT*:: + Determines where the metadata is stored for locally tracked packages. Defaults + to '$HOME/asp'. + +Authors +------- +Dave Reisner <d@falconindy.com> diff --git a/package.inc.sh b/package.inc.sh new file mode 100644 index 0000000..09af4b1 --- /dev/null +++ b/package.inc.sh @@ -0,0 +1,148 @@ +package_init() { + local pkgname=$1 + local do_update=1 + + if [[ $1 = -n ]]; then + do_update=0 + shift + pkgname=$1 + fi + + package_find_remote "$pkgname" "$2" || return 1 + + (( do_update )) || return 0 + + if ! remote_is_tracking "${!2}" "$pkgname"; then + package_update "$pkgname" "${!2}" || return 1 + fi +} + +package_update() { + local pkgname=$1 remote=$2 + + git fetch "$remote" "packages/$pkgname" +} + +package_find_remote() { + local pkgname=$1 out=$2 + + # fastpath, checks local caches only + for r in "${ARCH_GIT_REPOS[@]}"; do + if remote_is_tracking "$r" "$pkgname"; then + printf -v "$out" %s "$r" + return 0 + fi + done + + # slowpath, needs to talk to the remote + for r in "${ARCH_GIT_REPOS[@]}"; do + if remote_has_package "$r" "$pkgname"; then + printf -v "$out" %s "$r" + return 0 + fi + done + + log_error 'unknown package: %s' "$pkgname" + + return 1 +} + +package_log() { + local pkgname=$1 method=$2 remote + + package_init "$pkgname" remote || return + + "_package_$method" "$pkgname" "$remote" +} + +package_export() { + local pkgname=$1 remote repo arch + local mode objtype objid path + + if [[ $pkgname = */* ]]; then + IFS=/ read -r repo pkgname <<<"$pkgname" + fi + + package_init "$pkgname" remote || return 1 + + # support $repo/$pkgname syntax + if [[ $repo ]]; then + # TODO: add an --arch flag + subtree=repos/$repo-$OPT_ARCH + else + subtree=trunk + fi + + if [[ -z $(git ls-tree "$remote/packages/$pkgname" "$subtree/") ]]; then + if [[ $repo ]]; then + log_error "package '%s' not found in repo '%s-%s'" "$pkgname" "$repo" "$OPT_ARCH" + return 1 + else + log_error "package '%s' has no trunk directory!" "$pkgname" + return 1 + fi + fi + + if (( ! OPT_FORCE )); then + mkdir "$startdir/$pkgname" || return 1 + fi + + log_info 'exporting %s:%s' "$pkgname" "$subtree" + git archive --format=tar "$remote/packages/$pkgname" "$subtree/" | + bsdtar -C "$startdir" -s ",^$subtree/,$pkgname/," -xf - "$subtree/" +} + +package_get_repos_with_arch() { + local pkgname=$1 remote=$2 + local objtype path arch repo + + while read _ objtype _ path; do + [[ $objtype = tree ]] || continue + IFS=- read repo arch <<<"${path#repos/}" + printf '%s %s\n' "$repo" "$arch" + done < <(git ls-tree "$remote/packages/$pkgname" repos/) +} + +package_get_arches() { + local pkgname=$1 remote arch + declare -A arches + + package_init "$pkgname" remote || return 1 + + while read _ arch; do + arches["$arch"]=1 + done < <(package_get_repos_with_arch "$pkgname" "$remote") + + printf '%s\n' "${!arches[@]}" +} + +package_get_repos() { + local pkgname=$1 remote repo + declare -A repos + + package_init "$pkgname" remote || return 1 + + while read repo _; do + repos["$repo"]=1 + done < <(package_get_repos_with_arch "$pkgname" "$remote") + + printf '%s\n' "${!repos[@]}" +} + +_package_shortlog() { + local pkgname=$1 remote=$2 + + git log --pretty=oneline "$remote/packages/$pkgname" +} + +_package_difflog() { + local pkgname=$1 remote=$2 + + git log -p "$remote/packages/$pkgname" +} + +_package_log() { + local pkgname=$1 remote=$2 + + git log "$remote/packages/$pkgname" +} diff --git a/remote.inc.sh b/remote.inc.sh new file mode 100644 index 0000000..b125419 --- /dev/null +++ b/remote.inc.sh @@ -0,0 +1,50 @@ +remote_get_all_refs() { + local remote=$1 + + mapfile -t "$2" < <(git ls-remote "$remote" 'refs/heads/packages/*' | + awk '{ sub(/refs\/heads\//, "", $2); print $2 }') +} + +remote_has_package() { + local remote=$1 pkgname=$2 + + [[ $(git ls-remote "$remote" "$pkgname") ]] +} + +remote_is_tracking() { + local repo=$1 pkgname=$2 + + git rev-parse "$repo/packages/$pkgname" &>/dev/null +} + +remote_get_tracked_refs() { + local remote=$1 + + mapfile -t "$2" < <(git branch --remote 2>/dev/null | + awk -F'( +|/)' -v "remote=$1" \ + '$2 == remote && $3 == "packages" { print "packages/" $4 }') +} + +remote_update_refs() { + local remote=$1 refspecs=("${@:2}") + + git fetch "$remote" "${refspecs[@]}" +} + +remote_update() { + local remote=$1 refspecs + + remote_get_tracked_refs "$remote" refspecs + + # refuse to update everything + # TODO: allow this with a flag + [[ -z $refspecs ]] && return 0 + + remote_update_refs "$remote" "${refspecs[@]}" +} + +remote_get_url() { + local remote=$1 + + git ls-remote --get-url "$remote" +} |