bashbrew.sh 7.7 KB

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