diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index 8dc4e8ba..37ce7440 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -13,6 +13,7 @@ from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers +from pre_commit.languages.node_env import install_node from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix from pre_commit.util import clean_path_on_failure @@ -89,27 +90,22 @@ def install_environment( if sys.platform == 'win32': # pragma: no cover envdir = fr'\\?\{os.path.normpath(envdir)}' with clean_path_on_failure(envdir): - cmd = [ - sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, - ] - if version != C.DEFAULT: - cmd.extend(['-n', version]) - cmd_output_b(*cmd) + npm = install_node(envdir, version) with in_env(prefix, version): # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 # install as if we installed from git local_install_cmd = ( - 'npm', 'install', '--dev', '--prod', + npm, 'install', '--dev', '--prod', '--ignore-prepublish', '--no-progress', '--no-save', ) helpers.run_setup_cmd(prefix, local_install_cmd) - _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) + _, pkg, _ = cmd_output(npm, 'pack', cwd=prefix.prefix_dir) pkg = prefix.path(pkg.strip()) - install = ('npm', 'install', '-g', pkg, *additional_dependencies) + install = (npm, 'install', '-g', pkg, *additional_dependencies) helpers.run_setup_cmd(prefix, install) # clean these up after installation diff --git a/pre_commit/languages/node_env.py b/pre_commit/languages/node_env.py new file mode 100644 index 00000000..ade375fa --- /dev/null +++ b/pre_commit/languages/node_env.py @@ -0,0 +1,108 @@ +import io +import os +import platform +import re +import sys +import sysconfig +from tarfile import TarFile +from tarfile import TarInfo +from typing import Generator +from urllib.parse import urljoin +from urllib.request import urlopen +from zipfile import ZipFile + +import pre_commit.constants as C + +ARCHITECTURES = { + '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 +} + +NIX_NODE_SUBDIRS = re.compile(r'node[^/]+/(bin|lib|include|share)') +WINDOWS_NODE_SUBDIRS = re.compile(r'node[^/]+(np|node)') + + +def install_node(envdir: str, language_version: str) -> str: + windows = sys.platform in ('cygwin', 'win32') + + if sysconfig.get_config_var('HOST_GNU_TYPE') == 'x86_64-pc-linux-musl': + domain = 'https://unofficial-builds.nodejs.org' + suffix = 'linux-x64-musl.tar.gz' + else: + domain = 'https://nodejs.org' + + arch = ARCHITECTURES[platform.machine()] + if windows: + suffix = f'win-{arch}.zip' + else: + suffix = f'{platform.system().lower()}-{arch}.tar.gz' + + version = _node_version(domain, language_version) + + archive_name = f'node-{version}-{suffix}' + with urlopen( + urljoin(domain, f'download/release/{version}/{archive_name}'), + ) as release: + compressed = io.BytesIO(release.read()) + + # TODO should i just extractall + rm the extra bits instead of partially + # extracting & renaming? + + if windows: + # TODO this doesn't quite work + def renamed_members(z: ZipFile) -> Generator[str, None, None]: + prefix_len = len(f'{archive_name}/') + for m in z.filelist: + if WINDOWS_NODE_SUBDIRS.match(m.filename): + m.filename = m.filename[prefix_len:] + yield m.filename + + with ZipFile(compressed) as z: + z.extractall(envdir, renamed_members(z)) + + raise NotImplementedError('TODO') + + else: + def rename_members_tf(tf: TarFile) -> Generator[TarInfo, None, None]: + prefix_len = len(f'{archive_name}/') + for m in tf.getmembers(): + if NIX_NODE_SUBDIRS.match(m.name): + m.path = m.path[prefix_len:] + yield m + + with TarFile.open(fileobj=compressed) as tf: + tf.extractall(envdir, rename_members_tf(tf)) # type: ignore + + for file in ('npm', 'npx', 'node'): + path = os.path.join(envdir, 'bin', file) + os.chmod(path, os.stat(path).st_mode | 0o111) + os.symlink('node', os.path.join(envdir, 'bin', 'nodejs')) + + return os.path.join(envdir, 'bin', 'npm') + + +def _node_version(domain: str, language_version: str) -> str: + """Looks up the node.js version based on the configured + version (uses the latest by default)""" + if language_version == C.DEFAULT: + # grab latest + with urlopen(urljoin(domain, 'download/release/index.tab')) as index: + _ = next(index) # header + version = next(index).split()[0].decode() + else: + version = f'v{language_version.replace("v", "")}' + + return version diff --git a/setup.cfg b/setup.cfg index ceb1cd4c..d4fbb630 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,6 @@ packages = find: install_requires = cfgv>=2.0.0 identify>=1.0.0 - nodeenv>=0.11.1 pyyaml>=5.1 toml virtualenv>=20.0.8