检查Visual Studio项目的一致性

13

您有一个大型的Visual Studio解决方案,其中包含数十个项目文件。您如何验证所有项目遵循其属性设置中的某些规则,并在添加新项目时强制执行这些规则。例如,检查所有项目是否具有以下内容:

TargetFrameworkVersion = "v4.5"
Platform = "AnyCPU"
WarningLevel = 4
TreatWarningsAsErrors = true
OutputPath = $(SolutionDir)bin
SignAssembly = true
AssemblyName = $(ProjectFolderName)

我知道两种方法,我将在下面的答案中添加它们,但我想知道人们如何进行这种项目测试。我特别想了解可用的解决方案,例如库或构建任务,而不是必须发明新东西或从头编写。


你有没有考虑在所有项目中包含一个单一的通用文件,其中包含你提到的设置?这样就可以减少出错的机会。无论如何,要想100%确定,唯一的方法是解析所有项目并检查设置 - 你可以编写MsBuild代码来完成这个任务(类似于你的答案,但自动化,以便它可以自动运行每个项目,而且不需要修改项目),或者使用Microsoft.Build.Evaluation命名空间中的类来编写工具,例如C#来完成此任务。 - stijn
*. *proj文件是XML格式的,您可以编写程序查找任何违反规则的情况,然后采取适当的措施。您还可以将其与您选择的CI框架连接起来。 - Chris O
你最终找到解决方案了吗?我有一个类似的问题,我想跟踪不同版本的dll在包中使用的情况。 - Sam
@Sam 没有,但我曾与 PSBuild 的作者 Ibrahim Hashimi 进行了一些讨论,也许我们可以改进这个工具,以支持使用 PowerShell 进行此类验证。 - orad
10个回答

9
*.sln文件是纯文本且易于解析,而*.*proj文件则是XML格式的。
你可以添加一个虚拟项目,并添加一个预构建步骤,解析sln文件以检索所有项目文件,验证它们的设置,生成报告,并在必要时终止构建。
另外,你应该查看这篇帖子以确保始终执行预构建步骤。基本上,在自定义构建步骤中指定一个空输出以强制重新构建。

6
下面的列表列出了使用Visual Studio .NET集成开发环境(IDE)将解决方案添加到源代码控制时自动添加到VSS中的关键文件类型: - 解决方案文件(.sln)。这些文件中维护的关键项目包括组成项目的列表、依赖项信息、生成配置详细信息和源代码控制提供程序详细信息。 - 项目文件(.csproj或.vbproj)。这些文件中维护的关键项目包括程序集构建设置、已引用程序集(按名称和路径)和文件清单。 - 应用程序配置文件。这些是基于可扩展标记语言(XML)的配置文件,用于控制项目运行时行为的各个方面。
尽可能使用单一解决方案模型
另请参见:https://msdn.microsoft.com/en-us/library/ee817677.aspx, https://msdn.microsoft.com/en-us/library/ee817675.aspx 而对于持续集成: 有许多可用的工具,如MSBuild、Jenkins、Apache的Continuum、Cruise Control(CC)和Hudson(插件可以扩展到C#)。

我们采用单一解决方案模型。问题是,当解决方案包含30个项目时,如何确保它们所有的配置设置都是一致的,例如针对相同的处理器架构、存储在与AssemblyName相同的文件夹名称中,将所有警告视为错误等? - orad

4
在我们的工作中,我们使用一个PowerShell脚本来检查项目设置,并在设置不正确时进行修改。例如,我们通过这种方式删除调试配置,禁用C++优化和SSE2支持。我们目前手动运行该脚本,但可以将其自动化运行,例如作为预/后构建步骤。
以下是示例:
`function Prepare-Solution {  
param (  
    [string]$SolutionFolder
)  
$files = gci -Recurse -Path $SolutionFolder -file *.vcxproj | select -    ExpandProperty fullname  
$files | %{  
    $file = $_  
    [xml]$xml = get-content $file  

    #Deleting Debug configurations...
    $xml.Project.ItemGroup.ProjectConfiguration | ?{$_.Configuration -eq "Debug"} | %{$_.ParentNode.RemoveChild($_)} | Out-Null
    $xml.SelectNodes("//*[contains(@Condition,'Debug')]") |%{$_.ParentNode.RemoveChild($_)} | Out-Null

    if($xml.Project.ItemDefinitionGroup.ClCompile) {  
        $xml.Project.ItemDefinitionGroup.ClCompile | %{  
            #Disable SSE2
            if (-not($_.EnableEnhancedInstructionSet)){
                $_.AppendChild($xml.CreateElement("EnableEnhancedInstructionSet", $xml.DocumentElement.NamespaceURI)) | Out-Null  
            }   

            if($_.ParentNode.Condition.Contains("Win32")){  
                $_.EnableEnhancedInstructionSet = "StreamingSIMDExtensions"
            }
            elseif($_.ParentNode.Condition.Contains("x64")) {
                $_.EnableEnhancedInstructionSet = "NotSet"
            } else {
                Write-Host "Neither x86 nor x64 config. Very strange!!"
            }

            #Disable Optimization
            if (-not($_.Optimization)){  
                $_.AppendChild($xml.CreateElement("Optimization", $xml.DocumentElement.NamespaceURI)) | Out-Null  
            }   
            $_.Optimization = "Disabled" 
        } 
    } 
    $xml.Save($file);  
} }`

4
这是我自己的内容:
一种方法是创建一个带有错误条件的MSBuild目标:
<Error Condition="'$(TreatWarningsAsErrors)'!='true'" Text="Invalid project setting" />

我喜欢这种方法,因为它与MSBuild集成并且可以提供早期错误,但是你必须修改每个项目来导入它或让所有团队成员使用一个特殊的命令提示符和环境变量,在构建过程中注入自定义的预构建步骤,这很繁琐。
我知道的第二种方法是使用一些类似于VSUnitTest的库,它提供了API以测试项目属性。VSUnitTest目前不是开源的,并且未列在NuGet服务中。

4

让我们尝试完全不同的方法:您可以通过从模板生成它们或使用构建生成工具(如CMake)来确保它们在构建过程中是一致的。这可能比事后使它们一致更简单。


那是一个选项,虽然它有点更加复杂。 - orad

4
你可以编写代码,将解决方案作为文本文件打开,以识别引用的所有csproj文件,依次将每个项目作为xml文件打开,并编写单元测试以确保项目的特定节点与您的期望相匹配。
这是一种快速而粗略的解决方案,但适用于CI,并让您有灵活性忽略您不关心的节点。它听起来实际上很有用。我有一个包含35个项目的解决方案,也想要扫描。

3
只有当文件是托管的,并且在其元数据中包含程序集条目时,该文件才是程序集。有关程序集和元数据的更多信息,请参阅程序集清单主题。
如何手动确定文件是否为程序集:
1.启动Ildasm.exe(IL 反汇编器)。
2.加载要测试的文件。
3.如果 Ildasm 报告文件不是可移植可执行 (PE) 文件,则它不是程序集。如需详细了解,请参阅查看程序集内容。
如何以编程方式确定文件是否为程序集:
1.调用GetAssemblyName方法,传递您正在测试的文件的完整文件路径和名称。
2.如果引发BadImageFormatException异常,则文件不是程序集。
以下示例测试 DLL 是否为程序集。
class TestAssembly
{
static void Main()
   {

    try
    {
        System.Reflection.AssemblyName testAssembly = System.Reflection.AssemblyName.GetAssemblyName(@"C:\Windows\Microsoft.NET\Framework\v3.5\System.Net.dll");

        System.Console.WriteLine("Yes, the file is an assembly.");
    }

    catch (System.IO.FileNotFoundException)
    {
        System.Console.WriteLine("The file cannot be found.");
    }

    catch (System.BadImageFormatException)
    {
        System.Console.WriteLine("The file is not an assembly.");
    }

    catch (System.IO.FileLoadException)
    {
        System.Console.WriteLine("The assembly has already been loaded.");
    }
   }
}
  // Output (with .NET Framework 3.5 installed):
 // Yes, the file is an assembly.

框架是已安装的最高版本,SP是该版本的服务包。

  RegistryKey installed_versions =   Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP");
  string[] version_names = installed_versions.GetSubKeyNames();
  //version names start with 'v', eg, 'v3.5' which needs to be trimmed off    before conversion
  double Framework = Convert.ToDouble(version_names[version_names.Length - 1].Remove(0, 1), CultureInfo.InvariantCulture);
  int SP =  Convert.ToInt32(installed_versions.OpenSubKey(version_names[version_names.Length     - 1]).GetValue("SP", 0));

 For .Net 4.5


 using System;
 using Microsoft.Win32;


 ...


 private static void Get45or451FromRegistry()
{
using (RegistryKey ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine,    RegistryView.Registry32).OpenSubKey("SOFTWARE\\Microsoft\\NET Framework  Setup\\NDP\\v4\\Full\\")) {
    int releaseKey = Convert.ToInt32(ndpKey.GetValue("Release"));
    if (true) {
        Console.WriteLine("Version: " + CheckFor45DotVersion(releaseKey));
     }
   }
 }


 ...


// Checking the version using >= will enable forward compatibility,  
// however you should always compile your code on newer versions of 
// the framework to ensure your app works the same. 
private static string CheckFor45DotVersion(int releaseKey)
{
if (releaseKey >= 393273) {
   return "4.6 RC or later";
}
if ((releaseKey >= 379893)) {
    return "4.5.2 or later";
}
if ((releaseKey >= 378675)) {
    return "4.5.1 or later";
}
if ((releaseKey >= 378389)) {
    return "4.5 or later";
}
// This line should never execute. A non-null release key should mean 
// that 4.5 or later is installed. 
return "No 4.5 or later version detected";
}

谢谢,但这并没有回答问题。我们没有涉及任何程序集。项目文件基本上是xml文件,我们正在寻找除手动处理之外的解决方案。 - orad
也许可以使用一些辅助库来解决这个问题。我想这个问题并不只是我一个人遇到的。实际上,每个大型项目都需要进行这种测试。如果我必须从头开始创建一个解决方案,那么我可能会为此编写一个库,供所有人使用。 - orad
如果您想这样做,那么您必须制作一个dll,这可能会在Visual Studio调试过程中使用。 - Manraj

1
您可以使用手写的C#、脚本、PowerShell或类似方法来进行搜索和替换正则表达式,但会有以下问题:
  • 难以阅读(在三个月或更长时间内阅读您的漂亮正则表达式)
  • 难以增强(为新的搜索/替换/检查功能编写新的正则表达式)
  • 易于破坏(新版本/格式的ms build项目或未预测的标记可能无法工作)
  • 较难进行测试(必须检查没有意外匹配发生)
  • 难以维护(因为上述原因)

还有以下优点:

  • 不执行任何额外的验证,这可能使其适用于任何类型的项目(mono或visual)。
  • 不关心\r :)

最好的方法是使用Microsoft.Build.Evaluation 并构建一个C#工具,它可以完成所有的测试/检查/修复等操作。

我已经完成了一个命令行工具,它使用一个源文件列表(由Mono使用)并更新csproj的源文件,另外还可以在控制台上输出csproj内容。这很容易做到,非常直接和易于测试。
然而,它可能会失败(就像我经历过的那样),因为被“非”Ms工具修改的项目(如Mono Studio)或缺少\r。无论如何,你总是可以通过异常捕获和良好的消息来处理它。
这里有一个使用Microsoft.Build.dll的示例(不要使用Microsof.Build.Engine,因为它已经过时了):
using System;
using Microsoft.Build.Evaluation;

internal class Program
{
    private static void Main(string[] args)
    {
        var project = new Project("PathToYourProject.csproj");
        Console.WriteLine(project.GetProperty("TargetFrameworkVersion", true, string.Empty));
        Console.WriteLine(project.GetProperty("Platform", true, string.Empty));
        Console.WriteLine(project.GetProperty("WarningLevel", true, string.Empty));
        Console.WriteLine(project.GetProperty("TreatWarningsAsErrors", true, "false"));
        Console.WriteLine(project.GetProperty("OutputPath", false, string.Empty));
        Console.WriteLine(project.GetProperty("SignAssembly", true, "false"));
        Console.WriteLine(project.GetProperty("AssemblyName", false, string.Empty));
        Console.ReadLine();
    }
}

public static class ProjectExtensions
{
    public static string GetProperty(this Project project, string propertyName, bool afterEvaluation, string defaultValue)
    {
        var property = project.GetProperty(propertyName);
        if (property != null)
        {
            if (afterEvaluation)
                return property.EvaluatedValue;
            return property.UnevaluatedValue;
        }
        return defaultValue;
    }
}

1

为了类似的目的,我们使用自定义的MSBuild片段来共享项目之间想要共享的常见属性,例如这个(build.common.props文件):

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <TargetFrameworkVersion>v2.0</TargetFrameworkVersion>
    <PlatformToolset>v90</PlatformToolset>
    <OutputPath>$(SolutionDir)..\bin\$(PlatformPath)\$(Configuration)\</OutputPath>

   <!-- whatever you need here -->
  </PropertyGroup>

</Project>

然后我们只需将此代码片段包含到我们想应用这些属性的实际VS项目中:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <CommonProps>$(SolutionDir)..\Build\build.common.props</CommonProps>
  </PropertyGroup>

  <Import Project="$(CommonProps)" />

  <!-- the rest of the project -->

</Project>

我们使用这种方法来处理许多事情:
  • 常见属性,如您所提到的
  • 静态分析(FxCop、StyleCop)
  • 程序集的数字签名
  • 等等。
唯一的缺点是你需要将这些MSBuild片段包含在每个项目文件中,但一旦你这样做了,你就可以获得易于管理和更新的模块化构建系统的所有好处。

有一种方法可以避免将此添加到每个项目中。将您的自定义 MSBuild 代码放入单独的 .proj 文件中,然后使用 CustomBeforeMicrosoftCommonTargets 环境变量指向它。 - orad
在我看来,这有点极端了。使用环境变量,您可以将单独的.proj文件添加到解决方案中的每个MSBuild项目中。这意味着极其难以处理各种类型的项目(C#,C ++,Sandcastle)和不同的项目设置。此外,除非环境变量在用户的计算机上是全局的,否则无法在VS内部工作。 - Dmitry Kolomiets

0

你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Pankwood

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接