├── .gitignore ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README ├── README.rst ├── RELEASING.rst ├── bin └── android-sdk-updater ├── sdk_updater ├── __init__.py ├── list.py ├── package.py ├── scan.py └── update.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | dist 4 | build 5 | .idea 6 | .DS_Store -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | Change Log 4 | ---------- 5 | 6 | 1.0.1 (2016-08-01) 7 | ++++++++++++++++++ 8 | 9 | Version bump to fix a packaging issue. 10 | 11 | 1.0.0 (2016-07-24) 12 | ++++++++++++++++++ 13 | 14 | We're stable enough. 15 | 16 | 17 | 0.0.4 (2016-07-24) 18 | ++++++++++++++++++ 19 | 20 | **Bugfixes** 21 | 22 | - Display a warning for missing remote packages instead of exiting fatally. 23 | - Exit fatally when the remote is missing explicitly-requested packages. 24 | - Standardize error messaging: 25 | - Prepend "WARNING:" for non-fatal messages 26 | - Prepend "FATAL:" for fatal messages 27 | 28 | 29 | 0.0.3 (2016-04-16) 30 | ++++++++++++++++++ 31 | 32 | **Features** 33 | 34 | - `--show` lists packages that are `installed`, `available`, and `updates`. Thanks to jules2689 for this feature. 35 | 36 | **Bugfixes** 37 | 38 | - Handle symlinked SDK install directories. Thanks to jules2689. 39 | - Fix name-mangling for system-image packages. Thanks again to jules2689. 40 | - Blacklist `temp` directories which may be created by newer versions of Android Studio. 41 | - Ignore the `license` property for `platform-tools` packages, which has been removed for release candidates. 42 | 43 | 44 | 0.0.2 (2015-12-17) 45 | ++++++++++++++++++ 46 | 47 | **Bugfixes** 48 | 49 | - Handle Unicode strings properly for Python 2. 50 | - Blacklist the NDK package. Plans are in place to support this in a future release. 51 | - Use non-zero exit code when installation of one or more packages fail. 52 | 53 | 54 | 0.0.1 (2015-11-27) 55 | ++++++++++++++++++ 56 | 57 | **Birth** 58 | 59 | - Support incremental updates of an SDK installation. 60 | - Support forcing installation of arbitrary SDK packages. 61 | - Read packages from the command line or STDIN. 62 | - Configurable timeouts for package installation. 63 | - Dry-run and verbose output modes for debugging. 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Tad Fisher 2 | Copyright 2016 Tristan Waddington 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include HISTORY.rst 2 | include LICENSE 3 | include README 4 | include README.rst 5 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | See README.rst. 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DEPRECATED 2 | ========== 3 | 4 | This project will no longer be maintained. Version 2.2 of the Android Gradle 5 | plugin automatically downloads Android tools and SDK dependencies, which 6 | obviates the rationale for this project. 7 | 8 | android-sdk-updater 9 | =================== 10 | 11 | A tool for keeping your Android SDK dependencies up-to-date in unattended environments. Pass it your 12 | :code:`$ANDROID_HOME` and let it update your installed SDK packages as new revisions are released. Optionally, provide a 13 | list of package names (from :code:`android list sdk --all --extended`) to bootstrap your environment, or to ensure your 14 | latest set of dependencies are installed for your next CI run. 15 | 16 | This tool is especially useful for continuous-integration servers. 17 | 18 | Why do I need this? 19 | ------------------- 20 | 21 | The command-line tools provided by the SDK are not especially useful for unattended use. Among other problems: 22 | 23 | - There is no built-in method to list packages that are already installed. This may be because there is no 24 | easily-consumed index of installed packages provided, so the SDK manager relies on directory scanning and 25 | name-mangling. So does this tool, by the way. 26 | 27 | - Some packages are unnecessarily downloaded and re-installed with no rhyme or reason when the latest version is already 28 | installed. 29 | 30 | - Some packages are *not* automatically updated when an incremental update is available. 31 | 32 | - The package installer requires input from STDIN to actually install packages, because it assumes a human is present to 33 | accept software licenses. 34 | 35 | This tool performs all of the gritty scanning, mangling, parsing, and input-faking necessary to determine: 36 | 37 | - Packages you have installed, and their revisions. 38 | - Packages that are available from the official update sites, and their revisions. 39 | - Local packages which should be updated due to an available revision-bump. 40 | - Which packages were actually installed, and which failed to install, after the installer has run. 41 | 42 | The ultimate goal of this project is to cease its existence when the Android Tools team addresses these pain points. 43 | These are mostly solved problems in the GUI tool, but they make unattended builds a hassle. 44 | 45 | Disclaimer 46 | ---------- 47 | 48 | **By using this tool you acknowledge that associated licenses of the components downloaded are accepted automatically on 49 | your behalf. You are required to have accepted the respective licenses of these components prior to using this tool.** 50 | 51 | Requirements 52 | ------------ 53 | 54 | Tested with Python versions 2.7 and 3.5. 55 | 56 | Dependencies: 57 | 58 | - :code:`jprops` 59 | - :code:`pexpect` 60 | - :code:`semantic_version` 61 | 62 | Installing 63 | ---------- 64 | 65 | Using :code:`pip`:: 66 | 67 | $ pip install android-sdk-updater 68 | 69 | From source:: 70 | 71 | $ git clone https://github.com/tadfisher/android-sdk-updater.git 72 | $ cd android-sdk-updater 73 | $ python setup.py install 74 | 75 | For development:: 76 | 77 | $ python setup.py develop 78 | 79 | Usage 80 | ----- 81 | 82 | :: 83 | 84 | usage: android-sdk-updater [-h] [-v] [-a ANDROID_HOME] [-d] [-t TIMEOUT] [-vv] 85 | [-o ...] [-s {available,installed,updates}] 86 | [package [package ...]] 87 | 88 | Update an Android SDK installation 89 | 90 | positional arguments: 91 | package name of SDK package to install if not already 92 | installed 93 | 94 | optional arguments: 95 | -h, --help show this help message and exit 96 | -v, --version show program's version number and exit 97 | -a ANDROID_HOME, --android-home ANDROID_HOME 98 | the path to your Android SDK 99 | -d, --dry-run compute packages to install but do not install 100 | anything 101 | -t TIMEOUT, --timeout TIMEOUT 102 | timeout in seconds for package installation, or 0 to 103 | wait indefinitely (default) 104 | -vv, --verbose show extra output from android tools 105 | -o ..., --options ... 106 | options to pass to the "android" tool; must be the 107 | final option specified 108 | -s {available,installed,updates}, --show {available,installed,updates} 109 | Show available or installed packages 110 | 111 | Additional whitespace-delimited :code:`package` arguments can be piped to this tool over the standard input. 112 | 113 | Examples 114 | -------- 115 | 116 | Perform an incremental update of all packages in :code:`$ANDROID_HOME`:: 117 | 118 | $ android-sdk-updater 119 | 120 | Perform an incremental update of all packages in :code:`/foo/sdk`:: 121 | 122 | $ android-sdk-updater --android-home=/foo/sdk 123 | 124 | Update all packages in :code:`$ANDROID_HOME` and ensure the installation of packages :code:`android-23` and 125 | :code:`extra-google-google_play_services`:: 126 | 127 | $ android-sdk-updater android-23 extra-google-google_play_services 128 | 129 | Update all packages in :code:`ANDROID_HOME` and ensure the installation of packages contained in a file:: 130 | 131 | $ cat packages.txt 132 | tools 133 | platform-tools 134 | build-tools-23.0.2 135 | android-23 136 | addon-google_apis-google-23 137 | extra-android-m2repository 138 | extra-google-m2repository 139 | extra-android-support 140 | extra-google-google_play_services 141 | sys-img-x86_64-addon-google_apis-google-23 142 | 143 | $ cat packages.txt | android-sdk-updater 144 | 145 | Same as the above, but through a proxy:: 146 | 147 | $ cat packages.txt | android-sdk-updater -o --no-https --proxy-host example.com --proxy-port 3218 148 | 149 | Show installed packages, available packags, or packages with updates:: 150 | 151 | $ android-sdk-updater -s installed 152 | 153 | $ android-sdk-updater -s available 154 | 155 | $ android-sdk-updater -s updates 156 | 157 | Caveats 158 | ------- 159 | 160 | The Android NDK is not supported. We plan to support installing and updating the NDK in a future release. In the 161 | meantime, you may see output that includes the following:: 162 | 163 | Ignoring 'ndk-bundle' as it is blacklisted. 164 | 165 | These warnings may be safely ignored. 166 | 167 | License 168 | ------- 169 | 170 | :: 171 | 172 | Copyright 2016 Tad Fisher 173 | Copyright 2016 Tristan Waddington 174 | 175 | Licensed under the Apache License, Version 2.0 (the "License"); 176 | you may not use this file except in compliance with the License. 177 | You may obtain a copy of the License at 178 | 179 | http://www.apache.org/licenses/LICENSE-2.0 180 | 181 | Unless required by applicable law or agreed to in writing, software 182 | distributed under the License is distributed on an "AS IS" BASIS, 183 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 184 | See the License for the specific language governing permissions and 185 | limitations under the License. 186 | -------------------------------------------------------------------------------- /RELEASING.rst: -------------------------------------------------------------------------------- 1 | Publishing 2 | ========== 3 | 4 | :: 5 | 6 | # Register with pypi (only done once) 7 | $ python setup.py register 8 | 9 | # Upload a new distribution to PyPI 10 | $ python setup.py sdist bdist_wheel upload 11 | -------------------------------------------------------------------------------- /bin/android-sdk-updater: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import os 7 | import sys 8 | 9 | from sdk_updater import __version__ 10 | from sdk_updater import update as update_sdk 11 | from sdk_updater.scan import scan 12 | from sdk_updater.list import list_packages 13 | from sdk_updater.update import updates_available 14 | 15 | 16 | def show(android_home, args): 17 | android = os.path.join(android_home, 'tools', 'android') 18 | 19 | if args.show == "installed": 20 | packages = scan(android_home, verbose=args.verbose) 21 | elif args.show == "updates": 22 | ops, missing = updates_available(android, 23 | android_home, 24 | packages=None, 25 | options=None, 26 | verbose=args.verbose) 27 | packages = [op.package for op in ops] 28 | elif args.show == "available": 29 | packages = list_packages(android, 30 | options=args.options, 31 | verbose=args.verbose) 32 | else: 33 | raise NotImplementedError("Unknown argument '{}'".format(args.show)) 34 | 35 | if packages is not None and packages: 36 | print('\n'.join([str(p) for p in packages])) 37 | 38 | 39 | def default(android_home, args): 40 | # Read packages from stdin 41 | packages = list(args.package) 42 | if not sys.stdin.isatty(): 43 | for line in sys.stdin: 44 | packages.extend(line.split()) 45 | 46 | # Run the updater 47 | update_sdk.main(android_home, 48 | packages=packages, 49 | options=args.options, 50 | verbose=args.verbose, 51 | timeout=args.timeout, 52 | dry_run=args.dry_run) 53 | 54 | 55 | def main(): 56 | # Create our parser 57 | parser = argparse.ArgumentParser( 58 | prog='android-sdk-updater', 59 | description='Update an Android SDK installation') 60 | 61 | # Set up our command-line arguments 62 | parser.add_argument('-v', '--version', action='version', 63 | version='%(prog)s {v}'.format(v=__version__)) 64 | parser.add_argument('-a', '--android-home', 65 | help='the path to your Android SDK') 66 | parser.add_argument('-d', '--dry-run', action='store_true', 67 | help='compute packages to install but do not install anything') 68 | parser.add_argument('-t', '--timeout', type=int, default=0, 69 | help='timeout in seconds for package installation, or 0 to wait indefinitely (default)') 70 | parser.add_argument('-vv', '--verbose', action='store_true', 71 | help='show extra output from android tools') 72 | parser.add_argument('-o', '--options', nargs=argparse.REMAINDER, 73 | help='options to pass to the "android" tool; must be the final option specified') 74 | parser.add_argument('package', nargs='*', 75 | help='name of SDK package to install if not already installed') 76 | parser.add_argument('-s', '--show', choices=['available', 'installed', 'updates'], 77 | help='Show available or installed packages') 78 | 79 | # Get our arguments 80 | args = parser.parse_args() 81 | 82 | # Find the Android SDK 83 | android_home = os.environ.get('ANDROID_HOME') 84 | 85 | if args.android_home: 86 | android_home = args.android_home 87 | 88 | if not android_home: 89 | parser.error('Please set --android-home or export $ANDROID_HOME in your environment') 90 | 91 | if args.timeout < 0: 92 | parser.error('Timeout must be a positive number of seconds') 93 | 94 | if args.show: 95 | show(android_home, args) 96 | else: 97 | default(android_home, args) 98 | 99 | 100 | if __name__ == '__main__': 101 | try: 102 | sys.exit(main()) 103 | except KeyboardInterrupt: 104 | sys.exit() 105 | -------------------------------------------------------------------------------- /sdk_updater/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | android-sdk-updater 3 | 4 | :copyright: (c) 2016 Tad Fisher 5 | :license: Apache 2.0, see LICENSE for more details. 6 | 7 | """ 8 | 9 | __version__ = '1.0.1' 10 | __author__ = 'Tad Fisher' 11 | __license__ = 'Apache 2.0' 12 | __copyright__ = 'Copyright 2016 Tad Fisher' 13 | -------------------------------------------------------------------------------- /sdk_updater/list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import re 7 | import sys 8 | import subprocess 9 | 10 | from semantic_version import Version 11 | 12 | from sdk_updater.package import Package 13 | 14 | categories = { 15 | 'Addon': 'add-ons', 16 | 'BuildTool': 'build-tools', 17 | 'Doc': 'docs', 18 | 'Extra': 'extras', 19 | 'Platform': 'platforms', 20 | 'PlatformTool': 'platform-tools', 21 | 'Sample': 'samples', 22 | 'Source': 'sources', 23 | 'SystemImage': 'system-images', 24 | 'Tool': 'tools' 25 | } 26 | 27 | 28 | def list_packages(android, options=None, verbose=False): 29 | if options is None: 30 | options = [] 31 | packages = [] 32 | separator = '----------' 33 | 34 | out = subprocess.check_output( 35 | [android, 'list', 'sdk', '--all', '--extended'] + options, 36 | stderr=subprocess.PIPE) 37 | 38 | out = out.decode('utf-8') 39 | 40 | if verbose: 41 | print(out) 42 | 43 | fields = out.split(separator)[1:] 44 | p_id = re.compile('^id: (\d+) or "(.+)"$', flags=re.MULTILINE) 45 | p_revision = re.compile('[Rr]evision (.+)') 46 | p_type = re.compile('Type: (\w+)') 47 | for field in fields: 48 | m = p_id.search(field) 49 | if m is None: 50 | print("WARNING: Failed to parse package ID:", field, file=sys.stderr) 51 | continue 52 | num, name = m.groups() 53 | m = p_revision.search(field) 54 | if m is None: 55 | print("WARNING: Failed to parse revision:", field, file=sys.stderr) 56 | continue 57 | revision, = m.groups() 58 | revision = revision.replace(' (Obsolete)', '') 59 | semver = Version.coerce(revision) 60 | 61 | m = p_type.search(field) 62 | if m is None: 63 | print('WARNING: Failed to parse type:', field, file=sys.stderr) 64 | continue 65 | ptype, = m.groups() 66 | category = categories[ptype] 67 | if category is None: 68 | print('WARNING: Unrecognized type:', ptype, file=sys.stderr) 69 | category = ptype.lower() 70 | packages.append(Package(category, name, revision, semver, num)) 71 | return packages 72 | 73 | 74 | def main(android): 75 | packages = list_packages(android) 76 | for p in packages: 77 | print(p) 78 | 79 | if __name__ == '__main__': 80 | main(sys.argv[1]) 81 | -------------------------------------------------------------------------------- /sdk_updater/package.py: -------------------------------------------------------------------------------- 1 | class Package: 2 | def __init__(self, category, name, revision, semver, num=0): 3 | self.category = category 4 | self.name = name 5 | self.revision = revision 6 | self.semver = semver 7 | self.num = num 8 | 9 | def __str__(self): 10 | return '{0.name} [{0.revision}]'.format(self) 11 | 12 | def __eq__(self, other): 13 | return self.name == other.name and self.semver == other.semver 14 | 15 | def __ne__(self, other): 16 | return not __eq__(other) 17 | 18 | def __hash__(self): 19 | const = 37 20 | total = 13 21 | total += self.name.__hash__() * total + const 22 | total += self.semver.__hash__() * total + const 23 | return total 24 | -------------------------------------------------------------------------------- /sdk_updater/scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import sys 7 | import jprops 8 | 9 | from semantic_version import Version 10 | 11 | from sdk_updater.package import Package 12 | 13 | 14 | # TODO: Support NDK installs 15 | blacklist = [ 16 | 'ndk-bundle', 17 | 'temp' 18 | ] 19 | 20 | 21 | def add_ons(props, parts): 22 | return parts[1] 23 | 24 | 25 | def build_tools(props, parts): 26 | return '-'.join(parts) 27 | 28 | 29 | def docs(props, parts): 30 | return 'doc-' + props['api'] 31 | 32 | 33 | def extras(props, parts): 34 | return '-'.join(['extra'] + parts[1:]) 35 | 36 | 37 | def platforms(props, parts): 38 | return 'android-' + props['api'] 39 | 40 | 41 | def platform_tools(props, parts): 42 | return parts[0] 43 | 44 | 45 | def samples(props, parts): 46 | return 'sample-' + props['api'] 47 | 48 | 49 | def sources(props, parts): 50 | return 'source-' + props['api'] 51 | 52 | 53 | def system_images(props, parts): 54 | tag = props['tag'] 55 | if tag == 'default': 56 | tag = 'android' 57 | return '-'.join(['sys-img', props['abi'], tag, props['api']]) 58 | 59 | 60 | def tools(props, parts): 61 | return 'tools' 62 | 63 | 64 | def default(props, parts): 65 | return None 66 | 67 | 68 | def parse(top, root): 69 | path = os.path.relpath(root, top) 70 | 71 | parts = path.split(os.path.sep) 72 | 73 | if parts[0] in blacklist: 74 | print('WARNING: Ignoring \'{:s}\' as it is blacklisted.'.format(path), file=sys.stderr) 75 | return None 76 | 77 | props = parse_properties(os.path.join(root, 'source.properties')) 78 | name = { 79 | 'add-ons': add_ons, 80 | 'build-tools': build_tools, 81 | 'docs': docs, 82 | 'extras': extras, 83 | 'platforms': platforms, 84 | 'platform-tools': platform_tools, 85 | 'samples': samples, 86 | 'sources': sources, 87 | 'system-images': system_images, 88 | 'tools': tools 89 | }.get(parts[0], default)(props, parts) 90 | if not name: 91 | print("WARNING: Failed to parse package:", path, file=sys.stderr) 92 | return None 93 | return Package(parts[0], name, props['revision'], Version.coerce(props['revision'])) 94 | 95 | 96 | def parse_properties(file): 97 | props = {} 98 | with open(file, 'rb') as fp: 99 | for key, value in jprops.iter_properties(fp): 100 | if key == 'AndroidVersion.ApiLevel': 101 | props['api'] = value 102 | elif key == 'Pkg.LicenseRef': 103 | props['license'] = value 104 | elif key == 'Pkg.Revision': 105 | props['revision'] = value 106 | elif key == 'SystemImage.Abi': 107 | props['abi'] = value 108 | elif key == 'SystemImage.TagId': 109 | props['tag'] = value 110 | return props 111 | 112 | 113 | def scan(top, verbose=False): 114 | packages = [] 115 | for root, subdirs, files in os.walk(top, followlinks=True): 116 | if 'source.properties' in files: 117 | del subdirs[:] 118 | package = parse(top, root) 119 | if package: 120 | packages.append(package) 121 | if verbose: 122 | print('Found package "{:s}" in {:s}'.format(str(package), root)) 123 | return packages 124 | 125 | 126 | def main(top): 127 | packages = scan(top) 128 | for package in packages: 129 | print(str(package)) 130 | 131 | 132 | if __name__ == '__main__': 133 | main(sys.argv[1]) 134 | -------------------------------------------------------------------------------- /sdk_updater/update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function 4 | import os 5 | import sys 6 | import pexpect 7 | from sdk_updater.scan import scan 8 | from sdk_updater.list import list_packages 9 | 10 | 11 | class PackageOperation(object): 12 | def __init__(self, package): 13 | self.package = package 14 | 15 | 16 | class Install(PackageOperation): 17 | def __init__(self, package, revision): 18 | super(Install, self).__init__(package) 19 | self.revision = revision 20 | 21 | def __str__(self): 22 | return 'Installing: {:s} [{:s}]'\ 23 | .format(self.package.name, self.revision) 24 | 25 | 26 | class Update(PackageOperation): 27 | def __init__(self, package, revision_installed, revision_available): 28 | super(Update, self).__init__(package) 29 | self.revision_installed = revision_installed 30 | self.revision_available = revision_available 31 | 32 | def __str__(self): 33 | return 'Updating: {:s} [{:s} -> {:s}]'\ 34 | .format(self.package.name, self.revision_installed, self.revision_available) 35 | 36 | 37 | def to_dict(packages): 38 | d = {} 39 | for p in packages: 40 | d[p.name] = p 41 | return d 42 | 43 | 44 | def compute_package_ops(package_names, installed, available): 45 | ops = [] 46 | missing = set() 47 | 48 | ad = to_dict(available) 49 | installed_names = [p.name for p in installed] 50 | 51 | if package_names is not None: 52 | not_installed = set(package_names) - set(installed_names) 53 | 54 | for name in not_installed: 55 | p = ad.get(name) 56 | if p is None: 57 | missing.add(name) 58 | else: 59 | ops.append(Install(p, p.revision)) 60 | 61 | for i in installed: 62 | p = ad.get(i.name) 63 | if p is None: 64 | missing.add(i.name) 65 | elif p.semver > i.semver: 66 | ops.append(Update(p, i.revision, p.revision)) 67 | 68 | return ops, missing 69 | 70 | 71 | def scan_missing(sdk, packages, verbose=False): 72 | # Re-scan SDK root to check for failed installs. 73 | print('Re-scanning {:s}...'.format(sdk)) 74 | installed = scan(sdk, verbose=verbose) 75 | return [p for p in packages if p not in installed] 76 | 77 | 78 | def install_packages(packages, android, options=None, verbose=False, timeout=None): 79 | if options is None: 80 | options = [] 81 | package_filter = ','.join([p.num for p in packages]) 82 | args = ['update', 'sdk', '--no-ui', '--all', '--filter', package_filter] + options 83 | installer = pexpect.spawn(android, args=args, timeout=timeout) 84 | if verbose: 85 | if sys.version_info >= (3,): 86 | installer.logfile = sys.stdout.buffer 87 | else: 88 | installer.logfile = sys.stdout 89 | while True: 90 | i = installer.expect([r"Do you accept the license '.+' \[y/n\]:", 91 | pexpect.TIMEOUT, pexpect.EOF]) 92 | if i == 0: 93 | # Prompt 94 | installer.sendline('y') 95 | elif i == 1: 96 | # Timeout 97 | print('WARNING: Package installation timed out after {:d} seconds.' 98 | .format(timeout), file=sys.stderr) 99 | break 100 | else: 101 | break 102 | 103 | def updates_available(android, sdk, packages=None, options=None, verbose=False): 104 | print('Scanning', sdk, 'for installed packages...') 105 | installed = scan(sdk, verbose=verbose) 106 | print(' ', str(len(installed)), 'packages installed.') 107 | 108 | print('Querying update sites for available packages...') 109 | available = list_packages(android, options=options, verbose=verbose) 110 | print(' ', str(len(available)), 'packages available.') 111 | 112 | return compute_package_ops(packages, installed, available) 113 | 114 | 115 | def main(sdk, packages=None, options=None, verbose=False, timeout=None, dry_run=False): 116 | if timeout == 0: 117 | timeout = None 118 | 119 | android = os.path.join(sdk, 'tools', 'android') 120 | if not os.path.isfile(android): 121 | print('{:s} not found. Is ANDROID_HOME correct?'.format(android)) 122 | exit(1) 123 | 124 | ops, missing_remote = updates_available(android, sdk, packages, options, verbose) 125 | 126 | if missing_remote: 127 | print('WARNING: Remote repository is missing package(s):', file=sys.stderr) 128 | for name in missing_remote: 129 | print(' ', name, file=sys.stderr) 130 | if packages: 131 | missing_requested = set(missing_remote).intersection(set(packages)) 132 | if missing_requested: 133 | print('FATAL: Remote repository is missing explicitly requested package(s):', file=sys.stderr) 134 | for p in missing_requested: 135 | print(' ', p, file=sys.stderr) 136 | exit(1) 137 | 138 | if not ops: 139 | print("All packages are up-to-date.") 140 | exit(0) 141 | 142 | if dry_run: 143 | print("--dry-run was set; exiting.") 144 | exit(0) 145 | 146 | for op in ops: 147 | print(op) 148 | 149 | to_install = [op.package for op in ops] 150 | 151 | install_packages(to_install, android, options, verbose, timeout) 152 | 153 | # Re-scan SDK dir for packages we missed. 154 | missing = scan_missing(sdk, to_install, verbose=verbose) 155 | 156 | if missing: 157 | print('FATAL: Finished: {:d} packages installed. Failed to install {:d} package(s):' 158 | .format(len(to_install) - len(missing), len(missing)), file=sys.stderr) 159 | for p in missing: 160 | print(' ', p, file=sys.stderr) 161 | exit(1) 162 | else: 163 | print('Finished: {:d} packages installed.'.format(len(to_install))) 164 | exit(0) 165 | 166 | if __name__ == '__main__': 167 | main(sys.argv[1:]) 168 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | import sdk_updater 7 | 8 | requires = [ 9 | 'jprops', 10 | 'pexpect', 11 | 'semantic_version', 12 | ] 13 | 14 | setup( 15 | name='android-sdk-updater', 16 | version=sdk_updater.__version__, 17 | description='A command-line utility for keeping your Android dependencies up-to-date.', 18 | long_description=open('README.rst').read(), 19 | keywords='android', 20 | license=open('LICENSE').read(), 21 | author='Tad Fisher', 22 | author_email='tadfisher@gmail.com', 23 | url='https://github.com/tadfisher/android-sdk-updater', 24 | install_requires=requires, 25 | packages=['sdk_updater'], 26 | scripts=['bin/android-sdk-updater'], 27 | zip_safe=False, 28 | classifiers=[ 29 | 'Development Status :: 3 - Alpha', 30 | 'Environment :: Console', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: Apache Software License', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python', 35 | 'Programming Language :: Python :: 2', 36 | 'Programming Language :: Python :: 3', 37 | 'Topic :: Software Development :: Build Tools' 38 | ] 39 | ) 40 | --------------------------------------------------------------------------------