nodejs/tools/install.py
Jan Staněk cb88e99bfe
build: search for libnode.so in multiple places
As the actual location of built libnode.so may differ depending on which
system was used to orchestrate the build (ninja or make),
multiple places should be searched for it's location.

PR-URL: https://github.com/nodejs/node/pull/58213
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Richard Lau <rlau@redhat.com>
2025-05-22 12:16:25 +00:00

437 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import ast
import errno
import os
import platform
import shutil
import sys
import re
current_system = platform.system()
SYSTEM_AIX = "AIX"
def abspath(*args):
path = os.path.join(*args)
return os.path.abspath(path)
def is_child_dir(child, parent):
p = os.path.abspath(parent)
c = os.path.abspath(child)
return c.startswith(p) and c != p
def try_unlink(path):
try:
os.unlink(path)
except OSError as e:
if e.errno != errno.ENOENT: raise
def try_symlink(options, source_path, link_path):
if not options.silent:
print('symlinking %s -> %s' % (source_path, link_path))
try_unlink(link_path)
try_mkdir_r(os.path.dirname(link_path))
os.symlink(source_path, link_path)
def try_mkdir_r(path):
try:
os.makedirs(path)
except OSError as e:
if e.errno != errno.EEXIST: raise
def try_rmdir_r(options, path):
path = abspath(path)
while is_child_dir(path, options.install_path):
try:
os.rmdir(path)
except OSError as e:
if e.errno == errno.ENOTEMPTY: return
if e.errno == errno.ENOENT: return
if e.errno == errno.EEXIST and current_system == SYSTEM_AIX: return
raise
path = abspath(path, '..')
def mkpaths(options, path, dest):
if dest.endswith('/') or dest.endswith('\\'):
target_path = abspath(options.install_path, dest, os.path.basename(path))
else:
target_path = abspath(options.install_path, dest)
if os.path.isabs(path):
source_path = path
else:
source_path = abspath(options.root_dir, path)
return source_path, target_path
def try_copy(options, path, dest):
source_path, target_path = mkpaths(options, path, dest)
if not options.silent:
print('installing %s' % target_path)
try_mkdir_r(os.path.dirname(target_path))
try_unlink(target_path) # prevent ETXTBSY errors
return shutil.copy2(source_path, target_path)
def try_remove(options, path, dest):
source_path, target_path = mkpaths(options, path, dest)
if not options.silent:
print('removing %s' % target_path)
try_unlink(target_path)
try_rmdir_r(options, os.path.dirname(target_path))
def install(options, paths, dest):
for path in paths:
try_copy(options, path, dest)
def uninstall(options, paths, dest):
for path in paths:
try_remove(options, path, dest)
def package_files(options, action, name, bins):
target_path = os.path.join('lib/node_modules', name)
# don't install npm if the target path is a symlink, it probably means
# that a dev version of npm is installed there
if os.path.islink(abspath(options.install_path, target_path)): return
# npm has a *lot* of files and it'd be a pain to maintain a fixed list here
# so we walk its source directory instead...
root = os.path.join('deps', name)
for dirname, subdirs, basenames in os.walk(root, topdown=True):
subdirs[:] = [subdir for subdir in subdirs if subdir != 'test']
paths = [os.path.join(dirname, basename) for basename in basenames]
action(options, paths,
os.path.join(target_path, dirname[len(root) + 1:]) + os.path.sep)
# create/remove symlinks
for bin_name, bin_target in bins.items():
link_path = abspath(options.install_path, os.path.join('bin', bin_name))
if action == uninstall:
action(options, [link_path], os.path.join('bin', bin_name))
elif action == install:
try_symlink(options, os.path.join('../lib/node_modules', name, bin_target), link_path)
else:
assert 0 # unhandled action type
def npm_files(options, action):
package_files(options, action, 'npm', {
'npm': 'bin/npm-cli.js',
'npx': 'bin/npx-cli.js',
})
def corepack_files(options, action):
package_files(options, action, 'corepack', {
'corepack': 'dist/corepack.js',
# Not the default just yet:
# 'yarn': 'dist/yarn.js',
# 'yarnpkg': 'dist/yarn.js',
# 'pnpm': 'dist/pnpm.js',
# 'pnpx': 'dist/pnpx.js',
})
# On z/OS, we install node-gyp for convenience, as some vendors don't have
# external access and may want to build native addons.
if sys.platform == 'zos':
link_path = abspath(options.install_path, 'bin/node-gyp')
if action == uninstall:
action(options, [link_path], 'bin/node-gyp')
elif action == install:
try_symlink(options, '../lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js', link_path)
else:
assert 0 # unhandled action type
def subdir_files(options, path, dest, action):
source_path, _ = mkpaths(options, path, dest)
ret = {}
for dirpath, dirnames, filenames in os.walk(source_path):
files_in_path = [os.path.join(os.path.relpath(dirpath, options.root_dir), f) for f in filenames if f.endswith('.h')]
ret[os.path.join(dest, os.path.relpath(dirpath, source_path))] = files_in_path
for subdir, files_in_path in ret.items():
action(options, files_in_path, subdir + os.path.sep)
def files(options, action):
node_bin = 'node'
if options.is_win:
node_bin += '.exe'
action(options, [os.path.join(options.build_dir, node_bin)], os.path.join('bin', node_bin))
if 'true' == options.variables.get('node_shared'):
if options.is_win:
action(options, [os.path.join(options.build_dir, 'libnode.dll')], 'bin/libnode.dll')
action(options, [os.path.join(options.build_dir, 'libnode.lib')], 'lib/libnode.lib')
elif sys.platform == 'zos':
# GYP will output to lib.target; see _InstallableTargetInstallPath
# function in tools/gyp/pylib/gyp/generator/make.py
output_prefix = os.path.join(options.build_dir, 'lib.target')
output_lib = 'libnode.' + options.variables.get('shlib_suffix')
action(options, [os.path.join(output_prefix, output_lib)], os.path.join('lib', output_lib))
# create libnode.x that references libnode.so (C++ addons compat)
os.system(os.path.dirname(os.path.realpath(__file__)) +
'/zos/modifysidedeck.sh ' +
abspath(options.install_path, 'lib', output_lib) + ' ' +
abspath(options.install_path, 'lib/libnode.x') + ' libnode.so')
# install libnode.version.so
so_name = 'libnode.' + re.sub(r'\.x$', '.so', options.variables.get('shlib_suffix'))
action(options, [os.path.join(output_prefix, so_name)], options.variables.get('libdir') + '/' + so_name)
# create symlink of libnode.so -> libnode.version.so (C++ addons compat)
link_path = abspath(options.install_path, 'lib/libnode.so')
try_symlink(options, so_name, link_path)
else:
# Ninja and Makefile generators output the library in different directories;
# find out which one we have, and install first found
output_lib_name = 'libnode.' + options.variables.get('shlib_suffix')
output_lib_candidate_paths = [
os.path.join(options.build_dir, output_lib_name),
os.path.join(options.build_dir, "lib", output_lib_name),
]
try:
output_lib = next(filter(os.path.exists, output_lib_candidate_paths))
except StopIteration as not_found:
raise RuntimeError("No libnode.so to install!") from not_found
action(options, [output_lib],
os.path.join(options.variables.get('libdir'), output_lib_name))
action(options, [os.path.join(options.v8_dir, 'tools/gdbinit')], 'share/doc/node/')
action(options, [os.path.join(options.v8_dir, 'tools/lldb_commands.py')], 'share/doc/node/')
if 'openbsd' in sys.platform:
action(options, ['doc/node.1'], 'man/man1/')
else:
action(options, ['doc/node.1'], 'share/man/man1/')
if 'true' == options.variables.get('node_install_npm'):
npm_files(options, action)
if 'true' == options.variables.get('node_install_corepack'):
corepack_files(options, action)
headers(options, action)
def headers(options, action):
def wanted_v8_headers(options, files_arg, dest):
v8_headers = [
# The internal cppgc headers are depended on by the public
# ones, so they need to be included as well.
'include/cppgc/internal/api-constants.h',
'include/cppgc/internal/atomic-entry-flag.h',
'include/cppgc/internal/base-page-handle.h',
'include/cppgc/internal/caged-heap-local-data.h',
'include/cppgc/internal/caged-heap.h',
'include/cppgc/internal/compiler-specific.h',
'include/cppgc/internal/conditional-stack-allocated.h',
'include/cppgc/internal/finalizer-trait.h',
'include/cppgc/internal/gc-info.h',
'include/cppgc/internal/logging.h',
'include/cppgc/internal/member-storage.h',
'include/cppgc/internal/name-trait.h',
'include/cppgc/internal/persistent-node.h',
'include/cppgc/internal/pointer-policies.h',
'include/cppgc/internal/write-barrier.h',
# cppgc headers
'include/cppgc/allocation.h',
'include/cppgc/common.h',
'include/cppgc/cross-thread-persistent.h',
'include/cppgc/custom-space.h',
'include/cppgc/default-platform.h',
'include/cppgc/explicit-management.h',
'include/cppgc/garbage-collected.h',
'include/cppgc/heap-consistency.h',
'include/cppgc/heap-handle.h',
'include/cppgc/heap-state.h',
'include/cppgc/heap-statistics.h',
'include/cppgc/heap.h',
'include/cppgc/liveness-broker.h',
'include/cppgc/macros.h',
'include/cppgc/member.h',
'include/cppgc/name-provider.h',
'include/cppgc/object-size-trait.h',
'include/cppgc/persistent.h',
'include/cppgc/platform.h',
'include/cppgc/prefinalizer.h',
'include/cppgc/process-heap-statistics.h',
'include/cppgc/sentinel-pointer.h',
'include/cppgc/source-location.h',
'include/cppgc/testing.h',
'include/cppgc/trace-trait.h',
'include/cppgc/type-traits.h',
'include/cppgc/visitor.h',
# libplatform headers
'include/libplatform/libplatform-export.h',
'include/libplatform/libplatform.h',
'include/libplatform/v8-tracing.h',
# v8 headers
'include/v8-array-buffer.h',
'include/v8-callbacks.h',
'include/v8-container.h',
'include/v8-context.h',
'include/v8-cppgc.h',
'include/v8-data.h',
'include/v8-date.h',
'include/v8-debug.h',
'include/v8-embedder-heap.h',
'include/v8-embedder-state-scope.h',
'include/v8-exception.h',
'include/v8-extension.h',
'include/v8-external.h',
'include/v8-forward.h',
'include/v8-function-callback.h',
'include/v8-function.h',
'include/v8-handle-base.h',
'include/v8-initialization.h',
'include/v8-internal.h',
'include/v8-isolate.h',
'include/v8-json.h',
'include/v8-local-handle.h',
'include/v8-locker.h',
'include/v8-maybe.h',
'include/v8-memory-span.h',
'include/v8-message.h',
'include/v8-microtask-queue.h',
'include/v8-microtask.h',
'include/v8-object.h',
'include/v8-persistent-handle.h',
'include/v8-platform.h',
'include/v8-primitive-object.h',
'include/v8-primitive.h',
'include/v8-profiler.h',
'include/v8-promise.h',
'include/v8-proxy.h',
'include/v8-regexp.h',
'include/v8-sandbox.h',
'include/v8-script.h',
'include/v8-snapshot.h',
'include/v8-source-location.h',
'include/v8-statistics.h',
'include/v8-template.h',
'include/v8-traced-handle.h',
'include/v8-typed-array.h',
'include/v8-unwinder.h',
'include/v8-value-serializer.h',
'include/v8-value.h',
'include/v8-version.h',
'include/v8-wasm.h',
'include/v8-weak-callback-info.h',
'include/v8.h',
'include/v8config.h',
]
if sys.platform == 'win32':
# Native win32 python uses \ for path separator.
v8_headers = [os.path.normpath(path) for path in v8_headers]
if os.path.isabs(options.v8_dir):
rel_v8_dir = os.path.relpath(options.v8_dir, options.root_dir)
else:
rel_v8_dir = options.v8_dir
files_arg = [name for name in files_arg if os.path.relpath(name, rel_v8_dir) in v8_headers]
action(options, files_arg, dest)
def wanted_zoslib_headers(options, files_arg, dest):
import glob
zoslib_headers = glob.glob(zoslibinc + '/*.h')
files_arg = [name for name in files_arg if name in zoslib_headers]
action(options, files_arg, dest)
action(options, [
options.config_gypi_path,
'common.gypi',
'src/node.h',
'src/node_api.h',
'src/js_native_api.h',
'src/js_native_api_types.h',
'src/node_api_types.h',
'src/node_buffer.h',
'src/node_object_wrap.h',
'src/node_version.h',
], 'include/node/')
# Add the expfile that is created on AIX
if sys.platform.startswith('aix') or sys.platform == "os400":
action(options, ['out/Release/node.exp'], 'include/node/')
subdir_files(options, os.path.join(options.v8_dir, 'include'), 'include/node/', wanted_v8_headers)
if 'false' == options.variables.get('node_shared_libuv'):
subdir_files(options, 'deps/uv/include', 'include/node/', action)
if 'true' == options.variables.get('node_use_openssl') and \
'false' == options.variables.get('node_shared_openssl'):
subdir_files(options, 'deps/openssl/openssl/include/openssl', 'include/node/openssl/', action)
subdir_files(options, 'deps/openssl/config/archs', 'include/node/openssl/archs', action)
subdir_files(options, 'deps/openssl/config', 'include/node/openssl', action)
if 'false' == options.variables.get('node_shared_zlib'):
action(options, [
'deps/zlib/zconf.h',
'deps/zlib/zlib.h',
], 'include/node/')
if sys.platform == 'zos':
zoslibinc = os.environ.get('ZOSLIB_INCLUDES')
if not zoslibinc:
raise RuntimeError('Environment variable ZOSLIB_INCLUDES is not set\n')
if not os.path.isfile(zoslibinc + '/zos-base.h'):
raise RuntimeError('ZOSLIB_INCLUDES is not set to a valid location\n')
subdir_files(options, zoslibinc, 'include/node/zoslib/', wanted_zoslib_headers)
def run(options):
if options.headers_only:
if options.command == 'install':
headers(options, install)
return
if options.command == 'uninstall':
headers(options, uninstall)
return
else:
if options.command == 'install':
files(options, install)
return
if options.command == 'uninstall':
files(options, uninstall)
return
raise RuntimeError('Bad command: %s\n' % options.command)
def parse_options(args):
parser = argparse.ArgumentParser(
description='Install headers and binaries into filesystem')
parser.add_argument('command', choices=['install', 'uninstall'])
parser.add_argument('--dest-dir', help='custom install prefix for packagers, i.e. DESTDIR',
default=os.getcwd())
parser.add_argument('--prefix', help='custom install prefix, i.e. PREFIX',
default='/usr/local')
parser.add_argument('--headers-only', help='only install headers',
action='store_true', default=False)
parser.add_argument('--root-dir', help='the root directory of source code',
default=os.getcwd())
parser.add_argument('--build-dir', help='the location of built binaries',
default='out/Release')
parser.add_argument('--v8-dir', help='the location of V8',
default='deps/v8')
parser.add_argument('--config-gypi-path', help='the location of config.gypi',
default='config.gypi')
parser.add_argument('--is-win', help='build for Windows target',
action='store_true',
default=(sys.platform in ['win32', 'cygwin']))
parser.add_argument('--silent', help='do not output log',
action='store_true', default=False)
options = parser.parse_args(args)
# |dest_dir| is a custom install prefix for packagers (think DESTDIR)
# |prefix| is a custom install prefix (think PREFIX)
# Difference is that dest_dir won't be included in shebang lines etc.
# |install_path| thus becomes the base target directory.
options.install_path = os.path.join(options.dest_dir + options.prefix)
# Read variables from the config.gypi.
with open(options.config_gypi_path) as f:
config = ast.literal_eval(f.read())
options.variables = config['variables']
return options
if __name__ == '__main__':
run(parse_options(sys.argv[1:]))