├── 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 |
--------------------------------------------------------------------------------