sindresorhus / global-directory

Get the directory of globally installed packages and binaries
MIT License
75 stars 10 forks source link

Doesn't seem to work with Homebrew #5

Closed boneskull closed 5 years ago

boneskull commented 6 years ago

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 commented 6 years ago

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

sindresorhus commented 6 years ago

Sure. PR welcome :)

vladimyr commented 5 years ago

Coming late to the party; better late than never :man_dancing: :wink:

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 :bulb: While we are here we could fix #4 too.
  2. reading local npm config ~/.npmrc
  3. determine global npm config location: a) read PREFIX env variable :bulb: While we are here we could fix #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 :warning: ← 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 :wink: 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) :disappointed:

  2. 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 :thinking:

@boneskull also suggested following: https://github.com/sindresorhus/global-dirs/pull/6#issuecomment-444750246 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) :rotating_light:
vladimyr fails because /usr/local/lib/node_modules/npm/npmrc is harcoded i.e. brew prefix /usr/local is hardcoded/assumed :rotating_light:
  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 :rotating_light:
vladimyr still works because path to global npmrc is hardcoded instead :white_check_mark:

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 :pray:); yet I just did it just to do this little research :upside_down_face:

vladimyr commented 5 years ago

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 commented 5 years ago

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

byCedric commented 5 years ago

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

vladimyr commented 5 years ago

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 commented 5 years ago

Fixed by #6