[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(
'[vercel dev] Should set the `ts-node` "target" to match Node.js version',
testFixtureStdio('node-ts-node-target', async (testPath: any) => {

View File

@@ -9,7 +9,7 @@ import {
remove,
symlink,
} from 'fs-extra';
import { join, delimiter, dirname } from 'path';
import { delimiter, dirname, join } from 'path';
import stringArgv from 'string-argv';
import { cloneEnv, debug } from '@vercel/build-utils';
import { pipeline } from 'stream';
@@ -22,7 +22,7 @@ import type { Env } from '@vercel/build-utils';
const streamPipeline = promisify(pipeline);
const versionMap = new Map([
['1.20', '1.20.1'],
['1.20', '1.20.2'],
['1.19', '1.19.6'],
['1.18', '1.18.10'],
['1.17', '1.17.13'],
@@ -36,17 +36,32 @@ const archMap = new Map([
['x86', '386'],
]);
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_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) {
const { arch, platform } = process;
const goArch = getArch(arch);
const goPlatform = getPlatform(platform);
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}`;
return {
filename,
@@ -61,32 +76,91 @@ export const goGlobalCachePath = join(
export const OUT_EXTENSION = process.platform === 'win32' ? '.exe' : '';
export async function getAnalyzedEntrypoint(
workPath: string,
filePath: string,
modulePath: string
) {
const bin = join(__dirname, `analyze${OUT_EXTENSION}`);
interface Analyzed {
functionName: string;
packageName: string;
watch?: boolean;
}
/**
* 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');
const go = await createGo({
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(`Analyzing entrypoint ${filePath} with modulePath ${modulePath}`);
const args = [`-modpath=${modulePath}`, filePath];
const analyzed = await execa.stdout(bin, args);
debug(`Analyzed entrypoint ${analyzed}`);
return 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;
}
class GoWrapper {
export class GoWrapper {
private env: Env;
private opts: execa.Options;
@@ -101,10 +175,11 @@ class GoWrapper {
private execute(...args: string[]) {
const { opts, env } = this;
debug(
`Exec: go ${args
.map(a => (a.includes(' ') ? `"${a}"` : a))
.join(' ')} CWD=${opts.cwd}`
`Exec: go ${args.map(a => (a.includes(' ') ? `"${a}"` : a)).join(' ')}`
);
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 });
}
@@ -127,9 +202,8 @@ class GoWrapper {
debug(`Building optimized 'go' binary ${src} -> ${dest}`);
const sources = Array.isArray(src) ? src : [src];
const flags = process.env.GO_BUILD_FLAGS
? stringArgv(process.env.GO_BUILD_FLAGS)
: GO_FLAGS;
const envGoBuildFlags = (this.env || this.opts.env).GO_BUILD_FLAGS;
const flags = envGoBuildFlags ? stringArgv(envGoBuildFlags) : GO_FLAGS;
return this.execute('build', ...flags, '-o', dest, ...sources);
}
@@ -186,7 +260,7 @@ export async function createGo({
goGlobalCachePath,
`${goSelectedVersion}_${platform}_${process.arch}`
);
const goCacheDir = join(workPath, cacheDir);
const goCacheDir = join(workPath, localCacheDir);
if (goPreferredVersion) {
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`);
}
if (version === goSelectedVersion || short === goSelectedVersion) {
console.log(`Selected go ${version} (from ${label})`);
debug(`Selected go ${version} (from ${label})`);
await setGoEnv(goDir);
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.
*
@@ -359,6 +437,12 @@ async function parseGoModVersion(
const full = versionMap.get(`${major}.${minor}`);
if (major === 1 && minor >= GO_MIN_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 {
console.log(`Warning: Unknown Go version in ${file}`);
}

View File

@@ -4,7 +4,7 @@ import { homedir, tmpdir } from 'os';
import { spawn } from 'child_process';
import { Readable } from 'stream';
import once from '@tootallnate/once';
import { join, dirname, basename, normalize, posix, sep } from 'path';
import { basename, dirname, join, normalize, posix, relative } from 'path';
import {
readFile,
writeFile,
@@ -17,6 +17,7 @@ import {
rmdir,
readdir,
unlink,
copy,
} from 'fs-extra';
import {
BuildOptions,
@@ -26,7 +27,7 @@ import {
StartDevServerResult,
glob,
download,
createLambda,
Lambda,
getWriteableDirectory,
shouldServe,
debug,
@@ -36,21 +37,20 @@ import {
const TMP = tmpdir();
import {
cacheDir,
localCacheDir,
createGo,
getAnalyzedEntrypoint,
GoWrapper,
OUT_EXTENSION,
} from './go-helpers';
const handlerFileName = `handler${OUT_EXTENSION}`;
export { shouldServe };
interface Analyzed {
functionName: string;
packageName: string;
watch?: boolean;
}
// in order to allow the user to have `main.go`,
// we need our `main.go` to be called something else
const MAIN_GO_FILENAME = 'main__vc__go__.go';
const HANDLER_FILENAME = `handler${OUT_EXTENSION}`;
interface PortInfo {
port: number;
@@ -100,6 +100,12 @@ type UndoFunctionRename = {
to: string;
};
type UndoActions = {
fileActions: UndoFileAction[];
directoryCreation: string[];
functionRenames: UndoFunctionRename[];
};
export const version = 3;
export async function build({
@@ -117,17 +123,24 @@ export async function build({
// keep track of file system actions we need to undo
// the keys "from" and "to" refer to what needs to be done
// in order to undo the action, not what the original action was
const undoFileActions: UndoFileAction[] = [];
const undoDirectoryCreation: string[] = [];
const undoFunctionRenames: UndoFunctionRename[] = [];
const undo: UndoActions = {
fileActions: [],
directoryCreation: [],
functionRenames: [],
};
const env = cloneEnv(process.env, meta.env, {
GOARCH: 'amd64',
GOOS: 'linux',
});
try {
if (process.env.GIT_CREDENTIALS) {
if (env.GIT_CREDENTIALS) {
debug('Initialize Git credentials...');
await initPrivateGit(process.env.GIT_CREDENTIALS);
await initPrivateGit(env.GIT_CREDENTIALS);
}
if (process.env.GO111MODULE) {
if (env.GO111MODULE) {
console.log(`\nManually assigning 'GO111MODULE' is not recommended.
By default:
@@ -143,7 +156,7 @@ export async function build({
const renamedEntrypoint = getRenamedEntrypoint(entrypoint);
if (renamedEntrypoint) {
await move(join(workPath, entrypoint), join(workPath, renamedEntrypoint));
undoFileActions.push({
undo.fileActions.push({
to: join(workPath, entrypoint),
from: join(workPath, renamedEntrypoint),
});
@@ -151,113 +164,32 @@ export async function build({
}
const entrypointAbsolute = join(workPath, entrypoint);
const entrypointArr = entrypoint.split(posix.sep);
debug(`Parsing AST for "${entrypoint}"`);
let analyzed: string;
try {
const goModAbsPath = await findGoModPath(workPath);
if (goModAbsPath) {
debug(`Found ${goModAbsPath}"`);
}
analyzed = await getAnalyzedEntrypoint(
workPath,
entrypointAbsolute,
dirname(goModAbsPath)
);
} catch (err) {
console.error(`Failed to parse AST for "${entrypoint}"`);
throw err;
}
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;
}
const parsedAnalyzed = JSON.parse(analyzed) as Analyzed;
// find `go.mod` in modFiles
const entrypointDirname = dirname(entrypointAbsolute);
let isGoModExist = false;
let goModPath = '';
let isGoModInRootDir = false;
const modFileRefs = await glob('**/*.mod', workPath);
const modFiles = Object.keys(modFileRefs);
const { goModPath, isGoModInRootDir } = await findGoModPath(
entrypointDirname,
workPath
);
for (const file of modFiles) {
const fileDirname = dirname(file);
if (file === 'go.mod') {
isGoModExist = true;
isGoModInRootDir = true;
goModPath = join(workPath, fileDirname);
} else if (file.endsWith('go.mod')) {
if (entrypointDirname === fileDirname) {
isGoModExist = true;
goModPath = join(workPath, fileDirname);
debug(`Found file dirname equals entrypoint dirname: ${fileDirname}`);
break;
if (!goModPath && (await pathExists(join(workPath, 'vendor')))) {
throw new Error('`go.mod` is required to use a `vendor` directory.');
}
if (!isGoModInRootDir && config.zeroConfig && file === 'api/go.mod') {
// We didn't find `/go.mod` but we found `/api/go.mod` so move it to the root
isGoModExist = true;
isGoModInRootDir = true;
goModPath = join(fileDirname, '..');
const pathParts = file.split(sep);
pathParts.pop(); // Remove go.mod
pathParts.pop(); // Remove api
pathParts.push('go.mod');
const newRoot = pathParts.join(sep);
const newFsPath = join(workPath, newRoot);
debug(`Moving api/go.mod to root: ${file} to ${newFsPath}`);
await move(file, newFsPath);
undoFileActions.push({
to: file,
from: newFsPath,
const analyzed = await getAnalyzedEntrypoint({
entrypoint,
modulePath: goModPath ? dirname(goModPath) : undefined,
workPath,
});
const oldSumPath = join(dirname(file), 'go.sum');
const newSumPath = join(dirname(newFsPath), 'go.sum');
if (await pathExists(oldSumPath)) {
debug(`Moving api/go.sum to root: ${oldSumPath} to ${newSumPath}`);
await move(oldSumPath, newSumPath);
undoFileActions.push({
to: oldSumPath,
from: newSumPath,
});
}
break;
}
}
// check if package name other than main
// using `go.mod` way building the handler
const packageName = analyzed.packageName;
if (goModPath && packageName === 'main') {
throw new Error('Please change `package main` to `package handler`');
}
const input = entrypointDirname;
const includedFiles: Files = {};
if (config && config.includeFiles) {
const patterns = Array.isArray(config.includeFiles)
? config.includeFiles
: [config.includeFiles];
for (const pattern of patterns) {
const fsFiles = await glob(pattern, input);
for (const [assetName, asset] of Object.entries(fsFiles)) {
includedFiles[assetName] = asset;
}
}
}
const originalFunctionName = parsedAnalyzed.functionName;
// rename the Go handler function name in the original entrypoint file
const originalFunctionName = analyzed.functionName;
const handlerFunctionName = getNewHandlerFunctionName(
originalFunctionName,
entrypoint
@@ -267,120 +199,183 @@ export async function build({
originalFunctionName,
handlerFunctionName
);
undoFunctionRenames.push({
undo.functionRenames.push({
fsPath: originalEntrypointAbsolute,
from: handlerFunctionName,
to: originalFunctionName,
});
if (!isGoModExist) {
if (await pathExists(join(workPath, 'vendor'))) {
throw new Error('`go.mod` is required to use a `vendor` directory.');
const includedFiles: Files = {};
if (config && config.includeFiles) {
const patterns = Array.isArray(config.includeFiles)
? config.includeFiles
: [config.includeFiles];
for (const pattern of patterns) {
const fsFiles = await glob(pattern, entrypointDirname);
for (const [assetName, asset] of Object.entries(fsFiles)) {
includedFiles[assetName] = asset;
}
}
}
// check if package name other than main
// using `go.mod` way building the handler
const packageName = parsedAnalyzed.packageName;
if (isGoModExist && packageName === 'main') {
throw new Error('Please change `package main` to `package handler`');
}
const outDir = await getWriteableDirectory();
// in order to allow the user to have `main.go`,
// we need our `main.go` to be called something else
const mainGoFileName = 'main__vc__go__.go';
const modulePath = goModPath ? dirname(goModPath) : undefined;
const go = await createGo({
modulePath: goModPath,
modulePath,
opts: {
cwd: entrypointDirname,
env: {
GOARCH: 'amd64',
GOOS: 'linux',
},
env,
},
workPath,
});
if (packageName !== 'main') {
if (!isGoModExist) {
const outDir = await getWriteableDirectory();
const buildOptions: BuildHandlerOptions = {
downloadPath,
entrypoint,
entrypointAbsolute,
entrypointDirname,
go,
goModPath,
handlerFunctionName,
isGoModInRootDir,
outDir,
packageName,
undo,
};
if (packageName === 'main') {
await buildHandlerAsPackageMain(buildOptions);
} else {
await buildHandlerWithGoMod(buildOptions);
}
const lambda = new Lambda({
files: { ...(await glob('**', outDir)), ...includedFiles },
handler: HANDLER_FILENAME,
runtime: 'go1.x',
supportsWrapper: true,
environment: {},
});
return {
output: lambda,
};
} catch (error) {
debug(`Go Builder Error: ${error}`);
throw error;
} finally {
try {
const defaultGoModContent = `module ${packageName}`;
await cleanupFileSystem(undo);
} catch (error) {
if (error instanceof Error) {
console.error(`Build cleanup failed: ${error.message}`);
}
debug('Cleanup Error: ' + error);
}
}
}
await writeFile(
join(entrypointDirname, 'go.mod'),
defaultGoModContent
type BuildHandlerOptions = {
downloadPath: string;
entrypoint: string;
entrypointAbsolute: string;
entrypointDirname: string;
go: GoWrapper;
goModPath?: string;
handlerFunctionName: string;
isGoModInRootDir: boolean;
outDir: string;
packageName: string;
undo: UndoActions;
};
/**
* Build the Go function where the package name is not `"main"`. If a `go.mod`
* does not exist, a default one will be used.
*/
async function buildHandlerWithGoMod({
downloadPath,
entrypoint,
entrypointAbsolute,
entrypointDirname,
go,
goModPath,
handlerFunctionName,
isGoModInRootDir,
outDir,
packageName,
undo,
}: BuildHandlerOptions): Promise<void> {
debug(
`Building Go handler as package "${packageName}" (with${
goModPath ? '' : 'out'
} go.mod)`
);
undoFileActions.push({
to: undefined, // delete
from: join(entrypointDirname, 'go.mod'),
let goModDirname: string | undefined;
if (goModPath !== undefined) {
goModDirname = dirname(goModPath);
// first we backup the original
const backupFile = join(goModDirname, `__vc_go.mod.bak`);
await copy(goModPath, backupFile);
undo.fileActions.push({
to: goModPath,
from: backupFile,
});
const goSumPath = join(goModDirname, 'go.sum');
const isGoSumExists = await pathExists(goSumPath);
if (!isGoSumExists) {
// remove the `go.sum` file that will be generated as well
undoFileActions.push({
undo.fileActions.push({
to: undefined, // delete
from: join(entrypointDirname, 'go.sum'),
from: goSumPath,
});
} catch (err) {
console.error(`Failed to create default go.mod for ${packageName}`);
throw err;
}
}
const modMainGoContents = await readFile(
join(__dirname, 'main.go'),
'utf8'
);
const entrypointArr = entrypoint.split(posix.sep);
let goPackageName = `${packageName}/${packageName}`;
const goFuncName = `${packageName}.${handlerFunctionName}`;
if (isGoModExist) {
const goModContents = await readFile(join(goModPath, 'go.mod'), 'utf8');
const usrModName = goModContents.split('\n')[0].split(' ')[1];
if (entrypointArr.length > 1 && isGoModInRootDir) {
const cleanPackagePath = [...entrypointArr];
cleanPackagePath.pop();
goPackageName = `${usrModName}/${cleanPackagePath.join('/')}`;
// if we have a go.mod, determine the relative path of the entrypoint to the
// go.mod directory and use that for the import package name in main.go
const relPackagePath = goModDirname
? posix.relative(goModDirname, entrypointDirname)
: '';
if (relPackagePath) {
goPackageName = posix.join(packageName, relPackagePath);
}
let mainGoFile: string;
if (goModPath && isGoModInRootDir) {
debug(`[mod-root] Write main file to ${downloadPath}`);
mainGoFile = join(downloadPath, MAIN_GO_FILENAME);
} else if (goModDirname && !isGoModInRootDir) {
debug(`[mod-other] Write main file to ${goModDirname}`);
mainGoFile = join(goModDirname, MAIN_GO_FILENAME);
} else {
goPackageName = `${usrModName}/${packageName}`;
}
debug(`[entrypoint] Write main file to ${entrypointDirname}`);
mainGoFile = join(entrypointDirname, MAIN_GO_FILENAME);
}
const mainModGoContents = modMainGoContents
.replace('__VC_HANDLER_PACKAGE_NAME', goPackageName)
.replace('__VC_HANDLER_FUNC_NAME', goFuncName);
await Promise.all([
writeEntrypoint(mainGoFile, goPackageName, goFuncName),
writeGoMod({
destDir: goModDirname ? goModDirname : entrypointDirname,
goModPath,
packageName,
}),
]);
if (isGoModExist && isGoModInRootDir) {
debug('[mod-root] Write main file to ' + downloadPath);
await writeFile(join(downloadPath, mainGoFileName), mainModGoContents);
undoFileActions.push({
undo.fileActions.push({
to: undefined, // delete
from: join(downloadPath, mainGoFileName),
from: mainGoFile,
});
} else if (isGoModExist && !isGoModInRootDir) {
debug('[mod-other] Write main file to ' + goModPath);
await writeFile(join(goModPath, mainGoFileName), mainModGoContents);
undoFileActions.push({
to: undefined, // delete
from: join(goModPath, mainGoFileName),
});
} else {
debug('[entrypoint] Write main file to ' + entrypointDirname);
await writeFile(
join(entrypointDirname, mainGoFileName),
mainModGoContents
);
undoFileActions.push({
to: undefined, // delete
from: join(entrypointDirname, mainGoFileName),
});
}
// move user go file to folder
try {
@@ -396,17 +391,17 @@ export async function build({
);
}
if (dirname(entrypointAbsolute) === goModPath || !isGoModExist) {
if (!goModPath || dirname(entrypointAbsolute) === dirname(goModPath)) {
debug(
`moving entrypoint "${entrypointAbsolute}" to "${finalDestination}"`
);
await move(entrypointAbsolute, finalDestination);
undoFileActions.push({
undo.fileActions.push({
to: entrypointAbsolute,
from: finalDestination,
});
undoDirectoryCreation.push(dirname(finalDestination));
undo.directoryCreation.push(dirname(finalDestination));
}
} catch (err) {
console.error('Failed to move entry to package folder');
@@ -414,10 +409,10 @@ export async function build({
}
let baseGoModPath = '';
if (isGoModExist && isGoModInRootDir) {
if (goModPath && isGoModInRootDir) {
baseGoModPath = downloadPath;
} else if (isGoModExist && !isGoModInRootDir) {
baseGoModPath = goModPath;
} else if (goModPath && !isGoModInRootDir) {
baseGoModPath = dirname(goModPath);
} else {
baseGoModPath = entrypointDirname;
}
@@ -432,34 +427,42 @@ export async function build({
}
debug('Running `go build`...');
const destPath = join(outDir, handlerFileName);
const destPath = join(outDir, HANDLER_FILENAME);
try {
const src = [join(baseGoModPath, mainGoFileName)];
const src = [join(baseGoModPath, MAIN_GO_FILENAME)];
await go.build(src, destPath);
} catch (err) {
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 originalMainGoContents = await readFile(
join(__dirname, 'main.go'),
'utf8'
);
const mainGoContents = originalMainGoContents
.replace('"__VC_HANDLER_PACKAGE_NAME"', '')
.replace('__VC_HANDLER_FUNC_NAME', handlerFunctionName);
}
// Go doesn't like to build files in different directories,
// so now we place `main.go` together with the user code
await writeFile(join(entrypointDirname, mainGoFileName), mainGoContents);
undoFileActions.push({
/**
* Builds the wrapped Go function using the legacy mode where package name is
* `"main"` and we need `main.go` in the same dir as the entrypoint, otherwise
* `go build` will refuse to build.
*/
async function buildHandlerAsPackageMain({
entrypointAbsolute,
entrypointDirname,
go,
handlerFunctionName,
outDir,
undo,
}: BuildHandlerOptions): Promise<void> {
debug('Building Go handler as package "main" (legacy)');
await writeEntrypoint(
join(entrypointDirname, MAIN_GO_FILENAME),
'',
handlerFunctionName
);
undo.fileActions.push({
to: undefined, // delete
from: join(entrypointDirname, mainGoFileName),
from: join(entrypointDirname, MAIN_GO_FILENAME),
});
// `go get` will look at `*.go` (note we set `cwd`), parse the `import`s
@@ -473,10 +476,10 @@ export async function build({
}
debug('Running `go build`...');
const destPath = join(outDir, handlerFileName);
const destPath = join(outDir, HANDLER_FILENAME);
try {
const src = [
join(entrypointDirname, mainGoFileName),
join(entrypointDirname, MAIN_GO_FILENAME),
entrypointAbsolute,
].map(file => normalize(file));
await go.build(src, destPath);
@@ -484,37 +487,6 @@ export async function build({
console.error('failed to `go build`');
throw err;
}
}
const lambda = await createLambda({
files: { ...(await glob('**', outDir)), ...includedFiles },
handler: handlerFileName,
runtime: 'go1.x',
supportsWrapper: true,
environment: {},
});
return {
output: lambda,
};
} catch (error) {
debug('Go Builder Error: ' + error);
throw error;
} finally {
try {
await cleanupFileSystem(
undoFileActions,
undoDirectoryCreation,
undoFunctionRenames
);
} catch (error) {
if (error instanceof Error) {
console.error(`Build cleanup failed: ${error.message}`);
}
debug('Cleanup Error: ' + error);
}
}
}
async function renameHandlerFunction(fsPath: string, from: string, to: string) {
@@ -560,17 +532,20 @@ export function getNewHandlerFunctionName(
return newHandlerName;
}
async function cleanupFileSystem(
undoFileActions: UndoFileAction[],
undoDirectoryCreation: string[],
undoFunctionRenames: UndoFunctionRename[]
) {
/**
* Remove any temporary files, directories, and file changes.
*/
async function cleanupFileSystem({
fileActions,
directoryCreation,
functionRenames,
}: UndoActions) {
// we have to undo the actions in reverse order in cases
// where one file was moved multiple times, which happens
// using files that start with brackets
for (const action of undoFileActions.reverse()) {
for (const action of fileActions.reverse()) {
if (action.to) {
await move(action.from, action.to);
await move(action.from, action.to, { overwrite: true });
} else {
await remove(action.from);
}
@@ -578,11 +553,11 @@ async function cleanupFileSystem(
// after files are moved back, we can undo function renames
// these reference the original file location
for (const rename of undoFunctionRenames) {
for (const rename of functionRenames) {
await renameHandlerFunction(rename.fsPath, rename.from, rename.to);
}
const undoDirectoryPromises = undoDirectoryCreation.map(async directory => {
const undoDirectoryPromises = directoryCreation.map(async directory => {
const contents = await readdir(directory);
// only delete an empty directory
// if it has contents, either something went wrong during cleanup or this
@@ -595,18 +570,34 @@ async function cleanupFileSystem(
await Promise.all(undoDirectoryPromises);
}
async function findGoModPath(workPath: string): Promise<string> {
let checkPath = join(workPath, 'go.mod');
if (await pathExists(checkPath)) {
return checkPath;
/**
* Attempts to find a `go.mod` starting in the entrypoint directory and
* scanning up the directory tree.
* @param entrypointDir The entrypoint directory (e.g. `/path/to/project/api`)
* @param workPath The work path (e.g. `/path/to/project`)
* @returns The absolute path to the `go.mod` and a flag if the `go.mod` is in
* the work path root
*/
async function findGoModPath(entrypointDir: string, workPath: string) {
let goModPath: string | undefined = undefined;
let isGoModInRootDir = false;
let dir = entrypointDir;
while (!isGoModInRootDir) {
isGoModInRootDir = dir === workPath;
const goMod = join(dir, 'go.mod');
if (await pathExists(goMod)) {
goModPath = goMod;
debug(`Found ${goModPath}"`);
break;
}
dir = dirname(dir);
}
checkPath = join(workPath, 'api/go.mod');
if (await pathExists(checkPath)) {
return checkPath;
}
return '';
return {
goModPath,
isGoModInRootDir,
};
}
function isPortInfo(v: any): v is PortInfo {
@@ -638,17 +629,159 @@ async function copyDevServer(
await writeFile(join(dest, 'vercel-dev-server-main.go'), patched);
}
async function writeDefaultGoMod(
entrypointDirname: string,
packageName: string
async function writeEntrypoint(
dest: string,
goPackageName: string,
goFuncName: string
) {
const defaultGoModContent = `module ${packageName}`;
const modMainGoContents = await readFile(join(__dirname, 'main.go'), 'utf8');
const mainModGoContents = modMainGoContents
.replace('__VC_HANDLER_PACKAGE_NAME', goPackageName)
.replace('__VC_HANDLER_FUNC_NAME', goFuncName);
await writeFile(dest, mainModGoContents, 'utf-8');
}
await writeFile(
join(entrypointDirname, 'go.mod'),
defaultGoModContent,
'utf-8'
/**
* Writes a `go.mod` file in the specified directory. If a `go.mod` file
* exists, then update the module name and any relative `replace` statements,
* otherwise write the minimum module name.
* @param workPath The work path; required if `goModPath` exists
* @param goModPath The path to the `go.mod`, or `undefined` if not found
* @param destDir The directory to write the `go.mod` to
* @param packageName The module name to inject into the `go.mod`
*/
async function writeGoMod({
destDir,
goModPath,
packageName,
}: {
destDir: string;
goModPath?: string;
packageName: string;
}) {
let contents = `module ${packageName}`;
if (goModPath) {
const goModRelPath = relative(destDir, dirname(goModPath));
const goModContents = await readFile(goModPath, 'utf-8');
contents = goModContents
.replace(/^module\s+.+$/m, contents)
.replace(
/^(replace .+=>\s*)(.+)$/gm,
(orig, replaceStmt, replacePath) => {
if (replacePath.startsWith('.')) {
return replaceStmt + join(goModRelPath, replacePath);
}
return orig;
}
);
// get the module name, then add the 'replace' mapping if it doesn't
// already exist
const matches = goModContents.match(/module\s+(.+)/);
const moduleName = matches ? matches[1] : null;
if (moduleName) {
let relPath = normalize(goModRelPath);
if (!relPath.endsWith('/')) {
relPath += '/';
}
const requireRE = new RegExp(`require\\s+${moduleName}`);
const requireGroupRE = new RegExp(
`require\\s*\\(.*${moduleName}.*\\)`,
's'
);
if (!requireRE.test(contents) && !requireGroupRE.test(contents)) {
contents += `require ${moduleName} v0.0.0-unpublished\n`;
}
const replaceRE = new RegExp(`replace.+=>\\s+${relPath}(\\s|$)`);
if (!replaceRE.test(contents)) {
contents += `replace ${moduleName} => ${relPath}\n`;
}
}
}
const destGoModPath = join(destDir, 'go.mod');
debug(`Writing ${destGoModPath}`);
// console.log(contents);
await writeFile(destGoModPath, contents, 'utf-8');
}
/**
* Attempts to find the `go.work` file. It will stop once it hits the
* `workPath`.
* @param goWorkDir The directory under the `wordPath` to start searching.
* @param workPath The project root to stop looking for the file.
* @returns The path to the `go.work` file or `undefined`.
*/
async function findGoWorkFile(goWorkDir: string, workPath: string) {
while (!(await pathExists(join(goWorkDir, 'go.work')))) {
if (goWorkDir === workPath) {
return;
}
goWorkDir = dirname(goWorkDir);
}
return join(goWorkDir, 'go.work');
}
/**
* For simple cases, a `go.work` file is not required. However when a Go
* program requires source files outside the work path, we need a `go.work` so
* Go can find the root of the project being built.
* @param destDir The destination directory to write the `go.work` file.
* @param workPath The path to the work directory.
* @param modulePath The path to the directory containing the `go.mod`.
*/
async function writeGoWork(
destDir: string,
workPath: string,
modulePath?: string
) {
const workspaces = new Set(['.']);
const goWorkPath = await findGoWorkFile(modulePath || workPath, workPath);
if (goWorkPath) {
const contents = await readFile(goWorkPath, 'utf-8');
const addPath = (path: string) => {
if (path) {
if (path.startsWith('.')) {
workspaces.add(relative(destDir, join(workPath, path)));
} else {
workspaces.add(path);
}
}
};
// find grouped paths
const multiRE = /use\s*\(([^)]+)/g;
let match = multiRE.exec(contents);
while (match) {
if (match[1]) {
for (const line of match[1].split(/\r?\n/)) {
addPath(line.trim());
}
}
match = multiRE.exec(contents);
}
// find single paths
const singleRE = /use\s+(?!\()(.+)/g;
match = singleRE.exec(contents);
while (match) {
addPath(match[1].trim());
match = singleRE.exec(contents);
}
} else if (modulePath) {
workspaces.add(relative(destDir, modulePath));
}
const contents = `use (\n${Array.from(workspaces)
.map(w => ` ${w}\n`)
.join('')})\n`;
// console.log(contents);
await writeFile(join(destDir, 'go.work'), contents, 'utf-8');
}
export async function startDevServer(
@@ -669,27 +802,26 @@ export async function startDevServer(
const tmpPackage = join(tmp, entrypointDir);
await mkdirp(tmpPackage);
let goModAbsPathDir = '';
if (await pathExists(join(workPath, 'go.mod'))) {
goModAbsPathDir = workPath;
}
const analyzedRaw = await getAnalyzedEntrypoint(
const { goModPath } = await findGoModPath(
join(workPath, entrypointDir),
workPath
);
const modulePath = goModPath ? dirname(goModPath) : undefined;
const analyzed = await getAnalyzedEntrypoint({
entrypoint: entrypointWithExt,
modulePath,
workPath,
entrypointWithExt,
goModAbsPathDir
);
if (!analyzedRaw) {
throw new Error(
`Could not find an exported function in "${entrypointWithExt}"
Learn more: https://vercel.com/docs/runtimes#official-runtimes/go`
);
}
const analyzed: Analyzed = JSON.parse(analyzedRaw);
});
await Promise.all([
copyEntrypoint(entrypointWithExt, tmpPackage),
copyDevServer(analyzed.functionName, tmpPackage),
goModAbsPathDir ? null : writeDefaultGoMod(tmp, analyzed.packageName),
writeGoMod({
destDir: tmp,
goModPath,
packageName: analyzed.packageName,
}),
writeGoWork(tmp, workPath, modulePath),
]);
const portFile = join(
@@ -711,7 +843,7 @@ Learn more: https://vercel.com/docs/runtimes#official-runtimes/go`
// build the dev server
const go = await createGo({
modulePath: goModAbsPathDir,
modulePath,
opts: {
cwd: tmp,
env,
@@ -815,7 +947,7 @@ export async function prepareCache({
//
// 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 goCacheDir = join(workPath, localCacheDir);
const stat = await lstat(goCacheDir);
if (stat.isSymbolicLink()) {
const goGlobalCacheDir = await readlink(goCacheDir);
@@ -824,6 +956,6 @@ export async function prepareCache({
await move(goGlobalCacheDir, goCacheDir);
}
const cache = await glob(`${cacheDir}/**`, workPath);
const cache = await glob(`${localCacheDir}/**`, workPath);
return cache;
}

View File

@@ -5,10 +5,10 @@
{ "src": "subdirectory/index.go", "use": "@vercel/go" }
],
"probes": [
{ "path": "/", "mustContain": "cow:go1.20.1:RANDOMNESS_PLACEHOLDER" },
{ "path": "/", "mustContain": "cow:go1.20.2:RANDOMNESS_PLACEHOLDER" },
{
"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
go 1.12
go 1.20

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
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,
"builds": [{ "src": "index.go", "use": "@vercel/go" }],
"build": { "env": { "GO_BUILD_FLAGS": "-tags first -ldflags '-s -w'" } },
"probes": [
{
"path": "/",
"mustContain": "version:go1.14.15:first:RANDOMNESS_PLACEHOLDER"
}
]
"build": { "env": { "GO_BUILD_FLAGS": "-tags first -ldflags '-s -w'" } }
}

View File

@@ -2,4 +2,10 @@ module github.com/vercel/does-not-exist
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": [
{
"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"
}
]
}