├── .azuredevops └── pipelines │ ├── official.yml │ ├── pr-ci.yml │ └── templates │ ├── e2e-test.yml │ └── variables.yml ├── .editorconfig ├── .gitignore ├── .vsconfig ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Directory.Build.props ├── Directory.Build.rsp ├── Directory.Build.targets ├── Directory.Packages.props ├── LICENSE ├── MSBuildCache.sln ├── MicrosoftCorporation.snk ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── Test.snk ├── build ├── Attributes │ ├── CompilerFeatureRequiredAttribute.cs │ ├── IsExternalInit.cs │ ├── MaybeNullWhenAttribute.cs │ ├── MemberNotNullAttribute.cs │ ├── MemberNotNullWhenAttribute.cs │ ├── NotNullWhenAttribute.cs │ └── RequiredMemberAttribute.cs ├── MSBuildExtensionPackage.targets └── MSBuildReference.targets ├── nuget.config ├── src ├── AzureBlobStorage │ ├── AzureBlobStoragePluginSettings.cs │ ├── MSBuildCacheAzureBlobStoragePlugin.cs │ ├── Microsoft.MSBuildCache.AzureBlobStorage.csproj │ ├── StaticTokenCredential.cs │ ├── build │ │ ├── Microsoft.MSBuildCache.AzureBlobStorage.props │ │ └── Microsoft.MSBuildCache.AzureBlobStorage.targets │ └── buildMultitargeting │ │ ├── Microsoft.MSBuildCache.AzureBlobStorage.props │ │ └── Microsoft.MSBuildCache.AzureBlobStorage.targets ├── AzurePipelines │ ├── AzDOHelpers.cs │ ├── MSBuildCacheAzurePipelinesPlugin.cs │ ├── Microsoft.MSBuildCache.AzurePipelines.csproj │ ├── PipelineCachingCacheClient.cs │ ├── build │ │ ├── Microsoft.MSBuildCache.AzurePipelines.props │ │ └── Microsoft.MSBuildCache.AzurePipelines.targets │ └── buildMultitargeting │ │ ├── Microsoft.MSBuildCache.AzurePipelines.props │ │ └── Microsoft.MSBuildCache.AzurePipelines.targets ├── Common.Tests │ ├── DirectoryLockTests.cs │ ├── Hashing │ │ ├── CompositeInputHasherTests.cs │ │ ├── DirectoryFileHasherTests.cs │ │ ├── HashingExtensionsTests.cs │ │ ├── OutputHasherTests.cs │ │ └── SourceControlFileHasherTests.cs │ ├── HexUtilitiesTests.cs │ ├── Microsoft.MSBuildCache.Common.Tests.csproj │ ├── Mocks │ │ ├── MockPluginLogger.cs │ │ └── NullPluginLogger.cs │ ├── NodeBuildResultTests.cs │ ├── NodeTargetResultTaskItemTests.cs │ ├── PathHelperTests.cs │ ├── PluginInterfaceTypeCheckTests.cs │ ├── PluginSettingsExtensibilityTests.cs │ ├── PluginSettingsTests.cs │ └── SourceControl │ │ └── GitFileHashProviderTest.cs ├── Common │ ├── Caching │ │ ├── CacheClient.cs │ │ ├── CacheException.cs │ │ ├── CasCacheClient.cs │ │ ├── ICacheClient.cs │ │ ├── LocalCacheFactory.cs │ │ └── LocalCacheStateManager.cs │ ├── ContentHashJsonConverter.cs │ ├── DirectoryLock.cs │ ├── FileAccess │ │ ├── FileAccessRepository.cs │ │ └── FileAccesses.cs │ ├── Fingerprinting │ │ ├── FingerprintFactory.cs │ │ ├── IFingerprintFactory.cs │ │ └── PathSet.cs │ ├── Hashing │ │ ├── CompositeInputHasher.cs │ │ ├── DirectoryFileHasher.cs │ │ ├── HashingExtensions.cs │ │ ├── IInputHasher.cs │ │ ├── OutputHasher.cs │ │ └── SourceControlFileHasher.cs │ ├── HexUtilities.cs │ ├── MSBuildCachePluginBase.cs │ ├── Microsoft.MSBuildCache.Common.csproj │ ├── NetFrameworkPolyfills.cs │ ├── NodeBuildResult.cs │ ├── NodeContext.cs │ ├── NodeDescriptor.cs │ ├── NodeDescriptorFactory.cs │ ├── NodeTargetResult.cs │ ├── NodeTargetResultTaskItem.cs │ ├── Parsing │ │ ├── CompositeProjectPredictionCollector.cs │ │ ├── Parser.cs │ │ ├── PredictedInput.cs │ │ └── ProjectPredictionCollector.cs │ ├── PathHelper.cs │ ├── PathNormalizer.cs │ ├── PluginSettings.cs │ ├── ProjectInstanceExtensions.cs │ ├── SerializationHelper.cs │ ├── SortedDictionaryConverter.cs │ ├── SourceControl │ │ ├── Git.cs │ │ ├── GitFileHashProvider.cs │ │ ├── GitProcess.cs │ │ ├── ISourceControlFileHashProvider.cs │ │ └── SourceControlHashException.cs │ ├── SourceGenerationContext.cs │ ├── UInt32FlagsFormatter.cs │ ├── build │ │ ├── Microsoft.MSBuildCache.Common.props │ │ ├── Microsoft.MSBuildCache.Common.targets │ │ └── Microsoft.MSBuildCache.Cpp.targets │ └── buildMultitargeting │ │ ├── Microsoft.MSBuildCache.Common.props │ │ └── Microsoft.MSBuildCache.Common.targets ├── Local │ ├── MSBuildCacheLocalPlugin.cs │ ├── Microsoft.MSBuildCache.Local.csproj │ ├── build │ │ ├── Microsoft.MSBuildCache.Local.props │ │ └── Microsoft.MSBuildCache.Local.targets │ └── buildMultitargeting │ │ ├── Microsoft.MSBuildCache.Local.props │ │ └── Microsoft.MSBuildCache.Local.targets ├── Repack.Tests │ ├── Microsoft.MSBuildCache.Repack.Tests.csproj │ └── RepackTests.cs └── SharedCompilation │ ├── CompilerUtilities.cs │ ├── Microsoft.MSBuildCache.SharedCompilation.csproj │ ├── ResolveFileAccesses.cs │ ├── VBCSCompilerReporter.cs │ └── build │ ├── Microsoft.MSBuildCache.SharedCompilation.props │ └── Microsoft.MSBuildCache.SharedCompilation.targets ├── tests ├── TestProject │ ├── .gitignore │ ├── Directory.Build.props │ ├── Directory.Build.targets │ ├── Program.cs │ ├── TestProject.csproj │ └── nuget.config └── test.ps1 └── version.json /.azuredevops/pipelines/official.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | repositories: 3 | - repository: MicroBuildTemplate 4 | type: git 5 | name: 1ESPipelineTemplates/MicroBuildTemplate 6 | ref: refs/tags/release 7 | 8 | variables: 9 | - template: /.azuredevops/pipelines/templates/variables.yml@self 10 | - name: SignType 11 | value: Real 12 | - name: TeamName 13 | value: MSBuild 14 | trigger: 15 | batch: true 16 | branches: 17 | include: 18 | - 'main' 19 | - 'refs/tags/*' 20 | pr: none 21 | extends: 22 | template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate 23 | parameters: 24 | sdl: 25 | sbom: 26 | enabled: false 27 | pool: 28 | name: VSEngSS-MicroBuild2022-1ES 29 | demands: 30 | - msbuild 31 | - visualstudio 32 | os: windows 33 | stages: 34 | - stage: '' 35 | displayName: 'Build' 36 | jobs: 37 | - job: OfficialBuild 38 | displayName: Official Build 39 | variables: 40 | VsInstallDir: $(Build.ArtifactStagingDirectory)/vs 41 | pool: 42 | name: VSEngSS-MicroBuild2022-1ES 43 | templateContext: 44 | mb: 45 | signing: 46 | enabled: true 47 | signType: 'real' 48 | zipSources: false 49 | outputs: 50 | - output: nuget 51 | displayName: 'Push NuGet Packages to nuget.org' 52 | condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) 53 | packageParentPath: '$(Build.ArtifactStagingDirectory)' 54 | packagesToPush: '$(ArtifactsDirectory)/**/*.nupkg' 55 | nuGetFeedType: 'external' 56 | publishFeedCredentials: 'MSBuildCache-Push' 57 | - output: pipelineArtifact 58 | displayName: 'Publish Logs' 59 | condition: always() 60 | targetPath: $(LogDirectory) 61 | artifactName: 'logs' 62 | - output: pipelineArtifact 63 | displayName: 'Publish Artifacts' 64 | condition: always() 65 | targetPath: $(ArtifactsDirectory) 66 | artifactName: artifacts 67 | steps: 68 | - task: PowerShell@2 69 | displayName: 'Update SignType, Build Number, and Add Build Tag for tagged commits' 70 | condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) 71 | inputs: 72 | targetType: 'inline' 73 | script: | 74 | Write-Host "Updating SignType to 'Real'" 75 | Write-Host "##vso[task.setvariable variable=SignType;]Real" 76 | Write-Host "" 77 | $buildTag = [System.Text.RegularExpressions.Regex]::Match("$(Build.SourceBranchName)", "v.*") 78 | if($buildTag.Success -eq $true) 79 | { 80 | Write-Host "Updating VSTS build number to ""$buildTag""" 81 | Write-Host "##vso[build.updatebuildnumber]$buildTag" 82 | Write-Host "" 83 | Write-Host "Adding build tag ""$buildTag""" 84 | Write-Host "##vso[build.addbuildtag]$buildTag" 85 | } 86 | - checkout: self 87 | fetchDepth: 0 88 | - task: UseDotNet@2 89 | displayName: 'Install .NET $(DotNetVersion)' 90 | inputs: 91 | version: '$(DotNetVersion)' 92 | - task: NuGetAuthenticate@1 93 | displayName: NuGet Authenticate 94 | - script: | 95 | dotnet build $(Build.SourcesDirectory)/MSBuildCache.sln --configuration $(BuildConfiguration) -BinaryLogger:$(LogDirectory)/msbuild.binlog 96 | displayName: Build 97 | - task: DotNetCoreCLI@2 98 | displayName: Run Unit Tests 99 | inputs: 100 | command: test 101 | projects: $(Build.SourcesDirectory)/MSBuildCache.sln 102 | arguments: -restore:false --no-build --configuration $(BuildConfiguration) -------------------------------------------------------------------------------- /.azuredevops/pipelines/templates/e2e-test.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: RepoRoot 3 | type: string 4 | - name: MSBuildPath 5 | type: string 6 | default: "" 7 | 8 | steps: 9 | - task: PowerShell@2 10 | displayName: "E2E Test: Microsoft.MSBuildCache.Local" 11 | inputs: 12 | filePath: ${{ parameters.RepoRoot }}\tests\test.ps1 13 | arguments: -MSBuildPath "${{ parameters.MSBuildPath }}" -Configuration $(BuildConfiguration) -LogDirectory "$(LogDirectory)\Tests\Local" -LocalPackageDir "$(Pipeline.Workspace)\artifacts\$(BuildConfiguration)\packages" -CachePackage Microsoft.MSBuildCache.Local 14 | pwsh: true 15 | 16 | - task: PowerShell@2 17 | displayName: "E2E Test: Microsoft.MSBuildCache.AzurePipelines" 18 | # The access token from forks do not have enough scopes to access the pipeline cache, so skip these tests. 19 | # Note to repo maintainers: You can manually run the pipeline against the commit, even if the commit is from a fork, if you wish to test this. 20 | condition: ne(variables['System.PullRequest.IsFork'], 'True') 21 | inputs: 22 | filePath: ${{ parameters.RepoRoot }}\tests\test.ps1 23 | arguments: -MSBuildPath "${{ parameters.MSBuildPath }}" -Configuration $(BuildConfiguration) -LogDirectory "$(LogDirectory)\Tests\AzurePipelines" -LocalPackageDir "$(Pipeline.Workspace)\artifacts\$(BuildConfiguration)\packages" -CachePackage Microsoft.MSBuildCache.AzurePipelines 24 | pwsh: true 25 | env: 26 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) -------------------------------------------------------------------------------- /.azuredevops/pipelines/templates/variables.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | BuildConfiguration: Release 3 | DotNetVersion: '8.x' 4 | LogDirectory: $(Build.ArtifactStagingDirectory)\logs 5 | ArtifactsDirectory: $(Build.ArtifactStagingDirectory)\artifacts 6 | # https://github.com/microsoft/azure-pipelines-agent/pull/4077 7 | VSO_DEDUP_REDIRECT_TIMEOUT_IN_SEC: 5 8 | EnablePipelineCache: true -------------------------------------------------------------------------------- /.vsconfig: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "components": [ 4 | "Microsoft.Component.MSBuild", 5 | "Microsoft.Net.Component.4.7.2.TargetingPack", 6 | "Microsoft.NetCore.Component.SDK", 7 | "Microsoft.VisualStudio.Component.NuGet.BuildTools", 8 | "Microsoft.VisualStudio.Component.Roslyn.Compiler" 9 | ] 10 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | ## Building 16 | 17 | ### Building MSBuildCache 18 | 19 | You can build `MSBuildCache.sln` and/or open it in VS. 20 | 21 | ### Using a custom MSBuild (optional) 22 | 23 | Clone the [MSBuild repo](https://github.com/dotnet/msbuild) adjacent to the MSBuildCache repo. 24 | 25 | The remaining steps assume the `cwd` is the root of the MSBuildCache repo. You may need to adjust the paths if this is not the case. 26 | 27 | Build the msbuild repo: 28 | ``` 29 | ..\msbuild\build.cmd /p:CreateBootstrap=true /p:Configuration=Release 30 | ``` 31 | Note, MSBuild only needs to be built every time you update MSBuild, not every time you want to build MSBuildCache. 32 | 33 | The path to MSBuild is: `..\msbuild\artifacts\bin\bootstrap\net472\MSBuild\Current\Bin\amd64\MSBuild.exe`. 34 | 35 | Note: when using a locally built MSBuild, many scenario may not work properly, for example C++ builds. 36 | 37 | ## Testing 38 | 39 | ### Running 40 | 41 | Ensure you've built MSBuildCache as described above. 42 | 43 | Now add the following package source to your test repo's `NuGet.config`: 44 | 45 | ```xml 46 | 47 | ``` 48 | 49 | You may need to adjust the above path based on the paths to your test repo and MSBuildCache. 50 | 51 | Finally, add a `PackageReference` to MSBuildCache to your test repo with version `*-*` to use the latest MSBuildCache version. This may look different from repo to repo, but here is an example: 52 | 53 | ```xml 54 | 55 | ``` 56 | 57 | **NOTE!** Because you're using a locally built package, you may need to clear it from your package cache after each iteration via a command like `rmdir /S /Q %NUGET_PACKAGES%\Microsoft.MSBuildCache` (if you set `%NUGET_PACKAGES%`) or `rmdir /S /Q %USERPROFILE%\.nuget\packages\Microsoft.MSBuildCache` if you're using the dfault package cache location. Additionally, to ensure you're not using the head version of the package, you may need to create a branch and dummy commit locally to ensure the version is higher. 58 | 59 | **NOTE!** MSBuildCache currently does not handle incremental builds well! The current target scenario is for CI environments, so **it's expected that the repo is always clean before building**. The main reason for this gap is because file probes and directory enumerations are not currently considered. 60 | 61 | To enable file reporting via detours in MSBuild, ensure `/graph` and `/reportfileaccesses` are used. 62 | 63 | Example of a set of commands to test MSBuildCache e2e in some repo: 64 | 65 | ``` 66 | rmdir /S /Q %USERPROFILE%\.nuget\packages\Microsoft.MSBuildCache 67 | git clean -fdx 68 | /restore /graph /m /nr:false /reportfileaccesses 69 | ``` 70 | 71 | Example of a set of commands to test MSBuildCache e2e in a subdirectory of some repo: 72 | 73 | ``` 74 | rmdir /S /Q %USERPROFILE%\.nuget\packages\Microsoft.MSBuildCache 75 | pushd 76 | git clean -fdx 77 | popd 78 | /restore /graph /m /nr:false /reportfileaccesses 79 | ``` 80 | 81 | ### Settings 82 | 83 | There is a built-in mechanism to passing settings to a plugin, which is to add metadata to the `ProjectCachePlugin` item. This is then exposed to the plugin via the `CacheContext` during initialialization. 84 | 85 | To add additional settings, add it to [`PluginSettings`](src\Common\PluginSettings.cs) and [`Microsoft.MSBuildCache.Common.targets`](src\Common\build\Microsoft.MSBuildCache.Common.targets). By convention the MSBuild property name should be the setting name prefixed by "MSBuildCache". 86 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildThisFileDirectory) 4 | 5 | enable 6 | 7 | 8 | Latest 9 | 10 | 11 | $(ArtifactsDirectory)\$(Configuration)\ 12 | $(MSBuildThisFileDirectory)artifacts\$(Configuration)\ 13 | 14 | 15 | $(BaseArtifactsPath)\packages 16 | 17 | 18 | true 19 | true 20 | 21 | 22 | true 23 | true 24 | All 25 | 26 | 27 | embedded 28 | 29 | 30 | true 31 | 32 | 33 | true 34 | true 35 | 36 | 37 | $(NoWarn);NU5104 38 | 39 | 40 | 41 | 42 | Microsoft 43 | Microsoft 44 | © Microsoft Corporation. All rights reserved. 45 | A project cache plugin for MSBuild 46 | LICENSE 47 | $(MSBuildThisFileDirectory)$(PackageLicenseFile) 48 | https://github.com/microsoft/MSBuildCache 49 | https://github.com/microsoft/MSBuildCache 50 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | true 61 | true 62 | 63 | 64 | -------------------------------------------------------------------------------- /Directory.Build.rsp: -------------------------------------------------------------------------------- 1 | -ConsoleLoggerParameters:Verbosity=Minimal;Summary;ForceNoAlign 2 | -MaxCPUCount 3 | -NodeReuse:false 4 | -Restore 5 | -Property:NuGetInteractive=True 6 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | true 9 | false 10 | true 11 | $(MSBuildThisFileDirectory)MicrosoftCorporation.snk 12 | $(MSBuildThisFileDirectory)Test.snk 13 | 14 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19.250.35814-buildid29613111 5 | 0.1.0-20250207.7.2 6 | 17.11.4 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /MSBuildCache.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EFFB5949-347C-4F28-8964-571D5C6B6209}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MSBuildCache.Common", "src\Common\Microsoft.MSBuildCache.Common.csproj", "{33CD5B46-ECC1-47B1-B9B4-972218BFC4AC}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MSBuildCache.Common.Tests", "src\Common.Tests\Microsoft.MSBuildCache.Common.Tests.csproj", "{41E23EB7-6313-4DD0-9F4D-39304CD030A8}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0392E290-973E-4086-A58E-F927AAA65B9A}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | .gitignore = .gitignore 16 | .gitmodules = .gitmodules 17 | azure-pipelines.yml = azure-pipelines.yml 18 | Directory.Build.props = Directory.Build.props 19 | Directory.Build.rsp = Directory.Build.rsp 20 | Directory.Build.targets = Directory.Build.targets 21 | Directory.Packages.props = Directory.Packages.props 22 | nuget.config = nuget.config 23 | README.md = README.md 24 | version.json = version.json 25 | EndProjectSection 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MSBuildCache.SharedCompilation", "src\SharedCompilation\Microsoft.MSBuildCache.SharedCompilation.csproj", "{53A7161F-16F8-4672-807B-153603AB9A9A}" 28 | EndProject 29 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MSBuildCache.AzureBlobStorage", "src\AzureBlobStorage\Microsoft.MSBuildCache.AzureBlobStorage.csproj", "{D0195D37-E001-4283-B51A-A0B51B1D54D1}" 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MSBuildCache.AzurePipelines", "src\AzurePipelines\Microsoft.MSBuildCache.AzurePipelines.csproj", "{97357681-C75E-445D-8547-46F312D01CED}" 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MSBuildCache.Local", "src\Local\Microsoft.MSBuildCache.Local.csproj", "{F6586428-E047-42C8-B0AC-048DF6DFAF18}" 34 | EndProject 35 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MSBuildCache.Repack.Tests", "src\Repack.Tests\Microsoft.MSBuildCache.Repack.Tests.csproj", "{3BCB6452-B087-4A03-8418-C79F2715DDE7}" 36 | EndProject 37 | Global 38 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 39 | Debug|x64 = Debug|x64 40 | Release|x64 = Release|x64 41 | EndGlobalSection 42 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 43 | {33CD5B46-ECC1-47B1-B9B4-972218BFC4AC}.Debug|x64.ActiveCfg = Debug|x64 44 | {33CD5B46-ECC1-47B1-B9B4-972218BFC4AC}.Debug|x64.Build.0 = Debug|x64 45 | {33CD5B46-ECC1-47B1-B9B4-972218BFC4AC}.Release|x64.ActiveCfg = Release|x64 46 | {33CD5B46-ECC1-47B1-B9B4-972218BFC4AC}.Release|x64.Build.0 = Release|x64 47 | {41E23EB7-6313-4DD0-9F4D-39304CD030A8}.Debug|x64.ActiveCfg = Debug|x64 48 | {41E23EB7-6313-4DD0-9F4D-39304CD030A8}.Debug|x64.Build.0 = Debug|x64 49 | {41E23EB7-6313-4DD0-9F4D-39304CD030A8}.Release|x64.ActiveCfg = Release|x64 50 | {41E23EB7-6313-4DD0-9F4D-39304CD030A8}.Release|x64.Build.0 = Release|x64 51 | {53A7161F-16F8-4672-807B-153603AB9A9A}.Debug|x64.ActiveCfg = Debug|x64 52 | {53A7161F-16F8-4672-807B-153603AB9A9A}.Debug|x64.Build.0 = Debug|x64 53 | {53A7161F-16F8-4672-807B-153603AB9A9A}.Release|x64.ActiveCfg = Release|x64 54 | {53A7161F-16F8-4672-807B-153603AB9A9A}.Release|x64.Build.0 = Release|x64 55 | {D0195D37-E001-4283-B51A-A0B51B1D54D1}.Debug|x64.ActiveCfg = Debug|x64 56 | {D0195D37-E001-4283-B51A-A0B51B1D54D1}.Debug|x64.Build.0 = Debug|x64 57 | {D0195D37-E001-4283-B51A-A0B51B1D54D1}.Release|x64.ActiveCfg = Release|x64 58 | {D0195D37-E001-4283-B51A-A0B51B1D54D1}.Release|x64.Build.0 = Release|x64 59 | {97357681-C75E-445D-8547-46F312D01CED}.Debug|x64.ActiveCfg = Debug|x64 60 | {97357681-C75E-445D-8547-46F312D01CED}.Debug|x64.Build.0 = Debug|x64 61 | {97357681-C75E-445D-8547-46F312D01CED}.Release|x64.ActiveCfg = Release|x64 62 | {97357681-C75E-445D-8547-46F312D01CED}.Release|x64.Build.0 = Release|x64 63 | {F6586428-E047-42C8-B0AC-048DF6DFAF18}.Debug|x64.ActiveCfg = Debug|x64 64 | {F6586428-E047-42C8-B0AC-048DF6DFAF18}.Debug|x64.Build.0 = Debug|x64 65 | {F6586428-E047-42C8-B0AC-048DF6DFAF18}.Release|x64.ActiveCfg = Release|x64 66 | {F6586428-E047-42C8-B0AC-048DF6DFAF18}.Release|x64.Build.0 = Release|x64 67 | {3BCB6452-B087-4A03-8418-C79F2715DDE7}.Debug|x64.ActiveCfg = Debug|x64 68 | {3BCB6452-B087-4A03-8418-C79F2715DDE7}.Debug|x64.Build.0 = Debug|x64 69 | {3BCB6452-B087-4A03-8418-C79F2715DDE7}.Release|x64.ActiveCfg = Release|x64 70 | {3BCB6452-B087-4A03-8418-C79F2715DDE7}.Release|x64.Build.0 = Release|x64 71 | EndGlobalSection 72 | GlobalSection(SolutionProperties) = preSolution 73 | HideSolutionNode = FALSE 74 | EndGlobalSection 75 | GlobalSection(NestedProjects) = preSolution 76 | {33CD5B46-ECC1-47B1-B9B4-972218BFC4AC} = {EFFB5949-347C-4F28-8964-571D5C6B6209} 77 | {41E23EB7-6313-4DD0-9F4D-39304CD030A8} = {EFFB5949-347C-4F28-8964-571D5C6B6209} 78 | {53A7161F-16F8-4672-807B-153603AB9A9A} = {EFFB5949-347C-4F28-8964-571D5C6B6209} 79 | {D0195D37-E001-4283-B51A-A0B51B1D54D1} = {EFFB5949-347C-4F28-8964-571D5C6B6209} 80 | {97357681-C75E-445D-8547-46F312D01CED} = {EFFB5949-347C-4F28-8964-571D5C6B6209} 81 | {F6586428-E047-42C8-B0AC-048DF6DFAF18} = {EFFB5949-347C-4F28-8964-571D5C6B6209} 82 | {3BCB6452-B087-4A03-8418-C79F2715DDE7} = {EFFB5949-347C-4F28-8964-571D5C6B6209} 83 | EndGlobalSection 84 | GlobalSection(ExtensibilityGlobals) = postSolution 85 | SolutionGuid = {F1CDA78F-A666-431B-BF44-56DA7DF193BA} 86 | EndGlobalSection 87 | EndGlobal 88 | -------------------------------------------------------------------------------- /MicrosoftCorporation.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MSBuildCache/84e8d211a75e01fa83b2b17a79b7d1fd35633228/MicrosoftCorporation.snk -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /Test.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/MSBuildCache/84e8d211a75e01fa83b2b17a79b7d1fd35633228/Test.snk -------------------------------------------------------------------------------- /build/Attributes/CompilerFeatureRequiredAttribute.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // Copied from: https://github.com/dotnet/runtime/blob/fdd104ec5e1d0d2aa24a6723995a98d0124f724b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CompilerFeatureRequiredAttribute.cs 5 | 6 | #if NETFRAMEWORK || NETSTANDARD 7 | namespace System.Runtime.CompilerServices 8 | { 9 | /// 10 | /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. 11 | /// 12 | [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] 13 | internal sealed class CompilerFeatureRequiredAttribute : Attribute 14 | { 15 | /// 16 | public CompilerFeatureRequiredAttribute(string featureName) 17 | { 18 | FeatureName = featureName; 19 | } 20 | 21 | /// 22 | /// The name of the compiler feature. 23 | /// 24 | public string FeatureName { get; } 25 | 26 | /// 27 | /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . 28 | /// 29 | public bool IsOptional { get; init; } 30 | 31 | /// 32 | /// The used for the ref structs C# feature. 33 | /// 34 | public const string RefStructs = nameof(RefStructs); 35 | 36 | /// 37 | /// The used for the required members C# feature. 38 | /// 39 | public const string RequiredMembers = nameof(RequiredMembers); 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /build/Attributes/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #if NETFRAMEWORK || NETSTANDARD 5 | namespace System.Runtime.CompilerServices 6 | { 7 | /// 8 | internal sealed class IsExternalInit 9 | { 10 | } 11 | } 12 | #endif 13 | -------------------------------------------------------------------------------- /build/Attributes/MaybeNullWhenAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #if NETFRAMEWORK || NETSTANDARD 5 | namespace System.Diagnostics.CodeAnalysis 6 | { 7 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 8 | internal sealed class MaybeNullWhenAttribute : Attribute 9 | { 10 | public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 11 | 12 | public bool ReturnValue { get; } 13 | } 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /build/Attributes/MemberNotNullAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #if NETFRAMEWORK || NETSTANDARD 5 | namespace System.Diagnostics.CodeAnalysis 6 | { 7 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] 8 | internal sealed class MemberNotNullAttribute : Attribute 9 | { 10 | public MemberNotNullAttribute(string member) => Members = new[] { member }; 11 | 12 | public MemberNotNullAttribute(params string[] members) => Members = members; 13 | 14 | public string[] Members { get; } 15 | } 16 | } 17 | #endif -------------------------------------------------------------------------------- /build/Attributes/MemberNotNullWhenAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #if NETFRAMEWORK || NETSTANDARD 5 | namespace System.Diagnostics.CodeAnalysis 6 | { 7 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] 8 | internal sealed class MemberNotNullWhenAttribute : Attribute 9 | { 10 | public MemberNotNullWhenAttribute(bool returnValue, string member) 11 | { 12 | ReturnValue = returnValue; 13 | Members = new[] { member }; 14 | } 15 | 16 | public MemberNotNullWhenAttribute(bool returnValue, params string[] members) 17 | { 18 | ReturnValue = returnValue; 19 | Members = members; 20 | } 21 | 22 | public bool ReturnValue { get; } 23 | 24 | public string[] Members { get; } 25 | } 26 | } 27 | #endif -------------------------------------------------------------------------------- /build/Attributes/NotNullWhenAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #if NETFRAMEWORK || NETSTANDARD 5 | namespace System.Diagnostics.CodeAnalysis 6 | { 7 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 8 | internal sealed class NotNullWhenAttribute : Attribute 9 | { 10 | public bool ReturnValue { get; } 11 | 12 | public NotNullWhenAttribute(bool returnValue) 13 | { 14 | ReturnValue = returnValue; 15 | } 16 | } 17 | } 18 | #endif -------------------------------------------------------------------------------- /build/Attributes/RequiredMemberAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the MIT license. See License.txt in the project root for license information. 3 | 4 | // Copied from: 5 | // https://github.com/dotnet/runtime/blob/fdd104ec5e1d0d2aa24a6723995a98d0124f724b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RequiredMemberAttribute.cs 6 | 7 | #if NETFRAMEWORK || NETSTANDARD 8 | namespace System.Runtime.CompilerServices 9 | { 10 | /// Specifies that a type has required members or that a member is required. 11 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] 12 | internal sealed class RequiredMemberAttribute : Attribute 13 | { 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /build/MSBuildExtensionPackage.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | build\ 4 | true 5 | true 6 | 7 | true 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | <_FrameworkAssemblyReferences Remove="@(_FrameworkAssemblyReferences)" /> 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | true 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | all 57 | runtime; build; native; contentfiles; analyzers 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 78 | 79 | 80 | 81 | $(ILRepack) /allowduplicateresources /union /parallel /out:$(OutputPath)$(AssemblyName)$(TargetExt) @(LibraryPaths->'/lib:%(FullPath)', ' ') 82 | $(IlRepackCommand) /verbose 83 | $(IlRepackCommand) /delaysign 84 | $(IlRepackCommand) /keyfile:$(AssemblyOriginatorKeyFile) 85 | $(IlRepackCommand) @(InputAssemblies->'%(FullPath)', ' ') 86 | 87 | 88 | 92 | 93 | -------------------------------------------------------------------------------- /build/MSBuildReference.targets: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/AzureBlobStorage/AzureBlobStoragePluginSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.MSBuildCache.AzureBlobStorage; 7 | 8 | public class AzureBlobStoragePluginSettings : PluginSettings 9 | { 10 | public Uri? BlobUri { get; init; } 11 | 12 | public string? ManagedIdentityClientId { get; init; } 13 | 14 | public bool AllowInteractiveAuth { get; set; } 15 | 16 | public string InteractiveAuthTokenDirectory { get; init; } = Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\MSBuildCache\AuthTokenCache"); 17 | 18 | public string? BuildCacheConfigurationFile { get; init; } 19 | 20 | public string? BuildCacheResourceId { get; init; } 21 | } 22 | -------------------------------------------------------------------------------- /src/AzureBlobStorage/Microsoft.MSBuildCache.AzureBlobStorage.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | x64 5 | $(Platform) 6 | net472;net8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | true 18 | build\ 19 | 20 | 21 | PreserveNewest 22 | true 23 | build\ 24 | 25 | 26 | PreserveNewest 27 | true 28 | buildMultiTargeting\ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/AzureBlobStorage/StaticTokenCredential.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Azure.Core; 5 | using System.Threading.Tasks; 6 | using System.Threading; 7 | using System; 8 | 9 | namespace Microsoft.MSBuildCache.AzureBlobStorage; 10 | 11 | internal sealed class StaticTokenCredential : TokenCredential 12 | { 13 | private readonly string _accessToken; 14 | 15 | public StaticTokenCredential(string accessToken) 16 | { 17 | _accessToken = accessToken; 18 | } 19 | 20 | public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) 21 | // The token is static and we don't know the expiry, so just say it's a day from now. 22 | => new AccessToken(_accessToken, DateTimeOffset.Now.AddDays(1)); 23 | 24 | public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) 25 | => new ValueTask(GetToken(requestContext, cancellationToken)); 26 | } 27 | -------------------------------------------------------------------------------- /src/AzureBlobStorage/build/Microsoft.MSBuildCache.AzureBlobStorage.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildThisFileDirectory)net472\Microsoft.MSBuildCache.AzureBlobStorage.dll 4 | $(MSBuildThisFileDirectory)net8.0\Microsoft.MSBuildCache.AzureBlobStorage.dll 5 | 6 | 7 | false 8 | false 9 | true 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/AzureBlobStorage/build/Microsoft.MSBuildCache.AzureBlobStorage.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheBlobUri 4 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheManagedIdentityClientId 5 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheAllowInteractiveAuth 6 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheInteractiveAuthTokenDirectory 7 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheBuildCacheConfigurationFile 8 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheBuildCacheResourceId 9 | 10 | 11 | 12 | 13 | 14 | 15 | $(MSBuildCacheBlobUri) 16 | $(MSBuildCacheManagedIdentityClientId) 17 | $(MSBuildCacheAllowInteractiveAuth) 18 | $(MSBuildCacheInteractiveAuthTokenDirectory) 19 | $(MSBuildCacheBuildCacheConfigurationFile) 20 | $(MSBuildCacheBuildCacheResourceId) 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/AzureBlobStorage/buildMultitargeting/Microsoft.MSBuildCache.AzureBlobStorage.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/AzureBlobStorage/buildMultitargeting/Microsoft.MSBuildCache.AzureBlobStorage.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/AzurePipelines/AzDOHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using Microsoft.VisualStudio.Services.Common; 6 | using Microsoft.VisualStudio.Services.WebApi; 7 | 8 | namespace Microsoft.MSBuildCache.AzurePipelines; 9 | 10 | internal static class AzDOHelpers 11 | { 12 | private static readonly string? AccessToken = Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN"); 13 | 14 | private static readonly string? CollectionUri = Environment.GetEnvironmentVariable("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"); 15 | 16 | public static Guid SessionGuid => VssClientHttpRequestSettings.Default.SessionId; 17 | 18 | public static VssBasicCredential GetCredentials() 19 | { 20 | EnsureAzureDevopsEnvironment(); 21 | if (AccessToken == null) 22 | { 23 | throw new InvalidOperationException("Access token is not available. See: https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken"); 24 | } 25 | 26 | return new VssBasicCredential("user", AccessToken); 27 | } 28 | 29 | private static Uri GetTfsUrl() 30 | { 31 | EnsureAzureDevopsEnvironment(); 32 | return new Uri(CollectionUri!); 33 | } 34 | 35 | public static bool IsRunningInPipeline() => CollectionUri != null; 36 | 37 | public static Uri GetServiceUriFromEnv(string service) 38 | { 39 | Uri tfs = GetTfsUrl(); 40 | return new Uri( 41 | tfs.AbsoluteUri 42 | .Replace("https://dev.azure.com", $"https://{service}.dev.azure.com", StringComparison.OrdinalIgnoreCase) 43 | .Replace(".visualstudio.com", $".{service}.visualstudio.com", StringComparison.OrdinalIgnoreCase) 44 | ); 45 | } 46 | 47 | private static void EnsureAzureDevopsEnvironment() 48 | { 49 | if (!IsRunningInPipeline()) 50 | { 51 | throw new InvalidOperationException("Not running in an Azure DevOps environment"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/AzurePipelines/MSBuildCacheAzurePipelinesPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BuildXL.Cache.ContentStore.Distributed.NuCache; 9 | using BuildXL.Cache.ContentStore.Hashing; 10 | using BuildXL.Cache.ContentStore.Interfaces.Results; 11 | using BuildXL.Cache.ContentStore.Interfaces.Stores; 12 | using BuildXL.Cache.ContentStore.Interfaces.Tracing; 13 | using BuildXL.Cache.ContentStore.Logging; 14 | using BuildXL.Cache.MemoizationStore.Interfaces.Sessions; 15 | using BuildXL.Cache.MemoizationStore.Sessions; 16 | using Microsoft.Build.Experimental.ProjectCache; 17 | using Microsoft.Build.Framework; 18 | using Microsoft.MSBuildCache.Caching; 19 | 20 | namespace Microsoft.MSBuildCache.AzurePipelines; 21 | 22 | public sealed class MSBuildCacheAzurePipelinesPlugin : MSBuildCachePluginBase 23 | { 24 | protected override HashType HashType => HashType.Dedup1024K; 25 | 26 | protected override async Task CreateCacheClientAsync(PluginLoggerBase logger, CancellationToken cancellationToken) 27 | { 28 | if (Settings == null 29 | || FingerprintFactory == null 30 | || ContentHasher == null 31 | || NugetPackageRoot == null) 32 | { 33 | throw new InvalidOperationException(); 34 | } 35 | 36 | logger.LogMessage($"Using Azure DevOps with session '{AzDOHelpers.SessionGuid}'.", MessageImportance.Normal); 37 | 38 | FileLog fileLog = new(Path.Combine(Settings.LogDirectory, "CacheClient.log")); 39 | #pragma warning disable CA2000 // Dispose objects before losing scope. Expected to be disposed using Context.Logger.Dispose in the cache client implementation. 40 | Logger cacheLogger = new(fileLog); 41 | #pragma warning restore CA2000 // Dispose objects before losing scope 42 | Context context = new(cacheLogger); 43 | 44 | #pragma warning disable CA2000 // Dispose objects before losing scope. Expected to be disposed by TwoLevelCache 45 | LocalCache localCache = LocalCacheFactory.Create(cacheLogger, Settings.LocalCacheRootPath, Settings.LocalCacheSizeInMegabytes); 46 | #pragma warning restore CA2000 // Dispose objects before losing scope 47 | 48 | ICacheSession localCacheSession = await StartCacheSessionAsync(context, localCache, "local"); 49 | 50 | return new PipelineCachingCacheClient( 51 | context, 52 | FingerprintFactory, 53 | ContentHasher, 54 | localCache, 55 | localCacheSession, 56 | cacheLogger, 57 | Settings.CacheUniverse, 58 | Settings.RepoRoot, 59 | NugetPackageRoot, 60 | GetFileRealizationMode, 61 | Settings.MaxConcurrentCacheContentOperations, 62 | Settings.RemoteCacheIsReadOnly, 63 | Settings.AsyncCachePublishing, 64 | Settings.AsyncCacheMaterialization, 65 | Settings.SkipUnchangedOutputFiles, 66 | Settings.TouchOutputFiles); 67 | } 68 | 69 | private static async Task StartCacheSessionAsync(Context context, LocalCache cache, string name) 70 | { 71 | await cache.StartupAsync(context).ThrowIfFailure(); 72 | CreateSessionResult cacheSessionResult = cache 73 | .CreateSession(context, name, ImplicitPin.PutAndGet) 74 | .ThrowIfFailure(); 75 | ICacheSession session = cacheSessionResult.Session!; 76 | 77 | (await session.StartupAsync(context)).ThrowIfFailure(); 78 | 79 | return session; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/AzurePipelines/Microsoft.MSBuildCache.AzurePipelines.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | x64 5 | $(Platform) 6 | net472;net8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | true 25 | build\ 26 | 27 | 28 | PreserveNewest 29 | true 30 | build\ 31 | 32 | 33 | PreserveNewest 34 | true 35 | buildMultiTargeting\ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/AzurePipelines/build/Microsoft.MSBuildCache.AzurePipelines.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildThisFileDirectory)net472\Microsoft.MSBuildCache.AzurePipelines.dll 4 | $(MSBuildThisFileDirectory)net8.0\Microsoft.MSBuildCache.AzurePipelines.dll 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/AzurePipelines/build/Microsoft.MSBuildCache.AzurePipelines.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/AzurePipelines/buildMultitargeting/Microsoft.MSBuildCache.AzurePipelines.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/AzurePipelines/buildMultitargeting/Microsoft.MSBuildCache.AzurePipelines.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Common.Tests/DirectoryLockTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.MSBuildCache.Tests.Mocks; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | namespace Microsoft.MSBuildCache.Tests; 12 | 13 | [TestClass] 14 | public class DirectoryLockTests 15 | { 16 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. Justification: Always set by MSTest 17 | public TestContext TestContext { get; set; } 18 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 19 | 20 | [TestMethod] 21 | public void Acquire() 22 | { 23 | string lockFilePath = GetLockFilePath(); 24 | using (DirectoryLock directoryLock1 = new(lockFilePath, NullPluginLogger.Instance)) 25 | { 26 | Assert.IsTrue(directoryLock1.Acquire()); 27 | } 28 | } 29 | 30 | [TestMethod] 31 | public void Reentry() 32 | { 33 | string lockFilePath = GetLockFilePath(); 34 | using (DirectoryLock directoryLock1 = new(lockFilePath, NullPluginLogger.Instance)) 35 | { 36 | for (int i = 0; i < 100; i++) 37 | { 38 | Assert.IsTrue(directoryLock1.Acquire()); 39 | } 40 | } 41 | } 42 | 43 | [TestMethod] 44 | public void Contention() 45 | { 46 | string lockFilePath = GetLockFilePath(); 47 | using (DirectoryLock directoryLock1 = new(lockFilePath, NullPluginLogger.Instance)) 48 | using (DirectoryLock directoryLock2 = new(lockFilePath, NullPluginLogger.Instance)) 49 | { 50 | Assert.IsTrue(directoryLock1.Acquire()); 51 | 52 | // Second locker cannot acquire 53 | Assert.IsFalse(directoryLock2.Acquire()); 54 | 55 | directoryLock1.Dispose(); 56 | 57 | // Second locker can now acquire 58 | Assert.IsTrue(directoryLock2.Acquire()); 59 | } 60 | } 61 | 62 | [TestMethod] 63 | public void StressTest() 64 | { 65 | string lockFilePath = GetLockFilePath(); 66 | 67 | int lockCount = Environment.ProcessorCount * 100; 68 | int successCount = 0; 69 | Parallel.For( 70 | 0, 71 | lockCount, 72 | new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, 73 | i => 74 | { 75 | using (DirectoryLock directoryLock = new(lockFilePath, NullPluginLogger.Instance)) 76 | { 77 | // Keep trying in a tight loop 78 | while (!directoryLock.Acquire()) 79 | { 80 | } 81 | 82 | Interlocked.Increment(ref successCount); 83 | } 84 | }); 85 | 86 | Assert.AreEqual(lockCount, successCount); 87 | } 88 | 89 | private string GetLockFilePath() => Path.Combine(TestContext.TestRunDirectory!, TestContext.TestName! + ".lock"); 90 | } 91 | -------------------------------------------------------------------------------- /src/Common.Tests/Hashing/CompositeInputHasherTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Microsoft.MSBuildCache.Hashing; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | 10 | namespace Microsoft.MSBuildCache.Tests.Hashing; 11 | 12 | [TestClass] 13 | public class CompositeInputHasherTests 14 | { 15 | [TestMethod] 16 | public async Task PrecedenceAndFallback() 17 | { 18 | Dictionary hashes1 = new(StringComparer.OrdinalIgnoreCase) 19 | { 20 | { @"X:\1-Only.txt", new byte[] { 0x01, 0x01 } }, 21 | { @"X:\Shared.txt", new byte[] { 0x01, 0x02 } }, 22 | }; 23 | Dictionary hashes2 = new(StringComparer.OrdinalIgnoreCase) 24 | { 25 | { @"X:\2-Only.txt", new byte[] { 0x02, 0x01 } }, 26 | { @"X:\Shared.txt", new byte[] { 0x02, 0x02 } }, 27 | }; 28 | 29 | CompositeInputHasher hasher = new(new[] { new MockInputHasher(hashes1), new MockInputHasher(hashes2) }); 30 | 31 | Assert.IsTrue(hasher.ContainsPath(@"X:\1-Only.txt")); 32 | Assert.IsTrue(hasher.ContainsPath(@"X:\2-Only.txt")); 33 | Assert.IsTrue(hasher.ContainsPath(@"X:\Shared.txt")); 34 | Assert.IsFalse(hasher.ContainsPath(@"X:\DoesNotExist.txt")); 35 | 36 | await AssertHashAsync(@"X:\1-Only.txt", hashes1); 37 | await AssertHashAsync(@"X:\2-Only.txt", hashes2); 38 | await AssertHashAsync(@"X:\Shared.txt", hashes1); // the first one wins 39 | Assert.IsNull(await hasher.GetHashAsync(@"X:\DoesNotExist.txt")); 40 | 41 | async Task AssertHashAsync(string path, Dictionary hashes) 42 | { 43 | Assert.IsTrue(hashes.TryGetValue(path, out byte[]? expectedHash)); 44 | CollectionAssert.AreEqual(expectedHash, await hasher.GetHashAsync(path)); 45 | } 46 | } 47 | 48 | private sealed class MockInputHasher(IReadOnlyDictionary hashes) : IInputHasher 49 | { 50 | public bool ContainsPath(string absolutePath) => hashes.ContainsKey(absolutePath); 51 | 52 | public ValueTask GetHashAsync(string absolutePath) => new ValueTask(hashes.TryGetValue(absolutePath, out byte[]? hash) ? hash : null); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Common.Tests/Hashing/DirectoryFileHasherTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.IO; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using BuildXL.Cache.ContentStore.Hashing; 8 | using Microsoft.MSBuildCache.Hashing; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | namespace Microsoft.MSBuildCache.Tests.Hashing; 12 | 13 | [TestClass] 14 | public class DirectoryFileHasherTests 15 | { 16 | private static readonly IContentHasher ContentHasher = HashInfoLookup.Find(HashType.MD5).CreateContentHasher(); 17 | 18 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 19 | public TestContext TestContext { get; set; } 20 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 21 | 22 | [DataTestMethod] 23 | [DataRow(@"X:\Dir\Foo\1.0.0\lib\Foo.dll", true)] 24 | [DataRow(@"x:\dIR\Foo\1.0.0\lib\Foo.dll", true)] 25 | [DataRow(@"x:\OtherDir\foo.txt", false)] 26 | public void ContainsPath(string path, bool expectedResult) 27 | { 28 | const string directory = @"X:\Dir"; 29 | DirectoryFileHasher hasher = new(directory, ContentHasher); 30 | Assert.AreEqual(expectedResult, hasher.ContainsPath(path)); 31 | } 32 | 33 | [DataTestMethod] 34 | [DataRow(@"Dir\Foo\1.0.0\lib\Foo.dll", true)] 35 | [DataRow(@"dIR\Foo\2.0.0\lib\Foo.dll", true)] 36 | [DataRow(@"OtherDir\foo.txt", false)] 37 | public async Task ComputeHash(string relativePath, bool expectedToHaveHash) 38 | { 39 | string baseDir = CreateTestDirectory(); 40 | string directory = Path.Combine(baseDir, "Dir"); 41 | 42 | DirectoryFileHasher hasher = new(directory, ContentHasher); 43 | 44 | string absolutePath = Path.Combine(baseDir, relativePath); 45 | string fileContent = absolutePath; // Just use the file name itself as the content. 46 | Directory.CreateDirectory(Path.GetDirectoryName(absolutePath)!); 47 | #if NETFRAMEWORK 48 | File.WriteAllText(absolutePath, fileContent); 49 | #else 50 | await File.WriteAllTextAsync(absolutePath, fileContent); 51 | #endif 52 | 53 | byte[]? hash = await hasher.GetHashAsync(absolutePath); 54 | if (expectedToHaveHash) 55 | { 56 | byte[] expectedHash = ContentHasher.GetContentHash(Encoding.Default.GetBytes(fileContent)).ToHashByteArray(); 57 | CollectionAssert.AreEqual(expectedHash, hash); 58 | } 59 | else 60 | { 61 | Assert.IsNull(hash); 62 | } 63 | } 64 | 65 | private string CreateTestDirectory() 66 | { 67 | string testDirectory = Path.Combine(TestContext.TestRunDirectory!, nameof(DirectoryFileHasherTests), TestContext.TestName!); 68 | Directory.CreateDirectory(testDirectory); 69 | return testDirectory; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Common.Tests/Hashing/HashingExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using BuildXL.Cache.ContentStore.Hashing; 6 | using Microsoft.MSBuildCache.Hashing; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | 9 | namespace Microsoft.MSBuildCache.Tests.Hashing; 10 | 11 | [TestClass] 12 | public class HashingExtensionsTests 13 | { 14 | private static readonly IContentHasher ContentHasher = HashInfoLookup.Find(HashType.MD5).CreateContentHasher(); 15 | 16 | [TestMethod] 17 | public void CombineHashes() 18 | { 19 | var hashes = new byte[][] 20 | { 21 | new byte[] { 0x01, 0x02, 0x03 }, 22 | new byte[] { 0x04, 0x05, 0x06 }, 23 | new byte[] { 0x07, 0x08, 0x09 }, 24 | }; 25 | 26 | // This doesn't mean anything to a human; it's just intended to exercise the code 27 | byte[] expectedHash = new byte[] { 0x77, 0x8a, 0xaa, 0x14, 0x80, 0x06, 0x5d, 0xf8, 0x87, 0xe0, 0xab, 0xb5, 0x59, 0xd8, 0x26, 0xc5 }; 28 | CollectionAssert.AreEqual(expectedHash, ContentHasher.CombineHashes(hashes)); 29 | } 30 | 31 | [TestMethod] 32 | public void CombineHashesNullHashes() 33 | { 34 | var hashes = new byte[][] 35 | { 36 | new byte[] { 0x01, 0x02, 0x03 }, 37 | new byte[] { 0x04, 0x05, 0x06 }, 38 | new byte[] { 0x07, 0x08, 0x09 }, 39 | }; 40 | 41 | var hashesWithGaps = new byte[]?[] 42 | { 43 | Array.Empty(), 44 | hashes[0], 45 | null, 46 | hashes[1], 47 | Array.Empty(), 48 | null, 49 | hashes[2], 50 | null, 51 | }; 52 | 53 | CollectionAssert.AreEqual( 54 | ContentHasher.CombineHashes(hashesWithGaps), 55 | ContentHasher.CombineHashes(hashes)); 56 | } 57 | 58 | [TestMethod] 59 | public void CombineNoHashes() 60 | { 61 | Assert.IsNull(ContentHasher.CombineHashes(Array.Empty())); 62 | Assert.IsNull(ContentHasher.CombineHashes(new byte[]?[] { Array.Empty(), null })); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Common.Tests/Hashing/OutputHasherTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using BuildXL.Cache.ContentStore.Hashing; 12 | using Microsoft.MSBuildCache.Hashing; 13 | using Microsoft.VisualStudio.TestTools.UnitTesting; 14 | 15 | namespace Microsoft.MSBuildCache.Tests.Hashing; 16 | 17 | [TestClass] 18 | public class OutputHasherTests 19 | { 20 | private static readonly IContentHasher ContentHasher = HashInfoLookup.Find(HashType.MD5).CreateContentHasher(); 21 | 22 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 23 | public TestContext TestContext { get; set; } 24 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 25 | 26 | [TestMethod] 27 | public async Task ComputeHash() 28 | { 29 | await using OutputHasher hasher = new(ContentHasher); 30 | 31 | string dir = CreateTestDirectory(); 32 | string file = Path.Combine(dir, "file.txt"); 33 | #if NETFRAMEWORK 34 | File.WriteAllText(file, "someContent"); 35 | #else 36 | await File.WriteAllTextAsync(file, "someContent"); 37 | #endif 38 | 39 | ContentHash hash = await hasher.ComputeHashAsync(file, CancellationToken.None); 40 | ContentHash expectedHash = ContentHasher.GetContentHash(Encoding.Default.GetBytes("someContent")); 41 | Assert.AreEqual(expectedHash, hash); 42 | } 43 | 44 | [TestMethod] 45 | public async Task ComputeHashFileNotFound() 46 | { 47 | await using OutputHasher hasher = new(ContentHasher); 48 | 49 | string dir = CreateTestDirectory(); 50 | string file = Path.Combine(dir, "file.txt"); 51 | 52 | // Ensure exceptions are propagated correctly. 53 | await Assert.ThrowsExceptionAsync(async () => await hasher.ComputeHashAsync(file, CancellationToken.None)); 54 | } 55 | 56 | [TestMethod] 57 | public async Task StressTest() 58 | { 59 | const int NumFiles = 1000; 60 | const int NumFileLines = 100; 61 | 62 | await using OutputHasher hasher = new(ContentHasher); 63 | 64 | string dir = CreateTestDirectory(); 65 | 66 | Dictionary files = Enumerable.Range(0, NumFiles) 67 | .Select(i => Path.Combine(dir, $"{i}.txt")) 68 | .ToDictionary( 69 | filePath => filePath, 70 | filePath => 71 | { 72 | StringBuilder sb = new(); 73 | for (int i = 0; i < NumFileLines; i++) 74 | { 75 | sb.AppendLine(filePath); 76 | } 77 | 78 | return sb.ToString(); 79 | }); 80 | 81 | // Write all files to disk 82 | Parallel.ForEach(files, file => File.WriteAllText(file.Key, file.Value)); 83 | 84 | // Hash them all at the same time 85 | ConcurrentDictionary> tasks = new(); 86 | Parallel.ForEach(files, file => tasks.TryAdd(file.Key, hasher.ComputeHashAsync(file.Key, CancellationToken.None))); 87 | 88 | await Task.WhenAll(tasks.Values); 89 | 90 | // Ensure hashing was actually correct 91 | Parallel.ForEach(files, file => 92 | { 93 | ContentHash expectedHash = ContentHasher.GetContentHash(Encoding.Default.GetBytes(file.Value)); 94 | Assert.AreEqual(expectedHash, tasks[file.Key].Result, $"Hash did not match: {file.Key}"); 95 | }); 96 | } 97 | 98 | private string CreateTestDirectory() 99 | { 100 | string testDirectory = Path.Combine(TestContext.TestRunDirectory!, nameof(OutputHasherTests), TestContext.TestName!); 101 | Directory.CreateDirectory(testDirectory); 102 | return testDirectory; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Common.Tests/Hashing/SourceControlFileHasherTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using BuildXL.Cache.ContentStore.Hashing; 8 | using Microsoft.MSBuildCache.Hashing; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | namespace Microsoft.MSBuildCache.Tests.Hashing; 12 | 13 | [TestClass] 14 | public class SourceControlFileHasherTests 15 | { 16 | private static readonly IContentHasher ContentHasher = HashInfoLookup.Find(HashType.MD5).CreateContentHasher(); 17 | 18 | private static readonly PathNormalizer PathNormalizer = new PathNormalizer(@"X:\Repo", @"X:\Nuget"); 19 | 20 | [TestMethod] 21 | public void ContainsPath() 22 | { 23 | Dictionary fileHashes = new(StringComparer.OrdinalIgnoreCase) 24 | { 25 | { @"X:\Repo\1.txt", new byte[] { 0x01, 0x02, 0x03 } }, 26 | { @"X:\Repo\2.txt", new byte[] { 0x04, 0x05, 0x06 } }, 27 | { @"X:\Repo\3.txt", new byte[] { 0x07, 0x08, 0x09 } }, 28 | }; 29 | SourceControlFileHasher hasher = new(ContentHasher, PathNormalizer, fileHashes); 30 | 31 | Assert.IsTrue(hasher.ContainsPath(@"X:\Repo\1.txt")); 32 | Assert.IsTrue(hasher.ContainsPath(@"X:\Repo\2.txt")); 33 | Assert.IsTrue(hasher.ContainsPath(@"X:\Repo\3.txt")); 34 | 35 | Assert.IsFalse(hasher.ContainsPath(@"X:\Repo\4.txt")); 36 | Assert.IsFalse(hasher.ContainsPath(@"X:\Repo\5.txt")); 37 | Assert.IsFalse(hasher.ContainsPath(@"X:\Repo\6.txt")); 38 | 39 | // Case doesn't matter 40 | Assert.IsTrue(hasher.ContainsPath(@"X:\Repo\1.Txt")); 41 | Assert.IsTrue(hasher.ContainsPath(@"X:\Repo\2.tXt")); 42 | Assert.IsTrue(hasher.ContainsPath(@"X:\Repo\3.txT")); 43 | } 44 | 45 | [TestMethod] 46 | public async Task GetHash() 47 | { 48 | Dictionary fileHashes = new(StringComparer.OrdinalIgnoreCase) 49 | { 50 | { @"X:\Repo\1.txt", new byte[] { 0x01, 0x02, 0x03 } }, 51 | { @"X:\Repo\2.txt", new byte[] { 0x04, 0x05, 0x06 } }, 52 | { @"X:\Repo\3.txt", new byte[] { 0x07, 0x08, 0x09 } }, 53 | }; 54 | SourceControlFileHasher hasher = new(ContentHasher, PathNormalizer, fileHashes); 55 | 56 | Assert.IsNotNull(await hasher.GetHashAsync(@"X:\Repo\1.txt")); 57 | Assert.IsNotNull(await hasher.GetHashAsync(@"X:\Repo\2.txt")); 58 | Assert.IsNotNull(await hasher.GetHashAsync(@"X:\Repo\3.txt")); 59 | 60 | Assert.IsNull(await hasher.GetHashAsync(@"X:\Repo\4.txt")); 61 | Assert.IsNull(await hasher.GetHashAsync(@"X:\Repo\5.txt")); 62 | Assert.IsNull(await hasher.GetHashAsync(@"X:\Repo\6.txt")); 63 | 64 | foreach (KeyValuePair kvp in fileHashes) 65 | { 66 | string file = kvp.Key; 67 | byte[] fileHash = kvp.Value; 68 | 69 | // The hash should not equal the content hash of the file. It should tak the file path into account too. 70 | CollectionAssert.AreNotEqual(fileHash, await hasher.GetHashAsync(file)); 71 | 72 | foreach (string otherFile in fileHashes.Keys) 73 | { 74 | if (file == otherFile) 75 | { 76 | CollectionAssert.AreEqual(await hasher.GetHashAsync(file), await hasher.GetHashAsync(otherFile)); 77 | } 78 | else 79 | { 80 | CollectionAssert.AreNotEqual(await hasher.GetHashAsync(file), await hasher.GetHashAsync(otherFile)); 81 | } 82 | } 83 | } 84 | 85 | CollectionAssert.AreNotEqual(await hasher.GetHashAsync(@"X:\Repo\1.txt"), await hasher.GetHashAsync(@"X:\Repo\2.txt")); 86 | CollectionAssert.AreNotEqual(await hasher.GetHashAsync(@"X:\Repo\1.txt"), await hasher.GetHashAsync(@"X:\Repo\3.txt")); 87 | 88 | // Case doesn't matter 89 | CollectionAssert.AreEqual(await GetHashFreshHasherAsync(@"X:\Repo\1.txt"), await GetHashFreshHasherAsync(@"X:\Repo\1.Txt")); 90 | CollectionAssert.AreEqual(await GetHashFreshHasherAsync(@"X:\Repo\2.txt"), await GetHashFreshHasherAsync(@"X:\Repo\2.tXt")); 91 | CollectionAssert.AreEqual(await GetHashFreshHasherAsync(@"X:\Repo\3.txt"), await GetHashFreshHasherAsync(@"X:\Repo\3.txT")); 92 | 93 | // Using a new hasher to avoid caching 94 | ValueTask GetHashFreshHasherAsync(string relativePath) => new SourceControlFileHasher(ContentHasher, PathNormalizer, fileHashes).GetHashAsync(relativePath); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Common.Tests/HexUtilitiesTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | namespace Microsoft.MSBuildCache.Tests; 9 | 10 | [TestClass] 11 | public class HexUtilitiesTests 12 | { 13 | [DataTestMethod] 14 | [DataRow("0123456789ABCDEFabcdef", new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xAB, 0xCD, 0xEF, })] 15 | [DataRow("", null)] 16 | [DataRow(null, null)] 17 | // 0x prefix. 18 | [DataRow("0x1234", new byte[] { 0x12, 0x34 })] 19 | // Whitespace - should be trimmed. 20 | [DataRow(" ABCD\t\t", new byte[] { 0xAB, 0xCD })] 21 | // Whitespace + 0x 22 | [DataRow(" 0x9876", new byte[] { 0x98, 0x76 })] 23 | public void HexToBytes(string hex, byte[]? expectedBytes) 24 | { 25 | byte[] bytes = HexUtilities.HexToBytes(hex); 26 | 27 | expectedBytes ??= Array.Empty(); 28 | 29 | Assert.AreEqual(expectedBytes.Length, bytes.Length); 30 | for (int i = 0; i < expectedBytes.Length; i++) 31 | { 32 | Assert.AreEqual(expectedBytes[i], bytes[i], "Index {0}", i); 33 | } 34 | } 35 | 36 | [TestMethod] 37 | public void HexToBytesOddChars() => Assert.ThrowsException(() => HexUtilities.HexToBytes("fAbCd")); 38 | 39 | [TestMethod] 40 | public void HexToBytesBadChars() 41 | { 42 | const string goodChars = "0123456789ABCDEFabcdef"; 43 | 44 | var badCharactersMistakenlyAllowed = new List(); 45 | 46 | for (char c = '!'; c <= '~'; c++) 47 | { 48 | #if NETFRAMEWORK 49 | if (!goodChars.Contains(c)) 50 | #else 51 | if (!goodChars.Contains(c, StringComparison.Ordinal)) 52 | #endif 53 | { 54 | try 55 | { 56 | HexUtilities.HexToBytes(new string(new[] { c, c })); 57 | 58 | // Should not get here. 59 | badCharactersMistakenlyAllowed.Add(c); 60 | } 61 | catch (ArgumentException) 62 | { 63 | } 64 | } 65 | } 66 | 67 | Assert.AreEqual(0, badCharactersMistakenlyAllowed.Count, "Bad characters were allowed that should not have been: " + string.Concat(badCharactersMistakenlyAllowed)); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Common.Tests/Microsoft.MSBuildCache.Common.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | x64 5 | $(Platform) 6 | net472;net8.0 7 | Microsoft.MSBuildCache.Tests 8 | 9 | $(NoWarn);CA1861 10 | 11 | $(NoWarn);CA1034 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Common.Tests/Mocks/MockPluginLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Microsoft.Build.Experimental.ProjectCache; 7 | using Microsoft.Build.Framework; 8 | 9 | namespace Microsoft.MSBuildCache.Tests.Mocks; 10 | 11 | internal enum PluginLogLevel { Message, Warning, Error }; 12 | 13 | internal readonly record struct PluginLogEntry(PluginLogLevel LogLevel, string Message); 14 | 15 | internal sealed class MockPluginLogger : PluginLoggerBase 16 | { 17 | private readonly List _logEntries = new(); 18 | 19 | public IReadOnlyList LogEntries => _logEntries; 20 | 21 | public override bool HasLoggedErrors 22 | { 23 | get => _logEntries.Any(entry => entry.LogLevel == PluginLogLevel.Error); 24 | protected set { } 25 | } 26 | 27 | public override void LogError(string error) => _logEntries.Add(new PluginLogEntry(PluginLogLevel.Error, error)); 28 | 29 | public override void LogMessage(string message, MessageImportance? messageImportance = null) => _logEntries.Add(new PluginLogEntry(PluginLogLevel.Message, message)); 30 | 31 | public override void LogWarning(string warning) => _logEntries.Add(new PluginLogEntry(PluginLogLevel.Warning, warning)); 32 | } 33 | -------------------------------------------------------------------------------- /src/Common.Tests/Mocks/NullPluginLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Build.Experimental.ProjectCache; 5 | using Microsoft.Build.Framework; 6 | 7 | namespace Microsoft.MSBuildCache.Tests.Mocks; 8 | 9 | internal sealed class NullPluginLogger : PluginLoggerBase 10 | { 11 | private NullPluginLogger() : base() 12 | { 13 | } 14 | 15 | public static NullPluginLogger Instance { get; } = new NullPluginLogger(); 16 | 17 | public override bool HasLoggedErrors 18 | { 19 | get => false; 20 | protected set { } 21 | } 22 | 23 | public override void LogError(string error) 24 | { 25 | } 26 | 27 | public override void LogMessage(string message, MessageImportance? messageImportance = null) 28 | { 29 | } 30 | 31 | public override void LogWarning(string warning) 32 | { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Common.Tests/NodeBuildResultTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using BuildXL.Cache.ContentStore.Hashing; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using MoreLinq; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text.Json; 11 | 12 | namespace Microsoft.MSBuildCache.Tests; 13 | 14 | [TestClass] 15 | public class NodeBuildResultTests 16 | { 17 | private static readonly Dictionary Outputs = new Dictionary { 18 | {"Lib-link.write.1.tlog", ContentHash.Random()}, 19 | {"Lib.command.1.tlog", ContentHash.Random()}, 20 | {"logger.lastbuildstate", ContentHash.Random()}, 21 | }; 22 | 23 | [TestMethod] 24 | public void SortWorksConsistently() 25 | { 26 | List names = Outputs.Keys.ToList(); 27 | var baseline = new SortedSet(names, StringComparer.OrdinalIgnoreCase); 28 | foreach (IList permutation in names.Permutations()) 29 | { 30 | var ordinal_sorted = new SortedSet(permutation, StringComparer.OrdinalIgnoreCase); 31 | CollectionAssert.AreEqual(baseline, ordinal_sorted); 32 | } 33 | } 34 | 35 | [TestMethod] 36 | public void SortWorksConsistentlyAcrossJson() 37 | { 38 | List names = Outputs.Keys.ToList(); 39 | var expected = new SortedDictionary(Outputs, StringComparer.OrdinalIgnoreCase); 40 | 41 | foreach (IList permutation in names.Permutations()) 42 | { 43 | var maybeMixed = new SortedDictionary( 44 | permutation.ToDictionary(name => name, name => Outputs[name])); 45 | 46 | NodeBuildResult nodeBuildResult = new( 47 | maybeMixed, 48 | new SortedDictionary(), 49 | new List(), 50 | DateTime.UtcNow, 51 | DateTime.UtcNow, 52 | null 53 | ); 54 | 55 | string serialized = JsonSerializer.Serialize(nodeBuildResult, SourceGenerationContext.Default.NodeBuildResult); 56 | NodeBuildResult deserialized = JsonSerializer.Deserialize(serialized, SourceGenerationContext.Default.NodeBuildResult)!; 57 | 58 | CollectionAssert.AreEqual(expected.Keys, deserialized.Outputs.Keys, "\n" + 59 | "Permutation: " + string.Join(", ", permutation) + "\n" + 60 | "Serialized: " + serialized + "\n" + 61 | "Deserialized: " + string.Join(", ", deserialized.Outputs.Keys) + "\n" + 62 | "Expected: " + string.Join(", ", permutation) + "\n\n"); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/Common.Tests/NodeTargetResultTaskItemTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Build.Framework; 4 | using Microsoft.Build.Utilities; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | namespace Microsoft.MSBuildCache.Tests; 8 | 9 | [TestClass] 10 | public class NodeTargetResultTaskItemTests 11 | { 12 | private const string RepoRoot = @"X:\Repo"; 13 | 14 | private const string NugetPackageRoot = @"X:\Nuget"; 15 | 16 | private static readonly PathNormalizer PathNormalizer = new(RepoRoot, NugetPackageRoot); 17 | 18 | [TestMethod] 19 | public void FromTaskItem() 20 | { 21 | TaskItem taskItem = new( 22 | RepoRoot + @"\src\HelloWorld\bin\x64\Release\HelloWorld.dll", 23 | new Dictionary(StringComparer.OrdinalIgnoreCase) 24 | { 25 | { "TargetFrameworkIdentifier", ".NETStandard" }, 26 | { "TargetPlatformMoniker", "Windows,Version=7.0" }, 27 | { "CopyUpToDateMarker", RepoRoot + @"\src\HelloWorld\bin\x64\Release\HelloWorld.csproj.CopyComplete" }, 28 | { "TargetPlatformIdentifier", "Windows" }, 29 | { "TargetFrameworkVersion", "2.0" }, 30 | { "ReferenceAssembly", RepoRoot + @"\src\HelloWorld\bin\x64\Release\ref\HelloWorld.dll" }, 31 | }); 32 | 33 | NodeTargetResultTaskItem nodeTargetResultTaskItem = NodeTargetResultTaskItem.FromTaskItem(taskItem, PathNormalizer); 34 | 35 | Assert.IsNotNull(nodeTargetResultTaskItem); 36 | Assert.AreEqual(@"{RepoRoot}src\HelloWorld\bin\x64\Release\HelloWorld.dll", nodeTargetResultTaskItem.ItemSpec); 37 | Assert.AreEqual(taskItem.CloneCustomMetadata().Count, nodeTargetResultTaskItem.Metadata.Count); 38 | Assert.AreEqual(".NETStandard", nodeTargetResultTaskItem.Metadata["TargetFrameworkIdentifier"]); 39 | Assert.AreEqual("Windows,Version=7.0", nodeTargetResultTaskItem.Metadata["TargetPlatformMoniker"]); 40 | Assert.AreEqual(@"{RepoRoot}src\HelloWorld\bin\x64\Release\HelloWorld.csproj.CopyComplete", nodeTargetResultTaskItem.Metadata["CopyUpToDateMarker"]); 41 | Assert.AreEqual("Windows", nodeTargetResultTaskItem.Metadata["TargetPlatformIdentifier"]); 42 | Assert.AreEqual("2.0", nodeTargetResultTaskItem.Metadata["TargetFrameworkVersion"]); 43 | Assert.AreEqual(@"{RepoRoot}src\HelloWorld\bin\x64\Release\ref\HelloWorld.dll", nodeTargetResultTaskItem.Metadata["ReferenceAssembly"]); 44 | } 45 | 46 | [TestMethod] 47 | public void ToTaskItem() 48 | { 49 | NodeTargetResultTaskItem nodeTargetResultTaskItem = new( 50 | @"{RepoRoot}src\HelloWorld\bin\x64\Release\HelloWorld.dll", 51 | new Dictionary(StringComparer.OrdinalIgnoreCase) 52 | { 53 | { "TargetFrameworkIdentifier", ".NETStandard" }, 54 | { "TargetPlatformMoniker", "Windows,Version=7.0" }, 55 | { "CopyUpToDateMarker", @"{RepoRoot}src\HelloWorld\bin\x64\Release\HelloWorld.csproj.CopyComplete" }, 56 | { "TargetPlatformIdentifier", "Windows" }, 57 | { "TargetFrameworkVersion", "2.0" }, 58 | { "ReferenceAssembly", @"{RepoRoot}src\HelloWorld\bin\x64\Release\ref\HelloWorld.dll" }, 59 | }); 60 | 61 | ITaskItem2 taskItem = nodeTargetResultTaskItem.ToTaskItem(PathNormalizer); 62 | 63 | Assert.IsNotNull(taskItem); 64 | Assert.AreEqual(RepoRoot + @"\src\HelloWorld\bin\x64\Release\HelloWorld.dll", taskItem.ItemSpec); 65 | Assert.AreEqual(nodeTargetResultTaskItem.Metadata.Count, taskItem.CloneCustomMetadata().Count); 66 | Assert.AreEqual(".NETStandard", taskItem.GetMetadata("TargetFrameworkIdentifier")); 67 | Assert.AreEqual("Windows,Version=7.0", taskItem.GetMetadata("TargetPlatformMoniker")); 68 | Assert.AreEqual(RepoRoot + @"\src\HelloWorld\bin\x64\Release\HelloWorld.csproj.CopyComplete", taskItem.GetMetadata("CopyUpToDateMarker")); 69 | Assert.AreEqual("Windows", taskItem.GetMetadata("TargetPlatformIdentifier")); 70 | Assert.AreEqual("2.0", taskItem.GetMetadata("TargetFrameworkVersion")); 71 | Assert.AreEqual(RepoRoot + @"\src\HelloWorld\bin\x64\Release\ref\HelloWorld.dll", taskItem.GetMetadata("ReferenceAssembly")); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Common.Tests/PathHelperTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace Microsoft.MSBuildCache.Tests; 7 | 8 | [TestClass] 9 | public class PathHelperTests 10 | { 11 | [DataTestMethod] 12 | [DataRow(@"X:\A\B\C", @"X:\A", @"B\C")] 13 | // Lots of .. and . 14 | [DataRow(@"X:\Z\..\A\B\.\C", @"X:\Y\..\D\..\A\.\.", @"B\C")] 15 | // Equal paths 16 | [DataRow(@"X:\A\B\C", @"X:\A\B\C", @"")] 17 | // Drive root 18 | [DataRow(@"X:\A", @"X:\", @"A")] 19 | // Not relative to the base 20 | [DataRow(@"X:\D\E\F", @"X:\A\B\D", null)] 21 | // Different drives 22 | [DataRow(@"X:\A", @"Y:\", null)] 23 | // Trailing slashes 24 | [DataRow(@"X:\A\B\C\", @"X:\A\", @"B\C\")] 25 | [DataRow(@"X:\A\B\C\", @"X:\A", @"B\C\")] 26 | [DataRow(@"X:\A\B\C", @"X:\A\", @"B\C")] 27 | public void MakePathRelative(string path, string basePath, string? expectedResult) 28 | => Assert.AreEqual(expectedResult, path.MakePathRelativeTo(basePath)); 29 | 30 | [DataTestMethod] 31 | [DataRow(@"X:\A\B\C\file.txt", @"X:\A", true)] 32 | // Lots of .. and . 33 | [DataRow(@"X:\Z\..\A\B\.\C\file.txt", @"X:\Y\..\D\..\A\.\.", true)] 34 | // Drive root 35 | [DataRow(@"X:\A\file.txt", @"X:\", true)] 36 | // Not under the dir 37 | [DataRow(@"X:\D\E\F\file.txt", @"X:\A\B\D", false)] 38 | // Different drives 39 | [DataRow(@"X:\A", @"Y:\", false)] 40 | // Trailing slash 41 | [DataRow(@"X:\A\B\C\file.txt", @"X:\A\B\C\", true)] 42 | public void IsUnderDirectory(string filePath, string directoryPath, bool expectedResult) 43 | => Assert.AreEqual(expectedResult, filePath.IsUnderDirectory(directoryPath)); 44 | } 45 | -------------------------------------------------------------------------------- /src/Common.Tests/PluginInterfaceTypeCheckTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Reflection; 8 | using Microsoft.Build.Experimental.ProjectCache; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | namespace Microsoft.MSBuildCache.Tests; 12 | 13 | // Make sure that types used by the plugin interface are limited to the assemblies we expect 14 | [TestClass] 15 | public class PluginInterfaceTypeCheckTests 16 | { 17 | public static readonly HashSet PluginInterfaceNuGetAssemblies = new HashSet() 18 | { 19 | // specific CODESYNC[DO_NOT_ILMERGE_ASSEMBLIES] 20 | "Microsoft.Build.dll", 21 | "Microsoft.Build.Framework.dll", 22 | "Microsoft.Build.Utilities.Core.dll", 23 | "System.Collections.Immutable.dll", 24 | }; 25 | 26 | private static readonly HashSet PluginInterfaceAssemblies = new HashSet(PluginInterfaceNuGetAssemblies) 27 | { 28 | // general 29 | "mscorlib.dll", 30 | "System.Private.CoreLib.dll", 31 | "System.Core.dll", 32 | }; 33 | 34 | [TestMethod] 35 | public void ProjectCachePluginBase() 36 | { 37 | CheckAssembliesForType(typeof(ProjectCachePluginBase)); 38 | } 39 | 40 | private static void AssertAssembly(Type t) 41 | { 42 | Assert.IsTrue(PluginInterfaceAssemblies.Contains(Path.GetFileName(t.Assembly.Location)), 43 | $"Type {t.FullName} is in assembly {t.Assembly.Location} which is not expected"); 44 | } 45 | 46 | private static void CheckAssembliesForType(Type t) 47 | { 48 | var alreadyChecked = new HashSet(); 49 | CheckAssemblies(t, alreadyChecked, 5); 50 | Assert.IsTrue(alreadyChecked.Count > 10, "Failed to find types."); 51 | } 52 | 53 | private static void CheckAssemblies(Type t, HashSet alreadyChecked, int depth) 54 | { 55 | if (depth <= 0 || t == null || !alreadyChecked.Add(t)) 56 | { 57 | return; 58 | } 59 | 60 | if (t.FullName == null) 61 | { 62 | return; 63 | } 64 | 65 | AssertAssembly(t); 66 | foreach (Type nested in t.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) 67 | { 68 | CheckAssemblies(nested, alreadyChecked, depth - 1); 69 | } 70 | 71 | foreach (PropertyInfo p in t.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) 72 | { 73 | CheckAssemblies(p.PropertyType, alreadyChecked, depth - 1); 74 | } 75 | 76 | foreach (MethodInfo? m in t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) 77 | { 78 | CheckAssemblies(m.ReturnType, alreadyChecked, depth - 1); 79 | foreach (var p in m.GetParameters()) 80 | { 81 | CheckAssemblies(p.ParameterType, alreadyChecked, depth - 1); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Common/Caching/CacheException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.MSBuildCache.Caching; 7 | 8 | public sealed class CacheException : Exception 9 | { 10 | public CacheException() 11 | { 12 | } 13 | 14 | public CacheException(string message) 15 | : base(message) 16 | { 17 | } 18 | 19 | public CacheException(string message, Exception innerException) 20 | : base(message, innerException) 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Common/Caching/ICacheClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BuildXL.Cache.ContentStore.Hashing; 9 | using Microsoft.MSBuildCache.Fingerprinting; 10 | 11 | namespace Microsoft.MSBuildCache.Caching; 12 | 13 | public interface ICacheClient : IAsyncDisposable 14 | { 15 | Task AddNodeAsync( 16 | NodeContext nodeContext, 17 | PathSet? pathSet, 18 | IReadOnlyCollection outputPaths, 19 | Func, NodeBuildResult> nodeBuildResultBuilder, 20 | CancellationToken cancellationToken); 21 | 22 | Task<(PathSet?, NodeBuildResult?)> GetNodeAsync(NodeContext nodeContext, bool materializeOutputs, CancellationToken cancellationToken); 23 | 24 | Task ShutdownAsync(CancellationToken cancellationToken); 25 | } 26 | -------------------------------------------------------------------------------- /src/Common/Caching/LocalCacheFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using BuildXL.Cache.ContentStore.Distributed.NuCache; 6 | using BuildXL.Cache.ContentStore.Interfaces.FileSystem; 7 | using BuildXL.Cache.ContentStore.Interfaces.Logging; 8 | using BuildXL.Cache.ContentStore.Stores; 9 | using BuildXL.Cache.MemoizationStore.Sessions; 10 | using BuildXL.Cache.MemoizationStore.Stores; 11 | 12 | namespace Microsoft.MSBuildCache.Caching; 13 | 14 | public static class LocalCacheFactory 15 | { 16 | public static LocalCache Create(ILogger logger, string cacheRootPath, uint localCacheSizeInMegabytes) 17 | { 18 | AbsolutePath rootPath = new(cacheRootPath); 19 | 20 | // Note: this only works in x64 processes, which this might not be... 21 | RocksDbMemoizationStoreConfiguration rocksDbMemoizationStoreConfiguration = new() 22 | { 23 | Database = new RocksDbContentLocationDatabaseConfiguration(rootPath / "RocksDbMemoizationStore") 24 | { 25 | CleanOnInitialize = false, 26 | GarbageCollectionInterval = TimeSpan.FromHours(1), 27 | OnFailureDeleteExistingStoreAndRetry = true, 28 | }, 29 | }; 30 | 31 | ContentStoreConfiguration contentStoreConfiguration = ContentStoreConfiguration.CreateWithMaxSizeQuotaMB(localCacheSizeInMegabytes); 32 | 33 | return LocalCache.CreateUnknownContentStoreInProcMemoizationStoreCache( 34 | logger, 35 | rootPath, 36 | rocksDbMemoizationStoreConfiguration, 37 | LocalCacheConfiguration.CreateServerDisabled(), 38 | new ConfigurationModel(contentStoreConfiguration), 39 | assumeCallerCreatesDirectoryForPlace: true); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Common/Caching/LocalCacheStateManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Text.Json; 8 | using System.Threading.Tasks; 9 | using BuildXL.Cache.ContentStore.Hashing; 10 | using BuildXL.Cache.ContentStore.Interfaces.Tracing; 11 | using BuildXL.Cache.ContentStore.Tracing; 12 | 13 | namespace Microsoft.MSBuildCache.Caching; 14 | 15 | internal sealed record LocalCacheStateEntry(string Hash, long LastWriteTime, long FileSize); 16 | 17 | internal sealed record LocalCacheStateFile(Dictionary Files); 18 | 19 | internal sealed class LocalCacheStateManager 20 | { 21 | private const string CacheStateDirName = ".msbuildcache"; 22 | 23 | private readonly Tracer _tracer = new(nameof(LocalCacheStateManager)); 24 | private readonly string _repoRoot; 25 | private readonly string _cacheStateDir; 26 | 27 | public LocalCacheStateManager(string repoRoot) 28 | { 29 | _repoRoot = repoRoot; 30 | _cacheStateDir = Path.Combine(repoRoot, CacheStateDirName); 31 | } 32 | 33 | internal async Task WriteStateFileAsync( 34 | NodeContext nodeContext, 35 | NodeBuildResult nodeBuildResult) 36 | { 37 | Dictionary files = new(StringComparer.OrdinalIgnoreCase); 38 | foreach (KeyValuePair kvp in nodeBuildResult.Outputs) 39 | { 40 | string relativeFilePath = kvp.Key; 41 | ContentHash contentHash = kvp.Value; 42 | FileInfo fileInfo = new(Path.Combine(_repoRoot, relativeFilePath)); 43 | files[relativeFilePath] = new LocalCacheStateEntry(contentHash.ToShortString(), fileInfo.LastWriteTimeUtc.Ticks, fileInfo.Length); 44 | } 45 | 46 | Directory.CreateDirectory(_cacheStateDir); 47 | 48 | string stateFilePath = Path.Combine(_cacheStateDir, nodeContext.Id + ".json"); 49 | LocalCacheStateFile stateFile = new(files); 50 | 51 | using FileStream fileStream = File.Create(stateFilePath); 52 | await JsonSerializer.SerializeAsync(fileStream, stateFile, SourceGenerationContext.Default.LocalCacheStateFile); 53 | } 54 | 55 | internal async Task>> GetOutOfDateFilesAsync( 56 | Context context, 57 | NodeContext nodeContext, 58 | NodeBuildResult nodeBuildResult) 59 | { 60 | string stateFilePath = Path.Combine(_repoRoot, CacheStateDirName, nodeContext.Id + ".json"); 61 | 62 | LocalCacheStateFile? depFile = null; 63 | if (File.Exists(stateFilePath)) 64 | { 65 | try 66 | { 67 | using FileStream fileStream = File.OpenRead(stateFilePath); 68 | depFile = await JsonSerializer.DeserializeAsync(fileStream, SourceGenerationContext.Default.LocalCacheStateFile); 69 | } 70 | catch (JsonException ex) 71 | { 72 | _tracer.Debug(context, $"Error reading local cache state for node {nodeContext.Id}. {ex.Message}"); 73 | 74 | File.Delete(stateFilePath); 75 | depFile = null; 76 | } 77 | } 78 | else 79 | { 80 | _tracer.Debug(context, $"Local cache state for build target {nodeContext.Id} did not exist."); 81 | } 82 | 83 | if (depFile == null) 84 | { 85 | _tracer.Debug(context, "Considering all output files out of date."); 86 | } 87 | 88 | List> outOfDateFiles = new(nodeBuildResult.Outputs.Count); 89 | foreach (KeyValuePair kvp in nodeBuildResult.Outputs) 90 | { 91 | string relativeFilePath = kvp.Key; 92 | ContentHash contentHash = kvp.Value; 93 | if (!IsFileUpToDate(context, depFile, relativeFilePath, contentHash)) 94 | { 95 | outOfDateFiles.Add(kvp); 96 | } 97 | } 98 | 99 | return outOfDateFiles; 100 | } 101 | 102 | private bool IsFileUpToDate(Context context, LocalCacheStateFile? depFile, string relativeFilePath, ContentHash expectedHash) 103 | { 104 | if (depFile == null) 105 | { 106 | return false; 107 | } 108 | 109 | if (!depFile.Files.TryGetValue(relativeFilePath, out LocalCacheStateEntry? cachedInfo)) 110 | { 111 | _tracer.Debug(context, $"File {relativeFilePath} was out of date. It was missing from the state file."); 112 | return false; 113 | } 114 | 115 | if (!expectedHash.ToShortString().Equals(cachedInfo.Hash, StringComparison.OrdinalIgnoreCase)) 116 | { 117 | _tracer.Debug(context, $"File {relativeFilePath} was out of date. The hash did not match."); 118 | return false; 119 | } 120 | 121 | FileInfo fileInfo = new(Path.Combine(_repoRoot, relativeFilePath)); 122 | if (!fileInfo.Exists) 123 | { 124 | _tracer.Debug(context, $"File {relativeFilePath} was out of date. The file does not exist."); 125 | return false; 126 | } 127 | 128 | if (cachedInfo.LastWriteTime != fileInfo.LastWriteTimeUtc.Ticks) 129 | { 130 | _tracer.Debug(context, $"File {relativeFilePath} was out of date. The timestamp did not match."); 131 | return false; 132 | } 133 | 134 | if (cachedInfo.FileSize != fileInfo.Length) 135 | { 136 | _tracer.Debug(context, $"File {relativeFilePath} was out of date. The file size did not match."); 137 | return false; 138 | } 139 | 140 | _tracer.Debug(context, $"Skipping unchanged file: {relativeFilePath}"); 141 | return true; 142 | } 143 | } -------------------------------------------------------------------------------- /src/Common/ContentHashJsonConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | using BuildXL.Cache.ContentStore.Hashing; 8 | 9 | namespace Microsoft.MSBuildCache; 10 | 11 | internal sealed class ContentHashJsonConverter : JsonConverter 12 | { 13 | public override ContentHash Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 14 | => new ContentHash(reader.GetString()!); 15 | 16 | public override void Write(Utf8JsonWriter writer, ContentHash value, JsonSerializerOptions options) 17 | => writer.WriteStringValue(value.Serialize()); 18 | } 19 | -------------------------------------------------------------------------------- /src/Common/DirectoryLock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.IO; 6 | using Microsoft.Build.Experimental.ProjectCache; 7 | using Microsoft.Build.Framework; 8 | 9 | namespace Microsoft.MSBuildCache; 10 | 11 | /// 12 | /// This is loosely based on BXL's DirectoryLockFile, but with a fail-fast mode of operation. 13 | /// 14 | internal sealed class DirectoryLock : IDisposable 15 | { 16 | private readonly string _lockFilePath; 17 | private readonly PluginLoggerBase _logger; 18 | private Stream? _lockFile; 19 | 20 | public DirectoryLock(string lockFilePath, PluginLoggerBase logger) 21 | { 22 | _lockFilePath = lockFilePath; 23 | _logger = logger; 24 | } 25 | 26 | public bool Acquire() 27 | { 28 | if (_lockFile != null) 29 | { 30 | _logger.LogMessage($"Lock file already held=[{_lockFilePath}]", MessageImportance.Low); 31 | return true; 32 | } 33 | 34 | _logger.LogMessage($"Acquiring lock file=[{_lockFilePath}]", MessageImportance.Low); 35 | Directory.CreateDirectory(Path.GetDirectoryName(_lockFilePath)!); 36 | 37 | try 38 | { 39 | _lockFile = File.Open(_lockFilePath, FileMode.OpenOrCreate, System.IO.FileAccess.Write, FileShare.Read); 40 | _logger.LogMessage($"Acquired lock file=[{_lockFilePath}]", MessageImportance.Low); 41 | return true; 42 | } 43 | catch (IOException) 44 | { 45 | // Swallow, someone else is holding the lock 46 | } 47 | catch (UnauthorizedAccessException) 48 | { 49 | // Swallow, someone else is holding the lock 50 | } 51 | 52 | return false; 53 | } 54 | 55 | /// 56 | public void Dispose() 57 | { 58 | if (_lockFile != null) 59 | { 60 | _lockFile.Dispose(); 61 | _lockFile = null; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Common/FileAccess/FileAccesses.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.MSBuildCache.FileAccess; 7 | 8 | internal sealed class FileAccesses 9 | { 10 | public FileAccesses(IReadOnlyCollection inputs, IReadOnlyCollection outputs) 11 | { 12 | Inputs = inputs; 13 | Outputs = outputs; 14 | } 15 | 16 | public IReadOnlyCollection Inputs { get; } 17 | 18 | public IReadOnlyCollection Outputs { get; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Common/Fingerprinting/IFingerprintFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | 7 | namespace Microsoft.MSBuildCache.Fingerprinting; 8 | 9 | public interface IFingerprintFactory 10 | { 11 | Task GetWeakFingerprintAsync(NodeContext nodeContext); 12 | 13 | PathSet? GetPathSet(NodeContext nodeContext, IEnumerable observedInputs); 14 | 15 | Task GetStrongFingerprintAsync(PathSet? pathSet); 16 | } 17 | -------------------------------------------------------------------------------- /src/Common/Fingerprinting/PathSet.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace Microsoft.MSBuildCache.Fingerprinting; 8 | 9 | public sealed class PathSet : IEquatable 10 | { 11 | public PathSet(IReadOnlyList filesRead) 12 | { 13 | FilesRead = filesRead; 14 | } 15 | 16 | /// 17 | /// Gets the set of files read which were not predicted. These paths are normalized. 18 | /// 19 | public IReadOnlyList FilesRead { get; } 20 | 21 | public bool Equals(PathSet? other) 22 | { 23 | if (ReferenceEquals(this, other)) 24 | { 25 | return true; 26 | } 27 | 28 | if (other is null) 29 | { 30 | return false; 31 | } 32 | 33 | if (FilesRead.Count != other.FilesRead.Count) 34 | { 35 | return false; 36 | } 37 | 38 | for (int i = 0; i < FilesRead.Count; i++) 39 | { 40 | if (!FilesRead[i].Equals(other.FilesRead[i], StringComparison.OrdinalIgnoreCase)) 41 | { 42 | return false; 43 | } 44 | } 45 | 46 | return true; 47 | } 48 | 49 | public override bool Equals(object? obj) => Equals(obj as PathSet); 50 | 51 | public override int GetHashCode() 52 | { 53 | var hashCode = default(HashCode); 54 | foreach (string file in FilesRead) 55 | { 56 | hashCode.Add(file, StringComparer.OrdinalIgnoreCase); 57 | } 58 | 59 | return hashCode.ToHashCode(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Common/Hashing/CompositeInputHasher.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Threading.Tasks; 5 | 6 | namespace Microsoft.MSBuildCache.Hashing; 7 | 8 | /// 9 | /// Represents a hasher which combines multiple hasher implementations together. 10 | /// 11 | internal sealed class CompositeInputHasher : IInputHasher 12 | { 13 | private readonly IInputHasher[] _inputHashers; 14 | 15 | public CompositeInputHasher(IInputHasher[] inputHashers) 16 | { 17 | _inputHashers = inputHashers; 18 | } 19 | 20 | public bool ContainsPath(string absolutePath) 21 | { 22 | foreach (IInputHasher inputHasher in _inputHashers) 23 | { 24 | if (inputHasher.ContainsPath(absolutePath)) 25 | { 26 | return true; 27 | } 28 | } 29 | 30 | return false; 31 | } 32 | 33 | public async ValueTask GetHashAsync(string absolutePath) 34 | { 35 | foreach (IInputHasher inputHasher in _inputHashers) 36 | { 37 | byte[]? hash = await inputHasher.GetHashAsync(absolutePath); 38 | if (hash != null) 39 | { 40 | return hash; 41 | } 42 | } 43 | 44 | return null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Common/Hashing/DirectoryFileHasher.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.IO; 7 | using System.Threading.Tasks; 8 | using BuildXL.Cache.ContentStore.Hashing; 9 | 10 | namespace Microsoft.MSBuildCache.Hashing; 11 | 12 | /// 13 | /// Represents a hasher implementation which hashes files under a particular directory. 14 | /// 15 | internal sealed class DirectoryFileHasher : IInputHasher 16 | { 17 | private readonly ConcurrentDictionary>> _cachedHashes = new(StringComparer.OrdinalIgnoreCase); 18 | 19 | private readonly string _directory; 20 | 21 | private readonly IContentHasher _contentHasher; 22 | 23 | public DirectoryFileHasher(string directory, IContentHasher contentHasher) 24 | { 25 | _directory = directory; 26 | _contentHasher = contentHasher; 27 | } 28 | 29 | public bool ContainsPath(string absolutePath) => absolutePath.IsUnderDirectory(_directory); 30 | 31 | public async ValueTask GetHashAsync(string absolutePath) 32 | { 33 | if (!ContainsPath(absolutePath)) 34 | { 35 | return null; 36 | } 37 | 38 | // Hashing is expensive so wrap it in a Lazy so that it happens *exactly* once. 39 | Lazy> lazyTask = _cachedHashes.GetOrAdd( 40 | absolutePath, 41 | path => new Lazy>( 42 | async () => 43 | { 44 | if (!File.Exists(path)) 45 | { 46 | return null; 47 | } 48 | 49 | ContentHash contentHash = await _contentHasher.GetFileHashAsync(path); 50 | return contentHash.ToHashByteArray(); 51 | })); 52 | 53 | return await lazyTask.Value; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Common/Hashing/HashingExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Security.Cryptography; 8 | using System.Threading.Tasks; 9 | using BuildXL.Cache.ContentStore.Hashing; 10 | 11 | namespace Microsoft.MSBuildCache.Hashing; 12 | 13 | internal static class HashingExtensions 14 | { 15 | private static readonly byte[] HashCharacterBuffer = { (byte)'#' }; 16 | 17 | /// 18 | /// Combines hashes into a single hash. 19 | /// 20 | public static byte[]? CombineHashes(this IContentHasher contentHasher, IEnumerable hashes) 21 | { 22 | using (HasherToken hasherToken = contentHasher.CreateToken()) 23 | { 24 | HashAlgorithm hasher = hasherToken.Hasher; 25 | int totalBytes = 0; 26 | foreach (byte[]? hash in hashes) 27 | { 28 | if (hash == null || hash.Length == 0) 29 | { 30 | continue; 31 | } 32 | 33 | hasher.TransformBlock(HashCharacterBuffer, 0, 1, HashCharacterBuffer, 0); 34 | hasher.TransformBlock(hash, 0, hash.Length, hash, 0); 35 | totalBytes += 1 + hash.Length; 36 | } 37 | 38 | hasher.TransformFinalBlock(Array.Empty(), 0, 0); 39 | 40 | if (totalBytes > 0) 41 | { 42 | return hasher.Hash; 43 | } 44 | else 45 | { 46 | return null; 47 | } 48 | } 49 | } 50 | 51 | public static async Task GetFileHashAsync(this IContentHasher contentHasher, string filePath) 52 | { 53 | using FileStream fileStream = new( 54 | filePath, 55 | FileMode.Open, 56 | System.IO.FileAccess.Read, 57 | FileShare.Read, 58 | bufferSize: 4096, // Copied from FileStream's DefaultBufferSize 59 | FileOptions.Asynchronous | FileOptions.SequentialScan); 60 | return await contentHasher.GetContentHashAsync(fileStream); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Common/Hashing/IInputHasher.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Threading.Tasks; 5 | 6 | namespace Microsoft.MSBuildCache.Hashing; 7 | 8 | public interface IInputHasher 9 | { 10 | /// 11 | /// Check if a file path is in the repository. 12 | /// 13 | bool ContainsPath(string absolutePath); 14 | 15 | /// 16 | /// Return a hash value for the path. 17 | /// 18 | /// 19 | /// Returns null if no matching files are found. 20 | /// 21 | ValueTask GetHashAsync(string absolutePath); 22 | } 23 | -------------------------------------------------------------------------------- /src/Common/Hashing/OutputHasher.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Channels; 7 | using System.Threading.Tasks; 8 | using BuildXL.Cache.ContentStore.Hashing; 9 | 10 | namespace Microsoft.MSBuildCache.Hashing; 11 | 12 | internal sealed class OutputHasher : IAsyncDisposable 13 | { 14 | private static readonly int HashingParallelism = Environment.ProcessorCount; 15 | 16 | private readonly Func> _computeHashAsync; 17 | private readonly CancellationTokenSource _cancellationTokenSource; 18 | private readonly Channel _hashingChannel; 19 | private readonly Task[] _channelWorkerTasks; 20 | 21 | public OutputHasher(IContentHasher hasher) 22 | : this(async (path, ct) => await hasher.GetFileHashAsync(path)) 23 | { } 24 | 25 | public OutputHasher(Func> computeHashAsync) 26 | { 27 | _computeHashAsync = computeHashAsync; 28 | _cancellationTokenSource = new CancellationTokenSource(); 29 | _hashingChannel = Channel.CreateUnbounded(); 30 | 31 | // Create a bunch of worker tasks to process the hash operations. 32 | _channelWorkerTasks = new Task[HashingParallelism]; 33 | for (int i = 0; i < _channelWorkerTasks.Length; i++) 34 | { 35 | _channelWorkerTasks[i] = Task.Run( 36 | async () => 37 | { 38 | // Not using 'Reader.ReadAllAsync' because its not available in the version we use here. 39 | // Also not passing using the cancellation token here as we need to drain the entire channel to ensure we don't leave dangling Tasks. 40 | while (await _hashingChannel.Reader.WaitToReadAsync()) 41 | { 42 | while (_hashingChannel.Reader.TryRead(out HashOperationContext context)) 43 | { 44 | await ComputeHashInternalAsync(context, _cancellationTokenSource.Token); 45 | } 46 | } 47 | }); 48 | } 49 | } 50 | 51 | public async Task ComputeHashAsync(string filePath, CancellationToken cancellationToken) 52 | { 53 | TaskCompletionSource taskCompletionSource = new(); 54 | HashOperationContext context = new(filePath, taskCompletionSource); 55 | await _hashingChannel.Writer.WriteAsync(context, cancellationToken); 56 | return await taskCompletionSource.Task; 57 | } 58 | 59 | public async ValueTask DisposeAsync() 60 | { 61 | #if NETFRAMEWORK 62 | _cancellationTokenSource.Cancel(); 63 | #else 64 | await _cancellationTokenSource.CancelAsync(); 65 | #endif 66 | _hashingChannel.Writer.Complete(); 67 | await _hashingChannel.Reader.Completion; 68 | await Task.WhenAll(_channelWorkerTasks); 69 | _cancellationTokenSource.Dispose(); 70 | } 71 | 72 | private async Task ComputeHashInternalAsync(HashOperationContext context, CancellationToken cancellationToken) 73 | { 74 | if (cancellationToken.IsCancellationRequested) 75 | { 76 | context.TaskCompletionSource.TrySetCanceled(cancellationToken); 77 | return; 78 | } 79 | 80 | try 81 | { 82 | ContentHash contentHash = await _computeHashAsync(context.FilePath, cancellationToken); 83 | context.TaskCompletionSource.TrySetResult(contentHash); 84 | } 85 | catch (Exception ex) 86 | { 87 | context.TaskCompletionSource.TrySetException(ex); 88 | } 89 | } 90 | 91 | private readonly struct HashOperationContext 92 | { 93 | public HashOperationContext(string filePath, TaskCompletionSource taskCompletionSource) 94 | { 95 | FilePath = filePath; 96 | TaskCompletionSource = taskCompletionSource; 97 | } 98 | 99 | public string FilePath { get; } 100 | 101 | public TaskCompletionSource TaskCompletionSource { get; } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Common/Hashing/SourceControlFileHasher.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using BuildXL.Cache.ContentStore.Hashing; 10 | 11 | namespace Microsoft.MSBuildCache.Hashing; 12 | 13 | /// 14 | /// This class provides hash values for file under source control. 15 | /// 16 | internal sealed class SourceControlFileHasher : IInputHasher 17 | { 18 | private readonly ConcurrentDictionary _cachedCalculatedHashes = new(StringComparer.OrdinalIgnoreCase); 19 | 20 | private readonly IContentHasher _contentHasher; 21 | 22 | private readonly PathNormalizer _pathNormalizer; 23 | 24 | private readonly IReadOnlyDictionary _fileHashes; 25 | 26 | public SourceControlFileHasher( 27 | IContentHasher contentHasher, 28 | PathNormalizer pathNormalizer, 29 | IReadOnlyDictionary fileHashes) 30 | { 31 | _contentHasher = contentHasher; 32 | _pathNormalizer = pathNormalizer; 33 | _fileHashes = fileHashes; 34 | } 35 | 36 | /// 37 | public bool ContainsPath(string absolutePath) => _fileHashes.ContainsKey(absolutePath); 38 | 39 | /// 40 | public ValueTask GetHashAsync(string absolutePath) 41 | { 42 | byte[]? hash = _cachedCalculatedHashes.GetOrAdd( 43 | absolutePath, 44 | path => _fileHashes.TryGetValue(path, out byte[]? contentHash) 45 | ? CalculateHashForFilePathAndContent(path, contentHash) 46 | : null); 47 | return new ValueTask(hash); 48 | } 49 | 50 | // Filenames sometimes matter and impact build results, so consider the file path as well as the content for hashing. 51 | private byte[]? CalculateHashForFilePathAndContent(string filePath, byte[] contentHash) 52 | { 53 | string normalizedFilePath = _pathNormalizer.Normalize(filePath); 54 | return _contentHasher.CombineHashes(GetHashParts(normalizedFilePath, contentHash)); 55 | 56 | static IEnumerable GetHashParts(string normalizedFilePath, byte[] contentHash) 57 | { 58 | yield return Encoding.UTF8.GetBytes(normalizedFilePath.ToUpperInvariant()); 59 | yield return contentHash; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Common/HexUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.MSBuildCache; 7 | 8 | public static class HexUtilities 9 | { 10 | private static readonly ushort[] HexToNybble = 11 | { 12 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, // Character codes 0-9. 13 | 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, // Character codes ":;<=>?@" 14 | 10, 11, 12, 13, 14, 15, // Character codes A-F 15 | 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, // G-P 16 | 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, // Q-Z 17 | 0x100, 0x100, 0x100, 0x100, 0x100, 0x100, // Character codes "[\]^_`" 18 | 10, 11, 12, 13, 14, 15, // Character codes a-f 19 | }; 20 | 21 | public static byte[] HexToBytes(string? hex) 22 | => hex == null 23 | ? Array.Empty() 24 | : HexToBytes(hex.AsSpan()); 25 | 26 | /// 27 | /// Parses hexadecimal strings the form '1234abcd' or '0x9876fedb' into 28 | /// an array of bytes. 29 | /// 30 | public static byte[] HexToBytes(ReadOnlySpan hex) 31 | { 32 | hex = hex.Trim(); 33 | 34 | ReadOnlySpan cur = hex; 35 | if (cur.StartsWith("0x".AsSpan(), StringComparison.OrdinalIgnoreCase)) 36 | { 37 | cur = cur.Slice(2); 38 | } 39 | 40 | byte[] result = new byte[cur.Length / 2]; 41 | int index = 0; 42 | 43 | try 44 | { 45 | while (!cur.IsEmpty) 46 | { 47 | int b = (HexToNybble[cur[0] - '0'] << 4) | HexToNybble[cur[1] - '0']; 48 | if (b < 256) 49 | { 50 | result[index++] = (byte)b; 51 | } 52 | else 53 | { 54 | throw new ArgumentException($"Invalid hex string {new string(hex.ToArray())}", nameof(hex)); 55 | } 56 | 57 | cur = cur.Slice(2); 58 | } 59 | } 60 | catch (IndexOutOfRangeException) 61 | { 62 | throw new ArgumentException($"Invalid hex string {new string(hex.ToArray())}", nameof(hex)); 63 | } 64 | 65 | return result; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Common/Microsoft.MSBuildCache.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | x64 5 | $(Platform) 6 | net472;net8.0 7 | Microsoft.MSBuildCache 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Common/NetFrameworkPolyfills.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #if NETFRAMEWORK 5 | #pragma warning disable IDE0060 // Remove unused parameter. This is intentional to match the missing APIs 6 | 7 | using System; 8 | using System.IO; 9 | using System.Net.Http; 10 | using System.Text; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | namespace Microsoft.MSBuildCache; 15 | 16 | /// 17 | /// Instead of adding #ifs multiple places, implement APIs missing from .NET Framework here. 18 | /// 19 | public static class NetFrameworkPolyfills 20 | { 21 | public static Task GetStreamAsync(this HttpClient @this, string? requestUri, CancellationToken cancellationToken) 22 | => @this.GetStreamAsync(requestUri); 23 | 24 | public static Task ReadAsStreamAsync(this HttpContent @this, CancellationToken cancellationToken) 25 | => @this.ReadAsStreamAsync(); 26 | 27 | public static Task ReadToEndAsync(this StreamReader @this, CancellationToken cancellationToken) 28 | => @this.ReadToEndAsync(); 29 | 30 | public static bool Contains(this string @this, char value) 31 | => @this.IndexOf(value) >= 0; 32 | 33 | public static string Replace(this string @this, string oldValue, string? newValue, StringComparison comparisonType) 34 | { 35 | if (comparisonType == StringComparison.Ordinal) 36 | { 37 | return @this.Replace(oldValue, newValue); 38 | } 39 | 40 | StringBuilder sb = new StringBuilder(); 41 | 42 | int previousIndex = 0; 43 | int index = @this.IndexOf(oldValue, comparisonType); 44 | while (index != -1) 45 | { 46 | sb.Append(@this.Substring(previousIndex, index - previousIndex)); 47 | sb.Append(newValue); 48 | index += oldValue.Length; 49 | 50 | previousIndex = index; 51 | index = @this.IndexOf(oldValue, index, comparisonType); 52 | } 53 | 54 | sb.Append(@this.Substring(previousIndex)); 55 | 56 | return sb.ToString(); 57 | } 58 | 59 | public static bool EndsWith(this string @this, char value) 60 | { 61 | int thisLen = @this.Length; 62 | return thisLen != 0 && @this[thisLen - 1] == value; 63 | } 64 | } 65 | 66 | #pragma warning restore IDE0060 // Remove unused parameter 67 | #endif 68 | -------------------------------------------------------------------------------- /src/Common/NodeBuildResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text.Json.Serialization; 7 | using BuildXL.Cache.ContentStore.Hashing; 8 | using Microsoft.Build.Execution; 9 | using Microsoft.Build.Experimental.ProjectCache; 10 | 11 | namespace Microsoft.MSBuildCache; 12 | 13 | public sealed class NodeBuildResult 14 | { 15 | public const uint CurrentVersion = 1; 16 | 17 | [JsonConstructor] 18 | public NodeBuildResult( 19 | SortedDictionary outputs, 20 | SortedDictionary packageFilesToCopy, 21 | IReadOnlyList targetResults, 22 | DateTime startTimeUtc, 23 | DateTime endTimeUtc, 24 | string? buildId) 25 | { 26 | Outputs = outputs; 27 | PackageFilesToCopy = packageFilesToCopy; 28 | TargetResults = targetResults; 29 | StartTimeUtc = startTimeUtc; 30 | EndTimeUtc = endTimeUtc; 31 | BuildId = buildId; 32 | } 33 | 34 | // Use a sorted dictionary so the JSON output is deterministically sorted and easier to compare build-to-build. 35 | // These paths are repo-relative. 36 | public SortedDictionary Outputs { get; } 37 | 38 | // Use a sorted dictionary so the JSON output is deterministically sorted and easier to compare build-to-build. 39 | public SortedDictionary PackageFilesToCopy { get; } 40 | 41 | public IReadOnlyList TargetResults { get; } 42 | 43 | public DateTime StartTimeUtc { get; } 44 | 45 | public DateTime EndTimeUtc { get; } 46 | 47 | public string? BuildId { get; } 48 | 49 | public static NodeBuildResult FromBuildResult( 50 | SortedDictionary outputs, 51 | SortedDictionary packageFilesToCopy, 52 | BuildResult buildResult, 53 | DateTime creationTimeUtc, 54 | DateTime endTimeUtc, 55 | string? buildId, 56 | PathNormalizer pathNormalizer) 57 | { 58 | List targetResults = new(buildResult.ResultsByTarget.Count); 59 | foreach (KeyValuePair kvp in buildResult.ResultsByTarget) 60 | { 61 | targetResults.Add(NodeTargetResult.FromTargetResult(kvp.Key, kvp.Value, pathNormalizer)); 62 | } 63 | 64 | return new NodeBuildResult(outputs, packageFilesToCopy, targetResults, creationTimeUtc, endTimeUtc, buildId); 65 | } 66 | 67 | public CacheResult ToCacheResult(PathNormalizer pathNormalizer) 68 | { 69 | List targetResults = new(TargetResults.Count); 70 | foreach (NodeTargetResult targetResult in TargetResults) 71 | { 72 | targetResults.Add(targetResult.ToPluginTargetResult(pathNormalizer)); 73 | } 74 | 75 | return CacheResult.IndicateCacheHit(targetResults); 76 | } 77 | } -------------------------------------------------------------------------------- /src/Common/NodeContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Security.Cryptography; 8 | using System.Text; 9 | using Microsoft.Build.Execution; 10 | 11 | namespace Microsoft.MSBuildCache; 12 | 13 | public sealed class NodeContext 14 | { 15 | private static readonly byte[] PropertyHashDelimiter = [0x01]; 16 | private static readonly byte[] PropertyValueHashDelimiter = [0x02]; 17 | 18 | private readonly string _logDirectory; 19 | private bool _logDirectoryCreated; 20 | 21 | public NodeContext( 22 | string baseLogDirectory, 23 | ProjectInstance projectInstance, 24 | IReadOnlyList dependencies, 25 | string projectFileRelativePath, 26 | IReadOnlyDictionary filteredGlobalProperties, 27 | IReadOnlyList inputs, 28 | string? referenceAssemblyRelativePath, 29 | HashSet targetNames) 30 | { 31 | Id = GenerateId(projectFileRelativePath, filteredGlobalProperties); 32 | _logDirectory = Path.Combine(baseLogDirectory, Id); 33 | ProjectInstance = projectInstance; 34 | Dependencies = dependencies; 35 | ProjectFileRelativePath = projectFileRelativePath; 36 | FilteredGlobalProperties = filteredGlobalProperties; 37 | Inputs = inputs; 38 | ReferenceAssemblyRelativePath = referenceAssemblyRelativePath; 39 | TargetNames = targetNames; 40 | } 41 | 42 | public string Id { get; } 43 | 44 | public string LogDirectory 45 | { 46 | get 47 | { 48 | // If something is accessing it, assume it wants to use it, so create the directory. 49 | if (!_logDirectoryCreated) 50 | { 51 | Directory.CreateDirectory(_logDirectory); 52 | _logDirectoryCreated = true; 53 | } 54 | 55 | return _logDirectory; 56 | } 57 | } 58 | 59 | public ProjectInstance ProjectInstance { get; } 60 | 61 | public IReadOnlyList Dependencies { get; } 62 | 63 | public string ProjectFileRelativePath { get; } 64 | 65 | public IReadOnlyDictionary FilteredGlobalProperties { get; } 66 | 67 | public IReadOnlyList Inputs { get; } 68 | 69 | public string? ReferenceAssemblyRelativePath { get; } 70 | 71 | public HashSet TargetNames { get; } 72 | 73 | public DateTime? StartTimeUtc { get; private set; } 74 | 75 | public void SetStartTime() => StartTimeUtc = DateTime.UtcNow; 76 | 77 | public DateTime? EndTimeUtc { get; private set; } 78 | 79 | public void SetEndTime() => EndTimeUtc = DateTime.UtcNow; 80 | 81 | public NodeBuildResult? BuildResult { get; private set; } 82 | 83 | public void SetBuildResult(NodeBuildResult buildResult) 84 | { 85 | if (BuildResult != null) 86 | { 87 | throw new InvalidOperationException("Build result already set"); 88 | } 89 | 90 | BuildResult = buildResult; 91 | } 92 | 93 | /// 94 | /// Generate a stable Id which we can use for sorting and comparison purposes across builds. 95 | /// 96 | private static string GenerateId(string projectFileRelativePath, IReadOnlyDictionary filteredGlobalProperties) 97 | { 98 | // In practice, the dictionary we're given is SortedDictionary, so try casting. 99 | if (filteredGlobalProperties is not SortedDictionary sortedProperties) 100 | { 101 | sortedProperties = new(StringComparer.OrdinalIgnoreCase); 102 | foreach (KeyValuePair kvp in filteredGlobalProperties) 103 | { 104 | sortedProperties.Add(kvp.Key, kvp.Value); 105 | } 106 | } 107 | 108 | #pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms. This is not used for crypto. 109 | using MD5 hasher = MD5.Create(); 110 | #pragma warning restore CA5351 // Do Not Use Broken Cryptographic Algorithms 111 | 112 | foreach (KeyValuePair kvp in sortedProperties) 113 | { 114 | AddCaseInsensitiveStringToHash(hasher, kvp.Key); 115 | AddBytesToHash(hasher, PropertyValueHashDelimiter); 116 | AddCaseInsensitiveStringToHash(hasher, kvp.Value); 117 | AddBytesToHash(hasher, PropertyHashDelimiter); 118 | 119 | static void AddCaseInsensitiveStringToHash(MD5 hasher, string str) => AddBytesToHash(hasher, Encoding.UTF8.GetBytes(str.ToUpperInvariant())); 120 | static void AddBytesToHash(MD5 hasher, byte[] bytes) => hasher.TransformBlock(bytes, 0, bytes.Length, null, 0); 121 | } 122 | 123 | hasher.TransformFinalBlock(Array.Empty(), 0, 0); 124 | byte[] hash = hasher.Hash!; 125 | 126 | string id = $"{projectFileRelativePath}_{Convert.ToBase64String(hash)}"; 127 | 128 | // Avoid casing issues 129 | id = id.ToUpperInvariant(); 130 | 131 | // Ensure the id is path-friendly 132 | id = id 133 | .Replace('\\', '_') 134 | .Replace('/', '_') 135 | .Replace('+', '.') 136 | .TrimEnd('='); 137 | 138 | return id; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Common/NodeDescriptor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace Microsoft.MSBuildCache; 8 | 9 | internal readonly struct NodeDescriptor : IEquatable 10 | { 11 | private readonly string _projectFullPath; 12 | 13 | private readonly SortedDictionary _filteredGlobalProperties; 14 | 15 | public NodeDescriptor(string projectFullPath, SortedDictionary filteredGlobalProperties) 16 | { 17 | _projectFullPath = projectFullPath; 18 | _filteredGlobalProperties = filteredGlobalProperties; 19 | } 20 | 21 | /// 22 | /// Sorted by StringComparison.OrdinalIgnoreCase. 23 | /// 24 | public IReadOnlyDictionary FilteredGlobalProperties => _filteredGlobalProperties; 25 | 26 | public bool Equals(NodeDescriptor other) 27 | { 28 | if (!_projectFullPath.Equals(other._projectFullPath, StringComparison.OrdinalIgnoreCase)) 29 | { 30 | return false; 31 | } 32 | 33 | if (_filteredGlobalProperties.Count != other._filteredGlobalProperties.Count) 34 | { 35 | return false; 36 | } 37 | 38 | foreach (KeyValuePair kvp in _filteredGlobalProperties) 39 | { 40 | if (!other._filteredGlobalProperties.TryGetValue(kvp.Key, out string? otherValue)) 41 | { 42 | return false; 43 | } 44 | 45 | if (!kvp.Value.Equals(otherValue, StringComparison.OrdinalIgnoreCase)) 46 | { 47 | return false; 48 | } 49 | } 50 | 51 | return true; 52 | } 53 | 54 | public override bool Equals(object? obj) => obj is NodeDescriptor key && Equals(key); 55 | 56 | public static bool operator ==(NodeDescriptor left, NodeDescriptor right) => left.Equals(right); 57 | 58 | public static bool operator !=(NodeDescriptor left, NodeDescriptor right) => !(left == right); 59 | 60 | public override int GetHashCode() 61 | { 62 | HashCode hashCode = new(); 63 | hashCode.Add(_projectFullPath, StringComparer.OrdinalIgnoreCase); 64 | 65 | foreach (KeyValuePair kvp in _filteredGlobalProperties) 66 | { 67 | hashCode.Add(kvp.Key, StringComparer.OrdinalIgnoreCase); 68 | hashCode.Add(kvp.Value, StringComparer.OrdinalIgnoreCase); 69 | } 70 | 71 | return hashCode.ToHashCode(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Common/NodeDescriptorFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Microsoft.Build.Execution; 7 | 8 | namespace Microsoft.MSBuildCache; 9 | 10 | internal sealed class NodeDescriptorFactory 11 | { 12 | // There seems to be a bug in MSBuild where this global property gets added for the scheduler node. 13 | private const string MSBuildProjectInstancePrefix = "MSBuildProjectInstance"; 14 | 15 | private readonly HashSet _globalPropertiesToIgnore; 16 | 17 | public NodeDescriptorFactory(HashSet globalPropertiesToIgnore) 18 | { 19 | _globalPropertiesToIgnore = globalPropertiesToIgnore; 20 | } 21 | 22 | public NodeDescriptor Create(ProjectInstance projectInstance) => Create(projectInstance.FullPath, projectInstance.GlobalProperties); 23 | 24 | public NodeDescriptor Create(string projectFullPath, IEnumerable> globalProperties) 25 | { 26 | // Sort to ensure a consistent hash for equivalent sets of properties. 27 | // This allocates with the assumption that the hash code is used multiple times. 28 | SortedDictionary filteredGlobalProperties = new(StringComparer.OrdinalIgnoreCase); 29 | foreach (KeyValuePair kvp in globalProperties) 30 | { 31 | if (kvp.Key.StartsWith(MSBuildProjectInstancePrefix, StringComparison.OrdinalIgnoreCase)) 32 | { 33 | continue; 34 | } 35 | 36 | if (_globalPropertiesToIgnore.Contains(kvp.Key)) 37 | { 38 | continue; 39 | } 40 | 41 | filteredGlobalProperties.Add(kvp.Key, kvp.Value); 42 | } 43 | 44 | return new NodeDescriptor(projectFullPath, filteredGlobalProperties); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Common/NodeTargetResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Text.Json.Serialization; 6 | using Microsoft.Build.Execution; 7 | using Microsoft.Build.Experimental.ProjectCache; 8 | using Microsoft.Build.Framework; 9 | 10 | namespace Microsoft.MSBuildCache; 11 | 12 | public sealed class NodeTargetResult 13 | { 14 | public const uint CurrentVersion = 0; 15 | 16 | [JsonConstructor] 17 | public NodeTargetResult(string targetName, IReadOnlyList taskItems) 18 | { 19 | TargetName = targetName; 20 | TaskItems = taskItems; 21 | } 22 | 23 | public string TargetName { get; } 24 | 25 | public IReadOnlyList TaskItems { get; } 26 | 27 | public static NodeTargetResult FromTargetResult(string targetName, TargetResult targetResult, PathNormalizer pathNormalizer) 28 | { 29 | List taskItems = new(targetResult.Items.Length); 30 | foreach (ITaskItem taskItem in targetResult.Items) 31 | { 32 | taskItems.Add(NodeTargetResultTaskItem.FromTaskItem(taskItem, pathNormalizer)); 33 | } 34 | 35 | return new NodeTargetResult(targetName, taskItems); 36 | } 37 | 38 | public PluginTargetResult ToPluginTargetResult(PathNormalizer pathNormalizer) 39 | { 40 | List taskItems = new(TaskItems.Count); 41 | foreach (NodeTargetResultTaskItem item in TaskItems) 42 | { 43 | taskItems.Add(item.ToTaskItem(pathNormalizer)); 44 | } 45 | 46 | // We only cache successful results. 47 | return new PluginTargetResult(TargetName, taskItems, BuildResultCode.Success); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Common/NodeTargetResultTaskItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Text.Json.Serialization; 7 | using Microsoft.Build.Framework; 8 | using Microsoft.Build.Utilities; 9 | 10 | namespace Microsoft.MSBuildCache; 11 | 12 | public sealed class NodeTargetResultTaskItem 13 | { 14 | [JsonConstructor] 15 | public NodeTargetResultTaskItem(string itemSpec, IReadOnlyDictionary metadata) 16 | { 17 | ItemSpec = itemSpec; 18 | Metadata = metadata; 19 | } 20 | 21 | public string ItemSpec { get; set; } 22 | 23 | public IReadOnlyDictionary Metadata { get; set; } 24 | 25 | public static NodeTargetResultTaskItem FromTaskItem(ITaskItem taskItem, PathNormalizer pathNormalizer) 26 | { 27 | string itemSpec = pathNormalizer.Normalize(taskItem.ItemSpec); 28 | 29 | IDictionary clonedMetadata = taskItem.CloneCustomMetadata(); 30 | Dictionary metadata = new(clonedMetadata.Count); 31 | foreach (DictionaryEntry entry in clonedMetadata) 32 | { 33 | string key = (string)entry.Key; 34 | string value = (string)entry.Value!; 35 | 36 | value = pathNormalizer.Normalize(value); 37 | 38 | metadata.Add(key, value); 39 | } 40 | 41 | return new NodeTargetResultTaskItem(itemSpec, metadata); 42 | } 43 | 44 | public ITaskItem2 ToTaskItem(PathNormalizer pathNormalizer) 45 | { 46 | string itemSpec = pathNormalizer.Unnormalize(ItemSpec); 47 | 48 | Dictionary metadata = new(Metadata.Count); 49 | foreach (KeyValuePair kvp in Metadata) 50 | { 51 | string key = kvp.Key; 52 | string value = kvp.Value; 53 | 54 | value = pathNormalizer.Unnormalize(value); 55 | 56 | metadata.Add(key, value); 57 | } 58 | 59 | return new TaskItem(itemSpec, metadata); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Common/Parsing/CompositeProjectPredictionCollector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using Microsoft.Build.Execution; 7 | using Microsoft.Build.Experimental.ProjectCache; 8 | using Microsoft.Build.Prediction; 9 | 10 | namespace Microsoft.MSBuildCache.Parsing; 11 | 12 | internal sealed class CompositeProjectPredictionCollector : IProjectPredictionCollector 13 | { 14 | private static readonly char[] InvalidPathChars = Path.GetInvalidPathChars(); 15 | 16 | private readonly PluginLoggerBase _logger; 17 | private readonly Dictionary _collectorByProjectInstance; 18 | 19 | public CompositeProjectPredictionCollector(PluginLoggerBase logger, Dictionary collectorByProjectInstance) 20 | { 21 | _logger = logger; 22 | _collectorByProjectInstance = collectorByProjectInstance; 23 | } 24 | 25 | public void AddInputFile(string path, ProjectInstance projectInstance, string predictorName) 26 | { 27 | if (path.IndexOfAny(InvalidPathChars) != -1) 28 | { 29 | _logger.LogMessage($"Ignoring input file with invalid path '{path}'. Predictor: {predictorName}. Project: {projectInstance.FullPath}"); 30 | return; 31 | } 32 | 33 | GetProjectCollector(projectInstance)?.AddInputFile(path, projectInstance, predictorName); 34 | } 35 | 36 | public void AddInputDirectory(string path, ProjectInstance projectInstance, string predictorName) 37 | { 38 | if (path.IndexOfAny(InvalidPathChars) != -1) 39 | { 40 | _logger.LogMessage($"Ignoring input directory with invalid path '{path}'. Predictor: {predictorName}. Project: {projectInstance.FullPath}"); 41 | return; 42 | } 43 | 44 | GetProjectCollector(projectInstance)?.AddInputDirectory(path, projectInstance, predictorName); 45 | } 46 | 47 | public void AddOutputFile(string path, ProjectInstance projectInstance, string predictorName) 48 | { 49 | if (path.IndexOfAny(InvalidPathChars) != -1) 50 | { 51 | _logger.LogMessage($"Ignoring output file with invalid path '{path}'. Predictor: {predictorName}. Project: {projectInstance.FullPath}"); 52 | return; 53 | } 54 | 55 | GetProjectCollector(projectInstance)?.AddOutputFile(path, projectInstance, predictorName); 56 | } 57 | 58 | public void AddOutputDirectory(string path, ProjectInstance projectInstance, string predictorName) 59 | { 60 | if (path.IndexOfAny(InvalidPathChars) != -1) 61 | { 62 | _logger.LogMessage($"Ignoring output directory with invalid path '{path}'. Predictor: {predictorName}. Project: {projectInstance.FullPath}"); 63 | return; 64 | } 65 | 66 | GetProjectCollector(projectInstance)?.AddOutputDirectory(path, projectInstance, predictorName); 67 | } 68 | private ProjectPredictionCollector? GetProjectCollector(ProjectInstance projectInstance) 69 | => _collectorByProjectInstance.TryGetValue(projectInstance, out ProjectPredictionCollector? collector) ? collector : null; 70 | } 71 | -------------------------------------------------------------------------------- /src/Common/Parsing/Parser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using Microsoft.Build.Execution; 10 | using Microsoft.Build.Experimental.ProjectCache; 11 | using Microsoft.Build.Graph; 12 | using Microsoft.Build.Prediction; 13 | using Microsoft.Build.Prediction.Predictors; 14 | 15 | namespace Microsoft.MSBuildCache.Parsing; 16 | 17 | internal record class ParserInfo( 18 | string ProjectFileRelativePath, 19 | IReadOnlyList Inputs, 20 | string? ReferenceAssemblyRelativePath); 21 | 22 | internal sealed class Parser 23 | { 24 | private readonly PluginLoggerBase _logger; 25 | private readonly string _repoRoot; 26 | 27 | public Parser(PluginLoggerBase logger, string repoRoot) 28 | { 29 | _logger = logger; 30 | _repoRoot = repoRoot; 31 | } 32 | 33 | public IReadOnlyDictionary Parse(ProjectGraph graph) 34 | { 35 | var predictionCollectorForProjects = new Dictionary(graph.ProjectNodes.Count); 36 | foreach (ProjectGraphNode node in graph.ProjectNodes) 37 | { 38 | // Don't consider anything outside the repository. 39 | if (!node.ProjectInstance.FullPath.IsUnderDirectory(_repoRoot)) 40 | { 41 | _logger.LogMessage($"Ignoring project outside the repository: {node.ProjectInstance.FullPath}"); 42 | continue; 43 | } 44 | 45 | ProjectPredictionCollector predictionCollector = new(node); 46 | predictionCollectorForProjects.Add(node.ProjectInstance, predictionCollector); 47 | } 48 | 49 | Stopwatch stopwatch = Stopwatch.StartNew(); 50 | // AdditionalIncludeDirectoriesPredictor overpredicts and these can be covered by PathSet anyway. 51 | var projectPredictors = ProjectPredictors.AllProjectPredictors.Where(p => p is not AdditionalIncludeDirectoriesPredictor); 52 | var predictionExecutor = new ProjectGraphPredictionExecutor(ProjectPredictors.AllProjectGraphPredictors, projectPredictors); 53 | var compositePredictionCollector = new CompositeProjectPredictionCollector(_logger, predictionCollectorForProjects); 54 | predictionExecutor.PredictInputsAndOutputs(graph, compositePredictionCollector); 55 | _logger.LogMessage($"Executed project prediction on {graph.ProjectNodes.Count} nodes in {stopwatch.Elapsed.TotalSeconds:F2}s."); 56 | 57 | // Build the final collection to return 58 | var parserInfoForNodes = new Dictionary(predictionCollectorForProjects.Count); 59 | foreach (KeyValuePair kvp in predictionCollectorForProjects) 60 | { 61 | ProjectInstance projectInstance = kvp.Key; 62 | ProjectPredictionCollector predictionCollector = kvp.Value; 63 | 64 | string projectFilePath = projectInstance.FullPath; 65 | string projectFileRelativePath = projectFilePath.MakePathRelativeTo(_repoRoot) ?? throw new InvalidOperationException($"Project \"{projectFilePath}\" is not under the repo root \"{_repoRoot}\""); 66 | 67 | string targetRefPath = projectInstance.GetPropertyValue("TargetRefPath"); 68 | string? referenceAssemblyPath = !string.IsNullOrEmpty(targetRefPath) ? Path.Combine(projectInstance.Directory, targetRefPath) : null; 69 | 70 | ParserInfo parserInfo = new(projectFileRelativePath, predictionCollector.Inputs, referenceAssemblyPath); 71 | parserInfoForNodes.Add(predictionCollector.Node, parserInfo); 72 | } 73 | 74 | return parserInfoForNodes; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Common/Parsing/PredictedInput.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace Microsoft.MSBuildCache.Parsing; 8 | 9 | internal sealed class PredictedInput 10 | { 11 | private readonly List _predictorNames = new(1); 12 | 13 | public PredictedInput(string absolutePath) 14 | { 15 | AbsolutePath = absolutePath; 16 | } 17 | 18 | public string AbsolutePath { get; } 19 | 20 | public IReadOnlyList PredictorNames => _predictorNames; 21 | 22 | public void AddPredictorName(string predictorName) 23 | { 24 | // Only need to lock on add, not get. 25 | // Parsing populates this collection and it's only read for diagnostic purposes after. 26 | lock (_predictorNames) 27 | { 28 | // Iterate instead of using a HashSet as this is expected to be a very small collection. 29 | foreach (string existingPredictorName in _predictorNames) 30 | { 31 | if (predictorName.Equals(existingPredictorName, StringComparison.OrdinalIgnoreCase)) 32 | { 33 | return; 34 | } 35 | } 36 | 37 | _predictorNames.Add(predictorName); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Common/Parsing/ProjectPredictionCollector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using Microsoft.Build.Execution; 9 | using Microsoft.Build.Graph; 10 | using Microsoft.Build.Prediction; 11 | 12 | namespace Microsoft.MSBuildCache.Parsing; 13 | 14 | internal sealed class ProjectPredictionCollector : IProjectPredictionCollector 15 | { 16 | private static readonly char[] DirectorySeparatorChars = { Path.DirectorySeparatorChar }; 17 | private readonly string _projectDirectory; 18 | 19 | // A cache of paths (relative or absolute) to their absolute form. 20 | // This is scoped per build file since the paths are relative to different directories. 21 | private readonly Dictionary _absolutePathFileCache = new(StringComparer.OrdinalIgnoreCase); 22 | 23 | private readonly ConcurrentDictionary _inputs = new(StringComparer.OrdinalIgnoreCase); 24 | 25 | public ProjectPredictionCollector(ProjectGraphNode node) 26 | { 27 | Node = node; 28 | _projectDirectory = Path.GetDirectoryName(node.ProjectInstance.FullPath)!; 29 | } 30 | 31 | public ProjectGraphNode Node { get; } 32 | 33 | public IReadOnlyList Inputs 34 | { 35 | get 36 | { 37 | var inputs = new PredictedInput[_inputs.Count]; 38 | int inputIndex = 0; 39 | foreach (KeyValuePair kvp in _inputs) 40 | { 41 | inputs[inputIndex++] = kvp.Value; 42 | } 43 | 44 | return inputs; 45 | } 46 | } 47 | 48 | public void AddInputFile(string path, ProjectInstance projectInstance, string predictorName) 49 | { 50 | string absolutePath = GetFullPath(path); 51 | AddInput(absolutePath, predictorName); 52 | } 53 | 54 | public void AddInputDirectory(string path, ProjectInstance projectInstance, string predictorName) 55 | { 56 | string absoluteDirPath = GetFullPath(path); 57 | if (!Directory.Exists(absoluteDirPath)) 58 | { 59 | // Ignore inputs to output directories which don't exist 60 | return; 61 | } 62 | 63 | foreach (string filePath in Directory.EnumerateFiles(absoluteDirPath, "*", SearchOption.TopDirectoryOnly)) 64 | { 65 | AddInputFile(filePath, projectInstance, predictorName); 66 | } 67 | } 68 | 69 | public void AddOutputFile(string path, ProjectInstance projectInstance, string predictorName) 70 | { 71 | // No need to track these 72 | } 73 | 74 | public void AddOutputDirectory(string path, ProjectInstance projectInstance, string predictorName) 75 | { 76 | // No need to track these 77 | } 78 | 79 | private void AddInput(string absolutePath, string predictorName) 80 | { 81 | PredictedInput input = _inputs.GetOrAdd(absolutePath, static path => new PredictedInput(path)); 82 | input.AddPredictorName(predictorName); 83 | } 84 | 85 | /// 86 | /// Gets the full path of a path which may be absolute or project-relative. 87 | /// 88 | /// An absolute or project-relative file path 89 | /// The absolute file path. 90 | private string GetFullPath(string path) 91 | { 92 | if (!_absolutePathFileCache.TryGetValue(path, out string? absolutePath)) 93 | { 94 | absolutePath = path; 95 | 96 | // If the path contains forward slash, normalize it with backward slash 97 | // Note that Replace returns the same string instance if there was no replacement, so there is no need for a Contains call to avoid allocating. 98 | absolutePath = absolutePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); 99 | 100 | // Make the path absolute 101 | if (!Path.IsPathRooted(absolutePath)) 102 | { 103 | absolutePath = Path.Combine(_projectDirectory, absolutePath); 104 | } 105 | 106 | // Remove any \.\ or \..\ stuff 107 | absolutePath = Path.GetFullPath(absolutePath); 108 | 109 | // Always trim trailing slashes 110 | absolutePath = absolutePath.TrimEnd(DirectorySeparatorChars); 111 | 112 | _absolutePathFileCache.Add(path, absolutePath); 113 | } 114 | 115 | return absolutePath; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Common/PathHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.IO; 6 | 7 | namespace Microsoft.MSBuildCache; 8 | 9 | public static class PathHelper 10 | { 11 | public static string? MakePathRelativeTo(this string path, string basePath) 12 | { 13 | ReadOnlySpan pathSpan = Path.GetFullPath(path).AsSpan(); 14 | ReadOnlySpan basePathSpan = Path.GetFullPath(basePath).AsSpan(); 15 | 16 | basePathSpan = basePathSpan.TrimEnd(Path.DirectorySeparatorChar); 17 | 18 | if (pathSpan.StartsWith(basePathSpan, StringComparison.OrdinalIgnoreCase)) 19 | { 20 | // Relative path. 21 | if (basePathSpan.Length == pathSpan.Length) 22 | { 23 | return string.Empty; 24 | } 25 | else if (pathSpan[basePathSpan.Length] == '\\') 26 | { 27 | return new string(pathSpan.Slice(basePathSpan.Length + 1).ToArray()); 28 | } 29 | } 30 | 31 | return null; 32 | } 33 | 34 | public static bool IsUnderDirectory(this string filePath, string directoryPath) 35 | { 36 | filePath = Path.GetFullPath(filePath); 37 | directoryPath = Path.GetFullPath(directoryPath); 38 | 39 | if (!filePath.StartsWith(directoryPath, StringComparison.OrdinalIgnoreCase)) 40 | { 41 | return false; 42 | } 43 | 44 | if (directoryPath[directoryPath.Length - 1] == Path.DirectorySeparatorChar) 45 | { 46 | return true; 47 | } 48 | else 49 | { 50 | return filePath.Length > directoryPath.Length 51 | && filePath[directoryPath.Length] == Path.DirectorySeparatorChar; 52 | } 53 | } 54 | 55 | /// 56 | /// File paths returned by Detours have some prefixes that need to be removed: 57 | /// \\?\ - removes the file name limit of 260 chars. It makes it 32735 (+ a null terminator) 58 | /// \??\ - this is a native Win32 FS path WinNt32 59 | /// 60 | public static string RemoveLongPathPrefixes(string absolutePath) 61 | { 62 | if (absolutePath.Length < 4 || absolutePath[0] != '\\') 63 | { 64 | return absolutePath; 65 | } 66 | 67 | // We already checked index 0 68 | ReadOnlySpan span = absolutePath.AsSpan(1, 3); 69 | if (span.SequenceEqual(['\\', '?', '\\']) 70 | || span.SequenceEqual(['?', '?', '\\'])) 71 | { 72 | return absolutePath.Substring(4); 73 | } 74 | 75 | return absolutePath; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Common/PathNormalizer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.IO; 6 | 7 | namespace Microsoft.MSBuildCache; 8 | 9 | /// 10 | /// Normalizes paths to be portable across machines. 11 | /// 12 | /// 13 | /// "Normalizing" a path will replace well-known base paths with placeholders. This enables the base paths to be different across machines 14 | /// and thus make the paths portable, at least for the well-known base paths. 15 | /// 16 | public sealed class PathNormalizer 17 | { 18 | private const string RepoRootPlaceholder = "{RepoRoot}"; 19 | private const string NugetPackageRootPlaceholder = "{NugetPackageRoot}"; 20 | 21 | private readonly string _repoRoot; 22 | 23 | private readonly string _nugetPackageRoot; 24 | 25 | public PathNormalizer(string repoRoot, string nugetPackageRoot) 26 | { 27 | _repoRoot = EnsureTrailingSlash(Path.GetFullPath(repoRoot)); 28 | _nugetPackageRoot = EnsureTrailingSlash(Path.GetFullPath(nugetPackageRoot)); 29 | 30 | static string EnsureTrailingSlash(string path) => path[path.Length - 1] == '\\' ? path : (path + '\\'); 31 | } 32 | 33 | public string Normalize(string path) 34 | => path 35 | .Replace(_repoRoot, RepoRootPlaceholder, StringComparison.OrdinalIgnoreCase) 36 | .Replace(_nugetPackageRoot, NugetPackageRootPlaceholder, StringComparison.OrdinalIgnoreCase); 37 | 38 | public string Unnormalize(string normalized) 39 | => normalized 40 | .Replace(RepoRootPlaceholder, _repoRoot, StringComparison.Ordinal) 41 | .Replace(NugetPackageRootPlaceholder, _nugetPackageRoot, StringComparison.Ordinal); 42 | } 43 | -------------------------------------------------------------------------------- /src/Common/ProjectInstanceExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using Microsoft.Build.Execution; 6 | 7 | namespace Microsoft.MSBuildCache; 8 | 9 | internal static class ProjectInstanceExtensions 10 | { 11 | /// 12 | /// Parses a property that may contain a bool, returning the default value if the property is not 13 | /// set, or returns false if the property is set to 'false', otherwise returning true. 14 | /// 15 | public static bool GetBoolPropertyValue(this ProjectInstance projectInstance, string propName, bool defaultValue = false) 16 | { 17 | string prop = projectInstance.GetPropertyValue(propName); 18 | if (string.IsNullOrWhiteSpace(prop)) 19 | { 20 | return defaultValue; 21 | } 22 | 23 | if (string.Equals(prop, "false", StringComparison.OrdinalIgnoreCase)) 24 | { 25 | return false; 26 | } 27 | 28 | return true; 29 | } 30 | 31 | public static bool IsPlatformNegotiationBuild(this ProjectInstance projectInstance) 32 | { 33 | // the fact that there is zero global properties here allows us to determine this was an evaluation done for the sole use of determining a referenced project's compatible platforms. We know that this is a build for this use 34 | // because "IsGraphBuild" will be set to true in the global properties of all other project instances. 35 | return projectInstance.IsPlatformNegotiationEnabled() && projectInstance.GlobalProperties.Count == 0; 36 | } 37 | 38 | public static bool IsPlatformNegotiationEnabled(this ProjectInstance projectInstance) 39 | { 40 | // Determine whether or not the platform negotiation is turned on in msbuild 41 | return !string.IsNullOrWhiteSpace(projectInstance.GetPropertyValue("EnableDynamicPlatformResolution")); 42 | } 43 | 44 | public static bool IsInnerBuild(this ProjectInstance projectInstance) 45 | { 46 | // This follows the logic of MSBuild's ProjectInterpretation.GetProjectType. 47 | // See: https://github.com/microsoft/msbuild/blob/master/src/Build/Graph/ProjectInterpretation.cs 48 | return !projectInstance.IsPlatformNegotiationBuild() && !string.IsNullOrWhiteSpace(projectInstance.GetPropertyValue(projectInstance.GetPropertyValue("InnerBuildProperty"))) && !string.IsNullOrWhiteSpace(projectInstance.GetPropertyValue(projectInstance.GetPropertyValue("InnerBuildPropertyValues"))); 49 | } 50 | 51 | public static bool IsOuterBuild(this ProjectInstance projectInstance) 52 | { 53 | // This follows the logic of MSBuild's ProjectInterpretation.GetProjectType. 54 | // See: https://github.com/microsoft/msbuild/blob/master/src/Build/Graph/ProjectInterpretation.cs 55 | return !projectInstance.IsPlatformNegotiationBuild() && !projectInstance.IsInnerBuild() && !string.IsNullOrWhiteSpace(projectInstance.GetPropertyValue(projectInstance.GetPropertyValue("InnerBuildPropertyValues"))); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Common/SerializationHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization.Metadata; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Microsoft.MSBuildCache; 13 | 14 | internal static class SerializationHelper 15 | { 16 | public static JsonWriterOptions WriterOptions { get; } = new JsonWriterOptions { Indented = true }; 17 | 18 | internal static async Task DeserializeAsync(this Stream stream, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default) 19 | where T : class 20 | { 21 | try 22 | { 23 | return await JsonSerializer.DeserializeAsync(stream, typeInfo, cancellationToken); 24 | } 25 | catch (JsonException) 26 | { 27 | var message = $"Can't successfully deserialize a value of type {typeof(T)} from stream."; 28 | 29 | if (stream.CanSeek) 30 | { 31 | stream.Position = 0; 32 | 33 | using (var streamReader = new StreamReader( 34 | stream, 35 | Encoding.UTF8, 36 | #if NETFRAMEWORK 37 | detectEncodingFromByteOrderMarks: true, 38 | bufferSize: 1024, 39 | #endif 40 | leaveOpen: true)) 41 | { 42 | string content = await streamReader.ReadToEndAsync(cancellationToken); 43 | 44 | // Truncating the string to avoid a very long error message. 45 | const int maxLength = 512; 46 | if (content.Length > maxLength) 47 | { 48 | content = content.Substring(0, maxLength).Trim(); 49 | } 50 | 51 | message = $"{message} Content: '{content}'"; 52 | } 53 | } 54 | 55 | throw new InvalidOperationException(message); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Common/SortedDictionaryConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | using BuildXL.Cache.ContentStore.Hashing; 8 | using System.Collections.Generic; 9 | 10 | namespace Microsoft.MSBuildCache; 11 | 12 | internal sealed class SortedDictionaryConverter : JsonConverter> 13 | { 14 | public override SortedDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 15 | { 16 | var contentHashConverter = (JsonConverter)options.GetConverter(typeof(ContentHash)); 17 | var outputs = new SortedDictionary(StringComparer.OrdinalIgnoreCase); 18 | while (reader.Read()) 19 | { 20 | if (reader.TokenType == JsonTokenType.EndObject) 21 | { 22 | break; 23 | } 24 | 25 | if (reader.TokenType != JsonTokenType.PropertyName) 26 | { 27 | throw new JsonException($"Unexpected token: {reader.TokenType}"); 28 | } 29 | 30 | string propertyName = reader.GetString()!; 31 | if (!reader.Read()) 32 | { 33 | throw new JsonException($"Property name '{propertyName}' does not have a value."); 34 | } 35 | 36 | ContentHash? contentHash = contentHashConverter.Read(ref reader, typeof(ContentHash), options); 37 | if (contentHash == null) 38 | { 39 | throw new JsonException($"Property value for '{propertyName}' could not be parsed."); 40 | } 41 | 42 | outputs.Add(propertyName, contentHash.Value); 43 | } 44 | 45 | return outputs; 46 | } 47 | 48 | public override void Write(Utf8JsonWriter writer, SortedDictionary value, JsonSerializerOptions options) 49 | { 50 | var defaultConverter = (JsonConverter>)options.GetConverter(typeof(IDictionary)); 51 | defaultConverter.Write(writer, value, options); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Common/SourceControl/Git.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Build.Experimental.ProjectCache; 9 | #if NETFRAMEWORK 10 | using Process = Microsoft.MSBuildCache.SourceControl.GitProcess; 11 | #endif 12 | 13 | namespace Microsoft.MSBuildCache.SourceControl; 14 | 15 | internal static class Git 16 | { 17 | // UTF8 - NO BOM 18 | private static readonly Encoding InputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); 19 | 20 | public static async Task RunAsync( 21 | PluginLoggerBase logger, 22 | string workingDir, string args, 23 | Func> onRunning, 24 | Func onExit, 25 | CancellationToken cancellationToken) 26 | { 27 | using Process process = new(); 28 | process.StartInfo.FileName = "git"; // Git is expected to be on the PATH 29 | process.StartInfo.Arguments = args; 30 | process.StartInfo.UseShellExecute = false; 31 | process.StartInfo.CreateNoWindow = true; 32 | process.StartInfo.EnvironmentVariables["GIT_FLUSH"] = "1"; // https://git-scm.com/docs/git#git-codeGITFLUSHcode 33 | process.StartInfo.WorkingDirectory = workingDir; 34 | process.StartInfo.RedirectStandardInput = true; 35 | process.StartInfo.RedirectStandardOutput = true; 36 | process.StartInfo.RedirectStandardError = true; 37 | process.StartInfo.StandardOutputEncoding = Encoding.UTF8; 38 | process.StartInfo.StandardInputEncoding = InputEncoding; 39 | 40 | Stopwatch sw = Stopwatch.StartNew(); 41 | 42 | process.Start(); 43 | 44 | static void KillProcess(Process process) 45 | { 46 | try 47 | { 48 | if (!process.HasExited) 49 | { 50 | process.Kill(); 51 | } 52 | } 53 | catch 54 | { 55 | // Swallow. This is best-effort 56 | } 57 | } 58 | 59 | using (cancellationToken.Register(() => KillProcess(process))) 60 | { 61 | using (StreamWriter stdin = process.StandardInput) 62 | using (StreamReader stdout = process.StandardOutput) 63 | using (StreamReader stderr = process.StandardError) 64 | { 65 | Task resultTask = Task.Run(async () => 66 | { 67 | try 68 | { 69 | return await onRunning(stdin, stdout); 70 | } 71 | finally 72 | { 73 | stdin.Close(); 74 | } 75 | }); 76 | Task errorTask = Task.Run(() => stderr.ReadToEndAsync()); 77 | 78 | #if NETFRAMEWORK 79 | process.WaitForExit(); 80 | cancellationToken.ThrowIfCancellationRequested(); 81 | #else 82 | await process.WaitForExitAsync(cancellationToken); 83 | #endif 84 | 85 | if (process.ExitCode == 0) 86 | { 87 | logger.LogMessage($"git.exe {args} (@{process.StartInfo.WorkingDirectory}) took {sw.ElapsedMilliseconds} msec and returned {process.ExitCode}."); 88 | } 89 | else 90 | { 91 | logger.LogMessage($"git.exe {args} (@{process.StartInfo.WorkingDirectory}) took {sw.ElapsedMilliseconds} msec and returned {process.ExitCode}. Stderr: {await errorTask}"); 92 | } 93 | 94 | return onExit(process.ExitCode, await resultTask); 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/Common/SourceControl/ISourceControlFileHashProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.MSBuildCache.SourceControl; 9 | 10 | /// 11 | /// The contract for getting the hashes of files under source control. 12 | /// 13 | internal interface ISourceControlFileHashProvider 14 | { 15 | /// 16 | /// Get files under source control and their hash values. 17 | /// 18 | /// The repository root 19 | /// A token to cancel the operation 20 | /// All files within repository root with their hash values. The file paths are relative to the repository root. 21 | Task> GetFileHashesAsync(string repoRoot, CancellationToken cancellationToken); 22 | } 23 | -------------------------------------------------------------------------------- /src/Common/SourceControl/SourceControlHashException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.MSBuildCache.SourceControl; 7 | 8 | /// 9 | /// Specific Source Control HashException thrown when the hash generation fails. 10 | /// 11 | public class SourceControlHashException : Exception 12 | { 13 | public SourceControlHashException() 14 | { 15 | } 16 | 17 | public SourceControlHashException(string message) 18 | : base(FormatMessage(message)) 19 | { 20 | } 21 | 22 | public SourceControlHashException(string message, Exception inner) 23 | : base(FormatMessage(message), inner) 24 | { 25 | } 26 | 27 | private static string FormatMessage(string message) => $"Hash generation failed with message: {message}"; 28 | } 29 | -------------------------------------------------------------------------------- /src/Common/SourceGenerationContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Text.Json.Serialization; 6 | using BuildXL.Cache.ContentStore.Hashing; 7 | using Microsoft.MSBuildCache.Caching; 8 | using Microsoft.MSBuildCache.Fingerprinting; 9 | 10 | namespace Microsoft.MSBuildCache; 11 | 12 | [JsonSourceGenerationOptions(WriteIndented = true, Converters = [typeof(ContentHashJsonConverter), typeof(SortedDictionaryConverter)])] 13 | [JsonSerializable(typeof(NodeBuildResult))] 14 | [JsonSerializable(typeof(PathSet))] 15 | [JsonSerializable(typeof(LocalCacheStateFile))] 16 | [JsonSerializable(typeof(IDictionary))] 17 | internal partial class SourceGenerationContext : JsonSerializerContext 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/Common/UInt32FlagsFormatter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | #if DEBUG 6 | using System.Diagnostics; 7 | #endif 8 | using System.IO; 9 | 10 | namespace Microsoft.MSBuildCache; 11 | 12 | /// 13 | /// Fast flags enum formatter that separates bits with '|' 14 | /// 15 | internal static class UInt32FlagsFormatter 16 | where TEnum : struct, Enum 17 | { 18 | private static readonly string[] Names = GetNames(); 19 | 20 | #if DEBUG 21 | static UInt32FlagsFormatter() 22 | { 23 | Debug.Assert(typeof(TEnum).IsEnum); 24 | Debug.Assert(typeof(TEnum).GetEnumUnderlyingType() == typeof(uint)); 25 | } 26 | #endif 27 | 28 | private static string[] GetNames() 29 | { 30 | var names = new string[32]; 31 | int k = 1; 32 | for (int j = 0; j < 32; j++, k <<= 1) 33 | { 34 | names[j] = unchecked(((TEnum)(object)(uint)k).ToString()); 35 | } 36 | 37 | return names; 38 | } 39 | 40 | public static void Write(StreamWriter writer, uint value) 41 | { 42 | bool first = true; 43 | uint currentFlag = 1; 44 | for (int j = 0; j < 32; j++, currentFlag <<= 1) 45 | { 46 | if ((value & currentFlag) != 0) 47 | { 48 | if (first) 49 | { 50 | first = false; 51 | } 52 | else 53 | { 54 | writer.Write('|'); 55 | } 56 | 57 | writer.Write(Names[j]); 58 | } 59 | } 60 | 61 | if (first) 62 | { 63 | writer.Write(0); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Common/build/Microsoft.MSBuildCache.Common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | true 12 | 13 | 14 | 18 | 19 | 20 | $(MSBuildCacheIgnoredOutputPatterns);*.assets.cache 21 | 22 | 23 | $(MSBuildCacheIgnoredOutputPatterns);*assemblyreference.cache 24 | 25 | 26 | 29 | 30 | 31 | \**\vctip.exe 32 | 33 | 34 | 35 | \**\mspdbsrv.exe 36 | 37 | 38 | 41 | 42 | 48 | $(MSBuildCacheGlobalPropertiesToIgnore);CurrentSolutionConfigurationContents 49 | $(MSBuildCacheGlobalPropertiesToIgnore);ShouldUnsetParentConfigurationAndPlatform 50 | 51 | 55 | $(MSBuildCacheGlobalPropertiesToIgnore);BuildingInsideVisualStudio 56 | $(MSBuildCacheGlobalPropertiesToIgnore);BuildingSolutionFile 57 | $(MSBuildCacheGlobalPropertiesToIgnore);SolutionDir 58 | $(MSBuildCacheGlobalPropertiesToIgnore);SolutionExt 59 | $(MSBuildCacheGlobalPropertiesToIgnore);SolutionFileName 60 | $(MSBuildCacheGlobalPropertiesToIgnore);SolutionName 61 | $(MSBuildCacheGlobalPropertiesToIgnore);SolutionPath 62 | 63 | 64 | $(MSBuildCacheGlobalPropertiesToIgnore);_MSDeployUserAgent 65 | 66 | 67 | 70 | 71 | $(MSBuildCacheTargetsToIgnore);GetTargetFrameworks 72 | $(MSBuildCacheTargetsToIgnore);GetNativeManifest 73 | $(MSBuildCacheTargetsToIgnore);GetCopyToOutputDirectoryItems 74 | $(MSBuildCacheTargetsToIgnore);GetTargetFrameworksWithPlatformForSingleTargetFramework 75 | 76 | -------------------------------------------------------------------------------- /src/Common/build/Microsoft.MSBuildCache.Common.targets: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheEnabled 8 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheLogDirectory 9 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheCacheUniverse 10 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheMaxConcurrentCacheContentOperations 11 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheLocalCacheRootPath 12 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheLocalCacheSizeInMegabytes 13 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheIgnoredInputPatterns 14 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheIgnoredOutputPatterns 15 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheIdenticalDuplicateOutputPatterns 16 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheRemoteCacheIsReadOnly 17 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheAsyncCachePublishing 18 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheAsyncCacheMaterialization 19 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheAllowFileAccessAfterProjectFinishProcessPatterns 20 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheAllowFileAccessAfterProjectFinishFilePatterns 21 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheAllowProcessCloseAfterProjectFinishProcessPatterns 22 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheGlobalPropertiesToIgnore 23 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheGetResultsForUnqueriedDependencies 24 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheTargetsToIgnore 25 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheIgnoreDotNetSdkPatchVersion 26 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheSkipUnchangedOutputFiles 27 | $(MSBuildCacheGlobalPropertiesToIgnore);MSBuildCacheTouchOutputFiles 28 | 29 | 30 | 31 | 32 | $(MSBuildCacheLogDirectory) 33 | $(MSBuildCacheCacheUniverse) 34 | $(MSBuildCacheMaxConcurrentCacheContentOperations) 35 | $(MSBuildCacheLocalCacheRootPath) 36 | $(MSBuildCacheLocalCacheSizeInMegabytes) 37 | $(MSBuildCacheIgnoredInputPatterns) 38 | $(MSBuildCacheIgnoredOutputPatterns) 39 | $(MSBuildCacheIdenticalDuplicateOutputPatterns) 40 | $(MSBuildCacheRemoteCacheIsReadOnly) 41 | $(MSBuildCacheAsyncCachePublishing) 42 | $(MSBuildCacheAsyncCacheMaterialization) 43 | $(MSBuildCacheAllowFileAccessAfterProjectFinishProcessPatterns) 44 | $(MSBuildCacheAllowFileAccessAfterProjectFinishFilePatterns) 45 | $(MSBuildCacheAllowProcessCloseAfterProjectFinishProcessPatterns) 46 | $(MSBuildCacheGlobalPropertiesToIgnore) 47 | $(MSBuildCacheGetResultsForUnqueriedDependencies) 48 | $(MSBuildCacheTargetsToIgnore) 49 | $(MSBuildCacheIgnoreDotNetSdkPatchVersion) 50 | $(MSBuildCacheSkipUnchangedOutputFiles) 51 | $(MSBuildCacheTouchOutputFiles) 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/Common/build/Microsoft.MSBuildCache.Cpp.targets: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Common/buildMultitargeting/Microsoft.MSBuildCache.Common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Common/buildMultitargeting/Microsoft.MSBuildCache.Common.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Local/MSBuildCacheLocalPlugin.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BuildXL.Cache.ContentStore.Distributed.NuCache; 9 | using BuildXL.Cache.ContentStore.Hashing; 10 | using BuildXL.Cache.ContentStore.Interfaces.Results; 11 | using BuildXL.Cache.ContentStore.Interfaces.Stores; 12 | using BuildXL.Cache.ContentStore.Interfaces.Tracing; 13 | using BuildXL.Cache.ContentStore.Logging; 14 | using BuildXL.Cache.MemoizationStore.Interfaces.Sessions; 15 | using BuildXL.Cache.MemoizationStore.Sessions; 16 | using Microsoft.Build.Experimental.ProjectCache; 17 | using Microsoft.MSBuildCache.Caching; 18 | 19 | namespace Microsoft.MSBuildCache; 20 | 21 | public sealed class MSBuildCacheLocalPlugin : MSBuildCachePluginBase 22 | { 23 | protected override HashType HashType => HashType.Murmur; 24 | 25 | protected override async Task CreateCacheClientAsync(PluginLoggerBase logger, CancellationToken cancellationToken) 26 | { 27 | if (Settings == null 28 | || FingerprintFactory == null 29 | || ContentHasher == null 30 | || NugetPackageRoot == null) 31 | { 32 | throw new InvalidOperationException(); 33 | } 34 | 35 | FileLog fileLog = new(Path.Combine(Settings.LogDirectory, "CacheClient.log")); 36 | #pragma warning disable CA2000 // Dispose objects before losing scope. Expected to be disposed using Context.Logger.Dispose in the cache client implementation. 37 | Logger cacheLogger = new(fileLog); 38 | #pragma warning restore CA2000 // Dispose objects before losing scope 39 | Context context = new(cacheLogger); 40 | 41 | #pragma warning disable CA2000 // Dispose objects before losing scope. Expected to be disposed by TwoLevelCache 42 | LocalCache cache = LocalCacheFactory.Create(cacheLogger, Settings.LocalCacheRootPath, Settings.LocalCacheSizeInMegabytes); 43 | #pragma warning restore CA2000 // Dispose objects before losing scope 44 | 45 | await cache.StartupAsync(context).ThrowIfFailure(); 46 | CreateSessionResult cacheSessionResult = cache 47 | .CreateSession(context, "local", ImplicitPin.PutAndGet) 48 | .ThrowIfFailure(); 49 | ICacheSession cacheSession = cacheSessionResult.Session!; 50 | 51 | (await cacheSession.StartupAsync(context)).ThrowIfFailure(); 52 | 53 | return new CasCacheClient( 54 | context, 55 | FingerprintFactory, 56 | cache, 57 | cacheSession, 58 | remoteCache: null, 59 | ContentHasher, 60 | Settings.RepoRoot, 61 | NugetPackageRoot, 62 | GetFileRealizationMode, 63 | Settings.MaxConcurrentCacheContentOperations, 64 | Settings.AsyncCachePublishing, 65 | Settings.AsyncCacheMaterialization, 66 | Settings.SkipUnchangedOutputFiles, 67 | Settings.TouchOutputFiles); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Local/Microsoft.MSBuildCache.Local.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | x64 5 | $(Platform) 6 | net472;net8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | true 19 | build\ 20 | 21 | 22 | PreserveNewest 23 | true 24 | build\ 25 | 26 | 27 | PreserveNewest 28 | true 29 | buildMultiTargeting\ 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Local/build/Microsoft.MSBuildCache.Local.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildThisFileDirectory)net472\Microsoft.MSBuildCache.Local.dll 4 | $(MSBuildThisFileDirectory)net8.0\Microsoft.MSBuildCache.Local.dll 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Local/build/Microsoft.MSBuildCache.Local.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Local/buildMultitargeting/Microsoft.MSBuildCache.Local.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Local/buildMultitargeting/Microsoft.MSBuildCache.Local.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Repack.Tests/Microsoft.MSBuildCache.Repack.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | x64 5 | AnyCPU;x64 6 | net472;net8.0 7 | Microsoft.MSBuildCache.Repack.Tests 8 | 9 | $(NoWarn);CA1861 10 | 11 | $(NoWarn);CA1034 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Repack.Tests/RepackTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using Microsoft.MSBuildCache.AzureBlobStorage; 9 | using Microsoft.MSBuildCache.AzurePipelines; 10 | using Microsoft.MSBuildCache.Tests; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | 13 | namespace Microsoft.MSBuildCache.Repack.Tests; 14 | 15 | // Check to make sure that the assemblies that defined the MSBuild/Plugin interface are not merged 16 | [TestClass] 17 | public class RepackTests 18 | { 19 | [DataTestMethod] 20 | [DataRow(typeof(MSBuildCacheAzureBlobStoragePlugin))] 21 | [DataRow(typeof(MSBuildCacheAzurePipelinesPlugin))] 22 | [DataRow(typeof(MSBuildCacheLocalPlugin))] 23 | [DataRow(typeof(SharedCompilation.ResolveFileAccesses))] 24 | public void PluginInterfaceAssembliesNotMerged(Type typeToCheck) 25 | { 26 | #if DEBUG 27 | // Using Assert.Inconclusive instead of removing the entire test for visibility that the test exists. 28 | Assert.Inconclusive("This test only applies to Release builds."); 29 | #endif 30 | 31 | HashSet references = typeToCheck.Assembly 32 | .GetReferencedAssemblies() 33 | .Where(a => a.Name is not null) 34 | .Select(reference => reference.Name!) 35 | .ToHashSet(StringComparer.Ordinal); 36 | 37 | // Check to make sure that each of the interface assemblies are still actually referenced 38 | foreach (string expectedRefFileName in PluginInterfaceTypeCheckTests.PluginInterfaceNuGetAssemblies) 39 | { 40 | string expectedRef = Path.GetFileNameWithoutExtension(expectedRefFileName); 41 | Assert.IsTrue(references.Contains(expectedRef)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SharedCompilation/CompilerUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Reflection; 8 | using System.Runtime.InteropServices; 9 | using Microsoft.CodeAnalysis; 10 | using Microsoft.CodeAnalysis.CSharp; 11 | using Microsoft.CodeAnalysis.VisualBasic; 12 | 13 | namespace Microsoft.MSBuildCache.SharedCompilation; 14 | 15 | /// 16 | /// Parses csc/vbc compiler command line arguments. 17 | /// 18 | internal static class CompilerUtilities 19 | { 20 | // Define our delegates. 21 | private delegate bool TryParseOptionFxn(string arg, out string name, out string value); 22 | private delegate void ParseResourceDescriptionFxn( 23 | string resourceDescriptor, 24 | string? baseDirectory, 25 | bool skipLeadingSeparators, //VB does this 26 | out string filePath, 27 | out string fullPath, 28 | out string fileName, 29 | out string resourceName, 30 | out string accessibility); 31 | 32 | // Delegate properties for reflection methods. 33 | private static TryParseOptionFxn TryParseOption { get; } 34 | private static ParseResourceDescriptionFxn ParseResourceDescription { get; } 35 | 36 | static CompilerUtilities() 37 | { 38 | MethodInfo tryParseOptionMethod = typeof(CommandLineParser).GetMethod("TryParseOption", BindingFlags.NonPublic | BindingFlags.Static)!; 39 | TryParseOption = (TryParseOptionFxn)Delegate.CreateDelegate(typeof(TryParseOptionFxn), null, tryParseOptionMethod); 40 | 41 | MethodInfo parseResourceDescriptionMethod = typeof(CommandLineParser).GetMethod("ParseResourceDescription", BindingFlags.NonPublic | BindingFlags.Static)!; 42 | ParseResourceDescription = (ParseResourceDescriptionFxn)Delegate.CreateDelegate(typeof(ParseResourceDescriptionFxn), null, parseResourceDescriptionMethod); 43 | } 44 | 45 | /// 46 | /// Uses the Roslyn command line parser to understand the arguments passed to the compiler. 47 | /// 48 | public static CommandLineArguments GetParsedCommandLineArguments(string language, string[] arguments, string projectFile) 49 | { 50 | if (arguments.Length == 0) 51 | { 52 | throw new ArgumentOutOfRangeException(nameof(arguments), "Length must be greater than 0."); 53 | } 54 | 55 | string sdkDirectory = RuntimeEnvironment.GetRuntimeDirectory(); 56 | string? projectDirectory = Path.GetDirectoryName(projectFile); 57 | CommandLineArguments result = language switch 58 | { 59 | LanguageNames.CSharp => CSharpCommandLineParser.Default.Parse(arguments, projectDirectory, sdkDirectory), 60 | LanguageNames.VisualBasic => VisualBasicCommandLineParser.Default.Parse(arguments, projectDirectory, sdkDirectory), 61 | _ => throw new InvalidOperationException($"Unexpected language '{language}'"), 62 | }; 63 | 64 | return result; 65 | } 66 | 67 | /// 68 | /// Uses the Roslyn command line parser to resolve embedded resources to their file path. 69 | /// /resource: parameters end up in CommandLineArguments.ManifestResources, but the returned class drops the file path. 70 | /// Thus we need this method in order to resolve the file paths. 71 | /// We should be able to remove this if/when this gets resolved: https://github.com/dotnet/roslyn/issues/41372. 72 | /// 73 | /// The embedded resource arguments passed to the compiler. 74 | /// The base directory of the project. 75 | /// An array of file paths to the embedded resource inputs. 76 | public static string[] GetEmbeddedResourceFilePaths(IEnumerable embeddedResourceArgs, string? baseDirectory) 77 | { 78 | var embeddedResourceFilePaths = new List(); 79 | foreach (string embeddedResourceArg in embeddedResourceArgs) 80 | { 81 | if (TryParseOption(embeddedResourceArg, out string argName, out string argValue)) 82 | { 83 | ParseResourceDescription( 84 | argValue, 85 | baseDirectory, 86 | skipLeadingSeparators: false, 87 | out string filePath, 88 | out string fullPath, 89 | out string fileName, 90 | out string resourceName, 91 | out string accessibility); 92 | embeddedResourceFilePaths.Add(fullPath); 93 | } 94 | } 95 | 96 | return embeddedResourceFilePaths.ToArray(); 97 | } 98 | } -------------------------------------------------------------------------------- /src/SharedCompilation/Microsoft.MSBuildCache.SharedCompilation.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | x64 5 | $(Platform) 6 | net472;net8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | PreserveNewest 14 | true 15 | build\ 16 | 17 | 18 | PreserveNewest 19 | true 20 | buildMultiTargeting\ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/SharedCompilation/ResolveFileAccesses.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using Microsoft.Build.Framework; 7 | using Microsoft.Build.Utilities; 8 | using Microsoft.CodeAnalysis; 9 | using static Microsoft.MSBuildCache.SharedCompilation.VBCSCompilerReporter; 10 | 11 | namespace Microsoft.MSBuildCache.SharedCompilation; 12 | 13 | /// 14 | /// When shared compilation is on, this task is used to resolve file accesses 15 | /// by passing in the command line arguments returned by a Csc or Vbc task to 16 | /// the , which parses 17 | /// them and reports file accesses. 18 | /// 19 | public sealed class ResolveFileAccesses : Task 20 | { 21 | private string? _language; 22 | 23 | [Required] 24 | public string? Language 25 | { 26 | get => _language; 27 | set 28 | { 29 | if (LanguageNames.CSharp.Equals(value, StringComparison.OrdinalIgnoreCase)) 30 | { 31 | _language = LanguageNames.CSharp; 32 | } 33 | else if (LanguageNames.VisualBasic.Equals(value, StringComparison.OrdinalIgnoreCase)) 34 | { 35 | _language = LanguageNames.VisualBasic; 36 | } 37 | else 38 | { 39 | // F# is disallowed. 40 | throw new ArgumentException("Language must be either C# or Visual Basic."); 41 | } 42 | } 43 | } 44 | 45 | [Required] 46 | public ITaskItem[]? CommandLineArguments { get; set; } 47 | 48 | [Required] 49 | public string? ProjectFile { get; set; } 50 | 51 | public override bool Execute() 52 | { 53 | if (CommandLineArguments == null) 54 | { 55 | throw new ArgumentNullException(nameof(CommandLineArguments)); 56 | } 57 | 58 | if (Language == null) 59 | { 60 | throw new ArgumentNullException(nameof(Language)); 61 | } 62 | 63 | if (string.IsNullOrWhiteSpace(ProjectFile)) 64 | { 65 | throw new ArgumentException($"{nameof(ProjectFile)} cannot be null or whitespace."); 66 | } 67 | 68 | ResolveFileAccesses( 69 | Language, 70 | CommandLineArguments.Select(item => item.ItemSpec).ToArray(), 71 | ProjectFile!, 72 | ((IBuildEngine10)BuildEngine).EngineServices); 73 | return true; 74 | } 75 | } -------------------------------------------------------------------------------- /src/SharedCompilation/build/Microsoft.MSBuildCache.SharedCompilation.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildThisFileDirectory)net472\Microsoft.MSBuildCache.SharedCompilation.dll 4 | $(MSBuildThisFileDirectory)net8.0\Microsoft.MSBuildCache.SharedCompilation.dll 5 | 6 | -------------------------------------------------------------------------------- /src/SharedCompilation/build/Microsoft.MSBuildCache.SharedCompilation.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /tests/TestProject/.gitignore: -------------------------------------------------------------------------------- 1 | nuget_packages 2 | MSBuildCache 3 | MSBuildCacheLogs* 4 | obj/ 5 | bin/ -------------------------------------------------------------------------------- /tests/TestProject/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/TestProject/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/TestProject/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | Console.WriteLine("Hello, World!"); 3 | -------------------------------------------------------------------------------- /tests/TestProject/TestProject.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/TestProject/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "0.1-preview", 4 | // For historical reasons, use a specific offset. Reset to -1 when bumping the version above. 5 | "buildNumberOffset": 204, 6 | "publicReleaseRefSpec": [ 7 | "^refs/heads/main$", 8 | "^refs/tags/v\\d+\\.\\d+\\.\\d+" 9 | ], 10 | "inherit": false 11 | } 12 | --------------------------------------------------------------------------------