22import copy
33import glob
44import os
5+ import json
6+ import tempfile
57from os import environ
68from os .path import (
7- abspath , join , realpath , dirname , expanduser , exists
9+ abspath , join , realpath , dirname , expanduser , exists , basename
810)
911import re
1012import shutil
1113import subprocess
1214
1315import sh
1416
17+ from packaging .utils import parse_wheel_filename
18+ from packaging .requirements import Requirement
19+
1520from pythonforandroid .androidndk import AndroidNDK
1621from pythonforandroid .archs import ArchARM , ArchARMv7_a , ArchAarch_64 , Archx86 , Archx86_64
17- from pythonforandroid .logger import (info , warning , info_notify , info_main , shprint )
22+ from pythonforandroid .logger import (info , warning , info_notify , info_main , shprint , Out_Style , Out_Fore )
1823from pythonforandroid .pythonpackage import get_package_name
1924from pythonforandroid .recipe import CythonRecipe , Recipe
2025from pythonforandroid .recommendations import (
@@ -90,6 +95,8 @@ class Context:
9095
9196 recipe_build_order = None # Will hold the list of all built recipes
9297
98+ python_modules = None # Will hold resolved pure python packages
99+
93100 symlink_bootstrap_files = False # If True, will symlink instead of copying during build
94101
95102 java_build_tool = 'auto'
@@ -444,6 +451,12 @@ def has_package(self, name, arch=None):
444451 # Failed to look up any meaningful name.
445452 return False
446453
454+ # normalize name to remove version tags
455+ try :
456+ name = Requirement (name ).name
457+ except Exception :
458+ pass
459+
447460 # Try to look up recipe by name:
448461 try :
449462 recipe = Recipe .get_recipe (name , self )
@@ -649,6 +662,104 @@ def run_setuppy_install(ctx, project_dir, env=None, arch=None):
649662 os .remove ("._tmp_p4a_recipe_constraints.txt" )
650663
651664
665+ def is_wheel_platform_independent (whl_name ):
666+ name , version , build , tags = parse_wheel_filename (whl_name )
667+ return all (tag .platform == "any" for tag in tags )
668+
669+
670+ def process_python_modules (ctx , modules ):
671+ """Use pip --dry-run to resolve dependencies and filter for pure-Python packages
672+ """
673+ modules = list (modules )
674+ build_order = list (ctx .recipe_build_order )
675+
676+ _requirement_names = []
677+ processed_modules = []
678+
679+ for module in modules + build_order :
680+ try :
681+ # we need to normalize names
682+ # eg Requests>=2.0 becomes requests
683+ _requirement_names .append (Requirement (module ).name )
684+ except Exception :
685+ # name parsing failed; skip processing this module via pip
686+ processed_modules .append (module )
687+ if module in modules :
688+ modules .remove (module )
689+
690+ if len (processed_modules ) > 0 :
691+ warning (f'Ignored by module resolver : { processed_modules } ' )
692+
693+ # preserve the original module list
694+ processed_modules .extend (modules )
695+
696+ # temp file for pip report
697+ fd , path = tempfile .mkstemp ()
698+ os .close (fd )
699+
700+ # setup hostpython recipe
701+ host_recipe = Recipe .get_recipe ("hostpython3" , ctx )
702+
703+ env = environ .copy ()
704+ _python_path = host_recipe .get_path_to_python ()
705+ libdir = glob .glob (join (_python_path , "build" , "lib*" ))
706+ env ['PYTHONPATH' ] = host_recipe .site_dir + ":" + join (
707+ _python_path , "Modules" ) + ":" + (libdir [0 ] if libdir else "" )
708+
709+ shprint (
710+ host_recipe .pip , 'install' , * modules ,
711+ '--dry-run' , '--break-system-packages' , '--ignore-installed' ,
712+ '--report' , path , '-q' , _env = env
713+ )
714+
715+ with open (path , "r" ) as f :
716+ report = json .load (f )
717+
718+ os .remove (path )
719+
720+ info ('Extra resolved pure python dependencies :' )
721+
722+ ignored_str = " (ignored)"
723+ # did we find any non pure python package?
724+ any_not_pure_python = False
725+
726+ # just for style
727+ info (" " )
728+ for module in report ["install" ]:
729+
730+ mname = module ["metadata" ]["name" ]
731+ mver = module ["metadata" ]["version" ]
732+ filename = basename (module ["download_info" ]["url" ])
733+ pure_python = True
734+
735+ if (filename .endswith (".whl" ) and not is_wheel_platform_independent (filename )):
736+ any_not_pure_python = True
737+ pure_python = False
738+
739+ # does this module matches any recipe name?
740+ if mname .lower () in _requirement_names :
741+ continue
742+
743+ color = Out_Fore .GREEN if pure_python else Out_Fore .RED
744+ ignored = "" if pure_python else ignored_str
745+
746+ info (
747+ f" { color } { mname } { Out_Fore .WHITE } : "
748+ f"{ Out_Style .BRIGHT } { mver } { Out_Style .RESET_ALL } "
749+ f"{ ignored } "
750+ )
751+
752+ if pure_python :
753+ processed_modules .append (f"{ mname } =={ mver } " )
754+ info (" " )
755+
756+ if any_not_pure_python :
757+ warning ("Some packages were ignored because they are not pure Python." )
758+ warning ("To install the ignored packages, explicitly list them in your requirements file." )
759+
760+ return processed_modules
761+
762+
652763def run_pymodules_install (ctx , arch , modules , project_dir = None ,
653764 ignore_setup_py = False ):
654765 """ This function will take care of all non-recipe things, by:
@@ -663,6 +774,10 @@ def run_pymodules_install(ctx, arch, modules, project_dir=None,
663774
664775 info ('*** PYTHON PACKAGE / PROJECT INSTALL STAGE FOR ARCH: {} ***' .format (arch ))
665776
777+ # don't run process_python_modules in tests
778+ if ctx .recipe_build_order .__class__ .__name__ != "Mock" :
779+ modules = process_python_modules (ctx , modules )
780+
666781 modules = [m for m in modules if ctx .not_has_package (m , arch )]
667782
668783 # We change current working directory later, so this has to be an absolute
0 commit comments