55
66This module defines the entry point for command line and programmatic use.
77"""
8-
98from os import environ
109from pythonforandroid import __version__
1110from pythonforandroid .pythonpackage import get_dep_names_of_package
@@ -651,9 +650,12 @@ def add_parser(subparsers, *args, **kwargs):
651650 "pyproject.toml" ))):
652651 have_setup_py_or_similar = True
653652
654- # Process requirements and put version in environ
655- if hasattr (args , 'requirements' ):
656- requirements = []
653+ # Process requirements and put version in environ:
654+ if hasattr (args , 'requirements' ) and args .requirements :
655+ all_recipes = [
656+ recipe .lower () for recipe in
657+ set (Recipe .list_recipes (self .ctx ))
658+ ]
657659
658660 # Add dependencies from setup.py, but only if they are recipes
659661 # (because otherwise, setup.py itself will install them later)
@@ -672,10 +674,6 @@ def add_parser(subparsers, *args, **kwargs):
672674 )
673675 ]
674676 info ("Dependencies obtained: " + str (dependencies ))
675- all_recipes = [
676- recipe .lower () for recipe in
677- set (Recipe .list_recipes (self .ctx ))
678- ]
679677 dependencies = set (dependencies ).intersection (
680678 set (all_recipes )
681679 )
@@ -691,7 +689,126 @@ def add_parser(subparsers, *args, **kwargs):
691689 "package? Will continue WITHOUT setup.py deps."
692690 )
693691
694- # Parse --requirements argument list:
692+ non_recipe_requirements = []
693+ for requirement in args .requirements .split (',' ):
694+ requirement_name = re .sub (r'==\d+(\.\d+)*' , '' , requirement )
695+ if requirement_name not in all_recipes :
696+ non_recipe_requirements .append (requirement )
697+ args .requirements = re .sub (
698+ r',?{}' .format (requirement ), '' , args .requirements )
699+
700+ # Compile "non-recipe" requirements' dependencies and add to list.
701+ # Otherwise, only recipe requirements' dependencies get installed.
702+ # More info https://github.com/kivy/python-for-android/issues/2529
703+ if non_recipe_requirements :
704+ info ("Compiling dependencies for: "
705+ "{}" .format (non_recipe_requirements ))
706+
707+ output = shprint (
708+ sh .bash , '-c' ,
709+ "echo -e '{}' > requirements.in && "
710+ "pip-compile -v --dry-run --annotation-style=line && "
711+ "rm requirements.in" .format (
712+ '\n ' .join (non_recipe_requirements )))
713+
714+ # Parse pip-compile output
715+ parsed_requirement_info_list = []
716+ for line in output .splitlines ():
717+ match_data = re .match (
718+ r'^([\w.-]+)==(\d+(\.\d+)*).*'
719+ r'#\s+via\s+([\w\s,.-]+)' , line )
720+
721+ if match_data :
722+ parent_requirements = match_data .group (4 ).split (', ' )
723+ requirement_name = match_data .group (1 )
724+ requirement_version = match_data .group (2 )
725+
726+ # Requirement is a "non-recipe" one we started with.
727+ if '-r requirements.in' in parent_requirements :
728+ parent_requirements .remove ('-r requirements.in' )
729+
730+ parsed_requirement_info_list .append ([
731+ requirement_name ,
732+ requirement_version ,
733+ parent_requirements ])
734+
735+ info ("Requirements obtained from pip-compile: "
736+ "{}" .format (["{}=={}" .format (x [0 ], x [1 ])
737+ for x in parsed_requirement_info_list ]))
738+
739+ # Remove indirect requirements ultimately installed by a recipe
740+ original_parsed_requirement_count = - 1
741+ while len (parsed_requirement_info_list ) != \
742+ original_parsed_requirement_count :
743+
744+ original_parsed_requirement_count = \
745+ len (parsed_requirement_info_list )
746+
747+ for i , parsed_requirement_info in \
748+ enumerate (reversed (parsed_requirement_info_list )):
749+
750+ index = original_parsed_requirement_count - i - 1
751+ requirement_name , requirement_version , \
752+ parent_requirements = parsed_requirement_info
753+
754+ # If any parent requirement has a recipe, this
755+ # requirement ought also to be installed by it.
756+ # Hence, it's better not to add this requirement the
757+ # expanded list.
758+ parent_requirements_with_recipe = list (
759+ set (parent_requirements ).intersection (
760+ set (all_recipes )))
761+
762+ # Any parent requirement removed for the expanded list
763+ # implies that it and its own requirements (including
764+ # this requirement) will be installed by a recipe.
765+ # Hence, it's better not to add this requirement the
766+ # expanded list.
767+ requirement_name_list = \
768+ [x [0 ] for x in parsed_requirement_info_list ]
769+ parent_requirements_still_in_list = list (
770+ set (parent_requirements ).intersection (
771+ set (requirement_name_list )))
772+
773+ is_ultimately_installed_by_a_recipe = \
774+ len (parent_requirements ) and \
775+ (parent_requirements_with_recipe or
776+ len (parent_requirements_still_in_list ) !=
777+ len (parent_requirements ))
778+
779+ if is_ultimately_installed_by_a_recipe :
780+ info (
781+ '{} will be installed by a recipe. Removing '
782+ 'it from requirement list expansion.' .format (
783+ requirement_name ))
784+ del parsed_requirement_info_list [index ]
785+
786+ for parsed_requirement_info in parsed_requirement_info_list :
787+ requirement_name , requirement_version , \
788+ parent_requirements = parsed_requirement_info
789+
790+ # If the requirement has a recipe, don't use specific
791+ # version constraints determined by pip-compile. Some
792+ # recipes may not support the specified version. Therefor,
793+ # it's probably safer to just let them use their default
794+ # version. User can still force the usage of specific
795+ # version by explicitly declaring it with --requirements.
796+ requirement_has_recipe = requirement_name in all_recipes
797+ requirement_str = \
798+ requirement_name if requirement_has_recipe else \
799+ '{}=={}' .format (requirement_name , requirement_version )
800+
801+ requirement_names_arg = re .sub (
802+ r'==\d+(\.\d+)*' , '' , args .requirements ).split (',' )
803+
804+ # This expansion was carried out based on "non-recipe"
805+ # requirements. Hence,the counter-part, requirements
806+ # with a recipe, may already be part of list.
807+ if requirement_name not in requirement_names_arg :
808+ args .requirements += ',' + requirement_str
809+
810+ # Handle specific version requirement constraints (e.g. foo==x.y)
811+ requirements = []
695812 for requirement in split_argument_list (args .requirements ):
696813 if "==" in requirement :
697814 requirement , version = requirement .split (u"==" , 1 )
@@ -701,6 +818,9 @@ def add_parser(subparsers, *args, **kwargs):
701818 requirements .append (requirement )
702819 args .requirements = u"," .join (requirements )
703820
821+ info ('Expanded Requirements List: '
822+ '{}' .format (args .requirements .split (',' )))
823+
704824 self .warn_on_deprecated_args (args )
705825
706826 self .storage_dir = args .storage_dir
0 commit comments