diff --git a/pythonforandroid/toolchain.py b/pythonforandroid/toolchain.py index aa242a4170..f0b4a6dd47 100644 --- a/pythonforandroid/toolchain.py +++ b/pythonforandroid/toolchain.py @@ -5,7 +5,6 @@ This module defines the entry point for command line and programmatic use. """ - from os import environ from pythonforandroid import __version__ from pythonforandroid.pythonpackage import get_dep_names_of_package @@ -651,9 +650,12 @@ def add_parser(subparsers, *args, **kwargs): "pyproject.toml"))): have_setup_py_or_similar = True - # Process requirements and put version in environ - if hasattr(args, 'requirements'): - requirements = [] + # Process requirements and put version in environ: + if hasattr(args, 'requirements') and args.requirements: + all_recipes = [ + recipe.lower() for recipe in + set(Recipe.list_recipes(self.ctx)) + ] # Add dependencies from setup.py, but only if they are recipes # (because otherwise, setup.py itself will install them later) @@ -672,10 +674,6 @@ def add_parser(subparsers, *args, **kwargs): ) ] info("Dependencies obtained: " + str(dependencies)) - all_recipes = [ - recipe.lower() for recipe in - set(Recipe.list_recipes(self.ctx)) - ] dependencies = set(dependencies).intersection( set(all_recipes) ) @@ -691,7 +689,118 @@ def add_parser(subparsers, *args, **kwargs): "package? Will continue WITHOUT setup.py deps." ) - # Parse --requirements argument list: + non_recipe_requirements = [] + for requirement in args.requirements.split(','): + requirement_name = re.sub(r'==\d+(\.\d+)*', '', requirement) + if requirement_name not in all_recipes: + non_recipe_requirements.append(requirement) + args.requirements = re.sub( + r',?{}'.format(requirement), '', args.requirements) + + # Compile "non-recipe" requirements' dependencies and add to list. + # Otherwise, only recipe requirements' dependencies get installed. + # More info https://github.com/kivy/python-for-android/issues/2529 + if non_recipe_requirements: + info("Compiling dependencies for: " + "{}".format(non_recipe_requirements)) + + output = shprint( + sh.bash, '-c', + "echo -e '{}' > requirements.in && " + "pip-compile -v --dry-run --annotation-style=line && " + "rm requirements.in".format( + '\n'.join(non_recipe_requirements))) + + # Parse pip-compile output + parsed_requirement_info_list = [] + for line in output.splitlines(): + match_data = re.match( + r'^([\w.-]+)==(\d+(\.\d+)*).*' + r'#\s+via\s+([\w\s,.-]+)', line) + + if match_data: + parent_requirements = match_data.group(4).split(', ') + requirement_name = match_data.group(1) + requirement_version = match_data.group(2) + + # Requirement is a "non-recipe" one we started with. + if '-r requirements.in' in parent_requirements: + parent_requirements.remove('-r requirements.in') + + parsed_requirement_info_list.append([ + requirement_name, + requirement_version, + parent_requirements]) + + # Remove indirect requirements ultimately installed by a recipe + original_parsed_requirement_count = -1 + while len(parsed_requirement_info_list) != \ + original_parsed_requirement_count: + + original_parsed_requirement_count = \ + len(parsed_requirement_info_list) + + for i, parsed_requirement_info in \ + enumerate(reversed(parsed_requirement_info_list)): + + index = original_parsed_requirement_count - i - 1 + requirement_name, requirement_version, \ + parent_requirements = parsed_requirement_info + + # If any parent requirement has a recipe, this + # requirement ought also to be installed by it. + # Hence, it's better not to add this requirement the + # expanded list. + parent_requirements_with_recipe = list( + set(parent_requirements).intersection( + set(all_recipes))) + + # Any parent requirement removed for the expanded list + # implies that it and its own requirements (including + # this requirement) will be installed by a recipe. + # Hence, it's better not to add this requirement the + # expanded list. + requirement_name_list = \ + [x[0] for x in parsed_requirement_info_list] + parent_requirements_still_in_list = list( + set(parent_requirements).intersection( + set(requirement_name_list))) + + is_ultimately_installed_by_a_recipe = \ + len(parent_requirements) and \ + (parent_requirements_with_recipe or + len(parent_requirements_still_in_list) != + len(parent_requirements)) + + if is_ultimately_installed_by_a_recipe: + del parsed_requirement_info_list[index] + + for parsed_requirement_info in parsed_requirement_info_list: + requirement_name, requirement_version, \ + parent_requirements = parsed_requirement_info + + # If the requirement has a recipe, don't use specific + # version constraints determined by pip-compile. Some + # recipes may not support the specified version. Therefor, + # it's probably safer to just let them use their default + # version. User can still force the usage of specific + # version by explicitly declaring it with --requirements. + requirement_has_recipe = requirement_name in all_recipes + requirement_str = \ + requirement_name if requirement_has_recipe else \ + '{}=={}'.format(requirement_name, requirement_version) + + requirement_names_arg = re.sub( + r'==\d+(\.\d+)*', '', args.requirements).split(',') + + # This expansion was carried out based on "non-recipe" + # requirements. Hence,the counter-part, requirements + # with a recipe, may already be part of list. + if requirement_name not in requirement_names_arg: + args.requirements += ',' + requirement_str + + # Handle specific version requirement constraints (e.g. foo==x.y) + requirements = [] for requirement in split_argument_list(args.requirements): if "==" in requirement: requirement, version = requirement.split(u"==", 1) @@ -701,6 +810,9 @@ def add_parser(subparsers, *args, **kwargs): requirements.append(requirement) args.requirements = u",".join(requirements) + info('Expanded Requirements List: ' + '{}'.format(args.requirements.split(','))) + self.warn_on_deprecated_args(args) self.storage_dir = args.storage_dir diff --git a/setup.py b/setup.py index ef9ee0cfb2..c200027801 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ install_reqs = [ 'appdirs', 'colorama>=0.3.3', 'jinja2', 'six', 'enum34; python_version<"3.4"', 'sh>=1.10; sys_platform!="nt"', - 'pep517<0.7.0', 'toml', + 'pep517<0.7.0', 'toml', 'pip-tools' ] # (pep517 and toml are used by pythonpackage.py)