5
5
6
6
This module defines the entry point for command line and programmatic use.
7
7
"""
8
-
9
8
from os import environ
10
9
from pythonforandroid import __version__
11
10
from pythonforandroid .pythonpackage import get_dep_names_of_package
@@ -227,6 +226,249 @@ def split_argument_list(arg_list):
227
226
return re .split (r'[ ,]+' , arg_list )
228
227
229
228
229
+ def __expand_requirements_arg_from_project_files (ctx , args ):
230
+ """Parse additional requirements from setup.py or pyproject.toml file
231
+ and add to --requirements arg if --use_setup_py argument was specified"""
232
+ all_recipes = [
233
+ recipe .lower () for recipe in
234
+ set (Recipe .list_recipes (ctx ))
235
+ ]
236
+
237
+ has_setup_py_or_toml = False
238
+ if getattr (args , "private" , None ) is not None :
239
+ project_dir = getattr (args , "private" )
240
+ has_setup_py = os .path .exists (
241
+ os .path .join (project_dir , "setup.py" ))
242
+ has_toml = os .path .exists (
243
+ os .path .join (project_dir , "pyproject.toml" ))
244
+ has_setup_py_or_toml = has_setup_py or has_toml
245
+
246
+ # Add dependencies from setup.py, but only if they are recipes
247
+ # (because otherwise, setup.py itself will install them later)
248
+ if has_setup_py_or_toml and getattr (args , "use_setup_py" , False ):
249
+ try :
250
+ info ("Analyzing package dependencies. MAY TAKE A WHILE." )
251
+ # Get all the dependencies corresponding to a recipe:
252
+ dependencies = [
253
+ dep .lower () for dep in
254
+ get_dep_names_of_package (
255
+ args .private ,
256
+ keep_version_pins = True ,
257
+ recursive = True ,
258
+ verbose = True ,
259
+ )
260
+ ]
261
+ info ("Dependencies obtained: " + str (dependencies ))
262
+ dependencies = set (dependencies ).intersection (
263
+ set (all_recipes )
264
+ )
265
+
266
+ # Add dependencies to argument list:
267
+ if len (dependencies ) > 0 :
268
+ if len (args .requirements ) > 0 :
269
+ args .requirements += u","
270
+ args .requirements += u"," .join (dependencies )
271
+
272
+ except ValueError :
273
+ # Not a python package, apparently.
274
+ warning (
275
+ "Processing failed, is this project a valid "
276
+ "package? Will continue WITHOUT setup.py deps."
277
+ )
278
+
279
+
280
+ def has_a_recipe (ctx , requirement ):
281
+ all_recipes = [
282
+ recipe .lower () for recipe in
283
+ set (Recipe .list_recipes (ctx ))
284
+ ]
285
+ requirement_name = re .sub (r'==\d+(\.\d+)*' , '' , requirement )
286
+ return requirement_name in all_recipes
287
+
288
+
289
+ def __run_pip_compile (requirements ):
290
+ return shprint (
291
+ sh .bash , '-c' ,
292
+ "echo -e '{}' > requirements.in && "
293
+ "{} -m piptools compile --dry-run --annotation-style=line && "
294
+ "rm requirements.in" .format (
295
+ '\n ' .join (requirements ), sys .executable ))
296
+
297
+
298
+ def __parse_pip_compile_output (output ):
299
+ parsed_requirement_info_list = []
300
+ for line in output .splitlines ():
301
+ match_data = re .match (
302
+ r'^([\w.-]+)==(\d+(\.\d+)*).*'
303
+ r'#\s+via\s+([\w\s,.-]+)' , line )
304
+
305
+ if match_data :
306
+ parent_requirements = match_data .group (4 ).split (', ' )
307
+ requirement_name = match_data .group (1 )
308
+ requirement_version = match_data .group (2 )
309
+
310
+ # Requirement is a "non-recipe" one we started with.
311
+ if '-r requirements.in' in parent_requirements :
312
+ parent_requirements .remove ('-r requirements.in' )
313
+
314
+ parsed_requirement_info_list .append ([
315
+ requirement_name ,
316
+ requirement_version ,
317
+ parent_requirements ])
318
+
319
+ return parsed_requirement_info_list
320
+
321
+
322
+ def __run_pip_compile_and_parse_output (requirements ):
323
+ return __parse_pip_compile_output (__run_pip_compile (requirements ))
324
+
325
+
326
+ def __is_requirement_installed_by_recipe (
327
+ ctx , current_requirement_info , remaining_requirement_names ):
328
+
329
+ requirement_name , requirement_version , \
330
+ parent_requirements = current_requirement_info
331
+
332
+ # If any parent requirement has a recipe, this
333
+ # requirement ought also to be installed by it.
334
+ # Hence, it's better not to add this requirement the
335
+ # expanded list.
336
+ parent_requirements_with_recipe = [
337
+ parent_requirement
338
+ for parent_requirement in parent_requirements
339
+ if has_a_recipe (ctx , parent_requirement )
340
+ ]
341
+
342
+ # Any parent requirement removed for the expanded list
343
+ # implies that it and its own requirements (including
344
+ # this requirement) will be installed by a recipe.
345
+ # Hence, it's better not to add this requirement the
346
+ # expanded list.
347
+ parent_requirements_still_in_list = [
348
+ parent_requirement
349
+ for parent_requirement in parent_requirements
350
+ if parent_requirement in remaining_requirement_names
351
+ ]
352
+
353
+ is_installed_by_a_recipe = \
354
+ len (parent_requirements ) and \
355
+ (parent_requirements_with_recipe or
356
+ len (parent_requirements_still_in_list ) !=
357
+ len (parent_requirements ))
358
+
359
+ if is_installed_by_a_recipe :
360
+ info ('{}\n \t {}\n \t {}\n \t {}' .format (
361
+ requirement_name ,
362
+ parent_requirements ,
363
+ parent_requirements_with_recipe ,
364
+ parent_requirements_still_in_list ))
365
+
366
+ return is_installed_by_a_recipe
367
+
368
+
369
+ def __prune_requirements_installed_by_recipe (ctx , requirement_info_list ):
370
+ original_requirement_count = - 1
371
+
372
+ while len (requirement_info_list ) != original_requirement_count :
373
+ original_requirement_count = len (requirement_info_list )
374
+
375
+ for i , requirement_info in enumerate (reversed (requirement_info_list )):
376
+ index = original_requirement_count - i - 1
377
+
378
+ remaining_requirement_names = \
379
+ [x [0 ] for x in requirement_info_list ]
380
+
381
+ if __is_requirement_installed_by_recipe (
382
+ ctx , requirement_info , remaining_requirement_names ):
383
+ info ('{} will be installed by a recipe. Removing '
384
+ 'it from requirement list expansion.' .format (
385
+ requirement_info [0 ]))
386
+
387
+ del requirement_info_list [index ]
388
+
389
+
390
+ def __add_compiled_requirements_to_args (
391
+ ctx , args , compiled_requirment_info_list ):
392
+
393
+ for requirement_info in compiled_requirment_info_list :
394
+ requirement_name , requirement_version , \
395
+ parent_requirements = requirement_info
396
+
397
+ # If the requirement has a recipe, don't use specific
398
+ # version constraints determined by pip-compile. Some
399
+ # recipes may not support the specified version. Therefor,
400
+ # it's probably safer to just let them use their default
401
+ # version. User can still force the usage of specific
402
+ # version by explicitly declaring it with --requirements.
403
+ requirement_str = \
404
+ requirement_name if has_a_recipe (ctx , requirement_name ) else \
405
+ '{}=={}' .format (requirement_name , requirement_version )
406
+
407
+ requirement_names_arg = split_argument_list (re .sub (
408
+ r'==\d+(\.\d+)*' , '' , args .requirements ))
409
+
410
+ # This expansion was carried out based on "non-recipe"
411
+ # requirements. Hence,the counter-part, requirements
412
+ # with a recipe, may already be part of list.
413
+ if requirement_name not in requirement_names_arg :
414
+ args .requirements += ',' + requirement_str
415
+
416
+
417
+ def __expand_requirements_arg_from_pip_compile (ctx , args ):
418
+ """Use pip-compile to generate requirement dependencies and add to
419
+ --requirements command line argument."""
420
+
421
+ non_recipe_requirements = [
422
+ requirement for requirement in split_argument_list (args .requirements )
423
+ if not has_a_recipe (ctx , requirement )
424
+ ]
425
+ non_recipe_requirements_regex = \
426
+ r',?\s+' + r'|,?\s+' .join (non_recipe_requirements )
427
+ args .requirements = \
428
+ re .sub (non_recipe_requirements_regex , '' , args .requirements )
429
+
430
+ # Compile "non-recipe" requirements' dependencies and add to
431
+ # args.requirement. Otherwise, only recipe requirements'
432
+ # dependencies would get installed.
433
+ # More info https://github.com/kivy/python-for-android/issues/2529
434
+ if non_recipe_requirements :
435
+ info ("Compiling dependencies for: "
436
+ "{}" .format (non_recipe_requirements ))
437
+
438
+ parsed_requirement_info_list = \
439
+ __run_pip_compile_and_parse_output (non_recipe_requirements )
440
+
441
+ info ("Requirements obtained from pip-compile: "
442
+ "{}" .format (["{}=={}" .format (x [0 ], x [1 ])
443
+ for x in parsed_requirement_info_list ]))
444
+
445
+ __prune_requirements_installed_by_recipe (
446
+ ctx , parsed_requirement_info_list )
447
+
448
+ info ("Requirements remaining after recipe dependency \" prunage\" : "
449
+ "{}" .format (["{}=={}" .format (x [0 ], x [1 ])
450
+ for x in parsed_requirement_info_list ]))
451
+
452
+ __add_compiled_requirements_to_args (
453
+ ctx , args , parsed_requirement_info_list )
454
+
455
+
456
+ def expand_requirements_args (ctx , args ):
457
+ """Expand --requirements arg value to include what may have not
458
+ been specified by the user, such as:
459
+ * requirements specified in local project setup.py or pyproject.toml
460
+ (if --use_setup_py was used)
461
+ * indirect requirements (i.e., the requirements of our requirements).
462
+ (e.g., if user specifies beautifulsoup4, the appropriate version of
463
+ soupsieve is added).
464
+ """
465
+ __expand_requirements_arg_from_project_files (ctx , args )
466
+ __expand_requirements_arg_from_pip_compile (ctx , args )
467
+
468
+ info ('Expanded Requirements List: '
469
+ '{}' .format (split_argument_list (args .requirements )))
470
+
471
+
230
472
class NoAbbrevParser (argparse .ArgumentParser ):
231
473
"""We want to disable argument abbreviation so as not to interfere
232
474
with passing through arguments to build.py, but in python2 argparse
@@ -645,65 +887,6 @@ def add_parser(subparsers, *args, **kwargs):
645
887
self .ctx .with_debug_symbols = getattr (
646
888
args , "with_debug_symbols" , False
647
889
)
648
-
649
- have_setup_py_or_similar = False
650
- if getattr (args , "private" , None ) is not None :
651
- project_dir = getattr (args , "private" )
652
- if (os .path .exists (os .path .join (project_dir , "setup.py" )) or
653
- os .path .exists (os .path .join (project_dir ,
654
- "pyproject.toml" ))):
655
- have_setup_py_or_similar = True
656
-
657
- # Process requirements and put version in environ
658
- if hasattr (args , 'requirements' ):
659
- requirements = []
660
-
661
- # Add dependencies from setup.py, but only if they are recipes
662
- # (because otherwise, setup.py itself will install them later)
663
- if (have_setup_py_or_similar and
664
- getattr (args , "use_setup_py" , False )):
665
- try :
666
- info ("Analyzing package dependencies. MAY TAKE A WHILE." )
667
- # Get all the dependencies corresponding to a recipe:
668
- dependencies = [
669
- dep .lower () for dep in
670
- get_dep_names_of_package (
671
- args .private ,
672
- keep_version_pins = True ,
673
- recursive = True ,
674
- verbose = True ,
675
- )
676
- ]
677
- info ("Dependencies obtained: " + str (dependencies ))
678
- all_recipes = [
679
- recipe .lower () for recipe in
680
- set (Recipe .list_recipes (self .ctx ))
681
- ]
682
- dependencies = set (dependencies ).intersection (
683
- set (all_recipes )
684
- )
685
- # Add dependencies to argument list:
686
- if len (dependencies ) > 0 :
687
- if len (args .requirements ) > 0 :
688
- args .requirements += u","
689
- args .requirements += u"," .join (dependencies )
690
- except ValueError :
691
- # Not a python package, apparently.
692
- warning (
693
- "Processing failed, is this project a valid "
694
- "package? Will continue WITHOUT setup.py deps."
695
- )
696
-
697
- # Parse --requirements argument list:
698
- for requirement in split_argument_list (args .requirements ):
699
- if "==" in requirement :
700
- requirement , version = requirement .split (u"==" , 1 )
701
- os .environ ["VERSION_{}" .format (requirement )] = version
702
- info ('Recipe {}: version "{}" requested' .format (
703
- requirement , version ))
704
- requirements .append (requirement )
705
- args .requirements = u"," .join (requirements )
706
-
707
890
self .warn_on_deprecated_args (args )
708
891
709
892
self .storage_dir = args .storage_dir
@@ -723,6 +906,21 @@ def add_parser(subparsers, *args, **kwargs):
723
906
self .ctx .activity_class_name = args .activity_class_name
724
907
self .ctx .service_class_name = args .service_class_name
725
908
909
+ # Process requirements and put version in environ:
910
+ if getattr (args , 'requirements' , []):
911
+ expand_requirements_args (self .ctx , args )
912
+
913
+ # Handle specific version requirement constraints (e.g. foo==x.y)
914
+ requirements = []
915
+ for requirement in split_argument_list (args .requirements ):
916
+ if "==" in requirement :
917
+ requirement , version = requirement .split (u"==" , 1 )
918
+ os .environ ["VERSION_{}" .format (requirement )] = version
919
+ info ('Recipe {}: version "{}" requested' .format (
920
+ requirement , version ))
921
+ requirements .append (requirement )
922
+ args .requirements = u"," .join (requirements )
923
+
726
924
# Each subparser corresponds to a method
727
925
command = args .subparser_name .replace ('-' , '_' )
728
926
getattr (self , command )(args )
0 commit comments