From 6d1aca2cfdcc52beb356663cbdddcb89cacb337e Mon Sep 17 00:00:00 2001 From: Benjamin Brienen Date: Thu, 20 Mar 2025 13:13:37 +0100 Subject: [PATCH] markdown in documentation/specs formatting/linting/cleanup --- documentation/specs/event-source.md | 3 +- documentation/specs/low-priority-switch.md | 2 +- documentation/specs/project-cache.md | 73 +++++++++++-------- documentation/specs/question.md | 11 ++- documentation/specs/rar-core-scenarios.md | 2 + documentation/specs/remote-host-object.md | 7 +- .../specs/sdk-resolvers-algorithm.md | 33 +++++---- .../specs/single-project-isolated-builds.md | 14 ++-- documentation/specs/static-graph.md | 49 ++++++++++--- .../specs/task-isolation-and-dependencies.md | 33 ++++++--- documentation/specs/test-target.md | 67 ++++++++++------- 11 files changed, 187 insertions(+), 107 deletions(-) diff --git a/documentation/specs/event-source.md b/documentation/specs/event-source.md index 198791bb356..dcaab9c9cba 100644 --- a/documentation/specs/event-source.md +++ b/documentation/specs/event-source.md @@ -3,6 +3,7 @@ [EventSource](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.tracing.eventsource?view=netframework-4.8) is the tool that allows Event Tracing for Windows (ETW) used in MSBuild. Among its useful features, functions with names ending in "start" and "stop" correlate between calls such that it can automatically record how long the event between the two calls took. It also provides an easy way to cheaply opt in or out, log auxiliary messages in addition to time, and add progress updates in the middle of an event as needed. ## EventSource in MSBuild + EventSource is primarily used to profile code. For MSBuild specifically, a major goal is to reduce the time it takes to run, as measured (among other metrics) by the Regression Prevention System (RPS), i.e., running specific scenarios. To find which code segments were likely candidates for improvement, EventSources were added around a mix of code segments. Larger segments that encompass several steps within a build occur nearly every time MSBuild is run and take a long time. They generally run relatively few times. Smaller methods with well-defined purposes may occur numerous times. Profiling both types of events provides both broad strokes to identify large code segments that underperform and, more specifically, which parts of them. Profiled functions include: | Event | Description | @@ -20,7 +21,7 @@ EventSource is primarily used to profile code. For MSBuild specifically, a major | ExecuteTaskYield | Requests to yield the node, often while the task completes other work. | | ExpandGlob | Identifies a list of files that correspond to an item, potentially with a wildcard. | | GenerateResourceOverall | Uses resource APIs to transform resource files into strongly-typed resource classes. | -| LoadDocument | Loads an XMLDocumentWithLocation from a path. +| LoadDocument | Loads an XMLDocumentWithLocation from a path. | | MSBuildExe | Executes MSBuild from the command line. | | MSBuildServerBuild | Executes a build from the MSBuildServer node. | | PacketReadSize | Reports the size of a packet sent between nodes. Note that this does not include time information. | diff --git a/documentation/specs/low-priority-switch.md b/documentation/specs/low-priority-switch.md index ba33826a89e..bec5a72fc74 100644 --- a/documentation/specs/low-priority-switch.md +++ b/documentation/specs/low-priority-switch.md @@ -17,7 +17,6 @@ Visual Studio, on the other hand, should always run at normal priority. This ens 4. Any reused nodes are at the priority they themselves specify. Normal priority nodes are actually at normal priority, and low priority nodes are actually at BelowNormal priority. 5. All nodes are at the priority they should be when being used to build even if a normal priority process had connected to them as normal priority worker nodes, and they are now executing a low priority build. - ## Non-goals Perfect parity between windows and mac or linux. Windows permits processes to raise their own priority or that of another process, whereas other operating systems do not. This is very efficient, so we should use it. As we expect this feature to be used in Visual Studio, we anticipate it being less used on mac and linux, hence not being as high priority to make it just as efficient. @@ -27,6 +26,7 @@ Perfect parity between windows and mac or linux. Windows permits processes to ra Each node (including worker nodes) initially takes its priority from its parent process. Since we now need the priority to align with what it is passed instead of its parent, attempt to adjust priority afterwards if necessary as part of node startup. BuildManager.cs remembers the priority of the previous build it had executed. If that was set to a value that differs from the priority of the current build: + 1. On windows or when decreasing the priority: lowers the priority of all connected nodes 2. On linux and mac when increasing the priority: disconnects from all nodes. diff --git a/documentation/specs/project-cache.md b/documentation/specs/project-cache.md index b0ce961313d..90d19466fdc 100644 --- a/documentation/specs/project-cache.md +++ b/documentation/specs/project-cache.md @@ -1,8 +1,10 @@ -# Summary +# Project Cache + +## Summary Project cache is a new assembly-based plugin extension point in MSBuild which determines whether a build request (a project) can be skipped during build. The main expected benefit is reduced build times via [caching and/or distribution](static-graph.md#weakness-of-the-old-model-caching-and-distributability). -# Motivation +## Motivation As the introduction to [static graph](static-graph.md#what-is-static-graph-for) suggests, large and complex repos expose the weaknesses in MSBuild's scheduling and incrementality models as build times elongate. This project cache plugin lets MSBuild natively communicate with existing tools that enable build caching and/or distribution, enabling true scalability. @@ -10,31 +12,33 @@ Visual Studio is one beneficiary. This plugin inverts dependencies among build s This change also simplifies and unifies user experiences. MSBuild works the same from Visual Studio or the command line without dramatically changing how it works. -# Plugin requirements +## Plugin requirements - The plugin should tell MSBuild whether a build request needs building. If a project is skipped, then the plugin needs to ensure that: - it makes the filesystem look as if the project built - it returns sufficient information back to MSBuild such that MSBuild can construct a valid [`BuildResult`](/src/Build/BackEnd/Shared/BuildResult.cs#L30-L33) for its internal scheduling logic, such that future requests to build a skipped project are served directly from MSBuild's internal caches. -# High-level design +## High-level design Conceptually, there are two parts of caching: "cache get" and "cache add". "Cache get" is MSBuild asking the plugin if it wants to handle a build request, ie by fetching from some cache. "Cache add" is, upon cache miss, MSBuild providing enough information to the plugin during the build of the build request for the plugin to add the results to its cache and safely be able to retrieve it for some future build. The "cache get" functionality was introduced in 16.9, while "cache add" was added in 17.8. -## Plugin discovery +### Plugin discovery - Plugin dlls are discovered by MSBuild via a new special purpose `ProjectCachePlugin` [items](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-items). - These items can get injected into a project's import graph by package managers via the [PackageReference](https://docs.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files) item. - MSBuild will discover the plugin by searching project evaluations for `ProjectCachePlugin` items. -```xml - - - -``` + + ```xml + + + + ``` + - Programmatic usage of `BuildManager` can also set `BuildParameters.ProjectCacheDescriptor` to apply a plugin to all requests. -## Plugin lifetime +### Plugin lifetime - Plugin instances reside only in the `BuildManager` node. Having it otherwise (plugin instances residing in all nodes) means forcing the plugins to either deal with distributed state or implement a long lived service. We consider this high complexity cost to not be worth it. We also want to avoid serializing the `ProjectInstance` between nodes, which is expensive. - `BuildManager.BeginBuild` calls `ProjectCacheBase.BeginBuildAsync` on all discovered plugins. This allows plugins to start any required initialization work. It does not wait for the plugins to fully initialize, ie it is a "fire-and-forget" call at this point. The first query on the plugin will wait for plugin initialization. @@ -43,10 +47,10 @@ The "cache get" functionality was introduced in 16.9, while "cache add" was adde - The plugin instance will get called in reverse topological sort order (from referenced projects up towards referencing projects). This happens when performing a graph build (`/graph`), Visual Studio solution builds, and commonly in higher build engines. - Only the top-level build requests are checked against the cache. Build requests issued recursively from the top-level requests, for example a project building its dependencies, are not checked against the cache. However, because the build requests are assumed to be issued in reverse topological sort order, those requests should have already been built and present in MSBuild's internal result cache, provided either by the project cache plugin or real builds. A consequence of this is that projects which are not well-described in the graph (e.g. using `` tasks directly) will not benefit from the cache. -## Cache get scenario +### Cache get scenario - For each [`BuildRequestData`](/src/Build/BackEnd/BuildManager/BuildRequestData.cs#L83) ([`ProjectInstance`](/src/Build/Instance/ProjectInstance.cs#L71), Global Properties, Targets) submitted to the [`BuildManager`](/src/Build/BackEnd/BuildManager/BuildManager.cs#L38), MSBuild asks the plugin whether to build the request or not. - + - If the `BuildRequestData` is based on a project path instead of a `ProjectInstance`, the project is evaluated by the `BuildManager`. - If the plugin decides to build, then MSBuild proceeds building the project as usual. - If the plugin decides to skip the build, it needs to return back to MSBuild the target results that the build request would have produced. It can either provide the results directly, or instruct MSBuild to run a set of less expensive targets on the projects with the same effect as the expensive targets ("proxy targets"). @@ -62,7 +66,7 @@ The "cache get" functionality was introduced in 16.9, while "cache add" was adde - Best: A real `BuildResult` from a previous build is provided. This can either be done by serializing the `HandleProjectFinishedAsync`, or when the plugin's infrastructure (e.g. CloudBuild or AnyBuild builder nodes) runs and caches the build, it can tell MSBuild to serialize the BuildResult to a file via [BuildParameters.OutputResultsCacheFile](/src/Build/BackEnd/BuildManager/BuildParameters.cs#L767) or the `/outputResultsCache` command line argument. Then, on cache hits, the plugins deserialize the `BuildResult` and send it back to MSBuild. This is the most correct option, as it requires neither guessing nor proxy targets. Whatever a previous build did, that's exactly what's returned. - Potential Issue: serialization format may change between writing and reading the `BuildResult`, especially if binary serialization is used. -## Cache add scenario +### Cache add scenario - Upon a cache miss, MSBuild will generally handle a request as normal, ie by building it. - MSBuild uses [Detours](https://github.com/microsoft/Detours) to observe file accesses of the worker nodes. To facilitate the plugin being able to handle future builds, it forwards this information as well as the build result to the plugin for it to use as desired, for example to add to a cache. @@ -72,9 +76,10 @@ The "cache get" functionality was introduced in 16.9, while "cache add" was adde - Due to the experimental nature of the feature, `/ReportFileAccesses` is only available with MSBuild.exe (ie. the Visual Studio install; not `dotnet`), only for the x64 flavor (not x86 or arm64), and only from the command-line. The Visual Studio IDE does not set `BuildParameters.ReportFileAccesses`. - As described above, it is recommended to serialize the `BuildResult` from `HandleProjectFinishedAsync` for later replay. -# APIs and calling patterns +## APIs and calling patterns + +### Plugin API -## Plugin API [ProjectCachePluginBase](/src/Build/BackEnd/Components/ProjectCache/ProjectCachePluginBase.cs) is an abstract class which plugin implementors will subclass. See the [Plugin implementation guidance and simple example design](#plugin-implementation-guidance-and-simple-example-design) section for guidance for plugin implementations. @@ -97,14 +102,14 @@ This can then be accessed by the plugin in `BeginBuildAsync` as a dictionary via Note: As it is likely that plugins will be distributed through NuGet packages and those packages would define the `ProjectCachePlugin` item in a props or targets file in the package, it's recommended for plugin authors to have settings backed by MSBuild properties as in the example above. This allows the user to easily configure a plugin simply by setting the properties and including the `PackageReference`. -## Enabling from command line +### Enabling from command line - Requires `/graph` to light up cache get scenarios. - Requires `/reportfileaccesses` to light up cache add scenarios. - The static graph has all the project instances in the same process, making it easy to find and keep plugin instances in one process. - MSBuild constructs the static graph and build bottom up, so by the time a project is considered, all of its references and their build results are already present in the Scheduler. -## Enabling from Visual Studio, a temporary workaround +### Enabling from Visual Studio, a temporary workaround - Ideally, Visual Studio would provide a `ProjectGraph` instance. Until that happens, a workaround is needed. - The workaround logic activates only when MSBuild detects that it's running under VS. @@ -113,29 +118,30 @@ Note: As it is likely that plugins will be distributed through NuGet packages an - Plugins will be given the graph entry points instead of the entire graph in this scenario. - There is currently no way to enable cache add scenarios in Visual Studio. -# Detours (cache add scenario) +## Detours (cache add scenario) In order for MSBuild to observe the file accesses as part of the build, it uses Detours on the worker nodes. In this way the Scheduler node will emit events for all file accesses done by the worker nodes. As the Scheduler knows what build request a worker node is working on at any given moment, it is able to properly associate the file access with a build request and dispatch these augmented events to plugins via the plugins' `HandleFileAccess` and `HandleProcess` implementations. Note that the Scheduler node cannot use Detours on itself, so the in-proc node is disabled when repoting file accesses. Additionally task yielding is disabled since it would leave to improperly associated file accesses. -## Pipe synchronization +### Pipe synchronization Because the Detours implementation being used communicates over a pipe, and nodes communicate over a pipe as well, and pipes are async, there is some coordination required to ensure that file accesses are associated with the proper build request. For example, if a "project finished" signal comes through the node communication pipe, but the detours pipe still has a queue of file accesses which have not been processed yet, those file accesses might be processed after the worker node has moved onto some other project. To address this problem, when a worker node finishes a project it will emit a dummy file access with a specific format known to MSBuild. When the scheduler node receives as "project finished" event over the node communication pipe, it will wait to determine that the project is actually finished until it also receives the dummy file access. This ensures that the all file accesses associated with the project have fully flushed from the pipe before the scheduler determines the project is finished and schedules new work to the worker node (which would trigger new file accesses). -# Plugin implementation guidance and simple example design +## Plugin implementation guidance and simple example design The following will describe a very basic (and not very correct) plugin implementation. In practice, plugins will have to choose the specific level of correctness they're willing to trade off for the ability to get cache hits. Any machine state *could* impact build results, and the plugin implementation will need to determine what state matters and what doesn't. An obvious example to consider would be the content of the project file. An example which has trade-offs would be the processes' environment variables. Even the current time could possibly impact the build ("if Tuesday copy this file"), but if considered caching would be quite infeasible. -## Fingerprinting +### Fingerprinting A "fingerprint" describes each unique input which went into the building a build request. The more granular the fingerprint, the more "correct" the caching is, as described above. In this example, we will only consider the following as inputs, and thus part of the fingerprint: + - The global properties of the build request (eg `Configuration=Debug`, `Platform=AnyCPU`) - The content hash of the project file - The content hash of files defined in specific items we know contribute to the build, like `` and `` @@ -147,13 +153,13 @@ It can make sense for a fingerprint to be a hash of its inputs, so effectively i At the beginning of the build, the plugin's `BeginBuildAsync` method will be called. As part of the `CacheContext`, the plugin is either given the graph or the entry points to the graph for which it can create a graph from. The plugin can use this graph to do some initial processing, like predicting various inputs which a project is likely to use. This information can then be stored to help construct a fingerprint for a build request later. -## Cache storage +### Cache storage Any storage mechanism can be used as a cache implementation, for example [Azure Blob Storage](https://azure.microsoft.com/products/storage/blobs/), or even just the local filesystem. At least in this example the only real requirement is that it can be used effectively as a key-value store. In many cases it can be useful for content to be keyed by its hash, and for the metadata file to be keyed by the fingerprint. In particular when content is keyed by hash, it is effectively deduplicated across multiple copies of the same file, which is common in builds. For illustration purposes, consider our cache implementation is based on a simple filesystem with a separate metadata and content directory inside it. Under the metadata dir, each file is a metadata file where the filename matches the fingerprint it's describing. Under the content dir, each file is a content file where the filename matches the hash of the content itself. -## First build (cache population) +### First build (cache population) In the very first build there will be no cache hits so the "cache add" scenario will be most relevant here. @@ -168,22 +174,24 @@ In our example, we can use the read files to construct a fingerprint for the bui The plugin would then create some metadata describing the outputs (eg. the paths and hashes) and the serialized `BuildResult`, and associate it with the fingerprint and put that assocation in the cache. To illustrate this, consider a project with fingerprint `F` which wrote a single file `O` with hash `H` and had `BuildResult R`. The plugin could create a metadata file `M` which describes the outputs of the build (the path and hash of `O`) as well as the serialized `R`. Using the cache implementation described above, the plugin would write the following two files to the cache: - - `metadata/F -> M:"{outputs: [{path: 'path/to/O', hash: H}], result: R}"` - - `content/H -> O` + +- `metadata/F -> M:"{outputs: [{path: 'path/to/O', hash: H}], result: R}"` +- `content/H -> O` This can then be used for future builds. - ## Second Build (cache hits) - - In the second build we have a populated cache and so it could be possible to get cache hits. +### Second Build (cache hits) - For a given project, `GetCacheResultAsync` will be invoked. The plugin can fingerprint the request and use that fingerprint to look up in its cache. If the cache entry exists, it can declare a cache hit. +In the second build we have a populated cache and so it could be possible to get cache hits. + +For a given project, `GetCacheResultAsync` will be invoked. The plugin can fingerprint the request and use that fingerprint to look up in its cache. If the cache entry exists, it can declare a cache hit. In the example above, if all inputs are the same as in the first build, we should end up with a fingerprint `F`. We look up in the metadata part of the cache (file `metadata/F`) and find that it exists. This means we have a cache hit. We can fetch that metadata `M` from the cache and find that it describes the output with path `O` and hash `H`. The plugin would then copy `content/H` to `O` and return the deserialized `BuildResult R` contained in `M` to MSBuild. If the inputs were not the same as in the first build, for example if a `Compile` item (a .cs file) changed, the fingerprint would be something else besides `F` and so would not have corresponding cache entries for it, indicating a cache miss. This will then go through the "cache add" scenario described above to populate the cache with the new fingerprint. -# Caveats +## Caveats + - Without the "cache add" scenario enabled, the content which powers "cache get" must be populated by some external entity, for example some higher-order build engine. - Absolute paths circulating through the saved build results - Absolute paths will likely break the build, since they'd be captured on the machine that writes to the cache. @@ -193,6 +201,7 @@ If the inputs were not the same as in the first build, for example if a `Compile - Msbuild /graph requires that the [target inference protocol](static-graph.md#inferring-which-targets-to-run-for-a-project-within-the-graph) is good enough. - Small repos will probably be slower with plugin implementations that access the network. Remote distribution and caching will only be worth it for repos that are large enough. -# Potential future work of dubious value +## Potential future work of dubious value + - Enable plugins to work with the just-in-time top down msbuild traversal that msbuild natively does when it's not using `/graph`. - Extend the project cache API to allow skipping individual targets or tasks instead of entire projects. This would allow for smaller specialized plugins, like plugins that only know to distribute, cache, and skip CSC.exe calls. diff --git a/documentation/specs/question.md b/documentation/specs/question.md index 84fac8ed9f3..f46aa910af8 100644 --- a/documentation/specs/question.md +++ b/documentation/specs/question.md @@ -8,15 +8,19 @@ Question switch ask if the next build is up-to-date. It will start a build, but [Fast Up-To-Date Check](https://github.com/dotnet/project-system/blob/cd275918ef9f181f6efab96715a91db7aabec832/docs/up-to-date-check.md) is a system that is implemented by the Project System, that decides, if it needs to run MSBuild. MSBuild takes a non-trival amount of time to load, evaluate, and run through each target and task. Fast Up-To-Date is faster, but can be less accurate, suitable for an IDE and a human interface. It is not accurate enough for a CI. ## Usage + Question mode is designed to be used on the command line. Run your normal build, then run again with /question. -``` + +```cmd msbuild /p:Configuration=Debug Project1.csproj /bl:build.binlog msbuild /p:Configuration=Debug Project1.csproj /bl:incremental.binlog /question ``` + If there are no errors, then your build is up-to-date. If there are errors, then investigate the error. See common errors below. Keep both logs to help with your investigation. ## Custom Tasks + Task author can implement the optional `IIncrementalTask` interface that will expose `FailIfNotIncremental`. `FailIfNotIncremental` is true when /question switch is used. The custom task will need to decide how it want to handle their behavior. For example. If there is already a message describing why the task cannot be skipped, then simply convert the message to a error. Remember to return false to stop the build. For the best reproducibility, do not modify any files on disk. ```C# @@ -32,6 +36,7 @@ else ``` ## Shipping Tasks + When question switch is used, it will modify the shipping task with these behavior. Note: this is still experimental and can change. `Exec` @@ -73,11 +78,11 @@ Error when SkipUnchangedFiles is true. `ZipDirectory` Error if the destination zip file doesn't exists. - ## Common Error + - **Typographical error**. Spelling, casing, or incorrect path. Check if the target inputs and outputs real files. - Inputs and Outputs are sometimes used for Cross Product. Try to move all to Outputs. If not possible, use Returns instead of Inputs. - **Double Checks**. Since target and task could be incremental, if both are implemented, then it can lead task skipping but not the task. For example, a Target has inputs A and outputs B. If A is newer, than B, then the target will start. If the task compares the content of A and B and deems nothing has changed, then B is not updated. If such case, this leads to target rerunning. - **Exec Task** are not Skipable, thus they should be wrapped with Target Inputs and Outputs or other systems. For backwards compatibility, Question will not issue an error. - **FileWritten**. The common clean system will remove files that aren't in the FileWritten itemgroup. Sometimes task output won't be add to FileWritten itemgroup. -- **Build, then Build**. Sometimes, a 2nd build will break up to date. Question after the 2nd build. \ No newline at end of file +- **Build, then Build**. Sometimes, a 2nd build will break up to date. Question after the 2nd build. diff --git a/documentation/specs/rar-core-scenarios.md b/documentation/specs/rar-core-scenarios.md index 3fb19ad7846..48af2458d6d 100644 --- a/documentation/specs/rar-core-scenarios.md +++ b/documentation/specs/rar-core-scenarios.md @@ -139,6 +139,7 @@ effect. Be it eliminating allocations, simplifying tight loops, reordering cases address the elephant in the room: the file I/O resulting from scanning of assemblies, checking their timestamps, and reading/writing on-disk caches. For regular project references the system works as about as efficient as possible. + - In a cold scenario, where there is no state in memory or on disk, the referenced assembly file has to be scanned for its name and dependencies. - In a warm scenario, where there is no state in memory but a disk cache exists, the assembly name and dependencies are read from the cache, together with the corresponding timestamp which is compared to the current timestamp of the assembly file. If they match the cached data is used. @@ -188,6 +189,7 @@ enlistment - the system may prime by building the full solution and then the dev cache and get sub-optimal first-time build performance. Saving of the per-project disk cache may be further optimized by + - Keeping the timestamp of the cache file in memory and skipping the save if the relevant cache items haven't become dirty (i.e. the dependencies have not changed) *and* the timestamp of the cache file hasn't changed since the last save. In hot inner loop scenarios this would reduce the save to a timestamp check. - Saving the file asynchronously, i.e. not blocking the build on completing the save operation. diff --git a/documentation/specs/remote-host-object.md b/documentation/specs/remote-host-object.md index 536535637d2..a64a8836ab4 100644 --- a/documentation/specs/remote-host-object.md +++ b/documentation/specs/remote-host-object.md @@ -4,8 +4,9 @@ A remote host object must be registered in the [Running Object Table (ROT)](http [The registration of interfaces](https://docs.microsoft.com/en-us/dotnet/framework/interop/how-to-register-primary-interop-assemblies) is the only thing interop with COM that need extra care. There are 3 interfaces involved in out-of-proc tasks work: `IVsMSBuildTaskFileManager`, `IPersistFileCheckSum` and `ITaskHost`. `IVsMSBuildTaskFileManager` and `IPersistFileCheckSum` are registered globally in Windows registry by VS existing setup. `ITaskHost` is also configured in VS using registration-free. So the only work is to configure it using registration-free in **MSBuild**. That results the change in msbuild.exe.manifest file and the change to generate tlb file for ITaskHost. -## Annotated additions to the msbuild.exe.manifest file. -``` +## Annotated additions to the msbuild.exe.manifest file + +```xml -- Location of the tlb, it should be in the same directory as msbuild.exe MySdkResolver.dll MySdk.* @@ -23,11 +29,12 @@ By default the resolvers are general. To make all the resolvers from some dll sp Note, that the manifest file, if exists, from ChangeWave 17.4 would have preference over the dll. The sdk discovery works according to the following algorithm: -- First try locate the manifest file and use it. -- If it is not found, we try to locate the dll in the resolver's folder. + +- First try locate the manifest file and use it. +- If it is not found, we try to locate the dll in the resolver's folder. Both xml and dll name should match the following name pattern `...\SdkResolvers\(ResolverName)\(ResolverName).(xml/dll)`. -### Failed SDK Resolution +## Failed SDK Resolution > 🚧 Note > @@ -35,15 +42,15 @@ Both xml and dll name should match the following name pattern `...\SdkResolvers\ SDK resolvers previously attempted to continue when one critically fails (throws an unhandled exception). This lead to misleading error messages such as: -``` +```text warning MSB4242: The SDK resolver "Microsoft.DotNet.MSBuildWorkloadSdkResolver" failed to run. 's' is an invalid start of a property name. Expected a '"'. LineNumber: 14 | BytePositionInLine: 8. error MSB4236: The SDK 'Microsoft.NET.SDK.WorkloadAutoImportPropsLocator' specified could not be found. [C:\foo\bar.csproj] ``` `MSB4236` is a red herring while `MSB4242` is the real error despite being logged as a warning. Because of this, SDK resolvers now fail the build _immediately_ upon unhandled exceptions. These exceptions are propogated as `SdkResolverException`s, and `MSB4242` has been promoted to an error code. The new error message appears like so: -``` -C:\src\temp\8-18>"C:\foo\dotnet-sdk-6.0.100-preview.7.21379.14-win-x64\dotnet.exe" build +```text +C:\src\temp\8-18>"C:\foo\dotnet-sdk-6.0.100-preview.7.21379.14-win-x64\dotnet.exe" build Microsoft (R) Build Engine version 17.0.0-dev-21420-01+5df152759 for .NET Copyright (C) Microsoft Corporation. All rights reserved. @@ -56,4 +63,4 @@ C:\foo\bar.csproj : error MSB4242: SDK Resolver Failure: "The SDK resolver "Micr 1 Error(s) Time Elapsed 00:00:00.15 -``` \ No newline at end of file +``` diff --git a/documentation/specs/single-project-isolated-builds.md b/documentation/specs/single-project-isolated-builds.md index 75b15fc5b82..8a8f9e68413 100644 --- a/documentation/specs/single-project-isolated-builds.md +++ b/documentation/specs/single-project-isolated-builds.md @@ -19,25 +19,27 @@ In a build, the input and output cache files have the same lifetime as the `Conf When loading input cache files, MSBuild merges incoming instances of `ConfigCache`s and `ResultsCache`s into one instance of each with the help of the [`CacheAggregator`](https://github.com/dotnet/msbuild/blob/51df47643a8ee2715ac67fab8d652b25be070cd2/src/Build/BackEnd/BuildManager/CacheAggregator.cs#L15), which enforces the following constraints: + - No duplicate cache entries - Bijection: - - `ConfigCache.Entries.Size == ResultsCache.Entries.Size` - - `BuildResult.ConfigurationId` == `BuildRequestConfiguration.ConfigurationId` + - `ConfigCache.Entries.Size == ResultsCache.Entries.Size` + - `BuildResult.ConfigurationId == BuildRequestConfiguration.ConfigurationId` Note that the output cache file contains a single `BuildResult` with the `TargetResult`s from the project specified to be built in the `BeginBuild` / `EndBuild` session, as any `BuildResult`s obtained through isolation exemption are excluded to prevent potential duplicate input cache entries; Entries from input caches are not transferred to the output cache. -Input cache entries are separated from output cache entries with the composite caches [`ConfigCacheWithOverride`](https://github.com/dotnet/msbuild/blob/main/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs) and [`ResultsCacheWithOverride`](https://github.com/dotnet/msbuild/blob/main/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs). Each composite cache contains two underlying caches: a cache where input caches files are loaded into (the override cache), and a cache where new results are written into (the current cache).* In the `ConfigCacheWithOverride`, these caches are instances of `ConfigCache`s and, in the `ResultsCacheWithOverride`, these caches are instances of `ResultsCache`s. A query for a cache entry is first attempted from the override cache and, if unsatisfied, a second attempt is made from the current cache. Writes are only written to the current cache, never into the override cache.* It is illegal for both the current cache and override cache to contain entries for the same project configuration, a constraint that is checked by the two override caches on each cache query. +Input cache entries are separated from output cache entries with the composite caches [`ConfigCacheWithOverride`](https://github.com/dotnet/msbuild/blob/main/src/Build/BackEnd/Components/Caching/ConfigCacheWithOverride.cs) and [`ResultsCacheWithOverride`](https://github.com/dotnet/msbuild/blob/main/src/Build/BackEnd/Components/Caching/ResultsCacheWithOverride.cs). Each composite cache contains two underlying caches: a cache where input caches files are loaded into (the override cache), and a cache where new results are written into (the current cache). *In the `ConfigCacheWithOverride`, these caches are instances of `ConfigCache`s and, in the `ResultsCacheWithOverride`, these caches are instances of `ResultsCache`s. A query for a cache entry is first attempted from the override cache and, if unsatisfied, a second attempt is made from the current cache. Writes are only written to the current cache, never into the override cache.* It is illegal for both the current cache and override cache to contain entries for the same project configuration, a constraint that is checked by the two override caches on each cache query. ## Isolation Implementation [Isolation constraints](static-graph.md##single-project-isolated-builds) are implemented in the `Scheduler` and `TaskBuilder`. [`TaskBuilder.ExecuteInstantiatedTask`](https://github.com/dotnet/msbuild/blob/37c5a9fec416b403212a63f95f15b03dbd5e8b5d/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs#L743) ensures that the `MSBuild` task is only called on projects declared in a `ProjectReference` item. [`Scheduler.CheckIfCacheMissOnReferencedProjectIsAllowedAndErrorIfNot`](https://github.com/dotnet/msbuild/blob/37c5a9fec416b403212a63f95f15b03dbd5e8b5d/src/Build/BackEnd/Components/Scheduler/Scheduler.cs#L1818) ensures that all `MSBuild` tasks are cache hits. ### Isolation Exemption + The `Scheduler` [skips isolation constraints](static-graph.md#exempting-references-from-isolation-constraints) on project references via the: -* `GraphIsolationExemptReference` item. The `RequestBuilder` sets the `SkipStaticGraphIsolationConstraints` property of a `BuildRequest` to `true` if the `RequestBuilder` matches it against a `GraphIsolationExemptReference` item defined in the calling project. Additionally, the `RequestBuilder` marks the `BuildRequest`'s corresponding `BuildRequestConfiguration` as exempt to allow the `TaskBuilder` to verify exemption from isolation constraints. +- `GraphIsolationExemptReference` item. The `RequestBuilder` sets the `SkipStaticGraphIsolationConstraints` property of a `BuildRequest` to `true` if the `RequestBuilder` matches it against a `GraphIsolationExemptReference` item defined in the calling project. Additionally, the `RequestBuilder` marks the `BuildRequest`'s corresponding `BuildRequestConfiguration` as exempt to allow the `TaskBuilder` to verify exemption from isolation constraints. -* `isolate:MessageUponIsolationViolation` switch. The `RequestBuilder` sets the `SkipStaticGraphIsolationConstraints` property of _every_ `BuildRequest` to `true`. The `TaskBuilder` verifies exemption from isolation constraints just by the switch value. +- `isolate:MessageUponIsolationViolation` switch. The `RequestBuilder` sets the `SkipStaticGraphIsolationConstraints` property of _every_ `BuildRequest` to `true`. The `TaskBuilder` verifies exemption from isolation constraints just by the switch value. -\* Except in the following scenario when a `ProjectReference` is exempted from isolation constraints: a dependency project A outputs a cache file F containing a `BuildResult` with `TargetResult`s Tcached for targets t1, t2, ..., tm and a dependent project B uses F as an input cache file but builds and obtains the `TargetResult`s Tnew for targets tm + 1, tm + 2, ..., tn such that 0 < m < n. In this case, Tnew will be placed into the `ResultsCache` containing Tcached to enforce no overlap between the override and current caches in the `ConfigCacheWithOverride`. \ No newline at end of file +\* Except in the following scenario when a `ProjectReference` is exempted from isolation constraints: a dependency project A outputs a cache file F containing a `BuildResult` with `TargetResult`s Tcached for targets t1, t2, ..., tm and a dependent project B uses F as an input cache file but builds and obtains the `TargetResult`s Tnew for targets tm + 1, tm + 2, ..., tn such that 0 < m < n. In this case, Tnew will be placed into the `ResultsCache` containing Tcached to enforce no overlap between the override and current caches in the `ConfigCacheWithOverride`. diff --git a/documentation/specs/static-graph.md b/documentation/specs/static-graph.md index 49acebe57fe..6112072349a 100644 --- a/documentation/specs/static-graph.md +++ b/documentation/specs/static-graph.md @@ -85,6 +85,7 @@ Static graph functionality can be used in three ways: ## Project Graph ### Constructing the project graph + Calculating the project graph will be very similar to the MS internal build engine's existing Traversal logic. For a given evaluated project, all project references will be identified and recursively evaluated (with deduping). Project references are identified via the `ProjectReference` item. @@ -112,14 +113,16 @@ Multitargeting refers to projects that specify multiple build dimensions applica Multitargeting is implemented by having a project reference itself multiple times, once for each combination of multitargeting global properties. This leads to multiple evaluations of the same project, with different global properties. These evaluations can be classified in two groups -1. Multiple inner builds. Each inner build is evaluated with one set of multitargeting global properties (e.g. the `TargetFramework=net472` inner build, or the `TargetFramework=netcoreapp2.2` inner build). -2. One outer build. This evaluation does not have any multitargeting global properties set. It can be viewed as a proxy for the inner builds. Other projects query the outer build in order to learn the set of valid multitargeting global properties (the set of valid inner builds). When the outer build is also the root of the project to project graph, the outer build multicasts the entry target (i.e. `Build`, `Clean`, etc) to all inner builds. + +1. Multiple inner builds. Each inner build is evaluated with one set of multitargeting global properties (e.g. the `TargetFramework=net472` inner build, or the `TargetFramework=netcoreapp2.2` inner build). +2. One outer build. This evaluation does not have any multitargeting global properties set. It can be viewed as a proxy for the inner builds. Other projects query the outer build in order to learn the set of valid multitargeting global properties (the set of valid inner builds). When the outer build is also the root of the project to project graph, the outer build multicasts the entry target (i.e. `Build`, `Clean`, etc) to all inner builds. In order for the graph to represent inner and outer builds as nodes, it imposes a contract on what multitargeting means, and requires the multitargeting supporting SDKs to implement this contract. Multitargeting supporting SDKs MUST implement the following properties and semantics: + - `InnerBuildProperty`. It contains the property name that defines the multitargeting build dimension. - `InnerBuildPropertyValues`. It contains the property name that holds the possible values for the `InnerBuildProperty`. - Project classification: @@ -136,6 +139,7 @@ These specific rules represent the minimal rules required to represent multitarg For example, `InnerBuildProperty` could become `InnerBuildProperties` for SDKs where there's multiple multitargeting global properties. For example, here is a trimmed down `Microsoft.Net.Sdk` multitargeting project: + ```xml @@ -152,14 +156,17 @@ For example, here is a trimmed down `Microsoft.Net.Sdk` multitargeting project: ``` To summarize, there are two main patterns for specifying build dimensions: + 1. Multitargeting based. A multitargeting project self describes supported build dimensions. In this case the SDK needs to specify the multitargeting build dimensions. The graph then extracts innerbuilds from a given outer build. For example, the `TargetFramework` build dimension gets specified this way. 2. Global Property based: A top level set of global properties get applied to the graph entrypoints and get propagated downward through the graph. For example, the `Configuration` and `Platform` build dimensions get specified this way. Why does an outerbuild need to generate speculative edges to all of its innerbuilds? Why can't it use nuget to prune the speculative edges down to the compatible set? + - One big design constraint we imposed on static graph was to keep it agnostic of SDK implementation details. So the graph must not know about particular details of one language's SDK. We wanted a generic design that all language SDKs can leverage. We considered that calling nuget to get the compatible TargetFramework values breaks this rule, as both the concept of "nuget" and the concept of "TargetFramework" are implementation details of the .net SDK. If someone were to write a Java SDK, would "calling nuget to get the compatible TargetFramework" still be relevant? A solution to this is to allow SDKs to configure the graph with an extension point on "how to collapse multiple speculative innerbuild edges into a smaller compatible set", but we didn't have the time to design it yet. - There is a conflicting need between build everything or just building a "TF slice" through the graph. Outer loop builds (CI builds) that publish binaries need to build all the packages for all the supported TFs, so they need the graph to express all possible combinations. Inner loop builds (dev-at-work) can be sliced down to only the TF that the dev is working on in order to reduce build times. Again, we didn't have time to design how to express these two things so we went with "express everything" because that allows both scenarios to work. ### Executing targets on a graph + When building a graph, project references should be built before the projects that reference them, as opposed to the existing msbuild scheduler which builds projects just in time. For example if project A depends on project B, then project B should build first, then project A. Existing msbuild scheduling would start building project A, reach an MSBuild task for project B, yield project A, build project B, then resume project A once unblocked. @@ -169,6 +176,7 @@ Building in this way should make better use of parallelism as all CPU cores can Note that graph cycles are disallowed, even if they're using disconnected targets. This is a breaking change, as today you can have two projects where each project depends on a target from the other project, but that target doesn't depend on the default target or anything in its target graph. #### Command line + `msbuild /graph` - msbuild will create a static graph from the entry point project and build it in topological order with the specified targets. Targets to call on each node are inferred via the rules in [this section](#inferring-which-targets-to-run-for-a-project-within-the-graph). #### APIs @@ -176,11 +184,12 @@ Note that graph cycles are disallowed, even if they're using disconnected target [BuildManager.PendBuildRequest(GraphBuildRequestData requestData)](https://github.com/dotnet/msbuild/blob/37c5a9fec416b403212a63f95f15b03dbd5e8b5d/src/Build/BackEnd/BuildManager/BuildManager.cs#L676) ### Inferring which targets to run for a project within the graph + In the classic MSBuild build (i.e. execution of targets), the referencing project chooses which targets to call on the referenced projects and may call into a project multiple times with different target lists and global properties (examples in [project reference protocol](../ProjectReference-Protocol.md)). This is a top-down traversal of dependencies. These calls are made via the [MSBuild task](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-task?view=vs-2019). When building a graph, projects are built before the projects that reference them. This is a bottom-up traversal. Therefore the graph needs to determine the list of targets to execute on a specific project `B` **before** building the referencing projects that reference `B`. The static graph contains the structural information on which reference projects a referencing project depends on. But it does not contain information on what "depends" means. At build time "depends" means that a referencing evaluated project will call a subset of reference evaluations with some targets. Subset because the static graph is an inferred graph, therefore there are ambiguities during graph construction, and thus it needs to be conservative and represent a superset of the "runtime graph". The "runtime graph" is the actual graph that gets executed during a real build. We cannot know the runtime graph because that would require us to analyze msbuild xml code inside of targets in order to find the `MSBuild task` invocations. This means doing heavy program analysis, like symbolic execution. That would make things very complicated, slower, and would probably introduce even more ambiguity, so a larger superset conservative graph. So we kept it simple and only looked at evaluation time msbuild xml code (i.e. msbuild xml code outside of `` elements). To summarize, the static graph does not have insights into the `MSBuild task` callsites. It does not know callsite specific information such as the `Targets="Foo;Bar"` or `Properties="Foo=Bar"` `MSBuild task` attributes. -Since the graph does not have access to MSBuild task callsites, it does not know what targets will get called for a given graph edge. +Since the graph does not have access to MSBuild task callsites, it does not know what targets will get called for a given graph edge. To infer target information we use a flow analysis to propagate target information down the graph. The flow analysis uses the `ProjectReferenceTargets` protocol (described further down) to infer how one incoming target on a graph node (e.g. `Build`) generates multiple outgoing targets to its referenced nodes (e.g. `GetTargetFrameworks`, `GetNativeManifest`, `Build`). SDKs **must** explicitly describe the project-to-project calling patterns via the `ProjectReferenceTargets` protocol in such a way that a graph based build can correctly infer the entry targets for a graph node. @@ -189,7 +198,7 @@ Each project needs to specify the project reference protocol targets it supports For example, a simple recursive rule would be `A -> A`, which says that a project called with target `A` will call target `A` on its referenced projects. Here's an example execution with two nodes: -``` +```text Execute target A+-->Proj1 A->A + | @@ -202,11 +211,12 @@ Execute target A+-->Proj1 A->A Proj1 depends on Proj2, and we want to build the graph with target `A`. Proj1 gets inspected for the project reference protocol for target `A` (represented to the right of Proj1). The protocol says the referenced projects will be called with `A`. Therefore Proj2 gets called with target `A`. After Proj2 builds, Proj1 then also builds with `A` because Proj1 is an entry point and `A` is what was requested by the user. A project reference protocol may contain multiple targets, for example `A -> B, A`. This means that building `A` on the referencing project will lead to `B` and `A` getting called on the referenced projects. If all nodes in the graph repeat the same rule, then the rule is repeated recursively on all nodes. However, a project can choose to implement the protocol differently. In the following example, the entry targets are: + - Proj4 is called with targets `B, A, C, D`. On multiple references, the incoming targets get concatenated. The order of these target lists does not matter, as MSBuild has non-deterministic p2p ordering, however the order within the target lists does. IE. `B, A, C, D` and `C, D, B, A` are valid, while `A, B, C, D` is not. - Proj3 and Proj2 get called with `B, A`, as specified by the rule in Proj1. - Proj1 builds with `A`, because it's the root of the graph. -``` +```text A+-->Proj1 A->B, A / \ B, A / \ B, A @@ -238,6 +248,7 @@ Here are the rules for the common protocols: `Rebuild` actually calls `Clean` and `Build`, which in turn uses the concatenation of the `Clean` and `Build` mappings. `GetTargetFrameworks` is repeated so only the first call to it remains in the final target list. Restore is a composition of two rules: + - `Restore -> _IsProjectRestoreSupported, _GenerateRestoreProjectPathWalk, _GenerateRestoreGraphProjectEntry` - `_GenerateRestoreProjectPathWalk -> _IsProjectRestoreSupported, _GenerateRestoreProjectPathWalk, _GenerateRestoreGraphProjectEntry` @@ -258,22 +269,24 @@ We'll represent the project reference protocols as `ProjectReferenceTargets` ite #### Multitargeting details A multitargeting project can get called with different targets for the outer build and the inner builds. In this case, the `ProjectReferenceTargets` items containing targets for the outer build are marked with the `OuterBuild=true` metadata. Here are the rules for how targets from `ProjectReferenceTargets` get assigned to different project types: - - *Outer build*: targets with `OuterBuild=true` metadata - - *Dependent inner build*: targets without `OuterBuild=true` metadata - - *Standalone inner build*: the same as non multitargeting builds. - - *Non multitargeting build*: concatenation of targets with `OuterBuild=true` metadata and targets without `OuterBuild=true` metadata + +- *Outer build*: targets with `OuterBuild=true` metadata +- *Dependent inner build*: targets without `OuterBuild=true` metadata +- *Standalone inner build*: the same as non multitargeting builds. +- *Non multitargeting build*: concatenation of targets with `OuterBuild=true` metadata and targets without `OuterBuild=true` metadata **OPEN ISSUE:** Current implementation does not disambiguate between the two types of inner builds, leading to overbuilding certain targets by conservatively treating both inner build types as standalone inner builds. For example, consider the graph of `A (non multitargeting) -> B (multitargeting with 2 innerbuilds) -> C (standalone inner build)`, with the following target propagation rules: -``` + +```text A -> Ao when OuterBuild=true A -> Ai, A ``` According to the graph construction rules defined in the [multitargeting section](#multitargeting), we get the following graph, annotated with the target propagation for target `A`. -``` +```text A+-->ProjA / | \ / | \ @@ -294,6 +307,7 @@ According to the graph construction rules defined in the [multitargeting section ``` ### Underspecified graphs + The intention is that the project graph and the target lists for each node be exactly correct, however MSBuild is quite flexible and particular projects or project types may not adequately describe these for the project graph. If a project calls into another project which either isn't represented in the graph or with a target list which isn't represented by the graph, it will fall back to classical MSBuild behavior and execute that target on the project reference just-in-time. This has the consequence of still requiring all project state be kept in memory in case any arbitrary project wants to execute targets on any other arbitrary project. @@ -301,6 +315,7 @@ If a project calls into another project which either isn't represented in the gr To enable further optimizations (and strictness), graph builds can run [isolated](#isolated-builds) which enforces that the graph be entirely accurate. ### Public API + This is a proposal for what the public API for ProjectGraph may look like: ```csharp @@ -360,6 +375,7 @@ namespace Microsoft.Build.Experimental.Graph ``` ## Isolated builds + Building a project in isolation means enforcing the constraint that whenever a graph node is built, all the target calls that it does on its references **do not execute** because their results are already available. This means that any `BuildResult` objects for project references must be precomputed and somehow provided as inputs to the referencing project. If a project uses the MSBuild task, the build result must be in MSBuild's build result cache instead of just-in-time executing targets on that referenced project. If it is not in the build result cache, an error will be logged and the build will fail. If the project is calling into itself either via `CallTarget` or the MSBuild task with a different set of global properties, this will be allowed to support multitargeting and other build dimensions implemented in a similar way. @@ -367,6 +383,7 @@ If a project uses the MSBuild task, the build result must be in MSBuild's build Because referenced projects and their entry targets are guaranteed to be in the cache, they will not build again. Therefore we do not need to set `/p:BuildProjectReferences=false` or any other gesture that tells SDKs to not do recursive operations. ### Isolated graph builds + When building a graph in isolated mode, the graph is used to traverse and build the projects in the right order, but each individual project is built in isolation. The build result cache will just be in memory exactly as it is today, but on cache miss it will error. This enforces that both the graph and target mappings are complete and correct. Furthermore, running in this mode enforces that each `(project, global properties)` pair is executed only once and must execute all targets needed by all projects which reference that node. This gives it a concrete start and end time, which leads to some potential perf optimizations, like garbage collecting all project state (except the build results) once it finishes building. This can greatly reduce the memory overhead for large builds. @@ -374,9 +391,11 @@ Furthermore, running in this mode enforces that each `(project, global propertie This discrete start and end time also allows for easy integration with [I/O Tracking](#io-tracking) to observe all inputs and outputs for a project. Note however that I/O during target execution, particular target execution which may not normally happen as part of a project's individual build execution, would be attributed to the project reference project rather the project with the project reference. This differs from today's behavior, but seems like a desirable difference anyway. ### Single project isolated builds + When building a single project in isolation, all project references' build results must be provided to the project externally. Specifically, the results will need to be [deserialized](#deserialization) from files and loaded into the build result cache in memory. When MSBuild runs in isolation mode, it fails the build when it detects: + 1. `MSBuild` task calls which cannot be served from the cache. Cache misses are illegal. 2. `MSBuild` task calls to project files which were not defined in the `ProjectReference` item. @@ -389,16 +408,20 @@ These incremental builds could be extended to the entire graph by keeping a proj Details on how isolation and cache files are implemented in MSBuild can be found [here](./static-graph-implementation-details.md). #### APIs + Cache file information is provided via [`BuildParameters`](https://github.com/dotnet/msbuild/blob/2d4dc592a638b809944af10ad1e48e7169e40808/src/Build/BackEnd/BuildManager/BuildParameters.cs#L746-L764). Input caches are applied in `BuildManager.BeginBuild`. Output cache files are written in `BuildManager.EndBuild`. Thus, the scope of the caches are one `BuildManager` `BeginBuild`/`EndBuild` session. Isolation constraints are turned on via [`BuildParameters.IsolateProjects`](https://github.com/dotnet/msbuild/blob/b111470ae61eba02c6102374c2b7d62aebe45f5b/src/Build/BackEnd/BuildManager/BuildParameters.cs#L742). Isolation constraints are also automatically turned on if either input or output cache files are used, except when the `isolate:MessageUponIsolationViolation` switch is used. #### Command line + Caches are provided to MSBuild.exe via the multi value `/inputResultsCaches` and the single value `/outputResultsCache`. Isolation constraints are turned on via `/isolate` (they are also implicitly activated when either input or output caches are used). #### Exempting references from isolation constraints + In certain situations one may want to exempt a reference from isolation constraints. A few potential cases: + - debugging / onboarding to isolation constraints - exempting references whose project files are generated at build times with random names (for example, each WPF project, before the Build target, generates and builds a helper .csproj with a random file name) - relaxing constraints for MSBuild task calling patterns that static graph cannot express (for exemple, if a project is calculating references, or the targets to call on references, at runtime via an arbitrary algorithm) @@ -422,6 +445,7 @@ If multiple projects need to exempt the same reference, all of them need to add For now, self-builds (a project building itself with different global properties) are also exempt from isolation constraints, but this behaviour is of dubious value and might be changed in the future. ## I/O Tracking + To help facilitate caching of build outputs by a higher-order build engine, MSBuild needs to track all I/O that happens as part of a build. **OPEN ISSUE:** This isn't actually true in most scenarios. Today the MS internal build engine can wrap any arbitrary process to track the I/O that happens as part of its execution as well as its children. That's sufficient for all scenarios except compiler servers or an MSBuild server (see below). Additionally, if the MS internal build engine supports any other build type besides MSBuild (or older versions of MSBuild), it will still need to be able to detour the process itself anyway. @@ -429,6 +453,7 @@ To help facilitate caching of build outputs by a higher-order build engine, MSBu **NOTE**: Based on the complexity and challenges involved, the feature of I/O tracking in MSBuild is currently on hold and not scheduled to be implemented. This section intends to describe these challenges and be a dump of the current thinking on the subject. ### Detours + [Detours](https://github.com/microsoft/detours) will be used to intercept Windows API calls to track I/O. This is the same technology that [FileTracker](../../src/Utilities/TrackedDependencies/FileTracker.cs) and [FullTracking](../../src/Build/BackEnd/Components/RequestBuilder/FullTracking.cs) use as well as what the MS internal build engine ("BuildXL Tracker") uses to track I/O. Today FileTracker and FullTracking are currently a bit specific to generating tlogs, and do not collect all the I/O operations we would want to collect like directory enumerations and probes. Additionally, the BuildXL Tracker implementation does not currently have the ability to attach to the currently running process. @@ -438,11 +463,13 @@ Either existing implementation would require some work to fit this scenario. Bec Elsewhere in this spec the final Detours-based file tracking implementation will simply be referred to as "Tracker". ### Isolation requirement + I/O Tracking will only be available when running isolated builds, as the current implementation of project yielding in MSBuild makes it exceedingly difficult to attribute any observed I/O to the correct project. Isolated builds make this feasible since each MSBuild node will be building exactly one project configuration at any given moment and each project configuration has a concrete start and stop time. This allows us to turn on I/O tracking for the MSBuild process and start and stop tracking with the project start and stop. **OPEN ISSUE:** For graph-based isolated builds, project evaluation happens in parallel on the main node. Any I/O that happens as part of evaluation should be reported for that specific project, but there's no good way to do that here. ### Tool servers + Tool servers are long-lived processes which can be reused multiple times across builds. This causes problems for Tracker, as that long-lived process is not a child process of MSBuild, so many I/O operations would be missed. For example, when `SharedCompilation=true`, the Roslyn compiler (csc.exe) will launch in server mode. This causes the `Csc` task to connect to any existing csc.exe process and pass the compilation request over a named pipe. diff --git a/documentation/specs/task-isolation-and-dependencies.md b/documentation/specs/task-isolation-and-dependencies.md index 2ec96c8eb18..2994335dfc8 100644 --- a/documentation/specs/task-isolation-and-dependencies.md +++ b/documentation/specs/task-isolation-and-dependencies.md @@ -1,39 +1,50 @@ # Task isolation + ## Problem definition + Tasks in MSBuild are dynamically loaded assemblies with potentially separate and colliding dependency trees. Currently MSBuild on .NET Core has no isolation between tasks and as such only one version of any given assembly can be loaded. Prime example of this is Newtonsoft.Json which has multiple versions, but all the tasks must agree on it to work. This problem is also described in #1754. ## Solution + Use [`AssemblyLoadContext`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblyloadcontext?view=netcore-2.2) (ALC) to provide binding isolation for task assemblies. Each task assembly would be loaded into its own ALC instance. -* The ALC would resolve all dependencies of the task assemblies (see dependency resolution below) -* ALC would fallback to the Default for dependencies which the assembly doesn't carry with itself (frameworks and so on) -* ALC would probably have to forcefully fallback for MSBuild assemblies since it's possible that tasks will carry these, but the system requires for the MSBuild assemblies to be shared. + +- The ALC would resolve all dependencies of the task assemblies (see dependency resolution below) +- ALC would fallback to the Default for dependencies which the assembly doesn't carry with itself (frameworks and so on) +- ALC would probably have to forcefully fallback for MSBuild assemblies since it's possible that tasks will carry these, but the system requires for the MSBuild assemblies to be shared. We also want to load groups of tasks which belong together into the same ALC (for example based on their location on disk) to improve performance. This will need some care as there's no guarantee that two random tasks have compatible dependency trees. As implemented, each task assembly is loaded into its own ALC. ## Potential risks -* Has some small probability of causing breaks. Currently all assemblies from all tasks are loaded into the default context and thus are "visible" to everybody. Tasks with following properties might not work: - * Task has a dependency on an assembly, but it doesn't declare this dependency in its .deps.json and this dependency gets loaded through some other task. This is mostly fixable by implementing probing similar to today's behavior. - * Two tasks from different assemblies which somehow rely on sharing certain types. If the new system decides to load these in isolation they won't share types anymore and might not work. -* Performance - task isolation inherently (and by design) leads to loading certain assemblies multiple times. This increases memory pressure and causes additional JITing and other related work. + +- Has some small probability of causing breaks. Currently all assemblies from all tasks are loaded into the default context and thus are "visible" to everybody. Tasks with following properties might not work: + - Task has a dependency on an assembly, but it doesn't declare this dependency in its .deps.json and this dependency gets loaded through some other task. This is mostly fixable by implementing probing similar to today's behavior. + - Two tasks from different assemblies which somehow rely on sharing certain types. If the new system decides to load these in isolation they won't share types anymore and might not work. +- Performance - task isolation inherently (and by design) leads to loading certain assemblies multiple times. This increases memory pressure and causes additional JITing and other related work. ## Additional consideration -* None of these changes would have any effect on MSBuild on .NET Framework -* Task isolation alone could be achieved on existing MSBuild + +- None of these changes would have any effect on MSBuild on .NET Framework +- Task isolation alone could be achieved on existing MSBuild # Task dependency resolution + ## Problem definition + Tasks with complex and specifically platform specific dependencies don't work out of the box. For example if a task uses [`LibGit2Sharp`](https://www.nuget.org/packages/LibGit2Sharp) package it will not work as is. `LibGit2Sharp` has native dependencies which are platform specific. While the package carries all of them, there's no built in support for the task to load the right ones. For example [source link](https://github.com/dotnet/sourcelink/blob/29b3197e824c05d03427c05d56700e4c704233e4/src/Microsoft.Build.Tasks.Git/GitLoaderContext.cs) runs into this problem. ## Solution + .NET Core uses `.deps.json` files to describe dependencies of components. It would be natural to treat task assemblies as components and use associated .deps.json file to determine their dependencies. This would make the system work nicely end to end with the .NET Core CLI/SDK and VS integration. In .NET Core 3 there's a new type [`AssemblyDependencyResolver`](https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyDependencyResolver.cs) which implements parsing and processing of a `.deps.json` for a component (or assembly). The usage is to create an instance of the resolver pointing to the assembly (in MSBuild case the task assembly). The resolver parses the `.deps.json` and stores the information. It exposes two methods to resolve managed and native dependencies. It was designed to be used as the underlying piece to implement custom ALC. So it would work nicely with task isolation above. ## Potential risks -* Small probability of breaking tasks which have `.deps.json` with them and those are not correct. With this change the file would suddenly be used and could cause either load failures or different versions of assemblies to get loaded. + +- Small probability of breaking tasks which have `.deps.json` with them and those are not correct. With this change the file would suddenly be used and could cause either load failures or different versions of assemblies to get loaded. ## Additional consideration -* Task dependency resolution requires APIs which are only available in .NET Core 3.0 (no plan to backport), as such MSBuild will have to target netcoreapp3.0 to use these APIs. + +- Task dependency resolution requires APIs which are only available in .NET Core 3.0 (no plan to backport), as such MSBuild will have to target netcoreapp3.0 to use these APIs. We decided not to implement `AssemblyDependencyResolver` in the .NET Core 3.x timeframe because of the uncertain impact of the change. We should reconsider in the .NET 5 timeframe. diff --git a/documentation/specs/test-target.md b/documentation/specs/test-target.md index 7726f1f6971..e5818eb6c81 100644 --- a/documentation/specs/test-target.md +++ b/documentation/specs/test-target.md @@ -1,62 +1,77 @@ -## MSBuild Test Target and Task +# MSBuild Test Target and Task + See: [MSBuild Test Target](https://github.com/dotnet/msbuild/pull/9193) -### Motivation +## Motivation + The primary motivation of the MSBuild Test Target is to offer a convienent and standardardized way for executing tests within the msbuild environment. This is inspired by the simplicity of the `dotnet test` command. The proposed command for initiating test within MSBuild would be `msbuild /t:Test` Another significatnt benefit of integrating this target is to faciliatet the caching of test executions, using MSBuild project caching capabilities. This enhancement will optimize the testing process by reducing test runs which could significantly reduce time spent building and testing, as tests would only execute, (after the initial run) if there are changes to those tests. As an example running with [MSBuildCache](https://github.com/microsoft/MSBuildCache) we can cache both build and test executions. Functionally, this means skipping test executions that have been determined to have not changed. Example usage: `msbuild /graph /restore:false /m /nr:false /reportfileaccesses /t:"Build;Test"` -### Design Overview +## Design Overview + The 'Microsoft.Common.Test.targets' file contains a stub test target. -``` + +```xml ``` + This target serves a placeholder and entry point for test target implementations. -#### Conditional Import -* This stub target is conditionally imported, determined by a condition named +### Conditional Import + +- This stub target is conditionally imported, determined by a condition named `$(UseMSBuildTestInfrastructure)`. -* This condition allows for users to opt-in to this test target, which helps to prevent breaking changes, with respect the the target name, since there are likely 'Test' targets that exist in the wild already. +- This condition allows for users to opt-in to this test target, which helps to prevent breaking changes, with respect the the target name, since there are likely 'Test' targets that exist in the wild already. The 'Microsoft.Common.CurrentVersion.targets' file contains. -``` + +```xml false ``` -#### Extensibility for Test Runners -* Test runner implemenations can hook into the provided stub using the `AfterTargets` property. -* This approach enables different test runners to extend the basic funcionarlity of the test target. + +### Extensibility for Test Runners + +- Test runner implemenations can hook into the provided stub using the `AfterTargets` property. +- This approach enables different test runners to extend the basic funcionarlity of the test target. For instance, an implementation for running VSTest would look like: -``` + +```xml ``` -#### Usage Scenario -* Users who wish to utilize this target will set the `$(UseMSBuildTestInfrastructure)` condition in their project file, rsp or via the command line. -* By executing `msbuild /t:Test`, the MSBuild engine will envoke the `Test` taget, which in turn triggers any test runner targets defined to run after it. +### Usage Scenario + +- Users who wish to utilize this target will set the `$(UseMSBuildTestInfrastructure)` condition in their project file, rsp or via the command line. +- By executing `msbuild /t:Test`, the MSBuild engine will envoke the `Test` taget, which in turn triggers any test runner targets defined to run after it. + +## Default Task Implementation -### Default Task Implementation See: [MSBuild Test Task](https://github.com/microsoft/MSBuildSdks/pull/473) -#### Nuget package for default implementaion -* The default implementation will be provided through a nuget package. -* This package will contain an MSBuild Task deigned to execute `vstest.console.exe`. +### Nuget package for default implementaion + +- The default implementation will be provided through a nuget package. +- This package will contain an MSBuild Task deigned to execute `vstest.console.exe`. + +### MSBuild Task Functionality + +- The core of this implemenation is an MSBuild task that interfaces with `vstest.console.exe`. +- This task will accept arguments as properties and pass them directly into the command line test runner. -#### MSBuild Task Functionality -* The core of this implemenation is an MSBuild task that interfaces with `vstest.console.exe`. -* This task will accept arguments as properties and pass them directly into the command line test runner. +### Using The Default Implementation -#### Using The Default Implementation -* Users would install the provided Nuget Package to incorporate it into their projects. -* Add the package to their GlobalPackageReferences or specific projects. -* Once integrated, executing `msbuild /t:Test` would trigger the MSBuild Task, ultimately executing `vstest.console.exe`. +- Users would install the provided Nuget Package to incorporate it into their projects. +- Add the package to their GlobalPackageReferences or specific projects. +- Once integrated, executing `msbuild /t:Test` would trigger the MSBuild Task, ultimately executing `vstest.console.exe`.