2019-09-10 00:27:23 +07:00
|
|
|
// Load tempDirectory before it gets wiped by tool-cache
|
|
|
|
let tempDirectory = process.env['RUNNER_TEMPDIRECTORY'] || '';
|
|
|
|
import * as core from '@actions/core';
|
|
|
|
import * as exec from '@actions/exec';
|
|
|
|
import * as io from '@actions/io';
|
|
|
|
import * as tc from '@actions/tool-cache';
|
2020-01-26 13:37:54 +07:00
|
|
|
import hc = require('@actions/http-client');
|
2019-09-10 00:27:23 +07:00
|
|
|
import {chmodSync} from 'fs';
|
|
|
|
import * as os from 'os';
|
|
|
|
import * as path from 'path';
|
|
|
|
import * as semver from 'semver';
|
2020-04-04 23:05:12 +07:00
|
|
|
import { stringWriter } from 'xmlbuilder';
|
2019-09-10 00:27:23 +07:00
|
|
|
|
|
|
|
const IS_WINDOWS = process.platform === 'win32';
|
|
|
|
|
|
|
|
if (!tempDirectory) {
|
|
|
|
let baseLocation;
|
|
|
|
if (IS_WINDOWS) {
|
|
|
|
// On windows use the USERPROFILE env variable
|
|
|
|
baseLocation = process.env['USERPROFILE'] || 'C:\\';
|
|
|
|
} else {
|
|
|
|
if (process.platform === 'darwin') {
|
|
|
|
baseLocation = '/Users';
|
|
|
|
} else {
|
|
|
|
baseLocation = '/home';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
tempDirectory = path.join(baseLocation, 'actions', 'temp');
|
|
|
|
}
|
|
|
|
|
2020-04-04 23:05:12 +07:00
|
|
|
class DotNetVersionInfo {
|
|
|
|
major: number;
|
|
|
|
minor: number;
|
|
|
|
patch?: number;
|
|
|
|
|
2019-09-10 00:27:23 +07:00
|
|
|
constructor(version: string) {
|
2020-04-04 23:05:12 +07:00
|
|
|
//todo: add support for previews!
|
|
|
|
let regexResult = version.match(/^(\d+\.)(\d+\.)(\*|x|\d+)$/);
|
|
|
|
if(regexResult == null) {
|
|
|
|
throw 'Invalid version. Supported formats: 1.2.3, 1.2, 1.2.x, 1.2.*';
|
|
|
|
}
|
|
|
|
|
|
|
|
let parts : string[] = (regexResult as RegExpMatchArray).slice(1);
|
|
|
|
|
|
|
|
this.major = +(parts[0].replace('.',''));
|
|
|
|
this.minor = +(parts[1].replace('.',''));
|
|
|
|
|
|
|
|
if(parts.length > 2) {
|
|
|
|
// just set if it is a number
|
|
|
|
if(!isNaN(Number(parts[2]))) {
|
|
|
|
this.patch = +parts[2];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public isGeneric() : boolean {
|
|
|
|
return this.patch ? true : false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public toString() : string {
|
|
|
|
let version = this.major + "." + this.minor;
|
|
|
|
|
|
|
|
if(this.patch)
|
|
|
|
version += "." + this.patch;
|
|
|
|
|
|
|
|
return version;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ResolvedVersionInfo {
|
|
|
|
downloadUrls: string[];
|
|
|
|
resolvedVersion: string;
|
|
|
|
|
|
|
|
constructor(downloadUrls: string[], resolvedVersion: string) {
|
|
|
|
if(downloadUrls.length === 0) {
|
|
|
|
throw 'DownloadUrls can not be empty';
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!resolvedVersion) {
|
|
|
|
throw 'Resolved version is invalid';
|
2019-09-10 00:27:23 +07:00
|
|
|
}
|
2020-04-04 23:05:12 +07:00
|
|
|
|
|
|
|
this.downloadUrls = downloadUrls;
|
|
|
|
this.resolvedVersion = resolvedVersion;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class DotnetCoreInstaller {
|
|
|
|
constructor(version: string) {
|
|
|
|
this.versionInfo = new DotNetVersionInfo(version);
|
2019-09-10 00:27:23 +07:00
|
|
|
this.cachedToolName = 'dncs';
|
|
|
|
this.arch = 'x64';
|
|
|
|
}
|
|
|
|
|
|
|
|
public async installDotnet() {
|
|
|
|
// Check cache
|
2020-04-04 23:05:12 +07:00
|
|
|
let toolPath: string = "";
|
2019-09-10 00:27:23 +07:00
|
|
|
let osSuffixes = await this.detectMachineOS();
|
|
|
|
let parts = osSuffixes[0].split('-');
|
|
|
|
if (parts.length > 1) {
|
|
|
|
this.arch = parts[1];
|
|
|
|
}
|
2020-04-04 23:05:12 +07:00
|
|
|
|
|
|
|
// If version is not generic -> look up cache
|
|
|
|
if(!this.versionInfo.isGeneric())
|
|
|
|
toolPath = this.getLocalTool(this.versionInfo.toString());
|
2019-09-10 00:27:23 +07:00
|
|
|
|
|
|
|
if (!toolPath) {
|
|
|
|
// download, extract, cache
|
2020-04-04 23:05:12 +07:00
|
|
|
console.log('Getting a download url', this.versionInfo.toString());
|
|
|
|
let resolvedVersionInfo = await this.resolveInfos(osSuffixes, this.versionInfo);
|
|
|
|
|
|
|
|
//Check if cache exists for resolved version
|
|
|
|
toolPath = this.getLocalTool(resolvedVersionInfo.resolvedVersion);
|
|
|
|
if(!toolPath) {
|
|
|
|
//If not exists install it
|
|
|
|
toolPath = await this.downloadAndInstall(resolvedVersionInfo);
|
|
|
|
} else {
|
|
|
|
console.log('Using cached tool');
|
|
|
|
}
|
2019-09-10 00:27:23 +07:00
|
|
|
} else {
|
|
|
|
console.log('Using cached tool');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Need to set this so that .NET Core global tools find the right locations.
|
2019-11-08 23:15:28 +07:00
|
|
|
core.exportVariable('DOTNET_ROOT', toolPath);
|
2019-09-10 00:27:23 +07:00
|
|
|
|
|
|
|
// Prepend the tools path. instructs the agent to prepend for future tasks
|
|
|
|
core.addPath(toolPath);
|
|
|
|
}
|
|
|
|
|
2020-04-04 23:05:12 +07:00
|
|
|
private getLocalTool(version: string): string {
|
|
|
|
console.log('Checking tool cache', version);
|
|
|
|
return tc.find(this.cachedToolName, version, this.arch);
|
2019-09-10 00:27:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
private async detectMachineOS(): Promise<string[]> {
|
|
|
|
let osSuffix: string[] = [];
|
|
|
|
let output = '';
|
|
|
|
|
|
|
|
let resultCode = 0;
|
|
|
|
if (IS_WINDOWS) {
|
|
|
|
let escapedScript = path
|
|
|
|
.join(__dirname, '..', 'externals', 'get-os-platform.ps1')
|
|
|
|
.replace(/'/g, "''");
|
|
|
|
let command = `& '${escapedScript}'`;
|
|
|
|
|
|
|
|
const powershellPath = await io.which('powershell', true);
|
|
|
|
resultCode = await exec.exec(
|
|
|
|
`"${powershellPath}"`,
|
|
|
|
[
|
|
|
|
'-NoLogo',
|
|
|
|
'-Sta',
|
|
|
|
'-NoProfile',
|
|
|
|
'-NonInteractive',
|
|
|
|
'-ExecutionPolicy',
|
|
|
|
'Unrestricted',
|
|
|
|
'-Command',
|
|
|
|
command
|
|
|
|
],
|
|
|
|
{
|
|
|
|
listeners: {
|
|
|
|
stdout: (data: Buffer) => {
|
|
|
|
output += data.toString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
let scriptPath = path.join(
|
|
|
|
__dirname,
|
|
|
|
'..',
|
|
|
|
'externals',
|
|
|
|
'get-os-distro.sh'
|
|
|
|
);
|
|
|
|
chmodSync(scriptPath, '777');
|
|
|
|
|
|
|
|
const toolPath = await io.which(scriptPath, true);
|
|
|
|
resultCode = await exec.exec(`"${toolPath}"`, [], {
|
|
|
|
listeners: {
|
|
|
|
stdout: (data: Buffer) => {
|
|
|
|
output += data.toString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (resultCode != 0) {
|
|
|
|
throw `Failed to detect os with result code ${resultCode}. Output: ${output}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
let index;
|
|
|
|
if ((index = output.indexOf('Primary:')) >= 0) {
|
|
|
|
let primary = output.substr(index + 'Primary:'.length).split(os.EOL)[0];
|
|
|
|
osSuffix.push(primary);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((index = output.indexOf('Legacy:')) >= 0) {
|
|
|
|
let legacy = output.substr(index + 'Legacy:'.length).split(os.EOL)[0];
|
|
|
|
osSuffix.push(legacy);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (osSuffix.length == 0) {
|
|
|
|
throw 'Could not detect platform';
|
|
|
|
}
|
|
|
|
|
|
|
|
return osSuffix;
|
|
|
|
}
|
|
|
|
|
2020-04-04 23:05:12 +07:00
|
|
|
private async downloadAndInstall(resolvedVersionInfo: ResolvedVersionInfo) {
|
2019-09-10 00:27:23 +07:00
|
|
|
let downloaded = false;
|
|
|
|
let downloadPath = '';
|
2020-04-04 23:05:12 +07:00
|
|
|
for (const url of resolvedVersionInfo.downloadUrls) {
|
2019-09-10 00:27:23 +07:00
|
|
|
try {
|
|
|
|
downloadPath = await tc.downloadTool(url);
|
|
|
|
downloaded = true;
|
|
|
|
break;
|
|
|
|
} catch (error) {
|
2020-04-04 23:05:12 +07:00
|
|
|
console.log('Could not Download', url, JSON.stringify(error));
|
2019-09-10 00:27:23 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!downloaded) {
|
|
|
|
throw 'Failed to download package';
|
|
|
|
}
|
|
|
|
|
|
|
|
// extract
|
|
|
|
console.log('Extracting Package', downloadPath);
|
|
|
|
let extPath: string = IS_WINDOWS
|
|
|
|
? await tc.extractZip(downloadPath)
|
|
|
|
: await tc.extractTar(downloadPath);
|
|
|
|
|
|
|
|
// cache tool
|
|
|
|
console.log('Caching tool');
|
|
|
|
let cachedDir = await tc.cacheDir(
|
|
|
|
extPath,
|
|
|
|
this.cachedToolName,
|
2020-04-04 23:05:12 +07:00
|
|
|
resolvedVersionInfo.resolvedVersion,
|
2019-09-10 00:27:23 +07:00
|
|
|
this.arch
|
|
|
|
);
|
|
|
|
|
2020-04-04 23:05:12 +07:00
|
|
|
console.log('Successfully installed', resolvedVersionInfo.resolvedVersion);
|
2019-09-10 00:27:23 +07:00
|
|
|
return cachedDir;
|
|
|
|
}
|
|
|
|
|
|
|
|
// OsSuffixes - The suffix which is a part of the file name ex- linux-x64, windows-x86
|
|
|
|
// Type - SDK / Runtime
|
|
|
|
// Version - Version of the SDK/Runtime
|
2020-04-04 23:05:12 +07:00
|
|
|
private async resolveInfos(
|
2019-09-10 00:27:23 +07:00
|
|
|
osSuffixes: string[],
|
2020-04-04 23:05:12 +07:00
|
|
|
versionInfo: DotNetVersionInfo
|
|
|
|
): Promise<ResolvedVersionInfo> {
|
2019-09-10 00:27:23 +07:00
|
|
|
|
2020-01-26 13:37:54 +07:00
|
|
|
const httpClient = new hc.HttpClient('actions/setup-dotnet', [], {
|
|
|
|
allowRetries: true,
|
|
|
|
maxRetries: 3
|
|
|
|
});
|
2019-11-08 23:15:28 +07:00
|
|
|
const releasesJsonUrl: string = await this.getReleasesJsonUrl(
|
2020-01-26 13:37:54 +07:00
|
|
|
httpClient,
|
2020-04-04 23:05:12 +07:00
|
|
|
[String(versionInfo.major), String(versionInfo.minor)]
|
2019-11-08 23:15:28 +07:00
|
|
|
);
|
|
|
|
|
2020-01-26 13:37:54 +07:00
|
|
|
const releasesResponse = await httpClient.getJson<any>(releasesJsonUrl);
|
|
|
|
const releasesResult = releasesResponse.result || {};
|
|
|
|
let releasesInfo: any[] = releasesResult['releases'];
|
2019-09-10 00:27:23 +07:00
|
|
|
releasesInfo = releasesInfo.filter((releaseInfo: any) => {
|
|
|
|
return (
|
2020-04-04 23:05:12 +07:00
|
|
|
semver.satisfies(releaseInfo['sdk']['version'], versionInfo.toString()) ||
|
|
|
|
semver.satisfies(releaseInfo['sdk']['version-display'], versionInfo.toString())
|
2019-09-10 00:27:23 +07:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2020-04-04 23:05:12 +07:00
|
|
|
//Sort for latest version
|
|
|
|
releasesInfo = releasesInfo.sort((a,b) => semver.rcompare(a['sdk']['version'],b['sdk']['version']));
|
|
|
|
|
|
|
|
let downloadedVersion : string = '';
|
|
|
|
let downloadUrls: string[] = [];
|
|
|
|
|
2019-09-10 00:27:23 +07:00
|
|
|
if (releasesInfo.length != 0) {
|
|
|
|
let release = releasesInfo[0];
|
2020-04-04 23:05:12 +07:00
|
|
|
|
|
|
|
downloadedVersion = release['sdk']['version'];
|
|
|
|
|
2019-11-08 23:15:28 +07:00
|
|
|
let files: any[] = release['sdk']['files'];
|
|
|
|
files = files.filter((file: any) => {
|
|
|
|
if (file['rid'] == osSuffixes[0] || file['rid'] == osSuffixes[1]) {
|
|
|
|
return (
|
|
|
|
file['url'].endsWith('.zip') || file['url'].endsWith('.tar.gz')
|
|
|
|
);
|
2019-09-10 00:27:23 +07:00
|
|
|
}
|
2019-11-08 23:15:28 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
if (files.length > 0) {
|
|
|
|
files.forEach((file: any) => {
|
|
|
|
downloadUrls.push(file['url']);
|
|
|
|
});
|
2019-09-10 00:27:23 +07:00
|
|
|
} else {
|
2019-11-08 23:15:28 +07:00
|
|
|
throw `The specified version's download links are not correctly formed in the supported versions document => ${releasesJsonUrl}`;
|
2019-09-10 00:27:23 +07:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.log(
|
2020-04-04 23:05:12 +07:00
|
|
|
`Could not fetch download information for version ${versionInfo.toString()}`
|
2019-09-10 00:27:23 +07:00
|
|
|
);
|
2020-04-04 23:05:12 +07:00
|
|
|
|
|
|
|
if(!versionInfo.isGeneric()) {
|
|
|
|
console.log('Using fallback');
|
|
|
|
|
|
|
|
downloadUrls = await this.getFallbackDownloadUrls(versionInfo.toString());
|
|
|
|
downloadedVersion = versionInfo.toString();
|
|
|
|
} else {
|
|
|
|
console.log('Unable to use fallback, version is generic!');
|
|
|
|
}
|
2019-09-10 00:27:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (downloadUrls.length == 0) {
|
2020-04-04 23:05:12 +07:00
|
|
|
throw `Could not construct download URL. Please ensure that specified version ${versionInfo.toString()}/${downloadedVersion} is valid.`;
|
2019-09-10 00:27:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
core.debug(`Got download urls ${downloadUrls}`);
|
|
|
|
|
2020-04-04 23:05:12 +07:00
|
|
|
return new ResolvedVersionInfo(downloadUrls, downloadedVersion);
|
2019-09-10 00:27:23 +07:00
|
|
|
}
|
|
|
|
|
2019-11-08 23:15:28 +07:00
|
|
|
private async getReleasesJsonUrl(
|
2020-01-26 13:37:54 +07:00
|
|
|
httpClient: hc.HttpClient,
|
2019-11-08 23:15:28 +07:00
|
|
|
versionParts: string[]
|
|
|
|
): Promise<string> {
|
2020-01-26 13:37:54 +07:00
|
|
|
const response = await httpClient.getJson<any>(DotNetCoreIndexUrl);
|
|
|
|
const result = response.result || {};
|
|
|
|
let releasesInfo: any[] = result['releases-index'];
|
2019-11-08 23:15:28 +07:00
|
|
|
releasesInfo = releasesInfo.filter((info: any) => {
|
|
|
|
// channel-version is the first 2 elements of the version (e.g. 2.1), filter out versions that don't match 2.1.x.
|
|
|
|
const sdkParts: string[] = info['channel-version'].split('.');
|
|
|
|
if (versionParts.length >= 2 && versionParts[1] != 'x') {
|
|
|
|
return versionParts[0] == sdkParts[0] && versionParts[1] == sdkParts[1];
|
|
|
|
}
|
|
|
|
return versionParts[0] == sdkParts[0];
|
|
|
|
});
|
|
|
|
if (releasesInfo.length === 0) {
|
|
|
|
throw `Could not find info for version ${versionParts.join(
|
|
|
|
'.'
|
|
|
|
)} at ${DotNetCoreIndexUrl}`;
|
|
|
|
}
|
|
|
|
return releasesInfo[0]['releases.json'];
|
2019-09-10 00:27:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
private async getFallbackDownloadUrls(version: string): Promise<string[]> {
|
|
|
|
let primaryUrlSearchString: string;
|
|
|
|
let legacyUrlSearchString: string;
|
|
|
|
let output = '';
|
|
|
|
let resultCode = 0;
|
|
|
|
|
|
|
|
if (IS_WINDOWS) {
|
|
|
|
let escapedScript = path
|
|
|
|
.join(__dirname, '..', 'externals', 'install-dotnet.ps1')
|
|
|
|
.replace(/'/g, "''");
|
|
|
|
let command = `& '${escapedScript}' -Version ${version} -DryRun`;
|
|
|
|
|
|
|
|
const powershellPath = await io.which('powershell', true);
|
|
|
|
resultCode = await exec.exec(
|
|
|
|
`"${powershellPath}"`,
|
|
|
|
[
|
|
|
|
'-NoLogo',
|
|
|
|
'-Sta',
|
|
|
|
'-NoProfile',
|
|
|
|
'-NonInteractive',
|
|
|
|
'-ExecutionPolicy',
|
|
|
|
'Unrestricted',
|
|
|
|
'-Command',
|
|
|
|
command
|
|
|
|
],
|
|
|
|
{
|
|
|
|
listeners: {
|
|
|
|
stdout: (data: Buffer) => {
|
|
|
|
output += data.toString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
primaryUrlSearchString = 'dotnet-install: Primary named payload URL: ';
|
|
|
|
legacyUrlSearchString = 'dotnet-install: Legacy named payload URL: ';
|
|
|
|
} else {
|
|
|
|
let escapedScript = path
|
|
|
|
.join(__dirname, '..', 'externals', 'install-dotnet.sh')
|
|
|
|
.replace(/'/g, "''");
|
|
|
|
chmodSync(escapedScript, '777');
|
|
|
|
|
|
|
|
const scriptPath = await io.which(escapedScript, true);
|
|
|
|
resultCode = await exec.exec(
|
|
|
|
`"${scriptPath}"`,
|
|
|
|
['--version', version, '--dry-run'],
|
|
|
|
{
|
|
|
|
listeners: {
|
|
|
|
stdout: (data: Buffer) => {
|
|
|
|
output += data.toString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
primaryUrlSearchString = 'dotnet-install: Primary named payload URL: ';
|
|
|
|
legacyUrlSearchString = 'dotnet-install: Legacy named payload URL: ';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (resultCode != 0) {
|
|
|
|
throw `Failed to get download urls with result code ${resultCode}. ${output}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
let primaryUrl: string = '';
|
|
|
|
let legacyUrl: string = '';
|
|
|
|
if (!!output && output.length > 0) {
|
|
|
|
let lines: string[] = output.split(os.EOL);
|
|
|
|
|
|
|
|
// Fallback to \n if initial split doesn't work (not consistent across versions)
|
|
|
|
if (lines.length === 1) {
|
|
|
|
lines = output.split('\n');
|
|
|
|
}
|
|
|
|
if (!!lines && lines.length > 0) {
|
|
|
|
lines.forEach((line: string) => {
|
|
|
|
if (!line) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var primarySearchStringIndex = line.indexOf(primaryUrlSearchString);
|
|
|
|
if (primarySearchStringIndex > -1) {
|
|
|
|
primaryUrl = line.substring(
|
|
|
|
primarySearchStringIndex + primaryUrlSearchString.length
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var legacySearchStringIndex = line.indexOf(legacyUrlSearchString);
|
|
|
|
if (legacySearchStringIndex > -1) {
|
|
|
|
legacyUrl = line.substring(
|
|
|
|
legacySearchStringIndex + legacyUrlSearchString.length
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return [primaryUrl, legacyUrl];
|
|
|
|
}
|
|
|
|
|
2020-04-04 23:05:12 +07:00
|
|
|
private versionInfo: DotNetVersionInfo;
|
2019-09-10 00:27:23 +07:00
|
|
|
private cachedToolName: string;
|
|
|
|
private arch: string;
|
|
|
|
}
|
|
|
|
|
2019-11-08 23:15:28 +07:00
|
|
|
const DotNetCoreIndexUrl: string =
|
|
|
|
'https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json';
|