在软件开发过程中,代码复用是一种常见的实践,它可以帮助提高开发效率,减少重复劳动。Git作为目前最流行的版本控制系统,为提供了强大的代码管理功能。然而,当涉及到在不同仓库之间复用代码时,如何做到这一点并没有统一的标准。可以在Google上搜索Git子模块(submodules)或Bitbucket子树(subtrees),会发现有人推荐使用它们,但紧接着也会有人警告要避免使用,就像避免瘟疫一样。在尝试了不同的解决方案后,最终形成了自己的一套系统,并在之前的文章《吃自己的狗粮》中进行了描述。它展示了如何使用符号链接来组织多个C/C++项目。收到的反馈非常积极。
现在决定更进一步,提供一个自动化这个过程的工具。这就是CPM(C Package Manager)。这是一个简单的工具,只做非常特定的工作:构建依赖于许多其他C/C++库的C/C++项目。它不做的事情包括但不限于:炸薯条、早上帮整理床铺……好吧,懂的。
使用这个工具,有两个部分:一是必须遵循一定模式的项目布局,二是使用一个小的JSON文件描述项目的依赖树。
例如,考虑两个库cool_A和cool_B,它们需要在应用程序super_App中使用。cool_A和cool_B都使用另一个库utils的代码。每个都有自己的Git仓库。
必须遵循之前提到的提示中展示的原则。
规则1:所有项目都有自己的文件夹,所有项目文件夹都在一个父文件夹中。环境变量DEV_ROOT指向开发树的根。以下是一些ASCII艺术,展示了一般代码布局:
DevTreeRoot
|
+-- cool_A
| |
| +-- include
| | |
| | +-- cool_A
| | |
| | +-- hdr1.h
| | |
| | +-- hdr2.h
| +-- src
| | |
| | +-- file1.cpp
| | |
| | +-- file2.cpp
| +-- 项目文件(cool_A.vcxproj)和其他东西
|
+-- cool_B
|
+-- include
| |
| +-- cool_B
| |
| +-- hdr1.h
| |
| +-- hdr4.h
+-- src
| |
| +-- file1.cpp
| |
| +-- file2.cpp
|
+-- 项目文件(cool_B.vcxproj)和其他东西
规则2:需要对用户可见的包含文件放置在include文件夹的子文件夹中。子文件夹与库同名。如果cool_A的用户可以这样引用hdr1.h文件:
C++
#include
<
cool_A/hdr1.h
>
这种组织的额外好处是,它防止了不同库之间的名称冲突。在这种情况下,如果一个程序同时使用cool_A和cool_B,相应的include指令将是:
C++
#include
<
cool_A/hdr1.h
>
#include
<
cool_B/hdr1.h
>
规则3:依赖模块的包含文件夹通过符号链接变得可见。在之前展示的结构中,使用cool_A和cool_B的应用程序将有一个include文件夹,但在这个文件夹中,有指向cool_A和cool_B包含文件夹的符号链接。文件夹结构看起来像这样(尖括号表示符号链接):
DevTreeRoot
|
+-- SuperApp
| |
| +-- include
| | |
| | +--
| | | |
| | | +-- hdr1.h
| | | |
| | | +-- hdr2.h
| | |
| | +--
| | | |
| | | +-- hdr1.h
| | | |
| | | +-- hdr4.h
| | other header files
| |
| +-- src
| | |
| | +-- source files
| other files
...
规则4:所有库都位于开发树根的lib文件夹中。每个模块都包含一个指向这个文件夹的符号链接。不重复已经展示的文件布局部分,这里是与lib文件夹相关的一部分(再次,尖括号表示符号链接):
DevTreeRoot
|
+-- cool_A
| |
| ...
| +--
| |
| 所有链接库都在这里
+-- cool_B
| |
| ...
| +--
| |
| 所有链接库都在这里
+-- SuperApp
| |
| ...
| +--
| |
| 所有链接库都在这里
+-- lib
|
所有链接库都在这里
如果有不同的链接库版本(调试、发布、32位、64位),它们可以作为lib文件夹的子文件夹来容纳。
为了描述项目之间的关系,每个项目使用一个名为CPM.JSON的文件。super_App文件夹中的CPM.JSON具有以下内容:
{
"name": "super_App",
"git": "git@github.com:user/super_App.git",
"depends": [
{
"name": "cool_A",
"git": "git@github.com:user/cool_A.git"
},
{
"name": "cool_B",
"git": "git@github.com:user/cool_B.git"
}
],
"build": [
{
"os": "windows",
"command": "msbuild",
"args": ["super_app.proj"]
},
{
"os": "linux",
"command": "cmake"
}
]
}
对于每个依赖项目,都有一行描述依赖并给出Git仓库地址。构建部分指定了用于构建包的命令,针对每个操作系统。
类似地,cool_A文件夹中的描述如下:
{
"name": "cool_A",
"git": "git@github.com:user/cool_A.git",
"depends": [
{
"name": "utils",
"git": "git@github.com:user/utils.git"
}
],
"build": [
{
"os": "windows",
"command": "msbuild",
"args": ["cool_a.proj"]
},
{
"os": "linux",
"command": "cmake"
}
]
}
在cool_B中:
{
"name": "cool_B",
"git": "git@github.com:user/cool_B.git",
"depends": [
{
"name": "utils",
"git": "git@github.com:user/utils.git"
}
],
"build": [
{
"os": "windows",
"command": "msbuild",
"args": ["cool_b.proj"]
},
{
"os": "linux",
"command": "cmake"
}
]
}
最后,在utils中,没有依赖;只有构建规则:
{
"name": "utils",
"git": "git@github.com:user/utils.git",
"build": [
{
"os": "windows",
"command": "msbuild",
"args": ["utils.proj"]
},
{
"os": "linux",
"command": "cmake"
}
]
}
一旦创建了依赖描述文件并设置了DEV_ROOT环境变量,只需要克隆最顶层的仓库(super_App)并使用如下命令调用CPM工具:
cpm -v super_app