tsy77 / blog

78 stars 2 forks source link

Node.js源码-编译 #6

Open tsy77 opened 6 years ago

tsy77 commented 6 years ago

os:macOS 10.13.4,ide:cLion,node版本:v8.2.1

前言

编译node源码主要有三个步骤

$ ./configure
$ make
$ make install

./configue主要用来生成与操作平台相关的编译配置,比如软件装到哪里、什么参数等信息,执行过后在./out目录生成如下文件:

make指令根据Makefile的配置对node源码进行编译(包括预编译、编译、链接)生成可执行文件,感兴趣的可以参考刨根问底之node-gyp

make install根据配置将其安装到系统路径下,我们一般自己看源码调试是用不上的

编译过程详解

./configue

收集命令行参数

# Options should be in alphabetical order but keep --prefix at the top,
# that's arguably the one people will be looking for most.
parser.add_option('--prefix',
    action='store',
    dest='prefix',
    default='/usr/local',
    help='select the install prefix [default: %default]')

parser.add_option('--coverage',
    action='store_true',
    dest='coverage',
    help='Build node with code coverage enabled')

parser.add_option('--debug',
    action='store_true',
    dest='debug',
    help='also build debug build')

......

(options, args) = parser.parse_args()

收集到的参数是一个map,如下所示:

当然最终版的参数信息原本也会打印出来。

其中要注意的是在调试时别忘了加上prefix和debug。如果不定义prefix的话,执行make install会安装到默认的local/user/目录下;定义debug会按照调试的配置编译,最终会编译到out/Debug目录下(下述./makefile中有描述),同时增加一些配置方便大家调试(打断点等)。

收集编译器和以下library的参数

# Print a warning when the compiler is too old.
check_compiler(output)

# determine the "flavor" (operating system) we're building for,
# leveraging gyp's GetFlavor function
flavor_params = {}
if (options.dest_os):
  flavor_params['flavor'] = options.dest_os
flavor = GetFlavor(flavor_params)

configure_node(output)
configure_library('zlib', output)
configure_library('http_parser', output)
configure_library('libuv', output)
configure_library('libcares', output)
configure_library('nghttp2', output)
# stay backwards compatible with shared cares builds
output['variables']['node_shared_cares'] = \
    output['variables'].pop('node_shared_libcares')
configure_v8(output)
configure_openssl(output)
configure_intl(output)
configure_static(output)
configure_inspector(output)
check_compiler

在这里我们简单看下python是如何检查编译器的

def try_check_compiler(cc, lang):
  try:
    proc = subprocess.Popen(shlex.split(cc) + ['-E', '-P', '-x', lang, '-'],
                            stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  except OSError:
    return (False, False, '', '')

  proc.stdin.write('__clang__ __GNUC__ __GNUC_MINOR__ __GNUC_PATCHLEVEL__ '
                   '__clang_major__ __clang_minor__ __clang_patchlevel__')

  values = (proc.communicate()[0].split() + ['0'] * 7)[0:7]
  is_clang = values[0] == '1'
  gcc_version = '%s.%s.%s' % tuple(values[1:1+3])
  clang_version = '%s.%s.%s' % tuple(values[4:4+3])

  return (True, is_clang, clang_version, gcc_version)

其实就是新开了一个子进程,在其上执行CXX,然后获取版本信息。

CXX = os.environ.get('CXX', 'c++' if sys.platform == 'darwin' else 'g++')

在OSX中,GYP的Makefile底层依赖的的c++,其他操作系统都是g++。

run_gyp

最后执行了run_gyp(gyp_args)

在run_gyp中又做了什么呢?

output_dir = os.path.join(os.path.abspath(node_root), 'out')

def run_gyp(args):
  # GYP bug.
  # On msvs it will crash if it gets an absolute path.
  # On Mac/make it will crash if it doesn't get an absolute path.
  a_path = node_root if sys.platform == 'win32' else os.path.abspath(node_root)
  args.append(os.path.join(a_path, 'node.gyp'))
  common_fn = os.path.join(a_path, 'common.gypi')
  options_fn = os.path.join(a_path, 'config.gypi')
  options_fips_fn = os.path.join(a_path, 'config_fips.gypi')

  if os.path.exists(common_fn):
    args.extend(['-I', common_fn])

  if os.path.exists(options_fn):
    args.extend(['-I', options_fn])

  if os.path.exists(options_fips_fn):
    args.extend(['-I', options_fips_fn])

  args.append('--depth=' + node_root)

  # There's a bug with windows which doesn't allow this feature.
  if sys.platform != 'win32' and 'ninja' not in args:
    # Tell gyp to write the Makefiles into output_dir
    args.extend(['--generator-output', output_dir])

    # Tell make to write its output into the same dir
    args.extend(['-Goutput_dir=' + output_dir])

  args.append('-Dcomponent=static_library')
  args.append('-Dlibrary=static_library')

  # Don't compile with -B and -fuse-ld=, we don't bundle ld.gold.  Can't be
  # set in common.gypi due to how deps/v8/build/toolchain.gypi uses them.
  args.append('-Dlinux_use_bundled_binutils=0')
  args.append('-Dlinux_use_bundled_gold=0')
  args.append('-Dlinux_use_gold_flags=0')

  rc = gyp.main(args)
  if rc != 0:
    print 'Error running GYP'
    sys.exit(rc)

主要做了两件事:

1.收集参数
2.执行gyp.main(args)

比较重要的也是两点:

1.加入node.gyp配置文件
    args.append(os.path.join(a_path, 'node.gyp'))
2.确定了生成目录 generator-output
node.gyp

我们继续深入,看下node.gyp。

node.gyp是一个python的数据结构,打眼看上去似乎很多,容易看着看着就乱了,我们其实可以从target_name入手。这里就不贴代码了,大家感兴趣的可以顺着target_name一个个顺下去。比较重要的是以下几个:

1.node可执行文件
2.定义node_js2c的输出node_javascript.cc
3.node_dtrace动态跟踪框架
4.cctest测试

make

Makefile

执行make的话实际上就是按照当前目录下的makefile执行动作,我们看一下makefile。

ifeq ($(BUILDTYPE),Release)
all: out/Makefile $(NODE_EXE) ## Default target, builds node in out/Release/node.
else
all: out/Makefile $(NODE_EXE) $(NODE_G_EXE)
endif

....

$(NODE_EXE): config.gypi out/Makefile
    $(MAKE) -C out BUILDTYPE=Release V=$(V)
    if [ ! -r $@ -o ! -L $@ ]; then ln -fs out/Release/$(NODE_EXE) $@; fi

$(NODE_G_EXE): config.gypi out/Makefile
    $(MAKE) -C out BUILDTYPE=Debug V=$(V)
    if [ ! -r $@ -o ! -L $@ ]; then ln -fs out/Debug/$(NODE_EXE) $@; fi

如果是debug模式,则多执行了$(NODE_G_EXE),$(NODE_G_EXE)将BUILDTYPE设置为debug,不同之处在于BUILDTYPE=Debugout/Debug/$(NODE_EXE)。随后两个命令都对out/Debug/node做了一个软链,如果是第一次编译,会建立一个软链,链接到node_g。

out/Makefile

我们再接着看out/makefile

执行编译

TOOLSET := host
# Suffix rules, putting all outputs into $(obj).
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.c FORCE_DO_CMD
    @$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cc FORCE_DO_CMD
    @$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cpp FORCE_DO_CMD
    @$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.cxx FORCE_DO_CMD
    @$(call do_cmd,cxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.m FORCE_DO_CMD
    @$(call do_cmd,objc,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.mm FORCE_DO_CMD
    @$(call do_cmd,objcxx,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.S FORCE_DO_CMD
    @$(call do_cmd,cc,1)
$(obj).$(TOOLSET)/%.o: $(srcdir)/%.s FORCE_DO_CMD
    @$(call do_cmd,cc,1)

这里执行do_cmd将所有源文件进行编译。

do_cmd

接下来我们看下do_cmd做了什么?

# do_cmd: run a command via the above cmd_foo names, if necessary.
# Should always run for a given target to handle command-line changes.
# Second argument, if non-zero, makes it do asm/C/C++ dependency munging.
# Third argument, if non-zero, makes it do POSTBUILDS processing.
# Note: We intentionally do NOT call dirx for depfile, since it contains ? for
# spaces already and dirx strips the ? characters.
define do_cmd
$(if $(or $(command_changed),$(prereq_changed)),
  @$(call exact_echo,  $($(quiet)cmd_$(1)))
  @mkdir -p "$(call dirx,$@)" "$(dir $(depfile))"
  $(if $(findstring flock,$(word 2,$(cmd_$1))),
    @$(cmd_$(1))
    @echo "  $(quiet_cmd_$(1)): Finished",
    @$(cmd_$(1))
  )
  @$(call exact_echo,$(call escape_vars,cmd_$(call replace_spaces,$@) := $(cmd_$(1)))) > $(depfile)
  @$(if $(2),$(fixup_dep))
  $(if $(and $(3), $(POSTBUILDS)),
    $(call do_postbuilds)
  )
)
endef

其实就是根据参数(源文件类型),执行不同的指令,比如.cc文件就是利用CXX进行编译。

quiet_cmd_cxx = CXX($(TOOLSET)) $@
cmd_cxx = $(CXX.$(TOOLSET)) $(GYP_CXXFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c -o $@ $<

监听所有.mk文件

ifeq ($(strip $(foreach prefix,$(NO_LOAD),\
    $(findstring $(join ^,$(prefix)),\
                 $(join ^,cctest.target.mk)))),)
  include cctest.target.mk
endif
ifeq ($(strip $(foreach prefix,$(NO_LOAD),\
    $(findstring $(join ^,$(prefix)),\
                 $(join ^,deps/cares/cares.target.mk)))),)
  include deps/cares/cares.target.mk
endif

这里监听了所有.mk文件,相当于这里监听了所有的node相关的文件,只要include的关联文件有改动,在make的时候都会造成out/Makefile的重新编译。

make install

make install用于将可执行文件安装到./configue中的prefix文件中,我们看源码调试过程中用不上。

总结

本问介绍了node源码编译的大致过程,至于调试的话,用clion ide即可,网上有很多文章都介绍过,大家试着配一下就好了。

本文可能有很多不准确的地方,欢迎大家纠正。