January 12, 2025

两种项目编译方式及其延伸

tsoding 大大写项目用 shell 脚本+ clang 来编译,CMU15445 的 Bustub 项目用 CMake 来编译,我来简单搜集信息

Shell

GUI -- 用户图形界面

CLI -- 命令行界面

Shell 相当于用户与操作系统之间的中介

More Infomation: Shell 编程范例

更加系统的学习,推荐:The Linux Command Line

脚本的运行

假设我编写好了一个脚本 test.sh,先给该文件可执行权限:

1
chmod +x test.sh

然后执行即可

1
test.sh

或是直接

1
bash test.sh
1
source test.sh
1
. test.sh

脚本的编写

脚本开头

1
#!/bin/bash

用于指定该脚本的解释器

剩下编译的部分

1
g++ -std=c++17 -o [filename] [filename].cpp

就好了

如果还有更复杂的要求可以问 GPT

GPT 写的一个脚本

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
#!/bin/bash

# 设置编译器和编译选项
CXX=g++
CXXFLAGS="-std=c++17 -O2 -Wall"
OUTPUT="program"

# 定义源文件和目标文件
SRC_DIR="src"
SRC_FILES=$(find $SRC_DIR -name "*.cpp")
OBJ_FILES=""

# 编译每个源文件为目标文件
for src in $SRC_FILES; do
obj="${src%.cpp}.o"
$CXX $CXXFLAGS -c $src -o $obj
OBJ_FILES="$OBJ_FILES $obj"
done

# 链接目标文件生成可执行文件
$CXX $OBJ_FILES -o $OUTPUT

# 清理目标文件(可选)
rm -f $OBJ_FILES

echo "编译完成,生成可执行文件:$OUTPUT"

g++-c 是仅编译不连接,生成 .o 文件

$ 在 Shell 中的用法

  1. 用于变量引用

    1
    2
    3
    4
    5
    $ name="John"
    $ echo $name
    John
    $ echo "Hello, ${name}Doe"
    Hello, JohnDoe

  2. 用于命令替换

    1
    2
    3
    $ current_date=$(date)
    $ echo "Today is $current_date"
    Today is Sat Jan 12 12:34:56 UTC 2025

  3. 用于表示一些参数

    $0 表示当前脚本的名称

    $1, $2,... 表示脚本的参数

    $# 表示参数个数

    $? 表示上一个命令的退出状态码(0 表示成功)

CMake

cmake 可以自动构建 Makefile,所以这个编译过程分为两个过程,先构建 Makefile 文件,再 make

CMakeLists.txt 是配置文件

构建过程

1
2
3
cmake -B build # 生成构建目录
cmake --build build # 执行构建
./build/project_name # 运行可执行文件

一些语法

基本的三个东西:

1
2
3
4
5
6
7
# 最低支持版本
cmake_minimum_required(VERSION 3.10)
# 项目名称
project(project_name)
# 添加可执行文件
# 头文件会自动在当前文件夹里找 第一个参数是 target 后面的参数是 dependency
add_executable(project_name pro1.cpp pro2.cpp)

添加库目标,可多次复用

1
2
3
4
add_library(lib_name STATIC lib.cpp)
add_executable(project pro.cpp)
# 链接库
target_link_libraries(project lib_name)

子目录中也有 CMakelist.txt,对子目录运行 CMake

1
add_subdirectory(dir_name)

若头文件被装到了一个子文件夹中,下面的语句给库头文件支持,PUBLIC 表示这些头文件可以被任何依赖该库的文件通过 #include<当前文件夹名/头文件名> 引用

1
target_include_directories(lib_name PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)

打印调试信息

1
message(STATUS "Current source dir: ${CMAKE_CURRENT_SOURCE_DIR}")

别的问 GPT

e.g. 1

记录我构建做算法题的工作流的过程

我的一个基本的文件布局

1
2
3
4
5
6
7
8
9
10
11
12
/ACM/2025/
├── 001/
│ ├── CSP-S2024
│ | ├── A.cpp
| | ├── B.cpp
| | ├── C.cpp
| | └── D.cpp
│ └── NOIP2024
├── 002/
├── 003/
├── aliases.sh
└── run_code.sh

默认用的 shell 是 zsh,可以拿 echo $SHELL 查看

zsh 的配置文件在 ~/.zshrc

aliases.sh 是用来定义函数和别名的,方便编译运行文件

run_code.sh 是编译并运行 .cpp 文件的脚本

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
# aliases.sh
#!/bin/zsh

acm() {
if [ $# -lt 2 ]; then
echo "Please input the correct parameters: "
echo " acm np [folder_name] [create_file_quantity]"
echo " acm run [cpp_file_name]"
return 1
fi
identifier=$1 # 标识符(如 np)
if [ "$identifier" = "np" ]; then
if [ $# -ne 3 ]; then
echo "Please input the correct parameters: "
echo " acm np [folder_name] [create_file_quantity]"
echo " acm run [cpp_file_name]"
return 1
fi
folder_name=$2 # 新建文件夹名
cpp_count=$3 # 要创建的 cpp 文件数量(如 3)
# 检查参数3是否是有效数字
if ! [[ "$cpp_count" =~ ^[0-9]+$ ]] || [ "$cpp_count" -le 0 ]; then
echo "请输入有效的数字参数!"
return 1
fi
# 获取当前目录下按时间排序的文件夹,选择最新的一个
# 查找符合条件的 3 位数字文件夹
latest_folder=$(find . -maxdepth 1 -type d | grep -E './[0-9]{3}$' | xargs -I {} stat -f "%m %N" {} | sort -n | tail -n 1 | awk '{print $2}')
latest_folder=$(basename "$latest_folder") # 提取文件夹名
latest_folder="${latest_folder}/" # 确保添加尾部斜杠
if [ -z "$latest_folder" ]; then
echo "当前目录下没有文件夹!"
return 1
fi
# 在最新文件夹下创建目标文件夹
target_folder="${latest_folder}${folder_name}"
# 如果目标文件夹不存在,则创建它
if [ ! -d "$target_folder" ]; then
mkdir "$target_folder"
echo "创建文件夹 $target_folder"
fi
# 在目标文件夹中创建指定数量的 cpp 文件
for i in $(seq 1 $cpp_count); do
# 获取字母序列 A、B、C...
letter=$(printf \\$(printf '%03o' $((65 + i - 1))))
touch "${target_folder}/${letter}.cpp"
echo "${letter}.cpp 创建完成"
done
echo "操作完成!"
elif [ "$identifier" = "run" ]; then
if [ $# -ne 2 ]; then
echo "Please input the correct parameters: "
echo " acm np [folder_name] [create_file_quantity]"
echo " acm run [cpp_file_name]"
return 1
fi
/Users/wsy/Back_end/ACM/2025/run_code.sh $2
else
echo "Please input the correct parameters: "
echo " acm np [folder_name] [create_file_quantity]"
echo " acm run [cpp_file_name]"
return 1
fi
}
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
# run_code.sh
#!/bin/bash

# 检查是否提供了参数
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <source_file>"
exit 1
fi

# 获取传入的文件路径
SOURCE_FILE="$1"

# 检查文件是否存在
if [ ! -f "$SOURCE_FILE" ]; then
echo "Error: File '$SOURCE_FILE' does not exist."
exit 1
fi

# 提取文件目录和文件名(不带扩展名)
DIR=$(dirname "$SOURCE_FILE")
BASE_NAME=$(basename "$SOURCE_FILE" .cpp)

# 设置输出的可执行文件路径(无后缀)
OUTPUT_FILE="$DIR/$BASE_NAME"

# 编译源文件
echo "Compiling '$SOURCE_FILE'..."
g++ -o "$OUTPUT_FILE" "$SOURCE_FILE"

# 检查编译是否成功
if [ "$?" -ne 0 ]; then
echo "Error: Compilation failed."
exit 1
fi

# 执行生成的可执行文件
echo "Executing '$OUTPUT_FILE'..."

touch "${OUTPUT_FILE}.out"

# 这里我将输出重定向到 .out 文件中再输出,解决了输入输出乱套的情况
"$OUTPUT_FILE" >"${OUTPUT_FILE}.out"
echo "Output:"
cat "${OUTPUT_FILE}.out"

GPT 写的脚本就是安全可靠

因为 aliases.sh 定义的别名只有在 source 后才会适用,我们希望每次进入这个文件夹后就可以使用 run 命令了,这个需要动态加载 aliases.sh 文件,在 .zshrc 中利用 chpwd 钩子(每次切换目录的时候会执行)动态加载 aliases.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# .zshrc

# 定义函数,用于寻找并加载 aliases.sh
function load_aliases() {
local dir=$(pwd)
while [ "$dir" != "/" ]; do
if [ -f "$dir/aliases.sh" ]; then
source "$dir/aliases.sh"
return
fi
dir=$(dirname "$dir")
done
}

# 使用 chpwd 钩子,每次目录切换时执行 load_aliases
function chpwd() {
load_aliases
}

# 初始加载,确保第一次打开 shell 时也加载
load_aliases

结合上我之前写过的 nvim 配置,这样的话就可以完整的构建一个做算法题时编译运行的工作流了,我发现这个 CLI 具有高度的定制化,是时候将 GUI 迁移到 CLI 了

关于本文

由 wsy_jim 撰写, 采用 CC BY-NC 4.0 许可协议.

#shell#cmake