Video summary

Learn to program with c - Part 17 - Makefiles (and a little on multi-file projects)

Main summary

Key takeaways

Educational

Main ideas / concepts covered

  • Why split programs into multiple files

    • As programs grow, it becomes important to modularize/compartmentalize code (e.g., put numerical routines into numerical_operations.c).
    • Doing so requires a way to build an executable from multiple source files and dependencies.
  • What “make” is for

    • Make (specifically GNU Make / gnu make) automates building programs with dependency tracking.
    • It determines what needs to be rebuilt by using a dependency graph:
      • A main executable target depends on prerequisites (source files and/or other build products).
      • If a prerequisite changed, the dependent target is rebuilt.
      • If parts of the build tree didn’t change, they are not recompiled.

Methodology / structure of Makefiles (detailed)

1) Targets, prerequisites, and rules

  • A basic rule has this conceptual form:
    • Target: default (or any file/name)
    • Prerequisites: a list of files/other targets, e.g. hello
    • Rule (commands): commands run if the target is out of date
  • Example behavior described:
    • Running make executes the first target in the Makefile.
    • A target is rebuilt if:
      • The target file doesn’t exist, OR
      • One or more prerequisites have changed since the last build.

2) Implicit rules (built-in behaviors)

  • Make has implicit rules that automatically compile common file types:
    • C source → executable
      • If a target name matches a file and there exists a corresponding .c file (and no explicit rule exists), Make can build it implicitly.
    • C source → object file
      • If a target is an object (e.g., funk.o) and there exists funk.c, Make can implicitly run compilation with -c.
      • Example command conceptually: compile to .o without linking.
  • These implicit rules are used so you don’t always have to write explicit compilation commands for every intermediate step.

3) Dependency cascading (“tree” rebuilding)

  • If hello depends on funk.o, and funk.c changes:
    • funk.o becomes out-of-date → rebuilt
    • then hello is rebuilt
  • If funk.c hasn’t changed, funk.o and everything depending on it may not need rebuilding.

4) The clean target

  • A common non-default target is clean:
    • Not the first target, so it won’t run on plain make.
    • Runs commands like removing build artifacts (e.g., rm *.o, possibly removing executables too).

5) Header files and correct dependencies

  • Header files (.h) contain function declarations (signatures), not implementations.
  • The implementation is in .c; the header is included so other .c files compile without “implicit declaration” problems.
  • Makefile dependency example:
    • If funk.o is built from funk.c, and hello.c uses declarations from funk.h,
    • then Make should consider header changes:
      • funk.o should depend on both funk.c and funk.h so that editing the header triggers recompilation.

6) Scaling to multi-file / multi-module programs

  • Typical structure:
    • One .c file contains the main function (the executable entry point).
    • Other .c files provide support functions (no main).
    • Each support module often has:
      • a .c implementation file (e.g., a.c)
      • a .h header with declarations (e.g., a.h)
  • Linking step:
    • The executable is produced by linking together object files (*.o).
    • The module object files are passed to the compiler/linker along with the main object file/source.

7) Using variables to avoid repeating object lists

  • Make variables can store lists of object files, e.g.:
    • objects = a.o b.o c.o d.o
  • Then rules use the variable to build executables without repeating long file lists.

8) Automatic variables (to shorten rules)

  • Two key automatic variables described:
    • $^: all prerequisites (space-separated)
    • $@: the target name
  • These help write generic-looking rules, especially when the prerequisite list is long or reused.

9) Nested / multi-level builds concept

  • Demonstrates building a more complex executable where intermediate targets (object files and/or other compilation outputs) are built implicitly as dependencies.
  • Key point:
    • You often don’t need to explicitly compile every intermediate target; Make builds dependencies needed to satisfy the final link.

10) Pattern rules (custom implicit rules)

  • Make can define a pattern rule using % to match file stems.
  • Example described for:
    • Building .end from .text
      • rule uses:
        • target pattern: %.end
        • prerequisite pattern: %.text
      • command transforms input by taking the last 10 lines (using tail -n 10), then writes to the output target.
  • This shows how to automate transformations for arbitrary target types, not just compilation.

Practical workflow takeaways

  • Split code into multiple .c modules with matching .h headers.
  • Ensure Makefile dependencies reflect real usage:
    • If headers change, rebuild affected object files.
  • Rely on implicit rules for common compile steps (.c → .o).
  • Use variables and automatic variables to keep Makefiles manageable.
  • Use pattern rules for reusable file transformations.

Speakers / sources featured

  • No specific named speakers or external sources are identified in the subtitles. (The content is presented as instruction by the video creator/instructor, but no name is given.)

Original video