It looks like this:
default: your_task
VENV_DIR := venv
$(VENV_DIR)/bin/python: requirements.txt
python -m venv $(VENV_DIR)
. $(VENV_DIR)/bin/activate && pip install -r requirements.txt
your_task: $(VENV_DIR)/bin/python
do_your_thing
clean:
rm -rf $(VENV_DIR)
> C and C++ compilers at this stage are the only places that I work with file based dependencies. Unless you explicitly declare all the headers as dependencies in a makefile, make won't rebuild a cpp file if you change a header. IME ninja (plus cmake to generate it) has entirely superseded make in this space. Tools and languages comes with it's own build tool (see above - go/cargo/dotnet/etc) or state tracking (docker) that break make's assumption of file based dependencies.In our Python example above, you have a file-based dependency between requirements.txt and your virtual environment. And then you have a file-based dependency on your virtual-environment's Python for whatever task you're trying to run.
> Because every make file I've ever used in recent memory has been 30% workarounds to avoid file based dependencies, and work around subtle footguns that make has, rather than actually doing what I want it to do - execute a build command.
Why would you want to avoid expressing your dependencies? File dependencies exist, and are the simplest / most correct model for 75% of all build automation. When somebody has to sit down and read your Makefile, isn't it nice to have a way to express "these are the files that actually matter for task X"?
GOOS=darwin GOARCH=arm64 go build -ldflags="-X 'main.Version=v1.2.3'"
Executing this with variables for os/arch/version is not straightforward in a makefile. You might argue that it's a flaw with the `go build` command, but replace go with cargo/dotnet/maven and you have the same problem.What makes that hard? I think the following works correctly:
$ cat Makefile
goos := FOO
goarch := BAR
mainversion := BAZ
.PHONY: build
build: ; @GOOS=$(goos) GOARCH=$(goarch) go build -ldflags="-X 'main.Version=v$(mainversion)'"*
$ make -n
GOOS=FOO GOARCH=BAR go build -ldflags="-X 'main.Version=vBAZ'"This is pretty easy in Make, especially since GOOS and GOARCH are environment variables. One way to do it could look like this:
export GOOS ?= darwin
export GOARCH ?= arm64
VERSION ?= 0.0.0+$(shell git describe --dirty --always)
build:
$(if $(VERSION),,$(error VERSION was not set))
go build -ldflags="-X 'main.Version=v$(VERSION)'"
you could run it as: $ make build (uses darwin/arm64/0.0.0+<git ref>)
$ make build GOOS=linux VERSION=1.0.0 (uses linux/arm64/1.0.0)
$ GOOS=templeos GOARCH=sparc make build VERSION=1.1.0 (uses templeos/sparc/1.1.0)
> AFAIK, Just doesn't have any way to mark "this task is complete, so don't re-run it"
What does a makefile look like for "install this list of dependencies with pip/homebrew/apt, and don't run it twice?"
> It also doesn't have file-based dependendicies, which are pretty common for any kind of programming
C and C++ compilers at this stage are the only places that I work with file based dependencies. Unless you explicitly declare all the headers as dependencies in a makefile, make won't rebuild a cpp file if you change a header. IME ninja (plus cmake to generate it) has entirely superseded make in this space. Tools and languages comes with it's own build tool (see above - go/cargo/dotnet/etc) or state tracking (docker) that break make's assumption of file based dependencies.
> Why would I want to lose all of Make's flexibility and power in exchange for slightly prettier UI syntax?
Because every make file I've ever used in recent memory has been 30% workarounds to avoid file based dependencies, and work around subtle footguns that make has, rather than actually doing what I want it to do - execute a build command.