├── 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 |
--------------------------------------------------------------------------------