I'm tired of makefiles
dmitryfrank.com
dmitryfrank.com
I also feel like good makefiles are usually very small and dont need much tweaking once they are in place.
I dont know your project structure but it may be advisable to actually split those builds completely or merge them together.
For the rebuilds you can just use the -B switch.
I am personally also a big fan of implicit compile and link rules, but I guess thats a matter of taste
Every time I've setup makefiles for projects someone always comes in and tries to make determining of dependencies happen before building rather than "determining dependencies as we build for the next build", which is just as robust. I don't know why software engineers always don't like that haha.
This only works for small projects. Once your project grows beyond a handful of files and dependencies this breaks down real fast.
I've been on this train more than once: most non-trivial projects reach a point where you have to build for multiple targets, support multiple configurations, test in all those weird configurations, manage CI, manage external dependencies, build said dependencies, etc. A makefile that supports all of the above becomes a monstrosity long before you get there and at some point you give up and pull in a real build system to take care of it (they're all bad in their own way, pick your poison really with cmake/meson/bazel/<new shiny thing>, just less bad than make).
Sure you can manage a giant pile of target-specific variables, platform-dependent scripts, and what not in make, but really it's much less painful to just bite the bullet and migrate to a build system that thought of these problems before. Things like "this dependency is mandatory is this configuration, optional in these other configurations, and is liked differently depending on whether we're building static or shared objects and whether we're using the platform library or falling back to a vendored version" is basically impossible in my experience.
Makefiles are great when your project is a dozen `.c` files. The moment you grow beyond that it's time to move on and use something else.
That said, it is *useful* which is why it survives. A working makefile will generally build your product from scratch efficiently, and rebuilt it as well if you're developing.
But the shame is, all the effort I've put into writing makefiles over the years is tremendous. Spread among all software writers, excessive.
I think if effort had been put into evolving make (instead of writing makefiles), it would have been the tide to raise all the boats.
some simple examples
- add a "expect makefile version > n.nn" statement
- fix tab vs spaces
- better conditional stuff (think if/then/else, etc), maybe just logic in general
- allow snippets of other languages (shell or python) without embedding everything into makefile syntax
- more conscious handling of real targets (files) vs directories vs fake targets (like "clean" in make clean)
- easier (for users) handling of paths/directories/files
- maybe well-defined plugins, say a cross-compile-for-arm toolchain or similar
- lots more I can't recall.
I think cmake is kind of nice in a lot of respect (although it has its own issues as you get further along solving cross-platform build problems)
make up
make down
make clean
make upgrade
make migrate
Easier than having to think about it
mything() {
# ...
}
otherthing() {
# ...
}
if [ $# -eq 0 ]; then
echo "Usage: ./run.sh [mything, otherthing]"
exit 1
fi
"$@"Each lifecycle hook should be a an executable of some sort like "scripts/{{command}}", be it a shell script or something else.
Make gives me a single, predictable and stable interface to the repo.
From the root of the repo, I can type `make <tab>` and get a list of available tasks. Alternatively, there's a Makefile on the root.
With scripts, I need to type... what? `./<tab>` and look for clues? Maybe the scripts are in `scripts/`, maybe they're in `build/`, maybe they're in `ci/`...
Even if I know where the scripts are, `make task` is quicker and easier to type than `./scripts/task.sh`.
One could just use a make task to call a script, which I have done, but in most cases if you do that, what's the point of the script?
For starters, not having to indent every line with a tab, put backslashes at the end of each line, or double every $ sigil in the script. Being able to transparently replace it with perl or python or some other non-shell-script is an occasional bonus.
The sibling's comment (re. just shell in files) is good too, and often I'll put all the various commands into a subdir, and `just` is just an interface to that, particularly so if a given script/command gets complex.
Then, `just` is really a signal "hey, this repo supports this interface" and things like `just -l` for discoverability.
I made an entire presentation about this[1], trying to sell it to work mates.
[1]: https://jiby.tech/presentation/makefiles/makefiles.html
It solves the cflags-changed problem by writing out the command used to build and updating it in the filesystem only if it has been changed (see "if_changed").
It has some external (to make) dependencies (a c program and numerous shell scripts) to make it work but it's great and can be relied upon!
It wouldn't be that crazy to get content-based dependencies working, especially looking at kbuild as an example.
> it's make for grownups
1. a way to track target freshness based on content hash
2. a way to declare variable values as dependencies
Python-based make-like tool.
cmake and ninja, which can both be installed from PyPI. There's also a compiled 1:1 clone of ninja called samurai (samu).
Meson is another python based alternative which doesn't bleed into the makefile equivalent files. I don't use it myself because it lacked assembler support at one time.
../bin/ok:
#!/usr/bin/env xonsh
"""
$> ok android shell
$> ok android ssh
$> ok android sshd
$> ok android sms
"""
import fire as _fire
class android:
ip = '...;
def shell(self):
adb connect f'{self.ip}:5555'
adb shell
def sshd(self):
adb connect f'{self.ip}:5555'
adb shell input keyevent 26
adb shell input swipe 500 800 500 300 200
sleep 1
adb shell am start -n com.termux/.app.TermuxActivity
adb shell input text "sshd"
adb shell input keyevent 66
adb shell input text "exit"
adb shell input keyevent 66
def ssh(self):
ssh -p 8022 f'user@{self.ip}'
def sms(self):
result = $(ssh -p 8022 f'user@{self.ip}' termux-sms-list -l 20 | jq ".[].body")
print(result)
def long_echo(): print(result)
@(long_echo) | llm -m llama3 -s 'please translate messages to English, quote the original messages too'
if __name__ == '__main__':
_fire.Fire()I'm going the Dagger path
This CNCF talk covers both: https://www.youtube.com/watch?v=nZLz0o4duRs
https://github.com/dimonomid/bulgaria-freelance-taxes/blame/...
Everything runs in a container
You can of course run whatever tool you want inside containerize workflows with Dagger, which can be much more than a build tool
Run Bazel in Dagger for consistency, or more generally build your binary in a container with Dagger using whatever tools you like. One main point of Dagger is the consistency between local & cloud for the build scripts, whereas containers have always been thought of as for consistency when running locally and cloud