diff --git a/aptfile b/aptfile index 49fa6e2e..d5e1bc79 100755 --- a/aptfile +++ b/aptfile @@ -1,6 +1,11 @@ #!/usr/bin/env aptfile # ^ note the above shebang # https://github.com/seatgeek/bash-aptfile +# Copied locally into tools/bash-aptfile +# This file is used to install packages on a development environment +# using the apt-get package manager. +# Run using the following command: +# sudo ./tools/bash-aptfile/bin/aptfile ./aptfile # trigger an apt-get update update @@ -19,6 +24,7 @@ package "mingw-w64" package "git" package "gitk" package "gitg" +package "pre-commit" # install source code format packages package "tofrodos" diff --git a/tools/bash-aptfile/.editorconfig b/tools/bash-aptfile/.editorconfig new file mode 100644 index 00000000..74df3a01 --- /dev/null +++ b/tools/bash-aptfile/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +insert_final_newline = true +indent_style = space +indent_size = 2 + +[Makefile] +insert_final_newline = true +indent_style = tab +indent_size = 4 diff --git a/tools/bash-aptfile/Dockerfile b/tools/bash-aptfile/Dockerfile new file mode 100644 index 00000000..6bd9b66f --- /dev/null +++ b/tools/bash-aptfile/Dockerfile @@ -0,0 +1,48 @@ +# This dockerfile is used to build debian packages +# it should not be invoked directly. +# To build a debian package, run: +# +# make deb +# +# The debian package will be copied into the working +# directory. You can change the version by modifying +# the version in the Makefile. +# +FROM ubuntu:14.04 + +RUN apt-get -qq update + +RUN \ + export DEBIAN_FRONTEND=noninteractive && \ + apt-get -qq install -qq -y ruby ruby-dev ruby-bundler > /dev/null && \ + apt-get -qq install -qq -y build-essential rpm > /dev/null && \ + rm -rf /var/lib/apt/lists/* + +RUN gem install fpm -q > /dev/null + +WORKDIR /data + +RUN mkdir -p /data/build/usr/local/bin /data/build/var/lib/aptfile + +COPY bin/aptfile /data/build/usr/local/bin/aptfile + +RUN echo "VERSION" > /data/build/var/lib/aptfile/VERSION \ + && chmod +x /data/build/usr/local/bin/aptfile + +RUN fpm --log warn \ + -s dir \ + -t deb \ + -C /data/build \ + --name aptfile \ + --version "VERSION" \ + --description "a simple method of defining apt-get dependencies for an application" \ + --maintainer "SeatGeek " \ + --vendor "SeatGeek" \ + --license "BSD 3-Clause" \ + --url "https://github.com/seatgeek/bash-aptfile" \ + --deb-no-default-config-files \ + . + +RUN dpkg -i /data/aptfile_"VERSION"_amd64.deb && \ + dpkg -s aptfile && \ + aptfile -v diff --git a/tools/bash-aptfile/LICENSE.txt b/tools/bash-aptfile/LICENSE.txt new file mode 100644 index 00000000..e5838c4c --- /dev/null +++ b/tools/bash-aptfile/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) 2015, SeatGeek, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tools/bash-aptfile/Makefile b/tools/bash-aptfile/Makefile new file mode 100644 index 00000000..8d8f3b63 --- /dev/null +++ b/tools/bash-aptfile/Makefile @@ -0,0 +1,30 @@ +VERSION = 1.1.0 +DOCKER_IMAGE = aptfile-$(VERSION) + +shellcheck: +ifeq ($(shell shellcheck > /dev/null 2>&1 ; echo $$?),127) +ifeq ($(shell uname),Darwin) + brew install shellcheck +else + sudo add-apt-repository 'deb http://archive.ubuntu.com/ubuntu trusty-backports main restricted universe multiverse' + sudo apt-get update -qq && sudo apt-get install -qq -y shellcheck +endif +endif + +lint: shellcheck + shellcheck bin/aptfile + +clean: + rm -f *.deb + +deb: clean + sed -i -e 's/"VERSION"/$(VERSION)/' Dockerfile && rm Dockerfile-e + docker build -t $(DOCKER_IMAGE) . + bash -c 'ID=$$(docker run -i -a stdin $(DOCKER_IMAGE)) && docker cp $$ID:/data/aptfile_$(VERSION)_amd64.deb . && docker rm $$ID' + git checkout -- Dockerfile + +release: + @git status | grep -q "working directory clean" || (echo "You have uncomitted changes" && exit 1) + $(MAKE) deb + +.PHONY: shellcheck lint clean deb release diff --git a/tools/bash-aptfile/README.md b/tools/bash-aptfile/README.md new file mode 100644 index 00000000..0b83db6a --- /dev/null +++ b/tools/bash-aptfile/README.md @@ -0,0 +1,197 @@ +# bash-aptfile + +a simple method of defining apt-get dependencies for an application + +## requirements + +- apt-get +- dpkg + +## installation + +```shell +# curl all the things! +curl -o /usr/local/bin/aptfile https://raw.githubusercontent.com/seatgeek/bash-aptfile/master/bin/aptfile +chmod +x /usr/local/bin/aptfile +``` + +You can *also* create a debian package via docker using `make` with the provided `Dockerfile`. The debian package is made via the wonders of `fpm`: + +```shell +git clone https://github.com/seatgeek/bash-aptfile.git +cd bash-aptfile +make deb + +# sudo all the things! +sudo dpkg -i *.deb +``` + +## usage + +Create an `aptfile` in the base of your project: + +```shell +#!/usr/bin/env aptfile +# ^ note the above shebang + +# trigger an apt-get update +update + +# install some packages +package "build-essential" +package "git-core" +package "software-properties-common" + +# install a ppa +ppa "fkrull/deadsnakes-python2.7" + +# install a few more packages from that ppa +package "python2.7" +package "python-pip" +package "python-dev" + +# setup some debian configuration +debconf_selection "mysql mysql-server/root_password password root" +debconf_selection "mysql mysql-server/root_password_again password root" + +# install another package +package "mysql-server" + +# install a package from a download url +package_from_url "google-chrome-stable" "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" + +# you can also execute arbitrary bash +echo "🚀 ALL GOOD TO GO" +``` + +Now you can run it: + +```shell +# aptfile *does not* use sudo by default! +sudo aptfile + +# enable bash tracing +TRACE=1 sudo -E aptfile + +# you can also execute a specific aptfile +sudo aptfile path/to/your/aptfile + +# or make the file executable and run it directly +chmod +x path/to/your/aptfile +sudo ./aptfile +``` + +And you'll see the following lovely output: + +``` +Running update +[NEW] package build-essential +[NEW] package git-core +[NEW] package software-properties-common +[NEW] ppa fkrull/deadsnakes-python2.7 +[NEW] package python2.7 +[NEW] package python-pip +[NEW] package python-dev +[OK] set debconf line: mysql mysql-server/root_password password root +[OK] set debconf line: mysql mysql-server/root_password_again password root +[NEW] package mysql-server +[NEW] package google-chrome-stable +🚀 ALL GOOD TO GO +``` + +Note that `aptfile` runs uses `--force-confnew` - it will forcibly use the package's version of a conf file if a conflict is found. + +## aptfile primitives + +You can use any of the following primitives when creating your service's aptfile: + +### update + +Runs apt-get update: + +```shell +update +``` + +### package + +Installs a single package: + +```shell +package "git-core" +``` + +### package_from_url + +Installs a single package from a url: + +```shell +package_from_url "google-chrome-stable" "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" +``` + +### packagelist + +Installs a multiple packages in one apt call: + +```shell +packagelist "git-core" "gitsome" +``` + +### repository + +Installs an aptitude repository via `add-apt-repository`: + +```shell +repository "deb http://us.archive.ubuntu.com/ubuntu/ saucy universe multiverse" +repository "ppa:mozillateam/firefox-next" +``` + +### repository_file + +Installs an aptitude repository in `/etc/apt/sources.list.d`: + +```shell +# Add the exact line to /etc/apt/sources.list.d/google-chrome.list +repository_file "google-chrome" "deb http://dl.google.com/linux/chrome/deb/ stable main" +# without 'deb', suite (defaults to lsb_release -sc) and components (defaults to 'main') are added +# All three lines do the same (on xenial) +repository_file "git-lfs.list" "deb https://packagecloud.io/github/git-lfs/ubuntu/ xenial main" +repository_file "git-lfs.list" "https://packagecloud.io/github/git-lfs/ubuntu/ main" +repository_file "git-lfs.list" "https://packagecloud.io/github/git-lfs/ubuntu/" +``` + +### ppa + +The preferred method for installing a ppa as it properly handles not re-running `add-apt-repository`: + +```shell +ppa "mozillateam/firefox-next" +``` + +### debconf_selection + +Allows you to set a debconf selection: + +```shell +debconf_selection "mysql mysql-server/root_password password root" +``` + +## helper functions + +These helper functions can be used inside your custom aptfile. + +### log_fail + +Logs a message to standard error and exits. If this is called, the full output from the dpkg calls will be output as well for further inspection. + +```shell +log_fail "Unable to find the proper package version" +``` + +### log_info + +Outputs a message to stdout. + +```shell +log_info "🚀 ALL GOOD TO GO" +``` diff --git a/tools/bash-aptfile/bin/aptfile b/tools/bash-aptfile/bin/aptfile new file mode 100755 index 00000000..6e42f649 --- /dev/null +++ b/tools/bash-aptfile/bin/aptfile @@ -0,0 +1,292 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $TRACE ]] && set -x && export TRACE=$TRACE + +version() { + local VERSION="dev-master" + if [[ -f /var/lib/aptfile/VERSION ]]; then + VERSION=$(cat /var/lib/aptfile/VERSION) + fi + echo "aptfile $VERSION" +} + +usage() { + version + echo "Usage: aptfile " +} + +help() { + usage + echo + echo " is the path to a bash file with apt instructions." + echo + echo " -h, --help Display this help message" + echo " -v, --version Display the version number" + echo + echo " For more information, see https://github.com/seatgeek/bash-aptfile" + echo +} + +resolve_link() { + $(type -p greadlink readlink | head -1) "$1" +} + +abs_dirname() { + local cwd + local path="$1" + cwd="$(pwd)" + + while [ -n "$path" ]; do + cd "${path%/*}" + local name="${path##*/}" + path="$(resolve_link "$name" || true)" + done + + pwd + cd "$cwd" +} + +expand_path() { + { + cd "$(dirname "$1")" 2>/dev/null + local dirname="$PWD" + cd "$OLDPWD" + echo "$dirname/$(basename "$1")" + } || echo "$1" +} + +options=() +arguments=() +for arg in "$@"; do + if [ "${arg:0:1}" = "-" ]; then + if [ "${arg:1:1}" = "-" ]; then + options[${#options[*]}]="${arg:2}" + else + index=1 + while option="${arg:$index:1}"; do + [ -n "$option" ] || break + options[${#options[*]}]="$option" + let index+=1 + done + fi + else + arguments[${#arguments[*]}]="$arg" + fi +done + +for option in "${options[@]}"; do + case "$option" in + "h" | "help") + help + exit 0 + ;; + "v" | "version") + version + exit 0 + ;; + *) + usage >&2 + exit 1 + ;; + esac +done + +if [ "${#arguments[@]}" -eq 0 ]; then + APTFILE_PATH=$(expand_path "aptfile") + if [[ ! -f $APTFILE_PATH ]]; then + usage >&2 + exit 1 + fi + set -- "aptfile" "$@" +fi + +export APTFILE_COLOR_OFF="\033[0m" # unsets color to term fg color +export APTFILE_RED="\033[0;31m" # red +export APTFILE_GREEN="\033[0;32m" # green +export APTFILE_YELLOW="\033[0;33m" # yellow +export APTFILE_MAGENTA="\033[0;35m" # magenta +export APTFILE_CYAN="\033[0;36m" # cyan + +logfile=$(basename "$0") +TMP_APTFILE_LOGFILE=$(mktemp "/tmp/${logfile}.XXXXXX") || { + log_fail "${APTFILE_RED}WARNING: Cannot create temp file using mktemp in /tmp dir ${APTFILE_COLOR_OFF}\n" +} +export TMP_APTFILE_LOGFILE="$TMP_APTFILE_LOGFILE" +trap 'rm -rf "$TMP_APTFILE_LOGFILE" > /dev/null' INT TERM EXIT + +log_fail() { + [[ $TRACE ]] && set -x + echo -e "${APTFILE_RED}$*${APTFILE_COLOR_OFF}" 1>&2 + [[ -f "$TMP_APTFILE_LOGFILE" ]] && echo -e "verbose logs:\n" 1>&2 && sed -e 's/^/ /' "$TMP_APTFILE_LOGFILE" + exit 1 +} + +log_info() { + [[ $TRACE ]] && set -x + echo -e "$@" +} + +update() { + [[ $TRACE ]] && set -x + log_info "Running update" + apt-get update >"$TMP_APTFILE_LOGFILE" 2>&1 + [[ $? -eq 0 ]] || log_fail "Failed to run update" +} + +package() { + [[ $TRACE ]] && set -x + [[ -z $1 ]] && log_fail "Please specify a package to install" + local pkg="$1" + dpkg --force-confnew -s "$pkg" >"$TMP_APTFILE_LOGFILE" 2>&1 && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} package $pkg" && return 0 + apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" -qq -y install "$pkg" + [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} package $pkg" + log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} package $pkg" +} + +package_from_url() { + [[ $TRACE ]] && set -x + [[ -z $2 ]] && log_fail "Please specify a name and a download url to install the package from" + local name=$1 + local url=$2 + if type curl >/dev/null 2>&1; then + local dl_cmd="curl" + local dl_options="-so" + elif type wget >/dev/null 2>&1; then + local dl_cmd="wget" + local dl_options="-qO" + else + log_fail "Neither curl nor wget found. Unable to download $url" + fi + dpkg --force-confnew -s "$name" >"$TMP_APTFILE_LOGFILE" 2>&1 && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} package $name" && return 0 + tempdir=$(mktemp -d) + $dl_cmd $dl_options $tempdir/${name}.deb $url \ + && apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" -qq -y install "$tempdir/${name}.deb" + if [[ $? -ne 0 ]]; then + rm -r $tempdir + log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} package $name" + fi + rm -r $tempdir + log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} package $name" +} + +packagelist() { + [[ $TRACE ]] && set -x + [[ -z $1 ]] && log_fail "Please specify at least one package to install" + local input_packages=$@ + local install_packages=() + for pkg in $input_packages; do + dpkg --force-confnew -s "$pkg" >"$TMP_APTFILE_LOGFILE" 2>&1 && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} package $pkg" && continue + install_packages+=($pkg) + done + if [[ ${#install_packages[@]} -gt 0 ]]; then + apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" -qq -y install ${install_packages[@]} + [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} packages ${install_packages[@]}" + log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} packages ${install_packages[@]}" + fi +} + +ppa() { + [[ $TRACE ]] && set -x + [[ -z $1 ]] && log_fail "Please specify a repository to setup" + local repo="$1" + if [[ -d /etc/apt/sources.list.d/ ]]; then + grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep -q "$repo" && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} ppa $repo" && return 0 + fi + repository "ppa:$1" +} + +repository() { + [[ $TRACE ]] && set -x + [[ -z $1 ]] && log_fail "Please specify a repository to setup" + local repo="$1" + if [[ -d /etc/apt/sources.list.d/ ]]; then + grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep -Fq "$repo" && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} repository $repo" && return 0 + fi + add-apt-repository -y "$repo" >"$TMP_APTFILE_LOGFILE" 2>&1 + [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} repository $pkg" + update + log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} repository $repo" +} + +repository_file() { + [[ $TRACE ]] && set -x + [[ -z $2 ]] && log_fail "Please specify a filename and sourceline to setup" + local repofile="$1" + local repo="$2" + # sourceline is not a complete repo configuration, needs modifying + # i.e. not sourceline="deb http://domain.invalid/debian buster main extra" + if [[ "$repo" != "deb "* ]]; then + releasename=$(lsb_release -sc) + if [[ "$repo" == *" "* ]]; then + # Components given in sourceline, adding suite + # i.e. sourceline="http://domain.invalid/debian main" + repo="deb ${repo/ / $releasename }" + else + # only URL given, adding suite and component + # i.e. sourceline="http://domain.invalid/debian" + repo="deb ${repo} $releasename main" + fi + fi + + if [[ "$repofile" != *.list ]]; then + # Adding extension to enable parsing file + repofile=${repofile}.list + fi + # Adding path + repofile="/etc/apt/sources.list.d/$repofile" + + [[ -d "/etc/apt/sources.list.d" ]] || mkdir -p /etc/apt/sources.list.d + + grep ^ /etc/apt/sources.list /etc/apt/sources.list.d/* | grep -Fq "$repo" && log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} repository $repo" && return 0 + + echo "Writing '$repo' to file '$repofile'" >"$TMP_APTFILE_LOGFILE" + echo "$repo" >"$repofile" 2>>"$TMP_APTFILE_LOGFILE" + [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} repository $pkg" + update + log_info "${APTFILE_GREEN}[NEW]${APTFILE_COLOR_OFF} repository $repo" +} + +debconf_selection() { + [[ $TRACE ]] && set -x + [[ -z $1 ]] && log_fail "Please specify a debconf line" + echo "$1" | debconf-set-selections + [[ $? -eq 0 ]] || log_fail "${APTFILE_RED}[FAIL]${APTFILE_COLOR_OFF} debconf: $1" + log_info "${APTFILE_CYAN}[OK]${APTFILE_COLOR_OFF} set debconf line: $1" +} + +if [ -z "$TMPDIR" ]; then + APTFILE_TMPDIR="/tmp" +else + APTFILE_TMPDIR="${TMPDIR%/}" +fi + +APTFILE_INPUT=$(expand_path "$1") +APTFILE_TMPNAME="$APTFILE_TMPDIR/aptfile.$$" +APTFILE_OUTPUT="${APTFILE_TMPNAME}.out" + +aptfile_preprocess_source() { + tail -n +2 "$1" >"$APTFILE_OUTPUT" + trap "aptfile_cleanup_preprocessed_source" err exit + trap "aptfile_cleanup_preprocessed_source; exit 1" int +} + +aptfile_cleanup_preprocessed_source() { + rm -f "$APTFILE_TMPNAME" + rm -f "$APTFILE_OUTPUT" +} + +aptfile_preprocess_source "$APTFILE_INPUT" + +export -f update +export -f package +export -f package_from_url +export -f packagelist +export -f ppa +export -f repository +export -f repository_file +export -f debconf_selection +export -f log_fail +export -f log_info + +chmod +x "$APTFILE_OUTPUT" +exec "$APTFILE_OUTPUT"