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