-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(turborepo): Rationalize the install and execution process. #5695
Changes from 2 commits
8d4a52b
c5b5658
aa8e7c2
1d5e4d5
c4f0709
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,257 @@ | ||
#!/usr/bin/env node | ||
|
||
const { generateBinPath } = require("../node-platform"); | ||
/** | ||
* We need to run a platform-specific `turbo`. The dependency _should_ | ||
* have already been installed, but it's possible that it has not. | ||
*/ | ||
|
||
const child_process = require('child_process'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
|
||
// If we do not find the correct platform binary, should we attempt to install it? | ||
const SHOULD_INSTALL = true; | ||
|
||
// If we do not find the correct platform binary, should we trust calling an emulated variant? | ||
const SHOULD_ATTEMPT_EMULATED = true; | ||
|
||
// Relies on the fact that each tarball publishes the `package.json`. | ||
// We can simply cd into the `turbo` directory and install there. | ||
function installUsingNPM() { | ||
const turboPath = path.dirname(require.resolve('turbo/package')); | ||
|
||
// Erase "npm_config_global" so that "npm install --global turbo" works. | ||
// Otherwise this nested "npm install" will also be global, and the install | ||
// will deadlock waiting for the global installation lock. | ||
const env = { ...process.env, npm_config_global: undefined }; | ||
|
||
child_process.execSync( | ||
`npm install --loglevel=error --prefer-offline --no-audit --progress=false`, | ||
{ cwd: turboPath, stdio: "pipe", env } | ||
); | ||
Comment on lines
+28
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previously this had to carefully track which package and version, clean up after itself, and more. Now we just rely on |
||
} | ||
|
||
// This provides logging messages as it progresses towards calculating the binary path. | ||
function getBinaryPath() { | ||
// First we see if the user has configured a particular binary path. | ||
const TURBO_BINARY_PATH = process.env.TURBO_BINARY_PATH; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We still need to allow the user to specify a particular binary path. |
||
if (TURBO_BINARY_PATH) { | ||
if (!fs.existsSync(TURBO_BINARY_PATH)) { | ||
console.warn( | ||
nathanhammond marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`[turbo] Ignoring bad configuration: TURBO_BINARY_PATH=${TURBO_BINARY_PATH}` | ||
); | ||
} else { | ||
return TURBO_BINARY_PATH; | ||
} | ||
} | ||
|
||
const availablePlatforms = [ | ||
'darwin', | ||
'linux', | ||
'windows', | ||
]; | ||
|
||
const availableArchs = [ | ||
'64', | ||
'arm64', | ||
]; | ||
|
||
// We need to figure out which binary to hand the user. | ||
// The only place where the binary can be at this point is `require.resolve`-able | ||
// relative to this package as it should be installed as an optional dependency. | ||
|
||
const { platform, arch } = process; | ||
const resolvedArch = arch === 'x64' ? '64' : arch; | ||
const ext = platform === 'windows' ? '.exe' : ''; | ||
|
||
// Try all places in order until we get a hit. | ||
|
||
// 1. The package which contains the binary we _should_ be running. | ||
const correctBinary = availablePlatforms.includes(platform) && availableArchs.includes(resolvedArch) ? `turbo-${platform}-${resolvedArch}/bin/turbo${ext}` : null; | ||
if (correctBinary !== null) { | ||
try { | ||
return require.resolve(`${correctBinary}`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By rule we should be able to |
||
} catch (e) {} | ||
} | ||
|
||
// 2. Install the binary that they need just in time. | ||
if (SHOULD_INSTALL && correctBinary !== null) { | ||
console.warn('Turborepo did not find the correct binary for your platform.'); | ||
console.warn('We will attempt to install it now.'); | ||
|
||
try { | ||
installUsingNPM(); | ||
const resolvedPath = require.resolve(`${correctBinary}`); | ||
console.warn('Installation has succeeded.'); | ||
Comment on lines
+89
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't succeed unless we can resolve it, so this gets split across lines. |
||
return resolvedPath; | ||
} catch (e) { | ||
console.warn('Installation has failed.'); | ||
} | ||
} | ||
|
||
// 3. Both Windows and macOS ARM boxes can run x64 binaries. Attempt to run under emulation. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a chance that this triggers an unfriendly error or even pops up a "install Rosetta" GUI window, but that should be a very low-frequency outcome. The primary cause for hitting this state is a user who has emulation set up on their box accidentally switching between Given the high odds of it working I'm comfortable doing this. |
||
const alternateBinary = (arch === "arm64" && ['darwin', 'windows'].includes(platform)) ? `turbo-${platform}-64/bin/turbo${ext}` : null; | ||
nathanhammond marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (SHOULD_ATTEMPT_EMULATED && alternateBinary !== null) { | ||
try { | ||
const resolvedPath = require.resolve(`${alternateBinary}`); | ||
console.warn(`Turborepo detected that you're running:\n${platform} ${resolvedArch}.`); | ||
console.warn(`We were not able to find the binary at:\n${correctBinary}`); | ||
console.warn(`We found a possibly-compatible binary at:\n${alternateBinary}`); | ||
console.warn(`We will attempt to run that binary.`); | ||
Comment on lines
+103
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also provide detailed output about what we're doing so that things going wrong are at least understandable. |
||
return resolvedPath; | ||
} catch (e) {} | ||
} | ||
|
||
// We are not going to run `turbo` this invocation. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
// Let's give the best error message that we can. | ||
|
||
// Possible error scenarios: | ||
// - The user is on a platform/arch combination we do not support. | ||
// - We somehow got detection wrong and never attempted to run the _actual_ `correctBinary` or `alternateBinary`. | ||
// - The user doesn't have the correct packages installed for their platform. | ||
|
||
// Explain our detection attempt: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is one other branch not represented in that output:
|
||
console.error(); | ||
console.error('***'); | ||
console.error(); | ||
console.error(`Turborepo failed to start.`); | ||
console.error(); | ||
console.error(`Turborepo detected that you are running:\n${platform} ${resolvedArch}`); | ||
|
||
// Tell them if we support their platform at all. | ||
if (!availablePlatforms.includes(platform)) { | ||
console.error(); | ||
console.error('Turborepo does not presently support your platform.'); | ||
process.exit(1); | ||
} else if (!availableArchs.includes(resolvedArch)) { | ||
if (availablePlatforms.includes(platform)) { | ||
console.error(); | ||
console.error('Turborepo supports your platform, but does not support your processor architecture.'); | ||
process.exit(1); | ||
} else { | ||
console.error(); | ||
console.error('Turborepo does not either of your platform or processor architecture.'); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
if (correctBinary !== null) { | ||
console.error(); | ||
console.error('***'); | ||
console.error(); | ||
console.error(`We were not able to find the binary at:\n${correctBinary}`); | ||
console.error(); | ||
console.error(`We looked for it at:`); | ||
console.error(require.resolve.paths(correctBinary).join('\n')); | ||
} | ||
if (alternateBinary !== null) { | ||
console.error(); | ||
console.error('***'); | ||
console.error(); | ||
console.error(`Your platform (${platform}) can sometimes run x86 under emulation.`); | ||
console.error(`We did not find a possibly-compatible binary at:\n${alternateBinary}`); | ||
console.error(); | ||
console.error(`We looked for it at:`); | ||
console.error(require.resolve.paths(alternateBinary).join('\n')); | ||
} | ||
|
||
// Investigate other failure modes. | ||
|
||
// Has the wrong platform's binaries available. | ||
const availableBinaries = availablePlatforms.map(platform => availableArchs.map(arch => `turbo-${platform}-${arch}/bin/turbo${platform === 'windows' ? '.exe': ''}`)).flat(); | ||
const definitelyWrongBinaries = availableBinaries.filter(binary => binary !== correctBinary || binary !== correctBinary);; | ||
const otherInstalled = definitelyWrongBinaries.filter(binaryPath => { | ||
try { | ||
return require.resolve(binaryPath); | ||
} catch (e) {} | ||
}); | ||
|
||
console.error(); | ||
console.error('***'); | ||
console.error(); | ||
|
||
if (otherInstalled.length > 0) { | ||
console.error('Turborepo checked to see if binaries for another platform are installed.'); | ||
console.error('This typically indicates an error in sharing of pre-resolved node_modules across platforms.'); | ||
console.error('One common reason for this is copying files to Docker.'); | ||
console.error(); | ||
console.error(`We found these unnecessary binaries:`); | ||
console.error(otherInstalled.join('\n')); | ||
} else { | ||
console.error(`We did not find any binaries on this system.`); | ||
console.error(`This can happen if you run installation with the --no-optional flag.`); | ||
} | ||
|
||
// Check to see if we have partially-populated dependencies in the npm lockfile. | ||
const MAX_LOOKUPS = 10; | ||
const availablePackages = availablePlatforms.map(platform => availableArchs.map(arch => `turbo-${platform}-${arch}`)).flat(); | ||
|
||
try { | ||
// Attempt to find project root. | ||
const selfPath = require.resolve('turbo/package'); | ||
|
||
let previous = null; | ||
let current = path.join(selfPath, '..', '..', 'package-lock.json'); | ||
|
||
for (let i = 0; previous !== current && i < MAX_LOOKUPS; i++) { | ||
try { | ||
const lockfile = fs.readFileSync(current); | ||
const parsedLockfile = JSON.parse(lockfile); | ||
|
||
// If we don't show up in the lockfile it's the wrong lockfile. | ||
if (parsedLockfile?.dependencies?.turbo) { | ||
// Check to see if all of `turbo-<PLATFORM>-<ARCH>` is included. | ||
const hasAllPackages = Object.keys(parsedLockfile?.dependencies ?? {}).filter(dependency => availablePackages.includes(dependency)).length === availablePackages.length; | ||
nathanhammond marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!hasAllPackages) { | ||
console.error(); | ||
console.error('***'); | ||
console.error(); | ||
console.error(`Turborepo detected that your lockfile (${current}) does not enumerate all available platforms.`); | ||
console.error('This is likely a consequence of an npm issue: https://github.com/npm/cli/issues/4828.'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only shows up when we detect the issue so I think it's worthwhile to include. |
||
|
||
// Let's build their repair command: | ||
let version = ''; | ||
let environment = ''; | ||
if (parsedLockfile?.packages[""]?.dependencies?.turbo) { | ||
version = `@${parsedLockfile.packages[""].dependencies.turbo}`; | ||
environment = ' --save-prod'; | ||
} else if (parsedLockfile?.packages[""]?.devDependencies?.turbo) { | ||
version = `@${parsedLockfile.packages[""].devDependencies.turbo}`; | ||
environment = ' --save-dev'; | ||
} else if (parsedLockfile?.packages[""]?.optionalDependencies?.turbo) { | ||
version = `@${parsedLockfile.packages[""].optionalDependencies.turbo}`; | ||
environment = ' --save-optional'; | ||
} | ||
nathanhammond marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
console.error(); | ||
console.error('To resolve this issue for your repository, run:'); | ||
console.error(`npm install turbo${version} --package-lock-only${environment} && npm install`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @chris-olszewski This is the magic command that fixes things without There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😮 |
||
console.error(); | ||
console.error(`You will need to commit the updated lockfile.`); | ||
} | ||
break; | ||
} | ||
break; | ||
} catch (e) {} | ||
|
||
let next = path.join(current, '..', '..', 'package-lock.json'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No-dependencies |
||
previous = current; | ||
current = next; | ||
} | ||
} catch (e) {} | ||
|
||
console.error(); | ||
console.error('***'); | ||
console.error(); | ||
console.error(`If you believe this is an error, please include this message in your report.`); | ||
|
||
process.exit(1); | ||
} | ||
|
||
// Run the binary we got. | ||
try { | ||
require("child_process").execFileSync( | ||
generateBinPath(), | ||
getBinaryPath(), | ||
process.argv.slice(2), | ||
{ stdio: "inherit" } | ||
); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left these in as configuration variables; but we can actually hardcode the decisions here.