Hacker Read top | best | new | newcomments | leaders | about | bookmarklet login

> Though almost always, "run tasks" implies some level of state.

Yes! But isn't one of the major design principles of Make that the host filesystem is the container for the state? (iirc Make decides when to re-run rules based on when the timestamp on a target is older than the timestamp on an input file)



sort by: page size:

> Most (all?) of your Makefile targets don't represent actual files to 'make'.

> You're using make to run a bunch of tasks, including where tasks may depend on other tasks having been run before.

And the cool thing is....it works for both!

It can represent an actual file (e.g. a compiled binary) or abstract result (e.g., a successful test).


> ...idempotent, or at least stateless...

Even for a Makefile, "start", "test", "install" are likely to be phony (and not literally refer to files with those names).

The execution of a "start" task defers the role of "has it already been done" to other places.

One way of describing "task runner" is "nicer UX for .phony targets".


> When my shell scripts depend on another script ... they run the other script.

I hear you, but you're running the other script unconditionally. If it downloads something, it will download it every time you run the first script.

In this simple case, make runs the other script conditionally, so it need not run every time.


> A pet peeve of mind is having to "make clean" between a debug and a release build, and nearly all usages of Make have that problem, e.g. Python and bash's build system. You could say they are violating your rules because each artifact doesn't have a unique name on the file system (i.e. debug and release versions of the same object file.)

There are at least two ways that this problem can be addressed. One is to support out-of-tree builds, one side directory per configuration. Builds based on the autotools do this by default.

The other is to use a separate build directory per configuration within the build. My current project uses local in-tree directories named .host-release, .host-debug (including asan), .host-tsan, .cross-release, and .cross-debug. All of them are built in parallel with a single invocation of Make, and I use target-scoped variables to control the various toolchain options.

The engineer's incremental work factor to add another build configuration isn't quite constant time, since each top-level target needs to opt into each build configuration that is relevant for that target.

> I hit the multiple outputs problem

I wouldn't really classify that as a problem in GNU Make, as long as you can specify the rule as a pattern rule.

I hear you on the testing problem. Make certainly behaves as if the Makefile's correctness isn't decidable. Even if you levied the requirement that a Makefile's decidability was predicated on the recipes being well-behaved, I'm not sure that the correctness is decidable.


> Every time I type "make" in an Autotools program only my code runs.

Says who? Make is just as good at calling arbitrary code as Cargo. Including code that reaches out over the network. Have you audited every single makefile to ensure that isn't the case?


> A typical example that most handcrafted Makefiles will fail is that changing a build flag should only rebuild the files affected.

If you change a build flag, how aren't all the files affected?


> The real insanity with make is that it has no interface to check whether a Makefile contains a given target.

>

> Best you can do is to run make -n target to probe, but it's possible to write Makefiles that run code even though -n is used, which will defeat such probes at distribution scale. It quickly becomes an exercise in heuristics and output parsing.


> and using patterns to save keystrokes rather than match multiple files.

To be fair, I'm still yet to see a document clarifying how to use patterns with make.


> If you find yourself running "make clean" because it is "too dumb" you've built your Makefile wrong.

What's built wrong is make itself. Because the signal it uses for "this dependency has changed" is the mtime, any change which does not move the mtime forwards will be ignored. Switching between concurrent git branches, for instance.

> Usually it means you've tried to manually re-create the inherent Make rules and have gotten it wrong somewhere.

And pray tell how do you "manually re-create the implicit make rules" when such rules don't exist? GNU Make has implicit rules for C, C++, Pascal, Fortran, Raftor, Modula-2, and assembly. That is, frankly, not much. And mostly outdated.


> The completely static nature of a makefile also means it is ill-suited to rapidly evolving codebases where new files come and go almost by the minute as we refactor.

Make is so static that there was a Lisp interpreter written in make. Oh wait...

There are several mechanisms in make to have your rules dynamically adapt to your codebase, e.g. $(shell ...), $(wildcard ...), and pattern rules (and that's not all of them).

You just need to put a little effort to actually read the documentation, not just stop at finishing one of the plethora tutorials that stop after showing variables usage.

> We can do much better than Make.

You mean, "we can do much better than my `make' knowledge". Of course we can.


Naming isn’t consistent with what make does and why it’s called Makefile.

Makefile is for make, which “makes files”.

You don’t “task files”

*file in general is wrong. Of course it’s a file


> You'd have to do something like:

That's exactly right, but notice that you are not actually using make at all any more at this point, except as a vehicle to run

check_query.sh && doaction

which is doing all work.


> If you have an atomic step that produces multiple targets, it confuses make, and encoding the fact properly in the Makefile is really not straightforward.

Make does have a lot of warts, but this isn’t one of them any more. Grouped targets were added in the most recent release of GNU Make, and they address exactly this problem.

Re: your other points, many of them boil down to “the syntax is weird”. I agree 100%, and also it’s not so tough once you get used to it. I use GNU Make for tons of things, and I like it a lot.


> Nothing stops you from creating an interface that maps that row to a file.

That's true, nothing stops you, though it is worth noting that no one actually does this, and there's a reason for that. So suppose you did this; how are you going to use that in a makefile?


> Make has a bunch of cryptic magic variables that refer to things like the targets and prerequisites of rules. I mostly think these should be avoided, because they are hard to read.

> However, for the sentinel file pattern, the magic variable $(@D), which refers to the directory the target should go in, and $@, which refers to the target, are common enough that you quickly learn to recognize what they mean:

So, avoid using the magic variables, but actually you should use them because they're useful and common. Got it.


> The Makefile abstraction is "run this command when the output files don't exist or are older than the input files." You manually specify every tool to be run and all of its arguments.

> The BUILD file abstraction is something like "I am declaring a C library with these sources and headers."

This is wrong. Even ninja has generic rules. Here's an example of a minimal makefile:

  OBJ = src/a.o src/b.o src/c.o
  libfoo.so: $(OBJ)
          $(CC) -shared -o libfoo.so $(OBJ)
> When the tool is in charge, it has more control and can sandbox each build step so that undeclared dependencies simply won't be available in the sandbox.

There's no reason this shouldn't be possible with make; it just hasn't been implemented so. Do bazel/buck/please actually do this? As far as I know tup is the only tool that actually verifies inputs/outputs of rules, and it needs FUSE to do so.

> For example, unless you implement your own header scanning (or manually list your header dependencies) "make" won't know to rebuild a source file when a header changes.

At least with GNU make, it's very easy:

  CFLAGS += -MMD
  -include $(patsubst %.o,%.d,$(OBJ))
True, it's a bit of a footgun, but by no means difficult.

Make has problems, but the ones you listed aren't they.


And? That's no different than the build script calling any external executable. It may stop, it may not stop, it may format the hard drive. Make doesn't (and shouldn't) care.

> Make by default uses the file change timestamp to trigger actions. But this is definitely not the only way

Can you point to some documentation for this? I haven't been able to find anything.


> You can use .DEFAULT_GOAL := goal_name to set the default target run by 'make' with no target name specified.

I had always, perhaps erroneously, thought that the very first goal in the file was the default one.

next

Legal | privacy