Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Crafting a Clean, Maintainable, and Understandable Makefile for a C Project (lucavall.in)
41 points by ingve on Oct 24, 2023 | hide | past | favorite | 8 comments


I love it when I find a project and it's entire build system is just a make file. It usually builds without much drama and anything I need to fix is straight-forward and direct. autoconf is the worst. If software were an eldrich horror it would be autoconf. cmake is a little better. but it still feels like you are trying to change something by telling a person who does not speak your language how to do do it.

While I am sure these advanced build systems bring something to the table. as someone who is more sysadmin than developer, what that thing is, is sometimes hard to see.


> when I find a project and it's entire build system is just a make file.

reason might be, you only find small projects that don't need a build system. It's not like developers have a choice. As soon as you need dependencies, alternative dependencies, cross platform compatibility etc., there's no way around choosing a build system.


My expierience is totally the opposite. Every time I have to touch a make file, I don't understand shit and hate my life. I have to google basically everything. Syntax, commands, etc.. Obscure Syntax rules I forget when not touching the build files for more than a week.. same goes for basically every build system I ever used, except Cargo


amen. i submit postgresql and linux kernel as two examples.


The variables are pointless. Your lib and include directories are already defined somewhere else: your project's directory structure.

On the compiler command line, you just want -iquote include; -iquote $(INC_DIR) doesn't help anything.

If someone renames or moves directories in the future, they can search and replace the Makefile.

Variables hide things. If I see $(INC_DIR), is that something external? I have to look it up.

By default I assume that a variable exists for a reason, which is that something is being made configurable. Usually, configurable things are uncertainties that are external: where other stuff is, and what not.

The article further recommends nonstandard practices.

- LFLAGS: what is that? The standard variables are LDFLAGS and LDLIBS. LDFLAGS are options for linking, and LDLIBS are just the libraries like -lwhatever. They are separated because they go into different parts of the command line. For Pete's sake, do not invent your own variables. Learn the standards. Distro maintainers will thank you when your program is packaged.

- Believe it it or not, but you must should not touch CFLAGS. CFLAGS belongs to the user who is invoking your Makefile. Put any required options or debug options and whatnot into a different variable. Combine that with CFLAGS. The same goes for the aforementioned LDFLAGS and LDLIBS.

Let's look at this:

  ifeq ($(debug), 1)
    CFLAGS := $(CFLAGS) -g -O0
  else
    CFLAGS := $(CFLAGS) -Oz
  endif
This does not always do what the author thinks. It looks like it wants to add some options to CFLAGS, if CFLAGS is already defined. But this is not what GNU Make will do if CFLAGS is coming from the command line:

   make CFLAGS=-O2
then the := assignment will be entirely suppressed. That -O2 will be the CFLAGS. The combination will happen if CFLAGS is coming from the environment.

Thus if you have essential code generation options that your program needs, the above pattern will break in some situations. Don't use CFLAGS as the container where you accumulate all your options. Assemble the options in your own variable, where you interpolate CFLAGS:

  OUR_CFLAGS := $(CFLAGS) $(OUR_DIALECT_FLAGS) $(OUR_CODE_GEN_CFLAGS) $(OUR_DEBUG_FLAGS) ...

  OUR_LDFLAGS := $(LDFLAGS) -Llibdir

  OUR_LDLIBS := $(LDLIBS) -lutils
I see the article is using a double star operator in a $(wildcard ...) call. I don't see that documented in GNU Make, and it's not obvious from looking at all the glob-related code that any such thing is implemented.

The following has a subtle problem:

  # Build object files and third-party libraries
  $(OBJS): dir
          @mkdir -p $(BUILD_DIR)/$(@D)
          @$(CC) $(CFLAGS) -o $(BUILD_DIR)/$@ -c $*.c
The problem is that a directory timestamp changes often. This rule will cause the $(OBJS) to be considered out of date whenever the timestamp of dir is touched.

For this you need to use the GNU Make "order-only prerequisite" mechanism, where order-only prerequisites are separated by a bar:

  # Build object files and third-party libraries
  $(OBJS): | dir
          @mkdir -p $(BUILD_DIR)/$(@D)
          @$(CC) $(CFLAGS) -o $(BUILD_DIR)/$@ -c $*.c
The GNU Make manual has exactly this kind of example in the section on order-only prerequisites.

  $(NAME): format lint dir $(OBJS)
This is running a format step that touches all the sources in the link step of the program. This is just silly and far removed from the purported goal of crafting a clean Makefile. Not only shouldn't build steps be touching the sources, but the dependencies this cruft brings in are unpalatable.


Why is the business with CFLAGS and LDFLAGS a big deal? Because if you follow the standard ways, your program is easier to package in a distro.

CFLAGS, LDFLAGS and LDLIBS are set by a distro build. They specify system-wide settings that the distro wants. Anything from special ways of linking the program, to tuning optimization for a particular CPU and whatnot.

The more your Makefile sticks to standards, the less they have to do.

Make sure you support "make DESTDIR=<directory>" to install into a temporary directory of the caller's choice. E.g. if your prefix (another thing to support) is /usr/bin then "make DESTDIR=tmp-dir" installs into tmp-dir/usr/bin. The software is configured to run in /usr/bin though and won't necessarily execute properly out of there; that tmp-dir is just for packaging.

However you do compiler selection, make sure that the default is to use $(CC).

Regarding the OUR_CFLAGS thing in my above post: the reason some Makefiles clobber CFLAGS is that they can then use built-in recipes. The built-in recipe for compiling a .c to a .o only knows about CC and CFLAGS. It's worth it to replace the built-in recipes with your own though in order not to disturb the standard variables.


To expand on this, the de facto comprehensive standard for Makefile variables is the GNU standard, if only because there is no other widely used, consistent convention beyond the very small number of POSIX-defined built-in variables. The GNU project blazed the trail for portable, generic build conventions. The GNU Makefile Conventions are documented at https://www.gnu.org/prep/standards/standards.html#Makefile-C... In practice there's a wider set of variable conventions based on or baked into GNU Make and Autotools. See, e.g. https://www.gnu.org/software/make/manual/html_node/Implicit-...

A long time ago I put together my own little summary of applied GNU'ish conventions. See https://25thandClement.com/~william/cuda/Makefile.md.html Notably I specified LIBS instead of LDLIBS. These days I use LDLIBS. I forget why I listed LIBS, but there is some ambiguity and consistency across GNU tools. For example, pkg-config sort of ignored or simplified some older conventions (e.g. dropping CPPFLAGS and making CFLAGS do double duty) which IME wasn't for the better, at least if applied outside the domain of what pkg-config is used for.

Why use these conventions? Because such variables and their semantics will be useful for package maintainers, who won't need to spend as much time learning some bespoke convention and who often have templates for working with GNU-like builds, even when autotools aren't being used. Because they reflect decades of experience in how to write builds that in 80% of odd circumstances (including development, non-production environments) can be controlled without modifying the build rules directly. Relatedly, they're often the minimum necessary abstraction for cross-compiling (though alone not always sufficient).


Thanks for sharing!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: