bashbrew.sh 9.1 KB

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