-
Notifications
You must be signed in to change notification settings - Fork 19
/
cabal
executable file
·402 lines (349 loc) · 13 KB
/
cabal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
#!/bin/sh -eu
usage () {
status=${1:-0}
cat <<EOF
./cabal [command] [options]
Commands:
build Build this project, including all executables and test suites.
test Test this project, by default this runs all test suites.
testci Test this project, but process control characters (\b, \r) which
reposition the cursor, prior to emitting each line of output.
repl Start the repl, by default on the the main library source.
quick Start the repl directly skipping cabal, this is useful developing
across multiple source trees at once.
watch Watches filesystem for changes and stays running, compiles and gives quick feedback.
Similarly to quick needs an entrypoint.
To run tests use '-T EXPR' i.e. './cabal watch test/test.hs -T Test.Pure.tests'
demo If this project has a demo application, this builds and runs it.
exec If this project has an executable, this builds and runs it.
tags Generate tags for this project.
lint Lint the project.
init Start a new project.
update Cabal update, but limited to retrieving at most once per day.
clean Remove compiled artifacts.
depend Dependency organisation tooling check|deep-check|sync.
upgrade Upgrade this cabal extras script.
EOF
exit ${status}
}
fail () {
message="${1:-}"
[ -z "$message" ] || echo "${message}" 1>&2
usage 1 1>&2
}
#
# Determine the project name from the cabal file.
#
project_name () {
find . -maxdepth 1 -name \*.cabal -type f | xargs basename | sed -e 's/.cabal//'
}
#
# Initialize things for a build. This can be made faster by being
# a lot smarter about doing things conditionally, but for now,
# brute force wins.
#
initialize () {
# sandbox has all sources
project=$(project_name)
SANDBOX_DIR=.cabal-sandbox
[ ! -f "${project}.sandbox" ] || {
SANDBOX_DIR=$(cat ${project}.sandbox)
SANDBOX="--sandbox ${SANDBOX_DIR}"
}
# sandbox initialized if required, this should support sandboxes in parent directories
[ -f cabal.sandbox.config ] || {
cabal sandbox ${SANDBOX:-} init
}
PROJECT_ROOT=$(git rev-parse --show-toplevel)
# init any submodules that we need. Don't worry about explicit submodules
# file here, we just want to trust git to tell us things that haven't been
# initialized, we really _don't_ want to run this just because a module is
# dirty from development changes etc...
UNINITIALIZED=$(cd "$PROJECT_ROOT" && git submodule | grep "^-" | cut -d ' ' -f 2)
for submodule in $UNINITIALIZED; do
(cd "$PROJECT_ROOT" && git submodule update --init $submodule) || exit $?
done
# if there is an explicit submodules file, then use it (hysterical
# raisins, compatability or something...), otherwise auto-magicically
# discover the sources from the lib directory.
if [ -f "$project.submodules" ]; then
CABAL_SOURCES=$(cat $project.submodules)
echo "Using $project.submodules, however this is now optional, You can remove this file if you like and it will be calculated for you."
else
CABAL_SOURCES=$(cd "$PROJECT_ROOT" && find lib -maxdepth 4 ! -path "lib/*/bin/*" ! -path "lib/*/lib/*" -name \*.cabal | xargs -L 1 dirname)
fi
# Remove any cabal sources that are no longer available
for source in $(cabal sandbox list-sources | tail -n +4 | sed '$d' | sed '$d'); do
source_path=$(echo $source | sed s#$(pwd)/##)
if ! $(echo "${CABAL_SOURCES}" | grep -q "${source_path}"); then
echo "${source_path} no longer exists - removing from the sandbox"
cabal sandbox delete-source "$source" 1> /dev/null
fi
done
# We add-sources only if they haven't been added before. We should also
# remove them if they aren't needed, but @charleso has promised to do this
# so for now we live in anticipation.
for submodule in $CABAL_SOURCES; do
if ! grep -q "${submodule}\"" ${SANDBOX_DIR}/add-source-timestamps; then
cabal sandbox add-source ${SANDBOX:-} "$PROJECT_ROOT/$submodule"
fi
done
# all dependencies installed, and configure been run?
cabal install -j --only-dependencies --force-reinstalls --enable-tests
cabal configure --enable-tests
}
#
# Cabal build.
#
run_build () {
initialize
cabal build --ghc-option="-Werror"
}
#
# Cabal test.
#
run_test () {
initialize
cabal test --show-details=streaming "$@"
}
#
# Cabal test for CI build bots.
#
run_testci () {
# Create a temporary directory which will be deleted when the script
# terminates. Unfortunately `mktemp` doesn't behave the same on
# Linux and OS/X so we need to try two different approaches.
CABAL_EXTRAS_TEMP=$(mktemp -d 2>/dev/null || mktemp -d -t 'cabal-extras')
trap "rm -rf $CABAL_EXTRAS_TEMP" EXIT
# Write out a haskell program which performs the control character
# processing.
UNCTL=$CABAL_EXTRAS_TEMP/unctl.hs
cat << EOF > $UNCTL
import System.IO
main :: IO ()
main = do
hSetBuffering stdin LineBuffering
hSetBuffering stdout LineBuffering
interact (go [])
where
go :: [Char] -- ^ current line
-> [Char] -- ^ stdin
-> [Char] -- ^ stdout
-- backspace - delete previous character
go (_ : line) ('\b' : xs) = go line xs
go [] ('\b' : xs) = go [] xs
-- carriage return - delete the whole line
go _ ('\r' : xs) = go [] xs
-- line feed - emit the current line
go line ('\n' : xs) = reverse ('\n' : line) ++ go [] xs
-- normal character - add to current line
go line (x : xs) = go (x : line) xs
-- end of stream - emit the current line
go line [] = line
EOF
# Run the tests, but fix up their output before emitting it.
PIPE=$CABAL_EXTRAS_TEMP/testci.pipe
mkfifo $PIPE
runghc $UNCTL < $PIPE &
run_test "$@" > $PIPE
}
#
# Cabal repl.
#
run_repl () {
initialize
cabal repl "$@"
}
#
# Upgrade the script in place
#
run_upgrade () {
echo "Checking for a new version of cabal extras ..."
TZ=$(date +"%T")
curl --silent "https://raw.githubusercontent.com/ambiata/cabal-extras/master/src/cabal?$TZ" > /tmp/cabal
COMMIT_VERSION=$(git ls-remote https://github.com/ambiata/cabal-extras | grep refs/heads/master | cut -f 1)
echo "# Version: $COMMIT_VERSION" >> /tmp/cabal
if ! cmp ./cabal /tmp/cabal >/dev/null 2>&1
then
echo "New version found and upgraded. You can now commit it to your git repo."
mv /tmp/cabal ./cabal
chmod +x ./cabal
else
echo "You have latest cabal extras script"
fi
}
#
# Load up the repl in "quick" mode.
#
run_quick () {
initialize
[ $# -eq 1 ] || fail "'quick' requires a single argument, specifying the entry point you want to load."
[ -f "$1" ] || fail "The entry point does not exist."
[ ! -d src ] || SRC_DIRS="-isrc"
[ ! -d test ] || SRC_DIRS="${SRC_DIRS:-} -itest"
[ ! -d gen ] || SRC_DIRS="${SRC_DIRS:-} -igen"
[ ! -d dist/build/autogen ] || SRC_DIRS="${SRC_DIRS:-} -idist/build/autogen"
ghci -package-db=$(find .cabal-sandbox -name '*-packages.conf.d') ${SRC_DIRS:-} "$1"
}
#
# Load up ghcid
#
run_watch () {
command -v ghcid >/dev/null 2>&1 || { echo >&2 "ghcid required not installed. To install: create a fresh cabal sandbox, cabal install ghcid and add to your \$PATH."; exit 1; }
initialize
[ $# -ge 1 ] || fail "Watch requires at least one argument, specifying the entry point you want to load."
[ -f "$1" ] || fail "The entry point does not exist."
[ ! -d src ] || SRC_DIRS="-isrc"
[ ! -d test ] || SRC_DIRS="${SRC_DIRS:-} -itest"
[ ! -d gen ] || SRC_DIRS="${SRC_DIRS:-} -igen"
[ ! -d dist/build/autogen ] || SRC_DIRS="${SRC_DIRS:-} -idist/build/autogen"
ENTRY=$1
shift
ghcid -c "ghci -package-db=$(find .cabal-sandbox -name '*-packages.conf.d') ${SRC_DIRS:-} \"$ENTRY\"" "$@"
}
#
# Remove compiled artifacts.
#
run_clean () {
cabal clean
}
#
# Dependency tools.
#
# using this instead of git submodule foreach in the hope I can avoid recursive submodule init/updates (probably can't though, so this may be irrelevant).
list_modules () {
[ $# -eq 1 ] || fail "list_modules requires a single argument, specifying the root of the project."
MODULES="$1/.gitmodules"
if [ -f "${MODULES}" ]; then
grep path "${MODULES}" | sed 's/.*= //'
fi
}
force_sync () {
[ $# -eq 1 ] || fail "force sync requires a single argument, specifying the root of the project."
SYNC_ROOT="$1"
(
cd "${SYNC_ROOT}" ## required for submodule commands on older versions of git
git fetch
git submodule init
git submodule update
git submodule sync
) > /dev/null 2>&1
}
run_depend_check () {
TOP=$(git rev-parse --show-toplevel)
force_sync "${TOP}"
list_modules "${TOP}" | while read SUBMODULE; do
(
cd "${TOP}/${SUBMODULE}"
echo "$SUBMODULE"
git fetch > /dev/null 2>&1
OUT=$(git rev-list --left-right --count origin/master...)
BEHIND=$(echo $OUT | awk '{ print $1 }')
AHEAD=$(echo $OUT | awk '{ print $2 }')
if [ "$AHEAD" -eq 0 -a "$BEHIND" -eq 0 ]; then
echo ' `- [OK]'
elif [ "$AHEAD" -eq 0 -a "$BEHIND" -gt 0 ]; then
echo ' `- [WARNING] Behind master.'
elif [ "$AHEAD" -gt 0 -a "$BEHIND" -eq 0 ]; then
echo ' `- [WARNING] Commits not on master.'
elif [ "$AHEAD" -gt 0 -a "$BEHIND" -gt 0 ]; then
echo ' `- [WARNING] Commits not on master & behind master.'
fi
)
done
}
run_depend_deep_check () {
TOP=$(git rev-parse --show-toplevel)
force_sync "${TOP}"
list_modules "${TOP}" | while read SUBMODULE; do
(
echo "$SUBMODULE"
force_sync "${TOP}/${SUBMODULE}"
list_modules "${TOP}/${SUBMODULE}" | while read SUBSUBMODULE; do
if [ -e "${TOP}/${SUBSUBMODULE}" ]; then
NESTED=$(cd "${TOP}/${SUBMODULE}/${SUBSUBMODULE}" > /dev/null 2>&1; git rev-parse HEAD);
OUTER=$(cd "${TOP}/${SUBSUBMODULE}" > /dev/null 2>&1; git rev-parse HEAD);
if [ "${NESTED}" = "${OUTER}" ]; then
echo " \`- [OK] ${SUBSUBMODULE}"
else
(
cd "${TOP}/${SUBSUBMODULE}"
git fetch > /dev/null 2>&1
echo ${TOP}/${SUBSUBMODULE}
OUT=$(git rev-list --left-right --count "${NESTED}...${OUTER}")
BEHIND=$(echo $OUT | awk '{ print $1 }')
AHEAD=$(echo $OUT | awk '{ print $2 }')
echo " \`- [WARNING] ${SUBSUBMODULE} is out of sync, outer commit ${OUTER}, nested ${NESTED}, outer is ${BEHIND} commits behind, ${AHEAD} commits ahead."
)
fi
else
echo " \`- [WARNING] Potentially missing dependency ${SUBSUBMODULE}"
fi
done
)
done
}
run_depend_sync () {
TOP=$(git rev-parse --show-toplevel)
list_modules "${TOP}" | while read SUBMODULE; do
(
echo "$SUBMODULE"
force_sync "${TOP}/${SUBMODULE}"
cd "${TOP}/${SUBMODULE}"
if git branch -a | grep -q origin/ambiata; then
git checkout ambiata
git merge origin/ambiata
else
git checkout master
git merge origin/master
fi
force_sync "${TOP}/${SUBMODULE}"
)
done
}
run_depend () {
[ $# -eq 1 ] || fail "'depend' requires a single argument, specifying the mode as one of [check|deep-check|sync]."
case "$1" in
check) run_depend_check ;;
deep-check) run_depend_deep_check ;;
sync) run_depend_sync;;
*) fail "Unknown mode [$1], expected one of [check|deep-check|sync]" ;;
esac
}
#
# Run hasktags on this project.
#
run_tags () {
# TODO should install hasktags if it doesn't exist yet.
hasktags -e src test main
}
#
# Run hlint on this project (building hlint if necessary).
#
run_lint () {
# TODO should install hlint if it doesn't exist yet.
hlint src test main
}
#
# Run cabal update, but not more often than once a day.
#
run_update () {
if ! test -f $HOME/.cabal/packages/hackage.haskell.org/00-index.cache || find $HOME/.cabal/packages/hackage.haskell.org/00-index.cache -type f -mtime +1 | grep -q 00-index; then
cabal update
else
true
fi
}
#
# The actual start of the script.....
#
[ $# -gt 0 ] || fail
case "$1" in
-h|--help) usage ;;
esac
MODE="$1"; shift
case "$MODE" in
build|test|testci|repl|quick|tags|lint|update|clean|depend|watch|upgrade) run_$MODE "$@" ;;
*) fail "Unknown mode: $MODE"
esac
# Version: fb83ed7a2f70e6b1b0547118c1dc515021ff024d