├── dirs.proj ├── Directory.Build.targets ├── src ├── BuildChecks │ ├── IBuildCheckProvider.cs │ ├── IBuildCheck.cs │ ├── BuildCheckProvider.cs │ ├── CheckAlwaysCopyToOutput.cs │ ├── CheckUpToDateCheckBuiltItems.cs │ ├── CheckOutputsAreValid.cs │ ├── CheckAreCopyToOutputDirectoryFilesValid.cs │ └── CheckCopyUpToDateMarkersValid.cs ├── IProjectAnalyzer.cs ├── ILogger.cs ├── IDesignTimeBuildRunner.cs ├── ConsoleLogger.cs ├── BuildUpToDateChecker.csproj ├── Properties │ └── AssemblyInfo.cs ├── SimpleMsBuildLogger.cs ├── NativeMethods.cs ├── ResultsReporter.cs ├── Options.cs ├── Utilities.cs ├── GraphAnalyzer.cs ├── DesignTimeBuildRunner.cs ├── Program.cs └── ProjectAnalyzer.cs ├── global.json ├── CODE_OF_CONDUCT.md ├── test ├── TestLogger.cs ├── BuildUpToDateChecker.Tests.csproj ├── GraphAnalyzerTests.cs ├── TestUtilities.cs ├── ProgramTests.cs └── ProjectAnalyzerTests.cs ├── LICENSE ├── Packages.props ├── SECURITY.md ├── Directory.Build.props ├── README.md └── .gitignore /dirs.proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/BuildChecks/IBuildCheckProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System.Collections.Generic; 4 | 5 | namespace BuildUpToDateChecker.BuildChecks 6 | { 7 | /// 8 | /// Interface for a build check provider. 9 | /// 10 | public interface IBuildCheckProvider 11 | { 12 | IEnumerable GetBuildChecks(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | // Defines version of MSBuild project SDKs to use 3 | // https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk?view=vs-2017#how-project-sdks-are-resolved 4 | "msbuild-sdks": { 5 | "KluGet.Build.Sdk": "1.0.48-preview", 6 | "Microsoft.Build.CentralPackageVersions": "2.0.39", 7 | "Microsoft.Build.NoTargets": "1.0.85", 8 | "Microsoft.Build.Traversal": "2.0.19" 9 | } 10 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /test/TestLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System.Text; 4 | 5 | namespace BuildUpToDateChecker.Tests 6 | { 7 | internal class TestLogger : ILogger 8 | { 9 | private readonly StringBuilder _sb = new StringBuilder(); 10 | 11 | public string LogText => _sb.ToString(); 12 | 13 | public void Log(string message) 14 | { 15 | _sb.AppendLine(message); 16 | } 17 | 18 | public void LogVerbose(string message) 19 | { 20 | _sb.AppendLine(message); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/IProjectAnalyzer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | namespace BuildUpToDateChecker 4 | { 5 | /// 6 | /// Interface defining a Project Analyzer. 7 | /// 8 | public interface IProjectAnalyzer 9 | { 10 | /// 11 | /// Checks a project to see if it's up to date. 12 | /// 13 | /// The full path to the project file. 14 | /// True if the project is up to date. Else, false. Also returns the failure message, if it failed. 15 | (bool IsUpToDate, string failureMessage) IsBuildUpToDate(string fullPathToProjectFile); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ILogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | namespace BuildUpToDateChecker 4 | { 5 | /// 6 | /// Interface for logging. 7 | /// 8 | public interface ILogger 9 | { 10 | /// 11 | /// Writes a message to the log on a single line. 12 | /// 13 | /// The message to write to the log. 14 | void Log(string message); 15 | 16 | /// 17 | /// Writes a verbose message to the log on a single line (if verbose mode is enabled). 18 | /// 19 | /// The message to write to the log. 20 | void LogVerbose(string message); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/IDesignTimeBuildRunner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using Microsoft.Build.Evaluation; 4 | using Microsoft.Build.Execution; 5 | 6 | namespace BuildUpToDateChecker 7 | { 8 | /// 9 | /// Interface for the design-time build runner. 10 | /// 11 | public interface IDesignTimeBuildRunner 12 | { 13 | /// 14 | /// Executes a design-time build of a project. 15 | /// 16 | /// The object to run the design-time build on. 17 | /// The resulting object of the design-time build. 18 | ProjectInstance Execute(Project project); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace BuildUpToDateChecker 7 | { 8 | /// 9 | /// Console logger implementation. 10 | /// 11 | [ExcludeFromCodeCoverage] 12 | internal sealed class ConsoleLogger : ILogger 13 | { 14 | public readonly bool _verbose; 15 | 16 | public ConsoleLogger(bool verbose) 17 | { 18 | _verbose = verbose; 19 | } 20 | 21 | public void Log(string message) 22 | { 23 | if (message != null) 24 | { 25 | Console.WriteLine(message); 26 | } 27 | } 28 | 29 | public void LogVerbose(string message) 30 | { 31 | if (_verbose) 32 | { 33 | Log(message); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/BuildUpToDateChecker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | BuildUpToDateChecker 5 | BuildUpToDateChecker 6 | x64 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/BuildUpToDateChecker.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System.Reflection; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyCopyright("Copyright © 2019")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | // The following GUID is for the ID of the typelib if this project is exposed to COM 15 | [assembly: Guid("086bb804-3b8d-47a3-96d2-b46c2069d2a3")] 16 | 17 | [assembly: InternalsVisibleTo("BuildUpToDateChecker.Tests")] 18 | [assembly: InternalsVisibleTo("ToDynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /src/SimpleMsBuildLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System.Text; 4 | using Microsoft.Build.Framework; 5 | 6 | namespace BuildUpToDateChecker 7 | { 8 | /// 9 | /// A simple MsBuild ILogger implementation to get the MsBuild log output. 10 | /// 11 | internal class SimpleMsBuildLogger : Microsoft.Build.Framework.ILogger 12 | { 13 | private readonly StringBuilder _sb = new StringBuilder(); 14 | private readonly StringBuilder _sbError = new StringBuilder(); 15 | 16 | public string LogText => _sb.ToString(); 17 | public string ErrorText => _sbError.ToString(); 18 | 19 | public void Initialize(IEventSource eventSource) 20 | { 21 | eventSource.AnyEventRaised += (sender, args) => 22 | { 23 | _sb.AppendLine($"{args.GetType().Name}: {args.Message}"); 24 | }; 25 | 26 | eventSource.ErrorRaised += (sender, args) => 27 | { 28 | _sbError.AppendLine(args.Message); 29 | }; 30 | } 31 | 32 | public void Shutdown() 33 | { 34 | } 35 | 36 | public LoggerVerbosity Verbosity { get; set; } = LoggerVerbosity.Diagnostic; 37 | 38 | public string Parameters { get; set; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/BuildChecks/IBuildCheck.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using Microsoft.Build.Execution; 4 | using Microsoft.Build.Prediction; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace BuildUpToDateChecker.BuildChecks 9 | { 10 | /// 11 | /// Context class for consumption by IBuildCheck. 12 | /// 13 | public class ProjectBuildCheckContext 14 | { 15 | public ILogger Logger { get; set; } 16 | public ProjectInstance Instance { get; set; } 17 | public ProjectPredictions Predictions { get; set; } 18 | public HashSet Inputs { get; set; } 19 | public HashSet Outputs { get; set; } 20 | public Dictionary TimeStampCache { get; set; } 21 | public bool VerboseOutput { get; set; } 22 | } 23 | 24 | /// 25 | /// Defines the interface for build checkers. 26 | /// 27 | public interface IBuildCheck 28 | { 29 | /// 30 | /// Checks if the build is up to date. 31 | /// 32 | /// The context for this particular check. 33 | /// A list for found errors to be added to. 34 | /// True if the build is up to date. False if not up to date. 35 | bool Check(ProjectBuildCheckContext context, out string checkFailureMessage); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.IO; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | 9 | namespace BuildUpToDateChecker 10 | { 11 | [ExcludeFromCodeCoverage] 12 | internal static class NativeMethods 13 | { 14 | public static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); 15 | 16 | public const uint FILE_READ_EA = 0x0008; 17 | public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x2000000; 18 | 19 | [DllImport("Kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", SetLastError = true, CharSet = CharSet.Auto)] 20 | public static extern uint GetFinalPathNameByHandle( 21 | IntPtr hFile, 22 | [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpszFilePath, 23 | uint cchFilePath, 24 | uint dwFlags); 25 | 26 | [DllImport("kernel32.dll", SetLastError = true)] 27 | [return: MarshalAs(UnmanagedType.Bool)] 28 | public static extern bool CloseHandle(IntPtr hObject); 29 | 30 | [DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Auto, SetLastError = true)] 31 | public static extern IntPtr CreateFile( 32 | [MarshalAs(UnmanagedType.LPWStr)] string filename, 33 | [MarshalAs(UnmanagedType.U4)] uint access, 34 | [MarshalAs(UnmanagedType.U4)] FileShare share, 35 | IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero 36 | [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, 37 | [MarshalAs(UnmanagedType.U4)] uint flagsAndAttributes, 38 | IntPtr templateFile); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/BuildChecks/BuildCheckProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace BuildUpToDateChecker.BuildChecks 7 | { 8 | /// 9 | /// Implementation of IBuildCheckProvider 10 | /// 11 | internal class BuildCheckProvider : IBuildCheckProvider 12 | { 13 | private static HashSet ItemTypesForUpToDateCheckInput = new HashSet(StringComparer.OrdinalIgnoreCase) 14 | { 15 | "SplashScreen", 16 | "CodeAnalysisDictionary", 17 | "Resource", 18 | "DesignDataWithDesignTimeCreatableTypes", 19 | "ApplicationDefinition", 20 | "EditorConfigFiles", 21 | "Fakes", 22 | "EmbeddedResource", 23 | "EntityDeploy", 24 | "Compile", 25 | "Content", 26 | "DesignData", 27 | "AdditionalFiles", 28 | "XamlAppDef", 29 | "None", 30 | "Page" 31 | }; 32 | 33 | /// 34 | /// Returns all of the build checks that are: 35 | /// a. In this assembly, 36 | /// b. Implements IBuildCheck 37 | /// c. Is NOT an abstract class 38 | /// d. Is NOT an interface. 39 | /// 40 | /// An enumeration of the build checks to run for a project. 41 | public IEnumerable GetBuildChecks() 42 | { 43 | return new IBuildCheck[] 44 | { 45 | new CheckAlwaysCopyToOutput(ItemTypesForUpToDateCheckInput), 46 | new CheckAreCopyToOutputDirectoryFilesValid(ItemTypesForUpToDateCheckInput), 47 | new CheckCopyUpToDateMarkersValid(), 48 | new CheckOutputsAreValid(), 49 | new CheckUpToDateCheckBuiltItems() 50 | }; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/BuildChecks/CheckAlwaysCopyToOutput.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using Microsoft.Build.Execution; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace BuildUpToDateChecker.BuildChecks 9 | { 10 | /// 11 | /// Checks for any items with CopyToOutputDirectory=Always. 12 | /// 13 | internal class CheckAlwaysCopyToOutput : IBuildCheck 14 | { 15 | private readonly HashSet _itemTypesForUpToDateCheckInput; 16 | 17 | public CheckAlwaysCopyToOutput(HashSet itemTypesForUpToDateCheckInput) 18 | { 19 | _itemTypesForUpToDateCheckInput = itemTypesForUpToDateCheckInput ?? throw new ArgumentNullException(nameof(itemTypesForUpToDateCheckInput)); 20 | } 21 | 22 | public bool Check(ProjectBuildCheckContext context, out string failureMessage) 23 | { 24 | context.Logger.LogVerbose(string.Empty); 25 | context.Logger.LogVerbose("CheckAlwaysCopyToOutput:"); 26 | 27 | IEnumerable itemsUpToDateCheckInput = context.Instance.Items.Where(i => _itemTypesForUpToDateCheckInput.Contains(i.ItemType)); 28 | 29 | foreach (ProjectItemInstance a in itemsUpToDateCheckInput) 30 | { 31 | if (a.HasMetadata("CopyToOutputDirectory") && a.GetMetadataValue("CopyToOutputDirectory").Equals("Always", StringComparison.OrdinalIgnoreCase)) 32 | { 33 | failureMessage = $"Item '{a.GetMetadataValue("FullPath")}' has CopyToOutputDirectory set to 'Always', not up to date."; 34 | context.Logger.LogVerbose($" {failureMessage}"); 35 | return false; 36 | } 37 | } 38 | 39 | context.Logger.LogVerbose(" Up to date."); 40 | 41 | failureMessage = string.Empty; 42 | return true; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ResultsReporter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using Newtonsoft.Json; 7 | 8 | namespace BuildUpToDateChecker 9 | { 10 | public sealed class BuildCheckResult 11 | { 12 | public string FullProjectPath { get; set; } 13 | public bool IsUpToDate { get; set; } 14 | public DateTime ScanStart { get; set; } 15 | public TimeSpan ScanDuration { get; set; } 16 | public string FailureMessage { get; set; } 17 | } 18 | 19 | public interface IResultsReporter 20 | { 21 | void Initialize(); 22 | void ReportProjectAnalysisResult(BuildCheckResult result); 23 | void TearDown(); 24 | } 25 | 26 | internal class ResultsReporter : IResultsReporter 27 | { 28 | private List _results = new List(); 29 | private string _outputFilePath; 30 | 31 | public ResultsReporter(string outputFilePath) 32 | { 33 | if (outputFilePath == null) 34 | { 35 | throw new ArgumentNullException(nameof(outputFilePath)); 36 | } 37 | 38 | outputFilePath = Path.GetFullPath(outputFilePath); // Convert relative to absolute, if necessary. 39 | 40 | string outputFileDir = Path.GetDirectoryName(outputFilePath); 41 | if (!Directory.Exists(outputFileDir)) 42 | { 43 | Directory.CreateDirectory(outputFileDir); 44 | } 45 | 46 | // Delete any previous file. 47 | if (File.Exists(outputFilePath)) 48 | { 49 | File.Delete(outputFilePath); 50 | } 51 | 52 | _outputFilePath = outputFilePath; 53 | } 54 | 55 | public void Initialize() 56 | { 57 | } 58 | 59 | public void ReportProjectAnalysisResult(BuildCheckResult result) 60 | { 61 | _results.Add(result); 62 | } 63 | 64 | public void TearDown() 65 | { 66 | string json = JsonConvert.SerializeObject(_results.ToArray()); 67 | File.WriteAllText(_outputFilePath, json); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Options.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Diagnostics.CodeAnalysis; 5 | using McMaster.Extensions.CommandLineUtils; 6 | 7 | namespace BuildUpToDateChecker 8 | { 9 | [ExcludeFromCodeCoverage] 10 | [Command(Name = "BuildUpToDateChecker", Description = "Analyzes a build tree to determine if the build is up-to-date.")] 11 | [HelpOption("-?")] 12 | public class Options 13 | { 14 | [Argument(0, "Path to project (or traversal project) to load.")] 15 | [Required] 16 | [FileExists] 17 | public string InputProjectFile { get; set; } 18 | 19 | [Option("--out:", Description = "Full path to output report file.")] 20 | public string OutputReportFile { get; set; } 21 | 22 | [Option("--prop:=", "An additional global MsBuild property to set. By default the following are set: Configuration=Verbose and Platform=AnyCPU", CommandOptionType.MultipleValue)] 23 | public string[] AdditionalMsBuildProperties { get; set; } 24 | 25 | [Option("--msbuild ", Description = "Path to MSBuild.exe.")] 26 | [FileExists] 27 | public string MsBuildPath { get; set; } 28 | 29 | [Option("-v|--verbose", Description = "Outputs additional debugging information to standard output.")] 30 | public bool Verbose { get; set; } 31 | 32 | [Option("-d|--debug", Description = "Breaks waiting for a debugger to attach.")] 33 | public bool AttachDebugger { get; set; } 34 | 35 | [Option("-ff|--failfast", Description = "Analysis will stop after hitting the first up-to-date check failure for a project.")] 36 | public bool FailOnFirstError { get; set; } 37 | 38 | [Option("-sbl|--showbuildlogs", Description = "When specifying --verbose, always write out the design-time build's log. By default, this is only written if there is a design-time build failure.")] 39 | public bool AlwaysDumpBuildLogOnVerbose { get; set; } 40 | 41 | public Options() 42 | { 43 | // Set defaults here. 44 | OutputReportFile = ".\\project-results.json"; 45 | } 46 | 47 | public int OnExecute() 48 | { 49 | return new Program(this).Run() ? 0 : 1; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(MSBuildThisFileFullPath);$(MSBuildAllProjects) 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Utilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Text.RegularExpressions; 11 | 12 | namespace BuildUpToDateChecker 13 | { 14 | [ExcludeFromCodeCoverage] 15 | internal static class Utilities 16 | { 17 | public static DateTime? GetTimestampUtc(string path, IDictionary timestampCache) 18 | { 19 | // If already in cache, return that value. 20 | if (timestampCache.TryGetValue(path, out DateTime time)) return time; 21 | 22 | // If the file doesn't exist, return null. 23 | if (!FileExists(path)) return null; 24 | 25 | // Get the last write timestamp from either this file or, if a symlink, the symlink target. 26 | time = File.GetLastWriteTimeUtc(IsSymLink(path) ? GetTargetOfSymlink(path) : path); 27 | timestampCache[path] = time; 28 | 29 | return time; 30 | } 31 | 32 | public static bool FileExists(string filePath) 33 | { 34 | // If the file doesn't exist, it doesn't matter if it's supposed to be a regular file or a symlink. 35 | if (!File.Exists(filePath)) return false; 36 | 37 | // If it's not a symlink or if the target of the symlink exists, return true. 38 | return !IsSymLink(filePath) || File.Exists(GetTargetOfSymlink(filePath)); 39 | } 40 | 41 | private static bool IsSymLink(string filePath) 42 | { 43 | FileAttributes attr = File.GetAttributes(filePath); 44 | return ((attr & FileAttributes.ReparsePoint) != 0); 45 | } 46 | 47 | private static string GetTargetOfSymlink(string filePath) 48 | { 49 | IntPtr h = NativeMethods.CreateFile(filePath, 50 | NativeMethods.FILE_READ_EA, 51 | FileShare.ReadWrite | FileShare.Delete, 52 | IntPtr.Zero, 53 | FileMode.Open, 54 | NativeMethods.FILE_FLAG_BACKUP_SEMANTICS, 55 | IntPtr.Zero); 56 | 57 | if (h == NativeMethods.INVALID_HANDLE_VALUE) 58 | { 59 | throw new Win32Exception($"Unable to open file '{filePath}'."); 60 | } 61 | 62 | try 63 | { 64 | var sb = new StringBuilder(1024); 65 | var res = NativeMethods.GetFinalPathNameByHandle(h, sb, 1024, 0); 66 | 67 | if (res == 0) 68 | { 69 | throw new Win32Exception((int)res, $"Unable to get target from symlink '{filePath}'."); 70 | } 71 | 72 | return sb.ToString(); 73 | } 74 | finally 75 | { 76 | NativeMethods.CloseHandle(h); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/GraphAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.IO; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Moq; 7 | 8 | namespace BuildUpToDateChecker.Tests 9 | { 10 | [TestClass] 11 | public class GraphAnalyzerTests 12 | { 13 | [TestMethod] 14 | [ExpectedException(typeof(ArgumentNullException))] 15 | public void TestMissingLogger() 16 | { 17 | var mockProjectAnalyzer = new Mock(); 18 | var mockResultsReporter = new Mock(); 19 | 20 | new GraphAnalyzer(null, mockProjectAnalyzer.Object, mockResultsReporter.Object, false); 21 | } 22 | 23 | [TestMethod] 24 | [ExpectedException(typeof(ArgumentNullException))] 25 | public void TestMissingAnalyzer() 26 | { 27 | var mockLogger = new Mock(); 28 | var mockResultsReporter = new Mock(); 29 | 30 | new GraphAnalyzer(mockLogger.Object, null, mockResultsReporter.Object, false); 31 | } 32 | 33 | [TestMethod] 34 | [ExpectedException(typeof(ArgumentNullException))] 35 | public void TestMissingReporter() 36 | { 37 | var mockLogger = new Mock(); 38 | var mockProjectAnalyzer = new Mock(); 39 | 40 | new GraphAnalyzer(mockLogger.Object, mockProjectAnalyzer.Object, null, false); 41 | } 42 | 43 | [TestMethod] 44 | [ExpectedException(typeof(FileNotFoundException))] 45 | public void TestMissingProjectFile() 46 | { 47 | var mockLogger = new Mock(); 48 | var mockProjectAnalyzer = new Mock(); 49 | var mockResultsReporter = new Mock(); 50 | 51 | var analyzer = new GraphAnalyzer(mockLogger.Object, mockProjectAnalyzer.Object, mockResultsReporter.Object, true); 52 | 53 | string doesNotExistPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}"); 54 | analyzer.AnalyzeGraph(doesNotExistPath); 55 | } 56 | 57 | [TestMethod] 58 | public void TestExistingProjectFile() 59 | { 60 | string testProject = TestUtilities.CreateTestProject(); 61 | 62 | var mockLogger = new Mock(); 63 | var mockProjectAnalyzer = new Mock(); 64 | var mockResultsReporter = new Mock(); 65 | 66 | mockProjectAnalyzer.Setup(a => a.IsBuildUpToDate(It.IsAny())).Returns((true, string.Empty)); 67 | 68 | var analyzer = new GraphAnalyzer(mockLogger.Object, mockProjectAnalyzer.Object, mockResultsReporter.Object, true); 69 | bool isUpToDate = analyzer.AnalyzeGraph(testProject); 70 | Assert.IsTrue(isUpToDate); 71 | 72 | TestUtilities.CleanUpTestProject(testProject); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/TestUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace BuildUpToDateChecker.Tests 10 | { 11 | internal class TestUtilities 12 | { 13 | public static string CreateTestProject( 14 | string rawMsBuildXmlToInsert = null, 15 | IEnumerable filesToCreate = null, 16 | bool outputBinaries = false, 17 | bool createStandardOutputs = false) 18 | { 19 | string newDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); 20 | Directory.CreateDirectory(newDir); 21 | 22 | string projectName = Guid.NewGuid().ToString(); 23 | string newProjectFile = Path.Combine(newDir, $"{projectName}.csproj"); 24 | StringBuilder sb = new StringBuilder(); 25 | sb.AppendLine(""); 26 | 27 | sb.AppendLine(""); 28 | sb.AppendLine("net472"); 29 | if (outputBinaries) 30 | { 31 | sb.AppendLine("Exe"); 32 | } 33 | sb.AppendLine(""); 34 | 35 | // Add desired raw XML. 36 | if (rawMsBuildXmlToInsert != null) 37 | sb.AppendLine(rawMsBuildXmlToInsert); 38 | 39 | sb.AppendLine(""); 40 | 41 | File.WriteAllText(newProjectFile, sb.ToString()); 42 | 43 | if (createStandardOutputs) 44 | { 45 | List standardOutputs = new List(); 46 | standardOutputs.Add($"bin\\Debug\\net472\\{projectName}.exe"); 47 | standardOutputs.Add($"bin\\Debug\\net472\\{projectName}.pdb"); 48 | standardOutputs.Add($"obj\\Debug\\net472\\{projectName}.exe"); 49 | standardOutputs.Add($"obj\\Debug\\net472\\{projectName}.pdb"); 50 | 51 | filesToCreate = filesToCreate == null ? standardOutputs : filesToCreate.Concat(standardOutputs); 52 | } 53 | 54 | // Create any desired files. 55 | if (filesToCreate != null) 56 | { 57 | foreach (string fileToCreate in filesToCreate) 58 | { 59 | string filePath = Path.Combine(newDir, fileToCreate); 60 | string fileDir = Path.GetDirectoryName(filePath); 61 | 62 | if (!Directory.Exists(fileDir)) 63 | Directory.CreateDirectory(fileDir); 64 | 65 | File.WriteAllText(filePath, "// Test"); 66 | } 67 | } 68 | 69 | return newProjectFile; 70 | } 71 | 72 | public static void CleanUpTestProject(string projectFile) 73 | { 74 | if (string.IsNullOrEmpty(projectFile)) return; 75 | if (!File.Exists(projectFile)) return; 76 | 77 | string dir = Path.GetDirectoryName(projectFile); 78 | Directory.Delete(dir, true); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/BuildChecks/CheckUpToDateCheckBuiltItems.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using Microsoft.Build.Execution; 4 | using System; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace BuildUpToDateChecker.BuildChecks 9 | { 10 | /// 11 | /// Checks the status of the project's UpToDateCheckBuilt items. 12 | /// 13 | internal class CheckUpToDateCheckBuiltItems : IBuildCheck 14 | { 15 | public bool Check(ProjectBuildCheckContext context, out string failureMessage) 16 | { 17 | context.Logger.LogVerbose(string.Empty); 18 | context.Logger.LogVerbose("CheckUpToDateCheckBuiltItems:"); 19 | 20 | foreach (ProjectItemInstance copiedOutputFiles in context.Instance.GetItems("UpToDateCheckBuilt").Where(i => i.HasMetadata("Original") && !string.IsNullOrEmpty(i.GetMetadataValue("Original")))) 21 | { 22 | var source = ConvertToAbsolutePath(copiedOutputFiles.GetMetadataValue("Original"), Path.GetDirectoryName(context.Instance.FullPath)); 23 | var destination = copiedOutputFiles.GetMetadataValue("FullPath"); 24 | 25 | context.Logger.LogVerbose($" Checking copied output (UpToDateCheckBuilt with Original property) file '{source}':"); 26 | 27 | DateTime? sourceTime = Utilities.GetTimestampUtc(source, context.TimeStampCache); 28 | 29 | if (sourceTime != null) 30 | { 31 | context.Logger.LogVerbose($" Source {sourceTime}: '{source}'."); 32 | } 33 | else 34 | { 35 | failureMessage = $"Source '{source}' does not exist, not up to date."; 36 | context.Logger.LogVerbose($" {failureMessage}"); 37 | return false; 38 | } 39 | 40 | DateTime? destinationTime = Utilities.GetTimestampUtc(destination, context.TimeStampCache); 41 | 42 | if (destinationTime != null) 43 | { 44 | context.Logger.LogVerbose($" Destination {destinationTime}: '{destination}'."); 45 | } 46 | else 47 | { 48 | failureMessage = "Destination '{destination}' does not exist, not up to date."; 49 | context.Logger.LogVerbose($" {failureMessage}"); 50 | return false; 51 | } 52 | 53 | if (destinationTime < sourceTime) 54 | { 55 | failureMessage = "Source is newer than build output destination, not up to date."; 56 | context.Logger.LogVerbose($" {failureMessage}"); 57 | return false; 58 | } 59 | } 60 | 61 | context.Logger.LogVerbose(" Up to date."); 62 | 63 | failureMessage = string.Empty; 64 | return true; 65 | } 66 | 67 | private static string ConvertToAbsolutePath(string path, string projectPath) 68 | { 69 | return Path.IsPathRooted(path) ? path : Path.Combine(projectPath, path); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Debug 4 | AnyCPU 5 | 6 | 7 | $(MSBuildProjectDirectory)\.pkgrefgen\ 8 | 9 | 10 | None 11 | 12 | true 13 | 4 14 | 15 | v4.7.2 16 | net472 17 | 18 | 19 | true 20 | 21 | ExcludeRestorePackageImports=true;Platform=x64 22 | 23 | 24 | Latest 25 | 26 | 32 | AnyCPU 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | true 46 | full 47 | false 48 | $(DefineConstants);DEBUG;TRACE 49 | 50 | 51 | 52 | 53 | pdbonly 54 | true 55 | $(DefineConstants);TRACE 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/BuildChecks/CheckOutputsAreValid.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace BuildUpToDateChecker.BuildChecks 8 | { 9 | /// 10 | /// Checks that all the inputs are older than all the outputs. 11 | /// 12 | internal class CheckOutputsAreValid : IBuildCheck 13 | { 14 | public bool Check(ProjectBuildCheckContext context, out string failureMessage) 15 | { 16 | context.Logger.LogVerbose(string.Empty); 17 | context.Logger.LogVerbose("CheckOutputsAreValid:"); 18 | 19 | (DateTime? outputTime, string outputPath) = GetEarliestOutput(context.Outputs, context.TimeStampCache); 20 | 21 | if (outputTime != null) 22 | { 23 | // Search for an input that's either missing or newer than the earliest output. 24 | // As soon as we find one, we can stop the scan. 25 | // Due to some recently introduced issues (https://github.com/dotnet/project-system/issues/4736), 26 | // explicitly skip the CoreCompileInputs.cache file. 27 | foreach (string input in context.Inputs.Where(i => !i.EndsWith(".CoreCompileInputs.cache", StringComparison.OrdinalIgnoreCase))) 28 | { 29 | DateTime? time = Utilities.GetTimestampUtc(input, context.TimeStampCache); 30 | 31 | if (time == null) 32 | { 33 | failureMessage = $"Input '{input}' does not exist, not up to date."; 34 | context.Logger.LogVerbose($" {failureMessage}"); 35 | return false; 36 | } 37 | 38 | if (time > outputTime) 39 | { 40 | failureMessage = $"Input '{input}' is newer ({time.Value:O}) than earliest output '{outputPath}' ({outputTime.Value:O}), not up to date."; 41 | context.Logger.LogVerbose($" {failureMessage}"); 42 | return false; 43 | } 44 | } 45 | 46 | context.Logger.LogVerbose($" No inputs are newer than earliest output '{outputPath}' ({outputTime.Value})."); 47 | } 48 | else if (outputPath != null) 49 | { 50 | failureMessage = $"Output '{outputPath}' does not exist, not up to date."; 51 | context.Logger.LogVerbose($" {failureMessage}"); 52 | return false; 53 | } 54 | else 55 | { 56 | context.Logger.LogVerbose(" No build outputs defined."); 57 | } 58 | 59 | context.Logger.LogVerbose(" Up to date."); 60 | 61 | failureMessage = string.Empty; 62 | return true; 63 | } 64 | 65 | private static (DateTime? time, string path) GetEarliestOutput(IEnumerable outputs, IDictionary timestampCache) 66 | { 67 | DateTime? earliest = DateTime.MaxValue; 68 | string earliestPath = null; 69 | bool hasOutput = false; 70 | 71 | foreach (string output in outputs) 72 | { 73 | DateTime? time = Utilities.GetTimestampUtc(output, timestampCache); 74 | 75 | if (time == null) 76 | { 77 | return (null, output); 78 | } 79 | 80 | if (time < earliest) 81 | { 82 | earliest = time; 83 | earliestPath = output; 84 | } 85 | 86 | hasOutput = true; 87 | } 88 | 89 | return hasOutput 90 | ? (earliest, earliestPath) 91 | : (null, null); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/GraphAnalyzer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using Microsoft.Build.Graph; 8 | 9 | namespace BuildUpToDateChecker 10 | { 11 | internal interface IGraphAnalyzer 12 | { 13 | bool AnalyzeGraph(string rootProject); 14 | } 15 | 16 | internal class GraphAnalyzer : IGraphAnalyzer 17 | { 18 | private readonly ILogger _logger; 19 | private readonly IProjectAnalyzer _projectAnalyzer; 20 | private readonly bool _failFast; 21 | private readonly IResultsReporter _resultsReporter; 22 | 23 | public GraphAnalyzer(ILogger logger, IProjectAnalyzer projectAnalyzer, IResultsReporter resultsReporter, bool failFast) 24 | { 25 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 26 | _projectAnalyzer = projectAnalyzer ?? throw new ArgumentNullException(nameof(projectAnalyzer)); 27 | _resultsReporter = resultsReporter ?? throw new ArgumentNullException(nameof(resultsReporter)); 28 | _failFast = failFast; 29 | } 30 | 31 | public bool AnalyzeGraph(string rootProject) 32 | { 33 | // First get the nodes in our project graph 34 | _logger.Log("Calculating project graph..."); 35 | var sw = new Stopwatch(); 36 | sw.Start(); 37 | ProjectGraphNode[] nodes = GetProjectNodesForProjectGraph(rootProject); 38 | sw.Stop(); 39 | _logger.Log($"Graph generation took {sw.ElapsedMilliseconds}ms.\r\nProcessing {nodes.Length} project(s) in the tree.\r\n"); 40 | 41 | // Next, analyze them! 42 | sw.Reset(); 43 | sw.Start(); 44 | bool ultimateResult = AnalyzeProjectNodes(nodes); 45 | sw.Stop(); 46 | _logger.Log($"Graph analysis took {sw.ElapsedMilliseconds/1000/60}m, {sw.ElapsedMilliseconds / 1000}s."); 47 | 48 | return ultimateResult; 49 | } 50 | 51 | internal ProjectGraphNode[] GetProjectNodesForProjectGraph(string rootProjectFilePath) 52 | { 53 | if (!File.Exists(rootProjectFilePath)) throw new FileNotFoundException(rootProjectFilePath); 54 | 55 | var graph = new ProjectGraph(rootProjectFilePath); 56 | 57 | return graph 58 | .ProjectNodesTopologicallySorted 59 | .Where(n => !n.ProjectInstance.FullPath.Contains(".proj")) // Really anything that ends in .proj probably shouldn't be loaded in VS (file copy projects, etc.) 60 | .ToArray(); 61 | } 62 | 63 | internal bool AnalyzeProjectNodes(ProjectGraphNode[] nodes) 64 | { 65 | bool ultimateResult = true; 66 | 67 | foreach (var node in nodes) 68 | { 69 | _logger.Log($"Starting analysis of project '{node.ProjectInstance.FullPath}'."); 70 | 71 | DateTime scanStart = DateTime.Now; 72 | (bool result, string failureMessage) = _projectAnalyzer.IsBuildUpToDate(node.ProjectInstance.FullPath); 73 | DateTime scanStop = DateTime.Now; 74 | TimeSpan diff = scanStop - scanStart; 75 | 76 | _logger.Log($"Project build check took {diff.TotalSeconds:F2}s."); 77 | _logger.Log(string.Empty); 78 | 79 | _resultsReporter.ReportProjectAnalysisResult(new BuildCheckResult() { FullProjectPath = node.ProjectInstance.FullPath, IsUpToDate = result, ScanStart = scanStart, ScanDuration = (scanStop - scanStart), FailureMessage = failureMessage}); 80 | 81 | ultimateResult = ultimateResult && result; 82 | 83 | // If we have a failed up to date check, stop processing. 84 | if (_failFast && !ultimateResult) 85 | { 86 | break; 87 | } 88 | } 89 | 90 | return ultimateResult; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/BuildChecks/CheckAreCopyToOutputDirectoryFilesValid.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using Microsoft.Build.Execution; 8 | 9 | namespace BuildUpToDateChecker.BuildChecks 10 | { 11 | /// 12 | /// Checks that items with CopyToOutputDirectory=PreserveNewest are up to date. 13 | /// 14 | internal class CheckAreCopyToOutputDirectoryFilesValid : IBuildCheck 15 | { 16 | private readonly HashSet _itemTypesForUpToDateCheckInput; 17 | 18 | public CheckAreCopyToOutputDirectoryFilesValid(HashSet itemTypesForUpToDateCheckInput) 19 | { 20 | _itemTypesForUpToDateCheckInput = itemTypesForUpToDateCheckInput ?? throw new ArgumentNullException(nameof(itemTypesForUpToDateCheckInput)); 21 | } 22 | 23 | public bool Check(ProjectBuildCheckContext context, out string failureMessage) 24 | { 25 | context.Logger.LogVerbose(string.Empty); 26 | context.Logger.LogVerbose("CheckAreCopyToOutputDirectoryFilesValid:"); 27 | 28 | IEnumerable items = context.Instance.Items.Where(i => i.HasMetadata("CopyToOutputDirectory") && i.GetMetadataValue("CopyToOutputDirectory").Equals("PreserveNewest", StringComparison.OrdinalIgnoreCase)); 29 | 30 | IEnumerable itemsUpToDateCheckInput = items.Where(i => _itemTypesForUpToDateCheckInput.Contains(i.ItemType)); 31 | 32 | foreach (ProjectItemInstance item in itemsUpToDateCheckInput) 33 | { 34 | var rootedPath = item.GetMetadataValue("FullPath"); 35 | var link = item.GetMetadataValue("Link"); 36 | 37 | string filename = rootedPath; 38 | 39 | if (string.IsNullOrEmpty(filename)) 40 | { 41 | continue; 42 | } 43 | 44 | context.Logger.LogVerbose($" Checking PreserveNewest file '{rootedPath}':"); 45 | 46 | DateTime? itemTime = Utilities.GetTimestampUtc(filename, context.TimeStampCache); 47 | 48 | if (itemTime != null) 49 | { 50 | context.Logger.LogVerbose($" Source {itemTime}: '{filename}'."); 51 | } 52 | else 53 | { 54 | failureMessage = $"Source '{filename}' does not exist, not up to date."; 55 | context.Logger.LogVerbose($" {failureMessage}"); 56 | return false; 57 | } 58 | 59 | string outputFullPath = GetOutputFolder(context.Instance); 60 | string outputFileItem = string.IsNullOrEmpty(link) ? Path.Combine(outputFullPath, filename.Replace(context.Instance.Directory, string.Empty).Trim('\\')) : Path.Combine(outputFullPath, link); 61 | DateTime? outputItemTime = Utilities.GetTimestampUtc(outputFileItem, context.TimeStampCache); 62 | 63 | if (outputItemTime != null) 64 | { 65 | context.Logger.LogVerbose($" Destination {outputItemTime}: '{filename}'."); 66 | } 67 | else 68 | { 69 | failureMessage = $"Destination '{outputFileItem}' does not exist, not up to date."; 70 | context.Logger.LogVerbose($" {failureMessage}"); 71 | return false; 72 | } 73 | 74 | if (outputItemTime < itemTime) 75 | { 76 | failureMessage = "PreserveNewest source is newer than destination, not up to date."; 77 | context.Logger.LogVerbose($" {failureMessage}"); 78 | return false; 79 | } 80 | } 81 | 82 | context.Logger.LogVerbose(" Up to date."); 83 | 84 | failureMessage = string.Empty; 85 | return true; 86 | } 87 | 88 | private static string GetOutputFolder(ProjectInstance projectInstance) 89 | { 90 | return Path.Combine(projectInstance.Directory, projectInstance.GetPropertyValue("OutDir")); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/DesignTimeBuildRunner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using Microsoft.Build.Evaluation; 6 | using Microsoft.Build.Execution; 7 | 8 | namespace BuildUpToDateChecker 9 | { 10 | internal class DesignTimeBuildRunner : IDesignTimeBuildRunner 11 | { 12 | private const int DefaultBuildRetryCount = 3; 13 | 14 | private readonly ILogger _logger; 15 | private readonly IDictionary _globalProperties; 16 | private readonly int _numRetries; 17 | private readonly bool _alwaysLogBuildLog; 18 | 19 | public DesignTimeBuildRunner() : this(null, null) { } 20 | 21 | public DesignTimeBuildRunner(ILogger logger, IDictionary additionalGlobalProperties, int numRetries = DefaultBuildRetryCount, bool alwaysLogBuildLog = false) 22 | { 23 | _logger = logger; 24 | 25 | _globalProperties = additionalGlobalProperties ?? new Dictionary(); 26 | _globalProperties["DesignTimeBuild"] = "true"; 27 | _globalProperties["SkipCompilerExecution"] = "true"; 28 | _globalProperties["ProvideCommandLineArgs"] = "true"; 29 | _globalProperties["BuildingInsideVisualStudio"] = "true"; 30 | _globalProperties["ShouldUnsetParentConfigurationAndPlatform"] = "false"; 31 | _globalProperties["GenerateTargetFrameworkMonikerAttribute"] = "false"; 32 | 33 | _numRetries = numRetries; 34 | _alwaysLogBuildLog = alwaysLogBuildLog; 35 | } 36 | 37 | public ProjectInstance Execute(Project project) 38 | { 39 | _logger.LogVerbose($"Beginning design-time build of project {project.FullPath}."); 40 | 41 | _logger.LogVerbose("Setting the following global properties:"); 42 | foreach (KeyValuePair kvp in _globalProperties) 43 | { 44 | project.SetGlobalProperty(kvp.Key, kvp.Value); 45 | _logger.LogVerbose($" {kvp.Key}={kvp.Value}"); 46 | } 47 | 48 | ProjectInstance projectInstance = project.CreateProjectInstance(); 49 | 50 | var designTimeBuildTargets = new string[] { 51 | "CollectResolvedSDKReferencesDesignTime", 52 | "CollectPackageReferences", 53 | "ResolveComReferencesDesignTime", 54 | "ResolveProjectReferencesDesignTime", 55 | "BuiltProjectOutputGroup", 56 | "ResolveAssemblyReferencesDesignTime", 57 | "CollectSDKReferencesDesignTime", 58 | "ResolvePackageDependenciesDesignTime", 59 | "CompileDesignTime", 60 | "CollectFrameworkReferences", 61 | "CollectUpToDateCheckBuiltDesignTime", 62 | "CollectPackageDownloads", 63 | "CollectAnalyzersDesignTime", 64 | "CollectUpToDateCheckInputDesignTime", 65 | "CollectUpToDateCheckOutputDesignTime", 66 | "CollectResolvedCompilationReferencesDesignTime" }; 67 | 68 | SimpleMsBuildLogger buildLogger; 69 | bool result = false; 70 | int retries = 0; 71 | 72 | // Retrying here as there are some odd cases where a file will be in use by another process 73 | // long enough for this to fail, but will work on a subsequent attempt. 74 | do 75 | { 76 | buildLogger = new SimpleMsBuildLogger(); 77 | 78 | _logger.LogVerbose($"Attempting design-time build # {retries + 1}..."); 79 | result = projectInstance.Build(designTimeBuildTargets, new Microsoft.Build.Framework.ILogger[] { buildLogger }); 80 | } 81 | while (!result && (++retries < _numRetries)); 82 | 83 | if (!result || _alwaysLogBuildLog) 84 | { 85 | _logger.LogVerbose("Design time build log:"); 86 | _logger.LogVerbose(buildLogger.LogText); 87 | _logger.LogVerbose(string.Empty); 88 | 89 | } 90 | 91 | if (!result) 92 | { 93 | throw new Exception("Failed to build project.\r\n" + buildLogger.ErrorText); 94 | } 95 | 96 | return projectInstance; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/BuildChecks/CheckCopyUpToDateMarkersValid.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using Microsoft.Build.Execution; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace BuildUpToDateChecker.BuildChecks 9 | { 10 | /// 11 | /// Checks that the copy references marker is up to date. (Only applies to SDK-style projects.) 12 | /// 13 | internal class CheckCopyUpToDateMarkersValid : IBuildCheck 14 | { 15 | public bool Check(ProjectBuildCheckContext context, out string failureMessage) 16 | { 17 | failureMessage = string.Empty; 18 | 19 | context.Logger.LogVerbose(string.Empty); 20 | context.Logger.LogVerbose("CheckCopyUpToDateMarkersValid:"); 21 | 22 | ProjectItemInstance markerFileItem = context.Instance.GetItems("CopyUpToDateMarker").FirstOrDefault(); 23 | 24 | if (string.IsNullOrWhiteSpace(markerFileItem?.EvaluatedInclude)) 25 | { 26 | // This should happen on non-SDK Projects 27 | context.Logger.LogVerbose(" Not an SDK project. Marker files aren't used. Up to date."); 28 | return true; 29 | } 30 | 31 | // Get all of the reference assemblies. 32 | List copyReferenceInputs = context.Instance.GetItems("ReferencePathWithRefAssemblies").Select(i => i.GetMetadataValue("FullPath")).ToList(); 33 | if (copyReferenceInputs.Count == 0) 34 | { 35 | context.Logger.LogVerbose(" No input markers exist, skipping marker check. Up to date."); 36 | return true; 37 | } 38 | 39 | context.Logger.LogVerbose(" Adding input reference copy markers:"); 40 | foreach (string referenceMarkerFile in copyReferenceInputs.OrderBy(i => i)) 41 | { 42 | context.Logger.LogVerbose($" '{referenceMarkerFile}'"); 43 | } 44 | 45 | (DateTime latestInputMarkerTime, string latestInputMarkerPath) = GetLatestInput(copyReferenceInputs, context.TimeStampCache); 46 | context.Logger.LogVerbose($" Latest write timestamp on input marker is {latestInputMarkerTime} on '{latestInputMarkerPath}'."); 47 | 48 | // Get the marker file. 49 | string markerFile = markerFileItem.GetMetadataValue("FullPath"); 50 | context.Logger.LogVerbose(" Adding output reference copy marker:"); 51 | context.Logger.LogVerbose($" '{markerFile}'"); 52 | 53 | DateTime? outputMarkerTime = Utilities.GetTimestampUtc(markerFile, context.TimeStampCache); 54 | if (outputMarkerTime != null) 55 | { 56 | context.Logger.LogVerbose($" Write timestamp on output marker is {outputMarkerTime} on '{markerFile}'."); 57 | } 58 | else 59 | { 60 | context.Logger.LogVerbose($" Output marker '{markerFile}' does not exist, skipping marker check. Up to date."); 61 | return true; 62 | } 63 | 64 | if (outputMarkerTime < latestInputMarkerTime) 65 | { 66 | failureMessage = $"Input marker ('{latestInputMarkerPath}': {latestInputMarkerTime:O}) is newer than output marker ('{markerFile}': {outputMarkerTime:O}), not up to date."; 67 | context.Logger.LogVerbose($" {failureMessage}"); 68 | return false; 69 | } 70 | 71 | context.Logger.LogVerbose(" Up to date."); 72 | return true; 73 | } 74 | 75 | private static (DateTime time, string path) GetLatestInput(IEnumerable inputs, IDictionary timestampCache) 76 | { 77 | DateTime latest = DateTime.MinValue; 78 | string latestPath = null; 79 | 80 | foreach (string input in inputs) 81 | { 82 | DateTime? time = Utilities.GetTimestampUtc(input, timestampCache); 83 | 84 | if (time > latest) 85 | { 86 | // TODO remove pragmas when https://github.com/dotnet/roslyn/issues/37039 is fixed 87 | #pragma warning disable CS8629 // Nullable value type may be null 88 | latest = time.Value; 89 | #pragma warning restore CS8629 90 | latestPath = input; 91 | } 92 | } 93 | 94 | return (latest, latestPath); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The BuildUpToDateChecker is a tool that will help catch common incremental build problems. The tool will run against an MSBuild project (or project tree) and analyze it. Currently, it looks for five common issues: 4 | 1. Items with CopyToOutputDirectory set to 'Always'. 5 | 2. Items with CopyToOutputDirectory set to 'PreserveNewest' where the source file is newer (based on file timestamp) than the copied output file. 6 | 3. Reference items that are newer than the generated CopyUpToDateMarker (*.copycomplete) file. 7 | 4. Any input files that are newer than the newest output file. 8 | 5. Any output files that do not exist. 9 | 10 | This code is based on the [Visual Studio project system](https://github.com/dotnet/project-system/blob/c2c17ed3423a797fda4bab9fa71442006ace373e/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/UpToDate/BuildUpToDateCheck.cs) code to closely mimic Visual Studio's behavior in determining the "up-to-date" status of a project, but in a stand alone tool. 11 | 12 | **Note that this code is provided as-is as a proof-of-concept! No guarantees are given.** 13 | 14 | # Usage 15 | 16 | NOTE: The BuildUpToDateChecker tool uses a feature (project graph generation) requiring MSBuild 16.4 which is currently in preview. You can download this preview [here](https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-preview). If you have multiple installations (or aren't using a Visual Studio Command Prompt), be sure to use the `--msbuild` argument to point to this version. 17 | 18 | In order for the BuildUpToDateChecker tool to be accurate, the first step is to do a full build with MSBuild: 19 | 20 | cd c:\my_code\ 21 | msbuild my_root_project.proj 22 | 23 | Once built, use this tool as follows: 24 | 25 | .\BuildUpToDateChecker.exe my_root_project.proj 26 | 27 | Output will be generated to the console. 28 | Project results will (by default) be output to file ".\project-results.json". 29 | 30 | ## Arguments 31 | 32 | The following arguments will customize the tool's behavior: 33 | * `--out`. Specify an alternate, full path for the report file. By default, the file will be named "project-results.json" and be written to the current directory. 34 | * `--prop:name=value`, Add (or overwrite) additional msbuild properties that should be specified so that the tool's design-time build of a project will match the build you previously did. By default, property Configuration is set to "Debug", and property Platform is set to "AnyCPU". You can add to these properties or overwrite them. Again, set any properties necessary to match your previous build. E.g., if you did an x64 platform build, add `--prop:Platform=x64` to the command line. 35 | * `--msbuild`, Specify the full path to a specific msbuild.exe to use. By default, the tool uses logic to find installed Visual Studio instances. 36 | * `-v` or `--verbose`, This will output extra debugging information via the console. Note that this could be a LOT of output! 37 | * `-d` or `--debug`, This will cause the tool to break and wait for a debugger to attach. Note: the tool WILL HANG until a debugger attaches. 38 | * `-ff` or `--failfast`, This causes the tool to stop execution after the first project that fails any check. This can help speed up resolution as you don't have to wait for the analysis of every project in the dependency graph. 39 | * `-sbl` or `--showbuildlogs`, By default, the tool will only output the MSBuild logs of the design-time build if that build fails. If you need to debug an issue and need this MSBuild log information, set this switch (in conjunction with `-v`) and the tool will output it. Note: this will also create a LOT of output! 40 | 41 | 42 | # Contributing 43 | 44 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 45 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 46 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 47 | 48 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 49 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 50 | provided by the bot. You will only need to do this once across all repos using our CLA. 51 | 52 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 53 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 54 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 55 | -------------------------------------------------------------------------------- /test/ProgramTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using Microsoft.Build.Exceptions; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using Moq; 9 | 10 | namespace BuildUpToDateChecker.Tests 11 | { 12 | [TestClass] 13 | public class ProgramTests 14 | { 15 | [TestMethod] 16 | public void TestNoArgs() 17 | { 18 | int exitCode = Program.Main(Array.Empty()); 19 | Assert.AreNotEqual(0, exitCode); 20 | } 21 | 22 | [TestMethod] 23 | public void TestBadProjectPath() 24 | { 25 | int exitCode = Program.Main(new string[]{ ".\\myProject.csproj" }); 26 | Assert.AreNotEqual(0, exitCode); 27 | } 28 | 29 | [TestMethod] 30 | public void TestBadMsBuildPath() 31 | { 32 | string tempFile = Path.GetTempFileName(); 33 | int exitCode = Program.Main(new string[]{ tempFile, "--msbuild .\\not-a-valid-path\\msbuild.exe" }); 34 | Assert.AreNotEqual(0, exitCode); 35 | File.Delete(tempFile); 36 | } 37 | 38 | #region MSBuild Assembly Resolution Tests... 39 | // NOTE: These tests work if run individually/manually but will fail since MSBuild assembly resolution 40 | // is only supposed to be set up once, and BEFORE any MSBuild assemblies are loaded. 41 | // This creates race conditions when running these tests so they are ignored by default. 42 | 43 | [TestMethod] 44 | [Ignore] 45 | public void TestMsBuildAssemblyResolutionWithBadMsBuildPath() 46 | { 47 | var logger = new TestLogger(); 48 | 49 | string nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "MSBuild.exe"); 50 | 51 | Program.SetUpMsBuildAssemblyResolution(logger, nonExistentPath); 52 | 53 | string logText = logger.LogText; 54 | Assert.IsTrue(logText.Contains("Unable to find MSBuild at specified location")); 55 | } 56 | 57 | [TestMethod] 58 | [Ignore] 59 | public void TestMsBuildAssemblyResolutionWithGoodMsBuildPath() 60 | { 61 | var logger = new TestLogger(); 62 | string path = Path.GetTempFileName(); 63 | string usedPath = Program.SetUpMsBuildAssemblyResolution(logger, path); 64 | Assert.AreEqual(Path.GetDirectoryName(path), usedPath); 65 | } 66 | 67 | [TestMethod] 68 | [Ignore] 69 | public void TestMsBuildAssemblyResolutionInCoreXtBuild() 70 | { 71 | var logger = new TestLogger(); 72 | string envVarNme = "MSBuildToolsPath_160"; 73 | string dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); 74 | string currentValue = Environment.GetEnvironmentVariable(envVarNme) ?? string.Empty; 75 | 76 | Directory.CreateDirectory(dir); 77 | 78 | try 79 | { 80 | Environment.SetEnvironmentVariable(envVarNme, dir); 81 | string usedPath = Program.SetUpMsBuildAssemblyResolution(logger, null); 82 | Assert.AreEqual(dir, usedPath); 83 | } 84 | finally 85 | { 86 | Environment.SetEnvironmentVariable(envVarNme, currentValue); 87 | } 88 | } 89 | 90 | #endregion 91 | 92 | [TestMethod] 93 | public void TestAdditionalMsBuildPropertiesWithNoAdditionalPropArguments() 94 | { 95 | var mockLogger = new Mock(); 96 | IDictionary properties = Program.GetAdditionalMsBuildProperties(mockLogger.Object, null); 97 | 98 | Assert.IsNotNull(properties); 99 | Assert.IsTrue(properties.ContainsKey("Configuration")); 100 | Assert.IsTrue(properties.ContainsKey("Platform")); 101 | Assert.AreEqual("debug", properties["Configuration"], true); 102 | Assert.AreEqual("AnyCPU", properties["Platform"]); 103 | } 104 | 105 | [TestMethod] 106 | public void TestAdditionalMsBuildPropertiesAreUsed() 107 | { 108 | var mockLogger = new Mock(); 109 | IDictionary properties = Program.GetAdditionalMsBuildProperties(mockLogger.Object, new []{ "foo=bar", "bar=baz"}); 110 | 111 | Assert.IsNotNull(properties); 112 | 113 | // These should not have been removed. 114 | Assert.IsTrue(properties.ContainsKey("Configuration")); 115 | Assert.IsTrue(properties.ContainsKey("Platform")); 116 | Assert.AreEqual("debug", properties["Configuration"], true); 117 | Assert.AreEqual("AnyCPU", properties["Platform"]); 118 | 119 | // These should have been added. 120 | Assert.IsTrue(properties.ContainsKey("foo")); 121 | Assert.IsTrue(properties.ContainsKey("bar")); 122 | Assert.AreEqual("bar", properties["foo"]); 123 | Assert.AreEqual("baz", properties["bar"]); 124 | } 125 | 126 | [TestMethod] 127 | public void TestAdditionalMsBuildPropertiesCanOverrideTheDefaults() 128 | { 129 | var mockLogger = new Mock(); 130 | IDictionary properties = Program.GetAdditionalMsBuildProperties(mockLogger.Object, new[] { "Configuration=Retail", "Platform=ARM" }); 131 | 132 | Assert.IsNotNull(properties); 133 | 134 | // These should have been overridden. 135 | Assert.IsTrue(properties.ContainsKey("Configuration")); 136 | Assert.IsTrue(properties.ContainsKey("Platform")); 137 | Assert.AreEqual("Retail", properties["Configuration"]); 138 | Assert.AreEqual("ARM", properties["Platform"]); 139 | } 140 | 141 | [TestMethod] 142 | public void TestRun() 143 | { 144 | Options options = new Options() 145 | { 146 | InputProjectFile = Path.GetTempFileName() 147 | }; 148 | 149 | Program p = new Program(options); 150 | 151 | // Just test that the plumbing is hooked up and the GraphAnalyzer attempts to do its thing. 152 | try 153 | { 154 | p.Run(); 155 | } 156 | catch (Exception) { } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | **/.pkgrefgen/* 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush 296 | .cr/ 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using BuildUpToDateChecker.BuildChecks; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.Diagnostics.CodeAnalysis; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Threading; 12 | using McMaster.Extensions.CommandLineUtils; 13 | using Microsoft.Build.Locator; 14 | 15 | namespace BuildUpToDateChecker 16 | { 17 | [Command(Name = "BuildUpToDateChecker", Description = "Analyzes a build tree to determine if the build is up-to-date.")] 18 | [HelpOption("-?")] 19 | internal sealed class Program 20 | { 21 | private readonly Options _options; 22 | 23 | public Program(Options options) 24 | { 25 | _options = options ?? throw new ArgumentNullException(nameof(options)); 26 | } 27 | 28 | internal static int Main(string[] args) => CommandLineApplication.Execute(args); 29 | 30 | internal bool Run() 31 | { 32 | WaitForDebugger(); 33 | 34 | ILogger logger = new ConsoleLogger(_options.Verbose); 35 | OutputHeader(logger); 36 | DumpEnvironmentVariables(logger); 37 | 38 | IResultsReporter resultsReporter = null; 39 | try 40 | { 41 | logger.Log( 42 | $"Registered MSBuild from '{SetUpMsBuildAssemblyResolution(logger, _options.MsBuildPath)}'.\r\n"); 43 | 44 | resultsReporter = new ResultsReporter(_options.OutputReportFile); 45 | resultsReporter.Initialize(); 46 | 47 | GraphAnalyzer graphAnalyzer = new GraphAnalyzer( 48 | logger, 49 | new ProjectAnalyzer( 50 | logger, 51 | new DesignTimeBuildRunner( 52 | logger, 53 | GetAdditionalMsBuildProperties(logger, _options.AdditionalMsBuildProperties), alwaysLogBuildLog: _options.AlwaysDumpBuildLogOnVerbose), 54 | new BuildCheckProvider(), 55 | _options.Verbose), 56 | resultsReporter, 57 | _options.FailOnFirstError); 58 | 59 | return graphAnalyzer.AnalyzeGraph(_options.InputProjectFile); 60 | } 61 | catch (Exception ex) 62 | { 63 | logger.Log(ex.ToString()); 64 | throw; 65 | } 66 | finally 67 | { 68 | resultsReporter?.TearDown(); 69 | } 70 | } 71 | 72 | [ExcludeFromCodeCoverage] 73 | internal static string SetUpMsBuildAssemblyResolution(ILogger logger, string msBuildPathArgument) 74 | { 75 | // First, see if they've manually specified the path to MSBuild. 76 | if (!string.IsNullOrEmpty(msBuildPathArgument)) 77 | { 78 | if (File.Exists(msBuildPathArgument)) 79 | { 80 | string msBuildDirectory = Path.GetDirectoryName(msBuildPathArgument); 81 | MSBuildLocator.RegisterMSBuildPath(msBuildDirectory); 82 | return msBuildDirectory; 83 | } 84 | else 85 | { 86 | logger.Log($"Unable to find MSBuild at specified location '{msBuildPathArgument}'. Will attempt to find it automatically."); 87 | } 88 | } 89 | 90 | // Second, see if we're running in CoreXT. 91 | var coreXtBuildTools = Environment.GetEnvironmentVariable("MSBuildToolsPath_160"); 92 | if (!string.IsNullOrEmpty(coreXtBuildTools) && Directory.Exists(coreXtBuildTools)) 93 | { 94 | MSBuildLocator.RegisterMSBuildPath(coreXtBuildTools); 95 | return coreXtBuildTools; 96 | } 97 | 98 | // Lastly, see if we can find a VisualStudio instance. 99 | var vsInstance = MSBuildLocator.QueryVisualStudioInstances().FirstOrDefault(); 100 | if (vsInstance != null) 101 | { 102 | MSBuildLocator.RegisterInstance(vsInstance); 103 | return vsInstance.MSBuildPath; 104 | } 105 | 106 | throw new Exception("Unable to find an MSBuild instance. Aborting."); 107 | } 108 | 109 | internal static IDictionary GetAdditionalMsBuildProperties(ILogger logger, string[] msbuildPropertiesArgument) 110 | { 111 | var additionalMsBuildProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) 112 | { 113 | // Default to a Debug|AnyCPU configuration. This is because this is what a simple "msbuild.exe" call does. 114 | // This can be manually overridden. 115 | { "Configuration", "Debug" }, 116 | { "Platform", "AnyCPU" } 117 | }; 118 | 119 | // Add any additional specified properties. 120 | if (msbuildPropertiesArgument != null && msbuildPropertiesArgument.Length > 0) 121 | { 122 | foreach (string prop in msbuildPropertiesArgument) 123 | { 124 | string[] parts = prop.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries); 125 | if (parts.Length != 2) continue; 126 | 127 | additionalMsBuildProperties[parts[0]] = parts[1]; 128 | } 129 | 130 | // Output properties in use: 131 | logger.Log("Using the following MsBuild properties:"); 132 | foreach (KeyValuePair kvp in additionalMsBuildProperties) 133 | { 134 | logger.Log($" {kvp.Key}={kvp.Value}"); 135 | } 136 | logger.Log(string.Empty); 137 | } 138 | 139 | return additionalMsBuildProperties; 140 | } 141 | 142 | [ExcludeFromCodeCoverage] 143 | internal void DumpEnvironmentVariables(ILogger logger) 144 | { 145 | // Only dump env vars in debug mode. 146 | if (!_options.Verbose) return; 147 | 148 | logger.LogVerbose("Starting Environment Variable Values:"); 149 | IDictionary envVars = Environment.GetEnvironmentVariables(); 150 | foreach (DictionaryEntry item in envVars.Cast().OrderBy(de => de.Key)) 151 | { 152 | logger.LogVerbose($" {item.Key}: {item.Value}"); 153 | } 154 | 155 | logger.LogVerbose(string.Empty); 156 | } 157 | 158 | [ExcludeFromCodeCoverage] 159 | internal void OutputHeader(ILogger logger) 160 | { 161 | logger.Log("Build Up To Date Checker"); 162 | logger.Log(string.Empty); 163 | logger.Log("Arguments:"); 164 | logger.Log($" Root project to analyze: {_options.InputProjectFile}"); 165 | logger.Log($" Report output file: {_options.OutputReportFile}"); 166 | logger.Log($" MSBuild specified: { _options.MsBuildPath ?? "[None]" }"); 167 | logger.Log($" Additional MSBuild properties: { (_options.AdditionalMsBuildProperties == null ? "[None]" : string.Join(", ", _options.AdditionalMsBuildProperties)) }"); 168 | logger.Log($" Verbose output: {_options.Verbose}"); 169 | logger.Log($" Attach debugger: {_options.AttachDebugger}"); 170 | logger.Log($" Fail on first up-to-date check failure: {_options.FailOnFirstError}"); 171 | logger.Log(string.Empty); 172 | } 173 | 174 | [ExcludeFromCodeCoverage] 175 | private void WaitForDebugger() 176 | { 177 | if (!_options.AttachDebugger) 178 | { 179 | return; 180 | } 181 | 182 | Process p = Process.GetCurrentProcess(); 183 | Console.WriteLine($"Waiting for debugger to attach to {p.ProcessName} ({p.Id})..."); // Directly use Console.WriteLine. Logger isn't created yet. 184 | 185 | while (!Debugger.IsAttached) 186 | { 187 | Thread.Sleep(1000); 188 | } 189 | 190 | Debugger.Break(); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/ProjectAnalyzer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using Microsoft.Build.Evaluation; 4 | using Microsoft.Build.Execution; 5 | using Microsoft.Build.Prediction; 6 | using Microsoft.Build.Prediction.Predictors; 7 | using System; 8 | using System.Linq; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using BuildUpToDateChecker.BuildChecks; 12 | 13 | namespace BuildUpToDateChecker 14 | { 15 | /// 16 | /// Analyzes a project to determine if it is up to date and shouldn't need to be rebuilt. 17 | /// 18 | internal sealed class ProjectAnalyzer : IProjectAnalyzer 19 | { 20 | private readonly ILogger _logger; 21 | private readonly IDesignTimeBuildRunner _designTimeBuilder; 22 | private readonly IBuildCheckProvider _checkProvider; 23 | private readonly bool _verboseOutput; 24 | 25 | /// 26 | /// Create an instance of . 27 | /// 28 | public ProjectAnalyzer(ILogger logger, IDesignTimeBuildRunner designTimeBuilder, IBuildCheckProvider checkProvider, bool verboseOutput = false) 29 | { 30 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 31 | _designTimeBuilder = designTimeBuilder ?? throw new ArgumentNullException(nameof(designTimeBuilder)); 32 | _checkProvider = checkProvider ?? throw new ArgumentNullException(nameof(checkProvider)); 33 | _verboseOutput = verboseOutput; 34 | } 35 | 36 | /// 37 | /// Performs analysis on the given project to determine if the project is up to date or not. 38 | /// 39 | /// The full path to the project file. 40 | /// True, if all project checks pass and the project is up to date. Else, false. 41 | public (bool, string) IsBuildUpToDate(string fullPathToProjectFile) 42 | { 43 | _logger.Log($"Checking if project '{ fullPathToProjectFile }' is up to date."); 44 | 45 | // Get project objects 46 | (Project project, ProjectInstance projectInstance) = GetProjectObjects(fullPathToProjectFile); 47 | 48 | // Get predictions for project 49 | ProjectPredictions predictions = GetProjectPredictions(project); 50 | 51 | (bool projectBuildIsUpToDate, string failureMessage) = IsBuildUpToDate(projectInstance, predictions); 52 | 53 | _logger.Log(projectBuildIsUpToDate ? "Build is up to date." : "Build is not up to date."); 54 | 55 | return (projectBuildIsUpToDate, failureMessage); 56 | } 57 | 58 | private (bool, string) IsBuildUpToDate(ProjectInstance projectInstance, ProjectPredictions predictions) 59 | { 60 | HashSet inputs = GetAllFiles(predictions.InputFiles, predictions.InputDirectories); 61 | var outputs = new HashSet(StringComparer.OrdinalIgnoreCase); 62 | 63 | AddCustomInputs(inputs, projectInstance); 64 | AddCustomOutputs(outputs, projectInstance); 65 | 66 | // The prediction library can sometimes flag intermediate files as inputs. 67 | // For example, if a file was copied to $(OutDir) and then copied elsewhere, 68 | // it'll see that file as an input because it was the input to the 2nd copy. 69 | // To avoid this, we'll prune the input list, removing everything from $(OutDir). 70 | string outDir = projectInstance.GetPropertyValue("OutDir"); 71 | inputs.RemoveWhere((input) => input.StartsWith(outDir, StringComparison.OrdinalIgnoreCase)); 72 | _logger.LogVerbose($"Removing inputs residing in OutDir ({outDir})..."); 73 | 74 | var context = new ProjectBuildCheckContext() 75 | { 76 | Logger = _logger, 77 | Instance = projectInstance, 78 | Predictions = predictions, 79 | Inputs = inputs, 80 | Outputs = outputs, 81 | TimeStampCache = new Dictionary(StringComparer.OrdinalIgnoreCase), 82 | VerboseOutput = _verboseOutput 83 | }; 84 | 85 | // Log predicted inputs and outputs. 86 | _logger.LogVerbose(string.Empty); 87 | _logger.LogVerbose("Predicted Inputs:"); 88 | foreach (string input in inputs.OrderBy(i => i)) 89 | { 90 | _logger.LogVerbose($" {input}"); 91 | } 92 | 93 | _logger.LogVerbose(string.Empty); 94 | _logger.LogVerbose("Predicted Outputs:"); 95 | foreach (string output in outputs.OrderBy(i => i)) 96 | { 97 | _logger.LogVerbose($" {output}"); 98 | } 99 | 100 | // All checks must pass for the build to be up to date. 101 | string failureMessage = string.Empty; 102 | return (_checkProvider.GetBuildChecks().All(buildCheck => buildCheck.Check(context, out failureMessage)), failureMessage); 103 | } 104 | 105 | private (Project, ProjectInstance) GetProjectObjects(string projectFilePath) 106 | { 107 | var buildLogger = new SimpleMsBuildLogger(); 108 | 109 | Project project; 110 | try 111 | { 112 | project = new Project( 113 | projectFilePath, 114 | globalProperties: null, 115 | toolsVersion: null, 116 | new ProjectCollection( 117 | null, 118 | new Microsoft.Build.Framework.ILogger[] { buildLogger }, 119 | ToolsetDefinitionLocations.Default)); 120 | } 121 | catch(Exception ex) 122 | { 123 | this._logger.Log($"Error attempting to load project '{projectFilePath}'."); 124 | this._logger.Log(ex.Message); 125 | 126 | if (_verboseOutput) 127 | { 128 | this._logger.LogVerbose(buildLogger.LogText); 129 | } 130 | 131 | throw; 132 | } 133 | 134 | ProjectInstance projectInstance = _designTimeBuilder.Execute(project); 135 | 136 | return (project, projectInstance); 137 | } 138 | 139 | private static ProjectPredictions GetProjectPredictions(Project project) 140 | { 141 | // None and Content items are checked more carefully with other mechanisms. 142 | // Thus, we're excluding these items from the build predictor here. 143 | IEnumerable buildUpToDatePredictors = ProjectPredictors.AllPredictors.Where(p => !(p is NoneItemsPredictor) && !(p is ContentItemsPredictor)); 144 | var predictionExecutor = new ProjectPredictionExecutor(buildUpToDatePredictors); 145 | return predictionExecutor.PredictInputsAndOutputs(project); 146 | } 147 | 148 | private static HashSet GetAllFiles(IEnumerable files, IEnumerable folders) 149 | { 150 | var distinctFileSet = new HashSet(files.Select(p => p.Path), StringComparer.OrdinalIgnoreCase); 151 | 152 | foreach (string file in folders.Where(d => Directory.Exists(d.Path)).SelectMany(d => Directory.EnumerateFiles(d.Path, "*", SearchOption.TopDirectoryOnly))) 153 | { 154 | distinctFileSet.Add(file); 155 | } 156 | 157 | return distinctFileSet; 158 | } 159 | 160 | private static void AddCustomOutputs(ISet outputs, ProjectInstance projectInstance) 161 | { 162 | foreach (ProjectItemInstance customOutput in projectInstance.GetItems("UpToDateCheckOutput")) 163 | { 164 | outputs.Add(customOutput.GetMetadataValue("FullPath")); 165 | } 166 | 167 | foreach (ProjectItemInstance buildOutput in projectInstance.GetItems("UpToDateCheckBuilt").Where(i => string.IsNullOrEmpty(i.GetMetadataValue("Original")))) 168 | { 169 | outputs.Add(buildOutput.GetMetadataValue("FullPath")); 170 | } 171 | 172 | // See if this is a NoTarget SDK project. If so, skip the outputs. 173 | string usingNoTargets = projectInstance.GetPropertyValue("UsingMicrosoftNoTargetsSdk"); 174 | bool isNoTargetsProject = !string.IsNullOrEmpty(usingNoTargets) && usingNoTargets.Trim().Equals("true", StringComparison.OrdinalIgnoreCase); 175 | if (!isNoTargetsProject) return; 176 | 177 | // This IS a NoTarget SDK project, so we have to do some further adjusting. Because of: 178 | // Target "CollectUpToDateCheckBuiltDesignTime" in file "C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\VisualStudio\Managed\Microsoft.Managed.DesignTime.targets" 179 | RemoveNoTargetsOutputs(outputs, projectInstance); 180 | } 181 | 182 | private static void AddCustomInputs(ISet inputs, ProjectInstance projectInstance) 183 | { 184 | // See for context: https://github.com/dotnet/project-system/pull/2416 185 | // "In the old project system, a project file can specify UpToDateCheckInput 186 | // and UpToDateCheckOutput items that add items into inputs or outputs that 187 | // the fast up-to-date check uses. It's a kind of escape hatch for special 188 | // project extensions that the project system is not aware of." 189 | foreach (ProjectItemInstance customInput in projectInstance.GetItems("UpToDateCheckInput")) 190 | { 191 | inputs.Add(customInput.GetMetadataValue("FullPath")); 192 | } 193 | } 194 | 195 | private static void RemoveNoTargetsOutputs(ISet outputs, ProjectInstance projectInstance) 196 | { 197 | // Remove binaries and debug symbols in obj and bin. 198 | string targetPath = projectInstance.GetPropertyValue("TargetPath"); 199 | if (outputs.Contains(targetPath)) { outputs.Remove(targetPath); } 200 | 201 | RemoveMatchingItemsFromSet(outputs, projectInstance, "IntermediateAssembly"); 202 | RemoveMatchingItemsFromSet(outputs, projectInstance, "_DebugSymbolsIntermediatePath"); 203 | RemoveMatchingItemsFromSet(outputs, projectInstance, "_DebugSymbolsOutputPath"); 204 | } 205 | 206 | private static void RemoveMatchingItemsFromSet(ISet outputs, ProjectInstance projectInstance, string itemName) 207 | { 208 | var items = projectInstance.GetItems(itemName); 209 | foreach (var item in items) 210 | { 211 | string fullPath = item.GetMetadataValue("FullPath"); 212 | if (string.IsNullOrEmpty(fullPath)) 213 | { 214 | continue; 215 | } 216 | 217 | if (outputs.Contains(fullPath)) 218 | { 219 | outputs.Remove(fullPath); 220 | } 221 | } 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /test/ProjectAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | using BuildUpToDateChecker.BuildChecks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Moq; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | 10 | namespace BuildUpToDateChecker.Tests 11 | { 12 | [TestClass] 13 | public class ProjectAnalyzerTests 14 | { 15 | private static HashSet ItemTypesForUpToDateCheckInput = new HashSet(StringComparer.OrdinalIgnoreCase) 16 | { 17 | "SplashScreen", 18 | "CodeAnalysisDictionary", 19 | "Resource", 20 | "DesignDataWithDesignTimeCreatableTypes", 21 | "ApplicationDefinition", 22 | "EditorConfigFiles", 23 | "Fakes", 24 | "EmbeddedResource", 25 | "EntityDeploy", 26 | "Compile", 27 | "Content", 28 | "DesignData", 29 | "AdditionalFiles", 30 | "XamlAppDef", 31 | "None", 32 | "Page" 33 | }; 34 | 35 | #region Constructor Tests... 36 | [TestMethod] 37 | [ExpectedException(typeof(ArgumentNullException))] 38 | public void TestConstructorNullLogger() 39 | { 40 | Mock buildRunner = new Mock(); 41 | Mock checks = new Mock(); 42 | new ProjectAnalyzer(null, buildRunner.Object, checks.Object); 43 | } 44 | 45 | [TestMethod] 46 | [ExpectedException(typeof(ArgumentNullException))] 47 | public void TestConstructorNullRunner() 48 | { 49 | Mock logger = new Mock(); 50 | Mock checks = new Mock(); 51 | new ProjectAnalyzer(logger.Object, null, checks.Object); 52 | } 53 | 54 | [TestMethod] 55 | [ExpectedException(typeof(ArgumentNullException))] 56 | public void TestConstructorNullChecks() 57 | { 58 | Mock logger = new Mock(); 59 | Mock buildRunner = new Mock(); 60 | new ProjectAnalyzer(logger.Object, buildRunner.Object, null); 61 | } 62 | #endregion 63 | 64 | [TestMethod] 65 | public void TestEmptyProjectWithNoChecksIsUpToDate() 66 | { 67 | string projectFile = TestUtilities.CreateTestProject(); 68 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 69 | projectFile, 70 | logger: new Mock().Object, 71 | checks: new Mock().Object); 72 | 73 | Assert.IsTrue(isUpToDate); 74 | } 75 | 76 | #region CopyToOutput = Always Tests... 77 | [TestMethod] 78 | public void TestAlwaysCopyToOutputDirectory_AlwaysIsNotUpToDate() 79 | { 80 | string projectFile = TestUtilities.CreateTestProject( 81 | rawMsBuildXmlToInsert: "Always", 82 | filesToCreate: new string[] { "MyContent.js" }); 83 | 84 | var checks = new Mock(MockBehavior.Strict); 85 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckAlwaysCopyToOutput(ItemTypesForUpToDateCheckInput) }); 86 | 87 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 88 | projectFile, 89 | logger: new Mock().Object, 90 | checks: checks.Object); 91 | 92 | Assert.IsFalse(isUpToDate); 93 | } 94 | 95 | [TestMethod] 96 | public void TestAlwaysCopyToOutputDirectoryWithNonUpToDateInputItem_AlwaysIsUpToDate() 97 | { 98 | string projectFile = TestUtilities.CreateTestProject( 99 | rawMsBuildXmlToInsert: "Always", 100 | filesToCreate: new string[] { "MyContent.js" }); 101 | 102 | var checks = new Mock(MockBehavior.Strict); 103 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckAlwaysCopyToOutput(ItemTypesForUpToDateCheckInput) }); 104 | 105 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 106 | projectFile, 107 | logger: new Mock().Object, 108 | checks: checks.Object); 109 | 110 | Assert.IsTrue(isUpToDate); 111 | } 112 | 113 | [TestMethod] 114 | public void TestAlwaysCopyToOutputDirectory_NeverIsUpToDate() 115 | { 116 | string projectFile = TestUtilities.CreateTestProject( 117 | rawMsBuildXmlToInsert: "Never", 118 | filesToCreate: new string[] { "MyContent.js" }); 119 | 120 | var checks = new Mock(MockBehavior.Strict); 121 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckAlwaysCopyToOutput(ItemTypesForUpToDateCheckInput) }); 122 | 123 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 124 | projectFile, 125 | logger: new Mock().Object, 126 | checks: checks.Object); 127 | 128 | Assert.IsTrue(isUpToDate); 129 | } 130 | #endregion 131 | 132 | #region CopyToOutput = PreserveNewest Tests... 133 | 134 | [TestMethod] 135 | public void TestAreCopyToOutputDirectoryFilesValid_MissingInputIsNotUpToDate() 136 | { 137 | string projectFile = TestUtilities.CreateTestProject( 138 | rawMsBuildXmlToInsert: "PreserveNewest"); 139 | 140 | var checks = new Mock(MockBehavior.Strict); 141 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckAreCopyToOutputDirectoryFilesValid(ItemTypesForUpToDateCheckInput) }); 142 | 143 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 144 | projectFile, 145 | logger: new Mock().Object, 146 | checks: checks.Object); 147 | 148 | Assert.IsFalse(isUpToDate); 149 | } 150 | 151 | [TestMethod] 152 | public void TestAreCopyToOutputDirectoryFilesValid_MissingOutputIsNotUpToDate() 153 | { 154 | string projectFile = TestUtilities.CreateTestProject( 155 | rawMsBuildXmlToInsert: "PreserveNewest", 156 | filesToCreate: new string[] { "MyContent.js" }); 157 | 158 | var checks = new Mock(MockBehavior.Strict); 159 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckAreCopyToOutputDirectoryFilesValid(ItemTypesForUpToDateCheckInput) }); 160 | 161 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 162 | projectFile, 163 | logger: new Mock().Object, 164 | checks: checks.Object); 165 | 166 | Assert.IsFalse(isUpToDate); 167 | } 168 | 169 | [TestMethod] 170 | public void TestAreCopyToOutputDirectoryFilesValid_ExistingOutputIsUpToDate() 171 | { 172 | string projectFile = TestUtilities.CreateTestProject( 173 | rawMsBuildXmlToInsert: "PreserveNewest", 174 | filesToCreate: new string[] { "MyContent.js", "bin\\Debug\\net472\\MyContent.js" }); 175 | 176 | var checks = new Mock(MockBehavior.Strict); 177 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckAreCopyToOutputDirectoryFilesValid(ItemTypesForUpToDateCheckInput) }); 178 | 179 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 180 | projectFile, 181 | logger: new Mock().Object, 182 | checks: checks.Object); 183 | 184 | Assert.IsTrue(isUpToDate); 185 | } 186 | 187 | [TestMethod] 188 | public void TestAreCopyToOutputDirectoryFilesValid_ExistingOutputIsNotUpToDate() 189 | { 190 | string projectFile = TestUtilities.CreateTestProject( 191 | rawMsBuildXmlToInsert: "PreserveNewest", 192 | filesToCreate: new string[] { "bin\\Debug\\net472\\MyContent.js", "MyContent.js" }); 193 | 194 | var checks = new Mock(MockBehavior.Strict); 195 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckAreCopyToOutputDirectoryFilesValid(ItemTypesForUpToDateCheckInput) }); 196 | 197 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 198 | projectFile, 199 | logger: new Mock().Object, 200 | checks: checks.Object); 201 | 202 | Assert.IsFalse(isUpToDate); 203 | } 204 | 205 | [TestMethod] 206 | public void TestAreCopyToOutputDirectoryFilesValid_MissingOutputForNonUpToDateInputItem() 207 | { 208 | string projectFile = TestUtilities.CreateTestProject( 209 | rawMsBuildXmlToInsert: "PreserveNewest", 210 | filesToCreate: new string[] { "MyUpToDateInputItem.js" }); 211 | 212 | var checks = new Mock(MockBehavior.Strict); 213 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckAreCopyToOutputDirectoryFilesValid(ItemTypesForUpToDateCheckInput) }); 214 | 215 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 216 | projectFile, 217 | logger: new Mock().Object, 218 | checks: checks.Object); 219 | 220 | Assert.IsTrue(isUpToDate); 221 | } 222 | #endregion 223 | 224 | #region CopyUpToDateMarkersValid Tests... 225 | [TestMethod] 226 | public void TestCheckCopyUpToDateMarkersValid_NoReferencesIsUpToDate() 227 | { 228 | string projectFile = TestUtilities.CreateTestProject(outputBinaries: true); 229 | 230 | var checks = new Mock(MockBehavior.Strict); 231 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckCopyUpToDateMarkersValid() }); 232 | 233 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 234 | projectFile, 235 | logger: new Mock().Object, 236 | checks: checks.Object); 237 | 238 | Assert.IsTrue(isUpToDate); 239 | } 240 | 241 | [TestMethod] 242 | public void TestCheckCopyUpToDateMarkersValid_MissingMarkerFileIsUpToDate() 243 | { 244 | string projectFile = TestUtilities.CreateTestProject(outputBinaries: true); 245 | 246 | var checks = new Mock(MockBehavior.Strict); 247 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckCopyUpToDateMarkersValid() }); 248 | 249 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 250 | projectFile, 251 | logger: new Mock().Object, 252 | checks: checks.Object); 253 | 254 | // NOTE: This is counter-intuitive to me. Checking with 255 | // owners of original code to verify this is the expected result. 256 | Assert.IsTrue(isUpToDate); 257 | } 258 | 259 | [TestMethod] 260 | public void TestCheckCopyUpToDateMarkersValid_ExistingMarkerFileIsUpToDate() 261 | { 262 | string projectFile = TestUtilities.CreateTestProject(outputBinaries: true); 263 | 264 | // Set up the expected marker file. 265 | string markerFile = Path.Combine(Path.GetDirectoryName(projectFile), "obj\\Debug\\net472", Path.GetFileName(projectFile) + ".CopyComplete"); 266 | Directory.CreateDirectory(Path.GetDirectoryName(markerFile)); 267 | File.WriteAllText(markerFile, "// TEST"); 268 | 269 | var checks = new Mock(MockBehavior.Strict); 270 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckCopyUpToDateMarkersValid() }); 271 | 272 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 273 | projectFile, 274 | logger: new Mock().Object, 275 | checks: checks.Object); 276 | 277 | Assert.IsTrue(isUpToDate); 278 | } 279 | 280 | [TestMethod] 281 | public void TestCheckCopyUpToDateMarkersValid_ExistingMarkerFileIsNotUpToDate() 282 | { 283 | string projectFile = TestUtilities.CreateTestProject(outputBinaries: true); 284 | 285 | // Set up the expected marker file. 286 | string markerFile = Path.Combine(Path.GetDirectoryName(projectFile), "obj\\Debug\\net472", Path.GetFileName(projectFile) + ".CopyComplete"); 287 | Directory.CreateDirectory(Path.GetDirectoryName(markerFile)); 288 | File.WriteAllText(markerFile, "// TEST"); 289 | 290 | // Force the marker file to be "older". 291 | DateTime updatedTime = new DateTime(2001, 1, 1); 292 | FileInfo fi = new FileInfo(markerFile); 293 | fi.CreationTime = updatedTime; 294 | fi.LastWriteTime = updatedTime; 295 | 296 | var checks = new Mock(MockBehavior.Strict); 297 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckCopyUpToDateMarkersValid() }); 298 | 299 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 300 | projectFile, 301 | logger: new Mock().Object, 302 | checks: checks.Object); 303 | 304 | Assert.IsFalse(isUpToDate); 305 | } 306 | #endregion 307 | 308 | #region OutputsAreValid Tests... 309 | [TestMethod] 310 | public void TestCheckOutputsAreValid_MissingOutputIsNotUpToDate() 311 | { 312 | string projectFile = TestUtilities.CreateTestProject(filesToCreate: new string[]{ "SomeSource.cs" }, outputBinaries: true); 313 | 314 | var checks = new Mock(MockBehavior.Strict); 315 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckOutputsAreValid() }); 316 | 317 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 318 | projectFile, 319 | logger: new Mock().Object, 320 | checks: checks.Object); 321 | 322 | Assert.IsFalse(isUpToDate); 323 | } 324 | 325 | 326 | [TestMethod] 327 | public void TestCheckOutputsAreValid_ExistingOutputIsUpToDate() 328 | { 329 | string projectFile = TestUtilities.CreateTestProject( 330 | filesToCreate: new string[] { "SomeSource.cs" }, 331 | outputBinaries: true, 332 | createStandardOutputs: true); 333 | 334 | var checks = new Mock(MockBehavior.Strict); 335 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckOutputsAreValid() }); 336 | 337 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 338 | projectFile, 339 | logger: new Mock().Object, 340 | checks: checks.Object); 341 | 342 | Assert.IsTrue(isUpToDate); 343 | } 344 | 345 | [TestMethod] 346 | public void TestCheckOutputsAreValid_OldOutputIsNotUpToDate() 347 | { 348 | string projectFile = TestUtilities.CreateTestProject(filesToCreate: new string[] { "SomeSource.cs" }, outputBinaries: true, createStandardOutputs: true); 349 | 350 | // Update our inputs now that the outputs have been created. 351 | File.WriteAllText(Path.Combine(Path.GetDirectoryName(projectFile), "SomeSource.cs"), "Updated!"); 352 | 353 | var checks = new Mock(MockBehavior.Strict); 354 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckOutputsAreValid() }); 355 | 356 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 357 | projectFile, 358 | logger: new Mock().Object, 359 | checks: checks.Object); 360 | 361 | Assert.IsFalse(isUpToDate); 362 | } 363 | #endregion 364 | 365 | #region UpToDateCheckBuiltItems Tests... 366 | 367 | [TestMethod] 368 | public void TestAreCopiedOutputFilesValid_MissingInputIsNotUpToDate() 369 | { 370 | string projectFile = TestUtilities.CreateTestProject( 371 | rawMsBuildXmlToInsert: ""); 372 | 373 | var checks = new Mock(MockBehavior.Strict); 374 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckUpToDateCheckBuiltItems() }); 375 | 376 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 377 | projectFile, 378 | logger: new Mock().Object, 379 | checks: checks.Object); 380 | 381 | Assert.IsFalse(isUpToDate); 382 | } 383 | 384 | [TestMethod] 385 | public void TestAreCopiedOutputFilesValid_MissingOutputIsNotUpToDate() 386 | { 387 | string projectFile = TestUtilities.CreateTestProject( 388 | rawMsBuildXmlToInsert: "", 389 | filesToCreate: new string[] { "MyContent.js" }); 390 | 391 | var checks = new Mock(MockBehavior.Strict); 392 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckUpToDateCheckBuiltItems() }); 393 | 394 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 395 | projectFile, 396 | logger: new Mock().Object, 397 | checks: checks.Object); 398 | 399 | Assert.IsFalse(isUpToDate); 400 | } 401 | 402 | [TestMethod] 403 | public void TestAreCopiedOutputFilesValid_ExistingOutputIsUpToDate() 404 | { 405 | string projectFile = TestUtilities.CreateTestProject( 406 | rawMsBuildXmlToInsert: "", 407 | filesToCreate: new string[] { "MyContent.js", "bin\\Debug\\net472\\exists.js" }); // Files are created in order. So the "destination" file will be newest. 408 | 409 | var checks = new Mock(MockBehavior.Strict); 410 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckUpToDateCheckBuiltItems() }); 411 | 412 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 413 | projectFile, 414 | logger: new Mock().Object, 415 | checks: checks.Object); 416 | 417 | Assert.IsTrue(isUpToDate); 418 | } 419 | 420 | [TestMethod] 421 | public void TestAreCopiedOutputFilesValid_ExistingOutputIsNotUpToDate() 422 | { 423 | string projectFile = TestUtilities.CreateTestProject( 424 | rawMsBuildXmlToInsert: "", 425 | filesToCreate: new string[] { "bin\\Debug\\exists.js", "MyContent.js" }); // Files are created in order. So the "source" file will be newest. 426 | 427 | var checks = new Mock(MockBehavior.Strict); 428 | checks.Setup(c => c.GetBuildChecks()).Returns(new IBuildCheck[] { new CheckUpToDateCheckBuiltItems() }); 429 | 430 | bool isUpToDate = ProjectAnalyzerIsUpToDateCall( 431 | projectFile, 432 | logger: new Mock().Object, 433 | checks: checks.Object); 434 | 435 | Assert.IsFalse(isUpToDate); 436 | } 437 | #endregion 438 | 439 | private static bool ProjectAnalyzerIsUpToDateCall(string projectFile, ILogger logger = null, IDesignTimeBuildRunner runner = null, IBuildCheckProvider checks = null, bool cleanUp = true) 440 | { 441 | try 442 | { 443 | var usedLogger = logger ?? new ConsoleLogger(false); 444 | 445 | ProjectAnalyzer analyzer = new ProjectAnalyzer( 446 | usedLogger, 447 | runner ?? new DesignTimeBuildRunner(usedLogger, null), 448 | checks ?? new BuildCheckProvider()); 449 | 450 | (bool isUpToDate, string failureMessage) = analyzer.IsBuildUpToDate(projectFile); 451 | return isUpToDate; 452 | } 453 | finally 454 | { 455 | if (cleanUp) 456 | TestUtilities.CleanUpTestProject(projectFile); 457 | } 458 | } 459 | } 460 | } 461 | --------------------------------------------------------------------------------