bashbrew.sh 9.0 KB

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