Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
setup-go/src/installer.ts
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
494 lines (425 sloc)
13.2 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as tc from '@actions/tool-cache'; | |
import * as core from '@actions/core'; | |
import * as path from 'path'; | |
import * as semver from 'semver'; | |
import * as httpm from '@actions/http-client'; | |
import * as sys from './system'; | |
import fs from 'fs'; | |
import os from 'os'; | |
import {StableReleaseAlias} from './utils'; | |
type InstallationType = 'dist' | 'manifest'; | |
export interface IGoVersionFile { | |
filename: string; | |
// darwin, linux, windows | |
os: string; | |
arch: string; | |
} | |
export interface IGoVersion { | |
version: string; | |
stable: boolean; | |
files: IGoVersionFile[]; | |
} | |
export interface IGoVersionInfo { | |
type: InstallationType; | |
downloadUrl: string; | |
resolvedVersion: string; | |
fileName: string; | |
} | |
export async function getGo( | |
versionSpec: string, | |
checkLatest: boolean, | |
auth: string | undefined, | |
arch = os.arch() | |
) { | |
let manifest: tc.IToolRelease[] | undefined; | |
const osPlat: string = os.platform(); | |
if ( | |
versionSpec === StableReleaseAlias.Stable || | |
versionSpec === StableReleaseAlias.OldStable | |
) { | |
manifest = await getManifest(auth); | |
let stableVersion = await resolveStableVersionInput( | |
versionSpec, | |
arch, | |
osPlat, | |
manifest | |
); | |
if (!stableVersion) { | |
stableVersion = await resolveStableVersionDist(versionSpec, arch); | |
if (!stableVersion) { | |
throw new Error( | |
`Unable to find Go version '${versionSpec}' for platform ${osPlat} and architecture ${arch}.` | |
); | |
} | |
} | |
core.info(`${versionSpec} version resolved as ${stableVersion}`); | |
versionSpec = stableVersion; | |
} | |
if (checkLatest) { | |
core.info('Attempting to resolve the latest version from the manifest...'); | |
const resolvedVersion = await resolveVersionFromManifest( | |
versionSpec, | |
true, | |
auth, | |
arch, | |
manifest | |
); | |
if (resolvedVersion) { | |
versionSpec = resolvedVersion; | |
core.info(`Resolved as '${versionSpec}'`); | |
} else { | |
core.info(`Failed to resolve version ${versionSpec} from manifest`); | |
} | |
} | |
// check cache | |
const toolPath = tc.find('go', versionSpec, arch); | |
// If not found in cache, download | |
if (toolPath) { | |
core.info(`Found in cache @ ${toolPath}`); | |
return toolPath; | |
} | |
core.info(`Attempting to download ${versionSpec}...`); | |
let downloadPath = ''; | |
let info: IGoVersionInfo | null = null; | |
// | |
// Try download from internal distribution (popular versions only) | |
// | |
try { | |
info = await getInfoFromManifest(versionSpec, true, auth, arch, manifest); | |
if (info) { | |
downloadPath = await installGoVersion(info, auth, arch); | |
} else { | |
core.info( | |
'Not found in manifest. Falling back to download directly from Go' | |
); | |
} | |
} catch (err) { | |
if ( | |
err instanceof tc.HTTPError && | |
(err.httpStatusCode === 403 || err.httpStatusCode === 429) | |
) { | |
core.info( | |
`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` | |
); | |
} else { | |
core.info((err as Error).message); | |
} | |
core.debug((err as Error).stack ?? ''); | |
core.info('Falling back to download directly from Go'); | |
} | |
// | |
// Download from storage.googleapis.com | |
// | |
if (!downloadPath) { | |
info = await getInfoFromDist(versionSpec, arch); | |
if (!info) { | |
throw new Error( | |
`Unable to find Go version '${versionSpec}' for platform ${osPlat} and architecture ${arch}.` | |
); | |
} | |
try { | |
core.info('Install from dist'); | |
downloadPath = await installGoVersion(info, undefined, arch); | |
} catch (err) { | |
throw new Error(`Failed to download version ${versionSpec}: ${err}`); | |
} | |
} | |
return downloadPath; | |
} | |
async function resolveVersionFromManifest( | |
versionSpec: string, | |
stable: boolean, | |
auth: string | undefined, | |
arch: string, | |
manifest: tc.IToolRelease[] | undefined | |
): Promise<string | undefined> { | |
try { | |
const info = await getInfoFromManifest( | |
versionSpec, | |
stable, | |
auth, | |
arch, | |
manifest | |
); | |
return info?.resolvedVersion; | |
} catch (err) { | |
core.info('Unable to resolve a version from the manifest...'); | |
core.debug((err as Error).message); | |
} | |
} | |
// for github hosted windows runner handle latency of OS drive | |
// by avoiding write operations to C: | |
async function cacheWindowsDir( | |
extPath: string, | |
tool: string, | |
version: string, | |
arch: string | |
): Promise<string | false> { | |
if (os.platform() !== 'win32') return false; | |
// make sure the action runs in the hosted environment | |
if ( | |
process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' && | |
process.env['AGENT_ISSELFHOSTED'] === '1' | |
) | |
return false; | |
const defaultToolCacheRoot = process.env['RUNNER_TOOL_CACHE']; | |
if (!defaultToolCacheRoot) return false; | |
if (!fs.existsSync('d:\\') || !fs.existsSync('c:\\')) return false; | |
const actualToolCacheRoot = defaultToolCacheRoot | |
.replace('C:', 'D:') | |
.replace('c:', 'd:'); | |
// make toolcache root to be on drive d: | |
process.env['RUNNER_TOOL_CACHE'] = actualToolCacheRoot; | |
const actualToolCacheDir = await tc.cacheDir(extPath, tool, version, arch); | |
// create a link from c: to d: | |
const defaultToolCacheDir = actualToolCacheDir.replace( | |
actualToolCacheRoot, | |
defaultToolCacheRoot | |
); | |
fs.mkdirSync(path.dirname(defaultToolCacheDir), {recursive: true}); | |
fs.symlinkSync(actualToolCacheDir, defaultToolCacheDir, 'junction'); | |
core.info(`Created link ${defaultToolCacheDir} => ${actualToolCacheDir}`); | |
const actualToolCacheCompleteFile = `${actualToolCacheDir}.complete`; | |
const defaultToolCacheCompleteFile = `${defaultToolCacheDir}.complete`; | |
fs.symlinkSync( | |
actualToolCacheCompleteFile, | |
defaultToolCacheCompleteFile, | |
'file' | |
); | |
core.info( | |
`Created link ${defaultToolCacheCompleteFile} => ${actualToolCacheCompleteFile}` | |
); | |
// make outer code to continue using toolcache as if it were installed on c: | |
// restore toolcache root to default drive c: | |
process.env['RUNNER_TOOL_CACHE'] = defaultToolCacheRoot; | |
return defaultToolCacheDir; | |
} | |
async function addExecutablesToToolCache( | |
extPath: string, | |
info: IGoVersionInfo, | |
arch: string | |
): Promise<string> { | |
const tool = 'go'; | |
const version = makeSemver(info.resolvedVersion); | |
return ( | |
(await cacheWindowsDir(extPath, tool, version, arch)) || | |
(await tc.cacheDir(extPath, tool, version, arch)) | |
); | |
} | |
async function installGoVersion( | |
info: IGoVersionInfo, | |
auth: string | undefined, | |
arch: string | |
): Promise<string> { | |
core.info(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`); | |
// Windows requires that we keep the extension (.zip) for extraction | |
const isWindows = os.platform() === 'win32'; | |
const tempDir = process.env.RUNNER_TEMP || '.'; | |
const fileName = isWindows ? path.join(tempDir, info.fileName) : undefined; | |
const downloadPath = await tc.downloadTool(info.downloadUrl, fileName, auth); | |
core.info('Extracting Go...'); | |
let extPath = await extractGoArchive(downloadPath); | |
core.info(`Successfully extracted go to ${extPath}`); | |
if (info.type === 'dist') { | |
extPath = path.join(extPath, 'go'); | |
} | |
core.info('Adding to the cache ...'); | |
const toolCacheDir = await addExecutablesToToolCache(extPath, info, arch); | |
core.info(`Successfully cached go to ${toolCacheDir}`); | |
return toolCacheDir; | |
} | |
export async function extractGoArchive(archivePath: string): Promise<string> { | |
const platform = os.platform(); | |
let extPath: string; | |
if (platform === 'win32') { | |
extPath = await tc.extractZip(archivePath); | |
} else { | |
extPath = await tc.extractTar(archivePath); | |
} | |
return extPath; | |
} | |
export async function getManifest(auth: string | undefined) { | |
return tc.getManifestFromRepo('actions', 'go-versions', auth, 'main'); | |
} | |
export async function getInfoFromManifest( | |
versionSpec: string, | |
stable: boolean, | |
auth: string | undefined, | |
arch = os.arch(), | |
manifest?: tc.IToolRelease[] | undefined | |
): Promise<IGoVersionInfo | null> { | |
let info: IGoVersionInfo | null = null; | |
if (!manifest) { | |
core.debug('No manifest cached'); | |
manifest = await getManifest(auth); | |
} | |
core.info(`matching ${versionSpec}...`); | |
const rel = await tc.findFromManifest(versionSpec, stable, manifest, arch); | |
if (rel && rel.files.length > 0) { | |
info = <IGoVersionInfo>{}; | |
info.type = 'manifest'; | |
info.resolvedVersion = rel.version; | |
info.downloadUrl = rel.files[0].download_url; | |
info.fileName = rel.files[0].filename; | |
} | |
return info; | |
} | |
async function getInfoFromDist( | |
versionSpec: string, | |
arch: string | |
): Promise<IGoVersionInfo | null> { | |
const version: IGoVersion | undefined = await findMatch(versionSpec, arch); | |
if (!version) { | |
return null; | |
} | |
const downloadUrl = `https://storage.googleapis.com/golang/${version.files[0].filename}`; | |
return <IGoVersionInfo>{ | |
type: 'dist', | |
downloadUrl: downloadUrl, | |
resolvedVersion: version.version, | |
fileName: version.files[0].filename | |
}; | |
} | |
export async function findMatch( | |
versionSpec: string, | |
arch = os.arch() | |
): Promise<IGoVersion | undefined> { | |
const archFilter = sys.getArch(arch); | |
const platFilter = sys.getPlatform(); | |
let result: IGoVersion | undefined; | |
let match: IGoVersion | undefined; | |
const dlUrl = 'https://golang.org/dl/?mode=json&include=all'; | |
const candidates: IGoVersion[] | null = await module.exports.getVersionsDist( | |
dlUrl | |
); | |
if (!candidates) { | |
throw new Error(`golang download url did not return results`); | |
} | |
let goFile: IGoVersionFile | undefined; | |
for (let i = 0; i < candidates.length; i++) { | |
const candidate: IGoVersion = candidates[i]; | |
const version = makeSemver(candidate.version); | |
core.debug(`check ${version} satisfies ${versionSpec}`); | |
if (semver.satisfies(version, versionSpec)) { | |
goFile = candidate.files.find(file => { | |
core.debug( | |
`${file.arch}===${archFilter} && ${file.os}===${platFilter}` | |
); | |
return file.arch === archFilter && file.os === platFilter; | |
}); | |
if (goFile) { | |
core.debug(`matched ${candidate.version}`); | |
match = candidate; | |
break; | |
} | |
} | |
} | |
if (match && goFile) { | |
// clone since we're mutating the file list to be only the file that matches | |
result = <IGoVersion>Object.assign({}, match); | |
result.files = [goFile]; | |
} | |
return result; | |
} | |
export async function getVersionsDist( | |
dlUrl: string | |
): Promise<IGoVersion[] | null> { | |
// this returns versions descending so latest is first | |
const http: httpm.HttpClient = new httpm.HttpClient('setup-go', [], { | |
allowRedirects: true, | |
maxRedirects: 3 | |
}); | |
return (await http.getJson<IGoVersion[]>(dlUrl)).result; | |
} | |
// | |
// Convert the go version syntax into semver for semver matching | |
// 1.13.1 => 1.13.1 | |
// 1.13 => 1.13.0 | |
// 1.10beta1 => 1.10.0-beta.1, 1.10rc1 => 1.10.0-rc.1 | |
// 1.8.5beta1 => 1.8.5-beta.1, 1.8.5rc1 => 1.8.5-rc.1 | |
export function makeSemver(version: string): string { | |
version = version.replace('go', ''); | |
version = version.replace('beta', '-beta.').replace('rc', '-rc.'); | |
const parts = version.split('-'); | |
const semVersion = semver.coerce(parts[0])?.version; | |
if (!semVersion) { | |
throw new Error( | |
`The version: ${version} can't be changed to SemVer notation` | |
); | |
} | |
if (!parts[1]) { | |
return semVersion; | |
} | |
const fullVersion = semver.valid(`${semVersion}-${parts[1]}`); | |
if (!fullVersion) { | |
throw new Error( | |
`The version: ${version} can't be changed to SemVer notation` | |
); | |
} | |
return fullVersion; | |
} | |
export function parseGoVersionFile(versionFilePath: string): string { | |
const contents = fs.readFileSync(versionFilePath).toString(); | |
if ( | |
path.basename(versionFilePath) === 'go.mod' || | |
path.basename(versionFilePath) === 'go.work' | |
) { | |
const match = contents.match(/^go (\d+(\.\d+)*)/m); | |
return match ? match[1] : ''; | |
} | |
return contents.trim(); | |
} | |
async function resolveStableVersionDist(versionSpec: string, arch: string) { | |
const archFilter = sys.getArch(arch); | |
const platFilter = sys.getPlatform(); | |
const dlUrl = 'https://golang.org/dl/?mode=json&include=all'; | |
const candidates: IGoVersion[] | null = await module.exports.getVersionsDist( | |
dlUrl | |
); | |
if (!candidates) { | |
throw new Error(`golang download url did not return results`); | |
} | |
const fixedCandidates = candidates.map(item => { | |
return { | |
...item, | |
version: makeSemver(item.version) | |
}; | |
}); | |
const stableVersion = await resolveStableVersionInput( | |
versionSpec, | |
archFilter, | |
platFilter, | |
fixedCandidates | |
); | |
return stableVersion; | |
} | |
export async function resolveStableVersionInput( | |
versionSpec: string, | |
arch: string, | |
platform: string, | |
manifest: tc.IToolRelease[] | IGoVersion[] | |
) { | |
const releases = manifest | |
.map(item => { | |
const index = item.files.findIndex( | |
item => item.arch === arch && item.filename.includes(platform) | |
); | |
if (index === -1) { | |
return ''; | |
} | |
return item.version; | |
}) | |
.filter(item => !!item && !semver.prerelease(item)); | |
if (versionSpec === StableReleaseAlias.Stable) { | |
return releases[0]; | |
} else { | |
const versions = releases.map( | |
release => `${semver.major(release)}.${semver.minor(release)}` | |
); | |
const uniqueVersions = Array.from(new Set(versions)); | |
const oldstableVersion = releases.find(item => | |
item.startsWith(uniqueVersions[1]) | |
); | |
return oldstableVersion; | |
} | |
} |