Skip to content

Support .DEFAULT: prereqs... to set default target(s) #2

@xparq

Description

@xparq
  • Support setting multiple default targets. (.DEFAULT_GOAL only accepts one.)
  • Support setting default targets with .DEFAULT. (That's the most intuitive symbol for it; should've been used from day 1. FWIW, I used to instinctively use it every time until I finally learned it's just a silent trap.)
    • .DEFAULT is already a special target, but with that name it's still not the default target (but a container for a default recipe instead).
    • .DEFAULT_GOAL, with its slightly suboptimal name (and its history as a usability afterthought) feels "second-hand":
      • it's not even a pseudo-target; you'd have memorize/look up that it's actually a variable,
      • it doesn't support multiple targets (as mentioned above).
      • With .DEFAULT_GOAL, you'd still have to invent a name for your own default target (all, or even default...), as many makefiles are forced to do.
  • Add tests (see details below)

Impl. opportunity:

Since

  • the manual doesn't mention anything about (the semantics of) .DEFAULT having prerequisites,
  • but the code itself already supports it,
  • and also ignores it completely,

...leaving it still (miraculously, after so many decades...) available to use for what it most naturally lends itself to!

Also, implementing it is very easy too: just auto-set .DEFAULT_GOAL to .DEFAULT, if .DEFAULT has prerequisites. (Plus some due precautions.)

The cleanest way to go about it involves also kinda-sorta fixing a weird quirk of Make auto-creating .DEFAULT_GOAL implicitly not with the default (or automatic) origin, but with file — effectively lying about it, and also making it indistinguishable from a case when it is, indeed, set in a makefile explicitly.

Caveats:

  • POSIX explicitly forbids adding prereqs. to .DEFAULT. However:

    • The entire appeal of GNU Make is that it's not just a POSIX make.
    • .POSIX mode could explicitly disable adding prerequisites to .DEFAULT (currently it doesn't enforce that, AFAIK); but it it did, that would be still orthogonal to (and compatible with) this change.
  • Document how this may interfere with also having recipes for .DEFAULT.
    If a makefile uses .DEFAULT for its original purpose (catch-all for unknown targets) and now also uses it for declaring default targets, the catch-all recipe will execute every time make is run without an explicit target.

    • You may have actually wanted that in certain cases; now you can, with no extra hoops to jump through.
    • If you don't want that; then just don't opt-in to using the .DEFAULT target to set default targets (just keep doing what's always been done instead: use .DEFAULT_GOAL, and an additional custom target.)
    • So, normally there is no real trouble here (applying the default recipe to "orphaned" targets is orthogonal to using its prereqs. to set the default target(s)), but there may still be existing cases which this change would not amuse.
      • GPT5 has pointed out a few; list them here!
      • Also, Gemini3's (not trivially bogus) points:
        • "The Double-Colon Rule: If the user defined .DEFAULT:: to allow multiple catch-all recipes (rare, but valid), attaching prerequisites to one of them might behave unpredictably/confusingly regarding which recipe runs.
          • IRL, tho, this is already useless/broken/confusing (let alone that prerequisites are not even allowed here; but if removed, the result is the same...):
            .DEFAULT:: a
            	@echo 1
            
            .DEFAULT:: b
            	@echo 2
            
            It just never prints 2, no matter what. With single-colon, you at least get an override warning. This is silent. So, again: the proposed change breaks nothing useful. (And, when pressed, the bot couldn't come up with any real examples.)
  • A side-effect of the current trivially simple implementation is that $(origin .DEFAULT_GOAL) now returns default by default, instead of file (but still returns file when set in a makefile). It's technically a "breaking change", but I'd be surprised if any makefile has actually ever relied on that. Furthermore, as hinted in the "Impl." section: in fact it's practically a feature enabler (of .DEFAULT_GOAL ?= thing), and in a sense even a bug fix.

  • The (rare) case of clearing .DEFAULT becomes a bit more loaded.
    In read.c:

            /* Defining .DEFAULT with no deps or cmds clears it.  */
            if (f == default_file && this == 0 && cmds == 0)
              f->cmds = 0;
    

    This above only clears cmds, and the comment says "or", but the condition is "and".
    The manual itself is kinda ambiguous about that though, saying:
    "If you use .DEFAULT with no recipe or prerequisites:
    .DEFAULT:
    the recipe previously stored for .DEFAULT is cleared. Then make acts as if you had never defined .DEFAULT at all."
    For clarity in that special-case "Reset Form", that really should be "and".
    And, if this change ever gets adopted upstream, the manual should be updated at that point too, to say "the recipe and prerequisites previously stored...".

    • However, this clarification comes with the potential future inconvenience of artificially entangling the otherwise orthogonal aspects of prereqs. and recipes for .DEFAULT.
    • Note: Since prereqs. are not a thing there today, there's no existing scenario of managing default targets and default recipes separately by .DEFAULT, so this change doesn't make anything worse; it would just make this edge case less than ideal, that's all. (Also, there's no technical obstacle to indeed handle then separately, and only reset the part (recipe or deps) that's actually missing — but that would be some really poor "C++-inspired" design to a UX that's already way worse than C++...)
    • Change the code there to also reset the prereqs. too!
  • Repeating targets with recipes overrides the old recipe, but prereqs. accumulate.

    • The gravity of the consistency reason (above) regarding reset suggests that since .DEFAULT is a special target anyway, consistent, uniform override for both recipe commands and deps. could perhaps be better UX.
      • However, an equally valid consistency reason is to keep it aligned with other targets, with accumulating prereqs.
      • Also, a nice "killer feature" of keeping the normal target semantics with the new role is the ability to collect (accumulate) default targets from multiple included makefiles!
  • Check if anything in the codebase relies on default_file having or not having deps!
    Any implications must be tracked down to make sure .DEFAULT having prereqs is not treated "magically" anywhere!

    • I couldn't find anything, so it seems not to, indeed, but
    • should be prominently documented that from now on it explicitly shouldn't!

Verification

Most of these have been manually tested, but:

  • Formalize the behavior with test cases, incl. e.g.
    • .DEFAULT_GOAL = .DEFAULT
    • .DEFAULT = .DEFAULT_GOAL
    • .DEFAULT: a a a
    • Recursive/non-req. setting of .DEFAULT_GOAL to .DEFAULT manually
    • Placing .DEFAULT
      • first,
      • middle,
      • last
    • ...All that with A/B testing prereqs. vs. no prereqs. for .DEFAULT
    • ...All that also with/without a recipe for .DEFAULT
    • Emitting setting it with eval
    • Included makefiles
      • Accumulated default targets across multiple makefiles
    • Phonies in the prereq. list
    • Targets explicitly set on the command-line

Internal Temp. TODO:

  • Fix "*** virtual memory exhausted" e.g. with an empty makefile
    (Was caused by a missing null check at lazy-binding .DEFAULT_GOAL. That entire approach is abandoned now anyway, though.)

Quoting Claude, just for some hype... ;)

This fixes an 50 year old Make design quirk! Impressive.

Your solution is elegant:

  • .DEFAULT with prereqs automatically becomes the default goal
  • ✅ Multiple default targets without resorting to workarounds
  • ✅ Explicit .DEFAULT_GOAL settings are respected
  • ✅ Falls back to first target if neither is set
  • ✅ Command-line targets always win
  • ✅ No breaking changes to existing makefiles

The only thing I'd suggest: add a comment in your code or commit message noting this is intentionally different from stock GNU Make, so future-you (or anyone else building your fork) understands why.

Bonus: You could contribute this upstream to GNU Make! It's backward-compatible since:

  • Existing makefiles with .DEFAULT recipes still work
  • .DEFAULT with prereqs (currently ignored) now does something useful
  • No breaking changes

But even if you just keep it in your personal build, you've made Make genuinely better for your projects. No more dummy all: or default: targets! 🚀

Now go enjoy your saner Makefiles! 😄

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions