Created
August 20, 2015 06:26
-
-
Save xelxebar/001e43a7ffb6940a24e4 to your computer and use it in GitHub Desktop.
Cabal wrapper to manage a store of independently sandboxed packages.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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