mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 21:07:46 +00:00
[go] Go builder improvements (#9576)
This PR fixes a handful of Go builder issues all related to the selected Golang version being used to build the function: - `go.mod` version ignored for `vc build` and `vc dev`, uses system `PATH` version only - `vc dev` fails if `go.mod` does not exist - If the analyze bin doesn’t exist, downloads golang into `.vercel/cache/golang` instead of a global shared dir - When running `vc dev`, doesn’t reuse go build code/common settings - go tidy fails when `go.mod` set to 1.19 or 1.20, but 1.18 or older is installed - `vc build` builds wrong arch on Apple Silicon/arm64 - `vc build` on Windows doesn't properly resolve "builds" in `vercel.json` due to posix separator issue - `vc build` on Windows fails with `package <pkg/name> is not in GOROOT` due to posix separator issue - Removed `actions/setup-go` from all test workflows I added a test that tests the `go tidy` issue.
This commit is contained in:
3
.github/workflows/test-integration-cli.yml
vendored
3
.github/workflows/test-integration-cli.yml
vendored
@@ -31,9 +31,6 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -29,9 +29,6 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.13.15'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
@@ -66,9 +63,6 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.13.15'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import dotenv from 'dotenv';
|
||||
import { join, normalize, relative, resolve } from 'path';
|
||||
import { join, normalize, relative, resolve, sep } from 'path';
|
||||
import {
|
||||
getDiscontinuedNodeVersions,
|
||||
normalizePath,
|
||||
@@ -712,7 +712,9 @@ function expandBuild(files: string[], build: Builder): Builder[] {
|
||||
});
|
||||
}
|
||||
|
||||
let src = normalize(build.src || '**');
|
||||
let src = normalize(build.src || '**')
|
||||
.split(sep)
|
||||
.join('/');
|
||||
if (src === '.' || src === './') {
|
||||
throw new NowBuildError({
|
||||
code: `invalid_build_specification`,
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import tar from 'tar';
|
||||
import execa from 'execa';
|
||||
import fetch from 'node-fetch';
|
||||
import { mkdirp, pathExists, readFile } from 'fs-extra';
|
||||
import { join, delimiter } from 'path';
|
||||
import {
|
||||
createWriteStream,
|
||||
mkdirp,
|
||||
pathExists,
|
||||
readFile,
|
||||
remove,
|
||||
symlink,
|
||||
} from 'fs-extra';
|
||||
import { join, delimiter, dirname } from 'path';
|
||||
import stringArgv from 'string-argv';
|
||||
import { debug } from '@vercel/build-utils';
|
||||
import { cloneEnv, debug } from '@vercel/build-utils';
|
||||
import { pipeline } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
import { tmpdir } from 'os';
|
||||
import yauzl from 'yauzl-promise';
|
||||
import XDGAppPaths from 'xdg-app-paths';
|
||||
import type { Env } from '@vercel/build-utils';
|
||||
|
||||
const streamPipeline = promisify(pipeline);
|
||||
|
||||
const versionMap = new Map([
|
||||
['1.19', '1.19.5'],
|
||||
['1.18', '1.18.1'],
|
||||
['1.17', '1.17.3'],
|
||||
['1.16', '1.16.10'],
|
||||
['1.15', '1.15.8'],
|
||||
['1.19', '1.19.6'],
|
||||
['1.18', '1.18.10'],
|
||||
['1.17', '1.17.13'],
|
||||
['1.16', '1.16.15'],
|
||||
['1.15', '1.15.15'],
|
||||
['1.14', '1.14.15'],
|
||||
['1.13', '1.13.15'],
|
||||
]);
|
||||
@@ -20,17 +36,27 @@ const archMap = new Map([
|
||||
]);
|
||||
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) => {
|
||||
|
||||
function getGoUrl(version: string) {
|
||||
const { arch, platform } = process;
|
||||
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}`;
|
||||
};
|
||||
const filename = `go${version}.${goPlatform}-${goArch}.${ext}`;
|
||||
return {
|
||||
filename,
|
||||
url: `https://dl.google.com/go/${filename}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const goGlobalCachePath = join(
|
||||
XDGAppPaths('com.vercel.cli').cache(),
|
||||
'golang'
|
||||
);
|
||||
|
||||
export const OUT_EXTENSION = process.platform === 'win32' ? '.exe' : '';
|
||||
|
||||
@@ -39,39 +65,31 @@ export async function getAnalyzedEntrypoint(
|
||||
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) {
|
||||
debug(`Building analyze bin: ${bin}`);
|
||||
const src = join(__dirname, 'util', 'analyze.go');
|
||||
const go = await downloadGo(workPath, modulePath);
|
||||
const go = await createGo({
|
||||
modulePath,
|
||||
workPath,
|
||||
});
|
||||
await go.build(src, bin);
|
||||
}
|
||||
|
||||
debug(`Analyzing entrypoint ${filePath} with modulePath ${modulePath}`);
|
||||
const args = [`-modpath=${modulePath}`, filePath];
|
||||
|
||||
const analyzed = await execa.stdout(bin, args);
|
||||
debug('Analyzed entrypoint %o', analyzed);
|
||||
debug(`Analyzed entrypoint ${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 env: Env;
|
||||
private opts: execa.Options;
|
||||
|
||||
constructor(env: { [key: string]: string }, opts: execa.Options = {}) {
|
||||
constructor(env: Env, opts: execa.Options = {}) {
|
||||
if (!opts.cwd) {
|
||||
opts.cwd = process.cwd();
|
||||
}
|
||||
@@ -81,8 +99,12 @@ class GoWrapper {
|
||||
|
||||
private execute(...args: string[]) {
|
||||
const { opts, env } = this;
|
||||
debug('Exec %o', `go ${args.join(' ')}`);
|
||||
return execa('go', args, { stdio: 'pipe', ...opts, env });
|
||||
debug(
|
||||
`Exec: go ${args
|
||||
.map(a => (a.includes(' ') ? `"${a}"` : a))
|
||||
.join(' ')} CWD=${opts.cwd}`
|
||||
);
|
||||
return execa('go', args, { stdio: 'inherit', ...opts, env });
|
||||
}
|
||||
|
||||
mod() {
|
||||
@@ -92,16 +114,16 @@ class GoWrapper {
|
||||
get(src?: string) {
|
||||
const args = ['get'];
|
||||
if (src) {
|
||||
debug('Fetching `go` dependencies for file %o', src);
|
||||
debug(`Fetching 'go' dependencies for file ${src}`);
|
||||
args.push(src);
|
||||
} else {
|
||||
debug('Fetching `go` dependencies for cwd %o', this.opts.cwd);
|
||||
debug(`Fetching 'go' dependencies for cwd ${this.opts.cwd}`);
|
||||
}
|
||||
return this.execute(...args);
|
||||
}
|
||||
|
||||
build(src: string | string[], dest: string) {
|
||||
debug('Building optimized `go` binary %o -> %o', src, dest);
|
||||
debug(`Building optimized 'go' binary ${src} -> ${dest}`);
|
||||
const sources = Array.isArray(src) ? src : [src];
|
||||
|
||||
const flags = process.env.GO_BUILD_FLAGS
|
||||
@@ -112,72 +134,222 @@ class GoWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
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}${delimiter}${process.env.PATH}`;
|
||||
const env: { [key: string]: string } = {
|
||||
...process.env,
|
||||
PATH: path,
|
||||
GOPATH: goPath,
|
||||
...opts.env,
|
||||
};
|
||||
if (goMod) {
|
||||
env.GO111MODULE = 'on';
|
||||
type CreateGoOptions = {
|
||||
modulePath?: string;
|
||||
opts?: execa.Options;
|
||||
workPath: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a `GoWrapper` instance.
|
||||
*
|
||||
* This function determines the Go version to use by first looking in the
|
||||
* `go.mod`, if exists, otherwise uses the latest version from the version
|
||||
* map.
|
||||
*
|
||||
* Next it will attempt to find the desired Go version by checking the
|
||||
* following locations:
|
||||
* 1. The "local" project cache directory (e.g. `.vercel/cache/golang`)
|
||||
* 2. The "global" cache directory (e.g. `~/.cache/com.vercel.com/golang`)
|
||||
* 3. The system PATH
|
||||
*
|
||||
* If the Go version is not found, it's downloaded and installed in the
|
||||
* global cache directory so it can be shared across projects. When using
|
||||
* Linux or macOS, it creates a symlink from the global cache to the local
|
||||
* cache directory so that `prepareCache` will persist it.
|
||||
*
|
||||
* @param modulePath The path possibly containing a `go.mod` file
|
||||
* @param opts `execa` options (`cwd`, `env`, `stdio`, etc)
|
||||
* @param workPath The path to the project to be built
|
||||
* @returns An initialized `GoWrapper` instance
|
||||
*/
|
||||
export async function createGo({
|
||||
modulePath,
|
||||
opts = {},
|
||||
workPath,
|
||||
}: CreateGoOptions): Promise<GoWrapper> {
|
||||
// parse the `go.mod`, if exists
|
||||
let goPreferredVersion: string | undefined;
|
||||
if (modulePath) {
|
||||
goPreferredVersion = await parseGoModVersion(modulePath);
|
||||
}
|
||||
await createGoPathTree(goPath, platform, arch);
|
||||
|
||||
// default to newest (first) supported go version
|
||||
const goSelectedVersion =
|
||||
goPreferredVersion || Array.from(versionMap.values())[0];
|
||||
|
||||
const env = cloneEnv(process.env, opts.env);
|
||||
const { PATH } = env;
|
||||
const { platform } = process;
|
||||
const goGlobalCacheDir = join(
|
||||
goGlobalCachePath,
|
||||
`${goSelectedVersion}_${platform}_${process.arch}`
|
||||
);
|
||||
const goCacheDir = join(workPath, cacheDir);
|
||||
|
||||
if (goPreferredVersion) {
|
||||
debug(`Preferred go version ${goPreferredVersion} (from go.mod)`);
|
||||
env.GO111MODULE = 'on';
|
||||
} else {
|
||||
debug(
|
||||
`Preferred go version ${goSelectedVersion} (latest from version map)`
|
||||
);
|
||||
}
|
||||
|
||||
const setGoEnv = async (goDir: string | null) => {
|
||||
if (platform !== 'win32' && goDir === goGlobalCacheDir) {
|
||||
debug(`Symlinking ${goDir} -> ${goCacheDir}`);
|
||||
await remove(goCacheDir);
|
||||
await mkdirp(dirname(goCacheDir));
|
||||
await symlink(goDir, goCacheDir);
|
||||
goDir = goCacheDir;
|
||||
}
|
||||
env.GOROOT = goDir || undefined;
|
||||
env.PATH = goDir ? `${join(goDir, 'bin')}${delimiter}${PATH}` : PATH;
|
||||
};
|
||||
|
||||
// try each of these Go directories looking for the version we need
|
||||
const goDirs = {
|
||||
'local cache': goCacheDir,
|
||||
'global cache': goGlobalCacheDir,
|
||||
'system PATH': null,
|
||||
};
|
||||
|
||||
for (const [label, goDir] of Object.entries(goDirs)) {
|
||||
try {
|
||||
const goBinDir = goDir && join(goDir, 'bin');
|
||||
if (goBinDir && !(await pathExists(goBinDir))) {
|
||||
debug(`Go not found in ${label}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
env.GOROOT = goDir || undefined;
|
||||
env.PATH = goBinDir || PATH;
|
||||
|
||||
const { stdout } = await execa('go', ['version'], { env });
|
||||
const { minor, short, version } = parseGoVersionString(stdout);
|
||||
|
||||
if (minor < GO_MIN_VERSION) {
|
||||
debug(`Found go ${version} in ${label}, but version is unsupported`);
|
||||
}
|
||||
if (version === goSelectedVersion || short === goSelectedVersion) {
|
||||
console.log(`Selected go ${version} (from ${label})`);
|
||||
|
||||
await setGoEnv(goDir);
|
||||
return new GoWrapper(env, opts);
|
||||
} else {
|
||||
debug(`Found go ${version} in ${label}, but need ${goSelectedVersion}`);
|
||||
}
|
||||
} catch {
|
||||
debug(`Go not found in ${label}`);
|
||||
}
|
||||
}
|
||||
|
||||
// we need to download and cache the desired `go` version
|
||||
await download({
|
||||
dest: goGlobalCacheDir,
|
||||
version: goSelectedVersion,
|
||||
});
|
||||
|
||||
await setGoEnv(goGlobalCacheDir);
|
||||
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);
|
||||
/**
|
||||
* Download and installs the Go distribution.
|
||||
*
|
||||
* @param dest The directory to install Go into. If directory exists, it is
|
||||
* first deleted before installing.
|
||||
* @param version The Go version to download
|
||||
*/
|
||||
async function download({ dest, version }: { dest: string; version: string }) {
|
||||
const { filename, url } = getGoUrl(version);
|
||||
console.log(`Downloading go: ${url}`);
|
||||
const res = await fetch(url);
|
||||
|
||||
// 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);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to download: ${url} (${res.status})`);
|
||||
}
|
||||
|
||||
// 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);
|
||||
debug(`Installing go ${version} to ${dest}`);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to download: ${url} (${res.status})`);
|
||||
await remove(dest);
|
||||
await mkdirp(dest);
|
||||
|
||||
if (/\.zip$/.test(filename)) {
|
||||
const zipFile = join(tmpdir(), filename);
|
||||
try {
|
||||
await streamPipeline(res.body, createWriteStream(zipFile));
|
||||
const zip = await yauzl.open(zipFile);
|
||||
let entry = await zip.readEntry();
|
||||
while (entry) {
|
||||
const fileName = entry.fileName.split('/').slice(1).join('/');
|
||||
|
||||
if (fileName) {
|
||||
const destPath = join(dest, fileName);
|
||||
|
||||
if (/\/$/.test(fileName)) {
|
||||
await mkdirp(destPath);
|
||||
} else {
|
||||
const [entryStream] = await Promise.all([
|
||||
entry.openReadStream(),
|
||||
mkdirp(dirname(destPath)),
|
||||
]);
|
||||
const out = createWriteStream(destPath);
|
||||
await streamPipeline(entryStream, out);
|
||||
}
|
||||
}
|
||||
|
||||
entry = await zip.readEntry();
|
||||
}
|
||||
} finally {
|
||||
await remove(zipFile);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
return createGo(workPath, dir, platform, arch);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
res.body
|
||||
.on('error', reject)
|
||||
.pipe(tar.extract({ cwd: dest, strip: 1 }))
|
||||
.on('error', reject)
|
||||
.on('finish', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
async function parseGoVersion(modulePath: string): Promise<string> {
|
||||
// default to newest (first)
|
||||
let version = Array.from(versionMap.values())[0];
|
||||
const goVersionRegExp = /(\d+)\.(\d+)(?:\.(\d+))?/;
|
||||
|
||||
/**
|
||||
* Parses the raw output from `go version` and returns the version parts.
|
||||
*
|
||||
* @param goVersionOutput The output from `go version`
|
||||
*/
|
||||
function parseGoVersionString(goVersionOutput: string) {
|
||||
const matches = goVersionOutput.match(goVersionRegExp) || [];
|
||||
const major = parseInt(matches[1], 10);
|
||||
const minor = parseInt(matches[2], 10);
|
||||
const patch = parseInt(matches[3] || '0', 10);
|
||||
return {
|
||||
version: `${major}.${minor}.${patch}`,
|
||||
short: `${major}.${minor}`,
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse the preferred Go version from the `go.mod` file.
|
||||
*
|
||||
* @param modulePath The directory containing the `go.mod` file
|
||||
* @returns
|
||||
*/
|
||||
async function parseGoModVersion(
|
||||
modulePath: string
|
||||
): Promise<string | undefined> {
|
||||
let version;
|
||||
const file = join(modulePath, 'go.mod');
|
||||
|
||||
try {
|
||||
const content = await readFile(file, 'utf8');
|
||||
const matches = /^go (\d+)\.(\d+)\.?$/gm.exec(content) || [];
|
||||
@@ -189,7 +361,7 @@ async function parseGoVersion(modulePath: string): Promise<string> {
|
||||
} else {
|
||||
console.log(`Warning: Unknown Go version in ${file}`);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code === 'ENOENT') {
|
||||
debug(`File not found: ${file}`);
|
||||
} else {
|
||||
@@ -197,6 +369,5 @@ async function parseGoVersion(modulePath: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
debug(`Selected Go version ${version}`);
|
||||
return version;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import execa from 'execa';
|
||||
import retry from 'async-retry';
|
||||
import { homedir, tmpdir } from 'os';
|
||||
import { execFileSync, spawn } from 'child_process';
|
||||
import { spawn } from 'child_process';
|
||||
import { Readable } from 'stream';
|
||||
import once from '@tootallnate/once';
|
||||
import { join, dirname, basename, normalize, sep } from 'path';
|
||||
import { join, dirname, basename, normalize, posix, sep } from 'path';
|
||||
import {
|
||||
readFile,
|
||||
writeFile,
|
||||
lstat,
|
||||
pathExists,
|
||||
mkdirp,
|
||||
move,
|
||||
readlink,
|
||||
remove,
|
||||
rmdir,
|
||||
readdir,
|
||||
unlink,
|
||||
} from 'fs-extra';
|
||||
import {
|
||||
BuildOptions,
|
||||
@@ -33,19 +36,20 @@ import {
|
||||
const TMP = tmpdir();
|
||||
|
||||
import {
|
||||
cacheDir,
|
||||
createGo,
|
||||
getAnalyzedEntrypoint,
|
||||
cacheDir,
|
||||
OUT_EXTENSION,
|
||||
} from './go-helpers';
|
||||
|
||||
const handlerFileName = `handler${OUT_EXTENSION}`;
|
||||
|
||||
export { shouldServe };
|
||||
|
||||
interface Analyzed {
|
||||
found?: boolean;
|
||||
packageName: string;
|
||||
functionName: string;
|
||||
packageName: string;
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
interface PortInfo {
|
||||
@@ -147,7 +151,7 @@ export async function build({
|
||||
}
|
||||
|
||||
const entrypointAbsolute = join(workPath, entrypoint);
|
||||
const entrypointArr = entrypoint.split(sep);
|
||||
const entrypointArr = entrypoint.split(posix.sep);
|
||||
|
||||
debug(`Parsing AST for "${entrypoint}"`);
|
||||
let analyzed: string;
|
||||
@@ -163,7 +167,7 @@ export async function build({
|
||||
dirname(goModAbsPath)
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(`Failed to parse AST for "${entrypoint}"`);
|
||||
console.error(`Failed to parse AST for "${entrypoint}"`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -173,7 +177,7 @@ export async function build({
|
||||
Learn more: https://vercel.com/docs/runtimes#official-runtimes/go
|
||||
`
|
||||
);
|
||||
console.log(err.message);
|
||||
console.error(err.message);
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -289,18 +293,19 @@ export async function build({
|
||||
// we need our `main.go` to be called something else
|
||||
const mainGoFileName = 'main__vc__go__.go';
|
||||
|
||||
if (packageName !== 'main') {
|
||||
const go = await createGo(
|
||||
workPath,
|
||||
goPath,
|
||||
process.platform,
|
||||
process.arch,
|
||||
{
|
||||
cwd: entrypointDirname,
|
||||
stdio: 'inherit',
|
||||
const go = await createGo({
|
||||
modulePath: goModPath,
|
||||
opts: {
|
||||
cwd: entrypointDirname,
|
||||
env: {
|
||||
GOARCH: 'amd64',
|
||||
GOOS: 'linux',
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
workPath,
|
||||
});
|
||||
|
||||
if (packageName !== 'main') {
|
||||
if (!isGoModExist) {
|
||||
try {
|
||||
const defaultGoModContent = `module ${packageName}`;
|
||||
@@ -321,7 +326,7 @@ export async function build({
|
||||
from: join(entrypointDirname, 'go.sum'),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(`Failed to create default go.mod for ${packageName}`);
|
||||
console.error(`Failed to create default go.mod for ${packageName}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -353,6 +358,7 @@ export async function build({
|
||||
if (isGoModExist && isGoModInRootDir) {
|
||||
debug('[mod-root] Write main file to ' + downloadPath);
|
||||
await writeFile(join(downloadPath, mainGoFileName), mainModGoContents);
|
||||
|
||||
undoFileActions.push({
|
||||
to: undefined, // delete
|
||||
from: join(downloadPath, mainGoFileName),
|
||||
@@ -403,7 +409,7 @@ export async function build({
|
||||
undoDirectoryCreation.push(dirname(finalDestination));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Failed to move entry to package folder');
|
||||
console.error('Failed to move entry to package folder');
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -421,7 +427,7 @@ export async function build({
|
||||
// ensure go.mod up-to-date
|
||||
await go.mod();
|
||||
} catch (err) {
|
||||
console.log('failed to `go mod tidy`');
|
||||
console.error('failed to `go mod tidy`');
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -433,23 +439,13 @@ export async function build({
|
||||
|
||||
await go.build(src, destPath);
|
||||
} catch (err) {
|
||||
console.log('failed to `go build`');
|
||||
console.error('failed to `go build`');
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
// legacy mode
|
||||
// we need `main.go` in the same dir as the entrypoint,
|
||||
// otherwise `go build` will refuse to build
|
||||
const go = await createGo(
|
||||
workPath,
|
||||
goPath,
|
||||
process.platform,
|
||||
process.arch,
|
||||
{
|
||||
cwd: entrypointDirname,
|
||||
},
|
||||
false
|
||||
);
|
||||
const originalMainGoContents = await readFile(
|
||||
join(__dirname, 'main.go'),
|
||||
'utf8'
|
||||
@@ -472,7 +468,7 @@ export async function build({
|
||||
try {
|
||||
await go.get();
|
||||
} catch (err) {
|
||||
console.log('Failed to `go get`');
|
||||
console.error('Failed to `go get`');
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -485,7 +481,7 @@ export async function build({
|
||||
].map(file => normalize(file));
|
||||
await go.build(src, destPath);
|
||||
} catch (err) {
|
||||
console.log('failed to `go build`');
|
||||
console.error('failed to `go build`');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -513,7 +509,9 @@ export async function build({
|
||||
undoFunctionRenames
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`Build cleanup failed: ${error.message}`);
|
||||
if (error instanceof Error) {
|
||||
console.error(`Build cleanup failed: ${error.message}`);
|
||||
}
|
||||
debug('Cleanup Error: ' + error);
|
||||
}
|
||||
}
|
||||
@@ -640,6 +638,19 @@ async function copyDevServer(
|
||||
await writeFile(join(dest, 'vercel-dev-server-main.go'), patched);
|
||||
}
|
||||
|
||||
async function writeDefaultGoMod(
|
||||
entrypointDirname: string,
|
||||
packageName: string
|
||||
) {
|
||||
const defaultGoModContent = `module ${packageName}`;
|
||||
|
||||
await writeFile(
|
||||
join(entrypointDirname, 'go.mod'),
|
||||
defaultGoModContent,
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
export async function startDevServer(
|
||||
opts: StartDevServerOptions
|
||||
): Promise<StartDevServerResult> {
|
||||
@@ -678,6 +689,7 @@ Learn more: https://vercel.com/docs/runtimes#official-runtimes/go`
|
||||
await Promise.all([
|
||||
copyEntrypoint(entrypointWithExt, tmpPackage),
|
||||
copyDevServer(analyzed.functionName, tmpPackage),
|
||||
goModAbsPathDir ? null : writeDefaultGoMod(tmp, analyzed.packageName),
|
||||
]);
|
||||
|
||||
const portFile = join(
|
||||
@@ -693,13 +705,22 @@ Learn more: https://vercel.com/docs/runtimes#official-runtimes/go`
|
||||
process.platform === 'win32' ? '.exe' : ''
|
||||
}`;
|
||||
|
||||
debug(`SPAWNING go build -o ${executable} ./... CWD=${tmp}`);
|
||||
execFileSync('go', ['build', '-o', executable, './...'], {
|
||||
cwd: tmp,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
// Note: We must run `go build`, then manually spawn the dev server instead
|
||||
// of spawning `go run`. See https://github.com/vercel/vercel/pull/8718 for
|
||||
// more info.
|
||||
|
||||
// build the dev server
|
||||
const go = await createGo({
|
||||
modulePath: goModAbsPathDir,
|
||||
opts: {
|
||||
cwd: tmp,
|
||||
env,
|
||||
},
|
||||
workPath,
|
||||
});
|
||||
await go.build('./...', executable);
|
||||
|
||||
// run the dev server
|
||||
debug(`SPAWNING ${executable} CWD=${tmp}`);
|
||||
const child = spawn(executable, [], {
|
||||
cwd: tmp,
|
||||
@@ -770,10 +791,10 @@ async function waitForPortFile_(opts: {
|
||||
try {
|
||||
const port = Number(await readFile(opts.portFile, 'ascii'));
|
||||
retry(() => remove(opts.portFile)).catch((err: Error) => {
|
||||
console.error('Could not delete port file: %j: %s', opts.portFile, err);
|
||||
console.error(`Could not delete port file: ${opts.portFile}: ${err}`);
|
||||
});
|
||||
return { port };
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
@@ -784,6 +805,25 @@ async function waitForPortFile_(opts: {
|
||||
export async function prepareCache({
|
||||
workPath,
|
||||
}: PrepareCacheOptions): Promise<Files> {
|
||||
// When building the project for the first time, there won't be a cache and
|
||||
// `createGo()` will have downloaded Go to the global cache directory, then
|
||||
// symlinked it to the local `cacheDir`.
|
||||
//
|
||||
// If we detect the `cacheDir` is a symlink, unlink it, then move the global
|
||||
// cache directory into the local cache directory so that it can be
|
||||
// persisted.
|
||||
//
|
||||
// On the next build, the local cache will be restored and `createGo()` will
|
||||
// use it unless the preferred Go version changed in the `go.mod`.
|
||||
const goCacheDir = join(workPath, cacheDir);
|
||||
const stat = await lstat(goCacheDir);
|
||||
if (stat.isSymbolicLink()) {
|
||||
const goGlobalCacheDir = await readlink(goCacheDir);
|
||||
debug(`Preparing cache by moving ${goGlobalCacheDir} -> ${goCacheDir}`);
|
||||
await unlink(goCacheDir);
|
||||
await move(goGlobalCacheDir, goCacheDir);
|
||||
}
|
||||
|
||||
const cache = await glob(`${cacheDir}/**`, workPath);
|
||||
return cache;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@types/node": "14.18.33",
|
||||
"@types/node-fetch": "^2.3.0",
|
||||
"@types/tar": "^4.0.0",
|
||||
"@types/yauzl-promise": "2.1.0",
|
||||
"@vercel/build-utils": "6.3.4",
|
||||
"@vercel/ncc": "0.24.0",
|
||||
"async-retry": "1.3.1",
|
||||
@@ -44,6 +45,8 @@
|
||||
"node-fetch": "^2.2.1",
|
||||
"string-argv": "0.3.1",
|
||||
"tar": "4.4.6",
|
||||
"typescript": "4.3.4"
|
||||
"typescript": "4.3.4",
|
||||
"xdg-app-paths": "5.1.0",
|
||||
"yauzl-promise": "2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
{ "src": "subdirectory/index.go", "use": "@vercel/go" }
|
||||
],
|
||||
"probes": [
|
||||
{ "path": "/", "mustContain": "cow:go1.19.5:RANDOMNESS_PLACEHOLDER" },
|
||||
{ "path": "/", "mustContain": "cow:go1.19.6:RANDOMNESS_PLACEHOLDER" },
|
||||
{
|
||||
"path": "/subdirectory",
|
||||
"mustContain": "subcow:go1.19.5:RANDOMNESS_PLACEHOLDER"
|
||||
"mustContain": "subcow:go1.19.6:RANDOMNESS_PLACEHOLDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
3
packages/go/test/fixtures/10-go-mod-newer/go.mod
vendored
Normal file
3
packages/go/test/fixtures/10-go-mod-newer/go.mod
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module go-mod
|
||||
|
||||
go 1.19
|
||||
12
packages/go/test/fixtures/10-go-mod-newer/index.go
vendored
Normal file
12
packages/go/test/fixtures/10-go-mod-newer/index.go
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Handler func
|
||||
func Handler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "version:%s:RANDOMNESS_PLACEHOLDER", runtime.Version())
|
||||
}
|
||||
7
packages/go/test/fixtures/10-go-mod-newer/vercel.json
vendored
Normal file
7
packages/go/test/fixtures/10-go-mod-newer/vercel.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "index.go", "use": "@vercel/go" }],
|
||||
"probes": [
|
||||
{ "path": "/", "mustContain": "version:go1.19.6:RANDOMNESS_PLACEHOLDER" }
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
"version": 2,
|
||||
"builds": [{ "src": "index.go", "use": "@vercel/go" }],
|
||||
"probes": [
|
||||
{ "path": "/", "mustContain": "version:go1.15.8:RANDOMNESS_PLACEHOLDER" }
|
||||
{ "path": "/", "mustContain": "version:go1.15.15:RANDOMNESS_PLACEHOLDER" }
|
||||
]
|
||||
}
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -661,6 +661,7 @@ importers:
|
||||
'@types/node': 14.18.33
|
||||
'@types/node-fetch': ^2.3.0
|
||||
'@types/tar': ^4.0.0
|
||||
'@types/yauzl-promise': 2.1.0
|
||||
'@vercel/build-utils': 6.3.4
|
||||
'@vercel/ncc': 0.24.0
|
||||
async-retry: 1.3.1
|
||||
@@ -670,6 +671,8 @@ importers:
|
||||
string-argv: 0.3.1
|
||||
tar: 4.4.6
|
||||
typescript: 4.3.4
|
||||
xdg-app-paths: 5.1.0
|
||||
yauzl-promise: 2.1.3
|
||||
devDependencies:
|
||||
'@tootallnate/once': 1.1.2
|
||||
'@types/async-retry': 1.4.2
|
||||
@@ -679,6 +682,7 @@ importers:
|
||||
'@types/node': 14.18.33
|
||||
'@types/node-fetch': 2.6.2
|
||||
'@types/tar': 4.0.5
|
||||
'@types/yauzl-promise': 2.1.0
|
||||
'@vercel/build-utils': link:../build-utils
|
||||
'@vercel/ncc': 0.24.0
|
||||
async-retry: 1.3.1
|
||||
@@ -688,6 +692,8 @@ importers:
|
||||
string-argv: 0.3.1
|
||||
tar: 4.4.6
|
||||
typescript: 4.3.4
|
||||
xdg-app-paths: 5.1.0
|
||||
yauzl-promise: 2.1.3
|
||||
|
||||
packages/hydrogen:
|
||||
specifiers:
|
||||
|
||||
Reference in New Issue
Block a user