Skip to content
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

Merged
merged 5 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions packages/turbo/.dev-mode

This file was deleted.

251 changes: 249 additions & 2 deletions packages/turbo/bin/turbo
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;
Comment on lines +12 to +16
Copy link
Contributor Author

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.


// 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 npm to do the right thing when looking at the package.json for turbo, clean up in the event of failure, etc.

}

// 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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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}`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By rule we should be able to require.resolve() the platform-specific package from within the turbo directory, so we do that. (1:1 with the existing behavior.)

} 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 arm64 and x64 versions of Node during upgrade.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turborepo failed to start.

Turborepo detected that you are running:
darwin arm64

***

We were not able to find the binary at:
turbo-darwin-arm64/bin/turbo

We looked for it at:
/Users/nathanhammond/repos/triage/test-npm-lockfile/node_modules/turbo/bin/node_modules
/Users/nathanhammond/repos/triage/test-npm-lockfile/node_modules/turbo/node_modules
/Users/nathanhammond/repos/triage/test-npm-lockfile/node_modules
/Users/nathanhammond/repos/triage/node_modules
/Users/nathanhammond/repos/node_modules
/Users/nathanhammond/node_modules
/Users/node_modules
/node_modules
/Users/nathanhammond/.node_modules
/Users/nathanhammond/.node_libraries
/Users/nathanhammond/.nvm/versions/node/v18.15.0/lib/node

***

Your platform (darwin) can sometimes run x86 under emulation.
We did not find a possibly-compatible binary at:
turbo-darwin-64/bin/turbo

We looked for it at:
/Users/nathanhammond/repos/triage/test-npm-lockfile/node_modules/turbo/bin/node_modules
/Users/nathanhammond/repos/triage/test-npm-lockfile/node_modules/turbo/node_modules
/Users/nathanhammond/repos/triage/test-npm-lockfile/node_modules
/Users/nathanhammond/repos/triage/node_modules
/Users/nathanhammond/repos/node_modules
/Users/nathanhammond/node_modules
/Users/node_modules
/node_modules
/Users/nathanhammond/.node_modules
/Users/nathanhammond/.node_libraries
/Users/nathanhammond/.nvm/versions/node/v18.15.0/lib/node

***

We did not find any binaries on this system.
This can happen if you run installation with the --no-optional flag.

***

Turborepo detected that your lockfile (/Users/nathanhammond/repos/triage/test-npm-lockfile/package-lock.json) does not enumerate all available platforms.
This is likely a consequence of an npm issue: https://github.com/npm/cli/issues/4828.

To resolve this issue for your repository, run:
npm install turbo@^1.10.12 --package-lock-only --save-dev && npm install

You will need to commit the updated lockfile.

***

If you believe this is an error, please include this message in your report.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one other branch not represented in that output:

Turborepo checked to see if binaries for another platform are installed.
This typically indicates an error in sharing of pre-resolved node_modules across platforms.
One common reason for this is copying files to Docker.

We found these unnecessary binaries:
turbo-windows-64/bin/turbo.exe

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.');
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chris-olszewski This is the magic command that fixes things without rm -rf.

Copy link
Member

Choose a reason for hiding this comment

The 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');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No-dependencies find-up.

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" }
);
Expand Down
Loading
Loading