Diferență între revizuiri ale paginii „SDPT Lab 3”

De la WikiLabs
Jump to navigationJump to search
(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 Systems - From GNU Make to Modern CMake =
+
= Lab 3: Build Automation - Makefiles and Modern CMake =
  
 
== Introduction ==
 
== Introduction ==
In Week 2, we solved the human collaboration problem using Git. Today, we solve the compilation scaling problem.  
+
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.  
  
As embedded projects grow from a single file to hundreds of files with external dependencies (like cryptography or networking libraries), clicking a "Build" button in an IDE or typing <code>g++ main.cpp</code> in the terminal is no longer viable. By the end of this 2-hour lab, you will have:
+
This lab transitions you from manual terminal commands to scalable build automation.  
# Written a scalable GNU Makefile using pattern rules and automatic variables.
 
# Solved the "Header Dependency Problem" using GCC's <code>-MMD</code> flags.
 
# Migrated the project to Modern CMake.
 
# Enforced clean "Out-of-Source" builds.
 
# Built a static hardware driver library and linked it to an executable.
 
# Automatically downloaded and linked a 3rd-party library directly from GitHub using CMake's <code>FetchContent</code>.
 
  
'''Requirement:''' You may work individually or with your partner from last week.
+
=== 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.''
  
== Part 1: Project Setup and The GNU Make Baseline (30 Minutes) ==
+
=== 1.1 GNU Make Fundamentals ===
First, we need some source code to compile. We will simulate an IoT sensor node.
+
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!)''
  
=== 1. Create the Project Workspace ===
+
'''Automatic Variables:'''
Open your terminal and create a new directory:
+
Make provides shortcuts to avoid typing duplicate filenames:
<pre>
+
* <code>$@</code> : The target filename.
mkdir sdpt-lab3-builds
+
* <code>$<</code> : The first dependency.
cd sdpt-lab3-builds
+
* <code>$^</code> : All dependencies (space-separated).
mkdir src include
 
</pre>
 
  
=== 2. Create the Source Files ===
+
'''Pattern Rules:'''
Create a header file <code>include/sensor.h</code>:
+
Instead of writing a rule for every file, use a pattern template:
<pre>
+
<syntaxhighlight lang="make">
#ifndef SENSOR_H
+
%.o: %.cpp
#define SENSOR_H
+
$(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>
  
#endif
+
'''<code>src/sensor.cpp</code>'''
</pre>
+
<syntaxhighlight lang="cpp">
 
 
Create the implementation file <code>src/sensor.cpp</code>:
 
<pre>
 
 
#include <iostream>
 
#include <iostream>
 
#include "../include/sensor.h"
 
#include "../include/sensor.h"
Linia 54: Linia 114:
  
 
SensorData read_sensor() {
 
SensorData read_sensor() {
     SensorData data;
+
     return {24, 60};
    data.temperature = 24;
 
    data.humidity = 60;
 
    return data;
 
 
}
 
}
</pre>
+
</syntaxhighlight>
  
Create the main application <code>src/main.cpp</code>:
+
'''<code>src/main.cpp</code>'''
<pre>
+
<syntaxhighlight lang="cpp">
 
#include <iostream>
 
#include <iostream>
 
#include "../include/sensor.h"
 
#include "../include/sensor.h"
Linia 70: Linia 127:
 
     init_sensor();
 
     init_sensor();
 
      
 
      
     SensorData current_data = read_sensor();
+
     SensorData data = read_sensor();
     std::cout << "Temp: " << current_data.temperature << "C, Hum: " << current_data.humidity << "%" << std::endl;
+
     std::cout << "Temp: " << data.temperature << "C, Hum: " << data.humidity << "%" << std::endl;
 
      
 
      
 
     return 0;
 
     return 0;
 
}
 
}
</pre>
+
</syntaxhighlight>
 
 
=== 3. Write the Scalable Makefile ===
 
In the root directory (<code>sdpt-lab3-builds/</code>), create a file named exactly <code>Makefile</code>.
 
''Warning: You MUST use an actual TAB character for the indented lines, not spaces!''
 
 
 
<pre>
 
CXX = g++
 
CXXFLAGS = -Wall -Wextra -O2 -MMD -MP
 
TARGET = firmware.bin
 
 
 
# Dynamically find all .cpp files in the src/ directory
 
SRCS = $(wildcard src/*.cpp)
 
 
 
# String substitution: convert .cpp list to .o list
 
OBJS = $(SRCS:.cpp=.o)
 
 
 
# Create a list of .d (dependency) files
 
DEPS = $(OBJS:.o=.d)
 
 
 
# Default rule
 
all: $(TARGET)
 
 
 
# Linker rule using automatic variables ($^ = all dependencies, $@ = target)
 
$(TARGET): $(OBJS)
 
$(CXX) $^ -o $@
 
 
 
# Pattern rule to compile any .cpp into a .o file ($< = first dependency)
 
%.o: %.cpp
 
$(CXX) $(CXXFLAGS) -c $< -o $@
 
 
 
# Clean rule to remove binaries
 
clean:
 
rm -f $(OBJS) $(DEPS) $(TARGET)
 
 
 
# Include the auto-generated GCC dependencies
 
-include $(DEPS)
 
</pre>
 
 
 
=== 4. Test and Inspect the Build ===
 
Run the build:
 
<pre>
 
make
 
./firmware.bin
 
</pre>
 
Now, look inside your <code>src/</code> folder. Run <code>ls src/</code>.
 
Notice the <code>.d</code> files? Open <code>src/main.d</code> in a text editor. You will see that GCC automatically wrote a Makefile rule proving that <code>main.o</code> depends on <code>sensor.h</code>. This is how Make knows to recompile <code>main.cpp</code> if you only change the header file!
 
 
 
---
 
 
 
== Part 2: Migrating to Modern CMake (30 Minutes) ==
 
Makefiles are great, but they leave <code>.o</code> and <code>.d</code> files scattered all over our source code, and they are not cross-platform. Let's upgrade.
 
 
 
=== 1. Clean up the Make artifacts ===
 
Run the clean rule to delete all the generated binaries from Part 1.
 
<pre>
 
make clean
 
</pre>
 
 
 
=== 2. Write the CMakeLists.txt ===
 
In the root directory, create a file named <code>CMakeLists.txt</code>:
 
 
 
<pre>
 
cmake_minimum_required(VERSION 3.10)
 
project(IoTNode CXX)
 
 
 
set(CMAKE_CXX_STANDARD 14)
 
set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
 
 
# Define the executable and its source files
 
add_executable(firmware src/main.cpp src/sensor.cpp)
 
 
 
# Tell CMake where to look for header files
 
target_include_directories(firmware PRIVATE include/)
 
</pre>
 
 
 
=== 3. The Out-of-Source Build ===
 
We will never compile in the root directory again.
 
<pre>
 
mkdir build
 
cd build
 
 
 
# Tell CMake to read the parent directory and generate the build system
 
cmake ..
 
 
 
# Execute the build
 
make
 
</pre>
 
Run your program: <code>./firmware</code>. Notice that your <code>src/</code> directory is completely clean. All binaries are safely isolated inside the <code>build/</code> folder.
 
 
 
---
 
 
 
== Part 3: Modular Architecture (Static Libraries) (30 Minutes) ==
 
In professional embedded development, we don't dump all source files into the executable. We build independent, reusable Static Libraries (<code>.a</code> files) for hardware drivers.
 
 
 
=== 1. Refactor CMakeLists.txt ===
 
Go back to your root directory and open <code>CMakeLists.txt</code>. Rewrite it to decouple the sensor driver from the main application:
 
 
 
<pre>
 
cmake_minimum_required(VERSION 3.10)
 
project(IoTNode CXX)
 
 
 
set(CMAKE_CXX_STANDARD 14)
 
 
 
# 1. Build the sensor driver as a STATIC library
 
add_library(sensor_lib STATIC src/sensor.cpp)
 
 
 
# 2. Attach the include directory to the library (PUBLIC means anyone who links this library gets the headers too)
 
target_include_directories(sensor_lib PUBLIC include/)
 
 
 
# 3. Build the main executable (Notice we removed sensor.cpp!)
 
add_executable(firmware src/main.cpp)
 
 
 
# 4. Link the library to the executable
 
target_link_libraries(firmware PRIVATE sensor_lib)
 
</pre>
 
 
 
=== 2. Rebuild and Verify ===
 
Go into your <code>build/</code> directory and just run <code>make</code> (CMake automatically detects the changes to CMakeLists.txt and regenerates everything).
 
 
 
<pre>
 
cd build
 
make
 
</pre>
 
 
 
Look at the terminal output. You should see it building <code>libsensor_lib.a</code> first, and then linking it to <code>firmware</code>. Run <code>ls -l</code> to verify the <code>.a</code> archive exists.
 
 
 
---
 
 
 
== Part 4: Remote 3rd-Party Dependencies (30 Minutes) ==
 
Our IoT node generates data, but we need to format it as JSON to send it over the network. Writing a JSON parser in C++ from scratch is a terrible idea. We will use a popular 3rd-party library (Nlohmann JSON) and import it directly from GitHub using CMake's <code>FetchContent</code>.
 
 
 
=== 1. Fetching the Library ===
 
Open your <code>CMakeLists.txt</code> and add the FetchContent block ''before'' your executable definition:
 
 
 
<pre>
 
cmake_minimum_required(VERSION 3.14) # Note: Upgraded to 3.14 for FetchContent
 
project(IoTNode CXX)
 
  
set(CMAKE_CXX_STANDARD 14)
 
 
# --- Define local library ---
 
add_library(sensor_lib STATIC src/sensor.cpp)
 
target_include_directories(sensor_lib PUBLIC include/)
 
 
# --- Fetch 3rd Party Library from GitHub ---
 
include(FetchContent)
 
FetchContent_Declare(
 
  json
 
  GIT_REPOSITORY https://github.com/nlohmann/json.git
 
  GIT_TAG        v3.11.2
 
)
 
FetchContent_MakeAvailable(json)
 
  
# --- Define Executable ---
+
=== Challenge 1: The Scalable Makefile (Estimated time: 25 mins) ===
add_executable(firmware src/main.cpp)
+
'''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>).
  
# Link BOTH our local hardware library AND the remote JSON library
+
'''Verification:'''
# (The remote library exposes a target named 'nlohmann_json::nlohmann_json')
+
# Run <code>make</code>. It should compile successfully.
target_link_libraries(firmware PRIVATE sensor_lib nlohmann_json::nlohmann_json)
+
# Run <code>make</code> again immediately. It should say "Nothing to be done".
</pre>
+
# 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.
  
=== 2. Use the JSON Library in Code ===
 
Open <code>src/main.cpp</code> and update it to serialize our sensor data:
 
  
<pre>
+
=== Challenge 2: Migrating to CMake (Estimated time: 20 mins) ===
#include <iostream>
+
Makefiles are not cross-platform. We are moving to CMake.
#include "../include/sensor.h"
+
'''Your Task:'''
#include <nlohmann/json.hpp> // Included from the fetched library!
+
# 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.
  
using json = nlohmann::json;
+
'''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.
  
int main() {
+
=== Challenge 3: Modular Architecture (Estimated time: 25 mins) ===
    std::cout << "Starting IoT Node..." << std::endl;
+
Dumping all source files into a single executable is bad practice. Hardware drivers should be isolated into static libraries.
    init_sensor();
+
'''Your Task:''' Modify your <code>CMakeLists.txt</code>.
   
+
# Remove <code>src/sensor.cpp</code> from your executable target.
    SensorData current_data = read_sensor();
+
# 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>.
    // Create a JSON object and pack our data into it
+
# Link the <code>sensor_driver</code> library to your main executable.
    json payload;
 
    payload["device_id"] = "RPI_NODE_01";
 
    payload["status"] = "active";
 
    payload["data"]["temperature"] = current_data.temperature;
 
    payload["data"]["humidity"] = current_data.humidity;
 
   
 
    // Serialize and print the JSON string
 
    std::string network_message = payload.dump(4); // 4 spaces of indentation
 
    std::cout << "\n--- Transmission Payload ---\n" << network_message << std::endl;
 
   
 
    return 0;
 
}
 
</pre>
 
  
=== 3. The Final Build ===
+
'''Verification:'''
Go to your <code>build/</code> directory. Run <code>make</code>.
+
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.
''Note: This build will take slightly longer because CMake is reaching out to GitHub, downloading the library, and configuring it on the fly.''
 
  
Run <code>./firmware</code>. You should see a perfectly formatted JSON payload printed to your terminal!
+
=== 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 Deliverable ==
+
== Lab Evaluation ==
To receive full credit for this week's lab, demonstrate the following to your professor or TA:
+
To receive full credit, submit the following elements on Moodle:
# Run <code>./firmware</code> to show the successful JSON serialization.
+
# Final version of the Makefile.
# Show your <code>CMakeLists.txt</code> file demonstrating the <code>FetchContent</code> block and <code>target_link_libraries</code>.
+
# Final version of the CMakeLists.txt.
# Navigate to <code>build/_deps/json-src</code> in your terminal to prove that CMake successfully downloaded the external repository into the out-of-source build tree.
+
# Screenshot of the console output of your executable printing the JSON payload.
# Explain the difference between the <code>PUBLIC</code> and <code>PRIVATE</code> keywords in your CMake link step.
 

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 -MP to your CXXFLAGS.
  • 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 .cpp file names. Use the wildcard function to find them dynamically.
  • Use variables for the compiler (CXX) and flags (CXXFLAGS).
  • Use Pattern Rules and Automatic Variables to compile .cpp to .o.
  • Implement GCC header dependency tracking (-MMD -MP).

Verification:

  1. Run make. It should compile successfully.
  2. Run make again immediately. It should say "Nothing to be done".
  3. Open include/sensor.h and add a dummy integer to the SensorData struct. Run make again. It MUST recompile both .cpp 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:

  1. Run make clean (or manually delete all generated .o, .d, and .bin files).
  2. Create a CMakeLists.txt file to build the exact same executable.
  3. Set the C++ standard to C++14.
  4. 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.

  1. Remove src/sensor.cpp from your executable target.
  2. Create a new STATIC library target named sensor_driver from src/sensor.cpp.
  3. Attach the include/ directory to this library target using target_include_directories.
  4. Link the sensor_driver library 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:

  1. Use CMake's FetchContent module to download the popular "Nlohmann JSON" library directly from GitHub.
  2. Link the imported target (nlohmann_json::nlohmann_json) to your main executable.
  3. Modify main.cpp to include <nlohmann/json.hpp>. Serialize your SensorData into 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:

  1. Final version of the Makefile.
  2. Final version of the CMakeLists.txt.
  3. Screenshot of the console output of your executable printing the JSON payload.