mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-07 04:22:04 +00:00
On Windows 10 and 11 machines, environment variables are not case sensitive. The system PATH is actually defined as `process.env.Path`, however Node.js kindly handles the case sensitivity and will automatically return the system path when specifying `process.env.PATH`.
When we clone the environment variables via `{ ...process.env }`, we lose the automatic resolving of `Path` to `PATH`. To fix this, we need to explicitly copy the `PATH`.
#### Tests
- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`
#### Code Review
- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
219 lines
6.3 KiB
TypeScript
219 lines
6.3 KiB
TypeScript
import { join, dirname, relative } from 'path';
|
|
import execa from 'execa';
|
|
import {
|
|
ensureDir,
|
|
move,
|
|
remove,
|
|
pathExists,
|
|
readFile,
|
|
writeFile,
|
|
} from 'fs-extra';
|
|
import {
|
|
BuildOptions,
|
|
download,
|
|
getWriteableDirectory,
|
|
glob,
|
|
createLambda,
|
|
debug,
|
|
walkParentDirs,
|
|
cloneEnv,
|
|
} from '@vercel/build-utils';
|
|
import { installBundler } from './install-ruby';
|
|
|
|
async function matchPaths(
|
|
configPatterns: string | string[] | undefined,
|
|
workPath: string
|
|
) {
|
|
const patterns =
|
|
typeof configPatterns === 'string' ? [configPatterns] : configPatterns;
|
|
|
|
if (!patterns) {
|
|
return [];
|
|
}
|
|
|
|
const patternPaths = await Promise.all(
|
|
patterns.map(async pattern => {
|
|
const files = await glob(pattern, workPath);
|
|
return Object.keys(files);
|
|
})
|
|
);
|
|
|
|
return patternPaths.reduce((a, b) => a.concat(b), []);
|
|
}
|
|
|
|
async function bundleInstall(
|
|
bundlePath: string,
|
|
bundleDir: string,
|
|
gemfilePath: string
|
|
) {
|
|
debug(`running "bundle install --deployment"...`);
|
|
const bundleAppConfig = await getWriteableDirectory();
|
|
const gemfileContent = await readFile(gemfilePath, 'utf8');
|
|
if (gemfileContent.includes('ruby "~> 2.7.x"')) {
|
|
// Gemfile contains "2.7.x" which will cause an error message:
|
|
// "Your Ruby patchlevel is 0, but your Gemfile specified -1"
|
|
// See https://github.com/rubygems/bundler/blob/3f0638c6c8d340c2f2405ecb84eb3b39c433e36e/lib/bundler/errors.rb#L49
|
|
// We must correct to the actual version in the build container.
|
|
await writeFile(
|
|
gemfilePath,
|
|
gemfileContent.replace('ruby "~> 2.7.x"', 'ruby "~> 2.7.0"')
|
|
);
|
|
}
|
|
await execa(
|
|
bundlePath,
|
|
['install', '--deployment', '--gemfile', gemfilePath, '--path', bundleDir],
|
|
{
|
|
stdio: 'pipe',
|
|
env: cloneEnv(process.env, {
|
|
BUNDLE_SILENCE_ROOT_WARNING: '1',
|
|
BUNDLE_APP_CONFIG: bundleAppConfig,
|
|
BUNDLE_JOBS: '4',
|
|
}),
|
|
}
|
|
);
|
|
}
|
|
|
|
export const version = 3;
|
|
|
|
export async function build({
|
|
workPath,
|
|
files,
|
|
entrypoint,
|
|
config,
|
|
meta = {},
|
|
}: BuildOptions) {
|
|
await download(files, workPath, meta);
|
|
const entrypointFsDirname = join(workPath, dirname(entrypoint));
|
|
const gemfileName = 'Gemfile';
|
|
|
|
const gemfilePath = await walkParentDirs({
|
|
base: workPath,
|
|
start: entrypointFsDirname,
|
|
filename: gemfileName,
|
|
});
|
|
const gemfileContents = gemfilePath
|
|
? await readFile(gemfilePath, 'utf8')
|
|
: '';
|
|
const { gemHome, bundlerPath, vendorPath, runtime } = await installBundler(
|
|
meta,
|
|
gemfileContents
|
|
);
|
|
process.env.GEM_HOME = gemHome;
|
|
debug(`Checking existing vendor directory at "${vendorPath}"`);
|
|
const vendorDir = join(workPath, vendorPath);
|
|
const bundleDir = join(workPath, 'vendor', 'bundle');
|
|
const relativeVendorDir = join(entrypointFsDirname, vendorPath);
|
|
const hasRootVendorDir = await pathExists(vendorDir);
|
|
const hasRelativeVendorDir = await pathExists(relativeVendorDir);
|
|
const hasVendorDir = hasRootVendorDir || hasRelativeVendorDir;
|
|
|
|
if (hasRelativeVendorDir) {
|
|
if (hasRootVendorDir) {
|
|
debug(
|
|
'found two vendor directories, choosing the vendor directory relative to entrypoint'
|
|
);
|
|
} else {
|
|
debug('found vendor directory relative to entrypoint');
|
|
}
|
|
|
|
// vendor dir must be at the root for lambda to find it
|
|
await move(relativeVendorDir, vendorDir);
|
|
} else if (hasRootVendorDir) {
|
|
debug('found vendor directory in project root');
|
|
}
|
|
|
|
await ensureDir(vendorDir);
|
|
|
|
// no vendor directory, check for Gemfile to install
|
|
if (!hasVendorDir) {
|
|
if (gemfilePath) {
|
|
debug(
|
|
'did not find a vendor directory but found a Gemfile, bundling gems...'
|
|
);
|
|
|
|
const fileAtRoot = relative(workPath, gemfilePath) === gemfileName;
|
|
|
|
// If the `Gemfile` is located in the Root Directory of the project and
|
|
// the new File System API is used (`avoidTopLevelInstall`), the Install Command
|
|
// will have already installed its dependencies, so we don't need to do it again.
|
|
if (meta.avoidTopLevelInstall && fileAtRoot) {
|
|
debug('Skipping `bundle install` — already handled by Install Command');
|
|
} else {
|
|
// try installing. this won't work if native extesions are required.
|
|
// if that's the case, gems should be vendored locally before deploying.
|
|
await bundleInstall(bundlerPath, bundleDir, gemfilePath);
|
|
}
|
|
}
|
|
} else {
|
|
debug('found vendor directory, skipping "bundle install"...');
|
|
}
|
|
|
|
// try to remove gem cache to slim bundle size
|
|
try {
|
|
await remove(join(vendorDir, 'cache'));
|
|
} catch (e) {
|
|
// don't do anything here
|
|
}
|
|
|
|
const originalRbPath = join(__dirname, '..', 'vc_init.rb');
|
|
const originalHandlerRbContents = await readFile(originalRbPath, 'utf8');
|
|
|
|
// will be used on `require_relative '$here'` or for loading rack config.ru file
|
|
// for example, `require_relative 'api/users'`
|
|
debug('entrypoint is', entrypoint);
|
|
const userHandlerFilePath = entrypoint.replace(/\.rb$/, '');
|
|
const nowHandlerRbContents = originalHandlerRbContents.replace(
|
|
/__VC_HANDLER_FILENAME/g,
|
|
userHandlerFilePath
|
|
);
|
|
|
|
// in order to allow the user to have `server.rb`, we need our `server.rb` to be called
|
|
// somethig else
|
|
const handlerRbFilename = 'vc__handler__ruby';
|
|
|
|
await writeFile(
|
|
join(workPath, `${handlerRbFilename}.rb`),
|
|
nowHandlerRbContents
|
|
);
|
|
|
|
const outputFiles = await glob('**', workPath);
|
|
|
|
// static analysis is impossible with ruby.
|
|
// instead, provide `includeFiles` and `excludeFiles` config options to reduce bundle size.
|
|
if (config && (config.includeFiles || config.excludeFiles)) {
|
|
const includedPaths = await matchPaths(config.includeFiles, workPath);
|
|
const excludedPaths = await matchPaths(
|
|
config.excludeFiles as string | string[],
|
|
workPath
|
|
);
|
|
|
|
for (let i = 0; i < excludedPaths.length; i++) {
|
|
// whitelist includeFiles
|
|
if (includedPaths.includes(excludedPaths[i])) {
|
|
continue;
|
|
}
|
|
|
|
// whitelist handler
|
|
if (excludedPaths[i] === `${handlerRbFilename}.rb`) {
|
|
continue;
|
|
}
|
|
|
|
// whitelist vendor directory
|
|
if (excludedPaths[i].startsWith(vendorPath)) {
|
|
continue;
|
|
}
|
|
|
|
delete outputFiles[excludedPaths[i]];
|
|
}
|
|
}
|
|
|
|
const lambda = await createLambda({
|
|
files: outputFiles,
|
|
handler: `${handlerRbFilename}.vc__handler`,
|
|
runtime,
|
|
environment: {},
|
|
});
|
|
|
|
return { output: lambda };
|
|
}
|