Subject: pkg_stow
To: NetBSD Packages Technical Discussion List <tech-pkg@NetBSD.ORG>
From: Johnny C. Lam <jlam@pkgsrc.org>
List: tech-pkg
Date: 03/07/2007 16:52:36
This is a multi-part message in MIME format.
--------------040605050304020809030501
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit

I've attached a script called "pkg_stow" that does roughly what GNU stow
does, but implemented using system tools.  Probably only works right now
on NetBSD because I'm using the "readlink" command to compute the target
of a symlink.  There's probably a way to use "ls" to approximate it, but
that's not too important to me right now.  I'm not looking for
feedback... just thought I'd share something I was working on with
others that might see a use for it.

	Cheers,

	-- Johnny Lam <jlam@pkgsrc.org>

--------------040605050304020809030501
Content-Type: text/plain;
 name="pkg_stow"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="pkg_stow"

#!/bin/sh
#
# Copyright (c) 2007 Johnny C. Lam.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. Neither the name of The NetBSD Foundation nor the names of its
#    contributors may be used to endorse or promote products derived
#    from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

progname=pkg_stow

usage() {
	echo 1>&2 "usage: pkg_stow [-Dv] destdir srcdir1 [srcdir2 ...]"
	echo 1>&2 "   -D        Unstow instead of stow"
	echo 1>&2 "   -v        Be verbose about actions taken"
	echo 1>&2 "   destdir   Destination directory of stowed entries"
	echo 1>&2 "   srcdirN   Source directory of entries to stow"
	exit 1
}

stow_dir() {
	local _src _dest _pointee
	_src="$1"; _dest="$2"

	if [ ! -e $_dest ]; then
		$echo /bin/ln -s $_src $_dest
		/bin/ln -s $_src $_dest
		return 0
	fi
	if [ -d $_dest -a -h $_dest ]; then
		_pointee=`/usr/bin/readlink $_dest`
		case $_pointee in
		$_src/*)  return 0 ;;
		esac
		$echo /bin/rm $_dest
		/bin/rm $_dest
		$echo /bin/mkdir $_dest
		/bin/mkdir $_dest
		stow_dircontents $_pointee $_dest
	fi
	stow_dircontents $_src $_dest
}

stow_dircontents() {
	local _src _dest
	_src="$1"; _dest="$2"

	/bin/ls -1a $_src |
	while read _entry; do
		if [ "$_entry" = "." -o "$_entry" = ".." ]; then
			continue
		elif [ -d $_src/$_entry -a ! -h $_src/$_entry ]; then
			stow_dir $_src/$_entry $_dest/$_entry
		else
			$echo /bin/ln -s $_src/$_entry $_dest/$_entry
			/bin/ln -s $_src/$_entry $_dest/$_entry
		fi
	done
}

opt_verbose=0
opt_delete=0

while [ $# -gt 0 ]; do
	opt="$1"
	case "$opt" in
	-D|--delete)	opt_delete=1; shift ;;
	-v|--verbose)	opt_verbose=1; shift ;;
	--)		shift; break ;;
	-*)		usage ;;
	*)		break ;;
	esac
done

# We require at least two more arguments: the srcdir and the destdir.
[ $# -ge 2 ] || usage

case $opt_verbose in
1|yes)	echo="echo" ;;
*)	echo=: ;;
esac

# The first required argument is the destdir.
pwd=`/bin/pwd`
case "$1" in
/*)	dest="$1" ;;
*)	dest="$pwd/$1" ;;
esac
shift

# The remaining arguments are package directories to stow.
for srcdir; do
	case "$srcdir" in
	/*)	src="$srcdir" ;;
	*)	src="$pwd/$srcdir" ;;
	esac
	case $opt_delete in
	0|no)
		if [ ! -d "$dest" ]; then
			$echo /bin/mkdir -p $dest
			/bin/mkdir -p $dest || exit 1
		fi
		stow_dircontents "$src" "$dest"
		;;

	1|yes)
		/usr/bin/find "$dest" -type d | /usr/bin/sort -r |
		/usr/bin/awk -v src="$src" -v dest="$dest" \
		    -v opt_verbose="$opt_verbose" '
		{
			# For each directory, inspect each symlink in the
			# directory.  If it points within $src, then remove
			# it.  If the remaining links point within the same
			# parent directory, and the directory only contains
			# those symlinks, then remove the symlinks and
			# create a symlink to that parent directory in
			# place of the existing directory.
			#

			dir = $0

			# Count the number of entries in the directory (we
			# substract two to deal with . and ..)
			#
			cmd = "/bin/ls -1a " dir " | /usr/bin/wc -l"
			cmd | getline nentries
			close(cmd)
			nentries -= 2

			# Keep track of whether this directory was already
			# empty at the start.  We purposely avoid removing
			# these directories during tree-folding.
			#
			if (nentries == 0)
				is_empty = 1
			else
				is_empty = 0

			cmd = "/usr/bin/find " dir " -type l -maxdepth 1"
			nparents = 0
			nsymlinks = 0
			while (cmd | getline entry) {
				readlink_cmd = "/usr/bin/readlink " entry
				readlink_cmd | getline pointee

				if (match(pointee, "^" src "\/") != 0) {
					rm_cmd = "/bin/rm " entry
					if (opt_verbose) print rm_cmd
					system(rm_cmd)
					nentries--
					continue
				}

				# Save the symlinks and their parents for
				# later inspection to perform tree-folding.
				#
				parent = pointee; sub("\/[^\/]*$", "", parent)
				parents[parent]++
				if (parents[parent] == 1)
					nparents++
				symlinks[entry] = entry
				nsymlinks++
			}
			close(cmd)

			# Tree-fold
			if ((dir != dest) && (is_empty == 0) && \
			    (nparents == 1)) {
				if (nsymlinks == nentries) {
					for (link in symlinks) {
						cmd = "/bin/rm " link
						if (opt_verbose) print cmd
						system(cmd)
					}
					cmd = "rmdir " dir
					if (opt_verbose) print cmd
					system(cmd)
					for (parent in parents) {
						cmd = "ln -s " parent " " dir
						if (opt_verbose) print cmd
						system(cmd)
					}
				}
			}

			delete parents
			delete symlinks
		}'
		;;
	esac
done

--------------040605050304020809030501--