[go] Support 'go.work' file and resolve shared deps relative to work path (#9708)

This PR fixes a few issues related to `vc dev`.

1. Create a default `go.work` file in the cache dir when building the `vercel-dev-server-go` executable
2. Copy the existing `go.mod` file into the cache dir and update any "replace" relative paths
3. Split the "build" logic into separate functions for the legacy "main" package build and the `go.mod` build

Additionally, it fixes:

1. `vc build`: pass in `build.env` from `vercel.json`
2. Fix several tests to work with `vc dev` and `vc build`

Linear: https://linear.app/vercel/issue/VCCLI-638/vc-dev-go-builder-cant-resolve-workspace-dependencies

The user that reported the issue has tested this build and seems to fix their use case: https://github.com/vercel/vercel/issues/9393#issuecomment-1490726785.
This commit is contained in:
Chris Barber
2023-04-13 12:59:47 -05:00
committed by GitHub
parent 732ac2072c
commit 38eb0bca04
23 changed files with 729 additions and 425 deletions

View File

@@ -0,0 +1,3 @@
module go-work-with-shared/api
go 1.20

View File

@@ -0,0 +1,12 @@
package handler
import (
"fmt"
"net/http"
"go-work-with-shared/mylib"
)
// Handler function
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, mylib.Say("hello"))
}

View File

@@ -0,0 +1,6 @@
go 1.20
use (
./api/
./mylib/
)

View File

@@ -0,0 +1,3 @@
module go-work-with-shared/mylib
go 1.20

View File

@@ -0,0 +1,9 @@
package mylib
import (
"runtime"
)
func Say(text string) string {
return text + ":" + runtime.Version()
}

View File

@@ -182,6 +182,13 @@ test(
}) })
); );
test(
'[vercel dev] Should support `*.go` API serverless functions with `go.work` and lib',
testFixtureStdio('go-work-with-shared', async (testPath: any) => {
await testPath(200, `/api`, 'hello:go1.20.2');
})
);
test( test(
'[vercel dev] Should set the `ts-node` "target" to match Node.js version', '[vercel dev] Should set the `ts-node` "target" to match Node.js version',
testFixtureStdio('node-ts-node-target', async (testPath: any) => { testFixtureStdio('node-ts-node-target', async (testPath: any) => {

View File

@@ -9,7 +9,7 @@ import {
remove, remove,
symlink, symlink,
} from 'fs-extra'; } from 'fs-extra';
import { join, delimiter, dirname } from 'path'; import { delimiter, dirname, join } from 'path';
import stringArgv from 'string-argv'; import stringArgv from 'string-argv';
import { cloneEnv, debug } from '@vercel/build-utils'; import { cloneEnv, debug } from '@vercel/build-utils';
import { pipeline } from 'stream'; import { pipeline } from 'stream';
@@ -22,7 +22,7 @@ import type { Env } from '@vercel/build-utils';
const streamPipeline = promisify(pipeline); const streamPipeline = promisify(pipeline);
const versionMap = new Map([ const versionMap = new Map([
['1.20', '1.20.1'], ['1.20', '1.20.2'],
['1.19', '1.19.6'], ['1.19', '1.19.6'],
['1.18', '1.18.10'], ['1.18', '1.18.10'],
['1.17', '1.17.13'], ['1.17', '1.17.13'],
@@ -36,17 +36,32 @@ const archMap = new Map([
['x86', '386'], ['x86', '386'],
]); ]);
const platformMap = new Map([['win32', 'windows']]); const platformMap = new Map([['win32', 'windows']]);
export const cacheDir = join('.vercel', 'cache', 'golang'); export const localCacheDir = join('.vercel', 'cache', 'golang');
const GO_FLAGS = process.platform === 'win32' ? [] : ['-ldflags', '-s -w']; const GO_FLAGS = process.platform === 'win32' ? [] : ['-ldflags', '-s -w'];
const GO_MIN_VERSION = 13; const GO_MIN_VERSION = 13;
const getPlatform = (p: string) => platformMap.get(p) || p;
const getArch = (a: string) => archMap.get(a) || a;
/**
* Determines the URL to download the Golang SDK.
* @param version The desireed Go version
* @returns The Go download URL
*/
function getGoUrl(version: string) { function getGoUrl(version: string) {
const { arch, platform } = process; const { arch, platform } = process;
const goArch = getArch(arch);
const goPlatform = getPlatform(platform);
const ext = platform === 'win32' ? 'zip' : 'tar.gz'; const ext = platform === 'win32' ? 'zip' : 'tar.gz';
const goPlatform = platformMap.get(platform) || platform;
let goArch = archMap.get(arch) || arch;
// Go 1.16 was the first version to support arm64, so if the version is older
// we need to download the amd64 version
if (
platform === 'darwin' &&
goArch === 'arm64' &&
parseInt(version.split('.')[1], 10) < 16
) {
goArch = 'amd64';
}
const filename = `go${version}.${goPlatform}-${goArch}.${ext}`; const filename = `go${version}.${goPlatform}-${goArch}.${ext}`;
return { return {
filename, filename,
@@ -61,32 +76,91 @@ export const goGlobalCachePath = join(
export const OUT_EXTENSION = process.platform === 'win32' ? '.exe' : ''; export const OUT_EXTENSION = process.platform === 'win32' ? '.exe' : '';
export async function getAnalyzedEntrypoint( interface Analyzed {
workPath: string, functionName: string;
filePath: string, packageName: string;
modulePath: string watch?: boolean;
) {
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 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 ${analyzed}`);
return analyzed;
} }
class GoWrapper { /**
* Parses the AST of the specified entrypoint Go file.
* @param workPath The work path (e.g. `/path/to/project`)
* @param entrypoint The path to the entrypoint file (e.g.
* `/path/to/project/api/index.go`)
* @param modulePath The path to the directory containing the `go.mod` (e.g.
* `/path/to/project/api`)
* @returns The results from the AST parsing
*/
export async function getAnalyzedEntrypoint({
entrypoint,
modulePath,
workPath,
}: {
entrypoint: string;
modulePath?: string;
workPath: string;
}): Promise<Analyzed> {
const bin = join(__dirname, `analyze${OUT_EXTENSION}`);
let analyzed: string;
try {
// build the `analyze` binary if not found in the `dist` directory
const isAnalyzeExist = await pathExists(bin);
if (!isAnalyzeExist) {
debug(`Building analyze bin: ${bin}`);
const src = join(__dirname, 'util', 'analyze.go');
let go;
const createOpts = {
modulePath,
opts: { cwd: __dirname },
workPath,
};
try {
go = await createGo(createOpts);
} catch (err) {
// if the version in the `go.mod` is too old, then download the latest
if (
err instanceof GoError &&
err.code === 'ERR_UNSUPPORTED_GO_VERSION'
) {
delete createOpts.modulePath;
go = await createGo(createOpts);
} else {
throw err;
}
}
await go.build(src, bin);
}
} catch (err) {
console.error('Failed to build the Go AST analyzer');
throw err;
}
try {
debug(`Analyzing entrypoint ${entrypoint} with modulePath ${modulePath}`);
const args = [`-modpath=${modulePath}`, join(workPath, entrypoint)];
analyzed = await execa.stdout(bin, args);
} catch (err) {
console.error(`Failed to parse AST for "${entrypoint}"`);
throw err;
}
debug(`Analyzed entrypoint ${analyzed}`);
if (!analyzed) {
const err = new Error(
`Could not find an exported function in "${entrypoint}"
Learn more: https://vercel.com/docs/runtimes#official-runtimes/go
`
);
console.error(err.message);
throw err;
}
return JSON.parse(analyzed) as Analyzed;
}
export class GoWrapper {
private env: Env; private env: Env;
private opts: execa.Options; private opts: execa.Options;
@@ -101,10 +175,11 @@ class GoWrapper {
private execute(...args: string[]) { private execute(...args: string[]) {
const { opts, env } = this; const { opts, env } = this;
debug( debug(
`Exec: go ${args `Exec: go ${args.map(a => (a.includes(' ') ? `"${a}"` : a)).join(' ')}`
.map(a => (a.includes(' ') ? `"${a}"` : a))
.join(' ')} CWD=${opts.cwd}`
); );
debug(` CWD=${opts.cwd}`);
debug(` GOROOT=${(env || opts.env).GOROOT}`);
debug(` GO_BUILD_FLAGS=${(env || opts.env).GO_BUILD_FLAGS}`);
return execa('go', args, { stdio: 'inherit', ...opts, env }); return execa('go', args, { stdio: 'inherit', ...opts, env });
} }
@@ -127,9 +202,8 @@ class GoWrapper {
debug(`Building optimized 'go' binary ${src} -> ${dest}`); debug(`Building optimized 'go' binary ${src} -> ${dest}`);
const sources = Array.isArray(src) ? src : [src]; const sources = Array.isArray(src) ? src : [src];
const flags = process.env.GO_BUILD_FLAGS const envGoBuildFlags = (this.env || this.opts.env).GO_BUILD_FLAGS;
? stringArgv(process.env.GO_BUILD_FLAGS) const flags = envGoBuildFlags ? stringArgv(envGoBuildFlags) : GO_FLAGS;
: GO_FLAGS;
return this.execute('build', ...flags, '-o', dest, ...sources); return this.execute('build', ...flags, '-o', dest, ...sources);
} }
@@ -186,7 +260,7 @@ export async function createGo({
goGlobalCachePath, goGlobalCachePath,
`${goSelectedVersion}_${platform}_${process.arch}` `${goSelectedVersion}_${platform}_${process.arch}`
); );
const goCacheDir = join(workPath, cacheDir); const goCacheDir = join(workPath, localCacheDir);
if (goPreferredVersion) { if (goPreferredVersion) {
debug(`Preferred go version ${goPreferredVersion} (from go.mod)`); debug(`Preferred go version ${goPreferredVersion} (from go.mod)`);
@@ -234,7 +308,7 @@ export async function createGo({
debug(`Found go ${version} in ${label}, but version is unsupported`); debug(`Found go ${version} in ${label}, but version is unsupported`);
} }
if (version === goSelectedVersion || short === goSelectedVersion) { if (version === goSelectedVersion || short === goSelectedVersion) {
console.log(`Selected go ${version} (from ${label})`); debug(`Selected go ${version} (from ${label})`);
await setGoEnv(goDir); await setGoEnv(goDir);
return new GoWrapper(env, opts); return new GoWrapper(env, opts);
@@ -339,6 +413,10 @@ function parseGoVersionString(goVersionOutput: string) {
}; };
} }
class GoError extends Error {
code: string | undefined;
}
/** /**
* Attempts to parse the preferred Go version from the `go.mod` file. * Attempts to parse the preferred Go version from the `go.mod` file.
* *
@@ -359,6 +437,12 @@ async function parseGoModVersion(
const full = versionMap.get(`${major}.${minor}`); const full = versionMap.get(`${major}.${minor}`);
if (major === 1 && minor >= GO_MIN_VERSION && full) { if (major === 1 && minor >= GO_MIN_VERSION && full) {
version = full; version = full;
} else if (!isNaN(minor)) {
const err = new GoError(
`Unsupported Go version ${major}.${minor} in ${file}`
);
err.code = 'ERR_UNSUPPORTED_GO_VERSION';
throw err;
} else { } else {
console.log(`Warning: Unknown Go version in ${file}`); console.log(`Warning: Unknown Go version in ${file}`);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,10 @@
{ "src": "subdirectory/index.go", "use": "@vercel/go" } { "src": "subdirectory/index.go", "use": "@vercel/go" }
], ],
"probes": [ "probes": [
{ "path": "/", "mustContain": "cow:go1.20.1:RANDOMNESS_PLACEHOLDER" }, { "path": "/", "mustContain": "cow:go1.20.2:RANDOMNESS_PLACEHOLDER" },
{ {
"path": "/subdirectory", "path": "/subdirectory",
"mustContain": "subcow:go1.20.1:RANDOMNESS_PLACEHOLDER" "mustContain": "subcow:go1.20.2:RANDOMNESS_PLACEHOLDER"
} }
] ]
} }

View File

@@ -1,3 +1,3 @@
module go-example module go-example
go 1.12 go 1.20

View File

@@ -1,3 +1,3 @@
module sub-1 module sub-1
go 1.12 go 1.20

View File

@@ -1,3 +1,3 @@
module sub-2 module sub-2
go 1.12 go 1.20

View File

@@ -1,3 +1,3 @@
module other-folder module other-folder
go 1.12 go 1.20

View File

@@ -0,0 +1,8 @@
{
"probes": [
{
"path": "/",
"mustContain": "version:go1.14.15:first:RANDOMNESS_PLACEHOLDER"
}
]
}

View File

@@ -1,11 +1,5 @@
{ {
"version": 2, "version": 2,
"builds": [{ "src": "index.go", "use": "@vercel/go" }], "builds": [{ "src": "index.go", "use": "@vercel/go" }],
"build": { "env": { "GO_BUILD_FLAGS": "-tags first -ldflags '-s -w'" } }, "build": { "env": { "GO_BUILD_FLAGS": "-tags first -ldflags '-s -w'" } }
"probes": [
{
"path": "/",
"mustContain": "version:go1.14.15:first:RANDOMNESS_PLACEHOLDER"
}
]
} }

View File

@@ -2,4 +2,10 @@ module github.com/vercel/does-not-exist
go 1.13 go 1.13
require github.com/dhruvbird/go-cowsay v0.0.0-20131019225157-6fd7bd0281c0 // indirect require (
github.com/dhruvbird/go-cowsay v0.0.0-20131019225157-6fd7bd0281c0 // indirect
github.com/vercel/does-not-exist/api v0.0.0-unpublished
)
replace github.com/vercel/does-not-exist/api => ./

View File

@@ -1,5 +1,4 @@
{ {
"version": 2,
"probes": [ "probes": [
{ {
"path": "/api/v1/routes/someroute", "path": "/api/v1/routes/someroute",

View File

@@ -0,0 +1,3 @@
module go-work-with-shared/api
go 1.20

View File

@@ -0,0 +1,12 @@
package handler
import (
"fmt"
"net/http"
"go-work-with-shared/mylib"
)
// Handler function
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, mylib.Say("hello"))
}

View File

@@ -0,0 +1,6 @@
go 1.20
use (
./api
./mylib
)

View File

@@ -0,0 +1,3 @@
module go-work-with-shared/mylib
go 1.20

View File

@@ -0,0 +1,9 @@
package mylib
import (
"runtime"
)
func Say(text string) string {
return text + ":" + runtime.Version()
}

View File

@@ -0,0 +1,8 @@
{
"probes": [
{
"path": "/api/index.go",
"mustContain": "hello:go1.20.2"
}
]
}