Building and Debugging a C Project in Visual Studio Code with a Makefile
Introduction# I have been using Visual Studio Code (vscode) for my C language projects until I’m re-learning
this powerful programming language. One of the features that I like about vscode
is the ability to build
and debug
C language projects using a Makefile . In this post, I will show you how to build and debug a C project in vscode using a Makefile.
also, I built a GitHub repository template for this project called c-library-template , where you can use it to create a new C project with the following features:
NOTES:
This post is based on MacOS, but you can use it on other operating systems like Linux and Windows. I’m using Homebrew to install the required tools and libraries. Prerequisites# Install prerequisites# Install Visual Studio Code# Download and install Visual Studio Code manually or using the following terminal command:
1
brew install --cask visual-studio-code
Install Visual Studio Code extensions# For each extension mentioned before you can install it using the following steps:
Open Visual Studio Code, click on the Extensions
icon on the left sidebar, search for <extension name>
and click on the Install
button to install the extension.
Also, you can install the extensions using the following terminal commands:
1
2
3
4
code --install-extension ms-vscode.cpptools
code --install-extension ms-vscode.cpptools-extension-pack
code --install-extension ms-vscode.cpptools-themes
code --install-extension ms-vscode.makefile-tools
Install GCC, the GNU Compiler# Install gcc using the following terminal command:
Install GNU Make# Install make using the following terminal command:
Create a new C library project# I created a new repository template called c-library-template using GitHub CLI :
1
2
3
4
5
6
7
gh repo create slashdevops/c-library-template \
--add-readme \
--public \
--description "C library template" \
--gitignore "C" \
--license "Apache-2.0" \
--clone
Then inside the project directory c-library-template
, I created the following project structure:
1
2
3
4
5
mkdir { .vscode,src,include,tests}
touch { .gitignore,README.md,Makefile}
touch { include/linkedlist.h,src/linkedlist.c,tests/main.c}
touch { .vscode/launch.json,.vscode/tasks.json}
touch { .vscode/c_cpp_properties.json,.vscode/settings.json}
While you are in the project directory c-library-template
and after running the above commands, I used the tree
command, to show the project structure as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❯ tree -I .git -a
.
├── .gitignore
├── .vscode
│ ├── c_cpp_properties.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── LICENSE
├── Makefile
├── README.md
├── include
│ └── linkedlist.h
├── src
│ └── linkedlist.c
└── tests
└── main.c
5 directories, 11 files
These are the files and directories that I used as a project structure.
Complementing .gitignore
file# I added a extra files and directories to the c-library-template -> .gitignore file executing the following command:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat << EOF >> .gitignore
# Extra files and directories
lib/
obj/
build/
html/
latex/
.DS_Store
._.DS_Store
**/.DS_Store
**/._.DS_Store
EOF
C code files# The c-library-template -> include/linkedlist.h content is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#ifndef LINKEDLIST_H
#define LINKEDLIST_H
#include <stdio.h>
typedef struct Node
{
// size of the data type
size_t size ;
void * data ;
struct Node * next ;
} Node ;
typedef struct List
{
Node * head ;
size_t size ;
// used to have a reference to the last node, but
// this is not a circular linked list
Node * tail ;
} List ;
List * list_new ();
void list_destroy ( List * list );
void list_node_destroy ( Node * node );
void list_prepend ( List * list , Node * node );
void list_append ( List * list , Node * node );
void list_prepend_value ( List * list , void * value , size_t size );
void list_append_value ( List * list , void * value , size_t size );
Node * list_pop ( List * list );
size_t list_size ( List * list );
#endif // LINKEDLIST_H
The c-library-template -> src/linkedlist.c content is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include "../include/linkedlist.h"
#include <stdio.h>
#include <stdlib.h>
List * list_new ()
{
List * l = ( List * ) malloc ( sizeof ( List ));
l -> head = NULL ;
l -> tail = NULL ;
l -> size = 0 ;
return l ;
}
void list_destroy ( List * list )
{
if ( list -> head == NULL )
{
free ( list );
return ;
}
Node * temp_node = NULL ;
while ( list -> head != NULL )
{
temp_node = list -> head ;
list -> head = temp_node -> next ;
list_node_destroy ( temp_node );
}
free ( list );
}
void list_node_destroy ( Node * node )
{
free ( node -> data );
free ( node );
}
void list_prepend ( List * list , Node * node )
{
if ( list == NULL || node == NULL )
{
return ;
}
// store the pointer to the first element prepend to the list
// to keep track of the tail of the list
if ( list -> size == 0 )
{
list -> tail = node ;
}
node -> next = list -> head ;
list -> head = node ;
list -> size ++ ;
}
void list_append ( List * list , Node * node )
{
if ( list == NULL || node == NULL )
{
return ;
}
node -> next = NULL ;
// store the pointer to the first element prepend to the list
// to keep track of the tail of the list
if ( list -> size == 0 )
{
list -> head = node ;
list -> tail = node ;
}
// add the node to the tail of the list
list -> tail -> next = node ;
list -> tail = node ;
list -> size ++ ;
}
size_t list_size ( List * list )
{
return list -> size ;
}
void list_prepend_value ( List * list , void * value , size_t size )
{
Node * node = malloc ( sizeof ( Node ));
node -> next = NULL ;
node -> data = malloc ( size );
node -> data = value ;
node -> size = size ;
list_prepend ( list , node );
}
void list_append_value ( List * list , void * value , size_t size )
{
Node * node = malloc ( sizeof ( Node ));
node -> next = NULL ;
node -> data = malloc ( size );
node -> data = value ;
node -> size = size ;
list_append ( list , node );
}
Node * list_pop ( List * list )
{
if ( list == NULL || list -> head == NULL )
{
return NULL ;
}
Node * node = list -> head ;
if ( list -> head -> next != NULL )
{
list -> head = list -> head -> next ;
list -> size -- ;
}
else // this is the last
{
list -> head = NULL ;
list -> tail = NULL ;
list -> size = 0 ;
}
node -> next = NULL ;
return node ;
}
The c-library-template -> tests/main.c content is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#include "../include/linkedlist.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void test_list_new ()
{
List * list = list_new ();
assert ( list -> size == 0 );
assert ( list -> head == NULL );
assert ( list -> tail == NULL );
list_destroy ( list );
}
void test_list_size_new ()
{
List * list = list_new ();
assert ( list_size ( list ) == 0 );
list_destroy ( list );
}
void test_prepend_to_new_list ()
{
List * list = list_new ();
Node * node = malloc ( sizeof ( Node ));
list_prepend ( list , node );
assert ( list_size ( list ) == 1 );
list_destroy ( list );
}
void test_prepend_10 ()
{
List * list = list_new ();
for ( int i = 0 ; i < 10 ; i ++ )
{
Node * node = malloc ( sizeof ( Node ));
node -> next = NULL ;
node -> size = sizeof ( int );
node -> data = malloc ( sizeof ( Node ));
memcpy ( node -> data , & i , sizeof ( int ));
list_prepend ( list , node );
}
assert ( list_size ( list ) == 10 );
Node * node = list -> head ;
for ( int i = 0 ; i < 10 ; i ++ )
{
assert ( node -> size == sizeof ( int ));
int * val = ( int * ) node -> data ;
// printf("node value = %d, i= %d\n", *val, (9 - i));
assert ( * val == ( 9 - i ));
node = node -> next ;
}
list_destroy ( list );
}
void test_append_10 ()
{
List * list = list_new ();
for ( int i = 0 ; i < 10 ; i ++ )
{
Node * node = malloc ( sizeof ( Node ));
node -> next = NULL ;
node -> size = sizeof ( int );
node -> data = malloc ( sizeof ( Node ));
memcpy ( node -> data , & i , sizeof ( int ));
list_append ( list , node );
}
assert ( list_size ( list ) == 10 );
Node * node = list -> head ;
for ( int i = 0 ; i < 10 ; i ++ )
{
assert ( node -> size == sizeof ( int ));
int * val = ( int * ) node -> data ;
// printf("node value = %d, i= %d\n", *val, i);
assert ( * val == i );
node = node -> next ;
}
list_destroy ( list );
}
void test_list_destroy_10 ()
{
List * list = list_new ();
for ( int i = 0 ; i < 10 ; i ++ )
{
Node * node = malloc ( sizeof ( Node ));
node -> data = NULL ;
node -> next = NULL ;
list_prepend ( list , node );
}
assert ( list_size ( list ) == 10 );
list_destroy ( list );
}
void test_list_prepend_value ()
{
List * list = list_new ();
for ( int i = 10 ; i > 0 ; i -- )
{
int * val = malloc ( sizeof ( int ));
* val = i ;
list_prepend_value ( list , val , sizeof ( int ));
}
assert ( list_size ( list ) == 10 );
// check elemets in the list
Node * temp_node = list -> head ;
for ( int i = 0 ; i < 10 ; i ++ )
{
int * val = ( int * ) temp_node -> data ;
// printf("value = %d, size = %zu (bytes)\n", *val, temp_node->size);
assert ( * val == i + 1 );
assert ( sizeof ( int ) == temp_node -> size );
temp_node = temp_node -> next ;
}
list_destroy ( list );
}
void test_list_append_value ()
{
List * list = list_new ();
for ( int i = 10 ; i > 0 ; i -- )
{
int * val = malloc ( sizeof ( int ));
* val = i ;
list_append_value ( list , val , sizeof ( int ));
}
assert ( list_size ( list ) == 10 );
// check elements in the list
Node * temp_node = list -> head ;
for ( int i = 0 ; i < 10 ; i ++ )
{
int * val = ( int * ) temp_node -> data ;
// printf("value = %d, size = %zu (bytes)\n", *val, temp_node->size);
assert ( * val == 10 - i );
assert ( sizeof ( int ) == temp_node -> size );
temp_node = temp_node -> next ;
}
list_destroy ( list );
}
void test_list_pop_all ()
{
List * list = list_new ();
for ( int i = 0 ; i < 10 ; i ++ )
{
Node * node = malloc ( sizeof ( Node ));
node -> data = malloc ( sizeof ( int ));
memcpy ( node -> data , & i , sizeof ( int ));
node -> next = NULL ;
node -> size = sizeof ( int );
list_prepend ( list , node );
}
assert ( list_size ( list ) == 10 );
for ( int i = 0 ; i < 10 ; i ++ )
{
Node * pop_node = list_pop ( list );
assert ( pop_node != NULL );
// int *val = (int *)pop_node->data;
// printf("value: %d, size: %zu\n", *val, pop_node->size);
list_node_destroy ( pop_node );
}
list_destroy ( list );
}
void tests_run_all ( void )
{
test_list_new ();
test_list_size_new ();
test_prepend_to_new_list ();
test_prepend_10 ();
test_append_10 ();
test_list_destroy_10 ();
test_list_prepend_value ();
test_list_append_value ();
test_list_pop_all ();
}
int main ( void )
{
tests_run_all ();
}
Makefile# This Makefile
has a help command that you can use to display the available commands.
You can use the following command to display the help command:
The c-library-template -> Makefile content is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
.DELETE_ON_ERROR : clean
# Where to find tools
TEST_APP = test_linkedlist
TARGET_LIB = linkedlistlib.so
CC_MACOS ?= /opt/homebrew/bin/gcc-13
CC_LINUX ?= /usr/bin/gcc
AR_MACOS ?= /opt/homebrew/bin/gcc-ar-13
AR_LINUX ?= /usr/bin/ar
MEMCHECK_MACOS ?= /usr/bin/leaks
MEMCHECK_LINUX ?= /usr/bin/valgrind
# Determine OS
UNAME_S := $( shell uname -s)
ifeq ( $( UNAME_S ) ,Darwin)
MEMCHECK = $( MEMCHECK_MACOS)
MEMCHECK_ARGS = --atExit --
CC = $( CC_MACOS)
AR = $( AR_MACOS)
endif
ifeq ( $( UNAME_S ) ,Linux)
MEMCHECK = $( MEMCHECK_LINUX)
MEMCHECK_ARGS =
CC = $( CC_LINUX)
AR = $( AR_LINUX)
endif
# Check if OS is supported
ifneq ( $( UNAME_S ) ,Darwin)
ifneq ( $( UNAME_S ) ,Linux)
$( error "Unsupported OS ")
endif
endif
# Check if executables are in PATH
EXECUTABLES = $( CC) $( AR) $( MEMCHECK)
K := $( foreach exec,$( EXECUTABLES) ,\
$(if $( shell which $( exec )) ,some string,$( error "No $( exec ) in PATH)))
# Compiler and linker flags
CFLAGS = -Wall -Wextra -Werror -Wunused -O2 -g -std= c2x -pedantic # Compiler flags
LDFLAGS = -shared # Linker flags (shared library) (change to -static for static library)
SRC_DIR := src
OBJ_DIR := obj
LIB_DIR := lib
TEST_SRC_DIR := tests
TEST_OBJ_DIR := obj
BUILD_DIR := build
SRC_FILES = $( wildcard $( SRC_DIR) /*.c)
OBJ_FILES = $( SRC_FILES:$( SRC_DIR) /%.c= $( OBJ_DIR) /%.o)
TEST_FILES = $( wildcard $( TEST_SRC_DIR) /*.c)
TEST_OBJS = $( TEST_FILES:$( TEST_SRC_DIR) /%.c= $( TEST_OBJ_DIR) /%.o)
INCLUDE_DIRS = -Iinclude
# Targets
##@ Default target
.PHONY : all
all : clean build ## Clean and build the library
##@ Build commands
.PHONY : clean build
build : $( TARGET_LIB ) ## Clean and build the library
$(TARGET_LIB) : $( OBJ_FILES ) | $( LIB_DIR )
$( CC) $( LDFLAGS) -o $( LIB_DIR) /$@ $^
$(OBJ_DIR)/%.o : $( SRC_DIR ) /%.c | $( OBJ_DIR )
$( CC) $( CFLAGS) $( INCLUDE_DIRS) -c -o $@ $<
$(TEST_APP) : $( TEST_OBJS ) $( LIB_DIR ) /$( TARGET_LIB ) | $( BUILD_DIR )
$( CC) $( CFLAGS) $( INCLUDE_DIRS) -o $( BUILD_DIR) /$@ $^
$(TEST_OBJ_DIR)/%.o : $( TEST_SRC_DIR ) /%.c | $( TEST_OBJ_DIR )
$( CC) $( CFLAGS) $( INCLUDE_DIRS) -c -o $@ $<
$(BUILD_DIR) :
@mkdir -p $( BUILD_DIR)
$(OBJ_DIR) :
@mkdir -p $( OBJ_DIR)
$(LIB_DIR) :
@mkdir -p $( LIB_DIR)
##@ Test commands
.PHONY : test
test : clean build $( TEST_APP ) ## Run tests
@echo "Running tests..."
./$( BUILD_DIR) /$( TEST_APP)
.PHONY : memcheck
memcheck : test ## Run tests and check for memory leaks
@echo "Running tests with memory check..."
$( MEMCHECK) $( MEMCHECK_ARGS) ./$( BUILD_DIR) /$( TEST_APP)
##@ Clean commands
.PHONY : clean
clean : ## Clean built artifacts
@rm -rf $( BUILD_DIR)
@rm -rf $( OBJ_DIR)
@rm -rf $( LIB_DIR)
##@ Help commands
.PHONY : help
help : ## Display this help
@awk 'BEGIN {FS = ":.*##"; \
printf "Usage: make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ \
{ printf " \033[36m%-10s\033[0m %s\n", $$1, $$2 } /^##@/ \
{ printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' \
$( MAKEFILE_LIST)
The Makefile
has the following targets:
Visual Studio Code configuration files# The c-library-template -> .vscode/c_cpp_properties.json content is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"configurations" : [
{
"name" : "macos-gcc-arm64" ,
"compilerPath" : "/opt/homebrew/bin/gcc-13" ,
"intelliSenseMode" : "macos-gcc-arm64" ,
"includePath" : [
"${workspaceFolder}/**" ,
"${workspaceFolder}/include/**" ,
"/opt/homebrew/lib/gcc/13/**"
],
"defines" : [],
"macFrameworkPath" : [
"${workspaceFolder}/**" ,
"/System/Library/Frameworks"
],
"cStandard" : "c23" ,
"cppStandard" : "c++23" ,
"configurationProvider" : "ms-vscode.makefile-tools" ,
"browse" : {
"path" : [
"${workspaceFolder}/**" ,
"${workspaceFolder}/include/**" ,
"/opt/homebrew/lib/gcc/13/**"
]
}
}
],
"version" : 4
}
The c-library-template -> .vscode/launch.json content is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"version" : "2.0.0" ,
"configurations" : [
{
"name" : "C Debug -> linkedlist Makefile" ,
"type" : "cppdbg" ,
"request" : "launch" ,
"program" : "${workspaceFolder}/build/test_linkedlist" ,
"args" : [],
"stopAtEntry" : true ,
"cwd" : "${workspaceFolder}" ,
"environment" : [],
"externalConsole" : false ,
"MIMode" : "lldb" ,
"preLaunchTask" : "make"
}
]
}
The c-library-template -> .vscode/tasks.json content is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"version" : "2.0.0" ,
"tasks" : [
{
"type" : "cppbuild" ,
"label" : "make" ,
"command" : "make && make test" ,
"args" : [],
"options" : {
"cwd" : "${workspaceFolder}"
},
"problemMatcher" : [
"$gcc"
],
"group" : "build" ,
"detail" : "Build our program using make"
}
],
}
The c-library-template -> .vscode/settings.json content is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"C_Cpp.errorSquiggles" : "enabled" ,
"C_Cpp.enhancedColorization" : "enabled" ,
"C_Cpp.intelliSenseEngine" : "Tag Parser" ,
"files.associations" : {
"*.template" : "yaml" ,
"cstdlib" : "c" ,
"__hash_table" : "c" ,
"__split_buffer" : "c" ,
"array" : "c" ,
"bitset" : "c" ,
"deque" : "c" ,
"initializer_list" : "c" ,
"queue" : "c" ,
"span" : "c" ,
"stack" : "c" ,
"string" : "c" ,
"string_view" : "c" ,
"unordered_map" : "c" ,
"vector" : "c" ,
"format" : "c"
},
}
Build and Debug the C project# Open the project directory c-library-template
in Visual Studio Code: 1
2
cd c-library-template/
code .
Build the project using the make
command: Run the tests using the make test
command: 1
2
3
4
5
# Run the tests
make test
# Run the tests and check for memory leaks
make memcheck
Debug the project using the make
task: Set your breakpoints in the source code. Press Cmd + Shift + P
to open the command palette. Type C/C++: Debug
and select C/C++: Debug C/C++ File
. Select the C Debug -> linkedlist Makefile
task to build and start debugging the project.
Your browser does not support the video tag. Github Actions CI/CD Workflow# I created a GitHub Actions CI/CD workflow to build and test the project on every push to the main
branch.
The c-library-template -> .github/workflows/ci.yml content is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
name : C CI
on :
push :
branches : [ "main" ]
pull_request :
branches : [ "main" ]
workflow_dispatch :
jobs :
test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Install dependencies
run : |
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt-get update -y
sudo apt-get install -y gcc-13 valgrind
- name : Set up gcc-13
run : |
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 90
- name : Check versions
run : |
gcc --version
make --version
- name : make build
run : make build
- name : make test
run : make test
- name : make memcheck
run : make memcheck
build :
runs-on : ubuntu-latest
needs : test
if : github.event_name == 'push' && github.ref == 'refs/heads/main'
steps :
- uses : actions/checkout@v4
- name : Install dependencies
run : |
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt-get update -y
sudo apt-get install -y gcc-13 valgrind
- name : Set up gcc-13
run : |
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 90
- name : Check versions
run : |
gcc --version
make --version
- name : make build
run : make build
Conclusion# When you are (re-)learning a new programming language, it is important to have a good project structure and tools to help you build and debug your projects. I always spend time finding the way to make my life easier.
I hope this post helps you to build and debug your C projects using Visual Studio Code with a Makefile.
Leave a comment if you have any questions or suggestions.