| #!/bin/bash |
| # |
| # cqfd - a tool to wrap commands in controlled Docker containers |
| # |
| # Copyright (C) 2015-2016 Savoir-faire Linux, Inc. |
| # |
| # This program is free software: you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation, either version 3 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| |
| set -e |
| |
| PROGNAME=`basename $0` |
| cqfd_dir=".cqfd" |
| dockerfile="$cqfd_dir/docker/Dockerfile" |
| cqfdrc=".cqfdrc" |
| |
| ## usage() - print usage on stdout |
| usage() { |
| cat <<EOF |
| Usage: $PROGNAME [OPTION ARGUMENT] [COMMAND] [ARGUMENTS] |
| |
| Options: |
| -f <file> Use file as config file (default .cqfdrc) |
| -d <dir> Use directory as cqfd directory (default .cqfd) |
| -b <flavor_name> Target a specific build flavor. |
| |
| Commands: |
| init Initialize project build container |
| run Run argument(s) inside build container |
| release Run argument(s) and release software |
| help Show this help text |
| |
| By default, run is assumed, and the run command is the one |
| configured in .cqfdrc. |
| |
| cqfd is Copyright (C) 2015-2016 Savoir-faire Linux, Inc. |
| |
| This program comes with ABSOLUTELY NO WARRANTY. This is free |
| software, and you are welcome to redistribute it under the terms |
| of the GNU GPLv3 license; see the LICENSE for more informations. |
| EOF |
| } |
| |
| # cfg_parser() - parse ini-style config files |
| # Will parse a ini-style config file, and evaluate it to a bash array. |
| # Ref: http://theoldschooldevops.com/2008/02/09/bash-ini-parser/ |
| # arg$1: path to ini file |
| cfg_parser() { |
| # bash 4.3 and later break compatibility |
| if [ $BASH_VERSINFO -ge 4 -a ${BASH_VERSINFO[1]} -gt 2 ]; then |
| local compat=1 |
| shopt -s compat42 |
| fi |
| |
| if ! ini="$(<$1)"; then # read the file |
| die "$1: No such file!" |
| fi |
| ini="${ini//[/\[}" # escape [ |
| ini="${ini//]/\]}" # escape ] |
| IFS=$'\n' && ini=( ${ini} ) # convert to line-array |
| ini=( ${ini[*]//;*/} ) # remove comments with ; |
| ini=( ${ini[*]/\ =/=} ) # remove tabs before = |
| ini=( ${ini[*]/=\ /=} ) # remove tabs be = |
| ini=( ${ini[*]/\ =\ /=} ) # remove anything with a space around = |
| ini=( ${ini[*]/#\\[/\}$'\n'cfg.section.} ) # set section prefix |
| ini=( ${ini[*]/%\\]/ \(} ) # convert text2function (1) |
| ini=( ${ini[*]/=/=\( } ) # convert item to array |
| ini=( ${ini[*]/%/ \)} ) # close array parenthesis |
| ini=( ${ini[*]/%\\ \)/ \\} ) # the multiline trick |
| ini=( ${ini[*]/%\( \)/\(\) \{} ) # convert text2function (2) |
| ini=( ${ini[*]/%\} \)/\}} ) # remove extra parenthesis |
| ini[0]="" # remove first element |
| ini[${#ini[*]} + 1]='}' # add the last brace |
| if ! eval "$(echo "${ini[*]}")" 2>/dev/null; then # eval the result |
| die "$1: Invalid ini-file!" |
| fi |
| |
| # restore previous bash behaviour |
| [ "$compat" = "1" ] && shopt -u compat42 |
| } |
| |
| ## die() - exit when an error occured |
| # $@ messages and variables shown in the error message |
| die() { |
| echo "cqfd: fatal: $@" 1>&2 |
| exit 1 |
| } |
| |
| # docker_build() - Initialize build container |
| docker_build() { |
| config_load |
| if [ ! -f $dockerfile ]; then |
| die "no Dockerfile found at location $dockerfile" |
| fi |
| docker build -q -t "$docker_img_name" `dirname $dockerfile` |
| } |
| |
| # docker_run() - run command in configured container |
| # A few implementation details: |
| # |
| # - The user executing the build commands inside the container is |
| # named 'builder', with the same uid/gid as your user to keep |
| # filesystem permissions in sync. |
| # |
| # - Your project's source directory is always mapped to ~builder/src/ |
| # |
| # - Your ~/.ssh directory is mapped to ~builder/.ssh to provide access |
| # to the ssh keys (your build may pull authenticated git repos for |
| # example). |
| # |
| # arg$1: the command string to execute as builder |
| # |
| docker_run() { |
| [ -z "$JENKINS_URL" ] && local nojenkins=1 |
| |
| # The user may set the CQFD_EXTRA_VOLUMES environment variable |
| # to map custom volumes inside his development container. |
| if [ -n "$CQFD_EXTRA_VOLUMES" ]; then |
| local map extravol |
| for map in $CQFD_EXTRA_VOLUMES; do |
| extravol+="-v $map " |
| done |
| fi |
| |
| if [ -n "$CQFD_EXTRA_HOSTS" ]; then |
| local map extrahosts |
| for map in $CQFD_EXTRA_HOSTS; do |
| extrahosts+="--add-host $map " |
| done |
| fi |
| |
| # CQFD_EXTRA_ENV is a space-separated list like VAR=value |
| if [ -n "$CQFD_EXTRA_ENV" ]; then |
| local map extraenv |
| for map in $CQFD_EXTRA_ENV; do |
| extraenv+="-e $map " |
| done |
| fi |
| |
| docker run --privileged -v "$PWD":/home/builder/src \ |
| -v ~/.ssh:/home/builder/.ssh \ |
| --rm \ |
| $extravol \ |
| $extrahosts \ |
| $extraenv \ |
| ${nojenkins:+ -ti} \ |
| ${SSH_AUTH_SOCK:+ -v $SSH_AUTH_SOCK:/home/builder/.sockets/ssh} \ |
| ${SSH_AUTH_SOCK:+ -e SSH_AUTH_SOCK=/home/builder/.sockets/ssh} \ |
| $docker_img_name \ |
| /bin/bash -c "groupadd -og $GROUPS -f builders && \ |
| useradd -s /bin/bash -u $UID -g $GROUPS builder && \ |
| chown $UID:$GROUPS /home/builder && \ |
| su builder -p -c \"cd ~builder/src/ && $1\" 2>&1" |
| } |
| |
| # make_archive(): Create a release package. |
| # Note: the --transform option passed to tar allows to move all the |
| # specified files at the root of the archive. Therefore, you shouldn't |
| # include two files with the same name in the list of files to |
| # archive. |
| make_archive() { |
| if [ -z "$release_files" ]; then |
| die "No files to archive, check files in $cqfdrc" |
| fi |
| |
| for file in $release_files; do |
| [ -f $file ] || die "Cannot create release: missing $file" |
| done |
| |
| # template the generated archive's filename |
| local git_short=`git rev-parse --short HEAD 2>/dev/null` |
| local git_long=`git rev-parse HEAD 2>/dev/null` |
| local date_rfc3339=`date --rfc-3339='date'` |
| |
| # default name for the archive if not set |
| [ -z "$release_archive" ] && release_archive="%Po-%Pn.tar.xz" |
| |
| release_archive=`echo $release_archive | |
| sed -e 's!%%!%!g; |
| s!%Gh!'$git_short'!g; |
| s!%GH!'$git_long'!g; |
| s!%D3!'$date_rfc3339'!g; |
| s!%Po!'$project_org'!g; |
| s!%Pn!'$project_name'!g; |
| s!%Cf!'$flavor'!g;'` |
| |
| # also replace variable names - beware with eval |
| eval release_archive=`echo $release_archive` |
| |
| XZ_OPT=-9 tar --transform "s/.*\///g" -cJf \ |
| "$release_archive" $release_files |
| } |
| |
| # config_load() - load build settings from cqfdrc |
| # $1: optional "flavor" of the build, is a suffix of command. |
| config_load() { |
| local p_flavor="$1" |
| |
| IFS="$IFS" cfg_parser "$cqfdrc" |
| |
| cfg.section.project # load the [project] section |
| project_org="$org" |
| project_name="$name" |
| |
| cfg.section.build # load the [build] section |
| |
| # build parameters may be overriden by a flavor defined in the |
| # build section's 'flavors' parameter. |
| if [ -n "$p_flavor" ]; then |
| for flavor in $flavors; do |
| if [ "$flavor" = "$p_flavor" ]; then |
| local _found=1 |
| break |
| fi |
| done |
| |
| if [ -n "$_found" ]; then |
| cfg.section."$p_flavor" # load the [$p_flavor] section |
| else |
| die "flavor \"$p_flavor\" not found in flavors list" |
| fi |
| fi |
| |
| build_cmd="$command" |
| if [ -n "$distro" ]; then |
| dockerfile="$cqfd_dir/$distro/Dockerfile" |
| fi |
| |
| release_files="`eval echo $files`" |
| release_archive="$archive" |
| |
| # This will look like fooinc_reponame |
| if [ -n "$project_org" -a -n "$project_name" ]; then |
| docker_img_name="${project_org}_${project_name}" |
| else |
| die "project.org and project.name not configured" |
| fi |
| } |
| |
| while [ $# -gt 0 ]; do |
| case "$1" in |
| help|-h|"--help") |
| usage |
| exit 0 |
| ;; |
| init) |
| docker_build |
| exit $? |
| ;; |
| -b) |
| shift |
| flavor="$1" |
| ;; |
| -f) |
| shift |
| cqfdrc="$1" |
| ;; |
| -d) |
| shift |
| cqfd_dir="$1" |
| dockerfile="$cqfd_dir/docker/Dockerfile" |
| ;; |
| run|release) |
| [ "$1" = "release" ] && make_archive=1 |
| if [ $# -gt 1 ]; then |
| shift |
| build_cmd_alt="$@" |
| fi |
| break |
| ;; |
| ?*) |
| die "Unknown command: $1" |
| ;; |
| *) |
| # empty or no argument case |
| ;; |
| esac |
| shift |
| done |
| |
| config_load $flavor |
| |
| if [ -n "$build_cmd_alt" ]; then |
| build_cmd=$build_cmd_alt |
| elif [ -z "$build_cmd" ]; then |
| die "No build.command defined in $cqfdrc !" |
| fi |
| |
| docker_run "$build_cmd" |
| |
| if [ "$make_archive" = "1" ]; then |
| make_archive |
| fi |