2023-03-09 19:43:05 +07:00
// Load tempDirectory before it gets wiped by tool-cache
import * as core from '@actions/core' ;
import * as exec from '@actions/exec' ;
import * as io from '@actions/io' ;
import * as hc from '@actions/http-client' ;
import { chmodSync } from 'fs' ;
import path from 'path' ;
import os from 'os' ;
import semver from 'semver' ;
2023-05-25 18:11:13 +07:00
import { IS_WINDOWS , PLATFORM } from './utils' ;
2023-03-09 19:43:05 +07:00
import { QualityOptions } from './setup-dotnet' ;
export interface DotnetVersion {
type : string ;
value : string ;
qualityFlag : boolean ;
}
2023-05-22 17:27:33 +07:00
const QUALITY_INPUT_MINIMAL_MAJOR_TAG = 6 ;
const LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG = 5 ;
2023-03-09 19:43:05 +07:00
export class DotnetVersionResolver {
private inputVersion : string ;
private resolvedArgument : DotnetVersion ;
constructor ( version : string ) {
this . inputVersion = version . trim ( ) ;
this . resolvedArgument = { type : '' , value : '' , qualityFlag : false } ;
}
private async resolveVersionInput ( ) : Promise < void > {
2023-04-12 20:44:03 +07:00
if ( ! semver . validRange ( this . inputVersion ) && ! this . isLatestPatchSyntax ( ) ) {
2023-03-09 19:43:05 +07:00
throw new Error (
2023-05-18 17:39:22 +07:00
` The 'dotnet-version' was supplied in invalid format: ${ this . inputVersion } ! Supported syntax: A.B.C, A.B, A.B.x, A, A.x, A.B.Cxx `
2023-03-09 19:43:05 +07:00
) ;
}
if ( semver . valid ( this . inputVersion ) ) {
2023-04-12 20:44:03 +07:00
this . createVersionArgument ( ) ;
2023-03-09 19:43:05 +07:00
} else {
2023-04-12 20:44:03 +07:00
await this . createChannelArgument ( ) ;
2023-03-09 19:43:05 +07:00
}
}
private isNumericTag ( versionTag ) : boolean {
return /^\d+$/ . test ( versionTag ) ;
}
2023-04-12 20:44:03 +07:00
private isLatestPatchSyntax() {
const majorTag = this . inputVersion . match (
2023-05-18 16:46:40 +07:00
/^(?<majorTag>\d+)\.\d+\.\d{1}x{2}$/
2023-04-12 20:44:03 +07:00
) ? . groups ? . majorTag ;
2023-04-13 15:33:52 +07:00
if (
majorTag &&
2023-05-22 17:27:33 +07:00
parseInt ( majorTag ) < LATEST_PATCH_SYNTAX_MINIMAL_MAJOR_TAG
2023-04-13 15:33:52 +07:00
) {
2023-04-12 20:44:03 +07:00
throw new Error (
2023-05-18 17:39:22 +07:00
` The 'dotnet-version' was supplied in invalid format: ${ this . inputVersion } ! The A.B.Cxx syntax is available since the .NET 5.0 release. `
2023-04-12 20:44:03 +07:00
) ;
}
return majorTag ? true : false ;
}
private createVersionArgument() {
this . resolvedArgument . type = 'version' ;
this . resolvedArgument . value = this . inputVersion ;
}
private async createChannelArgument() {
this . resolvedArgument . type = 'channel' ;
const [ major , minor ] = this . inputVersion . split ( '.' ) ;
if ( this . isLatestPatchSyntax ( ) ) {
this . resolvedArgument . value = this . inputVersion ;
} else if ( this . isNumericTag ( major ) && this . isNumericTag ( minor ) ) {
this . resolvedArgument . value = ` ${ major } . ${ minor } ` ;
} else if ( this . isNumericTag ( major ) ) {
this . resolvedArgument . value = await this . getLatestByMajorTag ( major ) ;
} else {
2023-05-05 15:43:09 +07:00
// If "dotnet-version" is specified as *, x or X resolve latest version of .NET explicitly from LTS channel. The version argument will default to "latest" by install-dotnet script.
2023-04-12 20:44:03 +07:00
this . resolvedArgument . value = 'LTS' ;
}
2023-04-13 15:33:52 +07:00
this . resolvedArgument . qualityFlag =
2023-05-22 17:27:33 +07:00
parseInt ( major ) >= QUALITY_INPUT_MINIMAL_MAJOR_TAG ? true : false ;
2023-04-12 20:44:03 +07:00
}
2023-05-24 21:59:05 +07:00
public async createDotnetVersion ( ) : Promise < DotnetVersion > {
2023-03-09 19:43:05 +07:00
await this . resolveVersionInput ( ) ;
if ( ! this . resolvedArgument . type ) {
return this . resolvedArgument ;
}
if ( IS_WINDOWS ) {
this . resolvedArgument . type =
this . resolvedArgument . type === 'channel' ? '-Channel' : '-Version' ;
} else {
this . resolvedArgument . type =
this . resolvedArgument . type === 'channel' ? '--channel' : '--version' ;
}
return this . resolvedArgument ;
}
2023-04-11 18:20:34 +07:00
private async getLatestByMajorTag ( majorTag : string ) : Promise < string > {
const httpClient = new hc . HttpClient ( 'actions/setup-dotnet' , [ ] , {
allowRetries : true ,
maxRetries : 3
} ) ;
2024-12-27 05:21:39 +07:00
let response ;
try {
response = await httpClient . getJson < any > (
DotnetVersionResolver . DotnetCoreIndexUrl
) ;
} catch ( error ) {
response = await httpClient . getJson < any > (
DotnetVersionResolver . DotnetCoreIndexFallbackUrl
) ;
}
2023-03-09 19:43:05 +07:00
const result = response . result || { } ;
const releasesInfo : any [ ] = result [ 'releases-index' ] ;
const releaseInfo = releasesInfo . find ( info = > {
const sdkParts : string [ ] = info [ 'channel-version' ] . split ( '.' ) ;
2023-04-11 18:20:34 +07:00
return sdkParts [ 0 ] === majorTag ;
2023-03-09 19:43:05 +07:00
} ) ;
if ( ! releaseInfo ) {
throw new Error (
2023-05-24 21:59:05 +07:00
` Could not find info for version with major tag: " ${ majorTag } " at ${ DotnetVersionResolver . DotnetCoreIndexUrl } `
2023-03-09 19:43:05 +07:00
) ;
}
return releaseInfo [ 'channel-version' ] ;
}
2023-05-24 21:59:05 +07:00
static DotnetCoreIndexUrl =
2024-12-27 05:21:39 +07:00
'https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json' ;
static DotnetCoreIndexFallbackUrl =
2023-03-09 19:43:05 +07:00
'https://dotnetcli.azureedge.net/dotnet/release-metadata/releases-index.json' ;
}
2023-05-12 21:28:16 +07:00
export class DotnetInstallScript {
private scriptName = IS_WINDOWS ? 'install-dotnet.ps1' : 'install-dotnet.sh' ;
private escapedScript : string ;
private scriptArguments : string [ ] = [ ] ;
2023-03-09 19:43:05 +07:00
2023-05-12 21:28:16 +07:00
constructor ( ) {
this . escapedScript = path
2023-05-31 16:18:52 +07:00
. join ( __dirname , '..' , '..' , 'externals' , this . scriptName )
2023-05-12 21:28:16 +07:00
. replace ( /'/g , "''" ) ;
2023-05-24 20:22:01 +07:00
2023-05-30 17:54:41 +07:00
if ( IS_WINDOWS ) {
this . setupScriptPowershell ( ) ;
return ;
}
this . setupScriptBash ( ) ;
2023-05-12 21:28:16 +07:00
}
2023-05-30 18:19:37 +07:00
private setupScriptPowershell() {
2023-05-12 21:28:16 +07:00
this . scriptArguments = [
'-NoLogo' ,
'-Sta' ,
'-NoProfile' ,
'-NonInteractive' ,
'-ExecutionPolicy' ,
'Unrestricted' ,
'-Command'
] ;
this . scriptArguments . push ( '&' , ` ' ${ this . escapedScript } ' ` ) ;
if ( process . env [ 'https_proxy' ] != null ) {
this . scriptArguments . push ( ` -ProxyAddress ${ process . env [ 'https_proxy' ] } ` ) ;
}
// This is not currently an option
if ( process . env [ 'no_proxy' ] != null ) {
this . scriptArguments . push ( ` -ProxyBypassList ${ process . env [ 'no_proxy' ] } ` ) ;
}
}
2023-05-30 18:19:37 +07:00
private setupScriptBash() {
2023-05-12 21:28:16 +07:00
chmodSync ( this . escapedScript , '777' ) ;
2023-05-30 17:54:41 +07:00
}
private async getScriptPath() {
if ( IS_WINDOWS ) {
return ( await io . which ( 'pwsh' , false ) ) || io . which ( 'powershell' , true ) ;
}
return io . which ( this . escapedScript , true ) ;
2023-03-09 19:43:05 +07:00
}
2023-05-12 21:28:16 +07:00
public useArguments ( . . . args : string [ ] ) {
this . scriptArguments . push ( . . . args ) ;
return this ;
2023-03-09 19:43:05 +07:00
}
2023-05-12 21:28:16 +07:00
public useVersion ( dotnetVersion : DotnetVersion , quality? : QualityOptions ) {
if ( dotnetVersion . type ) {
this . useArguments ( dotnetVersion . type , dotnetVersion . value ) ;
}
if ( quality && ! dotnetVersion . qualityFlag ) {
core . warning (
2023-05-24 21:40:29 +07:00
` The 'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A, A.x and A.B.Cxx formats where the major tag is higher than 5. You specified: ${ dotnetVersion . value } . 'dotnet-quality' input is ignored. `
2023-05-12 21:28:16 +07:00
) ;
return this ;
}
if ( quality ) {
this . useArguments ( IS_WINDOWS ? '-Quality' : '--quality' , quality ) ;
}
return this ;
}
public async execute() {
const getExecOutputOptions = {
ignoreReturnCode : true ,
env : process.env as { string : string }
} ;
return exec . getExecOutput (
2023-05-30 17:54:41 +07:00
` " ${ await this . getScriptPath ( ) } " ` ,
2023-05-12 21:28:16 +07:00
this . scriptArguments ,
2023-05-24 20:22:01 +07:00
getExecOutputOptions
) ;
2023-05-12 21:28:16 +07:00
}
}
export abstract class DotnetInstallDir {
private static readonly default = {
linux : '/usr/share/dotnet' ,
mac : path.join ( process . env [ 'HOME' ] + '' , '.dotnet' ) ,
windows : path.join ( process . env [ 'PROGRAMFILES' ] + '' , 'dotnet' )
2023-05-24 20:22:01 +07:00
} ;
2023-05-12 21:28:16 +07:00
2023-05-30 17:14:34 +07:00
public static readonly dirPath = process . env [ 'DOTNET_INSTALL_DIR' ]
2023-05-24 20:22:01 +07:00
? DotnetInstallDir . convertInstallPathToAbsolute (
process . env [ 'DOTNET_INSTALL_DIR' ]
)
2023-05-25 18:11:13 +07:00
: DotnetInstallDir . default [ PLATFORM ] ;
2023-05-12 21:28:16 +07:00
2023-03-09 19:43:05 +07:00
private static convertInstallPathToAbsolute ( installDir : string ) : string {
2023-05-12 22:34:18 +07:00
if ( path . isAbsolute ( installDir ) ) return path . normalize ( installDir ) ;
const transformedPath = installDir . startsWith ( '~' )
? path . join ( os . homedir ( ) , installDir . slice ( 1 ) )
: path . join ( process . cwd ( ) , installDir ) ;
2023-03-09 19:43:05 +07:00
return path . normalize ( transformedPath ) ;
}
2023-05-12 21:28:16 +07:00
public static addToPath() {
2023-03-09 19:43:05 +07:00
core . addPath ( process . env [ 'DOTNET_INSTALL_DIR' ] ! ) ;
core . exportVariable ( 'DOTNET_ROOT' , process . env [ 'DOTNET_INSTALL_DIR' ] ) ;
}
2023-05-30 17:18:10 +07:00
public static setEnvironmentVariable() {
2023-05-30 17:14:34 +07:00
process . env [ 'DOTNET_INSTALL_DIR' ] = DotnetInstallDir . dirPath ;
2023-03-09 19:43:05 +07:00
}
2023-05-12 21:28:16 +07:00
}
2023-03-09 19:43:05 +07:00
2023-05-12 21:28:16 +07:00
export class DotnetCoreInstaller {
static {
2023-05-30 17:18:10 +07:00
DotnetInstallDir . setEnvironmentVariable ( ) ;
2023-05-12 21:28:16 +07:00
}
2023-03-09 19:43:05 +07:00
2023-12-04 21:17:27 +07:00
constructor (
private version : string ,
private quality : QualityOptions
) { }
2023-03-09 19:43:05 +07:00
2023-05-24 21:40:29 +07:00
public async installDotnet ( ) : Promise < string | null > {
2023-05-12 21:28:16 +07:00
const versionResolver = new DotnetVersionResolver ( this . version ) ;
2023-05-24 21:59:05 +07:00
const dotnetVersion = await versionResolver . createDotnetVersion ( ) ;
2023-03-09 19:43:05 +07:00
2023-05-12 22:00:39 +07:00
/ * *
* Install dotnet runitme first in order to get
* the latest stable version of dotnet CLI
* /
const runtimeInstallOutput = await new DotnetInstallScript ( )
// If dotnet CLI is already installed - avoid overwriting it
2023-05-24 20:22:01 +07:00
. useArguments (
IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files'
)
2023-05-12 22:00:39 +07:00
// Install only runtime + CLI
. useArguments ( IS_WINDOWS ? '-Runtime' : '--runtime' , 'dotnet' )
// Use latest stable version
. useArguments ( IS_WINDOWS ? '-Channel' : '--channel' , 'LTS' )
. execute ( ) ;
if ( runtimeInstallOutput . exitCode ) {
/ * *
* dotnetInstallScript will install CLI and runtime even if previous script haven ' t succeded ,
* so at this point it ' s too early to throw an error
* /
core . warning (
` Failed to install dotnet runtime + cli, exit code: ${ runtimeInstallOutput . exitCode } . ${ runtimeInstallOutput . stderr } `
) ;
}
2023-03-09 19:43:05 +07:00
2023-05-12 22:00:39 +07:00
/ * *
* Install dotnet over the latest version of
* dotnet CLI
* /
const dotnetInstallOutput = await new DotnetInstallScript ( )
// Don't overwrite CLI because it should be already installed
. useArguments (
IS_WINDOWS ? '-SkipNonVersionedFiles' : '--skip-non-versioned-files'
)
// Use version provided by user
. useVersion ( dotnetVersion , this . quality )
. execute ( ) ;
2023-03-09 19:43:05 +07:00
2023-05-12 22:00:39 +07:00
if ( dotnetInstallOutput . exitCode ) {
2023-03-09 19:43:05 +07:00
throw new Error (
2023-05-12 22:00:39 +07:00
` Failed to install dotnet, exit code: ${ dotnetInstallOutput . exitCode } . ${ dotnetInstallOutput . stderr } `
2023-03-09 19:43:05 +07:00
) ;
}
2023-05-12 22:00:39 +07:00
return this . parseInstalledVersion ( dotnetInstallOutput . stdout ) ;
2023-03-09 19:43:05 +07:00
}
2023-05-16 19:58:18 +07:00
private parseInstalledVersion ( stdout : string ) : string | null {
2023-05-15 16:45:07 +07:00
const regex = /(?<version>\d+\.\d+\.\d+[a-z0-9._-]*)/gm ;
const matchedResult = regex . exec ( stdout ) ;
2023-03-09 19:43:05 +07:00
2023-05-15 16:45:07 +07:00
if ( ! matchedResult ) {
2023-05-16 19:58:18 +07:00
core . warning ( ` Failed to parse installed by the script version of .NET ` ) ;
return null ;
2023-05-15 16:45:07 +07:00
}
return matchedResult . groups ! . version ;
2023-03-09 19:43:05 +07:00
}
}