Diferență între revizuiri ale paginii „SDPT Lab 3”
(Pagină nouă: = Lab 3: Build Systems - From GNU Make to Modern CMake = == Introduction == In Week 2, we solved the human collaboration problem using Git. Today, we solve the compilation scaling...) |
|||
| (Nu s-au afișat 2 versiuni intermediare efectuate de același utilizator) | |||
| Linia 1: | Linia 1: | ||
| − | = Lab 3: Build | + | = Lab 3: Build Automation - Makefiles and Modern CMake = |
== Introduction == | == Introduction == | ||
| − | In | + | In professional embedded development, clicking a "Build" button in an IDE is a luxury you rarely have. When working with Embedded Linux, RTOS environments, or CI/CD pipelines, you must define exactly how your source code is translated into machine code. |
| − | + | This lab transitions you from manual terminal commands to scalable build automation. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | === Goals === | |
| + | * Understand the C++ compilation pipeline (Preprocessing $\rightarrow$ Compilation $\rightarrow$ Linking). | ||
| + | * Master GNU Make: pattern rules, automatic variables, and header dependency generation. | ||
| + | * Master Modern CMake: out-of-source builds, targets, and static libraries. | ||
| + | * Integrate remote third-party libraries using CMake <code>FetchContent</code>. | ||
| − | + | == Part 1: Theoretical Reference == | |
| + | ''Use this section as a reference manual to complete the tasks in Part 2.'' | ||
| − | == | + | === 1.1 GNU Make Fundamentals === |
| − | + | Make tracks file modification timestamps to avoid recompiling code that hasn't changed. A <code>Makefile</code> consists of rules: | |
| + | <syntaxhighlight lang="make"> | ||
| + | target: dependencies | ||
| + | <TAB> command | ||
| + | </syntaxhighlight> | ||
| + | ''(Note: Makefiles strictly require a real TAB character for indentation, not spaces!)'' | ||
| − | + | '''Automatic Variables:''' | |
| − | + | Make provides shortcuts to avoid typing duplicate filenames: | |
| − | < | + | * <code>$@</code> : The target filename. |
| − | + | * <code>$<</code> : The first dependency. | |
| − | + | * <code>$^</code> : All dependencies (space-separated). | |
| − | |||
| − | </ | ||
| − | === 2. | + | '''Pattern Rules:''' |
| − | Create a | + | Instead of writing a rule for every file, use a pattern template: |
| − | < | + | <syntaxhighlight lang="make"> |
| − | # | + | %.o: %.cpp |
| − | + | $(CXX) $(CXXFLAGS) -c $< -o $@ | |
| + | </syntaxhighlight> | ||
| + | |||
| + | '''GCC Auto-Dependencies (The Header Problem):''' | ||
| + | If you change a <code>.h</code> file, Make won't normally detect it because object files depend on <code>.cpp</code> files. We ask GCC to generate dependency (<code>.d</code>) files for us: | ||
| + | * Add <code>-MMD -MP</code> to your <code>CXXFLAGS</code>. | ||
| + | * Include the generated files at the bottom of your Makefile using: <code>-include $(DEPS)</code> | ||
| + | |||
| + | === 1.2 Modern CMake Fundamentals === | ||
| + | CMake is a meta-build system. It generates Makefiles (or Ninja files, or Visual Studio solutions) based on a high-level <code>CMakeLists.txt</code> script. Modern CMake is "Target-Based"—you define targets (executables/libraries) and attach properties to them. | ||
| + | |||
| + | '''Core Commands:''' | ||
| + | <syntaxhighlight lang="cmake"> | ||
| + | cmake_minimum_required(VERSION 3.10) | ||
| + | project(MyProject CXX) | ||
| + | |||
| + | # Build an executable from source | ||
| + | add_executable(my_app main.cpp) | ||
| + | |||
| + | # Build a static library (.a archive) | ||
| + | add_library(my_driver STATIC driver.cpp) | ||
| + | |||
| + | # Define where a target should look for header files | ||
| + | target_include_directories(my_driver PUBLIC include/) | ||
| + | |||
| + | # Link a library to an executable | ||
| + | target_link_libraries(my_app PRIVATE my_driver) | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | '''Out-of-Source Builds:''' | ||
| + | Never pollute your source directory with binaries. Always build in a separate folder: | ||
| + | <syntaxhighlight lang="bash"> | ||
| + | mkdir build && cd build | ||
| + | cmake .. | ||
| + | make | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | '''Fetching External Libraries:''' | ||
| + | CMake can download libraries directly from Git during the configuration phase: | ||
| + | <syntaxhighlight lang="cmake"> | ||
| + | include(FetchContent) | ||
| + | FetchContent_Declare( | ||
| + | json_lib | ||
| + | GIT_REPOSITORY https://github.com/nlohmann/json.git | ||
| + | GIT_TAG v3.11.2 | ||
| + | ) | ||
| + | FetchContent_MakeAvailable(json_lib) | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | === 1.3 Useful Resources === | ||
| + | * [https://www.gnu.org/software/make/manual/make.html GNU Make Manual] | ||
| + | * [https://cmake.org/cmake/help/latest/ CMake Official Documentation] | ||
| + | |||
| + | == Part 2: The Challenges == | ||
| + | |||
| + | === Setup: The Raw Materials === | ||
| + | Create a new directory for this lab (<code>sdpt-lab3</code>). Inside, create an <code>include/</code> directory and a <code>src/</code> directory. Populate them with the following three files to simulate an IoT sensor node. | ||
| + | |||
| + | '''<code>include/sensor.h</code>''' | ||
| + | <syntaxhighlight lang="cpp"> | ||
| + | #pragma once | ||
struct SensorData { | struct SensorData { | ||
| Linia 40: | Linia 102: | ||
void init_sensor(); | void init_sensor(); | ||
SensorData read_sensor(); | SensorData read_sensor(); | ||
| + | </syntaxhighlight> | ||
| − | + | '''<code>src/sensor.cpp</code>''' | |
| − | + | <syntaxhighlight lang="cpp"> | |
| − | |||
| − | |||
| − | < | ||
#include <iostream> | #include <iostream> | ||
#include "../include/sensor.h" | #include "../include/sensor.h" | ||
| Linia 54: | Linia 114: | ||
SensorData read_sensor() { | SensorData read_sensor() { | ||
| − | + | return {24, 60}; | |
| − | |||
| − | |||
| − | |||
} | } | ||
| − | </ | + | </syntaxhighlight> |
| − | + | '''<code>src/main.cpp</code>''' | |
| − | < | + | <syntaxhighlight lang="cpp"> |
#include <iostream> | #include <iostream> | ||
#include "../include/sensor.h" | #include "../include/sensor.h" | ||
| Linia 70: | Linia 127: | ||
init_sensor(); | init_sensor(); | ||
| − | SensorData | + | SensorData data = read_sensor(); |
| − | std::cout << "Temp: " << | + | std::cout << "Temp: " << data.temperature << "C, Hum: " << data.humidity << "%" << std::endl; |
return 0; | return 0; | ||
} | } | ||
| − | </ | + | </syntaxhighlight> |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | === Challenge 1: The Scalable Makefile (Estimated time: 25 mins) === | |
| − | + | '''Your Task:''' Write a <code>Makefile</code> in the root directory that compiles these files into an executable named <code>iot_node.bin</code>. | |
| + | * Do '''not''' hardcode the <code>.cpp</code> file names. Use the <code>wildcard</code> function to find them dynamically. | ||
| + | * Use variables for the compiler (<code>CXX</code>) and flags (<code>CXXFLAGS</code>). | ||
| + | * Use Pattern Rules and Automatic Variables to compile <code>.cpp</code> to <code>.o</code>. | ||
| + | * Implement GCC header dependency tracking (<code>-MMD -MP</code>). | ||
| − | + | '''Verification:''' | |
| − | + | # Run <code>make</code>. It should compile successfully. | |
| − | + | # Run <code>make</code> again immediately. It should say "Nothing to be done". | |
| − | </ | + | # Open <code>include/sensor.h</code> and add a dummy integer to the <code>SensorData</code> struct. Run <code>make</code> again. It MUST recompile both <code>.cpp</code> files automatically. If it doesn't, your dependency tracking is broken. |
| − | |||
| − | |||
| − | < | + | === Challenge 2: Migrating to CMake (Estimated time: 20 mins) === |
| − | + | Makefiles are not cross-platform. We are moving to CMake. | |
| − | + | '''Your Task:''' | |
| − | # | + | # Run <code>make clean</code> (or manually delete all generated <code>.o</code>, <code>.d</code>, and <code>.bin</code> files). |
| + | # Create a <code>CMakeLists.txt</code> file to build the exact same executable. | ||
| + | # Set the C++ standard to C++14. | ||
| + | # Perform an '''Out-of-Source build''' using a <code>build/</code> directory. | ||
| − | + | '''Verification:''' | |
| + | Your root directory should contain only your source files and the <code>CMakeLists.txt</code>. Inside the <code>build/</code> directory, running <code>make</code> should yield the executable. | ||
| − | + | === Challenge 3: Modular Architecture (Estimated time: 25 mins) === | |
| − | + | Dumping all source files into a single executable is bad practice. Hardware drivers should be isolated into static libraries. | |
| − | + | '''Your Task:''' Modify your <code>CMakeLists.txt</code>. | |
| − | + | # Remove <code>src/sensor.cpp</code> from your executable target. | |
| − | + | # Create a new STATIC library target named <code>sensor_driver</code> from <code>src/sensor.cpp</code>. | |
| − | + | # Attach the <code>include/</code> directory to this library target using <code>target_include_directories</code>. | |
| − | + | # Link the <code>sensor_driver</code> library to your main executable. | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | </ | ||
| − | + | '''Verification:''' | |
| − | + | Run <code>make</code> inside the build folder. You must see it compile <code>libsensor_driver.a</code> first, and then link it to the main executable. | |
| − | |||
| − | + | === Challenge 4: Third-Party Git Dependencies (Estimated time: 30 mins) === | |
| + | Our IoT node needs to transmit its data as a JSON payload, but writing a JSON parser from scratch is inefficient. | ||
| + | '''Your Task:''' | ||
| + | # Use CMake's <code>FetchContent</code> module to download the popular "Nlohmann JSON" library directly from GitHub. | ||
| + | #* Repo: <code>https://github.com/nlohmann/json.git</code> | ||
| + | #* Tag: <code>v3.11.2</code> | ||
| + | # Link the imported target (<code>nlohmann_json::nlohmann_json</code>) to your main executable. | ||
| + | # Modify <code>main.cpp</code> to include <code><nlohmann/json.hpp></code>. Serialize your <code>SensorData</code> into a JSON object and print it to the console using <code>.dump(4)</code>. | ||
| − | - | + | '''Verification:''' |
| + | Running your executable should print a perfectly formatted, multi-line JSON string representing your sensor data. Check <code>build/_deps/</code> to see the downloaded source code. | ||
| − | == Lab | + | == Lab Evaluation == |
| − | To receive full credit | + | To receive full credit, submit the following elements on Moodle: |
| − | # | + | # Final version of the Makefile. |
| − | # | + | # Final version of the CMakeLists.txt. |
| − | # | + | # Screenshot of the console output of your executable printing the JSON payload. |
| − | |||
Versiunea curentă din 16 martie 2026 17:07
Lab 3: Build Automation - Makefiles and Modern CMake
Introduction
In professional embedded development, clicking a "Build" button in an IDE is a luxury you rarely have. When working with Embedded Linux, RTOS environments, or CI/CD pipelines, you must define exactly how your source code is translated into machine code.
This lab transitions you from manual terminal commands to scalable build automation.
Goals
- Understand the C++ compilation pipeline (Preprocessing $\rightarrow$ Compilation $\rightarrow$ Linking).
- Master GNU Make: pattern rules, automatic variables, and header dependency generation.
- Master Modern CMake: out-of-source builds, targets, and static libraries.
- Integrate remote third-party libraries using CMake
FetchContent.
Part 1: Theoretical Reference
Use this section as a reference manual to complete the tasks in Part 2.
1.1 GNU Make Fundamentals
Make tracks file modification timestamps to avoid recompiling code that hasn't changed. A Makefile consists of rules:
target: dependencies
<TAB> command
(Note: Makefiles strictly require a real TAB character for indentation, not spaces!)
Automatic Variables: Make provides shortcuts to avoid typing duplicate filenames:
$@: The target filename.$<: The first dependency.$^: All dependencies (space-separated).
Pattern Rules: Instead of writing a rule for every file, use a pattern template:
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
GCC Auto-Dependencies (The Header Problem):
If you change a .h file, Make won't normally detect it because object files depend on .cpp files. We ask GCC to generate dependency (.d) files for us:
- Add
-MMD -MPto yourCXXFLAGS. - Include the generated files at the bottom of your Makefile using:
-include $(DEPS)
1.2 Modern CMake Fundamentals
CMake is a meta-build system. It generates Makefiles (or Ninja files, or Visual Studio solutions) based on a high-level CMakeLists.txt script. Modern CMake is "Target-Based"—you define targets (executables/libraries) and attach properties to them.
Core Commands:
cmake_minimum_required(VERSION 3.10)
project(MyProject CXX)
# Build an executable from source
add_executable(my_app main.cpp)
# Build a static library (.a archive)
add_library(my_driver STATIC driver.cpp)
# Define where a target should look for header files
target_include_directories(my_driver PUBLIC include/)
# Link a library to an executable
target_link_libraries(my_app PRIVATE my_driver)
Out-of-Source Builds: Never pollute your source directory with binaries. Always build in a separate folder:
mkdir build && cd build
cmake ..
make
Fetching External Libraries: CMake can download libraries directly from Git during the configuration phase:
include(FetchContent)
FetchContent_Declare(
json_lib
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(json_lib)
1.3 Useful Resources
Part 2: The Challenges
Setup: The Raw Materials
Create a new directory for this lab (sdpt-lab3). Inside, create an include/ directory and a src/ directory. Populate them with the following three files to simulate an IoT sensor node.
include/sensor.h
#pragma once
struct SensorData {
int temperature;
int humidity;
};
void init_sensor();
SensorData read_sensor();
src/sensor.cpp
#include <iostream>
#include "../include/sensor.h"
void init_sensor() {
std::cout << "[Hardware] I2C Sensor Initialized." << std::endl;
}
SensorData read_sensor() {
return {24, 60};
}
src/main.cpp
#include <iostream>
#include "../include/sensor.h"
int main() {
std::cout << "Starting IoT Node..." << std::endl;
init_sensor();
SensorData data = read_sensor();
std::cout << "Temp: " << data.temperature << "C, Hum: " << data.humidity << "%" << std::endl;
return 0;
}
Challenge 1: The Scalable Makefile (Estimated time: 25 mins)
Your Task: Write a Makefile in the root directory that compiles these files into an executable named iot_node.bin.
- Do not hardcode the
.cppfile names. Use thewildcardfunction to find them dynamically. - Use variables for the compiler (
CXX) and flags (CXXFLAGS). - Use Pattern Rules and Automatic Variables to compile
.cppto.o. - Implement GCC header dependency tracking (
-MMD -MP).
Verification:
- Run
make. It should compile successfully. - Run
makeagain immediately. It should say "Nothing to be done". - Open
include/sensor.hand add a dummy integer to theSensorDatastruct. Runmakeagain. It MUST recompile both.cppfiles automatically. If it doesn't, your dependency tracking is broken.
Challenge 2: Migrating to CMake (Estimated time: 20 mins)
Makefiles are not cross-platform. We are moving to CMake. Your Task:
- Run
make clean(or manually delete all generated.o,.d, and.binfiles). - Create a
CMakeLists.txtfile to build the exact same executable. - Set the C++ standard to C++14.
- Perform an Out-of-Source build using a
build/directory.
Verification:
Your root directory should contain only your source files and the CMakeLists.txt. Inside the build/ directory, running make should yield the executable.
Challenge 3: Modular Architecture (Estimated time: 25 mins)
Dumping all source files into a single executable is bad practice. Hardware drivers should be isolated into static libraries.
Your Task: Modify your CMakeLists.txt.
- Remove
src/sensor.cppfrom your executable target. - Create a new STATIC library target named
sensor_driverfromsrc/sensor.cpp. - Attach the
include/directory to this library target usingtarget_include_directories. - Link the
sensor_driverlibrary to your main executable.
Verification:
Run make inside the build folder. You must see it compile libsensor_driver.a first, and then link it to the main executable.
Challenge 4: Third-Party Git Dependencies (Estimated time: 30 mins)
Our IoT node needs to transmit its data as a JSON payload, but writing a JSON parser from scratch is inefficient. Your Task:
- Use CMake's
FetchContentmodule to download the popular "Nlohmann JSON" library directly from GitHub.- Repo:
https://github.com/nlohmann/json.git - Tag:
v3.11.2
- Repo:
- Link the imported target (
nlohmann_json::nlohmann_json) to your main executable. - Modify
main.cppto include<nlohmann/json.hpp>. Serialize yourSensorDatainto a JSON object and print it to the console using.dump(4).
Verification:
Running your executable should print a perfectly formatted, multi-line JSON string representing your sensor data. Check build/_deps/ to see the downloaded source code.
Lab Evaluation
To receive full credit, submit the following elements on Moodle:
- Final version of the Makefile.
- Final version of the CMakeLists.txt.
- Screenshot of the console output of your executable printing the JSON payload.