65.9K
CodeProject 正在变化。 阅读更多。
Home

CPPkg:从 Visual Studio 创建源代码的 Zip 包

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (6投票s)

2020 年 7 月 8 日

MIT

8分钟阅读

viewsIcon

34149

downloadIcon

309

轻松地从源代码创建zip包,适合上传到CodeProject

引言

如果您在此处提交文章,您应该知道流程:要不就是退出Visual Studio以清除所有文件锁定,删除不必要的文件(例如binobj.vs目录),然后将剩余内容压缩成一个文件;或者小心地逐个添加所需文件到zip文件中,并排除上述目录。另一种选择是,如果您使用GitHub,可以直接将其项目下载为一个预打包的zip文件,然后上传到这里。

嗯,这足够麻烦了,我终于做了一个工具来自动化它。主要是,我厌倦了为了提交我的项目而退出Visual Studio。也许您也是。如果是这样,那么这个工具就是为您准备的。

更新: 修复了一个错误,当您使用链接在解决方案的多个项目之间共享同一个文件时,它会将文件添加两次。

使用这个烂摊子

有两种方式可以使用此工具

第一种,也是可能最常见的一种,是通过Visual Studio的工具菜单。安装此工具后,在工具菜单下,您会看到创建代码项目/Zip包。点击它将创建一个以解决方案命名的zip文件,并将其放置在解决方案目录中。然后它会启动一个资源管理器窗口,并选中新创建的zip文件。每次更新代码后,在提交到Code Project之前,您都需要执行此操作。任何之前的zip文件都会被自动静默覆盖,所以请注意这一点。

第二种方式是使用命令行实用程序cppkg。我们来看看它的用法屏幕

cppkg.exe <inputFile> [/output <outputFile>]

Creates a zip file out of the projects in the solution.

        <inputFile>     The input solution file to package
        <outputFile>    The output zip file to create

正如您所见,它非常简单。它接受一个不带开关的参数和一个可选的带开关的参数。前者是解决方案文件(.sln)的路径,后者是zip文件的路径。如果未指定output选项,则zip文件将与解决方案同名并在同一目录下创建。同样,任何之前的zip文件都会被静默覆盖。

您可以稍微“作弊”一下,并将此命令行作为解决方案中每个项目的生成后事件。这样,每次重新生成解决方案时,它都会重新生成zip文件。这比使用VS工具更自动化一些,但也有点过度。主要来说,这个CLI工具是为了在您需要批量处理的情况下提供的。

该工具的工作原理是扫描解决方案文件中的所有项目,然后对于每个项目,它会对项目文件执行各种查询,以确定要包含或排除哪些文件。因此,您必须在项目中包含任何您想包含在zip文件中的文件。仅仅在解决方案的文件夹中是不够的。这有优点也有缺点,优点是更精细地控制zip文件中的内容,缺点是需要记住将内容文件包含到项目中。

理解代码

代码的核心部分涉及处理解决方案文件格式以及项目文件格式,项目文件有两种风格。第一种风格是.NET Framework风格的项目文件,第二种是.NET Core和Standard项目文件。它们共享相似的格式,但遵循略有不同的规则。

项目文件是XML格式的,而解决方案文件采用其专有的格式,需要一些手工解析。为此,我们使用了我的LexContext代码,它大大简化了过程。对于XML部分,我们严重依赖XPath

首先,我们来看看解决方案的格式。这里,我们将使用这个cppkg.sln作为例子

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29905.134
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cppkg", _
        "cppkg\cppkg.csproj", "{76E9E90F-A812-468A-BE80-4B9577CCFB97}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", _
        "{C91DBE2D-B696-47DB-A04D-483D0E109321}"
    ProjectSection(SolutionItems) = preProject
        TextFile1.txt = TextFile1.txt
    EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cppkgvs", _
        "cppkgvs\cppkgvs.csproj", "{A4607C1A-CD0E-4878-9BF1-520146AFC55D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "testCore", _
        "testCore\testCore.csproj", "{88A06D5A-9993-4F2E-9B3B-DDF92061AB26}"
EndProject
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = _
        "testVBCore", "testVBCore\testVBCore.vbproj", "{261DF867-B1A4-41C2-AAD3-87BFB4B22151}"
EndProject
Global
    GlobalSection(SolutionConfigurationPlatforms) = preSolution
        Debug|Any CPU = Debug|Any CPU
        Release|Any CPU = Release|Any CPU
    EndGlobalSection
    GlobalSection(ProjectConfigurationPlatforms) = postSolution
        {76E9E90F-A812-468A-BE80-4B9577CCFB97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {76E9E90F-A812-468A-BE80-4B9577CCFB97}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {76E9E90F-A812-468A-BE80-4B9577CCFB97}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {76E9E90F-A812-468A-BE80-4B9577CCFB97}.Release|Any CPU.Build.0 = Release|Any CPU
        {A4607C1A-CD0E-4878-9BF1-520146AFC55D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {A4607C1A-CD0E-4878-9BF1-520146AFC55D}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {A4607C1A-CD0E-4878-9BF1-520146AFC55D}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {A4607C1A-CD0E-4878-9BF1-520146AFC55D}.Release|Any CPU.Build.0 = Release|Any CPU
        {88A06D5A-9993-4F2E-9B3B-DDF92061AB26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {88A06D5A-9993-4F2E-9B3B-DDF92061AB26}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {88A06D5A-9993-4F2E-9B3B-DDF92061AB26}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {88A06D5A-9993-4F2E-9B3B-DDF92061AB26}.Release|Any CPU.Build.0 = Release|Any CPU
        {261DF867-B1A4-41C2-AAD3-87BFB4B22151}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {261DF867-B1A4-41C2-AAD3-87BFB4B22151}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {261DF867-B1A4-41C2-AAD3-87BFB4B22151}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {261DF867-B1A4-41C2-AAD3-87BFB4B22151}.Release|Any CPU.Build.0 = Release|Any CPU
    EndGlobalSection
    GlobalSection(SolutionProperties) = preSolution
        HideSolutionNode = FALSE
    EndGlobalSection
    GlobalSection(ExtensibilityGlobals) = postSolution
        SolutionGuid = {3578F29E-FEF5-4B2A-8259-3FE6EAB153D9}
    EndGlobalSection
EndGlobal

我们只关心上面加粗的部分,所以不用担心其他部分。我们的代码首先在每行的第一个子字符串中查找“Project”。如果找到,它会区分包含“solution folders”的部分,其标识GUID为{2150E333-8FDC-42A3-9474-1A3956D46DE8},因为我们需要特殊处理它。这是执行此操作并处理其他项目的代码

var projects = new List<string>();
var files = new List<string>();
using (var sr = new StreamReader(solutionFile))
{
    var dir = Path.GetDirectoryName(solutionFile);
    string line;
    while (null != (line = sr.ReadLine()))
    {
        // i have no idea if the .sln format
        // is case sensitive or not so this 
        // assumes it isn't.
        if (line.ToLowerInvariant().StartsWith("project"))
        {
            var lc = LexContext.Create(line);
            lc.TrySkipLetters();
            lc.TrySkipWhiteSpace();
            if ('(' == lc.Current)
            {
                if(-1!=lc.Advance())
                {
                    lc.ClearCapture();
                    if(lc.TryReadDosString())
                    {
                        if ("\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\"" 
                              != lc.GetCapture().ToUpperInvariant())
                        {
                            lc.TrySkipWhiteSpace();
                            if (')' == lc.Current && -1 != lc.Advance())
                            {
                                lc.TrySkipWhiteSpace();
                                if ('=' == lc.Current && -1 != lc.Advance())
                                {
                                    lc.TrySkipWhiteSpace();
                                    string name;
                                    lc.ClearCapture();
                                    if (lc.TryReadDosString())
                                    {
                                        name = lc.GetCapture();
                                        lc.TrySkipWhiteSpace();
                                        if (',' == lc.Current && -1 != lc.Advance())
                                        {
                                            lc.TrySkipWhiteSpace();
                                            lc.ClearCapture();
                                            // I'm not actually sure what sort of escapes 
                                            // the string uses.
                                            string path;
                                            if (lc.TryReadDosString())
                                            {
                                                path = lc.GetCapture();
                                                path = path.Substring(1, path.Length - 2);
                                                path = path.Replace("\"\"", "\"");
                                                path = Path.Combine(dir, path);
                                                path = Path.GetFullPath(path);
                                                if(File.Exists(path))
                                                    projects.Add(path);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        else
                        {
                            lc.TrySkipWhiteSpace();
                            if (')' == lc.Current && -1 != lc.Advance())
                            {
                                while (null != (line = sr.ReadLine().Trim().ToLowerInvariant()) &&
                                    line != "endproject" &&
                                    !line.StartsWith("projectsection")) ;
                                if(line.StartsWith("projectsection") && 
                                    null != (line = sr.ReadLine()) &&
                                    line.Trim().ToLowerInvariant() != "endproject")
                                {
                                    do
                                    {
                                        lc = LexContext.Create(line);
                                        lc.TrySkipWhiteSpace();
                                        if(lc.TryReadUntil('=',false))
                                        {
                                            var fpath = lc.GetCapture().TrimEnd();
                                            fpath = Path.Combine(dir, fpath);
                                            fpath = Path.GetFullPath(fpath);
                                            if(File.Exists(fpath))
                                                files.Add(fpath);
                                        }
                                    } while (null != (line = sr.ReadLine()) &&
                                    line.Trim().ToLowerInvariant() != "endproject" &&
                                    !line.Trim().ToLowerInvariant().StartsWith_
                                                               ("endprojectsection"));
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
return (projects,files);

这有点复杂,但这就是解析过程的常态。幸运的是,lex上下文使得它比其他方式简单得多。基本上,它会忽略任何不以“project”开头的行,然后使用LexContext解析该行,直到找到项目路径。如果在任何时候解析没有产生预期的输入,它就会跳过该行的其余部分的解析。每次找到项目路径时,它都会将其添加到projects。对于Solution Folders部分,我们进行更深入的扫描,直到找到文件列表,然后将其添加到files

一旦代码获得了项目和文件的列表,它就会依次扫描每个项目,查找每个项目的独立内容。它主要使用.NET的XML功能,特别是XPath。由于存在两种不同的项目格式并且它们遵循不同的规则,所以会变得复杂,但至少XML本身在每种情况下都是相似的。无论哪种情况,我们只关心<ItemGroup>标签之间的内容

<ItemGroup>
  <Reference Include="System" />
  <Reference Include="System.IO.Compression" />
  <Reference Include="System.IO.Compression.FileSystem" />
  <Reference Include="System.Numerics" />
  <Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
  <Compile Include="LexContext.BaseExtensions.cs" />
  <Compile Include="LexContext.CommonExtensions.cs" />
  <Compile Include="LexContext.cs" />
  <Compile Include="Program.cs" />
  <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
  <None Include="App.config" />
</ItemGroup>

如您所见,有几个标签具有不同的本地名称,但每个标签都有一个Include属性。这些就是我们要寻找的。我们排除了<Reference><ProjectReference>标签,但除此之外,我们接受所有带有Include属性的内容。

对于.NET Core或.NET Standard项目,情况略有不同。虽然上述方法使用的是白名单方式包含文件,但Core/Standard项目文件使用的是黑名单方式。也就是说,它们包含子目录下的所有文件,除了binobj,或者除非它们被明确排除。同时,binobj下的文件可以通过明确包含来包含。这是一个典型的Core或Standard项目文件

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <None Remove="testExclude.txt" />
  </ItemGroup>
</Project>

项目文件非常小,我在这里只展示了整个文件。请注意,只有一个<ItemGroup>标签,它有一个带有Remove属性的元素。这表明该文件将从项目中排除。如上所述,目录和子目录下的其余文件(不包括binobj)将被包含。

我不会在此包含扫描项目的全部代码,因为它很长,而且由于规则不同,但基本上我们只是使用XPath在项目文件中查找上述所有感兴趣的项目,然后收集源路径,这也可以包括扫描项目目录以查找要包含的文件。这是代码主要功能的部分

iter = nav.Select("/e:Project/e:ItemGroup/*", res);
while (iter.MoveNext())
{
    if ("Reference" != iter.Current.LocalName && "ProjectReference" != iter.Current.LocalName)
    {
        var iter2 = iter.Current.Select("@Include");
        if (iter2.MoveNext())
        {
            var file = iter2.Current.Value;
            result.Add(file);
        }
    }
}

这大多是以上内容的变种,或者当我们还需要扫描文件系统时

var dir = Path.GetDirectoryName(projectFile);
var files = Directory.GetFiles(dir, "*.*");
for (var i = 0; i < files.Length; ++i)
{
    var f = Path.Combine(dir, files[i]);
    f = Path.GetFullPath(f);
    result.Add(f);
}
foreach (string d in Directory.GetDirectories(dir))
{
    _DirSearch(dir, d, result);
}

上面的_DirSearch()只是进行递归目录搜索。请注意,有一个参数和一些代码用于排除文件进行搜索。这是我CSBrick项目的遗留部分,该项目需要它。我将那段代码改编到这个项目中,因为它也扫描项目文件。我决定保留排除文件功能,以防将来需要它,因为它很复杂,而且我不想在将来需要时重新集成。

最后,一旦我们收集了所有项目和文件,我们就必须获取它们相对于解决方案的路径,并用它构建一个zip文件(代码已在第一次更新中修改,但未在此显示 - 大部分相同)

if (File.Exists(zipPath))
    File.Delete(zipPath);

using (var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create))
{
    zip.CreateEntryFromFile(sln, Path.Combine
        (Path.GetFileNameWithoutExtension(sln),Path.GetFileName(sln)));
    stdout.WriteLine(Path.GetFileName(sln));
    var ss = _GetSolutionStuff(sln);
    foreach(var fpath in ss.Files)
    {
        var relPath = Path.Combine(Path.GetFileNameWithoutExtension(sln), 
                      _GetRelativePath(fpath, dir, true));
        stderr.Write("\t");
        stdout.WriteLine(relPath);
        zip.CreateEntryFromFile(fpath,  relPath);
    }
    stderr.WriteLine();

    foreach (var projPath in ss.Projects)
    {
        stdout.WriteLine(_GetRelativePath(projPath, dir, true));
        var projRelPath = Path.Combine(Path.GetFileNameWithoutExtension(sln), 
                          _GetRelativePath(projPath, dir, true));
        zip.CreateEntryFromFile(projPath, projRelPath);
        foreach (var filePath in _GetProjectInputs(projPath, new HashSet<string>()))
        {
            var relPath = _GetRelativePath(filePath, dir, true);
            stderr.Write("\t");
            stdout.WriteLine(relPath);
            zip.CreateEntryFromFile(filePath, 
                Path.Combine(Path.GetFileNameWithoutExtension(sln), relPath));
        }
        stderr.WriteLine();
    }
}
stderr.WriteLine();

我们不会在此探讨的_GetRelativePathCode()不是我的代码。它是(c)2014年Yves Goergen的版权,http://unclassified.software/source/getrelativepath。我决定节省一些时间。版权声明也出现在源代码中。不幸的是,.NET 4.72尚未包含该功能。它计划在.NET 5中加入,尽管目前已包含在Core和Standard中。

您可能会注意到的一个地方是我向stdoutstderr写入,而不是使用Console。原因是我这个可执行文件也作为库被关联的Visual Studio扩展使用。在该环境中,没有控制台。当从该项目内部调用它时,我们传递TextReader.Null

限制

首先,如果您的某些文件不在解决方案文件夹下,该工具将失败,所以请不要这样做。它不知道如何将它们放入zip文件中,因为一切都相对于解决方案的路径。

这应该适用于C++项目,但我只测试过一个非常小的C++项目,而且我也没有测试过F#项目。您可能会遇到不同的结果。我很快会为C++进行更多测试。

我想,总的来说,它还需要更多的测试,因为您可以创建各种不同类型的项目。所以如果您遇到问题,请留下评论。只需记住包含您的readme、许可文档、图片和其他内容到您的项目中——任何您想包含在zip文件中的东西。否则,它们就不会被包含。

关注点

我最初打算编写完全独立的代码来从Visual Studio内部收集文件,但当我尝试时,我发现奇怪的是,它并没有给我准确的结果。一方面,在某些情况下它返回的是目录而不是文件,并且它丢失了诸如Resources文件夹下的文件之类的内容。我最初认为会更健壮的方法结果却是个失败。这确实很糟糕,但没关系。至少这样,我就不需要两套不同的代码来完成同一件事了。

历史

  • 2020年7月7日 - 首次提交
  • 2020年7月8日 - 修复了因时间问题引入的错误,该错误导致shell未能选中zip文件。
  • 2020年7月9日 - 修复了使用链接可能导致同一文件被添加两次的错误
CPPkg:从Visual Studio创建源代码Zip包 - CodeProject - 代码之家
© . All rights reserved.