Skip to content
1 change: 1 addition & 0 deletions pipelines/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ extends:
displayName: "Publish WinGet DSC Resources"
templateContext:
outputs:
# TODO: Add pipeline artifact for NET.SDK.ToolInstaller
- output: pipelineArtifact
displayName: 'Publish Pipeline Microsoft.Windows.Developer'
targetPath: $(Build.SourcesDirectory)\resources\Microsoft.Windows.Developer\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@{
RootModule = 'Microsoft.NET.SDK.ToolInstaller.psm1'
ModuleVersion = '0.1.0'
GUID = '2e883e78-1d91-4d08-9fc1-2a968e31009d'
Author = 'Microsoft Corporation'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@denelon - Is there an ownership concept with DSCs similar to Winget packages? How do you ensure the validity of the author and that they have sole ownership?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is an OSS repository, we'd consider PRs from the community, but since this is a Microsoft "package" the resource would require our approval to make sure we're good with the implementation. Anything published to the PowerShell gallery from here would need that validation before we would sign and publish the module.

CompanyName = 'Microsoft Corporation'
Copyright = '(c) Microsoft Corporation. All rights reserved.'
Description = 'DSC Resource for .NET SDK tool installer'
PowerShellVersion = '7.2'
DscResourcesToExport = @(
'NETSDKToolInstaller'
)
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
Tags = @(
'PSDscResource_NETSDKToolInstaller'
)

# Prerelease string of this module
Prerelease = 'alpha'
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

using namespace System.Collections.Generic

$ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $true
Set-StrictMode -Version Latest

#region Functions
function Get-DotNetPath {
if ($IsWindows) {
$dotNetPath = "$env:ProgramFiles\dotnet\dotnet.exe"
if (-not (Test-Path $dotNetPath)) {
$dotNetPath = "${env:ProgramFiles(x86)}\dotnet\dotnet.exe"
if (-not (Test-Path $dotNetPath)) {
throw "dotnet.exe not found in Program Files or Program Files (x86)"
}
}
} elseif ($IsMacOS) {
$dotNetPath = "/usr/local/share/dotnet/dotnet"
if (-not (Test-Path $dotNetPath)) {
$dotNetPath = "/usr/local/bin/dotnet"
if (-not (Test-Path $dotNetPath)) {
throw "dotnet not found in /usr/local/share/dotnet or /usr/local/bin"
}
}
} elseif ($IsLinux) {
$dotNetPath = "/usr/share/dotnet/dotnet"
if (-not (Test-Path $dotNetPath)) {
$dotNetPath = "/usr/bin/dotnet"
if (-not (Test-Path $dotNetPath)) {
throw "dotnet not found in /usr/share/dotnet or /usr/bin"
}
}
} else {
throw "Unsupported operating system"
}

Write-Verbose -Message "'dotnet' found at $dotNetPath"
return $dotNetPath
}

# TODO: when https://github.com/dotnet/sdk/pull/37394 is documented and version is released with option simple use --format=JSON

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linked PR is fixed in 9 it looks like. Were you waiting for 9 to be the default to replace the below code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, I forgot I couldn't install the latest preview from the official website. Everytime when I tried downloading, it gives me 0 bytes. When I used winget install Microsoft.DotNet.SDK.Preview, it did. I guess it installs from a different location.

Eventually, I guess we can move the logic to this:

image

But we should also be able to support older versions of the SDK?


function Convert-DotNetToolOutput {
[CmdletBinding()]
param (
[string[]] $Output
)

process {
# Split the output into lines
$lines = $Output | Select-Object -Skip 2

# Initialize an array to hold the custom objects
$inputObject = @()

# Skip the header lines and process each line
foreach ($line in $lines) {
# Split the line into columns
$columns = $line -split '\s{2,}'

# Create a custom object for each line
$customObject = [PSCustomObject]@{
PackageId = $columns[0]
Version = $columns[1]
Commands = $columns[2]
}

# Add the custom object to the array
$inputObject += $customObject
}

return $inputObject
}
}

function Install-DotNetToolPackage
{
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string] $Name,
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Version,
[Parameter(ValueFromPipelineByPropertyName)]
[bool] $PreRelease
)

begin
{
function Get-DotNetToolArguments
{
param([string]$Name, [string]$Version, [bool]$PreRelease)

$string = $Name

if (-not ([string]::IsNullOrEmpty($Version)))
{
$string += " --version $Version"
}

if ($PreRelease) {
$string += " --prerelease"
}

$string += " --no-cache"

return $string
}
}

process
{
$installArgument = Get-DotNetToolArguments @PSBoundParameters
$arguments = "tool install $installArgument --global --ignore-failed-sources"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are all of the tools you're installing supported on the current runtimes that are installed? if not, there may be issues with running those tools.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your statement here, however I do think users themselves should be aware what they should install. I know the architecture option is also available to be specified. It can be added later I would say. Let me know your thoughts.

Write-Verbose -Message "Installing dotnet tool package with arguments: $arguments"

Invoke-DotNet -Command $arguments
}
}

function Uninstall-DotNetToolPackage
{
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$Name
)

$arguments = "tool uninstall $Name --global"
Write-Verbose -Message "Uninstalling dotnet tool package with arguments: $arguments"

Invoke-DotNet -Command
}

function Invoke-DotNet {
param (
[Parameter(Mandatory = $true)]
[string] $Command
)

try {
Invoke-Expression "& `"$DotNetCliPath`" $Command"
}
catch {
throw "Executing dotnet.exe with {$Command} failed."
}
}

# Keeps the path of the code.exe CLI path.
$DotNetCliPath = Get-DotNetPath

#endregion Functions

#region Classes
[DSCResource()]
class NETSDKToolInstaller {
[DscProperty(Key)]
[string] $PackageId

[DscProperty()]
[string] $Version

[DscProperty()]
[string[]] $Commands

[DscProperty()]
[bool] $PreRelease = $false

# TODO: Add support for --tool-path

[DscProperty()]
[bool] $Exist = $true

static [hashtable] $InstalledPackages

NETSDKToolInstaller() {
[NETSDKToolInstaller]::GetInstalledPackages()
}

NETSDKToolInstaller([string] $PackageId, [string] $Version, [string[]] $Commands, [bool] $PreRelease) {
$this.PackageId = $PackageId
$this.Version = $Version
$this.Commands = $Commands
$this.PreRelease = $PreRelease
}

[NETSDKToolInstaller] Get() {
$currentState = [NETSDKToolInstaller]::InstalledPackages[$this.PackageId]
if ($null -ne $currentState)
{
return $currentState
}

return [NETSDKToolInstaller]@{
PackageId = $this.PackageId
Version = $this.Version
Commands = $this.Commands
Exist = $false
}
}

Set() {
# TODO: validate for upgrade/update scenarios

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the update model for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's imagine the following scenario. The user has already installed PowerShell version 7.4.4. The user now wants version 7.4.5. The user leaves empty the version property. When the user runs the configuration, it needs to check if 7.4.4 is installed and if so, update it with dotnet tool update. @ryfu-msft Would you agree with such logic, or should we add a boolean parameter to upgrade?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. If a package has a version that does not match the desired version, then this DSC resource should handle updating the package to match the desired state.

if ($this.Test())
{
return
}

if ($this.Exist)
{
$this.Install($false)
}
else
{
$this.Uninstall($false)
}
}

[bool] Test() {
$currentState = $this.Get()
if ($currentState.Exist -ne $this.Exist)
{
return $false
}

if ($null -ne $this.Version -and $this.Version -ne $currentState.Version)
{
return $false
}

return $true
}

static [NETSDKToolInstaller[]] Export() {
$packageList = Invoke-DotNet -Command "tool list --global"

$inputObject = Convert-DotNetToolOutput -Output $packageList

$results = [List[NETSDKToolInstaller]]::new()

foreach ($package in $inputObject)
{
$pre = $false
$preReleasePackage = $package.Version -Split "-"
if ($preReleasePackage.Count -gt 1)
{
$pre = $true
}
$results.Add([NETSDKToolInstaller]::new(
$package.PackageId, $package.Version, $package.Commands, $pre
))
}

return $results
}

#region NETSDKToolInstaller helper functions
static [void] GetInstalledPackages() {
[NETSDKToolInstaller]::InstalledPackages = @{}

foreach ($extension in [NETSDKToolInstaller]::Export()) {
[NETSDKToolInstaller]::InstalledPackages[$extension.PackageId] = $extension
}
}

[void] Install([bool] $preTest)
{
if ($preTest -and $this.Test())
{
return
}

Install-DotNetToolpackage -Name $this.PackageId -Version $this.Version -PreRelease $this.PreRelease
[NETSDKToolInstaller]::GetInstalledPackages()
}

[void] Install()
{
$this.Install($true)
}

[void] Uninstall([bool] $preTest)
{
Uninstall-DotNetToolpackage -Name $this.PackageId
[NETSDKToolInstaller]::GetInstalledPackages()
}

[void] Uninstall()
{
$this.Uninstall($true)
}
#endregion NETSDKToolInstaller helper functions
}
#endregion Classes
Loading