diff --git a/eng/common/dotnet-install.ps1 b/eng/common/dotnet-install.ps1 index 811f0f717f7..82c37530754 100644 --- a/eng/common/dotnet-install.ps1 +++ b/eng/common/dotnet-install.ps1 @@ -3,6 +3,7 @@ Param( [string] $verbosity = 'minimal', [string] $architecture = '', [string] $version = 'Latest', + [string] $channel = '', [string] $runtime = 'dotnet', [string] $RuntimeSourceFeed = '', [string] $RuntimeSourceFeedKey = '' @@ -17,7 +18,7 @@ try { if ($architecture -and $architecture.Trim() -eq 'x86') { $installdir = Join-Path $installdir 'x86' } - InstallDotNet $installdir $version $architecture $runtime $true -RuntimeSourceFeed $RuntimeSourceFeed -RuntimeSourceFeedKey $RuntimeSourceFeedKey + InstallDotNet $installdir $version $architecture $runtime $true -RuntimeSourceFeed $RuntimeSourceFeed -RuntimeSourceFeedKey $RuntimeSourceFeedKey -Channel $channel } catch { Write-Host $_.ScriptStackTrace diff --git a/eng/common/dotnet-install.sh b/eng/common/dotnet-install.sh index 61f302bb677..d6fd3e956f1 100755 --- a/eng/common/dotnet-install.sh +++ b/eng/common/dotnet-install.sh @@ -14,6 +14,7 @@ scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" . "$scriptroot/tools.sh" version='Latest' +channel='' architecture='' runtime='dotnet' runtimeSourceFeed='' @@ -25,6 +26,10 @@ while [[ $# -gt 0 ]]; do shift version="$1" ;; + -channel|-c) + shift + channel="$1" + ;; -architecture|-a) shift architecture="$1" @@ -85,7 +90,7 @@ if [[ $architecture != "" ]] && [[ $architecture != $buildarch ]]; then dotnetRoot="$dotnetRoot/$architecture" fi -InstallDotNet "$dotnetRoot" $version "$architecture" $runtime true $runtimeSourceFeed $runtimeSourceFeedKey || { +InstallDotNet "$dotnetRoot" $version "$architecture" $runtime true $runtimeSourceFeed $runtimeSourceFeedKey "$channel" || { local exit_code=$? Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "dotnet-install.sh failed (exit code '$exit_code')." >&2 ExitWithExitCode $exit_code diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index f6bde268379..8376b5baace 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -291,6 +291,7 @@ function InstallDotNet([string] $dotnetRoot, [bool] $skipNonVersionedFiles = $false, [string] $runtimeSourceFeed = '', [string] $runtimeSourceFeedKey = '', + [string] $channel = '', [switch] $noPath) { $dotnetVersionLabel = "'sdk v$version'" @@ -301,21 +302,36 @@ function InstallDotNet([string] $dotnetRoot, if ($runtime -eq "dotnet") { $runtimePath = $runtimePath + "\Microsoft.NETCore.App" } if ($runtime -eq "aspnetcore") { $runtimePath = $runtimePath + "\Microsoft.AspNetCore.App" } if ($runtime -eq "windowsdesktop") { $runtimePath = $runtimePath + "\Microsoft.WindowsDesktop.App" } - $runtimePath = $runtimePath + "\" + $version - - $dotnetVersionLabel = "runtime toolset '$runtime/$architecture v$version'" - - if (Test-Path $runtimePath) { - Write-Host " Runtime toolset '$runtime/$architecture v$version' already installed." - $installSuccess = $true - Exit + + # When using channel, we can't check for an existing installation by version + # since we don't know which version will be installed + if (-not $channel) { + $runtimePath = $runtimePath + "\" + $version + $dotnetVersionLabel = "runtime toolset '$runtime/$architecture v$version'" + + if (Test-Path $runtimePath) { + Write-Host " Runtime toolset '$runtime/$architecture v$version' already installed." + $installSuccess = $true + return + } + } else { + $dotnetVersionLabel = "runtime toolset '$runtime/$architecture channel $channel'" } } $installScript = GetDotNetInstallScript $dotnetRoot - $installParameters = @{ - Version = $version - InstallDir = $dotnetRoot + + # Use -Channel if specified, otherwise use -Version + if ($channel) { + $installParameters = @{ + Channel = $channel + InstallDir = $dotnetRoot + } + } else { + $installParameters = @{ + Version = $version + InstallDir = $dotnetRoot + } } if ($architecture) { $installParameters.Architecture = $architecture } diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 6c121300ac7..ed0e8d0b507 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -183,6 +183,7 @@ function InstallDotNet { local root=$1 local version=$2 local runtime=$4 + local channel="${8:-}" local dotnetVersionLabel="'$runtime v$version'" if [[ -n "${4:-}" ]] && [ "$4" != 'sdk' ]; then @@ -201,21 +202,32 @@ function InstallDotNet { *) ;; esac - runtimePath="$runtimePath/$version" - - dotnetVersionLabel="runtime toolset '$runtime/$architecture v$version'" - - if [ -d "$runtimePath" ]; then - echo " Runtime toolset '$runtime/$architecture v$version' already installed." - local installSuccess=1 - return + + # When using channel, we can't check for an existing installation by version + # since we don't know which version will be installed + if [[ -z "$channel" ]]; then + runtimePath="$runtimePath/$version" + dotnetVersionLabel="runtime toolset '$runtime/$architecture v$version'" + + if [ -d "$runtimePath" ]; then + echo " Runtime toolset '$runtime/$architecture v$version' already installed." + local installSuccess=1 + return + fi + else + dotnetVersionLabel="runtime toolset '$runtime/$architecture channel $channel'" fi fi GetDotNetInstallScript "$root" local install_script=$_GetDotNetInstallScript - local installParameters=(--version $version --install-dir "$root") + # Use --channel if specified, otherwise use --version + if [[ -n "$channel" ]]; then + local installParameters=(--channel $channel --install-dir "$root") + else + local installParameters=(--version $version --install-dir "$root") + fi if [[ -n "${3:-}" ]] && [ "$3" != 'unset' ]; then installParameters+=(--architecture $3) diff --git a/src/Microsoft.DotNet.Arcade.Sdk.Tests/InstallDotNetCoreTests.cs b/src/Microsoft.DotNet.Arcade.Sdk.Tests/InstallDotNetCoreTests.cs new file mode 100644 index 00000000000..6a83ab04dab --- /dev/null +++ b/src/Microsoft.DotNet.Arcade.Sdk.Tests/InstallDotNetCoreTests.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Reflection; +using Xunit; + +namespace Microsoft.DotNet.Arcade.Sdk.Tests +{ + public class InstallDotNetCoreTests + { + [Theory] + [InlineData("8.0", true)] + [InlineData("10.0", true)] + [InlineData("3.1", true)] + [InlineData("8.0.22", false)] + [InlineData("10.0.1", false)] + [InlineData("8.0.0-preview.1", false)] + [InlineData("", false)] + [InlineData(null, false)] + [InlineData("8", false)] + [InlineData("8.0.1.2", false)] + [InlineData("v8.0", false)] + [InlineData("8.x", false)] + public void IsTwoPartVersion_DetectsCorrectFormat(string versionString, bool expected) + { + // Use reflection to call the private method + var task = new InstallDotNetCore(); + var method = typeof(InstallDotNetCore).GetMethod("IsTwoPartVersion", BindingFlags.NonPublic | BindingFlags.Instance); + + var result = (bool)method.Invoke(task, new object[] { versionString }); + + Assert.Equal(expected, result); + } + } +} diff --git a/src/Microsoft.DotNet.Arcade.Sdk/src/InstallDotNetCore.cs b/src/Microsoft.DotNet.Arcade.Sdk/src/InstallDotNetCore.cs index d3ff77edbb5..d2a144df656 100644 --- a/src/Microsoft.DotNet.Arcade.Sdk/src/InstallDotNetCore.cs +++ b/src/Microsoft.DotNet.Arcade.Sdk/src/InstallDotNetCore.cs @@ -13,6 +13,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.Json; +using System.Text.RegularExpressions; namespace Microsoft.DotNet.Arcade.Sdk { @@ -106,8 +107,16 @@ public override bool Execute() } SemanticVersion version = null; + string channel = null; + + // Check if the version is in major.minor format (e.g., "8.0", "10.0") + // This format should use the -channel parameter instead of -version + if (IsTwoPartVersion(item.Key)) + { + channel = item.Key; + } // Try to parse version - if (!SemanticVersion.TryParse(item.Key, out version)) + else if (!SemanticVersion.TryParse(item.Key, out version)) { var propertyName = item.Key.Trim(s_keyTrimChars); @@ -123,9 +132,12 @@ public override bool Execute() } } - if (version != null) + if (version != null || channel != null) { - string arguments = $"-runtime \"{runtimeItem.Key}\" -version \"{version.ToNormalizedString()}\""; + string arguments = channel != null + ? $"-runtime \"{runtimeItem.Key}\" -channel \"{channel}\"" + : $"-runtime \"{runtimeItem.Key}\" -version \"{version.ToNormalizedString()}\""; + if (!string.IsNullOrEmpty(architecture)) { arguments += $" -architecture {architecture}"; @@ -204,6 +216,21 @@ private string GetArchitecture(string architecture) return null; } + /* + * Checks if a version string is in major.minor format (e.g., "8.0", "10.0"). + * Returns true if the version has exactly two numeric parts separated by a dot. + */ + private bool IsTwoPartVersion(string versionString) + { + if (string.IsNullOrWhiteSpace(versionString)) + { + return false; + } + + // Match exactly two numeric parts separated by a dot + return Regex.IsMatch(versionString, @"^\d+\.\d+$"); + } + /* * Parses a json token of this format * { (runtime): [(version), ..., (version)] }