|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace BuildBoss
{
internal sealed class ProjectCheckerUtil : ICheckerUtil
{
private readonly ProjectData _data;
private readonly ProjectUtil _projectUtil;
private readonly Dictionary<ProjectKey, ProjectData> _solutionMap;
private readonly bool _isPrimarySolution;
internal ProjectFileType ProjectType => _data.ProjectFileType;
internal string ProjectFilePath => _data.FilePath;
internal ProjectCheckerUtil(ProjectData data, Dictionary<ProjectKey, ProjectData> solutionMap, bool isPrimarySolution)
{
_data = data;
_projectUtil = data.ProjectUtil;
_solutionMap = solutionMap;
_isPrimarySolution = isPrimarySolution;
}
public bool Check(TextWriter textWriter)
{
var allGood = true;
if (ProjectType is ProjectFileType.CSharp or ProjectFileType.Basic)
{
if (!_projectUtil.IsNewSdk())
{
textWriter.WriteLine($"Project must new .NET SDK based");
allGood = false;
}
// Properties that aren't related to build but instead artifacts of Visual Studio.
allGood &= CheckForProperty(textWriter, "RestorePackages");
allGood &= CheckForProperty(textWriter, "SolutionDir");
allGood &= CheckForProperty(textWriter, "FileAlignment");
allGood &= CheckForProperty(textWriter, "FileUpgradeFlags");
allGood &= CheckForProperty(textWriter, "UpgradeBackupLocation");
allGood &= CheckForProperty(textWriter, "OldToolsVersion");
allGood &= CheckForProperty(textWriter, "SchemaVersion");
// Centrally controlled properties
allGood &= CheckForProperty(textWriter, "Configuration");
allGood &= CheckForProperty(textWriter, "CheckForOverflowUnderflow");
allGood &= CheckForProperty(textWriter, "RemoveIntegerChecks");
allGood &= CheckForProperty(textWriter, "Deterministic");
allGood &= CheckForProperty(textWriter, "HighEntropyVA");
allGood &= CheckForProperty(textWriter, "DocumentationFile");
// Items which are not necessary anymore in the new SDK
allGood &= CheckForProperty(textWriter, "ProjectGuid");
allGood &= CheckForProperty(textWriter, "ProjectTypeGuids");
allGood &= CheckForProperty(textWriter, "TargetFrameworkProfile");
allGood &= CheckTargetFrameworks(textWriter);
allGood &= CheckProjectReferences(textWriter);
if (_isPrimarySolution)
{
allGood &= CheckInternalsVisibleTo(textWriter);
}
allGood &= CheckDeploymentSettings(textWriter);
}
return allGood;
}
private bool CheckForProperty(TextWriter textWriter, string propertyName)
{
foreach (var element in _projectUtil.GetAllPropertyGroupElements())
{
if (element.Name.LocalName == propertyName)
{
textWriter.WriteLine($"\tDo not use {propertyName}");
return false;
}
}
return true;
}
private bool CheckProjectReferences(TextWriter textWriter)
{
var allGood = true;
var declaredEntryList = _projectUtil.GetDeclaredProjectReferences();
var declaredList = declaredEntryList.Select(x => x.ProjectKey).ToList();
allGood &= CheckProjectReferencesComplete(textWriter, declaredList);
allGood &= CheckUnitTestReferenceRestriction(textWriter, declaredList);
allGood &= CheckNoGuidsOnProjectReferences(textWriter, declaredEntryList);
return allGood;
}
private bool CheckNoGuidsOnProjectReferences(TextWriter textWriter, List<ProjectReferenceEntry> entryList)
{
var allGood = true;
foreach (var entry in entryList)
{
if (entry.Project != null)
{
textWriter.WriteLine($"Project reference for {entry.ProjectKey.FileName} should not have a GUID");
allGood = false;
}
}
return allGood;
}
private bool CheckInternalsVisibleTo(TextWriter textWriter)
{
var allGood = true;
foreach (var internalsVisibleTo in _projectUtil.GetInternalsVisibleTo())
{
if (string.Equals(internalsVisibleTo.LoadsWithinVisualStudio, "false", StringComparison.OrdinalIgnoreCase))
{
// IVTs explicitly declared with LoadsWithinVisualStudio="false" are allowed
continue;
}
if (_projectUtil.Key.FileName.StartsWith("Microsoft.CodeAnalysis.ExternalAccess."))
{
// External access layer may have external IVTs
continue;
}
if (!string.IsNullOrEmpty(internalsVisibleTo.WorkItem))
{
if (!Uri.TryCreate(internalsVisibleTo.WorkItem, UriKind.Absolute, out _))
{
textWriter.WriteLine($"InternalsVisibleTo for external assembly '{internalsVisibleTo.TargetAssembly}' does not have a valid URI specified for {nameof(InternalsVisibleTo.WorkItem)}.");
allGood = false;
}
// A work item is tracking elimination of this IVT
continue;
}
var builtByThisRepository = _solutionMap.Values.Any(projectData => GetAssemblyName(projectData) == internalsVisibleTo.TargetAssembly);
if (!builtByThisRepository)
{
textWriter.WriteLine($"InternalsVisibleTo not allowed for external assembly '{internalsVisibleTo.TargetAssembly}' that may load within Visual Studio.");
allGood = false;
}
}
return allGood;
// Local functions
static string GetAssemblyName(ProjectData projectData)
{
return projectData.ProjectUtil.FindSingleProperty("AssemblyName")?.Value.Trim()
?? Path.GetFileNameWithoutExtension(projectData.FileName);
}
}
private bool CheckDeploymentSettings(TextWriter textWriter)
{
var allGood = CheckForProperty(textWriter, "CopyNuGetImplementations");
allGood &= CheckForProperty(textWriter, "UseCommonOutputDirectory");
return allGood;
}
/// <summary>
/// It's important that every reference be included in the solution. MSBuild does not necessarily
/// apply all configuration entries to projects which are compiled via references but not included
/// in the solution.
/// </summary>
private bool CheckProjectReferencesComplete(TextWriter textWriter, IEnumerable<ProjectKey> declaredReferences)
{
var allGood = true;
foreach (var key in declaredReferences)
{
if (!_solutionMap.ContainsKey(key))
{
textWriter.WriteLine($"Project reference {key.FileName} is not included in the solution");
allGood = false;
}
}
return allGood;
}
/// <summary>
/// Unit test projects should not reference each other. In order for unit tests to be run / F5 they must be
/// modeled as deployment projects. Having Unit Tests reference each other hurts that because it ends up
/// putting two copies of the unit test DLL into the UnitTest folder:
///
/// 1. UnitTests\Current\TheUnitTest\TheUnitTest.dll
/// 2. UnitTests\Current\TheOtherTests\
/// TheUnitTests.dll
/// TheOtherTests.dll
///
/// This is problematic as all of our tools do directory based searches for unit test DLLs. Hence they end up
/// getting counted twice.
///
/// Consideration was given to fixing up all of the tools but it felt like fighting against the grain. Pretty
/// much every repo has this practice.
/// </summary>
private bool CheckUnitTestReferenceRestriction(TextWriter textWriter, IEnumerable<ProjectKey> declaredReferences)
{
if (!_data.IsTestProject)
{
return true;
}
var allGood = true;
foreach (var key in declaredReferences)
{
if (!_solutionMap.TryGetValue(key, out var projectData))
{
continue;
}
if (projectData.ProjectUtil.IsTestProject)
{
textWriter.WriteLine($"Cannot reference {key.FileName} as it is another unit test project");
allGood = false;
}
}
return allGood;
}
private bool CheckTargetFrameworks(TextWriter textWriter)
{
var allGood = true;
foreach (var targetFramework in _projectUtil.GetAllTargetFrameworks())
{
// !!!NOTE!!!
// This check ensures that projects match the target framework expectations laid out in
// Target Framework Strategy.md. Before changing this list, even simply adding a new
// tfm, please consult with the infrastructure team so they can validate the change is in
// line with how the product is constructed.
switch (targetFramework)
{
case "net472":
case "netstandard2.0":
case "$(NetRoslyn)":
case "$(NetRoslynNext)":
case "$(NetRoslynSourceBuild)":
case "$(NetRoslynToolset)":
case "$(NetRoslynAll)":
case "$(NetVS)":
case "$(NetVS)-windows":
case "$(NetVSCode)":
case "$(NetVSShared)":
continue;
case "$(NetRoslynBuildHostNetCoreVersion)":
{
// This property should only be used in one specific project
if (_data.FileName == "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.csproj")
continue;
else
break;
}
}
textWriter.WriteLine($"TargetFramework {targetFramework} is not supported in this build");
allGood = false;
}
return allGood;
}
}
}
|