[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}`);
/**
* 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); const isAnalyzeExist = await pathExists(bin);
if (!isAnalyzeExist) { if (!isAnalyzeExist) {
debug(`Building analyze bin: ${bin}`); debug(`Building analyze bin: ${bin}`);
const src = join(__dirname, 'util', 'analyze.go'); const src = join(__dirname, 'util', 'analyze.go');
const go = await createGo({ let go;
const createOpts = {
modulePath, modulePath,
opts: { cwd: __dirname },
workPath, 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); 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}`); 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 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}`);
} }

View File

@@ -4,7 +4,7 @@ import { homedir, tmpdir } from 'os';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { Readable } from 'stream'; import { Readable } from 'stream';
import once from '@tootallnate/once'; import once from '@tootallnate/once';
import { join, dirname, basename, normalize, posix, sep } from 'path'; import { basename, dirname, join, normalize, posix, relative } from 'path';
import { import {
readFile, readFile,
writeFile, writeFile,
@@ -17,6 +17,7 @@ import {
rmdir, rmdir,
readdir, readdir,
unlink, unlink,
copy,
} from 'fs-extra'; } from 'fs-extra';
import { import {
BuildOptions, BuildOptions,
@@ -26,7 +27,7 @@ import {
StartDevServerResult, StartDevServerResult,
glob, glob,
download, download,
createLambda, Lambda,
getWriteableDirectory, getWriteableDirectory,
shouldServe, shouldServe,
debug, debug,
@@ -36,21 +37,20 @@ import {
const TMP = tmpdir(); const TMP = tmpdir();
import { import {
cacheDir, localCacheDir,
createGo, createGo,
getAnalyzedEntrypoint, getAnalyzedEntrypoint,
GoWrapper,
OUT_EXTENSION, OUT_EXTENSION,
} from './go-helpers'; } from './go-helpers';
const handlerFileName = `handler${OUT_EXTENSION}`;
export { shouldServe }; export { shouldServe };
interface Analyzed { // in order to allow the user to have `main.go`,
functionName: string; // we need our `main.go` to be called something else
packageName: string; const MAIN_GO_FILENAME = 'main__vc__go__.go';
watch?: boolean;
} const HANDLER_FILENAME = `handler${OUT_EXTENSION}`;
interface PortInfo { interface PortInfo {
port: number; port: number;
@@ -100,6 +100,12 @@ type UndoFunctionRename = {
to: string; to: string;
}; };
type UndoActions = {
fileActions: UndoFileAction[];
directoryCreation: string[];
functionRenames: UndoFunctionRename[];
};
export const version = 3; export const version = 3;
export async function build({ export async function build({
@@ -117,17 +123,24 @@ export async function build({
// keep track of file system actions we need to undo // keep track of file system actions we need to undo
// the keys "from" and "to" refer to what needs to be done // the keys "from" and "to" refer to what needs to be done
// in order to undo the action, not what the original action was // in order to undo the action, not what the original action was
const undoFileActions: UndoFileAction[] = []; const undo: UndoActions = {
const undoDirectoryCreation: string[] = []; fileActions: [],
const undoFunctionRenames: UndoFunctionRename[] = []; directoryCreation: [],
functionRenames: [],
};
const env = cloneEnv(process.env, meta.env, {
GOARCH: 'amd64',
GOOS: 'linux',
});
try { try {
if (process.env.GIT_CREDENTIALS) { if (env.GIT_CREDENTIALS) {
debug('Initialize 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. console.log(`\nManually assigning 'GO111MODULE' is not recommended.
By default: By default:
@@ -143,7 +156,7 @@ export async function build({
const renamedEntrypoint = getRenamedEntrypoint(entrypoint); const renamedEntrypoint = getRenamedEntrypoint(entrypoint);
if (renamedEntrypoint) { if (renamedEntrypoint) {
await move(join(workPath, entrypoint), join(workPath, renamedEntrypoint)); await move(join(workPath, entrypoint), join(workPath, renamedEntrypoint));
undoFileActions.push({ undo.fileActions.push({
to: join(workPath, entrypoint), to: join(workPath, entrypoint),
from: join(workPath, renamedEntrypoint), from: join(workPath, renamedEntrypoint),
}); });
@@ -151,113 +164,32 @@ export async function build({
} }
const entrypointAbsolute = join(workPath, entrypoint); 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); const entrypointDirname = dirname(entrypointAbsolute);
let isGoModExist = false;
let goModPath = '';
let isGoModInRootDir = false;
const modFileRefs = await glob('**/*.mod', workPath); const { goModPath, isGoModInRootDir } = await findGoModPath(
const modFiles = Object.keys(modFileRefs); entrypointDirname,
workPath
);
for (const file of modFiles) { if (!goModPath && (await pathExists(join(workPath, 'vendor')))) {
const fileDirname = dirname(file); throw new Error('`go.mod` is required to use a `vendor` directory.');
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 (!isGoModInRootDir && config.zeroConfig && file === 'api/go.mod') { const analyzed = await getAnalyzedEntrypoint({
// We didn't find `/go.mod` but we found `/api/go.mod` so move it to the root entrypoint,
isGoModExist = true; modulePath: goModPath ? dirname(goModPath) : undefined,
isGoModInRootDir = true; workPath,
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 oldSumPath = join(dirname(file), 'go.sum'); // check if package name other than main
const newSumPath = join(dirname(newFsPath), 'go.sum'); // using `go.mod` way building the handler
if (await pathExists(oldSumPath)) { const packageName = analyzed.packageName;
debug(`Moving api/go.sum to root: ${oldSumPath} to ${newSumPath}`); if (goModPath && packageName === 'main') {
await move(oldSumPath, newSumPath); throw new Error('Please change `package main` to `package handler`');
undoFileActions.push({
to: oldSumPath,
from: newSumPath,
});
}
break;
}
}
} }
const input = entrypointDirname; // rename the Go handler function name in the original entrypoint file
const includedFiles: Files = {}; const originalFunctionName = analyzed.functionName;
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;
const handlerFunctionName = getNewHandlerFunctionName( const handlerFunctionName = getNewHandlerFunctionName(
originalFunctionName, originalFunctionName,
entrypoint entrypoint
@@ -267,120 +199,183 @@ export async function build({
originalFunctionName, originalFunctionName,
handlerFunctionName handlerFunctionName
); );
undoFunctionRenames.push({ undo.functionRenames.push({
fsPath: originalEntrypointAbsolute, fsPath: originalEntrypointAbsolute,
from: handlerFunctionName, from: handlerFunctionName,
to: originalFunctionName, to: originalFunctionName,
}); });
if (!isGoModExist) { const includedFiles: Files = {};
if (await pathExists(join(workPath, 'vendor'))) { if (config && config.includeFiles) {
throw new Error('`go.mod` is required to use a `vendor` directory.'); 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 const modulePath = goModPath ? dirname(goModPath) : undefined;
// 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 go = await createGo({ const go = await createGo({
modulePath: goModPath, modulePath,
opts: { opts: {
cwd: entrypointDirname, cwd: entrypointDirname,
env: { env,
GOARCH: 'amd64',
GOOS: 'linux',
},
}, },
workPath, workPath,
}); });
if (packageName !== 'main') { const outDir = await getWriteableDirectory();
if (!isGoModExist) { 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 { 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( type BuildHandlerOptions = {
join(entrypointDirname, 'go.mod'), downloadPath: string;
defaultGoModContent 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({ let goModDirname: string | undefined;
to: undefined, // delete
from: join(entrypointDirname, 'go.mod'), 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 // remove the `go.sum` file that will be generated as well
undoFileActions.push({ undo.fileActions.push({
to: undefined, // delete 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( const entrypointArr = entrypoint.split(posix.sep);
join(__dirname, 'main.go'),
'utf8'
);
let goPackageName = `${packageName}/${packageName}`; let goPackageName = `${packageName}/${packageName}`;
const goFuncName = `${packageName}.${handlerFunctionName}`; const goFuncName = `${packageName}.${handlerFunctionName}`;
if (isGoModExist) { // if we have a go.mod, determine the relative path of the entrypoint to the
const goModContents = await readFile(join(goModPath, 'go.mod'), 'utf8'); // go.mod directory and use that for the import package name in main.go
const usrModName = goModContents.split('\n')[0].split(' ')[1]; const relPackagePath = goModDirname
if (entrypointArr.length > 1 && isGoModInRootDir) { ? posix.relative(goModDirname, entrypointDirname)
const cleanPackagePath = [...entrypointArr]; : '';
cleanPackagePath.pop(); if (relPackagePath) {
goPackageName = `${usrModName}/${cleanPackagePath.join('/')}`; 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 { } else {
goPackageName = `${usrModName}/${packageName}`; debug(`[entrypoint] Write main file to ${entrypointDirname}`);
} mainGoFile = join(entrypointDirname, MAIN_GO_FILENAME);
} }
const mainModGoContents = modMainGoContents await Promise.all([
.replace('__VC_HANDLER_PACKAGE_NAME', goPackageName) writeEntrypoint(mainGoFile, goPackageName, goFuncName),
.replace('__VC_HANDLER_FUNC_NAME', goFuncName); writeGoMod({
destDir: goModDirname ? goModDirname : entrypointDirname,
goModPath,
packageName,
}),
]);
if (isGoModExist && isGoModInRootDir) { undo.fileActions.push({
debug('[mod-root] Write main file to ' + downloadPath);
await writeFile(join(downloadPath, mainGoFileName), mainModGoContents);
undoFileActions.push({
to: undefined, // delete 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 // move user go file to folder
try { try {
@@ -396,17 +391,17 @@ export async function build({
); );
} }
if (dirname(entrypointAbsolute) === goModPath || !isGoModExist) { if (!goModPath || dirname(entrypointAbsolute) === dirname(goModPath)) {
debug( debug(
`moving entrypoint "${entrypointAbsolute}" to "${finalDestination}"` `moving entrypoint "${entrypointAbsolute}" to "${finalDestination}"`
); );
await move(entrypointAbsolute, finalDestination); await move(entrypointAbsolute, finalDestination);
undoFileActions.push({ undo.fileActions.push({
to: entrypointAbsolute, to: entrypointAbsolute,
from: finalDestination, from: finalDestination,
}); });
undoDirectoryCreation.push(dirname(finalDestination)); undo.directoryCreation.push(dirname(finalDestination));
} }
} catch (err) { } catch (err) {
console.error('Failed to move entry to package folder'); console.error('Failed to move entry to package folder');
@@ -414,10 +409,10 @@ export async function build({
} }
let baseGoModPath = ''; let baseGoModPath = '';
if (isGoModExist && isGoModInRootDir) { if (goModPath && isGoModInRootDir) {
baseGoModPath = downloadPath; baseGoModPath = downloadPath;
} else if (isGoModExist && !isGoModInRootDir) { } else if (goModPath && !isGoModInRootDir) {
baseGoModPath = goModPath; baseGoModPath = dirname(goModPath);
} else { } else {
baseGoModPath = entrypointDirname; baseGoModPath = entrypointDirname;
} }
@@ -432,34 +427,42 @@ export async function build({
} }
debug('Running `go build`...'); debug('Running `go build`...');
const destPath = join(outDir, handlerFileName); const destPath = join(outDir, HANDLER_FILENAME);
try { try {
const src = [join(baseGoModPath, mainGoFileName)]; const src = [join(baseGoModPath, MAIN_GO_FILENAME)];
await go.build(src, destPath); await go.build(src, destPath);
} catch (err) { } catch (err) {
console.error('failed to `go build`'); console.error('failed to `go build`');
throw err; 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 * Builds the wrapped Go function using the legacy mode where package name is
await writeFile(join(entrypointDirname, mainGoFileName), mainGoContents); * `"main"` and we need `main.go` in the same dir as the entrypoint, otherwise
undoFileActions.push({ * `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 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 // `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`...'); debug('Running `go build`...');
const destPath = join(outDir, handlerFileName); const destPath = join(outDir, HANDLER_FILENAME);
try { try {
const src = [ const src = [
join(entrypointDirname, mainGoFileName), join(entrypointDirname, MAIN_GO_FILENAME),
entrypointAbsolute, entrypointAbsolute,
].map(file => normalize(file)); ].map(file => normalize(file));
await go.build(src, destPath); await go.build(src, destPath);
@@ -484,37 +487,6 @@ export async function build({
console.error('failed to `go build`'); console.error('failed to `go build`');
throw err; 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) { async function renameHandlerFunction(fsPath: string, from: string, to: string) {
@@ -560,17 +532,20 @@ export function getNewHandlerFunctionName(
return newHandlerName; return newHandlerName;
} }
async function cleanupFileSystem( /**
undoFileActions: UndoFileAction[], * Remove any temporary files, directories, and file changes.
undoDirectoryCreation: string[], */
undoFunctionRenames: UndoFunctionRename[] async function cleanupFileSystem({
) { fileActions,
directoryCreation,
functionRenames,
}: UndoActions) {
// we have to undo the actions in reverse order in cases // we have to undo the actions in reverse order in cases
// where one file was moved multiple times, which happens // where one file was moved multiple times, which happens
// using files that start with brackets // using files that start with brackets
for (const action of undoFileActions.reverse()) { for (const action of fileActions.reverse()) {
if (action.to) { if (action.to) {
await move(action.from, action.to); await move(action.from, action.to, { overwrite: true });
} else { } else {
await remove(action.from); await remove(action.from);
} }
@@ -578,11 +553,11 @@ async function cleanupFileSystem(
// after files are moved back, we can undo function renames // after files are moved back, we can undo function renames
// these reference the original file location // these reference the original file location
for (const rename of undoFunctionRenames) { for (const rename of functionRenames) {
await renameHandlerFunction(rename.fsPath, rename.from, rename.to); 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); const contents = await readdir(directory);
// only delete an empty directory // only delete an empty directory
// if it has contents, either something went wrong during cleanup or this // if it has contents, either something went wrong during cleanup or this
@@ -595,18 +570,34 @@ async function cleanupFileSystem(
await Promise.all(undoDirectoryPromises); await Promise.all(undoDirectoryPromises);
} }
async function findGoModPath(workPath: string): Promise<string> { /**
let checkPath = join(workPath, 'go.mod'); * Attempts to find a `go.mod` starting in the entrypoint directory and
if (await pathExists(checkPath)) { * scanning up the directory tree.
return checkPath; * @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'); return {
if (await pathExists(checkPath)) { goModPath,
return checkPath; isGoModInRootDir,
} };
return '';
} }
function isPortInfo(v: any): v is PortInfo { function isPortInfo(v: any): v is PortInfo {
@@ -638,17 +629,159 @@ async function copyDevServer(
await writeFile(join(dest, 'vercel-dev-server-main.go'), patched); await writeFile(join(dest, 'vercel-dev-server-main.go'), patched);
} }
async function writeDefaultGoMod( async function writeEntrypoint(
entrypointDirname: string, dest: string,
packageName: 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'), * Writes a `go.mod` file in the specified directory. If a `go.mod` file
defaultGoModContent, * exists, then update the module name and any relative `replace` statements,
'utf-8' * 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( export async function startDevServer(
@@ -669,27 +802,26 @@ export async function startDevServer(
const tmpPackage = join(tmp, entrypointDir); const tmpPackage = join(tmp, entrypointDir);
await mkdirp(tmpPackage); await mkdirp(tmpPackage);
let goModAbsPathDir = ''; const { goModPath } = await findGoModPath(
if (await pathExists(join(workPath, 'go.mod'))) { join(workPath, entrypointDir),
goModAbsPathDir = workPath; workPath
} );
const analyzedRaw = await getAnalyzedEntrypoint( const modulePath = goModPath ? dirname(goModPath) : undefined;
const analyzed = await getAnalyzedEntrypoint({
entrypoint: entrypointWithExt,
modulePath,
workPath, 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([ await Promise.all([
copyEntrypoint(entrypointWithExt, tmpPackage), copyEntrypoint(entrypointWithExt, tmpPackage),
copyDevServer(analyzed.functionName, 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( const portFile = join(
@@ -711,7 +843,7 @@ Learn more: https://vercel.com/docs/runtimes#official-runtimes/go`
// build the dev server // build the dev server
const go = await createGo({ const go = await createGo({
modulePath: goModAbsPathDir, modulePath,
opts: { opts: {
cwd: tmp, cwd: tmp,
env, env,
@@ -815,7 +947,7 @@ export async function prepareCache({
// //
// On the next build, the local cache will be restored and `createGo()` will // 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`. // 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); const stat = await lstat(goCacheDir);
if (stat.isSymbolicLink()) { if (stat.isSymbolicLink()) {
const goGlobalCacheDir = await readlink(goCacheDir); const goGlobalCacheDir = await readlink(goCacheDir);
@@ -824,6 +956,6 @@ export async function prepareCache({
await move(goGlobalCacheDir, goCacheDir); await move(goGlobalCacheDir, goCacheDir);
} }
const cache = await glob(`${cacheDir}/**`, workPath); const cache = await glob(`${localCacheDir}/**`, workPath);
return cache; return cache;
} }

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"
}
]
}