dotnet Roslyn 通过读取 suo 文件获取解决方案的启动项目

  • A+
所属分类:.NET技术
摘要

本文来告诉大家一个黑科技,通过 .suo 文件读取 VisualStudio 的启动项目。在 sln 项目里面,都会生成对应的 suo 文件,这个文件是 OLE 格式的文件,文件的格式没有公开,本文的方法适合用在 VisualStudio 2019 上,对于其他版本的 VisualStudio 也许会不适合

本文来告诉大家一个黑科技,通过 .suo 文件读取 VisualStudio 的启动项目。在 sln 项目里面,都会生成对应的 suo 文件,这个文件是 OLE 格式的文件,文件的格式没有公开,本文的方法适合用在 VisualStudio 2019 上,对于其他版本的 VisualStudio 也许会不适合

感谢 Simon Cropp 大佬提供的方法

默认在 sln 解决方案文件的相同文件夹里面,将会存放 .vs{解决方案名}v{VS版本}.suo 文件,如解决方案文件名为 HairhechallchujurKairbilairlem.sln 在 VisualStudio 2019 下将会存放 .vsHairhechallchujurKairbilairlemv16.suo 文件

这个 .suo 文件是包含了 VisualStudio 解决方案的一些配置,如启动项目。关多关于此文件,请参阅 Solution User Options (.Suo) File 文档

预计这个 suo 格式文件基本不会更改,在 1995 年的时候就开始使用这个格式

读取 .suo 需要使用到 Open MCDF 库。这是一个完全由 C# 实现的读取 OLE 格式文档的库,我在做 OFFICE 组件也用到这个库

在 suo 文件里面,通过 SolutionConfiguration 内容存放当前的启动项,这里面的内容是使用 UTF-16 编码的字符串,读取的方法如下

            using (var fileStream = new FileStream(suoFilePath, FileMode.Open))             {                 using CompoundFile compoundFile = new CompoundFile(fileStream, CFSUpdateMode.ReadOnly, CFSConfiguration.SectorRecycle | CFSConfiguration.EraseFreeSectors);                 var cfStream = compoundFile.RootStorage.GetStream("SolutionConfiguration");                 var byteList = cfStream.GetData();                 var encoding = Encoding.GetEncodings()                     .Single(x => string.Equals(x.Name, "utf-16", StringComparison.OrdinalIgnoreCase));                 var text = encoding.GetEncoding().GetString(byteList);             } 

这里的 text 的内容大概如下

"u0011MultiStartupProj=u0003;4{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B}.dwStartupOpt=u0003;u000fStartupProject=b&{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B};A{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B}.Release|Any CPU.fBatchBld=u0003;?{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B}.Debug|Any CPU.fBatchBld=u0003;4{AE3577E5-5D4E-44F8-B181-88A31B92584A}.dwStartupOpt=u0003;A{AE3577E5-5D4E-44F8-B181-88A31B92584A}.Release|Any CPU.fBatchBld=u0003;?{AE3577E5-5D4E-44F8-B181-88A31B92584A}.Debug|Any CPU.fBatchBld=u0003;4{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}.dwStartupOpt=u0003;nActiveCfg=brDebug|Any CPU;" 

通过读取 StartupProject 后续的内容即可找到当前的启动项目的 GUID 值,以下是我写的正则

                var text = encoding.GetEncoding().GetString(byteList);                  const char nul = 'u0000';                 const char dc1 = 'u0011';                 const char etx = 'u0003';                 const char soh = 'u0001';                  var startupProjectRegex = new Regex(@$"StartupProject{nul}={'b'}&{nul}(.{'{'}{38}{'}'});A");                 var startupProjectMatch = startupProjectRegex.Match(text);                 if (startupProjectMatch.Success)                 {                     var guid = Guid.Parse(startupProjectMatch.Groups[1].Value);                 } 

上面代码拿到的 guid 就是启动项目的 guid 内容

咱可以采用 Simon Cropp 大佬的开源项目 https://github.com/SimonCropp/SetStartupProjects 来辅助读取当前 sln 里面包含的 csproj 的 GUID 和路径

代码如下

var projectList = SetStartupProjects.SolutionProjectExtractor.GetAllProjectFiles(solutionFile.FullName).ToList(); 

通过 guid 获取当前的 csproj 项目文件路径方法如下

                    var guid = Guid.Parse(startupProjectMatch.Groups[1].Value);                     var project = projectList.FirstOrDefault(temp => new Guid(temp.Guid) == guid); 

我封装了方法,传入的是 sln 文件,返回启动项目的路径

        private static FileInfo GetStartupProject(FileInfo solutionFile)         {             var solutionFilePath = solutionFile.FullName;             var solutionDirectory = solutionFile.DirectoryName;              var solutionName = Path.GetFileNameWithoutExtension(solutionFilePath);             var suoDirectoryPath = Path.Combine(solutionDirectory, ".vs", solutionName, "v16");             Directory.CreateDirectory(suoDirectoryPath);             var suoFilePath = Path.Combine(suoDirectoryPath, ".suo");              var projectList = SetStartupProjects.SolutionProjectExtractor.GetAllProjectFiles(solutionFile.FullName).ToList();             using (var fileStream = new FileStream(suoFilePath, FileMode.Open))             {                 using CompoundFile compoundFile = new CompoundFile(fileStream, CFSUpdateMode.ReadOnly, CFSConfiguration.SectorRecycle | CFSConfiguration.EraseFreeSectors);                 var cfStream = compoundFile.RootStorage.GetStream("SolutionConfiguration");                 var byteList = cfStream.GetData();                 var encoding = Encoding.GetEncodings()                     .Single(x => string.Equals(x.Name, "utf-16", StringComparison.OrdinalIgnoreCase));                 var text = encoding.GetEncoding().GetString(byteList);                  const char nul = 'u0000';                 const char dc1 = 'u0011';                 const char etx = 'u0003';                 const char soh = 'u0001';                  var startupProjectRegex = new Regex(@$"StartupProject{nul}={'b'}&{nul}(.{'{'}{38}{'}'});A");                 var startupProjectMatch = startupProjectRegex.Match(text);                 if (startupProjectMatch.Success)                 {                     var guid = Guid.Parse(startupProjectMatch.Groups[1].Value);                     var project = projectList.FirstOrDefault(temp => new Guid(temp.Guid) == guid);                     return new FileInfo(project.FullPath);                 }             }              return null;         } 

需要先在项目安装 SetStartupProjects 库,才能使用这个方法

本文所有代码放在 githubgitee 欢迎小伙伴访问

除了读取启动项目,还可以读取断点等内容,读取 suo 里面的所有内容的方法如下

                compoundFile.RootStorage.VisitEntries(item =>                 {                     if (item.IsStream)                     {                         Console.WriteLine(item.Name);                          var stream = item as CFStream;                         byteList = stream.GetData();                         text = encoding.GetEncoding().GetString(byteList);                     }                 }, true); 

当然了,获取到的内容不一定使用 UTF-16 编码格式,还需要自己尝试,里面的数据只是二进制而是,上面代码的转换字符串只是用来调试

更多请看

SimonCropp/SetStartupProjects: Setting Visual Studio startup projects by hacking the suo

Solution User Options (.Suo) File

更多编译相关请看手把手教你写 Roslyn 修改编译