写一个小工具来可视化VisualStudio中项目之间的依赖关系(1.针对sln中的信息)
目标Visual Studio 中,一个“解决方案”(.sln)包含多个“项目”(.vcxproj)。而项目之间有依赖关系:当对一个项目进行生成时,它总会先确保其所依赖的项目先被生成。这样整体就会以一个正确的顺序生成。当项目变多时,我期望能用一个图来可视化这些关系以便观察。但我暂时还没找到特别方便的工具。不过,想了一下,其实这样的工具不难实现,我在之前的博客《为代码文件的include关系生成Me
本篇主要对问题进行分析,尝试对项目间依赖关系进行解析,但只是解析出了.sln
中的信息。对于完整的工具,详见下一篇。
目标
Visual Studio 中,一个“解决方案”(.sln
)包含多个“项目”(.vcxproj
)。
而项目之间有依赖关系:
当对一个项目进行生成时,它总会先确保其所依赖的项目先被生成。这样整体就会以一个正确的顺序生成。
当项目变多时,我期望能用一个图来可视化这些关系以便观察。但我暂时还没找到特别方便的工具。
不过,想了一下,其实这样的工具不难实现,我在之前的博客《为代码文件的include关系生成Mermaid图》和《为代码中的类继承关系生成Mermaid图》都做了类似的东西。这次的区别只在于——所谓的“关系”变为了“项目间依赖关系”。
而“项目间依赖关系”的数据一定是保存在文件中的,我想应该是.sln
文件或.vcxproj
文件,他们都是可阅读的文本文件,而非不可阅读的二进制文件。因此我可以对其进行解析。
因此,本篇的目标是完成这样的小工具。大体步骤如下:
- 研究“项目间依赖关系”的信息如何从代码上获取
- 解析出“项目间依赖关系”并生成Mermaid图
找到“项目间依赖关系”信息存在哪里
对于这个问题,有多种方法解决,例如:
- 查阅官方文档。优点是权威,缺点是不一定能找到,花费的时间长短也不一定。
- 先有一个自己的假设,比如 是
.sln
文件或.vcxproj
文件中写了一个项目依赖的项目的名字 。然后通过 ”字符串搜索“ 的方式来找到具体的位置。当自己有假设时这个方法的优点就是快。但缺点是不稳定,因为假设不一定对,而”字符串搜索“的结果也可能有歧义。 - 通过实验:比较指定项目依赖项的“前”与“后”,文件之间的差异。
此时我决定采用第3种。因为构建这样一个实验是相对简单的,而且通过实验一定能知道结果。
(当然并不是说这三种方法是互斥的,它们其实也可以结合,比如从 2 和 3 方法中获得思路并最后在官方文档中求证)
我做了小实验(详细操作见【附录:实验细节】)之后发现,在指定了一个项目的依赖项目之后,.sln
会添加内容:
(下图中test2
项目依赖testProjA
项目)
又经过对更复杂的工程的.sln
文件进行比对后,我得出结论:
.sln
中,每一个项目都以Project后跟一系列项目的数据的一行做开头,以EndProject这一行做结尾。在其中 ProjectSection(ProjectDependencies) = postProject 一行之后跟着数行其依赖的项目所对应的编码,然后以EndProjectSection结束。
写成代码的话就是:
Project("{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}") = "项目A的名字", "项目A的.vcxproj文件", "{项目A的编码}"
ProjectSection(ProjectDependencies) = postProject
{项目A的所依赖的第1个项目的编码} = {项目A的所依赖的第1个项目的编码}
{项目A的所依赖的第2个项目的编码} = {项目A的所依赖的第2个项目的编码}
{项目A的所依赖的第3个项目的编码} = {项目A的所依赖的第3个项目的编码}
EndProjectSection
EndProject
编写代码
在上一步分析出格式之后,“解析出依赖关系”变成了一个纯字符串处理的问题。
我的思路大概是这样:
- 以“行”为单位读取
.sln
文件内容 - 将行按项目来分割,标准是:以Project为开头的行起始,以EndProject的行结束。这些行的内容对应存储为一个
ProjectInfo
- 解析出
ProjectInfo
项目的名字。和项目的编码。 - 遍历每一个
ProjectInfo
,找到 ProjectSection(ProjectDependencies) = postProject 一行,然后对接下来的每一行读取其编码,只需要比对其他ProjectInfo
中的编码就可以知道其依赖项目的名字了。重复此操作直到 EndProjectSection 。 - 将上一步得到的依赖关系按照 Mermaid 的格式输出出来
具体代码如下:
#include <iostream>
#include<vector>
using namespace std;
//一个项目所对应的信息
struct ProjectInfo
{
//读取到的每一行信息
vector<string> lines;
//项目名字
string Name;
//项目编码
string Code;
//所依赖的项目
vector<ProjectInfo* >DependProjects;
};
//从字符串中分割得到信息的辅助函数
vector<string> StringSplitter(string source, char startChar, char endChar)
{
vector<string> result;
string current;
bool working = false;
for (int i = 0; i < source.size(); i++)
{
if (working)
{
if (source[i] == endChar)
{
result.push_back(current);
current.clear();
working = false;
}
else
current.push_back(source[i]);
}
else if (source[i] == startChar)
working = true;
}
return result;
}
int main()
{
//.sln文件的路径
const string slnFile = "D:/Temp/renderdoc.sln";
//读取文件
FILE* file;
{
fopen_s(&file, slnFile.c_str(), "r");
if (!file)
{
cout << "打开文件失败" << endl;
return -1;
}
}
//读取所有的项目信息
vector<ProjectInfo> projects;
{
ProjectInfo* CurrentProject = nullptr;//当前正在添加信息的项目
//读取每一行
char buff[1024];
while (fgets(buff, 1024, file) != nullptr)
{
string line = buff;
if (line.find("Project") == 0)//如果找到"Project"且是在开头,则说明是新项目
{
projects.push_back(ProjectInfo());
CurrentProject = &projects[projects.size() - 1];
}
if (CurrentProject != nullptr)//如果当前项目不为空,则添加此行到项目信息中
CurrentProject->lines.push_back(line);
if (line.find("EndProject") == 0)//如果找到"EndProject"且是在开头,则说明项目结束
CurrentProject = nullptr;
}
}
//遍历每一个项目找到其名字与编号
for (ProjectInfo& p : projects)
{
//Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "renderdocshim", "renderdocshim\renderdocshim.vcxproj", "{6DEE3F12-F2F8-42CA-865A-578D0FD11387}"
auto SubStrs = StringSplitter(p.lines[0],'\"', '\"');
p.Name = SubStrs[1];
p.Code = StringSplitter(SubStrs[3], '{', '}')[0];
}
//遍历每一个项目找到其依赖的项目
for (ProjectInfo& p : projects)
{
bool DependenciesLines = false;
for (auto line : p.lines)
{
if (DependenciesLines)
{
if (line.find("EndProjectSection") != string::npos)//如果找到"EndProjectSection"则结束写依赖的项目
DependenciesLines = false;
else //这是一条表示依赖的行
{
//依赖的项目的编码
string DpdProjCode = StringSplitter(line, '{', '}')[0];
//找到依赖的项目
for (ProjectInfo& other : projects)
if (other.Code == DpdProjCode)
p.DependProjects.push_back(&other);
}
}
if (line.find("ProjectSection(ProjectDependencies) = postProject") != string::npos)//如果找到"ProjectSection(ProjectDependencies) = postProject"则开始写依赖的项目
DependenciesLines = true;
}
}
//画Mermaid图
for (auto p : projects)
{
for (auto d : p.DependProjects)
cout << p.Name << "-->" << d->Name << endl;
}
}
测试
我对 baldurk/renderdoc/renderdoc.sln测试后结果如下:
发现问题:原来信息不止存于sln,还存于vcxproj
我发现在上一个测试中,依赖的信息明显比在VisualStudio的IDE中看到的少。
观察后发现,在IDE中能看到一些依赖关系,例如:
但是在.sln
文件中这些依赖关系并不能看到:
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "qrenderdoc", "qrenderdoc\qrenderdoc_local.vcxproj", "{A14A6AE5-02B1-35FE-BE59-B3E7C273B40B}"
EndProject
看来.sln
中并不包含所有的依赖信息。
我假设.vcxproj
中也有信息,并也以“项目的编码”的形式来指定其依赖的项目。
因此在qrenderdoc_local.vcxproj
中对其所依赖的项目version
(代码257FD75C-4D17-4A23-A754-23BFD85887A0
)进行搜索,发现可以找到:
很明显:.vcxproj
的格式是xml,其ProjectReference
节点中指明了其依赖的项目。
看来,当前的代码需要扩展。内容就放在之后吧。
(下一篇:《写一个小工具来可视化VisualStudio中项目之间的依赖关系(2.补全vcxproj中的信息)》)
附录:实验细节
1. 创建解决方案与项目
这会为我创建一个解决方案test2.sln
和一个同名的工程test2.vcxproj
2. 在解决方案下创建新项目
右键解决方案,选择新建项目
随后解决方案将有两个项目了
3. 复制此时的文件夹,作为【改动前版本】
注意保存当前的工程文件(比如关闭VisualStudio,会提示保存)
然后复制
4. 做操作,指定依赖关系
打开解决方案的属性面板
指定:test2
依赖于testProjA
然后点确定和应用
5. 复制此时的文件夹,作为【改动后版本】
注意保存当前的工程文件(比如关闭VisualStudio,会提示保存)
然后复制
6. 比较【改动前版本】与【改动后版本】
我这里使用 Meld 作为工具来比较:
结果:
改动不止一个,不过由于只有.sln
是可阅读的文本文件,因此只能从其入手:
幸运的是,这里面确实有想要的信息:
可以推断出,每个项目都对应一个编码,比如testProjA
对应82C67BD6-F17D-442B-B964-5067771F555E
,而test2
想要指定其依赖于testProjA
时将用编码来指代。
更多推荐
所有评论(0)