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,103 @@ 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+ _requirement_names = []
676+ processed_modules = []
677+
678+ for module in modules + build_order :
679+ try :
680+ # we need to normalize names
681+ # eg Requests>=2.0 becomes requests
682+ _requirement_names .append (Requirement (module ).name )
683+ except Exception :
684+ # name parsing failed; skip processing this module via pip
685+ processed_modules .append (module )
686+ if module in modules :
687+ modules .remove (module )
688+
689+ if len (processed_modules ) > 0 :
690+ warning (f'Ignored by module resolver : { processed_modules } ' )
691+
692+ # preserve the original module list
693+ processed_modules .extend (modules )
694+
695+ # temp file for pip report
696+ fd , path = tempfile .mkstemp ()
697+ os .close (fd )
698+
699+ # setup hostpython recipe
700+ host_recipe = Recipe .get_recipe ("hostpython3" , ctx )
701+
702+ env = environ .copy ()
703+ _python_path = host_recipe .get_path_to_python ()
704+ libdir = glob .glob (join (_python_path , "build" , "lib*" ))
705+ env ['PYTHONPATH' ] = host_recipe .site_dir + ":" + join (
706+ _python_path , "Modules" ) + ":" + (libdir [0 ] if libdir else "" )
707+
708+ shprint (
709+ host_recipe .pip , 'install' , * modules ,
710+ '--dry-run' , '--break-system-packages' , '--ignore-installed' ,
711+ '--report' , path , '-q' , _env = env
712+ )
713+
714+ with open (path , "r" ) as f :
715+ report = json .load (f )
716+
717+ os .remove (path )
718+
719+ info ('Extra resolved pure python dependencies :' )
720+
721+ ignored_str = " (ignored)"
722+ # did we find any non pure python package?
723+ any_not_pure_python = False
724+
725+ # just for style
726+ info (" " )
727+ for module in report ["install" ]:
728+
729+ mname = module ["metadata" ]["name" ]
730+ mver = module ["metadata" ]["version" ]
731+ filename = basename (module ["download_info" ]["url" ])
732+ pure_python = True
733+
734+ if (filename .endswith (".whl" ) and not is_wheel_platform_independent (filename )):
735+ any_not_pure_python = True
736+ pure_python = False
737+
738+ # does this module matches any recipe name?
739+ if mname .lower () in _requirement_names :
740+ continue
741+
742+ color = Out_Fore .GREEN if pure_python else Out_Fore .RED
743+ ignored = "" if pure_python else ignored_str
744+
745+ info (
746+ f" { color } { mname } { Out_Fore .WHITE } : "
747+ f"{ Out_Style .BRIGHT } { mver } { Out_Style .RESET_ALL } "
748+ f"{ ignored } "
749+ )
750+
751+ if pure_python :
752+ processed_modules .append (f"{ mname } =={ mver } " )
753+ info (" " )
754+
755+ if any_not_pure_python :
756+ warning ("Some packages were ignored because they are not pure Python." )
757+ warning ("To install the ignored packages, explicitly list them in your requirements file." )
758+
759+ return processed_modules
760+
761+
652762def run_pymodules_install (ctx , arch , modules , project_dir = None ,
653763 ignore_setup_py = False ):
654764 """ This function will take care of all non-recipe things, by:
@@ -663,6 +773,7 @@ def run_pymodules_install(ctx, arch, modules, project_dir=None,
663773
664774 info ('*** PYTHON PACKAGE / PROJECT INSTALL STAGE FOR ARCH: {} ***' .format (arch ))
665775
776+ modules = process_python_modules (ctx , modules )
666777 modules = [m for m in modules if ctx .not_has_package (m , arch )]
667778
668779 # We change current working directory later, so this has to be an absolute
0 commit comments