From 7d4f8541cf538d344f968bb398585fff917d036f Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Tue, 28 Jul 2026 02:01:16 +0000 Subject: [PATCH 01/14] Update dependencies from https://github.com/dotnet/dotnet build 20260726.6 On relative base path root Microsoft.DotNet.Arcade.Sdk , Microsoft.DotNet.Build.Tasks.Feed From Version 11.0.0-beta.26365.101 -> To Version 11.0.0-beta.26376.106 Microsoft.DotNet.Cecil From Version 0.11.5-preview.26365.101 -> To Version 0.11.5-preview.26376.106 Microsoft.NET.ILLink , Microsoft.NETCore.App.Ref From Version 11.0.0-preview.7.26365.101 -> To Version 11.0.0-preview.7.26376.106 Microsoft.NET.Sdk , Microsoft.TemplateEngine.Authoring.Tasks From Version 11.0.100-preview.7.26365.101 -> To Version 11.0.100-preview.7.26376.106 --- eng/Version.Details.xml | 28 ++++----- eng/Versions.props | 12 ++-- eng/common/build.ps1 | 2 + eng/common/build.sh | 11 ++++ .../core-templates/job/helix-job-monitor.yml | 19 ++++-- eng/common/core-templates/job/onelocbuild.yml | 20 +++++- eng/common/cross/build-rootfs.sh | 5 +- eng/common/cross/toolchain.cmake | 2 + eng/common/native/init-os-and-arch.sh | 6 +- eng/common/sdk-task.ps1 | 17 ++++- eng/common/sdk-task.sh | 22 +++++-- eng/common/tools.ps1 | 50 +++++++++++++-- eng/common/tools.sh | 62 +++++++++++++++++-- global.json | 2 +- 14 files changed, 209 insertions(+), 49 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1e2c2cc07c1..acae7dfa890 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,20 +1,20 @@ - + https://github.com/dotnet/dotnet - cb8306a63c5cf24e9381108a3a9eb58907fd0f60 + 7bcb9009c40399c9073e7db938250486229cc85d - + https://github.com/dotnet/dotnet - cb8306a63c5cf24e9381108a3a9eb58907fd0f60 + 7bcb9009c40399c9073e7db938250486229cc85d - + https://github.com/dotnet/dotnet - cb8306a63c5cf24e9381108a3a9eb58907fd0f60 + 7bcb9009c40399c9073e7db938250486229cc85d - + https://github.com/dotnet/dotnet - cb8306a63c5cf24e9381108a3a9eb58907fd0f60 + 7bcb9009c40399c9073e7db938250486229cc85d https://github.com/dotnet/dotnet @@ -36,17 +36,17 @@ - + https://github.com/dotnet/dotnet - cb8306a63c5cf24e9381108a3a9eb58907fd0f60 + 7bcb9009c40399c9073e7db938250486229cc85d - + https://github.com/dotnet/dotnet - cb8306a63c5cf24e9381108a3a9eb58907fd0f60 + 7bcb9009c40399c9073e7db938250486229cc85d - + https://github.com/dotnet/dotnet - cb8306a63c5cf24e9381108a3a9eb58907fd0f60 + 7bcb9009c40399c9073e7db938250486229cc85d https://github.com/microsoft/testfx diff --git a/eng/Versions.props b/eng/Versions.props index 47d7a3c9ea7..04b7b1da665 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,20 +1,20 @@ - 11.0.100-preview.7.26365.101 + 11.0.100-preview.7.26376.106 $(MicrosoftNETSdkPackageVersion) - 11.0.0-preview.7.26365.101 - 11.0.0-preview.7.26365.101 + 11.0.0-preview.7.26376.106 + 11.0.0-preview.7.26376.106 7.0.0-beta.22103.1 11.0.0-beta.26060.102 - 11.0.0-beta.26365.101 + 11.0.0-beta.26376.106 11.0.100-preview.4.26215.121 11.0.100-preview.4.26215.121 $(MicrosoftNETWorkloadMonoToolChainCurrentManifest110100preview4PackageVersion) $(MicrosoftNETWorkloadEmscriptenCurrentManifest110100preview4PackageVersion) - 11.0.100-preview.7.26365.101 - 0.11.5-preview.26365.101 + 11.0.100-preview.7.26376.106 + 0.11.5-preview.26376.106 4.4.0-preview.26367.7 10.0.10 11.0.0-preview.1.26104.118 diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 index 2cbb725323e..dd84699f500 100644 --- a/eng/common/build.ps1 +++ b/eng/common/build.ps1 @@ -23,6 +23,7 @@ Param( [switch] $clean, [switch][Alias('pb')]$productBuild, [switch]$fromVMR, + [switch]$disablePipelineSetResult, [switch][Alias('bl')]$binaryLog, [string][Alias('bln')]$binaryLogName = '', [switch][Alias('nobl')]$excludeCIBinarylog, @@ -80,6 +81,7 @@ function Print-Usage() { Write-Host " -nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" Write-Host " -buildCheck Sets /check msbuild parameter" Write-Host " -fromVMR Set when building from within the VMR" + Write-Host " -disablePipelineSetResult Set to disable masking the actual exit code in the pipeline when the build fails" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." diff --git a/eng/common/build.sh b/eng/common/build.sh index 3a9fdcfd0f5..e37edd6cff3 100644 --- a/eng/common/build.sh +++ b/eng/common/build.sh @@ -40,12 +40,14 @@ usage() echo " --projects Project or solution file(s) to build" echo " --ci Set when running on CI server" echo " --excludeCIBinarylog Don't output binary log (short: -nobl)" + echo " --pipelinesLog Promote msbuild errors/warnings to Azure Pipelines timeline issues; defaults to on in CI (short: -pl)" echo " --prepareMachine Prepare machine for CI run, clean up processes after build" echo " --nodeReuse Sets nodereuse msbuild parameter ('true' or 'false')" echo " --warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" echo " --warnNotAsError Sets a semi-colon delimited list of warning codes that should not be treated as errors" echo " --buildCheck Sets /check msbuild parameter" echo " --fromVMR Set when building from within the VMR" + echo " --disablePipelineSetResult Set to disable masking the actual exit code in the pipeline when the build fails" echo "" echo "Command line arguments not listed above are passed thru to msbuild." echo "Arguments can also be passed in with a single hyphen." @@ -68,6 +70,7 @@ build=false source_build=false product_build=false from_vmr=false +disable_pipeline_set_result=false rebuild=false test=false integration_test=false @@ -86,6 +89,7 @@ build_check=false binary_log=false binary_log_name='' exclude_ci_binary_log=false +pipelines_log=false projects='' configuration='' @@ -124,6 +128,9 @@ while [[ $# -gt 0 ]]; do -excludecibinarylog|-nobl) exclude_ci_binary_log=true ;; + -pipelineslog|-pl) + pipelines_log=true + ;; -restore|-r) restore=true ;; @@ -152,6 +159,9 @@ while [[ $# -gt 0 ]]; do -fromvmr|-from-vmr) from_vmr=true ;; + -disablepipelinesetresult|-disable-pipeline-set-result) + disable_pipeline_set_result=true + ;; -test|-t) test=true ;; @@ -213,6 +223,7 @@ if [[ -z "$configuration" ]]; then fi if [[ "$ci" == true ]]; then + pipelines_log=true # Disable node reuse on CI unless explicitly opted in via MSBUILD_NODEREUSE_ENABLED. # Internal testing only; this env var will be replaced with a switch (https://github.com/dotnet/arcade/issues/17013) and must not be depended on. if [[ "${MSBUILD_NODEREUSE_ENABLED:-}" != "1" ]]; then diff --git a/eng/common/core-templates/job/helix-job-monitor.yml b/eng/common/core-templates/job/helix-job-monitor.yml index 96287e55a15..0da13cf69db 100644 --- a/eng/common/core-templates/job/helix-job-monitor.yml +++ b/eng/common/core-templates/job/helix-job-monitor.yml @@ -65,6 +65,14 @@ parameters: type: boolean default: true +# When true, test results are reported to Azure DevOps using the fully qualified test name +# (Namespace.Type.Method) as the stable automatedTestName and the visible title is qualified as +# well (--use-fully-qualified-test-name). Opt-in because it changes AzDO test identity and display; +# primarily useful for frameworks like MSTest whose display name is only the method name. +- name: useFullyQualifiedTestName + type: boolean + default: false + # Advanced: optional pipeline artifact (produced earlier in this run) that contains the tool # nupkg. When set, the artifact is downloaded and the tool is installed from the nupkg into # a local tool-path; this bypasses the repo's .config/dotnet-tools.json manifest and is @@ -176,11 +184,12 @@ jobs: set -euo pipefail toolArgs=( - --helix-base-uri '${{ parameters.helixBaseUri }}' - --polling-interval-seconds '${{ parameters.pollingIntervalSeconds }}' - --fail-on-failed-tests '${{ parameters.failWorkItemsWithFailedTests }}' - --max-wait-minutes "$((${{ parameters.timeoutInMinutes }} - 5))" # Set the tool's timeout slightly lower than the Azure DevOps job timeout to allow it to exit gracefully. - --stage-name '$(System.StageName)' + --helix-base-uri '${{ parameters.helixBaseUri }}' + --polling-interval-seconds '${{ parameters.pollingIntervalSeconds }}' + --fail-on-failed-tests '${{ parameters.failWorkItemsWithFailedTests }}' + --use-fully-qualified-test-name '${{ parameters.useFullyQualifiedTestName }}' + --max-wait-minutes "$((${{ parameters.timeoutInMinutes }} - 5))" # Set the tool's timeout slightly lower than the Azure DevOps job timeout to allow it to exit gracefully. + --stage-name '$(System.StageName)' ) organization='${{ parameters.organization }}' diff --git a/eng/common/core-templates/job/onelocbuild.yml b/eng/common/core-templates/job/onelocbuild.yml index 86ea9f63504..2816d2905a0 100644 --- a/eng/common/core-templates/job/onelocbuild.yml +++ b/eng/common/core-templates/job/onelocbuild.yml @@ -8,6 +8,12 @@ parameters: CeapexPat: $(dn-bot-ceapex-package-r) # PAT for the loc AzDO instance https://dev.azure.com/ceapex GithubPat: $(BotAccount-dotnet-bot-repo-PAT) + # Service connection for WIF-based Entra authentication to ceapex feeds (replaces CeapexPat). + # When set, dnceng/internal builds acquire a federated Entra token instead of using a PAT. + # All other projects (e.g. DevDiv, public), where this dnceng-scoped service connection does not + # exist, and any pipeline that sets this to '' fall back to PAT-based auth via the CeapexPat parameter. + CeapexServiceConnection: 'dnceng-onelocbuild-ceapex' + SourcesDirectory: $(System.DefaultWorkingDirectory) CreatePr: true AutoCompletePr: false @@ -74,6 +80,15 @@ jobs: displayName: Generate LocProject.json condition: ${{ parameters.condition }} + # Acquire an Entra token for ceapex feed access via WIF (dnceng/internal only). + # All other projects use PAT-based auth, since the ceapex service connection is scoped to dnceng/internal. + - ${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}: + - template: /eng/common/templates/steps/get-federated-access-token.yml + parameters: + federatedServiceConnection: ${{ parameters.CeapexServiceConnection }} + outputVariableName: 'CeapexEntraToken' + condition: ${{ parameters.condition }} + - task: OneLocBuild@2 displayName: OneLocBuild env: @@ -89,7 +104,10 @@ jobs: isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }} isShouldReusePrSelected: ${{ parameters.ReusePr }} packageSourceAuth: patAuth - patVariable: ${{ parameters.CeapexPat }} + ${{ if and(ne(parameters.CeapexServiceConnection, ''), eq(variables['System.TeamProject'], 'internal')) }}: + patVariable: $(CeapexEntraToken) + ${{ if or(eq(parameters.CeapexServiceConnection, ''), ne(variables['System.TeamProject'], 'internal')) }}: + patVariable: ${{ parameters.CeapexPat }} ${{ if eq(parameters.RepoType, 'gitHub') }}: repoType: ${{ parameters.RepoType }} gitHubPatVariable: "${{ parameters.GithubPat }}" diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh index 453bb1ba5b1..38a3512f148 100644 --- a/eng/common/cross/build-rootfs.sh +++ b/eng/common/cross/build-rootfs.sh @@ -453,9 +453,12 @@ case "$__AlpineVersion" in elif [[ "$__AlpineArch" == "x86" ]]; then __AlpineVersion=3.17 # minimum version that supports lldb-dev __AlpinePackages+=" llvm15-libs" - elif [[ "$__AlpineArch" == "riscv64" || "$__AlpineArch" == "loongarch64" ]]; then + elif [[ "$__AlpineArch" == "loongarch64" ]]; then __AlpineVersion=3.21 # minimum version that supports lldb-dev __AlpinePackages+=" llvm19-libs" + elif [[ "$__AlpineArch" == "riscv64" ]]; then + __AlpineVersion=3.22 # lldb-dev requires 3.21+, but 3.22+ provides the newer linux-headers needed for RISC-V extension probes + __AlpinePackages+=" llvm20-libs" elif [[ -n "$__AlpineMajorVersion" ]]; then # use whichever alpine version is provided and select the latest toolchain libs __AlpineLlvmLibsLookup=1 diff --git a/eng/common/cross/toolchain.cmake b/eng/common/cross/toolchain.cmake index ead7fe3ef26..70b71395e3b 100644 --- a/eng/common/cross/toolchain.cmake +++ b/eng/common/cross/toolchain.cmake @@ -87,6 +87,8 @@ elseif(TARGET_ARCH_NAME STREQUAL "ppc64le") set(CMAKE_SYSTEM_PROCESSOR ppc64le) if(EXISTS ${CROSS_ROOTFS}/usr/lib/gcc/powerpc64le-alpine-linux-musl) set(TOOLCHAIN "powerpc64le-alpine-linux-musl") + elseif(FREEBSD) + set(TOOLCHAIN "powerpc64le-unknown-freebsd14") else() set(TOOLCHAIN "powerpc64le-linux-gnu") endif() diff --git a/eng/common/native/init-os-and-arch.sh b/eng/common/native/init-os-and-arch.sh index 38921d4338f..62d62fed522 100644 --- a/eng/common/native/init-os-and-arch.sh +++ b/eng/common/native/init-os-and-arch.sh @@ -27,6 +27,10 @@ if [ "$os" = "sunos" ]; then os="solaris" fi CPUName=$(isainfo -n) +elif [ "$os" = "freebsd" ]; then + # FreeBSD's `uname -m` is the machine class ("powerpc" for every PowerPC + # variant); `uname -p` gives the specific processor (e.g. powerpc64le). + CPUName=$(uname -p) else # For the rest of the operating systems, use uname(1) to determine what the CPU is. CPUName=$(uname -m) @@ -75,7 +79,7 @@ case "$CPUName" in arch=s390x ;; - ppc64le) + ppc64le|powerpc64le) arch=ppc64le ;; *) diff --git a/eng/common/sdk-task.ps1 b/eng/common/sdk-task.ps1 index 68119de603e..8d72d803dd2 100644 --- a/eng/common/sdk-task.ps1 +++ b/eng/common/sdk-task.ps1 @@ -4,7 +4,9 @@ Param( [string] $task, [string] $verbosity = 'minimal', [string] $msbuildEngine = $null, - [switch] $restore, + # Restore defaults to on; -restore is retained only so existing consumers that pass it don't break. Use -norestore to opt out. + [switch] $restore = $true, + [switch] $norestore, [switch] $prepareMachine, [switch][Alias('nobl')]$excludeCIBinaryLog, [switch]$noWarnAsError, @@ -18,12 +20,23 @@ $ci = $true $binaryLog = if ($excludeCIBinaryLog) { $false } else { $true } $warnAsError = if ($noWarnAsError) { $false } else { $true } +# Reconcile the restore state before importing tools.ps1: it reads $restore at import time to +# decide whether toolset/SDK acquisition installs. -norestore must win so that skipping restore +# also skips toolset initialization, not just the explicit Restore build below. +if ($norestore) { $restore = $false } + +# sdk-task runs a standalone Arcade SDK task and does not need repo-specific toolset setup. +# Skip importing configure-toolset.ps1 so its side effects (e.g. a repo's configure-toolset.ps1 +# calling exit) don't terminate this script before the task runs. +$disableConfigureToolsetImport = $true + . $PSScriptRoot\tools.ps1 function Print-Usage() { Write-Host "Common settings:" Write-Host " -task Name of Arcade task (name of a project in toolset directory of the Arcade SDK package)" - Write-Host " -restore Restore dependencies" + Write-Host " -restore (Legacy) Restore runs by default; retained for backward compatibility. Use -norestore to skip" + Write-Host " -norestore Skip restoring dependencies" Write-Host " -verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" Write-Host " -help Print help and exit" Write-Host "" diff --git a/eng/common/sdk-task.sh b/eng/common/sdk-task.sh index 1cf71bb2aea..a7f1ba060d7 100644 --- a/eng/common/sdk-task.sh +++ b/eng/common/sdk-task.sh @@ -3,7 +3,8 @@ show_usage() { echo "Common settings:" echo " --task Name of Arcade task (name of a project in toolset directory of the Arcade SDK package)" - echo " --restore Restore dependencies" + echo " --restore (Legacy) Restore runs by default; retained for backward compatibility. Use --norestore to skip" + echo " --norestore Skip restoring dependencies" echo " --verbosity Msbuild verbosity: q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]" echo " --help Print help and exit" echo "" @@ -50,10 +51,11 @@ binary_log=true configuration="Debug" verbosity="minimal" exclude_ci_binary_log=false -restore=false +# restore defaults to on; --restore is retained only so existing consumers that pass it don't break. Use --norestore to opt out. +restore=true help=false properties='' -warnAsError=true +warn_as_error=true while (($# > 0)); do lowerI="$(echo $1 | tr "[:upper:]" "[:lower:]")" @@ -63,7 +65,10 @@ while (($# > 0)); do shift 2 ;; --restore) - restore=true + shift 1 + ;; + --norestore) + restore=false shift 1 ;; --verbosity) @@ -75,8 +80,8 @@ while (($# > 0)); do exclude_ci_binary_log=true shift 1 ;; - --noWarnAsError) - warnAsError=false + --nowarnaserror) + warn_as_error=false shift 1 ;; --help) @@ -97,6 +102,11 @@ if $help; then exit 0 fi +# sdk-task runs a standalone Arcade SDK task and does not need repo-specific toolset setup. +# Skip importing configure-toolset.sh so its side effects (e.g. a repo's configure-toolset.sh +# calling exit) don't terminate this script before the task runs. +disable_configure_toolset_import=1 + . "$scriptroot/tools.sh" InitializeToolset diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 6f664ad890b..da07386ff1f 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -13,6 +13,12 @@ # Set to true to output binary log from msbuild. Note that emitting binary log slows down the build. [bool]$binaryLog = if (Test-Path variable:binaryLog) { $binaryLog } else { $ci -and !$excludeCIBinarylog } +# Set to true to use the pipelines logger which will enable Azure logging output. +# https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md +# This flag is meant as a temporary opt-in for the feature while validating it across +# our consumers. It will be deleted in the future. +[bool]$pipelinesLog = if (Test-Path variable:pipelinesLog) { $pipelinesLog } else { $ci } + # Turns on machine preparation/clean up code that changes the machine state (e.g. kills build processes). [bool]$prepareMachine = if (Test-Path variable:prepareMachine) { $prepareMachine } else { $false } @@ -65,6 +71,8 @@ $ErrorActionPreference = 'Stop' # True when the build is running within the VMR. [bool]$fromVMR = if (Test-Path variable:fromVMR) { $fromVMR } else { $false } +[bool]$disablePipelineSetResult = if (Test-Path variable:disablePipelineSetResult) { $disablePipelineSetResult } else { $false } + function Create-Directory ([string[]] $path) { New-Item -Path $path -Force -ItemType 'Directory' | Out-Null } @@ -713,7 +721,17 @@ function InitializeToolset() { $downloadArgs += "--configfile" $downloadArgs += $nugetConfig } - DotNet @downloadArgs + + # 'dotnet package download' fails outright if any source in the repo's NuGet.config is + # unavailable (for example a transport feed that was decommissioned after a release). The + # Arcade SDK is always published to the public dotnet-eng feed, so if the config-driven + # download fails, retry once against that feed directly (which ignores the other sources) + # before giving up, so a single dead source doesn't block the build. + $downloadExitCode = DotNet -ignoreFailure @downloadArgs + if ($downloadExitCode) { + Write-Host "Restoring the Arcade SDK from the configured sources failed; retrying from the public dotnet-eng feed." + DotNet @downloadArgs --source "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" + } $packageDir = Join-Path $nugetPackageCachePath (Join-Path 'microsoft.dotnet.arcade.sdk' $toolsetVersion) $packageToolsetDir = Join-Path $packageDir 'toolset' @@ -775,6 +793,19 @@ function MSBuild() { $buildTool = InitializeBuildTool + if ($pipelinesLog) { + $toolsetBuildProject = InitializeToolset + $basePath = Split-Path -parent $toolsetBuildProject + $selectedPath = Join-Path $basePath (Join-Path 'net' 'Microsoft.DotNet.ArcadeLogging.dll') + + # Only inject the logger when it's present. A last-known-good Arcade used to bootstrap + # the build may not ship the logger yet, so its absence must not be a hard error. + # Specify the logger type explicitly so loading is deterministic. + if (Test-Path $selectedPath) { + $args += "/logger:Microsoft.DotNet.ArcadeLogging.PipelinesLogger,$selectedPath" + } + } + $cmdArgs = "$($buildTool.Command) /m /nologo /clp:Summary /v:$verbosity /nr:$nodeReuse /p:ContinuousIntegrationBuild=$ci" # Add -mt flag for MSBuild multithreaded mode if enabled via environment variable @@ -790,7 +821,8 @@ function MSBuild() { } if ($warnAsError -and $warnNotAsError) { - $cmdArgs += " /warnnotaserror:$warnNotAsError /p:AdditionalWarningsNotAsErrors=$warnNotAsError" + $escapedWarnNotAsError = $warnNotAsError -replace ';', '%3B' + $cmdArgs += " /warnnotaserror:$warnNotAsError /p:AdditionalWarningsNotAsErrors=$escapedWarnNotAsError" } foreach ($arg in $args) { @@ -813,8 +845,8 @@ function MSBuild() { Write-Host "Build failed with exit code $exitCode. Check errors above." -ForegroundColor Red # When running on Azure Pipelines, override the returned exit code to avoid double logging. - # Skip this when the build is a child of the VMR build. - if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$fromVMR) { + # Skip this when the build is a child of the VMR build, or when -disablePipelineSetResult is set so the real exit code propagates. + if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$fromVMR -and !$disablePipelineSetResult) { Write-PipelineSetResult -Result "Failed" -Message "msbuild execution failed." # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error @@ -829,7 +861,7 @@ function MSBuild() { # Executes a dotnet command with arguments passed to the function. # Terminates the script if the command fails. # -function DotNet() { +function DotNet([switch]$ignoreFailure) { $dotnetRoot = InitializeDotNetCli -install:$restore $dotnetPath = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet') @@ -848,9 +880,15 @@ function DotNet() { $exitCode = Exec-Process $dotnetPath $cmdArgs if ($exitCode -ne 0) { + # When -ignoreFailure is set, return the exit code to the caller so it can implement + # its own fallback logic instead of terminating the script. + if ($ignoreFailure) { + return $exitCode + } + Write-Host "dotnet command failed with exit code $exitCode. Check errors above." -ForegroundColor Red - if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$fromVMR) { + if ($ci -and $env:SYSTEM_TEAMPROJECT -ne $null -and !$fromVMR -and !$disablePipelineSetResult) { Write-PipelineSetResult -Result "Failed" -Message "dotnet command execution failed." ExitWithExitCode 0 } else { diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 347a29b8884..20f791c11de 100644 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -8,6 +8,16 @@ ci=${ci:-false} # Build mode source_build=${source_build:-false} +# Set to true to use the pipelines logger which will enable Azure logging output. +# https://github.com/Microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md +# This flag is meant as a temporary opt-in for the feature while validating it across +# our consumers. It will be deleted in the future. +if [[ "$ci" == true ]]; then + pipelines_log=${pipelines_log:-true} +else + pipelines_log=${pipelines_log:-false} +fi + # Build configuration. Common values include 'Debug' and 'Release', but the repository may use other names. configuration=${configuration:-'Debug'} @@ -68,6 +78,8 @@ runtime_source_feed_key=${runtime_source_feed_key:-''} # True when the build is running within the VMR. from_vmr=${from_vmr:-false} +disable_pipeline_set_result=${disable_pipeline_set_result:-false} + # Resolve any symlinks in the given path. function ResolvePath { local path=$1 @@ -442,7 +454,16 @@ function InitializeToolset { if [[ -n "$nuget_config" ]]; then download_args+=("--configfile" "$nuget_config") fi - DotNet "${download_args[@]}" + + # 'dotnet package download' fails outright if any source in the repo's NuGet.config is + # unavailable (for example a transport feed that was decommissioned after a release). The + # Arcade SDK is always published to the public dotnet-eng feed, so if the config-driven + # download fails, retry once against that feed directly (which ignores the other sources) + # before giving up, so a single dead source doesn't block the build. + if ! DotNet true "${download_args[@]}"; then + echo "Restoring the Arcade SDK from the configured sources failed; retrying from the public dotnet-eng feed." + DotNet "${download_args[@]}" --source "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" + fi local package_dir="$_InitializeNuGetPackageCachePath/microsoft.dotnet.arcade.sdk/$toolset_version" @@ -481,6 +502,15 @@ function StopProcesses { } function DotNet { + # When the first argument is 'true' or 'false' it controls the exit behavior on failure: + # 'true' returns the dotnet exit code to the caller (so it can implement its own fallback), + # while the default terminates the script. Any other first argument is treated as a dotnet argument. + local ignore_failure=false + if [[ "$1" == 'true' || "$1" == 'false' ]]; then + ignore_failure="$1" + shift + fi + InitializeDotNetCli $restore local dotnet_path="$_InitializeDotNetCli/dotnet" @@ -489,9 +519,14 @@ function DotNet { "$dotnet_path" "$@" || { local exit_code=$? + + if [[ "$ignore_failure" == true ]]; then + return $exit_code + fi + echo "dotnet command failed with exit code $exit_code. Check errors above." - if [[ "$ci" == true && -n ${SYSTEM_TEAMPROJECT:-} && "$from_vmr" != true ]]; then + if [[ "$ci" == true && -n ${SYSTEM_TEAMPROJECT:-} && "$from_vmr" != true && "$disable_pipeline_set_result" != true ]]; then Write-PipelineSetResult -result "Failed" -message "dotnet command execution failed." ExitWithExitCode 0 else @@ -510,6 +545,21 @@ function MSBuild { InitializeBuildTool + local logger_switch=() + if [[ "$pipelines_log" == true ]]; then + InitializeToolset + + local toolset_dir="${_InitializeToolset%/*}" + local selectedPath="$toolset_dir/net/Microsoft.DotNet.ArcadeLogging.dll" + + # Only inject the logger when it's present. A last-known-good Arcade used to bootstrap + # the build may not ship the logger yet, so its absence must not be a hard error. + # Specify the logger type explicitly so loading is deterministic. + if [[ -f "$selectedPath" ]]; then + logger_switch=("-logger:Microsoft.DotNet.ArcadeLogging.PipelinesLogger,$selectedPath") + fi + fi + local warnaserror_switch="" if [[ $warn_as_error == true ]]; then warnaserror_switch="/warnaserror" @@ -525,8 +575,8 @@ function MSBuild { echo "Build failed with exit code $exit_code. Check errors above." # When running on Azure Pipelines, override the returned exit code to avoid double logging. - # Skip this when the build is a child of the VMR build. - if [[ "$ci" == true && -n ${SYSTEM_TEAMPROJECT:-} && "$from_vmr" != true ]]; then + # Skip this when the build is a child of the VMR build, or when -disablePipelineSetResult is set so the real exit code propagates. + if [[ "$ci" == true && -n ${SYSTEM_TEAMPROJECT:-} && "$from_vmr" != true && "$disable_pipeline_set_result" != true ]]; then Write-PipelineSetResult -result "Failed" -message "msbuild execution failed." # Exiting with an exit code causes the azure pipelines task to log yet another "noise" error # The above Write-PipelineSetResult will cause the task to be marked as failure without adding yet another error @@ -545,10 +595,10 @@ function MSBuild { local warnnotaserror_switch="" if [[ -n "$warn_not_as_error" && "$warn_as_error" == true ]]; then - warnnotaserror_switch="/warnnotaserror:$warn_not_as_error /p:AdditionalWarningsNotAsErrors=$warn_not_as_error" + warnnotaserror_switch="/warnnotaserror:$warn_not_as_error /p:AdditionalWarningsNotAsErrors=${warn_not_as_error//;/%3B}" fi - RunBuildTool "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch $mt_switch $warnnotaserror_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" + RunBuildTool "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch $mt_switch $warnnotaserror_switch ${logger_switch[@]+"${logger_switch[@]}"} /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" } function GetDarc { diff --git a/global.json b/global.json index b53a75f8d5d..246f19575f9 100644 --- a/global.json +++ b/global.json @@ -4,6 +4,6 @@ }, "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.134", - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26365.101" + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26376.106" } } From ebd916a18f47373755280cfadc5045ef57ebfc32 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 28 Jul 2026 08:48:42 -0500 Subject: [PATCH 02/14] Apply `*.runtimeconfig.dev.json` to CoreCLR apps Context: https://github.com/dotnet/sdk/pull/53715 Starting with `11.0.100-preview.7.26376.106`, the .NET SDK emits a `.runtimeconfig.dev.json` file for `Debug` builds containing the Hot Reload feature switches: { "runtimeOptions": { "configProperties": { "System.Reflection.Metadata.MetadataUpdater.IsSupported": true, "System.StartupHookProvider.IsSupported": true } } } `hostfxr` layers this file on top of `*.runtimeconfig.json` at startup, but .NET for Android does not use `hostfxr`: it bakes the runtime properties into the application at build time. So the file was simply ignored, and the switches never reached the app. Do the same layering at build time for CoreCLR: read the dev file after `*.runtimeconfig.json` in `RuntimePropertiesParser`, so its `configProperties` win. This matches what Blazor WebAssembly does in https://github.com/dotnet/runtime/pull/130825. This does not conflict with the switches we set ourselves. `$(StartupHookSupport)` is only set to `false` when `'$(Optimize)' == 'true'`, and the SDK only generates the dev file for `Debug` builds, so the two are disjoint in practice. We never set `$(MetadataUpdaterSupport)`. Mono is unchanged: it reads runtime properties from the `rc.bin` blob produced by `RuntimeConfigParserTask`, which only accepts a single input file. Teaching that task about multiple files requires a change in dotnet/runtime. While here, add both `runtimeconfig` files to `_GetGeneratePackageManagerJavaInputs`. Neither was an input before, so editing them would not trigger a rebuild. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- .../GenerateNativeApplicationConfigSources.cs | 3 +- .../Xamarin.Android.Build.Tests/BuildTest.cs | 2 + .../Tasks/RuntimePropertiesParserTests.cs | 105 ++++++++++++++++++ .../Utilities/RuntimePropertiesParser.cs | 49 ++++++-- .../Xamarin.Android.Common.targets | 4 + .../Tests/InstallAndRunTests.cs | 32 ++++++ 6 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/RuntimePropertiesParserTests.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index 17a4748d6fa..7d7b2eab820 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -61,6 +61,7 @@ public class GenerateNativeApplicationConfigSources : AndroidTask public bool AndroidEnableAssemblyStoreDecompressionCache { get; set; } public string? RuntimeConfigBinFilePath { get; set; } public string ProjectRuntimeConfigFilePath { get; set; } = String.Empty; + public string? ProjectRuntimeConfigDevFilePath { get; set; } public string? BoundExceptionType { get; set; } public string? PackageNamingPolicy { get; set; } @@ -266,7 +267,7 @@ static bool ShouldSkipAssembly (ITaskItem assembly) LLVMIR.LlvmIrComposer appConfigAsmGen; if (TargetsCLR) { - Dictionary? runtimeProperties = RuntimePropertiesParser.ParseConfig (ProjectRuntimeConfigFilePath); + Dictionary? runtimeProperties = RuntimePropertiesParser.ParseConfig (ProjectRuntimeConfigFilePath, ProjectRuntimeConfigDevFilePath); appConfigAsmGen = new ApplicationConfigNativeAssemblyGeneratorCLR (envBuilder.EnvironmentVariables, envBuilder.SystemProperties, runtimeProperties, Log) { UsesAssemblyPreload = envBuilder.Parser.UsesAssemblyPreload, AndroidPackageName = AndroidPackageName, diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index d5641a715e9..ef194705183 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -140,6 +140,8 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease, bool aot, bo } else { expectedFiles.Add ($"{proj.PackageName}.apk"); expectedFiles.Add ($"{proj.PackageName}-Signed.apk.idsig"); + // The .NET SDK emits this in Debug to enable Hot Reload switches + expectedFiles.Add ($"{proj.ProjectName}.runtimeconfig.dev.json"); } expectedFiles.Sort(StringComparer.OrdinalIgnoreCase); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/RuntimePropertiesParserTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/RuntimePropertiesParserTests.cs new file mode 100644 index 00000000000..a3128403b32 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/RuntimePropertiesParserTests.cs @@ -0,0 +1,105 @@ +#nullable enable +using System.Collections.Generic; +using System.IO; + +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests.Tasks; + +[TestFixture] +public class RuntimePropertiesParserTests : BaseTest +{ + const string RuntimeConfigJson = """ + { + "runtimeOptions": { + "tfm": "net11.0", + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, + "System.StartupHookProvider.IsSupported": false, + "Microsoft.Android.Runtime.RuntimeFeature.IsAssignableFromCheck": true + } + } + } + """; + + string directory = ""; + string runtimeConfigPath = ""; + string runtimeConfigDevPath = ""; + + [SetUp] + public void SetUp () + { + directory = Path.Combine (Root, "temp", TestName); + Directory.CreateDirectory (directory); + runtimeConfigPath = Path.Combine (directory, "App.runtimeconfig.json"); + runtimeConfigDevPath = Path.Combine (directory, "App.runtimeconfig.dev.json"); + File.WriteAllText (runtimeConfigPath, RuntimeConfigJson); + } + + static Dictionary Parse (string runtimeConfigPath, string? runtimeConfigDevPath = null) => + RuntimePropertiesParser.ParseConfig (runtimeConfigPath, runtimeConfigDevPath) ?? + throw new AssertionException ($"Could not parse '{runtimeConfigPath}'."); + + [Test] + public void MissingConfigReturnsNull () + { + Assert.IsNull (RuntimePropertiesParser.ParseConfig (Path.Combine (directory, "does-not-exist.json"))); + } + + [Test] + public void ConfigPropertiesAreParsed () + { + var properties = Parse (runtimeConfigPath); + + Assert.AreEqual ("false", properties ["System.Reflection.Metadata.MetadataUpdater.IsSupported"]); + Assert.AreEqual ("true", properties ["Microsoft.Android.Runtime.RuntimeFeature.IsAssignableFromCheck"]); + } + + [Test] + public void DevConfigPropertiesWin () + { + // This is what the .NET SDK emits for `Debug` builds + File.WriteAllText (runtimeConfigDevPath, """ + { + "runtimeOptions": { + "configProperties": { + "System.Reflection.Metadata.MetadataUpdater.IsSupported": true, + "System.StartupHookProvider.IsSupported": true + } + } + } + """); + + var properties = Parse (runtimeConfigPath, runtimeConfigDevPath); + + Assert.AreEqual ("true", properties ["System.Reflection.Metadata.MetadataUpdater.IsSupported"], "Hot Reload should be enabled by the dev file."); + Assert.AreEqual ("true", properties ["System.StartupHookProvider.IsSupported"], "Startup hooks should be enabled by the dev file."); + Assert.AreEqual ("true", properties ["Microsoft.Android.Runtime.RuntimeFeature.IsAssignableFromCheck"], "Properties absent from the dev file should be preserved."); + } + + [Test] + public void DevConfigWithoutConfigPropertiesIsIgnored () + { + // Pre-net6.0 style dev file, which only carries probing paths + File.WriteAllText (runtimeConfigDevPath, """ + { + "runtimeOptions": { + "additionalProbingPaths": [ "/home/user/.nuget/packages" ] + } + } + """); + + var properties = Parse (runtimeConfigPath, runtimeConfigDevPath); + + Assert.AreEqual ("false", properties ["System.Reflection.Metadata.MetadataUpdater.IsSupported"]); + } + + [Test] + public void MissingDevConfigIsIgnored () + { + var properties = Parse (runtimeConfigPath, runtimeConfigDevPath); + + Assert.AreEqual ("false", properties ["System.StartupHookProvider.IsSupported"]); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/RuntimePropertiesParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/RuntimePropertiesParser.cs index bf47af55265..36830ec50ed 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/RuntimePropertiesParser.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/RuntimePropertiesParser.cs @@ -7,30 +7,61 @@ namespace Xamarin.Android.Tasks; class RuntimePropertiesParser { - public static Dictionary? ParseConfig (string projectRuntimeConfigFilePath) + /// + /// Reads the `configProperties` from `*.runtimeconfig.json`, layering the ones from the + /// companion `*.runtimeconfig.dev.json` on top when it exists. + /// + /// `hostfxr` performs this merge when the app starts, but .NET for Android does not use + /// `hostfxr`: the properties are baked into the app at build time and handed to + /// `coreclr_initialize()`, so the merge has to happen here instead. The .NET SDK uses the + /// dev file to turn on Hot Reload switches for `Debug` builds. + /// + public static Dictionary? ParseConfig (string projectRuntimeConfigFilePath, string? projectRuntimeConfigDevFilePath = null) { if (String.IsNullOrEmpty (projectRuntimeConfigFilePath) || !File.Exists (projectRuntimeConfigFilePath)) { return null; } - using var fs = File.OpenRead (projectRuntimeConfigFilePath); + var ret = new Dictionary (StringComparer.OrdinalIgnoreCase); + AddConfigProperties (projectRuntimeConfigFilePath, ret, required: true); + + if (!String.IsNullOrEmpty (projectRuntimeConfigDevFilePath) && File.Exists (projectRuntimeConfigDevFilePath)) { + // Values from the dev file win, matching `hostfxr` + AddConfigProperties (projectRuntimeConfigDevFilePath, ret, required: false); + } + + return ret; + } + + static void AddConfigProperties (string path, Dictionary properties, bool required) + { + using var fs = File.OpenRead (path); var jsonOptions = new JsonDocumentOptions { AllowTrailingCommas = true, // yes, please! CommentHandling = JsonCommentHandling.Skip, }; using JsonDocument config = JsonDocument.Parse (fs, jsonOptions); - JsonElement runtimeOptions = config.RootElement.GetProperty ("runtimeOptions"); - JsonElement properties = runtimeOptions.GetProperty ("configProperties"); - var ret = new Dictionary (StringComparer.OrdinalIgnoreCase); - foreach (JsonProperty prop in properties.EnumerateObject ()) { + + JsonElement configProperties; + if (required) { + JsonElement runtimeOptions = config.RootElement.GetProperty ("runtimeOptions"); + configProperties = runtimeOptions.GetProperty ("configProperties"); + } else { + // The dev file is generated by the .NET SDK and may legitimately have neither key, + // such as when it only contains `additionalProbingPaths`. + if (!config.RootElement.TryGetProperty ("runtimeOptions", out JsonElement runtimeOptions) || + !runtimeOptions.TryGetProperty ("configProperties", out configProperties)) { + return; + } + } + + foreach (JsonProperty prop in configProperties.EnumerateObject ()) { string? value = GetJsonValueAsString (prop.Value); if (value is not null) { - ret[prop.Name] = value; + properties[prop.Name] = value; } } - - return ret; } static string? GetJsonValueAsString (JsonElement element) => diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 7b91ddeb5cb..fb0c50fd594 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1748,6 +1748,9 @@ because xbuild doesn't support framework reference assemblies. <_GeneratePackageManagerJavaInputs Include="@(_GenerateJavaStubsInputs)" /> <_GeneratePackageManagerJavaInputs Include="$(_XARemapMembersFilePath)" Condition=" '@(_AndroidRemapMembers->Count())' != '0' " /> + + <_GeneratePackageManagerJavaInputs Include="$(ProjectRuntimeConfigFilePath)" Condition="Exists('$(ProjectRuntimeConfigFilePath)')" /> + <_GeneratePackageManagerJavaInputs Include="$(ProjectRuntimeConfigDevFilePath)" Condition="Exists('$(ProjectRuntimeConfigDevFilePath)')" /> @@ -1819,6 +1822,7 @@ because xbuild doesn't support framework reference assemblies. TargetsCLR="$(_AndroidUseCLR)" AndroidRuntime="$(_AndroidRuntime)" ProjectRuntimeConfigFilePath="$(ProjectRuntimeConfigFilePath)" + ProjectRuntimeConfigDevFilePath="$(ProjectRuntimeConfigDevFilePath)" > diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 4ff7544eefb..5b8e59fdc88 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -934,6 +934,38 @@ public void SubscribeToAppDomainUnhandledException ([Values (AndroidRuntime.Core } + [Test] + public void RuntimeConfigDevJsonIsApplied () + { + var proj = new XamarinAndroidApplicationProject { + IsRelease = false, + }; + proj.SetRuntimeIdentifiers (new [] {"arm64-v8a", "x86_64"}); + + // For `Debug` builds the .NET SDK writes these two switches into `*.runtimeconfig.dev.json` + // and nowhere else. `hostfxr` layers that file over `*.runtimeconfig.json` at startup, but + // .NET for Android bakes the properties into the app at build time, so seeing them here + // proves the dev file was picked up. Asking for `$(StartupHookSupport)` to be `false` also + // proves the dev file wins over `*.runtimeconfig.json`. + proj.SetProperty ("StartupHookSupport", "false"); + + proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", +@" AppContext.TryGetSwitch (""System.StartupHookProvider.IsSupported"", out bool startupHookProvider); + AppContext.TryGetSwitch (""System.Reflection.Metadata.MetadataUpdater.IsSupported"", out bool metadataUpdater); + Console.WriteLine (""# runtimeconfig.dev.json: StartupHookProvider={0}; MetadataUpdater={1}"", + startupHookProvider, metadataUpdater); +"); + builder = CreateApkBuilder (); + Assert.IsTrue (builder.Install (proj), "Install should have succeeded."); + RunProjectAndAssert (proj, builder); + + const string expectedLogcatOutput = "# runtimeconfig.dev.json: StartupHookProvider=True; MetadataUpdater=True"; + Assert.IsTrue ( + MonitorAdbLogcat (CreateLineChecker (expectedLogcatOutput), + logcatFilePath: Path.Combine (Root, builder.ProjectDirectory, "runtimeconfig-logcat.log"), timeout: 60), + $"Output did not contain {expectedLogcatOutput}!"); + } + public static Func CreateLineChecker (string expectedLogcatOutput) { // On .NET 6, `adb logcat` output may be line-wrapped in unexpected ways. From 097a98226b701148b4194ffab0cefd76aa2206cf Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 28 Jul 2026 10:23:22 -0500 Subject: [PATCH 03/14] Fix CS8604 in `RuntimePropertiesParser` `String.IsNullOrEmpty()` is not NRT annotated in `netstandard2.0`, so the compiler could not tell that `projectRuntimeConfigDevFilePath` is not `null` inside the `if`. Use our own `IsNullOrEmpty()` extension method, which is annotated with `[NotNullWhen (false)]`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- .../Utilities/RuntimePropertiesParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/RuntimePropertiesParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/RuntimePropertiesParser.cs index 36830ec50ed..432e5ffd259 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/RuntimePropertiesParser.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/RuntimePropertiesParser.cs @@ -25,7 +25,7 @@ class RuntimePropertiesParser var ret = new Dictionary (StringComparer.OrdinalIgnoreCase); AddConfigProperties (projectRuntimeConfigFilePath, ret, required: true); - if (!String.IsNullOrEmpty (projectRuntimeConfigDevFilePath) && File.Exists (projectRuntimeConfigDevFilePath)) { + if (!projectRuntimeConfigDevFilePath.IsNullOrEmpty () && File.Exists (projectRuntimeConfigDevFilePath)) { // Values from the dev file win, matching `hostfxr` AddConfigProperties (projectRuntimeConfigDevFilePath, ret, required: false); } From 092678d1157378ca5fc710e4164eb3a4ccbd51a2 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 28 Jul 2026 13:37:07 -0500 Subject: [PATCH 04/14] Ignore `DotNetNewAndroidTest` for `dotnet test` Context: https://github.com/dotnet/android/issues/12254 Starting with .NET SDK `11.0.100-preview.7.26376.106`, `dotnet test` is device-aware: it selects a device with `ComputeAvailableDevices`, then deploys with `DeployToDevice` and computes the launch command with `ComputeRunArguments`. Those are separate `ProjectInstance.Build ()` calls against the same `ProjectInstance`, so `ProcessFrameworkReferences` runs twice. MSBuild task outputs append, so `@(ResolvedRuntimePack)` ends up with two `Microsoft.NETCore.App` entries and `ResolveFrameworkReferences` throws: error MSB4018: The "ResolveFrameworkReferences" task failed unexpectedly. System.ArgumentException: An item with the same key has already been added. Key: Microsoft.NETCore.App This reproduces with a stock SDK and the released `Microsoft.Android.Sdk.Windows` `37.0.0-preview.6.59`, so it is not caused by anything in this repo. Pinning a single `$(RuntimeIdentifier)` does not help, and `dotnet run` works on the same project, so there is nothing we can do from our targets. Ignore the `dotnet test` cases until the fix flows back from dotnet/sdk. The `dotnet run` case is unaffected and stays enabled. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 5b8e59fdc88..c7624c441eb 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2679,6 +2679,9 @@ public enum MSTestPackageChannel [TestCase ("test", MSTestPackageChannel.Nightly)] public void DotNetNewAndroidTest (string mode, MSTestPackageChannel msTestPackageChannel) { + if (mode == "test") + Assert.Ignore ("`dotnet test` fails with a duplicate `Microsoft.NETCore.App` runtime pack: https://github.com/dotnet/android/issues/12254"); + var templateName = $"DotNetNewAndroidTest_{mode}_{msTestPackageChannel}"; var projectDirectory = Path.Combine (Root, "temp", templateName); if (Directory.Exists (projectDirectory)) From 5775af61757b0dabfe35d467e35022ced9c59396 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Wed, 29 Jul 2026 02:01:53 +0000 Subject: [PATCH 05/14] Update dependencies from https://github.com/dotnet/dotnet build 20260728.6 On relative base path root Microsoft.DotNet.Arcade.Sdk , Microsoft.DotNet.Build.Tasks.Feed From Version 11.0.0-beta.26365.101 -> To Version 11.0.0-beta.26378.106 Microsoft.DotNet.Cecil From Version 0.11.5-preview.26365.101 -> To Version 0.11.5-preview.26378.106 Microsoft.NET.ILLink , Microsoft.NETCore.App.Ref From Version 11.0.0-preview.7.26365.101 -> To Version 11.0.0-preview.7.26378.106 Microsoft.NET.Sdk , Microsoft.TemplateEngine.Authoring.Tasks From Version 11.0.100-preview.7.26365.101 -> To Version 11.0.100-preview.7.26378.106 --- eng/Version.Details.xml | 28 ++++++++++++++-------------- eng/Versions.props | 12 ++++++------ global.json | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index acae7dfa890..f8c6a07001b 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,20 +1,20 @@ - + https://github.com/dotnet/dotnet - 7bcb9009c40399c9073e7db938250486229cc85d + ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 - + https://github.com/dotnet/dotnet - 7bcb9009c40399c9073e7db938250486229cc85d + ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 - + https://github.com/dotnet/dotnet - 7bcb9009c40399c9073e7db938250486229cc85d + ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 - + https://github.com/dotnet/dotnet - 7bcb9009c40399c9073e7db938250486229cc85d + ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 https://github.com/dotnet/dotnet @@ -36,17 +36,17 @@ - + https://github.com/dotnet/dotnet - 7bcb9009c40399c9073e7db938250486229cc85d + ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 - + https://github.com/dotnet/dotnet - 7bcb9009c40399c9073e7db938250486229cc85d + ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 - + https://github.com/dotnet/dotnet - 7bcb9009c40399c9073e7db938250486229cc85d + ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 https://github.com/microsoft/testfx diff --git a/eng/Versions.props b/eng/Versions.props index 04b7b1da665..c5c22ef6288 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,20 +1,20 @@ - 11.0.100-preview.7.26376.106 + 11.0.100-preview.7.26378.106 $(MicrosoftNETSdkPackageVersion) - 11.0.0-preview.7.26376.106 - 11.0.0-preview.7.26376.106 + 11.0.0-preview.7.26378.106 + 11.0.0-preview.7.26378.106 7.0.0-beta.22103.1 11.0.0-beta.26060.102 - 11.0.0-beta.26376.106 + 11.0.0-beta.26378.106 11.0.100-preview.4.26215.121 11.0.100-preview.4.26215.121 $(MicrosoftNETWorkloadMonoToolChainCurrentManifest110100preview4PackageVersion) $(MicrosoftNETWorkloadEmscriptenCurrentManifest110100preview4PackageVersion) - 11.0.100-preview.7.26376.106 - 0.11.5-preview.26376.106 + 11.0.100-preview.7.26378.106 + 0.11.5-preview.26378.106 4.4.0-preview.26367.7 10.0.10 11.0.0-preview.1.26104.118 diff --git a/global.json b/global.json index 246f19575f9..c993596b7ff 100644 --- a/global.json +++ b/global.json @@ -4,6 +4,6 @@ }, "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.134", - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26376.106" + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26378.106" } } From ce407b5e0b6a91dd267dd898133d764cc86077e1 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 29 Jul 2026 09:51:03 -0500 Subject: [PATCH 06/14] [ci] Work around broken `dotnet test` by using `dotnet run` `dotnet test` currently fails to deploy to a device, so every APK instrumentation lane fails with "Deployment to device failed. Fix any deployment errors and run again." See dotnet/android#12254. Drive the instrumentation with `dotnet run` instead. We do not lose the test report: the on-device runner writes its own TRX and reports the location back through the instrumentation bundle as `INSTRUMENTATION_RESULT: resultsPath=`. `dotnet run` invokes `adb shell am instrument -w`, which prints that bundle, so the path can be scraped and the TRX pulled to where `PublishTestResults` expects it. No product code changes; revert to `dotnet test` once the SDK fix lands. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- .../yaml-templates/apk-instrumentation.yaml | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/build-tools/automation/yaml-templates/apk-instrumentation.yaml b/build-tools/automation/yaml-templates/apk-instrumentation.yaml index d3c6bf54183..580cb6df7ce 100644 --- a/build-tools/automation/yaml-templates/apk-instrumentation.yaml +++ b/build-tools/automation/yaml-templates/apk-instrumentation.yaml @@ -56,14 +56,23 @@ steps: continueOnError: true timeoutInMinutes: 3 - # Run dotnet test from the project directory so it finds the project-local - # global.json with "test": { "runner": "Microsoft.Testing.Platform" }. - # This enables MTP mode, which uses RunCommand/RunArguments to invoke - # Microsoft.Android.Run for on-device test execution. + # WORKAROUND: 'dotnet test' currently fails to deploy to a device (dotnet/android#12254), + # so drive the instrumentation with 'dotnet run' instead. + # + # We do not lose the test report by doing so: the on-device test runner writes its + # own TRX (tests/TestRunner.Core/TestInstrumentation.cs) and reports the location + # back through the instrumentation bundle as + # 'INSTRUMENTATION_RESULT: resultsPath='. 'dotnet run' invokes + # 'adb shell am instrument -w', which prints that bundle, so we can scrape the path + # and pull the TRX to where PublishTestResults expects it. + # + # Revert to 'dotnet test' once the SDK fix flows in. - powershell: | + $ErrorActionPreference = "Continue" $projectDir = Split-Path -Parent "${{ parameters.project }}" $projectFile = Split-Path -Leaf "${{ parameters.project }}" $resultsDir = "${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/TestResults" + New-Item -ItemType Directory -Force -Path $resultsDir | Out-Null Push-Location $projectDir if ([Environment]::OSVersion.Platform -eq "Unix") { $DOTNET_ROOT = "${{ parameters.xaSourcePath }}/bin/${{ parameters.buildConfiguration }}/dotnet" @@ -74,8 +83,23 @@ steps: $env:PATH = "${DOTNET_ROOT};$env:PATH" $dotnetPath = "${DOTNET_ROOT}\dotnet.exe" } - & $dotnetPath test $projectFile --no-build -bl:${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/run-${{ parameters.testName }}.binlog -c ${{ parameters.configuration }} --results-directory $resultsDir --report-trx --report-trx-filename ${{ parameters.testName }}.trx --output Detailed ${{ parameters.extraBuildArgs }} + & $dotnetPath run --project $projectFile --no-build -bl:${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/run-${{ parameters.testName }}.binlog -c ${{ parameters.configuration }} ${{ parameters.extraBuildArgs }} 2>&1 | Tee-Object -Variable runOutput + $runExitCode = $LASTEXITCODE Pop-Location + $match = $runOutput | Select-String -Pattern 'INSTRUMENTATION_RESULT:\s*resultsPath=(.+)$' | Select-Object -Last 1 + if (-not $match) { + Write-Host "##vso[task.logissue type=error]Instrumentation did not report a resultsPath; no TRX to publish. 'dotnet run' exited with $runExitCode." + exit 1 + } + $devicePath = $match.Matches[0].Groups[1].Value.Trim() + $trxPath = "$resultsDir/${{ parameters.testName }}.trx" + Write-Host "Pulling TRX from device: $devicePath" + & adb pull "$devicePath" "$trxPath" + if (-not (Test-Path $trxPath)) { + Write-Host "##vso[task.logissue type=error]Failed to pull TRX from device path '$devicePath'." + exit 1 + } + exit $runExitCode displayName: run ${{ parameters.testName }} condition: ${{ parameters.condition }} continueOnError: true From 7561e4d11bc7c87bebb42891eafe050dbcba3e9b Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 29 Jul 2026 11:13:48 -0500 Subject: [PATCH 07/14] [tests] Don't bind Java.Interop-Tests' own test JAR BuildTestJarFile writes the test JAR to Jars\ inside the project directory. The .NET Android SDK's default @(AndroidLibrary) glob is `**\*.jar` and %(Bind) defaults to true, so once that file exists on disk any subsequent evaluation of the project picks it up as a JAR to bind. That does not happen during a plain `-t:Install` build, because the JAR is produced by the same build that globbed for it. It does happen on the second MSBuild submission that `dotnet test` / `dotnet run` make when deploying to a device: the JAR is already on disk, gets bound, and the generated Net.Dot.Jni.Test.* sources fail to compile with CS0108/CS0114/CS0436. That is fatal here because this project sets TreatWarningsAsErrors=true, and it surfaces only as the generic "Deployment to device failed" message, taking out every APK instrumentation lane. Remove the JAR from @(AndroidLibrary); it is already included explicitly as @(AndroidJavaLibrary), which packages it without binding it. See dotnet/android#12254. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- .../Java.Interop-Tests/Java.Interop-Tests.targets | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index 838470a434b..6a3e5e514ac 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -6,6 +6,18 @@ + + From c6ee6a62f436bff2670fc3840e474b634353ece4 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 29 Jul 2026 11:19:35 -0500 Subject: [PATCH 08/14] [tests] List test JARs explicitly instead of globbing Use the exact JAR file names rather than `Jars\**\*.jar`. Both the non-trimmable and trimmable variants are listed, since a single job builds several test configurations in one workspace and the two can coexist. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- .../Java.Interop-Tests.targets | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index 6a3e5e514ac..92fca4bef0a 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -7,17 +7,13 @@ - + + From f8bd456b2a0c3ac1048c2d3096be3848d347deb4 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 29 Jul 2026 12:58:23 -0500 Subject: [PATCH 09/14] [ci] Set DOTNET_HOST_PATH for the `dotnet run` test step `dotnet run` deploys to a device using an in-process MSBuild, bypassing the `MSBuildForwardingApp` that normally sets `DOTNET_HOST_PATH`. MSBuild needs that variable to bootstrap task hosts, so ILLink's `ComputeManagedAssemblies` (`Runtime="NET"`) failed with: error MSB4221: Could not run the "ComputeManagedAssemblies" task because MSBuild could not create or connect to a task host with runtime "NET" and architecture "x64" ... DOTNET_HOST_PATH is not set. Set it explicitly to the same `dotnet` we invoke. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- .../automation/yaml-templates/apk-instrumentation.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build-tools/automation/yaml-templates/apk-instrumentation.yaml b/build-tools/automation/yaml-templates/apk-instrumentation.yaml index 580cb6df7ce..2258277aef7 100644 --- a/build-tools/automation/yaml-templates/apk-instrumentation.yaml +++ b/build-tools/automation/yaml-templates/apk-instrumentation.yaml @@ -83,6 +83,10 @@ steps: $env:PATH = "${DOTNET_ROOT};$env:PATH" $dotnetPath = "${DOTNET_ROOT}\dotnet.exe" } + # 'dotnet run' deploys to the device with an in-process MSBuild, which skips the + # MSBuildForwardingApp that normally sets DOTNET_HOST_PATH. Without it, MSBuild + # cannot launch task hosts (MSB4221 on ILLink's ComputeManagedAssemblies). + $env:DOTNET_HOST_PATH = $dotnetPath & $dotnetPath run --project $projectFile --no-build -bl:${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/run-${{ parameters.testName }}.binlog -c ${{ parameters.configuration }} ${{ parameters.extraBuildArgs }} 2>&1 | Tee-Object -Variable runOutput $runExitCode = $LASTEXITCODE Pop-Location From 317c1ae1f5b63f51cc3265c9dab08626bba1cd3f Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 29 Jul 2026 16:12:11 -0500 Subject: [PATCH 10/14] [ci] Use a single `dotnet run` for APK instrumentation lanes Drop the separate `-t:Install` build step. `dotnet run` builds, installs and runs in one invocation. The two-step shape was broken: `_CollectJavaSourceForBinding` and `_CompileBindingJava` only run as part of `Build`, so when the deploy pass rebuilt anything it re-ran `_CompileJava` without the binding classes jar on javac's classpath, producing `XAJVC0000: cannot find symbol`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- .../yaml-templates/apk-instrumentation.yaml | 80 +++++++------------ 1 file changed, 29 insertions(+), 51 deletions(-) diff --git a/build-tools/automation/yaml-templates/apk-instrumentation.yaml b/build-tools/automation/yaml-templates/apk-instrumentation.yaml index 2258277aef7..407a1499f32 100644 --- a/build-tools/automation/yaml-templates/apk-instrumentation.yaml +++ b/build-tools/automation/yaml-templates/apk-instrumentation.yaml @@ -11,53 +11,9 @@ parameters: condition: succeeded() steps: - # Build and install the APK to the device first, so that ComputeRunArguments - # can resolve the package name from AndroidManifest.xml. - # - # Retry the install up to 3 times: the emulator/ADB connection occasionally - # drops mid-install (XAGCPU7000 "device offline") and a retry usually recovers. - # If every attempt fails, continueOnError: false fails the lane immediately - - # there is no point running the tests against a device where the app was never - # installed (that only wastes CI time and produces a misleading - # INSTRUMENTATION_FAILED). See dotnet/android#11830. -- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml - parameters: - configuration: ${{ parameters.buildConfiguration }} - xaSourcePath: ${{ parameters.xaSourcePath }} - displayName: build ${{ parameters.testName }} - project: ${{ parameters.project }} - arguments: >- - -t:Install - -bl:${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/build-${{ parameters.testName }}.binlog - -c ${{ parameters.configuration }} ${{ parameters.extraBuildArgs }} - condition: ${{ parameters.condition }} - retryCountOnTaskFailure: 3 - continueOnError: false - - # If the install above failed, snapshot device state so we can classify WHY on - # the next iteration instead of guessing: connectivity (adb devices/get-state), - # disk pressure (df), storage-service readiness (the StorageStatsManager NPE - # from install-create), boot completion, and how many test apps have piled up. - # Best-effort and time-bounded so it never hangs or fails the lane itself. -- script: | - set +e - echo "===== adb devices -l ====="; adb devices -l - echo "===== adb get-state ====="; adb get-state - echo "===== boot completion ====="; adb shell getprop sys.boot_completed; adb shell getprop dev.bootcomplete - echo "===== df /data ====="; adb shell df /data - echo "===== df /storage/emulated/0 ====="; adb shell df /storage/emulated/0 - echo "===== dumpsys diskstats ====="; adb shell dumpsys diskstats - echo "===== storaged (storage stats service) ====="; adb shell dumpsys storaged - echo "===== installed 3rd-party packages ====="; adb shell pm list packages -3 - echo "===== end device state =====" - exit 0 - displayName: diagnose install failure ${{ parameters.testName }} - condition: failed() - continueOnError: true - timeoutInMinutes: 3 - # WORKAROUND: 'dotnet test' currently fails to deploy to a device (dotnet/android#12254), - # so drive the instrumentation with 'dotnet run' instead. + # so drive the instrumentation with 'dotnet run' instead. A single 'dotnet run' builds, + # installs and runs, so there is no separate build step. # # We do not lose the test report by doing so: the on-device test runner writes its # own TRX (tests/TestRunner.Core/TestInstrumentation.cs) and reports the location @@ -87,7 +43,7 @@ steps: # MSBuildForwardingApp that normally sets DOTNET_HOST_PATH. Without it, MSBuild # cannot launch task hosts (MSB4221 on ILLink's ComputeManagedAssemblies). $env:DOTNET_HOST_PATH = $dotnetPath - & $dotnetPath run --project $projectFile --no-build -bl:${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/run-${{ parameters.testName }}.binlog -c ${{ parameters.configuration }} ${{ parameters.extraBuildArgs }} 2>&1 | Tee-Object -Variable runOutput + & $dotnetPath run --project $projectFile -bl:${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/run-${{ parameters.testName }}.binlog -c ${{ parameters.configuration }} ${{ parameters.extraBuildArgs }} 2>&1 | Tee-Object -Variable runOutput $runExitCode = $LASTEXITCODE Pop-Location $match = $runOutput | Select-String -Pattern 'INSTRUMENTATION_RESULT:\s*resultsPath=(.+)$' | Select-Object -Last 1 @@ -108,6 +64,28 @@ steps: condition: ${{ parameters.condition }} continueOnError: true + # If the deploy above failed, snapshot device state so we can classify WHY on + # the next iteration instead of guessing: connectivity (adb devices/get-state), + # disk pressure (df), storage-service readiness (the StorageStatsManager NPE + # from install-create), boot completion, and how many test apps have piled up. + # Best-effort and time-bounded so it never hangs or fails the lane itself. +- script: | + set +e + echo "===== adb devices -l ====="; adb devices -l + echo "===== adb get-state ====="; adb get-state + echo "===== boot completion ====="; adb shell getprop sys.boot_completed; adb shell getprop dev.bootcomplete + echo "===== df /data ====="; adb shell df /data + echo "===== df /storage/emulated/0 ====="; adb shell df /storage/emulated/0 + echo "===== dumpsys diskstats ====="; adb shell dumpsys diskstats + echo "===== storaged (storage stats service) ====="; adb shell dumpsys storaged + echo "===== installed 3rd-party packages ====="; adb shell pm list packages -3 + echo "===== end device state =====" + exit 0 + displayName: diagnose install failure ${{ parameters.testName }} + condition: failed() + continueOnError: true + timeoutInMinutes: 3 + - script: > DEST="$(Build.StagingDirectory)/Test${{ parameters.configuration }}/${{ parameters.artifactFolder }}/" && mkdir -p "$DEST" && @@ -123,10 +101,10 @@ steps: # any UnsatisfiedLinkError / dlopen messages only appear in logcat. # # Use always() (not the default succeeded()) so we still capture logcat when an - # earlier step failed or the job was canceled - in particular a failed -t:Install - # (which now fails the lane fast) or a hung/timed-out test run. Losing logcat in - # exactly those cases would defeat the diagnostics this template exists for; the - # capture is best-effort (continueOnError + '|| echo'). See dotnet/android#11830. + # earlier step failed or the job was canceled - in particular a failed deploy or a + # hung/timed-out test run. Losing logcat in exactly those cases would defeat the + # diagnostics this template exists for; the capture is best-effort + # (continueOnError + '|| echo'). See dotnet/android#11830. - script: > DEST="$(Build.StagingDirectory)/Test${{ parameters.configuration }}/${{ parameters.artifactFolder }}/" && mkdir -p "$DEST" && From e399d904d6db200db83ca68205f5bca1ac194412 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Thu, 30 Jul 2026 02:01:40 +0000 Subject: [PATCH 11/14] Update dependencies from https://github.com/dotnet/dotnet build 20260728.19 On relative base path root Microsoft.DotNet.Arcade.Sdk , Microsoft.DotNet.Build.Tasks.Feed From Version 11.0.0-beta.26365.101 -> To Version 11.0.0-beta.26378.119 Microsoft.DotNet.Cecil From Version 0.11.5-preview.26365.101 -> To Version 0.11.5-preview.26378.119 Microsoft.NET.ILLink , Microsoft.NETCore.App.Ref From Version 11.0.0-preview.7.26365.101 -> To Version 11.0.0-preview.7.26378.119 Microsoft.NET.Sdk , Microsoft.TemplateEngine.Authoring.Tasks From Version 11.0.100-preview.7.26365.101 -> To Version 11.0.100-preview.7.26378.119 --- eng/Version.Details.xml | 28 ++++++++++++++-------------- eng/Versions.props | 12 ++++++------ global.json | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index f8c6a07001b..60e92f3da8b 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,20 +1,20 @@ - + https://github.com/dotnet/dotnet - ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 + 283ef97f4038f1fc0c75c53cc06667b1ec8b1d6b - + https://github.com/dotnet/dotnet - ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 + 283ef97f4038f1fc0c75c53cc06667b1ec8b1d6b - + https://github.com/dotnet/dotnet - ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 + 283ef97f4038f1fc0c75c53cc06667b1ec8b1d6b - + https://github.com/dotnet/dotnet - ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 + 283ef97f4038f1fc0c75c53cc06667b1ec8b1d6b https://github.com/dotnet/dotnet @@ -36,17 +36,17 @@ - + https://github.com/dotnet/dotnet - ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 + 283ef97f4038f1fc0c75c53cc06667b1ec8b1d6b - + https://github.com/dotnet/dotnet - ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 + 283ef97f4038f1fc0c75c53cc06667b1ec8b1d6b - + https://github.com/dotnet/dotnet - ab4a9a5f25db0b29e980a7acfab46bc4eca924c3 + 283ef97f4038f1fc0c75c53cc06667b1ec8b1d6b https://github.com/microsoft/testfx diff --git a/eng/Versions.props b/eng/Versions.props index c5c22ef6288..702aef41160 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,20 +1,20 @@ - 11.0.100-preview.7.26378.106 + 11.0.100-preview.7.26378.119 $(MicrosoftNETSdkPackageVersion) - 11.0.0-preview.7.26378.106 - 11.0.0-preview.7.26378.106 + 11.0.0-preview.7.26378.119 + 11.0.0-preview.7.26378.119 7.0.0-beta.22103.1 11.0.0-beta.26060.102 - 11.0.0-beta.26378.106 + 11.0.0-beta.26378.119 11.0.100-preview.4.26215.121 11.0.100-preview.4.26215.121 $(MicrosoftNETWorkloadMonoToolChainCurrentManifest110100preview4PackageVersion) $(MicrosoftNETWorkloadEmscriptenCurrentManifest110100preview4PackageVersion) - 11.0.100-preview.7.26378.106 - 0.11.5-preview.26378.106 + 11.0.100-preview.7.26378.119 + 0.11.5-preview.26378.119 4.4.0-preview.26367.7 10.0.10 11.0.0-preview.1.26104.118 diff --git a/global.json b/global.json index c993596b7ff..2e7fbda513e 100644 --- a/global.json +++ b/global.json @@ -4,6 +4,6 @@ }, "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.134", - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26378.106" + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26378.119" } } From 5992926c99511d94ebc0c922594262c6681b4670 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 29 Jul 2026 21:07:02 -0500 Subject: [PATCH 12/14] [ci] Clean tests obj/bin before each APK lane APKs 2 builds the same project six times with different settings in a shared workspace. `-t:Clean` does not remove enough, so each lane starts on top of the previous lane's output. Wipe obj/bin under tests/ before each lane instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- .../yaml-templates/apk-instrumentation.yaml | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/build-tools/automation/yaml-templates/apk-instrumentation.yaml b/build-tools/automation/yaml-templates/apk-instrumentation.yaml index 407a1499f32..6b9338b183f 100644 --- a/build-tools/automation/yaml-templates/apk-instrumentation.yaml +++ b/build-tools/automation/yaml-templates/apk-instrumentation.yaml @@ -11,6 +11,22 @@ parameters: condition: succeeded() steps: + # Each APK lane builds the same project again with different settings + # (_AndroidTypeMapImplementation, UseMonoRuntime) in a shared workspace, and + # `-t:Clean` does not remove enough for the next lane to start fresh. Wipe the + # obj/bin folders under tests/ outright. Only tests/** is touched, so the + # repo-root bin/ (which holds the built SDK) is left alone. +- powershell: | + $roots = Get-ChildItem -Path "${{ parameters.xaSourcePath }}/tests" -Directory -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq 'obj' -or $_.Name -eq 'bin' } + foreach ($r in $roots) { + Write-Host "Removing $($r.FullName)" + Remove-Item -LiteralPath $r.FullName -Recurse -Force -ErrorAction SilentlyContinue + } + displayName: clean tests obj/bin before ${{ parameters.testName }} + condition: ${{ parameters.condition }} + continueOnError: true + # WORKAROUND: 'dotnet test' currently fails to deploy to a device (dotnet/android#12254), # so drive the instrumentation with 'dotnet run' instead. A single 'dotnet run' builds, # installs and runs, so there is no separate build step. @@ -121,13 +137,3 @@ steps: testResultsFiles: ${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/TestResults/${{ parameters.testName }}.trx testRunTitle: ${{ parameters.testName }} condition: ${{ parameters.condition }} - -- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml - parameters: - configuration: ${{ parameters.buildConfiguration }} - xaSourcePath: ${{ parameters.xaSourcePath }} - project: ${{ parameters.project }} - arguments: -t:Clean -c ${{ parameters.configuration }} --no-restore ${{ parameters.extraBuildArgs }} - displayName: Clean ${{ parameters.testName }} - condition: ${{ parameters.condition }} - continueOnError: false From bcc924cb43ac9178ff9fb3e8cfaeb950ea05b9aa Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 30 Jul 2026 00:01:00 -0500 Subject: [PATCH 13/14] Revert "[ci] Clean tests obj/bin before each APK lane" This reverts commit ddb16deeee037faf65118021b40cc6a4de024d54. --- .../yaml-templates/apk-instrumentation.yaml | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/build-tools/automation/yaml-templates/apk-instrumentation.yaml b/build-tools/automation/yaml-templates/apk-instrumentation.yaml index 6b9338b183f..407a1499f32 100644 --- a/build-tools/automation/yaml-templates/apk-instrumentation.yaml +++ b/build-tools/automation/yaml-templates/apk-instrumentation.yaml @@ -11,22 +11,6 @@ parameters: condition: succeeded() steps: - # Each APK lane builds the same project again with different settings - # (_AndroidTypeMapImplementation, UseMonoRuntime) in a shared workspace, and - # `-t:Clean` does not remove enough for the next lane to start fresh. Wipe the - # obj/bin folders under tests/ outright. Only tests/** is touched, so the - # repo-root bin/ (which holds the built SDK) is left alone. -- powershell: | - $roots = Get-ChildItem -Path "${{ parameters.xaSourcePath }}/tests" -Directory -Recurse -Force -ErrorAction SilentlyContinue | - Where-Object { $_.Name -eq 'obj' -or $_.Name -eq 'bin' } - foreach ($r in $roots) { - Write-Host "Removing $($r.FullName)" - Remove-Item -LiteralPath $r.FullName -Recurse -Force -ErrorAction SilentlyContinue - } - displayName: clean tests obj/bin before ${{ parameters.testName }} - condition: ${{ parameters.condition }} - continueOnError: true - # WORKAROUND: 'dotnet test' currently fails to deploy to a device (dotnet/android#12254), # so drive the instrumentation with 'dotnet run' instead. A single 'dotnet run' builds, # installs and runs, so there is no separate build step. @@ -137,3 +121,13 @@ steps: testResultsFiles: ${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/TestResults/${{ parameters.testName }}.trx testRunTitle: ${{ parameters.testName }} condition: ${{ parameters.condition }} + +- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml + parameters: + configuration: ${{ parameters.buildConfiguration }} + xaSourcePath: ${{ parameters.xaSourcePath }} + project: ${{ parameters.project }} + arguments: -t:Clean -c ${{ parameters.configuration }} --no-restore ${{ parameters.extraBuildArgs }} + displayName: Clean ${{ parameters.testName }} + condition: ${{ parameters.condition }} + continueOnError: false From 1ec5689142a80fa4320493e49a6495cdc403601e Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 30 Jul 2026 00:03:20 -0500 Subject: [PATCH 14/14] [ci] Start APK instrumentation with `adb shell am instrument` Both `dotnet test` and `dotnet run` deploy from a second MSBuild submission that does not go through `Build`. `_CollectJavaSourceForBinding` and `_CompileBindingJava` only run as part of `Build`, so that second pass re-runs `_CompileJava` without the binding classes jar on javac's classpath and fails with XAJVC0000 "cannot find symbol". `-t:Install` is a real `Build`, so all that is left is to start the instrumentation. Do that directly with `adb shell am instrument -w -r`, the same command Microsoft.Android.Run issues, and skip the second MSBuild pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b158ad70-1e5e-44cd-a1f2-60353f3bb560 --- .../yaml-templates/apk-instrumentation.yaml | 93 +++++++++++++------ 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/build-tools/automation/yaml-templates/apk-instrumentation.yaml b/build-tools/automation/yaml-templates/apk-instrumentation.yaml index 407a1499f32..9041d4b43ef 100644 --- a/build-tools/automation/yaml-templates/apk-instrumentation.yaml +++ b/build-tools/automation/yaml-templates/apk-instrumentation.yaml @@ -11,44 +11,82 @@ parameters: condition: succeeded() steps: - # WORKAROUND: 'dotnet test' currently fails to deploy to a device (dotnet/android#12254), - # so drive the instrumentation with 'dotnet run' instead. A single 'dotnet run' builds, - # installs and runs, so there is no separate build step. + # Build and install the APK to the device. # - # We do not lose the test report by doing so: the on-device test runner writes its - # own TRX (tests/TestRunner.Core/TestInstrumentation.cs) and reports the location - # back through the instrumentation bundle as - # 'INSTRUMENTATION_RESULT: resultsPath='. 'dotnet run' invokes - # 'adb shell am instrument -w', which prints that bundle, so we can scrape the path - # and pull the TRX to where PublishTestResults expects it. + # Retry the install up to 3 times: the emulator/ADB connection occasionally + # drops mid-install (XAGCPU7000 "device offline") and a retry usually recovers. + # If every attempt fails, continueOnError: false fails the lane immediately - + # there is no point running the tests against a device where the app was never + # installed (that only wastes CI time and produces a misleading + # INSTRUMENTATION_FAILED). See dotnet/android#11830. +- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml + parameters: + configuration: ${{ parameters.buildConfiguration }} + xaSourcePath: ${{ parameters.xaSourcePath }} + displayName: build ${{ parameters.testName }} + project: ${{ parameters.project }} + arguments: >- + -t:Install + -bl:${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/build-${{ parameters.testName }}.binlog + -c ${{ parameters.configuration }} ${{ parameters.extraBuildArgs }} + condition: ${{ parameters.condition }} + retryCountOnTaskFailure: 3 + continueOnError: false + + # WORKAROUND: neither 'dotnet test' nor 'dotnet run' can be used to start the + # tests right now. + # + # 'dotnet test' fails to deploy to a device (dotnet/android#12254). 'dotnet run' + # gets further, but both of them deploy from a *second* MSBuild submission that + # does not go through 'Build'. _CollectJavaSourceForBinding and _CompileBindingJava + # only run as part of 'Build', so that second pass re-runs _CompileJava without the + # binding classes jar on javac's classpath and fails with XAJVC0000 'cannot find + # symbol'. + # + # The install above is a real 'Build', so it is correct. All that is left is to + # start the instrumentation, which is exactly what Microsoft.Android.Run does: + # adb shell am instrument -w -r / + # (src/Microsoft.Android.Run/AndroidTestAdapter.cs). Do that directly and skip the + # second MSBuild pass entirely. + # + # The on-device runner writes its own TRX (tests/TestRunner.Core/TestInstrumentation.cs) + # and reports it back as 'INSTRUMENTATION_RESULT: resultsPath=', so we + # scrape that and pull the TRX to where PublishTestResults expects it. # # Revert to 'dotnet test' once the SDK fix flows in. - powershell: | $ErrorActionPreference = "Continue" - $projectDir = Split-Path -Parent "${{ parameters.project }}" - $projectFile = Split-Path -Leaf "${{ parameters.project }}" + $projectDir = Join-Path "${{ parameters.xaSourcePath }}" (Split-Path -Parent "${{ parameters.project }}") $resultsDir = "${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/TestResults" New-Item -ItemType Directory -Force -Path $resultsDir | Out-Null - Push-Location $projectDir - if ([Environment]::OSVersion.Platform -eq "Unix") { - $DOTNET_ROOT = "${{ parameters.xaSourcePath }}/bin/${{ parameters.buildConfiguration }}/dotnet" - $env:PATH = "${DOTNET_ROOT}:$env:PATH" - $dotnetPath = "${DOTNET_ROOT}/dotnet" - } else { - $DOTNET_ROOT = "${{ parameters.xaSourcePath }}\bin\${{ parameters.buildConfiguration }}\dotnet" - $env:PATH = "${DOTNET_ROOT};$env:PATH" - $dotnetPath = "${DOTNET_ROOT}\dotnet.exe" + + # The element is generated into the manifest at build time + # from the [Instrumentation] attribute, so read the built manifest, not the + # one in the project. + $manifest = Get-ChildItem -Path (Join-Path $projectDir "obj/${{ parameters.configuration }}") -Recurse -Filter AndroidManifest.xml -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -replace '\\','/' -match '/android/manifest/AndroidManifest.xml$' } | + Select-Object -First 1 + if (-not $manifest) { + Write-Host "##vso[task.logissue type=error]Could not find a generated AndroidManifest.xml under '$projectDir/obj/${{ parameters.configuration }}'." + exit 1 + } + Write-Host "Using manifest: $($manifest.FullName)" + [xml]$xml = Get-Content $manifest.FullName + $ns = "http://schemas.android.com/apk/res/android" + $package = $xml.manifest.package + $instrumentation = $xml.manifest.instrumentation | Select-Object -First 1 + if (-not $package -or -not $instrumentation) { + Write-Host "##vso[task.logissue type=error]Manifest '$($manifest.FullName)' has no package and/or element." + exit 1 } - # 'dotnet run' deploys to the device with an in-process MSBuild, which skips the - # MSBuildForwardingApp that normally sets DOTNET_HOST_PATH. Without it, MSBuild - # cannot launch task hosts (MSB4221 on ILLink's ComputeManagedAssemblies). - $env:DOTNET_HOST_PATH = $dotnetPath - & $dotnetPath run --project $projectFile -bl:${{ parameters.xaSourcePath }}/bin/Test${{ parameters.configuration }}/run-${{ parameters.testName }}.binlog -c ${{ parameters.configuration }} ${{ parameters.extraBuildArgs }} 2>&1 | Tee-Object -Variable runOutput + $runner = $instrumentation.GetAttribute("name", $ns) + Write-Host "Running $package/$runner" + & adb shell am instrument -w -r "$package/$runner" 2>&1 | Tee-Object -Variable runOutput $runExitCode = $LASTEXITCODE - Pop-Location + $match = $runOutput | Select-String -Pattern 'INSTRUMENTATION_RESULT:\s*resultsPath=(.+)$' | Select-Object -Last 1 if (-not $match) { - Write-Host "##vso[task.logissue type=error]Instrumentation did not report a resultsPath; no TRX to publish. 'dotnet run' exited with $runExitCode." + Write-Host "##vso[task.logissue type=error]Instrumentation did not report a resultsPath; no TRX to publish. 'am instrument' exited with $runExitCode." exit 1 } $devicePath = $match.Matches[0].Groups[1].Value.Trim() @@ -59,6 +97,7 @@ steps: Write-Host "##vso[task.logissue type=error]Failed to pull TRX from device path '$devicePath'." exit 1 } + if ($runOutput -match 'INSTRUMENTATION_FAILED') { exit 1 } exit $runExitCode displayName: run ${{ parameters.testName }} condition: ${{ parameters.condition }}