dotnet Roslyn 通過讀取 suo 文件獲取解決方案的啟動項目


本文來告訴大家一個黑科技,通過 .suo 文件讀取 VisualStudio 的啟動項目。在 sln 項目里面,都會生成對應的 suo 文件,這個文件是 OLE 格式的文件,文件的格式沒有公開,本文的方法適合用在 VisualStudio 2019 上,對於其他版本的 VisualStudio 也許會不適合

感謝 Simon Cropp 大佬提供的方法

默認在 sln 解決方案文件的相同文件夾里面,將會存放 .vs\{解決方案名}\v{VS版本}\.suo 文件,如解決方案文件名為 HairhechallchujurKairbilairlem.sln 在 VisualStudio 2019 下將會存放 .vs\HairhechallchujurKairbilairlem\v16\.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 的內容大概如下

"\u0011\0MultiStartupProj\0=\u0003\0\0;4\0{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B}.dwStartupOpt\0=\u0003\0\0;\u000f\0StartupProject\0=\b&\0{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B};A\0{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B}.Release|Any CPU.fBatchBld\0=\u0003\0\0;?\0{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B}.Debug|Any CPU.fBatchBld\0=\u0003\0\0;4\0{AE3577E5-5D4E-44F8-B181-88A31B92584A}.dwStartupOpt\0=\u0003\0\0;A\0{AE3577E5-5D4E-44F8-B181-88A31B92584A}.Release|Any CPU.fBatchBld\0=\u0003\0\0;?\0{AE3577E5-5D4E-44F8-B181-88A31B92584A}.Debug|Any CPU.fBatchBld\0=\u0003\0\0;4\0{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}.dwStartupOpt\0=\u0003\0\0;\n\0ActiveCfg\0=\b\r\0Debug|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 修改編譯


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM