bashbrew.sh 11 KB

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