Files
vercel/packages/go/go-helpers.ts
Nathan Rajlich 1dc05428d7 [go][node][python][ruby] Remove @now/build-utils backwards compat hack (#6603)
`@now/build-utils` has not been published in a long time, so let's remove this logic.
2021-08-17 15:29:52 +00:00

200 lines
5.9 KiB
TypeScript

import tar from 'tar';
import execa from 'execa';
import fetch from 'node-fetch';
import { mkdirp, pathExists, readFile } from 'fs-extra';
import { join } from 'path';
import stringArgv from 'string-argv';
import { debug } from '@vercel/build-utils';
const versionMap = new Map([
['1.16', '1.16'],
['1.15', '1.15.8'],
['1.14', '1.14.15'],
['1.13', '1.13.15'],
]);
const archMap = new Map([
['x64', 'amd64'],
['x86', '386'],
]);
const platformMap = new Map([['win32', 'windows']]);
export const cacheDir = join('.vercel', 'cache', 'golang');
const getGoDir = (workPath: string) => join(workPath, cacheDir);
const GO_FLAGS = process.platform === 'win32' ? [] : ['-ldflags', '-s -w'];
const GO_MIN_VERSION = 13;
const getPlatform = (p: string) => platformMap.get(p) || p;
const getArch = (a: string) => archMap.get(a) || a;
const getGoUrl = (version: string, platform: string, arch: string) => {
const goArch = getArch(arch);
const goPlatform = getPlatform(platform);
const ext = platform === 'win32' ? 'zip' : 'tar.gz';
return `https://dl.google.com/go/go${version}.${goPlatform}-${goArch}.${ext}`;
};
export const OUT_EXTENSION = process.platform === 'win32' ? '.exe' : '';
export async function getAnalyzedEntrypoint(
workPath: string,
filePath: string,
modulePath: string
) {
debug('Analyzing entrypoint %o with modulePath %o', filePath, modulePath);
const bin = join(__dirname, `analyze${OUT_EXTENSION}`);
const isAnalyzeExist = await pathExists(bin);
if (!isAnalyzeExist) {
const src = join(__dirname, 'util', 'analyze.go');
const go = await downloadGo(workPath, modulePath);
await go.build(src, bin);
}
const args = [`-modpath=${modulePath}`, filePath];
const analyzed = await execa.stdout(bin, args);
debug('Analyzed entrypoint %o', analyzed);
return analyzed;
}
// Creates a `$GOPATH` directory tree, as per `go help gopath` instructions.
// Without this, `go` won't recognize the `$GOPATH`.
function createGoPathTree(goPath: string, platform: string, arch: string) {
const tuple = `${getPlatform(platform)}_${getArch(arch)}`;
debug('Creating GOPATH directory structure for %o (%s)', goPath, tuple);
return Promise.all([
mkdirp(join(goPath, 'bin')),
mkdirp(join(goPath, 'pkg', tuple)),
]);
}
class GoWrapper {
private env: { [key: string]: string };
private opts: execa.Options;
constructor(env: { [key: string]: string }, opts: execa.Options = {}) {
if (!opts.cwd) {
opts.cwd = process.cwd();
}
this.env = env;
this.opts = opts;
}
private execute(...args: string[]) {
const { opts, env } = this;
debug('Exec %o', `go ${args.join(' ')}`);
return execa('go', args, { stdio: 'pipe', ...opts, env });
}
mod() {
return this.execute('mod', 'tidy');
}
get(src?: string) {
const args = ['get'];
if (src) {
debug('Fetching `go` dependencies for file %o', src);
args.push(src);
} else {
debug('Fetching `go` dependencies for cwd %o', this.opts.cwd);
}
return this.execute(...args);
}
build(src: string | string[], dest: string) {
debug('Building optimized `go` binary %o -> %o', src, dest);
const sources = Array.isArray(src) ? src : [src];
const flags = process.env.GO_BUILD_FLAGS
? stringArgv(process.env.GO_BUILD_FLAGS)
: GO_FLAGS;
return this.execute('build', ...flags, '-o', dest, ...sources);
}
}
export async function createGo(
workPath: string,
goPath: string,
platform = process.platform,
arch = process.arch,
opts: execa.Options = {},
goMod = false
) {
const binPath = join(getGoDir(workPath), 'bin');
debug(`Adding ${binPath} to PATH`);
const path = `${binPath}:${process.env.PATH}`;
const env: { [key: string]: string } = {
...process.env,
PATH: path,
GOPATH: goPath,
...opts.env,
};
if (goMod) {
env.GO111MODULE = 'on';
}
await createGoPathTree(goPath, platform, arch);
return new GoWrapper(env, opts);
}
export async function downloadGo(workPath: string, modulePath: string) {
const dir = getGoDir(workPath);
const { platform, arch } = process;
const version = await parseGoVersion(modulePath);
// Check if `go` is already installed in user's `$PATH`
const { failed, stdout } = await execa('go', ['version'], { reject: false });
if (!failed && parseInt(stdout.split('.')[1]) >= GO_MIN_VERSION) {
debug('Using system installed version of `go`: %o', stdout.trim());
return createGo(workPath, dir, platform, arch);
}
// Check `go` bin in cacheDir
const isGoExist = await pathExists(join(dir, 'bin'));
if (!isGoExist) {
debug('Installing `go` v%s to %o for %s %s', version, dir, platform, arch);
const url = getGoUrl(version, platform, arch);
debug('Downloading `go` URL: %o', url);
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to download: ${url} (${res.status})`);
}
// TODO: use a zip extractor when `ext === "zip"`
await mkdirp(dir);
await new Promise((resolve, reject) => {
res.body
.on('error', reject)
.pipe(tar.extract({ cwd: dir, strip: 1 }))
.on('error', reject)
.on('finish', resolve);
});
}
return createGo(workPath, dir, platform, arch);
}
async function parseGoVersion(modulePath: string): Promise<string> {
// default to newest (first)
let version = Array.from(versionMap.values())[0];
const file = join(modulePath, 'go.mod');
try {
const content = await readFile(file, 'utf8');
const matches = /^go (\d+)\.(\d+)\.?$/gm.exec(content) || [];
const major = parseInt(matches[1], 10);
const minor = parseInt(matches[2], 10);
const full = versionMap.get(`${major}.${minor}`);
if (major === 1 && minor >= GO_MIN_VERSION && full) {
version = full;
} else {
console.log(`Warning: Unknown Go version in ${file}`);
}
} catch (err) {
if (err.code === 'ENOENT') {
debug(`File not found: ${file}`);
} else {
throw err;
}
}
debug(`Selected Go version ${version}`);
return version;
}