Packaging:Node.js

From FedoraProject

Revision as of 17:20, 8 March 2014 by Spot (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Contents

Naming Guidelines

  • The name of a Node.js extension/library package must start with nodejs- then the upstream name or name used in the npm registry. For example: nodejs-foomodule. While it is uncommon for a package's name to contain node, if it does, you should still add the nodejs prefix. For instance, the npm registry contains a uuid and a node-uuid module, which would need to be named nodejs-uuid and nodejs-node-uuid, repsectively.
  • Application packages that mainly provide tools (as opposed to libraries) that happen to be written for Node.js must follow the general naming guidelines instead.

BuildRequires

To build a package containing pure JavaScript node modules, you need to have:

BuildRequires: nodejs-packaging

Additional BuildRequires are necessary for native modules. See #Building native modules with node-gyp for more information.

Macros

In Fedora 18 and later, as well as EPEL 6, the following macros are defined for you:

Macro Normal Definition Notes
__nodejs %{_bindir}/node The Node.js interpreter
nodejs_version e.g. 0.9.5 The currently installed version of Node.js.
nodejs_sitelib %{_prefix}/lib/node_modules Where Node.js modules written purely in JavaScript are installed
nodejs_sitearch %{_prefix}/lib/node_modules Where native C++ Node.js modules are installed
nodejs_symlink_deps %{_prefix}/lib/rpm/nodejs-symlink-deps See #Symlinking Dependencies below.
nodejs_fixdep %{_prefix}/lib/rpm/nodejs-fixdep See #Correcting Dependencies
nodejs_arches %{ix86} x86_64 %{arm} See #ExclusiveArch. This macro is provided by redhat-rpm-config in F19+ so it works with Koji properly.
nodejs_default_filter %global __provides_exclude_from ^%{nodejs_sitearch}/.*\\.node$ Filters unwanted provides from native modules. See #Filtering Unwanted Provides below.
Note.png
Node does not support multilib.
Since npm and the node interpreter have no knowledge of multilib, %nodejs_sitelib and %nodejs_sitearch currently point to the same directory. The split is reserved in the event that the interpreter gains multiarch support or pure JavaScript modules move to /usr/share.

These macros are provided by the nodejs-packaging package.

During %install or when listing %files you can use the %nodejs_sitelib or %nodejs_sitearch macro to specify where the installed modules are to be found. For instance:

%files
# A pure JavaScript node module
%{nodejs_sitelib}/foomodule/
# A native node module
%{nodejs_sitearch}/barmodule/

Using this macro instead of hardcoding the directory in the specfile ensures your spec remains compatible with the installed Node.js version even if the directory structure changes radically (for instance, if %nodejs_sitelib moves into %{_datadir}).

Using tarballs from the npm registry

The canonical method for shipping most node modules is tarballs from the npm registry. The Source0 for such modules should be of the form http://registry.npmjs.org/<modulename>/-/<modulename>-<version>.tgz. For instance:

Source0:  http://registry.npmjs.org/npm/-/npm-1.1.70.tgz

This method should be preferred to using checkouts from git or automatically generated tarballs from GitHub.

These tarballs store the contents of the module inside a package directory, so every package using these tarballs should use the following in %prep:

%prep
%setup -q -n package
Idea.png
Verifying hashes
The SHA1SUM of tarballs delivered by the npm registry is available as the dist['shasum'] JSON property of the npm metadata for the module, which can be obtained by visiting http://registry.npmjs.org/<modulename>/<version>

ExclusiveArch

The V8 JavaScript runtime used by Node.js uses a Just-in-Time (JIT) compiler that is specially tuned to individual architectures, and must be manually ported to any new architectures that wish to support it. Node.js packages must include an ExclusiveArch line that restricts them to only these architectures.

In Fedora 19 and later, the %{nodejs_arches} macro is provided by the redhat-rpm-config package to make this easy, so pure JavaScript modules must use:

ExclusiveArch: %{nodejs_arches} noarch

In Fedora 18 and EPEL, the architectures must be manually specified, so pure JavaScript modules must use:

ExclusiveArch: %{ix86} x86_64 %{arm} noarch

Native (binary) modules must omit noarch and list only %{nodejs_arches} or the list of architectures as appropriate.

Installing Modules

Most node modules do not contain their own install mechanism. Instead they rely on npm to do it for them. npm must not be used to install modules in Fedora packages, since it usually requires network access, always tries to bundle libraries, and installs files in a manner inconsistent with the Filesystem Hierarchy Standard (FHS).

Instead, install files in their proper place manually using install or cp. Most files should be installed in %{nodejs_sitelib}/<npm module name> but documentation should be installed via %doc. In the event that the module ships arch independent content other than JavaScript code, that content should be installed in %{_datadir} and the module should be patched to cope with that.

Client-side JavaScript

Many node modules contain JavaScript that can be used both client-side and server-side and it is sometimes hard to identify code intended only for use in the browser. Since there are no current packaging guidelines for client-side JavaScript and bundling of such code is currently permitted in Fedora, it is currently permissible for client-side JavaScript to be bundled with nodejs modules in %{nodejs_sitelib}.

Note.png
These exceptions will probably go away.
With the introduction of Node.js into Fedora it is likely that standards for client-side JavaScript will be established and the bundling exception for client-side JavaScript will be removed. Many client-side JavaScript technologies use node in some way or use node modules as the canonical method of distribution (sometimes alongside code intended to be used with node) so such guidelines must certainly take Node.js into account. At this time, node module packages will be expected to conform to these guidelines, possibly shipping the client-side and server-side portions separately.

Automatic Requires and Provides

The nodejs package includes an automatic Requires and Provides generator that automatically adds versioned dependencies based on the information provided in a module's package.json file. Additional Requires are added to native (binary) modules to protect against ABI breaks in Node or the V8 JavaScript runtime.

Warning (medium size).png
Additonal action required for EPEL
The version of RPM included with RHEL does not support automatically invoking dependency generators as in Fedora. To invoke the dependency generator in EPEL packages, add %{?nodejs_find_provides_and_requires} to the top of the package's spec file.

Provides

It also adds virtual provides in the form npm(<module name>) to identify modules listed in the npm registry (the module is listed at npmjs.org) . If a module is not listed in the npm registry, it must not provide this. Modules that aren't listed in the npm registry should set private to true in their package.json file. If not, you must patch package.json to include that.

Correcting Dependencies

Occasionally the dependencies listed in package.json are inaccurate. For instance, the module may work with a newer version of a dependency than the one explictly listed in the package.json file. To correct this, use the %nodejs_fixdep RPM macro. This macro should be used in %prep and will patch package.json to contain the correct dependency information.

Warning (medium size).png
Always fix package.json
RPM macros like %nodejs_symlink_deps rely on accurate information in package.json to work properly, as does the Node.js interpreter and npm at runtime.

To convert any dependency to not list a specific version, just call %nodejs_fixdep with the npm module name of the dependency. This changes the version in package.json to *. (Or adds one if it wasn't already listed.) For example:

%prep
%setup -q -n package
%nodejs_fixdep foomodule

You can also specify a version:

%prep
%setup -q -n package
%nodejs_fixdep foomodule '>2.0'

The second argument to %nodejs_fixdep must be a valid package.json version specifier as explained in `man npm json`.

You can also remove a dependency:

%prep
%setup -q -n package
%nodejs_fixdep -r foomodule
Important.png
Notify upstream
You should always send patches or bugs upstream when correcting dependencies for packages.

Symlinking Dependencies

Node.js and npm require that dependencies explicitly be included or linked into a node_modules directory inside the module directory. To make this easier, a %nodejs_symlink_deps macro is provided and will automatically create a node_modules tree with symlinks for each dependency listed in package.json. This macro should be called in the %install section of every Node.js module package.

Note.png
Include even in modules without dependencies
While it isn't strictly necessary in modules that don't have dependencies, include it anyway so you don't have to worry about adding it if the package adds dependencies in the future.

 %check

For convienence, %nodejs_symlink_deps also accepts a --check argument, which will make it operate in the current working directory instead of the buildroot. You can use this in the %check section of RPM spec files to make dependencies available for running tests. When this argument is used, development dependencies as listed in the "devDependencies" key in package.json are also linked.

Removing bundled modules

Some node modules contain copies of other libraries in their source tree. You must remove these in %prep. Simply running rm -rf node_modules is sufficient for most modules.

%nodejs_symlink_deps outputs a warning when a node_modules directory already exists in the %buildroot, and will fail if it cannot create a symlink because a directory for it already exists.

Building native modules with node-gyp

Most native modules use the node-gyp tool to build themselves, which configures and uses the gyp build framework to build addon modules that can interact with Node.js and the V8 JavaScript interpreter used by it.

The WAF build framework has been abandoned by upstream and is not supported in Fedora.

BuildRequires

To build a native module designed to be built with node-gyp, add BuildRequires: node-gyp, along with BuildRequires: nodejs-devel and -devel packages for any shared libraries needed by the module.

 %build

Some native modules have Makefiles or other build processes that handle any special needs that module has, such as linking to system versions of dependencies. If present, these should be used. Check the module's package.json file for information about what command npm will run to build these modules.

Most modules use vanilla node-gyp, and may not have build instructions in package.json. To build these, simply use the following:

%build
export CXXFLAGS="%{optflags}"
node-gyp rebuild

Note that some modules may specify something like node-gyp configure && node-gyp build. This is equivalent to node-gyp rebuild.

 %install

node-gyp creates a shared object file with the extension .node as part of its build process, which should be located in build/Release. This file may be used as the main entry point for the library, or is utilized by JavaScript wrapper code included with the module.

Idea.png
Identifying whether a module contains JavaScript wrapper code
The easiest way to identify whether a module contains JavaScript wrapper code is simply to check for the existence of JavaScript (.js) files in the tarball that are used in a library context (i.e. make sure they aren't just used for tests or other ancillary purposes). You can also check if the main entrypoint is defined in package.json in the main property.

If the shared object is used as the main entry point, it should be installed at %{nodejs_sitelib}/<module name>/index.node. The `require()` function will automatically load this if there is no corresponding `index.js` or entry point defined in package.json to override it. For example:

%install
mkdir -p %{buildroot}%{nodejs_sitelib}/foomodule
cp -p build/Release/index.node package.json %{buildroot}%{nodejs_sitelib}/foomodule/

If the shared object is called by JavaScript wrapper code, the situation is slightly more complicated.

If the module uses the npm bindings module, the shared object file should be installed in %{nodejs_sitelib}/<module name>/build/<module name>.node, which is at the top of bindings' search path and where node-gyp usually creates a symlink to wherever the real shared object file exists. For example:

%install
mkdir -p %{buildroot}%{nodejs_sitelib}/foomodule/build
cp -p package.json wrapper.js %{buildroot}%{nodejs_sitelib}/foomodule/
cp -p build/Release/foomodule.node %{buildroot}%{nodejs_sitelib}/foomodule/build/

If the module hardcodes build/Release/<module name>.node, the module should be patched to use build/<module name>.node instead, and upstream should be advised that they should use the bindings module, because their module could break when users use debug builds of node.

If the module uses it's own Makefiles to locate the shared object file(s) to a specific location, then those files should installed in that location.

Note.png
Don't install unnecessary development files
npm will leave C++ source files, object (.o) files, etc. in the module directory after installing it. These files must not be installed in Fedora packages.

Dealing with Bundled Libraries

Many native modules contain bundled copies of libraries already present in Fedora. You must build against the system version of these libraries. For more information, see Packaging:No Bundled Libraries.

The Fedora version of node-gyp will handle the fact that shared versions of libuv, v8, and http_parser without modification to the module, since node-gyp always unconditionally configures these libraries. However, some modules may rely on other libraries bundled with node, such as openssl or c-ares. These may need to be patched to use the system headers for these libraries.

Filtering Unwanted Provides

RPM automatically adds some unwanted virtual provides to the shared object files included with native modules. To remove them, add %{?nodejs_default_filter} to the top of the package's spec file. For more information, see Packaging:AutoProvidesAndRequiresFiltering.

Handling Multiple Version Scenarios

Occasionally, there may be an update to a Node.js module that involves backwards-incompatible changes. In such situations, it may be necessary to ship a compatibility package with the older version to allow time for dependencies to migrate to the new version. The Requires generator and %nodejs_symlink_deps contain logic that handles such situations transparently for packages that depend on these modules. No special action is required for other modules that depend on multiply-versioned modules that already exist.

Warning (medium size).png
This only works with the major version
The semantic versioning specification used by npm requires that the major version be incremented when backwards-incompatible changes are introduced. Therefore, Fedora currently only supports multiply-versioned packages where the major version is different. If upstream introduces breaking changes in a minor release, please notify them that they are violating npm's conventions and ask that they bump the major version.

However, the actual multiply-versioned packages require special attention in order for this to work. To implement this, first contact the Node.js SIG mailing list, explain the situation, and include the npm module name of the affected package. The nodejs-packaging maintainers will then add it to the /usr/share/node/multiver_modules file so the package is treated specially by %nodejs_symlink_deps.

Then, in both the original package and the compatibility package, change the %install section to install the module into a versioned directory in %{nodejs_sitelib} of the form <npm_name>@<major_version>. For instance, the 2.x uglify-js module should install into uglify-js@2.

Note.png
What's with the @?
npm uses the @-sign to differentiate versions in its commands. For instance, npm install uglify-js@1. Replicating this in the directory structure ensures commands like npm link uglify-js@1 will work similarly to the way npm install does.

One of the packages must additionally provide a symlink from the usual location to the versioned location described above. For instance, %{nodejs_sitelib}/uglify-js would point to %{nodejs_sitelib}/uglify-js@2 in the previous example. Typically the newest package should provide this symlink, but it might be prudent for the older version to provide it instead when introducing such a package into an existing Fedora release, so as not to break dependent packages. The symlink can then be migrated to the newest version in the next Fedora release.

Finally, any packages that depend on a version that does not provide the aforementioned symlink from the base name to the versioned directory must be rebuilt in order to work properly. To obtain a list of potentially affected packages, run reqoquery --whatrequires 'npm(module_name)' or npm view module_name dependencies. Please coordinate with the Node.js SIG if any rebuilds are necessary.