bashbrew.sh 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. #!/bin/bash
  2. set -e
  3. # so we can have fancy stuff like !(pattern)
  4. shopt -s extglob
  5. dir="$(dirname "$(readlink -f "$BASH_SOURCE")")"
  6. library="$dir/../library"
  7. src="$dir/src"
  8. logs="$dir/logs"
  9. namespaces='library stackbrew'
  10. docker='docker'
  11. library="$(readlink -f "$library")"
  12. src="$(readlink -f "$src")"
  13. logs="$(readlink -f "$logs")"
  14. self="$(basename "$0")"
  15. usage() {
  16. cat <<EOUSAGE
  17. usage: $self [build|push] [options] [repo[:tag] ...]
  18. ie: $self build --all
  19. $self push debian ubuntu:12.04
  20. This script processes the specified Docker images using the corresponding
  21. repository manifest files.
  22. common options:
  23. --all Build all repositories specified in library
  24. --docker="$docker"
  25. Use a custom Docker binary
  26. --help, -h, -? Print this help message
  27. --library="$library"
  28. Where to find repository manifest files
  29. --logs="$logs"
  30. Where to store the build logs
  31. --namespaces="$namespaces"
  32. Space separated list of namespaces to tag images in after
  33. building
  34. build options:
  35. --no-build Don't build, print what would build
  36. --no-clone Don't pull/clone Git repositories
  37. --src="$src"
  38. Where to store cloned Git repositories (GOPATH style)
  39. push options:
  40. --no-push Don't push, print what would push
  41. EOUSAGE
  42. }
  43. # arg handling
  44. opts="$(getopt -o 'h?' --long 'all,docker:,help,library:,logs:,namespaces:,no-build,no-clone,no-push,src:' -- "$@" || { usage >&2 && false; })"
  45. eval set -- "$opts"
  46. doClone=1
  47. doBuild=1
  48. doPush=1
  49. buildAll=
  50. while true; do
  51. flag=$1
  52. shift
  53. case "$flag" in
  54. --all) buildAll=1 ;;
  55. --docker) docker="$1" && shift ;;
  56. --help|-h|'-?') usage && exit 0 ;;
  57. --library) library="$1" && shift ;;
  58. --logs) logs="$1" && shift ;;
  59. --namespaces) namespaces="$1" && shift ;;
  60. --no-build) doBuild= ;;
  61. --no-clone) doClone= ;;
  62. --no-push) doPush= ;;
  63. --src) src="$1" && shift ;;
  64. --) break ;;
  65. *)
  66. {
  67. echo "error: unknown flag: $flag"
  68. usage
  69. } >&2
  70. exit 1
  71. ;;
  72. esac
  73. done
  74. # which subcommand
  75. subcommand="$1"
  76. case "$subcommand" in
  77. build|push)
  78. shift
  79. ;;
  80. *)
  81. {
  82. echo "error: unknown subcommand: $1"
  83. usage
  84. } >&2
  85. exit 1
  86. ;;
  87. esac
  88. repos=()
  89. if [ "$buildAll" ]; then
  90. repos=( "$library"/!(MAINTAINERS) )
  91. fi
  92. repos+=( "$@" )
  93. repos=( "${repos[@]%/}" )
  94. if [ "${#repos[@]}" -eq 0 ]; then
  95. {
  96. echo 'error: no repos specified'
  97. usage
  98. } >&2
  99. exit 1
  100. fi
  101. # globals for handling the repo queue and repo info parsed from library
  102. queue=()
  103. declare -A repoGitRepo=()
  104. declare -A repoGitRef=()
  105. declare -A repoGitDir=()
  106. logDir="$logs/$subcommand-$(date +'%Y-%m-%d--%H-%M-%S')"
  107. mkdir -p "$logDir"
  108. latestLogDir="$logs/latest" # this gets shiny symlinks to the latest buildlog for each repo we've seen since the creation of the logs dir
  109. mkdir -p "$latestLogDir"
  110. didFail=
  111. # gather all the `repo:tag` combos to build
  112. for repoTag in "${repos[@]}"; do
  113. repo="${repoTag%%:*}"
  114. tag="${repoTag#*:}"
  115. [ "$repo" != "$tag" ] || tag=
  116. if [ "$repo" = 'http' -o "$repo" = 'https' ] && [[ "$tag" == //* ]]; then
  117. # IT'S A URL!
  118. repoUrl="$repo:${tag%:*}"
  119. repo="$(basename "$repoUrl")"
  120. if [ "${tag##*:}" != "$tag" ]; then
  121. tag="${tag##*:}"
  122. else
  123. tag=
  124. fi
  125. repoTag="${repo}${tag:+:$tag}"
  126. echo "$repoTag ($repoUrl)" >> "$logDir/repos.txt"
  127. cmd=( curl -sSL --compressed "$repoUrl" )
  128. else
  129. if [ -f "$repo" ]; then
  130. repoFile="$repo"
  131. repo="$(basename "$repoFile")"
  132. repoTag="${repo}${tag:+:$tag}"
  133. else
  134. repoFile="$library/$repo"
  135. fi
  136. repoFile="$(readlink -f "$repoFile")"
  137. echo "$repoTag ($repoFile)" >> "$logDir/repos.txt"
  138. cmd=( cat "$repoFile" )
  139. fi
  140. if [ "${repoGitRepo[$repoTag]}" ]; then
  141. queue+=( "$repoTag" )
  142. continue
  143. fi
  144. # parse the repo library file
  145. IFS=$'\n'
  146. repoTagLines=( $("${cmd[@]}" | grep -vE '^#|^\s*$') )
  147. unset IFS
  148. tags=()
  149. for line in "${repoTagLines[@]}"; do
  150. tag="$(echo "$line" | awk -F ': +' '{ print $1 }')"
  151. for parsedRepoTag in "${tags[@]}"; do
  152. if [ "$repo:$tag" = "$parsedRepoTag" ]; then
  153. echo >&2 "error: tag '$tag' is duplicated in '${cmd[@]}'"
  154. exit 1
  155. fi
  156. done
  157. repoDir="$(echo "$line" | awk -F ': +' '{ print $2 }')"
  158. gitUrl="${repoDir%%@*}"
  159. commitDir="${repoDir#*@}"
  160. gitRef="${commitDir%% *}"
  161. gitDir="${commitDir#* }"
  162. if [ "$gitDir" = "$commitDir" ]; then
  163. gitDir=
  164. fi
  165. gitRepo="${gitUrl#*://}"
  166. gitRepo="${gitRepo%/}"
  167. gitRepo="${gitRepo%.git}"
  168. gitRepo="${gitRepo%/}"
  169. gitRepo="$src/$gitRepo"
  170. if [ "$subcommand" == 'build' ]; then
  171. if [ -z "$doClone" ]; then
  172. if [ "$doBuild" -a ! -d "$gitRepo" ]; then
  173. echo >&2 "error: directory not found: $gitRepo"
  174. exit 1
  175. fi
  176. else
  177. if [ ! -d "$gitRepo" ]; then
  178. mkdir -p "$(dirname "$gitRepo")"
  179. echo "Cloning $repo ($gitUrl) ..."
  180. git clone -q "$gitUrl" "$gitRepo"
  181. else
  182. # if we don't have the "ref" specified, "git fetch" in the hopes that we get it
  183. if ! (
  184. cd "$gitRepo"
  185. git rev-parse --verify "${gitRef}^{commit}" &> /dev/null
  186. ); then
  187. echo "Fetching $repo ($gitUrl) ..."
  188. (
  189. cd "$gitRepo"
  190. git fetch -q --all
  191. git fetch -q --tags
  192. )
  193. fi
  194. fi
  195. # disable any automatic garbage collection too, just to help make sure we keep our dangling commit objects
  196. ( cd "$gitRepo" && git config gc.auto 0 )
  197. fi
  198. fi
  199. repoGitRepo[$repo:$tag]="$gitRepo"
  200. repoGitRef[$repo:$tag]="$gitRef"
  201. repoGitDir[$repo:$tag]="$gitDir"
  202. tags+=( "$repo:$tag" )
  203. done
  204. if [ "$repo" = "$repoTag" ]; then
  205. # add all tags we just parsed
  206. queue+=( "${tags[@]}" )
  207. else
  208. queue+=( "$repoTag" )
  209. fi
  210. done
  211. set -- "${queue[@]}"
  212. while [ "$#" -gt 0 ]; do
  213. repoTag="$1"
  214. gitRepo="${repoGitRepo[$repoTag]}"
  215. gitRef="${repoGitRef[$repoTag]}"
  216. gitDir="${repoGitDir[$repoTag]}"
  217. shift
  218. if [ -z "$gitRepo" ]; then
  219. echo >&2 'Unknown repo:tag:' "$repoTag"
  220. didFail=1
  221. continue
  222. fi
  223. thisLog="$logDir/$subcommand-$repoTag.log"
  224. touch "$thisLog"
  225. ln -sf "$thisLog" "$latestLogDir/$(basename "$thisLog")"
  226. case "$subcommand" in
  227. build)
  228. echo "Processing $repoTag ..."
  229. if ! ( cd "$gitRepo" && git rev-parse --verify "${gitRef}^{commit}" &> /dev/null ); then
  230. echo "- failed; invalid ref: $gitRef"
  231. didFail=1
  232. continue
  233. fi
  234. dockerfilePath="$gitDir/Dockerfile"
  235. dockerfilePath="${dockerfilePath#/}" # strip leading "/" (for when gitDir is '') because "git show" doesn't like it
  236. if ! dockerfile="$(cd "$gitRepo" && git show "$gitRef":"$dockerfilePath")"; then
  237. echo "- failed; missing '$dockerfilePath' at '$gitRef' ?"
  238. didFail=1
  239. continue
  240. fi
  241. IFS=$'\n'
  242. froms=( $(echo "$dockerfile" | awk 'toupper($1) == "FROM" { print $2 ~ /:/ ? $2 : $2":latest" }') )
  243. unset IFS
  244. for from in "${froms[@]}"; do
  245. for queuedRepoTag in "$@"; do
  246. if [ "$from" = "$queuedRepoTag" ]; then
  247. # a "FROM" in this image is being built later in our queue, so let's bail on this image for now and come back later
  248. echo "- deferred; FROM $from"
  249. set -- "$@" "$repoTag"
  250. continue 3
  251. fi
  252. done
  253. done
  254. if [ "$doBuild" ]; then
  255. if ! (
  256. set -x
  257. cd "$gitRepo"
  258. git reset -q HEAD
  259. git checkout -q -- .
  260. git clean -dfxq
  261. git checkout -q "$gitRef" --
  262. cd "$gitRepo/$gitDir"
  263. "$dir/git-set-mtimes"
  264. ) &>> "$thisLog"; then
  265. echo "- failed 'git checkout'; see $thisLog"
  266. didFail=1
  267. continue
  268. fi
  269. if ! (
  270. set -x
  271. "$docker" build -t "$repoTag" "$gitRepo/$gitDir"
  272. ) &>> "$thisLog"; then
  273. echo "- failed 'docker build'; see $thisLog"
  274. didFail=1
  275. continue
  276. fi
  277. for namespace in $namespaces; do
  278. if ! (
  279. set -x
  280. "$docker" tag -f "$repoTag" "$namespace/$repoTag"
  281. ) &>> "$thisLog"; then
  282. echo "- failed 'docker tag'; see $thisLog"
  283. didFail=1
  284. continue
  285. fi
  286. done
  287. fi
  288. ;;
  289. push)
  290. for namespace in $namespaces; do
  291. if [ "$doPush" ]; then
  292. echo "Pushing $namespace/$repoTag..."
  293. if ! "$docker" push "$namespace/$repoTag" &> "$thisLog" < /dev/null; then
  294. echo >&2 "- $namespace/$repoTag failed to push; see $thisLog"
  295. fi
  296. else
  297. echo "$docker push" "$namespace/$repoTag"
  298. fi
  299. done
  300. ;;
  301. esac
  302. done
  303. [ -z "$didFail" ]