From 019a85ad746fc355993b835ca26c8d0dce8e9080 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 11 Sep 2026 21:49:10 -0700 Subject: [PATCH] Add opt-in declared-I/O task caching backed by BuildXL Cache file artifacts, typed task outputs, and warnings for explicitly annotated tasks while preserving normal target incrementality. Share build-scoped cache ownership across workers and configure storage with -buildCacheDirectory. Assisted-by: GitHub Copilot CLI:gpt-6-astra Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e3db8d0-e831-445d-bf0b-756131d8070e --- Directory.Build.props | 4 + MSBuild.slnx | 3 + NuGet.config | 11 + documentation/specs/task-cache-buildxl.md | 67 + documentation/specs/task-cache-decisions.md | 89 ++ documentation/specs/task-cache.md | 117 ++ documentation/wiki/Contributing-Tasks.md | 6 +- documentation/wiki/Results-Cache.md | 4 + documentation/wiki/Task-Cache.md | 137 ++ eng/BootStrapMsBuild.targets | 16 + eng/TaskCacheDependencies.props | 7 + eng/TaskCachePackaging.targets | 30 + eng/dependabot/Directory.Packages.props | 4 + .../BuildXLTaskCacheBackend.cs | 289 ++++ .../Microsoft.Build.TaskCache.csproj | 51 + src/Build.TaskCache/README.md | 10 + .../Microsoft.Build.TaskCache.targets | 12 + .../BackEnd/BuildXLTaskCacheBackend_Tests.cs | 150 ++ .../BackEnd/NodeManager_Tests.cs | 267 ++++ .../BackEnd/TaskCacheAvailability_Tests.cs | 30 + .../TaskCacheDiagnosticCapture_Tests.cs | 371 +++++ .../BackEnd/TaskCacheGraph_Tests.cs | 153 ++ .../BackEnd/TaskCacheOwner_Tests.cs | 379 +++++ .../BackEnd/TaskCachePackaging_Tests.cs | 143 ++ .../BackEnd/TaskCacheStore_Tests.cs | 439 +++++ .../BackEnd/TaskCacheUnannotatedTask.cs | 18 + .../BackEnd/TaskCacheWarning_Tests.cs | 126 ++ .../BackEnd/TaskCache_Tests.cs | 1420 +++++++++++++++++ .../Microsoft.Build.Engine.UnitTests.csproj | 7 + .../BackEnd/BuildManager/BuildManager.cs | 118 +- .../BackEnd/BuildManager/BuildParameters.cs | 65 + .../Components/Communications/NodeManager.cs | 15 +- .../Logging/ITaskCacheDiagnosticSource.cs | 19 + .../Components/Logging/LoggingService.cs | 6 + .../Logging/LoggingServiceTaskDiagnostics.cs | 126 ++ .../Logging/TaskCacheDiagnosticCapture.cs | 148 ++ .../Components/Logging/TaskCacheWarning.cs | 160 ++ .../RequestBuilder/RequestBuilder.cs | 2 +- .../Components/RequestBuilder/TaskBuilder.cs | 30 +- .../RequestBuilder/TaskCacheBackend.cs | 81 + .../RequestBuilder/TaskCacheClient.cs | 148 ++ .../RequestBuilder/TaskCacheOwner.cs | 283 ++++ .../RequestBuilder/TaskCachePacket.cs | 56 + .../RequestBuilder/TaskCacheStore.cs | 400 +++++ .../Components/RequestBuilder/TaskHost.cs | 15 + .../RequestBuilder/TaskInvocationCache.cs | 617 +++++++ src/Build/BackEnd/Node/OutOfProcNode.cs | 16 + .../Shared/BuildRequestConfiguration.cs | 13 +- .../TaskExecutionHost.TaskCache.cs | 210 +++ .../TaskExecutionHost/TaskExecutionHost.cs | 38 +- src/Build/Microsoft.Build.csproj | 17 + src/Build/README.md | 5 + src/Build/Resources/Strings.resx | 75 + src/Build/Resources/xlf/Strings.cs.xlf | 95 ++ src/Build/Resources/xlf/Strings.de.xlf | 95 ++ src/Build/Resources/xlf/Strings.es.xlf | 95 ++ src/Build/Resources/xlf/Strings.fr.xlf | 95 ++ src/Build/Resources/xlf/Strings.it.xlf | 95 ++ src/Build/Resources/xlf/Strings.ja.xlf | 95 ++ src/Build/Resources/xlf/Strings.ko.xlf | 95 ++ src/Build/Resources/xlf/Strings.pl.xlf | 95 ++ src/Build/Resources/xlf/Strings.pt-BR.xlf | 95 ++ src/Build/Resources/xlf/Strings.ru.xlf | 95 ++ src/Build/Resources/xlf/Strings.tr.xlf | 95 ++ src/Build/Resources/xlf/Strings.zh-Hans.xlf | 95 ++ src/Build/Resources/xlf/Strings.zh-Hant.xlf | 95 ++ .../MSBuildDeclaredIOAttribute_Tests.cs | 150 ++ src/Framework/BackEnd/Handshake.cs | 5 +- src/Framework/BackEnd/NodePacketType.cs | 11 +- src/Framework/MSBuildConstants.cs | 5 + .../MSBuildDeclaredIOInputAttribute.cs | 47 + .../MSBuildDeclaredIOOutputAttribute.cs | 50 + .../MSBuildDeclaredIOTaskAttribute.cs | 41 + src/Framework/PublicAPI.Unshipped.txt | 9 + .../MSBuild.Benchmarks.csproj | 3 + .../MSBuild.Bootstrap.Utils.csproj | 3 + .../AddBootstrapTaskCacheDependencies.cs | 161 ++ .../Tasks/GetTaskCachePackageContents.cs | 286 ++++ .../MSBuild.Bootstrap.csproj | 4 +- .../CoordinatorClient_Tests.cs | 42 + .../CommandLineSwitches_Tests.cs | 55 + ...crosoft.Build.CommandLine.UnitTests.csproj | 2 + .../TaskCacheSwitch_Tests.cs | 225 +++ .../CommandLine/CommandLineSwitches.cs | 4 + src/MSBuild/MSBuild.csproj | 3 + src/MSBuild/Resources/Strings.resx | 38 +- src/MSBuild/Resources/xlf/Strings.cs.xlf | 51 + src/MSBuild/Resources/xlf/Strings.de.xlf | 51 + src/MSBuild/Resources/xlf/Strings.es.xlf | 51 + src/MSBuild/Resources/xlf/Strings.fr.xlf | 51 + src/MSBuild/Resources/xlf/Strings.it.xlf | 51 + src/MSBuild/Resources/xlf/Strings.ja.xlf | 51 + src/MSBuild/Resources/xlf/Strings.ko.xlf | 51 + src/MSBuild/Resources/xlf/Strings.pl.xlf | 51 + src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 51 + src/MSBuild/Resources/xlf/Strings.ru.xlf | 51 + src/MSBuild/Resources/xlf/Strings.tr.xlf | 51 + src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 51 + src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 51 + src/MSBuild/XMake.cs | 66 +- src/Shared/TaskParameter.cs | 39 + src/Tasks.UnitTests/DeclaredIOTask_Tests.cs | 32 + src/Tasks/FileIO/ReadLinesFromFile.cs | 2 + 103 files changed, 10174 insertions(+), 30 deletions(-) create mode 100644 documentation/specs/task-cache-buildxl.md create mode 100644 documentation/specs/task-cache-decisions.md create mode 100644 documentation/specs/task-cache.md create mode 100644 documentation/wiki/Task-Cache.md create mode 100644 eng/TaskCacheDependencies.props create mode 100644 eng/TaskCachePackaging.targets create mode 100644 src/Build.TaskCache/BuildXLTaskCacheBackend.cs create mode 100644 src/Build.TaskCache/Microsoft.Build.TaskCache.csproj create mode 100644 src/Build.TaskCache/README.md create mode 100644 src/Build.TaskCache/buildTransitive/Microsoft.Build.TaskCache.targets create mode 100644 src/Build.UnitTests/BackEnd/BuildXLTaskCacheBackend_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/NodeManager_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskCacheAvailability_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskCacheDiagnosticCapture_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskCacheGraph_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskCacheOwner_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskCachePackaging_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskCacheStore_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskCacheUnannotatedTask.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskCacheWarning_Tests.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskCache_Tests.cs create mode 100644 src/Build/BackEnd/Components/Logging/ITaskCacheDiagnosticSource.cs create mode 100644 src/Build/BackEnd/Components/Logging/LoggingServiceTaskDiagnostics.cs create mode 100644 src/Build/BackEnd/Components/Logging/TaskCacheDiagnosticCapture.cs create mode 100644 src/Build/BackEnd/Components/Logging/TaskCacheWarning.cs create mode 100644 src/Build/BackEnd/Components/RequestBuilder/TaskCacheBackend.cs create mode 100644 src/Build/BackEnd/Components/RequestBuilder/TaskCacheClient.cs create mode 100644 src/Build/BackEnd/Components/RequestBuilder/TaskCacheOwner.cs create mode 100644 src/Build/BackEnd/Components/RequestBuilder/TaskCachePacket.cs create mode 100644 src/Build/BackEnd/Components/RequestBuilder/TaskCacheStore.cs create mode 100644 src/Build/BackEnd/Components/RequestBuilder/TaskInvocationCache.cs create mode 100644 src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.TaskCache.cs create mode 100644 src/Framework.UnitTests/MSBuildDeclaredIOAttribute_Tests.cs create mode 100644 src/Framework/MSBuildDeclaredIOInputAttribute.cs create mode 100644 src/Framework/MSBuildDeclaredIOOutputAttribute.cs create mode 100644 src/Framework/MSBuildDeclaredIOTaskAttribute.cs create mode 100644 src/Framework/PublicAPI.Unshipped.txt create mode 100644 src/MSBuild.Bootstrap.Utils/Tasks/AddBootstrapTaskCacheDependencies.cs create mode 100644 src/MSBuild.Bootstrap.Utils/Tasks/GetTaskCachePackageContents.cs create mode 100644 src/MSBuild.UnitTests/TaskCacheSwitch_Tests.cs create mode 100644 src/Tasks.UnitTests/DeclaredIOTask_Tests.cs diff --git a/Directory.Build.props b/Directory.Build.props index c44a3a518ca..65c1f629a63 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,10 @@ + + $(DefineConstants);FEATURE_BUILDXL_TASK_CACHE + + $(CopyrightNetFoundation) MIT diff --git a/MSBuild.slnx b/MSBuild.slnx index cc6ac3f706f..edf0b31c0b0 100644 --- a/MSBuild.slnx +++ b/MSBuild.slnx @@ -57,6 +57,9 @@ + + + diff --git a/NuGet.config b/NuGet.config index d77a20699a1..5022267d0b6 100644 --- a/NuGet.config +++ b/NuGet.config @@ -19,12 +19,23 @@ + + + + + + + + + + + diff --git a/documentation/specs/task-cache-buildxl.md b/documentation/specs/task-cache-buildxl.md new file mode 100644 index 00000000000..1cb26f5b4e9 --- /dev/null +++ b/documentation/specs/task-cache-buildxl.md @@ -0,0 +1,67 @@ +# BuildXL cache storage + +Use BuildXL LocalCache for CAS and memoization while MSBuild owns execution, +fingerprints, and result replay. There is no alternative backend or cache daemon. + +## Dependencies + +Package-based API hosts reference `Microsoft.Build.TaskCache` alongside +`Microsoft.Build`. It carries the optional implementation and managed/native +runtime closure; ordinary engine consumers do not need cache-specific feeds. +Transitive package targets copy native files for both build and publish. + +`Microsoft.BuildXL.Cache.MemoizationStore.Library` supplies LocalCache and +RocksDB memoization. `RocksDbNative` is referenced directly because its native +copy targets are not transitive. Dependency versions and feed mappings are +maintained in the repository's package configuration. + +The packaged native assets cover Windows/Linux/macOS x64. Linux requires glibc +and libstdc++; runtime validation is currently Linux-only. Source-build excludes +the dependencies and consuming implementation through `FEATURE_BUILDXL_TASK_CACHE`. +Ordinary source-built MSBuild works; opting into task caching reports MSB1077. + +## Layout and API boundary + +Under the configured root: + +```text +buildxl/ + owner.lock + cache/ + memoization/ + ... BuildXL-managed content and metadata ... +``` + +The invocation key becomes a `StrongFingerprint` with a fixed empty-content +selector. `GetContentHashListAsync` looks up results; +`AddOrGetContentHashListAsync` publishes the manifest hash and artifact hashes. +`PutFileAsync` hashes/copies artifacts; `PutStreamAsync` stores manifests. +Each publication has its own artifact list. Copy insertion and private output +staging prevent writable inode sharing with CAS content. + +One coordinator-owned cache/session holds exclusive directory ownership for the +top-level build. Acquisition is fail-fast. Workers use the existing coordinator +connection rather than opening the database themselves. Concurrent operations +do not serialize task execution. Shutdown drains calls, closes the session/cache, +then releases ownership. A leftover lock file is not a held OS lock. + +## Retention and maintenance + +The build-long `ImplicitPin.PutAndGet` session protects content until shutdown. +Lookup explicitly pins all referenced hashes; already-evicted content is a miss. +BuildXL enforces a 10 GiB content quota. Pinned content can exhaust that quota; +insertion failures are build errors, not silent cache bypass. + +At cache startup, run metadata GC if the last successful collection is at least +an hour old or its recorded time is in the future. Keep the timestamp in +`owner.lock`; do not run a periodic GC timer during the build. +The 64 MB metadata target is size-driven with older last-access records as +eviction candidates, not a TTL. Metadata removal and CAS eviction are separate. +Database bookkeeping means these limits do not cap total directory size. + +## Bootstrap + +`AddBootstrapTaskCacheDependencies` merges the cache dependency closure into +the bootstrap SDK's runtime manifests and copies required assets, preserving +existing SDK package versions. Baseline manifests allow replacement of cache +additions on subsequent builds. This integration is excluded from source-build. diff --git a/documentation/specs/task-cache-decisions.md b/documentation/specs/task-cache-decisions.md new file mode 100644 index 00000000000..7aaa870fbff --- /dev/null +++ b/documentation/specs/task-cache-decisions.md @@ -0,0 +1,89 @@ +# Task-cache design decisions + +MSBuild owns task execution and result semantics. BuildXL supplies content +storage and memoization, not execution or dependency discovery. + +## Scope and configuration + +- Cache individual task invocations. Preserve target timestamp skipping and + output inference; skipped targets do not consult the task cache. +- Require `-taskCache` or `BuildParameters.TaskCache`. Configuring a directory + alone enables nothing. +- Configure the shared storage root through `-buildCacheDirectory:` or + `BuildParameters.BuildCacheDirectory`. Resolve it once for the top-level build. + Project properties and environment variables cannot override it. +- Use platform-default user cache directories when configuration is unspecified. + Do not expose the directory as a project property. +- Use only BuildXL storage. Do not add a runner, persistent service, sandbox, + access monitoring, learned dependencies, or a separate timing facility. + +## Task contract + +- Describe file I/O with unconditional class-level annotations naming task + parameters. Inputs and outputs must be disjoint within an invocation. +- Trust the author to provide complete conservative dependencies and deterministic + behavior. Structural validation cannot prove the absence of hidden effects. +- Resolve the requested task normally and inspect its actual type. Never + substitute tasks to make an invocation cacheable. +- Use a separate restricted task when an existing task's parameter surface + cannot meet the contract. Keep existing invocations unchanged. +- Allow conservative output-path supersets. Record whether each candidate exists + after execution, including outputs removed or not produced. + +## Keys and results + +- Hash task implementation, bound inputs, execution context/environment, declared + input contents and absence, output paths, and referenced output-parameter names. + Workers compute input hashes; revalidate inputs before publication. +- Include engine `ModuleVersionId` in every key. This isolates serialization + changes between engine binaries, so the cache format has no separate version + fields or migration promise. +- Reuse `TaskParameter` and `BinaryTranslator` for typed values and item metadata. + Keep one manifest per invocation, containing file records, output values, + and supported warnings. See the [format reference](task-cache.md). +- Capture every referenced output getter after successful execution, regardless + of binding conditions. Getters must be safe to read then. Read each once before + cleanup; publish afterward so cleanup failures cannot produce a reusable result. +- Keep binding conditions and destination property/item names outside the cached + result. Apply current bindings to independent restored values. + +## Ownership and publication + +- One coordinator owns LocalCache and its session for the top-level build. + Acquire directory ownership without waiting; competing builds fail. + Ownership remains held while tools execute, without serializing task execution. +- Workers access the owner through existing node communication. Exchange paths, + hashes, and result metadata, not artifact contents through in-memory transport. +- Keep session pins for the build. Await each publication in the task path; + do not add a background publication queue. +- Let CAS hash output files during insertion. Use the returned hashes rather + than prehashing and rereading the same outputs in MSBuild. +- At shutdown, stop new operations, cancel active calls, and await completion + before closing storage and releasing ownership. Do not abandon active calls + through a separate shutdown timeout. + +## Diagnostics and failure recovery + +- Cache standard warnings from execution and output getters before warning-policy + conversion. Replay them in the current task context under current warning policy. +- Setup/cleanup diagnostics and unsupported warning subclasses prevent caching + rather than being replayed incompletely or twice. Raw errors, failed tasks, + exceptions, and cancellation prevent publication. Do not replay ordinary messages. +- Missing entries or evicted content are misses. Corruption and operational + failures fail the build, without task-execution fallback. +- Validate and stage all artifacts before replacing destinations. Honor + cancellation before replacement, then finish replacement without cancellation + interruption. Filesystem errors remain fatal. +- Do not implement multi-file rollback. Preserve the primary error during cleanup + and require clean/rebuild recovery after partial restoration; timestamp skipping + prevents a guarantee of automatic incremental recovery. + +## Storage policy + +Use a 10 GiB content quota and build-long pins. Check metadata collection only +when opening the cache, with a one-hour minimum interval and a 64 MB size target. +Metadata eviction is based on size and last access, not a fixed expiration age. + +Exclude BuildXL dependencies and implementation from source-build. Permit the +packaged Windows/Linux/macOS x64 assets; runtime validation is currently Linux-only. +See the [storage reference](task-cache-buildxl.md). diff --git a/documentation/specs/task-cache.md b/documentation/specs/task-cache.md new file mode 100644 index 00000000000..594318bf254 --- /dev/null +++ b/documentation/specs/task-cache.md @@ -0,0 +1,117 @@ +# Task-cache implementation and format + +The [decision record](task-cache-decisions.md) explains the design. +The [user guide](../wiki/Task-Cache.md) describes usage and task-author requirements. + +## Execution and ownership + +After conditions, batching, and parameter binding, `TaskInvocationCache` keys +the invocation and queries storage. A hit restores files and typed outputs; +normal MSBuild binding applies the current `` elements. +Target timestamp skipping remains unchanged. + +`TaskCacheOwner` holds one BuildXL cache/session from `BeginBuild` to `EndBuild`. +`TaskCacheClient` routes worker requests through existing node communication. +Workers hash inputs and restore files locally; the owner stores files by path. +No artifact bytes are transported through node messages. +Unix output staging files are created owner-only before writing content; +recorded final permissions are applied after validation. + +Requests carry an ID, operation, and key. Responses complete waiters directly +on receipt, independently of scheduler work that might be waiting for a task. +Cancellation waits for operation completion; connection loss releases waiters. +Shutdown drains active calls before closing storage. Worker protocol compatibility +is checked by the handshake, independently of the on-disk format. + +`BuildManager` resets configuration/results between cache-enabled build sessions +so an old in-memory build result cannot bypass invocation processing. + +## Invocation key + +The key is SHA-256 over an encoded invocation descriptor and declared inputs: + +- Engine/task module identities and task type. +- Project/toolset, runtime/platform, culture, and task execution directory. +- Bound parameter values and effective declared file parameters. +- Referenced output-parameter names, effective environment, and output paths. +- Each declared input's absolute path, existence flag, and content hash. + +Names are sorted where ordering is not meaningful; item metadata is canonicalized. +Item inputs retain both the transport representation and task-visible custom +metadata values, so inherited expressions and literal metadata remain distinct. +File bytes are hashed directly, not embedded in the descriptor. Engine module +identity isolates incompatible encodings; there are no explicit format-version +fields or cross-engine migration guarantees. + +## Result manifest + +`TaskCacheStore` writes one manifest per invocation. BuildXL memoization maps +the invocation key to a content-hash list: manifest first, then present artifacts. +The manifest itself is an opaque CAS object. + +Integers use `BinaryWriter`'s little-endian representation. Booleans occupy one +byte. A *manifest string* is an Int32 UTF-8 byte length followed by its bytes. + +| Field, in serialization order | Encoding | +| --- | --- | +| Output-path count | Int32 | +| For each output: absolute path | Manifest string | +| Output exists | Boolean | +| Digest, only if present | Manifest string: uppercase hexadecimal SHA-256 | +| Original last-write time | Int64 UTC ticks; zero if absent | +| Unix permissions | Int32, masked to `0x1ff`; zero if absent/not captured | +| Task-state byte count | Int32 | +| Task state | Bytes, described below | +| Checksum | SHA-256 of all preceding manifest bytes | + +Output records follow sorted, deduplicated paths and must match the current +contract exactly. An absent record deletes that destination on restore. +Stored timestamps are validated, but restored files receive the current time. +Limits: 64 MiB per manifest and 1 MiB per manifest string. + +### Typed outputs + +Task state begins with an Int32 output count. Entries are ordinally sorted by +task parameter name. Each contains: + +1. Name via `BinaryWriter.Write(string)` (7-bit UTF-8 byte length). +2. Int32 value byte count. +3. Value bytes from `TaskParameter.Translate` through `BinaryTranslator`. + +Supported values are nulls, the primitive/array subset accepted by +`TaskInvocationCache.IsSupportedParameterType`, and task items with metadata. +Type tags and restored values are validated; arbitrary task-defined objects +are not deserialized. Each binding receives a fresh value. + +### Warnings + +Typed outputs are followed by an Int32 warning count and these records: + +1. Subcategory, Code, File, Message, HelpKeyword, SenderName, HelpLink: + Int32 UTF-8 byte length and bytes, with -1 meaning null. +2. LineNumber, ColumnNumber, EndLineNumber, EndColumnNumber: Int32 each. + +Null/empty strings remain distinct. UTF-8 is strict. Limits are 1,024 warnings, +1 MiB per string, and 16 MiB for the warning section. Invalid bounds, negative +positions, malformed encoding, truncation, and trailing state are rejected +before restoration. + +Only exact `BuildWarningEventArgs` instances are supported. Snapshot formatted +fields before asynchronous logging or policy conversion, not raw argument +objects. Replay regenerates task context, project association, timestamp, +and thread identity. Live replay preserves HelpLink; the existing binlog +encoding's omission of that field is unchanged. + +## Capture and publication + +The diagnostic watch spans initialization through cleanup. Replayable warning +capture covers only execution and referenced output getters on a cache miss. +Setup, binding, input-getter, cleanup, and unsupported diagnostics block caching. +Raw errors are observed before `ContinueOnError` conversion. + +Capture each referenced getter once before cleanup, including getters whose +binding conditions are false. Publish after cleanup and input revalidation. +On a hit, validate state and restore files, then replay warnings before current +output binding. Current warning policy applies without entering the key. + +See [BuildXL storage](task-cache-buildxl.md) for lifecycle and maintenance. diff --git a/documentation/wiki/Contributing-Tasks.md b/documentation/wiki/Contributing-Tasks.md index c77aaa7cd02..a6fa39e6d5e 100644 --- a/documentation/wiki/Contributing-Tasks.md +++ b/documentation/wiki/Contributing-Tasks.md @@ -17,6 +17,10 @@ Review the existing documentation on [Task Writing](https://learn.microsoft.com/ Tasks are generally simple and should not require much effort to develop. If you find a task becoming very complicated, consider breaking it up into smaller tasks which can be run together in a target. +For file I/O annotations and task-cache author requirements, see +[Task invocation caching](Task-Cache.md). That documentation applies to task +authors generally, not just contributors to this repository. + ## Developing unit tests Contributed tasks must have unit tests in place to prove they work and to prevent regressions caused by other code changes. There are a lot of examples in the [Microsoft.Build.Tasks.UnitTests](https://github.com/dotnet/msbuild/tree/main/src/Tasks.UnitTests) project. Please provide a reasonable amount of test coverage so ensure the quality of the product. @@ -25,5 +29,3 @@ You can document the new task in the [visualstudio-docs](https://github.com/Micr ## Ship schedule MSBuild ships regularly with Visual Studio. It also is updated in Preview releases. Once your contribution is merged, expect it to be available in the next release. - - diff --git a/documentation/wiki/Results-Cache.md b/documentation/wiki/Results-Cache.md index b3e5f5f0175..fcfdabf0de7 100644 --- a/documentation/wiki/Results-Cache.md +++ b/documentation/wiki/Results-Cache.md @@ -4,6 +4,10 @@ MSBuild uses caching to speed up builds. It does this by remembering the outcome ![MSBuild Cache Flow](CacheFlow.png) +The experimental [task invocation cache](Task-Cache.md) is a separate persistent +cache. Unlike the in-memory structures described below, its entries survive +build completion and process exit. + ## `ResultsCache` (The Core Cache Component) `ResultsCache` is the primary storage mechanism where MSBuild keeps the outcomes of its build targets. diff --git a/documentation/wiki/Task-Cache.md b/documentation/wiki/Task-Cache.md new file mode 100644 index 00000000000..2744e2c3a0f --- /dev/null +++ b/documentation/wiki/Task-Cache.md @@ -0,0 +1,137 @@ +# Task invocation caching + +Enable the experimental persistent task cache explicitly: + +```sh +dotnet msbuild -taskCache -buildCacheDirectory:/path/to/private/cache +``` + +`-taskCache:false` disables it. API hosts use `BuildParameters.TaskCache` and +`BuildParameters.BuildCacheDirectory`. Setting a directory alone enables nothing. +Without opt-in, ordinary task execution is unchanged. + +## Configuration + +The directory is captured once for the top-level build. Project properties and +environment variables do not configure it, and no directory property is injected +into projects. Relative command-line paths resolve against the invocation's +working directory. Whitespace configuration selects the default; the directory +switch requires a value. + +| Platform | Default | +| --- | --- | +| Linux | `~/.cache/msbuild-cache` | +| macOS | `~/Library/Caches/msbuild-cache` | +| Windows | Local application-data directory, under `msbuild-cache` | + +Storage uses BuildXL cache libraries with x64 native assets. Source-build and +unsupported architectures reject cache opt-in. Runtime validation is Linux-only; +see [storage details](../specs/task-cache-buildxl.md). + +## Execution behavior + +Normal target `Inputs`/`Outputs` timestamp checks run first. A skipped target +does not hash task inputs or access task results. Caching does not detect content +changes hidden by a successful target timestamp check. + +For eligible tasks, lookup follows condition evaluation, batching, and parameter +binding. A hit restores declared files and typed task outputs instead of calling +`Execute()`. Current `` conditions and destinations apply normally. +Restored files receive fresh timestamps. + +MSBuild resolves the requested task normally and checks that type's annotations; +it never substitutes tasks. Unannotated tasks run without participation warnings. +The mode cannot currently combine with `-question`, `-inputResultsCaches`, or +`-outputResultsCache`. + +## Declaring a task contract + +```csharp +[MSBuildDeclaredIOTask] +[MSBuildDeclaredIOInput(nameof(Sources))] +[MSBuildDeclaredIOOutput(nameof(DestinationFiles))] +public sealed class MyTask : Task +{ + // Task implementation and parameters. +} +``` + +The marker promises complete logical file I/O. Repeatable input/output attributes +name public instance parameters; attributes are not inherited, so every concrete +task must declare its contract. A marker without file declarations asserts no +logical file I/O. + +`string` and `ITaskItem` describe one file path; arrays describe multiple paths. +Item metadata is not implicitly a file dependency. Strings are not globs or +semicolon-separated path lists. Relative paths use `TaskEnvironment.ProjectDirectory`, +normally the project directory rather than the imported targets directory. + +Inputs and outputs must be disjoint. Output candidates may be a conservative +superset: the result records which exist and which are absent. Private scratch +files with no external effects are excluded; output parent-directory creation +is implicit. Directory-valued contracts are not supported. + +Declarations must cover deterministic execution without unmodeled process, +build-engine, or filesystem effects. The engine validates shape and overlap, +not completeness: there is no sandbox or access monitor. Keys include contents +and absence, not all filesystem attributes. Tasks relying on other metadata need +additional modeling. + +### Task output values + +`[Output]` marks returned values, not physical file writes. The cache records +each parameter referenced by an `` element, including item metadata. +These getters must be safe to read after successful execution even when a binding +condition is false. Each is read once before cleanup; publication follows cleanup. + +Referenced parameter names enter the key. Destination property/item names and +binding conditions do not. Each binding receives an independent restored value. +Unreferenced output getters are not read. + +### Optional mode awareness + +When caching is enabled, the engine supplies `MSBuildTaskCacheEnabled=true` +before file-based evaluation. Explicit `-taskCache:false` supplies false. +The property alone does not enable engine caching. + +A task need not expose a parameter of that name. If it does expose a Boolean, +an explicit true binding is required for that invocation to participate. +Hosts with already-evaluated projects must provide any mode globals needed by +their targets during evaluation. + +## Warnings and failures + +Successful executions can cache exact `BuildWarningEventArgs` warnings from +execution and referenced output getters. Replay applies the current warning +policy and task context. A warning cached as a message can become an error on +a later hit; `-warnNotAsError` applies anew. + +Initialization, binding, input-getter, cleanup, and unsupported warning-subclass +diagnostics prevent caching instead of being replayed twice or incompletely. +Raw errors, failed tasks, exceptions, and cancellation prevent publication, +including errors demoted by `ContinueOnError`. Ordinary messages are not replayed. + +An otherwise successful task can be cached even when warning promotion fails the +overall build; replay under that policy still fails. Invalid declarations remain +subject to normal warning policy. + +Missing entries or evicted content are misses. Corruption and operational +failures fail the build, not fall back to executing the task. +Restoration validates/stages all artifacts, then completes destination replacement +without cancellation interruption. Filesystem errors can leave partial outputs: +fix the cause and clean/rebuild. There is no multi-file rollback or guarantee that +an ordinary incremental build will repair the state. + +## Storage lifetime + +Results survive process exit. One coordinator owns the cache for the top-level +build; workers share it through existing MSBuild communication. Another build +using the same root fails immediately. Pins last for the build. + +Keep storage private: manifests can contain task values and warnings, and blobs +contain build outputs. Keys include absolute paths and the effective environment, +so moved checkouts or changing environment values can prevent reuse. +Stop builds before removing the cache. + +See the [design decisions](../specs/task-cache-decisions.md) and +[manifest format](../specs/task-cache.md). diff --git a/eng/BootStrapMsBuild.targets b/eng/BootStrapMsBuild.targets index 0f2dc763b64..809a7a23d0a 100644 --- a/eng/BootStrapMsBuild.targets +++ b/eng/BootStrapMsBuild.targets @@ -305,6 +305,12 @@ + + + + + + diff --git a/eng/TaskCachePackaging.targets b/eng/TaskCachePackaging.targets new file mode 100644 index 00000000000..ee4d93a07ff --- /dev/null +++ b/eng/TaskCachePackaging.targets @@ -0,0 +1,30 @@ + + + <_TaskCacheNewtonsoftVersion>@(PackageVersion->WithMetadataValue('Identity', 'Newtonsoft.Json')->'%(Version)') + + + + + + + + + + + + + + + + + diff --git a/eng/dependabot/Directory.Packages.props b/eng/dependabot/Directory.Packages.props index d1413a9c59e..dac84345120 100644 --- a/eng/dependabot/Directory.Packages.props +++ b/eng/dependabot/Directory.Packages.props @@ -28,6 +28,10 @@ + + + + diff --git a/src/Build.TaskCache/BuildXLTaskCacheBackend.cs b/src/Build.TaskCache/BuildXLTaskCacheBackend.cs new file mode 100644 index 00000000000..a13f64fa285 --- /dev/null +++ b/src/Build.TaskCache/BuildXLTaskCacheBackend.cs @@ -0,0 +1,289 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if FEATURE_BUILDXL_TASK_CACHE +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading; +using BuildXL.Cache.ContentStore.Distributed.NuCache; +using BuildXL.Cache.ContentStore.Hashing; +using BuildXL.Cache.ContentStore.Interfaces.FileSystem; +using BuildXL.Cache.ContentStore.Interfaces.Logging; +using BuildXL.Cache.ContentStore.Interfaces.Results; +using BuildXL.Cache.ContentStore.Interfaces.Sessions; +using BuildXL.Cache.ContentStore.Interfaces.Stores; +using BuildXL.Cache.ContentStore.Interfaces.Time; +using BuildXL.Cache.ContentStore.Interfaces.Tracing; +using BuildXL.Cache.ContentStore.Logging; +using BuildXL.Cache.ContentStore.Stores; +using BuildXL.Cache.ContentStore.Tracing.Internal; +using BuildXL.Cache.Host.Configuration; +using BuildXL.Cache.MemoizationStore.Interfaces.Sessions; +using BuildXL.Cache.MemoizationStore.Interfaces.Stores; +using BuildXL.Cache.MemoizationStore.Sessions; +using BuildXL.Cache.MemoizationStore.Stores; + +namespace Microsoft.Build.BackEnd; + +/// +/// A build-scoped LocalCache session. The outer file lock is acquired before +/// BuildXL's directory/database locks and released only after session/cache shutdown. +/// Only the coordinator opens storage; workers use the existing node connection. +/// +internal sealed class BuildXLTaskCacheBackend : TaskCacheBackend +{ + internal const string DirectoryName = "buildxl"; + private readonly FileStream _lock; + private readonly Context _context = new(NullLogger.Instance); + private readonly LocalCache _cache; + private readonly AbsolutePath _cacheRoot; + private ICacheSession? _session; + private bool _disposed; + + internal BuildXLTaskCacheBackend(string directory, uint quotaMegabytes = 10240, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + TaskCacheStore.CreateCacheDirectory(directory); + string root = Path.Combine(directory, DirectoryName); + TaskCacheStore.CreateCacheDirectory(root); + string lockPath = Path.Combine(root, "owner.lock"); + TaskCacheStore.CheckPath(lockPath, allowDirectory: false); + try + { + _lock = new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + } + catch (IOException e) + { + throw new IOException(FormatOwnershipFailure(directory), e); + } + + try + { + _cacheRoot = new AbsolutePath(Path.Combine(root, "cache")); + TaskCacheStore.CreateCacheDirectory(_cacheRoot.Path); + TaskCacheStore.CheckPath((_cacheRoot / "memoization").Path, allowDirectory: true); + TaskCacheStore.CheckPath((_cacheRoot / "Shared").Path, allowDirectory: true); + CapturedMemoizationConfiguration configuration = new() + { + Database = new RocksDbContentLocationDatabaseConfiguration(_cacheRoot / "memoization") + { + CleanOnInitialize = false, + // Run metadata GC once on opening, with a persistent hourly cadence. + GarbageCollectionInterval = Timeout.InfiniteTimeSpan, + MetadataGarbageCollectionMaximumSizeMb = 64, + // Bound native background pools independently of build parallelism. + RocksDbPerformanceSettings = new RocksDbPerformanceSettings + { + CompactionDegreeOfParallelism = new() { ThreadCount = 1 }, + MaxBackgroundCompactions = 1, + MaxBackgroundFlushes = 1, + }, + }, + }; + _cache = LocalCache.CreateUnknownContentStoreInProcMemoizationStoreCache( + NullLogger.Instance, + _cacheRoot, + configuration, + LocalCacheConfiguration.CreateServerDisabled(), + new ConfigurationModel(ContentStoreConfiguration.CreateWithMaxSizeQuotaMB(quotaMegabytes)), + assumeCallerCreatesDirectoryForPlace: true); + Check(_cache.StartupAsync(_context).GetAwaiter().GetResult(), cancellationToken); + byte[] gcTime = new byte[sizeof(long)]; + long lastCollection = _lock.Read(gcTime, 0, gcTime.Length) == gcTime.Length ? BitConverter.ToInt64(gcTime, 0) : 0; + DateTime now = DateTime.UtcNow; + if (lastCollection < now.AddHours(-1).Ticks || lastCollection > now.Ticks) + { + Check(configuration.Store!.RocksDbDatabase.GarbageCollectAsync(new OperationContext(_context, cancellationToken)).GetAwaiter().GetResult(), cancellationToken); + gcTime = BitConverter.GetBytes(now.Ticks); + _lock.Position = 0; + _lock.Write(gcTime, 0, gcTime.Length); + _lock.Flush(flushToDisk: true); + } + + var session = _cache.CreateSession(_context, "MSBuildTaskCache", ImplicitPin.PutAndGet); + _session = session.Session; + Check(session, cancellationToken); + if (_session is null) + { + throw new IOException("BuildXL did not create a task cache session."); + } + Check(_session.StartupAsync(_context).GetAwaiter().GetResult(), cancellationToken); + } + catch + { + try + { + Dispose(); + } + catch (Exception e) when (!IsCriticalException(e)) + { + // Preserve the initialization error while still releasing ownership. + } + throw; + } + } + + internal override byte[]? ReadManifest(string key, int maximumSize, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var result = _session!.GetContentHashListAsync(_context, FingerprintFor(key), cancellationToken).GetAwaiter().GetResult(); + Check(result, cancellationToken); + ContentHashList? list = result.ContentHashListWithDeterminism.ContentHashList; + if (list is null) + { + return null; + } + + if (list.Hashes.Count == 0) + { + throw new InvalidDataException("Invalid task cache content hash list."); + } + + foreach (ContentHash contentHash in list.Hashes) + { + var pin = _session.PinAsync(_context, contentHash, cancellationToken).GetAwaiter().GetResult(); + if (pin.Code == PinResult.ResultCode.ContentNotFound) + { + return null; + } + + Check(pin, cancellationToken); + } + + ContentHash digest = list.Hashes[0]; + using Stream stream = Open(digest.ToHex(), cancellationToken); + if (stream.Length < 32 || stream.Length > maximumSize) + { + throw new InvalidDataException("Invalid task cache manifest size."); + } + + using BinaryReader reader = new(stream); + byte[] manifest = reader.ReadBytes(checked((int)stream.Length)); + using SHA256 hash = SHA256.Create(); + if (new ContentHash(HashType.SHA256, hash.ComputeHash(manifest)) != digest) + { + throw new InvalidDataException("Task cache manifest content checksum mismatch."); + } + + return manifest; + } + + internal override Stream Open(string digest, CancellationToken cancellationToken = default) + { + TaskCacheStore.CheckPath(ContentPath(digest), allowDirectory: false, allowReadOnlyFile: true); + cancellationToken.ThrowIfCancellationRequested(); + var result = _session!.OpenStreamAsync(_context, new ContentHash("SHA256:" + digest), cancellationToken).GetAwaiter().GetResult(); + Check(result, cancellationToken); + return result.Stream ?? throw new InvalidDataException("Task cache content is missing."); + } + + internal override string ContentPath(string digest) => + (_cacheRoot / FileSystemContentStoreInternal.GetPrimaryRelativePath(new ContentHash("SHA256:" + digest))).Path; + + internal override string PutFile(string path, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + TaskCacheStore.CheckPath(path, allowDirectory: false); + var result = _session!.PutFileAsync(_context, HashType.SHA256, new AbsolutePath(path), + FileRealizationMode.Copy, cancellationToken).GetAwaiter().GetResult(); + Check(result, cancellationToken); + return result.ContentHash.ToHex(); + } + + internal override bool Publish(string key, byte[] manifest, Func? canPublish = null, + IReadOnlyList? artifacts = null, CancellationToken cancellationToken = default) + { + // The first CAS object is the MSBuild manifest. Register every artifact with + // memoization, not just the manifest, so BuildXL can detect evicted content. + using MemoryStream stream = new(manifest, writable: false); + cancellationToken.ThrowIfCancellationRequested(); + var put = _session!.PutStreamAsync(_context, HashType.SHA256, stream, cancellationToken).GetAwaiter().GetResult(); + Check(put, cancellationToken); + + ContentHash[] hashes = new ContentHash[(artifacts?.Count ?? 0) + 1]; + hashes[0] = put.ContentHash; + for (int i = 1; i < hashes.Length; i++) + { + hashes[i] = new ContentHash("SHA256:" + TaskCacheStore.ValidateDigest(artifacts![i - 1])); + } + if (canPublish?.Invoke() == false) + { + return false; + } + + var result = _session.AddOrGetContentHashListAsync(_context, FingerprintFor(key), + new ContentHashListWithDeterminism(new ContentHashList(hashes, null), CacheDeterminism.None), + cancellationToken).GetAwaiter().GetResult(); + Check(result, cancellationToken); + return true; + } + + private static StrongFingerprint FingerprintFor(string key) => + new(new Fingerprint(key), new Selector(SHA256HashInfo.Instance.EmptyHash)); + + private static void Check(ResultBase result, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!result.Succeeded) + { + throw new IOException("BuildXL task cache operation failed: " + result); + } + } + + public override void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + try + { + try + { + if (_session is not null) + { + try + { + Check(_session.ShutdownAsync(_context).GetAwaiter().GetResult()); + } + finally + { + _session.Dispose(); + _session = null; + } + } + } + finally + { + if (_cache is not null) + { + try + { + Check(_cache.ShutdownAsync(_context).GetAwaiter().GetResult()); + } + finally + { + _cache.Dispose(); + } + } + } + } + finally + { + _lock.Dispose(); + } + } + + private sealed class CapturedMemoizationConfiguration : RocksDbMemoizationStoreConfiguration + { + internal RocksDbMemoizationStore? Store { get; private set; } + + public override IMemoizationStore CreateStore(ILogger logger, IClock clock) => + Store = (RocksDbMemoizationStore)base.CreateStore(logger, clock); + } +} +#endif diff --git a/src/Build.TaskCache/Microsoft.Build.TaskCache.csproj b/src/Build.TaskCache/Microsoft.Build.TaskCache.csproj new file mode 100644 index 00000000000..11439072b8e --- /dev/null +++ b/src/Build.TaskCache/Microsoft.Build.TaskCache.csproj @@ -0,0 +1,51 @@ + + + $(FullFrameworkTFM);$(LatestDotNetCoreForMSBuild) + true + false + false + true + false + Optional BuildXL-backed MSBuild task cache runtime with Windows, Linux, and macOS x64 native libraries. This package has no compile-time API surface. + $(NoWarn);NU5104 + $(TargetsForTfmSpecificContentInPackage);CollectBundledTaskCacheRuntime + + + <_TaskCacheEngineProject Include="..\Build\Microsoft.Build.csproj" /> + + + + + + + + + + + + + + + + + + + + + + + <_TaskCacheNativeFiles Include="@(_PackageFiles->WithMetadataValue('TaskCacheNativeAsset', 'true'))" /> + <_PackageFiles Remove="@(_TaskCacheNativeFiles)" /> + + + + + + diff --git a/src/Build.TaskCache/README.md b/src/Build.TaskCache/README.md new file mode 100644 index 00000000000..cfc1dad9e53 --- /dev/null +++ b/src/Build.TaskCache/README.md @@ -0,0 +1,10 @@ +# Microsoft.Build.TaskCache + +Optional runtime implementation for MSBuild's BuildXL-backed task cache, including +its managed dependencies and Windows, Linux, and macOS x64 native libraries. +Reference this package alongside `Microsoft.Build` when hosting cache-enabled +builds. The package exposes no compile-time API and does not enable caching: +hosts still opt in through `BuildParameters.TaskCache`. + +The MSBuild CLI distribution supplies this runtime. Package build assets copy +RocksDB libraries into the location expected by its loader for build and publish. diff --git a/src/Build.TaskCache/buildTransitive/Microsoft.Build.TaskCache.targets b/src/Build.TaskCache/buildTransitive/Microsoft.Build.TaskCache.targets new file mode 100644 index 00000000000..1cd0775e024 --- /dev/null +++ b/src/Build.TaskCache/buildTransitive/Microsoft.Build.TaskCache.targets @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/Build.UnitTests/BackEnd/BuildXLTaskCacheBackend_Tests.cs b/src/Build.UnitTests/BackEnd/BuildXLTaskCacheBackend_Tests.cs new file mode 100644 index 00000000000..23cc3b785fa --- /dev/null +++ b/src/Build.UnitTests/BackEnd/BuildXLTaskCacheBackend_Tests.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class BuildXLTaskCacheBackend_Tests(ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + private static readonly string s_key = new('A', 64); + + public static bool IsSupported => TaskCacheStore.IsSupported; + + [Fact] + public void BuildXLDependencyAndImplementationFollowSourceBuildGate() + { + var references = typeof(BuildManager).Assembly.GetReferencedAssemblies(); + references.ShouldNotContain(reference => reference.Name!.StartsWith("BuildXL.Cache.", StringComparison.Ordinal)); +#if FEATURE_BUILDXL_TASK_CACHE + typeof(BuildXLTaskCacheBackend).Assembly.GetName().Name.ShouldBe("Microsoft.Build.TaskCache"); + typeof(BuildXLTaskCacheBackend).Assembly.GetReferencedAssemblies() + .ShouldContain(reference => reference.Name == "BuildXL.Cache.MemoizationStore"); + typeof(BuildManager).Assembly.GetType("Microsoft.Build.BackEnd.BuildXLTaskCacheBackend").ShouldBeNull(); + TaskCacheStore.IsSupported.ShouldBe(RuntimeInformation.ProcessArchitecture == Architecture.X64); +#else + typeof(BuildManager).Assembly.GetType("Microsoft.Build.BackEnd.BuildXLTaskCacheBackend").ShouldBeNull(); + TaskCacheStore.IsSupported.ShouldBeFalse(); +#endif + } + +#if FEATURE_BUILDXL_TASK_CACHE + [ConditionalFact(typeof(BuildXLTaskCacheBackend_Tests), nameof(IsSupported))] + public void EmptyContentDoesNotRequireAPhysicalCasFile() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("empty.txt", String.Empty).Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(env.CreateFolder().Path); + string digest = backend.PutFile(path); + using Stream stream = backend.Open(digest); + stream.Length.ShouldBe(0); + File.Exists(backend.ContentPath(digest)).ShouldBeFalse(); + } + + [ConditionalFact(typeof(BuildXLTaskCacheBackend_Tests), nameof(IsSupported))] + public void BuildXLQuotaEvictsUnpinnedContentBetweenSessions() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string cache = env.CreateFolder().Path; + string path = env.CreateFile().Path; + byte[] content = new byte[700 * 1024]; + File.WriteAllBytes(path, content); + string firstDigest; + using (BuildXLTaskCacheBackend backend = new(cache, quotaMegabytes: 1)) + { + firstDigest = backend.PutFile(path); + } + + content[0] = 1; + File.WriteAllBytes(path, content); + using (BuildXLTaskCacheBackend backend = new(cache, quotaMegabytes: 1)) + { + string secondDigest = backend.PutFile(path); + Should.Throw(() => backend.Open(firstDigest)); + using Stream restored = backend.Open(secondDigest); + restored.ReadByte().ShouldBe(1); + } + } + + [ConditionalFact(typeof(BuildXLTaskCacheBackend_Tests), nameof(IsSupported))] + public void BuildLongPinsPreventQuotaEvictionUntilOwnerCloses() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string cache = env.CreateFolder().Path; + string first = env.CreateFile().Path; + string second = env.CreateFile().Path; + byte[] content = new byte[700 * 1024]; + File.WriteAllBytes(first, content); + content[0] = 1; + File.WriteAllBytes(second, content); + using BuildXLTaskCacheBackend backend = new(cache, quotaMegabytes: 1); + string digest = backend.PutFile(first); + Should.Throw(() => backend.PutFile(second)); + using Stream pinned = backend.Open(digest); + pinned.ReadByte().ShouldBe(0); + } + + [ConditionalFact(typeof(BuildXLTaskCacheBackend_Tests), nameof(IsSupported))] + public void ExclusiveLockFailsImmediatelyAndIsReleased() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string cache = env.CreateFolder().Path; + using (BuildXLTaskCacheBackend first = new(cache)) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + Should.Throw(() => new BuildXLTaskCacheBackend(cache)).Message.ShouldBe( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskCache.OwnershipUnavailable", cache)); + watch.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(2)); + } + + using BuildXLTaskCacheBackend reopened = new(cache); + reopened.ReadManifest(s_key, 1024).ShouldBeNull(); + } + + [ConditionalFact(typeof(BuildXLTaskCacheBackend_Tests), nameof(IsSupported))] + public void InitializationFailureAfterAcquiringLockReleasesOwnership() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string cache = env.CreateFolder().Path; + string root = Path.Combine(cache, BuildXLTaskCacheBackend.DirectoryName); + Directory.CreateDirectory(root); + string obstruction = Path.Combine(root, "cache"); + File.WriteAllText(obstruction, "not a directory"); + Should.Throw(() => new BuildXLTaskCacheBackend(cache)); + File.Delete(obstruction); + using BuildXLTaskCacheBackend backend = new(cache); + backend.ReadManifest(s_key, 1024).ShouldBeNull(); + } + + [ConditionalFact(typeof(BuildXLTaskCacheBackend_Tests), nameof(IsSupported))] + public void EvictedArtifactMakesMemoizedEntryACacheMiss() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string cache = env.CreateFolder().Path; + string path = env.CreateFile().Path; + byte[] content = new byte[700 * 1024]; + File.WriteAllBytes(path, content); + using (BuildXLTaskCacheBackend backend = new(cache, quotaMegabytes: 1)) + { + string digest = backend.PutFile(path); + backend.Publish(s_key, new byte[64], artifacts: [digest]).ShouldBeTrue(); + } + + content[0] = 1; + File.WriteAllBytes(path, content); + using (BuildXLTaskCacheBackend backend = new(cache, quotaMegabytes: 1)) + { + backend.PutFile(path); + backend.ReadManifest(s_key, 1024).ShouldBeNull(); + } + } +#endif +} diff --git a/src/Build.UnitTests/BackEnd/NodeManager_Tests.cs b/src/Build.UnitTests/BackEnd/NodeManager_Tests.cs new file mode 100644 index 00000000000..1ab0118b5b0 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/NodeManager_Tests.cs @@ -0,0 +1,267 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Execution; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class NodeManager_Tests(ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public async Task CacheRepliesRemainRoutableDuringNodeMapGrowthAndRemoval() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using RoutingHarness test = new(); + int node = test.CreateNodes(128)[127].NodeId; + TaskCachePacket reply = new(NodePacketType.TaskCacheResponse); + using CancellationTokenSource stop = new(); + using CountdownEvent ready = new(2); + using ManualResetEventSlim start = new(); + Task[] readers = new Task[2]; + for (int i = 0; i < readers.Length; i++) + { + readers[i] = Task.Factory.StartNew(() => + { + ready.Signal(); + start.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + while (!stop.IsCancellationRequested) + { + test.Manager.SendData(node, reply); + } + }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + try + { + ready.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + start.Set(); + SpinWait.SpinUntil(() => test.Provider.SentCount > 0, TimeSpan.FromSeconds(10)).ShouldBeTrue(); + for (int batch = 0; batch < 1024; batch++) + { + IList added = test.CreateNodes(32); + for (int i = 0; i < 8; i++) + { + test.Manager.RoutePacket(added[i].NodeId, new NodeShutdown(NodeShutdownReason.Requested)); + } + } + } + finally + { + stop.Cancel(); + start.Set(); + await Task.WhenAll(readers); + } + test.Provider.SentCount.ShouldBeGreaterThan(0); + test.Manager.SendData(node, reply); + } + + [Fact] + public async Task ProviderSendDoesNotBlockConcurrentClearAndRouteRecreation() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using RoutingHarness test = new(); + int node = test.CreateNodes(1)[0].NodeId; + using ManualResetEventSlim entered = new(); + using ManualResetEventSlim release = new(); + test.Provider.OnSend = (_, _) => + { + entered.Set(); + release.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + }; + Task send = Task.Run(() => test.Manager.SendData(node, new TaskCachePacket(NodePacketType.TaskCacheResponse))); + Task? mutation = null; + try + { + entered.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + mutation = Task.Run(() => + { + test.Manager.ClearPerBuildState(); + test.CreateNodes(1)[0].NodeId.ShouldBe(node); + }); + mutation.Wait(TimeSpan.FromSeconds(5)).ShouldBeTrue(); + } + finally + { + release.Set(); + await send; + if (mutation is not null) + { + await mutation; + } + } + test.Manager.SendData(node, new TaskCachePacket(NodePacketType.TaskCacheResponse)); + test.Provider.SentCount.ShouldBe(2); + } + + [Fact] + public async Task ProviderCreationCanWaitForAnIndependentReply() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using RoutingHarness test = new(); + int node = test.CreateNodes(1)[0].NodeId; + Task? reply = null; + test.Provider.OnCreate = () => + { + reply = Task.Run(() => test.Manager.SendData(node, new TaskCachePacket(NodePacketType.TaskCacheResponse))); + reply.Wait(TimeSpan.FromSeconds(5)).ShouldBeTrue(); + }; + try + { + test.CreateNodes(1).Count.ShouldBe(1); + } + finally + { + if (reply is not null) + { + await reply; + } + } + test.Provider.SentCount.ShouldBe(1); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ShutdownRemovesRouteBeforeCallingReentrantHandler(bool deserialize) + { + using TestEnvironment env = TestEnvironment.Create(_output); + using RoutingHarness test = new(); + int node = test.CreateNodes(1)[0].NodeId; + Task>? creation = null; + test.Manager.RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, + new PacketHandler((_, _) => + { + creation = Task.Run(() => test.CreateNodes(1)); + creation.Wait(TimeSpan.FromSeconds(5)).ShouldBeTrue(); + })); + NodeShutdown shutdown = new(NodeShutdownReason.Requested); + try + { + if (deserialize) + { + using MemoryStream writeStream = new(); + using (ITranslator writer = BinaryTranslator.GetWriteTranslator(writeStream)) + { + shutdown.Translate(writer); + } + byte[] bytes = writeStream.ToArray(); + using MemoryStream readStream = new(bytes, 0, bytes.Length, writable: false, publiclyVisible: true); + using ITranslator reader = BinaryTranslator.GetReadTranslator(readStream, InterningBinaryReader.PoolingBuffer); + test.Manager.DeserializeAndRoutePacket(node, NodePacketType.NodeShutdown, reader); + } + else + { + test.Manager.RoutePacket(node, shutdown); + } + } + finally + { + if (creation is not null) + { + (await creation)[0].NodeId.ShouldBe(node); + } + } + _ = creation.ShouldNotBeNull(); + test.Manager.SendData(node, new TaskCachePacket(NodePacketType.TaskCacheResponse)); + test.Provider.SentCount.ShouldBe(1); + } + + [Fact] + public void DuplicateNodeIdsStillFailWithoutReplacingTheRoute() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using RoutingHarness test = new(); + int node = test.CreateNodes(1)[0].NodeId; + test.Provider.ForcedNodeId = node; + Should.Throw(() => test.CreateNodes(1)); + test.Manager.SendData(node, new TaskCachePacket(NodePacketType.TaskCacheResponse)); + test.Provider.SentCount.ShouldBe(1); + } + + private sealed class RoutingHarness : IBuildComponentHost, IDisposable + { + internal NodeManager Manager { get; } + internal FakeProvider Provider { get; } = new(); + public BuildParameters BuildParameters { get; } = new() { DisableInProcNode = true }; + public string Name => nameof(NodeManager_Tests); + public LegacyThreadingData LegacyThreadingData => throw new NotSupportedException(); + public ILoggingService LoggingService => throw new NotSupportedException(); + + internal RoutingHarness() + { + Manager = (NodeManager)NodeManager.CreateComponent(BuildComponentType.NodeManager); + Manager.InitializeComponent(this); + Manager.RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, new PacketHandler((_, _) => { })); + } + + internal IList CreateNodes(int count) + { + NodeConfiguration configuration = new(0, BuildParameters, [], +#if FEATURE_APPDOMAIN + null, +#endif + default); + return Manager.CreateNodes(configuration, NodeAffinity.OutOfProc, count); + } + + public IBuildComponent GetComponent(BuildComponentType type) => type switch + { + BuildComponentType.InProcNodeProvider or BuildComponentType.OutOfProcNodeProvider => Provider, + _ => throw new NotSupportedException(), + }; + public TComponent GetComponent(BuildComponentType type) where TComponent : IBuildComponent => (TComponent)GetComponent(type); + public void RegisterFactory(BuildComponentType factoryType, BuildComponentFactoryDelegate factory) => throw new NotSupportedException(); + public void Dispose() => Manager.ShutdownComponent(); + } + + private sealed class FakeProvider : INodeProvider + { + private int _sent; + internal int SentCount => Volatile.Read(ref _sent); + internal Action? OnSend { get; set; } + internal Action? OnCreate { get; set; } + internal int? ForcedNodeId { get; set; } + public NodeProviderType ProviderType => NodeProviderType.OutOfProc; + public int AvailableNodes => int.MaxValue; + + public IList CreateNodes(int nextNodeId, INodePacketFactory packetFactory, + Func configurationFactory, int numberOfNodesToCreate) + { + OnCreate?.Invoke(); + List nodes = new(numberOfNodesToCreate); + for (int i = 0; i < numberOfNodesToCreate; i++) + { + NodeInfo node = new(ForcedNodeId ?? nextNodeId + i, ProviderType); + configurationFactory(node).NodeId.ShouldBe(node.NodeId); + nodes.Add(node); + } + return nodes; + } + public void SendData(int node, INodePacket packet) + { + Interlocked.Increment(ref _sent); + OnSend?.Invoke(node, packet); + } + public void InitializeComponent(IBuildComponentHost host) { } + public void ShutdownComponent() { } + public void ShutdownConnectedNodes(bool enableReuse) { } + public void ShutdownAllNodes() { } + public IEnumerable GetProcesses() => []; + } + + private sealed class PacketHandler(Action received) : INodePacketHandler + { + public void PacketReceived(int node, INodePacket packet) => received(node, packet); + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskCacheAvailability_Tests.cs b/src/Build.UnitTests/BackEnd/TaskCacheAvailability_Tests.cs new file mode 100644 index 00000000000..ab2cbaf2d7b --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskCacheAvailability_Tests.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class TaskCacheAvailability_Tests(ITestOutputHelper output) +{ + [Fact] + public void ExplicitCacheModeRequiresAStorageBackend() + { + using BuildManager manager = new(); + using TestEnvironment env = TestEnvironment.Create(output); + if (TaskCacheStore.IsSupported) + { + manager.BeginBuild(new BuildParameters { TaskCache = true, BuildCacheDirectory = env.CreateFolder().Path, Loggers = [new MockLogger(output)] }); + manager.EndBuild(); + } + else + { + Should.Throw(() => manager.BeginBuild(new BuildParameters { TaskCache = true })) + .Message.ShouldContain("TaskCache"); + } + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskCacheDiagnosticCapture_Tests.cs b/src/Build.UnitTests/BackEnd/TaskCacheDiagnosticCapture_Tests.cs new file mode 100644 index 00000000000..ae3cf90b502 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskCacheDiagnosticCapture_Tests.cs @@ -0,0 +1,371 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.UnitTests.BackEnd; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.Logging; + +public class TaskCacheDiagnosticCapture_Tests +{ + private readonly ITestOutputHelper _output; + + public TaskCacheDiagnosticCapture_Tests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [InlineData(false, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, true)] + [InlineData(true, false, false)] + [InlineData(true, true, false)] + [InlineData(true, false, true)] + public void CapturesBeforeAsynchronousDispatchAndWarningPolicy(bool forwarded, bool demote, bool promote) + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output, LoggerMode.Asynchronous); + LoggingService service = test.Service; + if (demote) + { + service.WarningsAsMessages = new HashSet { "CS0001" }; + } + else if (promote) + { + service.WarningsAsErrors = new HashSet { "CS0001" }; + } + + service.LogBuildEvent(new BuildMessageEventArgs("block", null, "test", MessageImportance.Normal)); + test.BlockingLogger.Entered.Wait(TimeSpan.FromSeconds(30)).ShouldBeTrue(); + + using TaskCacheDiagnosticCapture capture = + ((ITaskCacheDiagnosticSource)service).CaptureTaskDiagnostics(test.Context); + BuildWarningEventArgs warning = Warning(test.Context); + if (forwarded) + { + service.PacketReceived(1, new LogMessagePacket(new KeyValuePair(0, warning))); + } + else + { + service.LogBuildEvent(warning); + } + + // The logging thread is still blocked on the preceding message. + capture.HasDiagnostics.ShouldBeTrue(); + test.Logger.Warnings.ShouldBeEmpty(); + test.Logger.Errors.ShouldBeEmpty(); + + test.BlockingLogger.Release.Set(); + service.WaitForLoggingToProcessEvents(); + if (!forwarded) + { + test.Logger.Warnings.Count.ShouldBe(demote || promote ? 0 : 1); + test.Logger.Errors.Count.ShouldBe(promote ? 1 : 0); + test.Logger.AllBuildEvents.ShouldContain(e => + e.Message == "diagnostic" && (demote ? e is BuildMessageEventArgs : true)); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CapturesErrorsSynchronously(bool forwarded) + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + using TaskCacheDiagnosticCapture capture = test.Service.CaptureTaskDiagnostics(test.Context); + BuildErrorEventArgs error = new(null, "CS0001", "test.cs", 1, 1, 1, 1, "diagnostic", null, "Csc") + { + BuildEventContext = test.Context + }; + + if (forwarded) + { + test.Service.PacketReceived(1, new LogMessagePacket(new KeyValuePair(0, error))); + } + else + { + test.Service.LogBuildEvent(error); + } + + capture.HasDiagnostics.ShouldBeTrue(); + capture.HasBlockingDiagnostics.ShouldBeTrue(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReplayableWarningIsSnapshottedBeforeAsyncMutationAndPolicy(bool promote) + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output, LoggerMode.Asynchronous); + test.Service.WarningsAsErrors = promote ? new HashSet { "CS0001" } : null; + test.Service.WarningsAsMessages = promote ? null : new HashSet { "CS0001" }; + test.Service.LogBuildEvent(new BuildMessageEventArgs("block", null, "test", MessageImportance.Normal)); + test.BlockingLogger.Entered.Wait(TimeSpan.FromSeconds(30)).ShouldBeTrue(); + using TaskCacheDiagnosticCapture capture = test.Service.CaptureTaskDiagnostics(test.Context); + capture.BeginReplayablePhase(); + MutableArgument argument = new(); + BuildWarningEventArgs warning = new("subcategory", "CS0001", "test.cs", 1, 2, 3, 4, + "message {0}", "help", "sender", "https://example.invalid/help", DateTime.UtcNow, argument) + { + BuildEventContext = test.Context, + }; + test.Service.LogBuildEvent(warning); + argument.Value = "mutated"; + warning.BuildEventContext = BuildEventContext.Invalid; + capture.HasDiagnostics.ShouldBeTrue(); + capture.HasBlockingDiagnostics.ShouldBeFalse(); + TaskCacheWarning[] warnings = capture.GetWarnings(); + warnings.Length.ShouldBe(1); + warnings[0].Message.ShouldBe("message original"); + warnings[0].ToEvent(test.Context).BuildEventContext.ShouldBe(test.Context); + // Restore context before releasing the unrelated asynchronous logging test. + warning.BuildEventContext = test.Context; + test.BlockingLogger.Release.Set(); + } + + [Fact] + public void CleanupWarningBlocksPublicationWithoutJoiningReplayPayload() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + using TaskCacheDiagnosticCapture capture = test.Service.CaptureTaskDiagnostics(test.Context); + capture.BeginReplayablePhase(); + test.Service.LogBuildEvent(Warning(test.Context)); + capture.HasBlockingDiagnostics.ShouldBeFalse(); + capture.EndReplayablePhase(); + test.Service.LogBuildEvent(Warning(test.Context)); + capture.HasBlockingDiagnostics.ShouldBeTrue(); + capture.GetWarnings().Length.ShouldBe(1); + test.Logger.Warnings.Count.ShouldBe(2); + } + + [Fact] + public void TaskHostEndsReplayCaptureBeforeInvokingFactoryCleanup() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + using TaskCacheDiagnosticCapture capture = test.Service.CaptureTaskDiagnostics(test.Context); + using TaskExecutionHost host = new(); + CleanupFactory factory = new(() => test.Service.LogBuildEvent(Warning(test.Context))); + host._UNITTESTONLY_TaskFactoryWrapper = new TaskFactoryWrapper(factory, null!, "Cleanup", TaskHostParameters.Empty); + typeof(TaskExecutionHost).GetProperty("TaskInstance", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(host, new DeclaredIOEchoTask()); + capture.BeginReplayablePhase(); + host.CleanupForBatch(diagnostics: capture); + capture.HasBlockingDiagnostics.ShouldBeTrue(); + capture.GetWarnings().ShouldBeEmpty(); + test.Logger.Warnings.Count.ShouldBe(1); + } + + [Fact] + public void RawErrorNotificationCannotBecomeReplayableAfterContinueOnError() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + using TaskCacheDiagnosticCapture capture = test.Service.CaptureTaskDiagnostics(test.Context); + capture.BeginReplayablePhase(); + test.Service.RecordTaskError(test.Context); + test.Service.LogBuildEvent(Warning(test.Context)); + capture.HasBlockingDiagnostics.ShouldBeTrue(); + capture.GetWarnings().ShouldBeEmpty(); + } + + [Fact] + public void RawErrorBlocksEvenInsideReplayablePhase() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + using TaskCacheDiagnosticCapture capture = test.Service.CaptureTaskDiagnostics(test.Context); + capture.BeginReplayablePhase(); + test.Service.LogBuildEvent(new BuildErrorEventArgs(null, "ERR1", "test.cs", 1, 1, 1, 1, "error", null, "task") + { + BuildEventContext = test.Context, + }); + capture.HasBlockingDiagnostics.ShouldBeTrue(); + capture.GetWarnings().ShouldBeEmpty(); + } + + [Fact] + public void TooManyWarningsDisablePublicationButDoNotSuppressLogging() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + using TaskCacheDiagnosticCapture capture = test.Service.CaptureTaskDiagnostics(test.Context); + capture.BeginReplayablePhase(); + for (int i = 0; i <= TaskCacheWarning.MaximumCount; i++) + { + test.Service.LogBuildEvent(Warning(test.Context)); + } + capture.HasBlockingDiagnostics.ShouldBeTrue(); + capture.GetWarnings().Length.ShouldBe(TaskCacheWarning.MaximumCount); + test.Logger.Warnings.Count.ShouldBe(TaskCacheWarning.MaximumCount + 1); + } + + private sealed class MutableArgument + { + internal string Value = "original"; + public override string ToString() => Value; + } + + private sealed class CleanupFactory(Action cleanup) : ITaskFactory + { + public string FactoryName => "Cleanup"; + public Type TaskType => typeof(DeclaredIOEchoTask); + public bool Initialize(string taskName, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) => true; + public TaskPropertyInfo[] GetTaskParameters() => []; + public ITask CreateTask(IBuildEngine taskFactoryLoggingHost) => new DeclaredIOEchoTask(); + public void CleanupTask(ITask task) => cleanup(); + } + + [Fact] + public void MatchesNodeProjectTargetAndTask() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + using TaskCacheDiagnosticCapture capture = test.Service.CaptureTaskDiagnostics(test.Context); + BuildEventContext context = test.Context; + test.Service.ProcessLoggingEvent(Warning(new BuildEventContext(context.NodeId + 1, context.TargetId, context.ProjectContextId, 1))); + test.Service.ProcessLoggingEvent(Warning(new BuildEventContext(context.NodeId, context.TargetId, context.ProjectContextId + 1, 1))); + test.Service.ProcessLoggingEvent(Warning(new BuildEventContext(context.NodeId, context.TargetId + 1, context.ProjectContextId, 1))); + test.Service.ProcessLoggingEvent(Warning(null)); + test.Service.ProcessLoggingEvent(new BuildMessageEventArgs("message", null, "test", MessageImportance.Normal) + { + BuildEventContext = context + }); + test.Service.ProcessLoggingEvent(Warning(new BuildEventContext(context.NodeId, context.TargetId, context.ProjectContextId, 123))); + capture.HasDiagnostics.ShouldBeFalse(); + + test.Service.ProcessLoggingEvent(Warning(context)); + capture.HasDiagnostics.ShouldBeTrue(); + } + + [Fact] + public void OverlappingCapturesDisposeIndependentlyAndRetainTheirResult() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + using TaskCacheDiagnosticCapture first = test.Service.CaptureTaskDiagnostics(test.Context); + using TaskCacheDiagnosticCapture second = test.Service.CaptureTaskDiagnostics(test.Context); + first.Dispose(); + first.Dispose(); + test.Service.LogBuildEvent(Warning(test.Context)); + first.HasDiagnostics.ShouldBeFalse(); + second.HasDiagnostics.ShouldBeTrue(); + second.Dispose(); + second.HasDiagnostics.ShouldBeTrue(); + + using TaskCacheDiagnosticCapture subsequent = test.Service.CaptureTaskDiagnostics(test.Context); + subsequent.HasDiagnostics.ShouldBeFalse(); + test.Service.LogBuildEvent(Warning(test.Context)); + subsequent.HasDiagnostics.ShouldBeTrue(); + } + + [Fact] + public void ConcurrentCapturesAndDiagnosticsDoNotLoseRegistrations() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + using TaskCacheDiagnosticCapture survivor = test.Service.CaptureTaskDiagnostics(test.Context); + + Parallel.For(0, 32, i => + { + BuildEventContext context = new(test.Context.NodeId, i + 10, test.Context.ProjectContextId, i); + using TaskCacheDiagnosticCapture capture = test.Service.CaptureTaskDiagnostics(context); + test.Service.LogBuildEvent(Warning(context)); + capture.HasDiagnostics.ShouldBeTrue(); + }); + + survivor.HasDiagnostics.ShouldBeFalse(); + Parallel.For(0, 32, _ => test.Service.LogBuildEvent(Warning(test.Context))); + survivor.HasDiagnostics.ShouldBeTrue(); + } + + [Fact] + public void RejectsMissingTaskContext() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using TestLoggingService test = new(_output); + Should.Throw(() => test.Service.CaptureTaskDiagnostics(null!)); + Should.Throw(() => test.Service.CaptureTaskDiagnostics(BuildEventContext.Invalid)); + Should.Throw(() => test.Service.CaptureTaskDiagnostics( + new BuildEventContext(test.Context.NodeId, test.Context.TargetId, test.Context.ProjectContextId, BuildEventContext.InvalidTaskId))); + } + + private static BuildWarningEventArgs Warning(BuildEventContext? context) => + new(null, "CS0001", "test.cs", 1, 1, 1, 1, "diagnostic", null, "Csc") + { + BuildEventContext = context + }; + + private sealed class TestLoggingService : IDisposable + { + public LoggingService Service { get; } + public BuildEventContext Context { get; } + public MockLogger Logger { get; } + public BlockingLogger BlockingLogger { get; } = new(); + + public TestLoggingService(ITestOutputHelper output, LoggerMode mode = LoggerMode.Synchronous) + { + MockHost host = new(); + BuildRequestData request = new("test.proj", new Dictionary(), "Current", ["Build"], null); + ((ConfigCache)host.GetComponent(BuildComponentType.ConfigCache)).AddConfiguration(new BuildRequestConfiguration(1, request, request.ExplicitlySpecifiedToolsVersion)); + Service = (LoggingService)LoggingService.CreateLoggingService(mode, 1); + Service.InitializeComponent(host); + Logger = new MockLogger(output); + Service.RegisterLogger(Logger); + if (mode == LoggerMode.Asynchronous) + { + Service.RegisterLogger(BlockingLogger); + } + + BuildEventContext project = Service.LogProjectStarted( + new BuildEventContext(1, 1, 1, 1), 0, 1, BuildEventContext.Invalid, "test.proj", "Build", [], []); + Context = new BuildEventContext(project.SubmissionId, project.NodeId, project.ProjectInstanceId, project.ProjectContextId, 1, 1); + } + + public void Dispose() + { + BlockingLogger.Release.Set(); + Service.ShutdownComponent(); + BlockingLogger.Entered.Dispose(); + BlockingLogger.Release.Dispose(); + } + } + + private sealed class BlockingLogger : ILogger + { + public ManualResetEventSlim Entered { get; } = new(); + public ManualResetEventSlim Release { get; } = new(); + public LoggerVerbosity Verbosity { get; set; } = LoggerVerbosity.Diagnostic; + public string? Parameters { get; set; } + + public void Initialize(IEventSource eventSource) + { + eventSource.MessageRaised += (_, e) => + { + if (e.Message == "block") + { + Entered.Set(); + Release.Wait(TimeSpan.FromSeconds(30)); + } + }; + } + + public void Shutdown() + { + } + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskCacheGraph_Tests.cs b/src/Build.UnitTests/BackEnd/TaskCacheGraph_Tests.cs new file mode 100644 index 00000000000..bd53e286394 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskCacheGraph_Tests.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Graph; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class TaskCacheGraph_Tests(ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + private const string CacheMode = "MSBuildTaskCacheEnabled"; + public static bool IsSupported => TaskCacheStore.IsSupported; + + private const string ProjectText = """ + + $(MSBuildTaskCacheEnabled) + + + + $(MSBuildTaskCacheEnabled) + + + + + + """; + + [ConditionalTheory(typeof(TaskCacheGraph_Tests), nameof(IsSupported))] + [InlineData(true, null, "true")] + [InlineData(true, "false", "true")] + [InlineData(true, "", "true")] + [InlineData(false, null, "")] + [InlineData(false, "caller", "caller")] + public void FileBasedGraphModeIsAvailableDuringEvaluationWithoutMutatingGlobals(bool enabled, string? suppliedMode, string expectedMode) + { + using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable(CacheMode, null); + string project = env.CreateFile("mode.proj", ProjectText).Path; + Dictionary globals = new(); + if (suppliedMode is not null) + { + globals["msbuildtaskcacheenabled"] = suppliedMode; + } + GraphBuildRequestData request = new(project, globals, ["Build"], null); + GraphBuildResult result = Build(env, request, enabled); + result.ShouldHaveSucceeded(); + var nodeResult = result.ResultsByNode.Single(); + nodeResult.Value["Build"].Items.Single().ItemSpec.ShouldBe("mode=" + expectedMode); + nodeResult.Value["Build"].Items.Single().GetMetadata("LiveMode").ShouldBe(expectedMode); + nodeResult.Key.ProjectInstance.GetPropertyValue(CacheMode).ShouldBe(expectedMode); + globals.Count.ShouldBe(suppliedMode is null ? 0 : 1); + if (suppliedMode is not null) + { + globals["msbuildtaskcacheenabled"].ShouldBe(suppliedMode); + } + } + + [ConditionalFact(typeof(TaskCacheGraph_Tests), nameof(IsSupported))] + public void EveryEntryPointReceivesModeIncludingNullAndReadOnlyGlobals() + { + using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable(CacheMode, null); + Dictionary original = new() + { + [CacheMode] = "false", + ["Label"] = "caller", + }; + ReadOnlyDictionary globals = new(original); + ProjectGraphEntryPoint[] entries = + [ + new(env.CreateFile("first.proj", ProjectText).Path, globals), + new(env.CreateFile("second.proj", ProjectText).Path, globals), + new(env.CreateFile("third.proj", ProjectText).Path), + ]; + GraphBuildResult result = Build(env, new GraphBuildRequestData(entries, ["Build"]), enabled: true); + result.ShouldHaveSucceeded(); + result.ResultsByNode.Count.ShouldBe(3); + foreach (var nodeResult in result.ResultsByNode) + { + nodeResult.Value["Build"].Items.Single().ItemSpec.ShouldBe("mode=true"); + nodeResult.Key.ProjectInstance.GlobalProperties[CacheMode].ShouldBe("true"); + string expectedLabel = Path.GetFileName(nodeResult.Key.ProjectInstance.FullPath) == "third.proj" ? "" : "caller"; + nodeResult.Value["Build"].Items.Single().GetMetadata("Label").ShouldBe(expectedLabel); + } + original.Count.ShouldBe(2); + original[CacheMode].ShouldBe("false"); + entries[0].GlobalProperties.ShouldBeSameAs(globals); + entries[1].GlobalProperties.ShouldBeSameAs(globals); + entries[2].GlobalProperties.ShouldBeNull(); + } + + [ConditionalFact(typeof(TaskCacheGraph_Tests), nameof(IsSupported))] + public void ReferencedGraphProjectsInheritTheEntryPointMode() + { + using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable(CacheMode, null); + var folder = env.CreateFolder(); + env.CreateFile(folder, "child.proj", ProjectText); + string parent = env.CreateFile(folder, "parent.proj", ProjectText.Replace("", """ + + + + + + """)).Path; + GraphBuildResult result = Build(env, new GraphBuildRequestData(parent, new Dictionary(), ["Build"], null), enabled: true); + result.ShouldHaveSucceeded(); + result.ResultsByNode.Count.ShouldBe(2); + foreach (BuildResult nodeResult in result.ResultsByNode.Values) + { + nodeResult["Build"].Items.Single().ItemSpec.ShouldBe("mode=true"); + } + } + + [ConditionalFact(typeof(TaskCacheGraph_Tests), nameof(IsSupported))] + public void SuppliedGraphRetainsItsCallerEvaluationWithoutReevaluation() + { + using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable(CacheMode, null); + string project = env.CreateFile("supplied.proj", ProjectText).Path; + Dictionary globals = new() { [CacheMode] = "caller" }; + ProjectGraph graph = new(project, globals); + ProjectGraphNode node = graph.EntryPointNodes.Single(); + File.WriteAllText(project, """"""); + GraphBuildResult result = Build(env, new GraphBuildRequestData(graph, ["Build"]), enabled: true); + result.ShouldHaveSucceeded(); + result.ResultsByNode.Keys.Single().ShouldBeSameAs(node); + result[node]["Build"].Items.Single().ItemSpec.ShouldBe("mode=caller"); + node.ProjectInstance.GetPropertyValue(CacheMode).ShouldBe("caller"); + globals[CacheMode].ShouldBe("caller"); + } + + private GraphBuildResult Build(TestEnvironment env, GraphBuildRequestData request, bool enabled) + { + using BuildManager manager = new(); + BuildParameters parameters = new() + { + TaskCache = enabled, + BuildCacheDirectory = enabled ? env.CreateFolder().Path : env.CreateFile("not-a-cache", "untouched").Path, + EnableNodeReuse = false, + Loggers = [new MockLogger(_output)], + }; + return manager.Build(parameters, request); + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskCacheOwner_Tests.cs b/src/Build.UnitTests/BackEnd/TaskCacheOwner_Tests.cs new file mode 100644 index 00000000000..eb2bb5666ef --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskCacheOwner_Tests.cs @@ -0,0 +1,379 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class TaskCacheOwner_Tests(ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + private static readonly string s_digest = new('A', 64); + public static bool IsSupported => TaskCacheStore.IsSupported; + + [ConditionalFact(typeof(TaskCacheOwner_Tests), nameof(IsSupported))] + public void TopLevelBuildOwnershipFailsFastAndSequentialBuildsReacquire() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string directory = env.CreateFolder().Path; + using BuildManager first = new(); + using BuildManager second = new(); + BuildParameters Parameters() => new() + { + TaskCache = true, BuildCacheDirectory = directory, Loggers = [new MockLogger(_output)], + }; + first.BeginBuild(Parameters()); + try + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + Should.Throw(() => second.BeginBuild(Parameters())).Message.ShouldContain(directory); + watch.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(5)); + } + finally + { + first.EndBuild(); + } + second.BeginBuild(Parameters()); + second.EndBuild(); + first.BeginBuild(Parameters()); + first.EndBuild(); + } + + [ConditionalFact(typeof(TaskCacheOwner_Tests), nameof(IsSupported))] + public void CancellationAndFailedInitializationReleaseOwnership() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string directory = env.CreateFolder().Path; + using CancellationTokenSource cancellation = new(); + using (TaskCacheOwner owner = new(directory, (_, _) => { }, cancellation.Token)) + { + cancellation.Cancel(); + Should.Throw(() => owner.ReadManifest(s_digest, 1024)); + } + using (TaskCacheOwner reopened = new(directory, (_, _) => { }, default)) + { + reopened.ReadManifest(s_digest, 1024).ShouldBeNull(); + } + + string invalid = env.CreateFile("not-a-directory", "untouched").Path; + Should.Throw(() => new TaskCacheOwner(invalid, (_, _) => { }, default)); + File.Delete(invalid); + using TaskCacheOwner recovered = new(invalid, (_, _) => { }, default); + recovered.ReadManifest(s_digest, 1024).ShouldBeNull(); + } + + [Fact] + public void DirectoryConfigurationIgnoresAllEnvironmentOverrides() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string expected = BuildParameters.ResolveBuildCacheDirectory(); + string other = env.CreateFolder().Path; + env.SetEnvironmentVariable("MSBUILD_TASK_CACHE_DIR", other); + env.SetEnvironmentVariable("MSBuildTaskCacheDirectory", other); + env.SetEnvironmentVariable("XDG_CACHE_HOME", other); + BuildParameters.ResolveBuildCacheDirectory().ShouldBe(expected); + BuildParameters.ResolveBuildCacheDirectory(" \t").ShouldBe(expected); + BuildParameters.ResolveBuildCacheDirectory(other).ShouldBe(other); + BuildParameters.ResolveBuildCacheDirectory("relative-cache").ShouldBe(Path.GetFullPath("relative-cache")); + new BuildParameters + { + GlobalProperties = new Dictionary + { + ["MSBuildTaskCacheDirectory"] = other, + ["BuildCacheDirectory"] = other, + }, + }.BuildCacheDirectory.ShouldBe(expected); + } + + [Fact] + public void BuildCacheDirectoryReplacesTheExperimentalApi() + { + typeof(BuildParameters).GetProperty(nameof(BuildParameters.BuildCacheDirectory)).ShouldNotBeNull(); + typeof(BuildParameters).GetProperty("TaskCacheDirectory").ShouldBeNull(); + BuildParameters parameters = new() { BuildCacheDirectory = "relative-cache" }; + parameters.TaskCache.ShouldBeFalse(); + parameters.Clone().BuildCacheDirectory.ShouldBe(Path.GetFullPath("relative-cache")); + } + + [Fact] + public async Task RpcCancellationAcknowledgesDrainedOperation() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using CancellationTokenSource cancellation = new(); + using ManualResetEventSlim entered = new(); + using ManualResetEventSlim completed = new(); + using FakeBackend backend = new(token => + { + entered.Set(); + try + { + token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + token.ThrowIfCancellationRequested(); + return s_digest; + } + finally + { + completed.Set(); + } + }); + TaskCacheClient? client = null; + string path = env.CreateFile().Path; + using TaskCacheOwner owner = new(env.CreateFolder().Path, (_, packet) => client!.PacketReceived(0, packet), default, backend); + using (client = new TaskCacheClient(env.CreateFolder().Path, packet => owner.PacketReceived(7, packet))) + { + Task operation = Task.Run(() => client.PutFile(path, cancellation.Token)); + entered.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + cancellation.Cancel(); + await Should.ThrowAsync(async () => await operation); + completed.IsSet.ShouldBeTrue(); + } + owner.Dispose(); + backend.Disposed.ShouldBeTrue(); + } + + [Fact] + public async Task OwnerAllowsConcurrentStorageOperationsAndDrainsShutdown() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using CountdownEvent entered = new(2); + using FakeBackend backend = new(token => + { + entered.Signal(); + entered.Wait(TimeSpan.FromSeconds(10), token).ShouldBeTrue(); + token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + token.ThrowIfCancellationRequested(); + return s_digest; + }); + using TaskCacheOwner owner = new(env.CreateFolder().Path, (_, _) => { }, default, backend); + string path = env.CreateFile().Path; + Task first = Task.Run(() => owner.PutFile(path)); + Task second = Task.Run(() => owner.PutFile(path)); + entered.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + owner.Dispose(); + await Should.ThrowAsync(async () => await first); + await Should.ThrowAsync(async () => await second); + backend.Disposed.ShouldBeTrue(); + } + + [Fact] + public async Task CancellationCallbackFailureStillDrainsAndClosesStorage() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using ManualResetEventSlim entered = new(); + using FakeBackend backend = new(token => + { + using CancellationTokenRegistration registration = token.Register(() => throw new InvalidOperationException("cancellation callback failed")); + entered.Set(); + token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + token.ThrowIfCancellationRequested(); + return s_digest; + }); + using TaskCacheOwner owner = new(env.CreateFolder().Path, (_, _) => { }, default, backend); + string path = env.CreateFile().Path; + Task operation = Task.Run(() => owner.PutFile(path)); + entered.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + Should.Throw(owner.Dispose); + await Should.ThrowAsync(async () => await operation); + backend.Disposed.ShouldBeTrue(); + } + + [Fact] + public async Task ConnectionLossUnblocksPendingRequest() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using ManualResetEventSlim sent = new(); + using TaskCacheClient client = new(env.CreateFolder().Path, _ => sent.Set()); + Task request = Task.Run(() => client.ReadManifest(s_digest, 1024)); + sent.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + client.Dispose(); + await Should.ThrowAsync(async () => await request); + } + + [Fact] + public async Task FailedResponseSendReportsFailureAndDrainsOwner() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using ManualResetEventSlim reported = new(); + using FakeBackend backend = new(_ => s_digest); + TaskCacheClient? client = null; + using TaskCacheOwner owner = new(env.CreateFolder().Path, (_, _) => throw new IOException("send failed"), + default, backend, _ => + { + client!.Dispose(); + reported.Set(); + }); + using (client = new TaskCacheClient(env.CreateFolder().Path, packet => owner.PacketReceived(3, packet))) + { + string path = env.CreateFile().Path; + await Should.ThrowAsync(() => Task.Run(() => client.PutFile(path))); + reported.Wait(TimeSpan.FromSeconds(10)).ShouldBeTrue(); + } + owner.Dispose(); + backend.Disposed.ShouldBeTrue(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void RpcRejectsMismatchedResponse(bool mismatchedOperation) + { + using TestEnvironment env = TestEnvironment.Create(_output); + TaskCacheClient? client = null; + using (client = new TaskCacheClient(env.CreateFolder().Path, packet => + { + TaskCachePacket request = (TaskCachePacket)packet; + client!.PacketReceived(0, new TaskCachePacket(NodePacketType.TaskCacheResponse) + { + Id = request.Id, + Operation = mismatchedOperation ? (int)TaskCacheOperation.Publish : request.Operation, + Key = mismatchedOperation ? request.Key : new string('B', 64), + }); + })) + { + Should.Throw(() => client.ReadManifest(s_digest, 1024)); + } + } + + [Theory] + [InlineData("relative.txt")] + [InlineData("")] + public void OwnerRejectsNonAbsoluteArtifactPaths(string path) + { + using TestEnvironment env = TestEnvironment.Create(_output); + using FakeBackend backend = new(_ => s_digest); + using TaskCacheOwner owner = new(env.CreateFolder().Path, (_, _) => { }, default, backend); + Should.Throw(() => owner.PutFile(path)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EmptyContentClientOpenDoesNotDependOnExportedFile(bool pathExists) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string directory = env.CreateFolder().Path; + string path = Path.Combine(directory, "empty-content"); + if (pathExists) + { + File.WriteAllBytes(path, []); + } + int requests = 0; + TaskCacheClient? client = null; + using (client = new TaskCacheClient(directory, packet => + { + requests++; + TaskCachePacket request = (TaskCachePacket)packet; + request.Operation.ShouldBe((int)TaskCacheOperation.Open); + client!.PacketReceived(0, new TaskCachePacket(NodePacketType.TaskCacheResponse) + { + Id = request.Id, Operation = request.Operation, Key = request.Key, Path = path, + }); + })) + { + using Stream stream = client.Open(EmptyDigest()); + stream.Length.ShouldBe(0); + stream.ReadByte().ShouldBe(-1); + stream.CanWrite.ShouldBeFalse(); + requests.ShouldBe(1); + File.Exists(path).ShouldBe(pathExists); + } + } + + [Theory] + [InlineData("error")] + [InlineData("mismatch")] + [InlineData("cancelled")] + public void EmptyContentRequiresSuccessfulMatchingOwnerResponse(string outcome) + { + using TestEnvironment env = TestEnvironment.Create(_output); + TaskCacheClient? client = null; + using (client = new TaskCacheClient(env.CreateFolder().Path, packet => + { + TaskCachePacket request = (TaskCachePacket)packet; + client!.PacketReceived(0, new TaskCachePacket(NodePacketType.TaskCacheResponse) + { + Id = request.Id, + Operation = request.Operation, + Key = outcome == "mismatch" ? s_digest : request.Key, + Error = outcome == "error" ? "owner failed" : null, + Cancelled = outcome == "cancelled", + }); + })) + { + if (outcome == "cancelled") + { + Should.Throw(() => client.Open(EmptyDigest())); + } + else if (outcome == "mismatch") + { + Should.Throw(() => client.Open(EmptyDigest())); + } + else + { + Should.Throw(() => client.Open(EmptyDigest())).Message.ShouldBe("owner failed"); + } + } + } + + [Fact] + public void EmptyContentHonorsCancellationBeforeSending() + { + using TestEnvironment env = TestEnvironment.Create(_output); + using CancellationTokenSource cancellation = new(); + cancellation.Cancel(); + int requests = 0; + using TaskCacheClient client = new(env.CreateFolder().Path, _ => requests++); + Should.Throw(() => client.Open(EmptyDigest(), cancellation.Token)); + requests.ShouldBe(0); + } + + [Theory] + [InlineData(null)] + [InlineData("relative")] + [InlineData("outside")] + public void NonemptyContentStillRequiresAContainedPath(string? kind) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string directory = env.CreateFolder().Path; + string? path = kind == "outside" ? env.CreateFile().Path : kind; + TaskCacheClient? client = null; + using (client = new TaskCacheClient(directory, packet => + { + TaskCachePacket request = (TaskCachePacket)packet; + client!.PacketReceived(0, new TaskCachePacket(NodePacketType.TaskCacheResponse) + { + Id = request.Id, Operation = request.Operation, Key = request.Key, Path = path, + }); + })) + { + Should.Throw(() => client.Open(s_digest)); + } + } + + private static string EmptyDigest() + { + using SHA256 hash = SHA256.Create(); + return BitConverter.ToString(hash.ComputeHash([])).Replace("-", String.Empty); + } + + private sealed class FakeBackend(Func putFile) : TaskCacheBackend + { + internal bool Disposed { get; private set; } + internal override byte[]? ReadManifest(string key, int maximumSize, CancellationToken cancellationToken = default) => null; + internal override Stream Open(string digest, CancellationToken cancellationToken = default) => throw new NotSupportedException(); + internal override string ContentPath(string digest) => throw new NotSupportedException(); + internal override string PutFile(string path, CancellationToken cancellationToken = default) => putFile(cancellationToken); + internal override bool Publish(string key, byte[] manifest, Func? canPublish = null, + IReadOnlyList? artifacts = null, CancellationToken cancellationToken = default) => true; + public override void Dispose() => Disposed = true; + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskCachePackaging_Tests.cs b/src/Build.UnitTests/BackEnd/TaskCachePackaging_Tests.cs new file mode 100644 index 00000000000..5e8a60a759e --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskCachePackaging_Tests.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if FEATURE_BUILDXL_TASK_CACHE +using System; +using System.IO; +using System.Linq; +using MSBuild.Bootstrap.Utils.Tasks; +using Newtonsoft.Json.Linq; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class TaskCachePackaging_Tests(ITestOutputHelper output) +{ + [Fact] + public void BundlesPrivateRuntimeClosureAndPreservesRuntimeFallbacks() + { + using TestEnvironment env = TestEnvironment.Create(output); + GetTaskCachePackageContents task = CreateTask(env, out _); + task.Execute().ShouldBeTrue(); + var files = task.PackageFiles.ToDictionary(item => item.GetMetadata("PackagePath"), item => item.ItemSpec); + files.Keys.ShouldContain("lib/net11.0/Cache.Root.dll"); + files.Keys.ShouldContain("lib/net11.0/Cache.Leaf.dll"); + files.Keys.ShouldNotContain(path => path.Contains("Shared.Public") || path.Contains("Unrelated.Private")); + files.Keys.ShouldContain("runtimes/linux-x64/native/libcache.so"); + files["runtimes/win/lib/net11.0/Cache.Leaf.dll"].Replace('\\', '/').ShouldEndWith("runtimes/win/lib/net11.0/Cache.Leaf.dll"); + files["runtimes/win/lib/net11.0/Cache.Root.dll"].Replace('\\', '/').ShouldEndWith("lib/net11.0/Cache.Root.dll"); + files["runtimes/win-x64/lib/net11.0/Cache.Leaf.dll"].Replace('\\', '/').ShouldEndWith("runtimes/win/lib/net11.0/Cache.Leaf.dll"); + files["runtimes/win-x64/lib/net11.0/Cache.Root.dll"].Replace('\\', '/').ShouldEndWith("runtimes/win-x64/lib/net11.0/Cache.Root.dll"); + files.Keys.ShouldContain("task-cache-notices/net11.0/Cache.Root/1.0.0/LICENSE.txt"); + } + + [Fact] + public void RuntimeImplementationIsIncludedInEveryRuntimeGroup() + { + using TestEnvironment env = TestEnvironment.Create(output); + GetTaskCachePackageContents task = CreateTask(env, out _); + task.RuntimeAssembly = env.CreateFile("Microsoft.Build.TaskCache.dll", "runtime").Path; + task.Execute().ShouldBeTrue(); + var paths = task.PackageFiles.Select(item => item.GetMetadata("PackagePath")).ToArray(); + paths.ShouldContain("lib/net11.0/Microsoft.Build.TaskCache.dll"); + paths.ShouldContain("runtimes/win/lib/net11.0/Microsoft.Build.TaskCache.dll"); + paths.ShouldContain("runtimes/win-x64/lib/net11.0/Microsoft.Build.TaskCache.dll"); + } + + [Fact] + public void RocksDbBuildAssetsAreMappedToAllSupportedNativeRids() + { + using TestEnvironment env = TestEnvironment.Create(output); + GetTaskCachePackageContents task = CreateTask(env, out JObject assets); + assets["targets"]!["net11.0"]!["Cache.Root/1.0.0"]!["dependencies"]!["RocksDbNative"] = "1.0.0"; + assets["targets"]!["net11.0"]!["RocksDbNative/1.0.0"] = JObject.Parse("""{"type":"package"}"""); + string root = ((JObject)assets["packageFolders"]!).Properties().Single().Name; + string[] filenames = ["rocksdb.dll", "librocksdb.so", "librocksdb.dylib"]; + JArray files = []; + foreach (string filename in filenames) + { + string relative = "build/native/amd64/" + filename; + string path = Path.Combine(root, "rocksdbnative/1.0.0", relative); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, "native fixture"); + files.Add(relative); + } + assets["libraries"]!["RocksDbNative/1.0.0"] = new JObject { ["path"] = "rocksdbnative/1.0.0", ["files"] = files }; + File.WriteAllText(task.AssetsFile, assets.ToString()); + task.Execute().ShouldBeTrue(); + var paths = task.PackageFiles.Select(item => item.GetMetadata("PackagePath")).ToArray(); + paths.ShouldContain("runtimes/win-x64/native/rocksdb.dll"); + paths.ShouldContain("runtimes/linux-x64/native/librocksdb.so"); + paths.ShouldContain("runtimes/osx-x64/native/librocksdb.dylib"); + } + + [Fact] + public void MissingDependencyFailsRatherThanPublishingAnIncompleteClosure() + { + using TestEnvironment env = TestEnvironment.Create(output); + GetTaskCachePackageContents task = CreateTask(env, out JObject assets); + assets["targets"]!["net11.0"]!["Cache.Root/1.0.0"]!["dependencies"]!["Missing"] = "1.0.0"; + File.WriteAllText(task.AssetsFile, assets.ToString()); + Should.Throw(() => task.Execute()).Message.ShouldContain("Missing"); + } + + [Fact] + public void MissingManagedAssemblyFailsBeforePacking() + { + using TestEnvironment env = TestEnvironment.Create(output); + GetTaskCachePackageContents task = CreateTask(env, out _); + task.Execute().ShouldBeTrue(); + File.Delete(task.PackageFiles.Single(item => item.GetMetadata("PackagePath") == "lib/net11.0/Cache.Leaf.dll").ItemSpec); + Should.Throw(() => task.Execute()); + } + + private static GetTaskCachePackageContents CreateTask(TestEnvironment env, out JObject assets) + { + string directory = env.CreateFolder().Path; + assets = JObject.Parse(""" + { + "targets": { "net11.0": { + "Cache.Root/1.0.0": { "type":"package", "dependencies":{"Cache.Leaf":"1.0.0","Shared.Public":"1.0.0"}, + "runtime":{"lib/net11.0/Cache.Root.dll":{}}, + "runtimeTargets":{"runtimes/win-x64/lib/net11.0/Cache.Root.dll":{"rid":"win-x64","assetType":"runtime"}} }, + "Cache.Leaf/1.0.0": { "type":"package", "runtime":{"lib/net11.0/Cache.Leaf.dll":{}}, + "runtimeTargets":{ + "runtimes/win/lib/net11.0/Cache.Leaf.dll":{"rid":"win","assetType":"runtime"}, + "runtimes/linux-x64/native/libcache.so":{"rid":"linux-x64","assetType":"native"}} }, + "Shared.Public/1.0.0": { "type":"package", "runtime":{"lib/net11.0/Shared.Public.dll":{}} }, + "Unrelated.Private/1.0.0": { "type":"package", "runtime":{"lib/net11.0/Unrelated.Private.dll":{}} } + }}, + "project":{"frameworks":{"net11.0":{"dependencies":{ + "Cache.Root":{"suppressParent":"All"},"Shared.Public":{},"Unrelated.Private":{"suppressParent":"All"} + }}}}, + "libraries":{ + "Cache.Root/1.0.0":{"path":"cache.root/1.0.0","files":["lib/net11.0/Cache.Root.dll","runtimes/win-x64/lib/net11.0/Cache.Root.dll","LICENSE.txt"]}, + "Cache.Leaf/1.0.0":{"path":"cache.leaf/1.0.0","files":["lib/net11.0/Cache.Leaf.dll","runtimes/win/lib/net11.0/Cache.Leaf.dll","runtimes/linux-x64/native/libcache.so"]}, + "Shared.Public/1.0.0":{"path":"shared.public/1.0.0","files":[]}, + "Unrelated.Private/1.0.0":{"path":"unrelated.private/1.0.0","files":[]} + } + } + """); + assets["packageFolders"] = new JObject { [directory] = new JObject() }; + foreach (JProperty library in ((JObject)assets["libraries"]!).Properties()) + { + foreach (string? file in library.Value["files"]!.Values()) + { + string path = Path.Combine(directory, (string)library.Value["path"]!, file!); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, "fixture"); + } + } + string assetsFile = Path.Combine(directory, "project.assets.json"); + File.WriteAllText(assetsFile, assets.ToString()); + string graph = Path.Combine(directory, "runtime.json"); + File.WriteAllText(graph, """{"runtimes":{"win-x64":{"#import":["win"]},"win":{"#import":["any"]},"any":{}}}"""); + return new GetTaskCachePackageContents + { + AssetsFile = assetsFile, TargetFramework = "net11.0", FrameworkMoniker = ".NETCoreApp,Version=v11.0", + RuntimeGraphFile = graph, CacheRoots = ["Cache.Root"], + }; + } +} +#endif diff --git a/src/Build.UnitTests/BackEnd/TaskCacheStore_Tests.cs b/src/Build.UnitTests/BackEnd/TaskCacheStore_Tests.cs new file mode 100644 index 00000000000..327762b55c1 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskCacheStore_Tests.cs @@ -0,0 +1,439 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.BackEnd; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class TaskCacheStore_Tests(ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + private static readonly string s_key = HashBytes([1]); + + public static bool IsSupported => TaskCacheStore.IsSupported; + +#if NET + public static bool IsSupportedOnUnix => IsSupported && !OperatingSystem.IsWindows(); +#endif + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void StoresAndRestoresStateAndFileContents() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string outputPath = env.CreateFile("result.txt", "content").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + DateTime timestamp = DateTime.UtcNow.AddDays(-1); + File.SetLastWriteTimeUtc(outputPath, timestamp); + store.Store(s_key, [outputPath], [1, 2, 3]); + File.Delete(outputPath); + store.TryRestore(s_key, [outputPath], out byte[] state).ShouldBeTrue(); + state.ShouldBe([1, 2, 3]); + File.ReadAllText(outputPath).ShouldBe("content"); + File.GetLastWriteTimeUtc(outputPath).ShouldBeGreaterThan(timestamp); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void ChecksAllBlobsBeforeTouchingOutputs() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string first = env.CreateFile("first.txt", "first").Path; + string second = env.CreateFile("second.txt", "second").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [first, second], []); + File.WriteAllText(first, "untouched"); + CorruptContent(backend, "second"); + Should.Throw(() => store.TryRestore(s_key, [first, second], out _)); + File.ReadAllText(first).ShouldBe("untouched"); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void RejectsManifestPathsOutsideTheCurrentContract() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string first = env.CreateFile("first.txt", "first").Path; + string other = env.CreateFile("other.txt", "untouched").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [first], []); + Should.Throw(() => store.TryRestore(s_key, [other], out _)); + File.ReadAllText(other).ShouldBe("untouched"); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void RejectsCacheDirectoryOverlap() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + Should.Throw(() => store.Store(s_key, [Path.Combine(cache, "output.txt")], [])); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void ValidatesRawStateBeforeTouchingOutputs() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("result.txt", "cached").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [path], [42]); + File.WriteAllText(path, "untouched"); + Should.Throw(() => + store.TryRestore(s_key, [path], out _, _ => throw new InvalidDataException("Invalid raw outputs."))); + File.ReadAllText(path).ShouldBe("untouched"); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void RestoresAbsentOutputByRemovingCurrentFile() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = Path.Combine(env.CreateFolder().Path, "absent.txt"); + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [path], []); + File.WriteAllText(path, "new"); + store.TryRestore(s_key, [path], out _).ShouldBeTrue(); + File.Exists(path).ShouldBeFalse(); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void RestoresAbsentOutputWithoutCreatingMissingParent() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string parent = Path.Combine(env.CreateFolder().Path, "missing", "nested"); + string path = Path.Combine(parent, "absent.txt"); + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + + store.Store(s_key, [path], [42]).ShouldBeTrue(); + store.TryRestore(s_key, [path], out byte[] state).ShouldBeTrue(); + + state.ShouldBe([42]); + File.Exists(path).ShouldBeFalse(); + Directory.Exists(parent).ShouldBeFalse(); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void CancellationBeforePublicationDoesNotPublishManifest() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("result.txt", "content").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [path], [], () => false).ShouldBeFalse(); + store.TryRestore(s_key, [path], out _).ShouldBeFalse(); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void StagingFailureIsFatalRatherThanACacheMiss() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("result.txt", "content").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [path], []); + File.Delete(path); + Should.Throw(() => + store.TryRestore(s_key, [path], out _, _ => Directory.CreateDirectory(path))); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void CancellationDuringStagingPreservesAllDestinationsAndCleansFiles() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string first = env.CreateFile("first.txt", "first").Path; + string second = env.CreateFile("second.txt", "second").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [first, second], []).ShouldBeTrue(); + File.WriteAllText(first, "unchanged first"); + File.WriteAllText(second, "unchanged second"); + using CancellationTokenSource cancellation = new(); + using StagingCancellationBackend cancelling = new(backend, cancellation); + store = new(cache, cancelling); + Should.Throw(() => + store.TryRestore(s_key, [first, second], out _, cancellationToken: cancellation.Token)); + File.ReadAllText(first).ShouldBe("unchanged first"); + File.ReadAllText(second).ShouldBe("unchanged second"); + Directory.GetFiles(Path.GetDirectoryName(first)!, ".msbuild-cache-*").ShouldBeEmpty(); + Directory.GetFiles(Path.GetDirectoryName(second)!, ".msbuild-cache-*").ShouldBeEmpty(); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void CancellationBeforeStoreDoesNotPublishMapping() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string cache = env.CreateFolder().Path; + string path = env.CreateFile("result.txt", "content").Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + using CancellationTokenSource cancellation = new(); + cancellation.Cancel(); + Should.Throw(() => + store.Store(s_key, [path], [], cancellationToken: cancellation.Token)); + store.TryRestore(s_key, [path], out _).ShouldBeFalse(); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void CancellationAfterReplacementStartsFinishesAllFiles() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string first = env.CreateFile("first.txt", "cached first").Path; + string second = env.CreateFile("second.txt", "cached second").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [first, second], []).ShouldBeTrue(); + File.WriteAllText(first, "old first"); + File.WriteAllText(second, "old second"); + using CancellationTokenSource cancellation = new(); + Should.Throw(() => store.TryRestore(s_key, [first, second], out _, + cancellationToken: cancellation.Token, onReplacementStarted: cancellation.Cancel)); + File.ReadAllText(first).ShouldBe("cached first"); + File.ReadAllText(second).ShouldBe("cached second"); + Directory.GetFiles(Path.GetDirectoryName(first)!, ".msbuild-cache-*").ShouldBeEmpty(); + Directory.GetFiles(Path.GetDirectoryName(second)!, ".msbuild-cache-*").ShouldBeEmpty(); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void PartialReplacementFailureIsFatalAndRequiresCleanRebuild() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string first = env.CreateFile("first.txt", "cached first").Path; + string second = env.CreateFile("second.txt", "cached second").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [first, second], []).ShouldBeTrue(); + File.WriteAllText(first, "old first"); + var failure = Should.Throw(() => store.TryRestore(s_key, [first, second], out _, + onReplacementStarted: () => + { + File.Delete(second); + Directory.CreateDirectory(second); + })); + File.ReadAllText(first).ShouldBe("cached first"); + Directory.Exists(second).ShouldBeTrue(); + failure.Message.ShouldContain("clean and rebuild"); + Directory.GetFiles(Path.GetDirectoryName(second)!, ".msbuild-cache-*").ShouldBeEmpty(); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void CleanupFailureDoesNotReplacePrimaryFailure() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("output.txt", "cached").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [path], []).ShouldBeTrue(); + var failure = Should.Throw(() => store.TryRestore(s_key, [path], out _, + onReplacementStarted: () => + { + foreach (string staging in Directory.GetFiles(Path.GetDirectoryName(path)!, ".msbuild-cache-*")) + { + File.Delete(staging); + Directory.CreateDirectory(staging); + } + throw new IOException("primary replacement failure"); + })); + failure.InnerException!.Message.ShouldBe("primary replacement failure"); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public async Task ConcurrentPublicationProducesACompleteEntry() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("result.txt", "content").Path; + string cache = env.CreateFolder().Path; + using TaskCacheOwner owner = new(cache, (_, _) => { }, default); + Task[] writers = new Task[8]; + for (int i = 0; i < writers.Length; i++) + { + writers[i] = Task.Run(() => new TaskCacheStore(cache, owner).Store(s_key, [path], [42])); + } + + await Task.WhenAll(writers); + File.Delete(path); + new TaskCacheStore(cache, owner).TryRestore(s_key, [path], out byte[] state).ShouldBeTrue(); + state.ShouldBe([42]); + File.ReadAllText(path).ShouldBe("content"); + Directory.Exists(Path.Combine(cache, "entries")).ShouldBeFalse(); + Directory.Exists(Path.Combine(cache, "blobs")).ShouldBeFalse(); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void IgnoresLegacyCacheEntries() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string cache = env.CreateFolder().Path; + Directory.CreateDirectory(Path.Combine(cache, "entries")); + File.WriteAllText(Path.Combine(cache, "entries", s_key), "old incompatible manifest"); + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + new TaskCacheStore(cache, backend).TryRestore(s_key, [], out _).ShouldBeFalse(); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupported))] + public void RejectsCorruptManifestStoredThroughBackend() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("result.txt", "untouched").Path; + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + backend.Publish(s_key, new byte[64]).ShouldBeTrue(); + + Should.Throw(() => new TaskCacheStore(cache, backend).TryRestore(s_key, [path], out _)); + File.ReadAllText(path).ShouldBe("untouched"); + } + + internal static void CorruptContent(string cache, string content) + { + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + CorruptContent(backend, content); + } + + private static void CorruptContent(TaskCacheBackend backend, string content) + { + string digest = HashBytes(System.Text.Encoding.UTF8.GetBytes(content)); + string path = backend.ContentPath(digest); + File.SetAttributes(path, FileAttributes.Normal); + File.WriteAllText(path, "corrupt"); + } + +#if NET + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupportedOnUnix))] + [System.Runtime.Versioning.UnsupportedOSPlatform("windows")] + public void StagesPrivateOutputsWithOwnerOnlyPermissions() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("private.txt", "private content").Path; + const UnixFileMode mode = UnixFileMode.UserRead | UnixFileMode.UserWrite; + File.SetUnixFileMode(path, mode); + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + new TaskCacheStore(cache, backend).Store(s_key, [path], []).ShouldBeTrue(); + File.Delete(path); + int inspections = 0; + using StagingInspectionBackend inspecting = new(backend, staging => + { + File.GetUnixFileMode(staging).ShouldBe(mode); + inspections++; + }); + + new TaskCacheStore(cache, inspecting).TryRestore(s_key, [path], out _).ShouldBeTrue(); + + inspections.ShouldBe(2); + File.GetUnixFileMode(path).ShouldBe(mode); + File.ReadAllText(path).ShouldBe("private content"); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupportedOnUnix))] + [System.Runtime.Versioning.UnsupportedOSPlatform("windows")] + public void RestoresExecutableModeWithoutSharingCacheInodes() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("tool", "original").Path; + File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + store.Store(s_key, [path], []).ShouldBeTrue(); + File.WriteAllText(path, "changed after put"); + store.TryRestore(s_key, [path], out _).ShouldBeTrue(); + File.GetUnixFileMode(path).ShouldBe(UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + File.WriteAllText(path, "changed after restore"); + store.TryRestore(s_key, [path], out _).ShouldBeTrue(); + File.ReadAllText(path).ShouldBe("original"); + } + + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(IsSupportedOnUnix))] + public void RejectsSymbolicLinkDestinations() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string source = env.CreateFile("source.txt", "untouched").Path; + string link = Path.Combine(env.CreateFolder().Path, "link.txt"); + File.CreateSymbolicLink(link, source); + string cache = env.CreateFolder().Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + Should.Throw(() => store.Store(s_key, [link], [])); + File.ReadAllText(source).ShouldBe("untouched"); + } + + private sealed class StagingInspectionBackend(TaskCacheBackend backend, Action inspect) : TaskCacheBackend + { + internal override byte[]? ReadManifest(string key, int maximumSize, CancellationToken cancellationToken = default) => + backend.ReadManifest(key, maximumSize, cancellationToken); + + internal override Stream Open(string digest, CancellationToken cancellationToken = default) + { + using Stream source = backend.Open(digest, cancellationToken); + using MemoryStream contents = new(); + source.CopyTo(contents); + return new InspectingStream(contents.ToArray(), inspect); + } + + internal override string ContentPath(string digest) => backend.ContentPath(digest); + internal override string PutFile(string path, CancellationToken cancellationToken = default) => throw new NotSupportedException(); + internal override bool Publish(string key, byte[] manifest, Func? canPublish = null, + IReadOnlyList? artifacts = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(); + public override void Dispose() { } + } + + private sealed class InspectingStream(byte[] contents, Action inspect) : MemoryStream(contents, writable: false) + { + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + string path = destination.ShouldBeOfType().Name; + inspect(path); + await base.CopyToAsync(destination, bufferSize, cancellationToken); + inspect(path); + } + } +#endif + + private static string HashBytes(byte[] bytes) + { + using SHA256 hash = SHA256.Create(); + return BitConverter.ToString(hash.ComputeHash(bytes)).Replace("-", String.Empty); + } + + private sealed class StagingCancellationBackend(TaskCacheBackend backend, CancellationTokenSource cancellation) : TaskCacheBackend + { + private int _opened; + internal override byte[]? ReadManifest(string key, int maximumSize, CancellationToken cancellationToken = default) => + backend.ReadManifest(key, maximumSize, cancellationToken); + internal override Stream Open(string digest, CancellationToken cancellationToken = default) + { + if (++_opened == 2) + { + cancellation.Cancel(); + } + return backend.Open(digest, cancellationToken); + } + internal override string ContentPath(string digest) => backend.ContentPath(digest); + internal override string PutFile(string path, CancellationToken cancellationToken = default) => throw new NotSupportedException(); + internal override bool Publish(string key, byte[] manifest, Func? canPublish = null, + IReadOnlyList? artifacts = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(); + public override void Dispose() { } + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskCacheUnannotatedTask.cs b/src/Build.UnitTests/BackEnd/TaskCacheUnannotatedTask.cs new file mode 100644 index 00000000000..4207a95d15e --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskCacheUnannotatedTask.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class UnannotatedValueTask : ITask +{ + public IBuildEngine BuildEngine { get; set; } = null!; + public ITaskHost HostObject { get; set; } = null!; + public float Value { get; set; } + + [Output] + public float Result => Value; + + public bool Execute() => true; +} diff --git a/src/Build.UnitTests/BackEnd/TaskCacheWarning_Tests.cs b/src/Build.UnitTests/BackEnd/TaskCacheWarning_Tests.cs new file mode 100644 index 00000000000..045b4283c59 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskCacheWarning_Tests.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class TaskCacheWarning_Tests(ITestOutputHelper output) +{ + public static bool IsSupported => TaskCacheStore.IsSupported; + + [Fact] + public void StandardWarningPayloadRoundTripsWithoutOldInvocationIdentity() + { + BuildEventContext current = new(2, 3, 4, 5); + BuildWarningEventArgs original = new("category", "WARN1", "file.cs", 1, 2, 3, 4, + "literal {braces}", "help", "sender", "https://example.invalid/help", DateTime.UtcNow, messageArgs: null); + TaskCacheWarning warning = TaskCacheWarning.Snapshot(original, out int size)!; + size.ShouldBeGreaterThan(0); + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream, System.Text.Encoding.UTF8, leaveOpen: true); + TaskCacheWarning.Write(writer, [warning]); + stream.Position = 0; + using BinaryReader reader = new(stream); + TaskCacheWarning.Read(reader).ShouldBe([warning]); + BuildWarningEventArgs replayed = warning.ToEvent(current); + replayed.GetType().ShouldBe(typeof(BuildWarningEventArgs)); + replayed.BuildEventContext.ShouldBe(current); + replayed.ProjectFile.ShouldBeNull(); + replayed.HelpLink.ShouldBe(original.HelpLink); + } + + [Theory] + [InlineData(-1)] + [InlineData(1025)] + [InlineData(int.MaxValue)] + public void RejectsInvalidCountsBeforeAllocation(int count) + { + using MemoryStream stream = new(BitConverter.GetBytes(count)); + using BinaryReader reader = new(stream); + Should.Throw(() => TaskCacheWarning.Read(reader)); + } + + [Theory] + [InlineData(-2)] + [InlineData(1048577)] + [InlineData(int.MaxValue)] + public void RejectsInvalidStringLengths(int length) + { + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream, System.Text.Encoding.UTF8, leaveOpen: true); + writer.Write(1); + writer.Write(length); + stream.Position = 0; + using BinaryReader reader = new(stream); + Should.Throw(() => TaskCacheWarning.Read(reader)); + } + + [Fact] + public void RejectsUnknownWarningSubclassRatherThanDroppingItsFields() + { + BuildWarningEventArgs warning = new ExtendedBuildWarningEventArgs("extended", null, "WARN1", null, + 1, 1, 1, 1, "message", null, "sender", null, DateTime.UtcNow, messageArgs: null); + TaskCacheWarning.Snapshot(warning, out _).ShouldBeNull(); + } + + [Fact] + public void PreservesNullEmptyAndUnicodeStrings() + { + TaskCacheWarning warning = new(null, "", null, "warning λ\n", null, "", null, 0, 0, 0, 0); + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream, System.Text.Encoding.UTF8, leaveOpen: true); + TaskCacheWarning.Write(writer, [warning]); + stream.Position = 0; + using BinaryReader reader = new(stream); + TaskCacheWarning.Read(reader).ShouldBe([warning]); + } + + [Fact] + public void RejectsInvalidUtf8WithoutSubstitutingCharacters() + { + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream, System.Text.Encoding.UTF8, leaveOpen: true); + writer.Write(1); + writer.Write(1); + writer.Write((byte)0xff); + stream.Position = 0; + using BinaryReader reader = new(stream); + Should.Throw(() => TaskCacheWarning.Read(reader)); + } + + [Fact] + public void RejectsOversizedWarningStringsBeforeEncoding() + { + BuildWarningEventArgs warning = new(null, "WARN1", null, 0, 0, 0, 0, + new string('x', TaskCacheWarning.MaximumStringBytes + 1), null, "task"); + Should.Throw(() => TaskCacheWarning.Snapshot(warning, out _)); + } + + [ConditionalFact(typeof(TaskCacheWarning_Tests), nameof(IsSupported))] + public void TruncatedWarningsFailBeforeAnyArtifactReplacement() + { + using TestEnvironment env = TestEnvironment.Create(output); + string cache = env.CreateFolder().Path; + string path = env.CreateFile("artifact.txt", "cached").Path; + using TaskCacheBackend backend = TaskCacheBackend.Create(cache); + TaskCacheStore store = new(cache, backend); + string key = new('A', 64); + store.Store(key, [path], BitConverter.GetBytes(1)).ShouldBeTrue(); + File.WriteAllText(path, "untouched"); + Should.Throw(() => store.TryRestore(key, [path], out _, state => + { + using MemoryStream stream = new(state); + using BinaryReader reader = new(stream); + TaskCacheWarning.Read(reader); + })); + File.ReadAllText(path).ShouldBe("untouched"); + Directory.GetFiles(Path.GetDirectoryName(path)!, ".msbuild-cache-*").ShouldBeEmpty(); + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskCache_Tests.cs b/src/Build.UnitTests/BackEnd/TaskCache_Tests.cs new file mode 100644 index 00000000000..20185de9cbc --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskCache_Tests.cs @@ -0,0 +1,1420 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Logging; +using Microsoft.Build.Shared; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd; + +public sealed class TaskCache_Tests(ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + private string? _cacheDirectory; + + public static bool IsSupported => TaskCacheStore.IsSupported; + + [Fact] + public void InputFingerprintPreservesTaskVisibleMetadataSemantics() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("metadata.proj", """ + + + %(Filename) + + + + """).Path; + ProjectInstance project = new(path); + ProjectItemInstance source = project.GetItems("Source").Single(); + ITaskItem inherited = new ProjectItemInstance.TaskItem(source); + ITaskItem literal = new ProjectItemInstance.TaskItem(source); + literal.SetMetadata("NameMeta", "%(Filename)"); + + inherited.GetMetadata("NameMeta").ShouldBe("hello"); + literal.GetMetadata("NameMeta").ShouldBe("%(Filename)"); + TaskInvocationCache.SerializeParameter(inherited).ShouldBe(TaskInvocationCache.SerializeParameter(literal)); + TaskInvocationCache.SerializeInputParameter(inherited).ShouldNotBe(TaskInvocationCache.SerializeInputParameter(literal)); + ITaskItem?[] inheritedItems = [inherited, null]; + ITaskItem?[] literalItems = [literal, null]; + TaskInvocationCache.SerializeInputParameter(inheritedItems) + .ShouldNotBe(TaskInvocationCache.SerializeInputParameter(literalItems)); + + literal.SetMetadata("NameMeta", "hello"); + inherited.GetMetadata("NameMeta").ShouldBe(literal.GetMetadata("NameMeta")); + TaskInvocationCache.SerializeInputParameter(inherited).ShouldNotBe(TaskInvocationCache.SerializeInputParameter(literal)); + inherited.ItemSpec = literal.ItemSpec = "folder/changed.txt"; + inherited.GetMetadata("NameMeta").ShouldBe("changed"); + literal.GetMetadata("NameMeta").ShouldBe("hello"); + } + + private static string Invocation => """ + + + + + """; + + [Fact] + public void StructItemArrayMetadataParticipatesInInputKeys() + { + TaskItem first = new(42); + TaskItem changed = new(42); + first.SetMetadata("Label", "first"); + changed.SetMetadata("Label", "changed"); + TaskItem[] firstItems = [first]; + TaskItem[] changedItems = [changed]; + first.ItemSpec.ShouldBe(changed.ItemSpec); + TaskInvocationCache.SerializeInputParameter(firstItems).ShouldNotBe(TaskInvocationCache.SerializeInputParameter(changedItems)); + TaskInvocationCache.SerializeInputParameter(firstItems).ShouldBe(TaskInvocationCache.SerializeInputParameter(new ITaskItem[] { first })); + } + + [Fact] + public void StructItemArrayPreservesRawAndTaskVisibleMetadata() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string path = env.CreateFile("struct-metadata.proj", """ + + + + + """).Path; + ProjectItemInstance source = new ProjectInstance(path).GetItems("Source").Single(); + ITaskItem inherited = new ProjectItemInstance.TaskItem(source); + ITaskItem literal = new ProjectItemInstance.TaskItem(source); + literal.SetMetadata("Label", "%(Filename)"); + TaskItem[] inheritedItems = [new(inherited)]; + TaskItem[] literalItems = [new(literal)]; + TaskInvocationCache.SerializeParameter(inheritedItems).ShouldBe(TaskInvocationCache.SerializeParameter(literalItems)); + TaskInvocationCache.SerializeInputParameter(inheritedItems).ShouldNotBe(TaskInvocationCache.SerializeInputParameter(literalItems)); + + literal.SetMetadata("Label", "hello"); + inherited.GetMetadata("Label").ShouldBe(literal.GetMetadata("Label")); + TaskInvocationCache.SerializeInputParameter(inheritedItems).ShouldNotBe(TaskInvocationCache.SerializeInputParameter(literalItems)); + } + + [Fact] + public void StructItemArrayOutputSerializationPreservesMetadataAndOrder() + { + TaskItem first = new(42); + TaskItem second = new(7); + first.SetMetadata("Label", "first;value"); + second.SetMetadata("Label", "second%value"); + TaskItem[] items = [first, second]; + byte[] serialized = TaskInvocationCache.SerializeParameter(items); + serialized.ShouldBe(TaskInvocationCache.SerializeParameter(new ITaskItem[] { first, second })); + using MemoryStream stream = new(serialized, 0, serialized.Length, writable: false, publiclyVisible: true); + using ITranslator translator = BinaryTranslator.GetReadTranslator(stream, InterningBinaryReader.PoolingBuffer); + TaskParameter restored = TaskParameter.FactoryForDeserialization(translator); + restored.ParameterType.ShouldBe(TaskParameterType.ITaskItemArray); + ITaskItem[] outputs = restored.WrappedParameter.ShouldBeAssignableTo(); + outputs.Select(item => item.ItemSpec).ShouldBe(["42", "7"]); + outputs[0].GetMetadata("Label").ShouldBe("first;value"); + outputs[1].GetMetadata("Label").ShouldBe("second%value"); + } + + [Fact] + public void ItemArrayNormalizationPreservesEmptyAndNullElements() + { + TaskInvocationCache.SerializeParameter(Array.Empty>()) + .ShouldBe(TaskInvocationCache.SerializeParameter(Array.Empty())); + TaskInvocationCache.SerializeInputParameter(Array.Empty>()) + .ShouldBe(TaskInvocationCache.SerializeInputParameter(Array.Empty())); + ITaskItem?[] items = [null, new TaskItem(42), null]; + byte[] serialized = TaskInvocationCache.SerializeParameter(items); + using MemoryStream stream = new(serialized, 0, serialized.Length, writable: false, publiclyVisible: true); + using ITranslator translator = BinaryTranslator.GetReadTranslator(stream, InterningBinaryReader.PoolingBuffer); + ITaskItem[] outputs = TaskParameter.FactoryForDeserialization(translator).WrappedParameter.ShouldBeAssignableTo(); + outputs.Length.ShouldBe(3); + outputs[0].ShouldBeNull(); + outputs[1].ItemSpec.ShouldBe("42"); + outputs[2].ShouldBeNull(); + TaskInvocationCache.SerializeInputParameter(items) + .ShouldNotBe(TaskInvocationCache.SerializeInputParameter(new ITaskItem?[] { null, null, new TaskItem(42) })); + } + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData(false)] + [InlineData(true)] + public void StructItemArrayTaskRestoresTypedOutputsAndInvalidatesMetadata(bool outOfProcess) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, $""" + + + + + + + + + + + + """); + Dictionary properties = new() { ["Label"] = "first" }; + Run(project, properties, outOfProcess: outOfProcess, taskCache: false).Result.ShouldHaveSucceeded(); + var cold = Run(project, properties, outOfProcess: outOfProcess); + cold.Result.ShouldHaveSucceeded(); + cold.Logger.FullLog.ShouldContain(TaskCacheStructItemTask.ExecutionMessage); + var warm = Run(project, properties, outOfProcess: outOfProcess); + warm.Result.ShouldHaveSucceeded(); + warm.Logger.FullLog.ShouldContain(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskCache.Hit", nameof(TaskCacheStructItemTask))); + warm.Logger.FullLog.ShouldNotContain(TaskCacheStructItemTask.ExecutionMessage); + warm.Result.ProjectStateAfterBuild!.GetPropertyValue("Sum").ShouldBe("49"); + string[] outputNames = ["FirstResults", "SecondResults"]; + foreach (string name in outputNames) + { + var outputs = warm.Result.ProjectStateAfterBuild.GetItems(name).ToArray(); + outputs.Select(item => item.EvaluatedInclude).ShouldBe(["42", "7"]); + outputs[0].GetMetadataValue("Label").ShouldBe("first"); + outputs[1].GetMetadataValue("Label").ShouldBe("first"); + } + properties["Label"] = "changed"; + var changed = Run(project, properties, outOfProcess: outOfProcess); + changed.Result.ShouldHaveSucceeded(); + changed.Logger.FullLog.ShouldContain(TaskCacheStructItemTask.ExecutionMessage); + changed.Result.ProjectStateAfterBuild!.GetItems("FirstResults").First().GetMetadataValue("Label").ShouldBe("changed"); + } + + private static string ProjectText => $""" + + + + $(MSBuildTaskCacheEnabled) + true + + + + + + + + {Invocation} + + + """; + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData(false)] + [InlineData(true)] + public void UnconditionalTargetRestoresArtifactsAndReadOnlyRawOutputs(bool outOfProcess) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env); + var first = Run(project, outOfProcess: outOfProcess); + first.Result.ShouldHaveSucceeded(); + first.Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + first.Result.ProjectStateAfterBuild!.GetPropertyValue("ModeAtEvaluation").ShouldBe("true"); + File.Delete(Path.Combine(Path.GetDirectoryName(project)!, "output.txt")); + var second = Run(project, outOfProcess: outOfProcess); + second.Result.ShouldHaveSucceeded(); + AssertHit(second.Logger); + second.Logger.TaskStartedEvents.ShouldContain(task => task.TaskName == nameof(DeclaredIOEchoTask)); + second.Result.ProjectStateAfterBuild!.GetPropertyValue("Result").ShouldBe("a|b"); + second.Result.ProjectStateAfterBuild.GetPropertyValue("MSBuildLastTaskResult").ShouldBe("true"); + var items = second.Result.ProjectStateAfterBuild.GetItems("Results"); + items.Count.ShouldBe(1); + foreach (ProjectItemInstance item in items) + { + item.EvaluatedInclude.ShouldBe("a|b"); + item.GetMetadataValue("Metadata").ShouldBe("raw;metadata%value"); + } + + File.ReadAllText(Path.Combine(Path.GetDirectoryName(project)!, "output.txt")).ShouldBe("a|b"); + } + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData(false)] + [InlineData(true)] + public void WorkerRestoresZeroByteOutputAndNonemptyCompanion(bool emptyFirst) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, $""" + + + + + + + + + + + """); + string first = Path.Combine(Path.GetDirectoryName(project)!, "first.out"); + string second = Path.Combine(Path.GetDirectoryName(project)!, "second.out"); + string empty = emptyFirst ? first : second; + string nonempty = emptyFirst ? second : first; + var cold = Run(project, outOfProcess: true); + cold.Result.ShouldHaveSucceeded(); + cold.Logger.FullLog.ShouldContain(TaskCacheEmptyOutputTask.ExecutionMessage); + new FileInfo(empty).Length.ShouldBe(0); + File.ReadAllText(nonempty).ShouldBe("a"); + File.GetLastWriteTimeUtc(empty).ShouldBe(TaskCacheEmptyOutputTask.OriginalTimestamp); + File.Delete(first); + File.Delete(second); + + DateTime started = DateTime.UtcNow; + var warm = Run(project, outOfProcess: true); + warm.Result.ShouldHaveSucceeded(); + warm.Logger.FullLog.ShouldContain(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskCache.Hit", nameof(TaskCacheEmptyOutputTask))); + warm.Logger.FullLog.ShouldNotContain(TaskCacheEmptyOutputTask.ExecutionMessage); + new FileInfo(empty).Length.ShouldBe(0); + File.ReadAllText(nonempty).ShouldBe("a"); + File.GetLastWriteTimeUtc(first).ShouldBeInRange(started.AddSeconds(-2), DateTime.UtcNow.AddSeconds(2)); + File.GetLastWriteTimeUtc(second).ShouldBeInRange(started.AddSeconds(-2), DateTime.UtcNow.AddSeconds(2)); + warm.Result.ProjectStateAfterBuild!.GetPropertyValue("Content").ShouldBe("a"); + warm.Result.ProjectStateAfterBuild.GetPropertyValue("EmptyLength").ShouldBe("0"); + var produced = warm.Result.ProjectStateAfterBuild.GetItems("Produced").ToArray(); + produced.Select(item => Path.GetFullPath(item.EvaluatedInclude)).ShouldBe([first, second]); + produced.Select(item => item.GetMetadataValue("Kind")).ShouldBe(emptyFirst ? ["empty", "content"] : ["content", "empty"]); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void ConcurrentWorkerProcessesPublishCompleteEntries() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string barrier = env.CreateFolder().Path; + string xml = ProjectText.Replace(" submissions = []; + try + { + foreach (string project in projects) + { + File.Delete(Path.Combine(Path.GetDirectoryName(project)!, "output.txt")); + BuildSubmission submission = manager.PendBuildRequest(new BuildRequestData( + project, properties, null, ["Build"], null)); + submissions.Add(submission); + submission.ExecuteAsync(null, null); + } + } + finally + { + manager.EndBuild(); + } + + foreach (BuildSubmission submission in submissions) + { + submission.BuildResult.ShouldHaveSucceeded(); + } + } + + logger.Warnings.ShouldBeEmpty(); + Directory.GetFiles(barrier).Length.ShouldBe(2); + // Replay in the coordinator rather than starting a second pair of workers + // while the first pair is still exiting. Fresh-worker hits are covered by + // UnconditionalTargetRestoresArtifactsAndReadOnlyRawOutputs. + foreach (string project in projects) + { + File.Delete(Path.Combine(Path.GetDirectoryName(project)!, "output.txt")); + var restored = Run(project, properties); + restored.Result.ShouldHaveSucceeded(); + restored.Logger.Warnings.ShouldBeEmpty(); + AssertHit(restored.Logger); + } + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void UpToDateTargetDoesNotTouchTaskCache() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace( + " + + + + + + """); + env.SetEnvironmentVariable("MSBUILD_TASK_CACHE_DIR", env.CreateFile().Path); + var result = Run(project, warningsAsErrors: true); + result.Result.ShouldHaveSucceeded(); + result.Logger.Warnings.ShouldBeEmpty(); + result.Logger.FullLog.ShouldContain("UNANNOTATED_BUILTINS_EXECUTED"); + File.ReadAllText(Path.Combine(Path.GetDirectoryName(project)!, "output.txt")).ShouldBe("output" + Environment.NewLine); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void UnannotatedInputsAreNotSerializedForCaching() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, $""" + + + + + + + + + """); + env.SetEnvironmentVariable("MSBUILD_TASK_CACHE_DIR", env.CreateFile().Path); + var result = Run(project, warningsAsErrors: true); + result.Result.ShouldHaveSucceeded(); + result.Logger.Warnings.ShouldBeEmpty(); + result.Result.ProjectStateAfterBuild!.GetPropertyValue("Observed").ShouldBe("1.5"); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void DeclaredInputContentsInvalidateWithPreservedTimestamp() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env); + Run(project).Result.ShouldHaveSucceeded(); + AssertHit(Run(project).Logger); + string input = Path.Combine(Path.GetDirectoryName(project)!, "b.txt"); + DateTime timestamp = File.GetLastWriteTimeUtc(input); + File.WriteAllText(input, "c"); + File.SetLastWriteTimeUtc(input, timestamp); + var changed = Run(project); + changed.Result.ShouldHaveSucceeded(); + changed.Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + File.ReadAllText(Path.Combine(Path.GetDirectoryName(project)!, "output.txt")).ShouldBe("a|c"); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void BoundItemMetadataInvalidates() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env); + Dictionary properties = new() { ["Label"] = "one" }; + Run(project, properties).Result.ShouldHaveSucceeded(); + AssertHit(Run(project, properties).Logger); + properties["Label"] = "two"; + var changed = Run(project, properties); + changed.Result.ShouldHaveSucceeded(); + changed.Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + changed.Result.ProjectStateAfterBuild!.GetPropertyValue("Result").ShouldBe("a:two|b:two"); + } + + [Fact] + public void MetadataEnumerationOrderDoesNotChangeSerialization() + { + OrderedMetadataItem first = new(new Dictionary { ["A"] = "one", ["B"] = "two" }); + OrderedMetadataItem second = new(new Dictionary { ["B"] = "two", ["A"] = "one" }); + TaskInvocationCache.SerializeParameter(first).ShouldBe(TaskInvocationCache.SerializeParameter(second)); + TaskInvocationCache.SerializeParameter(new ITaskItem[] { first }).ShouldBe(TaskInvocationCache.SerializeParameter(new ITaskItem[] { second })); + TaskInvocationCache.SerializeInputParameter(first).ShouldBe(TaskInvocationCache.SerializeInputParameter(second)); + } + + private sealed class OrderedMetadataItem(Dictionary metadata) : ITaskItem + { + public string ItemSpec { get; set; } = "input"; + public ICollection MetadataNames => metadata.Keys; + public int MetadataCount => metadata.Count; + public string GetMetadata(string name) => metadata.TryGetValue(name, out string? value) ? value : String.Empty; + public void SetMetadata(string name, string value) => metadata[name] = value; + public void RemoveMetadata(string name) => metadata.Remove(name); + public IDictionary CloneCustomMetadata() => new Dictionary(metadata); + public void CopyMetadataTo(ITaskItem destinationItem) + { + foreach (var entry in metadata) + { + destinationItem.SetMetadata(entry.Key, entry.Value); + } + } + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void DeclaredAbsentInputInvalidatesWhenCreated() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string xml = ProjectText.Replace(" properties = new() { ["DestinationProperty"] = "First", ["Gather"] = "true" }; + Run(project, properties).Result.ShouldHaveSucceeded(); + properties["DestinationProperty"] = "Second"; + var result = Run(project, properties); + AssertHit(result.Logger); + result.Result.ProjectStateAfterBuild!.GetPropertyValue("Second").ShouldBe("a|b"); + result.Result.ProjectStateAfterBuild.GetPropertyValue("First").ShouldBeEmpty(); + properties["Gather"] = "false"; + result = Run(project, properties); + AssertHit(result.Logger); + result.Result.ProjectStateAfterBuild!.GetPropertyValue("Second").ShouldBeEmpty(); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void RequestedOutputCoverageIsPartOfTheKey() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace( + "", "")); + Run(project).Result.ShouldHaveSucceeded(); + File.WriteAllText(project, ProjectText); + var result = Run(project); + result.Result.ShouldHaveSucceeded(); + result.Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + result.Result.ProjectStateAfterBuild!.GetItems("Results").Count.ShouldBe(1); + AssertHit(Run(project).Logger); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void ConditionalOutputsAreCapturedForLaterTrueBindings() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace( + " { ["Gather"] = "true" }); + complete.Result.ShouldHaveSucceeded(); + AssertHit(complete.Logger); + complete.Result.ProjectStateAfterBuild!.GetItems("Results").Count.ShouldBe(1); + AssertHit(Run(project, new Dictionary { ["Gather"] = "true" }).Logger); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void RepeatedBindingsReadEachOutputGetterOnce() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText + .Replace("", + "")); + var cold = Run(project); + cold.Result.ShouldHaveSucceeded(); + cold.Result.ProjectStateAfterBuild!.GetPropertyValue("SecondResult").ShouldBe("a|b"); + var warm = Run(project); + warm.Result.ShouldHaveSucceeded(); + AssertHit(warm.Logger); + warm.Result.ProjectStateAfterBuild!.GetPropertyValue("SecondResult").ShouldBe("a|b"); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void FalseConditionGetterFailureIsFatalAndNotPublished() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace( + "", """ + + $(OtherDirectory) + """)); + Dictionary properties = new() + { + ["OtherDirectory"] = forbidden, + ["MSBuildTaskCacheDirectory"] = forbidden, + }; + env.SetEnvironmentVariable("MSBuildTaskCacheDirectory", forbidden); + env.SetEnvironmentVariable("MSBUILD_TASK_CACHE_DIR", forbidden); + Run(project, properties, outOfProcess: outOfProcess).Result.ShouldHaveSucceeded(); + var restored = Run(project, properties, outOfProcess: outOfProcess); + restored.Result.ShouldHaveSucceeded(); + AssertHit(restored.Logger); + File.ReadAllText(forbidden).ShouldBe("untouched"); + } + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData(false)] + [InlineData(true)] + public void CancelledBuildDrainsAndReleasesOwner(bool outOfProcess) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string marker = Path.Combine(env.CreateFolder().Path, "started"); + string project = CreateProject(env, ProjectText.Replace(" File.Exists(marker), TimeSpan.FromSeconds(30)).ShouldBeTrue(); + manager.CancelAllSubmissions(); + } + finally + { + manager.EndBuild(); + } + submission.BuildResult.ShouldHaveFailed(); + } + var subsequent = Run(project, outOfProcess: outOfProcess); + subsequent.Result.ShouldHaveSucceeded(); + subsequent.Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + AssertHit(Run(project, outOfProcess: outOfProcess).Logger); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void TaskBatchesAreCachedIndependently() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string xml = ProjectText.Replace("Destination=\"$(MSBuildProjectDirectory)/output.txt\"", + "Destination=\"$(MSBuildProjectDirectory)/%(Source.Filename).out\""); + string project = CreateProject(env, xml); + var first = Run(project); + first.Result.ShouldHaveSucceeded(); + first.Logger.TaskStartedEvents.Count.ShouldBe(2); + var second = Run(project); + second.Result.ShouldHaveSucceeded(); + AssertHit(second.Logger); + second.Logger.TaskStartedEvents.Count.ShouldBe(2); + File.ReadAllText(Path.Combine(Path.GetDirectoryName(project)!, "a.out")).ShouldBe("a"); + File.ReadAllText(Path.Combine(Path.GetDirectoryName(project)!, "b.out")).ShouldBe("b"); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void TaskCanRejectNondeterministicModeWithoutReusingPriorSuccess() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env); + Run(project).Result.ShouldHaveSucceeded(); + var rejected = Run(project, new Dictionary { ["Deterministic"] = "false" }); + rejected.Result.ShouldHaveFailed(); + rejected.Logger.FullLog.ShouldContain("IO0002"); + rejected.Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void EnvironmentChangesCannotBypassTaskValidation() + { + using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable(DeclaredIOEchoTask.UnsupportedEnvironmentVariable, null); + string project = CreateProject(env); + Run(project).Result.ShouldHaveSucceeded(); + AssertHit(Run(project).Logger); + env.SetEnvironmentVariable(DeclaredIOEchoTask.UnsupportedEnvironmentVariable, "true"); + var rejected = Run(project); + rejected.Result.ShouldHaveFailed(); + rejected.Logger.FullLog.ShouldContain("IO0004"); + rejected.Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + } + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData("MSBuildTaskCacheEnabled=\"false\"")] + [InlineData("")] + public void OptionalModeFalseOrUnboundOptsOutOfCaching(string mode) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace("MSBuildTaskCacheEnabled=\"$(MSBuildTaskCacheEnabled)\"", mode)); + Run(project).Result.ShouldHaveSucceeded(); + var second = Run(project); + second.Result.ShouldHaveSucceeded(); + second.Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + second.Logger.FullLog.ShouldNotContain(HitMessage); + second.Logger.FullLog.ShouldContain("MSB4287"); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void OptionalModeOptOutDoesNotOverwriteParticipatingEntry() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env); + Run(project).Result.ShouldHaveSucceeded(); + AssertHit(Run(project).Logger); + File.WriteAllText(project, ProjectText.Replace("MSBuildTaskCacheEnabled=\"$(MSBuildTaskCacheEnabled)\"", "MSBuildTaskCacheEnabled=\"false\"")); + var changed = Run(project); + changed.Result.ShouldHaveSucceeded(); + changed.Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + Run(project).Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + File.WriteAllText(project, ProjectText); + AssertHit(Run(project).Logger); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void AnnotatedTaskWithoutModePropertyIsReused() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string xml = $""" + + + + + + + + + """; + string project = CreateProject(env, xml); + var first = Run(project); + first.Result.ShouldHaveSucceeded(); + first.Logger.FullLog.ShouldContain(DeclaredIONoModeTask.ExecutionMessage); + File.Delete(Path.Combine(Path.GetDirectoryName(project)!, "output.txt")); + var second = Run(project); + second.Result.ShouldHaveSucceeded(); + second.Logger.Warnings.ShouldBeEmpty(); + second.Logger.FullLog.ShouldContain(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + "TaskCache.Hit", nameof(DeclaredIONoModeTask))); + second.Logger.FullLog.ShouldNotContain(DeclaredIONoModeTask.ExecutionMessage); + second.Result.ProjectStateAfterBuild!.GetPropertyValue("Result").ShouldBe("a"); + File.ReadAllText(Path.Combine(Path.GetDirectoryName(project)!, "output.txt")).ShouldBe("a"); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void RelativePathsUseTaskEnvironmentRatherThanProcessDirectory() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string taskDirectory = env.CreateFolder().Path; + string processDirectory = env.CreateFolder().Path; + env.SetCurrentDirectory(processDirectory); + TaskEnvironment environment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(taskDirectory); + TaskInvocationCache.NormalizePath("./input.txt", environment).ShouldBe(Path.Combine(taskDirectory, "input.txt")); + TaskInvocationCache.NormalizePath("nested/../output.txt", environment).ShouldBe(Path.Combine(taskDirectory, "output.txt")); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void MissingDeclaredBindingIsNotGuessedFromTaskDefaults() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace("OptionalOutput=\"\"", "")); + var result = Run(project); + result.Result.ShouldHaveSucceeded(); + result.Logger.FullLog.ShouldContain("MSB4287"); + result.Logger.FullLog.ShouldContain(nameof(DeclaredIOEchoTask.OptionalOutput)); + Run(project).Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void DifferentParametersResolvingToTheSameFileAreRejected() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace("Destination=\"$(MSBuildProjectDirectory)/output.txt\"", + "Destination=\"$(MSBuildProjectDirectory)/./a.txt\"")); + var result = Run(project); + result.Result.ShouldHaveSucceeded(); + result.Logger.FullLog.ShouldContain("MSB4287"); + Run(project).Logger.FullLog.ShouldContain(DeclaredIOEchoTask.ExecutionMessage); + } + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData(false)] + [InlineData(true)] + public void WarningsArePreservedAcrossWarningPolicyChanges(bool outOfProcess) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env); + Dictionary properties = new() { ["IssueWarning"] = "true" }; + Run(project, properties, warningsAsMessages: true, outOfProcess: outOfProcess).Result.ShouldHaveSucceeded(); + var rejected = Run(project, properties, warningsAsErrors: true, outOfProcess: outOfProcess); + rejected.Result.ShouldHaveFailed(); + rejected.Logger.FullLog.ShouldContain("IO0001"); + AssertHit(rejected.Logger); + rejected.Logger.Errors.Count(e => e.Code == "IO0001").ShouldBe(1); + var overridden = Run(project, properties, warningsAsErrors: true, warningsNotAsErrors: true, outOfProcess: outOfProcess); + overridden.Result.ShouldHaveSucceeded(); + AssertHit(overridden.Logger); + overridden.Logger.Warnings.Count(e => e.Code == "IO0001").ShouldBe(1); + } + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData(false)] + [InlineData(true)] + public void SuccessfulTaskWithPromotedColdWarningCanReplayDemoted(bool outOfProcess) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env); + Dictionary properties = new() { ["IssueWarning"] = "true" }; + Run(project, properties, warningsAsErrors: true, outOfProcess: outOfProcess).Result.ShouldHaveFailed(); + var warm = Run(project, properties, warningsAsMessages: true, outOfProcess: outOfProcess); + warm.Result.ShouldHaveSucceeded(); + AssertHit(warm.Logger); + warm.Logger.Warnings.ShouldBeEmpty(); + warm.Logger.Errors.ShouldBeEmpty(); + warm.Logger.AllBuildEvents.Count(e => e is BuildMessageEventArgs && e.Message == "Declared I/O diagnostic.").ShouldBe(1); + } + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData(false)] + [InlineData(true)] + public void WarningReplayPreservesOrderFieldsAndCurrentBinlogContext(bool outOfProcess) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace(" w.Code).ShouldBe(["TCW01", "TCW02"]); + for (int i = 0; i < 2; i++) + { + var warning = warm.Logger.Warnings[i]; + warning.Message.ShouldBe("warning {literal}"); + warning.File.ShouldBe("reported.cs"); + warning.Subcategory.ShouldBe("cache"); + warning.LineNumber.ShouldBe(12); + warning.ColumnNumber.ShouldBe(3); + warning.EndLineNumber.ShouldBe(14); + warning.EndColumnNumber.ShouldBe(5); + warning.HelpKeyword.ShouldBe("Task.Warning"); + warning.HelpLink.ShouldBe("https://example.invalid/task-warning"); + warning.SenderName.ShouldBe(nameof(DeclaredIOEchoTask)); + warning.BuildEventContext.ShouldBe(warm.Logger.TaskStartedEvents.Single(e => e.TaskName == nameof(DeclaredIOEchoTask)).BuildEventContext); + } + List replayed = []; + List started = []; + List finished = []; + BinaryLogReplayEventSource replay = new(); + replay.WarningRaised += (_, e) => replayed.Add(e); + replay.TaskStarted += (_, e) => started.Add(e); + replay.TaskFinished += (_, e) => finished.Add(e); + replay.Replay(binlog); + replayed.Select(w => w.Code).ShouldBe(["TCW01", "TCW02"]); + replayed[0].Message.ShouldBe(warm.Logger.Warnings[0].Message); + replayed[0].BuildEventContext.ShouldBe(started.Single(e => e.TaskName == nameof(DeclaredIOEchoTask)).BuildEventContext); + finished.Single(e => e.TaskName == nameof(DeclaredIOEchoTask)).Succeeded.ShouldBeTrue(); + } + + [ConditionalFact(typeof(TaskCache_Tests), nameof(IsSupported))] + public void FalseConditionOutputGetterWarningIsCapturedOnceAndReplayed() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText + .Replace("", + "")); + var cold = Run(project); + cold.Result.ShouldHaveSucceeded(); + cold.Logger.Warnings.Count.ShouldBe(1); + var warm = Run(project, new Dictionary { ["Gather"] = "true" }); + warm.Result.ShouldHaveSucceeded(); + AssertHit(warm.Logger); + warm.Logger.Warnings.Count.ShouldBe(1); + warm.Result.ProjectStateAfterBuild!.GetPropertyValue("Second").ShouldBe("a|b"); + } + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData("setup")] + [InlineData("contract")] + [InlineData("extended")] + public void LifecycleAndUnsupportedWarningsRemainNoncacheable(string phase) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace(" { "TCW01" } : null, + }; + return (manager.Build(parameters, new BuildRequestData(project, new Dictionary(), null, ["Build"], null)), logger); + } + } + +#if NET + [ConditionalFact(typeof(TaskCacheStore_Tests), nameof(TaskCacheStore_Tests.IsSupportedOnUnix))] + public void ContractGetterWarningAvoidsInputHashingAndLookup() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace(" + + + + + + """); + Run(project, outOfProcess: outOfProcess).Result.ShouldHaveSucceeded(); + var repeated = Run(project, outOfProcess: outOfProcess); + repeated.Result.ShouldHaveSucceeded(); + repeated.Logger.Warnings.Count.ShouldBe(1); + repeated.Logger.FullLog.ShouldContain("INIT_WARNING_TASK_EXECUTED"); + repeated.Logger.FullLog.ShouldNotContain("Task-cache hit"); + } + + [ConditionalTheory(typeof(TaskCache_Tests), nameof(IsSupported))] + [InlineData("error", false)] + [InlineData("error", true)] + [InlineData("false", false)] + [InlineData("throw", false)] + public void ContinueOnErrorDoesNotCacheRawErrorsOrFailures(string phase, bool outOfProcess) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = CreateProject(env, ProjectText.Replace(" { ["MSBuildTaskCacheEnabled"] = enabled ? "false" : "true" }, taskCache: enabled); + result.Result.ShouldHaveSucceeded(); + result.Result.ProjectStateAfterBuild!.GetPropertyValue("ModeAtEvaluation").ShouldBe("true"); + } + + private static string HitMessage => ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskCache.Hit", nameof(DeclaredIOEchoTask)); + + private static void AssertHit(MockLogger logger) + { + logger.FullLog.ShouldContain(HitMessage); + logger.FullLog.ShouldNotContain(DeclaredIOEchoTask.ExecutionMessage); + } + + private string CreateProject(TestEnvironment env, string? xml = null) + { + var folder = env.CreateFolder(); + _cacheDirectory ??= env.CreateFolder().Path; + env.CreateFile(folder, "a.txt", "a"); + env.CreateFile(folder, "b.txt", "b"); + return env.CreateFile(folder, "declared.proj", xml ?? ProjectText).Path; + } + + private (BuildResult Result, MockLogger Logger) Run( + string project, + IDictionary? properties = null, + bool outOfProcess = false, + bool warningsAsErrors = false, + bool warningsAsMessages = false, + bool taskCache = true, + bool allowTaskCrashes = false, + bool warningsNotAsErrors = false, + string? binlog = null) + { + MockLogger logger = new(_output) { AllowTaskCrashes = allowTaskCrashes }; + using BuildManager manager = new(); + BuildParameters parameters = new() + { + TaskCache = taskCache, + BuildCacheDirectory = _cacheDirectory!, + Loggers = binlog is null ? [logger] : [logger, new BinaryLogger { Parameters = binlog }], + DisableInProcNode = outOfProcess, + EnableNodeReuse = false, + WarningsAsErrors = warningsAsErrors ? new HashSet() : null, + WarningsNotAsErrors = warningsNotAsErrors ? new HashSet { "IO0001" } : null, + WarningsAsMessages = warningsAsMessages ? new HashSet { "IO0001" } : null + }; + BuildRequestData request = new(project, properties ?? new Dictionary(), null, + ["Build"], null, BuildRequestDataFlags.ProvideProjectStateAfterBuild); + return (manager.Build(parameters, request), logger); + } +} + +[MSBuildDeclaredIOTask] +[MSBuildDeclaredIOInput(nameof(Source))] +[MSBuildDeclaredIOOutput(nameof(First))] +[MSBuildDeclaredIOOutput(nameof(Second))] +public sealed class TaskCacheEmptyOutputTask : ITask +{ + internal const string ExecutionMessage = "EMPTY_OUTPUT_TASK_EXECUTED"; + internal static readonly DateTime OriginalTimestamp = new(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + public IBuildEngine BuildEngine { get; set; } = null!; + public ITaskHost HostObject { get; set; } = null!; + public string Source { get; set; } = String.Empty; + public string First { get; set; } = String.Empty; + public string Second { get; set; } = String.Empty; + public bool EmptyFirst { get; set; } + [Output] + public string Content { get; private set; } = String.Empty; + [Output] + public int EmptyLength { get; private set; } + [Output] + public ITaskItem[] Produced { get; private set; } = []; + + public bool Execute() + { + BuildEngine.LogMessageEvent(new BuildMessageEventArgs(ExecutionMessage, null, nameof(TaskCacheEmptyOutputTask), MessageImportance.High)); + Content = File.ReadAllText(Source); + File.WriteAllText(EmptyFirst ? Second : First, Content); + string empty = EmptyFirst ? First : Second; + File.WriteAllBytes(empty, []); + EmptyLength = checked((int)new FileInfo(empty).Length); + File.SetLastWriteTimeUtc(First, OriginalTimestamp); + File.SetLastWriteTimeUtc(Second, OriginalTimestamp); + ITaskItem first = new Microsoft.Build.Utilities.TaskItem(First); + ITaskItem second = new Microsoft.Build.Utilities.TaskItem(Second); + first.SetMetadata("Kind", EmptyFirst ? "empty" : "content"); + second.SetMetadata("Kind", EmptyFirst ? "content" : "empty"); + Produced = [first, second]; + return true; + } +} + +[MSBuildDeclaredIOTask] +public sealed class TaskCacheStructItemTask : ITask +{ + internal const string ExecutionMessage = "STRUCT_ITEM_ARRAY_TASK_EXECUTED"; + public IBuildEngine BuildEngine { get; set; } = null!; + public ITaskHost HostObject { get; set; } = null!; + public TaskItem[] Values { get; set; } = []; + [Output] + public TaskItem[] Results { get; private set; } = []; + [Output] + public int Sum { get; private set; } + + public bool Execute() + { + BuildEngine.LogMessageEvent(new BuildMessageEventArgs(ExecutionMessage, null, nameof(TaskCacheStructItemTask), MessageImportance.High)); + Results = new TaskItem[Values.Length]; + for (int i = 0; i < Values.Length; i++) + { + Sum += Values[i].Value; + Results[i] = new TaskItem(Values[i].Value); + Values[i].CopyMetadataTo(Results[i]); + } + return true; + } +} + +[MSBuildDeclaredIOTask] +[MSBuildDeclaredIOInput(nameof(Sources))] +[MSBuildDeclaredIOOutput(nameof(Destination))] +[MSBuildDeclaredIOOutput(nameof(OptionalOutput))] +public sealed class DeclaredIOEchoTask : ICancelableTask +{ + internal const string ExecutionMessage = "DECLARED_IO_TASK_EXECUTED"; + internal const string UnsupportedEnvironmentVariable = "MSBUILD_TASK_CACHE_TEST_UNSUPPORTED"; + private string _result = String.Empty; + + public IBuildEngine BuildEngine { get; set; } = null!; + public ITaskHost HostObject { get; set; } = null!; + private ITaskItem[] _sources = []; + public ITaskItem[] Sources + { + get + { + if (WarningPhase == "contract") + { + Warn("TCW01"); + } + return _sources; + } + set => _sources = value; + } + public string Destination { get; set; } = String.Empty; + public string OptionalOutput { get; set; } = String.Empty; + public bool MSBuildTaskCacheEnabled { get; set; } + public bool Deterministic { get; set; } + public bool IssueWarning { get; set; } + private string _warningPhase = String.Empty; + public string WarningPhase + { + get => _warningPhase; + set + { + _warningPhase = value; + if (value == "setup") + { + Warn("TCW01"); + } + } + } + public bool AllowMissing { get; set; } + public bool MutateInput { get; set; } + public bool FailOutputGetter { get; set; } + public bool FailRepeatedOutputReads { get; set; } + public string BarrierDirectory { get; set; } = String.Empty; + public string CancellationMarker { get; set; } = String.Empty; + private volatile bool _cancelled; + private int _outputReads; + public bool FailAfterWriting { get; set; } + [Output] + public string Result + { + get + { + if (WarningPhase == "getter") + { + Warn("TCW01"); + } + return FailOutputGetter || (FailRepeatedOutputReads && ++_outputReads > 1) + ? throw new InvalidOperationException("Output getter failed.") : _result; + } + } + [Output] + public ITaskItem[] Items + { + get + { + if (_result.Length == 0) + { + return []; + } + + Microsoft.Build.Utilities.TaskItem item = new(_result); + item.SetMetadata("Metadata", "raw;metadata%value"); + return [item]; + } + } + [Output] + public string Unrequested => throw new InvalidOperationException("This output was not requested."); + + public bool Execute() + { + if (CancellationMarker.Length > 0) + { + File.WriteAllText(CancellationMarker, "started"); + System.Threading.SpinWait.SpinUntil(() => _cancelled, TimeSpan.FromSeconds(30)); + return false; + } + + if (BarrierDirectory.Length > 0) + { + using var process = System.Diagnostics.Process.GetCurrentProcess(); + File.WriteAllText(Path.Combine(BarrierDirectory, process.Id.ToString(System.Globalization.CultureInfo.InvariantCulture)), "executing"); + var watch = System.Diagnostics.Stopwatch.StartNew(); + while (Directory.GetFiles(BarrierDirectory).Length < 2) + { + if (watch.Elapsed > TimeSpan.FromSeconds(30)) + { + throw new TimeoutException("Two worker tasks did not execute concurrently."); + } + System.Threading.Thread.Sleep(10); + } + } + + BuildEngine.LogMessageEvent(new BuildMessageEventArgs(ExecutionMessage, null, nameof(DeclaredIOEchoTask), MessageImportance.High)); + if (MSBuildTaskCacheEnabled && Environment.GetEnvironmentVariable(UnsupportedEnvironmentVariable) is not null) + { + BuildEngine.LogErrorEvent(new BuildErrorEventArgs(null, "IO0004", null, 0, 0, 0, 0, "The task does not support this environment setting.", null, nameof(DeclaredIOEchoTask))); + return false; + } + + if (MSBuildTaskCacheEnabled && !Deterministic) + { + BuildEngine.LogErrorEvent(new BuildErrorEventArgs(null, "IO0002", null, 0, 0, 0, 0, "Deterministic execution is required.", null, nameof(DeclaredIOEchoTask))); + return false; + } + + string[] values = new string[_sources.Length]; + for (int i = 0; i < _sources.Length; i++) + { + values[i] = AllowMissing && !File.Exists(_sources[i].ItemSpec) ? "absent" : File.ReadAllText(_sources[i].ItemSpec); + string label = _sources[i].GetMetadata("Label"); + if (label.Length > 0) + { + values[i] += ":" + label; + } + } + + _result = String.Join("|", values); + File.WriteAllText(Destination, _result); + if (OptionalOutput.Length > 0) + { + File.WriteAllText(OptionalOutput, _result); + } + + if (IssueWarning) + { + BuildEngine.LogWarningEvent(new BuildWarningEventArgs(null, "IO0001", null, 0, 0, 0, 0, "Declared I/O diagnostic.", null, nameof(DeclaredIOEchoTask))); + } + if (WarningPhase is "multiple" or "false" or "throw") + { + Warn("TCW01"); + if (WarningPhase == "multiple") + { + Warn("TCW02"); + } + if (WarningPhase == "false") + { + return false; + } + if (WarningPhase == "throw") + { + throw new InvalidOperationException("Task failed after warning."); + } + } + if (WarningPhase == "error") + { + BuildEngine.LogErrorEvent(new BuildErrorEventArgs(null, "TCE01", "reported.cs", 1, 1, 1, 1, "raw error", null, nameof(DeclaredIOEchoTask))); + } + if (WarningPhase == "extended") + { + BuildEngine.LogWarningEvent(new ExtendedBuildWarningEventArgs("custom", null, "TCW01", "reported.cs", + 1, 1, 1, 1, "extended", null, nameof(DeclaredIOEchoTask), null, DateTime.UtcNow, messageArgs: null)); + } + if (WarningPhase == "nonserializable") + { + BuildEngine.LogWarningEvent(new TaskCacheNonserializableWarning()); + } + + if (MutateInput) + { + File.AppendAllText(Sources[0].ItemSpec, "changed"); + } + + if (FailAfterWriting) + { + BuildEngine.LogErrorEvent(new BuildErrorEventArgs(null, "IO0003", null, 0, 0, 0, 0, "Task failed after writing outputs.", null, nameof(DeclaredIOEchoTask))); + return false; + } + + return true; + } + + public void Cancel() => _cancelled = true; + + public sealed class TaskCacheNonserializableWarning() : BuildWarningEventArgs( + null, "TCW01", "reported.cs", 1, 1, 1, 1, "custom warning", null, "cache test"); + + private void Warn(string code) => BuildEngine.LogWarningEvent(new BuildWarningEventArgs("cache", code, "reported.cs", + 12, 3, 14, 5, "warning {literal}", "Task.Warning", nameof(DeclaredIOEchoTask), + "https://example.invalid/task-warning", DateTime.UtcNow, messageArgs: null)); +} + +[MSBuildDeclaredIOTask] +[MSBuildDeclaredIOInput(nameof(Source))] +[MSBuildDeclaredIOOutput(nameof(Destination))] +public sealed class DeclaredIOInitializationWarningTask : ITask +{ + private IBuildEngine _buildEngine = null!; + public IBuildEngine BuildEngine + { + get => _buildEngine; + set + { + _buildEngine = value; + value.LogWarningEvent(new BuildWarningEventArgs(null, "INIT01", null, 0, 0, 0, 0, "initialization warning", null, nameof(DeclaredIOInitializationWarningTask))); + } + } + public ITaskHost HostObject { get; set; } = null!; + public string Source { get; set; } = String.Empty; + public string Destination { get; set; } = String.Empty; + public bool Execute() + { + BuildEngine.LogMessageEvent(new BuildMessageEventArgs("INIT_WARNING_TASK_EXECUTED", null, nameof(DeclaredIOInitializationWarningTask), MessageImportance.High)); + File.Copy(Source, Destination, overwrite: true); + return true; + } +} + +[MSBuildDeclaredIOTask] +[MSBuildDeclaredIOInput(nameof(Source))] +[MSBuildDeclaredIOOutput(nameof(Destination))] +public sealed class DeclaredIONoModeTask : ITask +{ + internal const string ExecutionMessage = "DECLARED_IO_NO_MODE_TASK_EXECUTED"; + private string _result = String.Empty; + + public IBuildEngine BuildEngine { get; set; } = null!; + public ITaskHost HostObject { get; set; } = null!; + public string Source { get; set; } = String.Empty; + public string Destination { get; set; } = String.Empty; + [Output] + public string Result => _result; + + public bool Execute() + { + BuildEngine.LogMessageEvent(new BuildMessageEventArgs(ExecutionMessage, null, nameof(DeclaredIONoModeTask), MessageImportance.High)); + _result = File.ReadAllText(Source); + File.WriteAllText(Destination, _result); + return true; + } +} diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 3ef70f60951..9f48c34f0a3 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -20,6 +20,12 @@ true + + + + + + @@ -35,6 +41,7 @@ + diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 96d1adc1349..ebbcbc68d4d 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -20,6 +20,7 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.BackEnd.SdkResolution; +using Microsoft.Build.Collections; using Microsoft.Build.Evaluation; using Microsoft.Build.Eventing; using Microsoft.Build.Exceptions; @@ -528,6 +529,21 @@ private void UpdatePriority(Process p, ProcessPriorityClass priority) [RequiresUnreferencedCode("Initializes loggers and project cache plugins by reflecting over assemblies discovered at runtime, which is incompatible with trimming.")] public void BeginBuild(BuildParameters parameters) { + ValidateTaskCacheParameters(parameters); + + static void ValidateTaskCacheParameters(BuildParameters? parameters) + { + if (parameters?.TaskCache == true && !TaskCacheStore.IsSupported) + { + throw new ArgumentException(ResourceUtilities.GetResourceString("TaskCache.StorageUnavailable"), nameof(parameters)); + } + + if (parameters?.TaskCache == true && (parameters.Question || parameters.UsesCachedResults())) + { + throw new ArgumentException(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskCache.IncompatibleParameters"), nameof(parameters)); + } + } + #if NETFRAMEWORK // Collect telemetry unless explicitly opted out via environment variable. // The decision to send telemetry is made at EndBuild to avoid eager loading of telemetry assemblies. @@ -614,6 +630,12 @@ public void BeginBuild(BuildParameters parameters) // Clone off the build parameters. _buildParameters = parameters?.Clone() ?? new BuildParameters(); + if (_buildParameters.TaskCache) + { + _buildParameters.BuildCacheDirectory = BuildParameters.ResolveBuildCacheDirectory(_buildParameters.BuildCacheDirectory); + _buildParameters.ResetCaches = true; + _buildParameters.DiscardBuildResults = true; + } // Initialize additional build parameters. _buildParameters.BuildId = GetNextBuildId(); @@ -754,6 +776,59 @@ public void BeginBuild(BuildParameters parameters) _noActiveSubmissionsEvent!.Set(); _noNodesActiveEvent!.Set(); + + if (_buildParameters.TaskCache) + { + try + { + TaskCacheOwner owner = new(_buildParameters.BuildCacheDirectory, + (node, packet) => _nodeManager.SendData(node, packet), _executionCancellationTokenSource.Token, + reportFailure: OnLoggingThreadException); + _buildParameters.TaskCacheBackend = owner; + _nodeManager.RegisterPacketHandler(NodePacketType.TaskCacheRequest, TaskCachePacket.ReadRequest, owner); + _nodeManager.RegisterPacketHandler(NodePacketType.TaskCacheCancel, TaskCachePacket.ReadCancel, owner); + } + catch + { + try + { + _buildParameters.TaskCacheBackend?.Dispose(); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + // Preserve the cache initialization failure. + } + finally + { + _buildParameters.TaskCacheBackend = null; + try + { + _coordinatorClient?.Dispose(); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + // Cleanup must not replace the initialization failure. + } + finally + { + _coordinatorClient = null; + try + { + ShutdownLoggingService(loggingService); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + // Cleanup must not replace the initialization failure. + } + finally + { + _buildManagerState = BuildManagerState.Idle; + } + } + } + throw; + } + } } ILoggingService InitializeLoggingService() @@ -1145,6 +1220,7 @@ public void EndBuild() } projectCacheDispose.Wait(); + _buildParameters.TaskCacheBackend?.Dispose(); #if DEBUG if (_projectStartedEvents.Count != 0) @@ -1195,6 +1271,22 @@ public void EndBuild() { try { + try + { + _buildParameters?.TaskCacheBackend?.Dispose(); + } + catch (Exception e) when (exceptionsThrownInEndBuild && !ExceptionHandling.IsCriticalException(e)) + { + // The original build/shutdown failure remains authoritative. + } + finally + { + if (_buildParameters is not null) + { + _buildParameters.TaskCacheBackend = null; + } + } + ILoggingService? loggingService = ((IBuildComponentHost)this).LoggingService; if (loggingService != null) @@ -1640,7 +1732,7 @@ private void ExecuteSubmission(BuildSubmission submission, bool allowMainThreadB } // Create/Retrieve a configuration for each request - var buildRequestConfiguration = new BuildRequestConfiguration(submission.BuildRequestData, _buildParameters.DefaultToolsVersion); + var buildRequestConfiguration = new BuildRequestConfiguration(submission.BuildRequestData, _buildParameters.DefaultToolsVersion, _buildParameters.TaskCache); var matchingConfiguration = _configCache!.GetMatchingConfiguration(buildRequestConfiguration); resolvedConfiguration = ResolveConfiguration( buildRequestConfiguration, @@ -2264,8 +2356,30 @@ private void ExecuteGraphBuildScheduler(GraphBuildSubmission submission) var projectGraph = submission.BuildRequestData.ProjectGraph; if (projectGraph == null) { + IEnumerable? entryPoints = submission.BuildRequestData.ProjectGraphEntryPoints; + if (_buildParameters!.TaskCache) + { + // Set the mode before graph construction so evaluation and graph + // identity use the same globals, without modifying caller data. + List cacheEntryPoints = []; + foreach (ProjectGraphEntryPoint entryPoint in entryPoints!) + { + Dictionary globals = new(MSBuildNameIgnoreCaseComparer.Default); + if (entryPoint.GlobalProperties is not null) + { + foreach (KeyValuePair property in entryPoint.GlobalProperties) + { + globals[property.Key] = property.Value; + } + } + globals[MSBuildConstants.MSBuildTaskCacheEnabled] = "true"; + cacheEntryPoints.Add(new ProjectGraphEntryPoint(entryPoint.ProjectFile, globals)); + } + entryPoints = cacheEntryPoints; + } + projectGraph = new ProjectGraph( - submission.BuildRequestData.ProjectGraphEntryPoints, + entryPoints, ProjectCollection.GlobalProjectCollection, (path, properties, collection) => { diff --git a/src/Build/BackEnd/BuildManager/BuildParameters.cs b/src/Build/BackEnd/BuildManager/BuildParameters.cs index ae8b126be22..d323febdfc1 100644 --- a/src/Build/BackEnd/BuildManager/BuildParameters.cs +++ b/src/Build/BackEnd/BuildManager/BuildParameters.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.IO; using System.Threading; using Microsoft.Build.BackEnd; using Microsoft.Build.Collections; @@ -17,6 +18,9 @@ using Microsoft.Build.Shared; using Microsoft.Build.Shared.FileSystem; using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; +#if NETFRAMEWORK +using OperatingSystem = Microsoft.Build.Framework.OperatingSystem; +#endif #nullable disable @@ -333,6 +337,9 @@ internal BuildParameters(BuildParameters other, bool resetEnvironment = false) DiscardBuildResults = other.DiscardBuildResults; LowPriority = other.LowPriority; Question = other.Question; + _taskCache = other._taskCache; + _buildCacheDirectory = other._buildCacheDirectory; + TaskCacheBackend = other.TaskCacheBackend; IsBuildCheckEnabled = other.IsBuildCheckEnabled; IsTelemetryEnabled = other.IsTelemetryEnabled; ProjectCacheDescriptor = other.ProjectCacheDescriptor; @@ -918,6 +925,62 @@ public bool Question set => _question = value; } + private bool _taskCache; + private string _buildCacheDirectory; + + /// + /// Gets or sets whether eligible task invocations are cached by content. + /// Normal target incrementality is preserved. + /// + public bool TaskCache + { + get => _taskCache; + set => _taskCache = value; + } + + /// + /// Gets or sets the build-wide cache directory. Null or whitespace selects + /// the platform default. Reading the property resolves relative paths against + /// the current directory; BeginBuild captures that resolved value for the build. + /// Project properties and environment variables do not configure this value. + /// + public string BuildCacheDirectory + { + get => ResolveBuildCacheDirectory(_buildCacheDirectory); + set => _buildCacheDirectory = value; + } + + internal TaskCacheBackend TaskCacheBackend { get; set; } + + internal static string ResolveBuildCacheDirectory(string configured = null) + { + if (!String.IsNullOrWhiteSpace(configured)) + { + return Path.GetFullPath(configured); + } + + string root; + if (OperatingSystem.IsWindows()) + { + root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + } + else if (OperatingSystem.IsMacOS()) + { + root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Caches"); + } + else + { + root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache"); + } + + if (String.IsNullOrEmpty(root) || !Path.IsPathRooted(root)) + { + throw new InvalidOperationException(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskCache.NoCacheDirectory")); + } + + return Path.GetFullPath(Path.Combine(root, "msbuild-cache")); + } + /// /// Gets or sets an indication of build check enablement. /// @@ -1013,6 +1076,8 @@ void ITranslatable.Translate(ITranslator translator) translator.Translate(ref _enableTargetOutputLogging); translator.Translate(ref _multiThreaded); translator.Translate(ref _ParserIgnoreConfiguration, ParserIgnoreConfiguration.FactoryForDeserialization); + translator.Translate(ref _taskCache); + translator.Translate(ref _buildCacheDirectory); // ProjectRootElementCache is not transmitted. // ResetCaches is not transmitted. diff --git a/src/Build/BackEnd/Components/Communications/NodeManager.cs b/src/Build/BackEnd/Components/Communications/NodeManager.cs index 72bc89ec156..d9f334ae850 100644 --- a/src/Build/BackEnd/Components/Communications/NodeManager.cs +++ b/src/Build/BackEnd/Components/Communications/NodeManager.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Threading; @@ -32,8 +33,11 @@ internal class NodeManager : INodeManager /// /// Mapping of manager-produced node IDs to the provider hosting the node. + /// Cache replies and provider shutdown notifications can access this map + /// independently of scheduler-driven node creation. Collection operations + /// must finish before invoking providers or packet handlers. /// - private readonly Dictionary _nodeIdToProvider; + private readonly ConcurrentDictionary _nodeIdToProvider; /// /// The packet factory used to translate and route packets @@ -74,7 +78,7 @@ internal class NodeManager : INodeManager /// private NodeManager() { - _nodeIdToProvider = new Dictionary(); + _nodeIdToProvider = new ConcurrentDictionary(); _packetFactory = new NodePacketFactory(); _nextNodeId = _inprocNodeId + 1; } @@ -290,8 +294,8 @@ internal static IBuildComponent CreateComponent(BuildComponentType type) /// private void RemoveNodeFromMapping(int nodeId) { - _nodeIdToProvider.Remove(nodeId); - if (_nodeIdToProvider.Count == 0) + _nodeIdToProvider.TryRemove(nodeId, out _); + if (_nodeIdToProvider.IsEmpty) { // The inproc node is always 1 therefore when new nodes are requested we need to start at 2 _nextNodeId = _inprocNodeId + 1; @@ -334,7 +338,8 @@ private IList AttemptCreateNode(INodeProvider nodeProvider, NodeConfig foreach (NodeInfo node in nodes) { - _nodeIdToProvider.Add(node.NodeId, nodeProvider); + // Keep duplicate node IDs an error, rather than replacing a route. + ((IDictionary)_nodeIdToProvider).Add(node.NodeId, nodeProvider); } return nodes; diff --git a/src/Build/BackEnd/Components/Logging/ITaskCacheDiagnosticSource.cs b/src/Build/BackEnd/Components/Logging/ITaskCacheDiagnosticSource.cs new file mode 100644 index 00000000000..6c792d9ff85 --- /dev/null +++ b/src/Build/BackEnd/Components/Logging/ITaskCacheDiagnosticSource.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd.Logging; + +/// +/// Observes diagnostics synchronously on the node executing a task, before logging is queued +/// or warning policy is applied. This does not drain events from remote nodes. +/// +internal interface ITaskCacheDiagnosticSource +{ + TaskCacheDiagnosticCapture CaptureTaskDiagnostics(BuildEventContext context); + + void RecordTaskError(BuildEventContext context); + + void RecordUnsupportedTaskWarning(BuildEventContext context, BuildWarningEventArgs warning); +} diff --git a/src/Build/BackEnd/Components/Logging/LoggingService.cs b/src/Build/BackEnd/Components/Logging/LoggingService.cs index 3ccb6b08c80..05b169ad7af 100644 --- a/src/Build/BackEnd/Components/Logging/LoggingService.cs +++ b/src/Build/BackEnd/Components/Logging/LoggingService.cs @@ -1351,6 +1351,12 @@ protected internal virtual void ProcessLoggingEvent(object buildEvent) } Assumed.NotNull(buildEvent, "buildEvent is null"); + TaskCacheDiagnosticCapture[] captures = Volatile.Read(ref _taskDiagnosticCaptures); + if (captures is not null) + { + RecordTaskDiagnostic(buildEvent, captures); + } + if (_logMode == LoggerMode.Asynchronous) { // Capture local references to prevent race with CleanLoggingEventProcessing diff --git a/src/Build/BackEnd/Components/Logging/LoggingServiceTaskDiagnostics.cs b/src/Build/BackEnd/Components/Logging/LoggingServiceTaskDiagnostics.cs new file mode 100644 index 00000000000..6c5cc109ab8 --- /dev/null +++ b/src/Build/BackEnd/Components/Logging/LoggingServiceTaskDiagnostics.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd.Logging; + +internal partial class LoggingService : ITaskCacheDiagnosticSource +{ + // Copy-on-write registration requires no allocations when disabled. Only a + // matching diagnostic takes the capture's gate to snapshot its payload and phase. + private TaskCacheDiagnosticCapture[]? _taskDiagnosticCaptures; + + public TaskCacheDiagnosticCapture CaptureTaskDiagnostics(BuildEventContext context) + { + ArgumentNullException.ThrowIfNull(context); + if (context.NodeId == BuildEventContext.InvalidNodeId + || context.ProjectContextId == BuildEventContext.InvalidProjectContextId + || context.TargetId == BuildEventContext.InvalidTargetId + || context.TaskId == BuildEventContext.InvalidTaskId) + { + throw new ArgumentException("A task build event context is required.", nameof(context)); + } + + TaskCacheDiagnosticCapture capture = new(this, context); + TaskCacheDiagnosticCapture[]? previous; + TaskCacheDiagnosticCapture[] updated; + do + { + previous = Volatile.Read(ref _taskDiagnosticCaptures); + int count = previous?.Length ?? 0; + updated = new TaskCacheDiagnosticCapture[count + 1]; + if (previous is not null) + { + Array.Copy(previous, updated, count); + } + + updated[count] = capture; + } + while (Interlocked.CompareExchange(ref _taskDiagnosticCaptures, updated, previous) != previous); + + return capture; + } + + internal void RemoveTaskDiagnosticCapture(TaskCacheDiagnosticCapture capture) + { + TaskCacheDiagnosticCapture[]? previous; + TaskCacheDiagnosticCapture[]? updated; + do + { + previous = Volatile.Read(ref _taskDiagnosticCaptures); + if (previous is null) + { + return; + } + + int index = Array.IndexOf(previous, capture); + if (index < 0) + { + return; + } + + updated = null; + if (previous.Length > 1) + { + updated = new TaskCacheDiagnosticCapture[previous.Length - 1]; + Array.Copy(previous, 0, updated, 0, index); + Array.Copy(previous, index + 1, updated, index, previous.Length - index - 1); + } + } + while (Interlocked.CompareExchange(ref _taskDiagnosticCaptures, updated, previous) != previous); + } + + public void RecordTaskError(BuildEventContext context) + { + TaskCacheDiagnosticCapture[]? captures = Volatile.Read(ref _taskDiagnosticCaptures); + if (captures is not null) + { + foreach (TaskCacheDiagnosticCapture capture in captures) + { + capture.RecordRawError(context); + } + } + } + + public void RecordUnsupportedTaskWarning(BuildEventContext context, BuildWarningEventArgs warning) + { + if (warning.GetType() == typeof(BuildWarningEventArgs)) + { + return; + } + + TaskCacheDiagnosticCapture[]? captures = Volatile.Read(ref _taskDiagnosticCaptures); + if (captures is not null) + { + foreach (TaskCacheDiagnosticCapture capture in captures) + { + capture.RecordDiagnostic(warning, context); + } + } + } + + private static void RecordTaskDiagnostic(object loggingEvent, TaskCacheDiagnosticCapture[] captures) + { + BuildEventArgs? buildEvent = loggingEvent switch + { + BuildEventArgs directEvent => directEvent, + KeyValuePair forwardedEvent => forwardedEvent.Value, + _ => null + }; + + if (buildEvent is not (BuildWarningEventArgs or BuildErrorEventArgs) + || buildEvent.BuildEventContext is not BuildEventContext context) + { + return; + } + + foreach (TaskCacheDiagnosticCapture capture in captures) + { + capture.RecordDiagnostic(buildEvent, context); + } + } +} diff --git a/src/Build/BackEnd/Components/Logging/TaskCacheDiagnosticCapture.cs b/src/Build/BackEnd/Components/Logging/TaskCacheDiagnosticCapture.cs new file mode 100644 index 00000000000..176ad4b31cc --- /dev/null +++ b/src/Build/BackEnd/Components/Logging/TaskCacheDiagnosticCapture.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd.Logging; + +/// +/// Watches the whole invocation while snapshotting only warnings from the phase +/// replaced by a hit. Setup, cleanup, raw errors, and unsupported warnings block caching. +/// +internal sealed class TaskCacheDiagnosticCapture : IDisposable +{ + private readonly LoggingService _source; + private readonly BuildEventContext _context; + private readonly object _gate = new(); + private List? _warnings; + private bool _disposed; + private bool _replayable; + private bool _hasDiagnostics; + private bool _blocking; + private int _warningBytes = sizeof(int); + + internal TaskCacheDiagnosticCapture(LoggingService source, BuildEventContext context) + { + _source = source; + _context = context; + } + + public bool HasDiagnostics + { + get + { + lock (_gate) + { + return _hasDiagnostics; + } + } + } + + internal bool HasBlockingDiagnostics + { + get + { + lock (_gate) + { + return _blocking; + } + } + } + + internal void BeginReplayablePhase() + { + lock (_gate) + { + _replayable = !_disposed; + } + } + + internal void EndReplayablePhase() + { + lock (_gate) + { + _replayable = false; + } + } + + internal TaskCacheWarning[] GetWarnings() + { + lock (_gate) + { + return _warnings?.ToArray() ?? []; + } + } + + internal void RecordRawError(BuildEventContext context) + { + if (context.NodeId == _context.NodeId && context.ProjectContextId == _context.ProjectContextId + && context.TargetId == _context.TargetId && context.TaskId == _context.TaskId) + { + lock (_gate) + { + if (!_disposed) + { + _hasDiagnostics = true; + _blocking = true; + } + } + } + } + + internal void RecordDiagnostic(BuildEventArgs buildEvent, BuildEventContext context) + { + // Task IDs alone are not unique across concurrent projects and nodes. + if (context.NodeId == _context.NodeId + && context.ProjectContextId == _context.ProjectContextId + && context.TargetId == _context.TargetId + && context.TaskId == _context.TaskId) + { + lock (_gate) + { + if (_disposed) + { + return; + } + _hasDiagnostics = true; + if (!_replayable || buildEvent is not BuildWarningEventArgs warning || _blocking) + { + _blocking = true; + return; + } + try + { + TaskCacheWarning? snapshot = TaskCacheWarning.Snapshot(warning, out int size); + if (snapshot is null || (_warnings?.Count ?? 0) >= TaskCacheWarning.MaximumCount + || size > TaskCacheWarning.MaximumPayloadBytes - _warningBytes) + { + _blocking = true; + return; + } + (_warnings ??= []).Add(snapshot); + _warningBytes += size; + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + // Logging must still handle the original event normally. + _blocking = true; + } + } + } + } + + public void Dispose() + { + lock (_gate) + { + if (_disposed) + { + return; + } + _disposed = true; + _replayable = false; + } + _source.RemoveTaskDiagnosticCapture(this); + } +} diff --git a/src/Build/BackEnd/Components/Logging/TaskCacheWarning.cs b/src/Build/BackEnd/Components/Logging/TaskCacheWarning.cs new file mode 100644 index 00000000000..5107f297edf --- /dev/null +++ b/src/Build/BackEnd/Components/Logging/TaskCacheWarning.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd.Logging; + +/// +/// Immutable, eagerly formatted warning payload. Context, project file, timestamp, +/// and thread identity belong to the current invocation, not to the cached result. +/// +internal sealed record TaskCacheWarning( + string? Subcategory, string? Code, string? File, string? Message, + string? HelpKeyword, string? SenderName, string? HelpLink, + int LineNumber, int ColumnNumber, int EndLineNumber, int EndColumnNumber) +{ + internal const int MaximumCount = 1024; + internal const int MaximumStringBytes = 1024 * 1024; + internal const int MaximumPayloadBytes = 16 * 1024 * 1024; + private static readonly UTF8Encoding s_encoding = new(false, true); + + internal static TaskCacheWarning? Snapshot(BuildWarningEventArgs warning, out int size) + { + size = 0; + // Unknown subclasses may carry additional mutable or structured state. + if (warning.GetType() != typeof(BuildWarningEventArgs) + || warning.LineNumber < 0 || warning.ColumnNumber < 0 + || warning.EndLineNumber < 0 || warning.EndColumnNumber < 0) + { + return null; + } + + TaskCacheWarning value = new(warning.Subcategory, warning.Code, warning.File, + warning.Message, warning.HelpKeyword, warning.SenderName, warning.HelpLink, + warning.LineNumber, warning.ColumnNumber, warning.EndLineNumber, warning.EndColumnNumber); + size = 4 * sizeof(int) + StringSize(value.Subcategory) + StringSize(value.Code) + + StringSize(value.File) + StringSize(value.Message) + StringSize(value.HelpKeyword) + + StringSize(value.SenderName) + StringSize(value.HelpLink); + return value; + } + + internal BuildWarningEventArgs ToEvent(BuildEventContext context) => + new(Subcategory, Code, File, LineNumber, ColumnNumber, EndLineNumber, EndColumnNumber, + Message, HelpKeyword, SenderName, HelpLink, DateTime.UtcNow, messageArgs: null) + { + BuildEventContext = context, + }; + + internal static void Write(BinaryWriter writer, IReadOnlyList warnings) + { + if (warnings.Count > MaximumCount) + { + throw new InvalidDataException("Too many cached task warnings."); + } + long start = writer.BaseStream.Position; + writer.Write(warnings.Count); + foreach (TaskCacheWarning warning in warnings) + { + WriteString(writer, warning.Subcategory); + WriteString(writer, warning.Code); + WriteString(writer, warning.File); + WriteString(writer, warning.Message); + WriteString(writer, warning.HelpKeyword); + WriteString(writer, warning.SenderName); + WriteString(writer, warning.HelpLink); + writer.Write(warning.LineNumber); + writer.Write(warning.ColumnNumber); + writer.Write(warning.EndLineNumber); + writer.Write(warning.EndColumnNumber); + if (writer.BaseStream.Position - start > MaximumPayloadBytes) + { + throw new InvalidDataException("Cached task warnings exceed the supported size."); + } + } + } + + internal static TaskCacheWarning[] Read(BinaryReader reader) + { + long remaining = reader.BaseStream.Length - reader.BaseStream.Position; + if (remaining < sizeof(int) || remaining > MaximumPayloadBytes) + { + throw new InvalidDataException("Invalid cached task warning payload size."); + } + int count = reader.ReadInt32(); + if (count < 0 || count > MaximumCount) + { + throw new InvalidDataException("Invalid cached task warning count."); + } + TaskCacheWarning[] warnings = new TaskCacheWarning[count]; + for (int i = 0; i < count; i++) + { + TaskCacheWarning warning = new(ReadString(reader), ReadString(reader), ReadString(reader), + ReadString(reader), ReadString(reader), ReadString(reader), ReadString(reader), + reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32()); + if (warning.LineNumber < 0 || warning.ColumnNumber < 0 + || warning.EndLineNumber < 0 || warning.EndColumnNumber < 0) + { + throw new InvalidDataException("Invalid cached task warning location."); + } + warnings[i] = warning; + } + return warnings; + } + + private static int StringSize(string? value) + { + if (value is null) + { + return sizeof(int); + } + if (value.Length > MaximumStringBytes) + { + throw new InvalidDataException("Cached task warning string exceeds the supported size."); + } + int bytes = s_encoding.GetByteCount(value); + if (bytes > MaximumStringBytes) + { + throw new InvalidDataException("Cached task warning string exceeds the supported size."); + } + return sizeof(int) + bytes; + } + + private static void WriteString(BinaryWriter writer, string? value) + { + StringSize(value); + if (value is null) + { + writer.Write(-1); + return; + } + byte[] bytes = s_encoding.GetBytes(value); + writer.Write(bytes.Length); + writer.Write(bytes); + } + + private static string? ReadString(BinaryReader reader) + { + int length = reader.ReadInt32(); + if (length == -1) + { + return null; + } + if (length < 0 || length > MaximumStringBytes || length > reader.BaseStream.Length - reader.BaseStream.Position) + { + throw new InvalidDataException("Invalid cached task warning string size."); + } + try + { + return s_encoding.GetString(reader.ReadBytes(length)); + } + catch (DecoderFallbackException e) + { + throw new InvalidDataException("Invalid cached task warning text.", e); + } + } +} diff --git a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs index f2d7f419c40..416725153ae 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs @@ -367,7 +367,7 @@ public async Task BuildProjects(string[] projectFiles, PropertyDi // Otherwise let the BuildRequestConfiguration figure out what tools version will be used BuildRequestData data = new BuildRequestData(projectFiles[i], properties[i].ToDictionary(), explicitToolsVersion, targets, null); - BuildRequestConfiguration config = new BuildRequestConfiguration(data, _componentHost.BuildParameters.DefaultToolsVersion); + BuildRequestConfiguration config = new BuildRequestConfiguration(data, _componentHost.BuildParameters.DefaultToolsVersion, _componentHost.BuildParameters.TaskCache); ProjectIsolationMode isolateProjects = _componentHost.BuildParameters.ProjectIsolationMode; bool skipStaticGraphIsolationConstraints = (isolateProjects != ProjectIsolationMode.False && _requestEntry.RequestConfiguration.ShouldSkipIsolationConstraintsForReference(config.ProjectFullPath)) || isolateProjects == ProjectIsolationMode.MessageUponIsolationViolation; diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs index 5222a68a822..93d14cd8c3f 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Build.BackEnd.Components.RequestBuilder; +using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Collections; using Microsoft.Build.Evaluation; using Microsoft.Build.Eventing; @@ -672,6 +673,17 @@ private void ExecuteIntrinsicTask(ItemBucket bucket) /// private async Task InitializeAndExecuteTask(TaskLoggingContext taskLoggingContext, ItemBucket bucket, TaskHostParameters taskIdentityParameters, TaskHost taskHost, TaskExecutionMode howToExecuteTask) { + using TaskCacheDiagnosticCapture diagnosticCapture = _componentHost.BuildParameters.TaskCache + && taskLoggingContext.LoggingService is ITaskCacheDiagnosticSource diagnosticSource + ? diagnosticSource.CaptureTaskDiagnostics(taskLoggingContext.BuildEventContext) + : null; + + if (diagnosticCapture is not null) + { + // Initialization diagnostics must belong to the task being watched. + taskHost.LoggingContext = taskLoggingContext; + } + if (!_taskExecutionHost.InitializeForBatch(taskLoggingContext, bucket, taskIdentityParameters, _buildRequestEntry.Request.ScheduledNodeId)) { ProjectErrorUtilities.ThrowInvalidProject(_targetChildInstance.Location, "TaskDeclarationOrUsageError", _taskNode.Name); @@ -679,15 +691,18 @@ private async Task InitializeAndExecuteTask(TaskLoggingContext t using var assemblyLoadsTracker = AssemblyLoadsTracker.StartTracking(taskLoggingContext, AssemblyLoadingContext.TaskRun, _taskExecutionHost?.TaskInstance?.GetType()); + bool succeeded = false; try { // UNDONE: Move this and the task host. taskHost.LoggingContext = taskLoggingContext; - return await ExecuteInstantiatedTask(_taskExecutionHost, taskLoggingContext, taskHost, bucket, howToExecuteTask); + WorkUnitResult result = await ExecuteInstantiatedTask(_taskExecutionHost, taskLoggingContext, taskHost, bucket, howToExecuteTask, diagnosticCapture); + succeeded = result.ResultCode == WorkUnitResultCode.Success; + return result; } finally { - _taskExecutionHost.CleanupForBatch(); + _taskExecutionHost.CleanupForBatch(succeeded && !_cancellationToken.IsCancellationRequested, diagnosticCapture); } } @@ -754,8 +769,9 @@ private void UpdateContinueOnError(ItemBucket bucket, TaskHost taskHost) /// The task host for the task. /// The batching bucket /// The task execution mode + /// Raw diagnostics observed since task initialization. /// The result of running the task. - private async ValueTask ExecuteInstantiatedTask(TaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask) + private async ValueTask ExecuteInstantiatedTask(TaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask, TaskCacheDiagnosticCapture diagnosticCapture) { UpdateContinueOnError(bucket, taskHost); @@ -771,13 +787,19 @@ private async ValueTask ExecuteInstantiatedTask(TaskExecutionHos } else { + bool cacheHit = taskExecutionHost.TryRestoreTaskCache(_taskNode, diagnosticCapture); bool taskReturned = false; Exception taskException = null; // If this is the MSBuild task, we need to execute it's special internal method. try { - if (taskExecutionHost.TaskInstance is MSBuild msbuildTask) + if (cacheHit) + { + taskExecutionHost.ReplayTaskCacheWarnings(); + taskResult = !_cancellationToken.IsCancellationRequested; + } + else if (taskExecutionHost.TaskInstance is MSBuild msbuildTask) { // https://github.com/dotnet/msbuild/issues/11025 // The metaproject's inner tasks are generator-authored plumbing, diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskCacheBackend.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskCacheBackend.cs new file mode 100644 index 00000000000..5fe14980c3c --- /dev/null +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskCacheBackend.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using Microsoft.Build.Shared; +#if FEATURE_BUILDXL_TASK_CACHE +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Runtime.ExceptionServices; +using Microsoft.Build.Framework; +#endif + +namespace Microsoft.Build.BackEnd; + +/// +/// Build-scoped storage for opaque manifests and immutable file content. +/// +internal abstract class TaskCacheBackend : IDisposable +{ + internal static bool IsSupported => +#if FEATURE_BUILDXL_TASK_CACHE + System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64; +#else + false; +#endif + + internal static TaskCacheBackend Create(string directory, CancellationToken cancellationToken = default) + { +#if FEATURE_BUILDXL_TASK_CACHE + if (FeatureSwitches.EnableReflectiveTaskExecution) + { + return CreateOptionalBackend(directory, cancellationToken); + } +#endif + throw new NotSupportedException(ResourceUtilities.GetResourceString("TaskCache.StorageUnavailable")); + } + +#if FEATURE_BUILDXL_TASK_CACHE + [RequiresUnreferencedCode("Loads the optional, untrimmed task cache runtime by its fixed assembly and type names.")] + private static TaskCacheBackend CreateOptionalBackend(string directory, CancellationToken cancellationToken) + { + try + { + Type type = Type.GetType("Microsoft.Build.BackEnd.BuildXLTaskCacheBackend, Microsoft.Build.TaskCache", throwOnError: true)!; + return (TaskCacheBackend)Activator.CreateInstance(type, BindingFlags.Instance | BindingFlags.NonPublic, + binder: null, args: [directory, 10240u, cancellationToken], culture: CultureInfo.InvariantCulture)!; + } + catch (TargetInvocationException e) when (e.InnerException is not null) + { + ExceptionDispatchInfo.Capture(e.InnerException).Throw(); + throw; + } + catch (FileNotFoundException e) + { + throw new IOException(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskCache.RuntimeUnavailable"), e); + } + } +#endif + + protected static bool IsCriticalException(Exception exception) => Microsoft.Build.Framework.ExceptionHandling.IsCriticalException(exception); + + protected static string FormatOwnershipFailure(string directory) => + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskCache.OwnershipUnavailable", directory); + + internal abstract byte[]? ReadManifest(string key, int maximumSize, CancellationToken cancellationToken = default); + + internal abstract Stream Open(string digest, CancellationToken cancellationToken = default); + + internal abstract string ContentPath(string digest); + + internal abstract string PutFile(string path, CancellationToken cancellationToken = default); + + internal abstract bool Publish(string key, byte[] manifest, Func? canPublish = null, + IReadOnlyList? artifacts = null, CancellationToken cancellationToken = default); + + public abstract void Dispose(); +} diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskCacheClient.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskCacheClient.cs new file mode 100644 index 00000000000..5c2291e3144 --- /dev/null +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskCacheClient.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Framework; + +namespace Microsoft.Build.BackEnd; + +/// A worker never opens LocalCache; storage calls use its existing coordinator connection. +internal sealed class TaskCacheClient(string directory, Action send) : TaskCacheBackend, INodePacketHandler +{ + private const string EmptySha256Digest = "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"; + private readonly ConcurrentDictionary> _pending = new(); + private int _nextId; + private int _stopped; + + private TaskCachePacket Call(TaskCachePacket request, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + request.Id = Interlocked.Increment(ref _nextId); + TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + _pending[request.Id] = completion; + try + { + if (Volatile.Read(ref _stopped) != 0) + { + throw new IOException("The task cache coordinator connection was closed."); + } + + send(request); + using CancellationTokenRegistration registration = token.Register(() => + { + try + { + send(new TaskCachePacket(NodePacketType.TaskCacheCancel) { Id = request.Id }); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + Dispose(); + } + }); + // Cancellation waits for the owner's completion/cleanup acknowledgement. + // Connection loss completes all waiters independently of this token. + TaskCachePacket response = completion.Task.GetAwaiter().GetResult(); + if (response.Id != request.Id || response.Operation != request.Operation || response.Key != request.Key) + { + throw new InvalidDataException("Mismatched task cache response."); + } + if (response.Cancelled) + { + throw new OperationCanceledException(token); + } + if (response.Error is not null) + { + throw new IOException(response.Error); + } + token.ThrowIfCancellationRequested(); + return response; + } + finally + { + _pending.TryRemove(request.Id, out _); + } + } + + public void PacketReceived(int node, INodePacket packet) + { + TaskCachePacket response = (TaskCachePacket)packet; + if (_pending.TryGetValue(response.Id, out TaskCompletionSource? completion)) + { + completion.TrySetResult(response); + } + else + { + Dispose(); + } + } + + internal override byte[]? ReadManifest(string key, int maximumSize, CancellationToken cancellationToken = default) => + Call(new(NodePacketType.TaskCacheRequest) + { + Operation = (int)TaskCacheOperation.ReadManifest, Key = key, MaximumSize = maximumSize, + }, cancellationToken).Data; + + internal override Stream Open(string digest, CancellationToken cancellationToken = default) + { + TaskCachePacket response = Call(new(NodePacketType.TaskCacheRequest) + { + Operation = (int)TaskCacheOperation.Open, Key = digest, + }, cancellationToken); + if (digest == EmptySha256Digest) + { + // BuildXL can synthesize this content without a physical CAS file. + // Still require the owner's successful, correlated response above. + return new MemoryStream([], writable: false); + } + string path = response.Path ?? throw new InvalidDataException("Missing task cache content path."); + if (!Path.IsPathRooted(path) + || !FileUtilities.PathComparer.Equals(Path.GetFullPath(path), path) + || !path.StartsWith(directory.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, + FileUtilities.IsFileSystemCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("Invalid task cache content path."); + } + TaskCacheStore.CheckPath(path, allowDirectory: false, allowReadOnlyFile: true); + return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete); + } + + internal override string ContentPath(string digest) => throw new NotSupportedException(); + + internal override string PutFile(string path, CancellationToken cancellationToken = default) => + TaskCacheStore.ValidateDigest(Call(new(NodePacketType.TaskCacheRequest) + { + Operation = (int)TaskCacheOperation.PutFile, Path = path, + }, cancellationToken).Path ?? String.Empty); + + internal override bool Publish(string key, byte[] manifest, Func? canPublish = null, + IReadOnlyList? artifacts = null, CancellationToken cancellationToken = default) + { + if (canPublish?.Invoke() == false) + { + return false; + } + string[] hashes = new string[artifacts?.Count ?? 0]; + for (int i = 0; i < hashes.Length; i++) + { + hashes[i] = artifacts![i]; + } + return Call(new(NodePacketType.TaskCacheRequest) + { + Operation = (int)TaskCacheOperation.Publish, Key = key, Data = manifest, Artifacts = hashes, + }, cancellationToken).Published; + } + + public override void Dispose() + { + Interlocked.Exchange(ref _stopped, 1); + foreach (TaskCompletionSource completion in _pending.Values) + { + completion.TrySetException(new IOException("The task cache coordinator connection was closed.")); + } + } +} diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskCacheOwner.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskCacheOwner.cs new file mode 100644 index 00000000000..c78b1acf337 --- /dev/null +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskCacheOwner.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using ExceptionDispatchInfo = System.Runtime.ExceptionServices.ExceptionDispatchInfo; + +namespace Microsoft.Build.BackEnd; + +/// +/// Owns storage until the build and all storage calls have drained. The gate only +/// protects call lifetimes; no I/O, task execution, or packet sending runs under it. +/// +internal sealed class TaskCacheOwner : TaskCacheBackend, INodePacketHandler +{ + private readonly TaskCacheBackend _backend; + private readonly string _directory; + private readonly object _gate = new(); + private readonly CancellationTokenSource _shutdown; + private readonly Dictionary<(int Node, int Id), CancellationTokenSource> _requests = []; + private readonly Action _send; + private readonly Action? _reportFailure; + private int _active; + private bool _stopping; + + internal TaskCacheOwner(string directory, Action send, CancellationToken cancellationToken, + TaskCacheBackend? backend = null, Action? reportFailure = null) + { + _directory = directory; + _send = send; + _reportFailure = reportFailure; + _shutdown = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + try + { + cancellationToken.ThrowIfCancellationRequested(); + _backend = backend ?? Create(directory, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + } + catch + { + try + { + _backend?.Dispose(); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + // Preserve the initialization failure. + } + _shutdown.Dispose(); + throw; + } + } + + private CancellationTokenSource Enter(CancellationToken token) + { + lock (_gate) + { + if (_stopping) + { + throw new OperationCanceledException("The task cache owner is shutting down."); + } + + CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(token, _shutdown.Token); + _active++; + return linked; + } + } + + private void Leave() + { + lock (_gate) + { + _active--; + Monitor.PulseAll(_gate); + } + } + + private T Run(CancellationToken token, Func action) + { + using CancellationTokenSource linked = Enter(token); + try + { + linked.Token.ThrowIfCancellationRequested(); + return action(linked.Token); + } + finally + { + Leave(); + } + } + + internal override byte[]? ReadManifest(string key, int maximumSize, CancellationToken cancellationToken = default) => + Run(cancellationToken, token => _backend.ReadManifest(TaskCacheStore.ValidateDigest(key), maximumSize, token)); + + internal override Stream Open(string digest, CancellationToken cancellationToken = default) => + Run(cancellationToken, token => _backend.Open(TaskCacheStore.ValidateDigest(digest), token)); + + internal override string ContentPath(string digest) => _backend.ContentPath(TaskCacheStore.ValidateDigest(digest)); + + internal override string PutFile(string path, CancellationToken cancellationToken = default) => + Run(cancellationToken, token => + { + ValidateInputPath(path); + return _backend.PutFile(path, token); + }); + + internal override bool Publish(string key, byte[] manifest, Func? canPublish = null, + IReadOnlyList? artifacts = null, CancellationToken cancellationToken = default) => + Run(cancellationToken, token => _backend.Publish(TaskCacheStore.ValidateDigest(key), manifest, canPublish, artifacts, token)); + + private void ValidateInputPath(string path) + { + if (!Path.IsPathRooted(path) || !FileUtilities.PathComparer.Equals(Path.GetFullPath(path), path) + || FileUtilities.PathComparer.Equals(path, _directory) + || path.StartsWith(_directory.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, + FileUtilities.IsFileSystemCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("Invalid task cache artifact path."); + } + + TaskCacheStore.CheckPath(path, allowDirectory: false); + } + + public void PacketReceived(int node, INodePacket packet) + { + TaskCachePacket request = (TaskCachePacket)packet; + if (packet.Type == NodePacketType.TaskCacheCancel) + { + CancellationTokenSource? cancellation; + lock (_gate) + { + _requests.TryGetValue((node, request.Id), out cancellation); + } + + try + { + cancellation?.Cancel(); + } + catch (ObjectDisposedException) + { + // Completion raced cancellation; its response has already been sent. + } + return; + } + + CancellationTokenSource operation = Enter(default); + lock (_gate) + { + if (_requests.ContainsKey((node, request.Id))) + { + Leave(); + operation.Dispose(); + throw new InvalidDataException("Duplicate task cache request."); + } + _requests.Add((node, request.Id), operation); + } + + // Do not run storage on the packet pump or the scheduler queue. Either may + // be needed to deliver the result that allows the requesting task to finish. + _ = Task.Run(() => + { + TaskCachePacket response = new(NodePacketType.TaskCacheResponse) + { + Id = request.Id, + Operation = request.Operation, + Key = request.Key, + }; + try + { + operation.Token.ThrowIfCancellationRequested(); + switch ((TaskCacheOperation)request.Operation) + { + case TaskCacheOperation.ReadManifest: + if (request.MaximumSize is < 32 or > 64 * 1024 * 1024) + { + throw new InvalidDataException("Invalid task cache manifest limit."); + } + response.Data = ReadManifest(request.Key, request.MaximumSize, operation.Token); + break; + case TaskCacheOperation.Open: + using (Open(request.Key, operation.Token)) + { + response.Path = ContentPath(request.Key); + } + break; + case TaskCacheOperation.PutFile: + response.Path = PutFile(request.Path ?? throw new InvalidDataException("Missing artifact path."), operation.Token); + break; + case TaskCacheOperation.Publish: + if (request.Data is null || request.Data.Length is < 32 or > 64 * 1024 * 1024) + { + throw new InvalidDataException("Invalid task cache manifest size."); + } + response.Published = Publish(request.Key, request.Data, artifacts: request.Artifacts, cancellationToken: operation.Token); + break; + default: + throw new InvalidDataException("Unknown task cache operation."); + } + } + catch (OperationCanceledException) + { + response.Cancelled = true; + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + response.Error = e.Message; + } + finally + { + try + { + _send(node, response); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + // Sending can fail before the transport reports connection loss. + // Fail the build so shutdown also releases the worker's waiter. + _reportFailure?.Invoke(e); + } + finally + { + lock (_gate) + { + _requests.Remove((node, request.Id)); + operation.Dispose(); + } + Leave(); + } + } + }); + } + + public override void Dispose() + { + lock (_gate) + { + if (_stopping) + { + return; + } + _stopping = true; + } + + Exception? cancellationFailure = null; + try + { + _shutdown.Cancel(); + } + catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) + { + cancellationFailure = e; + } + lock (_gate) + { + while (_active != 0) + { + Monitor.Wait(_gate); + } + } + + try + { + _backend.Dispose(); + } + catch (Exception e) when (cancellationFailure is not null && !ExceptionHandling.IsCriticalException(e)) + { + // Preserve the earlier cancellation failure, after releasing storage. + } + finally + { + _shutdown.Dispose(); + } + + if (cancellationFailure is not null) + { + ExceptionDispatchInfo.Capture(cancellationFailure).Throw(); + } + } +} diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskCachePacket.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskCachePacket.cs new file mode 100644 index 00000000000..e26b12e7270 --- /dev/null +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskCachePacket.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.BackEnd; + +internal enum TaskCacheOperation +{ + ReadManifest, + Open, + PutFile, + Publish, +} + +/// Storage-only RPC carried by the existing, version-negotiated worker connection. +internal sealed class TaskCachePacket(NodePacketType type) : INodePacket +{ + internal int Id; + internal int Operation; + internal string Key = String.Empty; + internal string? Path; + internal byte[]? Data; + internal string[]? Artifacts; + internal int MaximumSize; + internal string? Error; + internal bool Cancelled; + internal bool Published; + + public NodePacketType Type { get; } = type; + + public void Translate(ITranslator translator) + { + translator.Translate(ref Id); + translator.Translate(ref Operation); + translator.Translate(ref Key); + translator.Translate(ref Path); + translator.Translate(ref Data); + translator.Translate(ref Artifacts); + translator.Translate(ref MaximumSize); + translator.Translate(ref Error); + translator.Translate(ref Cancelled); + translator.Translate(ref Published); + } + + internal static INodePacket ReadRequest(ITranslator translator) => Read(NodePacketType.TaskCacheRequest, translator); + internal static INodePacket ReadResponse(ITranslator translator) => Read(NodePacketType.TaskCacheResponse, translator); + internal static INodePacket ReadCancel(ITranslator translator) => Read(NodePacketType.TaskCacheCancel, translator); + + private static TaskCachePacket Read(NodePacketType type, ITranslator translator) + { + TaskCachePacket packet = new(type); + packet.Translate(translator); + return packet; + } +} diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskCacheStore.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskCacheStore.cs new file mode 100644 index 00000000000..8e4e1272586 --- /dev/null +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskCacheStore.cs @@ -0,0 +1,400 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd; + +/// +/// Backend-independent task-contract validation, manifests, and safe restoration. +/// Restoration paths come from the current task contract, never from cache data alone. +/// +internal sealed class TaskCacheStore +{ + private const int MaxManifestSize = 64 * 1024 * 1024; + private readonly string _directory; + private readonly TaskCacheBackend _backend; + + internal TaskCacheStore(string directory, TaskCacheBackend backend) + { + EnsureSupported(); + _directory = Path.GetFullPath(directory); + _backend = backend ?? throw new ArgumentNullException(nameof(backend)); + CheckPath(_directory, allowDirectory: true); + } + + internal bool TryRestore(string key, IReadOnlyList outputPaths, out byte[] state, Action? validateState = null, + CancellationToken cancellationToken = default, Action? onReplacementStarted = null) + { + cancellationToken.ThrowIfCancellationRequested(); + ValidatePaths(outputPaths); + TaskCacheBackend backend = _backend; + byte[]? manifest = backend.ReadManifest(ValidateDigest(key), MaxManifestSize, cancellationToken); + if (manifest is null) + { + state = []; + return false; + } + + using SHA256 hash = SHA256.Create(); + byte[] checksum = hash.ComputeHash(manifest, 0, manifest.Length - 32); + for (int i = 0; i < checksum.Length; i++) + { + if (checksum[i] != manifest[manifest.Length - 32 + i]) + { + throw new InvalidDataException("Task cache manifest checksum mismatch."); + } + } + + List artifacts = []; + using (MemoryStream stream = new(manifest, 0, manifest.Length - 32, writable: false)) + using (BinaryReader reader = new(stream, Encoding.UTF8)) + { + if (reader.ReadInt32() != outputPaths.Count) + { + throw new InvalidDataException("Task cache manifest does not match the task contract."); + } + + for (int i = 0; i < outputPaths.Count; i++) + { + if (!String.Equals(ReadString(reader), outputPaths[i], StringComparison.Ordinal)) + { + throw new InvalidDataException("Task cache output path does not match the task contract."); + } + + bool exists = reader.ReadBoolean(); + string digest = exists ? ReadString(reader) : String.Empty; + long ticks = reader.ReadInt64(); + int mode = reader.ReadInt32(); + if (ticks < DateTime.MinValue.Ticks || ticks > DateTime.MaxValue.Ticks || mode < 0 || mode > 0x1ff) + { + throw new InvalidDataException("Invalid task cache file attributes."); + } + + if (exists) + { + ValidateDigest(digest); + } + + artifacts.Add(new Artifact(exists, digest, ticks, mode)); + } + + int stateLength = reader.ReadInt32(); + if (stateLength < 0 || stateLength > stream.Length - stream.Position) + { + throw new InvalidDataException("Invalid task cache state size."); + } + + state = reader.ReadBytes(stateLength); + if (stream.Position != stream.Length) + { + throw new InvalidDataException("Unexpected data in task cache manifest."); + } + } + + // Validate raw task outputs before any output files are changed. + validateState?.Invoke(state); + + List stagingPaths = []; + try + { + // All blobs are verified before any destination is touched. Once staging starts, + // failures are fatal: falling back cannot undo arbitrary partial restoration. + for (int i = 0; i < outputPaths.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + CheckPath(outputPaths[i], allowDirectory: false); + if (!artifacts[i].Exists) + { + stagingPaths.Add(String.Empty); + continue; + } + + string parent = Path.GetDirectoryName(outputPaths[i])!; + Directory.CreateDirectory(parent); + string staging = Path.Combine(parent, ".msbuild-cache-" + Guid.NewGuid().ToString("N")); + stagingPaths.Add(staging); + using (Stream content = backend.Open(artifacts[i].Digest, cancellationToken)) + using (FileStream destination = CreateStagingFile(staging)) + { + content.CopyToAsync(destination, 81920, cancellationToken).GetAwaiter().GetResult(); + } + + if (!String.Equals(HashFile(staging), artifacts[i].Digest, StringComparison.Ordinal)) + { + throw new InvalidDataException("Task cache content changed during restoration."); + } + +#if NET + if (!OperatingSystem.IsWindows()) + { + File.SetUnixFileMode(staging, (UnixFileMode)artifacts[i].Mode); + } +#endif + // Restored artifacts are newly produced for this invocation. Reusing an old + // cache timestamp would keep timestamp-based targets perpetually out of date. + File.SetLastWriteTimeUtc(staging, DateTime.UtcNow); + } + + cancellationToken.ThrowIfCancellationRequested(); + // Cancellation must not interrupt the bounded replacement phase. + onReplacementStarted?.Invoke(); + for (int i = 0; i < outputPaths.Count; i++) + { + CheckPath(outputPaths[i], allowDirectory: false); + if (artifacts[i].Exists) + { +#if NET + File.Move(stagingPaths[i], outputPaths[i], overwrite: true); +#else + if (File.Exists(outputPaths[i])) + { + File.Replace(stagingPaths[i], outputPaths[i], null); + } + else + { + File.Move(stagingPaths[i], outputPaths[i]); + } +#endif + } + else + { + try + { + File.Delete(outputPaths[i]); + } + catch (DirectoryNotFoundException) + { + // A missing parent already satisfies an absent-output record. + } + } + } + + cancellationToken.ThrowIfCancellationRequested(); + } + catch (Exception e) when (e is InvalidDataException || ExceptionHandling.IsIoRelatedException(e)) + { + throw new TaskCacheRestoreException(e); + } + finally + { + foreach (string path in stagingPaths) + { + if (path.Length > 0) + { + try + { + File.Delete(path); + } + catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) + { + // Best-effort cleanup must not hide the original restore failure. + } + } + } + } + + return true; + } + + private static FileStream CreateStagingFile(string path) + { +#if NET + if (!OperatingSystem.IsWindows()) + { + return new FileStream(path, new FileStreamOptions + { + Mode = FileMode.CreateNew, + Access = FileAccess.Write, + Share = FileShare.None, + UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite, + }); + } +#endif + return new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None); + } + + internal bool Store(string key, IReadOnlyList outputPaths, byte[] state, Func? canPublish = null, + CancellationToken cancellationToken = default) + { + ValidatePaths(outputPaths); + ValidateDigest(key); + cancellationToken.ThrowIfCancellationRequested(); + TaskCacheBackend backend = _backend; + List artifacts = []; + using MemoryStream stream = new(); + using (BinaryWriter writer = new(stream, Encoding.UTF8, leaveOpen: true)) + { + writer.Write(outputPaths.Count); + foreach (string output in outputPaths) + { + cancellationToken.ThrowIfCancellationRequested(); + CheckPath(output, allowDirectory: false); + WriteString(writer, output); + bool exists = File.Exists(output); + writer.Write(exists); + if (exists) + { + string digest = backend.PutFile(output, cancellationToken); + artifacts.Add(digest); + WriteString(writer, digest); + } + + writer.Write(exists ? File.GetLastWriteTimeUtc(output).Ticks : 0); + int mode = 0; +#if NET + if (exists && !OperatingSystem.IsWindows()) + { + mode = (int)File.GetUnixFileMode(output) & 0x1ff; + } +#endif + writer.Write(mode); + } + + writer.Write(state.Length); + writer.Write(state); + } + + if (stream.Length + 32 > MaxManifestSize) + { + throw new IOException("Task cache manifest exceeds the supported size."); + } + + byte[] payload = stream.ToArray(); + using SHA256 hash = SHA256.Create(); + byte[] checksum = hash.ComputeHash(payload); + stream.Write(checksum, 0, checksum.Length); + return backend.Publish(key, stream.ToArray(), canPublish, artifacts, cancellationToken); + } + + internal static void CreateCacheDirectory(string path) + { + CheckPath(path, allowDirectory: true); +#if NET + if (!OperatingSystem.IsWindows()) + { + Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + return; + } +#endif + Directory.CreateDirectory(path); + } + + private void ValidatePaths(IReadOnlyList paths) + { + CheckPath(_directory, allowDirectory: true); + HashSet unique = new(FileUtilities.PathComparer); + foreach (string path in paths) + { + if (!Path.IsPathRooted(path) || !unique.Add(path) + || FileUtilities.PathComparer.Equals(path, _directory) + || path.StartsWith(_directory.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, + FileUtilities.IsFileSystemCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidDataException("Invalid or overlapping task cache output path."); + } + + CheckPath(path, allowDirectory: false); + } + } + + internal static string ValidateDigest(string digest) + { + if (digest.Length != 64) + { + throw new InvalidDataException("Invalid task cache digest."); + } + + foreach (char c in digest) + { + if (c is not (>= '0' and <= '9' or >= 'A' and <= 'F')) + { + throw new InvalidDataException("Invalid task cache digest."); + } + } + + return digest; + } + internal static void CheckPath(string path, bool allowDirectory, bool allowReadOnlyFile = false) + { + bool first = true; + for (string? current = Path.GetFullPath(path); current is not null; current = Path.GetDirectoryName(current)) + { + FileAttributes attributes; + try + { + attributes = File.GetAttributes(current); + } + catch (FileNotFoundException) + { + first = false; + continue; + } + catch (DirectoryNotFoundException) + { + first = false; + continue; + } + + if ((attributes & FileAttributes.ReparsePoint) != 0 + || (first && !allowDirectory && (attributes & FileAttributes.Directory) != 0) + || (first && !allowDirectory && !allowReadOnlyFile && (attributes & FileAttributes.ReadOnly) != 0)) + { + throw new IOException("Task caching does not support symbolic links, directories as files, or read-only outputs: " + path); + } + + first = false; + } + } + + private static string HashFile(string path) + { + using FileStream stream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read); + return HashStream(stream); + } + + private static string HashStream(Stream stream) + { + using SHA256 hash = SHA256.Create(); + return BitConverter.ToString(hash.ComputeHash(stream)).Replace("-", String.Empty); + } + + private static void WriteString(BinaryWriter writer, string value) + { + byte[] bytes = Encoding.UTF8.GetBytes(value); + writer.Write(bytes.Length); + writer.Write(bytes); + } + + private static string ReadString(BinaryReader reader) + { + int length = reader.ReadInt32(); + if (length < 0 || length > 1024 * 1024 || length > reader.BaseStream.Length - reader.BaseStream.Position) + { + throw new InvalidDataException("Invalid task cache string size."); + } + + return Encoding.UTF8.GetString(reader.ReadBytes(length)); + } + + private readonly record struct Artifact(bool Exists, string Digest, long Ticks, int Mode); + internal static bool IsSupported => TaskCacheBackend.IsSupported; + + internal static void EnsureSupported() + { + if (!IsSupported) + { + throw new NotSupportedException(ResourceUtilities.GetResourceString("TaskCache.StorageUnavailable")); + } + } +} + +internal sealed class TaskCacheRestoreException(Exception inner) + : IOException("Task cache output restoration failed. Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. " + inner.Message, inner); diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs index 57e540e2d6b..ec1dce6b17d 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskHost.cs @@ -12,6 +12,7 @@ using System.Diagnostics; using System.Threading.Tasks; using Microsoft.Build.BackEnd.Components.Caching; +using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Collections; using Microsoft.Build.Eventing; using Microsoft.Build.Execution; @@ -428,6 +429,13 @@ public void LogErrorEvent(Microsoft.Build.Framework.BuildErrorEventArgs e) return; } + // Observe the raw error before either serialization rejection or + // ContinueOnError can turn it into an apparently replayable warning. + if (_taskLoggingContext.LoggingService is ITaskCacheDiagnosticSource diagnosticSource) + { + diagnosticSource.RecordTaskError(_taskLoggingContext.BuildEventContext); + } + // If we are in building across process we need the events to be serializable. This method will // check to see if we are building with multiple process and if the event is serializable. It will // also log a warning if the event is not serializable and drop the logging message. @@ -535,6 +543,13 @@ public void LogWarningEvent(Microsoft.Build.Framework.BuildWarningEventArgs e) return; } + // Transport filtering can replace an unsupported event with a standard warning. + // Observe the original type first so the replacement cannot make it cacheable. + if (_taskLoggingContext.LoggingService is ITaskCacheDiagnosticSource diagnosticSource) + { + diagnosticSource.RecordUnsupportedTaskWarning(_taskLoggingContext.BuildEventContext, e); + } + // If we are in building across process we need the events to be serializable. This method will // check to see if we are building with multiple process and if the event is serializable. It will // also log a warning if the event is not serializable and drop the logging message. diff --git a/src/Build/BackEnd/Components/RequestBuilder/TaskInvocationCache.cs b/src/Build/BackEnd/Components/RequestBuilder/TaskInvocationCache.cs new file mode 100644 index 00000000000..6919dfb55fe --- /dev/null +++ b/src/Build/BackEnd/Components/RequestBuilder/TaskInvocationCache.cs @@ -0,0 +1,617 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Collections; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd; + +/// +/// One bound task invocation, independent of target incremental analysis and output destinations +/// in the project. Only raw requested task outputs and explicitly declared file artifacts are cached. +/// +internal sealed class TaskInvocationCache +{ + private readonly TaskCacheStore _store; + private readonly byte[] _invocation; + private readonly List _inputs; + private readonly List _outputs; + private readonly Dictionary _requestedOutputs; + private readonly Dictionary _values = new(MSBuildNameIgnoreCaseComparer.Default); + private string _key = String.Empty; + private TaskCacheWarning[] _warnings = []; + private readonly CancellationToken _cancellationToken; + + private TaskInvocationCache(string directory, byte[] invocation, List inputs, List outputs, + Dictionary requestedOutputs, TaskCacheBackend backend, CancellationToken cancellationToken) + { + _store = new TaskCacheStore(directory, backend); + _cancellationToken = cancellationToken; + _invocation = invocation; + _inputs = inputs; + _outputs = outputs; + _requestedOutputs = requestedOutputs; + } + + internal bool IsHit { get; private set; } + + internal static bool HasDeclaredIO(Type taskType) + { + foreach (CustomAttributeData attribute in taskType.GetCustomAttributesData()) + { + if (attribute.AttributeType.FullName == "Microsoft.Build.Framework.MSBuildDeclaredIOTaskAttribute") + { + return true; + } + } + + return false; + } + + internal static TaskInvocationCache? TryCreate( + ProjectTaskInstance task, + ITask instance, + TaskFactoryWrapper factory, + Dictionary boundInputs, + TaskEnvironment environment, + ProjectInstance project, + string directory, + out string? rejection, + TaskCacheBackend backend, + CancellationToken cancellationToken) + { + rejection = null; + HashSet inputNames = new(MSBuildNameIgnoreCaseComparer.Default); + HashSet outputNames = new(MSBuildNameIgnoreCaseComparer.Default); + bool marked = false; + // Full-name matching accepts task-local attribute polyfills without constructing attributes. + foreach (CustomAttributeData attribute in factory.TaskFactoryLoadedType.Type.GetCustomAttributesData()) + { + string? name = attribute.AttributeType.FullName; + if (name == "Microsoft.Build.Framework.MSBuildDeclaredIOTaskAttribute") + { + if (attribute.ConstructorArguments.Count != 0 || attribute.NamedArguments.Count != 0) + { + return Reject("TaskCache.DeclaredIOInvalid", out rejection); + } + + marked = true; + } + else if (name is "Microsoft.Build.Framework.MSBuildDeclaredIOInputAttribute" or "Microsoft.Build.Framework.MSBuildDeclaredIOOutputAttribute") + { + if (attribute.ConstructorArguments.Count != 1 + || attribute.ConstructorArguments[0].Value is not string parameterName + || String.IsNullOrWhiteSpace(parameterName) + || attribute.NamedArguments.Count != 0) + { + return Reject("TaskCache.DeclaredIOInvalid", out rejection); + } + + (name == "Microsoft.Build.Framework.MSBuildDeclaredIOInputAttribute" ? inputNames : outputNames).Add(parameterName); + } + } + + if (!marked) + { + return Reject("TaskCache.DeclaredIOMarkerRequired", out rejection); + } + + // Authors may expose this optional switch when only a restricted execution mode + // satisfies their declarations. Tasks without the switch rely on the marker promise. + if (factory.GetProperty(MSBuildConstants.MSBuildTaskCacheEnabled)?.PropertyType == typeof(bool) + && (!boundInputs.TryGetValue(MSBuildConstants.MSBuildTaskCacheEnabled, out byte[]? mode) + || DeserializeParameter(mode) is not true)) + { + return Reject("TaskCache.DeclaredIOOptOut", out rejection); + } + + if (inputNames.Overlaps(outputNames)) + { + return Reject("TaskCache.ReadWriteOverlap", out rejection); + } + + Dictionary requestedOutputs = new(MSBuildNameIgnoreCaseComparer.Default); + foreach (ProjectTaskInstanceChild output in task.Outputs) + { + string name = output switch + { + ProjectTaskOutputItemInstance item => item.TaskParameter, + ProjectTaskOutputPropertyInstance outputProperty => outputProperty.TaskParameter, + _ => String.Empty + }; + // Conditions and destinations are evaluated only by normal output gathering. Requiring + // complete name coverage lets a future invocation use different destinations/conditions. + if (String.IsNullOrWhiteSpace(name) || name.IndexOfAny(['$', '@', '%', '(', ')', ';']) >= 0) + { + return Reject("TaskCache.DeclaredIOStructure", out rejection); + } + + TaskPropertyInfo? property = factory.GetProperty(name); + if (property is null || !factory.GetNamesOfPropertiesWithOutputAttribute.ContainsKey(name) + || !IsSupportedParameterType(property.PropertyType)) + { + return Reject("TaskCache.DeclaredIOStructure", out rejection); + } + + requestedOutputs[property.Name] = property; + } + + List inputs = []; + List outputs = []; + Dictionary effectiveFiles = new(MSBuildNameIgnoreCaseComparer.Default); + HashSet[] declarations = [inputNames, outputNames]; + foreach (HashSet names in declarations) + { + foreach (string name in names) + { + TaskPropertyInfo? property = factory.GetProperty(name); + if (property is null || !task.Parameters.ContainsKey(name)) + { + rejection = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("TaskCache.DeclaredIOUnbound", name); + return null; + } + + Type type = property.PropertyType.IsArray ? property.PropertyType.GetElementType()! : property.PropertyType; + if (type != typeof(string) && type != typeof(ITaskItem)) + { + return Reject("TaskCache.DeclaredIOInvalid", out rejection); + } + + // These are the file contract, not speculative task outputs. Inspect the effective + // paths after all setters have run, including defaults left by explicit empty XML + // values (which do not call setters in MSBuild). + object? value = factory.GetPropertyValue(instance, property); + effectiveFiles[property.Name] = SerializeInputParameter(value); + try + { + AddPaths(value, names == inputNames ? inputs : outputs, environment); + } + catch (Exception e) when (e is InvalidDataException or ArgumentException or NotSupportedException) + { + return Reject("TaskCache.DeclaredIOInvalid", out rejection); + } + } + } + + foreach (string output in outputs) + { + if (ContainsPath(inputs, output)) + { + return Reject("TaskCache.ReadWriteOverlap", out rejection); + } + } + + string assembly = factory.TaskFactoryLoadedType.Path; + if (String.IsNullOrEmpty(assembly) || !File.Exists(assembly)) + { + return Reject("TaskCache.DeclaredIOInvalid", out rejection); + } + + AddPath(assembly, inputs, environment); + foreach (string output in outputs) + { + if (ContainsPath(inputs, output)) + { + return Reject("TaskCache.ReadWriteOverlap", out rejection); + } + } + + inputs.Sort(StringComparer.Ordinal); + outputs.Sort(StringComparer.Ordinal); + using MemoryStream stream = new(); + using (BinaryWriter writer = new(stream, Encoding.UTF8, leaveOpen: true)) + { + writer.Write(typeof(TaskInvocationCache).Module.ModuleVersionId.ToString()); + writer.Write(factory.TaskFactoryLoadedType.Type.Module.ModuleVersionId.ToString()); + writer.Write(factory.TaskFactoryLoadedType.Type.FullName!); + writer.Write(project.FullPath); + writer.Write(project.ToolsVersion); + writer.Write(project.Toolset.ToolsPath); + writer.Write(environment.ProjectDirectory.Value); + writer.Write(Environment.Version.ToString()); + writer.Write(Environment.OSVersion.ToString()); + writer.Write(Environment.Is64BitProcess); + writer.Write(CultureInfo.CurrentCulture.Name); + writer.Write(CultureInfo.CurrentUICulture.Name); + WriteParameters(writer, boundInputs); + WriteParameters(writer, effectiveFiles); + List names = new(requestedOutputs.Keys); + names.Sort(StringComparer.Ordinal); + writer.Write(names.Count); + foreach (string name in names) + { + writer.Write(name); + } + + IReadOnlyDictionary variables = environment.GetEnvironmentVariables(); + names = new(variables.Keys); + names.Sort(StringComparer.Ordinal); + writer.Write(names.Count); + foreach (string name in names) + { + writer.Write(name); + writer.Write(variables[name]); + } + + writer.Write(outputs.Count); + foreach (string output in outputs) + { + writer.Write(output); + } + } + + return new TaskInvocationCache(directory, stream.ToArray(), inputs, outputs, requestedOutputs, backend, cancellationToken); + } + + internal bool TryRestore() + { + // Contract getters can log diagnostics. The host checks those before + // entering lookup, so defer input file access until that check succeeds. + _key = ComputeKey(); + IsHit = _store.TryRestore(_key, _outputs, out _, ValidateState, _cancellationToken); + return IsHit; + } + + internal object? GetOutput(string name) => DeserializeParameter(_values[name]); + internal bool HasOutput(string name) => _values.ContainsKey(name); + internal IEnumerable RequestedOutputs => _requestedOutputs.Values; + internal IReadOnlyList Warnings => _warnings; + + internal void CaptureOutput(string name, object? value) + { + byte[] serialized = SerializeParameter(value); + if (_values.TryGetValue(name, out byte[]? previous) + && !previous.AsSpan().SequenceEqual(serialized)) + { + throw new InvalidDataException("The task output getter returned different values for repeated bindings."); + } + + _values[name] = serialized; + } + + internal bool Publish(Func canPublish, IReadOnlyList warnings) + { + if (_values.Count != _requestedOutputs.Count) + { + throw new InvalidDataException("Task cache output capture is incomplete."); + } + + if (!String.Equals(_key, ComputeKey(), StringComparison.Ordinal)) + { + return false; + } + + using MemoryStream stream = new(); + using (BinaryWriter writer = new(stream, Encoding.UTF8, leaveOpen: true)) + { + WriteParameters(writer, _values); + TaskCacheWarning.Write(writer, warnings); + } + + return _store.Store(_key, _outputs, stream.ToArray(), + () => canPublish() && String.Equals(_key, ComputeKey(), StringComparison.Ordinal), _cancellationToken); + } + + private void ValidateState(byte[] state) + { + using MemoryStream stream = new(state, writable: false); + using BinaryReader reader = new(stream, Encoding.UTF8); + if (reader.ReadInt32() != _requestedOutputs.Count) + { + throw new InvalidDataException("Task cache output coverage does not match the invocation."); + } + + _values.Clear(); + for (int i = 0; i < _requestedOutputs.Count; i++) + { + string name = reader.ReadString(); + int length = reader.ReadInt32(); + if (!_requestedOutputs.TryGetValue(name, out TaskPropertyInfo? property) + || _values.ContainsKey(name) || length < 0 || length > stream.Length - stream.Position) + { + throw new InvalidDataException("Invalid task cache output."); + } + + byte[] value = reader.ReadBytes(length); + object? decoded = DeserializeParameter(value); + if (!IsCompatibleOutput(property.PropertyType, decoded)) + { + throw new InvalidDataException("Task cache output type does not match the invocation."); + } + + _values.Add(name, value); + } + + _warnings = TaskCacheWarning.Read(reader); + if (stream.Position != stream.Length) + { + throw new InvalidDataException("Unexpected data in task cache outputs."); + } + } + + private string ComputeKey() + { + using MemoryStream stream = new(); + using (BinaryWriter writer = new(stream, Encoding.UTF8, leaveOpen: true)) + { + writer.Write(_invocation.Length); + writer.Write(_invocation); + writer.Write(_inputs.Count); + foreach (string input in _inputs) + { + _cancellationToken.ThrowIfCancellationRequested(); + TaskCacheStore.CheckPath(input, allowDirectory: false, allowReadOnlyFile: true); + writer.Write(input); + bool exists = File.Exists(input); + writer.Write(exists); + if (exists) + { + using FileStream file = new(input, FileMode.Open, FileAccess.Read, FileShare.Read); + using SHA256 hash = SHA256.Create(); + writer.Write(hash.ComputeHash(file)); + } + } + } + + using SHA256 keyHash = SHA256.Create(); + return BitConverter.ToString(keyHash.ComputeHash(stream.ToArray())).Replace("-", String.Empty); + } + + internal static byte[] SerializeInputParameter(object? value) + { + value = NormalizeTaskItemArray(value); + byte[] serialized = SerializeParameter(value); + if (value is not ITaskItem && value is not ITaskItem[]) + { + return serialized; + } + + using MemoryStream stream = new(); + using (BinaryWriter writer = new(stream, Encoding.UTF8, leaveOpen: true)) + { + writer.Write(serialized.Length); + writer.Write(serialized); + if (value is ITaskItem item) + { + WriteTaskVisibleMetadata(writer, item); + } + else + { + foreach (ITaskItem? element in (ITaskItem[])value) + { + WriteTaskVisibleMetadata(writer, element); + } + } + } + return stream.ToArray(); + } + + private static void WriteTaskVisibleMetadata(BinaryWriter writer, ITaskItem? item) + { + writer.Write(item is not null); + if (item is null) + { + return; + } + + // Transport metadata alone does not preserve whether item-definition expressions + // expand on access. Keep both representations so neither distinction is lost. + IDictionary metadata = item.CloneCustomMetadata(); + List names = new(metadata.Count); + foreach (object key in metadata.Keys) + { + if (key is not string name) + { + throw new InvalidDataException("Invalid task item metadata name."); + } + names.Add(name); + } + names.Sort(StringComparer.Ordinal); + writer.Write(names.Count); + foreach (string name in names) + { + writer.Write(name); + writer.Write(item.GetMetadata(name)); + } + } + + internal static byte[] SerializeParameter(object? value) + { + if (value is not null && !IsSupportedParameterType(value.GetType())) + { + throw new InvalidDataException("The task parameter cannot be losslessly cached."); + } + + value = NormalizeTaskItemArray(value); + using MemoryStream stream = new(); + using (ITranslator translator = BinaryTranslator.GetWriteTranslator(stream)) + { + TaskParameter parameter = new(value); + parameter.CanonicalizeMetadata(); + parameter.Translate(translator); + } + + return stream.ToArray(); + } + + private static object? NormalizeTaskItemArray(object? value) + { + if (value is ITaskItem[] || value is not Array array || array.Rank != 1 + || !typeof(ITaskItem).IsAssignableFrom(array.GetType().GetElementType())) + { + return value; + } + + // Struct-backed item arrays are not covariant to ITaskItem[]. Box their + // elements so the existing translator preserves item data rather than strings. + ITaskItem[] items = new ITaskItem[array.Length]; + int lowerBound = array.GetLowerBound(0); + for (int i = 0; i < items.Length; i++) + { + items[i] = (ITaskItem)array.GetValue(lowerBound + i)!; + } + return items; + } + + private static object? DeserializeParameter(byte[] bytes) + { + using MemoryStream stream = new(bytes, 0, bytes.Length, writable: false, publiclyVisible: true); + using (BinaryReader reader = new(stream, Encoding.UTF8, leaveOpen: true)) + { + TaskParameterType kind = (TaskParameterType)reader.ReadInt32(); + if (kind is not (TaskParameterType.Null or TaskParameterType.PrimitiveType or TaskParameterType.PrimitiveTypeArray + or TaskParameterType.ITaskItem or TaskParameterType.ITaskItemArray)) + { + throw new InvalidDataException("Invalid task cache parameter type."); + } + + if (kind is TaskParameterType.PrimitiveType or TaskParameterType.PrimitiveTypeArray) + { + TypeCode typeCode = (TypeCode)reader.ReadInt32(); + if (typeCode is < TypeCode.Boolean or > TypeCode.String || (int)typeCode == 17) + { + throw new InvalidDataException("Invalid task cache primitive type."); + } + } + } + + stream.Position = 0; + using ITranslator translator = BinaryTranslator.GetReadTranslator(stream, InterningBinaryReader.PoolingBuffer); + TaskParameter parameter = TaskParameter.FactoryForDeserialization(translator); + if (parameter.ParameterType is TaskParameterType.Invalid or TaskParameterType.ValueType or TaskParameterType.ValueTypeArray + || stream.Position != stream.Length) + { + throw new InvalidDataException("Invalid task cache parameter."); + } + + return parameter.WrappedParameter; + } + + private static bool IsSupportedParameterType(Type type) + { + if (type.IsArray) + { + if (type.GetArrayRank() != 1) + { + return false; + } + + type = type.GetElementType()!; + // The native task translator represents these arrays with general-format strings, + // which do not preserve every bit/tick on all supported runtimes. + if (type == typeof(double) || type == typeof(DateTime)) + { + return false; + } + } + + return typeof(ITaskItem).IsAssignableFrom(type) + || (!type.IsEnum && Type.GetTypeCode(type) is not (TypeCode.Object or TypeCode.DBNull or TypeCode.Empty or TypeCode.Single)); + } + + private static bool IsCompatibleOutput(Type type, object? value) + { + if (value is null) + { + return true; + } + + return type.IsInstanceOfType(value) + || (type.IsArray && typeof(ITaskItem).IsAssignableFrom(type.GetElementType()) && value is ITaskItem[]) + || (typeof(ITaskItem).IsAssignableFrom(type) && value is ITaskItem); + } + + private static void AddPaths(object? value, List paths, TaskEnvironment environment) + { + if (value is Array array) + { + foreach (object? element in array) + { + AddPaths(element, paths, environment); + } + } + else if (value is string path) + { + AddPath(path, paths, environment); + } + else if (value is ITaskItem item) + { + AddPath(item.ItemSpec, paths, environment); + } + else if (value is not null) + { + throw new InvalidDataException("Invalid declared file parameter."); + } + } + + private static void AddPath(string path, List paths, TaskEnvironment environment) + { + if (path.Length == 0) + { + return; + } + + if (path.IndexOfAny(['*', '?']) >= 0) + { + throw new InvalidDataException("Declared task files must not contain wildcards."); + } + + string normalized = NormalizePath(path, environment); + if (!ContainsPath(paths, normalized)) + { + paths.Add(normalized); + } + } + + internal static string NormalizePath(string path, TaskEnvironment environment) => + Path.GetFullPath(environment.GetAbsolutePath(path).Value); + + private static void WriteParameters(BinaryWriter writer, Dictionary values) + { + List names = new(values.Keys); + names.Sort(StringComparer.Ordinal); + writer.Write(names.Count); + foreach (string name in names) + { + writer.Write(name); + writer.Write(values[name].Length); + writer.Write(values[name]); + } + } + + private static bool ContainsPath(List paths, string path) + { + foreach (string candidate in paths) + { + if (FileUtilities.PathComparer.Equals(candidate, path)) + { + return true; + } + } + + return false; + } + + private static TaskInvocationCache? Reject(string resource, out string? rejection) + { + rejection = ResourceUtilities.GetResourceString(resource); + return null; + } + + internal static bool IsCacheException(Exception exception) => + exception is InvalidDataException or ArgumentException or NotSupportedException or FormatException or OverflowException + or TargetInvocationException or InvalidOperationException + || ExceptionHandling.IsIoRelatedException(exception); +} diff --git a/src/Build/BackEnd/Node/OutOfProcNode.cs b/src/Build/BackEnd/Node/OutOfProcNode.cs index 31996716697..44f18c92880 100644 --- a/src/Build/BackEnd/Node/OutOfProcNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcNode.cs @@ -135,6 +135,7 @@ public class OutOfProcNode : INode, IBuildComponentHost, INodePacketFactory, INo /// The current instance. /// private readonly ISdkResolverService _sdkResolverService; + private TaskCacheClient _taskCacheClient; /// /// Constructor. @@ -187,6 +188,7 @@ public OutOfProcNode() (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this); (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.ResourceResponse, ResourceResponse.FactoryForDeserialization, this); (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.ResolveSdkResponse, SdkResult.FactoryForDeserialization, _sdkResolverService as INodePacketHandler); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskCacheResponse, TaskCachePacket.ReadResponse, this); } /// @@ -377,6 +379,12 @@ void INodePacketFactory.RoutePacket(int nodeId, INodePacket packet) /// The packet. void INodePacketHandler.PacketReceived(int node, INodePacket packet) { + if (packet.Type == NodePacketType.TaskCacheResponse) + { + _taskCacheClient?.PacketReceived(node, packet); + return; + } + _receivedPackets.Enqueue(packet); _packetReceivedEvent.Set(); } @@ -457,6 +465,8 @@ private void OnEngineException(Exception e) /// private NodeEngineShutdownReason HandleShutdown(out Exception exception) { + _taskCacheClient?.Dispose(); + _taskCacheClient = null; CommunicationsUtilities.Trace($"Shutting down with reason: {_shutdownReason}, and exception: {_shutdownException}."); MSBuildEventSource.Log.OutOfProcNodeShutDownStart(); @@ -582,6 +592,7 @@ private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) { case LinkStatus.ConnectionFailed: case LinkStatus.Failed: + _taskCacheClient?.Dispose(); _shutdownReason = NodeEngineShutdownReason.ConnectionFailed; _shutdownEvent.Set(); break; @@ -714,6 +725,11 @@ private void HandleNodeConfiguration(NodeConfiguration configuration) { // Grab the system parameters. _buildParameters = configuration.BuildParameters; + if (_buildParameters.TaskCache) + { + _taskCacheClient = new TaskCacheClient(_buildParameters.BuildCacheDirectory, SendPacket); + _buildParameters.TaskCacheBackend = _taskCacheClient; + } s_projectRootElementCacheBase.SetParserIgnoreConfiguration(configuration.BuildParameters.ParserIgnoreConfiguration); diff --git a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs index 2186467ad20..675ca24b37b 100644 --- a/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs +++ b/src/Build/BackEnd/Shared/BuildRequestConfiguration.cs @@ -223,8 +223,9 @@ internal static IReadOnlyCollection GetIsolationAllowedTopLevelTargets( /// /// The data containing the configuration information. /// The default ToolsVersion to use as a fallback - internal BuildRequestConfiguration(BuildRequestData data, string defaultToolsVersion) - : this(0, data, defaultToolsVersion) + /// Whether file-based evaluations should receive the task-cache global property. + internal BuildRequestConfiguration(BuildRequestData data, string defaultToolsVersion, bool? taskCacheEnabled = null) + : this(0, data, defaultToolsVersion, taskCacheEnabled) { } @@ -236,7 +237,8 @@ internal BuildRequestConfiguration(BuildRequestData data, string defaultToolsVer /// The configuration ID to assign to this new configuration. /// The data containing the configuration information. /// The default ToolsVersion to use as a fallback - internal BuildRequestConfiguration(int configId, BuildRequestData data, string defaultToolsVersion) + /// Whether file-based evaluations should receive the task-cache global property. + internal BuildRequestConfiguration(int configId, BuildRequestData data, string defaultToolsVersion, bool? taskCacheEnabled = null) { ArgumentNullException.ThrowIfNull(data); Assumed.NotNullOrEmpty(data.ProjectFullPath); @@ -246,6 +248,11 @@ internal BuildRequestConfiguration(int configId, BuildRequestData data, string d _explicitToolsVersionSpecified = data.ExplicitToolsVersionSpecified; _toolsVersion = ResolveToolsVersion(data, defaultToolsVersion); _globalProperties = data.GlobalPropertiesDictionary; + if (taskCacheEnabled == true && data.ProjectInstance is null) + { + _globalProperties = new PropertyDictionary(_globalProperties); + _globalProperties.Set(ProjectPropertyInstance.Create(MSBuildConstants.MSBuildTaskCacheEnabled, taskCacheEnabled.Value ? "true" : "false")); + } _requestedTargets = new List(data.TargetNames); // The following information only exists when the request is populated with an existing project. diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.TaskCache.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.TaskCache.cs new file mode 100644 index 00000000000..e154fe6b794 --- /dev/null +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.TaskCache.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd; + +internal partial class TaskExecutionHost +{ + private Dictionary? _taskCacheInputs; + private TaskInvocationCache? _taskCache; + private CancellationToken _taskCacheCancellationToken; + + private void CaptureTaskCacheInput(TaskPropertyInfo parameter, object value) + { + if (_taskCacheInputs is null) + { + return; + } + + try + { + _taskCacheInputs[parameter.Name] = TaskInvocationCache.SerializeInputParameter(value); + } + catch (Exception e) when (TaskInvocationCache.IsCacheException(e)) + { + if (e is not System.IO.InvalidDataException && ExceptionHandling.IsIoRelatedException(e)) + { + ProjectErrorUtilities.ThrowInvalidProject(_taskLocation, "TaskCache.OperationFailed", _taskName, e.Message); + } + + WarnTaskCacheUnavailable(e); + } + } + + internal bool TryRestoreTaskCache(ProjectTaskInstance task, TaskCacheDiagnosticCapture? diagnostics) + { + if (_taskCacheInputs is null || _cancelled) + { + return false; + } + + if (diagnostics is null || diagnostics.HasBlockingDiagnostics || _taskLoggingContext.HasLoggedErrors) + { + _taskLoggingContext.LogComment(MessageImportance.Low, "TaskCache.Diagnostics", _taskName); + return false; + } + + try + { + string? rejection = null; + if (_registeredTaskFactory is not null + || _taskFactoryWrapper.TaskFactory is not AssemblyTaskFactory + || TaskInstance.GetType() != _taskFactoryWrapper.TaskFactoryLoadedType.Type + || TaskInstance.HostObject is not null) + { + rejection = ResourceUtilities.GetResourceString("TaskCache.DeclaredIOStructure"); + } + else + { + _taskCache = TaskInvocationCache.TryCreate( + task, TaskInstance, _taskFactoryWrapper, _taskCacheInputs, + TaskEnvironment ?? Microsoft.Build.Framework.TaskEnvironment.Fallback, ProjectInstance, + _buildComponentHost.BuildParameters.BuildCacheDirectory, out rejection, + _buildComponentHost.BuildParameters.TaskCacheBackend, _taskCacheCancellationToken); + } + + if (_taskCache is null) + { + _taskLoggingContext.LogWarning(null, new BuildEventFileInfo(_taskLocation), + "TaskCache.Rejected", _taskName, rejection); + return false; + } + + // Input getters and metadata access may themselves log diagnostics. + if (diagnostics.HasBlockingDiagnostics || _taskLoggingContext.HasLoggedErrors) + { + _taskCache = null; + return false; + } + + bool hit = _taskCache.TryRestore(); + _taskLoggingContext.LogComment(hit ? MessageImportance.Normal : MessageImportance.Low, + hit ? "TaskCache.Hit" : "TaskCache.Miss", _taskName); + if (!hit) + { + diagnostics.BeginReplayablePhase(); + } + return hit; + } + catch (TaskCacheRestoreException e) + { + // A partial restoration cannot safely fall back, even with ContinueOnError. + ProjectErrorUtilities.ThrowInvalidProject(_taskLocation, "TaskCache.RestoreFailed", _taskName, e.Message); + return false; + } + catch (Exception e) when (TaskInvocationCache.IsCacheException(e)) + { + ProjectErrorUtilities.ThrowInvalidProject(_taskLocation, "TaskCache.OperationFailed", _taskName, e.Message); + return false; + } + } + + internal void ReplayTaskCacheWarnings() + { + if (_taskCache?.IsHit == true) + { + foreach (TaskCacheWarning warning in _taskCache.Warnings) + { + _taskLoggingContext.LoggingService.LogBuildEvent(warning.ToEvent(_taskLoggingContext.BuildEventContext)); + } + } + } + + private object? GetTaskOutputValue(TaskPropertyInfo parameter) + { + if (_taskCache?.HasOutput(parameter.Name) == true) + { + // Return a fresh raw value for each binding. Binding an item output can mutate it. + return _taskCache.GetOutput(parameter.Name); + } + + object? value = _taskFactoryWrapper.GetPropertyValue(TaskInstance, parameter); + if (_taskCache is not null) + { + _taskCache.CaptureOutput(parameter.Name, value); + return _taskCache.GetOutput(parameter.Name); + } + + return value; + } + + private void CaptureTaskCacheOutputs(TaskCacheDiagnosticCapture? diagnostics) + { + if (_taskCache is null || _taskCache.IsHit || _cancelled || diagnostics is null) + { + return; + } + + foreach (TaskPropertyInfo parameter in _taskCache.RequestedOutputs) + { + if (diagnostics.HasBlockingDiagnostics || _taskLoggingContext.HasLoggedErrors) + { + return; + } + if (_taskCache.HasOutput(parameter.Name)) + { + continue; + } + + try + { + GetTaskOutputValue(parameter); + } + catch (TargetInvocationException e) + { + ProjectErrorUtilities.ThrowInvalidProject(_taskLocation, "FailedToRetrieveTaskOutputs", + _taskName, parameter.Name, e.InnerException?.Message ?? e.Message); + } + catch (Exception e) when (!ExceptionHandling.NotExpectedReflectionException(e) || TaskInvocationCache.IsCacheException(e)) + { + ProjectErrorUtilities.ThrowInvalidProject(_taskLocation, "FailedToRetrieveTaskOutputs", + _taskName, parameter.Name, e.Message); + } + } + } + + internal void PublishTaskCache(TaskCacheDiagnosticCapture? diagnostics) + { + if (_taskCache is null || _taskCache.IsHit || _cancelled) + { + return; + } + + if (diagnostics is null || diagnostics.HasBlockingDiagnostics || _taskLoggingContext.HasLoggedErrors) + { + _taskLoggingContext.LogComment(MessageImportance.Low, "TaskCache.Diagnostics", _taskName); + return; + } + + try + { + if (!_taskCache.Publish(() => !_cancelled && !diagnostics.HasBlockingDiagnostics && !_taskLoggingContext.HasLoggedErrors, diagnostics.GetWarnings()) + && !_cancelled && !diagnostics.HasBlockingDiagnostics && !_taskLoggingContext.HasLoggedErrors) + { + _taskLoggingContext.LogWarning(null, new BuildEventFileInfo(_taskLocation), + "TaskCache.InputsChanged", _taskName); + } + } + catch (Exception e) when (TaskInvocationCache.IsCacheException(e)) + { + ProjectErrorUtilities.ThrowInvalidProject(_taskLocation, "TaskCache.OperationFailed", _taskName, e.Message); + } + } + + private void WarnTaskCacheUnavailable(Exception exception) + { + _taskCache = null; + _taskCacheInputs = null; + _taskLoggingContext.LogWarning(null, new BuildEventFileInfo(_taskLocation), + "TaskCache.Unavailable", _taskName, exception.Message); + } +} diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 46da48fa86f..884ad6c19b0 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -63,7 +63,7 @@ internal enum TaskRequirements /// reflection, and executing the task in the appropriate context.The TaskExecutionHost does not deal with any part of the task declaration or /// XML. /// - internal class TaskExecutionHost : IDisposable + internal partial class TaskExecutionHost : IDisposable { /// /// Time interval in miliseconds to wait between receiving a cancelation signal and emitting the first warning that a non-cancelable task has not finished @@ -296,6 +296,7 @@ public void InitializeForTask( _projectFile = projectFile; _taskLocation = taskLocation; _cancellationTokenRegistration = cancellationToken.Register(Cancel); + _taskCacheCancellationToken = cancellationToken; _taskHost = taskHost; _taskExecutionIdle.Set(); #if FEATURE_APPDOMAIN @@ -569,6 +570,12 @@ public bool SetTaskParameters(IDictionary par ArgumentNullException.ThrowIfNull(parameters); + _taskCache = null; + _taskCacheInputs = _buildComponentHost?.BuildParameters?.TaskCache == true + && TaskInvocationCache.HasDeclaredIO(_taskFactoryWrapper.TaskFactoryLoadedType.Type) + ? new Dictionary(MSBuildNameIgnoreCaseComparer.Default) + : null; + bool taskInitialized = true; // Get the properties that exist on this task. We need to gather all of the ones that are marked @@ -741,17 +748,35 @@ public bool GatherTaskOutputs(string parameterName, ElementLocation parameterLoc /// /// Cleans up after running a batch. /// - public void CleanupForBatch() + public void CleanupForBatch(bool publishCache = false, TaskCacheDiagnosticCapture diagnostics = null) { try { - if (_taskFactoryWrapper != null && TaskInstance != null) + try + { + if (publishCache) + { + CaptureTaskCacheOutputs(diagnostics); + } + } + finally + { + diagnostics?.EndReplayablePhase(); + if (_taskFactoryWrapper != null && TaskInstance != null) + { + _taskFactoryWrapper.TaskFactory.CleanupTask(TaskInstance); + } + } + + if (publishCache) { - _taskFactoryWrapper.TaskFactory.CleanupTask(TaskInstance); + PublishTaskCache(diagnostics); } } finally { + _taskCache = null; + _taskCacheInputs = null; TaskInstance = null; } } @@ -1118,7 +1143,7 @@ private void RecordItemForDisconnectIfNecessary(TaskItem item) /// private ITaskItem[] GetItemOutputs(TaskPropertyInfo parameter) { - object outputs = _taskFactoryWrapper.GetPropertyValue(TaskInstance, parameter); + object outputs = GetTaskOutputValue(parameter); if (outputs is ITaskItem[] taskItemOutputs) { @@ -1151,7 +1176,7 @@ private ITaskItem[] GetItemOutputs(TaskPropertyInfo parameter) /// private string[] GetValueOutputs(TaskPropertyInfo parameter) { - object outputs = _taskFactoryWrapper.GetPropertyValue(TaskInstance, parameter); + object outputs = GetTaskOutputValue(parameter); Array convertibleOutputs = parameter.PropertyType.IsArray ? (Array)outputs : new[] { outputs }; @@ -1742,6 +1767,7 @@ private bool InternalSetTaskParameter( try { + CaptureTaskCacheInput(parameter, parameterValue); _taskFactoryWrapper.SetPropertyValue(TaskInstance, parameter, parameterValue); success = true; } diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 1aa8eef4824..6a744793d8d 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -44,6 +44,12 @@ + + + + + + @@ -182,6 +188,17 @@ + + + + + + + + + + + diff --git a/src/Build/README.md b/src/Build/README.md index a5cf66a7d77..d87071857ad 100644 --- a/src/Build/README.md +++ b/src/Build/README.md @@ -8,6 +8,11 @@ This package contains `Microsoft.Build.dll`, which defines MSBuild's API, includ Developers should reference this package to write applications that create, edit, evaluate, or build MSBuild projects. +Task caching is opt-in. API hosts enabling `BuildParameters.TaskCache` can add +`Microsoft.Build.TaskCache` for the optional managed and native cache runtime. +Ordinary use of `Microsoft.Build` +does not require the cache package or a cache-specific NuGet feed. + To create or edit an MSBuild project, use the [Microsoft.Build.Construction.ProjectRootElement](https://docs.microsoft.com/dotnet/api/microsoft.build.construction.projectrootelement) class and call the [Create](https://docs.microsoft.com/dotnet/api/microsoft.build.construction.projectrootelement.create) or [Open](https://docs.microsoft.com/dotnet/api/microsoft.build.construction.projectrootelement.open) method. diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 9e46d3a987b..50d41aa5850 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -2649,4 +2649,79 @@ Utilization: {0} Average Utilization: {1:###.0} Unrecognized solution version "{0}", attempting to continue. + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index f4a3231df2a..213591665d0 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -1110,6 +1110,101 @@ Chyby: {3} Sestavení úlohy bylo načteno z{0}, ale požadované umístění bylo{1}. + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. Úloha „{0}“ běžela v procesu TaskHost {1}, který spustil volající proces {2}. Pro toto vyvolání byl spuštěn nový TaskHost: {3}. TaskHost je opakovaně použitelný sidecar: {4}. Opakované použití uzlu: {5}. diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index 0005b6abae0..87d8637943c 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -1110,6 +1110,101 @@ Fehler: {3} Die Aufgabenassembly wurde aus „{0}“ geladen, während der gewünschte Speicherort „{1}“ war. + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. Die Aufgabe „{0}“ wurde im TaskHost-Prozess {1} ausgeführt und wurde vom Aufruferprozess {2} erzeugt. Für diesen Aufruf wurde ein neuer TaskHost erzeugt: {3}. TaskHost ist ein wiederverwendbarer Sidecar: {4}. Knotenwiederverwendung: {5}. diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index 4800cf64ae0..d9a263d7dd8 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -1110,6 +1110,101 @@ Errores: {3} El ensamblado de tarea se cargó desde "{0}" mientras que la ubicación deseada era "{1}". + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. La tarea "{0}" se ejecutó en el proceso TaskHost {1}, generado por el proceso del llamador {2}. Nuevo TaskHost generado para esta invocación: {3}. TaskHost es un sidecar reutilizable: {4}. Reutilización de nodos: {5}. diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 58c9a5b0bc1..c0061f4b814 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -1110,6 +1110,101 @@ Erreurs : {3} L’assembly de tâche a été chargé à partir de « {0} » alors que l’emplacement souhaité était « {1} ». + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. La tâche « {0} » s’est exécutée dans le processus TaskHost {1}, créé par le processus appelant {2}. Nouveau TaskHost créé pour cet appel : {3}. TaskHost est un sidecar réutilisable : {4}. Réutilisation du nœud : {5}. diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index d2664235672..46cd21f9cca 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -1110,6 +1110,101 @@ Errori: {3} L'assembly attività è stato caricato da "{0}" mentre era "{1}" il percorso desiderato. + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. Attività "{0}" eseguita nel processo TaskHost {1}, avviato dal processo chiamante {2}. Nuovo TaskHost avviato per questa esecuzione: {3}. TaskHost è un sidecar riutilizzabile: {4}. Riutilizzo del nodo: {5}. diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index 633a3b116f8..7635825f8ae 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -1110,6 +1110,101 @@ Errors: {3} タスク アセンブリは '{0}' から読み込まれましたが、必要な場所は '{1}' でした。 + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. タスク "{0}" は、呼び出し元プロセス {2} によって生成された TaskHost プロセス {1} で実行されました。この呼び出し用に新しい TaskHost が生成されました: {3}。TaskHost は再利用可能なサイドカーです: {4}。ノードの再利用: {5}。 diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index ad36bdf7450..e686f254249 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -1110,6 +1110,101 @@ Errors: {3} 원하는 위치가 '{1}'인 동안 '{0}'에서 작업 어셈블리를 로드했습니다. + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. 작업 "{0}"은(는) 호출한 MSBuild 프로세스 {2}(이)가 생성한 TaskHost 프로세스 {1}에서 실행되었습니다. 이 호출에 대해 새 TaskHost가 생성되었습니다: {3}. TaskHost는 재사용 가능한 사이드카입니다: {4}. 노드 재사용: {5}. diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index 661eda991e1..f05d44beb00 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -1110,6 +1110,101 @@ Błędy: {3} Zestaw zadania został załadowany z lokalizacji „{0}”, gdy żądana lokalizacja to „{1}”. + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. Zadanie „{0}” zostało uruchomione w procesie TaskHost {1}, utworzonym przez proces wywołujący {2}. Nowy pasek boczny TaskHost został utworzony dla tego wywołania: {3}. TaskHost to pasek boczny wielokrotnego użytku: {4}. Ponowne użycie węzła: {5}. diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index 32d04b128b9..bcfebb7abc3 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -1110,6 +1110,101 @@ Erros: {3} O assembly da tarefa foi carregado de "{0}" enquanto o local desejado era "{1}". + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. A tarefa "{0}" foi executada no processo TaskHost {1}, gerado pelo processo chamador {2}. Novo TaskHost gerado para esta invocação: {3}. TaskHost é um sidecar reutilizável: {4}. Reutilização do nó: {5}. diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 95aa8580715..66c5200b0a0 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -1110,6 +1110,101 @@ Errors: {3} Сборка задачи была загружена из "{0}", а нужное расположение — "{1}". + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. Задача "{0}" выполнялась в процессе TaskHost {1}, запущенном вызывающим процессом {2}. Для этого вызова создан новый TaskHost: {3}. TaskHost — это повторно используемый вспомогательный контейнер: {4}. Повторное использование узла: {5}. diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index 2a53f095663..141d3b4c885 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -1110,6 +1110,101 @@ Hatalar: {3} İstenilen konum '{1}' iken görev derlemesi '{0}'dan yüklendi. + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. "{0}" görevi {1} TaskHost işleminde çalıştı, {2} çağıran işlemi tarafından üretildi. Bu çağrı için yeni bir TaskHost üretildi: {3}. TaskHost yeniden kullanılabilir bir sepettir: {4}. Düğüm yeniden kullanımı: {5}. diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index c5acaad8ffe..d643ddd193e 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -1110,6 +1110,101 @@ Errors: {3} 已从“{0}”加载任务程序集,但所需位置为“{1}”。 + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. 任务“{0}”在调用方进程 {2} 生成的 TaskHost 进程 {1} 中运行。为此调用生成了新的 TaskHost: {3}。TaskHost 是可重用的挎斗: {4}。节点重用: {5}。 diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index bbeab23d4d1..222eda78dbb 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -1110,6 +1110,101 @@ Errors: {3} 工作組件已從 '{0}' 載入,但 '{1}' 才是所需的位置。 + + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + The task's file I/O annotations are invalid or name unsupported parameter types. Use unconditional class-level declarations naming string or ITaskItem parameters, or arrays of those types. + Do not translate string or ITaskItem. + + + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + The task must have a class-level MSBuildDeclaredIOTask annotation. This annotation is the task author's promise that the file I/O declarations are complete and that the task has no undeclared side effects. + Do not translate MSBuildDeclaredIOTask. + + + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + This task exposes the optional Boolean MSBuildTaskCacheEnabled parameter but did not explicitly receive true. To enable caching for this task, pass MSBuildTaskCacheEnabled=true and satisfy the task's validation requirements. + Do not translate MSBuildTaskCacheEnabled or true. + + + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + The task requires a standard assembly task factory, no host object or separate task-host process, supported primitive or item parameters, and literal requested output parameter names. + + + + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + Declared file parameter "{0}" is missing or is not explicitly supplied to the task. Supply the path before task execution, or an empty value for an unused optional file. + {0} is a task parameter name. + + + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + Task "{0}" was not cached because it logged an error, emitted a non-replayable diagnostic, or synchronous diagnostic capture was unavailable. + {0} is the task name. + + + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + Task-cache hit for "{0}": restored file artifacts and raw task outputs. + {0} is the task name. + + + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + TaskCache cannot be combined with Question, InputResultsCacheFiles, or OutputResultsCacheFile. Disable TaskCache or remove the conflicting build parameter. + Do not translate property names. + + + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + MSB4291: Task "{0}" was not cached because its inputs changed during execution. Ensure that tasks do not modify each other's inputs while running. + {StrBegin="MSB4291: "}{0} is the task name. + + + No matching task-cache entry for "{0}"; executing the task. + No matching task-cache entry for "{0}"; executing the task. + {0} is the task name. + + + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + The user cache directory could not be determined. Set BuildCacheDirectory or supply -buildCacheDirectory with an absolute directory path. + Do not translate BuildCacheDirectory or -buildCacheDirectory. + + + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + MSB4292: The cache operation for task "{0}" failed: {1} Check the cache directory, available disk space, and file permissions. Fix the problem, then clean and rebuild. + {StrBegin="MSB4292: "}{0} is the task name; {1} is an exception message. + + + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + Cannot acquire build cache ownership at "{0}". Another build may own this directory. Use a different -buildCacheDirectory or BuildParameters.BuildCacheDirectory, or retry after that build ends. + {0} is the cache directory. Do not translate -buildCacheDirectory or BuildParameters.BuildCacheDirectory. + + + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + The task reads and writes the same file. Declare disjoint normalized input and output file paths. + + + + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + MSB4287: Task "{0}" will execute without caching: {1} Use a supported task invocation or disable -taskCache to suppress this warning. + {StrBegin="MSB4287: "}{0} is the task name; {1} is a localized rejection reason. Do not translate -taskCache. + + + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + MSB4289: Restoring cached task "{0}" failed: {1} Outputs may be partially replaced. Fix the filesystem problem, then clean and rebuild. + {StrBegin="MSB4289: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + + + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + The optional task cache runtime could not be loaded. Reference Microsoft.Build.TaskCache or use an MSBuild distribution that includes the cache runtime. + Do not translate Microsoft.Build.TaskCache or MSBuild. + + + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Disable TaskCache or use a distribution with a supported task-cache backend. + Do not translate TaskCache or MSBuild. + + + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + MSB4288: Inputs for task "{0}" could not be captured for caching: {1} Execution will continue without caching. Use supported task parameter values or disable -taskCache. + {StrBegin="MSB4288: "}{0} is the task name; {1} is an exception message. Do not translate -taskCache. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. 工作 "{0}" 已在 TaskHost 處理序 {1} 中執行,由呼叫者處理序 {2} 繁衍。為此引動過程繁衍的新 TaskHost: {3}。TaskHost 是可重複使用的邊車: {4}。節點重複使用: {5}。 diff --git a/src/Framework.UnitTests/MSBuildDeclaredIOAttribute_Tests.cs b/src/Framework.UnitTests/MSBuildDeclaredIOAttribute_Tests.cs new file mode 100644 index 00000000000..ad7e1dff0a1 --- /dev/null +++ b/src/Framework.UnitTests/MSBuildDeclaredIOAttribute_Tests.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Reflection; +using Microsoft.Build.Framework; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests; + +public sealed class MSBuildDeclaredIOAttribute_Tests +{ + [Theory] + [InlineData(typeof(MSBuildDeclaredIOTaskAttribute), false)] + [InlineData(typeof(MSBuildDeclaredIOInputAttribute), true)] + [InlineData(typeof(MSBuildDeclaredIOOutputAttribute), true)] + public void AttributesApplyOnlyToClassesAndAreNotInherited(Type attributeType, bool allowMultiple) + { + AttributeUsageAttribute usage = attributeType.GetCustomAttribute()!; + usage.ValidOn.ShouldBe(AttributeTargets.Class); + usage.Inherited.ShouldBeFalse(); + usage.AllowMultiple.ShouldBe(allowMultiple); + attributeType.IsSealed.ShouldBeTrue(); + } + + [Fact] + public void DeclarationsCanNameMultipleInheritedParameters() + { + typeof(DeclaredTask).GetCustomAttribute().ShouldNotBeNull(); + typeof(DeclaredTask).GetCustomAttributes() + .Select(attribute => attribute.ParameterName).ShouldBe([nameof(ParameterBase.Sources), nameof(ParameterBase.ConfigurationFile)], ignoreOrder: true); + typeof(DeclaredTask).GetCustomAttributes() + .Select(attribute => attribute.ParameterName).ShouldBe([nameof(ParameterBase.DestinationFiles), nameof(ParameterBase.LogFile)], ignoreOrder: true); + typeof(DeclaredTask).GetProperty(nameof(ParameterBase.Sources))!.DeclaringType.ShouldBe(typeof(ParameterBase)); + typeof(DeclaredTask).GetCustomAttributes() + .Select(attribute => attribute.ParameterName) + .Intersect(typeof(DeclaredTask).GetCustomAttributes() + .Select(attribute => attribute.ParameterName), StringComparer.OrdinalIgnoreCase) + .ShouldBeEmpty(); + } + + [Theory] + [InlineData(typeof(MSBuildDeclaredIOInputAttribute))] + [InlineData(typeof(MSBuildDeclaredIOOutputAttribute))] + public void DeclarationsHaveNoConditionalSettings(Type attributeType) + { + attributeType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Select(property => property.Name).ShouldBe(["ParameterName"]); + } + + [Fact] + public void DerivedClassesMustRedeclareTheWholeContract() + { + typeof(UndeclaredDerivedTask).GetCustomAttribute(inherit: true).ShouldBeNull(); + typeof(UndeclaredDerivedTask).GetCustomAttributes(inherit: true).ShouldBeEmpty(); + typeof(UndeclaredDerivedTask).GetCustomAttributes(inherit: true).ShouldBeEmpty(); + typeof(RedeclaredDerivedTask).GetCustomAttribute(inherit: true).ShouldNotBeNull(); + typeof(RedeclaredDerivedTask).GetCustomAttributes(inherit: true) + .Select(attribute => attribute.ParameterName).ShouldBe([nameof(ParameterBase.Sources)]); + typeof(RedeclaredDerivedTask).GetCustomAttributes(inherit: true) + .Select(attribute => attribute.ParameterName).ShouldBe([nameof(ParameterBase.LogFile)]); + } + + [Fact] + public void MarkerCanDeclareNoFileIO() + { + typeof(NoFileTask).GetCustomAttribute().ShouldNotBeNull(); + typeof(NoFileTask).GetCustomAttributes().ShouldBeEmpty(); + typeof(NoFileTask).GetCustomAttributes().ShouldBeEmpty(); + } + + [Fact] + public void DeclarationsCanBeInspectedWithoutConstructingTheTaskOrReadingParameters() + { + CustomAttributeData declaration = typeof(UninstantiableTask).GetCustomAttributesData() + .Single(attribute => attribute.AttributeType == typeof(MSBuildDeclaredIOInputAttribute)); + declaration.ConstructorArguments.Single().Value.ShouldBe(nameof(UninstantiableTask.Source)); + } + + [Fact] + public void FileOutputsDoNotImplyTaskOutputBindings() + { + typeof(ParameterBase).GetProperty(nameof(ParameterBase.DestinationFiles))!.GetCustomAttribute().ShouldBeNull(); + typeof(ParameterBase).GetProperty(nameof(ParameterBase.Result))!.GetCustomAttribute().ShouldNotBeNull(); + typeof(DeclaredTask).GetCustomAttributes() + .Select(attribute => attribute.ParameterName).ShouldNotContain(nameof(ParameterBase.Result)); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t\r\n")] + public void EmptyParameterNamesAreRejected(string name) + { + Should.Throw(() => new MSBuildDeclaredIOInputAttribute(name)).ParamName.ShouldBe("parameterName"); + Should.Throw(() => new MSBuildDeclaredIOOutputAttribute(name)).ParamName.ShouldBe("parameterName"); + } + + [Fact] + public void NullParameterNamesAreRejected() + { + Should.Throw(() => new MSBuildDeclaredIOInputAttribute(null!)).ParamName.ShouldBe("parameterName"); + Should.Throw(() => new MSBuildDeclaredIOOutputAttribute(null!)).ParamName.ShouldBe("parameterName"); + } + + private class ParameterBase + { + public ITaskItem[] Sources { get; set; } = []; + public string ConfigurationFile { get; set; } = String.Empty; + public string[] DestinationFiles { get; set; } = []; + public string LogFile { get; set; } = String.Empty; + [Output] + public bool Result { get; set; } + } + + [MSBuildDeclaredIOTask] + [MSBuildDeclaredIOInput(nameof(Sources))] + [MSBuildDeclaredIOInput(nameof(ConfigurationFile))] + [MSBuildDeclaredIOOutput(nameof(DestinationFiles))] + [MSBuildDeclaredIOOutput(nameof(LogFile))] + private class DeclaredTask : ParameterBase + { + } + + private sealed class UndeclaredDerivedTask : DeclaredTask + { + } + + [MSBuildDeclaredIOTask] + [MSBuildDeclaredIOInput(nameof(Sources))] + [MSBuildDeclaredIOOutput(nameof(LogFile))] + private sealed class RedeclaredDerivedTask : DeclaredTask + { + } + + [MSBuildDeclaredIOTask] + private sealed class NoFileTask + { + } + + [MSBuildDeclaredIOTask] + [MSBuildDeclaredIOInput(nameof(Source))] + private sealed class UninstantiableTask + { + public UninstantiableTask() => throw new InvalidOperationException(); + public string Source => throw new InvalidOperationException(); + } +} diff --git a/src/Framework/BackEnd/Handshake.cs b/src/Framework/BackEnd/Handshake.cs index 22b5c167d80..56e926eed07 100644 --- a/src/Framework/BackEnd/Handshake.cs +++ b/src/Framework/BackEnd/Handshake.cs @@ -98,7 +98,10 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string? to // connection and the parent starts a fresh node instead. Task hosts are excluded, see GetChangeWaveSalt. string changeWaveSalt = GetChangeWaveSalt(); - int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}{changeWaveSalt}"); + // BuildParameters is sent only to same-version workers, not to task hosts. + // Version its layout here: task-host packet negotiation does not apply to workers. + string workerProtocolSalt = IsTaskHost ? string.Empty : "|BuildParameters:4|TaskCacheRpc:1"; + int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}{changeWaveSalt}{workerProtocolSalt}"); // The .NET task host parent (.NET Framework MSBuild, e.g. Visual Studio) and child // (.NET SDK MSBuild) compute this salt independently from different sources, and on Windows diff --git a/src/Framework/BackEnd/NodePacketType.cs b/src/Framework/BackEnd/NodePacketType.cs index 9301a250a5b..cc6883de6b8 100644 --- a/src/Framework/BackEnd/NodePacketType.cs +++ b/src/Framework/BackEnd/NodePacketType.cs @@ -226,7 +226,16 @@ internal enum NodePacketType : byte /// RarNodeBufferedLogEvents, // 0x16 - // Packet types 0x17-0x1F reserved for future core functionality + /// Build-scoped task cache storage request. + TaskCacheRequest = 0x18, + + /// Correlated task cache storage response. + TaskCacheResponse = 0x19, + + /// Cancellation of a task cache storage request. + TaskCacheCancel = 0x1A, + + // Packet types 0x1B-0x1F reserved for future core functionality #region TaskHost callback packets (0x20-0x27) // These support bidirectional callbacks from TaskHost to parent for IBuildEngine implementations diff --git a/src/Framework/MSBuildConstants.cs b/src/Framework/MSBuildConstants.cs index 4bf8720d2de..1bda4de3dcd 100644 --- a/src/Framework/MSBuildConstants.cs +++ b/src/Framework/MSBuildConstants.cs @@ -19,6 +19,11 @@ internal static class MSBuildConstants /// internal const string ToolsPath = "MSBuildToolsPath"; + /// + /// Global property and explicit task parameter identifying task-cache execution. + /// + internal const string MSBuildTaskCacheEnabled = nameof(MSBuildTaskCacheEnabled); + /// /// Name of the property that indicates the X64 tools path /// diff --git a/src/Framework/MSBuildDeclaredIOInputAttribute.cs b/src/Framework/MSBuildDeclaredIOInputAttribute.cs new file mode 100644 index 00000000000..a875d8e0f1e --- /dev/null +++ b/src/Framework/MSBuildDeclaredIOInputAttribute.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.Framework; + +/// +/// Identifies a task parameter whose values name logical input files. +/// +/// +/// Apply this attribute to the task class, not to the parameter property. The named +/// public instance property may be declared on a base class. Prefer nameof +/// when specifying its name. +/// A string or represents one path; an array represents one +/// path per element. For items, the path is , not +/// metadata. Relative paths use +/// at invocation entry, normally the directory containing the project. The task +/// must interpret paths consistently with that execution environment or use +/// absolute paths; the process-wide current directory need not match in +/// multithreaded execution. +/// Input access includes inspecting file contents, attributes, or existence. +/// This declaration is unconditional. The parameter must not also have an +/// , and its file paths must not +/// overlap declared output paths. +/// This attribute neither asserts determinism nor makes a parameter required. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public sealed class MSBuildDeclaredIOInputAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the task parameter containing input file paths. + /// is null. + /// is empty or whitespace. + public MSBuildDeclaredIOInputAttribute(string parameterName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(parameterName); + ParameterName = parameterName; + } + + /// + /// Gets the name of the task parameter containing input file paths. + /// + public string ParameterName { get; } +} diff --git a/src/Framework/MSBuildDeclaredIOOutputAttribute.cs b/src/Framework/MSBuildDeclaredIOOutputAttribute.cs new file mode 100644 index 00000000000..97c4d834d4a --- /dev/null +++ b/src/Framework/MSBuildDeclaredIOOutputAttribute.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.Framework; + +/// +/// Identifies a task parameter whose values name logical output files. +/// +/// +/// Apply this attribute to the task class, not to the parameter property. The named +/// public instance property may be declared on a base class. Prefer nameof +/// when specifying its name. +/// A string or represents one path; an array represents one +/// path per element. For items, the path is , not +/// metadata. Relative paths use +/// at invocation entry, normally the directory containing the project. The task +/// must interpret paths consistently with that execution environment or use +/// absolute paths; the process-wide current directory need not match in +/// multithreaded execution. +/// Output access includes creating, modifying, or deleting a file and does not +/// guarantee that the file exists after execution. +/// This attribute is independent of , which exposes +/// values through a task's Output elements. It neither asserts determinism nor +/// makes output paths available before the task supplies their values. +/// This declaration is unconditional. The parameter must not also have an +/// , and its file paths must not +/// overlap declared input paths. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public sealed class MSBuildDeclaredIOOutputAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the task parameter containing output file paths. + /// is null. + /// is empty or whitespace. + public MSBuildDeclaredIOOutputAttribute(string parameterName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(parameterName); + ParameterName = parameterName; + } + + /// + /// Gets the name of the task parameter containing output file paths. + /// + public string ParameterName { get; } +} diff --git a/src/Framework/MSBuildDeclaredIOTaskAttribute.cs b/src/Framework/MSBuildDeclaredIOTaskAttribute.cs new file mode 100644 index 00000000000..30732dc1478 --- /dev/null +++ b/src/Framework/MSBuildDeclaredIOTaskAttribute.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Build.Framework; + +/// +/// Marks a concrete task class whose logical file inputs and outputs are completely +/// described by and +/// . +/// +/// +/// Declarations are unconditional and must conservatively cover all supported parameter +/// modes. The declared input and output files must be disjoint, including when different +/// parameters name the same file. Tasks whose modes require overlapping inputs and +/// outputs cannot provide this contract. +/// A marked task promises that its successful results are determined by its bound +/// parameters, declared file inputs, and execution context, and that restoring its +/// declared files and referenced output values can replace execution. This promise +/// does not imply thread safety. Tasks with hidden observable effects cannot provide +/// this contract. +/// Private temporary files with no externally observable effects are not task data +/// outputs. A marked task with no input or output declarations asserts that it has +/// no logical file inputs or outputs. +/// Each concrete task class must declare this attribute and its complete set of input +/// and output attributes explicitly; none of these attributes are inherited. +/// After successful execution, output getters referenced by the project must be safe +/// to read even when their output binding conditions are false. Task caching captures +/// each referenced getter once before task cleanup and reuses its raw value for bindings. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class MSBuildDeclaredIOTaskAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + public MSBuildDeclaredIOTaskAttribute() + { + } +} diff --git a/src/Framework/PublicAPI.Unshipped.txt b/src/Framework/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..a68f92fd6b8 --- /dev/null +++ b/src/Framework/PublicAPI.Unshipped.txt @@ -0,0 +1,9 @@ +#nullable enable +Microsoft.Build.Framework.MSBuildDeclaredIOInputAttribute +Microsoft.Build.Framework.MSBuildDeclaredIOInputAttribute.MSBuildDeclaredIOInputAttribute(string! parameterName) -> void +Microsoft.Build.Framework.MSBuildDeclaredIOInputAttribute.ParameterName.get -> string! +Microsoft.Build.Framework.MSBuildDeclaredIOOutputAttribute +Microsoft.Build.Framework.MSBuildDeclaredIOOutputAttribute.MSBuildDeclaredIOOutputAttribute(string! parameterName) -> void +Microsoft.Build.Framework.MSBuildDeclaredIOOutputAttribute.ParameterName.get -> string! +Microsoft.Build.Framework.MSBuildDeclaredIOTaskAttribute +Microsoft.Build.Framework.MSBuildDeclaredIOTaskAttribute.MSBuildDeclaredIOTaskAttribute() -> void diff --git a/src/MSBuild.Benchmarks/MSBuild.Benchmarks.csproj b/src/MSBuild.Benchmarks/MSBuild.Benchmarks.csproj index 418f68cf817..785d9ef3671 100644 --- a/src/MSBuild.Benchmarks/MSBuild.Benchmarks.csproj +++ b/src/MSBuild.Benchmarks/MSBuild.Benchmarks.csproj @@ -14,6 +14,9 @@ + + diff --git a/src/MSBuild.Bootstrap.Utils/MSBuild.Bootstrap.Utils.csproj b/src/MSBuild.Bootstrap.Utils/MSBuild.Bootstrap.Utils.csproj index 7539d0dcedb..ee2e5f887ee 100644 --- a/src/MSBuild.Bootstrap.Utils/MSBuild.Bootstrap.Utils.csproj +++ b/src/MSBuild.Bootstrap.Utils/MSBuild.Bootstrap.Utils.csproj @@ -5,6 +5,9 @@ + + + \ No newline at end of file diff --git a/src/MSBuild.Bootstrap.Utils/Tasks/AddBootstrapTaskCacheDependencies.cs b/src/MSBuild.Bootstrap.Utils/Tasks/AddBootstrapTaskCacheDependencies.cs new file mode 100644 index 00000000000..3493167534c --- /dev/null +++ b/src/MSBuild.Bootstrap.Utils/Tasks/AddBootstrapTaskCacheDependencies.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MSBuild.Bootstrap.Utils.Tasks; + +/// +/// Adds the task-cache dependency closure to the acquired SDK without replacing +/// the SDK's existing NuGet and framework dependency versions. +/// +public sealed class AddBootstrapTaskCacheDependencies : Task +{ + private static readonly string[] s_assetKinds = ["runtime", "native", "resources", "runtimeTargets"]; + + /// Gets or sets the freshly built CLI dependency manifest. + [Required] + public string SourceDepsFile { get; set; } = string.Empty; + + /// Gets or sets the acquired SDK's application dependency manifests. + [Required] + public string[] TargetDepsFiles { get; set; } = []; + + /// + public override bool Execute() + { + JObject source = JObject.Parse(File.ReadAllText(SourceDepsFile)); + JObject sourceTarget = (JObject)source["targets"]![(string)source["runtimeTarget"]!["name"]!]!; + Dictionary sourceEntries = new(StringComparer.OrdinalIgnoreCase); + foreach (JProperty entry in sourceTarget.Properties()) + { + sourceEntries.Add(PackageName(entry.Name), entry); + } + + foreach (string targetFile in TargetDepsFiles) + { + Merge(source, sourceEntries, targetFile); + } + + return true; + } + + private void Merge(JObject source, Dictionary sourceEntries, string targetFile) + { + // Keep the acquired SDK baseline so changing package versions on subsequent + // builds replaces our additions instead of treating them as SDK-owned. + string baseline = targetFile + ".without-task-cache"; + if (!File.Exists(baseline)) + { + File.Copy(targetFile, baseline); + } + + JObject target = JObject.Parse(File.ReadAllText(baseline)); + JObject targetEntries = (JObject)target["targets"]![(string)target["runtimeTarget"]!["name"]!]!; + Dictionary versions = new(StringComparer.OrdinalIgnoreCase); + foreach (JProperty entry in targetEntries.Properties()) + { + versions.Add(PackageName(entry.Name), PackageVersion(entry.Name)); + } + + string[] roots = ["Microsoft.Build.TaskCache", "Microsoft.BuildXL.Cache.MemoizationStore.Library", "RocksDbNative", "protobuf-net.Grpc"]; + Queue pending = new(roots); + List additions = []; + while (pending.Count > 0) + { + string name = pending.Dequeue(); + if (versions.ContainsKey(name)) + { + continue; + } + + JProperty entry = sourceEntries[name]; + additions.Add(entry); + versions.Add(name, PackageVersion(entry.Name)); + if (entry.Value["dependencies"] is JObject dependencies) + { + foreach (JProperty dependency in dependencies.Properties()) + { + pending.Enqueue(dependency.Name); + } + } + } + + foreach (JProperty entry in additions) + { + JObject value = (JObject)entry.Value.DeepClone(); + if (value["dependencies"] is JObject dependencies) + { + foreach (JProperty dependency in dependencies.Properties()) + { + dependency.Value = versions[dependency.Name]; + } + } + + targetEntries.Add(entry.Name, value); + target["libraries"]![entry.Name] = source["libraries"]![entry.Name]!.DeepClone(); + CopyAssets(value, Path.GetDirectoryName(targetFile)!); + } + + foreach (JProperty entry in targetEntries.Properties()) + { + if (PackageName(entry.Name).Equals("Microsoft.Build", StringComparison.OrdinalIgnoreCase)) + { + JObject dependencies = (JObject)(entry.Value["dependencies"] ??= new JObject()); + foreach (string name in roots) + { + dependencies[name] = versions[name]; + } + } + } + + string staging = targetFile + "." + Guid.NewGuid().ToString("N"); + try + { + File.WriteAllText(staging, target.ToString(Formatting.Indented)); + File.Replace(staging, targetFile, null); + } + finally + { + File.Delete(staging); + } + } + + private void CopyAssets(JObject entry, string destination) + { + string sourceDirectory = Path.GetDirectoryName(SourceDepsFile)!; + foreach (string kind in s_assetKinds) + { + if (entry[kind] is not JObject assets) + { + continue; + } + + foreach (JProperty asset in assets.Properties()) + { + string relative = kind == "runtimeTargets" ? asset.Name : Path.GetFileName(asset.Name); + if (kind == "resources") + { + relative = Path.Combine((string)asset.Value["locale"]!, relative); + } + + relative = relative.Replace('/', Path.DirectorySeparatorChar); + string path = Path.Combine(destination, relative); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.Copy(Path.Combine(sourceDirectory, relative), path, overwrite: true); + } + } + } + + private static string PackageName(string identity) => identity.Substring(0, identity.LastIndexOf('/')); + + private static string PackageVersion(string identity) => identity.Substring(identity.LastIndexOf('/') + 1); +} diff --git a/src/MSBuild.Bootstrap.Utils/Tasks/GetTaskCachePackageContents.cs b/src/MSBuild.Bootstrap.Utils/Tasks/GetTaskCachePackageContents.cs new file mode 100644 index 00000000000..c4afb278da6 --- /dev/null +++ b/src/MSBuild.Bootstrap.Utils/Tasks/GetTaskCachePackageContents.cs @@ -0,0 +1,286 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Newtonsoft.Json.Linq; + +namespace MSBuild.Bootstrap.Utils.Tasks; + +/// Packages the private cache runtime without exposing its restore feeds. +public sealed class GetTaskCachePackageContents : Task +{ + /// The restored engine assets file. + [Required] + public string AssetsFile { get; set; } = string.Empty; + + /// The package target framework alias. + [Required] + public string TargetFramework { get; set; } = string.Empty; + + /// The framework moniker used by NuGet's central transitive groups. + [Required] + public string FrameworkMoniker { get; set; } = string.Empty; + + /// The SDK runtime graph, used for managed RID asset fallbacks. + public string? RuntimeGraphFile { get; set; } + + /// The optional implementation assembly included in every managed runtime group. + public string? RuntimeAssembly { get; set; } + + /// Private cache package roots. + [Required] + public string[] CacheRoots { get; set; } = []; + + /// Files and their explicit package destinations. + [Output] + public ITaskItem[] PackageFiles { get; private set; } = []; + + /// + public override bool Execute() + { + JObject assets = JObject.Parse(File.ReadAllText(AssetsFile)); + JObject target = (JObject?)assets["targets"]?[TargetFramework] + ?? throw new InvalidDataException("The assets file has no target for " + TargetFramework); + Dictionary entries = new(StringComparer.OrdinalIgnoreCase); + foreach (JProperty entry in target.Properties()) + { + entries.Add(entry.Name.Substring(0, entry.Name.LastIndexOf('/')), entry); + } + + HashSet roots = new(CacheRoots, StringComparer.OrdinalIgnoreCase); + HashSet publicRoots = new(StringComparer.OrdinalIgnoreCase); + JObject dependencies = (JObject)assets["project"]!["frameworks"]![TargetFramework]!["dependencies"]!; + foreach (JProperty dependency in dependencies.Properties()) + { + if (!roots.Contains(dependency.Name) + && !string.Equals((string?)dependency.Value["suppressParent"], "All", StringComparison.OrdinalIgnoreCase)) + { + publicRoots.Add(dependency.Name); + } + } + foreach (var entry in entries) + { + if ((string?)entry.Value.Value["type"] == "project") + { + publicRoots.Add(entry.Key); + } + } + if (assets["centralTransitiveDependencyGroups"]?[FrameworkMoniker] is JObject central) + { + foreach (JProperty dependency in central.Properties()) + { + if (!roots.Contains(dependency.Name)) + { + publicRoots.Add(dependency.Name); + } + } + } + + HashSet bundled = Closure(roots, entries); + bundled.ExceptWith(Closure(publicRoots, entries)); + Dictionary common = new(StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrEmpty(RuntimeAssembly)) + { + common.Add(Path.GetFileName(RuntimeAssembly!), RuntimeAssembly!); + } + Dictionary> overrides = new(StringComparer.OrdinalIgnoreCase); + List output = []; + foreach (string name in bundled) + { + JProperty entry = entries[name]; + if ((string?)entry.Value["type"] != "package") + { + continue; + } + JObject library = (JObject)assets["libraries"]![entry.Name]!; + string packageDirectory = FindPackageDirectory(assets, (string)library["path"]!); + AddAssets(entry.Value["runtime"] as JObject, packageDirectory, common, resources: false); + AddAssets(entry.Value["resource"] as JObject, packageDirectory, common, resources: true); + if (entry.Value["runtimeTargets"] is JObject runtimeTargets) + { + foreach (JProperty asset in runtimeTargets.Properties()) + { + if ((string?)asset.Value["assetType"] == "native") + { + if (((string?)asset.Value["rid"])?.EndsWith("-x64", StringComparison.OrdinalIgnoreCase) == true + && !asset.Name.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase)) + { + output.Add(NativePackageFile(Path.Combine(packageDirectory, asset.Name), asset.Name)); + } + continue; + } + if ((string?)asset.Value["assetType"] != "runtime" || !asset.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + string rid = (string)asset.Value["rid"]!; + if (!overrides.TryGetValue(rid, out Dictionary? files)) + { + files = new(StringComparer.OrdinalIgnoreCase); + overrides.Add(rid, files); + } + AddFile(files, Path.GetFileName(asset.Name), Path.Combine(packageDirectory, asset.Name)); + } + } + foreach (string? file in library["files"]!.Values()) + { + if (file is null) + { + throw new InvalidDataException("Invalid restored package file entry."); + } + string filename = Path.GetFileName(file); + if (name.Equals("RocksDbNative", StringComparison.OrdinalIgnoreCase) + && file.StartsWith("build/native/amd64/", StringComparison.OrdinalIgnoreCase)) + { + string rid = filename switch + { + "rocksdb.dll" => "win-x64", + "librocksdb.so" => "linux-x64", + "librocksdb.dylib" => "osx-x64", + _ => throw new InvalidDataException("Unexpected RocksDB native asset: " + file), + }; + output.Add(NativePackageFile(Path.Combine(packageDirectory, file), "runtimes/" + rid + "/native/" + filename)); + } + if (filename.StartsWith("license", StringComparison.OrdinalIgnoreCase) + || filename.StartsWith("notice", StringComparison.OrdinalIgnoreCase) + || filename.StartsWith("third-party-notices", StringComparison.OrdinalIgnoreCase) + || filename.StartsWith("thirdparty", StringComparison.OrdinalIgnoreCase) + || filename.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase)) + { + output.Add(PackageFile(Path.Combine(packageDirectory, file), "task-cache-notices/" + TargetFramework + "/" + entry.Name + "/" + file)); + } + } + } + + foreach (var file in common) + { + output.Add(PackageFile(file.Value, "lib/" + TargetFramework + "/" + file.Key)); + } + JObject? runtimeGraph = string.IsNullOrEmpty(RuntimeGraphFile) ? null + : (JObject?)JObject.Parse(File.ReadAllText(RuntimeGraphFile!))["runtimes"]; + foreach (string rid in overrides.Keys) + { + // A RID group replaces the package's neutral runtime group. Include all + // neutral assemblies, not only the platform override. + Dictionary files = new(common, StringComparer.OrdinalIgnoreCase); + ApplyOverrides(rid, runtimeGraph, overrides, files, new HashSet(StringComparer.OrdinalIgnoreCase)); + foreach (var file in files) + { + output.Add(PackageFile(file.Value, "runtimes/" + rid + "/lib/" + TargetFramework + "/" + file.Key)); + } + } + PackageFiles = output.ToArray(); + return true; + } + + private static HashSet Closure(IEnumerable roots, Dictionary entries) + { + HashSet result = new(StringComparer.OrdinalIgnoreCase); + Queue pending = new(roots); + while (pending.Count != 0) + { + string name = pending.Dequeue(); + if (!result.Add(name)) + { + continue; + } + if (!entries.TryGetValue(name, out JProperty? entry)) + { + throw new InvalidDataException("Missing restored dependency " + name); + } + if (entry.Value["dependencies"] is JObject dependencies) + { + foreach (JProperty dependency in dependencies.Properties()) + { + pending.Enqueue(dependency.Name); + } + } + } + return result; + } + + private static void AddAssets(JObject? assets, string directory, Dictionary files, bool resources) + { + if (assets is null) + { + return; + } + foreach (JProperty asset in assets.Properties()) + { + if (asset.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + string destination = resources ? (string)asset.Value["locale"]! + "/" + Path.GetFileName(asset.Name) : Path.GetFileName(asset.Name); + AddFile(files, destination, Path.Combine(directory, asset.Name)); + } + } + } + + private static void AddFile(Dictionary files, string destination, string source) + { + if (files.TryGetValue(destination, out string? previous) && !string.Equals(previous, source, StringComparison.Ordinal)) + { + throw new InvalidDataException("Conflicting cache runtime assets: " + previous + " and " + source); + } + files[destination] = source; + } + + private static void ApplyOverrides(string rid, JObject? graph, Dictionary> overrides, + Dictionary files, HashSet visited) + { + if (!visited.Add(rid)) + { + return; + } + if (graph?[rid]?["#import"] is JArray imports) + { + for (int i = imports.Count - 1; i >= 0; i--) + { + ApplyOverrides((string)imports[i]!, graph, overrides, files, visited); + } + } + if (overrides.TryGetValue(rid, out Dictionary? replacements)) + { + foreach (var file in replacements) + { + files[file.Key] = file.Value; + } + } + } + + private static string FindPackageDirectory(JObject assets, string relative) + { + foreach (JProperty folder in ((JObject)assets["packageFolders"]!).Properties()) + { + string path = Path.Combine(folder.Name, relative); + if (Directory.Exists(path)) + { + return path; + } + } + throw new DirectoryNotFoundException("Restored package directory was not found: " + relative); + } + + private static ITaskItem PackageFile(string source, string destination) + { + if (!File.Exists(source)) + { + throw new FileNotFoundException("Cache runtime package file was not found.", source); + } + TaskItem item = new(source); + item.SetMetadata("PackagePath", destination); + return item; + } + + private static ITaskItem NativePackageFile(string source, string destination) + { + ITaskItem item = PackageFile(source, destination); + item.SetMetadata("TaskCacheNativeAsset", "true"); + return item; + } +} diff --git a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj index b6967115f22..384f98f1c91 100644 --- a/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj +++ b/src/MSBuild.Bootstrap/MSBuild.Bootstrap.csproj @@ -15,7 +15,7 @@ - + @@ -30,7 +30,7 @@ - + diff --git a/src/MSBuild.Coordinator.UnitTests/CoordinatorClient_Tests.cs b/src/MSBuild.Coordinator.UnitTests/CoordinatorClient_Tests.cs index 634a985f92a..83c4adc5564 100644 --- a/src/MSBuild.Coordinator.UnitTests/CoordinatorClient_Tests.cs +++ b/src/MSBuild.Coordinator.UnitTests/CoordinatorClient_Tests.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO.Pipes; +using System.Reflection; +using System.Runtime.InteropServices; using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; using Microsoft.Build.Framework.Coordinator; using Microsoft.Build.UnitTests; using Shouldly; @@ -12,6 +15,45 @@ namespace Microsoft.Build.Coordinator.UnitTests; public class CoordinatorClient_Tests(ITestOutputHelper testOutput) : IDisposable { + public static bool CanUseTaskCache => +#if FEATURE_BUILDXL_TASK_CACHE + RuntimeInformation.ProcessArchitecture == Architecture.X64; +#else + false; +#endif + + [ConditionalFact(typeof(CoordinatorClient_Tests), nameof(CanUseTaskCache))] + public async Task FailedCacheInitializationReleasesCoordinatorGrant() + { + using TestEnvironment env = TestEnvironment.Create(testOutput); + using CoordinatorServer server = CreateServer(totalNodeBudget: 1); + Task serverTask = server.RunAsync(_cts.Token); + using CoordinatorClient? first = TryConnectToServer(requestedNodes: 1, processId: Pid1); + first.ShouldNotBeNull(); + using BuildManager manager = new(); + FieldInfo field = typeof(BuildManager).GetField("_coordinatorClient", BindingFlags.Instance | BindingFlags.NonPublic)!; + field.SetValue(manager, first); + try + { + Should.Throw(() => manager.BeginBuild(new BuildParameters + { + TaskCache = true, + BuildCacheDirectory = env.CreateFile("invalid-cache", "not a directory").Path, + Loggers = [new MockLogger(testOutput)], + })); + field.GetValue(manager).ShouldBeNull(); + using CoordinatorClient? next = TryConnectToServer(requestedNodes: 1, processId: Pid2); + next.ShouldNotBeNull(); + next.GrantedNodes.ShouldBe(1); + } + finally + { + first.Dispose(); + _cts.Cancel(); + await serverTask; + } + } + // Use fake PIDs that won't collide with each other or the real process. // The coordinator server only uses PIDs for keying connections and liveness checks. private const int Pid1 = 90001; diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index 41db07eadf5..cd598073ae9 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -56,6 +56,59 @@ public void BogusSwitchIdentificationTests() unquoteParameters.ShouldBeFalse(); } + [Theory] + [InlineData("taskCache")] + [InlineData("TASKCACHE")] + public void TaskCacheSwitchIdentification(string name) + { + CommandLineSwitches.IsParameterizedSwitch(name, out CommandLineSwitches.ParameterizedSwitch result, + out _, out _, out _, out _, out _).ShouldBeTrue(); + result.ShouldBe(CommandLineSwitches.ParameterizedSwitch.TaskCache); + } + + [Theory] + [InlineData(null, true)] + [InlineData("true", true)] + [InlineData("false", false)] + public void TaskCacheBooleanSwitch(string value, bool expected) + { + MSBuildApp.ProcessBooleanSwitch(value is null ? [] : [value], true, "InvalidTaskCacheValue").ShouldBe(expected); + } + + [Fact] + public void TaskCacheBooleanSwitchRejectsInvalidValue() + { + Should.Throw(() => MSBuildApp.ProcessBooleanSwitch(["invalid"], true, "InvalidTaskCacheValue")); + } + + [Theory] + [InlineData("buildCacheDirectory")] + [InlineData("BUILDCACHEDIRECTORY")] + public void BuildCacheDirectorySwitchIdentification(string name) + { + CommandLineSwitches.IsParameterizedSwitch(name, out CommandLineSwitches.ParameterizedSwitch result, + out string duplicateError, out bool multiple, out string missingError, out bool unquote, out bool empty).ShouldBeTrue(); + result.ShouldBe(CommandLineSwitches.ParameterizedSwitch.BuildCacheDirectory); + duplicateError.ShouldBeNull(); + multiple.ShouldBeFalse(); + missingError.ShouldBe("MissingBuildCacheDirectoryError"); + unquote.ShouldBeTrue(); + empty.ShouldBeFalse(); + } + + [Theory] + [InlineData("\"cache directory;with,delimiters\"", "cache directory;with,delimiters")] + [InlineData("relative-cache", "relative-cache")] + [InlineData("\" \t\"", " \t")] + public void BuildCacheDirectoryPreservesOneUnquotedPath(string value, string expected) + { + CommandLineSwitches switches = new(); + switches.SetParameterizedSwitch(CommandLineSwitches.ParameterizedSwitch.BuildCacheDirectory, + "-buildCacheDirectory:" + value, value, multipleParametersAllowed: false, + unquoteParameters: true, emptyParametersAllowed: false).ShouldBeTrue(); + switches[CommandLineSwitches.ParameterizedSwitch.BuildCacheDirectory].ShouldBe([expected]); + } + [Theory] [InlineData("help")] [InlineData("HELP")] @@ -1197,6 +1250,8 @@ public void InvalidToolsVersionErrors() graphBuildOptions: null, lowPriority: false, question: false, + taskCache: false, + buildCacheDirectory: null, isTaskAndTargetItemLoggingRequired: false, isBuildCheckEnabled: false, inputResultsCaches: null, diff --git a/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj b/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj index 2e85ead1777..a4974dbbbe8 100644 --- a/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj +++ b/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj @@ -7,6 +7,8 @@ true + + diff --git a/src/MSBuild.UnitTests/TaskCacheSwitch_Tests.cs b/src/MSBuild.UnitTests/TaskCacheSwitch_Tests.cs new file mode 100644 index 00000000000..0918bd27bd0 --- /dev/null +++ b/src/MSBuild.UnitTests/TaskCacheSwitch_Tests.cs @@ -0,0 +1,225 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Build.CommandLine; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests; + +public sealed class TaskCacheSwitch_Tests(ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + public static bool IsSupported => MSBuildApp.IsTaskCacheSupported; + + [ConditionalTheory(typeof(TaskCacheSwitch_Tests), nameof(IsSupported))] + [InlineData(true)] + [InlineData(false)] + public void EvaluationOnlyQueryReceivesExplicitMode(bool enabled) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = env.CreateFile("evaluation.proj", """ + + + $(MSBuildTaskCacheEnabled) + + + """).Path; + string cache = Path.Combine(env.CreateFolder().Path, "unused-cache"); + TextWriter original = Console.Out; + using StringWriter output = new(); + try + { + Console.SetOut(output); + MSBuildApp.Execute(["msbuild.exe", project, $"-taskCache:{enabled}", + $"-p:MSBuildTaskCacheEnabled={!enabled}", $"-buildCacheDirectory:{cache}", + "-getProperty:ModeAtEvaluation", "-noAutoResponse", "-nr:false"]) + .ShouldBe(MSBuildApp.ExitType.Success); + } + finally + { + Console.SetOut(original); + } + output.ToString().Trim().ShouldBe(enabled ? "true" : "false"); + Directory.Exists(cache).ShouldBeFalse(); + } + + [Fact] + public void ExplicitCacheModeMatchesBackendAvailability() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string cacheDirectory = env.CreateFolder().Path; + string project = env.CreateFile("availability.proj", "").Path; + MSBuildApp.Execute(["msbuild.exe", project, "-taskCache", $"-buildCacheDirectory:{cacheDirectory}", "-noAutoResponse", "-nr:false"]) + .ShouldBe(MSBuildApp.IsTaskCacheSupported ? MSBuildApp.ExitType.Success : MSBuildApp.ExitType.SwitchError); + } + + [Theory] + [InlineData("-question")] + [InlineData("-inputResultsCaches:unused.cache")] + [InlineData("-outputResultsCache:unused.cache")] + public void RejectsConflictingSwitches(string conflict) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = env.CreateFile("cache.proj", "").Path; + MSBuildApp.Execute(["msbuild.exe", project, "-taskCache", conflict, "-noAutoResponse"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + } + + [ConditionalTheory(typeof(TaskCacheSwitch_Tests), nameof(IsSupported))] + [InlineData("-taskCache")] + [InlineData("-taskCache:true")] + public void CliPreservesTimestampSkipping(string option) + { + using TestEnvironment env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + env.CreateFile(folder, "input.txt", "input"); + string project = env.CreateFile(folder, "cache.proj", """ + + + + + + """).Path; + string destination = Path.Combine(folder.Path, "output.txt"); + File.WriteAllText(destination, "unchanged"); + File.SetLastWriteTimeUtc(destination, DateTime.UtcNow.AddDays(1)); + MSBuildApp.Execute(["msbuild.exe", project, option, $"-buildCacheDirectory:{env.CreateFolder().Path}", "-warnAsError", "-noAutoResponse", "-nr:false"]).ShouldBe(MSBuildApp.ExitType.Success); + File.ReadAllText(destination).ShouldBe("unchanged"); + } + + [ConditionalTheory(typeof(TaskCacheSwitch_Tests), nameof(IsSupported))] + [InlineData(true)] + [InlineData(false)] + public void ModeIsAvailableDuringEvaluationAndOverridesUserValue(bool enabled) + { + using TestEnvironment env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + string cacheDirectory = env.CreateFolder().Path; + string project = env.CreateFile(folder, "mode.proj", """ + + $(MSBuildTaskCacheEnabled) + + + + + """).Path; + MSBuildApp.Execute(["msbuild.exe", project, $"-taskCache:{enabled}", $"-buildCacheDirectory:{cacheDirectory}", $"-p:MSBuildTaskCacheEnabled={!enabled}", "-noAutoResponse", "-nr:false"]) + .ShouldBe(MSBuildApp.ExitType.Success); + File.ReadAllText(Path.Combine(folder.Path, "mode.txt")).ShouldBe((enabled ? "true" : "false") + Environment.NewLine); + } + + [ConditionalFact(typeof(TaskCacheSwitch_Tests), nameof(IsSupported))] + public void DirectoryIsResolvedAtCliAndProjectsCannotChangeStorage() + { + using TestEnvironment env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + string forbidden = env.CreateFile("not-a-cache", "untouched").Path; + env.SetEnvironmentVariable("MSBUILD_TASK_CACHE_DIR", forbidden); + env.SetEnvironmentVariable("MSBuildTaskCacheDirectory", forbidden); + env.SetCurrentDirectory(folder.Path); + string project = env.CreateFile(folder, "directory.proj", """ + + $(Forbidden) + + + """).Path; + MSBuildApp.Execute(["msbuild.exe", project, "-taskCache", "-buildCacheDirectory:relative-cache", + $"-p:MSBuildTaskCacheDirectory={forbidden}", $"-p:Forbidden={forbidden}", "-noAutoResponse", "-nr:false"]).ShouldBe(MSBuildApp.ExitType.Success); + Directory.Exists(Path.Combine(folder.Path, "relative-cache")).ShouldBeTrue(); + File.ReadAllText(forbidden).ShouldBe("untouched"); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DisabledModeDoesNotOpenStorageOrInjectProperties(bool explicitFalse) + { + using TestEnvironment env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + string forbidden = env.CreateFile("not-a-cache", "untouched").Path; + string project = env.CreateFile(folder, "disabled.proj", """ + + + + + + """).Path; + string[] args = explicitFalse + ? ["msbuild.exe", project, "-taskCache:false", $"-buildCacheDirectory:{forbidden}", "-noAutoResponse", "-nr:false"] + : ["msbuild.exe", project, $"-buildCacheDirectory:{forbidden}", "-noAutoResponse", "-nr:false"]; + MSBuildApp.Execute(args).ShouldBe(MSBuildApp.ExitType.Success); + File.ReadAllText(Path.Combine(folder.Path, "mode.txt")).ShouldBe((explicitFalse ? "mode:false" : "mode:") + Environment.NewLine); + File.ReadAllText(forbidden).ShouldBe("untouched"); + } + + [Theory] + [InlineData("-buildCacheDirectory")] + [InlineData("-buildCacheDirectory:")] + [InlineData("-buildCacheDirectory:\"\"")] + public void DirectorySwitchRequiresAnArgument(string option) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = env.CreateFile("missing-directory.proj", "").Path; + MSBuildApp.Execute(["msbuild.exe", project, option, "-noAutoResponse", "-nr:false"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + } + + [Fact] + public void WhitespaceDirectoryDoesNotEnableCaching() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string project = env.CreateFile("whitespace-directory.proj", "").Path; + MSBuildApp.Execute(["msbuild.exe", project, "-buildCacheDirectory:\" \t\"", "-noAutoResponse", "-nr:false"]) + .ShouldBe(MSBuildApp.ExitType.Success); + } + + [ConditionalFact(typeof(TaskCacheSwitch_Tests), nameof(IsSupported))] + public void LastDirectorySwitchWinsAndQuotedPathIsNotSplit() + { + using TestEnvironment env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + string first = Path.Combine(folder.Path, "unused"); + string selected = Path.Combine(folder.Path, "cache with spaces;and,delimiters"); + string project = env.CreateFile(folder, "directory.proj", "").Path; + MSBuildApp.Execute(["msbuild.exe", project, "-taskCache", $"-buildCacheDirectory:{first}", + $"-buildCacheDirectory:\"{selected}\"", "-noAutoResponse", "-nr:false"]).ShouldBe(MSBuildApp.ExitType.Success); + Directory.Exists(first).ShouldBeFalse(); + Directory.Exists(selected).ShouldBeTrue(); + } + + [ConditionalFact(typeof(TaskCacheSwitch_Tests), nameof(IsSupported))] + public void DirectorySwitchDoesNotSynthesizeGlobalProperties() + { + using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable("MSBuildTaskCacheDirectory", null); + env.SetEnvironmentVariable("BuildCacheDirectory", null); + string directory = env.CreateFolder().Path; + string project = env.CreateFile("no-directory-properties.proj", """ + + + + + + """).Path; + MSBuildApp.Execute(["msbuild.exe", project, "-taskCache", $"-buildCacheDirectory:{directory}", + "-noAutoResponse", "-nr:false"]).ShouldBe(MSBuildApp.ExitType.Success); + } + + [ConditionalFact(typeof(TaskCacheSwitch_Tests), nameof(IsSupported))] + public void ResponseFileDirectoryUsesExistingCommandLinePrecedence() + { + using TestEnvironment env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + string unused = Path.Combine(folder.Path, "response cache"); + string selected = Path.Combine(folder.Path, "command cache"); + string response = env.CreateFile(folder, "cache.rsp", $"-buildCacheDirectory:\"{unused}\"").Path; + string project = env.CreateFile(folder, "response.proj", "").Path; + MSBuildApp.Execute(["msbuild.exe", project, "-taskCache", "@" + response, + $"-buildCacheDirectory:\"{selected}\"", "-noAutoResponse", "-nr:false"]).ShouldBe(MSBuildApp.ExitType.Success); + Directory.Exists(unused).ShouldBeFalse(); + Directory.Exists(selected).ShouldBeTrue(); + } +} diff --git a/src/MSBuild/CommandLine/CommandLineSwitches.cs b/src/MSBuild/CommandLine/CommandLineSwitches.cs index c04765914b9..a625b4673f0 100644 --- a/src/MSBuild/CommandLine/CommandLineSwitches.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitches.cs @@ -112,6 +112,8 @@ internal enum ParameterizedSwitch #endif LowPriority, Question, + TaskCache, + BuildCacheDirectory, DetailedSummary, GetProperty, GetItem, @@ -290,6 +292,8 @@ internal ParameterizedSwitchInfo( #endif new ParameterizedSwitchInfo( ["lowpriority", "low"], ParameterizedSwitch.LowPriority, null, false, null, true, false, "HelpMessage_39_LowPrioritySwitch"), new ParameterizedSwitchInfo( ["question", "q"], ParameterizedSwitch.Question, null, false, null, true, false, "HelpMessage_41_QuestionSwitch"), + new ParameterizedSwitchInfo( ["taskCache"], ParameterizedSwitch.TaskCache, null, false, null, true, false, "HelpMessage_TaskCache"), + new ParameterizedSwitchInfo( ["buildCacheDirectory"], ParameterizedSwitch.BuildCacheDirectory, null, false, "MissingBuildCacheDirectoryError", true, false, "HelpMessage_BuildCacheDirectory"), new ParameterizedSwitchInfo( ["detailedsummary", "ds"], ParameterizedSwitch.DetailedSummary, null, false, null, true, false, "HelpMessage_26_DetailedSummarySwitch"), new ParameterizedSwitchInfo( ["getProperty"], ParameterizedSwitch.GetProperty, null, true, "MissingGetPropertyError", true, false, "HelpMessage_43_GetPropertySwitch"), new ParameterizedSwitchInfo( ["getItem"], ParameterizedSwitch.GetItem, null, true, "MissingGetItemError", true, false, "HelpMessage_44_GetItemSwitch"), diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 687286d99ae..4bed24235ae 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -136,6 +136,8 @@ + + @@ -155,6 +157,7 @@ + diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index 334a7add011..265ab423923 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1837,8 +1837,44 @@ + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index 8e6d99e7fa7..5c7ea468ff7 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: Hodnota s nízkou prioritou není platná. {0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: Hodnota terminálového protokolovacího nástroje není platná. Mělo by to být auto, true nebo false. {0} @@ -1626,6 +1662,11 @@ Protokoly MSBuild a informace o ladění budou dostupné v „{0}“ + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: Je nutné zadat název funkce pro přepínač featureAvailability. @@ -1938,6 +1979,16 @@ MSBUILD : error MSB1059: Cíle se nepovedlo vypsat. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Výstupní parametr úlohy {0} obsahoval elementy null (celkem {1}), které byly odfiltrovány. diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index 728ca0b4ab8..00693e0ba38 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: Der Wert mit niedriger Priorität ist ungültig. {0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: Der Terminalprotokollierungswert ist ungültig. Es sollte "auto", "true" oder "false" sein. {0} @@ -1618,6 +1654,11 @@ Hinweis: Ausführlichkeit der Dateiprotokollierungen MSBuild-Protokolle und Debuginformationen befinden sich auf "{0}" + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: Es muss ein Featurename für den featureAvailability-Schalter angegeben werden. @@ -1930,6 +1971,16 @@ Hinweis: Ausführlichkeit der Dateiprotokollierungen MSBUILD : error MSB1059: Ziele konnten nicht ausgegeben werden. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Der Taskausgabeparameter „{0}“ enthielt {1} herausgefilterte NULL-Element(e). diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index e19a0249178..ecbf101566a 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -304,6 +304,37 @@ Esta marca es experimental y puede que no funcione según lo previsto. LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: El valor de prioridad baja no es válido. {0}. @@ -334,6 +365,11 @@ Esta marca es experimental y puede que no funcione según lo previsto. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: El valor del registrador de terminales no es válido. Debe ser uno de "auto", "true", o "false".{0} @@ -1625,6 +1661,11 @@ Esta marca es experimental y puede que no funcione según lo previsto. Los registros de MSBuild y la información de depuración estarán en "{0}" + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: debe proporcionar un nombre de característica para el conmutador featureAvailability. @@ -1937,6 +1978,16 @@ Esta marca es experimental y puede que no funcione según lo previsto. MSBUILD : error MSB1059: No se pudieron imprimir los destinos. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. El parámetro de salida de tarea "{0}" contenía {1} elementos NULL que se han filtrado. diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index f4da3bf8c8b..e9f39409dc5 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: la valeur basse priorité n’est pas valide. {0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: La valeur du journal du terminal n’est pas valide. Il doit s’agir de 'auto', 'true' ou 'false'. {0} @@ -1618,6 +1654,11 @@ Remarque : verbosité des enregistreurs d’événements de fichiers Les journaux MSBuild et les informations de débogage seront au "{0}" + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: doit fournir un nom de fonctionnalité pour le commutateur featureAvailability. @@ -1930,6 +1971,16 @@ Remarque : verbosité des enregistreurs d’événements de fichiers MSBUILD : error MSB1059: les cibles n'ont pas pu être imprimées. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Le paramètre de sortie de la tâche « {0} » contenait {1} élément(s) null qui ont été filtrés. diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index c5ab93bea18..195908807c6 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -304,6 +304,37 @@ Questo flag è sperimentale e potrebbe non funzionare come previsto. LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: il valore di priorità bassa non è valido. {0} @@ -334,6 +365,11 @@ Questo flag è sperimentale e potrebbe non funzionare come previsto. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: il valore del logger del terminale non è valido. Deve essere 'auto', 'true' o 'false'. {0} @@ -1628,6 +1664,11 @@ Nota: livello di dettaglio dei logger di file I log e le informazioni di debug di MSBuild sono contenuti in "{0}" + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: È necessario fornire un nome funzionalità per l’opzione featureAvailability. @@ -1940,6 +1981,16 @@ Nota: livello di dettaglio dei logger di file MSBUILD : error MSB1059: non è stato possibile stampare le destinazioni. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Il parametro di output dell'attività "{0}" conteneva {1} elementi null che sono stati rimossi. diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 112c1ea0fd1..51ec0ad05c3 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: 低優先度値が無効です。 {0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: ターミナル ロガーの値が無効です。'auto'、'true'、または 'false' のいずれかである必要があります。 {0} @@ -1618,6 +1654,11 @@ MSBuild のログとデバッグ情報は、"{0}" にあります。 + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: featureAvailability スイッチの機能名を指定する必要があります。 @@ -1930,6 +1971,16 @@ MSBUILD : error MSB1059: ターゲットを出力できませんでした。{0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. タスク出力パラメーター "{0}" には、フィルターで除外された {1} 個の null 要素が含まれていました。 diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index 1ce2de26deb..145f278ccd9 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: 낮은 우선 순위 값이 유효하지 않습니다. {0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: 터미널 로거 값이 잘못되었습니다. 'auto', 'true' 또는 'false' 중 하나여야 합니다. {0} @@ -1618,6 +1654,11 @@ MSBuild 로그 및 디버그 정보는 "{0}"에 있습니다. + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: featureAvailability 스위치에 대한 기능 이름을 제공해야 합니다. @@ -1930,6 +1971,16 @@ MSBUILD : error MSB1059: 대상을 출력할 수 없습니다. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. 작업 출력 매개 변수 "{0}"에 필터링된 {1}개의 null 요소가 포함되어 있습니다. diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 97742a53c93..bbb640a7a09 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -304,6 +304,37 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: wartość niskiego priorytetu jest nieprawidłowa. {0} @@ -334,6 +365,11 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: Wartość rejestratora terminali jest nieprawidłowa. Powinna to być wartość „auto”, „true” lub „false”. {0} @@ -1627,6 +1663,11 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. Dzienniki i informacje debugowania programu MSBuild będą znajdować się w lokalizacji „{0}” + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: należy podać nazwę funkcji dla przełącznika dostępności funkcji. @@ -1939,6 +1980,16 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. MSBUILD : error MSB1059: Nie można wydrukować elementów docelowych. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Parametr wyjściowy zadania „{0}” zawierał elementy o wartości null ({1}), które zostały odfiltrowane. diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index 978e9b40fb3..ebf7bfd978b 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: o valor de baixa prioridade não é válido. {0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: o valor do agente de terminal não é válido. O valor deve ser "auto", "true" ou "false". {0} @@ -1619,6 +1655,11 @@ arquivo de resposta. Os logs e as informações de depuração do MSBuild estarão no "{0}" + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: É necessário fornecer um nome de recurso para a chave featureAvailability. @@ -1931,6 +1972,16 @@ arquivo de resposta. MSBUILD : error MSB1059: não foi possível imprimir destinos. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. O parâmetro de saída "{0}" continha {1} elemento(s) nulo(s) que foram filtrados. diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index 011e672f3b3..68f1b9fe539 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: недопустимое значение низкого приоритета. {0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: недопустимое значение регистратора терминала. Это должно быть одно из значений: "auto", "true" или "false". {0} @@ -1617,6 +1653,11 @@ Журналы MSBuild и отладочные сведения будут доступны по адресу "{0}" + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: необходимо указать имя функции для переключателя FeatureAvailability. @@ -1929,6 +1970,16 @@ MSBUILD : error MSB1059: не удалось вывести целевые объекты. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Выходной параметр задачи "{0}" содержал элементы null ({1}), которые были отфильтрованы. diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index 47e96ed0bae..75dec48c0f9 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: Düşük öncelikli değer geçerli değil. {0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: Terminal günlükçüsü değeri geçerli değil. Değer şunlardan biri olmalıdır: 'otomatik', 'doğru' veya 'yanlış'. {0} @@ -1622,6 +1658,11 @@ MSBuild günlükleri ve hata ayıklama bilgileri "{0}" yolunda olacak + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: featureAvailability anahtarı için özellik adı belirtilmelidir. @@ -1934,6 +1975,16 @@ MSBUILD : error MSB1059: Hedefler yazdırılamadı. {0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Görev çıkış parametresi "{0}" {1} null öğe içeriyordu ve bunlar filtrelendi. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 7d5d5629281..7fe075f30f8 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: 低优先级值无效。{0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: 终端记录器值无效。它应为 "auto"、"true" 或 "false" 之一。{0} @@ -1618,6 +1654,11 @@ MSBuild 日志和调试信息将位于"{0}" + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: 必须为 featureAvailability 开关提供功能名称。 @@ -1930,6 +1971,16 @@ MSBUILD : error MSB1059: 无法打印目标。{0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. 任务输出参数 "{0}" 包含 {1} 个 null 元素,这些元素已被筛选掉。 diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index ff866f9477c..12bf79466cd 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -304,6 +304,37 @@ LOCALIZATION: None of the lines should be longer than a standard width console window, eg 80 chars. + + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + -buildCacheDirectory:<path> + Set the build cache directory. Relative paths are resolved + from the command-line working directory. This switch does + not enable caching; use -taskCache to enable task caching. + Do not translate switch names. + + + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + -taskCache[:True|False] + Cache eligible task invocations by content. Normal target + incrementality is preserved. Use + -buildCacheDirectory:path to change storage. + Cannot be combined with -question or results cache + switches. + Do not translate switch names. + + + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + MSBUILD : error MSB1076: The build cache directory could not be opened: {0} Supply -buildCacheDirectory with an available, writable directory, or retry after its owning build ends. + {StrBegin="MSBUILD : error MSB1076: "}{0} is an exception message. Do not translate -buildCacheDirectory. + MSBUILD : error MSB1064: Low priority value is not valid. {0} MSBUILD : error MSB1064: 低優先順序值無效。{0} @@ -334,6 +365,11 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + MSBUILD : error MSB1074: The taskCache switch requires a Boolean value. Use -taskCache, -taskCache:true, or -taskCache:false. + {StrBegin="MSBUILD : error MSB1074: "}Do not translate switch names. + MSBUILD : error MSB1065: Terminal logger value is not valid. It should be one of 'auto', 'true', or 'false'. {0} MSBUILD : error MSB1065: 終端機記錄器值無效。它應該是 'auto'、'true' 或 'false' 其中之一。{0} @@ -1618,6 +1654,11 @@ MSBuild 記錄和偵錯工具資訊將位於 "{0}" + + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + MSBUILD : error MSB1078: A directory path must be specified for -buildCacheDirectory. Use -buildCacheDirectory:<path>. + {StrBegin="MSBUILD : error MSB1078: "}Do not translate -buildCacheDirectory. + MSBUILD : error MSB1067: Must provide a feature name for the featureAvailability switch. MSBUILD : error MSB1067: 必須提供 featureAvailability 切換的功能名稱。 @@ -1930,6 +1971,16 @@ MSBUILD : error MSB1059: 無法列印目標。{0} {StrBegin="MSBUILD : error MSB1059: "} + + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + MSBUILD : error MSB1075: Task caching cannot be combined with -question, -inputResultsCaches, or -outputResultsCache. Remove the conflicting switch or disable -taskCache. + {StrBegin="MSBUILD : error MSB1075: "}Do not translate switch names. + + + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + MSBUILD : error MSB1077: Task invocation caching has no supported storage backend in this MSBuild distribution or process architecture. Remove -taskCache or use a distribution with a supported task-cache backend. + {StrBegin="MSBUILD : error MSB1077: "}Do not translate -taskCache or MSBuild. + Task output parameter "{0}" contained {1} null element(s) which have been filtered out. 工作輸出參數 "{0}" 包含 {1} 個空元素,已遭篩選。 diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 0fd0245eb55..0df2acfc6a7 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -67,6 +67,13 @@ namespace Microsoft.Build.CommandLine /// public static class MSBuildApp { + internal static bool IsTaskCacheSupported => +#if FEATURE_BUILDXL_TASK_CACHE + System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64; +#else + false; +#endif + /// /// Enumeration of the various ways in which the MSBuild.exe application can exit. /// @@ -935,6 +942,8 @@ internal static ExitType Execute( string[] inputResultsCaches = null; string outputResultsCache = null; bool question = false; + bool taskCache = false; + string buildCacheDirectory = null; bool isTaskInputLoggingRequired = false; bool isBuildCheckEnabled = false; string[] getProperty = []; @@ -1001,6 +1010,8 @@ internal static ExitType Execute( #endif ref lowPriority, ref question, + ref taskCache, + ref buildCacheDirectory, ref isTaskInputLoggingRequired, ref isBuildCheckEnabled, ref getProperty, @@ -1059,7 +1070,7 @@ internal static ExitType Execute( { using (ProjectCollection collection = new(globalProperties, loggers, ToolsetDefinitionLocations.Default)) { - // globalProperties collection contains values only from CommandLine at this stage populated by ProcessCommandLineSwitches + // Includes command-line values and any engine-provided mode globals. collection.PropertiesFromCommandLine = [.. globalProperties.Keys]; // -getProperty/-getItem without a target only read data produced by the early @@ -1145,6 +1156,8 @@ internal static ExitType Execute( graphBuildOptions, lowPriority, question, + taskCache, + buildCacheDirectory, isTaskInputLoggingRequired, isBuildCheckEnabled, inputResultsCaches, @@ -1605,6 +1618,8 @@ internal static bool BuildProject( GraphBuildOptions graphBuildOptions, bool lowPriority, bool question, + bool taskCache, + string buildCacheDirectory, bool isTaskAndTargetItemLoggingRequired, bool isBuildCheckEnabled, string[] inputResultsCaches, @@ -1703,6 +1718,20 @@ internal static bool BuildProject( .. distributedLoggerRecords.Select(d => d.CentralLogger).Where(l => l is not null) ]; + if (taskCache) + { + try + { + buildCacheDirectory = new BuildParameters { BuildCacheDirectory = buildCacheDirectory }.BuildCacheDirectory; + } + catch (Exception e) when (e is ArgumentException or InvalidOperationException || ExceptionHandling.IsIoRelatedException(e)) + { + InitializationException.Throw("InvalidBuildCacheDirectory", "-buildCacheDirectory", e, false, e.Message); + } + + globalProperties[MSBuildConstants.MSBuildTaskCacheEnabled] = "true"; + } + projectCollection = new ProjectCollection( globalProperties, // When using the switch -preprocess, the project isn't built. No logger is needed to pass to avoid the crash when loading project. @@ -1813,6 +1842,15 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger).Where(l => l is not nul parameters.InputResultsCacheFiles = inputResultsCaches; parameters.OutputResultsCacheFile = outputResultsCache; parameters.Question = question; + try + { + parameters.TaskCache = taskCache; + parameters.BuildCacheDirectory = buildCacheDirectory; + } + catch (Exception e) when (e is ArgumentException or InvalidOperationException || ExceptionHandling.IsIoRelatedException(e)) + { + InitializationException.Throw("InvalidBuildCacheDirectory", "-buildCacheDirectory", e, false, e.Message); + } parameters.IsBuildCheckEnabled = isBuildCheckEnabled; #if FEATURE_REPORTFILEACCESSES parameters.ReportFileAccesses = reportFileAccesses; @@ -1877,7 +1915,14 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger).Where(l => l is not nul } } - buildManager.BeginBuild(parameters, messagesToLogInBuildLoggers); + try + { + buildManager.BeginBuild(parameters, messagesToLogInBuildLoggers); + } + catch (Exception e) when (taskCache && ExceptionHandling.IsIoRelatedException(e)) + { + InitializationException.Throw("InvalidBuildCacheDirectory", "-buildCacheDirectory", e, false, e.Message); + } Exception exception = null; try @@ -2453,6 +2498,8 @@ private static bool ProcessCommandLineSwitches( #endif ref bool lowPriority, ref bool question, + ref bool taskCache, + ref string buildCacheDirectory, ref bool isTaskInputLoggingRequired, ref bool isBuildCheckEnabled, ref string[] getProperty, @@ -2612,6 +2659,8 @@ private static bool ProcessCommandLineSwitches( #endif ref lowPriority, ref question, + ref taskCache, + ref buildCacheDirectory, ref isTaskInputLoggingRequired, ref isBuildCheckEnabled, ref getProperty, @@ -2710,6 +2759,19 @@ private static bool ProcessCommandLineSwitches( inputResultsCaches = ProcessInputResultsCaches(commandLineSwitches); outputResultsCache = ProcessOutputResultsCache(commandLineSwitches); + string[] cacheDirectories = commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.BuildCacheDirectory]; + buildCacheDirectory = cacheDirectories.Length == 0 ? null : cacheDirectories[cacheDirectories.Length - 1]; + + taskCache = commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.TaskCache) + && ProcessBooleanSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.TaskCache], defaultValue: true, resourceName: "InvalidTaskCacheValue"); + if (commandLineSwitches.IsParameterizedSwitchSet(CommandLineSwitches.ParameterizedSwitch.TaskCache)) + { + globalProperties[MSBuildConstants.MSBuildTaskCacheEnabled] = taskCache ? "true" : "false"; + } + CommandLineSwitchException.VerifyThrow(!taskCache || IsTaskCacheSupported, + "TaskCacheStorageUnavailable", "-taskCache"); + CommandLineSwitchException.VerifyThrow(!taskCache || (!question && inputResultsCaches is null && outputResultsCache is null), + "TaskCacheIncompatibleSwitches", "-taskCache"); loggers = ProcessLoggingSwitches( commandLineSwitches, diff --git a/src/Shared/TaskParameter.cs b/src/Shared/TaskParameter.cs index 079163c0f9a..9a384c0ead1 100644 --- a/src/Shared/TaskParameter.cs +++ b/src/Shared/TaskParameter.cs @@ -226,6 +226,24 @@ public override string ToString() return (WrappedParameter == null) ? String.Empty : WrappedParameter.ToString(); } + /// + /// Orders item metadata for stable fingerprints without changing ordinary IPC serialization. + /// + internal void CanonicalizeMetadata() + { + if (_wrappedParameter is TaskParameterTaskItem item) + { + item.CanonicalizeMetadata(); + } + else if (_parameterType == TaskParameterType.ITaskItemArray && _wrappedParameter is ITaskItem[] items) + { + foreach (TaskParameterTaskItem element in items) + { + element?.CanonicalizeMetadata(); + } + } + } + /// /// Serialize / deserialize this item. /// @@ -951,6 +969,27 @@ void ITaskItem2.SetMetadataValueLiteral(string metadataName, string metadataValu SetMetadata(metadataName, EscapingUtilities.Escape(metadataValue)); } + /// + /// Orders the copied metadata without modifying the original task item. + /// + internal void CanonicalizeMetadata() + { + if (_customEscapedMetadata is null || _customEscapedMetadata.Count < 2) + { + return; + } + + List names = new(_customEscapedMetadata.Keys); + names.Sort(StringComparer.Ordinal); + Dictionary ordered = new(names.Count, MSBuildNameIgnoreCaseComparer.Default); + foreach (string name in names) + { + ordered.Add(name, _customEscapedMetadata[name]); + } + + _customEscapedMetadata = ordered; + } + /// /// Returns a dictionary containing all metadata and their escaped forms. /// diff --git a/src/Tasks.UnitTests/DeclaredIOTask_Tests.cs b/src/Tasks.UnitTests/DeclaredIOTask_Tests.cs new file mode 100644 index 00000000000..632f0c108a4 --- /dev/null +++ b/src/Tasks.UnitTests/DeclaredIOTask_Tests.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Reflection; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests; + +public sealed class DeclaredIOTask_Tests +{ + [Fact] + public void ReadLinesDeclaresTheFileButNotItsReturnedTextAsFileIO() + { + typeof(ReadLinesFromFile).GetCustomAttribute().ShouldNotBeNull(); + typeof(ReadLinesFromFile).GetCustomAttributes() + .Select(attribute => attribute.ParameterName).ShouldBe([nameof(ReadLinesFromFile.File)]); + typeof(ReadLinesFromFile).GetCustomAttributes().ShouldBeEmpty(); + typeof(ReadLinesFromFile).GetProperty(nameof(ReadLinesFromFile.Lines))!.GetCustomAttribute().ShouldNotBeNull(); + } + + [Fact] + public void WriteLinesDoesNotDeclareIOBecauseItsModesCanReadAndWriteTheSameFile() + { + typeof(WriteLinesToFile).GetCustomAttribute().ShouldBeNull(); + typeof(WriteLinesToFile).GetCustomAttributes().ShouldBeEmpty(); + typeof(WriteLinesToFile).GetCustomAttributes().ShouldBeEmpty(); + } +} diff --git a/src/Tasks/FileIO/ReadLinesFromFile.cs b/src/Tasks/FileIO/ReadLinesFromFile.cs index 50b182da06f..97bb237427b 100644 --- a/src/Tasks/FileIO/ReadLinesFromFile.cs +++ b/src/Tasks/FileIO/ReadLinesFromFile.cs @@ -16,6 +16,8 @@ namespace Microsoft.Build.Tasks /// Read a list of items from a file. /// [MSBuildMultiThreadableTask] + [MSBuildDeclaredIOTask] + [MSBuildDeclaredIOInput(nameof(File))] public class ReadLinesFromFile : TaskExtension, IMultiThreadableTask { ///