Wednesday, March 14, 2018

Building Node.js 8.10.0

This post is more of a continuous effort in order to learn how to build by myself some more up-to-date artifacts, this time Node.js 8.10.0 (LTS), to my preferred software platform: Solaris 11. In particular, I'm still considering Solaris 11.3 GA, which by the time of this writing is about 3 years old with GCC 4.8.2. Solaris 11.4 GA is expected to arrive late this year (2018).

To partially quote Node.js:
 
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. ... .

Node.js is a typical case for a Solaris back-end system, where its cloud-ready infrastructure would be typically very well-suited to the task: SMF, ZFS, zones, higher threading count and advanced networking capabilities.

The version used in this post, Node.js 8.10.0 (LTS), is a March 2018 security update. I've chosen the LTS 8.x series because it's the latest branch that better aligns to the Solaris way-of-things. It's expected to live until Dec 2019 and not beyond because it's aligning to OpenSSL 1.0.2 life-cycle to which it depends.

This post's (at the time of this writing) up-to-date custom build is relevant not only because of the security side, but because the software isn't available in the official repositories and, even if it could be one day, it would be uncertain if the update pace of the support repository would be acceptable. Furthermore, by performing a custom build instead of just downloading pre-built binaries one ensures getting more exactly what's needed and optimized for a particular machine as per specific compiler (CPU) options and source-code modules selection.

Right from the start I was able to build a 64-bits Node.JS 8.10.0 (LTS) in Solaris 11.4 Beta (already available) with GCC 5.5.0, but at first I wasn't able to repeat the task under Solaris 11.3 GA and I was somewhat settled with it as the "build instructions" stated a more updated (4.9.4 - Aug 3, 2016) GCC was required and I only had an almost 3-years old 4.8.2 (from Oct 16, 2013) in Solaris 11.3. Although a newer GCC is generally better, by inspecting the initial build failures in Solaris 11.3 GA I've noticed that there weren't that many changes that could render the task impossible. This motivated me to take some time to further investigate a way of trying to accomplish the task.

The problems with Node.js and Solaris 11.3 were:

  1. Node.js doesn't (quite) follow the GNU build system/automation.
    As in Firefox, it attempts to reinvent the (build) wheel with Python.
    In the GNU make Python is invoked instead of the basic GNU build tools.
        
  2. By default, GCC 4.8.2 doesn't support certain C++11 features and it happens that just one of them is required in the source code of Node.js: std:to_string().
      
  3. There may be some subtle relationship to the fact that I'm targeting 64-bits and the userland in Solaris 11.3 is still 32-bits while in Solaris 11.4 (Beta) it's already 64-bits.

NOTE
I have nothing against Python! On the contrary I embrace evolution and welcome the many advances the industry is attempting in order to leverage scripting and interpreted code in general.
 
Problem #1

By reinventing the wheel one opens up a way to bugs that never happened or were already fixed on the previous way of doing things. This is normal in software development, but in general it's not acceptable in the Solaris-way that strives for stability and endurance.

I've detected and fortunately could work-around the following points:
 
  • There was an issue in one of the Python scripts (make.py).
    I've used the following source-code scannnig strategy:
    $ find . -type f |xargs -l egrep -l CXXFLAGS $1 |sort -u
    In fact it was more like a "left-over" / "unfinished (to-do)" task.
    Hence, there was no easily-found documentation about it.
     
  • There was an unexpected interaction with sudo upon gmake install.
    Yet to investigate: where the root cause resides: Solaris, Node.js, Python?
    Shell environment variables weren't being inherited to root as expected.
    This was masking the work-around for GCC 4.8.2 C++11 shortcoming.

Problem #2

The C++11 support was still incomplete in both 4.8.2 and 4.9.4 versions of GCC. But the point is that in version 4.8.2 part of the C++11 code relevant to Node.js is already there, but not enabled by default for two reasons:

  • The C99 support isn't enabled by default under a C++11 compilation.
    When using -std=gnu++0x, -std=gnu++11 or -std=c++11 one gets no C99.
    This prevents internal code supporting std::to_string() implementation.
      
  • Depending on the platform, fortunately not Solaris, vswprintf() is broken.
    Again, this may disable std::to_string() implementation.
 
Hence, when building on Solaris 11.3, the following work-around must be taken:
 
  1. Define a GCC directive to enable C99 under C++11.  (problem #2)
    The directive in question is _GLIBCXX_USE_C99 and it will be decisive on basic_string.h located in /usr/gcc/4.8/include/c++/4.8.2/bits.
     
  2. Define some undocumented environment variables. (problem #1)  
    They are CFLAGS_host and CXXFLAGS_host.
    Just in case I also define LDFLAGS_host as well.
     
  3. Perform the whole building workflow as root. (problem #1)

For Solaris 11.4 (at least for Beta) none of the above is necessary as the Python-related problems are masked-out (yet still there!) by the newer 5.5.0 GCC version which apparently has no C99 issues anymore.

I'll present my approach here beginning by Solaris 11.3.

$ ./gnu-build-preparation ../source/.../node-v8.10.0.tar.xz

Processing /stage/source/Node.JS/LTS/8.10.0/node-v8.10.0.tar.xz

------------------------------------
App: node
Ver: v8.10.0
------------------------------------

In the process, the following ZFS datasets will be created:

  root/stage/build/node
  root/stage/build/node/node-v8.10.0
  root/stage/build/node/node-v8.10.0-32
  root/stage/build/node/node-v8.10.0-64

  root/stage/prototype/node
  root/stage/prototype/node/node-v8.10.0
  root/stage/prototype/node/node-v8.10.0/32
  root/stage/prototype/node/node-v8.10.0/64

Enter "y" to proceed:
y

Creating root/stage/build/node...
Creating root/stage/build/node/node-v8.10.0...

root/stage/build/node
root/stage/build/node/node-v8.10.0
root/stage/build/node/node-v8.10.0@source
root/stage/build/node/node-v8.10.0-32
root/stage/build/node/node-v8.10.0-32@start
root/stage/build/node/node-v8.10.0-64
root/stage/build/node/node-v8.10.0-64@start

Creating root/stage/prototype/node subtree.

root/stage/prototype/node
root/stage/prototype/node/v8.10.0
root/stage/prototype/node/v8.10.0/32
root/stage/prototype/node/v8.10.0/64

Creating pre-configuration script.


Running a special version of my setenv script will produce:
(special just because I have carved the specific messages below)

$ cd node/node-v8.10.0-64

$ source ../setenv 64

CONFIG_SHELL=/usr/bin/bash

CC=/usr/bin/gcc 

CFLAGS=-m64 -march=core2
CFLAGS_host=-m64 -march=core2

CXX=/usr/bin/g++ 

CXXFLAGS=-m64 -march=core2 -D_GLIBCXX_USE_C99
CXXFLAGS_host=-m64 -march=core2 -D_GLIBCXX_USE_C99

LD=/usr/bin/ld 

LDFLAGS=-m64 -march=core2
LDFLAGS_host=-m64 -march=core2

PATH=/opt/gnu/bin:/usr/gnu/bin:/usr/bin:/usr/sbin

PKG_CONFIG_PATH=

Suggested build sequence (as root!):

# ./configure \
  --prefix=/opt/software/node/8.10.0 \

  --dest-os=solaris
  --dest-cpu=x64

# gmake -j3

For IPS pakcage:


# gmake DESTDIR=/stage/prototype/node/v8.10.0/64 install
 

For immediate use:

# gmake install
# zfs snapshot .../opt/software/node/8.10.0@release

If you don't do as above, after a while you'll get the following C++11 issue:
(except for the paths and the -j3 option which are particular to this post)

... In member function 
'void TraceIC(const char*, Handle, State, State)':
../deps/v8/src/ic/ic.cc:164:29: error:

'to_string' is not a member of 'std'
  ic_info.instance_type = std::to_string(map->instance_type());

                          ^
gmake[1]: *** [/stage/build/node/node-v8.10.0-64/out/Release/obj.host/v8_base/deps/v8/src/ic/ic.o] Error 1
gmake[1]: *** Waiting for unfinished jobs....
rm 10bbd01cccf827b07022e3f06f604b562b72d722.intermediate
gmake[1]: Leaving directory `/stage/build/node/node-v8.10.0-64/out'
gmake: *** [node] Error 2


Now proceeding to the main work-flow:

# cd /stage/build/node/node-v8.10.0-64
 
# ./configure \
    --prefix=/opt/software/node/8.10.0 \
    --dest-os=solaris \
    --dest-cpu=x64
WARNING: C++ compiler too old, need g++ 4.9.4 ...
creating icu_config.gypi
* Using ICU in deps/icu-small
creating icu_config.gypi
{ 'target_defaults': { 'cflags': [],
                       'default_configuration': 'Release',
                       'defines': [],
                       'include_dirs': [],
                       'libraries': []},
  'variables': { 'asan': 0,
                 'coverage': 'false',
                 'debug_devtools': 'node',
                 'debug_http2': 'false',
                 'debug_nghttp2': 'false',
                 'force_dynamic_crt': 0,
                 'gas_version': '2.23',
                 'host_arch': 'ia32',
                 'icu_data_file': 'icudt60l.dat',
                 'icu_data_in': '../../deps/icu-small/.../icudt60l.dat',
                 'icu_endianness': 'l',
                 'icu_gyp_path': 'tools/icu/icu-generic.gyp',
                 'icu_locales': 'en,root',
                 'icu_path': 'deps/icu-small',
                 'icu_small': 'true',
                 'icu_ver_major': '60',
                 'llvm_version': 0,
                 'node_byteorder': 'little',
                 'node_enable_d8': 'false',
                 'node_enable_v8_vtunejit': 'false',
                 'node_install_npm': 'true',
                 'node_module_version': 57,
                 'node_no_browser_globals': 'false',
                 'node_prefix': '/opt/software/node/8.10.0',
                 'node_release_urlbase': '',
                 'node_shared': 'false',
                 'node_shared_cares': 'false',
                 'node_shared_http_parser': 'false',
                 'node_shared_libuv': 'false',
                 'node_shared_nghttp2': 'false',
                 'node_shared_openssl': 'false',
                 'node_shared_zlib': 'false',
                 'node_tag': '',
                 'node_use_bundled_v8': 'true',
                 'node_use_dtrace': 'true',
                 'node_use_etw': 'false',
                 'node_use_lttng': 'false',
                 'node_use_openssl': 'true',
                 'node_use_perfctr': 'false',
                 'node_use_v8_platform': 'true',
                 'node_without_node_options': 'false',
                 'openssl_fips': '',
                 'openssl_no_asm': 0,
                 'shlib_suffix': 'so.57',
                 'target_arch': 'x64',
                 'uv_parent_path': '/deps/uv/',
                 'uv_use_dtrace': 'true',
                 'v8_enable_gdbjit': 0,
                 'v8_enable_i18n_support': 1,
                 'v8_enable_inspector': 1,
                 'v8_no_strict_aliasing': 1,
                 'v8_optimized_debug': 0,
                 'v8_promise_internal_field_count': 1,
                 'v8_random_seed': 0,
                 'v8_trace_maps': 0,
                 'v8_use_snapshot': 'true',
                 'want_separate_host_toolset': 1}}
creating config.gypi
creating config.mk

WARNING: warnings were emitted in the configure phase


# gmake -j3
...

# gmake DESTDIR=/stage/prototype/node/v8.10.0/64 install
...

# gmake install
...

# zfs snapshot -r .../opt/software/node/8.10.0@release

NOTE
The preceeding install on DESTDIR is in preparation to a subsequent IPS package creation not shown here.

Finally, after about half an hour, you can add /opt/software/node/8.10.0/64/bin to PATH and verify that it works "perfectly" ;-) as per the following lines:

$ node -e "console.log( 'Version is ' + process.version )"
Version is v8.10.0

$ node --version
v8.10.0

NOTE
Instead of /opt/software I would rather use /opt/sfw in the future, just because it's shorter and it's more familiar to Solaris admins.
NOTE
The building process is virtually the same for 32-bits artifacts but I don't perform it because I'm exclusively focusing on 64-bits artifacts.
NOTE
I haven't show gmake test. It virtually succeeds except by one test that attempts resizing the TCP window size (even as root). This could be a test bug or incompatibility with Solaris or a matter of setting some specific privilege to the process. But I don't address this topic on this post. The investigation should start by inspecting the test code and then the standard Information Library document: How to Determine Which Privileges a Program Requires.

Now for Solaris 11.4 (Beta) the good news are that the previous Solaris 11.3 work-around are totally unnecessary! You don't need the tweaks in the environment (setenv) nor need to build as root and can use sudo gmake install for the final installation.

Success!