diff --git a/.azure-pipelines/esrp/sign.yml b/.azure-pipelines/esrp/sign.yml new file mode 100644 index 0000000000..31c9822eba --- /dev/null +++ b/.azure-pipelines/esrp/sign.yml @@ -0,0 +1,176 @@ +# 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 + # Comma-separated list of MIME types (as reported by 'file --mime-type'). When + # set, the staged files are filtered down to just those whose type matches one + # of the listed types before signing, so a mixed payload can be passed straight + # through and only the matching files are signed (e.g. pass + # 'application/x-mach-binary' to sign only Mach-O executables and dylibs). Only + # applies when useArchive is true. + - name: archiveMimeFilter + type: string + default: '' + # Where to move the CodeSignSummary-*.md receipt that ESRP writes alongside + # the signed files. When empty (default) the receipt is deleted instead, so it + # is never packaged or published with the signed output. + - name: codeSignSummaryPath + type: string + default: '' + # 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' + - ${{ if ne(parameters.archiveMimeFilter, '') }}: + # Filter the staged files to the requested MIME type(s); ESRP would + # otherwise try to sign every file in a mixed payload. + - task: Bash@3 + displayName: 'Filter files by MIME type for ${{ parameters.displayName }}' + inputs: + targetType: inline + script: | + set -euo pipefail + dir="$(Agent.TempDirectory)/esrp-staging/contents" + # Comma-separated list of allowed 'file --mime-type' values; strip spaces. + filter="$(printf '%s' "${{ parameters.archiveMimeFilter }}" | tr -d '[:space:]')" + find "$dir" -type f -print0 \ + | while IFS= read -r -d '' f; do + mt="$(file --mime-type -b "$f" 2>/dev/null || true)" + # For fat/universal binaries 'file' prints a line per + # architecture; the first line is the overall type. + mt="${mt%%$'\n'*}" + case ",$filter," in + *",$mt,"*) : ;; # keep: type is in the filter list + *) rm -f "$f" ;; # drop: not a requested type + esac + done + # Drop any directories left empty by the filtering. + find "$dir" -type d -empty -delete 2>/dev/null || true + - 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 }}' + # Only the signed files are present in the returned archive (the MIME + # filter can exclude others), so cleaning the destination would delete + # every unsigned file that belongs in the folder. Overwrite the + # originals in place instead of cleaning first. + cleanDestinationFolder: false + overwriteExistingFiles: true + # ESRP writes a CodeSignSummary-*.md receipt into the staging dir; move it + # aside first when a destination is given, otherwise the cleanup below + # deletes it along with the rest of the staging dir. + - ${{ if ne(parameters.codeSignSummaryPath, '') }}: + - task: CopyFiles@2 + displayName: 'Move code signing summary for ${{ parameters.displayName }}' + inputs: + SourceFolder: '$(Agent.TempDirectory)/esrp-staging' + Contents: '**/CodeSignSummary-*.md' + TargetFolder: '${{ parameters.codeSignSummaryPath }}' + - 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 }} + # ESRP writes a CodeSignSummary-*.md receipt next to the signed files. Move + # it to the given destination when set, then always remove it from the + # signed folder so it is not packaged or published with the output. + - ${{ if ne(parameters.codeSignSummaryPath, '') }}: + - task: CopyFiles@2 + displayName: 'Move code signing summary for ${{ parameters.displayName }}' + inputs: + SourceFolder: '${{ parameters.folderPath }}' + Contents: '**/CodeSignSummary-*.md' + TargetFolder: '${{ parameters.codeSignSummaryPath }}' + - task: DeleteFiles@1 + displayName: 'Remove code signing summary for ${{ parameters.displayName }}' + inputs: + SourceFolder: '${{ parameters.folderPath }}' + Contents: '**/CodeSignSummary-*.md' diff --git a/.azure-pipelines/nuget.config b/.azure-pipelines/nuget.config new file mode 100644 index 0000000000..0cdfa50d8e --- /dev/null +++ b/.azure-pipelines/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index a957609d89..8ba259ffa7 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -134,52 +134,53 @@ extends: artifactName: '${{ dim.runtime }}' steps: - checkout: self + - task: CopyFiles@2 + displayName: 'Use Central Feed Services (CFS)' + inputs: + SourceFolder: '$(Build.SourcesDirectory)\.azure-pipelines' + Contents: 'nuget.config' + TargetFolder: '$(Build.SourcesDirectory)' + Overwrite: true + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to NuGet feeds' - task: PowerShell@2 - displayName: 'Read version file' + displayName: 'Install .NET 10 SDK' inputs: targetType: inline script: | - $version = (Get-Content .\VERSION) -replace '\.\d+$', '' - Write-Host "##vso[task.setvariable variable=version;isReadOnly=true]$version" - - task: UseDotNet@2 - displayName: 'Use .NET 10 SDK' + # UseDotNet@2 mis-detects Windows-on-Arm agents as win-x86 + # and installs the 32-bit SDK, which breaks the win-arm64 + # AOT publish. Call dotnet-install directly, keyed on the + # agent architecture, so each leg gets a matching host SDK. + $ProgressPreference = 'SilentlyContinue' + $installDir = Join-Path $env:AGENT_TEMPDIRECTORY 'dotnet' + $installer = Join-Path $env:AGENT_TEMPDIRECTORY 'dotnet-install.ps1' + Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $installer + & $installer -Channel 10.0 -Architecture ${{ dim.poolArch }} -InstallDir $installDir + Write-Host "##vso[task.prependpath]$installDir" + # Native AOT links the published binary with the MSVC linker, + # which our images do not ship by default. Install the VC++ + # build tools for the agent's architecture first. + - task: PowerShell@2 + displayName: 'Install C++ build tools for AOT' inputs: - packageType: sdk - version: '10.x' + filePath: '.azure-pipelines/scripts/windows/setup-aot-build-tools.ps1' + arguments: -Architecture ${{ dim.poolArch }} - task: PowerShell@2 - displayName: 'Build payload' + displayName: 'Publish payload' inputs: + pwsh: true targetType: filePath - filePath: '.\src\windows\Installer.Windows\layout.ps1' - arguments: | - -Configuration Release ` - -Output $(Build.ArtifactStagingDirectory)\payload ` - -SymbolOutput $(Build.ArtifactStagingDirectory)\symbols_raw ` - -RuntimeIdentifier ${{ dim.runtime }} - - task: ArchiveFiles@2 - displayName: 'Archive symbols' - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)\symbols_raw' - includeRootFolder: false - archiveType: zip - archiveFile: '$(Build.ArtifactStagingDirectory)\symbols\gcm-${{ dim.runtime }}-$(version)-symbols.zip' + filePath: '.\build\windows\publish.ps1' + arguments: -Configuration release -Runtime ${{ dim.runtime }} + # Sign the payload binaries before they are packaged into the + # installers. - ${{ if eq(parameters.esrp, true) }}: - - task: EsrpCodeSigning@5 - displayName: 'Sign payload' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)\payload' - pattern: | - **/*.exe - **/*.dll - useMinimatch: true - signConfigType: inlineSignParams + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Sign payload' + folderPath: '$(Build.SourcesDirectory)\out\publish\git-credential-manager\release_${{ dim.runtime }}' + pattern: '**/*.{exe,dll}' inlineOperation: | [ { @@ -203,40 +204,34 @@ extends: "Parameters": {} } ] - - task: PowerShell@2 - displayName: 'Clean up code signing artifacts' - inputs: - targetType: inline - script: | - Remove-Item "$(Build.ArtifactStagingDirectory)\payload\CodeSignSummary-*.md" + # Download the Inno Setup compiler (ISCC.exe) used by pack.ps1 to + # build the installers, and expose its path to the pack step. The + # script downloads the Tools.InnoSetup version pinned in + # Directory.Packages.props. - task: PowerShell@2 - displayName: 'Build installers' + displayName: 'Download Inno Setup' inputs: + pwsh: true targetType: inline script: | - dotnet build '.\src\windows\Installer.Windows\Installer.Windows.csproj' ` - --configuration Release ` - --no-dependencies ` - -p:NoLayout=true ` - -p:PayloadPath="$(Build.ArtifactStagingDirectory)\payload" ` - -p:OutputPath="$(Build.ArtifactStagingDirectory)\installers" ` - -p:RuntimeIdentifier="${{ dim.runtime }}" + $inno = & "$(Build.SourcesDirectory)\.azure-pipelines\scripts\windows\download-innosetup.ps1" -OutputPath "$(Agent.TempDirectory)\innosetup" + Write-Host "Resolved Inno Setup $($inno.Version): $($inno.Path)" + Write-Host "##vso[task.setvariable variable=innoSetupCompiler]$($inno.Path)" + # Build the installer packages (.exe) from the signed payload + - task: PowerShell@2 + displayName: 'Build installers' + inputs: + pwsh: true + targetType: filePath + filePath: '.\build\windows\pack.ps1' + arguments: -Configuration release -Runtime ${{ dim.runtime }} -InnoSetup "$(innoSetupCompiler)" + # Sign the installer executables - ${{ if eq(parameters.esrp, true) }}: - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign installers' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)\installers' + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Sign installers' + folderPath: '$(Build.SourcesDirectory)\out\package\release' pattern: '**/*.exe' - useMinimatch: true - signConfigType: inlineSignParams inlineOperation: | [ { @@ -260,23 +255,23 @@ extends: "Parameters": {} } ] - - task: ArchiveFiles@2 + # Archive the signed payload (.zip) alongside the installers + - task: PowerShell@2 displayName: 'Archive payload' inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)\payload' - includeRootFolder: false - archiveType: zip - archiveFile: '$(Build.ArtifactStagingDirectory)\installers\gcm-${{ dim.runtime }}-$(version).zip' + pwsh: true + targetType: filePath + filePath: '.\build\windows\archive.ps1' + arguments: -Configuration release -Runtime ${{ dim.runtime }} - task: PowerShell@2 displayName: 'Collect artifacts for publishing' inputs: + pwsh: true targetType: inline script: | New-Item -Path "$(Build.ArtifactStagingDirectory)\_final" -ItemType Directory -Force - Copy-Item "$(Build.ArtifactStagingDirectory)\installers\*.exe" -Destination "$(Build.ArtifactStagingDirectory)\_final" - Copy-Item "$(Build.ArtifactStagingDirectory)\installers\*.zip" -Destination "$(Build.ArtifactStagingDirectory)\_final" - Copy-Item "$(Build.ArtifactStagingDirectory)\symbols\*.zip" -Destination "$(Build.ArtifactStagingDirectory)\_final" - Copy-Item "$(Build.ArtifactStagingDirectory)\payload" -Destination "$(Build.ArtifactStagingDirectory)\_final" -Recurse + Copy-Item "$(Build.SourcesDirectory)\out\package\release\*.exe" -Destination "$(Build.ArtifactStagingDirectory)\_final" + Copy-Item "$(Build.SourcesDirectory)\out\package\release\*.zip" -Destination "$(Build.ArtifactStagingDirectory)\_final" # # macOS build jobs @@ -295,120 +290,61 @@ extends: artifactName: '${{ dim.runtime }}' steps: - checkout: self - - task: Bash@3 - displayName: 'Read version file' + - task: CopyFiles@2 + displayName: 'Use Central Feed Services (CFS)' inputs: - targetType: inline - script: | - echo "##vso[task.setvariable variable=version;isReadOnly=true]$(cat ./VERSION | sed -E 's/.[0-9]+$//')" - - task: UseDotNet@2 - displayName: 'Use .NET 8 SDK (ESRP dependency)' - inputs: - packageType: sdk - version: '8.x' + SourceFolder: '$(Build.SourcesDirectory)/.azure-pipelines' + Contents: 'nuget.config' + TargetFolder: '$(Build.SourcesDirectory)' + Overwrite: true + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to NuGet feeds' - task: UseDotNet@2 displayName: 'Use .NET 10 SDK' inputs: packageType: sdk version: '10.x' - task: Bash@3 - displayName: 'Build payload' + displayName: 'Publish payload' inputs: targetType: filePath - filePath: './src/osx/Installer.Mac/layout.sh' + filePath: './build/macos/publish.sh' arguments: | - --runtime="${{ dim.runtime }}" \ - --configuration="Release" \ - --output="$(Build.ArtifactStagingDirectory)/payload" \ - --symbol-output="$(Build.ArtifactStagingDirectory)/symbols_raw" - - task: ArchiveFiles@2 - displayName: 'Archive symbols' - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/symbols_raw' - includeRootFolder: false - archiveType: tar - tarCompression: gz - archiveFile: '$(Build.ArtifactStagingDirectory)/symbols/gcm-${{ dim.runtime }}-$(version)-symbols.tar.gz' + --configuration release \ + --runtime ${{ dim.runtime }} - ${{ if eq(parameters.esrp, true) }}: - - task: AzureKeyVault@2 - displayName: 'Download developer certificate' - inputs: - azureSubscription: '$(esrpMIConnectionName)' - keyVaultName: '$(esrpKeyVaultName)' - secretsFilter: 'mac-developer-certificate,mac-developer-certificate-password,mac-developer-certificate-identity' - - task: Bash@3 + - task: AzureCLI@2 displayName: 'Import developer certificate' inputs: - targetType: inline - script: | - # Create and unlock a keychain for the developer certificate - security create-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain - security default-keychain -s $(Agent.TempDirectory)/buildagent.keychain - security unlock-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain - - echo $(mac-developer-certificate) | base64 -D > $(Agent.TempDirectory)/cert.p12 - echo $(mac-developer-certificate-password) > $(Agent.TempDirectory)/cert.password - - # Import the developer certificate - security import $(Agent.TempDirectory)/cert.p12 \ - -k $(Agent.TempDirectory)/buildagent.keychain \ - -P "$(mac-developer-certificate-password)" \ - -T /usr/bin/codesign - - # Clean up the cert file immediately after import - rm $(Agent.TempDirectory)/cert.p12 - - # Set ACLs to allow codesign to access the private key - security set-key-partition-list \ - -S apple-tool:,apple:,codesign: \ - -s -k pwd \ - $(Agent.TempDirectory)/buildagent.keychain + azureSubscription: '$(esrpMIConnectionName)' + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + set -euo pipefail + identity=$(./.azure-pipelines/scripts/macos/import-developer-certificate.sh \ + --vault "$(esrpKeyVaultName)" \ + --keychain "$(Agent.TempDirectory)/buildagent.keychain") + echo "##vso[task.setvariable variable=macDevCertIdentity;issecret=true]$identity" + # Developer-sign the payload (attaching entitlements and the + # hardened runtime) and then ESRP-sign the Mach-O binaries in + # place. + - ${{ if eq(parameters.esrp, true) }}: - task: Bash@3 - displayName: 'Developer sign payload files' - inputs: - targetType: inline - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/tosign/payload - - # Copy the files that need signing (Mach-o executables and dylibs) - pushd $(Build.ArtifactStagingDirectory)/payload - find . -type f -exec file --mime {} + \ - | sed -n '/mach/s/: .*//p' \ - | while IFS= read -r f; do - rel="${f#./}" - tgt="$(Build.ArtifactStagingDirectory)/tosign/payload/$rel" - mkdir -p "$(dirname "$tgt")" - cp -- "$f" "$tgt" - done - popd - - # Developer sign the files - ./src/osx/Installer.Mac/codesign.sh \ - "$(Build.ArtifactStagingDirectory)/tosign/payload" \ - "$(mac-developer-certificate-identity)" \ - "$PWD/src/osx/Installer.Mac/entitlements.xml" - # ESRP code signing for macOS requires the files be packaged in a zip file for submission - - task: ArchiveFiles@2 - displayName: 'Archive files for signing' + displayName: 'Developer-sign payload' inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/tosign/payload' - includeRootFolder: false - archiveType: zip - archiveFile: '$(Build.ArtifactStagingDirectory)/tosign/payload.zip' - - task: EsrpCodeSigning@5 - displayName: 'Sign payload' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/tosign' - pattern: 'payload.zip' - useMinimatch: true - signConfigType: inlineSignParams + targetType: filePath + filePath: './build/macos/codesign.sh' + arguments: | + developer \ + --bindir "$(Build.SourcesDirectory)/out/publish/git-credential-manager/release_${{ dim.runtime }}" \ + --identity "$(macDevCertIdentity)" + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Sign payload' + folderPath: '$(Build.SourcesDirectory)/out/publish/git-credential-manager/release_${{ dim.runtime }}' + pattern: '**/*' + useArchive: true # Required for macOS signing + archiveMimeFilter: 'application/x-mach-binary' # Sign only Mach-O binaries inlineOperation: | [ { @@ -421,56 +357,23 @@ extends: } } ] - # Extract signed files, overwriting the unsigned files, ready for packaging - - task: Bash@3 - displayName: 'Extract signed payload files' - inputs: - targetType: inline - script: | - unzip -uo $(Build.ArtifactStagingDirectory)/tosign/payload.zip -d $(Build.ArtifactStagingDirectory)/payload - - task: Bash@3 - displayName: 'Build component package' - inputs: - targetType: filePath - filePath: './src/osx/Installer.Mac/pack.sh' - arguments: | - --version="$(version)" \ - --payload="$(Build.ArtifactStagingDirectory)/payload" \ - --output="$(Build.ArtifactStagingDirectory)/pkg/com.microsoft.gitcredentialmanager.component.pkg" + # Package the (signed) binaries into the installer (.pkg) - task: Bash@3 - displayName: 'Build installer package' + displayName: 'Build installer' inputs: targetType: filePath - filePath: './src/osx/Installer.Mac/dist.sh' + filePath: './build/macos/pack.sh' arguments: | - --version="$(version)" \ - --runtime="${{ dim.runtime }}" \ - --package-path="$(Build.ArtifactStagingDirectory)/pkg" \ - --output="$(Build.ArtifactStagingDirectory)/installers/gcm-${{ dim.runtime }}-$(version).pkg" + --configuration release \ + --runtime ${{ dim.runtime }} + # ESRP-sign and notarize the installer package - ${{ if eq(parameters.esrp, true) }}: - # ESRP code signing for macOS requires the files be packaged in a zip file first - - task: Bash@3 - displayName: 'Prepare installer package for signing' - inputs: - targetType: inline - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/tosign - cd $(Build.ArtifactStagingDirectory)/installers - zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg - - task: EsrpCodeSigning@5 - displayName: 'Sign installer package' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/tosign' - pattern: 'installers.zip' - useMinimatch: true - signConfigType: inlineSignParams + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Sign installer' + folderPath: '$(Build.SourcesDirectory)/out/package/release' + pattern: '*.pkg' + useArchive: true # Required for macOS signing inlineOperation: | [ { @@ -483,37 +386,12 @@ extends: } } ] - # Extract signed installer, overwriting the unsigned installer - - task: Bash@3 - displayName: 'Extract signed installer package' - inputs: - targetType: inline - script: | - unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers - - task: Bash@3 - displayName: 'Prepare installer package for notarization' - inputs: - targetType: inline - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/tosign - cd $(Build.ArtifactStagingDirectory)/installers - # Remove previous installers.zip to avoid any confusion - rm -f $(Build.ArtifactStagingDirectory)/tosign/installers.zip - zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg - - task: EsrpCodeSigning@5 - displayName: 'Notarize installer package' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/tosign' - pattern: 'installers.zip' - useMinimatch: true - signConfigType: inlineSignParams + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Notarize installer' + folderPath: '$(Build.SourcesDirectory)/out/package/release' + pattern: '*.pkg' + useArchive: false # Notarization takes the .pkg container directly without archiving inlineOperation: | [ { @@ -526,31 +404,23 @@ extends: } } ] - # Extract signed and notarized installer pkg files, overwriting the unsigned files, ready for upload - - task: Bash@3 - displayName: 'Extract signed and notarized installer package' - inputs: - targetType: inline - script: | - unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers - - task: ArchiveFiles@2 + # Create the (signed) binaries and symbols tarballs + - task: Bash@3 displayName: 'Archive payload' inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/payload' - includeRootFolder: false - archiveType: tar - tarCompression: gz - archiveFile: '$(Build.ArtifactStagingDirectory)/installers/gcm-${{ dim.runtime }}-$(version).tar.gz' + targetType: filePath + filePath: './build/macos/archive.sh' + arguments: | + --configuration release \ + --runtime ${{ dim.runtime }} - task: Bash@3 displayName: 'Collect artifacts for publishing' inputs: targetType: inline script: | mkdir -p $(Build.ArtifactStagingDirectory)/_final - cp $(Build.ArtifactStagingDirectory)/installers/*.pkg $(Build.ArtifactStagingDirectory)/_final - cp $(Build.ArtifactStagingDirectory)/installers/*.tar.gz $(Build.ArtifactStagingDirectory)/_final - cp $(Build.ArtifactStagingDirectory)/symbols/*.tar.gz $(Build.ArtifactStagingDirectory)/_final - cp -r $(Build.ArtifactStagingDirectory)/payload $(Build.ArtifactStagingDirectory)/_final + cp $(Build.SourcesDirectory)/out/package/release/*.pkg $(Build.ArtifactStagingDirectory)/_final + cp $(Build.SourcesDirectory)/out/package/release/*.tar.gz $(Build.ArtifactStagingDirectory)/_final # # Linux build jobs @@ -570,72 +440,45 @@ extends: artifactName: '${{ dim.runtime }}' steps: - checkout: self - - task: Bash@3 - displayName: 'Read version file' - inputs: - targetType: inline - script: | - echo "##vso[task.setvariable variable=version;isReadOnly=true]$(cat ./VERSION | sed -E 's/.[0-9]+$//')" - - task: UseDotNet@2 - displayName: 'Use .NET 8 SDK (ESRP dependency)' + - task: CopyFiles@2 + displayName: 'Use Central Feed Services (CFS)' inputs: - packageType: sdk - version: '8.x' + SourceFolder: '$(Build.SourcesDirectory)/.azure-pipelines' + Contents: 'nuget.config' + TargetFolder: '$(Build.SourcesDirectory)' + Overwrite: true + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to NuGet feeds' - task: UseDotNet@2 displayName: 'Use .NET 10 SDK' inputs: packageType: sdk version: '10.x' + # Publish the application payload - task: Bash@3 - displayName: 'Build payload' + displayName: 'Publish payload' inputs: targetType: filePath - filePath: './src/linux/Packaging.Linux/layout.sh' + filePath: './build/linux/publish.sh' arguments: | - --runtime="${{ dim.runtime }}" \ - --configuration="Release" \ - --output="$(Build.ArtifactStagingDirectory)/payload" \ - --symbol-output="$(Build.ArtifactStagingDirectory)/symbols_raw" + --configuration release \ + --runtime ${{ dim.runtime }} + # Build the package (.deb) from the payload - task: Bash@3 - displayName: 'Build packages' + displayName: 'Build package' inputs: targetType: filePath - filePath: './src/linux/Packaging.Linux/pack.sh' + filePath: './build/linux/pack.sh' arguments: | - --version="$(version)" \ - --runtime="${{ dim.runtime }}" \ - --payload="$(Build.ArtifactStagingDirectory)/payload" \ - --symbols="$(Build.ArtifactStagingDirectory)/symbols_raw" \ - --output="$(Build.ArtifactStagingDirectory)/pkg" - - task: Bash@3 - displayName: 'Move packages' - inputs: - targetType: inline - script: | - # Move symbols - mkdir -p $(Build.ArtifactStagingDirectory)/symbols - mv $(Build.ArtifactStagingDirectory)/pkg/tar/gcm-*-symbols.tar.gz $(Build.ArtifactStagingDirectory)/symbols - - # Move binary packages - mkdir -p $(Build.ArtifactStagingDirectory)/installers - mv $(Build.ArtifactStagingDirectory)/pkg/tar/*.tar.gz $(Build.ArtifactStagingDirectory)/installers - mv $(Build.ArtifactStagingDirectory)/pkg/deb/*.deb $(Build.ArtifactStagingDirectory)/installers + --configuration release \ + --runtime ${{ dim.runtime }} + # ESRP-sign the Debian package - ${{ if eq(parameters.esrp, true) }}: - - task: EsrpCodeSigning@5 - displayName: 'Sign Debian package' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/installers' - pattern: | - **/*.deb - useMinimatch: true - signConfigType: inlineSignParams + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Sign Debian package' + folderPath: '$(Build.SourcesDirectory)/out/package/release' + pattern: '**/*.deb' inlineOperation: | [ { @@ -646,75 +489,70 @@ extends: "Parameters": {} } ] + # Create the binaries and symbols tarballs + - task: Bash@3 + displayName: 'Archive payload' + inputs: + targetType: filePath + filePath: './build/linux/archive.sh' + arguments: | + --configuration release \ + --runtime ${{ dim.runtime }} - task: Bash@3 displayName: 'Collect artifacts for publishing' inputs: targetType: inline script: | mkdir -p $(Build.ArtifactStagingDirectory)/_final - cp $(Build.ArtifactStagingDirectory)/installers/*.deb $(Build.ArtifactStagingDirectory)/_final - cp $(Build.ArtifactStagingDirectory)/installers/*.tar.gz $(Build.ArtifactStagingDirectory)/_final - cp $(Build.ArtifactStagingDirectory)/symbols/*.tar.gz $(Build.ArtifactStagingDirectory)/_final - cp -r $(Build.ArtifactStagingDirectory)/payload $(Build.ArtifactStagingDirectory)/_final + cp $(Build.SourcesDirectory)/out/package/release/*.deb $(Build.ArtifactStagingDirectory)/_final + cp $(Build.SourcesDirectory)/out/package/release/*.tar.gz $(Build.ArtifactStagingDirectory)/_final # - # .NET Tool build job + # .NET tool build job # - job: dotnet_tool displayName: '.NET Tool NuGet Package' + # The tool is portable IL and doesn't require platform-specific + # builds. Linux is the fastest hosted agent to provision so use it! pool: name: GitClientPME-1ESHostedPool-intel-pc - image: win-x86_64-ado1es - os: windows + image: ubuntu-x86_64-ado1es + os: linux + hostArchitecture: amd64 templateContext: outputs: - output: pipelineArtifact - targetPath: '$(Build.ArtifactStagingDirectory)/packages' + targetPath: '$(Build.ArtifactStagingDirectory)/_final' artifactName: 'dotnet-tool' steps: - checkout: self - - task: PowerShell@2 - displayName: 'Read version file' + - task: CopyFiles@2 + displayName: 'Use Central Feed Services (CFS)' inputs: - targetType: inline - script: | - $version = (Get-Content .\VERSION) -replace '\.\d+$', '' - Write-Host "##vso[task.setvariable variable=version;isReadOnly=true]$version" + SourceFolder: '$(Build.SourcesDirectory)/.azure-pipelines' + Contents: 'nuget.config' + TargetFolder: '$(Build.SourcesDirectory)' + Overwrite: true + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to NuGet feeds' - task: UseDotNet@2 displayName: 'Use .NET 10 SDK' inputs: packageType: sdk version: '10.x' - - task: NuGetToolInstaller@1 - displayName: 'Install NuGet CLI' - inputs: - versionSpec: '>= 6.0' - - task: PowerShell@2 - displayName: 'Build payload' + - task: Bash@3 + displayName: 'Publish payload' inputs: targetType: filePath - filePath: './src/shared/DotnetTool/layout.ps1' - arguments: | - -Configuration Release ` - -Output "$(Build.ArtifactStagingDirectory)/nupkg" + filePath: './build/dntool/publish.sh' + arguments: --configuration release + # ESRP-sign (Authenticode) the managed assemblies before packing - ${{ if eq(parameters.esrp, true) }}: - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign payload' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/nupkg' - pattern: | - **/*.exe - **/*.dll - useMinimatch: true - signConfigType: inlineSignParams + - template: .azure-pipelines/esrp/sign.yml@self + parameters: + displayName: 'Sign payload' + folderPath: '$(Build.SourcesDirectory)/out/publish/dntool/release' + pattern: '**/*.dll' inlineOperation: | [ { @@ -738,44 +576,20 @@ extends: "Parameters": {} } ] - - task: PowerShell@2 + # Pack the signed payload into the .nupkg + - task: Bash@3 displayName: 'Create NuGet packages' inputs: targetType: filePath - filePath: './src/shared/DotnetTool/pack.ps1' - arguments: | - -Configuration Release ` - -Version "$(version)" ` - -PackageRoot "$(Build.ArtifactStagingDirectory)/nupkg" ` - -Output "$(Build.ArtifactStagingDirectory)/packages" - - ${{ if eq(parameters.esrp, true) }}: - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign NuGet packages' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/packages' - pattern: | - **/*.nupkg - **/*.snupkg - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + filePath: './build/dntool/pack.sh' + arguments: --configuration release + - task: Bash@3 + displayName: 'Collect artifacts for publishing' + inputs: + targetType: inline + script: | + mkdir -p $(Build.ArtifactStagingDirectory)/_final + cp $(Build.SourcesDirectory)/out/package/release/*.nupkg $(Build.ArtifactStagingDirectory)/_final - stage: release displayName: 'Release' @@ -865,7 +679,6 @@ extends: $(Pipeline.Workspace)/assets/linux-arm64/*.deb $(Pipeline.Workspace)/assets/linux-arm64/*.tar.gz $(Pipeline.Workspace)/assets/dotnet-tool/*.nupkg - $(Pipeline.Workspace)/assets/dotnet-tool/*.snupkg - job: nuget displayName: 'Publish NuGet package' @@ -890,7 +703,7 @@ extends: - output: nuget condition: and(succeeded(), eq('${{ parameters.nuget }}', true)) displayName: 'Publish .NET Tool NuGet package' - packagesToPush: '$(Pipeline.Workspace)/assets/dotnet-tool/*.nupkg;$(Pipeline.Workspace)/assets/dotnet-tool/*.snupkg' + packagesToPush: '$(Pipeline.Workspace)/assets/dotnet-tool/*.nupkg' packageParentPath: $(Pipeline.Workspace)/assets/dotnet-tool nuGetFeedType: external publishPackageMetadata: true diff --git a/.azure-pipelines/scripts/macos/import-developer-certificate.sh b/.azure-pipelines/scripts/macos/import-developer-certificate.sh new file mode 100755 index 0000000000..64d8bcb0f4 --- /dev/null +++ b/.azure-pipelines/scripts/macos/import-developer-certificate.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# +# Downloads the macOS developer signing certificate (and its signing identity) +# from Azure Key Vault and imports the certificate into a keychain so that +# codesign can use it. +# +# This uses the Azure CLI ('az') to read the secrets, so it must run with an +# authenticated CLI - e.g. inside an Azure Pipelines 'AzureCLI@2' task, which +# provides a CLI authenticated against the task's service connection. +# +# The resolved developer signing identity is written to stdout; all progress +# output goes to stderr, so a caller can capture just the identity with: +# +# identity="$(import-developer-certificate.sh --vault myvault)" +# +set -euo pipefail + +die () { + echo "fatal: $*" >&2 + exit 1 +} + +make_absolute () { + case "$1" in + /*) echo "$1" ;; + *) echo "$PWD/$1" ;; + esac +} + +print_usage () { + cat < [options] + +Download the macOS developer signing certificate from Azure Key Vault and import +it into a keychain for codesign. Requires an authenticated Azure CLI ('az'). The +developer signing identity is written to stdout. + +Options: + --vault Azure Key Vault to read the secrets from. (required) + --certificate-secret Key Vault secret holding the base64-encoded .p12 + certificate. (default: mac-developer-certificate) + --password-secret Key Vault secret holding the .p12 password. + (default: mac-developer-certificate-password) + --identity-secret Key Vault secret holding the signing identity. + (default: mac-developer-certificate-identity) + --keychain Keychain to create and import into. + (default: \$TMPDIR/gcm-build.keychain) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") --vault my-key-vault +EOF +} + +# Defaults +VAULT="" +CERT_SECRET="mac-developer-certificate" +PASSWORD_SECRET="mac-developer-certificate-password" +IDENTITY_SECRET="mac-developer-certificate-identity" +KEYCHAIN="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) print_usage; exit 0 ;; + --vault) VAULT="${2:?--vault requires a value}"; shift 2 ;; + --certificate-secret) CERT_SECRET="${2:?--certificate-secret requires a value}"; shift 2 ;; + --password-secret) PASSWORD_SECRET="${2:?--password-secret requires a value}"; shift 2 ;; + --identity-secret) IDENTITY_SECRET="${2:?--identity-secret requires a value}"; shift 2 ;; + --keychain) KEYCHAIN="${2:?--keychain requires a value}"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +[ "$#" -eq 0 ] || die "unexpected argument '$1' (try '$(basename "$0") --help')" +[ -n "$VAULT" ] || die "--vault was not specified" + +# Resolve the keychain path (default: an ephemeral keychain under the temp dir). +if [ -n "$KEYCHAIN" ]; then + KEYCHAIN="$(make_absolute "$KEYCHAIN")" +else + KEYCHAIN="${TMPDIR:-/tmp}/gcm-build.keychain" +fi + +command -v az >/dev/null 2>&1 || die "the Azure CLI ('az') is required but was not found on PATH" +command -v security >/dev/null 2>&1 || die "'security' is required (this script must run on macOS)" + +# Send everything except the final identity to stderr, so stdout carries only +# the signing identity for the caller to capture. +exec 3>&1 1>&2 + +echo "Importing developer certificate from Key Vault '$VAULT'..." +echo "certificate secret: $CERT_SECRET" +echo "password secret: $PASSWORD_SECRET" +echo "identity secret: $IDENTITY_SECRET" +echo "keychain: $KEYCHAIN" + +# Read the certificate password and signing identity (plain string secrets). +CERT_PASSWORD="$(az keyvault secret show --vault-name "$VAULT" --name "$PASSWORD_SECRET" --query value -o tsv)" \ + || die "failed to read secret '$PASSWORD_SECRET' from Key Vault '$VAULT'" +IDENTITY="$(az keyvault secret show --vault-name "$VAULT" --name "$IDENTITY_SECRET" --query value -o tsv)" \ + || die "failed to read secret '$IDENTITY_SECRET' from Key Vault '$VAULT'" + +# Download and base64-decode the .p12 certificate to a temporary file, removed +# again as soon as it has been imported (or if the script exits early). mktemp +# creates the file, so the download needs --overwrite to write into it (newer +# az CLI refuses to overwrite an existing --file otherwise). +CERT_FILE="$(mktemp)" +trap 'rm -f "$CERT_FILE"' EXIT +az keyvault secret download --vault-name "$VAULT" --name "$CERT_SECRET" \ + --encoding base64 --file "$CERT_FILE" --overwrite \ + || die "failed to download secret '$CERT_SECRET' from Key Vault '$VAULT'" + +# Use a random, ephemeral password for the throwaway build keychain; it is only +# ever used here (the keychain stays unlocked for codesign in later steps). +KEYCHAIN_PASSWORD="$(uuidgen)" + +# Create, unlock and default the keychain so codesign can find the identity. +echo "Creating keychain '$KEYCHAIN'..." +rm -f "$KEYCHAIN" +security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" +security default-keychain -s "$KEYCHAIN" +security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" + +# A new keychain keeps a default auto-lock (300s timeout and lock-on-sleep) that +# 'unlock-keychain' does not clear, so it re-locks part-way through a long +# signing run and makes codesign block on a GUI unlock prompt (which hangs +# headless CI). Disable auto-lock so it stays unlocked for the whole build. +security set-keychain-settings "$KEYCHAIN" + +# Import the certificate, authorising codesign to use the private key. The +# format is stated explicitly because 'security import' otherwise infers it +# from the file extension, and the mktemp file deliberately has none (without +# this it fails with "SecKeychainItemImport: Unknown format in import"). +echo "Importing certificate..." +security import "$CERT_FILE" -f pkcs12 -k "$KEYCHAIN" -P "$CERT_PASSWORD" -T /usr/bin/codesign + +# Allow codesign to use the private key without an interactive prompt. +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" + +echo "Developer certificate imported." +echo "signing identity: $IDENTITY" + +# Emit the signing identity on the real stdout for the caller. +printf '%s\n' "$IDENTITY" >&3 diff --git a/.azure-pipelines/scripts/windows/download-innosetup.ps1 b/.azure-pipelines/scripts/windows/download-innosetup.ps1 new file mode 100644 index 0000000000..430cb15b45 --- /dev/null +++ b/.azure-pipelines/scripts/windows/download-innosetup.ps1 @@ -0,0 +1,101 @@ +<# +.SYNOPSIS + Downloads the Inno Setup compiler (ISCC.exe) used to build the Windows + installers, without requiring a project file. + +.DESCRIPTION + Uses 'dotnet package download' (.NET SDK 10+) to fetch the Tools.InnoSetup + NuGet package into a local folder, then returns an object describing the + download. By default the version pinned for the build in + Directory.Packages.props is used, so the installers are compiled with the + same Inno Setup as a regular 'dotnet build'. + + The returned object lets the caller either pass the compiler path to + pack.ps1 (-InnoSetup) or add the tools directory to PATH (e.g. via an Azure + Pipelines '##vso[task.prependpath]' command); this script itself stays + agnostic of the build/CI system. + +.PARAMETER Version + The Tools.InnoSetup package version to download. Defaults to the version + pinned in Directory.Packages.props. + +.PARAMETER OutputPath + Directory to download the package into. Defaults to out/tools/innosetup under + the repository root. + +.OUTPUTS + A [pscustomobject] with these properties: + Version - the resolved Tools.InnoSetup package version. + ToolsDir - the directory containing the Inno Setup binaries (ISCC.exe etc.). + Path - the full path to ISCC.exe. +#> +[CmdletBinding()] +param ( + [string] $Version, + [string] $OutputPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$InformationPreference = 'Continue' + +# Repository root, three levels up from this script +# (.azure-pipelines/scripts/windows). +function Get-RepoRoot { + return (Get-Item $PSScriptRoot).Parent.Parent.Parent.FullName +} + +function Get-AbsolutePath { + param([Parameter(Mandatory)] [string] $Path) + if ([System.IO.Path]::IsPathRooted($Path)) { + return $Path + } + return (Join-Path (Get-Location).Path $Path) +} + +# Resolve the package version (default: the version pinned for the build in +# Directory.Packages.props, so installers use the same Inno Setup as the build). +if (-not $Version) { + $propsPath = Join-Path (Get-RepoRoot) 'Directory.Packages.props' + if (-not (Test-Path -LiteralPath $propsPath)) { + Write-Error "Central package versions file '$propsPath' not found" + } + [xml]$props = Get-Content -LiteralPath $propsPath + $Version = ($props.Project.ItemGroup.PackageVersion | + Where-Object { $_.Include -eq 'Tools.InnoSetup' }).Version + if (-not $Version) { + Write-Error "Tools.InnoSetup version not found in '$propsPath'" + } +} + +# Resolve the download directory (default: out/tools/innosetup). +if ($OutputPath) { + $OutputPath = Get-AbsolutePath $OutputPath +} else { + $OutputPath = Join-Path (Get-RepoRoot) 'out' 'tools' 'innosetup' +} + +Write-Information "Downloading Inno Setup $Version to '$OutputPath'..." + +# Display the download output on the host without letting it leak into this +# script's output stream (so the returned object is the only success output). +dotnet package download "Tools.InnoSetup@$Version" --output $OutputPath | Out-Host +if ($LASTEXITCODE -ne 0) { + Write-Error "dotnet package download failed (exit $LASTEXITCODE)" +} + +# 'dotnet package download' extracts each package to +# ///; the Inno Setup binaries live under tools/. +$toolsDir = Join-Path $OutputPath 'tools.innosetup' $Version 'tools' +$iscc = Join-Path $toolsDir 'ISCC.exe' +if (-not (Test-Path -LiteralPath $iscc)) { + Write-Error "ISCC.exe not found at '$iscc'" +} + +Write-Information "Inno Setup compiler: $iscc" + +return [pscustomobject]@{ + Version = $Version + ToolsDir = $toolsDir + Path = $iscc +} diff --git a/.azure-pipelines/scripts/windows/setup-aot-build-tools.ps1 b/.azure-pipelines/scripts/windows/setup-aot-build-tools.ps1 new file mode 100644 index 0000000000..20936e16d7 --- /dev/null +++ b/.azure-pipelines/scripts/windows/setup-aot-build-tools.ps1 @@ -0,0 +1,89 @@ +# Install the MSVC C++ build tools and Windows SDK that Native AOT needs to +# link the git-credential-manager binary on Windows agents. +# +# Publishing with PublishAot=true links the binary with the MSVC linker +# (link.exe) against the Windows SDK user-mode import libraries. +# https://aka.ms/nativeaot-prerequisites +# +# Install VS 2022 Build Tools with the VC.Tools component for the agent's +# architecture (x86.x64 on Intel, ARM64 on ARM) plus a Windows 11 SDK, +# which carries the import libraries for every target architecture. + +param( + [Parameter(Mandatory = $true)] + [ValidateSet('amd64', 'arm64')] + [string]$Architecture +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +# Always install the x86/x64 VC tools. The x64 tool set includes the VsDevCmd +# VC integration script (Common7\Tools\vsdevcmd\ext\vcvars.bat) that sets +# VCToolsInstallDir, the compiler/linker PATH, and LIB. +# Sadly the ARM64 component does NOT ship that script, so an arm64-only install +# leaves 'vcvarsall' unable to initialise the VC environment correctly. +# Native AOT fails with "Platform linker not found" even though the arm64 linker +# is present on disk. +$components = @('Microsoft.VisualStudio.Component.VC.Tools.x86.x64') + +# When targeting arm64, also install the ARM64 component for the arm64 target +# linker and import libraries. +if ($Architecture -eq 'arm64') { + $components += 'Microsoft.VisualStudio.Component.VC.Tools.ARM64' +} + +# The Windows SDK provides the user-mode import libraries (advapi32.lib, etc). +$components += 'Microsoft.VisualStudio.Component.Windows11SDK.22621' + +$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') +foreach ($c in $components) { $vsArgs += @('--add', $c) } +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)" +} + +# 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 "===== Confirm components via vswhere =====" +$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +if (Test-Path $vswhere) { + $installPath = @(& $vswhere -latest -products * -requires $components ` + -property installationPath)[0] + if ($installPath) { + Write-Host "Build tools and Windows SDK installed at $installPath" + } else { + throw "Required components ($($components -join ', ')) not found after install (see logs above)" + } +} else { + Write-Host "vswhere not found at $vswhere" +} + +# Native commands above can leave a stray non-zero $LASTEXITCODE behind +# (Windows PowerShell 5.1 tears down piped native commands early); every +# real failure throws, so reaching here means success. +exit 0 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 166a0b6b86..8e0b426431 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -13,16 +13,11 @@ jobs: # ================================ windows: name: Windows - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.runtime == 'win-arm64' && 'windows-11-arm' || 'windows-2025' }} strategy: + fail-fast: false matrix: - include: - - runtime: win-x86 - os: windows-latest - - runtime: win-x64 - os: windows-latest - - runtime: win-arm64 - os: windows-11-arm + runtime: [ win-x86, win-x64, win-arm64 ] steps: - uses: actions/checkout@v6 @@ -46,38 +41,31 @@ jobs: - name: Build run: | - dotnet build src/windows/Installer.Windows/Installer.Windows.csproj ` - --configuration=Release ` - --runtime=${{ matrix.runtime }} + dotnet build build/windows ` + --verbosity normal ` + --configuration=release ` + --runtime=${{ matrix.runtime }} - name: Test run: | - dotnet test --verbosity normal ` - --configuration=WindowsRelease ` - --runtime=${{ matrix.runtime }} - - - name: Prepare artifacts - shell: bash - run: | - mkdir -p artifacts/bin - mv out/windows/Installer.Windows/bin/Release/net472/gcm*.exe artifacts/ - mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }} artifacts/bin/ - cp out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}.sym/* artifacts/bin/${{ matrix.runtime }}/ + dotnet test --configuration=release - name: Upload artifacts uses: actions/upload-artifact@v7 with: name: ${{ matrix.runtime }} path: | - artifacts + out/package/release/*.exe + out/package/release/*.zip # ================================ # Linux # ================================ linux: name: Linux - runs-on: ubuntu-latest + runs-on: ${{ matrix.runtime == 'linux-x64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }} strategy: + fail-fast: false matrix: runtime: [ linux-x64, linux-arm64, linux-arm ] @@ -89,39 +77,43 @@ jobs: with: dotnet-version: 10.0.x + - name: Install cross-compilation toolchain (arm) + if: matrix.runtime == 'linux-arm' + run: | + sudo apt-get update + # Brings in cross-compilation toolchain for armhf including binutils + sudo apt-get install -y gcc-arm-linux-gnueabihf + - name: Install dependencies run: dotnet restore - name: Build run: | - dotnet build src/linux/Packaging.Linux/*.csproj \ - --configuration=Release --no-self-contained \ - --runtime=${{ matrix.runtime }} + dotnet build build/linux \ + --verbosity normal \ + --configuration=release \ + --runtime=${{ matrix.runtime }} - name: Test run: | - dotnet test --verbosity normal --configuration=LinuxRelease - - - name: Prepare artifacts - run: | - mkdir -p artifacts - mv out/linux/Packaging.Linux/Release/deb/*.deb artifacts/ - mv out/linux/Packaging.Linux/Release/tar/*.tar.gz artifacts/ + dotnet test --configuration=release - name: Upload artifacts uses: actions/upload-artifact@v7 with: name: ${{ matrix.runtime }} path: | - artifacts + out/package/release/*.deb + out/package/release/*.tar.gz # ================================ # macOS # ================================ osx: name: macOS - runs-on: macos-latest + runs-on: macos-15 strategy: + fail-fast: false matrix: runtime: [ osx-x64, osx-arm64 ] @@ -138,24 +130,47 @@ jobs: - name: Build run: | - dotnet build src/osx/Installer.Mac/*.csproj \ - --configuration=Release --no-self-contained \ + dotnet build build/macos \ + --verbosity normal \ + --configuration=release \ --runtime=${{ matrix.runtime }} - name: Test run: | - dotnet test --verbosity normal --configuration=MacRelease - - - name: Prepare artifacts - run: | - mkdir -p artifacts/bin - mv out/osx/Installer.Mac/pkg/Release/payload "artifacts/bin/${{ matrix.runtime }}" - cp out/osx/Installer.Mac/pkg/Release/payload.sym/* "artifacts/bin/${{ matrix.runtime }}/" - mv out/osx/Installer.Mac/pkg/Release/gcm*.pkg artifacts/ + dotnet test --configuration=release - name: Upload artifacts uses: actions/upload-artifact@v7 with: name: ${{ matrix.runtime }} path: | - artifacts + out/package/release/*.pkg + out/package/release/*.tar.gz + +# ================================ +# .NET tool +# ================================ + dntool: + name: .NET Tool + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5.2.0 + with: + dotnet-version: 10.0.x + + - name: Install dependencies + run: dotnet restore + + - name: Build .NET tool package + run: dotnet build build/dntool --configuration=release + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: dotnet-tool + path: | + out/package/release/*.nupkg diff --git a/.github/workflows/validate-install-from-source.yml b/.github/workflows/validate-install-from-source.yml index 5bc610714f..3f7bf18aff 100644 --- a/.github/workflows/validate-install-from-source.yml +++ b/.github/workflows/validate-install-from-source.yml @@ -46,9 +46,17 @@ jobs: GNUPGHOME=/root/.gnupg tdnf update -y && GNUPGHOME=/root/.gnupg tdnf install tar -y # needed for `actions/checkout` fi - - uses: actions/checkout@v6 + - run: | + sh "${GITHUB_WORKSPACE}/build/install-from-source.sh" -y + git-credential-manager --help || exit 1 + macos: + name: macos-latest + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 - run: | - sh "${GITHUB_WORKSPACE}/src/linux/Packaging.Linux/install-from-source.sh" -y + sh "${GITHUB_WORKSPACE}/build/install-from-source.sh" -y + export PATH="/usr/local/bin:$PATH" git-credential-manager --help || exit 1 diff --git a/Directory.Build.props b/Directory.Build.props index 58e8f8770b..f4cf82d5c7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,6 +19,13 @@ $(RepoPath)out\ $(RepoPath)assets\ + + true + $(RepoOutPath) + + + net10.0 + <_IsExeProject Condition="'$(OutputType)' == 'Exe' OR '$(OutputType)' == 'WinExe'">true @@ -26,8 +33,4 @@ true - - - - diff --git a/Directory.Build.targets b/Directory.Build.targets index 7ec523390d..3165b88fd2 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -3,17 +3,25 @@ - + - + + + + $(NoWarn);CA1416 + + $(IntermediateOutputPath)app.manifest @@ -21,7 +29,7 @@ - - - - - - + + + @@ -25,8 +22,7 @@ - - + diff --git a/Git-Credential-Manager.sln b/Git-Credential-Manager.sln deleted file mode 100644 index a883e760ed..0000000000 --- a/Git-Credential-Manager.sln +++ /dev/null @@ -1,308 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29927.169 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A7FC1234-95E3-4496-B5F7-4306F41E6A0E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Git-Credential-Manager", "src\shared\Git-Credential-Manager\Git-Credential-Manager.csproj", "{28F06D44-AB25-4CF5-93F9-978C23FAA9D6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "src\shared\Core\Core.csproj", "{31BCFC70-B767-4274-873F-1A076D422FC3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.Tests", "src\shared\Core.Tests\Core.Tests.csproj", "{AD41FA1E-51F5-4E4F-B7DA-32F921491313}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AzureRepos", "src\shared\Microsoft.AzureRepos\Microsoft.AzureRepos.csproj", "{714AF9EB-44E6-4058-BD3E-9039F29F4D7A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AzureRepos.Tests", "src\shared\Microsoft.AzureRepos.Tests\Microsoft.AzureRepos.Tests.csproj", "{97DC6241-1240-4A85-8035-F8404A983A82}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "windows", "windows", "{66722747-1B61-40E4-A89B-1AC8E6D62EA9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestInfrastructure", "src\shared\TestInfrastructure\TestInfrastructure.csproj", "{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitHub", "src\shared\GitHub\GitHub.csproj", "{3C840B06-A595-4FD9-9A76-56CD45B14780}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{D5277A0E-997E-453A-8CB9-4EFCC8B16A29}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitHub.Tests", "src\shared\GitHub.Tests\GitHub.Tests.csproj", "{3E524EA8-D31A-4394-997C-14B522E3D6FD}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "osx", "osx", "{3D279E2D-E011-45CF-8EA8-3D71D1300443}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Installer.Mac", "src\osx\Installer.Mac\Installer.Mac.csproj", "{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Installer.Windows", "src\windows\Installer.Windows\Installer.Windows.csproj", "{85903170-9E52-4B53-A6E4-3F416F684FAE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket", "src\shared\Atlassian.Bitbucket\Atlassian.Bitbucket.csproj", "{B49881A6-E734-490E-8EA7-FB0D9E296CFB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.Tests", "src\shared\Atlassian.Bitbucket.Tests\Atlassian.Bitbucket.Tests.csproj", "{025E5329-A0B1-4BA9-9203-B70B44A5F9E0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Packaging.Linux", "src\linux\Packaging.Linux\Packaging.Linux.csproj", "{AD2A935F-3720-4802-8119-6A9B35B254DF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "linux", "linux", "{8F9D7E67-7DD7-4E32-9134-423281AF00E9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab", "src\shared\GitLab\GitLab.csproj", "{570897DC-A85C-4598-B793-9A00CF710119}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.Tests", "src\shared\GitLab.Tests\GitLab.Tests.csproj", "{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - MacDebug|Any CPU = MacDebug|Any CPU - MacRelease|Any CPU = MacRelease|Any CPU - Release|Any CPU = Release|Any CPU - WindowsDebug|Any CPU = WindowsDebug|Any CPU - WindowsRelease|Any CPU = WindowsRelease|Any CPU - LinuxDebug|Any CPU = LinuxDebug|Any CPU - LinuxRelease|Any CPU = LinuxRelease|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.Release|Any CPU.Build.0 = Release|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.Release|Any CPU.Build.0 = Release|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {31BCFC70-B767-4274-873F-1A076D422FC3}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.Release|Any CPU.Build.0 = Release|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {AD41FA1E-51F5-4E4F-B7DA-32F921491313}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.Release|Any CPU.Build.0 = Release|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.Release|Any CPU.Build.0 = Release|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {97DC6241-1240-4A85-8035-F8404A983A82}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.Release|Any CPU.Build.0 = Release|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.Release|Any CPU.Build.0 = Release|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {3C840B06-A595-4FD9-9A76-56CD45B14780}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.Release|Any CPU.Build.0 = Release|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {3E524EA8-D31A-4394-997C-14B522E3D6FD}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.Release|Any CPU.ActiveCfg = Release|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {85903170-9E52-4B53-A6E4-3F416F684FAE}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.Release|Any CPU.Build.0 = Release|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {B49881A6-E734-490E-8EA7-FB0D9E296CFB}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.Release|Any CPU.Build.0 = Release|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.Debug|Any CPU.Build.0 = Debug|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.Release|Any CPU.ActiveCfg = Release|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.Release|Any CPU.Build.0 = Release|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {570897DC-A85C-4598-B793-9A00CF710119}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacDebug|Any CPU.Build.0 = Debug|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.Release|Any CPU.Build.0 = Release|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacRelease|Any CPU.Build.0 = Release|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {28F06D44-AB25-4CF5-93F9-978C23FAA9D6} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {31BCFC70-B767-4274-873F-1A076D422FC3} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {AD41FA1E-51F5-4E4F-B7DA-32F921491313} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {714AF9EB-44E6-4058-BD3E-9039F29F4D7A} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {97DC6241-1240-4A85-8035-F8404A983A82} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {66722747-1B61-40E4-A89B-1AC8E6D62EA9} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E} - {5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {3C840B06-A595-4FD9-9A76-56CD45B14780} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E} - {3E524EA8-D31A-4394-997C-14B522E3D6FD} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {3D279E2D-E011-45CF-8EA8-3D71D1300443} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E} - {74FA0AA4-B5C1-4F3B-B182-277FC2D50715} = {3D279E2D-E011-45CF-8EA8-3D71D1300443} - {85903170-9E52-4B53-A6E4-3F416F684FAE} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9} - {B49881A6-E734-490E-8EA7-FB0D9E296CFB} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {025E5329-A0B1-4BA9-9203-B70B44A5F9E0} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {8F9D7E67-7DD7-4E32-9134-423281AF00E9} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E} - {AD2A935F-3720-4802-8119-6A9B35B254DF} = {8F9D7E67-7DD7-4E32-9134-423281AF00E9} - {570897DC-A85C-4598-B793-9A00CF710119} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - {1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {0EF9FC65-E6BA-45D4-A455-262A9EA4366B} - EndGlobalSection -EndGlobal diff --git a/VERSION b/VERSION index fff5589e14..4a36342fca 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.0.0 +3.0.0 diff --git a/build/dntool/Dntool.Distribution.csproj b/build/dntool/Dntool.Distribution.csproj new file mode 100644 index 0000000000..de44797de5 --- /dev/null +++ b/build/dntool/Dntool.Distribution.csproj @@ -0,0 +1,69 @@ + + + + + + net10.0 + + + git-credential-manager + git-credential-manager + Secure, cross-platform Git credential storage with authentication to Azure Repos, GitHub, and other popular Git hosting services. + https://github.com/git-ecosystem/git-credential-manager + git + https://github.com/git-ecosystem/git-credential-manager + MIT + images/icon.png + git credential manager authentication azure-repos github gitlab bitbucket + DotnetTool + + + true + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/DotnetTool/DotnetToolSettings.xml b/build/dntool/DotnetToolSettings.xml similarity index 59% rename from src/shared/DotnetTool/DotnetToolSettings.xml rename to build/dntool/DotnetToolSettings.xml index 01233bfdde..ab5c8b170f 100644 --- a/src/shared/DotnetTool/DotnetToolSettings.xml +++ b/build/dntool/DotnetToolSettings.xml @@ -1,6 +1,6 @@ - + - + diff --git a/build/dntool/build.sh b/build/dntool/build.sh new file mode 100755 index 0000000000..902dea8780 --- /dev/null +++ b/build/dntool/build.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# +# Builds the Git Credential Manager .NET tool package (.nupkg + .snupkg) by +# publishing the framework-dependent IL and then packing it. +# +# In the release pipeline the published assemblies are code-signed between the +# publish and pack steps; for a local (unsigned) build this script runs both in +# sequence. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration to publish: Debug or Release. + (default: Release) + --version Version to stamp into the binaries and package. + (default: the repository VERSION file) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") --version 2.6.1 + $(basename "$0") --configuration Release --version 2.6.1 +EOF +} + +# Defaults +CONFIGURATION="" +VERSION="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) print_usage; exit 0 ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalize arguments / apply defaults. +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +info "Building .NET tool package..." +verbose "configuration: $CONFIGURATION" +verbose "version: $VERSION" + +# Publish the application as framework-dependent IL. +"$THISDIR/publish.sh" --configuration "$CONFIGURATION" --version "$VERSION" + +# Pack the published layout into the .nupkg/.snupkg. +"$THISDIR/pack.sh" --configuration "$CONFIGURATION" --version "$VERSION" + +info ".NET tool package build complete." diff --git a/src/shared/DotnetTool/icon.png b/build/dntool/icon.png similarity index 100% rename from src/shared/DotnetTool/icon.png rename to build/dntool/icon.png diff --git a/build/dntool/pack.sh b/build/dntool/pack.sh new file mode 100755 index 0000000000..19620abaed --- /dev/null +++ b/build/dntool/pack.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# +# Packs the published Git Credential Manager layout into the .NET tool NuGet +# package (.nupkg) via Dntool.Distribution.csproj. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +PACKAGE_PROJECT="$THISDIR/Dntool.Distribution.csproj" + +print_usage () { + cat </any +alongside DotnetToolSettings.xml. + +Options: + -c, --configuration Build configuration: Debug or Release. + (default: Release) + --version Version to stamp into the package. + (default: the repository VERSION file) + --bindir Directory of published binaries to pack. + (default: the tool's default publish dir) + --output Directory to write the package to. + (default: out/package/) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") + $(basename "$0") --configuration Release --version 2.6.1 +EOF +} + +# Defaults +CONFIGURATION="" +VERSION="" +BINDIR="" +OUTPUT="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) print_usage; exit 0 ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + --bindir) require_value "$@"; BINDIR="$2"; shift 2 ;; + --output) require_value "$@"; OUTPUT="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalise arguments / apply defaults. +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +# Source of the published binaries to pack (override with --bindir; defaults to +# the tool's default artifacts publish directory, from publish.sh). +if [ -n "$BINDIR" ]; then + BINDIR="$(make_absolute "$BINDIR")" +else + BINDIR="$(publish_dir dntool "$CONFIGURATION")" || exit 1 +fi + +# Destination directory for the package (override with --output; defaults to the +# top-level artifacts package directory). +if [ -n "$OUTPUT" ]; then + OUTDIR="$(make_absolute "$OUTPUT")" +else + OUTDIR="$(package_dir "$CONFIGURATION")" || exit 1 +fi + +# Pre-execution checks +[ -d "$BINDIR" ] || die "Publish directory '$BINDIR' not found. Did you publish first?" + +verbose "configuration: $CONFIGURATION" +verbose "version: $VERSION" +verbose "bin dir: $BINDIR" +verbose "output dir: $OUTDIR" + +# Pack the pre-published (and, in the release pipeline, code-signed) layout into +# the .NET tool package via Dntool.Distribution.csproj. The project compiles +# nothing, so pack simply zips the published files: it does not rebuild or +# re-publish, leaving the signed assemblies untouched. --no-build is essential - +# it skips the Build target (and thus the project's build.sh hook), so packing +# does not recurse. The version and the absolute publish path are passed as +# MSBuild properties; the _CustomPack target lays it out under tools/. +info "Packing .NET tool package..." +mkdir -p "$OUTDIR" + +# 'dotnet pack --no-build' below implies --no-restore, so restore the package +# project explicitly first. Running this script via the build.sh script and +# the Dntool project would have already restored it; this is only required when +# running publish.sh and pack.sh as separate steps directly. +# Without this the `dotnet pack` command fails with NETSDK1004 (missing +# project.assets.json). Restore does not run the project's build.sh hook (that +# fires AfterTargets=Build), so packing still does not recurse. +dotnet restore "$PACKAGE_PROJECT" || die "Failed to restore .NET tool project" + +dotnet pack "$PACKAGE_PROJECT" \ + --configuration "$CONFIGURATION" \ + --no-build \ + --output "$OUTDIR" \ + -p:PackageVersion="$VERSION" \ + -p:GcmPublishDir="$BINDIR" || die "Failed to pack .NET tool" + +info "Packaging complete: $OUTDIR" diff --git a/build/dntool/publish.sh b/build/dntool/publish.sh new file mode 100755 index 0000000000..ddc86baf1b --- /dev/null +++ b/build/dntool/publish.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# +# Publishes the Git Credential Manager application as framework-dependent IL for +# the .NET tool package. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration to publish: Debug or Release. + (default: Release) + -o, --output Directory to publish the application to. + (default: the tool's default artifacts dir) + --version Version to stamp into the published binaries. + (default: the repository VERSION file) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") + $(basename "$0") --configuration Release --version 2.6.1 +EOF +} + +# Defaults +CONFIGURATION="" +OUTPUT="" +VERSION="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) print_usage; exit 0 ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + -o|--output) require_value "$@"; OUTPUT="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalize arguments / apply defaults. +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +# Directories +GCM_SRC="$(repo_root)/src/git-credential-manager" + +# Resolve the publish output directory (default: the tool's runtime-agnostic +# artifacts publish directory). +if [ -n "$OUTPUT" ]; then + OUTDIR="$(make_absolute "$OUTPUT")" +else + OUTDIR="$(publish_dir dntool "$CONFIGURATION")" || exit 1 +fi + +verbose "configuration: $CONFIGURATION" +verbose "version: $VERSION" +verbose "output dir: $OUTDIR" + +# Publish the application as framework-dependent IL: +# --self-contained false run on the user's installed .NET runtime +# -p:PublishAot=false override the product default (a tool cannot be AOT) +# -p:PublishTrimmed=false override the non-AOT default (cannot trim a +# framework-dependent app) +# -p:UseAppHost=false no native launcher; the tool is invoked via 'dotnet' +# No runtime identifier is specified, so the output is portable (tools//any). +info "Publishing .NET tool application..." +dotnet publish "$GCM_SRC" \ + -v:normal \ + --configuration="$CONFIGURATION" \ + --framework net10.0 \ + --output "$OUTDIR" \ + --self-contained false \ + -p:PublishAot=false \ + -p:PublishTrimmed=false \ + -p:UseAppHost=false \ + -p:VersionOverride="$VERSION" || die "Failed to publish application" + +# Drop the native debug symbols (hundreds of MB of SkiaSharp/HarfBuzzSharp +# Windows .pdb files under runtimes/) that a portable publish drags in for every +# RID. They are useless in a .NET tool package and dwarf the actual payload; the +# managed .pdb files at the root are kept so installed-tool stack traces remain +# symbolicated. pack.sh does not re-publish, so this removal sticks. +info "Removing native debug symbols..." +find "$OUTDIR/runtimes" -name '*.pdb' -type f -delete 2>/dev/null || true + +info "Publish complete." diff --git a/build/install-from-source.sh b/build/install-from-source.sh new file mode 100755 index 0000000000..167e3b37ed --- /dev/null +++ b/build/install-from-source.sh @@ -0,0 +1,378 @@ +#!/bin/sh + +# Halt execution immediately on failure. +# Note there are some scenarios in which this will not exit; see +# https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html +# for additional details. +set -e + +is_ci= +aot= +for i in "$@"; do + case "$i" in + -y) + is_ci=true + shift # Past argument=value + ;; + --install-prefix=*) + installPrefix="${i#*=}" + shift # past argument=value + ;; + --aot) + # Build a native ahead-of-time (AOT) compiled binary, identical to the + # shipped package. Needs a C toolchain (clang + zlib headers) to link. + aot=true + shift + ;; + --no-aot) + # Build a trimmed, self-contained (non-AOT) binary. This is the default + # and needs only the .NET SDK. Listed for symmetry with --aot. + aot= + shift + ;; + esac +done + +# If install-prefix is not passed, use default value +if [ -z "$installPrefix" ]; then + installPrefix=/usr/local +fi + +# Ensure install directory exists +if [ ! -d "$installPrefix" ]; then + echo "The folder $installPrefix does not exist" + exit +fi + +# In non-ci scenarios, advertise what we will be doing and +# give user the option to exit. +if [ -z $is_ci ]; then + echo "This script will download, compile, and install Git Credential Manager to: + + $installPrefix/bin + +Git Credential Manager is licensed under the MIT License: https://aka.ms/gcm/license" + + while true; do + # Display prompt once before reading input + printf "Do you want to continue? [Y/n] " + + # Prefer reading from the controlling terminal (TTY) when available, + # so that input works even if the script is piped (e.g. curl URL | sh) + if [ -r /dev/tty ]; then + read yn < /dev/tty + # If no TTY is available, attempt to read from standard input (stdin) + elif ! read yn; then + # If input is not possible via TTY or stdin, assume a non-interactive environment + # and abort with guidance for automated usage + echo "Interactive prompt unavailable in this environment. Use 'sh -s -- -y' for automated install." + exit 1 + fi + + case "$yn" in + [Yy]*|"") break ;; + [Nn]*) exit ;; + *) echo "Please answer yes or no." ;; + esac + done +fi + +install_packages() { + pkg_manager=$1 + install_verb=$2 + packages=$3 + + for package in $packages; do + # Ensure we don't stomp on existing installations. + if type $package >/dev/null 2>&1; then + continue + fi + + if [ $pkg_manager = apk ]; then + $sudo_cmd $pkg_manager $install_verb $package + elif [ $pkg_manager = zypper ]; then + $sudo_cmd $pkg_manager -n $install_verb $package + elif [ $pkg_manager = pacman ]; then + $sudo_cmd $pkg_manager --noconfirm $install_verb $package + else + $sudo_cmd $pkg_manager $install_verb $package -y + fi + done +} + +ensure_dotnet_installed() { + if [ -z "$(verify_existing_dotnet_installation)" ]; then + curl -LO https://dot.net/v1/dotnet-install.sh + chmod +x ./dotnet-install.sh + ./dotnet-install.sh --channel 10.0 + + # dotnet-install.sh installs to $HOME/.dotnet by default but does not add + # dotnet to this process's PATH, so do that here. Avoid `cd` so we do not + # disturb the working directory the repo detection below relies on. + export DOTNET_ROOT="$HOME/.dotnet" + add_to_PATH "$DOTNET_ROOT" + fi +} + +verify_existing_dotnet_installation() { + # Get the major.minor of each installed SDK (empty if dotnet is absent). + sdks=$(dotnet --list-sdks 2>/dev/null | cut -d' ' -f1 | cut -d. -f1,2) + + # If a supported version is installed, echo the list; a non-empty result + # signals "already installed" to the caller. + supported_dotnet_versions="10.0" + for v in $supported_dotnet_versions; do + if echo "$sdks" | grep -q "$v"; then + echo "$sdks" + break + fi + done +} + +add_to_PATH () { + for directory; do + if [ ! -d "$directory" ]; then + continue; # Skip nonexistent directory. + fi + case ":$PATH:" in + *":$directory:"*) + break + ;; + *) + export PATH=$PATH:$directory + ;; + esac + done +} + +apt_install() { + pkg_name=$1 + + $sudo_cmd apt update + $sudo_cmd apt install $pkg_name -y 2>/dev/null +} + +print_unsupported_distro() { + prefix=$1 + distro=$2 + + echo "$prefix: $distro is not officially supported by the GCM project." + echo "See https://gh.io/gcm/linux for details." +} + +version_at_least() { + [ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ] +} + +sudo_cmd= + +# If the user isn't root, we need to use `sudo` for certain commands +# (e.g. installing packages). +if [ -z "$sudo_cmd" ]; then + if [ `id -u` != 0 ]; then + sudo_cmd=sudo + fi +fi + +# Install the dependencies needed to build and run GCM. The runtime libraries +# and the .NET SDK differ per operating system; the optional --aot build also +# needs a C toolchain (clang + zlib headers) to link the native binary. +os="$(uname -s)" +case "$os" in + Linux) + eval "$(sed -n 's/^ID=/distribution=/p' /etc/os-release)" + eval "$(sed -n 's/^VERSION_ID=/version=/p' /etc/os-release | tr -d '"')" + case "$distribution" in + debian | ubuntu) + $sudo_cmd apt update + install_packages apt install "curl git" + + # Install dotnet packages and dependencies if needed. + if [ -z "$(verify_existing_dotnet_installation)" ]; then + # First try to use native feeds (Ubuntu 22.04 and later). + if ! apt_install dotnet10; then + # If the native feeds fail, we fall back to + # packages.microsoft.com. We begin by adding the dotnet package + # repository/signing key. + $sudo_cmd apt update && $sudo_cmd apt install wget -y + curl -LO https://packages.microsoft.com/config/"$distribution"/"$version"/packages-microsoft-prod.deb + $sudo_cmd dpkg -i packages-microsoft-prod.deb + rm packages-microsoft-prod.deb + + # Proactively install tzdata to prevent prompts. + export DEBIAN_FRONTEND=noninteractive + $sudo_cmd apt install -y --no-install-recommends tzdata + + $sudo_cmd apt update + $sudo_cmd apt install apt-transport-https -y + $sudo_cmd apt update + $sudo_cmd apt install dotnet-sdk-10.0 dpkg-dev -y + fi + fi + + if [ -n "$aot" ]; then + install_packages apt install "clang zlib1g-dev" + fi + ;; + fedora | centos | rhel | ol) + $sudo_cmd dnf upgrade -y + + # Install dotnet/GCM dependencies. + install_packages dnf install "curl git krb5-libs libicu openssl-libs zlib findutils which bash" + + ensure_dotnet_installed + + if [ -n "$aot" ]; then + install_packages dnf install "clang zlib-devel" + fi + ;; + alpine) + $sudo_cmd apk update + + # Install dotnet/GCM dependencies. + # Alpine 3.14 and earlier need libssl1.1, while later versions need libssl3. + if ( version_at_least "3.15" $version ) then + libssl_pkg="libssl3" + else + libssl_pkg="libssl1.1" + fi + + install_packages apk add "curl git icu-libs krb5-libs libgcc libintl $libssl_pkg libstdc++ zlib which bash coreutils gcompat" + + ensure_dotnet_installed + + if [ -n "$aot" ]; then + install_packages apk add "clang build-base zlib-dev" + fi + ;; + sles | opensuse*) + $sudo_cmd zypper -n update + + # Install dotnet/GCM dependencies. + install_packages zypper install "curl git find krb5 libicu" + + ensure_dotnet_installed + + if [ -n "$aot" ]; then + install_packages zypper install "clang gcc zlib-devel" + fi + ;; + arch | cachyos) + print_unsupported_distro "WARNING" "$distribution" + + # --noconfirm required when running from container + $sudo_cmd pacman -Syu --noconfirm + + # Install dotnet/GCM dependencies. + install_packages pacman -Sy "curl git glibc gcc krb5 icu openssl libc++ zlib" + + ensure_dotnet_installed + + if [ -n "$aot" ]; then + install_packages pacman -Sy "clang" + fi + ;; + mariner | azurelinux*) + print_unsupported_distro "WARNING" "$distribution" + $sudo_cmd tdnf update -y + + # Install dotnet/GCM dependencies. + install_packages tdnf install "curl ca-certificates git krb5-libs libicu openssl-libs zlib findutils which bash awk" + + ensure_dotnet_installed + + if [ -n "$aot" ]; then + install_packages tdnf install "clang binutils zlib-devel" + fi + ;; + *) + print_unsupported_distro "ERROR" "$distribution" + exit + ;; + esac + ;; + Darwin) + # On macOS the runtime libraries are part of the system, so we only need + # git and the .NET SDK. Homebrew is not required (and its non-portable + # .NET cannot link an --aot build anyway). + if ! type git >/dev/null 2>&1; then + echo "git was not found. Install the Xcode Command Line Tools first:" + echo " xcode-select --install" + exit 1 + fi + + if [ -n "$aot" ] && ! type clang >/dev/null 2>&1; then + echo "A native (--aot) build needs clang. Install the Xcode Command Line Tools first:" + echo " xcode-select --install" + exit 1 + fi + + ensure_dotnet_installed + ;; + *) + echo "$os is not supported by this script." + echo "See https://gh.io/gcm/linux for supported Linux distributions." + exit 1 + ;; +esac + +# Detect whether we are running from a full source checkout or standalone +# (for example piped from `curl ... | sh`); clone the repository if standalone. +script_path="$(cd "$(dirname "$0")" 2>/dev/null && pwd)" +toplevel_path="${script_path%/build}" +if [ "z$script_path" = "z$toplevel_path" ] || [ ! -f "$toplevel_path/git-credential-manager.slnx" ]; then + toplevel_path="$PWD/git-credential-manager" + test -d "$toplevel_path" || git clone https://github.com/git-ecosystem/git-credential-manager +fi + +if [ -z "$DOTNET_ROOT" ]; then + DOTNET_ROOT="$(dirname $(command -v dotnet))" +fi + +# Select the per-OS publish script and the AOT mode. The default is a trimmed, +# self-contained (non-AOT) build that needs only the .NET SDK; --aot produces a +# native binary identical to the shipped package. +case "$os" in + Linux) publish_script="$toplevel_path/build/linux/publish.sh" ;; + Darwin) publish_script="$toplevel_path/build/macos/publish.sh" ;; +esac +if [ -n "$aot" ]; then + aot_arg=--aot +else + aot_arg=--no-aot +fi + +# Publish the application into a private staging directory. The per-OS publish +# script declares its own interpreter (via its shebang), so invoke it directly +# rather than assuming a particular shell is available here. +staging="$toplevel_path/out/install-from-source/payload" +rm -rf "$staging" +DOTNET_ROOT="$DOTNET_ROOT" PATH="$DOTNET_ROOT:$PATH" "$publish_script" \ + --configuration Release --output "$staging" "$aot_arg" + +# Install the published payload under /share/gcm-core and add a launcher +# symlink in /bin. This matches the layout produced by the .deb package. +install_to="$installPrefix/share/gcm-core" +link_to="$installPrefix/bin" + +# Only elevate for the install when the prefix is not writable by the current +# user (e.g. the default /usr/local). A user-writable prefix needs no sudo. +if [ "$(id -u)" = 0 ] || [ -w "$installPrefix" ]; then + install_sudo= +else + install_sudo=$sudo_cmd +fi + +$install_sudo mkdir -p "$install_to" "$link_to" +$install_sudo cp -R "$staging/." "$install_to/" +$install_sudo chmod -R 755 "$install_to" + +# Use a fixed relative target (bin and share are siblings under the prefix), so +# the link resolves once installed and we avoid GNU `ln -r` (absent on macOS). +$install_sudo ln -s -f "../share/gcm-core/git-credential-manager" "$link_to/git-credential-manager" + +add_to_PATH "$installPrefix/bin" + +echo "Install complete." +echo "You may need to restart your terminal, then configure GCM by running:" +echo " git-credential-manager configure" diff --git a/build/lib-cli.psm1 b/build/lib-cli.psm1 new file mode 100644 index 0000000000..f155009883 --- /dev/null +++ b/build/lib-cli.psm1 @@ -0,0 +1,197 @@ +<# +.SYNOPSIS + Shared helper functions for the Windows build scripts (build/windows). + +.DESCRIPTION + This file is a PowerShell module; import it from a build script one level + below build/, e.g.: + + Import-Module "$PSScriptRoot/../lib-cli.psm1" -Force + + It is the PowerShell counterpart of build/lib-cli.sh and mirrors the same + helpers (logging, repo/artifacts paths, runtime/version/configuration + normalisation), plus Inno Setup compiler resolution. Verbose output uses the + native PowerShell -Verbose / Write-Verbose mechanism. Only the public helpers + are exported (see Export-ModuleMember at the end of this file). +#> + +Set-StrictMode -Version Latest + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +# Echo the absolute, resolved path of the repository root: the directory one +# level above this library (build/..). It resolves relative to the library's own +# location, so it returns the same path regardless of the caller's working +# directory or which script dot-sourced it. +function Get-RepoRoot { + return (Get-Item $PSScriptRoot).Parent.FullName +} + +# Internal: build an artifacts directory /out///, +# where is , or _ when a runtime +# is given. The directory is not required to exist. +function Get-ArtifactsDir { + param( + [Parameter(Mandatory)] [string] $Kind, + [Parameter(Mandatory)] [string] $Project, + [Parameter(Mandatory)] [string] $Configuration, + [string] $Runtime + ) + $pivot = if ($Runtime) { "${Configuration}_${Runtime}" } else { $Configuration } + return (Join-Path (Get-RepoRoot) 'out' $Kind $Project $pivot) +} + +# Echo the artifacts build-output directory for project : +# out/bin//[_], mirroring where 'dotnet build' writes. +function Get-BinDir { + param( + [Parameter(Mandatory)] [string] $Project, + [Parameter(Mandatory)] [string] $Configuration, + [string] $Runtime + ) + return (Get-ArtifactsDir -Kind 'bin' -Project $Project -Configuration $Configuration -Runtime $Runtime) +} + +# Echo the artifacts publish directory for project : +# out/publish//[_], mirroring where 'dotnet publish' +# writes. +function Get-PublishDir { + param( + [Parameter(Mandatory)] [string] $Project, + [Parameter(Mandatory)] [string] $Configuration, + [string] $Runtime + ) + return (Get-ArtifactsDir -Kind 'publish' -Project $Project -Configuration $Configuration -Runtime $Runtime) +} + +# Echo the shared artifacts package directory for build configuration : +# out/package/. Unlike Get-BinDir/Get-PublishDir this is neither +# per-project nor per-runtime: it is the directory that final packages and +# archives are written to, with the runtime encoded in each file name. +function Get-PackageDir { + param([Parameter(Mandatory)] [string] $Configuration) + return (Join-Path (Get-RepoRoot) 'out' 'package' $Configuration) +} + +# Echo an absolute form of : an already-absolute path is returned +# unchanged, a relative path is resolved against the current directory. The path +# is not required to exist. +function Get-AbsolutePath { + param([Parameter(Mandatory)] [string] $Path) + if ([System.IO.Path]::IsPathRooted($Path)) { + return $Path + } + return (Join-Path (Get-Location).Path $Path) +} + +# --------------------------------------------------------------------------- +# Runtime identifiers +# --------------------------------------------------------------------------- + +# Echo a validated Windows runtime identifier. With no -Runtime (or an empty +# one), the host's own runtime is detected from the processor architecture; +# otherwise -Runtime is validated against the supported Windows runtimes. Writes +# a terminating error for an unsupported host or an invalid runtime. +function Resolve-Runtime { + param([string] $Runtime) + $valid = @('win-x64', 'win-x86', 'win-arm64') + if (-not $Runtime) { + switch ($env:PROCESSOR_ARCHITECTURE) { + 'AMD64' { return 'win-x64' } + 'x86' { return 'win-x86' } + 'ARM64' { return 'win-arm64' } + default { Write-Error "unsupported host architecture '$($env:PROCESSOR_ARCHITECTURE)'; specify -Runtime explicitly" -ErrorAction Stop } + } + } + if ($valid -notcontains $Runtime) { + Write-Error "unknown runtime '$Runtime' (expected one of: $($valid -join ', '))" -ErrorAction Stop + } + return $Runtime +} + +# --------------------------------------------------------------------------- +# Version +# --------------------------------------------------------------------------- + +# Echo the whitespace-trimmed contents of the repository VERSION file (in the +# repo root). Writes a terminating error if the file does not exist. +function Read-VersionFile { + $file = Join-Path (Get-RepoRoot) 'VERSION' + if (-not (Test-Path -LiteralPath $file)) { + Write-Error "version file '$file' not found" -ErrorAction Stop + } + return (Get-Content -Raw -LiteralPath $file).Trim() +} + +# Echo as a 3-component 'x.y.z' identifier: extra components are +# dropped and missing ones default to 0. When is empty, the version is +# read from the repository VERSION file instead. +function Resolve-Version { + param([string] $Version) + if (-not $Version) { + $Version = Read-VersionFile + } + $parts = $Version.Split('.') + $major = if ($parts.Count -ge 1 -and $parts[0]) { $parts[0] } else { '0' } + $minor = if ($parts.Count -ge 2 -and $parts[1]) { $parts[1] } else { '0' } + $patch = if ($parts.Count -ge 3 -and $parts[2]) { $parts[2] } else { '0' } + return "$major.$minor.$patch" +} + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +# Echo a validated, lower-cased build configuration name: 'debug' or 'release'. +# When is empty, 'release' is returned. Writes a terminating +# error for an unrecognised configuration. +function Resolve-Configuration { + param([string] $Configuration) + if (-not $Configuration) { + return 'release' + } + $c = $Configuration.ToLowerInvariant() + if ($c -ne 'debug' -and $c -ne 'release') { + Write-Error "unknown configuration '$Configuration' (expected 'Debug' or 'Release')" -ErrorAction Stop + } + return $c +} + +# --------------------------------------------------------------------------- +# Inno Setup +# --------------------------------------------------------------------------- + +# Resolve the path to the Inno Setup command-line compiler (ISCC.exe). When +# -InnoSetup is given it is used directly (and must exist); otherwise 'iscc' is +# looked up on PATH. Writes a terminating error if no compiler can be found. +# MSBuild callers can pass the Tools.InnoSetup package's $(InnoSetupCompiler) +# property; people running the scripts directly need iscc on PATH or must pass +# -InnoSetup. +function Resolve-InnoSetup { + param([string] $InnoSetup) + if ($InnoSetup) { + if (-not (Test-Path -LiteralPath $InnoSetup)) { + Write-Error "Inno Setup compiler not found at '$InnoSetup'" -ErrorAction Stop + } + return (Resolve-Path -LiteralPath $InnoSetup).Path + } + $cmd = Get-Command 'iscc' -CommandType Application -ErrorAction SilentlyContinue | + Select-Object -First 1 + if (-not $cmd) { + Write-Error "Inno Setup compiler (iscc) not found on PATH; pass -InnoSetup or install Inno Setup" -ErrorAction Stop + } + return $cmd.Source +} + +# --------------------------------------------------------------------------- +# Exports +# --------------------------------------------------------------------------- + +# Get-ArtifactsDir is an internal implementation helper and is intentionally not +# exported; only the public helpers below make up the module's surface. +Export-ModuleMember -Function ` + Get-RepoRoot, Get-BinDir, Get-PublishDir, Get-PackageDir, Get-AbsolutePath, ` + Resolve-Runtime, Read-VersionFile, Resolve-Version, Resolve-Configuration, ` + Resolve-InnoSetup diff --git a/build/lib-cli.sh b/build/lib-cli.sh new file mode 100755 index 0000000000..f355ce62cf --- /dev/null +++ b/build/lib-cli.sh @@ -0,0 +1,317 @@ +#!/bin/bash +# +# Shared helper functions for the platform build scripts (build//). +# +# This file is a function library; source it (don't execute it) from a build +# script two levels below build/, e.g.: +# +# THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +# . "$THISDIR/../../lib-cli.sh" +# + +# Guard against accidental direct execution. +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + echo "error: lib-cli.sh is a function library and must be sourced, not executed" >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- + +# info ... Write an informational message to stdout. +info () { + echo "$*" +} + +# verbose ... Write an informational message to stdout, but only when +# verbose output is enabled: the GCM_BUILD_VERBOSE environment variable is set to +# a non-empty value. Scripts enable this by parsing a -v/--verbose flag and +# calling enable_verbose, which child scripts then inherit. +verbose () { + [ -n "${GCM_BUILD_VERBOSE:-}" ] || return 0 + echo "$*" +} + +# enable_verbose +# Turn on verbose output by setting and exporting GCM_BUILD_VERBOSE, so that +# verbose() prints and any child scripts inherit the setting. This is the single +# place that knows the variable name. Call it directly (e.g. from a -v/--verbose +# argument handler), not inside a $(...) command substitution or pipeline, where +# the export would be confined to a subshell and lost. +enable_verbose () { + export GCM_BUILD_VERBOSE=1 +} + +# warn ... Write a warning message to stderr. +warn () { + echo "warning: $*" >&2 +} + +# error ... Write an error message to stderr. +error () { + echo "error: $*" >&2 +} + +# die ... Write an error message to stderr and exit with status 1. +die () { + echo "fatal: $*" >&2 + exit 1 +} + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +# make_absolute +# Echo an absolute form of : an already-absolute path is echoed +# unchanged, a relative path is resolved against the current directory. The +# path is not required to exist. +make_absolute () { + case "$1" in + /*) echo "$1" ;; + *) echo "$PWD/$1" ;; + esac +} + +# repo_root +# Echo the absolute, symlink-resolved path of the repository root: the directory +# one level above this library (build/..). It resolves relative to the library's +# own location, so it returns the same path regardless of the caller's working +# directory or which script sourced it. +repo_root () { + ( cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P ) +} + +# --------------------------------------------------------------------------- +# Artifacts layout +# --------------------------------------------------------------------------- + +# _artifacts_dir [runtime] +# Shared implementation for bin_dir/publish_dir. Echo the artifacts directory +# /out///, where is , or +# _ when a runtime is given (the repository root is located +# relative to this library). Returns 1 without output if or +# is missing. +_artifacts_dir () { + local kind="$1" project="${2:-}" config="${3:-}" runtime="${4:-}" + if [ -z "$project" ] || [ -z "$config" ]; then + return 1 + fi + local pivot + if [ -n "$runtime" ]; then + pivot="${config}_${runtime}" + else + pivot="$config" + fi + echo "$(repo_root)/out/$kind/$project/$pivot" +} + +# bin_dir [runtime] +# Echo the artifacts build-output directory for project : +# out/bin//[_], mirroring where 'dotnet build' writes +# under the repository artifacts output. The directory is not required to exist. +# Prints an error and returns 1 if or is missing, so call it +# as: +# +# dir="$(bin_dir "$PROJECT_NAME" "$CONFIGURATION" "$RUNTIME")" || exit 1 +bin_dir () { + _artifacts_dir bin "$@" || { error "bin_dir: usage: bin_dir [runtime]"; return 1; } +} + +# publish_dir [runtime] +# Echo the artifacts publish directory for project : +# out/publish//[_], mirroring where 'dotnet publish' +# writes under the repository artifacts output. The directory is not required to +# exist. Prints an error and returns 1 if or is missing, so +# call it as: +# +# dir="$(publish_dir "$PROJECT_NAME" "$CONFIGURATION" "$RUNTIME")" || exit 1 +publish_dir () { + _artifacts_dir publish "$@" || { error "publish_dir: usage: publish_dir [runtime]"; return 1; } +} + +# package_dir +# Echo the artifacts package directory for build configuration : +# out/package/ (the repository root is located relative to this library). +# Unlike bin_dir/publish_dir this is neither per-project nor per-runtime: it is +# the shared directory that final packages and archives are written to, with the +# runtime encoded in each file name. The directory is not required to exist. +# Prints an error and returns 1 if is missing, so call it as: +# +# dir="$(package_dir "$CONFIGURATION")" || exit 1 +package_dir () { + local config="${1:-}" + if [ -z "$config" ]; then + error "package_dir: usage: package_dir " + return 1 + fi + echo "$(repo_root)/out/package/$config" +} + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- + +# require_value [value]... +# Validate that the space-separated option was given a value; calls die() +# (which exits) if not. Pass the remaining argument list so the value can be +# inspected, e.g.: +# +# --version) require_value "$@"; VERSION="$2"; shift 2 ;; +# +# Because this exits the calling shell, it must be invoked directly, not inside a +# $(...) command substitution or pipeline where the exit would be swallowed. +require_value () { + if [ "$#" -lt 2 ]; then + die "option '$1' requires a value" + fi + case "$2" in + -*) die "option '$1' requires a value" ;; + esac +} + +# bool_flag <--name|--no-name> +# Echo "true" for an affirmative flag (e.g. --aot) or "false" for its negated +# form (--no-aot), so a script can accept a paired boolean option from a single +# branch of its argument loop: +# +# --aot|--no-aot) AOT="$(bool_flag "$1")"; shift ;; +# +bool_flag () { + case "$1" in + --no-*) printf 'false' ;; + --*) printf 'true' ;; + *) die "bool_flag: not a --flag/--no-flag option: '$1'" ;; + esac +} + +# --------------------------------------------------------------------------- +# Runtime identifiers +# --------------------------------------------------------------------------- + +# detect_runtime +# Echo the default .NET runtime identifier for the current host: the operating +# system (from 'uname -s': Darwin->osx, Linux->linux) joined to the architecture +# (from 'uname -m'), e.g. 'osx-arm64' or 'linux-x64'. Prints an error and returns +# 1 for an unrecognised host, so call it as: RID="$(detect_runtime)" || exit 1 +detect_runtime () { + local os arch + case "$(uname -s)" in + Darwin) os="osx" ;; + Linux) os="linux" ;; + *) + error "unsupported host OS '$(uname -s)'; specify --runtime explicitly" + return 1 + ;; + esac + case "$(uname -m)" in + x86_64|amd64) arch="x64" ;; + arm64|aarch64) arch="arm64" ;; + armv7l|armv6l) arch="arm" ;; + *) + error "unsupported host architecture '$(uname -m)'; specify --runtime explicitly" + return 1 + ;; + esac + echo "$os-$arch" +} + +# validate_runtime [os] +# Return 0 if is a recognised .NET runtime identifier, otherwise print +# an error and return 1. The valid set lives here; passing ('osx' or +# 'linux') restricts the check to that OS's runtimes, e.g.: +# +# validate_runtime "$RUNTIME" # any supported runtime +# validate_runtime "$RUNTIME" osx # osx-x64 or osx-arm64 only +validate_runtime () { + local runtime="$1" + local os="${2:-}" + local osx_runtimes="osx-x64 osx-arm64" + local linux_runtimes="linux-x64 linux-arm64 linux-arm" + local valid + + case "$os" in + osx) valid="$osx_runtimes" ;; + linux) valid="$linux_runtimes" ;; + "") valid="$osx_runtimes $linux_runtimes" ;; + *) error "unknown os '$os' (expected 'osx' or 'linux')" ; return 1 ;; + esac + + local r + for r in $valid; do + [ "$runtime" = "$r" ] && return 0 + done + error "unknown runtime '$runtime' (expected one of: $valid)" + return 1 +} + +# normalize_runtime [runtime] +# Echo a validated runtime identifier for the current host. With no +# (or an empty one), echoes the host's own runtime; otherwise echoes +# after validating it against the runtimes valid for the host's OS. Prints an +# error and returns 1 for an unsupported host or an invalid runtime. Call it in +# a command substitution: +# +# RUNTIME="$(normalize_runtime "$RUNTIME")" || exit 1 +normalize_runtime () { + local runtime="${1:-}" + local host_rid + host_rid="$(detect_runtime)" || return 1 + if [ -z "$runtime" ]; then + echo "$host_rid" + return 0 + fi + validate_runtime "$runtime" "${host_rid%%-*}" || return 1 + echo "$runtime" +} + +# --------------------------------------------------------------------------- +# Version +# --------------------------------------------------------------------------- + +# read_version_file +# Echo the whitespace-stripped contents of the repository VERSION file, which +# lives in the repo root (one directory up from this library). Prints an error +# and returns 1 if the file does not exist. +read_version_file () { + local file + file="$(repo_root)/VERSION" + if [ ! -f "$file" ]; then + error "version file '$file' not found" + return 1 + fi + tr -d '[:space:]' < "$file" +} + +# normalize_version [version] +# Echo as a 3-component 'x.y.z' identifier: extra components are +# dropped and missing ones default to 0. When is empty, the version is +# read from the repository VERSION file instead. +normalize_version () { + local version="${1:-}" + if [ -z "$version" ]; then + version="$(read_version_file)" || return 1 + fi + local major minor patch _rest + IFS='.' read -r major minor patch _rest <<< "$version" + echo "${major:-0}.${minor:-0}.${patch:-0}" +} + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +# normalize_configuration [configuration] +# Echo a validated, lowercased build configuration name: 'debug' or 'release'. +# When is empty, 'release' is returned. Prints an error and +# returns 1 for an unrecognised configuration. +normalize_configuration () { + local config + config="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" + case "$config" in + debug|release) echo "$config" ;; + "") echo "release" ;; + *) error "unknown configuration '${1:-}' (expected 'Debug' or 'Release')"; return 1 ;; + esac +} diff --git a/build/linux/Linux.Distribution.csproj b/build/linux/Linux.Distribution.csproj new file mode 100644 index 0000000000..372069d802 --- /dev/null +++ b/build/linux/Linux.Distribution.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/build/linux/archive.sh b/build/linux/archive.sh new file mode 100755 index 0000000000..c348f57b00 --- /dev/null +++ b/build/linux/archive.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# +# Archives the published Git Credential Manager application into distributable +# .tar.gz files: a payload tarball of the shipping binaries and a separate +# symbols tarball, written alongside the package. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration: Debug or Release. + (default: Release) + -r, --runtime Target runtime identifier: 'linux-x64', 'linux-arm64' + or 'linux-arm'. (default: auto-detected from the host) + --version Version to embed in the archive file names. + (default: the repository VERSION file) + --bindir Directory of published binaries to archive. + (default: the application's default publish dir) + --symdir Directory of debug symbols to archive. + (default: .sym) + --output Directory to write the archives to. + (default: out/package/) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") + $(basename "$0") --configuration Release --runtime linux-x64 --version 2.6.1 +EOF +} + +# Defaults +CONFIGURATION="" +RUNTIME="" +VERSION="" +BINDIR="" +SYMDIR="" +OUTPUT="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) print_usage; exit 0 ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + -r|--runtime) require_value "$@"; RUNTIME="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + --bindir) require_value "$@"; BINDIR="$2"; shift 2 ;; + --symdir) require_value "$@"; SYMDIR="$2"; shift 2 ;; + --output) require_value "$@"; OUTPUT="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalise arguments / apply defaults. +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +RUNTIME="$(normalize_runtime "$RUNTIME")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +# Source of the published binaries to archive (override with --bindir; defaults +# to the application's default artifacts publish directory, from publish.sh). +if [ -n "$BINDIR" ]; then + BINDIR="$(make_absolute "$BINDIR")" +else + BINDIR="$(publish_dir git-credential-manager "$CONFIGURATION" "$RUNTIME")" || exit 1 +fi + +# Source of the debug symbols to archive (override with --symdir; defaults to +# the sibling symbol directory produced by publish.sh). +if [ -n "$SYMDIR" ]; then + SYMDIR="$(make_absolute "$SYMDIR")" +else + SYMDIR="$BINDIR.sym" +fi + +# Destination directory for the archives (override with --output; defaults to +# the top-level artifacts package directory). +if [ -n "$OUTPUT" ]; then + OUTDIR="$(make_absolute "$OUTPUT")" +else + OUTDIR="$(package_dir "$CONFIGURATION")" || exit 1 +fi + +PAYLOAD_TARBALL="$OUTDIR/gcm-$RUNTIME-$VERSION.tar.gz" +SYMBOLS_TARBALL="$OUTDIR/gcm-$RUNTIME-$VERSION-symbols.tar.gz" + +# Pre-execution checks +[ -d "$BINDIR" ] || die "Binaries directory '$BINDIR' not found. Did you publish first?" + +mkdir -p "$OUTDIR" + +verbose "configuration: $CONFIGURATION" +verbose "runtime: $RUNTIME" +verbose "version: $VERSION" +verbose "bin dir: $BINDIR" +verbose "symbol dir: $SYMDIR" +verbose "output dir: $OUTDIR" + +# Archive the shipping binaries. +info "Archiving binaries from '$BINDIR'..." +rm -f "$PAYLOAD_TARBALL" +tar -czf "$PAYLOAD_TARBALL" -C "$BINDIR" . || die "Failed to create binaries archive" +info "Created $PAYLOAD_TARBALL" + +# Archive the debug symbols, if any were produced. +if [ -d "$SYMDIR" ]; then + info "Archiving symbols from '$SYMDIR'..." + rm -f "$SYMBOLS_TARBALL" + tar -czf "$SYMBOLS_TARBALL" -C "$SYMDIR" . || die "Failed to create symbols archive" + info "Created $SYMBOLS_TARBALL" +else + warn "symbols directory '$SYMDIR' not found; skipping symbols archive" +fi + +info "Archiving complete." diff --git a/build/linux/build.sh b/build/linux/build.sh new file mode 100755 index 0000000000..6a6276392e --- /dev/null +++ b/build/linux/build.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# +# Builds the Linux distributables for Git Credential Manager (the .deb package +# plus binary and symbol tarballs) by running the publish, pack and archive +# steps in sequence. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration to publish: Debug or Release. + (default: Release) + --version Version to stamp into the binaries and package. + (default: the repository VERSION file) + -r, --runtime Target runtime identifier: 'linux-x64', 'linux-arm64' + or 'linux-arm'. (default: auto-detected from the host) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") --version 2.6.1 + $(basename "$0") --configuration Release --version 2.6.1 --runtime linux-x64 +EOF +} + +# Defaults +CONFIGURATION="" +VERSION="" +RUNTIME="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) + print_usage + exit 0 + ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + -r|--runtime) require_value "$@"; RUNTIME="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalize arguments/use defaults if unset +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +RUNTIME="$(normalize_runtime "$RUNTIME")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +info "Building Linux distribution..." +verbose "configuration: $CONFIGURATION" +verbose "runtime: $RUNTIME" +verbose "version: $VERSION" + +# Publish the application. +"$THISDIR/publish.sh" --configuration "$CONFIGURATION" --runtime "$RUNTIME" --version "$VERSION" + +# Build the .deb package. +"$THISDIR/pack.sh" --configuration "$CONFIGURATION" --runtime "$RUNTIME" --version "$VERSION" + +# Create the binaries and symbols tarballs. +"$THISDIR/archive.sh" --configuration "$CONFIGURATION" --runtime "$RUNTIME" --version "$VERSION" + +info "Linux distribution build complete." diff --git a/build/linux/debian-package/control b/build/linux/debian-package/control new file mode 100644 index 0000000000..a07d80ef0d --- /dev/null +++ b/build/linux/debian-package/control @@ -0,0 +1,11 @@ +Package: gcm +Version: %%VERSION%% +Section: vcs +Priority: optional +Architecture: %%ARCH%% +Depends: +Maintainer: Microsoft Corporation +Description: Cross Platform Git Credential Manager command line utility. + GCM supports authentication with a number of Git hosting providers + including GitHub, BitBucket, and Azure DevOps. + For more information see https://aka.ms/gcm diff --git a/build/linux/pack.sh b/build/linux/pack.sh new file mode 100755 index 0000000000..54569feb94 --- /dev/null +++ b/build/linux/pack.sh @@ -0,0 +1,158 @@ +#!/bin/bash +# +# Builds the Linux package (.deb) for Git Credential Manager: stages the +# published application into a Debian package root, writes the control file and +# a launcher symlink, then builds the .deb with dpkg-deb. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration: Debug or Release. + (default: Release) + -r, --runtime Target runtime identifier: 'linux-x64', 'linux-arm64' + or 'linux-arm'. (default: auto-detected from the host) + --version Version to stamp into the package. + (default: the repository VERSION file) + --bindir Directory of published binaries to package. + (default: the application's default publish dir) + --output Directory to write the package to. + (default: out/package/) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") + $(basename "$0") --configuration Release --runtime linux-x64 --version 2.6.1 +EOF +} + +# Install location and package assets +INSTALL_LOCATION="/usr/local/share/gcm-core" +LINK_LOCATION="/usr/local/bin" +# The control file template (with %%VERSION%%/%%ARCH%% tokens) lives in the +# sibling debian-package/ directory. +CONTROL_TEMPLATE="$THISDIR/debian-package/control" + +# Defaults +CONFIGURATION="" +RUNTIME="" +VERSION="" +BINDIR="" +OUTPUT="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) + print_usage + exit 0 + ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + -r|--runtime) require_value "$@"; RUNTIME="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + --bindir) require_value "$@"; BINDIR="$2"; shift 2 ;; + --output) require_value "$@"; OUTPUT="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalise arguments / apply defaults. +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +RUNTIME="$(normalize_runtime "$RUNTIME")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +# Source of the published binaries to package (override with --bindir; defaults +# to the application's default artifacts publish directory, from publish.sh). +if [ -n "$BINDIR" ]; then + BINDIR="$(make_absolute "$BINDIR")" +else + BINDIR="$(publish_dir git-credential-manager "$CONFIGURATION" "$RUNTIME")" || exit 1 +fi + +# Private scratch (the staged Debian package root), kept per runtime under the +# artifacts bin directory so that builds for different runtimes don't collide. +SCRATCH="$(bin_dir Linux.Distribution "$CONFIGURATION" "$RUNTIME")" || exit 1 +DEBROOT="$SCRATCH/root" +INSTALL_DIR="$DEBROOT$INSTALL_LOCATION" +LINK_DIR="$DEBROOT$LINK_LOCATION" + +# Destination directory for the package (override with --output; defaults to the +# top-level artifacts package directory). The file name is always defined here. +if [ -n "$OUTPUT" ]; then + OUTDIR="$(make_absolute "$OUTPUT")" +else + OUTDIR="$(package_dir "$CONFIGURATION")" || exit 1 +fi +DEBOUT="$OUTDIR/gcm-$RUNTIME-$VERSION.deb" + +# Map the .NET runtime identifier to a Debian architecture. +case "$RUNTIME" in + linux-x64) ARCH="amd64" ;; + linux-arm64) ARCH="arm64" ;; + linux-arm) ARCH="armhf" ;; + *) die "unsupported runtime '$RUNTIME' for a Debian package" ;; +esac + +# Pre-execution checks +[ -d "$BINDIR" ] || die "Publish directory '$BINDIR' not found. Did you publish first?" +[ -f "$CONTROL_TEMPLATE" ] || die "Control file template '$CONTROL_TEMPLATE' not found" + +verbose "configuration: $CONFIGURATION" +verbose "runtime: $RUNTIME" +verbose "version: $VERSION" +verbose "architecture: $ARCH" +verbose "bin dir: $BINDIR" +verbose "scratch: $SCRATCH" +verbose "output: $DEBOUT" + +# Stage a fresh, private package root. +info "Staging package root from '$BINDIR'..." +rm -rf "$DEBROOT" +mkdir -p "$DEBROOT/DEBIAN" "$INSTALL_DIR" "$LINK_DIR" +cp -R "$BINDIR/." "$INSTALL_DIR/" || die "Failed to stage payload" + +# Normalise permissions. +info "Setting file permissions..." +chmod -R 755 "$INSTALL_DIR" || die "Failed to set payload permissions" + +# Create a launcher symlink (relative, so it resolves once installed). +info "Creating launcher symlink..." +ln -s -r "$INSTALL_DIR/git-credential-manager" "$LINK_DIR/git-credential-manager" \ + || die "Failed to create launcher symlink" + +# Generate the control file from the template, filling in the version and +# architecture tokens. +info "Writing Debian control file..." +sed -e "s/%%VERSION%%/$VERSION/g" \ + -e "s/%%ARCH%%/$ARCH/g" \ + "$CONTROL_TEMPLATE" > "$DEBROOT/DEBIAN/control" \ + || die "Failed to write Debian control file" + +# Build the package. +info "Building Debian package..." +rm -f "$DEBOUT" +mkdir -p "$(dirname "$DEBOUT")" +dpkg-deb -Zxz --root-owner-group --build "$DEBROOT" "$DEBOUT" \ + || die "Failed to build Debian package" + +info "Packaging complete: $DEBOUT" diff --git a/build/linux/publish.sh b/build/linux/publish.sh new file mode 100755 index 0000000000..653f26404c --- /dev/null +++ b/build/linux/publish.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# +# Publishes the Git Credential Manager application for the Linux package. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration to publish: Debug or Release. + (default: Release) + -r, --runtime Target runtime identifier: 'linux-x64', 'linux-arm64' + or 'linux-arm'. (default: auto-detected from the host) + -o, --output Directory to publish the application to. + (default: the project's default artifacts dir) + --symbol-output Directory to move debug symbols (.pdb/.dbg) into. + (default: .sym) + --version Version to stamp into the published binaries. + (default: the repository VERSION file) + --aot / --no-aot Publish a native ahead-of-time (AOT) build, or a + trimmed, self-contained non-AOT build instead. + (default: --aot) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") + $(basename "$0") --configuration Release --runtime linux-x64 +EOF +} + +# Defaults +CONFIGURATION="" +RUNTIME="" +OUTPUT="" +SYMBOL_OUTPUT="" +VERSION="" +AOT="true" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) + print_usage + exit 0 + ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + -r|--runtime) require_value "$@"; RUNTIME="$2"; shift 2 ;; + -o|--output) require_value "$@"; OUTPUT="$2"; shift 2 ;; + --symbol-output) require_value "$@"; SYMBOL_OUTPUT="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + --aot|--no-aot) AOT="$(bool_flag "$1")"; shift ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalize arguments/use defaults if unset +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +RUNTIME="$(normalize_runtime "$RUNTIME")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +# Directories +GCM_SRC="$(repo_root)/src/git-credential-manager" + +# Resolve the publish output directory (default: the project's artifacts publish +# directory) and the sibling directory that debug symbols are separated into. +if [ -n "$OUTPUT" ]; then + OUTDIR="$(make_absolute "$OUTPUT")" +else + OUTDIR="$(publish_dir git-credential-manager "$CONFIGURATION" "$RUNTIME")" || exit 1 +fi +if [ -n "$SYMBOL_OUTPUT" ]; then + SYMOUTDIR="$(make_absolute "$SYMBOL_OUTPUT")" +else + SYMOUTDIR="$OUTDIR.sym" +fi +[ "$SYMOUTDIR" != "$OUTDIR" ] || die "--symbol-output must differ from the publish output directory" + +verbose "configuration: $CONFIGURATION" +verbose "runtime: $RUNTIME" +verbose "version: $VERSION" +verbose "aot: $AOT" +verbose "output dir: $OUTDIR" +verbose "symbol dir: $SYMOUTDIR" + +# By default the application is published ahead-of-time (AOT) compiled, as +# configured in the project. For a non-AOT build, just turn AOT off: the +# project then enables trimming, which implies a self-contained publish, so +# --self-contained does not need to be passed explicitly. +AOT_ARGS= +if [ "$AOT" != "true" ]; then + AOT_ARGS="-p:PublishAot=false" +fi + +# Publish the application to the resolved output directory. +info "Publishing application..." +# shellcheck disable=SC2086 # AOT_ARGS is intentionally word-split (0 or 2 args). +dotnet publish "$GCM_SRC" \ + -v:normal \ + --configuration="$CONFIGURATION" \ + --runtime="$RUNTIME" \ + --output "$OUTDIR" \ + -p:VersionOverride="$VERSION" \ + $AOT_ARGS || die "Failed to publish application" + +# Separate debug symbols (managed .pdb files and native .dbg files) out of the +# shipping payload into the sibling symbol directory, so the published output +# holds only the files we ship. +info "Separating debug symbols into '$SYMOUTDIR'..." +rm -rf "$SYMOUTDIR" +mkdir -p "$SYMOUTDIR" +find "$OUTDIR" -maxdepth 1 -name '*.pdb' -exec mv {} "$SYMOUTDIR/" \; || die "Failed to move .pdb symbols" +find "$OUTDIR" -maxdepth 1 -name '*.dbg' -exec mv {} "$SYMOUTDIR/" \; || die "Failed to move .dbg symbols" + +info "Publish complete." diff --git a/build/macos/Mac.Distribution.csproj b/build/macos/Mac.Distribution.csproj new file mode 100644 index 0000000000..0b52ca2cb1 --- /dev/null +++ b/build/macos/Mac.Distribution.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/build/macos/archive.sh b/build/macos/archive.sh new file mode 100755 index 0000000000..a7b3de981d --- /dev/null +++ b/build/macos/archive.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# +# Archives the published Git Credential Manager application into distributable +# .tar.gz files: a payload tarball of the shipping binaries and a separate +# symbols tarball, written alongside the installer package. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration: Debug or Release. + (default: Release) + -r, --runtime Target runtime identifier: 'osx-x64' or 'osx-arm64'. + (default: auto-detected from the host architecture) + --version Version to embed in the archive file names. + (default: the repository VERSION file) + --bindir Directory of published binaries to archive. + (default: the application's default publish dir) + --symdir Directory of debug symbols to archive. + (default: .sym) + --output Directory to write the archives to. + (default: out/package/) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") + $(basename "$0") --configuration Release --runtime osx-arm64 --version 2.6.1 +EOF +} + +# Defaults +CONFIGURATION="" +RUNTIME="" +VERSION="" +BINDIR="" +SYMDIR="" +OUTPUT="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) print_usage; exit 0 ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + -r|--runtime) require_value "$@"; RUNTIME="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + --bindir) require_value "$@"; BINDIR="$2"; shift 2 ;; + --symdir) require_value "$@"; SYMDIR="$2"; shift 2 ;; + --output) require_value "$@"; OUTPUT="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalise arguments / apply defaults. +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +RUNTIME="$(normalize_runtime "$RUNTIME")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +# Source of the published binaries to archive (override with --bindir; defaults +# to the application's default artifacts publish directory, from publish.sh). +if [ -n "$BINDIR" ]; then + BINDIR="$(make_absolute "$BINDIR")" +else + BINDIR="$(publish_dir git-credential-manager "$CONFIGURATION" "$RUNTIME")" || exit 1 +fi + +# Source of the debug symbols to archive (override with --symdir; defaults to +# the sibling symbol directory produced by publish.sh). +if [ -n "$SYMDIR" ]; then + SYMDIR="$(make_absolute "$SYMDIR")" +else + SYMDIR="$BINDIR.sym" +fi + +# Destination directory for the archives (override with --output; defaults to +# the top-level artifacts package directory). +if [ -n "$OUTPUT" ]; then + OUTDIR="$(make_absolute "$OUTPUT")" +else + OUTDIR="$(package_dir "$CONFIGURATION")" || exit 1 +fi + +PAYLOAD_TARBALL="$OUTDIR/gcm-$RUNTIME-$VERSION.tar.gz" +SYMBOLS_TARBALL="$OUTDIR/gcm-$RUNTIME-$VERSION-symbols.tar.gz" + +# Pre-execution checks +[ -d "$BINDIR" ] || die "Binaries directory '$BINDIR' not found. Did you publish first?" + +mkdir -p "$OUTDIR" + +verbose "configuration: $CONFIGURATION" +verbose "runtime: $RUNTIME" +verbose "version: $VERSION" +verbose "bin dir: $BINDIR" +verbose "symbol dir: $SYMDIR" +verbose "output dir: $OUTDIR" + +# Archive the shipping binaries. +info "Archiving binaries from '$BINDIR'..." +rm -f "$PAYLOAD_TARBALL" +tar -czf "$PAYLOAD_TARBALL" -C "$BINDIR" . || die "Failed to create binaries archive" +info "Created $PAYLOAD_TARBALL" + +# Archive the debug symbols, if any were produced. +if [ -d "$SYMDIR" ]; then + info "Archiving symbols from '$SYMDIR'..." + rm -f "$SYMBOLS_TARBALL" + tar -czf "$SYMBOLS_TARBALL" -C "$SYMDIR" . || die "Failed to create symbols archive" + info "Created $SYMBOLS_TARBALL" +else + warn "symbols directory '$SYMDIR' not found; skipping symbols archive" +fi + +info "Archiving complete." diff --git a/build/macos/build.sh b/build/macos/build.sh new file mode 100755 index 0000000000..526a1d87a9 --- /dev/null +++ b/build/macos/build.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# +# Builds the macOS distributables for Git Credential Manager (the .pkg installer +# plus payload and symbol tarballs) by running the publish, codesign, pack and +# archive steps in sequence. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration to publish: Debug or Release. + (default: Release) + --version Version to stamp into the binaries and package. + (default: the repository VERSION file) + -r, --runtime Target runtime identifier: 'osx-x64' or 'osx-arm64'. + (default: auto-detected from the host architecture) + --dev-identity Developer signing identity. When set, the published + binaries are codesigned (with entitlements and the + hardened runtime) before packaging. (default: unsigned) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") --version 2.6.1 + $(basename "$0") --configuration Release --version 2.6.1 --runtime osx-arm64 +EOF +} + +# Defaults +CONFIGURATION="" +VERSION="" +RUNTIME="" +DEV_IDENTITY="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) + print_usage + exit 0 + ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + -r|--runtime) require_value "$@"; RUNTIME="$2"; shift 2 ;; + --dev-identity) require_value "$@"; DEV_IDENTITY="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalize arguments/use defaults if unset +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +RUNTIME="$(normalize_runtime "$RUNTIME")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +info "Building macOS distribution..." +verbose "configuration: $CONFIGURATION" +verbose "runtime: $RUNTIME" +verbose "version: $VERSION" + +# Publish the application +"$THISDIR/publish.sh" --configuration "$CONFIGURATION" --runtime "$RUNTIME" --version "$VERSION" + +# Developer-sign the published Mach-O files (attaching entitlements) before +# packaging, when a developer identity is given. +if [ -n "$DEV_IDENTITY" ]; then + BINDIR="$(publish_dir git-credential-manager "$CONFIGURATION" "$RUNTIME")" || exit 1 + "$THISDIR/codesign.sh" developer \ + --bindir "$BINDIR" \ + --identity "$DEV_IDENTITY" +fi + +# Package the (signed) binaries into the installer (.pkg) +"$THISDIR/pack.sh" --configuration "$CONFIGURATION" --runtime "$RUNTIME" --version "$VERSION" + +# Create the (signed) binaries and symbols tarballs. +"$THISDIR/archive.sh" --configuration "$CONFIGURATION" --runtime "$RUNTIME" --version "$VERSION" + +info "macOS distribution build complete." diff --git a/build/macos/codesign.sh b/build/macos/codesign.sh new file mode 100755 index 0000000000..b9532eb6a1 --- /dev/null +++ b/build/macos/codesign.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# +# Code-sign Git Credential Manager for macOS. Currently supports developer +# signing (attaching entitlements and enabling the hardened runtime). +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < [options] + +Code-sign Git Credential Manager for macOS. + +Commands: + developer Developer-sign the Mach-O files in a directory, attaching an + entitlements file and enabling the hardened runtime. + +Run '$(basename "$0") --help' for command-specific options. +EOF +} + +print_developer_usage () { + cat <, attaching an entitlements file and enabling the hardened +runtime. Files are signed in place. + +Options: + --bindir Directory of files to sign (searched recursively). (required) + --identity Signing identity, e.g. a Developer ID. (required) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. +EOF +} + +# log_keychain_info +# Log keychain state for diagnostics. +log_keychain_info () { + info "keychain diagnostics:" + security list-keychains -d user 2>&1 | sed 's/^/ search: /' || true + local default_kc + default_kc="$(security default-keychain -d user 2>/dev/null | sed -E 's/^[[:space:]]*"?//; s/"?[[:space:]]*$//')" + info " default: ${default_kc:-(none)}" + if [ -n "$default_kc" ]; then + security show-keychain-info "$default_kc" 2>&1 | sed 's/^/ settings: /' || true + fi + security find-identity -v -p codesigning 2>&1 | sed 's/^/ identity: /' || true +} + +# sign_developer +# Sign every Mach-O file under in place. +sign_developer () { + local bindir="$1" identity="$2" + local entitlements="$THISDIR/entitlements.xml" + local options="runtime" + + # Check for the entitlements file. + [ -f "$entitlements" ] || die "developer: entitlements file '$entitlements' not found" + + info "Developer-signing Mach-O files under '$bindir'" + info " identity: $identity" + info " entitlements: $entitlements" + info " options: ${options:-(none)}" + + log_keychain_info + + local count=0 f start dur + while IFS= read -r -d '' f; do + # Only Mach-O binaries (executables and dylibs) need code-signing. + file --mime "$f" 2>/dev/null | grep -q 'x-mach-binary' || continue + info " signing ${f#"$bindir"/} (started $(date -u '+%H:%M:%SZ'))" + start=$(date +%s) + codesign --sign "$identity" --force --timestamp --options $options \ + --entitlements "$entitlements" --verbose=4 "$f" || die "failed to sign '$f'" + dur=$(( $(date +%s) - start )) + info " signed ${f#"$bindir"/} in ${dur}s" + count=$((count + 1)) + done < <(find "$bindir" -type f -print0) + + if [ "$count" -eq 0 ]; then + warn "no Mach-O files found under '$bindir'; nothing was signed" + else + info "Developer signing complete ($count file(s) signed)" + fi +} + +cmd_developer () { + local bindir="" entitlements="" identity="" + while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) print_developer_usage; exit 0 ;; + -v|--verbose) enable_verbose; shift ;; + --bindir) require_value "$@"; bindir="$2"; shift 2 ;; + --identity) require_value "$@"; identity="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") developer --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") developer --help')" ;; + esac + done + + [ -n "$bindir" ] || die "developer: --bindir was not specified" + [ -n "$identity" ] || die "developer: --identity was not specified" + [ -d "$bindir" ] || die "developer: directory '$bindir' not found" + + # codesign needs the entitlements as an absolute path. + sign_developer "$bindir" "$identity" +} + +# Dispatch on the command. +[ "$#" -gt 0 ] || { print_usage; exit 1; } +COMMAND="$1"; shift +case "$COMMAND" in + developer) cmd_developer "$@" ;; + -h|--help) print_usage; exit 0 ;; + *) die "unknown command '$COMMAND' (try '$(basename "$0") --help')" ;; +esac diff --git a/src/osx/Installer.Mac/entitlements.xml b/build/macos/entitlements.xml similarity index 100% rename from src/osx/Installer.Mac/entitlements.xml rename to build/macos/entitlements.xml diff --git a/src/osx/Installer.Mac/distribution.arm64.xml b/build/macos/installer/distribution.arm64.xml similarity index 100% rename from src/osx/Installer.Mac/distribution.arm64.xml rename to build/macos/installer/distribution.arm64.xml diff --git a/src/osx/Installer.Mac/distribution.x64.xml b/build/macos/installer/distribution.x64.xml similarity index 100% rename from src/osx/Installer.Mac/distribution.x64.xml rename to build/macos/installer/distribution.x64.xml diff --git a/src/osx/Installer.Mac/resources/background.png b/build/macos/installer/resources/background.png similarity index 100% rename from src/osx/Installer.Mac/resources/background.png rename to build/macos/installer/resources/background.png diff --git a/src/osx/Installer.Mac/resources/en.lproj/LICENSE b/build/macos/installer/resources/en.lproj/LICENSE similarity index 100% rename from src/osx/Installer.Mac/resources/en.lproj/LICENSE rename to build/macos/installer/resources/en.lproj/LICENSE diff --git a/src/osx/Installer.Mac/resources/en.lproj/conclusion.html b/build/macos/installer/resources/en.lproj/conclusion.html similarity index 100% rename from src/osx/Installer.Mac/resources/en.lproj/conclusion.html rename to build/macos/installer/resources/en.lproj/conclusion.html diff --git a/src/osx/Installer.Mac/resources/en.lproj/welcome.html b/build/macos/installer/resources/en.lproj/welcome.html similarity index 100% rename from src/osx/Installer.Mac/resources/en.lproj/welcome.html rename to build/macos/installer/resources/en.lproj/welcome.html diff --git a/src/osx/Installer.Mac/scripts/postinstall b/build/macos/installer/scripts/postinstall similarity index 100% rename from src/osx/Installer.Mac/scripts/postinstall rename to build/macos/installer/scripts/postinstall diff --git a/src/osx/Installer.Mac/uninstall.sh b/build/macos/installer/uninstall.sh similarity index 100% rename from src/osx/Installer.Mac/uninstall.sh rename to build/macos/installer/uninstall.sh diff --git a/build/macos/pack.sh b/build/macos/pack.sh new file mode 100755 index 0000000000..1b14a5d3b3 --- /dev/null +++ b/build/macos/pack.sh @@ -0,0 +1,172 @@ +#!/bin/bash +# +# Builds the macOS installer (.pkg) for Git Credential Manager: stages a private +# copy of the published application, adds the uninstaller, builds a component +# package with pkgbuild, then wraps it in the final product (distribution) +# package with productbuild. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration: Debug or Release. + (default: Release) + -r, --runtime Target runtime identifier: 'osx-x64' or 'osx-arm64'. + (default: auto-detected from the host architecture) + --version Version to stamp into the package. + (default: the repository VERSION file) + --bindir Directory of published binaries to package. + (default: the application's default publish dir) + --output Directory to write the package to. + (default: out/package/) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") + $(basename "$0") --configuration Release --runtime osx-arm64 --version 2.6.1 +EOF +} + +# Package identifiers and install location +COMPONENT_IDENTIFIER="com.microsoft.gitcredentialmanager" +PRODUCT_IDENTIFIER="com.microsoft.gitcredentialmanager.dist" +INSTALL_LOCATION="/usr/local/share/gcm-core" + +# Defaults +CONFIGURATION="" +RUNTIME="" +VERSION="" +BINDIR="" +OUTPUT="" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) + print_usage + exit 0 + ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + -r|--runtime) require_value "$@"; RUNTIME="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + --bindir) require_value "$@"; BINDIR="$2"; shift 2 ;; + --output) require_value "$@"; OUTPUT="$2"; shift 2 ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalise arguments / apply defaults. +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +RUNTIME="$(normalize_runtime "$RUNTIME")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +# Installer package assets (distribution definitions, resources, scripts, the +# uninstaller) live in the sibling installer/ directory. +INSTALLER_SRC="$THISDIR/installer" +RESXPATH="$INSTALLER_SRC/resources" + +# Source of the published binaries to package (override with --bindir; defaults +# to the application's default artifacts publish directory, from layout.sh). +if [ -n "$BINDIR" ]; then + BINDIR="$(make_absolute "$BINDIR")" +else + BINDIR="$(publish_dir git-credential-manager "$CONFIGURATION" "$RUNTIME")" || exit 1 +fi + +# Private scratch (the staged payload copy and the intermediate component +# package), kept per runtime under the artifacts bin directory so that builds +# for different runtimes don't collide. +SCRATCH="$(bin_dir Mac.Distribution "$CONFIGURATION" "$RUNTIME")" || exit 1 +PAYLOAD="$SCRATCH/payload" +COMPONENTDIR="$SCRATCH/components" +COMPONENTOUT="$COMPONENTDIR/$COMPONENT_IDENTIFIER.component.pkg" + +# Destination directory for the package (override with --output; defaults to the +# top-level artifacts package directory). The file name is always defined here. +if [ -n "$OUTPUT" ]; then + OUTDIR="$(make_absolute "$OUTPUT")" +else + OUTDIR="$(package_dir "$CONFIGURATION")" || exit 1 +fi +DISTOUT="$OUTDIR/gcm-$RUNTIME-$VERSION.pkg" + +# Select the distribution definition for the target runtime. +if [ "$RUNTIME" = "osx-x64" ]; then + DISTPATH="$INSTALLER_SRC/distribution.x64.xml" +else + DISTPATH="$INSTALLER_SRC/distribution.arm64.xml" +fi + +# Pre-execution checks +[ -d "$BINDIR" ] || die "Publish directory '$BINDIR' not found. Did you publish first?" + +verbose "configuration: $CONFIGURATION" +verbose "runtime: $RUNTIME" +verbose "version: $VERSION" +verbose "bin dir: $BINDIR" +verbose "scratch: $SCRATCH" +verbose "distribution: $DISTPATH" +verbose "output: $DISTOUT" + +# Stage a fresh, private copy of the published application +info "Staging payload from '$BINDIR'..." +rm -rf "$PAYLOAD" +ditto "$BINDIR" "$PAYLOAD" || die "Failed to stage payload" + +# Add the uninstaller script +info "Copying uninstall script..." +cp "$INSTALLER_SRC/uninstall.sh" "$PAYLOAD" || die "Failed to copy uninstall script" + +# Remove any unwanted .DS_Store files +find "$PAYLOAD" -type f -name '.DS_Store' -delete || die "Failed to remove .DS_Store files" + +# Normalise permissions and clear extended attributes +info "Setting file permissions..." +/bin/chmod -R 755 "$PAYLOAD" || die "Failed to set payload permissions" +/usr/bin/xattr -rc "$PAYLOAD" || die "Failed to remove extended attributes" + +# Build the intermediate component package +info "Building app component package..." +rm -f "$COMPONENTOUT" +mkdir -p "$COMPONENTDIR" +/usr/bin/pkgbuild \ + --root "$PAYLOAD/" \ + --install-location "$INSTALL_LOCATION" \ + --scripts "$INSTALLER_SRC/scripts" \ + --identifier "$COMPONENT_IDENTIFIER" \ + --version "$VERSION" \ + "$COMPONENTOUT" || die "Failed to build app component package" + +# Build the final product (distribution) package +info "Building product package..." +rm -f "$DISTOUT" +mkdir -p "$(dirname "$DISTOUT")" +/usr/bin/productbuild \ + --package-path "$COMPONENTDIR" \ + --resources "$RESXPATH" \ + --distribution "$DISTPATH" \ + --identifier "$PRODUCT_IDENTIFIER" \ + --version "$VERSION" \ + "$DISTOUT" || die "Failed to build product package" + +info "Packaging complete: $DISTOUT" diff --git a/build/macos/publish.sh b/build/macos/publish.sh new file mode 100755 index 0000000000..5e764f0bf6 --- /dev/null +++ b/build/macos/publish.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# +# Publishes the Git Credential Manager application for the macOS installer. +# +set -euo pipefail + +THISDIR="$( cd "$(dirname "$0")" ; pwd -P )" +. "$THISDIR/../lib-cli.sh" + +print_usage () { + cat < Build configuration to publish: Debug or Release. + (default: Release) + -r, --runtime Target runtime identifier: 'osx-x64' or 'osx-arm64'. + (default: auto-detected from the host architecture) + -o, --output Directory to publish the application to. + (default: the project's default artifacts dir) + --symbol-output Directory to move debug symbols (.pdb/.dSYM) into. + (default: .sym) + --version Version to stamp into the published binaries. + (default: the repository VERSION file) + --aot / --no-aot Publish a native ahead-of-time (AOT) build, or a + trimmed, self-contained non-AOT build instead. + (default: --aot) + -v, --verbose Enable verbose output. (default: off) + -h, --help Show this help text and exit. + +Examples: + $(basename "$0") + $(basename "$0") --configuration Release --runtime osx-arm64 +EOF +} + +# Defaults +CONFIGURATION="" +RUNTIME="" +OUTPUT="" +SYMBOL_OUTPUT="" +VERSION="" +AOT="true" + +# Parse arguments. +while [ "$#" -gt 0 ]; do + case "$1" in + -h|--help) + print_usage + exit 0 + ;; + -v|--verbose) enable_verbose; shift ;; + -c|--configuration) require_value "$@"; CONFIGURATION="$2"; shift 2 ;; + -r|--runtime) require_value "$@"; RUNTIME="$2"; shift 2 ;; + -o|--output) require_value "$@"; OUTPUT="$2"; shift 2 ;; + --symbol-output) require_value "$@"; SYMBOL_OUTPUT="$2"; shift 2 ;; + --version) require_value "$@"; VERSION="$2"; shift 2 ;; + --aot|--no-aot) AOT="$(bool_flag "$1")"; shift ;; + --) shift; break ;; + -*) die "unknown option '$1' (try '$(basename "$0") --help')" ;; + *) die "unexpected argument '$1' (try '$(basename "$0") --help')" ;; + esac +done + +if [ "$#" -gt 0 ]; then + die "unexpected argument '$1' (try '$(basename "$0") --help')" +fi + +# Normalize arguments/use defaults if unset +CONFIGURATION="$(normalize_configuration "$CONFIGURATION")" || exit 1 +RUNTIME="$(normalize_runtime "$RUNTIME")" || exit 1 +VERSION="$(normalize_version "$VERSION")" || exit 1 + +# Directories +GCM_SRC="$(repo_root)/src/git-credential-manager" + +# Resolve the publish output directory (default: the project's artifacts publish +# directory) and the sibling directory that debug symbols are separated into. +if [ -n "$OUTPUT" ]; then + OUTDIR="$(make_absolute "$OUTPUT")" +else + OUTDIR="$(publish_dir git-credential-manager "$CONFIGURATION" "$RUNTIME")" || exit 1 +fi +if [ -n "$SYMBOL_OUTPUT" ]; then + SYMOUTDIR="$(make_absolute "$SYMBOL_OUTPUT")" +else + SYMOUTDIR="$OUTDIR.sym" +fi +[ "$SYMOUTDIR" != "$OUTDIR" ] || die "--symbol-output must differ from the publish output directory" + +verbose "configuration: $CONFIGURATION" +verbose "runtime: $RUNTIME" +verbose "version: $VERSION" +verbose "aot: $AOT" +verbose "output dir: $OUTDIR" +verbose "symbol dir: $SYMOUTDIR" + +# By default the application is published ahead-of-time (AOT) compiled, as +# configured in the project. For a non-AOT build, just turn AOT off: the +# project then enables trimming, which implies a self-contained publish, so +# --self-contained does not need to be passed explicitly. +AOT_ARGS= +if [ "$AOT" != "true" ]; then + AOT_ARGS="-p:PublishAot=false" +fi + +# Publish the application to the resolved output directory. +info "Publishing application..." +# shellcheck disable=SC2086 # AOT_ARGS is intentionally word-split (0 or 2 args). +dotnet publish "$GCM_SRC" \ + -v:normal \ + --configuration="$CONFIGURATION" \ + --runtime="$RUNTIME" \ + --output "$OUTDIR" \ + -p:VersionOverride="$VERSION" \ + $AOT_ARGS || die "Failed to publish application" + +# Separate debug symbols (managed .pdb files and native .dSYM bundles) out of the +# shipping payload into the sibling symbol directory, so the published output +# holds only the files we ship and code-sign. +info "Separating debug symbols into '$SYMOUTDIR'..." +rm -rf "$SYMOUTDIR" +mkdir -p "$SYMOUTDIR" +find "$OUTDIR" -maxdepth 1 -name '*.pdb' -exec mv {} "$SYMOUTDIR/" \; || die "Failed to move .pdb symbols" +find "$OUTDIR" -maxdepth 1 -name '*.dSYM' -exec mv {} "$SYMOUTDIR/" \; || die "Failed to move .dSYM bundles" + +info "Publish complete." diff --git a/build/GenerateWindowsAppManifest.cs b/build/msbuild/GenerateWindowsAppManifest.cs similarity index 100% rename from build/GenerateWindowsAppManifest.cs rename to build/msbuild/GenerateWindowsAppManifest.cs diff --git a/build/GetVersion.cs b/build/msbuild/GetVersion.cs similarity index 64% rename from build/GetVersion.cs rename to build/msbuild/GetVersion.cs index 0078c4c125..1e8b4c48a6 100644 --- a/build/GetVersion.cs +++ b/build/msbuild/GetVersion.cs @@ -9,6 +9,11 @@ public class GetVersion : Task [Required] public string VersionFile { get; set; } + // Optional. When set (non-empty), this value is used as the version + // instead of reading the VersionFile, letting callers override the + // version, e.g. 'dotnet publish -p:VersionOverride=2.6.1'. + public string VersionOverride { get; set; } + [Output] public string Version { get; set; } @@ -20,8 +25,17 @@ public class GetVersion : Task public override bool Execute() { - Log.LogMessage(MessageImportance.Normal, "Reading VERSION file..."); - string textVersion = File.ReadAllText(VersionFile); + string textVersion; + if (!string.IsNullOrWhiteSpace(VersionOverride)) + { + Log.LogMessage(MessageImportance.Normal, "Using override version '{0}'...", VersionOverride); + textVersion = VersionOverride; + } + else + { + Log.LogMessage(MessageImportance.Normal, "Reading VERSION file..."); + textVersion = File.ReadAllText(VersionFile); + } if (!System.Version.TryParse(textVersion, out System.Version fullVersion)) { diff --git a/build/GCM.MSBuild.csproj b/build/msbuild/MSBuildTasks.csproj similarity index 85% rename from build/GCM.MSBuild.csproj rename to build/msbuild/MSBuildTasks.csproj index bd25f83459..456bc64b62 100644 --- a/build/GCM.MSBuild.csproj +++ b/build/msbuild/MSBuildTasks.csproj @@ -3,6 +3,7 @@ net10.0 false + GitCredentialManager.MSBuild diff --git a/build/GCM.tasks b/build/msbuild/gcm.tasks similarity index 100% rename from build/GCM.tasks rename to build/msbuild/gcm.tasks diff --git a/build/windows/Windows.Distribution.csproj b/build/windows/Windows.Distribution.csproj new file mode 100644 index 0000000000..e7dc4f2f6c --- /dev/null +++ b/build/windows/Windows.Distribution.csproj @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/build/windows/archive.ps1 b/build/windows/archive.ps1 new file mode 100644 index 0000000000..3cd8276739 --- /dev/null +++ b/build/windows/archive.ps1 @@ -0,0 +1,87 @@ +<# +.SYNOPSIS + Archives the published Git Credential Manager application into .zip files. + +.DESCRIPTION + Creates distributable archives of the published application: a payload + archive of the shipping binaries and a separate symbols archive, written to + the top-level artifacts package directory. +#> +[CmdletBinding()] +param ( + [Alias('c')] [string] $Configuration, + [Alias('r')] [string] $Runtime, + [string] $Version, + [string] $BinDir, + [string] $SymbolDir, + [string] $Output +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$InformationPreference = 'Continue' + +Import-Module "$PSScriptRoot/../lib-cli.psm1" -Force + +# Normalise arguments / apply defaults. +$Configuration = Resolve-Configuration $Configuration +$Runtime = Resolve-Runtime $Runtime +$Version = Resolve-Version $Version + +# Source of the published binaries to archive (override with -BinDir; defaults to +# the application's default artifacts publish directory, from publish.ps1). +if ($BinDir) { + $BinDir = Get-AbsolutePath $BinDir +} else { + $BinDir = Get-PublishDir -Project 'git-credential-manager' -Configuration $Configuration -Runtime $Runtime +} + +# Source of the debug symbols to archive (override with -SymbolDir; defaults to +# the sibling symbol directory produced by publish.ps1). +if ($SymbolDir) { + $SymbolDir = Get-AbsolutePath $SymbolDir +} else { + $SymbolDir = "$BinDir.sym" +} + +# Destination directory for the archives. +if ($Output) { + $OutDir = Get-AbsolutePath $Output +} else { + $OutDir = Get-PackageDir -Configuration $Configuration +} + +$PayloadZip = Join-Path $OutDir "gcm-$Runtime-$Version.zip" +$SymbolsZip = Join-Path $OutDir "gcm-$Runtime-$Version-symbols.zip" + +Write-Verbose "configuration: $Configuration" +Write-Verbose "runtime: $Runtime" +Write-Verbose "version: $Version" +Write-Verbose "bin dir: $BinDir" +Write-Verbose "symbol dir: $SymbolDir" +Write-Verbose "output dir: $OutDir" + +# Pre-execution checks +if (-not (Test-Path -LiteralPath $BinDir)) { + Write-Error "Binaries directory '$BinDir' not found. Did you publish first?" +} + +New-Item -ItemType Directory -Path $OutDir -Force | Out-Null + +# Archive the shipping binaries. +Write-Information "Archiving binaries from '$BinDir'..." +if (Test-Path -LiteralPath $PayloadZip) { Remove-Item -Force -LiteralPath $PayloadZip } +Compress-Archive -Path "$BinDir/*" -DestinationPath $PayloadZip +Write-Information "Created $PayloadZip" + +# Archive the debug symbols, if any were produced. +if ((Test-Path -LiteralPath $SymbolDir) -and (Get-ChildItem -LiteralPath $SymbolDir -Force)) { + Write-Information "Archiving symbols from '$SymbolDir'..." + if (Test-Path -LiteralPath $SymbolsZip) { Remove-Item -Force -LiteralPath $SymbolsZip } + Compress-Archive -Path "$SymbolDir/*" -DestinationPath $SymbolsZip + Write-Information "Created $SymbolsZip" +} else { + Write-Warning "symbols directory '$SymbolDir' not found or empty; skipping symbols archive" +} + +Write-Information "Archiving complete." diff --git a/build/windows/build.ps1 b/build/windows/build.ps1 new file mode 100644 index 0000000000..0279b7dfca --- /dev/null +++ b/build/windows/build.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Builds the Windows distributables for Git Credential Manager. + +.DESCRIPTION + Runs the publish, pack and archive steps in sequence to produce the system + and user installer packages (.exe) together with the binary and symbol + archives (.zip), under the top-level artifacts package directory. + +.PARAMETER InnoSetup + Path to the Inno Setup compiler (ISCC.exe). MSBuild callers pass the + Tools.InnoSetup package's $(InnoSetupCompiler); when running this script + directly, either put iscc on PATH or pass this explicitly. +#> +[CmdletBinding()] +param ( + [Alias('c')] [string] $Configuration, + [string] $Version, + [Alias('r')] [string] $Runtime, + [string] $InnoSetup +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$InformationPreference = 'Continue' + +Import-Module "$PSScriptRoot/../lib-cli.psm1" -Force + +# Normalise arguments / apply defaults. +$Configuration = Resolve-Configuration $Configuration +$Runtime = Resolve-Runtime $Runtime +$Version = Resolve-Version $Version + +# Thread the verbose preference through to the child scripts. +$ChildVerbose = ($VerbosePreference -eq 'Continue') + +# Invoke a child build script and propagate a non-zero exit code. +function Invoke-Step { + param( + [Parameter(Mandatory)] [string] $Script, + [Parameter(Mandatory)] [hashtable] $Arguments + ) + & (Join-Path $PSScriptRoot $Script) @Arguments -Verbose:$ChildVerbose + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} + +Write-Information "Building Windows distribution..." +Write-Verbose "configuration: $Configuration" +Write-Verbose "runtime: $Runtime" +Write-Verbose "version: $Version" + +# Publish the application. +Invoke-Step 'publish.ps1' @{ Configuration = $Configuration; Runtime = $Runtime; Version = $Version } + +# Build the installer packages (.exe). +Invoke-Step 'pack.ps1' @{ Configuration = $Configuration; Runtime = $Runtime; InnoSetup = $InnoSetup } + +# Create the binary and symbol archives (.zip). +Invoke-Step 'archive.ps1' @{ Configuration = $Configuration; Runtime = $Runtime; Version = $Version } + +Write-Information "Windows distribution build complete." diff --git a/src/windows/Installer.Windows/Setup.iss b/build/windows/installer/Setup.iss similarity index 90% rename from src/windows/Installer.Windows/Setup.iss rename to build/windows/installer/Setup.iss index c15efe6d82..ab5eb86fa4 100644 --- a/src/windows/Installer.Windows/Setup.iss +++ b/build/windows/installer/Setup.iss @@ -15,8 +15,8 @@ #error Installer target property 'InstallTarget' must be specifed #endif -#ifndef GcmRuntimeIdentifier - #error GCM Runtime Identifier 'GcmRuntimeIdentifier' must be specifed (e.g. win-x64) +#ifndef RuntimeIdentifier + #error Runtime Identifier 'RuntimeIdentifier' must be specifed (e.g. win-x64) #endif #if InstallTarget == "user" @@ -71,16 +71,16 @@ AppContact={#GcmUrl} AppCopyright={#GcmCopyright} AppReadmeFile={#GcmReadme} ; Windows ARM64 supports installing and running x64 binaries, but not vice versa. -#if GcmRuntimeIdentifier=="win-x64" -ArchitecturesAllowed=x64compatible -ArchitecturesInstallIn64BitMode=x64compatible -#elif GcmRuntimeIdentifier=="win-arm64" -ArchitecturesAllowed=arm64 -ArchitecturesInstallIn64BitMode=arm64 +#if RuntimeIdentifier=="win-x64" + ArchitecturesAllowed=x64compatible + ArchitecturesInstallIn64BitMode=x64compatible +#elif RuntimeIdentifier=="win-arm64" + ArchitecturesAllowed=arm64 + ArchitecturesInstallIn64BitMode=arm64 #endif VersionInfoVersion={#GcmVersion} LicenseFile={#GcmRepoRoot}\LICENSE -OutputBaseFilename={#GcmSetupExe}-{#GcmRuntimeIdentifier}-{#GcmVersionSimple} +OutputBaseFilename={#GcmSetupExe}-{#RuntimeIdentifier}-{#GcmVersionSimple} DefaultDirName={autopf}\{#GcmShortName} Compression=lzma2 SolidCompression=yes diff --git a/build/windows/pack.ps1 b/build/windows/pack.ps1 new file mode 100644 index 0000000000..4b97b6f88d --- /dev/null +++ b/build/windows/pack.ps1 @@ -0,0 +1,88 @@ +<# +.SYNOPSIS + Builds the Windows installer packages (.exe) for Git Credential Manager. + +.DESCRIPTION + Compiles the Inno Setup script (installer/Setup.iss) with the Inno Setup + command-line compiler (ISCC) to produce the system and user installer + executables. The installer version is derived by Inno Setup from the + published git-credential-manager.exe itself. +#> +[CmdletBinding()] +param ( + [Alias('c')] [string] $Configuration, + [Alias('r')] [string] $Runtime, + [string] $BinDir, + [string] $Output, + # Path to the Inno Setup compiler (ISCC.exe). MSBuild callers pass the + # Tools.InnoSetup package's $(InnoSetupCompiler); direct callers may omit + # this if iscc is on PATH. + [string] $InnoSetup +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$InformationPreference = 'Continue' + +Import-Module "$PSScriptRoot/../lib-cli.psm1" -Force + +# Normalise arguments / apply defaults. +$Configuration = Resolve-Configuration $Configuration +$Runtime = Resolve-Runtime $Runtime +$Iscc = Resolve-InnoSetup $InnoSetup + +# The Inno Setup script lives in the sibling installer/ directory. +$SetupIss = Join-Path $PSScriptRoot 'installer' 'Setup.iss' + +# Source of the published binaries to package (override with -BinDir; defaults to +# the application's default artifacts publish directory, from publish.ps1). +if ($BinDir) { + $BinDir = Get-AbsolutePath $BinDir +} else { + $BinDir = Get-PublishDir -Project 'git-credential-manager' -Configuration $Configuration -Runtime $Runtime +} + +# Destination directory for the packages (override with -Output; defaults to the +# top-level artifacts package directory). Inno Setup defines each file name. +if ($Output) { + $OutDir = Get-AbsolutePath $Output +} else { + $OutDir = Get-PackageDir -Configuration $Configuration +} + +Write-Verbose "configuration: $Configuration" +Write-Verbose "runtime: $Runtime" +Write-Verbose "iscc: $Iscc" +Write-Verbose "bin dir: $BinDir" +Write-Verbose "output dir: $OutDir" + +# Pre-execution checks +if (-not (Test-Path -LiteralPath $BinDir)) { + Write-Error "Payload directory '$BinDir' not found. Did you publish first?" +} +if (-not (Test-Path -LiteralPath $SetupIss)) { + Write-Error "Inno Setup script '$SetupIss' not found" +} + +New-Item -ItemType Directory -Path $OutDir -Force | Out-Null + +# Compile the Inno Setup script for a given install target ('system' or 'user'). +function Build-Installer { + param([Parameter(Mandatory)] [string] $InstallTarget) + + Write-Information "Building $InstallTarget installer package..." + & $Iscc ` + "/O$OutDir" ` + "/DPayloadDir=$BinDir" ` + "/DInstallTarget=$InstallTarget" ` + "/DRuntimeIdentifier=$Runtime" ` + $SetupIss + if ($LASTEXITCODE -ne 0) { + Write-Error "Inno Setup failed for the $InstallTarget installer (ISCC exited $LASTEXITCODE)" + } +} + +Build-Installer -InstallTarget 'system' +Build-Installer -InstallTarget 'user' + +Write-Information "Packaging complete: $OutDir" diff --git a/build/windows/publish.ps1 b/build/windows/publish.ps1 new file mode 100644 index 0000000000..5035864486 --- /dev/null +++ b/build/windows/publish.ps1 @@ -0,0 +1,140 @@ +<# +.SYNOPSIS + Publishes the Git Credential Manager application for the Windows installer. + +.DESCRIPTION + Publishes git-credential-manager for the target runtime, removes files that + are not shipped on Windows, and moves debug symbols (.pdb files) out of the + published output into a sibling symbol directory. + + By default this is a native ahead-of-time (AOT) build; pass -Aot:$false for a + trimmed, self-contained non-AOT build instead. +#> +[CmdletBinding()] +param ( + [Alias('c')] [string] $Configuration, + [Alias('r')] [string] $Runtime, + [Alias('o')] [string] $Output, + [string] $SymbolOutput, + [string] $Version, + [bool] $Aot = $true +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$InformationPreference = 'Continue' + +Import-Module "$PSScriptRoot/../lib-cli.psm1" -Force + +# Normalise arguments / apply defaults. +$Configuration = Resolve-Configuration $Configuration +$Runtime = Resolve-Runtime $Runtime +$Version = Resolve-Version $Version + +# Directories +$GcmSrc = Join-Path (Get-RepoRoot) 'src' 'git-credential-manager' + +# Resolve the publish output directory (default: the project's artifacts publish +# directory) and the sibling directory that debug symbols are separated into. +if ($Output) { + $OutDir = Get-AbsolutePath $Output +} else { + $OutDir = Get-PublishDir -Project 'git-credential-manager' -Configuration $Configuration -Runtime $Runtime +} +if ($SymbolOutput) { + $SymOutDir = Get-AbsolutePath $SymbolOutput +} else { + $SymOutDir = "$OutDir.sym" +} +if ($SymOutDir -eq $OutDir) { + Write-Error "-SymbolOutput must differ from the publish output directory" +} + +Write-Verbose "configuration: $Configuration" +Write-Verbose "runtime: $Runtime" +Write-Verbose "version: $Version" +Write-Verbose "output dir: $OutDir" +Write-Verbose "symbol dir: $SymOutDir" + +# Clean any existing output and symbol directories. +foreach ($dir in @($OutDir, $SymOutDir)) { + if (Test-Path -LiteralPath $dir) { + Write-Verbose "Cleaning existing directory '$dir'..." + Remove-Item -Recurse -Force -LiteralPath $dir + } +} +New-Item -ItemType Directory -Path $OutDir, $SymOutDir -Force | Out-Null + +# By default the application is published ahead-of-time (AOT) compiled, as +# configured in the project. For a non-AOT build, just turn AOT off: the +# project then enables trimming, which implies a self-contained publish, so +# --self-contained does not need to be passed explicitly. +$aotArgs = @() +if (-not $Aot) { + $aotArgs = @('-p:PublishAot=false') +} + +# Publish the application to the resolved output directory. +Write-Information "Publishing application..." +dotnet publish "$GcmSrc" ` + -v:normal ` + --configuration $Configuration ` + --runtime $Runtime ` + --output $OutDir ` + -p:VersionOverride=$Version ` + @aotArgs +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to publish application (dotnet publish exited $LASTEXITCODE)" +} + +# Remove files that are published but not shipped on Windows: non-Windows native +# libraries, and the native libraries for the runtimes we are not building. +Write-Information "Removing files not shipped on Windows..." +Remove-Item -Path "$OutDir/*.dylib" -Force -ErrorAction Ignore +Remove-Item -Path "$OutDir/musl-x64/" -Recurse -Force -ErrorAction Ignore + +switch ($Runtime) { + 'win-x86' { + Remove-Item -Path "$OutDir/arm/", "$OutDir/arm64/", "$OutDir/x64/" -Recurse -Force -ErrorAction Ignore + Remove-Item -Path "$OutDir/runtimes/win-arm64/", "$OutDir/runtimes/win-x64/" -Recurse -Force -ErrorAction Ignore + # The Avalonia and MSAL binaries are already included in $OutDir directly. + Remove-Item -Path "$OutDir/x86/libSkiaSharp.dll", "$OutDir/x86/libHarfBuzzSharp.dll" -Force -ErrorAction Ignore + Remove-Item -Path "$OutDir/runtimes/win-x86/native/msalruntime_x86.dll" -Force -ErrorAction Ignore + } + 'win-x64' { + Remove-Item -Path "$OutDir/arm/", "$OutDir/arm64/", "$OutDir/x86/" -Recurse -Force -ErrorAction Ignore + Remove-Item -Path "$OutDir/runtimes/win-arm64/", "$OutDir/runtimes/win-x86/" -Recurse -Force -ErrorAction Ignore + # The Avalonia and MSAL binaries are already included in $OutDir directly. + Remove-Item -Path "$OutDir/x64/libSkiaSharp.dll", "$OutDir/x64/libHarfBuzzSharp.dll" -Force -ErrorAction Ignore + Remove-Item -Path "$OutDir/x64/libSkiaSharp.so", "$OutDir/x64/libHarfBuzzSharp.so" -Force -ErrorAction Ignore + Remove-Item -Path "$OutDir/runtimes/win-x64/native/msalruntime.dll" -Force -ErrorAction Ignore + } + 'win-arm64' { + Remove-Item -Path "$OutDir/arm/", "$OutDir/x86/", "$OutDir/x64/" -Recurse -Force -ErrorAction Ignore + Remove-Item -Path "$OutDir/runtimes/win-x86/", "$OutDir/runtimes/win-x64/" -Recurse -Force -ErrorAction Ignore + # The Avalonia and MSAL binaries are already included in $OutDir directly. + Remove-Item -Path "$OutDir/arm64/libSkiaSharp.dll", "$OutDir/arm64/libHarfBuzzSharp.dll" -Force -ErrorAction Ignore + Remove-Item -Path "$OutDir/arm64/libSkiaSharp.so", "$OutDir/arm64/libHarfBuzzSharp.so" -Force -ErrorAction Ignore + Remove-Item -Path "$OutDir/runtimes/win-arm64/native/msalruntime_arm64.dll" -Force -ErrorAction Ignore + } +} + +# Remove localized resource assemblies (the core GCM assembly is not localized). +Get-ChildItem -Path $OutDir -Recurse -Include '*.resources.dll' | + Remove-Item -Force -ErrorAction Ignore + +# Remove any now-empty directories. +Get-ChildItem -Path $OutDir -Recurse -Directory | + Sort-Object -Property FullName -Descending | + Where-Object { -not (Get-ChildItem -LiteralPath $_.FullName -File -Recurse) } | + Remove-Item -Force -ErrorAction Ignore + +# Separate debug symbols out of the shipping payload into the sibling symbol +# directory, so the published output holds only the files we ship. +Write-Information "Separating debug symbols into '$SymOutDir'..." +$pdbs = Get-ChildItem -Path $OutDir -Filter '*.pdb' -File -ErrorAction Ignore +if ($pdbs) { + $pdbs | Move-Item -Destination $SymOutDir -Force +} + +Write-Information "Publish complete." diff --git a/docs/development.md b/docs/development.md index 9eedda44a1..1c36eff4e6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -11,62 +11,131 @@ installed from the [.NET website][dotnet-web]. ## Building -The `Git-Credential-Manager.sln` solution can be opened and built in Visual -Studio, Visual Studio for Mac, Visual Studio Code, or JetBrains Rider. +The `git-credential-manager.slnx` solution can be opened and built in Visual +Studio, Visual Studio Code, or JetBrains Rider. -### macOS +Each platform's distributables (installers, packages, and archives) are produced +by building the matching project under `build/`, as shown below. These commands +mirror what CI runs. -To build from inside an IDE, make sure to select the `MacDebug` or `MacRelease` -solution configurations. +### macOS -To build from the command line, run: +To build the macOS distribution from the command line, run: ```shell -dotnet build -c MacDebug +dotnet build build/macos --configuration=Debug --runtime=osx-arm64 ``` -You can find a copy of the installer .pkg file in `out/osx/Installer.Mac/pkg/Debug`. - -The flat binaries can also be found in `out/osx/Installer.Mac/pkg/Debug/payload`. +Use `osx-x64` or `osx-arm64` for the `--runtime`, or omit it to build for the +host architecture. + +The installer package (`.pkg`) and the binary and symbol archives (`.tar.gz`) +are written to `out/package/debug` (`out/package/release` for a Release build). + +The flat binaries can also be found in +`out/publish/git-credential-manager/debug_osx-arm64`. + +> [!NOTE] +> **Building with Homebrew's .NET SDK** +> +> If your `dotnet` comes from Homebrew (`brew install dotnet`), building the +> macOS distribution fails during the Native AOT link step with an error like: +> +> ```text +> ld: library 'ssl' not found +> clang: error: linker command failed with exit code 1 +> ``` +> +> Homebrew's .NET is a _non-portable_ build: its Native AOT runtime pack ships a +> `nonportable.txt` marker that makes the linker reference `-lssl -lcrypto` (and +> brotli) directly, instead of the portable `dlopen` path macOS normally uses. +> Those Homebrew libraries aren't on the default linker search path, so the link +> fails. See [dotnet/runtime#120440][dotnet-runtime-120440] for details. The +> official .NET SDK is a portable build and is _not_ affected (it's also what CI +> uses). +> +> **Recommended:** use the official .NET SDK instead of Homebrew's. For example, +> install it side-by-side and put it ahead of Homebrew on your `PATH`: +> +> ```shell +> curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0 --install-dir ~/.dotnet +> export PATH="$HOME/.dotnet:$PATH" +> ``` +> +> This also matters for distributable builds: a binary linked by Homebrew's +> `dotnet` gains a hard dependency on the Homebrew OpenSSL dylibs and shouldn't +> be shipped. +> +> **Workaround:** to keep using Homebrew's `dotnet`, add the Homebrew OpenSSL and +> brotli library directories to the linker search path before building: +> +> ```shell +> export LIBRARY_PATH="$(brew --prefix openssl@3)/lib:$(brew --prefix brotli)/lib:$LIBRARY_PATH" +> ``` ### Windows -To build from inside an IDE, make sure to select the `WindowsDebug` or -`WindowsRelease` solution configurations. - -To build from the command line, run: +To build the Windows distribution from the command line, run: ```powershell -dotnet build -c WindowsDebug +dotnet build build\windows --configuration=Debug --runtime=win-x64 ``` -You can find a copy of the installer .exe file in `out\windows\Installer.Windows\bin\Debug\net472`. +Use `win-x64`, `win-x86`, or `win-arm64` for the `--runtime`, or omit it to +build for the host architecture. -The flat binaries can also be found in `out\windows\Payload.Windows\bin\Debug\net472\win-x86`. +The system and user installers (`gcm--.exe` and +`gcmuser--.exe`) and the binary archive (`.zip`) are written to +`out\package\debug` (`out\package\release` for a Release build). -### Linux +The flat binaries can also be found in +`out\publish\git-credential-manager\debug_win-x64`. -The two available solution configurations are `LinuxDebug` and `LinuxRelease`. +### Linux -To build from the command line, run: +To build the Linux distribution from the command line, run: ```shell -dotnet build -c LinuxDebug +dotnet build build/linux --configuration=Debug --runtime=linux-x64 ``` -If you want to build for a specific architecture, you can provide `linux-x64` or `linux-arm64` or `linux-arm` as the runtime: +Use `linux-x64`, `linux-arm64`, or `linux-arm` for the `--runtime`, or omit it +to build for the host architecture. + +The Debian package (`.deb`) and the binary and symbol archives (`.tar.gz`) are +written to `out/package/debug` (`out/package/release` for a Release build). + +The flat binaries can also be found in +`out/publish/git-credential-manager/debug_linux-x64`. + +### .NET tool + +The .NET tool NuGet package is platform-agnostic. Build it the way CI does, +through the distribution project, which publishes the product as portable IL and +packs it, stamping the version from the `VERSION` file: ```shell -dotnet build -c LinuxDebug -r linux-arm64 +dotnet build build/dntool --configuration=Debug ``` -You can find a copy of the Debian package (.deb) file in `out/linux/Packaging.Linux/deb/Debug`. +The package metadata and file layout live in +`build/dntool/Dntool.Distribution.csproj`; packing is just `dotnet pack` over +that project, so no `nuget.exe` and no hand-authored `.nuspec` are needed. The +`.nupkg` is written to `out/package/debug` (`out/package/release` for a Release +build). -The flat binaries can also be found in `out/linux/Packaging.Linux/payload/Debug`. +To try the freshly built tool without affecting your global tools, install it +into an isolated tool path: + +```shell +dotnet tool install --tool-path /tmp/gcm-tool \ + --add-source out/package/debug git-credential-manager +/tmp/gcm-tool/git-credential-manager --version +``` ## Debugging -To debug from inside an IDE you'll want to set `Git-Credential-Manager` as the +To debug from inside an IDE you'll want to set `git-credential-manager` as the startup project, and specify one of `get`, `store`, or `erase` as a program argument. @@ -117,9 +186,15 @@ error, or to an absolute file path to write trace information to a file. For example: ```shell -$ GCM_TRACE=1 git-credential-manager version -> 18:47:56.526712 ...er/Application.cs:69 trace: [RunInternalAsync] Git Credential Manager version 2.0.124-beta+e1ebbe1517 (macOS, .NET 5.0) 'version' -> Git Credential Manager version 2.0.124-beta+e1ebbe1517 (macOS, .NET 5.0) +GCM_TRACE=1 out/publish/git-credential-manager/debug_osx-arm64/git-credential-manager --version +16:26:50.667032 ...e/Application.cs:107 trace: [RunInternalAsync] Version: 3.0.0 +16:26:50.667295 ...e/Application.cs:108 trace: [RunInternalAsync] Runtime: .NET 10.0.9 +16:26:50.667303 ...e/Application.cs:109 trace: [RunInternalAsync] Platform: macOS (ARM64) +16:26:50.667310 ...e/Application.cs:110 trace: [RunInternalAsync] OSVersion: 26.5.1 +16:26:50.667316 ...e/Application.cs:111 trace: [RunInternalAsync] AppPath: /Users/user1/src/gcm/out/publish/git-credential-manager/debug_osx-arm64/git-credential-manager +16:26:50.667323 ...e/Application.cs:112 trace: [RunInternalAsync] InstallDir: /Users/user1/src/gcm/out/publish/git-credential-manager/debug_osx-arm64/ +16:26:50.667330 ...e/Application.cs:113 trace: [RunInternalAsync] Arguments: --version +3.0.0+a9ecd9c6e31bbc5cb44c530edfd88a8784de5fb0 ``` #### Git's Trace2 API @@ -247,6 +322,7 @@ Documents are checked for link validity using [lychee][lychee]. Lychee can be installed in a variety of ways depending on your platform, see the [docs on GitHub][lychee-docs]. Some URLs are ignored by lychee, per the [lycheeignore][lycheeignore]. +[dotnet-runtime-120440]: https://github.com/dotnet/runtime/issues/120440 [dotnet-web]: https://dotnet.microsoft.com/ [custom-helpers]: https://git-scm.com/docs/gitcredentials#_custom_helpers [ioformat]: https://git-scm.com/docs/git-credential#IOFMT diff --git a/docs/faq.md b/docs/faq.md index 3cc37cddc5..e5bf36680d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -299,6 +299,6 @@ is not removed and is left as `manager`, the default set by Git for Windows. [helper-config-docs]: https://git-scm.com/docs/gitcredentials#Documentation/gitcredentials.txt-helper [multiple-users]: multiple-users.md [netconfig-http-proxy]: netconfig.md#http-proxy -[linux-uninstall-from-src]: ./linux-fromsrc-uninstall.md +[linux-uninstall-from-src]: ./fromsrc-uninstall.md [windows-server-tls]: https://docs.microsoft.com/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/dn786418(v=ws.11)#tls-12 [wsl]: wsl.md diff --git a/docs/linux-fromsrc-uninstall.md b/docs/fromsrc-uninstall.md similarity index 73% rename from docs/linux-fromsrc-uninstall.md rename to docs/fromsrc-uninstall.md index b58801c54b..9407ed312f 100644 --- a/docs/linux-fromsrc-uninstall.md +++ b/docs/fromsrc-uninstall.md @@ -1,7 +1,8 @@ # Uninstalling after installing from source These instructions will guide you in removing GCM after running the -[install from source script][install-from-source] on your Linux distribution. +[install from source script][install-from-source] on macOS or your Linux +distribution. :rotating_light: PROCEED WITH CAUTION :rotating_light: @@ -32,11 +33,28 @@ sudo rm -rf ~/git-credential-manager && sudo rm ~/install-from-source.sh ``` +## macOS + +On macOS the script does not install any system packages — the runtime libraries +GCM needs are part of the operating system — so the steps under +[All distributions](#all-distributions) are normally all that is required. + +**Note:** If the script installed the .NET SDK for you (because a supported +version was not already present), it was placed in `~/.dotnet`. To remove it: + +```console +rm -rf ~/.dotnet +``` + +If you had a pre-existing .NET installation elsewhere, or want to remove only the +version the script installed while keeping others, follow +[these instructions][uninstall-dotnet-macos] instead. + ## Debian/Ubuntu **Note:** If you had a pre-existing installation of dotnet that was not installed via `apt` or `apt-get` when you ran the install from source script, -you will need to remove it using [these instructions][uninstall-dotnet] and +you will need to remove it using [these instructions][uninstall-dotnet-linux] and remove `dotnet-*` from the below command. ```console @@ -49,7 +67,7 @@ sudo apt remove dotnet-* dpkg-dev apt-transport-https git curl wget install from source script that was not located at `~/.dotnet`, you will need to modify the first command below to point to the custom install location. If you would like to remove the specific version of dotnet that the script installed -and keep other versions, you can do so with [these instructions][uninstall-dotnet]. +and keep other versions, you can do so with [these instructions][uninstall-dotnet-linux]. ```console sudo rm -rf ~/.dotnet && @@ -62,7 +80,7 @@ sudo apt remove git curl install from source script that was not located at `~/.dotnet`, you will need to modify the first command below to point to the custom install location. If you would like to remove the specific version of dotnet that the script installed -and keep other versions, you can do so with [these instructions][uninstall-dotnet]. +and keep other versions, you can do so with [these instructions][uninstall-dotnet-linux]. ```console sudo rm -rf ~/.dotnet @@ -74,7 +92,7 @@ sudo rm -rf ~/.dotnet install from source script that was not located at `~/.dotnet`, you will need to modify the first command below to point to the custom install location. If you would like to remove the specific version of dotnet that the script installed -and keep other versions, you can do so with [these instructions][uninstall-dotnet]. +and keep other versions, you can do so with [these instructions][uninstall-dotnet-linux]. ```console sudo rm -rf ~/.dotnet && @@ -82,5 +100,6 @@ sudo apk del icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib which bash coreutils gcompat git curl ``` -[install-from-source]: ../src/linux/Packaging.Linux/install-from-source.sh -[uninstall-dotnet]: https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net +[install-from-source]: ../build/install-from-source.sh +[uninstall-dotnet-linux]: https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-linux#uninstall-net +[uninstall-dotnet-macos]: https://docs.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions?pivots=os-macos#uninstall-net diff --git a/docs/install.md b/docs/install.md index 86ead9557a..d00b3036e7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -116,6 +116,13 @@ out the instructions [here][linux-validate-gpg-tarball]. ### Install from source helper script +If you wish to build and install GCM from source, you can use the +`install-from-source.sh` helper script. This script will download the latest +source code, build it, and install it for you. It will also install any +dependencies that are required to build GCM from source. + +This script supports macOS, and many Linux distributions. + #### Install Ensure `curl` is installed: @@ -130,7 +137,7 @@ to install it. Download and run the script: ```shell -curl -L https://aka.ms/gcm/linux-install-source.sh | sh +curl -L https://aka.ms/gcm/install-from-source.sh | sh git-credential-manager configure ``` @@ -138,9 +145,18 @@ git-credential-manager configure can download GCM's dependencies using your distribution's package manager. +By default the script builds a trimmed, self-contained binary that needs only +the .NET SDK. To instead build a native ahead-of-time (AOT) compiled binary — +identical to the published packages, but requiring a C toolchain (`clang` and +the `zlib` development headers) - pass `--aot`: + +```shell +curl -L https://aka.ms/gcm/install-from-source.sh | sh -s -- --aot +``` + #### Uninstall -[Follow these instructions][linux-uninstall] for your distribution. +[Follow these instructions][fromsrc-uninstall] for your distribution. --- @@ -215,20 +231,20 @@ SDK][dotnet-install] before attempting to run the following `dotnet tool` commands. After installing, you will also need to follow the output instructions to add the tools directory to your `PATH`. -#### Install +### Install ```shell dotnet tool install -g git-credential-manager git-credential-manager configure ``` -#### Update +### Update ```shell dotnet tool update -g git-credential-manager ``` -#### Uninstall +### Uninstall ```shell git-credential-manager unconfigure @@ -243,7 +259,7 @@ dotnet tool uninstall -g git-credential-manager [git-for-windows]: https://gitforwindows.org/ [git-for-windows-gcm-screenshot]: img/git-for-windows-gcm-screenshot.png [latest-release]: https://github.com/git-ecosystem/git-credential-manager/releases/latest -[linux-uninstall]: linux-fromsrc-uninstall.md +[fromsrc-uninstall]: fromsrc-uninstall.md [linux-validate-gpg-debian]: ./linux-validate-gpg.md#debian-package [linux-validate-gpg-tarball]: ./linux-validate-gpg.md#tarball [ms-wsl]: https://aka.ms/wsl# diff --git a/Git-Credential-Manager.sln.DotSettings b/git-credential-manager.sln.dotsettings similarity index 100% rename from Git-Credential-Manager.sln.DotSettings rename to git-credential-manager.sln.dotsettings diff --git a/git-credential-manager.slnx b/git-credential-manager.slnx new file mode 100644 index 0000000000..310135a7dc --- /dev/null +++ b/git-credential-manager.slnx @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/global.json b/global.json index 5cc6b13a63..c7ec8eb59d 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,9 @@ { "sdk": { "rollForward": "latestMajor", - "version": "8.0" + "version": "10.0" + }, + "msbuild-sdks": { + "Microsoft.Build.NoTargets" : "3.7.134" } } - diff --git a/src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj b/src/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj rename to src/Atlassian.Bitbucket.Tests/Atlassian.Bitbucket.Tests.csproj diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs b/src/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs rename to src/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHelperTest.cs b/src/Atlassian.Bitbucket.Tests/BitbucketHelperTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/BitbucketHelperTest.cs rename to src/Atlassian.Bitbucket.Tests/BitbucketHelperTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs b/src/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs rename to src/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiRegistryTest.cs b/src/Atlassian.Bitbucket.Tests/BitbucketRestApiRegistryTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/BitbucketRestApiRegistryTest.cs rename to src/Atlassian.Bitbucket.Tests/BitbucketRestApiRegistryTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketTokenEndpointResponseJsonTest.cs b/src/Atlassian.Bitbucket.Tests/BitbucketTokenEndpointResponseJsonTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/BitbucketTokenEndpointResponseJsonTest.cs rename to src/Atlassian.Bitbucket.Tests/BitbucketTokenEndpointResponseJsonTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs b/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs rename to src/Atlassian.Bitbucket.Tests/Cloud/BitbucketOAuth2ClientTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketRestApiTest.cs b/src/Atlassian.Bitbucket.Tests/Cloud/BitbucketRestApiTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/Cloud/BitbucketRestApiTest.cs rename to src/Atlassian.Bitbucket.Tests/Cloud/BitbucketRestApiTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/Cloud/UserInfoTest.cs b/src/Atlassian.Bitbucket.Tests/Cloud/UserInfoTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/Cloud/UserInfoTest.cs rename to src/Atlassian.Bitbucket.Tests/Cloud/UserInfoTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs b/src/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs rename to src/Atlassian.Bitbucket.Tests/DataCenter/BitbucketOAuth2ClientTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketRestApiTest.cs b/src/Atlassian.Bitbucket.Tests/DataCenter/BitbucketRestApiTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/DataCenter/BitbucketRestApiTest.cs rename to src/Atlassian.Bitbucket.Tests/DataCenter/BitbucketRestApiTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/LoginOptionsTest.cs b/src/Atlassian.Bitbucket.Tests/DataCenter/LoginOptionsTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/DataCenter/LoginOptionsTest.cs rename to src/Atlassian.Bitbucket.Tests/DataCenter/LoginOptionsTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/DataCenter/UserInfoTest.cs b/src/Atlassian.Bitbucket.Tests/DataCenter/UserInfoTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/DataCenter/UserInfoTest.cs rename to src/Atlassian.Bitbucket.Tests/DataCenter/UserInfoTest.cs diff --git a/src/shared/Atlassian.Bitbucket.Tests/OAuth2ClientRegistryTest.cs b/src/Atlassian.Bitbucket.Tests/OAuth2ClientRegistryTest.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket.Tests/OAuth2ClientRegistryTest.cs rename to src/Atlassian.Bitbucket.Tests/OAuth2ClientRegistryTest.cs diff --git a/src/shared/Atlassian.Bitbucket/Atlassian.Bitbucket.csproj b/src/Atlassian.Bitbucket/Atlassian.Bitbucket.csproj similarity index 63% rename from src/shared/Atlassian.Bitbucket/Atlassian.Bitbucket.csproj rename to src/Atlassian.Bitbucket/Atlassian.Bitbucket.csproj index a2d5c69bb1..0f8a62ef01 100644 --- a/src/shared/Atlassian.Bitbucket/Atlassian.Bitbucket.csproj +++ b/src/Atlassian.Bitbucket/Atlassian.Bitbucket.csproj @@ -1,8 +1,7 @@  - net10.0 - net10.0;net472 + net10.0 Atlassian.Bitbucket Atlassian.Bitbucket false @@ -13,10 +12,6 @@ - - - - diff --git a/src/shared/Atlassian.Bitbucket/AuthenticationMethod.cs b/src/Atlassian.Bitbucket/AuthenticationMethod.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/AuthenticationMethod.cs rename to src/Atlassian.Bitbucket/AuthenticationMethod.cs diff --git a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/Atlassian.Bitbucket/BitbucketAuthentication.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs rename to src/Atlassian.Bitbucket/BitbucketAuthentication.cs diff --git a/src/shared/Atlassian.Bitbucket/BitbucketConstants.cs b/src/Atlassian.Bitbucket/BitbucketConstants.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/BitbucketConstants.cs rename to src/Atlassian.Bitbucket/BitbucketConstants.cs diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHelper.cs b/src/Atlassian.Bitbucket/BitbucketHelper.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/BitbucketHelper.cs rename to src/Atlassian.Bitbucket/BitbucketHelper.cs diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/Atlassian.Bitbucket/BitbucketHostProvider.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs rename to src/Atlassian.Bitbucket/BitbucketHostProvider.cs diff --git a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs b/src/Atlassian.Bitbucket/BitbucketOAuth2Client.cs similarity index 90% rename from src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs rename to src/Atlassian.Bitbucket/BitbucketOAuth2Client.cs index 1ca23d0f57..84e76518f3 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs +++ b/src/Atlassian.Bitbucket/BitbucketOAuth2Client.cs @@ -42,7 +42,9 @@ protected override bool TryCreateTokenEndpointResult(string json, out OAuth2Toke // We override the token endpoint response parsing because the Bitbucket authority returns // the non-standard 'scopes' property for the list of scopes, rather than the (optional) // 'scope' (note the singular vs plural) property as outlined in the standard. - if (TryDeserializeJson(json, out BitbucketTokenEndpointResponseJson jsonObj)) + if (TryDeserializeJson(json, + BitbucketOAuthJsonContext.Default.BitbucketTokenEndpointResponseJson, + out BitbucketTokenEndpointResponseJson jsonObj)) { result = jsonObj.ToResult(); return true; diff --git a/src/shared/Atlassian.Bitbucket/BitbucketResources.Designer.cs b/src/Atlassian.Bitbucket/BitbucketResources.Designer.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/BitbucketResources.Designer.cs rename to src/Atlassian.Bitbucket/BitbucketResources.Designer.cs diff --git a/src/shared/Atlassian.Bitbucket/BitbucketResources.resx b/src/Atlassian.Bitbucket/BitbucketResources.resx similarity index 100% rename from src/shared/Atlassian.Bitbucket/BitbucketResources.resx rename to src/Atlassian.Bitbucket/BitbucketResources.resx diff --git a/src/shared/Atlassian.Bitbucket/BitbucketRestApiRegistry.cs b/src/Atlassian.Bitbucket/BitbucketRestApiRegistry.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/BitbucketRestApiRegistry.cs rename to src/Atlassian.Bitbucket/BitbucketRestApiRegistry.cs diff --git a/src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs b/src/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs similarity index 95% rename from src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs rename to src/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs index c28e63de11..afc8c43a5f 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs +++ b/src/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs @@ -5,6 +5,9 @@ namespace Atlassian.Bitbucket { + [JsonSerializable(typeof(BitbucketTokenEndpointResponseJson))] + public partial class BitbucketOAuthJsonContext : JsonSerializerContext; + [JsonConverter(typeof(BitbucketCustomTokenEndpointResponseJsonConverter))] public class BitbucketTokenEndpointResponseJson : TokenEndpointResponseJson { diff --git a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs b/src/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs rename to src/Atlassian.Bitbucket/Cloud/BitbucketOAuth2Client.cs diff --git a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs b/src/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs similarity index 86% rename from src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs rename to src/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs index 94021e14da..c90932f381 100644 --- a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs +++ b/src/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs @@ -8,6 +8,13 @@ namespace Atlassian.Bitbucket.Cloud { + [JsonSerializable(typeof(UserInfo))] + [JsonSourceGenerationOptions( + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + )] + public partial class BitbucketCloudRestApiJsonContext : JsonSerializerContext; + public class BitbucketRestApi : IBitbucketRestApi { private readonly ICommandContext _context; @@ -43,11 +50,7 @@ public async Task> GetUserInformationAsync(string userN if (response.IsSuccessStatusCode) { - var obj = JsonSerializer.Deserialize(json, - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }); + UserInfo obj = JsonSerializer.Deserialize(json, BitbucketCloudRestApiJsonContext.Default.UserInfo); return new RestApiResult(response.StatusCode, obj); } diff --git a/src/shared/Atlassian.Bitbucket/Cloud/CloudConstants.cs b/src/Atlassian.Bitbucket/Cloud/CloudConstants.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/Cloud/CloudConstants.cs rename to src/Atlassian.Bitbucket/Cloud/CloudConstants.cs diff --git a/src/shared/Atlassian.Bitbucket/Cloud/UserInfo.cs b/src/Atlassian.Bitbucket/Cloud/UserInfo.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/Cloud/UserInfo.cs rename to src/Atlassian.Bitbucket/Cloud/UserInfo.cs diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs b/src/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs rename to src/Atlassian.Bitbucket/DataCenter/BitbucketOAuth2Client.cs diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs b/src/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs similarity index 91% rename from src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs rename to src/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs index 159229885d..65c2520d16 100644 --- a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs +++ b/src/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs @@ -10,6 +10,15 @@ namespace Atlassian.Bitbucket.DataCenter { + [JsonSerializable(typeof(UserInfo))] + [JsonSerializable(typeof(LoginOption))] + [JsonSerializable(typeof(LoginOptions))] + [JsonSourceGenerationOptions( + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + )] + public partial class BitbucketDataCenterRestApiJsonContext : JsonSerializerContext; + public class BitbucketRestApi : IBitbucketRestApi { private readonly ICommandContext _context; @@ -107,12 +116,8 @@ public async Task> GetAuthenticationMethodsAsync() if (response.IsSuccessStatusCode) { - var loginOptions = JsonSerializer.Deserialize(json, - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }); + LoginOptions loginOptions = JsonSerializer.Deserialize( + json, BitbucketDataCenterRestApiJsonContext.Default.LoginOptions); if (loginOptions.Results.Any(r => "LOGIN_FORM".Equals(r.Type))) { @@ -151,4 +156,4 @@ private Uri ApiUri } } } -} \ No newline at end of file +} diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/DataCenterConstants.cs b/src/Atlassian.Bitbucket/DataCenter/DataCenterConstants.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/DataCenter/DataCenterConstants.cs rename to src/Atlassian.Bitbucket/DataCenter/DataCenterConstants.cs diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/LoginOption.cs b/src/Atlassian.Bitbucket/DataCenter/LoginOption.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/DataCenter/LoginOption.cs rename to src/Atlassian.Bitbucket/DataCenter/LoginOption.cs diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/LoginOptions.cs b/src/Atlassian.Bitbucket/DataCenter/LoginOptions.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/DataCenter/LoginOptions.cs rename to src/Atlassian.Bitbucket/DataCenter/LoginOptions.cs diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/UserInfo.cs b/src/Atlassian.Bitbucket/DataCenter/UserInfo.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/DataCenter/UserInfo.cs rename to src/Atlassian.Bitbucket/DataCenter/UserInfo.cs diff --git a/src/shared/Atlassian.Bitbucket/IBitbucketRestApi.cs b/src/Atlassian.Bitbucket/IBitbucketRestApi.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/IBitbucketRestApi.cs rename to src/Atlassian.Bitbucket/IBitbucketRestApi.cs diff --git a/src/shared/Atlassian.Bitbucket/IRegistry.cs b/src/Atlassian.Bitbucket/IRegistry.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/IRegistry.cs rename to src/Atlassian.Bitbucket/IRegistry.cs diff --git a/src/shared/Atlassian.Bitbucket/IUserInfo.cs b/src/Atlassian.Bitbucket/IUserInfo.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/IUserInfo.cs rename to src/Atlassian.Bitbucket/IUserInfo.cs diff --git a/src/shared/Atlassian.Bitbucket/InternalsVisibleTo.cs b/src/Atlassian.Bitbucket/InternalsVisibleTo.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/InternalsVisibleTo.cs rename to src/Atlassian.Bitbucket/InternalsVisibleTo.cs diff --git a/src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs b/src/Atlassian.Bitbucket/OAuth2ClientRegistry.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/OAuth2ClientRegistry.cs rename to src/Atlassian.Bitbucket/OAuth2ClientRegistry.cs diff --git a/src/shared/Atlassian.Bitbucket/RestApiResult.cs b/src/Atlassian.Bitbucket/RestApiResult.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/RestApiResult.cs rename to src/Atlassian.Bitbucket/RestApiResult.cs diff --git a/src/shared/Atlassian.Bitbucket/UI/Assets/atlassian-logo.png b/src/Atlassian.Bitbucket/UI/Assets/atlassian-logo.png similarity index 100% rename from src/shared/Atlassian.Bitbucket/UI/Assets/atlassian-logo.png rename to src/Atlassian.Bitbucket/UI/Assets/atlassian-logo.png diff --git a/src/shared/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs b/src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs rename to src/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs diff --git a/src/shared/Atlassian.Bitbucket/UI/ViewModels/CredentialsViewModel.cs b/src/Atlassian.Bitbucket/UI/ViewModels/CredentialsViewModel.cs similarity index 100% rename from src/shared/Atlassian.Bitbucket/UI/ViewModels/CredentialsViewModel.cs rename to src/Atlassian.Bitbucket/UI/ViewModels/CredentialsViewModel.cs diff --git a/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml b/src/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml similarity index 95% rename from src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml rename to src/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml index 717500b7be..895587b610 100644 --- a/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml +++ b/src/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml @@ -5,6 +5,7 @@ xmlns:vm="clr-namespace:Atlassian.Bitbucket.UI.ViewModels;assembly=Atlassian.Bitbucket" xmlns:converters="clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:DataType="vm:CredentialsViewModel" x:Class="Atlassian.Bitbucket.UI.Views.CredentialsView"> @@ -63,11 +64,11 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/shared/Core/UI/Controls/DialogWindow.axaml.cs b/src/Core/UI/Controls/DialogWindow.axaml.cs similarity index 100% rename from src/shared/Core/UI/Controls/DialogWindow.axaml.cs rename to src/Core/UI/Controls/DialogWindow.axaml.cs diff --git a/src/shared/Core/UI/Controls/IFocusable.cs b/src/Core/UI/Controls/IFocusable.cs similarity index 100% rename from src/shared/Core/UI/Controls/IFocusable.cs rename to src/Core/UI/Controls/IFocusable.cs diff --git a/src/shared/Core/UI/Controls/ProgressWindow.axaml b/src/Core/UI/Controls/ProgressWindow.axaml similarity index 89% rename from src/shared/Core/UI/Controls/ProgressWindow.axaml rename to src/Core/UI/Controls/ProgressWindow.axaml index 3bfc20f5ca..e8a7346774 100644 --- a/src/shared/Core/UI/Controls/ProgressWindow.axaml +++ b/src/Core/UI/Controls/ProgressWindow.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="182" d:DesignHeight="46" SizeToContent="WidthAndHeight" CanResize="False" Topmost="True" - ExtendClientAreaChromeHints="NoChrome" ExtendClientAreaToDecorationsHint="True" + WindowDecorations="None" ShowInTaskbar="False" Title="Git Credential Manager" WindowStartupLocation="CenterScreen" x:Class="GitCredentialManager.UI.Controls.ProgressWindow"> (); + public event EventHandler Accepted; public event EventHandler Canceled; @@ -18,6 +21,13 @@ public WindowViewModel() // Extend the client area on Windows and macOS only ExtendClientArea = PlatformUtils.IsMacOS() || PlatformUtils.IsWindows(); + + // On Windows we prefer to show our own title bar, but we want the system + // to continue to draw the window border for us, which includes rounded + // window corners and shadow. + WindowDecorations = PlatformUtils.IsWindows() + ? WindowDecorations.BorderOnly + : WindowDecorations.Full; } public bool WindowResult { get; private set; } @@ -28,39 +38,24 @@ public bool ShowDebugControls set => SetAndRaisePropertyChanged(ref _showDebugControls, value); } - public bool ShowCustomChrome - { - // On macOS we typically do NOT want to show the custom chrome if we've extended the client area - // because the native 'traffic light' controls will still be visible and we don't want to show our own. - get => ShowCustomChromeOverride || (ExtendClientArea && !PlatformUtils.IsMacOS()); - } - - public bool ShowCustomWindowBorder - { - // Draw the window border explicitly on Windows - get => ShowCustomChrome && PlatformUtils.IsWindows(); - } - - public bool ShowCustomChromeOverride + public WindowDecorations WindowDecorations { - get => _showCustomChromeOverride; + get => _windowDecorations; set { - SetAndRaisePropertyChanged(ref _showCustomChromeOverride, value); - RaisePropertyChanged(nameof(ShowCustomChrome)); - RaisePropertyChanged(nameof(ShowCustomWindowBorder)); + SetAndRaisePropertyChanged(ref _windowDecorations, value); + RaisePropertyChanged(nameof(ShowCustomTitleBar)); } } + // Without system window decorations there's now way to close the window, + // so we need to draw our own title bar and close button. + public bool ShowCustomTitleBar => WindowDecorations != WindowDecorations.Full; + public bool ExtendClientArea { get => _extendClientArea; - set - { - SetAndRaisePropertyChanged(ref _extendClientArea, value); - RaisePropertyChanged(nameof(ShowCustomChrome)); - RaisePropertyChanged(nameof(ShowCustomWindowBorder)); - } + set => SetAndRaisePropertyChanged(ref _extendClientArea, value); } public string Title diff --git a/src/shared/Core/UI/Views/CredentialsView.axaml b/src/Core/UI/Views/CredentialsView.axaml similarity index 92% rename from src/shared/Core/UI/Views/CredentialsView.axaml rename to src/Core/UI/Views/CredentialsView.axaml index 9e85c48720..0fb41261b3 100644 --- a/src/shared/Core/UI/Views/CredentialsView.axaml +++ b/src/Core/UI/Views/CredentialsView.axaml @@ -5,6 +5,7 @@ xmlns:vm="clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcore" xmlns:converters="clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore" mc:Ignorable="d" d:DesignWidth="420" + x:DataType="vm:CredentialsViewModel" x:Class="GitCredentialManager.UI.Views.CredentialsView"> @@ -34,10 +35,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/shared/Core/UI/Converters/WindowClientAreaConverters.cs b/src/shared/Core/UI/Converters/WindowClientAreaConverters.cs deleted file mode 100644 index e4c047163b..0000000000 --- a/src/shared/Core/UI/Converters/WindowClientAreaConverters.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia.Data.Converters; -using Avalonia.Platform; - -namespace GitCredentialManager.UI.Converters -{ - public static class WindowClientAreaConverters - { - public static readonly IValueConverter BoolToChromeHints = - new FuncValueConverter( - x => x - ? ExtendClientAreaChromeHints.NoChrome - : ExtendClientAreaChromeHints.PreferSystemChrome); - } -} diff --git a/src/shared/Directory.Build.props b/src/shared/Directory.Build.props deleted file mode 100644 index 97905067bd..0000000000 --- a/src/shared/Directory.Build.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - $(RepoOutPath)shared\ - $(PlatformOutPath)$(MSBuildProjectName)\ - $(ProjectOutPath)bin\ - $(ProjectOutPath)obj\ - - diff --git a/src/shared/DotnetTool/DotnetTool.csproj b/src/shared/DotnetTool/DotnetTool.csproj deleted file mode 100644 index 720f756938..0000000000 --- a/src/shared/DotnetTool/DotnetTool.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net10.0 - true - dotnet-tool.nuspec - - - version=$(PackageVersion); - publishDir=$(PublishDir); - - true - $(ProjectOutPath)nupkg\$(Configuration)\ - false - - diff --git a/src/shared/DotnetTool/dotnet-tool.nuspec b/src/shared/DotnetTool/dotnet-tool.nuspec deleted file mode 100644 index 9eb9a70203..0000000000 --- a/src/shared/DotnetTool/dotnet-tool.nuspec +++ /dev/null @@ -1,17 +0,0 @@ - - - - git-credential-manager - $version$ - Secure, cross-platform Git credential storage with authentication to Azure Repos, GitHub, and other popular Git hosting services. - git-credential-manager - images\icon.png - - - - - - - - - diff --git a/src/shared/DotnetTool/layout.ps1 b/src/shared/DotnetTool/layout.ps1 deleted file mode 100644 index f2bd872857..0000000000 --- a/src/shared/DotnetTool/layout.ps1 +++ /dev/null @@ -1,90 +0,0 @@ -<# -.SYNOPSIS - Lays out the .NET tool package directory. - -.PARAMETER Configuration - Build configuration (Debug/Release). Defaults to Debug. - -.PARAMETER Output - Root output directory for the nupkg layout. If omitted: - out/shared/DotnetTool/nupkg/ - -.EXAMPLE - pwsh ./layout.ps1 -Configuration Release - -.EXAMPLE - pwsh ./layout.ps1 -Output C:\temp\tool-layout - -#> - -[CmdletBinding()] -param( - [string]$Configuration = "Debug", - [string]$Output -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Make-Absolute { - param([string]$Path) - if ([string]::IsNullOrWhiteSpace($Path)) { return $null } - if ([System.IO.Path]::IsPathRooted($Path)) { return $Path } - return (Join-Path -Path (Get-Location) -ChildPath $Path) -} - -Write-Host "Starting layout..." -ForegroundColor Cyan - -# Directories -$ScriptDir = $PSScriptRoot -$Root = (Resolve-Path (Join-Path $ScriptDir "..\..\..")).Path -$Src = Join-Path $Root "src" -$Out = Join-Path $Root "out" -$DotnetToolRel = "shared/DotnetTool" -$GcmSrc = Join-Path $Src "shared\Git-Credential-Manager" -$ProjOut = Join-Path $Out $DotnetToolRel - -$Framework = "net10.0" - -if (-not $Output -or $Output.Trim() -eq "") { - $Output = Join-Path $ProjOut "nupkg\$Configuration" -} - -$ImgOut = Join-Path $Output "images" -$BinOut = Join-Path $Output "tools\$Framework\any" - -# Cleanup previous layout -if (Test-Path $Output) { - Write-Host "Cleaning existing output directory '$Output'..." - Remove-Item -Force -Recurse $Output -} - -# Recreate directories -$null = New-Item -ItemType Directory -Path $BinOut -Force -$null = New-Item -ItemType Directory -Path $ImgOut -Force - -# Determine DOTNET_ROOT if not set -if (-not $env:DOTNET_ROOT -or $env:DOTNET_ROOT.Trim() -eq "") { - $dotnetCmd = Get-Command dotnet -ErrorAction Stop - $env:DOTNET_ROOT = Split-Path -Parent $dotnetCmd.Source -} - -Write-Host "Publishing core application..." -& "$env:DOTNET_ROOT/dotnet" publish $GcmSrc ` - --configuration $Configuration ` - --framework $Framework ` - --output (Make-Absolute $BinOut) ` - -p:UseAppHost=false - -if ($LASTEXITCODE -ne 0) { - Write-Error "dotnet publish failed with exit code $LASTEXITCODE" - exit $LASTEXITCODE -} - -Write-Host "Copying package configuration file..." -Copy-Item -Path (Join-Path $Src "$DotnetToolRel\DotnetToolSettings.xml") -Destination $BinOut -Force - -Write-Host "Copying images..." -Copy-Item -Path (Join-Path $Src "$DotnetToolRel\icon.png") -Destination $ImgOut -Force - -Write-Host "Layout complete." -ForegroundColor Green diff --git a/src/shared/DotnetTool/pack.ps1 b/src/shared/DotnetTool/pack.ps1 deleted file mode 100644 index 6842d030ae..0000000000 --- a/src/shared/DotnetTool/pack.ps1 +++ /dev/null @@ -1,95 +0,0 @@ -<# -.SYNOPSIS - Creates the NuGet package for the .NET tool. - -.PARAMETER Configuration - Build configuration (Debug/Release). Defaults to Debug. - -.PARAMETER Version - Package version (required). - -.PARAMETER PackageRoot - Root of the pre-laid-out package structure (from layout). Defaults to: - out/shared/DotnetTool/nupkg/ - -.PARAMETER Output - Optional directory for the produced .nupkg/.snupkg. If omitted NuGet chooses. - -.EXAMPLE - pwsh ./pack.ps1 -Version 2.0.123-beta - -.EXAMPLE - pwsh ./pack.ps1 -Configuration Release -Version 2.1.0 -Output C:\pkgs - -#> - -[CmdletBinding()] -param( - [string]$Configuration = "Debug", - [Parameter(Mandatory = $true)] - [string]$Version, - [string]$PackageRoot, - [string]$Output -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -Write-Host "Starting pack..." -ForegroundColor Cyan - -# Directories -$ScriptDir = $PSScriptRoot -$Root = (Resolve-Path (Join-Path $ScriptDir "..\..\..")).Path -$Src = Join-Path $Root "src" -$Out = Join-Path $Root "out" -$DotnetToolRel = "shared\DotnetTool" -$NuspecFile = Join-Path $Src "$DotnetToolRel\dotnet-tool.nuspec" - -if (-not (Test-Path $NuspecFile)) { - Write-Error "Could not locate nuspec file at '$NuspecFile'" - exit 1 -} - -if (-not $PackageRoot -or $PackageRoot.Trim() -eq "") { - $PackageRoot = Join-Path $Out "$DotnetToolRel\nupkg\$Configuration" -} - -if (-not (Test-Path $PackageRoot)) { - Write-Error "Package root '$PackageRoot' does not exist. Run layout.ps1 first." - exit 1 -} - -# Locate nuget -$nugetCmd = Get-Command nuget -ErrorAction SilentlyContinue -if (-not $nugetCmd) { - Write-Error "nuget CLI not found in PATH (install: https://www.nuget.org/downloads)" - exit 1 -} -$nugetExe = $nugetCmd.Source - -Write-Host "Creating .NET tool package..." - -$packArgs = @( - "pack", "$NuspecFile", - "-Properties", "Configuration=$Configuration", - "-Version", $Version, - "-Symbols", "-SymbolPackageFormat", "snupkg", - "-BasePath", "$PackageRoot" -) - -if ($Output -and $Output.Trim() -ne "") { - if (-not (Test-Path $Output)) { - Write-Host "Creating output directory '$Output'..." - New-Item -ItemType Directory -Force -Path $Output | Out-Null - } - $packArgs += @("-OutputDirectory", "$Output") -} - -& $nugetExe @packArgs - -if ($LASTEXITCODE -ne 0) { - Write-Error "nuget pack failed with exit code $LASTEXITCODE" - exit $LASTEXITCODE -} - -Write-Host ".NET tool pack complete." -ForegroundColor Green diff --git a/src/windows/Directory.Build.props b/src/windows/Directory.Build.props deleted file mode 100644 index 10706a4177..0000000000 --- a/src/windows/Directory.Build.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - $(RepoOutPath)windows\ - $(PlatformOutPath)$(MSBuildProjectName)\ - $(ProjectOutPath)bin\ - $(ProjectOutPath)obj\ - - diff --git a/src/windows/Installer.Windows/Installer.Windows.csproj b/src/windows/Installer.Windows/Installer.Windows.csproj deleted file mode 100644 index b625be44df..0000000000 --- a/src/windows/Installer.Windows/Installer.Windows.csproj +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - win-x64 - win-x86 - win-arm64 - - - - net472 - false - false - $(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\$(RuntimeIdentifier)\ - - false - - - - - - - - - - - - - - - - - - - - "$(PkgTools_InnoSetup)\tools\ISCC.exe" /DPayloadDir="$(PayloadPath)" /DInstallTarget=system /DGcmRuntimeIdentifier="$(RuntimeIdentifier)" "$(RepoSrcPath)\windows\Installer.Windows\Setup.iss" /O"$(OutputPath)" - "$(PkgTools_InnoSetup)\tools\ISCC.exe" /DPayloadDir="$(PayloadPath)" /DInstallTarget=user /DGcmRuntimeIdentifier="$(RuntimeIdentifier)" "$(RepoSrcPath)\windows\Installer.Windows\Setup.iss" /O"$(OutputPath)" - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/windows/Installer.Windows/layout.ps1 b/src/windows/Installer.Windows/layout.ps1 deleted file mode 100644 index 53646764a4..0000000000 --- a/src/windows/Installer.Windows/layout.ps1 +++ /dev/null @@ -1,135 +0,0 @@ -# Inputs -param ([Parameter(Mandatory)] $Configuration, [Parameter(Mandatory)] $Output, $RuntimeIdentifier, $SymbolOutput) - -# Trim trailing slashes from output paths -$Output = $Output.TrimEnd('\','/') - -if ($SymbolOutput) { - $SymbolOutput = $SymbolOutput.TrimEnd('\','/') -} - -Write-Output "Output: $Output" - -# Determine a runtime if one was not provided -if (-not $RuntimeIdentifier) { - $arch = $env:PROCESSOR_ARCHITECTURE - switch ($arch) { - "AMD64" { $RuntimeIdentifier = "win-x64" } - "x86" { $RuntimeIdentifier = "win-x86" } - "ARM64" { $RuntimeIdentifier = "win-arm64" } - default { - Write-Host "Unknown architecture: $arch" - exit 1 - } - } -} - -Write-Output "Building for runtime '$RuntimeIdentifier'" - -if ($RuntimeIdentifier -ne 'win-x86' -and $RuntimeIdentifier -ne 'win-x64' -and $RuntimeIdentifier -ne 'win-arm64') { - Write-Host "Unsupported RuntimeIdentifier: $RuntimeIdentifier" - exit 1 -} - -# Directories -$THISDIR = $PSScriptRoot -$ROOT = (Get-Item $THISDIR).Parent.Parent.Parent.FullName -$SRC = "$ROOT\src" -$GCM_SRC = "$SRC\shared\Git-Credential-Manager" - -# Perform pre-execution checks -$PAYLOAD = "$Output" -if ($SymbolOutput) -{ - $SYMBOLS = "$SymbolOutput" -} -else -{ - $SYMBOLS = "$PAYLOAD.sym" -} - -# Clean up any old payload and symbols directories -if (Test-Path -Path $PAYLOAD) -{ - Write-Output "Cleaning old payload directory '$PAYLOAD'..." - Remove-Item -Recurse "$PAYLOAD" -Force -} - -if (Test-Path -Path $SYMBOLS) -{ - Write-Output "Cleaning old symbols directory '$SYMBOLS'..." - Remove-Item -Recurse "$SYMBOLS" -Force -} - -# Ensure payload and symbol directories exist -mkdir -p "$PAYLOAD","$SYMBOLS" | Out-Null - -# Publish core application executables -Write-Output "Publishing core application..." -dotnet publish "$GCM_SRC" ` - --framework net472 ` - --configuration "$Configuration" ` - --runtime $RuntimeIdentifier ` - --output "$PAYLOAD" - -# Delete libraries that are not needed for Windows but find their way -# into the publish output. -Remove-Item -Path "$PAYLOAD/*.dylib" -Force -ErrorAction Ignore - -# Delete extraneous files that get included for other runtimes -Remove-Item -Path "$PAYLOAD/musl-x64/" -Recurse -Force -ErrorAction Ignore - -switch ($RuntimeIdentifier) { - "win-x86" { - Remove-Item -Path "$PAYLOAD/arm/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/arm64/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/x64/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/runtimes/win-arm64/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/runtimes/win-x64/" -Recurse -Force -ErrorAction Ignore - # The Avalonia and MSAL binaries are already included in the $PAYLOAD directory directly - Remove-Item -Path "$PAYLOAD/x86/libSkiaSharp.dll" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/x86/libHarfBuzzSharp.dll" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/runtimes/win-x86/native/msalruntime_x86.dll" -Force -ErrorAction Ignore - } - "win-x64" { - Remove-Item -Path "$PAYLOAD/arm/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/arm64/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/x86/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/runtimes/win-arm64/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/runtimes/win-x86/" -Recurse -Force -ErrorAction Ignore - # The Avalonia and MSAL binaries are already included in the $PAYLOAD directory directly - Remove-Item -Path "$PAYLOAD/x64/libSkiaSharp.dll" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/x64/libHarfBuzzSharp.dll" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/x64/libSkiaSharp.so" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/x64/libHarfBuzzSharp.so" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/runtimes/win-x64/native/msalruntime.dll" -Force -ErrorAction Ignore - } - "win-arm64" { - Remove-Item -Path "$PAYLOAD/arm/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/x86/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/x64/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/runtimes/win-x86/" -Recurse -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/runtimes/win-x64/" -Recurse -Force -ErrorAction Ignore - # The Avalonia and MSAL binaries are already included in the $PAYLOAD directory directly - Remove-Item -Path "$PAYLOAD/arm64/libSkiaSharp.dll" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/arm64/libHarfBuzzSharp.dll" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/arm64/libSkiaSharp.so" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/arm64/libHarfBuzzSharp.so" -Force -ErrorAction Ignore - Remove-Item -Path "$PAYLOAD/runtimes/win-arm64/native/msalruntime_arm64.dll" -Force -ErrorAction Ignore - } -} - -# Delete localized resource assemblies - we don't localize the core GCM assembly anyway -Get-ChildItem "$PAYLOAD" -Recurse -Include "*.resources.dll" | Remove-Item -Force -ErrorAction Ignore - -# Delete any empty directories -Get-ChildItem "$PAYLOAD" -Recurse -Directory ` - | Sort-Object -Property FullName -Descending ` - | Where-Object { ! (Get-ChildItem $_.FullName -File -Recurse).Count } ` - | Remove-Item -Force - -# Collect symbols -Write-Output "Collecting managed symbols..." -Move-Item -Path "$PAYLOAD/*.pdb" -Destination "$SYMBOLS" - -Write-Output "Layout complete."