Skip to content

Instantly share code, notes, and snippets.

@xelxebar
Created August 20, 2015 06:26
Show Gist options
  • Save xelxebar/001e43a7ffb6940a24e4 to your computer and use it in GitHub Desktop.
Save xelxebar/001e43a7ffb6940a24e4 to your computer and use it in GitHub Desktop.
Cabal wrapper to manage a store of independently sandboxed packages.
#! /usr/bin/env sh
# vim: filetype=sh :
function __init__ () {
#--- Module Start ---#
### Global Variables ###
typeset -A ErrCode
ErrCode[100]="I/O Errors"
ErrCode[MisingExec]=110
ErrCode[PathEmpty]=120
ErrCode[PathInaccessible]=130
ErrCode[FileExists]=140
ErrCode[200]="Command Argument Errors"
ErrCode[UnknownArg]=210
ErrCode[UnrecognizedCommand]=220
ErrCode[300]="Config File Errors"
ErrCode[ConfigIncompatible]=310
ErrCode[1000]="JSON Errors"
ErrCode[UnsupportedJson]=1010
typeset -A Command
Command[StartIndex]=0
Command[Init]="init"
Command[Install]="install"
Command[Remove]="uninstall"
Command[Sync]="sync"
Command[List]="list"
Command[Help]="help"
typeset -A Conf
Conf[SupportedVersion]="0.0.1"
Conf[Description]="description"
Conf[Version]="config-version"
Conf[Path]="paths"
Conf[Bin]="bin"
Conf[Store]="store"
Conf[Packages]="packages"
Conf[ExtraDeps]="extra-deps"
typeset -A Default
Default[BasePath]="${HOME}/.cabal-store"
Default[ConfigPath]="${Default[BasePath]}/store.conf"
Default[StorePath]="${Default[BasePath]}/store"
Default[BinPath]="${Default[BasePath]}/bin"
typeset -A Path
Path[Config]=""
Path[Bin]=""
Path[Store]=""
Path[SandboxBin]=".cabal-sandbox/bin"
Path[CabalLog]=".cabal-sandbox/logs/build.log"
typeset -A Opt
Opt[ConfigPath]=""
Opt[StorePath]=""
Opt[BinPath]=""
### Helper Functions ###
function arrayDifference () {
if [[ ${#} != 2 ]]; then
exit -1
fi
local A=(${1})
local B=(${2})
local C=()
for p in "${A[@]}"; do
local isUnique="true"
for q in "${B[@]}"; do
if [[ ${p} == ${q} ]]; then
isUnique="false"
break
fi
done
[[ ${isUnique} == "false" ]] || C+=("${p}")
done
echo ${C[@]}
}
### Getters and Setters ###
function getPath () {
local key=${1}
echo ${Path["${key}"]}
}
function setPath () {
local key=${1}
local path=${2}
if [[ -z "${path}" ]]; then
errPathEmpty "${key}"
elif [[ ! -e "${path}" ]]; then
errPathInaccessible "${path}"
fi
Path["${key}"]="${path}"
}
function setPathFromConfig () {
local key=${1}
local rawPath="$(getConfigProp Path ${key})"
local absolutePath=""
if [[ "$(realpath ${rawPath})" == "${rawPath}" ]]; then
absolutePath="${rawPath}"
else
absolutePath="$(dirname $(getPath Config))/${rawPath}"
fi
setPath "${key}" "${absolutePath}"
}
function getConf () {
local key=${1}
echo "${Conf[${key}]}"
}
function getDefault () {
local key=${1}
echo ${Default["${key}"]}
}
function setOpt () {
local key=${1}
local value=${2}
Opt["${key}"]="${value}"
}
function getOpt () {
local key=${1}
echo ${Opt["${key}"]}
}
function getConfigProp () {
local props=${@}
local spec=""
local type=""
for prop in ${props}; do
local selector=$(getConf ${prop})
if [[ -z "${selector}" ]]; then
selector=${prop}
fi
spec="${spec}.\"${selector}\""
done
type=$(jq -r "${spec}|type" "$(getPath Config)")
if [[ ${type} == "number" ||
${type} == "boolean" ||
${type} == "string" ]]; then
jq -r "${spec}" "${Path[Config]}"
elif [[ ${type} == "array" ]]; then
jq -r "${spec}|join(\" \")" "$(getPath Config)"
elif [[ ${type} == "object" ]]; then
jq -r "${spec}|keys|join(\" \")" "$(getPath Config)"
elif [[ ${type} == "null" ]]; then
echo ""
else
errUnsupportedJson "${spec}"
fi
}
### Path Construction ###
function getPackagePath () {
local key=${1}
local pkg=${2}
local path=""
if [[ "${key}" == "SandboxBin" ]]; then
path="$(getPath Store)/${pkg}/$(getPath ${key})/${pkg}"
elif [[ "${key}" == "CabalLog" ]]; then
path="$(getPath Store)/${pkg}/$(getPath ${key})"
else
path="$(getPath ${key})/${pkg}"
fi
echo "${path}"
}
### Printing Functions ###
function printUsage () {
local exePath=${1}
local exeName=$(basename "${exePath}")
cat <<-USAGE
Usage: ${exeName} [<option list>] <command>
OPTION LIST
-c <config path>
Declare path to config file.
-s <store path>
Declare path to the "store" directory, where individual
packages are installed.
-b <bin path>
Declare path to the "bin" directory, where symlinks to
package executables are installed.
COMMAND LIST
${Command[Init]}
Initialize the cabal store.
${Command[Install]} <package list>
Install the list of packages in <package list> into the
cabal store.
${Command[Remove]} <package list>
Uninstall the list of packages in <package list> from the
cabal store.
${Command[Sync]}
Synchronize the installed packages to the ones declare in
config file.
${Command[List]}
List all packages currently installed in the store.
${Command[Help]}
Print this help.
USAGE
}
function errMissingExec () {
local exeName=${1}
cat <<-ERR
ERROR: Could not find ${exeName} on PATH.
ERR
exit ${ErrCode[MissingExec]}
}
function errPathEmpty () {
local pathSpec=${1}
cat <<-ERR
ERROR: No file specified: ${pathSpec}
ERR
exit ${ErrCode[PathEmpty]}
}
function errPathInaccessible () {
local path=${1}
cat <<-ERR
ERROR: Cannot access file ${path}
ERR
exit ${ErrCode[PathInaccessible]}
}
function errFileExists () {
local path=${1}
cat <<-ERR
ERROR: File exists: ${path}
ERR
exit ${ErrCode[FileExists]}
}
function errConfigIncompatible () {
local expectedVersion=${1}
local foundVersion=${2}
cat <<-ERR
ERROR: Incompatible configuration file format.
Expected version ${expectedVersion} but found version ${foundVersion}
ERR
exit ${ErrCode[ConfigIncompatible]}
}
function errUnsupportedJson () {
local key=${1}
cat <<-ERR
ERROR: Json in unsupported format for key ${key}
ERR
exit ${ErrCode[UnsupportedJson]}
}
function errUnrecognizedCommand () {
local commandSpec="${@}"
cat <<-ERR
ERROR: Unrecognized Command: ${commandSpec[*]}
ERR
printUsage ${0}
exit ${ErrCode[UnrecognizedCommand]}
}
### Environment Checks ###
function parsePrelimCommands () {
if [[ ${#} == 0 ||
"${@}" == "${Command[Help]}" ]]; then
printUsage ${0}
exit 0
elif [[ "${@}" == "${Command[Init]}" ]]; then
initializeCabalStore
exit 0
fi
}
function parseCommands () {
if [[ "${@}" == "${Command[List]}" ]]; then
listInstalledPackages
exit 0
elif [[ "${1}" == "${Command[Install]}" ]]; then
shift
installPackages ${@}
exit 0
elif [[ "${1}" == "${Command[Remove]}" ]]; then
shift
removePackages ${@}
exit 0
elif [[ "${@}" == "${Command[Sync]}" ]]; then
synchronizePackages
exit 0
else
errUnrecognizedCommand ${@}
exit 0
fi
}
function checkDependencies () {
local deps=( cabal jq cut )
for dep in ${deps[@]}; do
if [[ ! -x $(which ${dep} 2>/dev/null) ]]; then
errMissingExec ${dep}
fi
done
}
function checkConfigVersion () {
local configVersion=$(getConfigProp Version)
if [[ $(getConf SupportedVersion) != ${configVersion} ]]; then
errConfigIncompatible "$(getConf SupportedVersion)" "${configVersion}"
fi
}
### Package Management Functions ###
function installPackageLocally () {
local pkg=${1}
local deps=$(getConfigProp Packages ${pkg} ExtraDeps)
local startDir=${PWD}
mkdir ${pkg}
cd ${pkg}
cabal sandbox init
cabal install ${deps[@]} ${pkg}
cd "${startDir}"
}
function removePackage () {
local pkg=${1}
rm -r "$(getPackagePath Store ${pkg})"
rm -r "$(getPackagePath Bin ${pkg})"
}
function createPackageSymlink () {
local pkg=${1}
local dir=${2}
local target=$(getPackagePath SandboxBin ${pkg})
local linkPath=$(getPackagePath Bin ${pkg})
ln -s "${target}" "${linkPath}"
}
function makeConfig () {
local configPath=${1}
local storePath=${2}
local binPath=${3}
cat > "${configPath}" <<-JSON
{ "$(getConf Description)": "Cabal Store Configuration"
, "$(getConf Version)": "$(getConf SupportedVersion)"
, "$(getConf Path)": {
"$(getConf Store)": "${storePath}"
, "$(getConf Bin)": "${binPath}"
}
, "$(getConf Packages)": {}
}
JSON
}
### Command Functions ###
function initializeCabalStore () {
if [[ ! -e "$(getOpt ConfigPath)" ]]; then
mkdir -p "$(dirname $(getOpt ConfigPath))"
makeConfig "$(getOpt ConfigPath)" "$(getOpt StorePath)" "$(getOpt BinPath)"
else
errFileExists "$(getOpt ConfigPath)"
fi
if [[ ! -e "$(getOpt StorePath)" ]]; then
mkdir -p "$(getOpt StorePath)"
else
errFileExists "$(getOpt StorePath)"
fi
if [[ ! -e "$(getOpt BinPath)" ]]; then
mkdir -p "$(getOpt BinPath)"
else
errFileExists "$(getOpt BinPath)"
fi
}
function installPackages () {
local packages=${@}
local startDir=${PWD}
cd "$(getPath Store)"
for pkg in ${packages[@]}; do
installPackageLocally ${pkg}
createPackageSymlink ${pkg} "$(getPath Bin)"
done
cd "${startDir}"
}
function removePackages () {
local pkgs=${@}
for pkg in ${pkgs[@]}; do
removePackage ${pkg}
done
}
function synchronizePackages () {
local installedPkgs=$(ls -1 "$(getPath Store)")
local declaredPkgs=$(getConfigProp Packages)
local missingPkgs=$(arrayDifference "${declaredPkgs[*]}" "${installedPkgs[*]}")
local surplusPkgs=$(arrayDifference "${installedPkgs[*]}" "${declaredPkgs[*]}")
removePackages ${surplusPkgs[@]}
installPackages ${missingPkgs[@]}
}
function listInstalledPackages () {
local pkglist=$(ls -1 $(getPath Store))
for pkg in ${pkglist[@]}; do
grep "package: ${pkg}" \
"$(getPackagePath Store ${pkg})/.cabal-sandbox/logs/build.log" | \
cut -d' ' -f2
done
}
### Initialization Functions ###
function initOpts () {
local opts=(ConfigPath StorePath BinPath)
while getopts :s:b:c: OPT; do
local key=""
local setopt="false"
case ${OPT} in
b) key=BinPath
setopt="true";;
s) key=StorePath
setopt="true";;
c) key=ConfigPath
setopt="true";;
?) printUsage ${0}
exit ${ErrCode[UnknownArg]};;
esac
if [[ ${setopt} == "true" ]]; then
setOpt ${key} "$(realpath ${OPTARG})"
fi
done
for opt in ${opts[@]}; do
if [[ -z "$(getOpt ${opt})" ]]; then
setOpt ${opt} "$(getDefault ${opt})"
fi
done
Command[StartIndex]=$((${OPTIND} - 1))
}
function initPaths () {
setPath Config "$(getOpt ConfigPath)"
checkConfigVersion
setPathFromConfig Store
setPathFromConfig Bin
}
### Main Execution ###
function main () {
checkDependencies
initOpts ${@}
shift ${Command[StartIndex]}
parsePrelimCommands ${@}
initPaths
checkConfigVersion
parseCommands ${@}
return 0
}
main ${@}
#--- Module End ---#
}
__init__ ${@}
[[ -n $(typeset -f __init__) ]] && unset -f __init__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment