Skip to content
Snippets Groups Projects
Commit bc98fd37 authored by Christian Moritz's avatar Christian Moritz Committed by Dominyk Tiller
Browse files

Language::Node.npm_install_args: add helper method (#37)

* add Language::Node helper module

This adds a language module for Node module based formulas.
It contains the 2 public methods `std_npm_install_args(libexec)` and
`local_npm_install_args`:

* `std_npm_install_args` is intended to be used in formulas for
  standard node modules and returns `npm install` args for a global
  style module installation to libexec.
* `local_npm_install_args` is for formulas, in which the `npm install`
  step is only one of multiple parts of the installation process and
  returns `npm install` args for a default local installation in place.

Both methods have in common, that they are

* making sure that a working copy of npm and node-gyp from node's
  libexec is prepended to the PATH (to not rely of a user managed npm)
* seting the npm cache to HOMEBREW_CACHE/npm, which fixes issues caused
  by overriding $HOME resulting in long install times + high disk usage
  (see https://github.com/Homebrew/brew/pull/37#issuecomment-208840366)

* audit: update npm install check for Language::Node

* cleanup: remove npm_cache too

* doc: add Node-for-Formula-Authors.md
parent 05efd70d
No related branches found
No related tags found
No related merge requests found
......@@ -50,7 +50,7 @@ module Homebrew
cleanup_path(path) { path.unlink }
next
end
if path.basename.to_s == "java_cache" && path.directory?
if %w[java_cache npm_cache].include?(path.basename.to_s) && path.directory?
cleanup_path(path) { FileUtils.rm_rf path }
next
end
......
......@@ -656,13 +656,6 @@ class FormulaAuditor
if text =~ /def plist/ && text !~ /plist_options/
problem "Please set plist_options when using a formula-defined plist."
end
if text =~ /system "npm", "install"/ && text !~ %r[opt_libexec\}/npm/bin] && formula.name !~ /^kibana(\d{2})?$/
need_npm = "\#{Formula[\"node\"].opt_libexec\}/npm/bin"
problem <<-EOS.undent
Please add ENV.prepend_path \"PATH\", \"#{need_npm}"\ to def install
EOS
end
end
def audit_line(line, lineno)
......@@ -885,6 +878,10 @@ class FormulaAuditor
problem "Use `assert_match` instead of `assert ...include?`"
end
if line =~ /system "npm", "install"/ && line !~ /Language::Node/
problem "Use Language::Node for npm install args"
end
if @strict
if line =~ /system (["'][^"' ]*(?:\s[^"' ]*)+["'])/
bad_system = $1
......
module Language
module Node
def self.npm_cache_config
"cache=#{HOMEBREW_CACHE}/npm_cache\n"
end
def self.setup_npm_environment
npmrc = Pathname.new("#{ENV["HOME"]}/.npmrc")
# only run setup_npm_environment once per formula
return if npmrc.exist?
# explicitly set npm's cache path to HOMEBREW_CACHE/npm_cache to fix
# issues caused by overriding $HOME (long build times, high disk usage)
# https://github.com/Homebrew/brew/pull/37#issuecomment-208840366
npmrc.write npm_cache_config
# explicitly use our npm and node-gyp executables instead of the user
# managed ones in HOMEBREW_PREFIX/lib/node_modules which might be broken
ENV.prepend_path "PATH", Formula["node"].opt_libexec/"npm/bin"
end
def self.std_npm_install_args(libexec)
setup_npm_environment
# tell npm to not install .brew_home by adding it to the .npmignore file
# (or creating a new one if no .npmignore file already exists)
open(".npmignore", "a") { |f| f.write( "\n.brew_home\n") }
# npm install args for global style module format installed into libexec
["--verbose", "--global", "--prefix=#{libexec}", "."]
end
def self.local_npm_install_args
setup_npm_environment
# npm install args for local style module format
["--verbose"]
end
end
end
......@@ -73,4 +73,11 @@ class CleanupTests < Homebrew::TestCase
shutup { Homebrew::Cleanup.cleanup_cache }
refute_predicate java_cache, :exist?
end
def test_cleanup_cache_npm_cache
npm_cache = (HOMEBREW_CACHE/"npm_cache")
npm_cache.mkpath
shutup { Homebrew::Cleanup.cleanup_cache }
refute_predicate npm_cache, :exist?
end
end
# Node for formula authors
This document explains how to successfully use Node and npm in a Node module based Homebrew formula.
# Running `npm install`
Homebrew provides two helper methods in a `Language::Node` module, `std_npm_install_args` and `local_npm_install_args`. They both set up the correct environment for npm and return arguments for `npm install` for their specific use cases. Please use them instead of invoking `npm install` explicitly. The syntax for a standard Node module installation is:
```ruby
system "npm", "install", *Language::Node.std_npm_install_args(libexec)
```
where `libexec` is the destination prefix (usually the `libexec` variable).
# Download URL
If the Node module is also available on the npm registry, we prefer npm hosted release tarballs over GitHub (or elsewhere) hosted source tarballs. The advantages of these tarballs are that they doesn't include the files from the `.npmignore` (such as tests) resulting in a smaller download size and that a possibly transpilation step is already done (e.g. no need to compile CoffeeScript files as a build step).
The npm registry URLs have usually the format of:
```
https://registry.npmjs.org/<name>/-/<name>-<version>.tgz
```
Alternatively you could curl the JSON at `https://registry.npmjs.org/<name>` and look for the value of `versions[<version>].dist.tarball` for the correct tarball URL.
# Dependencies
Node modules, which are compatible with the latest Node version should declare a dependencies on the `node` formula.
```ruby
depends_on "node"
```
If your formula requires to be executed with an older Node version you must vendor this older Node version as done in the [`kibana` formula](https://github.com/Homebrew/homebrew-core/blob/c6202f91a129e2f994d904f299a308cc6fbd58e5/Formula/kibana.rb).
### Special requirements for native addons
If your node module is a native addon or has a native addon somewhere in it's dependency tree you have to declare an additional dependency. Since the compilation of the native addon results in a invocation of `node-gyp` we need an additional build time dependency on `:python` (because gyp depends on Python 2.7).
```ruby
depends_on :python => :build
```
Please also note, that such a formula would only be compatible with the same Node major version it originally was compiled with. This means that we need to revision every formula with a Node native addon with every major version bump of the `node` formula. To make sure we don't overlook your formula on a Node major version bump, write a meaningful test which would fail in such a case (invoked with an ABI incompatible Node version).
# Installation
Node modules should be installed to `libexec`. This prevents the Node modules from contaminating the global `node_modules`, which is important so that npm doesn't try to manage Homebrew-installed Node modules.
In the following we distinguish between 2 type of Node module using formulae:
* formulae for standard Node modules compatible with npm's global module format which should use [`std_npm_install_args`](#installing-global-style-modules-with-std_npm_install_args-to-libexec) (like [`azure-cli`](https://github.com/Homebrew/homebrew-core/blob/d93fe9ba3bcc9071b699c8da4e7d733518d3337e/Formula/azure-cli.rb) or [`autocode`](https://github.com/Homebrew/homebrew-core/blob/1a670a6269e1e07f86683c2d164977c9bd8a3fb6/Formula/autocode.rb)) and
* formulae were the `npm install` step is only one of multiple not exclusively Node related install steps (not compatible with npm's global module format) which have to use [`local_npm_install_args`](#installing-module-dependencies-locally-with-local_npm_install_args) (like [`elixirscript`](https://github.com/Homebrew/homebrew-core/blob/ec1e40d37e81af63122a354f0101c377f6a4e66d/Formula/elixirscript.rb) or [`kibana`](https://github.com/Homebrew/homebrew-core/blob/c6202f91a129e2f994d904f299a308cc6fbd58e5/Formula/kibana.rb))
Both methods have in common, that they are setting the correct environment for using npm inside Homebrew up and returning the arguments for invoking `npm install` for their specific use cases. This includes fixing an important edge case with the npm cache (Caused by Homebrew's redirection of `$HOME` during the build and test process) by using our own custom `npm_cache` inside `HOMEBREW_CACHE`, which would otherwise result in very long build times and high disk space usage.
To use them you have to require the Node language module at the beginning of your formula file with:
```ruby
require "language/node"
```
### Installing global style modules with `std_npm_install_args` to libexec
In your formula's `install` method, simply cd to the top level of your Node module if necessary and than use `system` to invoke `npm install` with `Language::Node.std_npm_install_args` like:
```ruby
system "npm", "install", *Language::Node.std_npm_install_args(libexec)
```
This will install your Node module in npm's global module style with a custom prefix to `libexec`. All your modules executable will be automatically resolved by npm into `libexec/"bin"` for you, which is not symlinked into Homebrew's prefix. We need to make sure these are installed. Do this with we need to symlink all executables to `bin` with:
```ruby
bin.install_symlink Dir["#{libexec}/bin/*"]
```
### Installing module dependencies locally with `local_npm_install_args`
In your formula's `install` method, do any installation steps which need to be done before the `npm install` step and than cd to the top level of the included Node module. Then, use `system` with `Language::Node.local_npm_install_args` to invoke `npm install` like:
```ruby
system "npm", "install", *Language::Node.local_npm_install_args
```
This will install all of your Node modules dependencies to your local build path. You can now continue with your build steps and take care of the installation into the Homebrew `prefix` by your own, following the [general Homebrew formula instructions](https://github.com/Homebrew/brew/blob/master/share/doc/homebrew/Formula-Cookbook.md).
# Example
Installing a standard Node module based formula would look like this:
```ruby
require "language/node"
class Foo < Formula
desc "..."
homepage "..."
url "https://registry.npmjs.org/foo/-/foo-1.4.2.tgz"
sha256 "..."
depends_on "node"
# uncomment if there is a native addon inside the dependency tree
# depends_on :python => :build
def install
system "npm", "install", *Language::Node.std_npm_install_args(libexec)
bin.install_symlink Dir["#{libexec}/bin/*"]
end
test do
# add a meaningful test here
end
end
```
For examples using the `local_npm_install_args` method look at the [`elixirscript`](https://github.com/Homebrew/homebrew-core/blob/ec1e40d37e81af63122a354f0101c377f6a4e66d/Formula/elixirscript.rb) or [`kibana`](https://github.com/Homebrew/homebrew-core/blob/c6202f91a129e2f994d904f299a308cc6fbd58e5/Formula/kibana.rb) formula.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment