def pip_install_package(source_name, pip_version=None, python_version=None, mode=InstallMode.min_deps, release=False): """Install a pip-compatible python package as a rez package. Args: source_name (str): Name of package or archive/url containing the pip package source. This is the same as the arg you would pass to the 'pip install' command. pip_version (str or `Version`): Version of pip to use to perform the install, uses latest if None. python_version (str or `Version`): Python version to use to perform the install, and subsequently have the resulting rez package depend on. mode (`InstallMode`): Installation mode, determines how dependencies are managed. release (bool): If True, install as a released package; otherwise, it will be installed as a local package. Returns: 2-tuple: List of `Variant`: Installed variants; List of `Variant`: Skipped variants (already installed). """ installed_variants = [] skipped_variants = [] pip_exe, context = find_pip(pip_version, python_version) # TODO: should check if packages_path is writable before continuing with pip # packages_path = (config.release_packages_path if release else config.local_packages_path) tmpdir = mkdtemp(suffix="-rez", prefix="pip-") stagingdir = os.path.join(tmpdir, "rez_staging") stagingsep = "".join([os.path.sep, "rez_staging", os.path.sep]) destpath = os.path.join(stagingdir, "python") # TODO use binpath once https://github.com/pypa/pip/pull/3934 is approved binpath = os.path.join(stagingdir, "bin") if context and config.debug("package_release"): buf = StringIO() print("\n\npackage download environment:", file=buf) context.print_info(buf) _log(buf.getvalue()) # Build pip commandline cmd = [pip_exe, "install", "--use-pep517", "--target=%s" % destpath] if mode == InstallMode.no_deps: cmd.append("--no-deps") cmd.append(source_name) _cmd(context=context, command=cmd) _system = System() def pure_python_package(installed_dist): true_table = {"true": True, "false": False} packages = pkg_resources.find_distributions(destpath) dist = next( (package for package in packages if package.key == installed_dist.key), None) wheel_data = dist.get_metadata('WHEEL') # see https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata wheel_data = Parser().parsestr(wheel_data) # see https://www.python.org/dev/peps/pep-0427/#what-s-the-deal-with-purelib-vs-platlib return true_table[wheel_data["Root-Is-Purelib"]] # Collect resulting python packages using distlib distribution_path = DistributionPath([destpath]) distributions = [d for d in distribution_path.get_distributions()] # moving bin folder to expected relative location as per wheel RECORD files staged_binpath = os.path.join(destpath, "bin") if os.path.isdir(staged_binpath): shutil.move(os.path.join(destpath, "bin"), binpath) for distribution in distribution_path.get_distributions(): requirements = [] if distribution.metadata.run_requires: # Handle requirements. Currently handles conditional environment based # requirements and normal requirements # TODO: Handle optional requirements? for requirement in distribution.metadata.run_requires: if "environment" in requirement: if interpret(requirement["environment"]): requirements.extend( _get_dependencies(requirement, distributions)) elif "extra" in requirement: # Currently ignoring optional requirements pass else: requirements.extend( _get_dependencies(requirement, distributions)) tools = [] src_dst_lut = {} for installed_file in distribution.list_installed_files(): # distlib expects the script files to be located in ../../bin/ # when in fact ../bin seems to be the resulting path after the # installation as such we need to point the bin files to the # expected location to match wheel RECORD files installed_filepath = os.path.normpath(installed_file[0]) bin_prefix = os.path.join('..', '..', 'bin') + os.sep if installed_filepath.startswith(bin_prefix): # account for extra parentdir as explained above installed = os.path.join(destpath, '_', installed_filepath) else: installed = os.path.join(destpath, installed_filepath) source_file = os.path.normpath(installed) if os.path.exists(source_file): destination_file = os.path.relpath(source_file, stagingdir) exe = False if is_exe(source_file) and destination_file.startswith("bin" + os.sep): _file = os.path.basename(destination_file) tools.append(_file) exe = True data = [destination_file, exe] src_dst_lut[source_file] = data else: _log("Source file does not exist: " + source_file + "!") def make_root(variant, path): """Using distlib to iterate over all installed files of the current distribution to copy files to the target directory of the rez package variant """ for source_file, data in src_dst_lut.items(): destination_file, exe = data destination_file = os.path.normpath( os.path.join(path, destination_file)) if not os.path.exists(os.path.dirname(destination_file)): os.makedirs(os.path.dirname(destination_file)) shutil.copyfile(source_file, destination_file) if exe: shutil.copystat(source_file, destination_file) # determine variant requirements variant_reqs = [] pure = pure_python_package(distribution) if not pure: variant_reqs.append("platform-%s" % _system.platform) variant_reqs.append("arch-%s" % _system.arch) # Add the python version requirement. Note that we specify python to # minor version because of environment markers - these often mean that # you cannot use a loose python requirement (ie major version only) # because then the package requirements would not be correct for all # versions of python within that major version. # # This is not perfect. It means that often we will overspecify the required # python version; and theoretically there could be markers that specify # python down to the patch version. However, accurately varianting on # python based on markers may be overly complicated, and may also # result in odd varianting cases. # # https://www.python.org/dev/peps/pep-0508/#environment-markers # if context is None: # since we had to use system pip, we have to assume system python version py_ver = '.'.join(map(str, sys.version_info[:2])) else: python_variant = context.get_resolved_package("python") py_ver = python_variant.version.trim(2) variant_reqs.append("python-%s" % py_ver) name = pip_to_rez_package_name(distribution) with make_package(name, packages_path, make_root=make_root) as pkg: pkg.version = pip_to_rez_version(distribution.version) if distribution.metadata.summary: pkg.description = distribution.metadata.summary pkg.variants = [variant_reqs] if requirements: pkg.requires = requirements commands = [] commands.append("env.PYTHONPATH.append('{root}/python')") if tools: pkg.tools = tools commands.append("env.PATH.append('{root}/bin')") pkg.commands = '\n'.join(commands) installed_variants.extend(pkg.installed_variants or []) skipped_variants.extend(pkg.skipped_variants or []) # cleanup shutil.rmtree(tmpdir) return installed_variants, skipped_variants
def pip_install_package(source_name, pip_version=None, python_version=None, mode=InstallMode.min_deps, release=False): """Install a pip-compatible python package as a rez package. Args: source_name (str): Name of package or archive/url containing the pip package source. This is the same as the arg you would pass to the 'pip install' command. pip_version (str or `Version`): Version of pip to use to perform the install, uses latest if None. python_version (str or `Version`): Python version to use to perform the install, and subsequently have the resulting rez package depend on. mode (`InstallMode`): Installation mode, determines how dependencies are managed. release (bool): If True, install as a released package; otherwise, it will be installed as a local package. Returns: 2-tuple: List of `Variant`: Installed variants; List of `Variant`: Skipped variants (already installed). """ installed_variants = [] skipped_variants = [] pip_exe, context = find_pip(pip_version, python_version) # TODO: should check if packages_path is writable before continuing with pip # packages_path = (config.release_packages_path if release else config.local_packages_path) tmpdir = mkdtemp(suffix="-rez", prefix="pip-") stagingdir = os.path.join(tmpdir, "rez_staging") stagingsep = "".join([os.path.sep, "rez_staging", os.path.sep]) destpath = os.path.join(stagingdir, "python") binpath = os.path.join(stagingdir, "bin") incpath = os.path.join(stagingdir, "include") datapath = stagingdir if context and config.debug("package_release"): buf = StringIO() print >> buf, "\n\npackage download environment:" context.print_info(buf) _log(buf.getvalue()) # Build pip commandline cmd = [pip_exe, "install", "--install-option=--install-lib=%s" % destpath, "--install-option=--install-scripts=%s" % binpath, "--install-option=--install-headers=%s" % incpath, "--install-option=--install-data=%s" % datapath] if mode == InstallMode.no_deps: cmd.append("--no-deps") cmd.append(source_name) _cmd(context=context, command=cmd) _system = System() # Collect resulting python packages using distlib distribution_path = DistributionPath([destpath], include_egg=True) distributions = [d for d in distribution_path.get_distributions()] for distribution in distribution_path.get_distributions(): requirements = [] if distribution.metadata.run_requires: # Handle requirements. Currently handles conditional environment based # requirements and normal requirements # TODO: Handle optional requirements? for requirement in distribution.metadata.run_requires: if "environment" in requirement: if interpret(requirement["environment"]): requirements.extend(_get_dependencies(requirement, distributions)) elif "extra" in requirement: # Currently ignoring optional requirements pass else: requirements.extend(_get_dependencies(requirement, distributions)) tools = [] src_dst_lut = {} for installed_file in distribution.list_installed_files(allow_fail=True): source_file = os.path.normpath(os.path.join(destpath, installed_file[0])) if os.path.exists(source_file): destination_file = installed_file[0].split(stagingsep)[1] exe = False if is_exe(source_file) and \ destination_file.startswith("%s%s" % ("bin", os.path.sep)): _, _file = os.path.split(destination_file) tools.append(_file) exe = True data = [destination_file, exe] src_dst_lut[source_file] = data else: _log("Source file does not exist: " + source_file + "!") def make_root(variant, path): """Using distlib to iterate over all installed files of the current distribution to copy files to the target directory of the rez package variant """ for source_file, data in src_dst_lut.items(): destination_file, exe = data destination_file = os.path.normpath(os.path.join(path, destination_file)) if not os.path.exists(os.path.dirname(destination_file)): os.makedirs(os.path.dirname(destination_file)) shutil.copyfile(source_file, destination_file) if exe: shutil.copystat(source_file, destination_file) # determine variant requirements # TODO detect if platform/arch/os necessary, no if pure python variant_reqs = [] variant_reqs.append("platform-%s" % _system.platform) variant_reqs.append("arch-%s" % _system.arch) variant_reqs.append("os-%s" % _system.os) if context is None: # since we had to use system pip, we have to assume system python version py_ver = '.'.join(map(str, sys.version_info[:2])) else: python_variant = context.get_resolved_package("python") py_ver = python_variant.version.trim(2) variant_reqs.append("python-%s" % py_ver) name, _ = parse_name_and_version(distribution.name_and_version) name = distribution.name[0:len(name)].replace("-", "_") with make_package(name, packages_path, make_root=make_root) as pkg: pkg.version = distribution.version if distribution.metadata.summary: pkg.description = distribution.metadata.summary pkg.variants = [variant_reqs] if requirements: pkg.requires = requirements commands = [] commands.append("env.PYTHONPATH.append('{root}/python')") if tools: pkg.tools = tools commands.append("env.PATH.append('{root}/bin')") pkg.commands = '\n'.join(commands) installed_variants.extend(pkg.installed_variants or []) skipped_variants.extend(pkg.skipped_variants or []) # cleanup shutil.rmtree(tmpdir) return installed_variants, skipped_variants
def get_rez_requirements(installed_dist, python_version, name_casings=None): """Get requirements of the given dist, in rez-compatible format. Example result: { "requires": ["foo-1.2+<2"], "variant_requires": ["future", "python-2.7"], "metadata": { # metadata pertinent to rez ... } } Each requirement has had its package name converted to the rez equivalent. The 'variant_requires' key contains requirements specific to the current variant. TODO: Currently there is no way to reflect extras that may have been chosen for this pip package. We need to wait for rez "package features" before this will be possible. You probably shouldn't use extras presently. Args: installed_dist (`distlib.database.InstalledDistribution`): Distribution to convert. python_version (`Version`): Python version used to perform the installation. name_casings (list of str): A list of pip package names in their correct casings (eg, 'Foo' rather than 'foo'). Any requirement whose name case-insensitive-matches a name in this list, is set to that name. This is needed because pip package names are case insensitive, but rez is case-sensitive. So a package may list a requirement for package 'foo', when in fact the package that pip has downloaded is called 'Foo'. Be sure to provide names in PIP format, not REZ format (the pip package 'foo-bah' will be converted to 'foo_bah' in rez). Returns: Dict: See example above. """ _system = System() result_requires = [] result_variant_requires = [] # create cased names lookup name_mapping = dict((x.lower(), x) for x in (name_casings or [])) # requirements such as platform, arch, os, and python sys_requires = set(["python"]) # assume package is platform- and arch- specific if it isn't pure python is_pure_python = is_pure_python_package(installed_dist) if not is_pure_python: sys_requires.update(["platform", "arch"]) # evaluate wrt python version, which may not be the current interpreter version marker_env = { "python_full_version": str(python_version), "python_version": str(python_version.trim(2)), "implementation_version": str(python_version) } # Note: This is supposed to give a requirements list that has already been # filtered down based on the extras requested at install time, and on any # environment markers present. However, this is not working in distlib. The # package gets assigned a LegacyMetadata metadata object (only if a package metadata # version is not equal to 2.0) and in that code path, this filtering # doesn't happen. # # See: vendor/distlib/metadata.py#line-892 # requires = installed_dist.run_requires # filter requirements for req_ in requires: reqs = normalize_requirement(req_) for req in reqs: # skip if env marker is present and doesn't evaluate if req.marker and not req.marker.evaluate(environment=marker_env): continue # skip if req is conditional on extras that weren't requested if req.conditional_extras and not \ (set(installed_dist.extras or []) & set(req.conditional_extras)): continue if req.conditional_extras: print_warning( "Skipping requirement %r - conditional requirements are " "not yet supported", str(req)) continue # Inspect marker(s) to see if this requirement should be varianted. # Markers may also cause other system requirements to be added to # the variant. # to_variant = False if req.marker: marker_reqs = get_marker_sys_requirements(str(req.marker)) if marker_reqs: sys_requires.update(marker_reqs) to_variant = True # remap the requirement name remapped = name_mapping.get(req.name.lower()) if remapped: req.name = remapped # convert the requirement to rez equivalent rez_req = str(packaging_req_to_rez_req(req)) if to_variant: result_variant_requires.append(rez_req) else: result_requires.append(rez_req) # prefix variant with system requirements sys_variant_requires = [] if "platform" in sys_requires: sys_variant_requires.append("platform-%s" % _system.platform) if "arch" in sys_requires: sys_variant_requires.append("arch-%s" % _system.arch) if "os" in sys_requires: sys_variant_requires.append("os-%s" % _system.os) if "python" in sys_requires: # Add python variant requirement. Note that this is always MAJOR.MINOR, # because to do otherwise would mean analysing any present env markers. # This could become quite complicated, and could also result in strange # python version ranges in the variants. # sys_variant_requires.append("python-%s" % str(python_version.trim(2))) return { "requires": result_requires, "variant_requires": sys_variant_requires + result_variant_requires, "metadata": { "is_pure_python": is_pure_python } }
def pip_install_package(source_name, pip_version=None, python_version=None, mode=InstallMode.min_deps, release=False): """Install a pip-compatible python package as a rez package. Args: source_name (str): Name of package or archive/url containing the pip package source. This is the same as the arg you would pass to the 'pip install' command. pip_version (str or `Version`): Version of pip to use to perform the install, uses latest if None. python_version (str or `Version`): Python version to use to perform the install, and subsequently have the resulting rez package depend on. mode (`InstallMode`): Installation mode, determines how dependencies are managed. release (bool): If True, install as a released package; otherwise, it will be installed as a local package. Returns: 2-tuple: List of `Variant`: Installed variants; List of `Variant`: Skipped variants (already installed). """ installed_variants = [] skipped_variants = [] py_exe, context = find_pip(pip_version, python_version) # TODO: should check if packages_path is writable before continuing with pip # packages_path = (config.release_packages_path if release else config.local_packages_path) tmpdir = mkdtemp(suffix="-rez", prefix="pip-") stagingdir = os.path.join(tmpdir, "rez_staging") stagingsep = "".join([os.path.sep, "rez_staging", os.path.sep]) destpath = os.path.join(stagingdir, "python") # TODO use binpath once https://github.com/pypa/pip/pull/3934 is approved binpath = os.path.join(stagingdir, "bin") if context and config.debug("package_release"): buf = StringIO() print("\n\npackage download environment:", file=buf) context.print_info(buf) _log(buf.getvalue()) # Build pip commandline cmd = [ py_exe, "-m", "pip", "install", "--use-pep517", "--target=%s" % destpath ] if mode == InstallMode.no_deps: cmd.append("--no-deps") cmd.append(source_name) _cmd(context=context, command=cmd) _system = System() # determine version of python in use if context is None: # since we had to use system pip, we have to assume system python version py_ver_str = '.'.join(map(str, sys.version_info)) py_ver = Version(py_ver_str) else: python_variant = context.get_resolved_package("python") py_ver = python_variant.version # moving bin folder to expected relative location as per wheel RECORD files staged_binpath = os.path.join(destpath, "bin") if os.path.isdir(staged_binpath): shutil.move(os.path.join(destpath, "bin"), binpath) # Collect resulting python packages using distlib distribution_path = DistributionPath([destpath]) distributions = list(distribution_path.get_distributions()) dist_names = [x.name for x in distributions] # get list of package and dependencies for distribution in distributions: # convert pip requirements into rez requirements rez_requires = get_rez_requirements(installed_dist=distribution, python_version=py_ver, name_casings=dist_names) # log the pip -> rez translation, for debugging _log("Pip to rez translation information for " + distribution.name_and_version + ":\n" + pformat({ "pip": { "run_requires": map(str, distribution.run_requires) }, "rez": rez_requires })) # iterate over installed files and determine dest filepaths tools = [] src_dst_lut = {} for installed_file in distribution.list_installed_files(): # distlib expects the script files to be located in ../../bin/ # when in fact ../bin seems to be the resulting path after the # installation as such we need to point the bin files to the # expected location to match wheel RECORD files installed_filepath = os.path.normpath(installed_file[0]) bin_prefix = os.path.join('..', '..', 'bin') + os.sep if installed_filepath.startswith(bin_prefix): # account for extra parentdir as explained above installed = os.path.join(destpath, '_', installed_filepath) else: installed = os.path.join(destpath, installed_filepath) source_file = os.path.normpath(installed) if os.path.exists(source_file): destination_file = os.path.relpath(source_file, stagingdir) exe = False if is_exe(source_file) and destination_file.startswith("bin" + os.sep): _file = os.path.basename(destination_file) tools.append(_file) exe = True src_dst_lut[source_file] = [destination_file, exe] else: _log("Source file does not exist: " + source_file + "!") def make_root(variant, path): """Using distlib to iterate over all installed files of the current distribution to copy files to the target directory of the rez package variant """ for source_file, data in src_dst_lut.items(): destination_file, exe = data destination_file = os.path.normpath( os.path.join(path, destination_file)) if not os.path.exists(os.path.dirname(destination_file)): os.makedirs(os.path.dirname(destination_file)) shutil.copyfile(source_file, destination_file) if exe: shutil.copystat(source_file, destination_file) # create the rez package name = pip_to_rez_package_name(distribution.name) version = pip_to_rez_version(distribution.version) requires = rez_requires["requires"] variant_requires = rez_requires["variant_requires"] metadata = rez_requires["metadata"] with make_package(name, packages_path, make_root=make_root) as pkg: # basics (version etc) pkg.version = version if distribution.metadata.summary: pkg.description = distribution.metadata.summary # requirements and variants if requires: pkg.requires = requires if variant_requires: pkg.variants = [variant_requires] # commands commands = [] commands.append("env.PYTHONPATH.append('{root}/python')") if tools: pkg.tools = tools commands.append("env.PATH.append('{root}/bin')") pkg.commands = '\n'.join(commands) # Make the package use hashed variants. This is required because we # can't control what ends up in its variants, and that can easily # include problematic chars (>, +, ! etc). # TODO: https://github.com/nerdvegas/rez/issues/672 # pkg.hashed_variants = True # add some custom attributes to retain pip-related info pkg.pip_name = distribution.name_and_version pkg.from_pip = True pkg.is_pure_python = metadata["is_pure_python"] installed_variants.extend(pkg.installed_variants or []) skipped_variants.extend(pkg.skipped_variants or []) # cleanup shutil.rmtree(tmpdir) return installed_variants, skipped_variants