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,92 @@ 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 .build_order )
675+ _requirement_names = []
676+ processed_modules = []
677+
678+ for module in modules + build_order :
679+ try :
680+ _requirement_names .append (Requirement (module ).name )
681+ except Exception :
682+ processed_modules .append (module )
683+ if module in modules :
684+ modules .remove (module )
685+
686+ if len (processed_modules ) > 0 :
687+ warning (f'Ignored by module resolver : { processed_modules } ' )
688+
689+ processed_modules .extend (modules )
690+
691+ # temp file for pip report
692+ fd , path = tempfile .mkstemp ()
693+ os .close (fd )
694+
695+ host_recipe = Recipe .get_recipe ("hostpython3" , ctx )
696+
697+ if not exists (path ):
698+ shprint (
699+ host_recipe .pip , 'install' , * modules ,
700+ '--dry-run' , '--break-system-packages' , '--ignore-installed' ,
701+ '--report' , path , '-q'
702+ )
703+
704+ with open (path , "r" ) as f :
705+ report = json .load (f )
706+
707+ os .remove (path )
708+
709+ info ('Extra resolved pure python dependencies :' )
710+
711+ ignored_str = " (ignored)"
712+ # did we find any non pure python package?
713+ any_not_pure_python = False
714+
715+ info (" " )
716+ for module in report ["install" ]:
717+
718+ mname = module ["metadata" ]["name" ]
719+ mver = module ["metadata" ]["version" ]
720+ filename = basename (module ["download_info" ]["url" ])
721+ pure_python = True
722+
723+ if (filename .endswith (".whl" ) and not is_wheel_platform_independent (filename )):
724+ any_not_pure_python = True
725+ pure_python = False
726+
727+ # does this module matches any recipe name?
728+ if mname .lower () in _requirement_names :
729+ continue
730+
731+ color = Out_Fore .GREEN if pure_python else Out_Fore .RED
732+ ignored = "" if pure_python else ignored_str
733+
734+ info (
735+ f" { color } { mname } { Out_Fore .WHITE } : "
736+ f"{ Out_Style .BRIGHT } { mver } { Out_Style .RESET_ALL } "
737+ f"{ ignored } "
738+ )
739+
740+ if pure_python :
741+ processed_modules .append (f"{ mname } =={ mver } " )
742+ info (" " )
743+
744+ if any_not_pure_python :
745+ warning ("Some packages were ignored because they are not pure Python." )
746+ warning ("To install the ignored packages, explicitly list them in your requirements file." )
747+
748+ return processed_modules
749+
750+
652751def run_pymodules_install (ctx , arch , modules , project_dir = None ,
653752 ignore_setup_py = False ):
654753 """ This function will take care of all non-recipe things, by:
@@ -663,6 +762,7 @@ def run_pymodules_install(ctx, arch, modules, project_dir=None,
663762
664763 info ('*** PYTHON PACKAGE / PROJECT INSTALL STAGE FOR ARCH: {} ***' .format (arch ))
665764
765+ modules = process_python_modules (ctx , modules )
666766 modules = [m for m in modules if ctx .not_has_package (m , arch )]
667767
668768 # We change current working directory later, so this has to be an absolute
0 commit comments