/
node_distribution.py
199 lines (167 loc) · 8.26 KB
/
node_distribution.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# coding=utf-8
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import absolute_import, division, print_function, unicode_literals
import filecmp
import logging
import os
import shutil
from pants.base.deprecated import deprecated_conditional
from pants.base.exceptions import TaskError
from pants.binaries.binary_tool import NativeTool
from pants.option.custom_types import dir_option, file_option
from pants.util.dirutil import safe_mkdir, safe_rmtree
from pants.util.memo import memoized_method, memoized_property
from pants.contrib.node.subsystems.command import command_gen
from pants.contrib.node.subsystems.package_managers import (PACKAGE_MANAGER_NPM,
PACKAGE_MANAGER_YARNPKG,
PACKAGE_MANAGER_YARNPKG_ALIAS,
VALID_PACKAGE_MANAGERS,
PackageManagerNpm,
PackageManagerYarnpkg)
from pants.contrib.node.subsystems.yarnpkg_distribution import YarnpkgDistribution
logger = logging.getLogger(__name__)
class NodeDistribution(NativeTool):
"""Represents a self-bootstrapping Node distribution."""
options_scope = 'node-distribution'
name = 'node'
default_version = 'v6.9.1'
archive_type = 'tgz'
@classmethod
def subsystem_dependencies(cls):
# Note that we use a YarnpkgDistribution scoped to the NodeDistribution, which may itself
# be scoped to a task.
return (super(NodeDistribution, cls).subsystem_dependencies() +
(YarnpkgDistribution.scoped(cls), ))
@classmethod
def register_options(cls, register):
super(NodeDistribution, cls).register_options(register)
register('--package-manager', advanced=True, default='npm', fingerprint=True,
choices=VALID_PACKAGE_MANAGERS,
help='Default package manager config for repo. Should be one of {}'.format(
VALID_PACKAGE_MANAGERS))
register('--eslint-setupdir', advanced=True, type=dir_option, fingerprint=True,
help='Find the package.json and yarn.lock under this dir '
'for installing eslint and plugins.')
register('--eslint-config', advanced=True, type=file_option, fingerprint=True,
help='The path to the global eslint configuration file specifying all the rules')
register('--eslint-ignore', advanced=True, type=file_option, fingerprint=True,
help='The path to the global eslint ignore path')
register('--eslint-version', default='4.15.0', fingerprint=True,
help='Use this ESLint version.')
@memoized_method
def _get_package_managers(self):
npm = PackageManagerNpm([self._install_node])
yarnpkg = PackageManagerYarnpkg([self._install_node, self._install_yarnpkg])
return {
PACKAGE_MANAGER_NPM: npm,
PACKAGE_MANAGER_YARNPKG: yarnpkg,
PACKAGE_MANAGER_YARNPKG_ALIAS: yarnpkg, # Allow yarn to be used as an alias for yarnpkg
}
def get_package_manager(self, package_manager=None):
package_manager = package_manager or self.get_options().package_manager
package_manager_obj = self._get_package_managers().get(package_manager)
if not package_manager_obj:
raise TaskError(
'Unknown package manager: {}.\nValid values are {}.'.format(
package_manager, list(NodeDistribution.VALID_PACKAGE_MANAGER_LIST.keys())
))
return package_manager_obj
@memoized_method
def version(self, context=None):
# The versions reported by node and embedded in distribution package names are 'vX.Y.Z'.
# TODO: After the deprecation cycle is over we'll expect the values of the version option
# to already include the 'v' prefix, so there will be no need to normalize, and we can
# delete this entire method override.
version = super(NodeDistribution, self).version(context)
deprecated_conditional(
lambda: not version.startswith('v'), entity_description='', removal_version='1.7.0.dev0',
hint_message='value of --version in scope {} must be of the form '
'vX.Y.Z'.format(self.options_scope))
return version if version.startswith('v') else 'v' + version
@classmethod
def _normalize_version(cls, version):
# The versions reported by node and embedded in distribution package names are 'vX.Y.Z' and not
# 'X.Y.Z'.
return version if version.startswith('v') else 'v' + version
@memoized_property
def eslint_setupdir(self):
return self.get_options().eslint_setupdir
@memoized_property
def eslint_version(self):
return self.get_options().eslint_version
@memoized_property
def eslint_config(self):
return self.get_options().eslint_config
@memoized_property
def eslint_ignore(self):
return self.get_options().eslint_ignore
@memoized_method
def _install_node(self):
"""Install the Node distribution from pants support binaries.
:returns: The Node distribution bin path.
:rtype: string
"""
node_package_path = self.select()
# Todo: https://github.com/pantsbuild/pants/issues/4431
# This line depends on repacked node distribution.
# Should change it from 'node/bin' to 'dist/bin'
node_bin_path = os.path.join(node_package_path, 'node', 'bin')
return node_bin_path
@memoized_method
def _install_yarnpkg(self):
"""Install the Yarnpkg distribution from pants support binaries.
:returns: The Yarnpkg distribution bin path.
:rtype: string
"""
yarnpkg_package_path = YarnpkgDistribution.scoped_instance(self).select()
yarnpkg_bin_path = os.path.join(yarnpkg_package_path, 'dist', 'bin')
return yarnpkg_bin_path
def node_command(self, args=None, node_paths=None):
"""Creates a command that can run `node`, passing the given args to it.
:param list args: An optional list of arguments to pass to `node`.
:param list node_paths: An optional list of paths to node_modules.
:returns: A `node` command that can be run later.
:rtype: :class:`NodeDistribution.Command`
"""
# NB: We explicitly allow no args for the `node` command unlike the `npm` command since running
# `node` with no arguments is useful, it launches a REPL.
return command_gen([self._install_node], 'node', args=args, node_paths=node_paths)
def _configure_eslinter(self, bootstrapped_support_path):
logger.debug('Copying {setupdir} to bootstrapped dir: {support_path}'
.format(setupdir=self.eslint_setupdir,
support_path=bootstrapped_support_path))
safe_rmtree(bootstrapped_support_path)
shutil.copytree(self.eslint_setupdir, bootstrapped_support_path)
return True
_eslint_required_files = ['yarn.lock', 'package.json']
def eslint_supportdir(self, task_workdir):
""" Returns the path where the ESLint is bootstrapped.
:param string task_workdir: The task's working directory
:returns: The path where ESLint is bootstrapped and whether or not it is configured
:rtype: (string, bool)
"""
bootstrapped_support_path = os.path.join(task_workdir, 'eslint')
# TODO(nsaechao): Should only have to check if the "eslint" dir exists in the task_workdir
# assuming fingerprinting works as intended.
# If the eslint_setupdir is not provided or missing required files, then
# clean up the directory so that Pants can install a pre-defined eslint version later on.
# Otherwise, if there is no configurations changes, rely on the cache.
# If there is a config change detected, use the new configuration.
if self.eslint_setupdir:
configured = all(os.path.exists(os.path.join(self.eslint_setupdir, f))
for f in self._eslint_required_files)
else:
configured = False
if not configured:
safe_mkdir(bootstrapped_support_path, clean=True)
else:
try:
installed = all(filecmp.cmp(
os.path.join(self.eslint_setupdir, f), os.path.join(bootstrapped_support_path, f))
for f in self._eslint_required_files)
except OSError:
installed = False
if not installed:
self._configure_eslinter(bootstrapped_support_path)
return bootstrapped_support_path, configured