mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 04:22:12 +00:00
[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:
@@ -0,0 +1,3 @@
|
|||||||
|
module go-work-with-shared/api
|
||||||
|
|
||||||
|
go 1.20
|
||||||
@@ -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"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
go 1.20
|
||||||
|
|
||||||
|
use (
|
||||||
|
./api/
|
||||||
|
./mylib/
|
||||||
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module go-work-with-shared/mylib
|
||||||
|
|
||||||
|
go 1.20
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package mylib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Say(text string) string {
|
||||||
|
return text + ":" + runtime.Version()
|
||||||
|
}
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module go-example
|
module go-example
|
||||||
|
|
||||||
go 1.12
|
go 1.20
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module sub-1
|
module sub-1
|
||||||
|
|
||||||
go 1.12
|
go 1.20
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module sub-2
|
module sub-2
|
||||||
|
|
||||||
go 1.12
|
go 1.20
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module other-folder
|
module other-folder
|
||||||
|
|
||||||
go 1.12
|
go 1.20
|
||||||
|
|||||||
8
packages/go/test/fixtures/16-custom-flag/probes.json
vendored
Normal file
8
packages/go/test/fixtures/16-custom-flag/probes.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"probes": [
|
||||||
|
{
|
||||||
|
"path": "/",
|
||||||
|
"mustContain": "version:go1.14.15:first:RANDOMNESS_PLACEHOLDER"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 => ./
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
|
||||||
"probes": [
|
"probes": [
|
||||||
{
|
{
|
||||||
"path": "/api/v1/routes/someroute",
|
"path": "/api/v1/routes/someroute",
|
||||||
3
packages/go/test/fixtures/26-go-work-with-shared/api/go.mod
vendored
Normal file
3
packages/go/test/fixtures/26-go-work-with-shared/api/go.mod
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module go-work-with-shared/api
|
||||||
|
|
||||||
|
go 1.20
|
||||||
12
packages/go/test/fixtures/26-go-work-with-shared/api/index.go
vendored
Normal file
12
packages/go/test/fixtures/26-go-work-with-shared/api/index.go
vendored
Normal 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"))
|
||||||
|
}
|
||||||
6
packages/go/test/fixtures/26-go-work-with-shared/go.work
vendored
Normal file
6
packages/go/test/fixtures/26-go-work-with-shared/go.work
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
go 1.20
|
||||||
|
|
||||||
|
use (
|
||||||
|
./api
|
||||||
|
./mylib
|
||||||
|
)
|
||||||
3
packages/go/test/fixtures/26-go-work-with-shared/mylib/go.mod
vendored
Normal file
3
packages/go/test/fixtures/26-go-work-with-shared/mylib/go.mod
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module go-work-with-shared/mylib
|
||||||
|
|
||||||
|
go 1.20
|
||||||
9
packages/go/test/fixtures/26-go-work-with-shared/mylib/main.go
vendored
Normal file
9
packages/go/test/fixtures/26-go-work-with-shared/mylib/main.go
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package mylib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Say(text string) string {
|
||||||
|
return text + ":" + runtime.Version()
|
||||||
|
}
|
||||||
8
packages/go/test/fixtures/26-go-work-with-shared/probes.json
vendored
Normal file
8
packages/go/test/fixtures/26-go-work-with-shared/probes.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"probes": [
|
||||||
|
{
|
||||||
|
"path": "/api/index.go",
|
||||||
|
"mustContain": "hello:go1.20.2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user