From 79971fc9b36056d1a04e32e035c58ad1fcb33682 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 30 Apr 2026 12:16:59 +0100 Subject: [PATCH 1/8] amend! azure-pipelines: add stub release pipeline for Azure azure-pipelines: add stub release pipeline for Azure Add a release-pipeline scaffold for microsoft/git on Azure Pipelines, structured around a prereqs stage, a per-platform build stage with placeholder jobs, and a release stage that downloads the build artifacts and publishes them to a draft GitHub release. The per-platform build, signing, and validation logic lands in subsequent commits. The pipeline targets Microsoft-internal 1ES-hosted images across Windows x64, Windows ARM64, macOS, Ubuntu x64, and Ubuntu ARM64. Windows and Linux matrix entries carry a `poolArch` dimension because the 1ES hosted pools select their image from `hostArchitecture`; an arm64 entry on a default x64 pool would silently grab the wrong image. macOS uses the same matrix-parameter shape as Windows and Linux, so future macOS variants drop in the same way an extra Windows toolchain would. The prereqs stage derives the Git version, tag name, and tag SHA via resolve-version.sh and exposes them as pipeline variables for downstream stages to pick up. For that walk to find tags at all, the prereqs checkout uses fetchDepth: 0 / fetchTags: true. setup-git-bash.cmd is the Windows-side prerequisite that prepends Git Bash to PATH, since the bare hosted image does not provide a bash for Bash@3 tasks to find. ESRP signing to be added later. Signed-off-by: Matthew John Cheetham Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 150 +++++++++++------- .azure-pipelines/scripts/resolve-version.sh | 48 ++++++ .../scripts/windows/setup-git-bash.cmd | 13 ++ 3 files changed, 156 insertions(+), 55 deletions(-) create mode 100755 .azure-pipelines/scripts/resolve-version.sh create mode 100644 .azure-pipelines/scripts/windows/setup-git-bash.cmd diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 0e0ce01ac71978..c6069ea20d29f6 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -26,6 +26,7 @@ parameters: - id: windows_x64 jobName: 'Windows (x64)' pool: GitClientPME-1ESHostedPool-intel-pc + poolArch: amd64 image: win-x86_64-ado1es os: windows toolchain: x86_64 @@ -34,13 +35,24 @@ parameters: - id: windows_arm64 jobName: 'Windows (ARM64)' pool: GitClientPME-1ESHostedPool-arm64-pc + poolArch: arm64 image: win-arm64-ado1es os: windows toolchain: clang-aarch64 mingwprefix: clangarm64 - # No matrix for macOS as we build both x64 and ARM64 in the same job - # and produce a universal binary. + - name: macos_matrix + type: object + default: + - id: macos_universal + jobName: 'macOS (x64 + ARM64)' + pool: 'Azure Pipelines' + # macOS-latest is an Intel x86_64 Mac Pro, which can't host the + # arm64 Homebrew the universal-binary build needs. We need to + # explictly target the newer Apple Silicon machines that can run + # both x86_64 and arm64 builds. + image: macOS-15-arm64 + os: macos - name: linux_matrix type: object @@ -48,6 +60,7 @@ parameters: - id: linux_x64 jobName: 'Linux (x64)' pool: GitClientPME-1ESHostedPool-intel-pc + poolArch: amd64 image: ubuntu-x86_64-ado1es os: linux cc_arch: x86_64 @@ -56,6 +69,7 @@ parameters: - id: linux_arm64 jobName: 'Linux (ARM64)' pool: GitClientPME-1ESHostedPool-arm64-pc + poolArch: arm64 image: ubuntu-arm64-ado1es os: linux cc_arch: aarch64 @@ -85,20 +99,15 @@ extends: image: ubuntu-x86_64-ado1es os: linux steps: + - checkout: self + fetchDepth: 0 + fetchTags: true - task: Bash@3 displayName: 'Resolve version and tag information' name: info inputs: - targetType: inline - script: | - # TODO: determine git_version, tag_name, and tag_sha - # TODO: error if the current commit is not an annotated tag - git_version=TODO_GITVER - tag_name=TODO_TAGNAME - tag_sha=TODO_TAGSHA - echo "##vso[task.setvariable variable=git_version;isOutput=true;isReadOnly=true]$git_version" - echo "##vso[task.setvariable variable=tag_name;isOutput=true;isReadOnly=true]$tag_name" - echo "##vso[task.setvariable variable=tag_sha;isOutput=true;isReadOnly=true]$tag_sha" + targetType: filePath + filePath: .azure-pipelines/scripts/resolve-version.sh - stage: build displayName: 'Build' @@ -114,6 +123,7 @@ extends: name: ${{ dim.pool }} image: ${{ dim.image }} os: ${{ dim.os }} + hostArchitecture: ${{ dim.poolArch }} variables: tag_name: $[stageDependencies.prereqs.prebuild.outputs['info.tag_name']] tag_sha: $[stageDependencies.prereqs.prebuild.outputs['info.tag_sha']] @@ -127,44 +137,60 @@ extends: artifactName: '${{ dim.id }}' steps: - checkout: self + # Add Git Bash to the PATH so Bash tasks can find it + - task: BatchScript@1 + displayName: 'Add Git Bash to PATH' + inputs: + filename: ./.azure-pipelines/scripts/windows/setup-git-bash.cmd # TODO: add tasks to set up Git for Windows SDK # TODO: add tasks to build Git and installers - script: | echo $(mingwprefix) echo $(toolchain) + mkdir $(Build.ArtifactStagingDirectory)\app + copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example1.exe + copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example2.exe + copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example3.exe displayName: 'Dummy build' # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - script: | - echo "TODO" > $(Build.ArtifactStagingDirectory)/_final/placeholder.txt + mkdir $(Build.ArtifactStagingDirectory)\_final + xcopy /s /y $(Build.ArtifactStagingDirectory)\app $(Build.ArtifactStagingDirectory)\_final + displayName: 'Dummy collect artifacts' # - # macOS build job (universal) + # macOS build jobs # - - job: macos_universal - displayName: 'macOS (x64 + ARM64)' - pool: - name: 'Azure Pipelines' - image: macOS-latest - os: macos - variables: - tag_name: $[stageDependencies.prereqs.prebuild.outputs['info.tag_name']] - tag_sha: $[stageDependencies.prereqs.prebuild.outputs['info.tag_sha']] - git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] - templateContext: - outputs: - - output: pipelineArtifact - targetPath: '$(Build.ArtifactStagingDirectory)/_final' - artifactName: 'macos_universal' - steps: - - checkout: self - # TODO: add tasks to set up build environment - # TODO: add tasks to build Git and installers - - script: | - echo "Hello, Mac!" - displayName: 'Dummy build' - # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - - script: | - echo "TODO" > $(Build.ArtifactStagingDirectory)/_final/placeholder.txt + - ${{ each dim in parameters.macos_matrix }}: + - job: ${{ dim.id }} + displayName: ${{ dim.jobName }} + pool: + name: ${{ dim.pool }} + image: ${{ dim.image }} + os: ${{ dim.os }} + variables: + tag_name: $[stageDependencies.prereqs.prebuild.outputs['info.tag_name']] + tag_sha: $[stageDependencies.prereqs.prebuild.outputs['info.tag_sha']] + git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] + templateContext: + outputs: + - output: pipelineArtifact + targetPath: '$(Build.ArtifactStagingDirectory)/_final' + artifactName: '${{ dim.id }}' + steps: + - checkout: self + # TODO: add tasks to set up build environment + # TODO: add tasks to build Git and installers + - script: | + echo "Hello, Mac!" + mkdir -p $(Build.ArtifactStagingDirectory)/app + cp /bin/echo $(Build.ArtifactStagingDirectory)/app/example + displayName: 'Dummy build' + # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final + - script: | + mkdir -p $(Build.ArtifactStagingDirectory)/_final + cp -R $(Build.ArtifactStagingDirectory)/app/* $(Build.ArtifactStagingDirectory)/_final/ + displayName: 'Dummy collect artifacts' # # Linux build jobs @@ -176,6 +202,7 @@ extends: name: ${{ dim.pool }} image: ${{ dim.image }} os: ${{ dim.os }} + hostArchitecture: ${{ dim.poolArch }} variables: tag_name: $[stageDependencies.prereqs.prebuild.outputs['info.tag_name']] tag_sha: $[stageDependencies.prereqs.prebuild.outputs['info.tag_sha']] @@ -194,15 +221,31 @@ extends: - script: | echo $(cc_arch) echo $(deb_arch) + mkdir -p $(Build.ArtifactStagingDirectory)/app + debroot=$(Build.ArtifactStagingDirectory)/pkgroot + mkdir -p $debroot/DEBIAN + cat > $debroot/DEBIAN/control < $(Build.ArtifactStagingDirectory)/_final/placeholder.txt + mkdir -p $(Build.ArtifactStagingDirectory)/_final + cp -R $(Build.ArtifactStagingDirectory)/app/* $(Build.ArtifactStagingDirectory)/_final/ + displayName: 'Dummy collect artifacts' - stage: release displayName: 'Release' dependsOn: [prereqs, build] jobs: + # + # GitHub release publishing + # - job: github displayName: 'Publish GitHub release' condition: and(succeeded(), eq('${{ parameters.github }}', true)) @@ -218,21 +261,18 @@ extends: type: releaseJob isProduction: true inputs: - - input: pipelineArtifact - artifactName: 'windows_x64' - targetPath: $(Pipeline.Workspace)/assets/windows_x64 - - input: pipelineArtifact - artifactName: 'windows_arm64' - targetPath: $(Pipeline.Workspace)/assets/windows_arm64 - - input: pipelineArtifact - artifactName: 'macos_universal' - targetPath: $(Pipeline.Workspace)/assets/macos_universal - - input: pipelineArtifact - artifactName: 'linux_x64' - targetPath: $(Pipeline.Workspace)/assets/linux_x64 - - input: pipelineArtifact - artifactName: 'linux_arm64' - targetPath: $(Pipeline.Workspace)/assets/linux_arm64 + - ${{ each dim in parameters.windows_matrix }}: + - input: pipelineArtifact + artifactName: '${{ dim.id }}' + targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} + - ${{ each dim in parameters.macos_matrix }}: + - input: pipelineArtifact + artifactName: '${{ dim.id }}' + targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} + - ${{ each dim in parameters.linux_matrix }}: + - input: pipelineArtifact + artifactName: '${{ dim.id }}' + targetPath: $(Pipeline.Workspace)/assets/${{ dim.id }} steps: - task: GitHubRelease@1 displayName: 'Create Draft GitHub Release' diff --git a/.azure-pipelines/scripts/resolve-version.sh b/.azure-pipelines/scripts/resolve-version.sh new file mode 100755 index 00000000000000..6169ae767bae1f --- /dev/null +++ b/.azure-pipelines/scripts/resolve-version.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Resolve version and tag information from the current HEAD commit. +# Validates that HEAD is an annotated version tag matching GIT-VERSION-GEN. +# +# Sets the following ADO output variables (via ##vso): +# git_version - Version string without "v" prefix (e.g., 2.53.0.vfs.0.0) +# tag_name - Full tag name (e.g., v2.53.0.vfs.0.0) +# tag_sha - Commit SHA of HEAD +# +# Also updates the build number to include the tag name. +# +set -euo pipefail + +echo "HEAD: $(git rev-parse HEAD)" + +# Determine the tag pointing at HEAD +tag_name=$(git describe --exact-match --match "v[0-9]*vfs*" HEAD 2>/dev/null) || { + echo "##vso[task.logissue type=error]HEAD is not tagged with a version tag" + exit 1 +} + +# Verify the tag is annotated (not lightweight) +tag_type=$(git cat-file -t "refs/tags/$tag_name") +if [ "$tag_type" != "tag" ]; then + echo "##vso[task.logissue type=error]Tag $tag_name is not annotated (type: $tag_type)" + exit 1 +fi + +tag_sha=$(git rev-parse HEAD) +git_version="${tag_name#v}" + +# Verify the version matches GIT-VERSION-GEN +make GIT-VERSION-FILE +expected_version="${git_version//-rc/.rc}" +actual_version=$(sed -n 's/^GIT_VERSION *= *//p' < GIT-VERSION-FILE) +if [ "$expected_version" != "$actual_version" ]; then + echo "##vso[task.logissue type=error]GIT-VERSION-FILE ($actual_version) does not match tag $tag_name ($expected_version)" + exit 1 +fi + +echo "Git version: $git_version" +echo "Tag name: $tag_name" +echo "Tag SHA: $tag_sha" +echo "##vso[task.setvariable variable=git_version;isOutput=true;isReadOnly=true]$git_version" +echo "##vso[task.setvariable variable=tag_name;isOutput=true;isReadOnly=true]$tag_name" +echo "##vso[task.setvariable variable=tag_sha;isOutput=true;isReadOnly=true]$tag_sha" +echo "##vso[build.updatebuildnumber]${tag_name} (${BUILD_BUILDNUMBER:-unknown})" diff --git a/.azure-pipelines/scripts/windows/setup-git-bash.cmd b/.azure-pipelines/scripts/windows/setup-git-bash.cmd new file mode 100644 index 00000000000000..b3ef5518cfc85d --- /dev/null +++ b/.azure-pipelines/scripts/windows/setup-git-bash.cmd @@ -0,0 +1,13 @@ +@echo off +setlocal enabledelayedexpansion +set "agentgit=%AGENT_HOMEDIRECTORY%\externals\git" +set "gitcopy=%AGENT_TEMPDIRECTORY%\git" +echo Copying !agentgit! to !gitcopy!... +xcopy /E /I /Q "!agentgit!" "!gitcopy!" +if not exist "!gitcopy!\usr\bin\sh.exe" ( + echo ##vso[task.logissue type=error]Could not find sh.exe at !gitcopy!\usr\bin\sh.exe + exit /b 1 +) +echo Copying !gitcopy!\usr\bin\sh.exe to !gitcopy!\usr\bin\bash.exe... +copy /Y "!gitcopy!\usr\bin\sh.exe" "!gitcopy!\usr\bin\bash.exe" +echo ##vso[task.prependpath]!gitcopy!\usr\bin From e782c90efdd80e96808dfc581f304406205ebe3e Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 30 Apr 2026 12:27:46 +0100 Subject: [PATCH 2/8] azure-pipelines: add ESRP code signing Bring ESRP code signing into the release pipeline, gated behind an `esrp` boolean parameter that defaults to false until the rest of the signing wiring catches up. Microsoft policy precludes shipping unsigned binaries from this pipeline, so every per-platform build job needs an obvious place to plug signing in. The Windows flow runs through a custom esrpsign.sh script rather than the EsrpCodeSigning ADO task. The custom script gives the later Windows installer commit a CLI-shaped seam it can register as Git for Windows' `git signtool` alias from build-extra's release pipeline, so every binary embedded inside the installer gets signed rather than only the outer .exe wrapper. The canned ADO task does not expose that integration point. The accompanying setup template uses AzureCLI@2 to bind to the WIF service connection by name rather than by GUID, and relies on addSpnToEnvironment to surface the service principal ID, tenant ID, and connection GUID at runtime via ENDPOINT_URL_* env vars. That way esrpsign.sh composes the auth JSON with no hardcoded identifiers leaking into the repository. EsrpClientTool@4 takes care of downloading and caching the ESRP client binary itself. macOS and Linux take the simpler path: the EsrpCodeSigning@6 task via a shared sign.yml template. macOS in particular requires an archive submission (useArchive: true), so centralising the copy/zip/sign/extract cycle in the template keeps each platform job from re-implementing it. The Linux hosted agents do not ship with .NET, which EsrpCodeSigning requires. UseDotNet@2 installs the .NET 8 SDK ahead of the signing template invocation so Linux signing works out of the box without per-platform plumbing. Signed-off-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- .azure-pipelines/esrp/sign.yml | 106 ++++++++++++ .azure-pipelines/esrp/windows/esrpsign.sh | 198 ++++++++++++++++++++++ .azure-pipelines/esrp/windows/setup.yml | 69 ++++++++ .azure-pipelines/release.yml | 82 +++++++++ 4 files changed, 455 insertions(+) create mode 100644 .azure-pipelines/esrp/sign.yml create mode 100755 .azure-pipelines/esrp/windows/esrpsign.sh create mode 100644 .azure-pipelines/esrp/windows/setup.yml diff --git a/.azure-pipelines/esrp/sign.yml b/.azure-pipelines/esrp/sign.yml new file mode 100644 index 00000000000000..b4d14d2713ee8f --- /dev/null +++ b/.azure-pipelines/esrp/sign.yml @@ -0,0 +1,106 @@ +# Reusable step template for ESRP code signing via EsrpCodeSigning@6. +# +# For macOS, ESRP requires files to be submitted as a zip archive. +# Set 'useArchive: true' to automatically handle the +# copy → zip → sign → extract cycle. For Windows/Linux where ESRP +# can sign files directly in a folder, leave it as false (default). +# +parameters: + - name: displayName + type: string + - name: folderPath + type: string + - name: pattern + type: string + - name: inlineOperation + type: string + # When true, matching files are copied to a staging dir, zipped, + # signed, and extracted back to folderPath. + - name: useArchive + type: boolean + default: false + # 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: + - ${{ if eq(parameters.useArchive, true) }}: + - task: DeleteFiles@1 + displayName: 'Clean staging dir for ${{ parameters.displayName }}' + inputs: + SourceFolder: '$(Agent.TempDirectory)/esrp-staging' + Contents: '*' + RemoveSourceFolder: true + - task: CopyFiles@2 + displayName: 'Collect files for ${{ parameters.displayName }}' + inputs: + SourceFolder: '${{ parameters.folderPath }}' + Contents: '${{ parameters.pattern }}' + TargetFolder: '$(Agent.TempDirectory)/esrp-staging/contents' + - task: ArchiveFiles@2 + displayName: 'Archive files for ${{ parameters.displayName }}' + inputs: + rootFolderOrFile: '$(Agent.TempDirectory)/esrp-staging/contents' + includeRootFolder: false + archiveType: zip + archiveFile: '$(Agent.TempDirectory)/esrp-staging/archive.zip' + - 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: '$(Agent.TempDirectory)/esrp-staging' + pattern: 'archive.zip' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: ${{ parameters.inlineOperation }} + - task: ExtractFiles@1 + displayName: 'Extract signed files for ${{ parameters.displayName }}' + inputs: + archiveFilePatterns: '$(Agent.TempDirectory)/esrp-staging/archive.zip' + destinationFolder: '${{ parameters.folderPath }}' + overwriteExistingFiles: true + - task: DeleteFiles@1 + displayName: 'Clean up staging dir for ${{ parameters.displayName }}' + condition: always() + inputs: + SourceFolder: '$(Agent.TempDirectory)/esrp-staging' + Contents: '*' + RemoveSourceFolder: true + - ${{ else }}: + - 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 }} diff --git a/.azure-pipelines/esrp/windows/esrpsign.sh b/.azure-pipelines/esrp/windows/esrpsign.sh new file mode 100755 index 00000000000000..a3bf1bc66ea4f8 --- /dev/null +++ b/.azure-pipelines/esrp/windows/esrpsign.sh @@ -0,0 +1,198 @@ +#!/bin/bash +# +# Sign Windows files using the ESRP client (Authenticode). +# Usage: esrpsign.sh [file2 ...] +# +# Required environment variables: +# ESRP_TOOL - Path to ESRPClient.exe +# ESRP_AUTH - Path to the ESRP auth JSON file +# SYSTEM_ACCESSTOKEN - ADO system access token (OAuth bearer) +# +# Optional environment variables: +# ESRP_KEYCODE - Signing key code (default: CP-231522) +# +# The script generates the auth and input JSON files and sets the +# following ESRP client environment variables automatically: +# ESRP_AUTH_CONFIG - Path to the auth JSON file +# ESRP_POLICY_CONFIG - Path to the policy JSON file +# ESRP_SESSION_CONFIG - Not set; ESRP client defaults are used +# +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "usage: esrpsign.sh [file ...]" >&2 + exit 1 +fi + +if [ -z "${ESRP_TOOL:-}" ]; then + echo "error: ESRP_TOOL environment variable must be set" >&2 + exit 1 +fi +if [ -z "${ESRP_AUTH:-}" ]; then + echo "error: ESRP_AUTH environment variable must be set" >&2 + exit 1 +fi +if [ -z "${SYSTEM_ACCESSTOKEN:-}" ]; then + echo "error: SYSTEM_ACCESSTOKEN environment variable must be set" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Check for overriden key code, otherwise use default (Microsoft Third-Party/OSS) +ESRP_KEYCODE="${ESRP_KEYCODE:-CP-231522}" + +# Create work dir and resolve its Windows path by cd-ing into it. +WORK_DIR="$(mktemp -d)" +WORK_DIR_WIN="$(cd "$WORK_DIR" && pwd -W | sed 's|/|\\|g')" + +echo "==> ESRP signing tool: $ESRP_TOOL" +echo "==> Working directory: $WORK_DIR" + +if [ ! -f "$ESRP_TOOL" ]; then + echo "error: ESRPClient.exe not found at $ESRP_TOOL" >&2 + exit 1 +fi + +# Convert an MSYS2 path to Windows format for ESRPClient.exe. +to_windows_path () { + # Prefer cygpath if available (full Git for Windows) + if command -v cygpath >/dev/null 2>&1; then + cygpath -w "$1" + return + fi + case "$1" in + /[a-zA-Z]/*) + # Drive path: /d/path -> D:\path + drive=$(echo "$1" | cut -c2 | tr 'a-z' 'A-Z') + rest=$(echo "$1" | cut -c3-) + echo "${drive}:${rest}" | sed 's|/|\\|g' + ;; + /*) + # Absolute path under MSYS2 root + root=$(cd / && pwd -W) + echo "${root}${1}" | sed 's|/|\\|g' + ;; + # Relative or already-Windows path: just flip slashes + *) + echo "$1" | sed 's|/|\\|g' + ;; + esac +} + +# Build the SignRequestFiles JSON array +echo "==> Preparing files for signing ($# file(s))..." +files_json="" +for file in "$@"; do + if [ ! -f "$file" ]; then + echo "error: file not found: $file" >&2 + exit 1 + fi + + abs_path="$(cd "$(dirname "$file")" && pwd)/$(basename "$file")" + win_path="$(to_windows_path "$abs_path")" + # Escape backslashes for JSON + win_path_escaped="${win_path//\\/\\\\}" + echo " - $win_path" + + if [ -n "$files_json" ]; then + files_json+="," + fi + files_json+=" + { + \"SourceLocation\": \"$win_path_escaped\", + \"DestinationLocation\": \"$win_path_escaped\" + }" +done + +# Generate the input JSON +input_json="$WORK_DIR/input.json" +output_json="$WORK_DIR/output.json" + +echo "==> Generating input JSON: $input_json" +cat > "$input_json" <<-EOF + { + "Version": "1.0.0", + "SignBatches": [ + { + "SourceLocationType": "UNC", + "DestinationLocationType": "UNC", + "SignRequestFiles": [$files_json + ], + "SigningInfo": { + "Operations": [ + { + "KeyCode": "$ESRP_KEYCODE", + "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": "$ESRP_KEYCODE", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} + } + ] + } + } + ] + } +EOF + +# Generate policy JSON +echo "==> Generating policy JSON..." +policy_json="$WORK_DIR/policy.json" +cat > "$policy_json" <<-EOF + { + "Version": "1.0.0", + "Intent": "ProductRelease", + "ContentType": "Binaries", + "ContentOrigin": "1stParty", + "ProductState": "Current", + "Audience": "ExternalBroad" + } +EOF + +# Use auth JSON from ESRP_AUTH +export ESRP_AUTH_CONFIG="$(to_windows_path "$ESRP_AUTH")" +export ESRP_POLICY_CONFIG="$WORK_DIR_WIN\\policy.json" + +# The ADO system access token is referenced in the auth JSON via the environment +# variable - export this so the ESRP client can pick it up when it runs. +export SYSTEM_ACCESSTOKEN + +# Print generated JSON files for debugging +echo "==> Auth JSON:" +cat "$ESRP_AUTH" +echo "" +echo "==> Policy JSON:" +cat "$policy_json" +echo "" +echo "==> Input JSON:" +cat "$input_json" +echo "" + +# Sign the files +esrp_tool_win="$(to_windows_path "$ESRP_TOOL")" +input_json_win="$WORK_DIR_WIN\\input.json" +output_json_win="$WORK_DIR_WIN\\output.json" + +echo "==> ESRP_AUTH_CONFIG=$ESRP_AUTH_CONFIG" +echo "==> ESRP_POLICY_CONFIG=$ESRP_POLICY_CONFIG" +echo "==> Running: $esrp_tool_win sign -i $input_json_win -o $output_json_win" +"$esrp_tool_win" sign \ + -i "$input_json_win" \ + -o "$output_json_win" + +echo "==> Signing complete." +echo "==> Output JSON:" +cat "$output_json" diff --git a/.azure-pipelines/esrp/windows/setup.yml b/.azure-pipelines/esrp/windows/setup.yml new file mode 100644 index 00000000000000..c7eb655c1586c2 --- /dev/null +++ b/.azure-pipelines/esrp/windows/setup.yml @@ -0,0 +1,69 @@ +parameters: + - name: serviceConnectionName + type: string + - name: esrpClientId + type: string + - name: keyVaultName + type: string + - name: signCertName + type: string + +steps: + - task: EsrpClientTool@5 + name: esrpinstall + displayName: 'Install ESRP client' + - task: AzureCLI@2 + displayName: 'Set up ESRP environment' + inputs: + azureSubscription: ${{ parameters.serviceConnectionName }} + addSpnToEnvironment: true + scriptType: ps + scriptLocation: inlineScript + inlineScript: | + # Resolve ESRP client tool path (passed via env to avoid PS subexpression issues) + $esrpTool = "$env:ESRPCLIENT_TOOLPATH\$env:ESRPCLIENT_TOOLNAME" + if (-not (Test-Path $esrpTool)) { Write-Error "ESRPClient.exe not found at $esrpTool"; exit 1 } + Write-Host "Found ESRP client: $esrpTool" + Write-Host "##vso[task.setvariable variable=ESRP_TOOL]$esrpTool" + + # Derive the service connection GUID from the ENDPOINT_URL_* env vars + # that the agent emits for the bound connection. Filter out the + # built-in SystemVssConnection which is always present. + $scId = (Get-ChildItem env:ENDPOINT_URL_*).Name ` + -replace '^ENDPOINT_URL_','' | + Where-Object { $_ -ne 'SYSTEMVSSCONNECTION' } + if (-not $scId) { Write-Error "Could not derive service connection GUID"; exit 1 } + Write-Host "Resolved service connection GUID: $scId" + + # servicePrincipalId and tenantId are provided by addSpnToEnvironment + $authJson = @{ + Version = "1.0.0" + AuthenticationType = "AAD_MSI_WIF" + EsrpClientId = "${{ parameters.esrpClientId }}" + ClientId = $env:servicePrincipalId + TenantId = $env:tenantId + AADAuthorityBaseUri = "https://login.microsoftonline.com/" + FederatedTokenData = @{ + JobId = "$(System.JobId)" + PlanId = "$(System.PlanId)" + ProjectId = "$(System.TeamProjectId)" + Hub = "$(System.HostType)" + Uri = "$(System.CollectionUri)" + ServiceConnectionId = $scId + SystemAccessToken = "SYSTEM_ACCESSTOKEN" + } + RequestSigningCert = @{ + GetCertFromKeyVault = $true + KeyVaultName = "${{ parameters.keyVaultName }}" + KeyVaultCertName = "${{ parameters.signCertName }}" + } + } | ConvertTo-Json -Depth 4 + + $authPath = "$(Agent.TempDirectory)\esrp-auth.json" + $authJson | Set-Content -Path $authPath -Encoding UTF8 + Write-Host "Generated ESRP auth JSON: $authPath" + Write-Host "##vso[task.setvariable variable=ESRP_AUTH]$authPath" + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + ESRPCLIENT_TOOLPATH: $(esrpinstall.esrpclient.toolpath) + ESRPCLIENT_TOOLNAME: $(esrpinstall.esrpclient.toolname) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index c6069ea20d29f6..c314b787a8ed8a 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -10,6 +10,10 @@ resources: ref: refs/tags/release parameters: + - name: 'esrp' + type: boolean + default: false # TODO: change default to true after testing + displayName: 'Enable ESRP code signing' - name: 'github' type: boolean default: false # TODO: change default to true after testing @@ -76,8 +80,17 @@ parameters: deb_arch: arm64 variables: + - name: 'esrpAppConnectionName' + value: '1ESGitClient-ESRP-App' - name: 'githubConnectionName' value: 'GitHub-MicrosoftGit' + # ESRP signing variables set in the pipeline settings: + # - esrpEndpointUrl + # - esrpMI + # - esrpClientId + # - esrpTenantId + # - esrpKeyVaultName + # - esrpSignReqCertName extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelines @@ -142,6 +155,14 @@ extends: displayName: 'Add Git Bash to PATH' inputs: filename: ./.azure-pipelines/scripts/windows/setup-git-bash.cmd + # Setup ESRP code signing for Windows (sets ESRP_TOOL, ESRP_AUTH) + - ${{ if eq(parameters.esrp, true) }}: + - template: .azure-pipelines/esrp/windows/setup.yml@self + parameters: + serviceConnectionName: $(esrpAppConnectionName) + esrpClientId: $(esrpClientId) + keyVaultName: $(esrpKeyVaultName) + signCertName: $(esrpSignReqCertName) # TODO: add tasks to set up Git for Windows SDK # TODO: add tasks to build Git and installers - script: | @@ -152,6 +173,25 @@ extends: copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example2.exe copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example3.exe displayName: 'Dummy build' + # + # To sign Windows binaries with ESRP, call esrpsign.sh + # with the files to sign as arguments. Requires the + # following environment variables to be set: + # ESRP_TOOL - set by the setup template above + # ESRP_AUTH - set by the setup template above + # SYSTEM_ACCESSTOKEN - $(System.AccessToken) + # + - ${{ if eq(parameters.esrp, true) }}: + - bash: | + .azure-pipelines/esrp/windows/esrpsign.sh \ + "$BUILD_ARTIFACTSTAGINGDIRECTORY/app/example1.exe" \ + "$BUILD_ARTIFACTSTAGINGDIRECTORY/app/example2.exe" \ + "$BUILD_ARTIFACTSTAGINGDIRECTORY/app/example3.exe" + displayName: 'Example ESRP signing' + env: + ESRP_TOOL: $(ESRP_TOOL) + ESRP_AUTH: $(ESRP_AUTH) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - script: | mkdir $(Build.ArtifactStagingDirectory)\_final @@ -186,6 +226,25 @@ extends: mkdir -p $(Build.ArtifactStagingDirectory)/app cp /bin/echo $(Build.ArtifactStagingDirectory)/app/example displayName: 'Dummy build' + - ${{ if eq(parameters.esrp, true) }}: + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Example sign binaries' + folderPath: '$(Build.ArtifactStagingDirectory)/app' + pattern: '**/*' + useArchive: true # Must be true when macOS signing + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppDeveloperSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "Hardening": "Enable" + } + } + ] # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - script: | mkdir -p $(Build.ArtifactStagingDirectory)/_final @@ -233,6 +292,29 @@ extends: CTRL dpkg-deb --build $debroot $(Build.ArtifactStagingDirectory)/app/example_$(deb_arch).deb displayName: 'Dummy build' + - ${{ if eq(parameters.esrp, true) }}: + # ESRP ADO tasks require .NET, so we install it here since the + # Linux images do not have it by default. + - task: UseDotNet@2 + displayName: 'Install .NET for ESRP' + inputs: + packageType: sdk + version: '8.x' + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Example sign Debian package' + folderPath: '$(Build.ArtifactStagingDirectory)/app' + pattern: '**/*.deb' + inlineOperation: | + [ + { + "KeyCode": "CP-453387-Pgp", + "OperationCode": "LinuxSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} + } + ] # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - script: | mkdir -p $(Build.ArtifactStagingDirectory)/_final From 938529cc437b29cba0bc8148bbbd58381f2e7a9e Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 15 May 2026 10:33:21 +0100 Subject: [PATCH 3/8] azure-pipelines: allow overriding Git version Smoke tests and dry runs of the pipeline itself need to build microsoft/git from an untagged tip, where resolve-version.sh's tag walk would either fail or pick up an unrelated tag. Add a queue-time `versionOverride` parameter; when it is set to anything other than the empty string or the sentinel `-`, the prereqs stage emits the override verbatim as the build version, labels the tag name as `untagged`, and bypasses resolve-version.sh entirely. A build from an untagged commit must not race a real release upload, so a non-empty override also forces the GitHub publishing job off regardless of the `github` parameter, and the prereqs step logs a warning to make that consequence visible in the run summary. Signed-off-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index c314b787a8ed8a..43969c40d56957 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -18,6 +18,10 @@ parameters: type: boolean default: false # TODO: change default to true after testing displayName: 'Enable GitHub release publishing' + - name: 'versionOverride' + type: string + default: '-' + displayName: 'Version override (release publishing is skipped if set)' # # 1ES Pipeline Templates do not allow using a matrix strategy so we create @@ -115,12 +119,29 @@ extends: - checkout: self fetchDepth: 0 fetchTags: true - - task: Bash@3 - displayName: 'Resolve version and tag information' - name: info - inputs: - targetType: filePath - filePath: .azure-pipelines/scripts/resolve-version.sh + - ${{ if or(eq(parameters.versionOverride, ''), eq(parameters.versionOverride, '-')) }}: + - task: Bash@3 + displayName: 'Resolve version and tag information' + name: info + inputs: + targetType: filePath + filePath: .azure-pipelines/scripts/resolve-version.sh + - ${{ if and(ne(parameters.versionOverride, ''), ne(parameters.versionOverride, '-')) }}: + - task: Bash@3 + displayName: 'Set version override information' + name: info + inputs: + targetType: inline + script: | + tag_sha=$(git rev-parse HEAD) + echo "##vso[task.logissue type=warning]Using version override: ${{ parameters.versionOverride }}. Release publishing will be skipped." + echo "Git version: ${{ parameters.versionOverride }}" + echo "Tag name: untagged" + echo "Tag SHA: ${tag_sha}" + echo "##vso[task.setvariable variable=git_version;isOutput=true;isReadOnly=true]${{ parameters.versionOverride }}" + echo "##vso[task.setvariable variable=tag_name;isOutput=true;isReadOnly=true]untagged" + echo "##vso[task.setvariable variable=tag_sha;isOutput=true;isReadOnly=true]${tag_sha}" + echo "##vso[build.updatebuildnumber][UNTAGGED] ${tag_sha} (${BUILD_BUILDNUMBER:-unknown})" - stage: build displayName: 'Build' @@ -330,7 +351,7 @@ extends: # - job: github displayName: 'Publish GitHub release' - condition: and(succeeded(), eq('${{ parameters.github }}', true)) + condition: and(succeeded(), eq('${{ parameters.github }}', true), or(eq('${{ parameters.versionOverride }}', ''), eq('${{ parameters.versionOverride }}', '-'))) pool: name: GitClientPME-1ESHostedPool-intel-pc image: ubuntu-x86_64-ado1es From 325ef64bb4e0ee36d9581f189834a5dcc05f1dac Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 15:37:56 +0200 Subject: [PATCH 4/8] azure-pipelines: build, sign and stage the Linux Debian package Add the per-platform Linux job that takes microsoft/git from source to a signed `microsoft-git__.deb` staged under `$(Build.ArtifactStagingDirectory)/_final/`. The flow ports `create-linux-unsigned-artifacts` and `create-linux-signed-artifacts` from the GitHub workflow at .github/workflows/build-git-installers.yml, keeping the Make recipe and DEBIAN/control body byte-for-byte identical so a diff against the workflow's output is empty modulo the deliberate departures called out below. The GitHub workflow runs everything inside an ubuntu:20.04 / ubuntu:22.04 container, both to pin the resulting .deb's glibc ABI floor and to give apt-get a root-owned filesystem. The 1ES pool images we run on (GitClientPME-1ESHostedPool-{intel,arm64}-pc) silently ignore a job-level `container:` directive, so the build executes on the bare Ubuntu host VM as the unprivileged agent user. apt-get therefore runs via `sudo`, and the job logs the Ubuntu version, kernel, and effective UID up front so an audit can read the .deb's effective glibc floor back from the build output. Re-introducing a real container later (whether via 1ES's container option, a custom container job, or docker invoked from a step) is a separate question. The workflow's `DEBIAN_FRONTEND=noninteractive` and `TZ=Etc/UTC` env vars exist only to keep `tzdata` quiet inside a fresh container; the bare 1ES image already has tzdata configured, so they are dropped. The Node.js workaround in the workflow similarly exists only to satisfy GitHub Actions' Node-based shim and is not needed under Azure Pipelines. A few intentional content changes: parallelism switches from the workflow's hard-coded `-j5` (a runner-specific holdover) to `-j$(nproc)`, which adapts to whatever the 1ES pool gives us; the shell prologue changes from `set -ex` to `set -euo pipefail` so an unbound variable or a failing stage in a pipe aborts the job rather than silently producing a broken .deb; `$(git_version)` now comes from the prereqs stage, dropping the workflow's runtime dpkg-architecture round-trip in favour of the matrix's explicit `amd64` / `arm64` entries via `$(deb_arch)`. The `s/-rc/.rc/g` substitution carries over because Git's GIT-VERSION-GEN spells release-candidate tags with a dot. The build drops its output under `$(Build.ArtifactStagingDirectory)/app/` so the existing ESRP signing template's `**/*.deb` pattern picks it up. A focused move of just the signed `microsoft-git__.deb` into `$(Build.ArtifactStagingDirectory)/_final/` then feeds the existing `templateContext.outputs.pipelineArtifact` for the `linux_x64` / `linux_arm64` artifact. Naming the file precisely turns "ESRP signed something else" into a missing-file error rather than a silent wrong-artifact upload. Assisted-by: Claude Opus 4.7 Co-authored-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 132 +++++++++++++++++++++++++++++------ 1 file changed, 110 insertions(+), 22 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 43969c40d56957..79f762deb1668b 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -296,23 +296,80 @@ extends: artifactName: '${{ dim.id }}' steps: - checkout: self - # TODO: add tasks to set up build environment - # TODO: add tasks to build Git and installers - - script: | - echo $(cc_arch) - echo $(deb_arch) - mkdir -p $(Build.ArtifactStagingDirectory)/app - debroot=$(Build.ArtifactStagingDirectory)/pkgroot - mkdir -p $debroot/DEBIAN - cat > $debroot/DEBIAN/control <version + make GIT-VERSION-FILE + + PKGNAME="microsoft-git_${VERSION}_$(deb_arch)" + PKGDIR="$(Build.ArtifactStagingDirectory)/pkgroot" + rm -rf "$PKGDIR" + mkdir -p "$PKGDIR/DEBIAN" + + DESTDIR="$PKGDIR" make -j"$(nproc)" V=1 DEVELOPER=1 \ + USE_LIBPCRE=1 \ + USE_CURL_FOR_IMAP_SEND=1 NO_OPENSSL=1 \ + NO_CROSS_DIRECTORY_HARDLINKS=1 \ + ASCIIDOC8=1 ASCIIDOC_NO_ROFF=1 \ + ASCIIDOC='TZ=UTC asciidoc' \ + prefix=/usr/local \ + gitexecdir=/usr/local/lib/git-core \ + libexecdir=/usr/local/lib/git-core \ + htmldir=/usr/local/share/doc/git/html \ + install install-doc install-html + + # Based on https://packages.ubuntu.com/xenial/vcs/git + cat >"$PKGDIR/DEBIAN/control" < + Description: Git client built from the https://github.com/microsoft/git repository, + specialized in supporting monorepo scenarios. Includes the Scalar CLI. + CTRL + + mkdir -p "$(Build.ArtifactStagingDirectory)/app" + dpkg-deb -Zxz --build "$PKGDIR" \ + "$(Build.ArtifactStagingDirectory)/app/$PKGNAME.deb" - ${{ if eq(parameters.esrp, true) }}: # ESRP ADO tasks require .NET, so we install it here since the # Linux images do not have it by default. @@ -336,11 +393,42 @@ extends: "Parameters": {} } ] - # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/_final - cp -R $(Build.ArtifactStagingDirectory)/app/* $(Build.ArtifactStagingDirectory)/_final/ - displayName: 'Dummy collect artifacts' + - task: Bash@3 + displayName: 'Stage Debian package for upload' + inputs: + targetType: inline + script: | + set -euo pipefail + mkdir -p "$(Build.ArtifactStagingDirectory)/_final" + mv "$(Build.ArtifactStagingDirectory)/app/microsoft-git_$(git_version)_$(deb_arch).deb" \ + "$(Build.ArtifactStagingDirectory)/_final/" + # Validate the freshly built .deb in-place: install it + # and assert `git --version`. Folded into the build job + # so it runs on the same agent without the 1ES job- + # startup overhead a separate validate job carries. + - bash: | + set -e + deb=$(find "$(Build.ArtifactStagingDirectory)/_final" \ + -name 'microsoft-git_*.deb' -type f | head -1) + if [ -z "$deb" ]; then + echo "No microsoft-git_*.deb found in _final" >&2 + exit 1 + fi + echo "Installing $deb" + # Wait up to 10 minutes for unattended-upgrades to + # release the dpkg lock; see comment on this job's + # 'Install build dependencies' step. + sudo apt-get -o DPkg::Lock::Timeout=600 update + sudo apt-get -o DPkg::Lock::Timeout=600 install -y "$deb" + displayName: 'Install Git' + - bash: | + set -e + actual=$(git --version | sed 's/^git version //') + expect=$(echo "$(git_version)" | sed 's/-rc/.rc/g') + echo "Expected: $expect" + echo "Actual: $actual" + test "$actual" = "$expect" + displayName: 'Validate installed version' - stage: release displayName: 'Release' From 7fa9458abf1ee99eedb3fc985f77bc432137fbf6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 30 Apr 2026 17:09:27 +0200 Subject: [PATCH 5/8] azure-pipelines: build, sign, notarize and stage the macOS installer Add the per-platform macOS job that takes microsoft/git from source to a signed-and-notarized `git--universal.pkg` plus the corresponding `.dmg`, both staged under `$(Build.ArtifactStagingDirectory)/_final/`. The flow ports `create-macos-artifacts` and `create-macos-signed-artifacts` from the GitHub workflow at .github/workflows/build-git-installers.yml and leans on .github/macos-installer/Makefile for the heavy lifting, but swaps the workflow's productsign + xcrun notarytool path for ESRP signing and ESRP MacAppNotarize. The native Homebrew on the macOS-15-arm64 pool image is arm64 and lives at /opt/homebrew. Producing a universal binary additionally requires the x86_64 build of gettext/libintl, so a separate x86_64 Homebrew gets installed under /usr/local via the upstream installer running under Rosetta and pulls gettext from there as well. The two arch-specific libintl.a copies are then combined with lipo into a universal archive at the workspace root, which the upcoming config.mak's `LDFLAGS = -L"$(pwd)"` resolves. libintl depends on iconv, but the system /usr/lib/libiconv.dylib is already universal and exports the `_iconv*` symbols Homebrew's gettext was built against; Homebrew's own libiconv exports `_libiconv*` and would not link, hence the explicit `USE_HOMEBREW_LIBICONV` / `ICONVDIR` overrides in config.mak. Spotlight indexing on the boot volume is disabled (`mdutil -i off /`) at the start of the job because leaving it on caused intermittent file-locking failures in subsequent steps. config.mak collects the Make flags that turn on the dual-arch compile and route around several macOS quirks: HOST_CPU=universal, dual-arch CFLAGS (the actual universal-binary driver), -DNO_OPENSSL for contrib Makefiles that do not see the main Makefile's NO_OPENSSL handling, the USE_HOMEBREW_LIBICONV / ICONVDIR overrides, gettext include dirs from both Homebrew prefixes, and CURL_LDFLAGS / CURL_CONFIG pinned against the OS-supplied libcurl rather than a Homebrew copy. SKIP_DASHED_BUILT_INS disables the dashed built-ins because on macOS the hard-link optimisation does not kick in for the staging tree and the resulting full copies would bloat the eventual .dmg. `make GIT-VERSION-FILE dist dist-doc` runs in the source tree; `git get-tar-commit-id` recovers the original commit OID from the resulting source tarball (this becomes GIT_BUILT_FROM_COMMIT, which the macos-installer Makefile bakes into `git version --build-options`); the source and manpage tarballs extract into payload/ and manpages/; a copy of config.mak is dropped inside the extracted source so the universal-build flags apply during the real compile; finally `make -C .github/macos-installer payload` produces the universal binary tree. `git get-tar-commit-id` reads only the leading pax header and then closes its stdin, which makes `gunzip -c` exit 141 (SIGPIPE) under the outer `set -o pipefail`; the pipeline is wrapped in a `set +o pipefail` subshell so the SIGPIPE does not abort the build. The macos-installer Makefile produces the install tree at stage/git-universal-/ but its `pkg` target packages from build-artifacts/, so the tree is copied across after `make payload` completes, mirroring the GitHub workflow. GITHUB_WORKSPACE=$(Build.SourcesDirectory) is exported because the Makefile derives BUILD_DIR from $(GITHUB_WORKSPACE), which is unset under Azure Pipelines. XML_CATALOG_FILES points at the catalogs from the Homebrew docbook installed in the dependencies step. (FUTURE: the duplication exists only because .github/macos-installer/Makefile hardcodes both DESTDIR=stage/... and ARTIFACTDIR=build-artifacts; overriding ARTIFACTDIR on the `make pkg` line below to point at stage/ would let us drop the cp entirely. Worth cleaning up alongside moving the macOS installer Makefiles out of .github/, where they live for historical reasons rather than because they are GitHub-specific.) Signing happens against the install tree at .github/macos-installer/build-artifacts/usr/local/git/, not the source tree under payload/git-/, because the macos-installer Makefile's `pkg` target packages from build-artifacts/; signing the source tree would have no effect on the resulting .pkg. Following the pattern in git-credential-manager/.azure-pipelines/release.yml, the install tree is pre-filtered to just the Mach-O files (using `file --mime` matching `mach`, the same heuristic .github/scripts/codesign.sh uses), copied into a staging directory under $(Build.ArtifactStagingDirectory)/macos-tosign/ preserving relative paths, handed to the existing .azure-pipelines/esrp/sign.yml template (which zips, signs via EsrpCodeSigning@6 with KeyCode CP-401337-Apple + OperationCode MacAppDeveloperSign + Hardening enabled, and extracts back into the staging dir), and finally copied back into the install tree. The pre-filter is necessary because the existing template's CopyFiles@2 step uses minimatch globs and the only reliable way to pick out Mach-O files is by file content; signing the entire install tree would either fail on non-binary files or sign things that should not be signed (shell scripts, perl, manpages, templates, the uninstall.sh). UseDotNet@2 (8.x) installs the .NET SDK that EsrpCodeSigning@6 depends on, since the macOS-15-arm64 pool image does not provide it. The macos-installer Makefile's `pkg` target then produces an unsigned .github/macos-installer/disk-image/git--universal.pkg from the signed payload tree. APPLE_INSTALLER_IDENTITY is deliberately left undefined so pkgbuild does not try to sign; the next step submits the .pkg back through ESRP for signing (KeyCode CP-401337-Apple covers both Developer ID Application and Developer ID Installer certs in this account, so MacAppDeveloperSign on a .pkg is the productsign equivalent), and then through ESRP for Apple notarization (MacAppNotarize, BundleId com.git.pkg, matching the identifier pkgbuild bakes in via `--identifier com.git.pkg` from the Makefile). The ESRP MacAppNotarize operation handles both submission and ticket stapling, returning the notarized .pkg back into disk-image/ via the same zip-extract template path the previous sign step used; this is what replaces the `xcrun notarytool submit ... --wait` plus `xcrun stapler staple` flow from .github/scripts/notarize.sh. Finally the Makefile's `image` target builds .github/macos-installer/git--universal.dmg from the contents of disk-image/. The .dmg lands at the macos-installer root, while the signed-and-notarized .pkg lives somewhere under disk-image/: ESRP's MacAppNotarize op repacks its output zip to wrap the notarized .pkg in a UUID-named .zip.unzipped/ subdirectory, so depending on whether notarization ran, the .pkg ends up either directly under disk-image/ or at disk-image/.zip.unzipped/git-...pkg. `find` locates it and moves it (along with the globbed .dmg) into $(Build.ArtifactStagingDirectory)/_final/, which the job's templateContext.outputs already publishes as the `macos_universal` pipeline artifact. `set -euo pipefail` means an empty `find` result, or a missing .dmg, fails the mv loudly rather than producing a silent half-empty upload, matching the same defensive choice the Linux stage step makes. Assisted-by: Claude Opus 4.7 Co-authored-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 382 +++++++++++++++++++++++++++++++++-- 1 file changed, 367 insertions(+), 15 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 79f762deb1668b..1adee4201d8cf8 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -238,22 +238,246 @@ extends: - output: pipelineArtifact targetPath: '$(Build.ArtifactStagingDirectory)/_final' artifactName: '${{ dim.id }}' + # macOS build flow: + # + # 1. Configure for a universal build and produce + # Git's own dist tarballs (`make dist dist-doc`). + # 2. Extract the source tarball into payload/, copy + # config.mak in, run `make payload` to compile and + # install into stage/git-universal-/. + # 3. Mirror stage/ into build-artifacts/ (which is + # what the macos-installer Makefile's `pkg` target + # consumes - see note in the build step). + # 4. ESRP-sign Mach-O files in build-artifacts/. + # 5. `make pkg` -> unsigned .pkg in disk-image/. + # 6. ESRP-sign and ESRP-notarize the .pkg in place. + # 7. `make image` wraps disk-image/ contents in a DMG. + # 8. Stage the .pkg and .dmg under _final/ for upload. steps: - checkout: self - # TODO: add tasks to set up build environment - # TODO: add tasks to build Git and installers - - script: | - echo "Hello, Mac!" - mkdir -p $(Build.ArtifactStagingDirectory)/app - cp /bin/echo $(Build.ArtifactStagingDirectory)/app/example - displayName: 'Dummy build' + - task: Bash@3 + displayName: 'Disable Spotlight indexing' + inputs: + targetType: inline + script: | + # Disable Spotlight indexing to prevent file + # locking issues. + set -euo pipefail + sudo mdutil -i off / || true + - task: Bash@3 + displayName: 'Install build dependencies' + inputs: + targetType: inline + script: | + set -euo pipefail + + # The agent's native arm64 Homebrew lives at + # /opt/homebrew. Install a separate x86_64 Homebrew + # under /usr/local so we can fetch the x86_64 build + # of gettext/libintl alongside the arm64 one. + arch -x86_64 /bin/bash -c \ + "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + arch -x86_64 /usr/local/bin/brew install gettext + + # Native (arm64) build dependencies. + brew install automake asciidoc xmlto docbook + brew link --force gettext + + # Make a universal libintl.a out of the two + # arch-specific copies, dropped at the workspace + # root so the build's LDFLAGS = -L"$(pwd)" can find + # it. libintl depends on iconv, but we deliberately + # use the system's universal /usr/lib/libiconv.dylib + # rather than Homebrew's libiconv (which exports + # _libiconv* symbols, while Homebrew's gettext was + # built against system iconv with _iconv* symbols). + lipo -create \ + -output libintl.a \ + /usr/local/opt/gettext/lib/libintl.a \ + /opt/homebrew/opt/gettext/lib/libintl.a + - task: Bash@3 + displayName: 'Configure universal build' + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + # Git's GIT-VERSION-GEN expects .rc rather than -rc + BUILD_VERSION="$(echo "$VERSION" | sed 's/-rc/.rc/g')" + echo "$BUILD_VERSION" >version + + # HOST_CPU is a bit of a lie and is only used in + # 'git version --build-options'; we'll fix that in + # code. The two -arch flags are what actually drive + # the universal build. + cat >config.mak <>config.mak <>config.mak <>config.mak + - task: Bash@3 + displayName: 'Build payload via macos-installer' + env: + # The macos-installer Makefile derives BUILD_DIR from + # $(GITHUB_WORKSPACE), which is unset in ADO. Point it + # at the worktree root. + GITHUB_WORKSPACE: $(Build.SourcesDirectory) + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + BUILD_VERSION="$(echo "$VERSION" | sed 's/-rc/.rc/g')" + + # The asciidoc/xmlto build steps need the catalogs + # from Homebrew docbook. + export XML_CATALOG_FILES="$(brew --prefix)/etc/xml/catalog" + + # `git commit` (in dist-doc) forks a detached + # `git maintenance run --auto` that keeps writing + # into .git/ after the commit returns, which then + # races with dist-doc's `rm -fr .doc-tmp-dir` and + # produces "Directory not empty". Disable + # auto-maintenance for every git invocation in + # this build. + export GIT_CONFIG_PARAMETERS="'maintenance.auto=false'" + + make -j"$(sysctl -n hw.physicalcpu)" GIT-VERSION-FILE dist dist-doc + + # Recover the source-tree commit OID from the dist + # tarball; the macos-installer Makefile bakes it + # into 'git version --build-options'. + # `git get-tar-commit-id` reads only the leading + # pax header and then closes its stdin, which + # makes `gunzip -c` exit 141 (SIGPIPE) under the + # outer `set -o pipefail`. Disable pipefail for + # the duration of this one pipeline. + GIT_BUILT_FROM_COMMIT="$( + set +o pipefail + gunzip -c "git-$BUILD_VERSION.tar.gz" | + git get-tar-commit-id + )" + export GIT_BUILT_FROM_COMMIT + export VERSION + + mkdir payload manpages + tar -xf "git-$BUILD_VERSION.tar.gz" -C payload + tar -xf "git-manpages-$BUILD_VERSION.tar.gz" -C manpages + + # The actual compile happens inside the extracted + # tree, against a copy of the config.mak we wrote + # at the worktree root in the previous step. + cp config.mak "payload/git-$BUILD_VERSION/config.mak" + + make -C .github/macos-installer V=1 payload + + # NOTE: the macos-installer Makefile produces + # the install tree at stage/git-universal-/ + # but its `pkg` target packages from + # build-artifacts/. Mirror the GitHub workflow + # by copying the tree across so the rest of the + # pipeline (signing, pkg) finds it where the + # Makefile expects. + # + # FUTURE: this duplication exists only because + # .github/macos-installer/Makefile hardcodes + # both DESTDIR (=stage/...) and ARTIFACTDIR + # (=build-artifacts). Overriding ARTIFACTDIR on + # the `make pkg` line below to point at stage/ + # would let us drop the cp entirely. Worth + # cleaning up alongside moving the macOS + # installer Makefiles out of .github/ (they are + # build infrastructure, not GitHub-specific). + mkdir -p .github/macos-installer/build-artifacts + cp -R "stage/git-universal-$BUILD_VERSION/." \ + .github/macos-installer/build-artifacts/ + # ESRP-sign the universal Mach-O binaries inside the + # payload tree. The existing esrp/sign.yml template's + # CopyFiles@2 step uses minimatch globs and the only + # reliable way to detect Mach-O is by file content + # (file --mime), so we pre-filter into a staging dir, + # let the template zip/sign/extract that staging dir, + # then copy the signed binaries back over the payload. - ${{ if eq(parameters.esrp, true) }}: + # ESRP ADO tasks require .NET, which the macOS pool + # image does not provide by default. + - task: UseDotNet@2 + displayName: 'Install .NET for ESRP' + inputs: + packageType: sdk + version: '8.x' + - task: Bash@3 + displayName: 'Stage Mach-O binaries for signing' + inputs: + targetType: inline + script: | + set -euo pipefail + + # Sign the install tree (build-artifacts/) - + # this is what `make pkg` packages. Signing + # the source tree under payload/ would have + # no effect on the resulting .pkg. + install_tree=".github/macos-installer/build-artifacts/usr/local/git" + stage_dir="$(Build.ArtifactStagingDirectory)/macos-tosign/binaries" + + rm -rf "$stage_dir" + mkdir -p "$stage_dir" + + pushd "$install_tree" + find . -type f -exec file --mime {} + \ + | sed -n '/mach/s/: .*//p' \ + | while IFS= read -r f; do + rel="${f#./}" + tgt="$stage_dir/$rel" + mkdir -p "$(dirname "$tgt")" + cp -- "$f" "$tgt" + done + popd - template: .azure-pipelines/esrp/sign.yml@self parameters: - displayName: 'Example sign binaries' - folderPath: '$(Build.ArtifactStagingDirectory)/app' + displayName: 'ESRP-sign Mach-O binaries' + folderPath: '$(Build.ArtifactStagingDirectory)/macos-tosign/binaries' pattern: '**/*' - useArchive: true # Must be true when macOS signing + useArchive: true # Required for macOS signing inlineOperation: | [ { @@ -266,11 +490,139 @@ extends: } } ] - # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/_final - cp -R $(Build.ArtifactStagingDirectory)/app/* $(Build.ArtifactStagingDirectory)/_final/ - displayName: 'Dummy collect artifacts' + - task: Bash@3 + displayName: 'Copy signed binaries back to install tree' + inputs: + targetType: inline + script: | + set -euo pipefail + + cp -R "$(Build.ArtifactStagingDirectory)/macos-tosign/binaries"/* \ + .github/macos-installer/build-artifacts/usr/local/git/ + - task: Bash@3 + displayName: 'Build unsigned installer pkg' + env: + GITHUB_WORKSPACE: $(Build.SourcesDirectory) + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + export VERSION + + # Leave APPLE_INSTALLER_IDENTITY undefined so the + # Makefile's `pkg` target produces an unsigned .pkg + # (the `ifdef APPLE_INSTALLER_IDENTITY` branch in + # pkg_cmd is skipped). ESRP signs it in the next + # step. + make -C .github/macos-installer V=1 pkg + - ${{ if eq(parameters.esrp, true) }}: + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'ESRP-sign installer pkg' + folderPath: '.github/macos-installer/disk-image' + pattern: '*.pkg' + useArchive: true # Required for macOS signing + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppDeveloperSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "Hardening": "Enable" + } + } + ] + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'ESRP-notarize installer pkg' + folderPath: '.github/macos-installer/disk-image' + pattern: '*.pkg' + useArchive: true # Required for macOS notarization + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppNotarize", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "BundleId": "com.git.pkg" + } + } + ] + - task: Bash@3 + displayName: 'Build DMG' + env: + GITHUB_WORKSPACE: $(Build.SourcesDirectory) + inputs: + targetType: inline + script: | + set -euo pipefail + + VERSION="$(git_version)" + export VERSION + + # Builds .github/macos-installer/git--universal.dmg + # from the contents of disk-image/, which now contains + # the signed and notarized .pkg. + make -C .github/macos-installer V=1 image + - task: Bash@3 + displayName: 'Stage installer artifacts for upload' + inputs: + targetType: inline + script: | + set -euo pipefail + + mkdir -p "$(Build.ArtifactStagingDirectory)/_final" + ls -la .github/macos-installer/ \ + .github/macos-installer/disk-image/ || true + # The .pkg lands either directly under disk-image/ + # or, after ESRP MacAppNotarize re-packs it, inside + # disk-image/.zip.unzipped/. Find it. + pkg=$(find .github/macos-installer/disk-image \ + -name 'git-*-universal.pkg' -type f \ + | head -1) + mv .github/macos-installer/git-*-universal.dmg \ + "$pkg" \ + "$(Build.ArtifactStagingDirectory)/_final/" + # Validate the freshly built pkg in-place: install it, + # assert `git --version`, and confirm the universal binary + # actually runs natively as the host architecture. Folded + # into the build job so it runs on the same agent without + # the 1ES job-startup overhead a separate validate job + # carries. + - bash: | + set -e + if [ "$(uname -m)" = arm64 ] && command -v brew >/dev/null; then + brew uninstall git || true + fi + pkg=$(find "$(Build.ArtifactStagingDirectory)/_final" \ + -name 'git-*-universal.pkg' -type f | head -1) + if [ -z "$pkg" ]; then + echo "No git-*-universal.pkg found in _final" >&2 + exit 1 + fi + echo "Installing $pkg" + sudo installer -pkg "$pkg" -target / + displayName: 'Install Git' + - bash: | + set -e + actual=$(git --version | sed 's/^git version //') + expect=$(echo "$(git_version)" | sed 's/-rc/.rc/g') + echo "Expected: $expect" + echo "Actual: $actual" + test "$actual" = "$expect" + displayName: 'Validate installed version' + - bash: | + set -ex + git version --build-options >actual + cat actual + grep "cpu: $(uname -m)" actual + displayName: 'Validate universal binary CPU architecture' # # Linux build jobs From 73af3876be80fd035cbbcecb9761ac5c78643e1d Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 1 May 2026 12:57:46 +0100 Subject: [PATCH 6/8] azure-pipelines: build, sign and stage the Windows installer Add the per-platform Windows job that takes microsoft/git from source to ESRP-signed `Git--.exe` and `PortableGit--.exe`, plus the matching sha-256.txt sidecar, all staged under $(Build.ArtifactStagingDirectory)/_final/. The flow ports `create-windows-artifacts` and `create-windows-signed-artifacts` from the GitHub workflow at .github/workflows/build-git-installers.yml, leaning on git-for-windows/build-extra (please.sh + installer/release.sh) for the build itself. GitHub Actions has the git-for-windows/setup-git-for-windows-sdk@v1 action that drops a full SDK onto the runner; Azure Pipelines has no equivalent task, so the Windows job has to bootstrap the SDK by hand before any of the bash-driven build steps can run. Bootstrap is driven by .azure-pipelines/scripts/windows/setup-git-sdk.sh, which a Bash@3 task invokes via `filePath:` so it runs under the agent's MinGit-provided bash. A `sdk_repo` field on each windows_matrix entry (git-for-windows/git-sdk-64 for x64, git-sdk-arm64 for ARM64) lets the script pick the right upstream; the script does a partial+bare clone of the SDK, clones git-for-windows/build-extra into a sibling directory, then runs `please.sh create-sdk-artifact --sdk= --out= build-installers` to materialise the build-installers flavour of the SDK at the requested output path. Routing through `please.sh create-sdk-artifact` keeps the bytes flowing via plain GitHub HTTPS clones (which 1ES allows) rather than the raw and release-asset CDNs that an earlier download-the-snapshot approach hit. Once the SDK is in place, its own usr/bin (which ships cygpath) and the matching MinGW toolchain bin/ are exposed to subsequent tasks via ##vso[task.prependpath]. The arm64 Windows hosted agents do not have Azure CLI pre-installed, which the AzureCLI@2 task in the ESRP setup step needs. Install the x64 MSI (which runs under x86-64 emulation on arm64 Windows) and prepend it to PATH, gated on a poolArch condition. This is a workaround until the bug preventing us from baking Azure CLI into the hosted pool image is fixed, at which point this step can be dropped. A small helper, .azure-pipelines/scripts/windows/utils.sh, provides `to_windows_path` / `to_unix_path` for scripts running on Windows agents. Both prefer `cygpath` when it is on PATH and fall back to a small pure-shell parser otherwise. The fallback matters because some bash steps run before the SDK is bootstrapped and only have MinGit's bash available, which does not ship cygpath. Both setup-git-sdk.sh and the ESRP-sign script source utils.sh rather than duplicating the conversion logic. Git for Windows' build tooling (please.sh, signtool.sh, the installer .iss templates, and the MINGW-packages helpers) lives in git-for-windows/build-extra rather than in the SDK snapshot. The GitHub workflow's Windows job clones it into /usr/src/build-extra of the SDK before invoking please.sh; this job does the same. A partial clone (--filter=blob:none) plus --single-branch -b main is enough for everything please.sh needs and avoids pulling the full blob history, matching the workflow's invocation byte for byte. The mingw-w64-git package is built via please.sh build-mingw-w64-git from a Bash@3 task using the SDK's bash that the bootstrap put on PATH. Outputs land in $(Build.SourcesDirectory)/artifacts/ so the subsequent installer-build step can pass them to please.sh make_installers_from_mingw_w64_git via --pkg= flags. Three small adaptations from the GitHub workflow's source step are worth flagging. First, the /usr/bin/git trampoline that delegates to the matching MinGW-built git.exe is the same one the workflow writes by hand; makepkg-mingw shells out to plain `git`, and the SDK bash's git candidates would otherwise come from MinGit, not the toolchain we are building against. Second, the user.name / user.email / PACKAGER values are hardcoded to a build-bot identity since Azure Pipelines has no GitHub-actor equivalent. Third, please.sh's --only- flag takes the bare CPU name (x86_64 or aarch64), not the toolchain triple, so a `cpu_arch` matrix dimension surfaces the right value next to each toolchain entry. The build task detaches stdin via `exec snapshot that microsoft/git does not currently ship. A single bash task drives please.sh make_installers_from_mingw_w64_git for both installer and portable variants. The GitHub workflow runs these as separate matrix jobs (one per type/arch combination); keeping both builds in the same Azure Pipelines job means the .pkg.tar.* artifacts produced by the previous step are available without an inter-job artifact passing trip. The PDB archive copy into build-extra/cached-source-packages is the same prerequisite that --include-pdbs needs in the GitHub workflow. The --pkg= filter that strips signatures and the optional archimport / cvs / p4 / gitweb / doc-man pieces matches the workflow's sed exactly so the resulting .exe sizes are comparable. The same `exec [file ...]`, sign in place) already matches signtool.sh's, so no further script changes are needed. Stage `Git-*.exe` and `PortableGit-*.exe` alongside a SHA-256 sidecar into $(Build.ArtifactStagingDirectory)/_final/, which the job's templateContext.outputs.pipelineArtifact already publishes as the `windows_x64` / `windows_arm64` artifact. The SHA-256 sidecar is computed here, post-build, rather than in the build step because ESRP signing rewrites the .exe contents; a SHA-256 computed before signing would mismatch the bytes that ship. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin --- .azure-pipelines/esrp/windows/esrpsign.sh | 27 +- .azure-pipelines/patches/.gitattributes | 1 + .../0000-installer-publisher.patch | 13 + .../0001-installer-vsintegration.patch | 50 +++ .../0002-installer-default-components.patch | 17 + ...03-installer-fork-from-microsoft-git.patch | 29 ++ .../git-sdk/0000-update-recently-seen.patch | 12 + .azure-pipelines/release.yml | 401 ++++++++++++++++-- .azure-pipelines/scripts/apply-patches.sh | 56 +++ .../scripts/windows/setup-cv2pdb-x64.ps1 | 99 +++++ .../scripts/windows/setup-git-sdk.sh | 74 ++++ .azure-pipelines/scripts/windows/utils.sh | 66 +++ 12 files changed, 787 insertions(+), 58 deletions(-) create mode 100644 .azure-pipelines/patches/.gitattributes create mode 100644 .azure-pipelines/patches/windows/build-extra/0000-installer-publisher.patch create mode 100644 .azure-pipelines/patches/windows/build-extra/0001-installer-vsintegration.patch create mode 100644 .azure-pipelines/patches/windows/build-extra/0002-installer-default-components.patch create mode 100644 .azure-pipelines/patches/windows/build-extra/0003-installer-fork-from-microsoft-git.patch create mode 100644 .azure-pipelines/patches/windows/git-sdk/0000-update-recently-seen.patch create mode 100755 .azure-pipelines/scripts/apply-patches.sh create mode 100644 .azure-pipelines/scripts/windows/setup-cv2pdb-x64.ps1 create mode 100755 .azure-pipelines/scripts/windows/setup-git-sdk.sh create mode 100755 .azure-pipelines/scripts/windows/utils.sh diff --git a/.azure-pipelines/esrp/windows/esrpsign.sh b/.azure-pipelines/esrp/windows/esrpsign.sh index a3bf1bc66ea4f8..ee2ed2db5d9e2d 100755 --- a/.azure-pipelines/esrp/windows/esrpsign.sh +++ b/.azure-pipelines/esrp/windows/esrpsign.sh @@ -38,6 +38,7 @@ if [ -z "${SYSTEM_ACCESSTOKEN:-}" ]; then fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$SCRIPT_DIR/../../scripts/windows/utils.sh" # Check for overriden key code, otherwise use default (Microsoft Third-Party/OSS) ESRP_KEYCODE="${ESRP_KEYCODE:-CP-231522}" @@ -54,32 +55,6 @@ if [ ! -f "$ESRP_TOOL" ]; then exit 1 fi -# Convert an MSYS2 path to Windows format for ESRPClient.exe. -to_windows_path () { - # Prefer cygpath if available (full Git for Windows) - if command -v cygpath >/dev/null 2>&1; then - cygpath -w "$1" - return - fi - case "$1" in - /[a-zA-Z]/*) - # Drive path: /d/path -> D:\path - drive=$(echo "$1" | cut -c2 | tr 'a-z' 'A-Z') - rest=$(echo "$1" | cut -c3-) - echo "${drive}:${rest}" | sed 's|/|\\|g' - ;; - /*) - # Absolute path under MSYS2 root - root=$(cd / && pwd -W) - echo "${root}${1}" | sed 's|/|\\|g' - ;; - # Relative or already-Windows path: just flip slashes - *) - echo "$1" | sed 's|/|\\|g' - ;; - esac -} - # Build the SignRequestFiles JSON array echo "==> Preparing files for signing ($# file(s))..." files_json="" diff --git a/.azure-pipelines/patches/.gitattributes b/.azure-pipelines/patches/.gitattributes new file mode 100644 index 00000000000000..ef9170ec0077a5 --- /dev/null +++ b/.azure-pipelines/patches/.gitattributes @@ -0,0 +1 @@ +*.patch whitespace=-trailing-space,-blank-at-eof diff --git a/.azure-pipelines/patches/windows/build-extra/0000-installer-publisher.patch b/.azure-pipelines/patches/windows/build-extra/0000-installer-publisher.patch new file mode 100644 index 00000000000000..aaef8b8a35b6d1 --- /dev/null +++ b/.azure-pipelines/patches/windows/build-extra/0000-installer-publisher.patch @@ -0,0 +1,13 @@ +diff --git a/installer/install.iss b/installer/install.iss +index 70787b7..137f660 100644 +--- a/installer/install.iss ++++ b/installer/install.iss +@@ -65,7 +65,7 @@ SignTool=signtool + ; Installer-related + AllowNoIcons=yes + AppName={#APP_NAME} +-AppPublisher=The Git Development Community ++AppPublisher=The Git Client Team at Microsoft + AppPublisherURL={#APP_URL} + AppSupportURL={#APP_CONTACT_URL} + AppVersion={#APP_VERSION} diff --git a/.azure-pipelines/patches/windows/build-extra/0001-installer-vsintegration.patch b/.azure-pipelines/patches/windows/build-extra/0001-installer-vsintegration.patch new file mode 100644 index 00000000000000..6797e6ab6c88ba --- /dev/null +++ b/.azure-pipelines/patches/windows/build-extra/0001-installer-vsintegration.patch @@ -0,0 +1,50 @@ +diff --git a/installer/helpers.inc.iss b/installer/helpers.inc.iss +index 3e3788d..fc81be7 100644 +--- a/installer/helpers.inc.iss ++++ b/installer/helpers.inc.iss +@@ -224,3 +224,25 @@ begin + DeleteFile(OutPath); + DeleteFile(ErrPath); + end; ++ ++procedure CustomPostInstall(); ++begin ++ if not RegWriteStringValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\15.0\TeamFoundation\GitSourceControl','GitPath',ExpandConstant('{app}')) or ++ not RegWriteStringValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\16.0\TeamFoundation\GitSourceControl','GitPath',ExpandConstant('{app}')) or ++ not RegWriteStringValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\17.0\TeamFoundation\GitSourceControl','GitPath',ExpandConstant('{app}')) or ++ not RegWriteStringValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\18.0\TeamFoundation\GitSourceControl','GitPath',ExpandConstant('{app}')) or ++ not RegWriteStringValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\19.0\TeamFoundation\GitSourceControl','GitPath',ExpandConstant('{app}')) or ++ not RegWriteStringValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\20.0\TeamFoundation\GitSourceControl','GitPath',ExpandConstant('{app}')) then ++ LogError('Could not register TeamFoundation\GitSourceControl'); ++end; ++ ++procedure CustomPostUninstall(); ++begin ++ if not RegDeleteValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\15.0\TeamFoundation\GitSourceControl','GitPath') or ++ not RegDeleteValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\16.0\TeamFoundation\GitSourceControl','GitPath') or ++ not RegDeleteValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\17.0\TeamFoundation\GitSourceControl','GitPath') or ++ not RegDeleteValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\18.0\TeamFoundation\GitSourceControl','GitPath') or ++ not RegDeleteValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\19.0\TeamFoundation\GitSourceControl','GitPath') or ++ not RegDeleteValue(HKEY_CURRENT_USER,'Software\Microsoft\VSCommon\20.0\TeamFoundation\GitSourceControl','GitPath') then ++ LogError('Could not register TeamFoundation\GitSourceControl'); ++end; +diff --git a/installer/install.iss b/installer/install.iss +index 70787b7..74d8375 100644 +--- a/installer/install.iss ++++ b/installer/install.iss +@@ -3603,6 +3603,7 @@ begin + Install a scheduled task to try to auto-update Git for Windows + } + ++ CustomPostInstall(); + if IsComponentInstalled('autoupdate') then begin + WizardForm.StatusLabel.Caption:='Set up daily up to date check'; + InstallAutoUpdater(); +@@ -3943,6 +3944,7 @@ begin + Remove the scheduled task to try to auto-update Git for Windows + } + ++ CustomPostUninstall(); + if IsComponentInstalled('autoupdate') then + UninstallAutoUpdater(); + diff --git a/.azure-pipelines/patches/windows/build-extra/0002-installer-default-components.patch b/.azure-pipelines/patches/windows/build-extra/0002-installer-default-components.patch new file mode 100644 index 00000000000000..2135ecf925bc84 --- /dev/null +++ b/.azure-pipelines/patches/windows/build-extra/0002-installer-default-components.patch @@ -0,0 +1,17 @@ +diff --git a/installer/install.iss b/installer/install.iss +index 70787b7..37d79b0 100644 +--- a/installer/install.iss ++++ b/installer/install.iss +@@ -1925,6 +1925,12 @@ begin + GetDefaultsFromGitConfig('system'); + + ChosenOptions:=''; ++ if (ExpandConstant('{param:components|/}')='/') then begin ++ WizardSelectComponents('autoupdate'); ++#ifdef WITH_SCALAR ++ WizardSelectComponents('scalar'); ++#endif ++ end; + + PrevPageID:=wpSelectProgramGroup; + diff --git a/.azure-pipelines/patches/windows/build-extra/0003-installer-fork-from-microsoft-git.patch b/.azure-pipelines/patches/windows/build-extra/0003-installer-fork-from-microsoft-git.patch new file mode 100644 index 00000000000000..28af996716dc03 --- /dev/null +++ b/.azure-pipelines/patches/windows/build-extra/0003-installer-fork-from-microsoft-git.patch @@ -0,0 +1,29 @@ +diff --git a/git-update-git-for-windows.config b/git-update-git-for-windows.config +new file mode 100644 +index 0000000..bfd0744 +--- /dev/null ++++ b/git-update-git-for-windows.config +@@ -0,0 +1,2 @@ ++[update] ++ fromFork = microsoft/git +diff --git a/installer/install.iss b/installer/install.iss +index 70787b7..71d5e72 100644 +--- a/installer/install.iss ++++ b/installer/install.iss +@@ -126,6 +126,7 @@ Filename: {app}\ReleaseNotes.html; Description: View Release Notes; Flags: shell + [Files] + ; Install files that might be in use during setup under a different name. + #include "file-list.iss" ++Source: {#SourcePath}\..\git-update-git-for-windows.config; DestDir: {app}\{#MINGW_BITNESS}\bin; Flags: replacesameversion; AfterInstall: DeleteFromVirtualStore + Source: {#SourcePath}\ReleaseNotes.html; DestDir: {app}; Flags: replacesameversion; AfterInstall: DeleteFromVirtualStore + Source: {#SourcePath}\..\LICENSE.txt; DestDir: {app}; Flags: replacesameversion; AfterInstall: DeleteFromVirtualStore + Source: {#SourcePath}\NOTICE.txt; DestDir: {app}; Flags: replacesameversion; AfterInstall: DeleteFromVirtualStore; Check: ParamIsSet('VSNOTICE') +@@ -275,6 +276,8 @@ Type: files; Name: {app}\etc\rebase.db.i386 + Type: files; Name: {app}\etc\install-options.txt + Type: dirifempty; Name: {app}\{#MINGW_BITNESS}\libexec\git-core + Type: dirifempty; Name: {app}\{#MINGW_BITNESS}\libexec ++Type: files; Name: {app}\{#MINGW_BITNESS}\bin\git-update-git-for-windows.config ++Type: dirifempty; Name: {app}\{#MINGW_BITNESS}\bin + Type: dirifempty; Name: {app}\{#MINGW_BITNESS} + Type: dirifempty; Name: {app} + diff --git a/.azure-pipelines/patches/windows/git-sdk/0000-update-recently-seen.patch b/.azure-pipelines/patches/windows/git-sdk/0000-update-recently-seen.patch new file mode 100644 index 00000000000000..cf73dfd5f3f449 --- /dev/null +++ b/.azure-pipelines/patches/windows/git-sdk/0000-update-recently-seen.patch @@ -0,0 +1,12 @@ +diff --git a/bin/git-update-git-for-windows b/bin/git-update-git-for-windows +index 29444d9..6705da1 100644 +--- a/bin/git-update-git-for-windows ++++ b/bin/git-update-git-for-windows +@@ -4,6 +4,7 @@ + # release. If versions differ, the bit matched installer is downloaded and run + # when confirmation to do so is given. + ++use_recently_seen=no + + # Compare version strings + # Prints -1, 0 or 1 to stdout diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 1adee4201d8cf8..dff8e8f3770b36 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -38,7 +38,10 @@ parameters: image: win-x86_64-ado1es os: windows toolchain: x86_64 + cpu_arch: x86_64 mingwprefix: mingw64 + msystem: MINGW64 + sdk_repo: git-for-windows/git-sdk-64 - id: windows_arm64 jobName: 'Windows (ARM64)' @@ -47,7 +50,10 @@ parameters: image: win-arm64-ado1es os: windows toolchain: clang-aarch64 + cpu_arch: aarch64 mingwprefix: clangarm64 + msystem: CLANGARM64 + sdk_repo: git-for-windows/git-sdk-arm64 - name: macos_matrix type: object @@ -164,6 +170,8 @@ extends: git_version: $[stageDependencies.prereqs.prebuild.outputs['info.git_version']] toolchain: ${{ dim.toolchain }} mingwprefix: ${{ dim.mingwprefix }} + sdk_repo: ${{ dim.sdk_repo }} + cpu_arch: ${{ dim.cpu_arch }} templateContext: outputs: - output: pipelineArtifact @@ -176,7 +184,59 @@ extends: displayName: 'Add Git Bash to PATH' inputs: filename: ./.azure-pipelines/scripts/windows/setup-git-bash.cmd - # Setup ESRP code signing for Windows (sets ESRP_TOOL, ESRP_AUTH) + # Install Azure CLI on arm64 (not pre-installed on these agents) + - ${{ if eq(dim.poolArch, 'arm64') }}: + - powershell: | + $ProgressPreference = 'SilentlyContinue' + $msi = "$env:TEMP\AzureCLI.msi" + Write-Host "Downloading Azure CLI (x64)..." + Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile $msi + Write-Host "Installing Azure CLI..." + Start-Process msiexec.exe -ArgumentList "/i", $msi, "/quiet", "/norestart" -Wait + $azPath = "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin" + Write-Host "##vso[task.prependpath]$azPath" + Write-Host "Azure CLI installed." + displayName: 'Install Azure CLI (x64 on ARM64)' + # Install VS 2022 Build Tools on x64 so cv2pdb-strip can locate + # mspdb140.dll. We do not need to do this on ARM64 since we use + # clang's llvm-strip there instead. + - ${{ if eq(dim.poolArch, 'amd64') }}: + - task: PowerShell@2 + displayName: 'Setup cv2pdb (x64)' + inputs: + filePath: ./.azure-pipelines/scripts/windows/setup-cv2pdb-x64.ps1 + - task: Bash@3 + displayName: 'Install Git for Windows SDK' + inputs: + filePath: ./.azure-pipelines/scripts/windows/setup-git-sdk.sh + arguments: '$(sdk_repo) $(mingwprefix) "$(Agent.TempDirectory)\gitsdk"' + env: + BOOTSTRAP_DIR: '$(Build.SourcesDirectory)' + # please.sh's `create-sdk-artifact` step does the + # final sparse-checkout of the build-installers + # SDK subset, which is I/O-bound (lots of small + # writes), not CPU-bound. The default + # checkout.workers=1 leaves the agent's I/O + # subsystem mostly idle; bumping it well beyond + # the core count gives a substantial speedup. + GIT_CONFIG_PARAMETERS: "'checkout.workers=56'" + - task: Bash@3 + displayName: 'Clone build-extra into SDK' + inputs: + targetType: inline + script: | + set -euo pipefail + # The please.sh + signtool.sh scripts the build + # relies on live in build-extra; the SDK ships + # without them. Partial clone to keep this fast. + git clone --filter=blob:none --single-branch -b main \ + https://github.com/git-for-windows/build-extra \ + /usr/src/build-extra + # Setup ESRP code signing for Windows (sets ESRP_TOOL, + # ESRP_AUTH) before the build steps so that the build + # itself (Inno Setup, makepkg-mingw) can invoke ESRP + # via the `git signtool` alias for in-line signing of + # individual binaries. - ${{ if eq(parameters.esrp, true) }}: - template: .azure-pipelines/esrp/windows/setup.yml@self parameters: @@ -184,40 +244,317 @@ extends: esrpClientId: $(esrpClientId) keyVaultName: $(esrpKeyVaultName) signCertName: $(esrpSignReqCertName) - # TODO: add tasks to set up Git for Windows SDK - # TODO: add tasks to build Git and installers - - script: | - echo $(mingwprefix) - echo $(toolchain) - mkdir $(Build.ArtifactStagingDirectory)\app - copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example1.exe - copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example2.exe - copy C:\Windows\System32\calc.exe $(Build.ArtifactStagingDirectory)\app\example3.exe - displayName: 'Dummy build' - # - # To sign Windows binaries with ESRP, call esrpsign.sh - # with the files to sign as arguments. Requires the - # following environment variables to be set: - # ESRP_TOOL - set by the setup template above - # ESRP_AUTH - set by the setup template above - # SYSTEM_ACCESSTOKEN - $(System.AccessToken) - # - - ${{ if eq(parameters.esrp, true) }}: - - bash: | - .azure-pipelines/esrp/windows/esrpsign.sh \ - "$BUILD_ARTIFACTSTAGINGDIRECTORY/app/example1.exe" \ - "$BUILD_ARTIFACTSTAGINGDIRECTORY/app/example2.exe" \ - "$BUILD_ARTIFACTSTAGINGDIRECTORY/app/example3.exe" - displayName: 'Example ESRP signing' - env: + - task: Bash@3 + displayName: 'Configure git signtool alias for ESRP' + inputs: + targetType: inline + script: | + set -euo pipefail + # please.sh, makepkg-mingw, and Inno Setup's + # release.sh all detect this alias and route + # their per-file code-signing through it; see + # build-extra's please.sh + installer/release.sh. + script="$(cygpath -au "$BUILD_SOURCESDIRECTORY/.azure-pipelines/esrp/windows/esrpsign.sh")" + git config --global alias.signtool "!sh \"$script\"" + git config --global --get alias.signtool + - task: Bash@3 + displayName: 'Apply Windows build patches' + inputs: + targetType: inline + script: | + set -euo pipefail + apply="$(cygpath -au "$BUILD_SOURCESDIRECTORY/.azure-pipelines/scripts/apply-patches.sh")" + patches="$(cygpath -au "$BUILD_SOURCESDIRECTORY/.azure-pipelines/patches/windows")" + bash "$apply" "$patches/build-extra" /usr/src/build-extra + bash "$apply" "$patches/git-sdk" "/$(mingwprefix)" + - task: Bash@3 + displayName: 'Build mingw-w64-git package' + env: + # The mingw-w64-git build is heavy on parallel work + # that the underlying compile (and especially the + # contrib + doc + i18n + perl-script generation + # stages) can soak up far more aggressively than the + # core count would suggest, since most of it is I/O + # against the SDK's pacman cache and the make rules + # have very few real serialisation points. + MAKEFLAGS: -j15 + ${{ if eq(parameters.esrp, true) }}: + ESRP_TOOL: $(ESRP_TOOL) + ESRP_AUTH: $(ESRP_AUTH) + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + inputs: + targetType: inline + script: | + set -euo pipefail + set -x + + # Detach stdin so descendants like the git-extra + # post_install hook (which runs `for s in $(grep + # -l PAT $(find /mingw*/bin/ ...))` and falls back + # to reading stdin when /mingw*/bin/ is absent + # and find produces empty output) cannot block + # the build waiting for input. Bash@3 leaves the + # task's stdin pipe open with no writer; the + # GitHub Actions runner closes it for the same + # reason (see actions/runner ProcessInvoker.cs). + exec /usr/bin/git + chmod +x /usr/bin/git + + USER_NAME='microsoft-git-build' + USER_EMAIL='microsoft-git-build@users.noreply.github.com' + git config --global user.name "$USER_NAME" + git config --global user.email "$USER_EMAIL" + export PACKAGER="$USER_NAME <$USER_EMAIL>" + + # please.sh build-mingw-w64-git derives the package + # version by running `git for-each-ref --points-at=HEAD + # 'refs/tags/v[0-9]*'` against this source tree (and + # then pushing the chosen tag into the freshly cloned + # /usr/src/MINGW-packages/mingw-w64-git/{git,src/git} + # worktrees that makepkg-mingw actually builds from). + # If no v[0-9]* tag is found at HEAD, please.sh falls + # back to `git describe + timestamp` and creates a + # lightweight tag whose name has nothing to do with + # the pipeline-resolved $(git_version). On a real + # release run HEAD is already tagged via the tag-push + # trigger; on a debug run (e.g. the TO-DROP static- + # version override that only fakes git_version in the + # prereqs stage) it is not, so create an annotated + # tag at HEAD ourselves so please.sh picks it up and + # the resulting mingw-w64-git package, the + # cv2pdb-stripped binaries inside it, and the + # downstream Inno Setup installer are all named after + # the resolved git_version. If the tag already exists + # but does not point at HEAD, fail loudly: silently + # skipping creation would let please.sh's for-each-ref + # not find it (since it is not at HEAD) and fall back + # to the describe+timestamp path this whole block is + # supposed to prevent. + if git rev-parse --verify "refs/tags/v$(git_version)" + then + tag_commit="$(git rev-parse "refs/tags/v$(git_version)^{commit}")" + head_commit="$(git rev-parse HEAD)" + test "$tag_commit" = "$head_commit" || { + echo "##vso[task.logissue type=error]Tag v$(git_version) points at $tag_commit, not HEAD ($head_commit)" >&2 + exit 1 + } + else + git tag -a -m "v$(git_version)" "v$(git_version)" HEAD + fi + + # Pre-create the worktree that makepkg-mingw will + # actually compile, instead of letting please.sh's + # `clone --reference $bare https://github.com/git-for-windows/git` + # do it. The PKGBUILD's `build()` -> `make` invokes + # microsoft/git's GIT-VERSION-GEN, which `git + # describe`s in $srcdir/git and rejects any version + # whose `${VN%%.vfs.*}` does not match the hard-coded + # `${DEF_VER%%.vfs.*}` (`v2.53.0`). For real release + # tags the two match, but for any debug tag (in + # particular the v9.99.99.vfs.0.0 the TO-DROP commit + # produces) the build aborts with "Found version + # v9.99.99.vfs.0.0, which is not based on + # v2.53.0.vfs.0.0". GIT-VERSION-GEN reads a `version` + # file at the source-tree root in preference to + # running git describe, and the validation only + # fires on the describe path; so plant that file + # ahead of the build. + # + # please.sh would clone /usr/src/MINGW-packages + # itself if missing; we do the same clone first so + # the package directory exists and please.sh skips + # its own clone (line 838-840 of please.sh). + test -d /usr/src/MINGW-packages || + git clone --depth 1 --single-branch -b main \ + https://github.com/git-for-windows/MINGW-packages \ + /usr/src/MINGW-packages + + # mingw-w64-git/src/git is what makepkg's extract_git + # `git fetch`s and `git checkout --force --no-track + # -B makepkg `s into. Make it a worktree of this + # agent's checkout so the v$VERSION tag we just + # created is already visible there (worktrees share + # refs and objects with the main repo) and we don't + # have to duplicate the source. + # + # Worktrees also share `.git/config` with the main + # repo, so origin would point at the Azure-supplied + # remote URL and extract_git's `git fetch` would go + # online (and abort the build if it failed). Enable + # `extensions.worktreeConfig` and override origin to + # the local checkout via `git config --worktree` so + # the fetch stays on local disk and is effectively a + # no-op. Mirror please.sh's `core.autoCRLF=false` + # under the same per-worktree namespace so the + # checksums calc_checksum_git() computes against + # this tree are reproducible regardless of the main + # repo's autocrlf setting. + mkdir -p /usr/src/MINGW-packages/mingw-w64-git/src + test -d /usr/src/MINGW-packages/mingw-w64-git/src/git || { + git -C "$BUILD_SOURCESDIRECTORY" \ + config extensions.worktreeConfig true + git -C "$BUILD_SOURCESDIRECTORY" worktree add \ + /usr/src/MINGW-packages/mingw-w64-git/src/git HEAD + git -C /usr/src/MINGW-packages/mingw-w64-git/src/git \ + config --worktree remote.origin.url \ + "$BUILD_SOURCESDIRECTORY" + git -C /usr/src/MINGW-packages/mingw-w64-git/src/git \ + config --worktree core.autoCRLF false + } + + # The actual `version` file write that side-steps + # GIT-VERSION-GEN's validation. The file is untracked, + # so makepkg's `git checkout --force --no-track -B + # makepkg ` does not remove it. + echo "$BUILD_VERSION" \ + >/usr/src/MINGW-packages/mingw-w64-git/src/git/version + + sh -x /usr/src/build-extra/please.sh build-mingw-w64-git \ + --only-"$(cpu_arch)" \ + -o artifacts \ + HEAD + + # NOTE: the GitHub workflow additionally GPG-signs + # each tarball and creates a MINGW-packages.bundle + # for downstream Pacman consumers; both are + # intentionally out of scope for the initial port + # and tracked as follow-ups. + - task: Bash@3 + displayName: 'Build installer and portable Git' + env: + # `please.sh make_installers_from_mingw_w64_git` invokes + # build-extra's installer/release.sh, which requires + # MSYSTEM to select the architecture branch. Bash@3 does + # not source /etc/profile, so we export it explicitly. + MSYSTEM: ${{ dim.msystem }} + ${{ if eq(parameters.esrp, true) }}: ESRP_TOOL: $(ESRP_TOOL) ESRP_AUTH: $(ESRP_AUTH) SYSTEM_ACCESSTOKEN: $(System.AccessToken) - # TODO: put final artifacts under $(Build.ArtifactStagingDirectory)/_final - - script: | - mkdir $(Build.ArtifactStagingDirectory)\_final - xcopy /s /y $(Build.ArtifactStagingDirectory)\app $(Build.ArtifactStagingDirectory)\_final - displayName: 'Dummy collect artifacts' + inputs: + targetType: inline + script: | + set -euo pipefail + set -x + + # Detach stdin so descendants like the git-extra + # post_install hook (which runs `for s in $(grep + # -l PAT $(find /mingw*/bin/ ...))` and falls back + # to reading stdin when /mingw*/bin/ is absent + # and find produces empty output) cannot block + # the build waiting for input. Bash@3 leaves the + # task's stdin pipe open with no writer; the + # GitHub Actions runner closes it for the same + # reason (see actions/runner ProcessInvoker.cs). + exec /tmp/setx.sh + export BASH_ENV=/tmp/setx.sh + + # please.sh make_installers_from_mingw_w64_git + # --include-pdbs reads PDB archives from + # cached-source-packages/. + mkdir -p "$b/cached-source-packages" + cp artifacts/*-pdb* "$b/cached-source-packages/" + + # The --pkg=... list excludes the optional pieces + # the workflow drops (signatures, archimport, cvs, + # p4, gitweb, doc-man); keep the same filter so + # the resulting .exe size is comparable. + pkg_args=$( + ls artifacts/mingw-w64-$(toolchain)-*.tar.* \ + | sed '/\.sig$/d;/archimport/d;/cvs/d;/p4/d;/gitweb/d;/doc-man/d;s/^/--pkg=/' \ + | tr '\n' ' ' + ) + + for type in installer portable; do + eval sh -x "$b"/please.sh make_installers_from_mingw_w64_git --include-pdbs \ + --version="$(git_version)" \ + -o artifacts --"$type" \ + $pkg_args + + # The installer .exe is signed inline by Inno + # Setup via the `git signtool` alias; the + # portable .exe is a 7z self-extractor that + # bypasses that path, so sign it explicitly. + if test "$type" = portable && \ + test -n "$(git config alias.signtool)" + then + git signtool artifacts/PortableGit-*.exe + fi + done + - task: Bash@3 + displayName: 'Stage installer artifacts for upload' + inputs: + targetType: inline + script: | + set -euo pipefail + + # Compute SHA-256 over the (possibly signed) + # binaries; if ESRP signing ran, this picks up + # the post-sign bytes, which is what we want to + # publish in the release notes. + openssl dgst -sha256 \ + artifacts/Git-*.exe \ + artifacts/PortableGit-*.exe \ + | sed 's/.* //' >artifacts/sha-256.txt + + mkdir -p "$(Build.ArtifactStagingDirectory)/_final" + cp artifacts/Git-*.exe \ + artifacts/PortableGit-*.exe \ + artifacts/sha-256.txt \ + "$(Build.ArtifactStagingDirectory)/_final/" + # Validate the freshly built installer in-place: silently + # install Git-*.exe and assert that `git --version` reports + # the version we resolved at the prereqs stage. Folded into + # the build job so it runs on the same agent without the + # 1ES job-startup overhead a separate validate job carries. + - powershell: | + $exe = Get-ChildItem -Path "$(Build.ArtifactStagingDirectory)\_final\Git-*.exe" | + Where-Object { $_.Name -notlike 'PortableGit-*' } | + Select-Object -First 1 -ExpandProperty FullName + if (-not $exe) { + Write-Error "No Git-*.exe installer found in _final" + exit 1 + } + Write-Host "Installing $exe" + $p = Start-Process -Wait -PassThru -FilePath "$exe" ` + -ArgumentList "/SILENT","/VERYSILENT","/NORESTART","/SUPPRESSMSGBOXES","/ALLOWDOWNGRADE=1" + if ($p.ExitCode -ne 0) { + Write-Error "Installer exited with code $($p.ExitCode)" + exit $p.ExitCode + } + displayName: 'Install Git' + - powershell: | + $raw = & "$env:ProgramW6432\Git\cmd\git.exe" --version + $actual = ($raw -replace '^git version ', '').Trim() + $expect = ('$(git_version)' -replace '-rc', '.rc').Trim() + Write-Host "Expected: $expect" + Write-Host "Actual: $actual" + if ($actual -ne $expect) { + Write-Error "Version mismatch: expected '$expect', got '$actual'" + exit 1 + } + displayName: 'Validate installed version' # # macOS build jobs diff --git a/.azure-pipelines/scripts/apply-patches.sh b/.azure-pipelines/scripts/apply-patches.sh new file mode 100755 index 00000000000000..f1325a7caa0064 --- /dev/null +++ b/.azure-pipelines/scripts/apply-patches.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Apply all numbered patches from a directory to a target tree. +# +# Patches are applied in lexicographic order, so name them with +# zero-padded numeric prefixes (e.g. 0000-foo.patch, 0001-bar.patch). +# +# Arguments: +# $1 patches_dir Directory containing *.patch files +# $2 target_dir Directory to apply patches in (need not be a +# git repository; git apply works on any tree) + +set -euo pipefail + +if test $# -ne 2 +then + echo "Usage: $0 " >&2 + exit 1 +fi + +patches_dir="$1" +target_dir="$2" + +if test ! -d "$patches_dir" +then + echo "Patches directory not found: $patches_dir" >&2 + exit 1 +fi + +if test ! -d "$target_dir" +then + echo "Target directory not found: $target_dir" >&2 + exit 1 +fi + +shopt -s nullglob +patches=("$patches_dir"/*.patch) +if test ${#patches[@]} -eq 0 +then + echo "No patches found in $patches_dir" + exit 0 +fi + +cd "$target_dir" +for patch in "${patches[@]}" +do + echo "Applying $(basename "$patch")..." + # Use patch(1) rather than `git apply` because the latter is + # strict about context whitespace; CRLF/LF mismatches between + # patch context (as authored) and the working tree (which may + # be CRLF on Windows checkouts) trip it up. patch is more + # forgiving by default. + # + # This matches the convention used by msys2/MINGW-packages + # PKGBUILDs and git-for-windows/build-extra's get-sources.sh. + command patch -p1 -i "$patch" +done diff --git a/.azure-pipelines/scripts/windows/setup-cv2pdb-x64.ps1 b/.azure-pipelines/scripts/windows/setup-cv2pdb-x64.ps1 new file mode 100644 index 00000000000000..a035f43897e4b8 --- /dev/null +++ b/.azure-pipelines/scripts/windows/setup-cv2pdb-x64.ps1 @@ -0,0 +1,99 @@ +# Set up cv2pdb-strip support on Windows x64 agents. +# +# build-extra's please.sh runs cv2pdb-strip during the strip phase of +# build-mingw-w64-git. cv2pdb-strip loads mspdb140.dll via PATH +# lookup, and the DLL is part of the MSVC C++ toolchain +# (Microsoft.VisualStudio.Component.VC.Tools.x86.x64) which is not +# present on the 1ES image by default. +# +# Install VS 2022 Build Tools with that single component (the +# smallest selection that ships the DLL), locate mspdb140.dll via +# vswhere with a filesystem fallback, and prepend its directory to +# PATH for subsequent tasks via the `##vso[task.prependpath]` logging +# command. +# +# This script is intended to be invoked by a PowerShell@2 task with +# `filePath:`. It takes no arguments and writes diagnostics to stdout +# so install failures can be diagnosed from the task log. + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +$bootstrapper = "$env:TEMP\vs_BuildTools.exe" +Write-Host "Downloading VS 2022 Build Tools bootstrapper..." +Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_BuildTools.exe' ` + -OutFile $bootstrapper + +$vsArgs = @( + '--quiet', '--wait', '--norestart', '--nocache', + '--add', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64' +) +Write-Host "Installing VS Build Tools (args: $($vsArgs -join ' '))..." +$start = Get-Date +$p = Start-Process -FilePath $bootstrapper -ArgumentList $vsArgs -Wait -PassThru +$elapsed = (Get-Date) - $start +Write-Host ("Installer exited with code {0} after {1:N0}s" -f ` + $p.ExitCode, $elapsed.TotalSeconds) + +Write-Host "" +Write-Host "===== Installer logs in `$env:TEMP =====" +$logs = Get-ChildItem $env:TEMP -Filter 'dd_*.log' -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending +if ($logs) { + foreach ($log in $logs | Select-Object -First 5) { + Write-Host "----- $($log.FullName) (last 50 lines) -----" + Get-Content $log.FullName -Tail 50 -ErrorAction SilentlyContinue + } +} else { + Write-Host "(no dd_*.log files found in `$env:TEMP)" +} + +Write-Host "" +Write-Host "===== vswhere -all -prerelease (every install) =====" +$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +if (-not (Test-Path $vswhere)) { + Write-Host "vswhere not found at $vswhere" +} else { + & $vswhere -all -prerelease -format json | + Out-String | Write-Host +} + +Write-Host "" +Write-Host "===== Filesystem search for mspdb*.dll =====" +$roots = @( + "${env:ProgramFiles(x86)}\Microsoft Visual Studio", + "${env:ProgramFiles}\Microsoft Visual Studio" +) | Where-Object { Test-Path $_ } +$hits = foreach ($r in $roots) { + Get-ChildItem -Path $r -Filter 'mspdb*.dll' -Recurse -File ` + -ErrorAction SilentlyContinue +} +if ($hits) { + $hits | ForEach-Object { Write-Host $_.FullName } +} else { + Write-Host "(no mspdb*.dll under any VS install root)" +} + +# 3010 = reboot required, treated as success. +if ($p.ExitCode -notin 0,3010) { + throw "VS Build Tools installer exited with code $($p.ExitCode)" +} + +Write-Host "" +Write-Host "===== Locate mspdb140.dll via vswhere -find =====" +$mspdb = & $vswhere -latest ` + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -find 'VC\Tools\MSVC\**\bin\Hostx64\x64\mspdb140.dll' | + Select-Object -First 1 +if (-not $mspdb) { + # Fall back to filesystem hits we already have. + $mspdb = $hits | + Where-Object { $_.Name -ieq 'mspdb140.dll' } | + Select-Object -First 1 -ExpandProperty FullName +} +if (-not $mspdb) { + throw "mspdb140.dll not found after install (see logs above)" +} +$dir = Split-Path -Parent $mspdb +Write-Host "Found mspdb140.dll at $mspdb" +Write-Host "##vso[task.prependpath]$dir" diff --git a/.azure-pipelines/scripts/windows/setup-git-sdk.sh b/.azure-pipelines/scripts/windows/setup-git-sdk.sh new file mode 100755 index 00000000000000..c123ec3682a9b5 --- /dev/null +++ b/.azure-pipelines/scripts/windows/setup-git-sdk.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Materialise the build-installers flavour of the Git for Windows SDK. +# +# Performs a partial + bare clone of the given Git SDK repository, +# then runs build-extra's please.sh to sparse-checkout just the +# build-installers subset into the requested SDK output directory. +# +# Environment: +# BOOTSTRAP_DIR (optional) - directory for transient bootstrap clones +# (the bare git-sdk fetch and build-extra +# checkout used to drive please.sh). +# Falls back to TEMP, then TMP, then errors +# if none are set. +# +# Arguments: +# $1 sdk_repo e.g. git-for-windows/git-sdk-64 +# $2 mingwprefix e.g. mingw64 or clangarm64 +# $3 sdk_output_dir Windows or MSYS path where the SDK will be installed +# +# See: +# https://github.com/git-for-windows/git-sdk-64/blob/main/.github/workflows/ci-artifacts.yml +# https://github.com/git-for-windows/build-extra/blob/main/please.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$SCRIPT_DIR/utils.sh" + +if test $# -ne 3 +then + echo "Usage: $0 " >&2 + exit 1 +fi + +sdk_repo="$1" +mingwprefix="$2" +sdk_output="$3" + +bootstrap_dir="${BOOTSTRAP_DIR:-${TEMP:-${TMP:-}}}" +if test -z "$bootstrap_dir" +then + echo "BOOTSTRAP_DIR (or TEMP/TMP) must be set" >&2 + exit 1 +fi + +bootstrap="$(to_unix_path "$bootstrap_dir")" +sdk="$(to_unix_path "$sdk_output")" + +sdk_bare="$bootstrap/sdk-bare.git" +bootstrap_be="$bootstrap/build-extra-bootstrap" + +git init --bare "$sdk_bare" +git --git-dir="$sdk_bare" remote add origin "https://github.com/$sdk_repo" +git --git-dir="$sdk_bare" config remote.origin.promisor true +git --git-dir="$sdk_bare" config remote.origin.partialCloneFilter blob:none +git --git-dir="$sdk_bare" fetch --depth=1 origin HEAD +git --git-dir="$sdk_bare" update-ref --no-deref HEAD FETCH_HEAD + +# please.sh is the bootstrap; build-extra gets cloned again into the SDK +# in a separate task so `please.sh build-mingw-w64-git` can find it at +# /usr/src/build-extra under the SDK's bash. +git clone --depth=1 --single-branch -b main \ + https://github.com/git-for-windows/build-extra \ + "$bootstrap_be" + +# Architecture is auto-detected from the bare clone's HEAD tree +# (clangarm64/ vs usr/x86_64-pc-msys/). +bash "$bootstrap_be/please.sh" create-sdk-artifact \ + --sdk="$sdk_bare" --out="$sdk" build-installers + +# Expose the SDK's bash and the matching MinGW toolchain to subsequent +# tasks. +echo "##vso[task.prependpath]$(to_windows_path "$sdk/usr/bin")" +echo "##vso[task.prependpath]$(to_windows_path "$sdk/$mingwprefix/bin")" diff --git a/.azure-pipelines/scripts/windows/utils.sh b/.azure-pipelines/scripts/windows/utils.sh new file mode 100755 index 00000000000000..f94c380a2b49af --- /dev/null +++ b/.azure-pipelines/scripts/windows/utils.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Utilities for bash scripts running on Windows. +# +# Source this file from another bash script: +# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# . "$SCRIPT_DIR/utils.sh" +# +# Functions: +# to_windows_path - output a Windows-form (D:\foo) path. +# to_unix_path - output an MSYS-form (/d/foo) path. + +# Convert a path to Windows form for tools that demand backslashes +# (e.g. ESRPClient.exe, ##vso[task.prependpath]). +# Useful when a script may run before the full Git for Windows SDK +# (which provides cygpath) is available. Falls back to pure-shell +# parsing when cygpath is not on PATH. +to_windows_path () { + local drive rest root + if command -v cygpath >/dev/null 2>&1; then + cygpath -w "$1" + return + fi + case "$1" in + /[A-Za-z]/*) + # /d/path -> D:\path + drive=$(echo "$1" | cut -c2 | tr 'a-z' 'A-Z') + rest=$(echo "$1" | cut -c3-) + echo "${drive}:${rest}" | sed 's|/|\\|g' + ;; + /*) + # Absolute path under MSYS root + root=$(cd / && pwd -W) + echo "${root}${1}" | sed 's|/|\\|g' + ;; + *) + # Relative or already-Windows: just flip slashes + echo "$1" | sed 's|/|\\|g' + ;; + esac +} + +# Convert a path to MSYS form for bash-friendly handling. Inverse of +# to_windows_path. +# Useful when a script may run before the full Git for Windows SDK +# (which provides cygpath) is available. Falls back to pure-shell +# parsing when cygpath is not on PATH. +to_unix_path () { + local p drive rest + if command -v cygpath >/dev/null 2>&1; then + cygpath -u "$1" + return + fi + # Normalize separators to forward slashes first. + p="${1//\\//}" + case "$p" in + [A-Za-z]:/*) + # D:/path -> /d/path + drive=$(echo "$p" | cut -c1 | tr 'A-Z' 'a-z') + rest=$(echo "$p" | cut -c3-) + echo "/${drive}${rest}" + ;; + *) + echo "$p" + ;; + esac +} From a652f28d10dc0c40efc8d692cdf1ca4372dfc0e9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 May 2026 17:32:57 +0200 Subject: [PATCH 7/8] azure-pipelines: enable on tag push, default ESRP and GitHub release on With build, signing, notarization, validation, and draft-release publishing all in place, the Azure Pipeline is ready to take over from the GitHub Actions build-git-installers workflow. Switch `trigger: none` to a tag-only trigger matching the same `v[0-9]*vfs*` pattern the GitHub workflow used (and that resolve-version.sh validates against), and explicitly exclude all branches so the pipeline does not fire on every topic-branch push. Flip the `esrp` and `github` parameter defaults from false to true. The GitHub release job still uses `isDraft: true`, so a maintainer inspects and publishes the release manually; manual runs in the Azure DevOps UI can still uncheck either box for a dry run. Assisted-by: Claude Opus 4.7 Co-authored-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- .azure-pipelines/release.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index dff8e8f3770b36..02e9fd434eacdc 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -1,5 +1,11 @@ name: $(Date:yyyyMMdd)$(Rev:.r) -trigger: none +trigger: + branches: + exclude: + - '*' + tags: + include: + - v[0-9]*vfs* pr: none resources: @@ -12,11 +18,11 @@ resources: parameters: - name: 'esrp' type: boolean - default: false # TODO: change default to true after testing + default: true displayName: 'Enable ESRP code signing' - name: 'github' type: boolean - default: false # TODO: change default to true after testing + default: true displayName: 'Enable GitHub release publishing' - name: 'versionOverride' type: string From 399666a4623d8ad2b2937c0ec864c41ba76662b3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 May 2026 17:37:50 +0200 Subject: [PATCH 8/8] amend! release: create initial Windows installer build workflow release: create initial Windows installer build workflow Add a manual-only GitHub Actions workflow for building the Windows installer (x86_64 plus portable Git), driven via `workflow_dispatch:`. The production release path for the official microsoft/git installers lives in .azure-pipelines/release.yml; this workflow is kept around as a fallback so the Windows installer can still be produced on demand for debugging or comparison. The build steps are pinned to `windows-2019` (rather than `windows-latest`) to ensure the correct Visual Studio version is used (verified in the pipeline via `type -p mspdb140.dll`), and the SDK used is the `full` flavor rather than `build-installers` due to a known (but not-yet-fixed) issue downloading the `build-installers` flavor with the `git-for-windows/setup-git-for-windows-sdk` Action. There is no code-signing certificate available to this workflow, so the artifacts it produces are unsigned and must not be published as releases; they are useful only for build-time debugging. Signed-off-by: Victoria Dye Assisted-by: Claude Opus 4.7 Co-authored-by: Matthew John Cheetham Signed-off-by: Johannes Schindelin --- .github/workflows/build-git-installers.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-git-installers.yml b/.github/workflows/build-git-installers.yml index ef07a5c27b15fe..af988faa97bc70 100644 --- a/.github/workflows/build-git-installers.yml +++ b/.github/workflows/build-git-installers.yml @@ -1,9 +1,17 @@ name: build-git-installers +# This workflow used to run automatically on every `v*vfs*` tag push to +# build and publish the official microsoft/git installers. The signed +# release builds have moved to the Azure Pipeline in +# `.azure-pipelines/release.yml`, which is what now runs on tag push. +# +# We no longer have access to a permissible code signing certificate +# from this workflow, so the artifacts it produces would not be properly +# code-signed and must not be published as releases. Keep the workflow +# around behind a `workflow_dispatch:` trigger only, so it can still be +# invoked manually for debugging or comparison purposes. on: - push: - tags: - - 'v[0-9]*vfs*' # matches "vvfs" + workflow_dispatch: permissions: id-token: write # required for Azure login via OIDC