def find_version_in_range(versions, semver_range): '''versions is an array of semver versions and semver_range is a semver range expression. Assumes the environment has `node` on the $PATH. ''' logging.debug('Trying to match %s against %s.', semver_range, versions) if platform_checker.is_windows(): shell = True # Because we have to use `shell=True` on Windows due to https://bugs.python.org/issue17023, # we have to do our own escaping. In particular, if semver_range is '>=0.12.0' and is not # escaped correctly, then Windows will try to write to a file with that name. The built-in # escaping done by subprocess.Popen() does not appear to do the right thing here (it uses # single quotes instead of double quotes, which is not sufficient), so we must do the # escaping ourselves and convert the args into a string to override the default escaping. semver_range = '"%s"' % semver_range else: shell = False args = [ platform_checker.get_node_executable(), semver, '--range', semver_range ] + versions if platform_checker.is_windows(): args = ' '.join(args) proc = subprocess.Popen(args, stdout=subprocess.PIPE, shell=shell) matching_versions = [] for line in proc.stdout: matching_versions.append(line.rstrip()) proc.wait() if proc.returncode: return None else: return matching_versions
def link_dependencys_executable(node_modules_path, dependency_name): dependency_root = os.path.join(node_modules_path, dependency_name) dependency_config = json_load(os.path.join(dependency_root, 'package.json')) # The bin field would ether be a dict or a string. if it's a dict, # such as `{ "name": "test", "bin" : { "myapp" : "./cli.js" } }`, we should create a # symlink from ./node_modules/test/cli.js to ./node_modules/.bin/myapp. # If it's a string, like `{ "name": "test", "bin" : "./cli.js" }`, then the symlink's name # should be name of the package, in this case, it should be ./node_modules/.bin/test . bin_config = dependency_config.get('bin') if not bin_config: return elif isinstance(bin_config, dict): symlinks_to_create = bin_config else: symlinks_to_create = {dependency_name: bin_config} dot_bin_path = os.path.join(node_modules_path, '.bin') if platform_checker.is_windows(): fs.mkdirs(dot_bin_path) for dst_name, relative_src_path in symlinks_to_create.items(): absolute_dst_path = os.path.join(dot_bin_path, dst_name) absolute_src_path = os.path.join(dependency_root, relative_src_path) if platform_checker.is_windows(): shutil.copyfile(absolute_src_path, absolute_dst_path) else: symlink(absolute_src_path, absolute_dst_path, relative=True)
def link_dependencys_executable(node_modules_path, dependency_name): dependency_root = os.path.join(node_modules_path, dependency_name) dependency_config = json_load(os.path.join(dependency_root, "package.json")) # The bin field would ether be a dict or a string. if it's a dict, # such as `{ "name": "test", "bin" : { "myapp" : "./cli.js" } }`, we should create a # symlink from ./node_modules/test/cli.js to ./node_modules/.bin/myapp. # If it's a string, like `{ "name": "test", "bin" : "./cli.js" }`, then the symlink's name # should be name of the package, in this case, it should be ./node_modules/.bin/test . bin_config = dependency_config.get("bin") if not bin_config: return elif isinstance(bin_config, dict): symlinks_to_create = bin_config else: symlinks_to_create = {dependency_name: bin_config} dot_bin_path = os.path.join(node_modules_path, ".bin") if platform_checker.is_windows(): fs.mkdirs(dot_bin_path) for dst_name, relative_src_path in symlinks_to_create.items(): absolute_dst_path = os.path.join(dot_bin_path, dst_name) absolute_src_path = os.path.join(dependency_root, relative_src_path) if platform_checker.is_windows(): shutil.copyfile(absolute_src_path, absolute_dst_path) else: symlink(absolute_src_path, absolute_dst_path, relative=True)
def find_version_in_range(versions, semver_range): '''versions is an array of semver versions and semver_range is a semver range expression. Assumes the environment has `node` on the $PATH. ''' logging.debug('Trying to match %s against %s.', semver_range, versions) if platform_checker.is_windows(): shell = True # Because we have to use `shell=True` on Windows due to https://bugs.python.org/issue17023, # we have to do our own escaping. In particular, if semver_range is '>=0.12.0' and is not # escaped correctly, then Windows will try to write to a file with that name. The built-in # escaping done by subprocess.Popen() does not appear to do the right thing here (it uses # single quotes instead of double quotes, which is not sufficient), so we must do the # escaping ourselves and convert the args into a string to override the default escaping. semver_range = '"%s"' % semver_range else: shell = False args = [platform_checker.get_node_executable(), semver, '--range', semver_range] + versions if platform_checker.is_windows(): args = ' '.join(args) proc = subprocess.Popen(args, stdout=subprocess.PIPE, shell=shell) matching_versions = [] for line in proc.stdout: matching_versions.append(line.rstrip()) proc.wait() if proc.returncode: return None else: return matching_versions
def run_js_test(test_runner, pkg_path, name): """Run `apm test` or `npm test` in the given pkg_path.""" logging.info('Running `%s test` in %s...', test_runner, pkg_path) # In Atom 1.2+, "apm test" exits with an error when there is no "spec" directory if test_runner == 'apm' and not os.path.isdir( os.path.join(pkg_path, 'spec')): logging.info('NO TESTS TO RUN FOR: %s', name) return if test_runner == 'apm': test_args = ['node', '--harmony', APM_TEST_WRAPPER, pkg_path] else: test_args = ['npm', 'test'] proc = subprocess.Popen(test_args, cwd=pkg_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=platform_checker.is_windows()) stdout = [] for line in proc.stdout: # line is a bytes string literal in Python 3. logging.info('[%s test %s]: %s', test_runner, name, line.rstrip().decode('utf-8')) stdout.append(line) proc.wait() if proc.returncode: logging.info('TEST FAILED: %s\nstdout:\n%s', name, '\n'.join(stdout)) raise Exception('TEST FAILED: %s test %s' % (test_runner, name)) else: logging.info('TEST PASSED: %s', name)
def run_js_test(test_runner, pkg_path, name): """Run `apm test` or `npm test` in the given pkg_path.""" logging.info('Running `%s test` in %s...', test_runner, pkg_path) # Add --one option to `apm test` to ensure no deprecated APIs are used: # http://blog.atom.io/2015/05/01/removing-deprecated-apis.html. test_args = [test_runner, 'test'] if test_runner == 'apm': test_args += ['--one'] proc = subprocess.Popen(test_args, cwd=pkg_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=platform_checker.is_windows()) stdout = [] for line in proc.stdout: # line is a bytes string literal in Python 3. logging.info('[%s test %s]: %s', test_runner, name, line.rstrip().decode('utf-8')) stdout.append(line) proc.wait() if proc.returncode: logging.info('TEST FAILED: %s\nstdout:\n%s', name, '\n'.join(stdout)) raise Exception('TEST FAILED: %s test %s' % (test_runner, name)) else: logging.info('TEST PASSED: %s', name)
def run_js_test(test_runner, pkg_path, name): """Run `apm test` or `npm test` in the given pkg_path.""" logging.info('Running `%s test` in %s...', test_runner, pkg_path) # Add --one option to `apm test` to ensure no deprecated APIs are used: # http://blog.atom.io/2015/05/01/removing-deprecated-apis.html. test_args = [test_runner, 'test'] if test_runner == 'apm': test_args += ['--one'] proc = subprocess.Popen( test_args, cwd=pkg_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=platform_checker.is_windows()) stdout = [] for line in proc.stdout: # line is a bytes string literal in Python 3. logging.info('[%s test %s]: %s', test_runner, name, line.rstrip().decode('utf-8')) stdout.append(line) proc.wait() if proc.returncode: logging.info('TEST FAILED: %s\nstdout:\n%s', name, '\n'.join(stdout)) raise Exception('TEST FAILED: %s test %s' % (test_runner, name)) else: logging.info('TEST PASSED: %s', name)
def run_js_test(test_runner, pkg_path, name): """Run `apm test` or `npm test` in the given pkg_path.""" logging.info('Running `%s test` in %s...', test_runner, pkg_path) # In Atom 1.2+, "apm test" exits with an error when there is no "spec" directory if test_runner == 'apm' and not os.path.isdir(os.path.join(pkg_path, 'spec')): logging.info('NO TESTS TO RUN FOR: %s', name) return if test_runner == 'apm': test_args = ['node', '--harmony', APM_TEST_WRAPPER, pkg_path] else: test_args = ['npm', 'test'] proc = subprocess.Popen( test_args, cwd=pkg_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=platform_checker.is_windows()) stdout = [] for line in proc.stdout: # line is a bytes string literal in Python 3. logging.info('[%s test %s]: %s', test_runner, name, line.rstrip().decode('utf-8')) stdout.append(line) proc.wait() if proc.returncode: logging.info('TEST FAILED: %s\nstdout:\n%s', name, '\n'.join(stdout)) raise Exception('TEST FAILED: %s test %s' % (test_runner, name)) else: logging.info('TEST PASSED: %s', name)
def install(self): if platform_checker.is_windows(): # Python's multiprocessing library has issues on Windows, so it seems easiest to avoid it: # https://docs.python.org/2/library/multiprocessing.html#windows self._do_serial_install() else: self._do_multiprocess_install()
def run_flow_check(pkg_path, name, show_all): """Run a flow typecheck in the given pkg_path.""" logging.info('Running `flow check` in %s...', pkg_path) test_args = ['flow', 'check'] if show_all: test_args.append('--show-all-errors') proc = subprocess.Popen( test_args, cwd=pkg_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=platform_checker.is_windows()) stdout = [] for line in proc.stdout: # line is a bytes string literal in Python 3. logging.info('[flow check %s]: %s', name, line.rstrip().decode('utf-8')) stdout.append(line) proc.wait() if proc.returncode: logging.info('FLOW CHECK FAILED: %s\nstdout:\n%s', name, '\n'.join(stdout)) raise Exception('FLOW CHECK FAILED: flow test %s' % name) else: logging.info('FLOW CHECK PASSED: %s', name)
def run_flow_check(pkg_path, name, show_all): """Run a flow typecheck in the given pkg_path.""" logging.info('Running `flow check` in %s...', pkg_path) test_args = ['flow', 'check'] if show_all: test_args.append('--show-all-errors') proc = subprocess.Popen(test_args, cwd=pkg_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=platform_checker.is_windows()) stdout = [] for line in proc.stdout: # line is a bytes string literal in Python 3. logging.info('[flow check %s]: %s', name, line.rstrip().decode('utf-8')) stdout.append(line) proc.wait() if proc.returncode: logging.info('FLOW CHECK FAILED: %s\nstdout:\n%s', name, '\n'.join(stdout)) raise Exception('FLOW CHECK FAILED: flow test %s' % name) else: logging.info('FLOW CHECK PASSED: %s', name)
def run_tests(self): apm_tests = [] npm_tests = [] serial_only_tests = [] for package_config in self._package_manager.get_configs(): pkg_path = package_config['packageRootAbsolutePath'] name = package_config['name'] # We run the tests in Nuclide/spec in a separate integration test step. if name == 'nuclide': continue if package_config['excludeTestsFromContinuousIntegration']: continue test_runner = package_config['testRunner'] if test_runner == 'apm' and not self._include_apm: continue if self._packages_to_test and name not in self._packages_to_test: continue test_args = (test_runner, pkg_path, name) if package_config['testsCannotBeRunInParallel']: test_bucket = serial_only_tests elif test_runner == 'npm': test_bucket = npm_tests else: test_bucket = apm_tests test_bucket.append(test_args) if self._run_in_band or platform_checker.is_windows(): # We run all tests in serial on Windows because Python's multiprocessing library has issues: # https://docs.python.org/2/library/multiprocessing.html#windows parallel_tests = [] serial_tests = npm_tests + apm_tests else: # Currently, all tests appear to be able to be run in parallel. We keep this code # here in case we have to special-case any tests (on a short-term basis) to be run # serially after all of the parallel tests have finished. parallel_tests = npm_tests + apm_tests serial_tests = [] serial_tests += serial_only_tests serial_tests += [('flow', self._package_manager.get_nuclide_path(), 'nuclide')] if parallel_tests: pool = Pool(processes=max(1, cpu_count() - 2)) results = [pool.apply_async(run_test, args=test_args) for test_args in parallel_tests] for async_result in results: async_result.wait() if not async_result.successful(): raise async_result.get() for test_args in serial_tests: (test_runner, pkg_path, name) = test_args run_test(test_runner, pkg_path, name)
def run_js_test(test_runner, pkg_path, name): """Run `apm test` or `npm test` in the given pkg_path.""" logging.info('Running `%s test` in %s...', test_runner, pkg_path) # In Atom 1.2+, "apm test" exits with an error when there is no "spec" directory if test_runner == 'apm' and not os.path.isdir( os.path.join(pkg_path, 'spec')): logging.info('NO TESTS TO RUN FOR: %s', name) return if test_runner == 'apm': test_args = ['node', '--harmony', APM_TEST_WRAPPER, pkg_path] else: test_args = ['npm', 'test'] for tries in xrange(0, RETRY_LIMIT): proc = subprocess.Popen(test_args, cwd=pkg_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=platform_checker.is_windows()) retry = False stdout = [] for line in proc.stdout: # line is a bytes string literal in Python 3. logging.info('[%s test %s]: %s', test_runner, name, line.rstrip().decode('utf-8')) stdout.append(line) # There are some loglines that correlate highly to test failures due to a bug in atom, # so we retry the test if we see this bad test output. See: D2809659 for an explanation. if test_runner == 'apm' and tries < RETRY_LIMIT - 1 and BAD_TEST_OUTPUT in line: logging.info('[%s test %s]: %s: %s', test_runner, name, 'Aborting test due to bad output', BAD_TEST_OUTPUT) retry = True break proc.wait() if retry: continue if proc.returncode: logging.info('TEST FAILED: %s\nstdout:\n%s', name, '\n'.join(stdout)) raise Exception('TEST FAILED: %s test %s' % (test_runner, name)) else: logging.info('TEST PASSED: %s', name) break
def run_tests(self): apm_tests = [] npm_tests = [] flow_tests = [] for package_config in self._package_manager.get_configs(): if package_config['excludeTestsFromContinuousIntegration']: continue test_runner = package_config['testRunner'] if test_runner == 'apm' and not self._include_apm: continue pkg_path = package_config['packageRootAbsolutePath'] name = package_config['name'] test_args = (test_runner, pkg_path, name) test_bucket = npm_tests if test_runner == 'npm' else apm_tests test_bucket.append(test_args) if package_config['flowCheck']: flow_tests.append(('flow', pkg_path, name)) if platform_checker.is_windows(): # We run all tests in serial on Windows because Python's multiprocessing library has issues: # https://docs.python.org/2/library/multiprocessing.html#windows parallel_tests = [] serial_tests = npm_tests + apm_tests + flow_tests else: # Currently, all tests appear to be able to be run in parallel. We keep this code # here in case we have to special-case any tests (on a short-term basis) to be run # serially after all of the parallel tests have finished. parallel_tests = npm_tests + apm_tests + flow_tests serial_tests = [] if parallel_tests: pool = Pool(processes=cpu_count()) results = [ pool.apply_async(run_test, args=test_args) for test_args in parallel_tests ] for async_result in results: async_result.wait() if not async_result.successful(): raise async_result.get() for test_args in serial_tests: (test_runner, pkg_path, name) = test_args run_test(test_runner, pkg_path, name)
def clean(self): import datetime start = datetime.datetime.now() logging.info("START CLEAN: %s", start) if platform_checker.is_windows(): # Python's multiprocessing library has issues on Windows, so it seems easiest to avoid it: # https://docs.python.org/2/library/multiprocessing.html#windows self._do_serial_clean() else: self._do_multiprocess_clean() end = datetime.datetime.now() logging.info("FINISH CLEAN: %s", end) logging.info("PackageManager.clean_dependencies() took %s seconds.", (end - start).seconds)
def find_version_in_range(versions, semver_range): '''versions is an array of semver versions and semver_range is a semver range expression. Assumes the environment has `node` on the $PATH. ''' logging.debug('Trying to match %s against %s.', semver_range, versions) args = [platform_checker.get_node_executable(), semver] + versions + ['-r', semver_range] shell = True if platform_checker.is_windows() else False proc = subprocess.Popen(args, stdout=subprocess.PIPE, shell=shell) matching_versions = [] for line in proc.stdout: matching_versions.append(line.rstrip()) proc.wait() if proc.returncode: return None else: return matching_versions
def run_tests(self): apm_tests = [] npm_tests = [] flow_tests = [] for package_config in self._package_manager.get_configs(): if package_config['excludeTestsFromContinuousIntegration']: continue test_runner = package_config['testRunner'] if test_runner == 'apm' and not self._include_apm: continue pkg_path = package_config['packageRootAbsolutePath'] name = package_config['name'] test_args = (test_runner, pkg_path, name) test_bucket = npm_tests if test_runner == 'npm' else apm_tests test_bucket.append(test_args) if package_config['flowCheck']: flow_tests.append(('flow', pkg_path, name)) if platform_checker.is_windows(): # We run all tests in serial on Windows because Python's multiprocessing library has issues: # https://docs.python.org/2/library/multiprocessing.html#windows parallel_tests = [] serial_tests = npm_tests + apm_tests + flow_tests else: # Currently, all tests appear to be able to be run in parallel. We keep this code # here in case we have to special-case any tests (on a short-term basis) to be run # serially after all of the parallel tests have finished. parallel_tests = npm_tests + apm_tests + flow_tests serial_tests = [] if parallel_tests: pool = Pool(processes=cpu_count()) results = [pool.apply_async(run_test, args=test_args) for test_args in parallel_tests] for async_result in results: async_result.wait() if not async_result.successful(): raise async_result.get() for test_args in serial_tests: (test_runner, pkg_path, name) = test_args run_test(test_runner, pkg_path, name)
def run_js_test(test_runner, pkg_path, name): """Run `apm test` or `npm test` in the given pkg_path.""" logging.info('Running `%s test` in %s...', test_runner, pkg_path) # In Atom 1.2+, "apm test" exits with an error when there is no "spec" directory if test_runner == 'apm' and not os.path.isdir(os.path.join(pkg_path, 'spec')): logging.info('NO TESTS TO RUN FOR: %s', name) return if test_runner == 'apm': test_args = ['node', '--harmony', APM_TEST_WRAPPER, pkg_path] else: test_args = ['npm', 'test'] for tries in xrange(0, RETRY_LIMIT): proc = subprocess.Popen( test_args, cwd=pkg_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=platform_checker.is_windows()) retry = False stdout = [] for line in proc.stdout: # line is a bytes string literal in Python 3. logging.info('[%s test %s]: %s', test_runner, name, line.rstrip().decode('utf-8')) stdout.append(line) # There are some loglines that correlate highly to test failures due to a bug in atom, # so we retry the test if we see this bad test output. See: D2809659 for an explanation. if test_runner == 'apm' and tries < RETRY_LIMIT - 1 and BAD_TEST_OUTPUT in line: logging.info('[%s test %s]: %s: %s', test_runner, name, 'Aborting test due to bad output', BAD_TEST_OUTPUT) retry = True break proc.wait() if retry: continue if proc.returncode: logging.info('TEST FAILED: %s\nstdout:\n%s', name, '\n'.join(stdout)) raise Exception('TEST FAILED: %s test %s' % (test_runner, name)) else: logging.info('TEST PASSED: %s', name) break
def cross_platform_check_output(cmd_args, **kwargs): ''' This is a subprocess.check_output() implementation providing cross-platform support ''' # Unfortunately, it appears that shell=True must be used on Windows to behave like it does on # OS X and Linux: https://bugs.python.org/issue17023. Alternatively, we could try to get the # full path to the executable, but that seems like a pain. if platform_checker.is_windows(): kwargs['shell'] = True kwargs['stdout'] = subprocess.PIPE process = subprocess.Popen(cmd_args, **kwargs) stdout, stderr = process.communicate() returncode = process.returncode if returncode: logging.error('Error running %s in %s', cmd_args, kwargs.get('cwd', os.getcwd())) raise subprocess.CalledProcessError(returncode, cmd_args) return stdout
def cross_platform_check_call(args, cwd=None, stdout=None, stderr=None): '''Use this instead of subprocess.check_call() when the executable in args is not an absolute path. args: Array of strings, such as ['npm', 'install']. ''' if platform_checker.is_windows(): # Unfortunately, it appears that shell=True must be used on Windows to get check_call to # behave like it does on OS X and Linux: https://bugs.python.org/issue17023. Alternatively, # we could try to get the full path to the executable, but that seems like a pain. subprocess.check_call(args, cwd=cwd, stdout=stdout, stderr=stderr, shell=True, ) else: subprocess.check_call(args, cwd=cwd, stdout=stdout, stderr=stderr, )
def symlink(src, dest, relative=False): """Create symlink from src to dest, create directory if dest's dirname doesn't exist, won't throw exception if dest already exists and its symlink points to src. Args: relative: Create relative symlink instead of absolute symlink (only works on *nix). """ dest_dir = os.path.dirname(dest) if not os.path.isdir(dest_dir): os.makedirs(dest_dir) if not os.path.islink(dest) or os.path.realpath(os.path.join(dest_dir, src)) != os.path.realpath(dest): try: if platform_checker.is_windows() and os.path.isdir(src): cross_platform_check_output(["mklink", "/J", "/D", dest, src]) else: if relative and os.path.isabs(src): src = os.path.relpath(src, os.path.dirname(dest)) os.symlink(src, dest) except OSError as e: if e.errno == errno.EEXIST: os.remove(dest) os.symlink(src, dest)
def symlink(src, dest, relative=False): """Create symlink from src to dest, create directory if dest's dirname doesn't exist, won't throw exception if dest already exists and its symlink points to src. Args: relative: Create relative symlink instead of absolute symlink (only works on *nix). """ dest_dir = os.path.dirname(dest) if not os.path.isdir(dest_dir): os.makedirs(dest_dir) if (not os.path.islink(dest) or os.path.realpath( os.path.join(dest_dir, src)) != os.path.realpath(dest)): try: if platform_checker.is_windows() and os.path.isdir(src): cross_platform_check_output(['mklink', '/J', '/D', dest, src]) else: if relative and os.path.isabs(src): src = os.path.relpath(src, os.path.dirname(dest)) os.symlink(src, dest) except OSError as e: if e.errno == errno.EEXIST: os.remove(dest) os.symlink(src, dest)
def install_dependencies(package_config, npm): name = package_config['name'] is_node_package = package_config['isNodePackage'] package_type = 'Node' if is_node_package else 'Atom' logging.info('Installing dependencies for %s package %s...', package_type, name) # Link private node dependencies. src_path = package_config['packageRootAbsolutePath'] fs.mkdirs(os.path.join(src_path, 'node_modules')) for local_dependency, local_dependency_config in package_config['localDependencies'].items(): src_dir = local_dependency_config['packageRootAbsolutePath'] dest_dir = os.path.join(src_path, 'node_modules', local_dependency) if platform_checker.is_windows(): shutil.rmtree(dest_dir, ignore_errors=True) shutil.copytree(src_dir, dest_dir) else: symlink(src_dir, dest_dir) link_dependencys_executable(src_path, local_dependency) # Install other public node dependencies. npm.install(src_path, local_packages=package_config['localDependencies'], include_dev_dependencies=package_config['includeDevDependencies']) logging.info('Done installing dependencies for %s', name) # Install libclang dependencies, if appropriate. if package_config.get('installLibClang', False): from fb.libclang import install_libclang logging.info('Installing libclang extra dependencies...') install_libclang(src_path) logging.info('Done installing libclang extra dependencies.') is_node_package = package_config.get('isNodePackage') if not is_node_package: logging.info('Running `apm link %s`...', src_path) args = ['apm', 'link', src_path] fs.cross_platform_check_call(args) logging.info('Done linking %s', name)