linux下使用vscode和makefile搭建C++開發環境

Z發表於2020-07-31

最近在linux上跑一些開源庫做學習用, 順手就搭了一下vscode的c++開發環境, 這裡分享一下vscode進行C++開發的基本環境結構.

1. 首先是編輯器, vscode直接官網下載的, 後期可以用 apt 直接更新, 個人覺得還是挺方便的, 有喜歡折騰的小夥伴可以去github上拉開源版本的下來自己編譯, 這裡不過多贅述

2. 其次是編譯器, 我使用的是GNU編譯器g++, 生成指令碼我選擇了makefile

以上是基礎工具, 如果把vscode換成vim + shell指令碼, 除錯直接gdb的話, 就基本上是原生環境開發了

 

接下來就是開發環境的搭建了, 這裡我先整理一下一個工程量稍微大一些的專案所應該包含的專案種類, 再根據整理的結果給出一個我寫的例子, 之後再對該例進行不斷完善

對於一個大型工程來說, 可能至少會包含以下幾種不同的工程:

1. 可執行程式 : 即專案主要的目標

2. 靜態庫 : 整合一些基礎的工具函式和一些基礎功能的封裝

3. 動態庫 : 作為外掛, 非核心功能之類的東西

4. 資原始檔 : 各種圖片, 檔案, 音訊, xml等等

以上是我認為的一個工程量稍大的程式可能會包含的專案種類, 根據上面這四類, 我構建瞭如下的檔案結構 :

.
├── debug
├── lib
├── project
│   ├── debug.makefile
│   ├── exe_test
│   │   ├── compile
│   │   ├── .d
│   │   ├── header
│   │   │   └── test.h
│   │   ├── makefile
│   │   └── src
│   │       └── test.cpp
│   ├── lib_a
│   │   ├── compile
│   │   ├── .d
│   │   ├── header
│   │   │   ├── a_1st.h
│   │   │   ├── a_2nd.h
│   │   │   └── a_3rd.h
│   │   ├── makefile
│   │   └── src
│   │       ├── a_1st.cpp
│   │       ├── a_2nd.cpp
│   │       └── a_3rd.cpp
│   ├── lib_so
│   │   ├── compile
│   │   ├── .d
│   │   ├── header
│   │   │   ├── so_1st.h
│   │   │   ├── so_2nd.h
│   │   │   └── so_3rd.h
│   │   ├── makefile
│   │   └── src
│   │       ├── so_1st.cpp
│   │       ├── so_2nd.cpp
│   │       └── so_3rd.cpp
│   └── makefile
├── release
└── .vscode
    ├── c_cpp_properties.json
    ├── launch.json
    ├── settings.json
    └── tasks.json

20 directories, 23 files

在當前專案目錄下共有4個子目錄和一個vscode專用的隱藏目錄 :

1. debug : 所有我們生成的debug版本的可執行程式以及debug版本程式所需的資源都會生成在這個目錄中

2. release : 同上, 但可執行程式和資原始檔都是release版的

3. lib : 所有動態庫, 靜態庫會生成在這個目錄中, debug版和release版用檔名結尾是否帶 D 來區分

4. project : 所有當前專案相關的工程都在這個目錄中

5. .vscode : vscode專用目錄, 其中包含了當前專案相關的vscode配置資訊

下面再看一下project目錄, 該目錄下共有3個專案目錄和兩個makefile :

1. lib_a : 該專案最終會生成一個靜態庫供程式使用

2. lib_so : 該專案最終會生成一個動態庫供程式使用

3. exe_test : 該專案最終會生成一個可執行程式, 該程式會使用到上述靜態庫和動態庫

4. 兩個makefile用於控制所有專案的debug版, release版生成

最後再解析一下每一個專案目錄, 每個專案都包含了4個子目錄和一個makefile :

1. src : 所有的原始檔放置在該目錄中

2. header : 所有的標頭檔案放置在該目錄中

3. compile : 編譯後的.o檔案會在這個目錄中生成

4. .d : 該目錄用於存放每個原始檔的依賴關係

5. makefile : 該makefile控制當前專案的生成

以上是例子檔案結構的大概說明, 下面我們就這個例子進行完善, 針對每一個工程和整個專案, 編寫makefile, 完成程式碼的編譯生成

 

首先針對整個專案, 我們要生成每一個工程, 並保證工程的生成順序符合每個工程間的依賴關係

這裡先看一下project/makefile, 這個makefile用於生成所有工程release版本

 1 export BUILD_VER := RELEASE
 2 export CXXFLAGS := -Wall -std=c++11
 3 export RM := rm -f
 4 
 5 .PHONY:build_all clean_all clean_all_cache
 6 
 7 build_all:
 8     cd ./lib_so && make
 9     cd ./lib_a && make
10     cd ./exe_test && make
11 
12 clean_all:
13     cd ./lib_so && make clean
14     cd ./lib_a && make clean 
15     cd ./exe_test && make clean
16 
17 clean_all_cache:
18     cd ./lib_so && make clean_cache
19     cd ./lib_a && make clean_cache
20     cd ./exe_test && make clean_cache

該makefile首先會覆寫3個變數, 並將變數匯出成為全域性變數, 其中BUILD_VER用於控制生成程式的版本, 緊隨其後的是3個偽目標, 分別用於生成每個工程, 清理所有生成檔案以及清理生成過程中的產生的.o和.d

接下來再來看project/debug.makefile, 這個makefile用於生成所有工程的debug版本

1 include ./makefile
2 
3 BUILD_VER := DEBUG

該makefile引入release版的makefile, 並修改BUILD_VER為DEBUG, 該makefile名稱不是make能夠自動識別的名稱, 使用需要加上 -f 引數, 如 : make -f debug.makefile

通過上面兩個makefile, 我們基本完成了對程式碼生成的版本控制和整個專案的生成流程, 下面只需要針對每一個工程, 編寫對應的makefile即可

下面是3個工程的makefile :

首先是靜態庫工程lib_a

 1 vpath %.cpp ./src
 2 vpath %.h ./header
 3 
 4 .PHONY: all clean clean_cache
 5 all : # 預設目標
 6 
 7 CXXINCLUDES = -I ./header
 8 ARFLAGS = -rcs
 9 SRCS_WITH_PATH = $(wildcard ./src/*.cpp)
10 SRCS = $(SRCS_WITH_PATH:./src/%.cpp=%.cpp)
11 DEPS = $(SRCS:.cpp=.d)
12 DEPS_WITH_PATH = $(SRCS:%.cpp=./.d/%.d)
13 OBJS = $(SRCS:.cpp=.o)
14 OBJS_WITH_PATH = $(SRCS:%.cpp=./compile/%.o)
15 TARGET_NAME = tsi.a
16 OUTDIR = ../../lib/
17 
18 ifeq ($(BUILD_VER), DEBUG)
19 CXXFLAGS += -g3
20 TARGET_NAME := tsiD.a
21 endif
22 
23 ifeq ($(BUILD_VER), RELEASE)
24 CXXFLAGS += -O2
25 endif
26 
27 #生成依賴關係,保證修改.h時也會重新編譯相關.cpp
28 -include $(DEPS)
29 
30 %.d:$(SRCS)
31     @set -e;\
32     $(RM) $@;\
33     $(CXX) $(CXXINCLUDES) -MM $< > .d/$@;
34 
35 %.o:%.cpp
36     $(CXX) $(CXXFLAGS) $(CXXINCLUDES) -c $< -o ./compile/$@
37 
38 all:$(TARGET_NAME)
39 
40 $(TARGET_NAME):$(OBJS)
41     $(AR) $(ARFLAGS) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH)
42 
43 clean:
44     $(RM) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
45 
46 clean_cache:
47     $(RM) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)

makefile中首先讀取了當前工程下的兩個目錄, 保證正確搜尋.h和.cpp之後宣告三個偽目標, 並以all為終極目標, 之後宣告瞭一系列變數, 這裡詳細解釋一下每一個變數, 跟大家解釋一下我的思路

CXXINCLUDES : 該變數包含了生成時c++的包含目錄

ARFLAGS : 靜態庫打包標誌

SRCS_WITH_PATH : 包含路徑的所有原始檔, 該寫法可以自動匹配指定目錄下的所有.cpp, 大型工程中可能會有很多原始檔, 每次更新刪除都要修改makefile的話會很不方便

SRCS : 剔除所有原始檔的字首路徑

DEPS : 對每一個原始檔, 生成一個對應的寫有依賴關係的.d檔案

DEPS_WITH_PATH : 包含字首路徑的全部.d檔案

OBJS : 原始檔編譯生成的全部.o檔案

OBJS_WITH_PATH : 包含字首路徑的全部.o檔案

TARGET_NAME : 生成目標的名稱

OUTDIR : 輸出目錄

 

在宣告瞭以上這些變數之後, 通過對全域性變數BUILD_VER的值的判斷, 在CXXFLAGS裡新增不同的引數以控制版本, 並對檔名等資訊做修改

接下來我用-include讓當前makefile讀取所有.d依賴關係, 當前檔案由於沒有找到這些.d檔案, 會在檔案中搜尋有無生成的靜態目標, 這時, make會搜尋到下方的%.d:$(SRCS)

根據該靜態目標, .d檔案便會被生成出來並被載入

假設我們當前指明生成的是偽目標all

all所依賴的目標是我們指定的檔名$(TARGET_NAME), 該變數所指向的目標又依賴於所有的.o檔案, 由於.o檔案沒有被生成, make又會搜尋並呼叫靜態目標%.o:%.cpp進行.o檔案的生成

在生成完所有的.o檔案之後, 目標$(TARGET_NAME)才會被執行, 最終在../../lib目錄中生成tsi.a或tsiD.a

理解了上面的內容之後, 接下來兩個工程 : 動態庫以及可執行檔案的makefile基本也可以套用上面的內容再進行修改得到, 這裡我貼出我的寫法供大家參考

動態庫makefile

 1 vpath %.cpp ./src
 2 vpath %.h ./header
 3 
 4 .PHONY: all clean clean_cache
 5 all : # 預設目標
 6 
 7 CXXFLAGS += -fPIC
 8 CXXINCLUDES = -I ./header
 9 SRCS_WITH_PATH = $(wildcard ./src/*.cpp)
10 SRCS = $(SRCS_WITH_PATH:./src/%.cpp=%.cpp)
11 DEPS = $(SRCS:.cpp=.d)
12 DEPS_WITH_PATH = $(SRCS:%.cpp=./.d/%.d)
13 OBJS = $(SRCS:.cpp=.o)
14 OBJS_WITH_PATH = $(SRCS:%.cpp=./compile/%.o)
15 TARGET_NAME = libtest.so
16 OUTDIR = ../../lib/
17 
18 ifeq ($(BUILD_VER), DEBUG)
19 CXXFLAGS += -g3
20 TARGET_NAME := libtestD.so
21 endif
22 
23 ifeq ($(BUILD_VER), RELEASE)
24 CXXFLAGS += -O2
25 endif
26 
27 #生成依賴關係,保證修改.h時也會重新編譯相關.cpp
28 -include $(DEPS)
29 
30 %.d:$(SRCS)
31     @set -e;\
32     $(RM) $@;\
33     $(CXX) $(CXXINCLUDES) -MM $< > .d/$@;
34 
35 %.o:%.cpp
36     $(CXX) $(CXXFLAGS) $(CXXINCLUDES) -c $< -o ./compile/$@
37 
38 all:$(TARGET_NAME)
39 
40 $(TARGET_NAME):$(OBJS)
41     $(CXX) -shared -o $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH)
42 
43 clean:
44     $(RM) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
45 
46 clean_cache:
47     $(RM) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)

可執行程式makefile

 1 vpath %.cpp ./src
 2 vpath %.h ./header
 3 
 4 .PHONY: all clean clean_cache
 5 all : # 預設目標
 6 
 7 CXXINCLUDES = -I ./header -I ../lib_a/header -I ../lib_so/header
 8 SRCS_WITH_PATH = $(wildcard ./src/*.cpp)
 9 SRCS = $(SRCS_WITH_PATH:./src/%.cpp=%.cpp)
10 DEPS = $(SRCS:.cpp=.d)
11 DEPS_WITH_PATH = $(SRCS:%.cpp=./.d/%.d)
12 OBJS = $(SRCS:.cpp=.o)
13 OBJS_WITH_PATH = $(SRCS:%.cpp=./compile/%.o)
14 LINK_LIB = ../../lib/tsi.a
15 LINK_USR_SO = -L ../../lib -Wl,-rpath=../lib -ltest
16 TARGET_NAME = test
17 OUTDIR = ../../release/
18 
19 ifeq ($(BUILD_VER), DEBUG)
20 CXXFLAGS += -g3
21 LINK_LIB := ../../lib/tsiD.a
22 LINK_USR_SO := -L ../../lib -Wl,-rpath=../lib -ltestD
23 TARGET_NAME := testD
24 OUTDIR := ../../debug/
25 endif
26 
27 ifeq ($(BUILD_VER), RELEASE)
28 CXXFLAGS += -O2
29 endif
30 
31 #生成依賴關係,保證修改.h時也會重新編譯相關.cpp
32 -include $(DEPS)
33 
34 %.d:$(SRCS)
35     @set -e;\
36     $(RM) $@;\
37     $(CXX) $(CXXINCLUDES) -MM $< > .d/$@;
38 
39 %.o:%.cpp
40     $(CXX) $(CXXFLAGS) $(CXXINCLUDES) -c $< -o ./compile/$@
41 
42 all:$(TARGET_NAME)
43 
44 $(TARGET_NAME):$(OBJS)
45     $(CXX) -o $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(LINK_LIB) $(LINK_USR_SO)
46 
47 clean:
48     $(RM) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
49 
50 clean_cache:
51     $(RM) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)

這裡有幾點需要注意的是, 在可執行程式連結時, 我用-Wl,-rpath指定了程式執行時回去何處尋找libtest.so這個動態庫, 如果不想這樣寫, 需要指定動態庫生成到系統預設載入的路徑中去, 比如/usr/lib, 同樣程式在其他機器上部署時也需要做同樣的操作

另外就是關於.d依賴生成我使用的引數是-MM, 因為GNU編譯器如果使用-M引數會自動加入一些其它的依賴關係, 具體內容可以用g++ -M xxx.cpp做簡單驗證, 如下圖:

-MM:

 

-M(後面還有....):

 

在完成了上述步驟之後, 我們的專案其實已經可以正常編譯生成執行了, 只是跟vscode沒什麼聯絡, 這裡我們先在project目錄中執行make, make clean_all, make, make clean_all_cache來看一下辛苦編寫makefile的成果

 

 

 

 很成功, 舒服了

接下來, 為了做到一鍵執行(F5)或者一鍵debug除錯, 我們要對vscode進行專案配置, 這裡我們要修改.vscode目錄下的三個檔案:launch.json task.json c_cpp_properties.json

在此之前先貼一下我在vscode中安裝的外掛, 這些外掛能讓開發環境更美觀, 程式碼編寫更順暢

 

 其中C/C++提供了只能高亮標頭檔案查詢等功能, Chinese是一些選項的漢化, Font Switcher可以快速更換字型(這個無所謂...), One Dark Pro是一款比較美觀的主題配色, Python(個人需求, 寫一些簡單的指令碼還是很方便的), TabOut可以針對各種括號按tab快速跳出, vscode-icons美化各種圖示

下面到了vscode的啟動配置, 在vscode的執行選項卡中, 我們可以選擇當前專案啟動的配置, 該配置集由launch.json來控制, 這裡我先貼出我的launch.json, 再進行詳細說明

 1 {
 2   "configurations": [
 3     {
 4       "name": "run release",
 5       "type": "cppdbg",
 6       "request": "launch",
 7       "program": "${workspaceFolder}/release/test",
 8       "args": ["-r", "-debug"],
 9       "stopAtEntry": false,
10       "cwd": "${workspaceFolder}/release",
11       "environment": [],
12       "externalConsole": false,
13       "MIMode": "gdb",
14       "setupCommands": [
15           {
16               "description": "為 gdb 啟用整齊列印",
17               "text": "-enable-pretty-printing",
18               "ignoreFailures": true
19           }
20       ],
21       "preLaunchTask": "make release"
22     },
23     {
24       "name": "run debug",
25       "type": "cppdbg",
26       "request": "launch",
27       "program": "${workspaceFolder}/debug/testD",
28       "args": ["-r", "-debug"],
29       "stopAtEntry": true,
30       "cwd": "${workspaceFolder}/debug",
31       "environment": [],
32       "externalConsole": false,
33       "MIMode": "gdb",
34       "setupCommands": [
35         {
36             "description": "為 gdb 啟用整齊列印",
37             "text": "-enable-pretty-printing",
38             "ignoreFailures": true
39         }
40       ],
41       "preLaunchTask": "make debug"
42     }
43   ]
44 }

這裡我配置了兩個啟動選項, 一個直接執行release程式, 另一個執行debug程式, 這裡針對debug啟動項進行解釋說明

name : 我們在啟動選項卡里看到的啟動項名稱

type : cppdbg就可以, 具體可以查閱vscode官方說明

request : 啟動項型別, 一種是附加程式一種是直接啟動, 這裡是直接啟動

program : 啟動程式路徑, 在vscode裡開啟的根目錄即為${workspaceFolder}, 後面加上release路徑

args : 傳入程式的引數

stopAtEntry : 程式是否自動在入口暫停, debug版才有用哦

cwd : 程式執行時的目錄

environment :要新增到程式環境中的環境變數, 具體可以查閱vscode官方說明, 這裡我直接沒填

externalConsole : 選擇程式是在新的控制檯中啟動還是在整合控制檯啟動

MIMode : 偵錯程式選擇

setupCommands : vscode官方文件查, 這裡我是直接用預設配置的

preLaunchTask : 這個是最重要的選項了, 該選項指明瞭在執行當前選項卡之前要執行的task任務, 這個task任務配置在同目錄下的tasks.json中, 這裡填的內容是task的label

 

為了解釋preLaunchTask這個選項, 我們引入tasks.json, 這裡貼出我的tasks.json, 進行說明

 1 {
 2   "version": "2.0.0",
 3   "tasks": [
 4     {
 5       "type": "shell",
 6       "label": "make release",
 7       "command": "make",
 8       "args": [],
 9       "options": {
10         "cwd": "${workspaceFolder}/project"
11       },
12       "group": "build"
13     },
14     {
15       "type": "shell",
16       "label": "make debug",
17       "command": "make -f debug.makefile",
18       "args": [],
19       "options": {
20         "cwd": "${workspaceFolder}/project"
21       },
22       "group": "build"
23     },
24     {
25       "type": "shell",
26       "label": "make clean",
27       "command": "make",
28       "args": ["clean_all"],
29       "options": {
30         "cwd": "${workspaceFolder}/project"
31       },
32       "group": "build"
33     },
34     {
35       "type": "shell",
36       "label": "make clean debug",
37       "command": "make -f debug.makefile",
38       "args": ["clean_all"],
39       "options": {
40         "cwd": "${workspaceFolder}/project"
41       },
42       "group": "build"
43     },
44     {
45       "type": "shell",
46       "label": "make clean cache",
47       "command": "make",
48       "args": ["clean_all_cache"],
49       "options": {
50         "cwd": "${workspaceFolder}/project"
51       },
52       "group": "build"
53     }
54   ]
55 }

在這個檔案中我配置了5個task, 其中前2個task : make release 和 make debug用於執行不同的makefile

這裡我針對make debug做個簡單說明

type : task的型別, 這裡填shell相當於執行shell命令

label : task的名字

command : 要執行的指令, 這裡要注意 make -f xxx.file這種命令, -f xxx這個內容要直接寫到命令內容中, 而不能寫到下面的args裡, 會無法識別, 這裡大家可以自行驗證一下

args : command要附加的引數

options : 其他選項

cwd : task執行的目錄

group : task的分組, 可以查一下vscode官方說明

 

經過以上配置, vscode就和我們的makefile聯絡在一起了, 選好啟動項f5就完事了, 這裡我貼出我的test.cpp, test.h, 和vscode斷點除錯執行截圖

 1 #include "test.h"
 2 
 3 int main(int argc, char* argv[])
 4 {
 5   if (argc > 0)
 6   {
 7     std::cout << "input param : ";
 8     for (int idx = 0; idx < argc; ++idx)
 9     {
10       std::cout << argv[idx] << " ";
11     }
12     std::cout << std::endl;
13   }
14 
15   std::cout << std::endl << "using a" << std::endl;
16   std::cout << tsi::a1st::lib_name() << std::endl
17             << tsi::a2nd::lib_author() << std::endl
18             << tsi::a3rd::lib_version() << std::endl;
19 
20   std::cout << std::endl << "using so" << std::endl;
21   std::cout << tsi::so1st::lib_name() << std::endl
22             << tsi::so2nd::lib_author() << std::endl
23             << tsi::so3rd::lib_version() << std::endl;
24   return 0;
25 }
 1 #ifndef _TSI_TEST_
 2 #define _TSI_TEST_
 3 
 4 #include <iostream>
 5 
 6 #include "a_1st.h"
 7 #include "a_2nd.h"
 8 #include "a_3rd.h"
 9 #include "so_1st.h"
10 #include "so_2nd.h"
11 #include "so_3rd.h"
12 
13 #endif

 

 

 

 

 

 

 

 

這樣, 一個簡單的專案工程的開發環境就搭建成功了. PS: 在除錯時, 我遇到了一個小問題, 這裡也貼一下

 

 這裡一開始我無法進行gdb除錯, 提示無法讀取檔案云云, 點選建立後, 有了上述提示, 在網上檢索了一下, 只有解決方案, 沒有詳細解釋其中機理, 這裡我先貼出解決辦法

在/目錄下建立build目錄,在該目錄中建立錯誤提示中對應的目錄, 並下載提示對應版本glibc到目錄中並解壓即可解決問題

關於該錯誤我認為是gdb除錯載入路徑錯誤導致, 如果有了解詳細原因的朋友, 請務必留言指點, 在此謝過

 

以上, 上方示例只是一個簡單的專案結構, 其中一定還有很多不足之處, 本文僅起到一個拋磚引玉的作用, 如有錯誤疏漏, 請務必指出, 有問題歡迎討論, 轉載註明出處, 感謝

相關文章