Skip to article frontmatterSkip to article content

PS4: Build system and documentation

IUT d'Orsay, Université Paris-Saclay

The goal of this session is to understand the following points:

Exercise 1: Makefile

Here is a minimal example of a C++ project:

minimal-project/
    hello.h
    hello.cpp
    main.cpp
    makefile
hello.h
hello.cpp
main.cpp
makefile
#ifndef HELLO_H
#define HELLO_H

void sayHello();

#endif
  1. Create the directory and files listed above in PS4/ with the corresponding code.
  1. Run make, make run, and make clean, and observe their effects in the terminal and on the files.
  1. Make the following modifications to the makefile:
makefile
all: executable

executable: main.o hello.o
    g++ -o executable main.o hello.o

main.o: main.cpp hello.h
    g++ -c main.cpp

hello.o: hello.cpp hello.h
    g++ -c hello.cpp

run:
    ./executable

clean:
    rm -f executable main.o hello.o

.PHONY: all run clean
  1. Run make, make run, and make clean again. The behavior of these commands should not change.

  2. Make the following modifications to the makefile:

makefile
all: executable

executable: main.o hello.o
    g++ -o executable main.o hello.o

main.o: main.cpp
    g++ -c main.cpp -MMD

hello.o: hello.cpp
    g++ -c hello.cpp -MMD

run:
    ./executable

clean:
    rm -f executable main.o hello.o main.d hello.d

.PHONY: all run clean

-include *.d
  1. Run make and examine the content of the generated .d files.
  1. Make the following modifications to the makefile:
makefile
all: build build/binaries/executable

build:
    mkdir -p build/dependencies build/objects build/binaries

build/binaries/executable: build/objects/main.o build/objects/hello.o
    g++ -o build/binaries/executable build/objects/main.o build/objects/hello.o

build/objects/main.o: main.cpp
    g++ -c main.cpp -o build/objects/main.o -MMD -MF build/dependencies/main.d  

build/objects/hello.o: hello.cpp
    g++ -c hello.cpp -o build/objects/hello.o -MMD -MF build/dependencies/hello.d  

run:
    ./build/binaries/executable

clean:
    rm -rf build

.PHONY: all run clean

-include build/dependencies/*.d
  1. Run make, make run, and make clean again and observe.

  2. Add comments to the makefile (using #) to clarify the discussed points.

Bonus

The current makefile code contains a lot of redundancies. We can refactor this code using variables as follows:

makefile
BUILD_DIRECTORY = build
DEPENDENCY_DIRECTORY = $(BUILD_DIRECTORY)/dependencies
OBJECT_DIRECTORY = $(BUILD_DIRECTORY)/objects
BINARY_DIRECTORY = $(BUILD_DIRECTORY)/binaries

EXECUTABLE = $(BINARY_DIRECTORY)/executable

SOURCE_FILES = $(wildcard *.cpp)

OBJECT_FILES = $(SOURCE_FILES:.cpp=.o)
OBJECT_FILES := $(OBJECT_FILES:%=$(OBJECT_DIRECTORY)/%)

DEPENDENCY_FILES = $(SOURCE_FILES:.cpp=.d)
DEPENDENCY_FILES := $(DEPENDENCY_FILES:%=$(DEPENDENCY_DIRECTORY)/%)

all: $(BUILD_DIRECTORY) $(EXECUTABLE)

$(BUILD_DIRECTORY):
    mkdir -p $(DEPENDENCY_DIRECTORY) $(OBJECT_DIRECTORY) $(BINARY_DIRECTORY)

$(EXECUTABLE): $(OBJECT_FILES)
    g++ -o $@ $^

$(OBJECT_DIRECTORY)/%.o: %.cpp
    g++ -c $< -o $@ -MMD -MF $(DEPENDENCY_DIRECTORY)/$*.d

run:
    ./$(EXECUTABLE)

clean:
    rm -rf $(BUILD_DIRECTORY)

.PHONY: all run clean $(BUILD_DIRECTORY)

-include $(DEPENDENCY_FILES)
  1. Run make, make run, and make clean again and observe.
  1. Rearrange your minimal project as follows:
minimal-project/
    source/
        first-module/
            hello.cpp
            hello.h
            makefile
        second-module/
            hi.cpp
            hi.h
            makefile
        main.cpp
    makefile
  1. In hi.h, copy the code from hello.h and modify the include guard and the function name to sayHi.

  2. In hi.cpp, copy the code from hello.cpp and modify the include directive to include hi.h, the function name to sayHi, and the console output to Hi! instead of Hello World!.

  3. In main.cpp, modify the include directives to include first-module/hello.h and second-module/hi.h and add sayHi(); after sayHello();.

For this minimal example, we will use the same makefile in both modules, but we can imagine that these makefiles might differ and even be written by different people.

  1. Modify the module makefiles as follows:
makefile
BUILD_DIRECTORY = ../../build
DEPENDENCY_DIRECTORY = $(BUILD_DIRECTORY)/dependencies
OBJECT_DIRECTORY = $(BUILD_DIRECTORY)/objects

SOURCE_FILES = $(wildcard *.cpp)

OBJECT_FILES = $(SOURCE_FILES:.cpp=.o)
OBJECT_FILES := $(OBJECT_FILES:%=$(OBJECT_DIRECTORY)/%)

DEPENDENCY_FILES = $(SOURCE_FILES:.cpp=.d)
DEPENDENCY_FILES := $(DEPENDENCY_FILES:%=$(DEPENDENCY_DIRECTORY)/%)

all: $(BUILD_DIRECTORY) $(OBJECT_FILES)

$(BUILD_DIRECTORY):
	mkdir -p $(DEPENDENCY_DIRECTORY) $(OBJECT_DIRECTORY) $(BINARY_DIRECTORY)

$(OBJECT_DIRECTORY)/%.o: %.cpp
	g++ -c $< -o $@ -MMD -MF $(DEPENDENCY_DIRECTORY)/$*.d

clean:
	rm -rf $(OBJECT_FILES) $(DEPENDENCY_FILES)

.PHONY: all clean $(BUILD_DIRECTORY)

-include $(DEPENDENCY_FILES)
  1. Modify the root project makefile as follows:
makefile
BUILD_DIRECTORY = build
DEPENDENCY_DIRECTORY = $(BUILD_DIRECTORY)/dependencies
OBJECT_DIRECTORY = $(BUILD_DIRECTORY)/objects
BINARY_DIRECTORY = $(BUILD_DIRECTORY)/binaries

SOURCE_DIRECTORY = source

EXECUTABLE = $(BINARY_DIRECTORY)/executable

MODULES = $(wildcard $(SOURCE_DIRECTORY)/*/)
SOURCE_FILES = $(wildcard $(SOURCE_DIRECTORY)/*.cpp)

OBJECT_FILES = $(SOURCE_FILES:$(SOURCE_DIRECTORY)/%.cpp=%.o)
OBJECT_FILES := $(OBJECT_FILES:%=$(OBJECT_DIRECTORY)/%)

DEPENDENCY_FILES = $(SOURCE_FILES:$(SOURCE_DIRECTORY)/%.cpp=%.d)
DEPENDENCY_FILES := $(DEPENDENCY_FILES:%=$(DEPENDENCY_DIRECTORY)/%)

all: $(BUILD_DIRECTORY) $(EXECUTABLE)

$(BUILD_DIRECTORY):
	mkdir -p $(DEPENDENCY_DIRECTORY) $(OBJECT_DIRECTORY) $(BINARY_DIRECTORY)

$(EXECUTABLE): $(MODULES) $(OBJECT_FILES) 
	g++ -o $@ $(wildcard $(OBJECT_DIRECTORY)/*.o)

$(MODULES):
	$(MAKE) -C $@

$(OBJECT_DIRECTORY)/%.o: $(SOURCE_DIRECTORY)/%.cpp
	g++ -c $< -o $@ -MMD -MF $(DEPENDENCY_DIRECTORY)/$*.d

run:
	./$(EXECUTABLE)

clean:
	rm -rf $(BUILD_DIRECTORY)

.PHONY: all run clean $(MODULES) $(BUILD_DIRECTORY)

-include $(DEPENDENCY_FILES)
  1. Add comments to the makefiles with # to clarify the different blocks of code and add examples for the commands to clarify the syntax.

For example:

# Compile source codes to object files
$(TARGET_VARIABLE): $(PREREQUISITE_VARIABLE)
    # For example: g++ -c prerequisite.cpp -o target.o
    g++ -c $< -o $@

Exercise 2: Documentation

  1. Rewrite your README and check the rendering of the README on GitLab.
  1. Create a documentation/ directory in PS4/.

In class, we saw an example of documentation using Doxygen (very similar to Javadoc syntax). Since Doxygen is not (yet) installed on the IUT machines, we will review the same example in Javadoc.

  1. Create the following file.
Temperature.java
/**
 * Represents a temperature in Celsius and Fahrenheit.
 *
 * This class allows you to set a temperature in Celsius, convert it to Fahrenheit,
 * and vice versa. It also provides the ability to get the current temperature in either unit.
 *
 * <p> Example usage: </p> 
 * <pre>
 * <code>
 * Temperature currentTemperature = new Temperature(25.0);  // 25°C
 * currentTemperature.displayTemperature();  // Output: Temperature: 25°C / 77°F
 * currentTemperature.setFahrenheit(100.0);
 * currentTemperature.displayTemperature();  // Output: Temperature: 37.7778°C / 100°F
 * </code>
 * </pre>
 */
public class Temperature {
    private double celsius;

    /**
     * Constructs a new Temperature object.
     *
     * Initializes the temperature in Celsius.
     *
     * @param celsius The initial temperature in Celsius.
     */
    public Temperature(double celsius) {
        this.celsius = celsius;
    }

    /**
     * Gets the current temperature in Celsius.
     *
     * @return The temperature in Celsius.
     */
    public double getCelsius() {
        return celsius;
    }

    /**
     * Sets the temperature in Celsius.
     *
     * This method sets the temperature value directly in Celsius.
     *
     * @param celsius The new temperature in Celsius.
     */
    public void setCelsius(double celsius) {
        this.celsius = celsius;
    }

    /**
     * Gets the current temperature in Fahrenheit.
     *
     * @return The temperature in Fahrenheit.
     */
    public double getFahrenheit() {
        return celsius * 9 / 5 + 32; // Formula to convert Farenheit to Celsius
    }

    /**
     * Sets the temperature using a value in Fahrenheit.
     *
     * This method converts the given Fahrenheit value to Celsius and sets it.
     *
     * @param fahrenheit The temperature in Fahrenheit.
     */
    public void setFahrenheit(double fahrenheit) {
        this.celsius = (fahrenheit - 32) * 5 / 9; // Formula to convert Celsius to Farenheit
    }

    /**
     * Converts and displays the temperature in both Celsius and Fahrenheit.
     *
     * This method prints the current temperature in both Celsius and Fahrenheit.
     */
    public void displayTemperature() {
        System.out.println("Temperature: " + celsius + "°C / " + getFahrenheit() + "°F");
    }
}
  1. Generate the documentation for Temperature.java with the command javadoc -d docs Temperature.java, which will create a docs/ directory.

  2. View the generated documentation by opening docs/index.html in a web browser.

  3. Revisit the Doxygen syntax covered in class to write the documentation for the Product class from the short-functions exercise in TP3.

Return to the objectives and check off the points you have mastered. Revisit the points you have not yet fully understood. Ask your instructor for help if needed.