Tup – an instrumenting file-based build system
gittup.org
gittup.org
"make install" for example is specifically something Tup can't do.
This is trivially solved by having "install.sh"
There are also various limitations, caveats, &c. such that I typically had a "build.sh" or some-such that did any necessary pre-tup work.
The most restrictive rule is the set of output pathnames cannot depend on the contents of input files; they must be fully realizable from just the set of input pathnames. Calculating this outside of tup is fine even if it's expensive, as long as it changes rarely. Tup will error if the list is wrong, so you can't accidentally build with out-of-date outputs.
`make` is different things to different people. What you describe, `make` as a tool to compute optimal strategies for recursive file-based build operations, is indeed its original purpose.
On the other hand, I tend to use `make` more as a standardized format for script collections. Whenever I open a new repository, and it has a Makefile in it, I'm very thankful because it's a useful jumping-off point for how to build and use the software contained in the repository. Therefore I do the same in my own projects. Even if the project is in a single language with established conventions, I tend to include a Makefile regardless as a way of documenting.
For example, if I have a Go project, I could rely on other people looking into it to know how Go is compiled, or I can just put in a little Makefile like:
PREFIX = /usr
build/foo:
go build -ldflags '-s -w' -o $@ ./cmd/foo
install: build/foo
install -D -m 0755 build/foo "$(DESTDIR)$(PREFIX)/bin/foo"
.PHONY: build/foo install
Which will make it entirely obvious to everyone, including non-Go-developers, how to build and install the software.> There are also various limitations, caveats, &c. such that I typically had a "build.sh" or some-such that did any necessary pre-tup work.
Frankly if I need to write custom wrapper scripts for a build system then the build system isn't up to scratch. On every project I work on, you can run
make list
To get a list of targets, and build/install/predeps/deploy are all defined. On any of of our projects, make predeps && make build will output a deployable binary.> Calculating this outside of tup is fine even if it's expensive, as long as it changes rarely.
So no include files? That's another deal-breaker!
One example of what I'm talking about is what another commentor mentioned: .java files will generate separate .class files for each inner-class in the file, so given "foo.java" you can't tell which .class files will be output when compiling "foo.java" without looking at the contents of "foo.java"
I actually originally had a Makefile with predeps/build/install targets where the "build" step just called tup. People complained that "if tup is supposed to replace make, why are you using make?" I switched it to predeps.sh/build.sh/install.sh and everybody shut up. I personally think it's less ergonomic but don't care enough to argue with a different person every week.
I'm with you here!
Huh? This definitely does work with Tup as expected.
Tup monitors any files that are read while the build command is running, and implicitly adds them as dependencies.
If you are a heavy shell user you'll also appreciate that tup can be executed from any subdir within the project, no need to keep around extra terminal just for compiling.
- Does the reported ninja time include the cmake step that generates the ninja files? (which typically doesn't need to be done for incremental builds).
- "make -j" doesn't limit the number of jobs, so you can run into resource contention if building too many things at once. Ninja (by default) limits the parallel jobs to the number of CPUs.
- Disabling make's built-in rules (with `-r -R`) can make a big difference to performance.
- Make spends a lot of time processing the ".d" files with header dependency information -- 98% of the time in my (artificial) benchmarks!
However, they do add a lot of bulk once made and increase with the size of the project.
A lot of the information is redundant. Among the files you may find many pairs which only differ in the principal file, the headers being the same:
foo.o: foo.c $SOME_HEADERS
bar.o: bar.c $SOME_HEADERS
In theory, if we have GNU Make, we should be able to compress these into static pattern rules. Concretely, in the above case, the pair condenses to the static pattern rule: foo.o bar.o: %.o: %.c $SOME_HEADERS
When, we check whether, say, bar.o is out of date, we find that it as a target in the left side of the rule. Then in the middle, the name matches the %.o target pattern, identifying the stem as "bar", and from there the full dependency rule is instantiated with all the prerequisites.To generalize that, we partition the $(OBJECTS) into subsets based on which patterns they belong to. Hopefully there are a lot fewer subsets than files.
Then for each subset we do
# The a.h b.h subset
$(OBJS_0): %.o: %.c a.h b.h
# The a.h d.h subset
$(OBJS_1): %.o: %.c a.h d.h
and so on. The only problem is managing these subsets (in particular, incrementally) in such a way that more time isn't spent doing that than time saved loading the rules.Stale dependencies can be troublesome when header files are renamed or deleted; this sort of scheme could make it worse.
One way to handle these subsets incrementally would be to keep them in some separate database outside of the Makefile. Whenever a file is compiled and the raw .d is generated, we run a script which looks up the header signature and matches it to a subset. Then it assigns that file to the correct subset (perhaps deleting it from an existing one). A step at the start of the build would regenerate the dependency file seen by make if the database indicates that it's out of date.
I included the time to generate ninja files, the measured time is for project to build from scratch. Tup generates its initial files _much_ faster (~0.2 seconds compared to ~4 seconds). The strength of tup is actually in incremental builds, but that's harder to measure: you get shorter times with larger relative variations between repeated tries.
I wasn't interested in working around the bad make defaults, I also didn't spend any time optimizing tup flags.
I say this because in my use case ninja spends almost all of its time running compiler processes, not resolving deps.
It's hard to say how much impact this would have in real-world scenarios.
[1]: https://snakemake.readthedocs.io/en/stable/
[2]: https://github.com/DarwinAwardWinner/CD4-csaw
[3]: https://github.com/DarwinAwardWinner/resume/blob/main/Snakef...
• Targets/outputs must be files, without even the opportunity to extend the space of possible targets e.g. using your own hash comparison function. This is not suitable for a workflow where the output is a directory of filenames that aren't known in advance, or a database table.
• By default, can only run commands through a shell, not directly as a subprocess. At the same time, it also lacks support for nontrivial chaining and composition of commands. In your CD4 example you hack around this by literally writing your own pipelining command runner in Python; hard to say if that's any better than a big blob of shell script in a string!
I agree that it definitely "makes hard things possible", but my experience did not give me the impression that it "makes easy things easy". It was frustratingly deficient in what I believe are obvious areas for improvement, and some of the most obnoxious Make pain points that I encounter on a regular basis, even for regular day-to-day software development.
I ended up switching from Snakemake to DVC basically as soon as it came out, because it at least supports using directories as outputs, and has a few other nice features that specifically work well for data science project version control and artifact management/sharing for collaboration. That said, I had/have much less of a need for complicated workflow orchestration than for easy tracking & sharing of datasets and large artifacts (fitted models, processed tabular datasets), and for tracking directories as outputs (partitioned Parquet datasets).
It would be interesting to go back and re-evaluate the space of workflow DSLs and task runners, because DVC does not have the complete feature set that Snakemake has, and IMO nor should it (I am already getting worried about its feature creep). I'm also not sure if or how well Snakemake and DVC could be made to work together; the former for task running and the latter for task input/output tracking with version control. The perfect tool (or better yet, the perfect combination of tools) remains elusive.
Would it be possible for Dud to push/pull from a DVC remote and use the DVC shared cache? That would be really useful so I (iconoclastic free software user) could use Dud on my machine/acocunt, but still share data and artifacts with other people (who don't give a shit what tool they use) using DVC on their machines/accounts.
Also: Does Dud support reflinks at all? Or does it only support symlinks?
Dud currently does not support reflinks, but I think adding reflink support would be fairly straight-forward. Just curious: What filesystem and OS are you using for reflinks?
I'd be happy to chat more about this. Feel free to open GitHub issues for these items. I welcome contributions as well. ;)
And I guess I'm kind of grading "easy" on a curve here, because the first build tool I tried for bioinformatics was GNU Make, and it was not up to the task, even when I exploited all the special GNU features to the hilt.
Tup fit nicely, until build variants or Java were needed.
Aside from not really working well on Windows, at least at the time, variants were completely broken on Windows.
Further, compiling any Java with inner classes was impossible since they emitted extra class files for each inner class, which meant the build artifact hierarchy depended on the code - something Tup isn't prepared to handle.
It just got really annoying over time.
Ninja-based systems quickly replaced it for me.
No need to map header (or other) files to C files. Tup detects when files are read and adds them as implicit dependencies.
Environment variables (that are used) are considered build inputs, so no need to `make clean` when you need a build with debug symbols.
---
Admittedly my project is quite straightforward, but I thoroughly recommend giving Tup a try.
[2] http://websvn.kolibrios.org/listing.php?repname=Kolibri+OS