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