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

Doesn't seem to work with Homebrew #5

Closed
boneskull opened this issue Nov 30, 2018 · 8 comments
Closed

Doesn't seem to work with Homebrew #5

boneskull opened this issue Nov 30, 2018 · 8 comments

Comments

@boneskull
Copy link
Contributor

I'm not sure when this changed, but when installing node with Homebrew, the npm.packages directory is nonexistent:

$ node -e "console.log(require('global-dirs').npm)"
{ prefix: '/usr/local/Cellar/node/11.3.0_1',
  packages: '/usr/local/Cellar/node/11.3.0_1/lib/node_modules',
  binaries: '/usr/local/Cellar/node/11.3.0_1/bin' }

In fact, the npm prefix seems to be incorrect:

$ npm get prefix
/usr/local

npm lives in /usr/local/bin, and the packages dir should be /usr/local/lib/node_modules. /usr/local/lib/node_modules/npm/npmrc contains a prefix of /usr/local.

node, OTOH, lives in /usr/local/bin but is symlinked into /usr/local/Cellar/node....

This means global-dirs can't reliably use process.execPath to determine where npm lives. However, the /usr/local/bin/node symlink will be present in process.env._ in some shells. Perhaps global-dirs should prefer process.env._ over process.execPath when present?

references: boneskull/create-yo#1

@boneskull
Copy link
Contributor Author

I'm happy to send a PR to that effect if you wish.

@sindresorhus sindresorhus changed the title doesn't seem to work with homebrew Doesn't seem to work with Homebrew Dec 3, 2018
@sindresorhus
Copy link
Owner

Sure. PR welcome :)

@vladimyr
Copy link
Contributor

vladimyr commented Jan 13, 2019

Coming late to the party; better late than never 🕺 😉

Just for sake of clarification:

npm lives in /usr/local/bin, and the packages dir should be /usr/local/lib/node_modules. /usr/local/lib/node_modules/npm/npmrc contains a prefix of /usr/local.

packages & binaries are just computed fields. Source of problem is that global-dirs fails to read prefix from /usr/local/lib/node_modules/npm/npmrc simply because it does not expect npm config to exist there.

So lets deep dive into this issue...

Where is node installed by homebrew located?

node, OTOH, lives in /usr/local/bin but is symlinked into /usr/local/Cellar/node....

Actually it is other way around:

$ which node
/usr/local/bin/node
$ realpath $(which node)
/usr/local/Cellar/node/11.6.0/bin/node

/usr/local/node is a symlink pointing to actual node located inside brew's cellar.

What went wrong?

This means global-dirs can't reliably use process.execPath to determine where npm lives.

process.execPath is just a fallback option if other means of getting npm prefix don't return valid result. Before that following steps are done:

  1. checking PREFIX env variable
    💡 While we are here we could fix Not respecting the NPM_CONFIG_PREFIX env variable #4 too.
  2. reading local npm config ~/.npmrc
  3. determine global npm config location:
    a) read PREFIX env variable
    💡 While we are here we could fix Not respecting the NPM_CONFIG_PREFIX env variable #4 too.
    b) parent dir of node binary on windows
    Example from code: c:\node\node.exe → prefix=c:\node\
    c) fallback to grandparent of node binary elsewhere ⚠️source of error
    Example from code: /usr/local/bin/node → prefix=/usr/local
    d) ...
  4. read prefix from global npm config
  5. ...

Reason why this fails is because node actually lives inside brew's cellar (as explained earlier) so global-dirs sees /usr/local/Cellar/node/<node_version>/bin/node as executable path (process.execPath resolves /usr/local/bin/node back to actual location), grandparent of which is /usr/local/Cellar/node/<node_version> and as we known that's wrong.

About using _ environment variable (possible solution)

However, the /usr/local/bin/node symlink will be present in process.env._ in some shells. Perhaps global-dirs should prefer process.env._ over process.execPath when present?

Quote from man bash:

When  bash invokes an external command, the variable _ is set to the full file
name of the command and passed to that command in its environment.
  1. This is shell specific (not defined by POSIX); yeah bash & zsh support this but:
$ tcsh
% node -e "console.log(process.env._)"
/bin/tcsh

If I remember correctly (too lazy to check) tcsh is default (Free)BSD shell and I don't want BSD folks to get mad at us 😉 Also I'm not sure about other shells like: fish, ksh or dash and I seriously doubt we want enter area of cross-shell compability (testing) 😞

  1. Manual says: set to the full file name of the command. That means I can do following:
$ brew --prefix node
/usr/local/opt/node
$ # this is symlink brew creates pointing to node's location inside its cellar
$ $(brew --prefix node)/bin/node -e "console.log(process.env._)"
/usr/local/opt/node/bin/node

Surely people don't do this regularly but my point is that it is invocation dependent same as you later described in #6:

If, for example, your executable is ava, we can't use process.env._ because it points to /path/to/ava. If we instead ran node node_modules/.bin/ava, then process.env._ would point to node in your PATH.

Changes you proposed in #6 (https://github.com/sindresorhus/global-dirs/pull/6/files#diff-168726dbe96b3ce427e7fedce31bb0bcR25) will fail in case I just described because:

// invoked with `/usr/local/opt/node/bin/node script.js`
// process.env._ → /usr/local/opt/node/bin/node
// path.basename(process.env._) → node

path.dirname(path.dirname(path.basename(process.env._) === 'node' ? process.env._ : process.execPath));
//=> process.dirname(process.dirname(process.env._)) → /usr/local/opt/node

and as we know that's wrong path/prefix.

Alternative solution

Going back to my global-dirs prefix getting steps list - special case before 3.c) could be introduced. The one that would check for existence of /usr/local/lib/node_modules/npm/npmrc and return that path instead.

This seems like magic path but it is actually calculated from: $(brew --prefix)/lib/node_modules/npm/npmrc. File gets created by brew's postinstall script for node formula:
https://github.com/Homebrew/homebrew-core/blob/9495d4020857454285be2ebb0c070edf24d30168/Formula/node.rb#L60-L85
where most interesting lines are:

node_modules = HOMEBREW_PREFIX/"lib/node_modules"
(node_modules/"npm/npmrc").atomic_write("prefix = #{HOMEBREW_PREFIX}\n")

In order to reliably get brew's prefix brew --prefix needs to get executed inside subprocess but I wonder how often people install brew outside of default installation directory 🤔

@boneskull also suggested following: #6 (comment) so lets compare it to my approach:

  1. what happens if user installs homebrew with different prefix?
solution by result
boneskull fails because global npmrc contains evaluated version of prefix = $(brew --prefix) 🚨
vladimyr fails because /usr/local/lib/node_modules/npm/npmrc is harcoded i.e. brew prefix /usr/local is hardcoded/assumed 🚨
  1. what happens if homebrew changes contents of global npmrc file populated by node's formula postinstall step?
solution by result
boneskull fails because npmrc contents/prefix is hardcoded/assumed 🚨
vladimyr still works because path to global npmrc is hardcoded instead ✅

As @boneskull concluded only reliable way to determine prefix in given scenario would require extra I/O but I firmly believe it is safe to assume that brew prefix is always set to /usr/local. I'm eager to hear any possible counterarguments or examples where that assumption is not true.


PS Just for the record I instantly get sick when someone mentions installing node with homebrew (use nvm or n please 🙏); yet I just did it just to do this little research 🙃

@vladimyr
Copy link
Contributor

Quoting myself:

but I firmly believe it is safe to assume that brew prefix is always set to /usr/local. I'm eager to hear any possible counterarguments or examples where that assumption is not true.

Quoting from official homebrew installation docs
From 2nd paragraph describing standard installation procedure:

The standard script installs Homebrew to /usr/local so that
you don’t need sudo when you
brew install. It is a careful script; it can be run even if you have stuff
installed to /usr/local already. It tells you exactly what it will do before
it does it too. And you have to confirm everything it will do before it starts.

Explaning alternative installation methods (alternative prefix):

However do yourself a favour and install to /usr/local. Some things may
not build when installed elsewhere. One of the reasons Homebrew just
works relative to the competition is because we recommend installing
to /usr/local. Pick another prefix at your peril!

If homebrew authors are fine with here be dragons approach when using custom prefix I don't see why global-dirs shouldn't follow. 😉

@sindresorhus
Copy link
Owner

@vladimyr Thanks for the very thorough research. I agree we can just assume /usr/local.

@byCedric
Copy link

I guess PR #6 will fix this issue right? If not, is there anything I can help with?

@vladimyr
Copy link
Contributor

I guess PR #6 will fix this issue right? If not, is there anything I can help with?

Probably but that's completely different pair of shoes from what I proposed here earlier...

@sindresorhus
Copy link
Owner

Fixed by #6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants