Repository: wurosh/cake
Branch: master
Commit: 655731025695
Files: 8
Total size: 9.3 KB
Directory structure:
gitextract_2ahv7lf5/
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cake
└── subdir/
├── Dockerfile
├── Makefile
└── example.dockerfile
================================================
FILE CONTENTS
================================================
================================================
FILE: Dockerfile
================================================
FROM alpine
RUN apk add --no-cache make
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2021 Uros Perisic
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Makefile
================================================
PREFIX?=/usr/local
.PHONY=cat shell install test test-basic test-directory test-dockerfiles test-find-dockerfiles
cat:
cat /etc/os-release
shell:
/bin/sh
# run naked
test: test-basic test-directory test-dockerfiles test-find-dockerfiles
test-basic:
./cake
./cake cat
test-directory:
./cake -C subdir
./cake --directory subdir
./cake --directory=subdir
test-dockerfiles:
CAKE_DOCKERFILES='subdir/Dockerfile' ./cake
CAKE_DOCKERFILES='subdir/example.dockerfile subdir/Dockerfile' ./cake -C subdir
test-find-dockerfiles:
CAKE_DOCKERFILES='subdir/' ./cake
install:
@mkdir -p ${DESTDIR}${PREFIX}/bin
cp -f cake "${DESTDIR}${PREFIX}/bin" && \
chmod 755 "${DESTDIR}${PREFIX}/bin/cake"
================================================
FILE: README.md
================================================
🍰
## What is Cake?
Cake is a *really* thin, drop-in replacement/wrapper around `make` that runs all
of your targets inside of a development Docker/Podman container.
### Vision
- Though Cake supports more complex workflows, most projects that currently have
a `Makefile` at their root should also place a developer-focused `Dockerfile`
there for convenience and portability
- The `Makefile` is the single source of truth for the build process
- The `Dockerfile` is the single source of truth for the build environment
- A container runtime should not be a hard dependency to build the project.
- Choosing between containerized and "naked" builds should be as easy as typing
`make` or `cake` interchangeably
- CI/CD pipelines should be able to reuse the instructions from the `Makefile`
in an ergonomic way without having to keep the build context in mind
## Why Cake?
Because I found myself constantly writing Makefiles that run their targets in a
container, then adding in add-hoc ways for people not to use the container
through environment variables, followed by a half-hearted attempt at
optimizations through bind-mounts and less frequent restarts, and some faulty
logic to avoid name and tag clashes. I figured it was time to extract this into
a script. Despite its simplicity, the script covers 99% of my use cases for
tools like [act](https://github.com/nektos/act) without being tied to a specific
forge.
## How-To
Just use `cake` instead of `make`. The defaults should fit most use cases.
If you really have to, you can specify additional `docker`/`podman` arguments
using `$CAKE_RUNTIME_ARGS`. I recommend placing these in your
[.envrc](https://direnv.net/) if you need them to stick around due to the
specific needs of your project.
If you're building/testing your software against multiple environments, you can
always set `$CAKE_DOCKERFILES` (defaults to Make's `${PWD}/Dockerfile` - which
is not necessarily the same as your shell's `${PWD}/Dockerfile`). This will run
your Make targets in one container per `Dockerfile`. If `$CAKE_DOCKERFILES` is a
directory, all `Dockerfile`s in that directory (and all of its sub-directories)
will be used. This is the one area in which Cake diverges from Make. You have to
specify Cake-relevant environment variables before the command, not after. You
can take a look at some of my test cases for example invocations:
``` sh
cake
cake all
cake -C subdir
CAKE_DOCKERFILES='subdir/' cake
CAKE_DOCKERFILES='subdir/Dockerfile' cake
CAKE_DOCKERFILES='subdir/one.dockerfile subdir/Dockerfile' cake
```
## Tips
If I want to debug my development container, I like to add a `shell` target
to my `Makefile` like so:
``` makefile
shell:
/bin/sh
```
It's more ergonomic then copying the container name.
The same goes for dealing with things like `./autogen.sh` and the `./configure`
script (often managed directly by the user). I tend to call those through a
`Makefile` as well. Take this snippet from the `GNUMakefile` in the Emacs source
tree as an example:
``` makefile
configure:
@echo >&2 'There seems to be no "configure" file in this directory.'
@echo >&2 Running ./autogen.sh ...
./autogen.sh
@echo >&2 '"configure" file built.'
Makefile: configure
@echo >&2 'There seems to be no Makefile in this directory.'
@echo >&2 'Running ./configure ...'
./configure
@echo >&2 'Makefile built.'
# 'make bootstrap' in a fresh checkout needn't run 'configure' twice.
bootstrap: Makefile
$(MAKE) -f Makefile all
```
### Why POSIX sh
Because additional dependencies are a problem, especially in corporate
environments. just `curl`/copy this script into a directory on your `$PATH` and
you're good to go.
## Completions
I might provide them for convenience later, but in principle all you need to do
is reuse existing make completions. In `zsh` that looks something like this:
``` zsh
compdef _make cake
```
================================================
FILE: cake
================================================
#!/bin/sh
log() {
if [ -t 1 ]; then
case $1 in
error) log_type="\e[1mcake: \e[31merror:\e[0m" ;;
warning) log_type="\e[1mcake: \e[33mwarning:\e[0m" ;;
info) log_type="\e[1mcake: \e[32minfo:\e[0m" ;;
esac
else
case $1 in
error) log_type="cake: error:" ;;
warning) log_type="cake: warning:" ;;
info) log_type="cake: info:" ;;
esac
fi
shift
printf "%b %s\n" "$log_type" "$1"
}
set_runtime() {
if type docker > /dev/null; then
log info 'using docker'
runtime=docker
elif type podman > /dev/null; then
log info 'using podman'
runtime=podman
else
log error 'cannot use docker or podman'
exit 1
fi
}
set_directory() {
# posixly parse first occurrence of -C dir/--directory dir/--directory=dir
# NOTE: consider removing the first directory argument and bind-mounting
# `$directory` instead of `$PWD` in the future - its slightly less tolerant
# but more consistent
while getopts ":C:-:" o; do
# NOTE: we're passing unknown arguments through
# shellcheck disable=SC2220
case "$o" in
C) directory="$OPTARG"; break;;
-) [ $OPTIND -ge 1 ] && optind=$((OPTIND - 1)) || optind=$OPTIND
eval option="\$$optind"
if [ "${option#*=}" != "$option" ]; then
# --option=arg style
optarg=$(echo "$option" | cut -d '=' -f 2)
option=$(echo "$option" | cut -d '=' -f 1)
if [ "$option" = '--directory' ]; then
directory="$optarg"
break
fi
else
# --option arg style
if [ "$option" = '--directory' ]; then
optind=$((optind + 1))
eval directory="\$$optind"
break
fi
fi;;
esac
done
if [ -z "$directory" ]; then
directory="$PWD"
else
if [ -d "$directory" ]; then
directory=$(cd "$directory" && echo "$PWD")
else
log error "'$directory' is not a directory"
exit 1
fi
fi
}
set_dockerfiles() {
if [ -d "$CAKE_DOCKERFILES" ]; then
dockerfiles=$(find "$CAKE_DOCKERFILES" \( -name Dockerfile -o -name '*.dockerfile' \))
else
dockerfiles="${CAKE_DOCKERFILES:-${directory}/Dockerfile}"
fi
}
run_command() {
set_runtime
set_directory "$@"
set_dockerfiles
for dockerfile in $dockerfiles; do
# NOTE: it's not enough to just use the `$dockerfile` as a different build
# context could result in a different container
checksum=$(echo "${directory}" "${dockerfile}" | cksum | cut -d ' ' -f 1)
basename=$(basename "${directory}")
container="cake-${basename}-${checksum}"
log info "running 'make $*'"
log info "in container '$container'"
log info "using Dockerfile '$dockerfile'"
log info "in directory '$directory'"
if [ -t 1 ]; then
tty_args='-it'
fi
if "$runtime" build -t "$container" -f "$dockerfile" "$directory" > /dev/null; then
# NOTE: we want `$CAKE_RUNTIME_ARGS` to be split and no $tty_args to be ignored
# shellcheck disable=SC2086
"$runtime" run -v "${PWD}:${PWD}" -w "$PWD" --rm \
$tty_args $CAKE_RUNTIME_ARGS \
"$container" make "$@"
fi
done
}
run_command "$@"
================================================
FILE: subdir/Dockerfile
================================================
FROM alpine
RUN apk add --no-cache make
================================================
FILE: subdir/Makefile
================================================
.PHONY=cat
cat:
cat /etc/os-release
================================================
FILE: subdir/example.dockerfile
================================================
FROM alpine
RUN apk add --no-cache make