2015-06-13 01:03:31 +02:00
#!/usr/bin/env python3
2017-11-19 14:29:49 +01:00
" Script to generate a build order respecting package dependencies. "
2015-06-13 01:03:31 +02:00
2015-12-24 04:43:35 +01:00
import os
2016-04-29 14:14:28 +02:00
import re
2015-12-24 04:43:35 +01:00
import sys
2015-06-13 01:03:31 +02:00
2015-12-24 07:52:06 +01:00
from itertools import filterfalse
def unique_everseen ( iterable , key = None ) :
2017-11-19 14:29:49 +01:00
""" List unique elements, preserving order. Remember all elements ever seen.
See https : / / docs . python . org / 3 / library / itertools . html #itertools-recipes
Examples :
unique_everseen ( ' AAAABBBCCDAABBB ' ) - - > A B C D
unique_everseen ( ' ABBCcAD ' , str . lower ) - - > A B C D """
2015-12-24 07:52:06 +01:00
seen = set ( )
seen_add = seen . add
if key is None :
for element in filterfalse ( seen . __contains__ , iterable ) :
seen_add ( element )
yield element
else :
for element in iterable :
k = key ( element )
if k not in seen :
seen_add ( k )
yield element
2015-12-24 04:43:35 +01:00
def die ( msg ) :
2017-11-19 14:29:49 +01:00
" Exit the process with an error message. "
2015-12-24 04:43:35 +01:00
sys . exit ( ' ERROR: ' + msg )
2015-06-13 01:03:31 +02:00
2017-11-19 14:29:49 +01:00
def parse_build_file_dependencies ( path ) :
" Extract the dependencies of a build.sh or *.subpackage.sh file. "
dependencies = [ ]
with open ( path , encoding = " utf-8 " ) as build_script :
for line in build_script :
2019-03-20 17:23:35 +01:00
if line . startswith ( ( ' TERMUX_PKG_DEPENDS ' , ' TERMUX_PKG_BUILD_DEPENDS ' , ' TERMUX_SUBPKG_DEPENDS ' , ' TERMUX_PKG_DEVPACKAGE_DEPENDS ' ) ) :
2019-01-13 18:45:40 +01:00
dependencies_string = line . split ( ' DEPENDS= ' ) [ 1 ]
for char in " \" ' \n " :
dependencies_string = dependencies_string . replace ( char , ' ' )
2017-11-19 14:29:49 +01:00
2019-01-13 18:45:40 +01:00
# Split also on '|' to dependencies with '|', as in 'nodejs | nodejs-current':
for dependency_value in re . split ( ' ,| \\ | ' , dependencies_string ) :
# Replace parenthesis to ignore version qualifiers as in "gcc (>= 5.0)":
dependency_value = re . sub ( r ' \ (.*? \ ) ' , ' ' , dependency_value ) . strip ( )
2015-12-24 07:20:47 +01:00
2019-01-13 18:45:40 +01:00
dependencies . append ( dependency_value )
2015-12-24 07:20:47 +01:00
2017-11-19 14:29:49 +01:00
return set ( dependencies )
2015-12-24 07:20:47 +01:00
2015-12-24 06:04:28 +01:00
class TermuxPackage ( object ) :
2017-11-19 14:29:49 +01:00
" A main package definition represented by a directory with a build.sh file. "
2019-03-20 17:54:44 +01:00
def __init__ ( self , dir_path , fast_build_mode ) :
2017-11-04 01:18:32 +01:00
self . dir = dir_path
self . name = os . path . basename ( self . dir )
2015-12-24 07:20:47 +01:00
# search package build.sh
build_sh_path = os . path . join ( self . dir , ' build.sh ' )
if not os . path . isfile ( build_sh_path ) :
2017-11-04 01:18:32 +01:00
raise Exception ( " build.sh not found for package ' " + self . name + " ' " )
2015-12-24 07:20:47 +01:00
2017-11-19 14:29:49 +01:00
self . deps = parse_build_file_dependencies ( build_sh_path )
2019-08-06 14:39:42 +02:00
2019-08-12 17:28:41 +02:00
if os . getenv ( ' TERMUX_ON_DEVICE_BUILD ' ) == " true " :
2019-08-06 14:39:42 +02:00
always_deps = [ ' libc++ ' ]
for dependency_name in always_deps :
if dependency_name not in self . deps and self . name not in always_deps :
self . deps . add ( dependency_name )
2015-12-24 07:20:47 +01:00
# search subpackages
self . subpkgs = [ ]
for filename in os . listdir ( self . dir ) :
2017-11-19 14:29:49 +01:00
if not filename . endswith ( ' .subpackage.sh ' ) :
continue
2017-11-04 01:18:32 +01:00
subpkg = TermuxSubPackage ( self . dir + ' / ' + filename , self )
2015-12-24 07:20:47 +01:00
self . subpkgs . append ( subpkg )
2019-03-26 13:21:44 +01:00
self . deps . add ( subpkg . name )
2015-12-24 07:20:47 +01:00
self . deps | = subpkg . deps
2019-08-12 21:48:55 +02:00
subpkg = TermuxSubPackage ( self . dir + ' / ' + self . name + ' -static ' + ' .subpackage.sh ' , self , virtual = True )
self . subpkgs . append ( subpkg )
2015-12-24 07:20:47 +01:00
# Do not depend on itself
self . deps . discard ( self . name )
# Do not depend on any sub package
2019-03-20 17:54:44 +01:00
if not fast_build_mode :
self . deps . difference_update ( [ subpkg . name for subpkg in self . subpkgs ] )
2015-12-24 07:20:47 +01:00
2017-11-19 14:29:49 +01:00
self . needed_by = set ( ) # Populated outside constructor, reverse of deps.
2015-12-24 07:20:47 +01:00
def __repr__ ( self ) :
return " < {} ' {} ' > " . format ( self . __class__ . __name__ , self . name )
2017-11-19 14:29:49 +01:00
def recursive_dependencies ( self , pkgs_map ) :
" All the dependencies of the package, both direct and indirect. "
result = [ ]
for dependency_name in sorted ( self . deps ) :
dependency_package = pkgs_map [ dependency_name ]
result + = dependency_package . recursive_dependencies ( pkgs_map )
result + = [ dependency_package ]
return unique_everseen ( result )
2015-12-24 07:20:47 +01:00
2017-11-19 14:29:49 +01:00
class TermuxSubPackage :
" A sub-package represented by a $ {PACKAGE_NAME} .subpackage.sh file. "
2019-03-20 19:46:03 +01:00
def __init__ ( self , subpackage_file_path , parent , virtual = False ) :
2015-12-24 07:20:47 +01:00
if parent is None :
raise Exception ( " SubPackages should have a parent " )
2017-11-04 01:18:32 +01:00
self . name = os . path . basename ( subpackage_file_path ) . split ( ' .subpackage.sh ' ) [ 0 ]
2015-12-24 07:20:47 +01:00
self . parent = parent
2019-04-06 00:45:58 +02:00
self . deps = set ( [ parent . name ] )
if not virtual :
self . deps | = parse_build_file_dependencies ( subpackage_file_path )
2019-03-02 23:34:54 +01:00
self . dir = parent . dir
self . needed_by = set ( ) # Populated outside constructor, reverse of deps.
2015-12-24 07:20:47 +01:00
def __repr__ ( self ) :
return " < {} ' {} ' parent= ' {} ' > " . format ( self . __class__ . __name__ , self . name , self . parent )
2015-12-24 04:43:35 +01:00
2019-03-02 23:34:54 +01:00
def recursive_dependencies ( self , pkgs_map ) :
""" All the dependencies of the subpackage, both direct and indirect.
Only relevant when building in fast - build mode """
result = [ ]
for dependency_name in sorted ( self . deps ) :
2019-03-20 17:54:44 +01:00
if dependency_name == self . parent . name :
self . parent . deps . discard ( self . name )
2019-03-02 23:34:54 +01:00
dependency_package = pkgs_map [ dependency_name ]
2019-03-26 13:21:44 +01:00
if dependency_package not in self . parent . subpkgs :
result + = dependency_package . recursive_dependencies ( pkgs_map )
2019-03-02 23:34:54 +01:00
result + = [ dependency_package ]
return unique_everseen ( result )
def read_packages_from_directories ( directories , fast_build_mode ) :
2017-11-19 14:29:49 +01:00
""" Construct a map from package name to TermuxPackage.
2019-03-02 23:34:54 +01:00
Subpackages are mapped to the parent package if fast_build_mode is false . """
2017-11-19 14:29:49 +01:00
pkgs_map = { }
2017-11-04 01:18:32 +01:00
all_packages = [ ]
2017-11-19 14:29:49 +01:00
for package_dir in directories :
2017-11-04 01:18:32 +01:00
for pkgdir_name in sorted ( os . listdir ( package_dir ) ) :
dir_path = package_dir + ' / ' + pkgdir_name
if os . path . isfile ( dir_path + ' /build.sh ' ) :
2019-03-20 17:54:44 +01:00
new_package = TermuxPackage ( package_dir + ' / ' + pkgdir_name , fast_build_mode )
2015-12-24 04:47:38 +01:00
2017-11-19 14:29:49 +01:00
if new_package . name in pkgs_map :
die ( ' Duplicated package: ' + new_package . name )
else :
pkgs_map [ new_package . name ] = new_package
all_packages . append ( new_package )
2015-12-24 07:20:47 +01:00
2017-11-19 14:29:49 +01:00
for subpkg in new_package . subpkgs :
if subpkg . name in pkgs_map :
die ( ' Duplicated package: ' + subpkg . name )
2019-03-02 23:34:54 +01:00
elif fast_build_mode :
pkgs_map [ subpkg . name ] = subpkg
2017-11-19 14:29:49 +01:00
else :
pkgs_map [ subpkg . name ] = new_package
all_packages . append ( subpkg )
2015-12-24 04:47:38 +01:00
2017-11-19 14:29:49 +01:00
for pkg in all_packages :
for dependency_name in pkg . deps :
if dependency_name not in pkgs_map :
die ( ' Package %s depends on non-existing package " %s " ' % ( pkg . name , dependency_name ) )
dep_pkg = pkgs_map [ dependency_name ]
2019-03-02 23:34:54 +01:00
if fast_build_mode or not isinstance ( pkg , TermuxSubPackage ) :
2017-11-19 14:29:49 +01:00
dep_pkg . needed_by . add ( pkg )
return pkgs_map
2015-12-24 07:20:47 +01:00
2017-11-19 14:29:49 +01:00
def generate_full_buildorder ( pkgs_map ) :
" Generate a build order for building all packages. "
2015-12-24 07:52:06 +01:00
build_order = [ ]
2015-12-24 06:04:28 +01:00
# List of all TermuxPackages without dependencies
2015-12-24 07:20:47 +01:00
leaf_pkgs = [ pkg for name , pkg in pkgs_map . items ( ) if not pkg . deps ]
if not leaf_pkgs :
die ( ' No package without dependencies - where to start? ' )
2015-12-24 04:47:38 +01:00
2016-09-16 11:48:02 +02:00
# Sort alphabetically:
pkg_queue = sorted ( leaf_pkgs , key = lambda p : p . name )
2015-12-24 04:47:38 +01:00
# Topological sorting
2015-12-24 07:20:47 +01:00
visited = set ( )
2017-11-19 14:29:49 +01:00
# Tracks non-visited deps for each package
remaining_deps = { }
for name , pkg in pkgs_map . items ( ) :
remaining_deps [ name ] = set ( pkg . deps )
for subpkg in pkg . subpkgs :
remaining_deps [ subpkg . name ] = set ( subpkg . deps )
2015-12-24 07:20:47 +01:00
while pkg_queue :
pkg = pkg_queue . pop ( 0 )
if pkg . name in visited :
continue
# print("Processing {}:".format(pkg.name), pkg.needed_by)
2015-12-24 07:34:27 +01:00
visited . add ( pkg . name )
2015-12-24 04:47:38 +01:00
build_order . append ( pkg )
2015-12-24 07:20:47 +01:00
for other_pkg in sorted ( pkg . needed_by , key = lambda p : p . name ) :
2015-12-24 07:34:27 +01:00
# Remove this pkg from deps
2015-12-24 07:20:47 +01:00
remaining_deps [ other_pkg . name ] . discard ( pkg . name )
# ... and all its subpackages
remaining_deps [ other_pkg . name ] . difference_update (
[ subpkg . name for subpkg in pkg . subpkgs ]
)
2015-12-24 07:34:27 +01:00
if not remaining_deps [ other_pkg . name ] : # all deps were already appended?
pkg_queue . append ( other_pkg ) # should be processed
2015-12-24 07:20:47 +01:00
if set ( pkgs_map . values ( ) ) != set ( build_order ) :
2015-12-24 04:47:38 +01:00
print ( " ERROR: Cycle exists. Remaining: " )
2015-12-24 07:20:47 +01:00
for name , pkg in pkgs_map . items ( ) :
2015-12-24 04:47:38 +01:00
if pkg not in build_order :
2015-12-24 07:20:47 +01:00
print ( name , remaining_deps [ name ] )
2015-12-24 04:47:38 +01:00
sys . exit ( 1 )
2015-12-24 07:52:06 +01:00
return build_order
2015-12-24 04:47:38 +01:00
2019-03-02 23:34:54 +01:00
def generate_target_buildorder ( target_path , pkgs_map , fast_build_mode ) :
2017-11-19 14:29:49 +01:00
" Generate a build order for building the dependencies of the specified package. "
if target_path . endswith ( ' / ' ) :
target_path = target_path [ : - 1 ]
2015-12-24 07:52:06 +01:00
2017-11-19 14:29:49 +01:00
package_name = os . path . basename ( target_path )
package = pkgs_map [ package_name ]
2019-03-20 17:54:44 +01:00
# Do not depend on any sub package
if fast_build_mode :
package . deps . difference_update ( [ subpkg . name for subpkg in package . subpkgs ] )
2017-11-19 14:29:49 +01:00
return package . recursive_dependencies ( pkgs_map )
2015-12-24 07:29:34 +01:00
2017-11-19 14:29:49 +01:00
def main ( ) :
" Generate the build order either for all packages or a specific one. "
2019-03-02 23:34:54 +01:00
import argparse
parser = argparse . ArgumentParser ( description = ' Generate order in which to build dependencies for a package. Generates ' )
parser . add_argument ( ' -i ' , default = False , action = ' store_true ' ,
help = ' Generate dependency list for fast-build mode. This includes subpackages in output since these can be downloaded. ' )
parser . add_argument ( ' package ' , nargs = ' ? ' ,
help = ' Package to generate dependency list for. ' )
parser . add_argument ( ' package_dirs ' , nargs = ' * ' ,
2022-04-16 08:50:09 +02:00
help = ' Directories with packages. Can for example point to " ../community-packages/packages " . Note that the packages suffix is no longer added automatically if not present. ' )
2019-03-02 23:34:54 +01:00
args = parser . parse_args ( )
fast_build_mode = args . i
package = args . package
2019-03-02 23:47:13 +01:00
packages_directories = args . package_dirs
2019-03-02 23:34:54 +01:00
if not package :
full_buildorder = True
else :
full_buildorder = False
if fast_build_mode and full_buildorder :
die ( ' -i mode does not work when building all packages ' )
2017-11-04 01:18:32 +01:00
if not full_buildorder :
2019-03-02 23:34:54 +01:00
for path in packages_directories :
2017-11-04 01:18:32 +01:00
if not os . path . isdir ( path ) :
die ( ' Not a directory: ' + path )
2019-03-02 23:34:54 +01:00
if package :
if package [ - 1 ] == " / " :
package = package [ : - 1 ]
if not os . path . isdir ( package ) :
die ( ' Not a directory: ' + package )
if not os . path . relpath ( os . path . dirname ( package ) , ' . ' ) in packages_directories :
packages_directories . insert ( 0 , os . path . dirname ( package ) )
pkgs_map = read_packages_from_directories ( packages_directories , fast_build_mode )
2015-12-24 07:29:34 +01:00
2017-11-04 01:18:32 +01:00
if full_buildorder :
2017-11-19 14:29:49 +01:00
build_order = generate_full_buildorder ( pkgs_map )
2015-12-24 07:52:06 +01:00
else :
2019-03-02 23:34:54 +01:00
build_order = generate_target_buildorder ( package , pkgs_map , fast_build_mode )
2015-12-24 07:29:34 +01:00
2017-11-19 14:29:49 +01:00
for pkg in build_order :
2019-03-02 23:34:54 +01:00
print ( " %-30s %s " % ( pkg . name , pkg . dir ) )
2017-11-19 14:29:49 +01:00
if __name__ == ' __main__ ' :
main ( )