├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cake └── subdir ├── Dockerfile ├── Makefile └── example.dockerfile /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk add --no-cache make 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Uros Perisic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX?=/usr/local 2 | .PHONY=cat shell install test test-basic test-directory test-dockerfiles test-find-dockerfiles 3 | 4 | cat: 5 | cat /etc/os-release 6 | 7 | shell: 8 | /bin/sh 9 | 10 | # run naked 11 | test: test-basic test-directory test-dockerfiles test-find-dockerfiles 12 | 13 | test-basic: 14 | ./cake 15 | ./cake cat 16 | 17 | test-directory: 18 | ./cake -C subdir 19 | ./cake --directory subdir 20 | ./cake --directory=subdir 21 | 22 | test-dockerfiles: 23 | CAKE_DOCKERFILES='subdir/Dockerfile' ./cake 24 | CAKE_DOCKERFILES='subdir/example.dockerfile subdir/Dockerfile' ./cake -C subdir 25 | 26 | test-find-dockerfiles: 27 | CAKE_DOCKERFILES='subdir/' ./cake 28 | 29 | install: 30 | @mkdir -p ${DESTDIR}${PREFIX}/bin 31 | cp -f cake "${DESTDIR}${PREFIX}/bin" && \ 32 | chmod 755 "${DESTDIR}${PREFIX}/bin/cake" 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

🍰

2 | 3 | ## What is Cake? 4 | Cake is a *really* thin, drop-in replacement/wrapper around `make` that runs all 5 | of your targets inside of a development Docker/Podman container. 6 | 7 | ### Vision 8 | - Though Cake supports more complex workflows, most projects that currently have 9 | a `Makefile` at their root should also place a developer-focused `Dockerfile` 10 | there for convenience and portability 11 | - The `Makefile` is the single source of truth for the build process 12 | - The `Dockerfile` is the single source of truth for the build environment 13 | - A container runtime should not be a hard dependency to build the project. 14 | - Choosing between containerized and "naked" builds should be as easy as typing 15 | `make` or `cake` interchangeably 16 | - CI/CD pipelines should be able to reuse the instructions from the `Makefile` 17 | in an ergonomic way without having to keep the build context in mind 18 | 19 | ## Why Cake? 20 | Because I found myself constantly writing Makefiles that run their targets in a 21 | container, then adding in add-hoc ways for people not to use the container 22 | through environment variables, followed by a half-hearted attempt at 23 | optimizations through bind-mounts and less frequent restarts, and some faulty 24 | logic to avoid name and tag clashes. I figured it was time to extract this into 25 | a script. Despite its simplicity, the script covers 99% of my use cases for 26 | tools like [act](https://github.com/nektos/act) without being tied to a specific 27 | forge. 28 | 29 | ## How-To 30 | Just use `cake` instead of `make`. The defaults should fit most use cases. 31 | 32 | If you really have to, you can specify additional `docker`/`podman` arguments 33 | using `$CAKE_RUNTIME_ARGS`. I recommend placing these in your 34 | [.envrc](https://direnv.net/) if you need them to stick around due to the 35 | specific needs of your project. 36 | 37 | If you're building/testing your software against multiple environments, you can 38 | always set `$CAKE_DOCKERFILES` (defaults to Make's `${PWD}/Dockerfile` - which 39 | is not necessarily the same as your shell's `${PWD}/Dockerfile`). This will run 40 | your Make targets in one container per `Dockerfile`. If `$CAKE_DOCKERFILES` is a 41 | directory, all `Dockerfile`s in that directory (and all of its sub-directories) 42 | will be used. This is the one area in which Cake diverges from Make. You have to 43 | specify Cake-relevant environment variables before the command, not after. You 44 | can take a look at some of my test cases for example invocations: 45 | 46 | ``` sh 47 | cake 48 | cake all 49 | cake -C subdir 50 | CAKE_DOCKERFILES='subdir/' cake 51 | CAKE_DOCKERFILES='subdir/Dockerfile' cake 52 | CAKE_DOCKERFILES='subdir/one.dockerfile subdir/Dockerfile' cake 53 | ``` 54 | 55 | 56 | ## Tips 57 | 58 | If I want to debug my development container, I like to add a `shell` target 59 | to my `Makefile` like so: 60 | ``` makefile 61 | shell: 62 | /bin/sh 63 | ``` 64 | It's more ergonomic then copying the container name. 65 | 66 | 67 | The same goes for dealing with things like `./autogen.sh` and the `./configure` 68 | script (often managed directly by the user). I tend to call those through a 69 | `Makefile` as well. Take this snippet from the `GNUMakefile` in the Emacs source 70 | tree as an example: 71 | 72 | ``` makefile 73 | configure: 74 | @echo >&2 'There seems to be no "configure" file in this directory.' 75 | @echo >&2 Running ./autogen.sh ... 76 | ./autogen.sh 77 | @echo >&2 '"configure" file built.' 78 | 79 | Makefile: configure 80 | @echo >&2 'There seems to be no Makefile in this directory.' 81 | @echo >&2 'Running ./configure ...' 82 | ./configure 83 | @echo >&2 'Makefile built.' 84 | 85 | # 'make bootstrap' in a fresh checkout needn't run 'configure' twice. 86 | bootstrap: Makefile 87 | $(MAKE) -f Makefile all 88 | ``` 89 | 90 | ### Why POSIX sh 91 | Because additional dependencies are a problem, especially in corporate 92 | environments. just `curl`/copy this script into a directory on your `$PATH` and 93 | you're good to go. 94 | 95 | ## Completions 96 | I might provide them for convenience later, but in principle all you need to do 97 | is reuse existing make completions. In `zsh` that looks something like this: 98 | ``` zsh 99 | compdef _make cake 100 | ``` 101 | 102 | 103 | -------------------------------------------------------------------------------- /cake: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | log() { 4 | if [ -t 1 ]; then 5 | case $1 in 6 | error) log_type="\e[1mcake: \e[31merror:\e[0m" ;; 7 | warning) log_type="\e[1mcake: \e[33mwarning:\e[0m" ;; 8 | info) log_type="\e[1mcake: \e[32minfo:\e[0m" ;; 9 | esac 10 | else 11 | case $1 in 12 | error) log_type="cake: error:" ;; 13 | warning) log_type="cake: warning:" ;; 14 | info) log_type="cake: info:" ;; 15 | esac 16 | fi 17 | shift 18 | printf "%b %s\n" "$log_type" "$1" 19 | } 20 | 21 | set_runtime() { 22 | if type docker > /dev/null; then 23 | log info 'using docker' 24 | runtime=docker 25 | elif type podman > /dev/null; then 26 | log info 'using podman' 27 | runtime=podman 28 | else 29 | log error 'cannot use docker or podman' 30 | exit 1 31 | fi 32 | } 33 | 34 | set_directory() { 35 | # posixly parse first occurrence of -C dir/--directory dir/--directory=dir 36 | # NOTE: consider removing the first directory argument and bind-mounting 37 | # `$directory` instead of `$PWD` in the future - its slightly less tolerant 38 | # but more consistent 39 | while getopts ":C:-:" o; do 40 | # NOTE: we're passing unknown arguments through 41 | # shellcheck disable=SC2220 42 | case "$o" in 43 | C) directory="$OPTARG"; break;; 44 | -) [ $OPTIND -ge 1 ] && optind=$((OPTIND - 1)) || optind=$OPTIND 45 | eval option="\$$optind" 46 | if [ "${option#*=}" != "$option" ]; then 47 | # --option=arg style 48 | optarg=$(echo "$option" | cut -d '=' -f 2) 49 | option=$(echo "$option" | cut -d '=' -f 1) 50 | if [ "$option" = '--directory' ]; then 51 | directory="$optarg" 52 | break 53 | fi 54 | else 55 | # --option arg style 56 | if [ "$option" = '--directory' ]; then 57 | optind=$((optind + 1)) 58 | eval directory="\$$optind" 59 | break 60 | fi 61 | fi;; 62 | esac 63 | done 64 | 65 | if [ -z "$directory" ]; then 66 | directory="$PWD" 67 | else 68 | if [ -d "$directory" ]; then 69 | directory=$(cd "$directory" && echo "$PWD") 70 | else 71 | log error "'$directory' is not a directory" 72 | exit 1 73 | fi 74 | fi 75 | } 76 | 77 | set_dockerfiles() { 78 | if [ -d "$CAKE_DOCKERFILES" ]; then 79 | dockerfiles=$(find "$CAKE_DOCKERFILES" \( -name Dockerfile -o -name '*.dockerfile' \)) 80 | else 81 | dockerfiles="${CAKE_DOCKERFILES:-${directory}/Dockerfile}" 82 | fi 83 | } 84 | 85 | run_command() { 86 | set_runtime 87 | set_directory "$@" 88 | set_dockerfiles 89 | 90 | for dockerfile in $dockerfiles; do 91 | # NOTE: it's not enough to just use the `$dockerfile` as a different build 92 | # context could result in a different container 93 | checksum=$(echo "${directory}" "${dockerfile}" | cksum | cut -d ' ' -f 1) 94 | basename=$(basename "${directory}") 95 | container="cake-${basename}-${checksum}" 96 | 97 | log info "running 'make $*'" 98 | log info "in container '$container'" 99 | log info "using Dockerfile '$dockerfile'" 100 | log info "in directory '$directory'" 101 | 102 | if [ -t 1 ]; then 103 | tty_args='-it' 104 | fi 105 | 106 | if "$runtime" build -t "$container" -f "$dockerfile" "$directory" > /dev/null; then 107 | # NOTE: we want `$CAKE_RUNTIME_ARGS` to be split and no $tty_args to be ignored 108 | # shellcheck disable=SC2086 109 | "$runtime" run -v "${PWD}:${PWD}" -w "$PWD" --rm \ 110 | $tty_args $CAKE_RUNTIME_ARGS \ 111 | "$container" make "$@" 112 | fi 113 | done 114 | } 115 | 116 | run_command "$@" 117 | -------------------------------------------------------------------------------- /subdir/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk add --no-cache make 4 | -------------------------------------------------------------------------------- /subdir/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY=cat 2 | 3 | cat: 4 | cat /etc/os-release 5 | -------------------------------------------------------------------------------- /subdir/example.dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk add --no-cache make 4 | --------------------------------------------------------------------------------