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