1# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- 2# vim: set filetype=python: 3# This Source Code Form is subject to the terms of the Mozilla Public 4# License, v. 2.0. If a copy of the MPL was not distributed with this 5# file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7from filecmp import dircmp 8import json 9import os 10import platform 11import re 12from mozfile.mozfile import remove as mozfileremove 13import subprocess 14import sys 15from distutils.version import LooseVersion 16sys.path.append(os.path.join( 17 os.path.dirname(__file__), "..", "..", "..", "third_party", "python", "which")) 18import which 19 20NODE_MIN_VERSION = "6.9.1" 21NPM_MIN_VERSION = "3.10.8" 22 23NODE_MACHING_VERSION_NOT_FOUND_MESSAGE = """ 24nodejs is out of date. You currently have node v%s but v%s is required. 25Please update nodejs from https://nodejs.org and try again. 26""".strip() 27 28NPM_MACHING_VERSION_NOT_FOUND_MESSAGE = """ 29npm is out of date. You currently have npm v%s but v%s is required. 30You can usually update npm with: 31 32npm i -g npm 33""".strip() 34 35NODE_NOT_FOUND_MESSAGE = """ 36nodejs is either not installed or is installed to a non-standard path. 37Please install nodejs from https://nodejs.org and try again. 38 39Valid installation paths: 40""".strip() 41 42NPM_NOT_FOUND_MESSAGE = """ 43Node Package Manager (npm) is either not installed or installed to a 44non-standard path. Please install npm from https://nodejs.org (it comes as an 45option in the node installation) and try again. 46 47Valid installation paths: 48""".strip() 49 50 51VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$") 52CARET_VERSION_RANGE_RE = re.compile(r"^\^((\d+)\.\d+\.\d+)$") 53 54project_root = None 55 56 57def eslint_maybe_setup(): 58 """Setup ESLint only if it is needed.""" 59 has_issues, needs_clobber = eslint_module_needs_setup() 60 61 if has_issues: 62 eslint_setup(needs_clobber) 63 64 65def eslint_setup(should_clobber=False): 66 """Ensure eslint is optimally configured. 67 68 This command will inspect your eslint configuration and 69 guide you through an interactive wizard helping you configure 70 eslint for optimal use on Mozilla projects. 71 """ 72 orig_cwd = os.getcwd() 73 sys.path.append(os.path.dirname(__file__)) 74 75 # npm sometimes fails to respect cwd when it is run using check_call so 76 # we manually switch folders here instead. 77 project_root = get_project_root() 78 os.chdir(project_root) 79 80 if should_clobber: 81 node_modules_path = os.path.join(project_root, "node_modules") 82 print("Clobbering node_modules...") 83 if sys.platform.startswith('win') and have_winrm(): 84 process = subprocess.Popen(['winrm', '-rf', node_modules_path]) 85 process.wait() 86 else: 87 mozfileremove(node_modules_path) 88 89 npm_path = get_node_or_npm_path("npm") 90 if not npm_path: 91 return 1 92 93 extra_parameters = ["--loglevel=error"] 94 95 # Install ESLint and external plugins 96 cmd = [npm_path, "install"] 97 cmd.extend(extra_parameters) 98 print("Installing eslint for mach using \"%s\"..." % (" ".join(cmd))) 99 if not call_process("eslint", cmd): 100 return 1 101 102 eslint_path = os.path.join(get_project_root(), "node_modules", ".bin", "eslint") 103 104 print("\nESLint and approved plugins installed successfully!") 105 print("\nNOTE: Your local eslint binary is at %s\n" % eslint_path) 106 107 os.chdir(orig_cwd) 108 109 110def call_process(name, cmd, cwd=None): 111 try: 112 with open(os.devnull, "w") as fnull: 113 subprocess.check_call(cmd, cwd=cwd, stdout=fnull) 114 except subprocess.CalledProcessError: 115 if cwd: 116 print("\nError installing %s in the %s folder, aborting." % (name, cwd)) 117 else: 118 print("\nError installing %s, aborting." % name) 119 120 return False 121 122 return True 123 124 125def expected_eslint_modules(): 126 # Read the expected version of ESLint and external modules 127 expected_modules_path = os.path.join(get_project_root(), "package.json") 128 with open(expected_modules_path, "r") as f: 129 expected_modules = json.load(f)["dependencies"] 130 131 # Also read the in-tree ESLint plugin mozilla information, to ensure the 132 # dependencies are up to date. 133 mozilla_json_path = os.path.join(get_eslint_module_path(), 134 "eslint-plugin-mozilla", "package.json") 135 with open(mozilla_json_path, "r") as f: 136 expected_modules.update(json.load(f)["dependencies"]) 137 138 # Also read the in-tree ESLint plugin spidermonkey information, to ensure the 139 # dependencies are up to date. 140 mozilla_json_path = os.path.join(get_eslint_module_path(), 141 "eslint-plugin-spidermonkey-js", "package.json") 142 with open(mozilla_json_path, "r") as f: 143 expected_modules.update(json.load(f)["dependencies"]) 144 145 return expected_modules 146 147 148def check_eslint_files(node_modules_path, name): 149 def check_file_diffs(dcmp): 150 # Diff files only looks at files that are different. Not for files 151 # that are only present on one side. This should be generally OK as 152 # new files will need to be added in the index.js for the package. 153 if dcmp.diff_files and dcmp.diff_files != ['package.json']: 154 return True 155 156 result = False 157 158 # Again, we only look at common sub directories for the same reason 159 # as above. 160 for sub_dcmp in dcmp.subdirs.values(): 161 result = result or check_file_diffs(sub_dcmp) 162 163 return result 164 165 dcmp = dircmp(os.path.join(node_modules_path, name), 166 os.path.join(get_eslint_module_path(), name)) 167 168 return check_file_diffs(dcmp) 169 170 171def eslint_module_needs_setup(): 172 has_issues = False 173 needs_clobber = False 174 node_modules_path = os.path.join(get_project_root(), "node_modules") 175 176 for name, expected_data in expected_eslint_modules().iteritems(): 177 # expected_eslint_modules returns a string for the version number of 178 # dependencies for installation of eslint generally, and an object 179 # for our in-tree plugins (which contains the entire module info). 180 if "version" in expected_data: 181 version_range = expected_data["version"] 182 else: 183 version_range = expected_data 184 185 path = os.path.join(node_modules_path, name, "package.json") 186 187 if not os.path.exists(path): 188 print("%s v%s needs to be installed locally." % (name, version_range)) 189 has_issues = True 190 continue 191 192 data = json.load(open(path)) 193 194 if version_range.startswith("file:"): 195 # We don't need to check local file installations for versions, as 196 # these are symlinked, so we'll always pick up the latest. 197 continue 198 199 if name == "eslint" and LooseVersion("4.0.0") > LooseVersion(data["version"]): 200 print("ESLint is an old version, clobbering node_modules directory") 201 needs_clobber = True 202 has_issues = True 203 continue 204 205 if not version_in_range(data["version"], version_range): 206 print("%s v%s should be v%s." % (name, data["version"], version_range)) 207 has_issues = True 208 continue 209 210 return has_issues, needs_clobber 211 212 213def version_in_range(version, version_range): 214 """ 215 Check if a module version is inside a version range. Only supports explicit versions and 216 caret ranges for the moment, since that's all we've used so far. 217 """ 218 if version == version_range: 219 return True 220 221 version_match = VERSION_RE.match(version) 222 if not version_match: 223 raise RuntimeError("mach eslint doesn't understand module version %s" % version) 224 version = LooseVersion(version) 225 226 # Caret ranges as specified by npm allow changes that do not modify the left-most non-zero 227 # digit in the [major, minor, patch] tuple. The code below assumes the major digit is 228 # non-zero. 229 range_match = CARET_VERSION_RANGE_RE.match(version_range) 230 if range_match: 231 range_version = range_match.group(1) 232 range_major = int(range_match.group(2)) 233 234 range_min = LooseVersion(range_version) 235 range_max = LooseVersion("%d.0.0" % (range_major + 1)) 236 237 return range_min <= version < range_max 238 239 return False 240 241 242def get_possible_node_paths_win(): 243 """ 244 Return possible nodejs paths on Windows. 245 """ 246 if platform.system() != "Windows": 247 return [] 248 249 return list({ 250 "%s\\nodejs" % os.environ.get("SystemDrive"), 251 os.path.join(os.environ.get("ProgramFiles"), "nodejs"), 252 os.path.join(os.environ.get("PROGRAMW6432"), "nodejs"), 253 os.path.join(os.environ.get("PROGRAMFILES"), "nodejs") 254 }) 255 256 257def simple_which(filename, path=None): 258 exts = [".cmd", ".exe", ""] if platform.system() == "Windows" else [""] 259 260 for ext in exts: 261 try: 262 return which.which(filename + ext, path) 263 except which.WhichError: 264 pass 265 266 # If we got this far, we didn't find it with any of the extensions, so 267 # just return. 268 return None 269 270 271def which_path(filename): 272 """ 273 Return the nodejs or npm path. 274 """ 275 # Look in the system path first. 276 path = simple_which(filename) 277 if path is not None: 278 return path 279 280 if platform.system() == "Windows": 281 # If we didn't find it fallback to the non-system paths. 282 path = simple_which(filename, get_possible_node_paths_win()) 283 elif filename == "node": 284 path = simple_which("nodejs") 285 286 return path 287 288 289def get_node_or_npm_path(filename, minversion=None): 290 node_or_npm_path = which_path(filename) 291 292 if not node_or_npm_path: 293 if filename in ('node', 'nodejs'): 294 print(NODE_NOT_FOUND_MESSAGE) 295 elif filename == "npm": 296 print(NPM_NOT_FOUND_MESSAGE) 297 298 if platform.system() == "Windows": 299 app_paths = get_possible_node_paths_win() 300 301 for p in app_paths: 302 print(" - %s" % p) 303 elif platform.system() == "Darwin": 304 print(" - /usr/local/bin/{}".format(filename)) 305 elif platform.system() == "Linux": 306 print(" - /usr/bin/{}".format(filename)) 307 308 return None 309 310 if not minversion: 311 return node_or_npm_path 312 313 version_str = get_version(node_or_npm_path).lstrip('v') 314 315 version = LooseVersion(version_str) 316 317 if version > minversion: 318 return node_or_npm_path 319 320 if filename == "npm": 321 print(NPM_MACHING_VERSION_NOT_FOUND_MESSAGE % (version_str.strip(), minversion)) 322 else: 323 print(NODE_MACHING_VERSION_NOT_FOUND_MESSAGE % (version_str.strip(), minversion)) 324 325 return None 326 327 328def get_version(path): 329 try: 330 version_str = subprocess.check_output([path, "--version"], 331 stderr=subprocess.STDOUT) 332 return version_str 333 except (subprocess.CalledProcessError, OSError): 334 return None 335 336 337def set_project_root(root=None): 338 """Sets the project root to the supplied path, or works out what the root 339 is based on looking for 'mach'. 340 341 Keyword arguments: 342 root - (optional) The path to set the root to. 343 """ 344 global project_root 345 346 if root: 347 project_root = root 348 return 349 350 file_found = False 351 folder = os.getcwd() 352 353 while (folder): 354 if os.path.exists(os.path.join(folder, 'mach')): 355 file_found = True 356 break 357 else: 358 folder = os.path.dirname(folder) 359 360 if file_found: 361 project_root = os.path.abspath(folder) 362 363 364def get_project_root(): 365 """Returns the absolute path to the root of the project, see set_project_root() 366 for how this is determined. 367 """ 368 global project_root 369 370 if not project_root: 371 set_project_root() 372 373 return project_root 374 375 376def get_eslint_module_path(): 377 return os.path.join(get_project_root(), "tools", "lint", "eslint") 378 379 380def check_node_executables_valid(): 381 # eslint requires at least node 6.9.1 382 node_path = get_node_or_npm_path("node", LooseVersion(NODE_MIN_VERSION)) 383 if not node_path: 384 return False 385 386 npm_path = get_node_or_npm_path("npm", LooseVersion(NPM_MIN_VERSION)) 387 if not npm_path: 388 return False 389 390 return True 391 392 393def have_winrm(): 394 # `winrm -h` should print 'winrm version ...' and exit 1 395 try: 396 p = subprocess.Popen(['winrm.exe', '-h'], 397 stdout=subprocess.PIPE, 398 stderr=subprocess.STDOUT) 399 return p.wait() == 1 and p.stdout.read().startswith('winrm') 400 except Exception: 401 return False 402