#!/usr/bin/env python # -*- coding: utf-8 -*- """ nodeenv ~~~~~~~ Node.js virtual environment :copyright: (c) 2014 by Eugene Kalinin :license: BSD, see LICENSE for more details. """ import contextlib import io import json import sys import os import re import ssl import stat import logging import operator import argparse import subprocess import tarfile import pipes import platform import zipfile import shutil import sysconfig import glob try: # pragma: no cover (py2 only) from ConfigParser import SafeConfigParser as ConfigParser # noinspection PyCompatibility import urllib2 iteritems = operator.methodcaller('iteritems') import httplib IncompleteRead = httplib.IncompleteRead except ImportError: # pragma: no cover (py3 only) from configparser import ConfigParser # noinspection PyUnresolvedReferences import urllib.request as urllib2 iteritems = operator.methodcaller('items') import http IncompleteRead = http.client.IncompleteRead from pkg_resources import parse_version nodeenv_version = '1.8.0' join = os.path.join abspath = os.path.abspath src_base_url = None is_PY3 = sys.version_info[0] >= 3 is_WIN = platform.system() == 'Windows' is_CYGWIN = platform.system().startswith(('CYGWIN', 'MSYS')) ignore_ssl_certs = False # --------------------------------------------------------- # Utils # https://github.com/jhermann/waif/blob/master/python/to_uft8.py def to_utf8(text): """Convert given text to UTF-8 encoding (as far as possible).""" if not text or is_PY3: return text try: # unicode or pure ascii return text.encode("utf8") except UnicodeDecodeError: try: # successful UTF-8 decode means it's pretty sure UTF-8 text.decode("utf8") return text except UnicodeDecodeError: try: # get desperate; and yes, this has a western hemisphere bias return text.decode("cp1252").encode("utf8") except UnicodeDecodeError: pass return text # return unchanged, hope for the best class Config(object): """ Configuration namespace. """ # Defaults node = 'latest' npm = 'latest' with_npm = False jobs = '2' without_ssl = False debug = False profile = False make = 'make' prebuilt = True ignore_ssl_certs = False mirror = None @classmethod def _load(cls, configfiles, verbose=False): """ Load configuration from the given files in reverse order, if they exist and have a [nodeenv] section. Additionally, load version from .node-version if file exists. """ for configfile in reversed(configfiles): configfile = os.path.expanduser(configfile) if not os.path.exists(configfile): continue ini_file = ConfigParser() ini_file.read(configfile) section = "nodeenv" if not ini_file.has_section(section): continue for attr, val in iteritems(vars(cls)): if attr.startswith('_') or not \ ini_file.has_option(section, attr): continue if isinstance(val, bool): val = ini_file.getboolean(section, attr) else: val = ini_file.get(section, attr) if verbose: print('CONFIG {0}: {1} = {2}'.format( os.path.basename(configfile), attr, val)) setattr(cls, attr, val) if os.path.exists(".node-version"): with open(".node-version", "r") as v_file: setattr(cls, "node", v_file.readlines(1)[0].strip()) @classmethod def _dump(cls): """ Print defaults for the README. """ print(" [nodeenv]") print(" " + "\n ".join( "%s = %s" % (k, v) for k, v in sorted(iteritems(vars(cls))) if not k.startswith('_'))) Config._default = dict( (attr, val) for attr, val in iteritems(vars(Config)) if not attr.startswith('_') ) def clear_output(out): """ Remove new-lines and """ return out.decode('utf-8').replace('\n', '') def remove_env_bin_from_path(env, env_bin_dir): """ Remove bin directory of the current environment from PATH """ return env.replace(env_bin_dir + ':', '') def node_version_from_args(args): """ Parse the node version from the argparse args """ if args.node == 'system': out, err = subprocess.Popen( ["node", "--version"], stdout=subprocess.PIPE).communicate() return parse_version(clear_output(out).replace('v', '')) return parse_version(args.node) def create_logger(): """ Create logger for diagnostic """ # create logger loggr = logging.getLogger("nodeenv") loggr.setLevel(logging.INFO) # monkey patch def emit(self, record): msg = self.format(record) fs = "%s" if getattr(record, "continued", False) else "%s\n" self.stream.write(fs % to_utf8(msg)) self.flush() logging.StreamHandler.emit = emit # create console handler and set level to debug ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) # create formatter formatter = logging.Formatter(fmt="%(message)s") # add formatter to ch ch.setFormatter(formatter) # add ch to logger loggr.addHandler(ch) return loggr logger = create_logger() def make_parser(): """ Make a command line argument parser. """ parser = argparse.ArgumentParser( usage="%(prog)s [OPTIONS] DEST_DIR") parser.add_argument( '--version', action='version', version=nodeenv_version) parser.add_argument( '-n', '--node', dest='node', metavar='NODE_VER', default=Config.node, help='The node.js version to use, e.g., ' '--node=0.4.3 will use the node-v0.4.3 ' 'to create the new environment. ' 'The default is last stable version (`latest`). ' 'Use `lts` to use the latest LTS release. ' 'Use `system` to use system-wide node.') parser.add_argument( '--mirror', action="store", dest='mirror', default=Config.mirror, help='Set mirror server of nodejs.org to download from.') if not is_WIN: parser.add_argument( '-j', '--jobs', dest='jobs', default=Config.jobs, help='Sets number of parallel commands at node.js compilation. ' 'The default is 2 jobs.') parser.add_argument( '--load-average', dest='load_average', help='Sets maximum load average for executing parallel commands ' 'at node.js compilation.') parser.add_argument( '--without-ssl', dest='without_ssl', action='store_true', default=Config.without_ssl, help='Build node.js without SSL support') parser.add_argument( '--debug', dest='debug', action='store_true', default=Config.debug, help='Build debug variant of the node.js') parser.add_argument( '--profile', dest='profile', action='store_true', default=Config.profile, help='Enable profiling for node.js') parser.add_argument( '--make', '-m', dest='make_path', metavar='MAKE_PATH', help='Path to make command', default=Config.make) parser.add_argument( '--source', dest='prebuilt', action='store_false', default=Config.prebuilt, help='Install node.js from the source') parser.add_argument( '-v', '--verbose', action='store_true', dest='verbose', default=False, help="Verbose mode") parser.add_argument( '-q', '--quiet', action='store_true', dest='quiet', default=False, help="Quiet mode") parser.add_argument( '-C', '--config-file', dest='config_file', default=None, help="Load a different file than '~/.nodeenvrc'. " "Pass an empty string for no config (use built-in defaults).") parser.add_argument( '-r', '--requirements', dest='requirements', default='', metavar='FILENAME', help='Install all the packages listed in the given requirements file.') parser.add_argument( '--prompt', dest='prompt', help='Provides an alternative prompt prefix for this environment') parser.add_argument( '-l', '--list', dest='list', action='store_true', default=False, help='Lists available node.js versions') parser.add_argument( '--update', dest='update', action='store_true', default=False, help='Install npm packages from file without node') parser.add_argument( '--with-npm', dest='with_npm', action='store_true', default=Config.with_npm, help='Build without installing npm into the new virtual environment. ' 'Required for node.js < 0.6.3. By default, the npm included with ' 'node.js is used. Under Windows, this defaults to true.') parser.add_argument( '--npm', dest='npm', metavar='NPM_VER', default=Config.npm, help='The npm version to use, e.g., ' '--npm=0.3.18 will use the npm-0.3.18.tgz ' 'tarball to install. ' 'The default is last available version (`latest`).') parser.add_argument( '--no-npm-clean', dest='no_npm_clean', action='store_true', default=False, help='Skip the npm 0.x cleanup. Cleanup is enabled by default.') parser.add_argument( '--python-virtualenv', '-p', dest='python_virtualenv', action='store_true', default=False, help='Use current python virtualenv') parser.add_argument( '--clean-src', '-c', dest='clean_src', action='store_true', default=False, help='Remove "src" directory after installation') parser.add_argument( '--force', dest='force', action='store_true', default=False, help='Force installation in a pre-existing directory') parser.add_argument( '--prebuilt', dest='prebuilt', action='store_true', default=Config.prebuilt, help='Install node.js from prebuilt package (default)') parser.add_argument( '--ignore_ssl_certs', dest='ignore_ssl_certs', action='store_true', default=Config.ignore_ssl_certs, help='Ignore certificates for package downloads. - UNSAFE -') parser.add_argument( metavar='DEST_DIR', dest='env_dir', nargs='?', help='Destination directory') return parser def parse_args(check=True): """ Parses command line arguments. Set `check` to False to skip validation checks. """ parser = make_parser() args = parser.parse_args() if args.config_file is None: args.config_file = ["./tox.ini", "./setup.cfg", "~/.nodeenvrc"] elif not args.config_file: args.config_file = [] else: # Make sure that explicitly provided files exist if not os.path.exists(args.config_file): parser.error("Config file '{0}' doesn't exist!".format( args.config_file)) args.config_file = [args.config_file] if not check: return args if not args.list: if not args.python_virtualenv and not args.env_dir: parser.error('You must provide a DEST_DIR or ' 'use current python virtualenv') return args def mkdir(path): """ Create directory """ if not os.path.exists(path): logger.debug(' * Creating: %s ... ', path, extra=dict(continued=True)) os.makedirs(path) logger.debug('done.') else: logger.debug(' * Directory %s already exists', path) def make_executable(filename): mode_0755 = (stat.S_IRWXU | stat.S_IXGRP | stat.S_IRGRP | stat.S_IROTH | stat.S_IXOTH) os.chmod(filename, mode_0755) # noinspection PyArgumentList def writefile(dest, content, overwrite=True, append=False): """ Create file and write content in it """ content = to_utf8(content) if is_PY3 and type(content) != bytes: content = bytes(content, 'utf-8') if not os.path.exists(dest): logger.debug(' * Writing %s ... ', dest, extra=dict(continued=True)) with open(dest, 'wb') as f: f.write(content) make_executable(dest) logger.debug('done.') return else: with open(dest, 'rb') as f: c = f.read() if content in c: logger.debug(' * Content %s already in place', dest) return if not overwrite: logger.info(' * File %s exists with different content; ' ' not overwriting', dest) return if append: logger.info(' * Appending data to %s', dest) with open(dest, 'ab') as f: f.write(content) return logger.info(' * Overwriting %s with new content', dest) with open(dest, 'wb') as f: f.write(content) def callit(cmd, show_stdout=True, in_shell=False, cwd=None, extra_env=None): """ Execute cmd line in sub-shell """ all_output = [] cmd_parts = [] for part in cmd: if len(part) > 45: part = part[:20] + "..." + part[-20:] if ' ' in part or '\n' in part or '"' in part or "'" in part: part = '"%s"' % part.replace('"', '\\"') cmd_parts.append(part) cmd_desc = ' '.join(cmd_parts) logger.debug(" ** Running command %s" % cmd_desc) if in_shell: cmd = ' '.join(cmd) # output stdout = subprocess.PIPE # env if extra_env: env = os.environ.copy() if extra_env: env.update(extra_env) else: env = None # execute try: proc = subprocess.Popen( cmd, stderr=subprocess.STDOUT, stdin=None, stdout=stdout, cwd=cwd, env=env, shell=in_shell) except Exception: e = sys.exc_info()[1] logger.error("Error %s while executing command %s" % (e, cmd_desc)) raise stdout = proc.stdout while stdout: line = stdout.readline() if not line: break try: if is_WIN: line = line.decode('mbcs').rstrip() else: line = line.decode('utf8').rstrip() except UnicodeDecodeError: line = line.decode('cp866').rstrip() all_output.append(line) if show_stdout: logger.info(line) proc.wait() # error handler if proc.returncode: if show_stdout: for s in all_output: logger.critical(s) raise OSError("Command %s failed with error code %s" % (cmd_desc, proc.returncode)) return proc.returncode, all_output def get_root_url(version): if parse_version(version) > parse_version("0.5.0"): return '%s/v%s/' % (src_base_url, version) else: return src_base_url def is_x86_64_musl(): return sysconfig.get_config_var('HOST_GNU_TYPE') == 'x86_64-pc-linux-musl' def is_riscv64(): return platform.machine() == 'riscv64' def get_node_bin_url(version): archmap = { 'x86': 'x86', # Windows Vista 32 'i686': 'x86', 'x86_64': 'x64', # Linux Ubuntu 64 'amd64': 'x64', # FreeBSD 64bits 'AMD64': 'x64', # Windows Server 2012 R2 (x64) 'armv6l': 'armv6l', # arm 'armv7l': 'armv7l', 'armv8l': 'armv7l', 'aarch64': 'arm64', 'arm64': 'arm64', 'arm64/v8': 'arm64', 'armv8': 'arm64', 'armv8.4': 'arm64', 'ppc64le': 'ppc64le', # Power PC 's390x': 's390x', # IBM S390x 'riscv64': 'riscv64', # RISCV 64 } sysinfo = { 'system': platform.system().lower(), 'arch': archmap[platform.machine()], } if is_WIN or is_CYGWIN: postfix = '-win-%(arch)s.zip' % sysinfo elif is_x86_64_musl(): postfix = '-linux-x64-musl.tar.gz' else: postfix = '-%(system)s-%(arch)s.tar.gz' % sysinfo filename = 'node-v%s%s' % (version, postfix) return get_root_url(version) + filename def get_node_src_url(version): tar_name = 'node-v%s.tar.gz' % version return get_root_url(version) + tar_name @contextlib.contextmanager def tarfile_open(*args, **kwargs): """Compatibility layer because py26.""" tf = tarfile.open(*args, **kwargs) try: yield tf finally: tf.close() def _download_node_file(node_url, n_attempt=3): """Do multiple attempts to avoid incomplete data in case of unstable network""" while n_attempt > 0: try: return io.BytesIO(urlopen(node_url).read()) except IncompleteRead as e: logger.warning( 'Incomplete read while reading' 'from {} - {}'.format(node_url, e) ) n_attempt -= 1 if n_attempt == 0: raise e def download_node_src(node_url, src_dir, args): """ Download source code """ logger.info('.', extra=dict(continued=True)) dl_contents = _download_node_file(node_url) logger.info('.', extra=dict(continued=True)) if is_WIN or is_CYGWIN: ctx = zipfile.ZipFile(dl_contents) members = operator.methodcaller('namelist') member_name = lambda s: s # noqa: E731 else: ctx = tarfile_open(fileobj=dl_contents) members = operator.methodcaller('getmembers') member_name = operator.attrgetter('name') with ctx as archive: node_ver = re.escape(args.node) rexp_string = r"node-v%s[^/]*/(README\.md|CHANGELOG\.md|LICENSE)"\ % node_ver extract_list = [ member for member in members(archive) if re.match(rexp_string, member_name(member)) is None ] archive.extractall(src_dir, extract_list) def urlopen(url): home_url = "https://github.com/ekalinin/nodeenv/" headers = {'User-Agent': 'nodeenv/%s (%s)' % (nodeenv_version, home_url)} req = urllib2.Request(url, None, headers) if ignore_ssl_certs: # py27: protocol required, py3: optional # https://github.com/ekalinin/nodeenv/issues/296 context = ssl.SSLContext(ssl.PROTOCOL_TLS) context.verify_mode = ssl.CERT_NONE return urllib2.urlopen(req, context=context) return urllib2.urlopen(req) # --------------------------------------------------------- # Virtual environment functions def copytree(src, dst, symlinks=False, ignore=None): for item in os.listdir(src): s = os.path.join(src, item) d = os.path.join(dst, item) if os.path.isdir(s): try: shutil.copytree(s, d, symlinks, ignore) except OSError: copytree(s, d, symlinks, ignore) else: if os.path.islink(s): # copy link only if it not exists. #189 if not os.path.islink(d): os.symlink(os.readlink(s), d) else: shutil.copy2(s, d) def copy_node_from_prebuilt(env_dir, src_dir, node_version): """ Copy prebuilt binaries into environment """ logger.info('.', extra=dict(continued=True)) if is_WIN: dest = join(env_dir, 'Scripts') mkdir(dest) elif is_CYGWIN: dest = join(env_dir, 'bin') mkdir(dest) # write here to avoid https://bugs.python.org/issue35650 writefile(join(env_dir, 'bin', 'node'), CYGWIN_NODE) else: dest = env_dir src_folder_tpl = src_dir + to_utf8('/node-v%s*' % node_version) src_folder, = glob.glob(src_folder_tpl) copytree(src_folder, dest, True) if is_CYGWIN: for filename in ('npm', 'npx', 'node.exe'): filename = join(env_dir, 'bin', filename) if os.path.exists(filename): make_executable(filename) logger.info('.', extra=dict(continued=True)) def build_node_from_src(env_dir, src_dir, node_src_dir, args): env = {} make_param_names = ['load-average', 'jobs'] make_param_values = map( lambda x: getattr(args, x.replace('-', '_')), make_param_names) make_opts = [ '--{0}={1}'.format(name, value) if len(value) > 0 else '--{0}'.format(name) for name, value in zip(make_param_names, make_param_values) if value is not None ] if getattr(sys.version_info, 'major', sys.version_info[0]) > 2: # Currently, the node.js build scripts are using python2.*, # therefore we need to temporarily point python exec to the # python 2.* version in this case. try: _, which_python2_output = callit( ['which', 'python2'], args.verbose, True, node_src_dir, env ) python2_path = which_python2_output[0] except (OSError, IndexError): raise OSError( 'Python >=3.0 virtualenv detected, but no python2 ' 'command (required for building node.js) was found' ) logger.debug(' * Temporarily pointing python to %s', python2_path) node_tmpbin_dir = join(src_dir, 'tmpbin') node_tmpbin_link = join(node_tmpbin_dir, 'python') mkdir(node_tmpbin_dir) if not os.path.exists(node_tmpbin_link): callit(['ln', '-s', python2_path, node_tmpbin_link]) env['PATH'] = '{}:{}'.format(node_tmpbin_dir, os.environ.get('PATH', '')) conf_cmd = [ './configure', '--prefix=%s' % pipes.quote(env_dir) ] if args.without_ssl: conf_cmd.append('--without-ssl') if args.debug: conf_cmd.append('--debug') if args.profile: conf_cmd.append('--profile') make_cmd = args.make_path callit(conf_cmd, args.verbose, True, node_src_dir, env) logger.info('.', extra=dict(continued=True)) callit([make_cmd] + make_opts, args.verbose, True, node_src_dir, env) logger.info('.', extra=dict(continued=True)) callit([make_cmd + ' install'], args.verbose, True, node_src_dir, env) def install_node(env_dir, src_dir, args): """ Download source code for node.js, unpack it and install it in virtual environment. """ try: install_node_wrapped(env_dir, src_dir, args) except BaseException: # this restores the newline suppressed by continued=True logger.info('') raise def install_node_wrapped(env_dir, src_dir, args): env_dir = abspath(env_dir) node_src_dir = join(src_dir, to_utf8('node-v%s' % args.node)) src_type = "prebuilt" if args.prebuilt else "source" logger.info(' * Install %s node (%s) ' % (src_type, args.node), extra=dict(continued=True)) if args.prebuilt: node_url = get_node_bin_url(args.node) else: node_url = get_node_src_url(args.node) # get src if not downloaded yet if not os.path.exists(node_src_dir): try: download_node_src(node_url, src_dir, args) except urllib2.HTTPError: if "arm64" in node_url: # if arm64 not found, try x64 download_node_src(node_url.replace('arm64', 'x64'), src_dir, args) else: logger.warning('Failed to download from %s' % node_url) logger.info('.', extra=dict(continued=True)) if args.prebuilt: copy_node_from_prebuilt(env_dir, src_dir, args.node) else: build_node_from_src(env_dir, src_dir, node_src_dir, args) logger.info(' done.') def install_npm(env_dir, _src_dir, args): """ Download source code for npm, unpack it and install it in virtual environment. """ logger.info(' * Install npm.js (%s) ... ' % args.npm, extra=dict(continued=True)) env = dict( os.environ, clean='no' if args.no_npm_clean else 'yes', npm_install=args.npm, ) proc = subprocess.Popen( ( 'bash', '-c', '. {0} && npm install -g npm@{1}'.format( pipes.quote(join(env_dir, 'bin', 'activate')), args.npm, ) ), env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) out, _ = proc.communicate() if args.verbose: logger.info(out) logger.info('done.') def install_npm_win(env_dir, src_dir, args): """ Download source code for npm, unpack it and install it in virtual environment. """ logger.info(' * Install npm.js (%s) ... ' % args.npm, extra=dict(continued=True)) npm_url = 'https://github.com/npm/cli/archive/v%s.zip' % args.npm npm_contents = io.BytesIO(urlopen(npm_url).read()) bin_path = join(env_dir, 'Scripts') node_modules_path = join(bin_path, 'node_modules', 'npm') if os.path.exists(node_modules_path): shutil.rmtree(node_modules_path) if os.path.exists(join(bin_path, 'npm.cmd')): os.remove(join(bin_path, 'npm.cmd')) if os.path.exists(join(bin_path, 'npm-cli.js')): os.remove(join(bin_path, 'npm-cli.js')) with zipfile.ZipFile(npm_contents, 'r') as zipf: zipf.extractall(src_dir) npm_ver = 'cli-%s' % args.npm shutil.copytree(join(src_dir, npm_ver), node_modules_path) shutil.copy(join(src_dir, npm_ver, 'bin', 'npm.cmd'), join(bin_path, 'npm.cmd')) shutil.copy(join(src_dir, npm_ver, 'bin', 'npm-cli.js'), join(bin_path, 'npm-cli.js')) if is_CYGWIN: shutil.copy(join(bin_path, 'npm-cli.js'), join(env_dir, 'bin', 'npm-cli.js')) shutil.copytree(join(bin_path, 'node_modules'), join(env_dir, 'bin', 'node_modules')) npm_gh_url = 'https://raw.githubusercontent.com/npm/cli' npm_bin_url = '{}/{}/bin/npm'.format(npm_gh_url, args.npm) writefile(join(env_dir, 'bin', 'npm'), urlopen(npm_bin_url).read()) def install_packages(env_dir, args): """ Install node.js packages via npm """ logger.info(' * Install node.js packages ... ', extra=dict(continued=True)) packages = [package.strip() for package in open(args.requirements).readlines()] activate_path = join(env_dir, 'bin', 'activate') real_npm_ver = args.npm if args.npm.count(".") == 2 else args.npm + ".0" if args.npm == "latest" or real_npm_ver >= "1.0.0": cmd = '. ' + pipes.quote(activate_path) + \ ' && npm install -g %(pack)s' else: cmd = '. ' + pipes.quote(activate_path) + \ ' && npm install %(pack)s' + \ ' && npm activate %(pack)s' for package in packages: if not package: continue callit(cmd=[ cmd % {"pack": package}], show_stdout=args.verbose, in_shell=True) logger.info('done.') def install_activate(env_dir, args): """ Install virtual environment activation script """ if is_WIN: files = { 'activate.bat': ACTIVATE_BAT, "deactivate.bat": DEACTIVATE_BAT, "Activate.ps1": ACTIVATE_PS1 } bin_dir = join(env_dir, 'Scripts') shim_node = join(bin_dir, "node.exe") shim_nodejs = join(bin_dir, "nodejs.exe") else: files = { 'activate': ACTIVATE_SH, 'activate.fish': ACTIVATE_FISH, 'shim': SHIM } bin_dir = join(env_dir, 'bin') shim_node = join(bin_dir, "node") shim_nodejs = join(bin_dir, "nodejs") if is_CYGWIN: mkdir(bin_dir) if args.node == "system": files["node"] = SHIM mod_dir = join('lib', 'node_modules') prompt = args.prompt or '(%s)' % os.path.basename(os.path.abspath(env_dir)) if args.node == "system": env = os.environ.copy() env.update({'PATH': remove_env_bin_from_path(env['PATH'], bin_dir)}) for candidate in ("nodejs", "node"): which_node_output, _ = subprocess.Popen( ["which", candidate], stdout=subprocess.PIPE, env=env).communicate() shim_node = clear_output(which_node_output) if shim_node: break assert shim_node, "Did not find nodejs or node system executable" for name, content in files.items(): file_path = join(bin_dir, name) content = content.replace('__NODE_VIRTUAL_PROMPT__', prompt) content = content.replace('__NODE_VIRTUAL_ENV__', os.path.abspath(env_dir)) content = content.replace('__SHIM_NODE__', shim_node) content = content.replace('__BIN_NAME__', os.path.basename(bin_dir)) content = content.replace('__MOD_NAME__', mod_dir) if is_CYGWIN: _, cyg_bin_dir = callit( ['cygpath', '-w', os.path.abspath(bin_dir)], show_stdout=False, in_shell=False) content = content.replace('__NPM_CONFIG_PREFIX__', cyg_bin_dir[0]) else: content = content.replace('__NPM_CONFIG_PREFIX__', '$NODE_VIRTUAL_ENV') # if we call in the same environment: # $ nodeenv -p --prebuilt # $ nodeenv -p --node=system # we should get `bin/node` not as binary+string. # `bin/activate` should be appended if we're inside # existing python's virtual environment need_append = False if args.python_virtualenv: disable_prompt = DISABLE_PROMPT.get(name, '') enable_prompt = ENABLE_PROMPT.get(name, '') content = disable_prompt + content + enable_prompt need_append = bool(disable_prompt) writefile(file_path, content, append=need_append) if not os.path.exists(shim_nodejs): if is_WIN: try: callit(['mklink', shim_nodejs, 'node.exe'], True, True) except OSError: logger.error('Error: Failed to create nodejs.exe link') else: os.symlink("node", shim_nodejs) def set_predeactivate_hook(env_dir): if not is_WIN: with open(join(env_dir, 'bin', 'predeactivate'), 'a') as hook: hook.write(PREDEACTIVATE_SH) def create_environment(env_dir, args): """ Creates a new environment in ``env_dir``. """ if os.path.exists(env_dir) and not args.python_virtualenv: logger.info(' * Environment already exists: %s', env_dir) if not args.force: sys.exit(2) src_dir = to_utf8(abspath(join(env_dir, 'src'))) mkdir(src_dir) if args.node != "system": install_node(env_dir, src_dir, args) else: mkdir(join(env_dir, 'bin')) mkdir(join(env_dir, 'lib')) mkdir(join(env_dir, 'lib', 'node_modules')) # activate script install must be # before npm install, npm use activate # for install install_activate(env_dir, args) if node_version_from_args(args) < parse_version("0.6.3") or args.with_npm: instfunc = install_npm_win if is_WIN or is_CYGWIN else install_npm instfunc(env_dir, src_dir, args) if args.requirements: install_packages(env_dir, args) if args.python_virtualenv: set_predeactivate_hook(env_dir) # Cleanup if args.clean_src: shutil.rmtree(src_dir) def _get_versions_json(): response = urlopen('%s/index.json' % src_base_url) return json.loads(response.read().decode('UTF-8')) def get_node_versions(): return [dct['version'].lstrip('v') for dct in _get_versions_json()][::-1] def print_node_versions(): """ Prints into stdout all available node.js versions """ versions = get_node_versions() chunks_of_8 = [ versions[pos:pos + 8] for pos in range(0, len(versions), 8) ] for chunk in chunks_of_8: logger.info('\t'.join(chunk)) def get_last_stable_node_version(): """ Return last stable node.js version """ return _get_versions_json()[0]['version'].lstrip('v') def get_last_lts_node_version(): """ Return the last node.js version marked as LTS """ return next((v['version'].lstrip('v') for v in _get_versions_json() if v['lts']), None) def get_env_dir(args): if args.python_virtualenv: if hasattr(sys, 'real_prefix'): res = sys.prefix elif hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix: res = sys.prefix elif 'CONDA_PREFIX' in os.environ: res = sys.prefix else: logger.error('No python virtualenv is available') sys.exit(2) else: res = args.env_dir return to_utf8(res) # noinspection PyProtectedMember def main(): """ Entry point """ # quick&dirty way to help update the README if "--dump-config-defaults" in sys.argv: Config._dump() return args = parse_args(check=False) # noinspection PyProtectedMember Config._load(args.config_file, args.verbose) args = parse_args() if args.node.lower() == 'system' and is_WIN: logger.error('Installing system node.js on win32 is not supported!') exit(1) global src_base_url global ignore_ssl_certs ignore_ssl_certs = args.ignore_ssl_certs src_domain = None if args.mirror: if '://' in args.mirror: src_base_url = args.mirror else: src_domain = args.mirror # use unofficial builds only if musl and no explicitly chosen mirror elif is_x86_64_musl() or is_riscv64(): src_domain = 'unofficial-builds.nodejs.org' else: src_domain = 'nodejs.org' if src_base_url is None: src_base_url = 'https://%s/download/release' % src_domain if not args.node or args.node.lower() == 'latest': args.node = get_last_stable_node_version() elif args.node.lower() == 'lts': args.node = get_last_lts_node_version() if args.list: print_node_versions() elif args.update: env_dir = get_env_dir(args) install_packages(env_dir, args) else: env_dir = get_env_dir(args) create_environment(env_dir, args) # --------------------------------------------------------- # Shell scripts content DISABLE_PROMPT = { 'activate': """ # disable nodeenv's prompt # (prompt already changed by original virtualenv's script) # https://github.com/ekalinin/nodeenv/issues/26 NODE_VIRTUAL_ENV_DISABLE_PROMPT=1 """, 'activate.fish': """ # disable nodeenv's prompt # (prompt already changed by original virtualenv's script) # https://github.com/ekalinin/nodeenv/issues/26 set NODE_VIRTUAL_ENV_DISABLE_PROMPT 1 """, } ENABLE_PROMPT = { 'activate': """ unset NODE_VIRTUAL_ENV_DISABLE_PROMPT """, 'activate.fish': """ set -e NODE_VIRTUAL_ENV_DISABLE_PROMPT """, } SHIM = """#!/usr/bin/env bash export NODE_PATH='__NODE_VIRTUAL_ENV__/lib/node_modules' export NPM_CONFIG_PREFIX='__NODE_VIRTUAL_ENV__' export npm_config_prefix='__NODE_VIRTUAL_ENV__' exec '__SHIM_NODE__' "$@" """ ACTIVATE_BAT = r""" @echo off set "NODE_VIRTUAL_ENV=__NODE_VIRTUAL_ENV__" if not defined PROMPT ( set "PROMPT=$P$G" ) if defined _OLD_VIRTUAL_PROMPT ( set "PROMPT=%_OLD_VIRTUAL_PROMPT%" ) if defined _OLD_VIRTUAL_NODE_PATH ( set "NODE_PATH=%_OLD_VIRTUAL_NODE_PATH%" ) set "_OLD_VIRTUAL_PROMPT=%PROMPT%" set "PROMPT=__NODE_VIRTUAL_PROMPT__ %PROMPT%" if defined NODE_PATH ( set "_OLD_VIRTUAL_NODE_PATH=%NODE_PATH%" set NODE_PATH= ) if defined _OLD_VIRTUAL_PATH ( set "PATH=%_OLD_VIRTUAL_PATH%" ) else ( set "_OLD_VIRTUAL_PATH=%PATH%" ) set "PATH=%NODE_VIRTUAL_ENV%\Scripts;%PATH%" :END """ DEACTIVATE_BAT = """\ @echo off if defined _OLD_VIRTUAL_PROMPT ( set "PROMPT=%_OLD_VIRTUAL_PROMPT%" ) set _OLD_VIRTUAL_PROMPT= if defined _OLD_VIRTUAL_NODE_PATH ( set "NODE_PATH=%_OLD_VIRTUAL_NODE_PATH%" set _OLD_VIRTUAL_NODE_PATH= ) if defined _OLD_VIRTUAL_PATH ( set "PATH=%_OLD_VIRTUAL_PATH%" ) set _OLD_VIRTUAL_PATH= set NODE_VIRTUAL_ENV= :END """ ACTIVATE_PS1 = r""" function global:deactivate ([switch]$NonDestructive) { # Revert to original values if (Test-Path function:_OLD_VIRTUAL_PROMPT) { copy-item function:_OLD_VIRTUAL_PROMPT function:prompt remove-item function:_OLD_VIRTUAL_PROMPT } if (Test-Path env:_OLD_VIRTUAL_NODE_PATH) { copy-item env:_OLD_VIRTUAL_NODE_PATH env:NODE_PATH remove-item env:_OLD_VIRTUAL_NODE_PATH } if (Test-Path env:_OLD_VIRTUAL_PATH) { copy-item env:_OLD_VIRTUAL_PATH env:PATH remove-item env:_OLD_VIRTUAL_PATH } if (Test-Path env:NODE_VIRTUAL_ENV) { remove-item env:NODE_VIRTUAL_ENV } if (!$NonDestructive) { # Self destruct! remove-item function:deactivate } } deactivate -nondestructive $env:NODE_VIRTUAL_ENV="__NODE_VIRTUAL_ENV__" # Set the prompt to include the env name # Make sure _OLD_VIRTUAL_PROMPT is global function global:_OLD_VIRTUAL_PROMPT {""} copy-item function:prompt function:_OLD_VIRTUAL_PROMPT function global:prompt { Write-Host -NoNewline -ForegroundColor Green '__NODE_VIRTUAL_PROMPT__ ' _OLD_VIRTUAL_PROMPT } # Clear NODE_PATH if (Test-Path env:NODE_PATH) { copy-item env:NODE_PATH env:_OLD_VIRTUAL_NODE_PATH remove-item env:NODE_PATH } # Add the venv to the PATH copy-item env:PATH env:_OLD_VIRTUAL_PATH $env:PATH = "$env:NODE_VIRTUAL_ENV\Scripts;$env:PATH" """ ACTIVATE_SH = r""" # This file must be used with "source bin/activate" *from bash* # you cannot run it directly deactivate_node () { # reset old environment variables if [ -n "$_OLD_NODE_VIRTUAL_PATH" ] ; then PATH="$_OLD_NODE_VIRTUAL_PATH" export PATH unset _OLD_NODE_VIRTUAL_PATH NODE_PATH="$_OLD_NODE_PATH" export NODE_PATH unset _OLD_NODE_PATH NPM_CONFIG_PREFIX="$_OLD_NPM_CONFIG_PREFIX" npm_config_prefix="$_OLD_npm_config_prefix" export NPM_CONFIG_PREFIX export npm_config_prefix unset _OLD_NPM_CONFIG_PREFIX unset _OLD_npm_config_prefix fi # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the $PATH changes we made may not be respected if [ -n "$BASH" -o -n "$ZSH_VERSION" ] ; then hash -r fi if [ -n "$_OLD_NODE_VIRTUAL_PS1" ] ; then PS1="$_OLD_NODE_VIRTUAL_PS1" export PS1 unset _OLD_NODE_VIRTUAL_PS1 fi unset NODE_VIRTUAL_ENV if [ ! "$1" = "nondestructive" ] ; then # Self destruct! unset -f deactivate_node fi } freeze () { local NPM_VER=`npm -v | cut -d '.' -f 1` local re="[a-zA-Z0-9\.\-]+@[0-9]+\.[0-9]+\.[0-9]+([\+\-][a-zA-Z0-9\.\-]+)*" if [ "$NPM_VER" = '0' ]; then NPM_LIST=`npm list installed active 2>/dev/null | \ cut -d ' ' -f 1 | grep -v npm` else local npmls="npm ls -g" if [ "$1" = "-l" ]; then npmls="npm ls" shift fi NPM_LIST=$(eval ${npmls} | grep -E '^.{4}\w{1}'| \ grep -o -E "$re"| grep -v npm) fi if [ -z "$@" ]; then echo "$NPM_LIST" else echo "$NPM_LIST" > $@ fi } # unset irrelevant variables deactivate_node nondestructive # find the directory of this script # http://stackoverflow.com/a/246128 if [ "${BASH_SOURCE}" ] ; then SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done DIR="$( command cd -P "$( dirname "$SOURCE" )" > /dev/null && pwd )" NODE_VIRTUAL_ENV="$(dirname "$DIR")" else # dash not movable. fix use case: # dash -c " . node-env/bin/activate && node -v" NODE_VIRTUAL_ENV="__NODE_VIRTUAL_ENV__" fi # NODE_VIRTUAL_ENV is the parent of the directory where this script is export NODE_VIRTUAL_ENV _OLD_NODE_VIRTUAL_PATH="$PATH" PATH="$NODE_VIRTUAL_ENV/lib/node_modules/.bin:$NODE_VIRTUAL_ENV/__BIN_NAME__:$PATH" export PATH _OLD_NODE_PATH="$NODE_PATH" NODE_PATH="$NODE_VIRTUAL_ENV/__MOD_NAME__" export NODE_PATH _OLD_NPM_CONFIG_PREFIX="$NPM_CONFIG_PREFIX" _OLD_npm_config_prefix="$npm_config_prefix" NPM_CONFIG_PREFIX="__NPM_CONFIG_PREFIX__" npm_config_prefix="__NPM_CONFIG_PREFIX__" export NPM_CONFIG_PREFIX export npm_config_prefix if [ -z "$NODE_VIRTUAL_ENV_DISABLE_PROMPT" ] ; then _OLD_NODE_VIRTUAL_PS1="$PS1" if [ "x__NODE_VIRTUAL_PROMPT__" != x ] ; then PS1="__NODE_VIRTUAL_PROMPT__ $PS1" else if [ "`basename \"$NODE_VIRTUAL_ENV\"`" = "__" ] ; then # special case for Aspen magic directories # see http://www.zetadev.com/software/aspen/ PS1="[`basename \`dirname \"$NODE_VIRTUAL_ENV\"\``] $PS1" else PS1="(`basename \"$NODE_VIRTUAL_ENV\"`) $PS1" fi fi export PS1 fi # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the $PATH changes we made may not be respected if [ -n "$BASH" -o -n "$ZSH_VERSION" ] ; then hash -r fi """ ACTIVATE_FISH = """ # This file must be used with "source bin/activate.fish" *from fish* # you cannot run it directly function deactivate_node -d 'Exit nodeenv and return to normal environment.' # reset old environment variables if test -n "$_OLD_NODE_VIRTUAL_PATH" set -gx PATH $_OLD_NODE_VIRTUAL_PATH set -e _OLD_NODE_VIRTUAL_PATH end if test -n "$_OLD_NODE_PATH" set -gx NODE_PATH $_OLD_NODE_PATH set -e _OLD_NODE_PATH else set -e NODE_PATH end if test -n "$_OLD_NPM_CONFIG_PREFIX" set -gx NPM_CONFIG_PREFIX $_OLD_NPM_CONFIG_PREFIX set -e _OLD_NPM_CONFIG_PREFIX else set -e NPM_CONFIG_PREFIX end if test -n "$_OLD_npm_config_prefix" set -gx npm_config_prefix $_OLD_npm_config_prefix set -e _OLD_npm_config_prefix else set -e npm_config_prefix end if test -n "$_OLD_NODE_FISH_PROMPT_OVERRIDE" # Set an empty local `$fish_function_path` to allow the removal of # `fish_prompt` using `functions -e`. set -l fish_function_path # Prevents error when using nested fish instances if functions -q _node_old_fish_prompt # Erase virtualenv's `fish_prompt` and restore the original. functions -e fish_prompt functions -c _node_old_fish_prompt fish_prompt functions -e _node_old_fish_prompt end set -e _OLD_NODE_FISH_PROMPT_OVERRIDE end set -e NODE_VIRTUAL_ENV if test (count $argv) = 0 -o "$argv[1]" != "nondestructive" # Self destruct! functions -e deactivate_node end end function freeze -d 'Show a list of installed packages - like `pip freeze`' set -l NPM_VER (npm -v | cut -d '.' -f 1) set -l RE "[a-zA-Z0-9\\.\\-]+@[0-9]+\\.[0-9]+\\.[0-9]+([\\+\\-][a-zA-Z0-9\\.\\-]+)*" if test "$NPM_VER" = "0" set -g NPM_LIST (npm list installed active >/dev/null ^/dev/null | \ cut -d ' ' -f 1 | grep -v npm) else set -l NPM_LS "npm ls -g" if test (count $argv) -gt 0 -a "$argv[1]" = "-l" set NPM_LS "npm ls" set -e argv[1] end set -l NPM_LIST (eval $NPM_LS | grep -E '^.{4}\\w{1}' | \ grep -o -E "$re" | \ grep -v npm) end if test (count $argv) = 0 echo $NPM_LIST else echo $NPM_LIST > $argv[1] end end # unset irrelevant variables deactivate_node nondestructive # find the directory of this script begin set -l SOURCE (status filename) while test -L "$SOURCE" set SOURCE (readlink "$SOURCE") end set -l DIR (dirname (realpath "$SOURCE")) # NODE_VIRTUAL_ENV is the parent of the directory where this script is set -gx NODE_VIRTUAL_ENV (dirname "$DIR") end set -gx _OLD_NODE_VIRTUAL_PATH $PATH # The node_modules/.bin path doesn't exists and it will print a warning, and # that's why we redirect stderr to /dev/null :) set -gx PATH "$NODE_VIRTUAL_ENV/lib/node_modules/.bin" "$NODE_VIRTUAL_ENV/__BIN_NAME__" $PATH ^/dev/null if set -q NODE_PATH set -gx _OLD_NODE_PATH $NODE_PATH set -gx NODE_PATH "$NODE_VIRTUAL_ENV/__MOD_NAME__" $NODE_PATH else set -gx NODE_PATH "$NODE_VIRTUAL_ENV/__MOD_NAME__" end if set -q NPM_CONFIG_PREFIX set -gx _OLD_NPM_CONFIG_PREFIX $NPM_CONFIG_PREFIX end set -gx NPM_CONFIG_PREFIX "__NPM_CONFIG_PREFIX__" if set -q npm_config_prefix set -gx _OLD_npm_config_prefix $npm_config_prefix end set -gx npm_config_prefix "__NPM_CONFIG_PREFIX__" if test -z "$NODE_VIRTUAL_ENV_DISABLE_PROMPT" # Copy the current `fish_prompt` function as `_node_old_fish_prompt`. functions -c fish_prompt _node_old_fish_prompt function fish_prompt # Save the current $status, for fish_prompts that display it. set -l old_status $status # Prompt override provided? # If not, just prepend the environment name. if test -n "__NODE_VIRTUAL_PROMPT__" printf '%s%s ' "__NODE_VIRTUAL_PROMPT__" (set_color normal) else printf '%s(%s) ' (set_color normal) (basename "$NODE_VIRTUAL_ENV") end # Restore the original $status echo "exit $old_status" | source _node_old_fish_prompt end set -gx _OLD_NODE_FISH_PROMPT_OVERRIDE "$NODE_VIRTUAL_ENV" end """ # noqa: E501 PREDEACTIVATE_SH = """ if type -p deactivate_node > /dev/null; then deactivate_node;fi """ CYGWIN_NODE = """#!/bin/sh if [ -r "$1" ]; then SCRIPT_PATH=$(cygpath -w "$1") shift set - $SCRIPT_PATH $@ unset SCRIPT_PATH fi exec $(dirname "$0")/node.exe "$@" """ if __name__ == '__main__': main()