From b20b55526da0e0ae918d22d0fba9efda87b290f4 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 13 May 2026 13:03:04 +0100 Subject: [PATCH 01/10] Build: drop MicroBuild signing wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove all MicroBuild.Core PackageReference declarations and the associated ItemGroups from GVFS.Payload, GVFS.Installers, and FastFetch, and remove MicroBuild.Core from Directory.Packages.props. Also drop the BeforeTargets="SignFiles" hooks from the CreatePayload and CreateInstaller targets — without MicroBuild.Core's targets the SignFiles target no longer exists, and the hooks are unnecessary now that signing is performed externally by the release pipeline. This commit is a pure removal of the in-build signing wiring; signing itself moves to ESRP-driven steps in the release pipeline in a follow-up commit. The GitHub Actions CI / PR build never signed anything and is unaffected. Signed-off-by: Matthew John Cheetham --- Directory.Packages.props | 1 - GVFS/FastFetch/FastFetch.csproj | 8 -------- GVFS/GVFS.Installers/GVFS.Installers.csproj | 10 +--------- GVFS/GVFS.Payload/GVFS.Payload.csproj | 18 +----------------- 4 files changed, 2 insertions(+), 35 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index faf9cf3ae..e72ceb6e6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,6 @@ - diff --git a/GVFS/FastFetch/FastFetch.csproj b/GVFS/FastFetch/FastFetch.csproj index e99f7b785..a8faae5ea 100644 --- a/GVFS/FastFetch/FastFetch.csproj +++ b/GVFS/FastFetch/FastFetch.csproj @@ -12,14 +12,6 @@ - - - - - - Microsoft400 - false - diff --git a/GVFS/GVFS.Installers/GVFS.Installers.csproj b/GVFS/GVFS.Installers/GVFS.Installers.csproj index 5961b569b..cfc2478f3 100644 --- a/GVFS/GVFS.Installers/GVFS.Installers.csproj +++ b/GVFS/GVFS.Installers/GVFS.Installers.csproj @@ -12,7 +12,6 @@ - @@ -25,14 +24,7 @@ - - - Microsoft400 - false - - - - + diff --git a/GVFS/GVFS.Payload/GVFS.Payload.csproj b/GVFS/GVFS.Payload/GVFS.Payload.csproj index c87428e9f..e400a2a94 100644 --- a/GVFS/GVFS.Payload/GVFS.Payload.csproj +++ b/GVFS/GVFS.Payload/GVFS.Payload.csproj @@ -11,10 +11,9 @@ - - + @@ -22,20 +21,5 @@ - - - Microsoft400 - false - - - From 21364aebffc0d5f55e6bfc9c229ab80f18f0d23c Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 13 May 2026 13:03:19 +0100 Subject: [PATCH 02/10] GVFS.Installers: gate CreateInstaller on $(SkipCreateInstaller) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Condition="'$(SkipCreateInstaller)' != 'true'" to the CreateInstaller MSBuild target so callers can opt out of the Inno Setup compile step. This lets the release pipeline build all of the managed binaries first (via Build.bat with the env var set), ESRP-sign them in place, and only then invoke the Inno Setup compile to produce SetupGVFS..exe — packaging the already-signed binaries in a single, deterministic pass instead of building, deleting, and rebuilding the installer. Default behavior is unchanged: with the property unset (the case for local builds and the GitHub Actions PR/CI workflow), CreateInstaller fires as before during the regular Build target. Signed-off-by: Matthew John Cheetham --- GVFS/GVFS.Installers/GVFS.Installers.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GVFS/GVFS.Installers/GVFS.Installers.csproj b/GVFS/GVFS.Installers/GVFS.Installers.csproj index cfc2478f3..e48c1229e 100644 --- a/GVFS/GVFS.Installers/GVFS.Installers.csproj +++ b/GVFS/GVFS.Installers/GVFS.Installers.csproj @@ -24,7 +24,7 @@ - + From f3af9cd7b3439655a133549c24d61e995239d214 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 13 May 2026 13:03:42 +0100 Subject: [PATCH 03/10] .azure-pipelines: migrate release pipeline from MicroBuild to 1ESPT + ESRP Rewrite the release pipeline to extend the 1ES Pipeline Templates directly and to perform code signing via inline ADO tasks, replacing the existing MicroBuild.1ES.Official.yml@MicroBuildTemplate + MicroBuild.Core mechanism. This new pipeline matches the pattern already used by microsoft/git and git-ecosystem/git-credential-manager. We build the Payload and FastFetch with SkipCreateInstaller=true so the Inno Setup compile is deferred until after ESRP signing, then build the installer in a dedicated dotnet build step (--no-dependencies prevents the Payload layout from re-running and overwriting the freshly signed binaries). We also add a release stage with a 1ES releaseJob that publishes a draft GitHub Release on microsoft/VFSForGit attaching the installer asset. Signed-off-by: Matthew John Cheetham --- .azure-pipelines/release.yml | 399 ++++++++++++++++++++++++++--------- 1 file changed, 304 insertions(+), 95 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 7350735bc..d4e37460d 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -1,95 +1,304 @@ -name: $(date:yy)$(DayOfYear)$(rev:.r) -trigger: none -pr: none - -variables: - GVFSMajorAndMinorVersion: 2.0 - GVFSRevision: $(Build.BuildNumber) - BuildConfiguration: Release - TeamName: GVFS - -resources: - repositories: - - repository: MicroBuildTemplate - type: git - name: 1ESPipelineTemplates/MicroBuildTemplate - ref: refs/tags/release - - - repository: VFSForGit - type: github - name: microsoft/VFSForGit - ref: releases/shipped - endpoint: GitHub-VFSForGit - -extends: - template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate - parameters: - pool: - name: VSEng-MicroBuildVSStable - - featureFlags: - incrementalSDLBinaryAnalysis: false - disableNetworkIsolation: true - - sdl: - binskim: - enabled: false - justificationForDisabling: "Guardian and BinSkim do not support a suppression for InnoSetup installer file" - sourceRepositoriesToScan: - include: - - repository: VFSForGit - - stages: - - stage: Release - - jobs: - - job: Build - templateContext: - mb: - signing: - enabled: true - feedSource: 'https://pkgs.dev.azure.com/mseng/_packaging/MicroBuildToolset/nuget/v3/index.json' - signType: real - signWithProd: true - - outputs: - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory)\GVFS.Installers - artifactName: Installer - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory)\FastFetch - artifactName: FastFetch - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory)\Symbols - artifactName: Symbols - - output: pipelineArtifact - targetPath: $(Build.ArtifactStagingDirectory)\GVFS.FunctionalTests - artifactName: FunctionalTests - - steps: - - checkout: VFSForGit - displayName: 'Checkout VFS for Git' - path: vfsforgit\src - - - task: NuGetToolInstaller@1 - displayName: 'Use NuGet 6.x' - inputs: - versionSpec: '6.x' - - - script: | - $(Agent.BuildDirectory)\vfsforgit\src\scripts\Build.bat ^ - $(BuildConfiguration) ^ - $(GVFSMajorAndMinorVersion).$(GVFSRevision) ^ - detailed - displayName: 'Build and sign ($(BuildConfiguration))' - - - script: | - $(Agent.BuildDirectory)\vfsforgit\src\scripts\RunUnitTests.bat ^ - $(BuildConfiguration) - displayName: 'Run unit tests' - - - script: | - $(Agent.BuildDirectory)\vfsforgit\src\scripts\CreateBuildArtifacts.bat ^ - $(BuildConfiguration) ^ - $(Build.ArtifactStagingDirectory) - displayName: 'Create artifacts' +name: $(date:yy)$(DayOfYear)$(rev:.r) +trigger: none +pr: none + +# +# Release pipeline for VFS for Git. +# +# Builds the Windows x64 installer, ESRP-signs the inner Payload binaries and +# the outer SetupGVFS installer, stages all release artifacts, and (optionally) +# publishes a draft GitHub Release. +# +# Designed to be run manually from Azure DevOps, typically against the +# `releases/shipped` branch. Triggers are intentionally `none`; PR/CI builds +# are handled by the GitHub Actions workflow at .github/workflows/build.yaml. +# + +resources: + repositories: + - repository: 1ESPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +parameters: + - name: 'esrp' + type: boolean + default: true + displayName: 'Enable ESRP code signing' + - name: 'github' + type: boolean + default: true + displayName: 'Enable GitHub release publishing' + +variables: + - name: 'GVFSMajorAndMinorVersion' + value: '2.0' + - name: 'GVFSRevision' + value: $(Build.BuildNumber) + - name: 'GVFSVersion' + value: $(GVFSMajorAndMinorVersion).$(GVFSRevision) + - name: 'BuildConfiguration' + value: 'Release' + - name: 'OutDir' + value: $(Agent.BuildDirectory)\vfsforgit\out + - name: 'esrpAppConnectionName' + value: '1ESGitClient-ESRP-App' + - name: 'githubConnectionName' + value: 'GitHub-VFSForGit' + # ESRP signing variables set in the pipeline settings: + # - esrpEndpointUrl + # - esrpClientId + # - esrpTenantId + # - esrpKeyVaultName + # - esrpSignReqCertName + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelines + parameters: + featureFlags: + incrementalSDLBinaryAnalysis: false + disableNetworkIsolation: true + + sdl: + sourceAnalysisPool: + name: GitClientPME-1ESHostedPool-intel-pc + image: win-x86_64-ado1es + os: windows + binskim: + enabled: false + justificationForDisabling: "Guardian and BinSkim do not support a suppression for InnoSetup installer file" + + stages: + - stage: build + displayName: 'Build and Sign' + jobs: + - job: Build + displayName: 'Build VFS for Git (Windows x64)' + pool: + name: GitClientPME-1ESHostedPool-intel-pc + image: win-x86_64-ado1es + os: windows + templateContext: + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)\GVFS.Installers + artifactName: Installer + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)\FastFetch + artifactName: FastFetch + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)\Symbols + artifactName: Symbols + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)\GVFS.FunctionalTests + artifactName: FunctionalTests + + steps: + - checkout: self + displayName: 'Checkout VFS for Git' + path: vfsforgit/src + + - task: NuGetToolInstaller@1 + displayName: 'Use NuGet 6.x' + inputs: + versionSpec: '6.x' + + - script: | + $(Build.SourcesDirectory)\scripts\Build.bat ^ + $(BuildConfiguration) ^ + $(GVFSVersion) ^ + detailed + env: + # Skip the Inno Setup compile step inside Build.bat so that + # the Payload binaries can be ESRP-signed before they get + # packaged into the installer. The installer is built in a + # dedicated step further down, after signing. + SkipCreateInstaller: 'true' + displayName: 'Build ($(BuildConfiguration))' + + - script: | + $(Build.SourcesDirectory)\scripts\RunUnitTests.bat ^ + $(BuildConfiguration) + displayName: 'Run unit tests' + + # ESRP signing of the standalone binaries (Payload + FastFetch). + # The installer hasn't been built yet, so it can be packaged from + # signed binaries in a single Inno Setup pass. + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@6 + displayName: 'Sign VFS for Git binaries' + inputs: + connectedServiceName: $(esrpAppConnectionName) + useMSIAuthentication: true + appRegistrationClientId: $(esrpClientId) + appRegistrationTenantId: $(esrpTenantId) + authAkvName: $(esrpKeyVaultName) + authSignCertName: $(esrpSignReqCertName) + serviceEndpointUrl: $(esrpEndpointUrl) + folderPath: $(OutDir)\GVFS.Payload\bin\$(BuildConfiguration)\win-x64 + pattern: | + GitHooksLoader.exe + GVFS.exe + GVFS.Hooks.exe + GVFS.Mount.exe + GVFS.PostIndexChangedHook.exe + GVFS.ReadObjectHook.exe + GVFS.Service.exe + GVFS.VirtualFileSystemHook.exe + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "https://www.microsoft.com", + "FileDigest": "/fd SHA256", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} + } + ] + + - task: EsrpCodeSigning@6 + displayName: 'Sign FastFetch' + inputs: + connectedServiceName: $(esrpAppConnectionName) + useMSIAuthentication: true + appRegistrationClientId: $(esrpClientId) + appRegistrationTenantId: $(esrpTenantId) + authAkvName: $(esrpKeyVaultName) + authSignCertName: $(esrpSignReqCertName) + serviceEndpointUrl: $(esrpEndpointUrl) + folderPath: $(OutDir)\FastFetch\bin\$(BuildConfiguration)\net10.0-windows10.0.17763.0\win-x64\publish + pattern: 'FastFetch.exe' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "https://www.microsoft.com", + "FileDigest": "/fd SHA256", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} + } + ] + + # Build the installer (Inno Setup compile) now that the Payload + # binaries are signed. --no-dependencies ensures the Payload's + # layout step does NOT re-run and overwrite our signed binaries + # with unsigned originals from each project's individual bin + # folder. + - script: | + dotnet build "$(Build.SourcesDirectory)\GVFS\GVFS.Installers\GVFS.Installers.csproj" ^ + -c $(BuildConfiguration) ^ + --no-restore --no-dependencies ^ + -p:GVFSVersion=$(GVFSVersion) || EXIT /B 1 + displayName: 'Build VFS for Git installer' + + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@6 + displayName: 'Sign VFS for Git installer' + inputs: + connectedServiceName: $(esrpAppConnectionName) + useMSIAuthentication: true + appRegistrationClientId: $(esrpClientId) + appRegistrationTenantId: $(esrpTenantId) + authAkvName: $(esrpKeyVaultName) + authSignCertName: $(esrpSignReqCertName) + serviceEndpointUrl: $(esrpEndpointUrl) + folderPath: $(OutDir)\GVFS.Installers\bin\$(BuildConfiguration)\win-x64 + pattern: 'SetupGVFS.*.exe' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "https://www.microsoft.com", + "FileDigest": "/fd SHA256", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} + } + ] + + - script: | + $(Build.SourcesDirectory)\scripts\CreateBuildArtifacts.bat ^ + $(BuildConfiguration) ^ + $(Build.ArtifactStagingDirectory) + displayName: 'Stage artifacts' + + - stage: release + displayName: 'Release' + dependsOn: [build] + # Only publish a draft GitHub release when ESRP signing was enabled in + # this run -- otherwise we would risk uploading unsigned installer + # binaries to the public release workflow. + condition: and(succeeded(), eq('${{ parameters.github }}', true), eq('${{ parameters.esrp }}', true)) + jobs: + - job: github + displayName: 'Publish GitHub release' + pool: + name: GitClientPME-1ESHostedPool-intel-pc + image: ubuntu-x86_64-ado1es + os: linux + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: Installer + targetPath: $(Pipeline.Workspace)/assets/Installer + steps: + - task: GitHubRelease@1 + displayName: 'Create draft GitHub Release' + inputs: + gitHubConnection: $(githubConnectionName) + repositoryName: microsoft/VFSForGit + tag: 'v$(GVFSVersion)' + tagSource: userSpecifiedTag + title: 'VFS for Git $(GVFSVersion)' + isDraft: true + addChangeLog: false + assets: | + $(Pipeline.Workspace)/assets/Installer/SetupGVFS.*.exe From 2fab771ab83b065854b66d3323bb1c6c219dd96b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 13 May 2026 14:38:38 +0100 Subject: [PATCH 04/10] .azure-pipelines: publish debug symbols with GitHub Release Download the Symbols pipeline artifact alongside the Installer in the release stage, zip it up via ArchiveFiles@2, and attach it to the draft GitHub Release so consumers (and crash-dump triage) can grab the matching .pdb / native debug symbols for the binaries shipped in SetupGVFS..exe. Both downloads now land under $(Pipeline.Workspace)/_final so the GitHubRelease@1 'assets' glob picks up SetupGVFS.*.exe and Symbols.zip from the same staging directory. Also enable the auto-generated change log on the release while we're already in the GitHubRelease@1 step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Matthew John Cheetham --- .azure-pipelines/release.yml | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index d4e37460d..5c97b8451 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -280,8 +280,8 @@ extends: displayName: 'Publish GitHub release' pool: name: GitClientPME-1ESHostedPool-intel-pc - image: ubuntu-x86_64-ado1es - os: linux + image: win-x86_64-ado1es + os: windows templateContext: type: releaseJob isProduction: true @@ -289,7 +289,30 @@ extends: - input: pipelineArtifact artifactName: Installer targetPath: $(Pipeline.Workspace)/assets/Installer + - input: pipelineArtifact + artifactName: Symbols + targetPath: $(Pipeline.Workspace)/assets/Symbols steps: + - task: CopyFiles@2 + displayName: 'Gather PDB files' + inputs: + SourceFolder: $(Pipeline.Workspace)/assets/Symbols + Contents: '**/*.pdb' + TargetFolder: $(Pipeline.Workspace)/_pdbs + - task: ArchiveFiles@2 + displayName: 'Prepare PDB files for upload' + inputs: + rootFolderOrFile: $(Pipeline.Workspace)/_pdbs + includeRootFolder: false + archiveType: zip + archiveFile: $(Pipeline.Workspace)/_final/Symbols.zip + replaceExistingArchive: true + - task: CopyFiles@2 + displayName: 'Prepare installer for upload' + inputs: + SourceFolder: $(Pipeline.Workspace)/assets/Installer + Contents: 'SetupGVFS.*.exe' + TargetFolder: $(Pipeline.Workspace)/_final - task: GitHubRelease@1 displayName: 'Create draft GitHub Release' inputs: @@ -299,6 +322,7 @@ extends: tagSource: userSpecifiedTag title: 'VFS for Git $(GVFSVersion)' isDraft: true - addChangeLog: false + isPreRelease: true + addChangeLog: true assets: | - $(Pipeline.Workspace)/assets/Installer/SetupGVFS.*.exe + $(Pipeline.Workspace)/_final/* From 11a4147626614c342de2fc714e2d20fdce06706c Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 13 May 2026 13:13:56 +0100 Subject: [PATCH 05/10] .azure-pipelines: install pinned .NET SDK from global.json Add a UseDotNet@2 task with useGlobalJson: true before the build step so the agent uses exactly the SDK version pinned in global.json This matches the behavior of the GitHub Actions build workflow, which already uses actions/setup-dotnet with global-json-file. Signed-off-by: Matthew John Cheetham --- .azure-pipelines/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 5c97b8451..8cde92ba9 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -100,6 +100,12 @@ extends: displayName: 'Checkout VFS for Git' path: vfsforgit/src + - task: UseDotNet@2 + displayName: 'Use .NET SDK (global.json)' + inputs: + useGlobalJson: true + workingDirectory: $(Build.SourcesDirectory) + - task: NuGetToolInstaller@1 displayName: 'Use NuGet 6.x' inputs: From d7a11ae77e94d064fe20130e72da9ac7aeecbb61 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 13 May 2026 14:26:57 +0100 Subject: [PATCH 06/10] Build.bat: fail when VS MSBuild is missing Previously Build.bat warned and continued when it could not locate VS MSBuild (via either PATH or vswhere), silently skipping the native C++ projects (GitHooksLoader, GVFS.NativeTests, GVFS.PostIndexChangedHook, GVFS.ReadObjectHook, GVFS.VirtualFileSystemHook). That meant a build on a machine without the C++ workload would happily produce an installer with the native binaries missing. VS MSBuild and the C++ workload are not optional for any GVFS build that's expected to run, so make a missing toolchain a hard error instead. Anyone hitting it should install Visual Studio with the 'Desktop development with C++' workload (or VS Build Tools with VCTools). Signed-off-by: Matthew John Cheetham --- scripts/Build.bat | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/Build.bat b/scripts/Build.bat index 32ab6f565..905359390 100644 --- a/scripts/Build.bat +++ b/scripts/Build.bat @@ -70,8 +70,8 @@ IF DEFINED MSBUILD_EXEC ( /p:SolutionDir="%VFS_SRCDIR%\\" || GOTO ERROR ) ) ELSE ( - ECHO WARNING: Could not find VS MSBuild. Native C++ projects will not be built. - ECHO Install Visual Studio with the C++ workload to build native projects. + ECHO ERROR: Could not find VS MSBuild. Install Visual Studio with the C++ workload to build native projects. + EXIT /B 1 ) ECHO ^***************************** From 8721e68c9e142aaa8bd6f24d650a02299f8ccba9 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 13 May 2026 14:27:18 +0100 Subject: [PATCH 07/10] .azure-pipelines: install VS C++ workload before build GVFS sets PublishAot=true in Directory.Build.props for every managed project, which means publishing GVFS.exe and friends needs the native C++ toolchain (link.exe, the Windows SDK, etc.) at build time. The 1ES win-x86_64-ado1es image used by the new release pipeline has no VS installed so the Native AOT publish step fails with: error : Platform linker not found. ..per the documented NativeAOT prerequisites (https://aka.ms/nativeaot-prerequisites), which call for VS 2022 with the 'Desktop development with C++' workload. Add a PowerShell script that ensures the C++ workload is present before Build.bat runs, and wire it into release.yml. The script: * Bootstraps vswhere.exe from its GitHub release if the standard Program Files location does not have it (so it works on minimal images too). * Uses 'vswhere -requires NativeDesktop VCTools -requiresAny' to decide whether the workload is already installed on any existing Visual Studio install. If yes, exit 0 and let the build run. * If a Visual Studio install exists but lacks the workload, run its setup.exe modify to add the appropriate workload ID (NativeDesktop for full VS, VCTools for Build Tools). * If no Visual Studio is installed at all, download the VS Build Tools bootstrapper from aka.ms and install it with the VCTools workload from scratch. * Treat exit codes 0 and 3010 (success, reboot needed) as success for both modify and fresh install. * Re-run the same vswhere requirement check post-install to verify. Signed-off-by: Matthew John Cheetham --- .azure-pipelines/release.yml | 5 + .../scripts/install-vs-cpp-workload.ps1 | 167 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 .azure-pipelines/scripts/install-vs-cpp-workload.ps1 diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 8cde92ba9..88665a1be 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -111,6 +111,11 @@ extends: inputs: versionSpec: '6.x' + - task: PowerShell@2 + displayName: 'Install VS C++ workload (NativeAOT prerequisite)' + inputs: + filePath: $(Build.SourcesDirectory)\.azure-pipelines\scripts\install-vs-cpp-workload.ps1 + - script: | $(Build.SourcesDirectory)\scripts\Build.bat ^ $(BuildConfiguration) ^ diff --git a/.azure-pipelines/scripts/install-vs-cpp-workload.ps1 b/.azure-pipelines/scripts/install-vs-cpp-workload.ps1 new file mode 100644 index 000000000..8ab9a5dcd --- /dev/null +++ b/.azure-pipelines/scripts/install-vs-cpp-workload.ps1 @@ -0,0 +1,167 @@ +# +# Ensure a Visual Studio 2022 (or newer) install with the "Desktop +# development with C++" workload is present on the build agent. +# +# .NET NativeAOT publishing (used by every product-facing managed VFS for +# Git project via PublishAot=true in Directory.Build.props) requires the +# C++ build tools from this workload at publish time. The native VFS +# projects also build against the v143 toolset, which ships with VS 2022. +# +# This script handles three situations: +# 1. A VS 2022+ install with the C++ workload is already present +# -> exit early. +# 2. A VS 2022+ install (any product) is present but the C++ workload +# is missing -> modify that install to add it. +# 3. No VS 2022+ install at all -> install VS Build Tools 2022 with +# the VC tools workload. (An older VS install, e.g. VS 2019, is +# ignored here -- we leave it alone and install VS 2022 alongside.) +# +# vswhere.exe is bootstrapped from GitHub if not already on disk. +# +# Workload presence is verified via vswhere's -requires/-requiresAny +# rather than by probing for individual files like link.exe; the +# documented prerequisite is the workload itself, not any specific +# implementation detail of NativeAOT. +# +# See https://aka.ms/nativeaot-prerequisites for the full prerequisite list. +# + +$ErrorActionPreference = 'Stop' + +# Force TLS 1.2+ for downloads (Windows PowerShell 5.1 may default to 1.0/1.1 +# on older OS images, which fails against modern HTTPS endpoints). +[Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +$vsRoot = "${env:ProgramFiles(x86)}\Microsoft Visual Studio" +$vsInstallerDir = Join-Path $vsRoot 'Installer' +$vswherePath = Join-Path $vsInstallerDir 'vswhere.exe' +$setupExePath = Join-Path $vsInstallerDir 'setup.exe' + +$vswhereDownloadUrl = 'https://github.com/microsoft/vswhere/releases/latest/download/vswhere.exe' +$buildToolsDownloadUrl = 'https://aka.ms/vs/17/release/vs_BuildTools.exe' + +# The native VFS projects build against the v143 toolset, which ships with +# Visual Studio 2022 (product line 17.x). VS 2019 (16.x) carries v142 and +# is not sufficient -- so all vswhere queries below are scoped to 17.0+. +$minVsVersion = '[17.0,)' + +# Either of these workloads provides the C++ build tools we need. +# Microsoft.VisualStudio.Workload.NativeDesktop = "Desktop development with C++" (Community/Pro/Enterprise). +# Microsoft.VisualStudio.Workload.VCTools = "C++ build tools" (Build Tools). +$cppWorkloads = @( + 'Microsoft.VisualStudio.Workload.NativeDesktop', + 'Microsoft.VisualStudio.Workload.VCTools' +) + +function Get-VsWhere { + if (Test-Path $script:vswherePath) { + return $script:vswherePath + } + $dest = Join-Path $env:TEMP 'vswhere.exe' + Write-Host "vswhere.exe not found at '$script:vswherePath'; downloading from $script:vswhereDownloadUrl..." + Invoke-WebRequest -Uri $script:vswhereDownloadUrl -OutFile $dest -UseBasicParsing + Write-Host "Downloaded vswhere to: $dest" + return $dest +} + +function Find-VsInstall { + param( + [Parameter(Mandatory = $true)] [string] $VswhereExe, + [string[]] $RequiredWorkloads + ) + $vswhereArgs = @('-latest', '-prerelease', '-products', '*', '-version', $script:minVsVersion, '-format', 'json') + if ($RequiredWorkloads -and $RequiredWorkloads.Count -gt 0) { + $vswhereArgs += '-requires' + $vswhereArgs += $RequiredWorkloads + if ($RequiredWorkloads.Count -gt 1) { + $vswhereArgs += '-requiresAny' + } + } + $output = & $VswhereExe @vswhereArgs + if ($LASTEXITCODE -ne 0) { + throw "vswhere.exe failed with exit code $LASTEXITCODE" + } + if (-not $output) { return $null } + $installs = $output | ConvertFrom-Json + if (-not $installs -or $installs.Count -eq 0) { return $null } + return $installs[0] +} + +function Invoke-VsSetup { + param( + [string] $ExePath, + [string[]] $ArgumentList, + [string] $Description + ) + Write-Host "Running $Description : `"$ExePath`" $($ArgumentList -join ' ')" + $proc = Start-Process -FilePath $ExePath -ArgumentList $ArgumentList -Wait -PassThru -NoNewWindow + Write-Host "$Description exit code: $($proc.ExitCode)" + # 0 = success, 3010 = success but reboot required. + if ($proc.ExitCode -ne 0 -and $proc.ExitCode -ne 3010) { + throw "$Description failed with exit code $($proc.ExitCode)" + } +} + +# --- Locate or bootstrap vswhere --- +$vswhereExe = Get-VsWhere + +# --- Quick exit if a VS install with the C++ workload is already present --- +$existing = Find-VsInstall -VswhereExe $vswhereExe -RequiredWorkloads $cppWorkloads +if ($existing) { + Write-Host "VS install with C++ workload already present: $($existing.installationPath) ($($existing.productId))" + exit 0 +} + +# --- Find any VS install (regardless of workloads) --- +$install = Find-VsInstall -VswhereExe $vswhereExe + +# --- If no VS 2022+ install at all, install VS Build Tools 2022 with the VC workload --- +if (-not $install) { + Write-Host "No Visual Studio 2022 (or newer) installation found; installing VS Build Tools 2022 with the C++ workload..." + $bootstrapper = Join-Path $env:TEMP 'vs_BuildTools.exe' + Write-Host "Downloading VS Build Tools bootstrapper from $buildToolsDownloadUrl..." + Invoke-WebRequest -Uri $buildToolsDownloadUrl -OutFile $bootstrapper -UseBasicParsing + Write-Host "Downloaded bootstrapper to: $bootstrapper" + + Invoke-VsSetup -ExePath $bootstrapper -Description 'VS Build Tools install' -ArgumentList @( + '--add', 'Microsoft.VisualStudio.Workload.VCTools', + '--includeRecommended', + '--quiet', + '--norestart', + '--wait', + '--nocache' + ) +} else { + # --- Existing VS install without C++ workload: modify it --- + # Build Tools needs Microsoft.VisualStudio.Workload.VCTools; full Visual + # Studio (Community/Pro/Enterprise) needs Microsoft.VisualStudio.Workload.NativeDesktop. + $workload = if ($install.productId -eq 'Microsoft.VisualStudio.Product.BuildTools') { + 'Microsoft.VisualStudio.Workload.VCTools' + } else { + 'Microsoft.VisualStudio.Workload.NativeDesktop' + } + Write-Host "Existing VS install at $($install.installationPath) ($($install.productId)) lacks the C++ workload; adding '$workload'..." + + if (-not (Test-Path $setupExePath)) { + throw "Visual Studio Installer setup.exe not found at '$setupExePath'" + } + + Invoke-VsSetup -ExePath $setupExePath -Description 'VS Installer modify' -ArgumentList @( + 'modify', + '--installPath', $install.installationPath, + '--add', $workload, + '--includeRecommended', + '--quiet', + '--norestart', + '--wait', + '--nocache' + ) +} + +# --- Final verification: vswhere must now report an install with the workload --- +$verified = Find-VsInstall -VswhereExe $vswhereExe -RequiredWorkloads $cppWorkloads +if (-not $verified) { + throw "Workload install reported success but vswhere still does not report a VS install with the C++ workload" +} +Write-Host "VS install with C++ workload now present: $($verified.installationPath) ($($verified.productId))" From 0f30e3db10f2c167d8e0eb4c47c6c8dcb6921b8b Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 13 May 2026 14:54:37 +0100 Subject: [PATCH 08/10] .azure-pipelines: enable Projected File System for unit tests VFS for Git's runtime and several of its unit tests P/Invoke into ProjectedFSLib.dll (e.g. Microsoft.Windows.ProjFS.ProjFSNative .PrjDoesNameContainWildCards via ActiveEnumeration). That DLL is only present on disk when the 'Client-ProjFS' Windows optional feature is enabled. The 1ES win-x86_64-ado1es image used by the new release pipeline does not have that feature enabled, so RunUnitTests.bat fails with: System.DllNotFoundException : Unable to load DLL 'ProjectedFSLib.dll' or one of its dependencies. (0x8007007E) at Microsoft.Windows.ProjFS.ProjFSNative.PrjDoesNameContainWildCards(...) at GVFS.Platform.Windows.ActiveEnumeration.SaveFilter(...) at GVFS.UnitTests.Windows.Virtualization.ActiveEnumerationTests .CannotSetMoreThanOneFilter() Add a small PowerShell helper that enables the feature, and run it as a build prereq. The script is a no-op when the feature is already enabled, so it's safe to keep across image refreshes. Signed-off-by: Matthew John Cheetham --- .azure-pipelines/release.yml | 5 +++ .azure-pipelines/scripts/enable-projfs.ps1 | 45 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .azure-pipelines/scripts/enable-projfs.ps1 diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 88665a1be..28a217a6b 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -116,6 +116,11 @@ extends: inputs: filePath: $(Build.SourcesDirectory)\.azure-pipelines\scripts\install-vs-cpp-workload.ps1 + - task: PowerShell@2 + displayName: 'Enable Projected File System (ProjFS)' + inputs: + filePath: $(Build.SourcesDirectory)\.azure-pipelines\scripts\enable-projfs.ps1 + - script: | $(Build.SourcesDirectory)\scripts\Build.bat ^ $(BuildConfiguration) ^ diff --git a/.azure-pipelines/scripts/enable-projfs.ps1 b/.azure-pipelines/scripts/enable-projfs.ps1 new file mode 100644 index 000000000..e4836b737 --- /dev/null +++ b/.azure-pipelines/scripts/enable-projfs.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS + Enable the Windows Projected File System (ProjFS) optional feature. + +.DESCRIPTION + VFS for Git -- both the runtime and several unit tests -- P/Invokes + into ProjectedFSLib.dll (e.g. PrjDoesNameContainWildCards). That DLL + is only present on disk when the 'Client-ProjFS' Windows optional + feature is enabled. Hosted CI images do not enable it by default, + so unit tests fail with: + + System.DllNotFoundException : Unable to load DLL + 'ProjectedFSLib.dll' or one of its dependencies. + + This script is a no-op when the feature is already enabled. +#> + +#Requires -Version 5 +#Requires -RunAsAdministrator + +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' + +$featureName = 'Client-ProjFS' + +$feature = Get-WindowsOptionalFeature -Online -FeatureName $featureName -ErrorAction Stop +if ($feature.State -eq 'Enabled') { + Write-Host "INFO: Windows optional feature '$featureName' is already enabled." + exit 0 +} + +Write-Host "INFO: Enabling Windows optional feature '$featureName'..." +$result = Enable-WindowsOptionalFeature -Online -FeatureName $featureName -NoRestart -ErrorAction Stop + +if ($result.RestartNeeded) { + # The pipeline runs unit tests immediately after this script which P/Invoke + # into ProjectedFSLib.dll. If the OS reports a reboot is required to make + # the feature usable, the build agent is in an inconsistent state and the + # tests will fail unpredictably -- so fail fast here instead. + throw "Windows optional feature '$featureName' was enabled but a restart is required to take effect; failing the build." +} else { + Write-Host "INFO: Windows optional feature '$featureName' is now enabled." +} From c21ed310795b45ace372a40d79a985f24598e9e7 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 14 May 2026 10:48:27 +0100 Subject: [PATCH 09/10] GVFS.Payload: drop VS code-analysis marker files from payload Visual Studio's MSBuild for native C++ projects emits a .exe.lastcodeanalysissucceeded marker file next to each produced executable to record that PREfast / code analysis ran successfully on it. Those marker files are pure build-time bookkeeping with no runtime purpose, but the GVFS.Payload layout step xcopies the entire native bin\x64\\ folder for each of GitHooksLoader, GVFS.PostIndexChangedHook, GVFS.ReadObjectHook and GVFS.VirtualFileSystemHook -- so the markers end up in the Payload, get packaged into SetupGVFS.exe, and finally land on end-user machines as e.g. GitHooksLoader.exe.lastcodeanalysissucceeded in 'C:\Program Files\VFS for Git\'. Extend the existing layout.bat cleanup block (which already strips *.runtimeconfig.json, *.deps.json, and orphaned managed PDBs) to also recursively delete *.lastcodeanalysissucceeded from the output directory so they're never shipped. Signed-off-by: Matthew John Cheetham --- GVFS/GVFS.Payload/layout.bat | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GVFS/GVFS.Payload/layout.bat b/GVFS/GVFS.Payload/layout.bat index e1ff77270..fbaf9ea7c 100644 --- a/GVFS/GVFS.Payload/layout.bat +++ b/GVFS/GVFS.Payload/layout.bat @@ -59,6 +59,8 @@ RMDIR /S /Q %OUTPUT%\x86 REM Remove stray managed artifacts (AOT binaries don't need these) DEL /Q %OUTPUT%\*.runtimeconfig.json 2>nul DEL /Q %OUTPUT%\*.deps.json 2>nul +REM Remove VS C++ code-analysis marker files generated next to native exes +DEL /S /Q %OUTPUT%\*.lastcodeanalysissucceeded 2>nul REM Remove orphaned managed PDBs (these libraries are compiled into AOT exes) DEL /Q %OUTPUT%\GVFS.Common.pdb 2>nul DEL /Q %OUTPUT%\GVFS.Platform.Windows.pdb 2>nul From 6eaea92f643413f931e77d6ca6c60237e9d4db34 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 14 May 2026 10:53:02 +0100 Subject: [PATCH 10/10] .azure-pipelines: introduce esrp/sign.yml template Refactor the three EsrpCodeSigning@6 invocations in release.yml (Payload binaries, FastFetch, installer) to use a shared .azure-pipelines/esrp/sign.yml step template, modeled on the same template in microsoft/git. The template: * Forwards the per-call inputs (displayName, folderPath, pattern, inlineOperation) to EsrpCodeSigning@6. * Provides defaults for the ESRP connection parameters that point at the standard pipeline variables ($(esrpAppConnectionName), $(esrpClientId), etc.), so callers don't repeat them. * Runs an inline PowerShell@2 step right after each signing operation that removes the CodeSignSummary-.md report ESRP CLI drops into the signing folder. Without this, those .md files would otherwise end up packaged into SetupGVFS.exe (Payload), or uploaded as part of the FastFetch and Installer pipeline artifacts. Net effect on release.yml is a small reduction in line count and, more importantly, cleanup is no longer something a future caller can forget to wire up. Signed-off-by: Matthew John Cheetham --- .azure-pipelines/esrp/sign.yml | 59 ++++++++++++++++++++++++++++++++++ .azure-pipelines/release.yml | 45 ++++++-------------------- 2 files changed, 68 insertions(+), 36 deletions(-) create mode 100644 .azure-pipelines/esrp/sign.yml diff --git a/.azure-pipelines/esrp/sign.yml b/.azure-pipelines/esrp/sign.yml new file mode 100644 index 000000000..4e316076e --- /dev/null +++ b/.azure-pipelines/esrp/sign.yml @@ -0,0 +1,59 @@ +# Reusable step template for ESRP code signing via EsrpCodeSigning@6. +# +# Wraps a single signing operation with automatic cleanup of the +# CodeSignSummary-.md report ESRP CLI drops into the signing +# folder -- otherwise that file is packaged into the installer or +# uploaded as part of the pipeline artifact. +# +parameters: + - name: displayName + type: string + - name: folderPath + type: string + - name: pattern + type: string + - name: inlineOperation + type: string + # ESRP connection parameters (defaults use pipeline variables) + - name: connectedServiceName + type: string + default: $(esrpAppConnectionName) + - name: appRegistrationClientId + type: string + default: $(esrpClientId) + - name: appRegistrationTenantId + type: string + default: $(esrpTenantId) + - name: authAkvName + type: string + default: $(esrpKeyVaultName) + - name: authSignCertName + type: string + default: $(esrpSignReqCertName) + - name: serviceEndpointUrl + type: string + default: $(esrpEndpointUrl) + +steps: + - task: EsrpCodeSigning@6 + displayName: '${{ parameters.displayName }}' + inputs: + connectedServiceName: '${{ parameters.connectedServiceName }}' + useMSIAuthentication: true + appRegistrationClientId: '${{ parameters.appRegistrationClientId }}' + appRegistrationTenantId: '${{ parameters.appRegistrationTenantId }}' + authAkvName: '${{ parameters.authAkvName }}' + authSignCertName: '${{ parameters.authSignCertName }}' + serviceEndpointUrl: '${{ parameters.serviceEndpointUrl }}' + folderPath: '${{ parameters.folderPath }}' + pattern: '${{ parameters.pattern }}' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: ${{ parameters.inlineOperation }} + + - task: PowerShell@2 + displayName: 'Clean up code signing artifacts (${{ parameters.displayName }})' + inputs: + targetType: inline + script: | + Remove-Item -Force "${{ parameters.folderPath }}\CodeSignSummary-*.md" -ErrorAction SilentlyContinue diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 28a217a6b..84b9a2b80 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -143,16 +143,9 @@ extends: # The installer hasn't been built yet, so it can be packaged from # signed binaries in a single Inno Setup pass. - ${{ if eq(parameters.esrp, true) }}: - - task: EsrpCodeSigning@6 - displayName: 'Sign VFS for Git binaries' - inputs: - connectedServiceName: $(esrpAppConnectionName) - useMSIAuthentication: true - appRegistrationClientId: $(esrpClientId) - appRegistrationTenantId: $(esrpTenantId) - authAkvName: $(esrpKeyVaultName) - authSignCertName: $(esrpSignReqCertName) - serviceEndpointUrl: $(esrpEndpointUrl) + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Sign VFS for Git binaries' folderPath: $(OutDir)\GVFS.Payload\bin\$(BuildConfiguration)\win-x64 pattern: | GitHooksLoader.exe @@ -163,8 +156,6 @@ extends: GVFS.ReadObjectHook.exe GVFS.Service.exe GVFS.VirtualFileSystemHook.exe - useMinimatch: true - signConfigType: inlineSignParams inlineOperation: | [ { @@ -189,20 +180,11 @@ extends: } ] - - task: EsrpCodeSigning@6 - displayName: 'Sign FastFetch' - inputs: - connectedServiceName: $(esrpAppConnectionName) - useMSIAuthentication: true - appRegistrationClientId: $(esrpClientId) - appRegistrationTenantId: $(esrpTenantId) - authAkvName: $(esrpKeyVaultName) - authSignCertName: $(esrpSignReqCertName) - serviceEndpointUrl: $(esrpEndpointUrl) + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Sign FastFetch' folderPath: $(OutDir)\FastFetch\bin\$(BuildConfiguration)\net10.0-windows10.0.17763.0\win-x64\publish pattern: 'FastFetch.exe' - useMinimatch: true - signConfigType: inlineSignParams inlineOperation: | [ { @@ -240,20 +222,11 @@ extends: displayName: 'Build VFS for Git installer' - ${{ if eq(parameters.esrp, true) }}: - - task: EsrpCodeSigning@6 - displayName: 'Sign VFS for Git installer' - inputs: - connectedServiceName: $(esrpAppConnectionName) - useMSIAuthentication: true - appRegistrationClientId: $(esrpClientId) - appRegistrationTenantId: $(esrpTenantId) - authAkvName: $(esrpKeyVaultName) - authSignCertName: $(esrpSignReqCertName) - serviceEndpointUrl: $(esrpEndpointUrl) + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Sign VFS for Git installer' folderPath: $(OutDir)\GVFS.Installers\bin\$(BuildConfiguration)\win-x64 pattern: 'SetupGVFS.*.exe' - useMinimatch: true - signConfigType: inlineSignParams inlineOperation: | [ {