From 72110a73e7d9d483780390e464aaf0a0c758aa36 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Fri, 8 Mar 2019 11:16:15 -0800 Subject: [PATCH] Move android_sdk_downloader so I can more easily deprecate it --- DEPS | 4 +- tools/android_sdk_downloader/.gitignore | 7 + tools/android_sdk_downloader/LICENSE | 27 ++ tools/android_sdk_downloader/README.md | 31 ++ .../analysis_options.yaml | 155 +++++++ tools/android_sdk_downloader/lib/main.dart | 220 +++++++++ .../lib/src/android_repository.dart | 416 ++++++++++++++++++ .../lib/src/checksums.dart | 31 ++ .../android_sdk_downloader/lib/src/http.dart | 130 ++++++ .../lib/src/options.dart | 129 ++++++ tools/android_sdk_downloader/lib/src/zip.dart | 41 ++ tools/android_sdk_downloader/pubspec.yaml | 12 + 12 files changed, 1201 insertions(+), 2 deletions(-) create mode 100644 tools/android_sdk_downloader/.gitignore create mode 100644 tools/android_sdk_downloader/LICENSE create mode 100644 tools/android_sdk_downloader/README.md create mode 100644 tools/android_sdk_downloader/analysis_options.yaml create mode 100644 tools/android_sdk_downloader/lib/main.dart create mode 100644 tools/android_sdk_downloader/lib/src/android_repository.dart create mode 100644 tools/android_sdk_downloader/lib/src/checksums.dart create mode 100644 tools/android_sdk_downloader/lib/src/http.dart create mode 100644 tools/android_sdk_downloader/lib/src/options.dart create mode 100644 tools/android_sdk_downloader/lib/src/zip.dart create mode 100644 tools/android_sdk_downloader/pubspec.yaml diff --git a/DEPS b/DEPS index b193361e99bfe..007c6e5e0b144 100644 --- a/DEPS +++ b/DEPS @@ -418,7 +418,7 @@ hooks = [ 'name': 'prepare_android_downloader', 'pattern': '.', 'condition': 'download_android_deps', - 'cwd': 'src/tools/android/android_sdk_downloader', + 'cwd': 'src/flutter/tools/android_sdk_downloader', 'action': [ '../../../third_party/dart/tools/sdks/dart-sdk/bin/pub', # this hook _must_ be run _after_ the dart hook. 'get' @@ -431,7 +431,7 @@ hooks = [ 'action': [ 'src/third_party/dart/tools/sdks/dart-sdk/bin/dart', # this hook _must_ be run _after_ the dart hook. '--enable-asserts', - 'src/tools/android/android_sdk_downloader/lib/main.dart', + 'src/flutter/tools/android_sdk_downloader/lib/main.dart', '-y', # Accept licenses '--out=src/third_party/android_tools', '--platform=28', diff --git a/tools/android_sdk_downloader/.gitignore b/tools/android_sdk_downloader/.gitignore new file mode 100644 index 0000000000000..d6a35f97f7659 --- /dev/null +++ b/tools/android_sdk_downloader/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.atom/ +.idea +.packages +.pub/ +.dart_tool/ +pubspec.lock \ No newline at end of file diff --git a/tools/android_sdk_downloader/LICENSE b/tools/android_sdk_downloader/LICENSE new file mode 100644 index 0000000000000..58ab87f9b99b9 --- /dev/null +++ b/tools/android_sdk_downloader/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/tools/android_sdk_downloader/README.md b/tools/android_sdk_downloader/README.md new file mode 100644 index 0000000000000..c705ee214278f --- /dev/null +++ b/tools/android_sdk_downloader/README.md @@ -0,0 +1,31 @@ +# Android SDK Downloader + +This program assists with downloading the Android SDK and NDK artifacts for +Flutter engine development. + +## Usage + +``` +-r, --repository-xml Specifies the location of the Android Repository XML file. + (defaults to "https://dl.google.com/android/repository/repository2-1.xml") + +-p, --platform Specifies the Android platform version, e.g. 28 + + --platform-revision Specifies the Android platform revision, e.g. 6 for 28_r06 + +-o, --out The directory to write downloaded files to. + + --os The OS type to download for. Defaults to current platform. + (defaults to current platform), accepts: [windows, macos, linux] + + --build-tools-version The build-tools version to download. Must be in format of .., e.g. 28.0.3; or ..., e.g. 28.0.0.2 + + --platform-tools-version The platform-tools version to download. Must be in format of .., e.g. 28.0.1; or ..., e.g. 28.0.0.2 + + --tools-version The tools version to download. Must be in format of .., e.g. 26.1.1; or ..., e.g. 28.1.1.2 + + --ndk-version The ndk version to download. Must be in format of .., e.g. 28.0.3; or ..., e.g. 28.0.0.2 + +-y, --[no-]accept-licenses Automatically accept Android SDK licenses. + --[no-]overwrite Skip download if the target directory exists. +``` \ No newline at end of file diff --git a/tools/android_sdk_downloader/analysis_options.yaml b/tools/android_sdk_downloader/analysis_options.yaml new file mode 100644 index 0000000000000..2023b29bda9fe --- /dev/null +++ b/tools/android_sdk_downloader/analysis_options.yaml @@ -0,0 +1,155 @@ +# Specify analysis options. +# +# Copied from https://github.com/flutter/flutter/blob/master/analysis_options.yaml + +analyzer: + strong-mode: + implicit-dynamic: false + errors: + # treat missing required parameters as a warning (not a hint) + missing_required_param: warning + # treat missing returns as a warning (not a hint) + missing_return: warning + # allow having TODOs in the code + todo: ignore + exclude: + - 'bin/cache/**' + # the following two are relative to the stocks example and the flutter package respectively + # see https://github.com/dart-lang/sdk/issues/28463 + - 'lib/i18n/stock_messages_*.dart' + - 'lib/src/http/**' + +linter: + rules: + # these rules are documented on and in the same order as + # the Dart Lint rules page to make maintenance easier + # https://github.com/dart-lang/linter/blob/master/example/all.yaml + - always_declare_return_types + - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 + - always_require_non_null_named_parameters + - always_specify_types + - annotate_overrides + # - avoid_annotating_with_dynamic # conflicts with always_specify_types + - avoid_as + # - avoid_bool_literals_in_conditional_expressions # not yet tested + # - avoid_catches_without_on_clauses # we do this commonly + # - avoid_catching_errors # we do this commonly + - avoid_classes_with_only_static_members + # - avoid_double_and_int_checks # only useful when targeting JS runtime + - avoid_empty_else + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + # - avoid_implementing_value_types # not yet tested + - avoid_init_to_null + # - avoid_js_rounded_ints # only useful when targeting JS runtime + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters # not yet tested + # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + # - avoid_returning_null # there are plenty of valid reasons to return null + - avoid_returning_null_for_void + # - avoid_returning_this # there are plenty of valid reasons to return this + # - avoid_setters_without_getters # not yet tested + # - avoid_single_cascade_in_expression_statements # not yet tested + - avoid_slow_async_io + - avoid_types_as_parameter_names + # - avoid_types_on_closure_parameters # conflicts with always_specify_types + - avoid_unused_constructor_parameters + - avoid_void_async + - await_only_futures + - camel_case_types + - cancel_subscriptions + # - cascade_invocations # not yet tested + # - close_sinks # not reliable enough + # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 + # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 + - control_flow_in_finally + # - curly_braces_in_flow_control_structures # not yet tested + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + # - file_names # not yet tested + - flutter_style_todos + - hash_and_equals + - implementation_imports + # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 + - iterable_contains_unrelated_type + # - join_return_with_assignment # not yet tested + - library_names + - library_prefixes + # - lines_longer_than_80_chars # not yet tested + - list_remove_unrelated_type + # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 + - no_adjacent_strings_in_list + - no_duplicate_case_values + - non_constant_identifier_names + # - null_closures # not yet tested + # - omit_local_variable_types # opposite of always_specify_types + # - one_member_abstracts # too many false positives + # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + # - parameter_assignments # we do this commonly + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + # - prefer_constructors_over_static_methods # not yet tested + - prefer_contains + - prefer_equal_for_default_values + # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods + - prefer_final_fields + - prefer_final_locals + - prefer_foreach + # - prefer_function_declarations_over_variables # not yet tested + - prefer_generic_function_type_aliases + - prefer_initializing_formals + # - prefer_int_literals # not yet tested + # - prefer_interpolation_to_compose_strings # not yet tested + - prefer_is_empty + - prefer_is_not_empty + - prefer_iterable_whereType + # - prefer_mixin # https://github.com/dart-lang/language/issues/32 + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - prefer_void_to_null + # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml + - recursive_getters + - slash_for_doc_comments + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - super_goes_last + - test_types_in_equals + - throw_in_finally + # - type_annotate_public_apis # subset of always_specify_types + - type_init_formals + # - unawaited_futures # too many false positives + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_rethrow_when_possible + # - use_setters_to_change_properties # not yet tested + # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 + # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review + - valid_regexps + # - void_checks # not yet tested diff --git a/tools/android_sdk_downloader/lib/main.dart b/tools/android_sdk_downloader/lib/main.dart new file mode 100644 index 0000000000000..f563889c82b29 --- /dev/null +++ b/tools/android_sdk_downloader/lib/main.dart @@ -0,0 +1,220 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as path; + +import 'src/android_repository.dart'; +import 'src/checksums.dart'; +import 'src/http.dart'; +import 'src/options.dart'; +import 'src/zip.dart'; + +const String _kAndroidRepositoryXml = 'https://dl.google.com/android/repository/repository2-1.xml'; + + +Future main(List args) async { + final ArgParser argParser = ArgParser() + ..addOption( + 'repository-xml', + abbr: 'r', + help: 'Specifies the location of the Android Repository XML file.', + defaultsTo: _kAndroidRepositoryXml, + ) + ..addOption( + 'platform', + abbr: 'p', + help: 'Specifies the Android platform version, e.g. 28', + ) + ..addOption( + 'platform-revision', + help: 'Specifies the Android platform revision, e.g. 6 for 28_r06', + ) + ..addOption( + 'out', + abbr: 'o', + help: 'The directory to write downloaded files to.', + defaultsTo: '.', + ) + ..addOption( + 'os', + help: 'The OS type to download for. Defaults to current platform.', + defaultsTo: Platform.operatingSystem, + allowed: osTypeMap.keys, + ) + ..addOption( + 'build-tools-version', + help: 'The build-tools version to download. Must be in format of ' + '.., e.g. 28.0.3; ' + 'or ..., e.g. 28.0.0.2', + ) + ..addOption( + 'platform-tools-version', + help: 'The platform-tools version to download. Must be in format of ' + '.., e.g. 28.0.1; ' + 'or ..., e.g. 28.0.0.2', + ) + ..addOption( + 'tools-version', + help: 'The tools version to download. Must be in format of ' + '.., e.g. 26.1.1; ' + 'or ..., e.g. 28.1.1.2', + ) + ..addOption( + 'ndk-version', + help: 'The ndk version to download. Must be in format of ' + '.., e.g. 28.0.3; ' + 'or ..., e.g. 28.0.0.2', + ) + ..addFlag('accept-licenses', + abbr: 'y', + defaultsTo: false, + help: 'Automatically accept Android SDK licenses.') + ..addFlag( + 'overwrite', + defaultsTo: false, + help: 'Skip download if the target directory exists.', + ); + + final bool help = args.contains('-h') + || args.contains('--help') + || (args.isNotEmpty && args.first == 'help'); + if (help) { + print(argParser.usage); + return; + } + + final Options options = Options.parseAndValidate(args, argParser); + + final AndroidRepository androidRepository = await _getAndroidRepository(options.repositoryXmlUri); + assert(androidRepository.platforms.isNotEmpty); + assert(androidRepository.buildTools.isNotEmpty); + + if (!options.acceptLicenses) { + for (final AndroidRepositoryLicense license in androidRepository.licenses) { + print('================================================================================\n\n'); + print(license.text); + stdout.write('Do you accept? (Y/n): '); + final String result = stdin.readLineSync().trim().toLowerCase(); + if (result != '' && result.startsWith('y') == false) { + print('Ending.'); + exit(-1); + } + } + } + + await options.outDirectory.create(recursive: true); + + final Directory tempDir = await Directory(options.outDirectory.path).createTemp(); + await tempDir.create(recursive: true); + + final Directory ndkDir = Directory(path.join(options.outDirectory.path, 'ndk')); + final Directory sdkDir = Directory(path.join(options.outDirectory.path, 'sdk')); + final Directory platformDir = Directory(path.join(sdkDir.path, 'platforms', 'android-${options.platformApiLevel}')); + final Directory buildToolsDir = Directory(path.join(sdkDir.path, 'build-tools', options.buildToolsRevision.raw)); + final Directory platformToolsDir = Directory(path.join(sdkDir.path, 'platform-tools')); + final Directory toolsDir = Directory(path.join(sdkDir.path, 'tools')); + + final Map checksums = + await loadChecksums(options.outDirectory); + + print('Downloading Android SDK and NDK artifacts...'); + final List> futures = >[]; + + futures.add(downloadArchive( + androidRepository.platforms, + OptionsRevision(null, options.platformRevision), + options.repositoryBase, + tempDir, + checksumToSkip: options.overwrite ? null : checksums[platformDir.path], + ).then((ArchiveDownloadResult result) { + if (result != ArchiveDownloadResult.empty) { + return unzipFile(result.zipFileName, platformDir).then((_) { + checksums[platformDir.path] = result.checksum; + return writeChecksums(checksums, options.outDirectory); + }); + } + return null; + })); + futures.add(downloadArchive( + androidRepository.buildTools, + options.buildToolsRevision, + options.repositoryBase, + tempDir, + osType: options.osType, + checksumToSkip: options.overwrite ? null : checksums[buildToolsDir.path], + ).then((ArchiveDownloadResult result) { + if (result != ArchiveDownloadResult.empty) { + return unzipFile(result.zipFileName, buildToolsDir).then((_) { + checksums[buildToolsDir.path] = result.checksum; + return writeChecksums(checksums, options.outDirectory); + }); + } + return null; + })); + futures.add(downloadArchive( + androidRepository.platformTools, + options.platformToolsRevision, + options.repositoryBase, + tempDir, + osType: options.osType, + checksumToSkip: options.overwrite ? null : checksums[platformToolsDir.path], + ).then((ArchiveDownloadResult result) { + if (result != ArchiveDownloadResult.empty) { + return unzipFile(result.zipFileName, platformToolsDir).then((_) { + checksums[platformToolsDir.path] = result.checksum; + return writeChecksums(checksums, options.outDirectory); + }); + } + return null; + })); + futures.add(downloadArchive( + androidRepository.tools, + options.toolsRevision, + options.repositoryBase, + tempDir, + osType: options.osType, + checksumToSkip: options.overwrite ? null : checksums[toolsDir.path], + ).then((ArchiveDownloadResult result) { + if (result != ArchiveDownloadResult.empty) { + return unzipFile(result.zipFileName, toolsDir).then((_) { + checksums[toolsDir.path] = result.checksum; + return writeChecksums(checksums, options.outDirectory); + }); + } + return null; + })); + futures.add(downloadArchive( + androidRepository.ndkBundles, + options.ndkRevision, + options.repositoryBase, + tempDir, + osType: options.osType, + checksumToSkip: options.overwrite ? null : checksums[ndkDir.path], + ).then((ArchiveDownloadResult result) { + if (result != ArchiveDownloadResult.empty) { + return unzipFile(result.zipFileName, ndkDir).then((_) { + checksums[ndkDir.path] = result.checksum; + return writeChecksums(checksums, options.outDirectory); + }); + } + return null; + })); + await Future.wait(futures); + await tempDir.delete(recursive: true); +} + +Future _getAndroidRepository(Uri repositoryXmlUri) async { + final StringBuffer repoXmlBuffer = StringBuffer(); + Future _repositoryXmlHandler(HttpClientResponse response) async { + await response.transform(utf8.decoder).forEach(repoXmlBuffer.write); + } + + await httpGet(repositoryXmlUri, _repositoryXmlHandler); + + return parseAndroidRepositoryXml(repoXmlBuffer.toString()); +} diff --git a/tools/android_sdk_downloader/lib/src/android_repository.dart b/tools/android_sdk_downloader/lib/src/android_repository.dart new file mode 100644 index 0000000000000..3490d02e95c3c --- /dev/null +++ b/tools/android_sdk_downloader/lib/src/android_repository.dart @@ -0,0 +1,416 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:xml/xml.dart' as xml show parse; +import 'package:xml/xml.dart'; + +// see https://android.googlesource.com/platform/tools/base/+/master/sdklib/src/main/java/com/android/sdklib/repository/sdk-repository-10.xsd + +const String _kXsi = 'http://www.w3.org/2001/XMLSchema-instance'; +const String _kSdk = 'http://schemas.android.com/sdk/android/repo/repository2/01'; + +void _debugCheckElement( + XmlElement element, + String name, { + String namespace, +}) { + assert(element != null); + assert(element.name.local == name, '${element.name.local} != $name'); + assert(element.name.namespaceUri == namespace, + '${element.name.namespaceUri} != $namespace'); +} + +XmlElement _firstOrDefault(Iterable list) { + if (list?.isEmpty == true) { + return null; + } + return list.first; +} + +String _getChildText( + XmlElement parent, + String childName, { + String def = '', + String namespace, +}) { + final String value = _firstOrDefault( + parent.findElements( + childName, + namespace: namespace, + ), + )?.text; + return value ?? def; +} + +OSType _parseHostType(String value) { + switch (value) { + case 'linux': + return OSType.linux; + case 'windows': + return OSType.windows; + case 'macosx': + return OSType.mac; + default: + return OSType.any; + } +} + +/// Parses a the Android SDK's https://dl.google.com/android/repository/repository2-1.xml +/// into an [AndroidRepository] object. +AndroidRepository parseAndroidRepositoryXml(String rawXml) { + final XmlDocument doc = xml.parse(rawXml); + return AndroidRepository.fromXml(doc.rootElement); +} + +XmlElement _getTypeDetails(XmlElement parent) { + final XmlElement typeDetails = + _firstOrDefault(parent.findAllElements('type-details')); + if (typeDetails == null) { + throw StateError('Missing .'); + } + return typeDetails; +} + +String _getTypeDetailsType(XmlElement typeDetails) { + return typeDetails.getAttribute('type', namespace: _kXsi); +} + +Iterable _getArchives(XmlElement parent) { + assert(parent != null); + final XmlElement archives = + _firstOrDefault(parent.findAllElements('archives')); + if (archives == null) { + return null; + } + return archives.findElements('archive'); +} + +/// Object class for https://dl.google.com/android/repository/repository2-1.xml. +class AndroidRepository { + const AndroidRepository( + this.licenses, + this.platforms, + this.buildTools, + this.platformTools, + this.tools, + this.ndkBundles, + ) : assert(licenses != null), + assert(platforms != null), + assert(buildTools != null), + assert(platformTools != null), + assert(tools != null), + assert(ndkBundles != null); + + /// Parses the `` element. + factory AndroidRepository.fromXml(XmlElement element) { + _debugCheckElement(element, 'sdk-repository', namespace: _kSdk); + final List licenses = + []; + final List platforms = + []; + final List buildTools = + []; + final List platformTools = + []; + final List tools = + []; + + final List ndkBundles = + []; + for (final XmlElement child in element.children.whereType()) { + switch (child.name.local) { + case 'license': + licenses.add(AndroidRepositoryLicense.fromXml(child)); + break; + case 'remotePackage': + final XmlElement typeDetails = _getTypeDetails(child); + switch (_getTypeDetailsType(typeDetails)) { + case 'sdk:platformDetailsType': + platforms.add( + AndroidRepositoryPlatform.fromXml(child, typeDetails), + ); + break; + case 'generic:genericDetailsType': + final String path = child.getAttribute('path'); + if (path.startsWith('build-tools;')) { + buildTools.add(AndroidRepositoryRemotePackage.fromXml(child)); + } else if (path.startsWith('platform-tools')) { + platformTools + .add(AndroidRepositoryRemotePackage.fromXml(child)); + } else if (path.startsWith('tools')) { + tools.add(AndroidRepositoryRemotePackage.fromXml(child)); + } else if (path.startsWith('ndk-bundle')) { + ndkBundles.add(AndroidRepositoryRemotePackage.fromXml(child)); + } + break; + default: + break; + } + break; + default: + break; + } + } + return AndroidRepository( + licenses, + platforms, + buildTools, + platformTools, + tools, + ndkBundles, + ); + } + + /// Licenses from the repository XML. + final List licenses; + + /// Platform information from the repository XML. + final List platforms; + + /// Build tools information from the repostiory XML. + final List buildTools; + + /// Platform tools information from the repostiory XML. + final List platformTools; + + /// Tools information from the repostiory XML. + final List tools; + + /// Tools information from the repostiory XML. + final List ndkBundles; +} + +/// Object class for the `` element in the Android repo XML. +/// +/// This node contains license information for the packages in the SDK. +class AndroidRepositoryLicense { + /// Creates a new RepositoryLicense holder. + const AndroidRepositoryLicense(this.id, this.text) + : assert(id != null), + assert(text != null); + + /// Parses a `` element. + factory AndroidRepositoryLicense.fromXml(XmlElement element) { + _debugCheckElement(element, 'license'); + return AndroidRepositoryLicense(element.getAttribute('id'), element.text); + } + + /// The identifier for this license. + final String id; + + /// The text of the license. + final String text; +} + +/// Object class for the `` nodes in the repo XML. +/// +/// These nodes contain information about where to download the zipped +/// binaries for various components of the SDK. +class AndroidRepositoryRemotePackage { + const AndroidRepositoryRemotePackage( + this.revision, + this.displayName, + this.archives, { + this.isObsolete = false, + }) : assert(revision != null), + assert(displayName != null), + assert(archives != null), + assert(isObsolete != null); + + factory AndroidRepositoryRemotePackage.fromXml(XmlElement element) { + _debugCheckElement(element, 'remotePackage'); + + return AndroidRepositoryRemotePackage( + AndroidRepositoryRevision.fromXml( + _firstOrDefault(element.findElements('revision'))), + _getChildText(element, 'display-name'), + _getArchives(element) + .map( + (XmlElement archive) => AndroidRepositoryArchive.fromXml(archive), + ) + .toList(), + isObsolete: element.getAttribute('obsolete') == 'true', + ); + } + + /// The `` element, if any. + final AndroidRepositoryRevision revision; + + /// The `` element. + final String displayName; + + /// The list of archives available for this package. + final List archives; + + /// Whether this package is marked as obsolete. + final bool isObsolete; + + @override + String toString() => '$runtimeType{revision: $revision, displayName: $displayName, archives: $archives}'; +} + +/// Object class for instances of `` elements that are for the +/// platform package. +class AndroidRepositoryPlatform extends AndroidRepositoryRemotePackage { + const AndroidRepositoryPlatform( + AndroidRepositoryRevision revision, + String displayName, + List archives, + this.apiLevel, { + bool isObsolete = false, + }) : assert(apiLevel != null), + super(revision, displayName, archives, isObsolete: isObsolete); + + /// Parses an platform from a `` element. + factory AndroidRepositoryPlatform.fromXml( + XmlElement element, + XmlElement typeDetails, + ) { + _debugCheckElement(element, 'remotePackage'); + assert(typeDetails != null); + + return AndroidRepositoryPlatform( + AndroidRepositoryRevision.fromXml( + _firstOrDefault(element.findElements('revision'))), + _getChildText(element, 'display-name'), + _getArchives(element) + .map( + (XmlElement archive) => AndroidRepositoryArchive.fromXml(archive), + ) + .toList(), + int.parse(_getChildText(typeDetails, 'api-level', def: '0')), + isObsolete: element.getAttribute('obsolete') == 'true', + ); + } + + /// The API level for this Platform. + final int apiLevel; + + @override + String toString() => '$runtimeType{revision: $revision, displayName: $displayName, archives: $archives, apiLevel: $apiLevel}'; +} + +/// The OS types supported by Android. +enum OSType { + /// Any OS is supported. + any, + + /// Suppoorts Linux only. + linux, + + /// Supports macOS only. + mac, + + /// Supports windows only. + windows, +} + +/// Object class for the `` element in the Android repo XML. +/// +/// Contains information about the size, checksum, and location of a binary +/// zip archive. Optionally contains information about what host OS is +/// supported. +class AndroidRepositoryArchive { + /// Creates a new AndroidRepositoryArchive. + const AndroidRepositoryArchive( + this.size, + this.checksum, + this.url, { + this.hostOS = OSType.any, + }) : assert(size != null), + assert(checksum != null), + assert(url != null), + assert(hostOS != null); + + /// Parses an `` element. + factory AndroidRepositoryArchive.fromXml(XmlElement element) { + _debugCheckElement(element, 'archive'); + final XmlElement complete = + _firstOrDefault(element.findElements('complete')); + if (complete == null) { + throw StateError('Found element without a node!'); + } + + return AndroidRepositoryArchive( + int.parse(_getChildText(complete, 'size', def: '0')), + _getChildText(complete, 'checksum'), + _getChildText(complete, 'url'), + hostOS: _parseHostType(_getChildText(element, 'host-os')), + ); + } + + /// The download size in bytes of the archive. + final int size; + + /// The SHA-1 checksum of the archive. + final String checksum; + + /// The absolute or relative URL of the file. + final String url; + + /// The OS type, if applicable, for this archive. + final OSType hostOS; + + @override + String toString() => '$runtimeType{size: $size, checksum: $checksum, url: $url, hostOS: $hostOS}'; +} + +/// Object class for a `` node in the Android repo XML. +/// +/// Contains information about the revision of the archive. +/// +/// In the case of the platform package, this is the revision of the platform. +/// +/// In all other cases, this basically works like semver. +class AndroidRepositoryRevision { + /// Creates a new Android repository revision object. All values are required. + const AndroidRepositoryRevision( + this.major, [ + this.minor = 0, + this.micro = 0, + this.preview = 0, + ]) : assert(major != null), + assert(minor != null), + assert(micro != null), + assert(preview != null); + + /// Parses a `` element from the Android repository XML. + factory AndroidRepositoryRevision.fromXml(XmlElement element) { + if (element == null) { + return const AndroidRepositoryRevision(0); + } + _debugCheckElement(element, 'revision'); + return AndroidRepositoryRevision( + int.tryParse(_getChildText(element, 'major', def: '0')), + int.tryParse(_getChildText(element, 'minor', def: '0')), + int.tryParse(_getChildText(element, 'micro', def: '0')), + ); + } + + /// The major revision value. + final int major; + + /// The minor revision value. + final int minor; + + /// The micro revision. + final int micro; + + /// Preview/Release candidate version. A value of 0 indicates that + /// this is not a preview. + final int preview; + + /// Whether this revision represents a preview or release. + bool get isPreview => preview > 0; + + bool matches(int major, int minor, int micro, [int preview = 0]) { + return this.major == major && + this.minor == minor && + this.micro == micro && + this.preview == preview; + } + + @override + String toString() => '$runtimeType:{$major.$minor.$micro.$preview}'; +} diff --git a/tools/android_sdk_downloader/lib/src/checksums.dart b/tools/android_sdk_downloader/lib/src/checksums.dart new file mode 100644 index 0000000000000..37e376217e9b6 --- /dev/null +++ b/tools/android_sdk_downloader/lib/src/checksums.dart @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +Future> loadChecksums(Directory directory) async { + final File checksumFile = File(path.join(directory.path, 'checksums.json')); + if (!checksumFile.existsSync()) { + return {}; + } + final Map result = {}; + final Map jsonResult = + json.decode(await checksumFile.readAsString()); + for (final String key in jsonResult.keys) { + result[key] = jsonResult[key]; + } + return result; +} + +Future writeChecksums( + Map checksums, + Directory directory, +) async { + final File checksumFile = File(path.join(directory.path, 'checksums.json')); + const JsonEncoder encoder = JsonEncoder.withIndent(' '); + await checksumFile.writeAsString(encoder.convert(checksums)); +} diff --git a/tools/android_sdk_downloader/lib/src/http.dart b/tools/android_sdk_downloader/lib/src/http.dart new file mode 100644 index 0000000000000..4b2fa6cc29cd5 --- /dev/null +++ b/tools/android_sdk_downloader/lib/src/http.dart @@ -0,0 +1,130 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'android_repository.dart'; +import 'options.dart'; + +typedef HttpResponseHandler = Future Function(HttpClientResponse); + +Future httpGet( + Uri url, + HttpResponseHandler handler, +) async { + assert(url != null); + assert(handler != null); + + final HttpClient httpClient = HttpClient(); + + try { + final HttpClientRequest request = await httpClient.getUrl(url); + final HttpClientResponse response = await request.close(); + await handler(response); + } finally { + httpClient.close(); + } +} + +class DownloadTracker { + DownloadTracker(this.name, this.total) : received = 0; + + final String name; + final int total; + int received; + + String get percent => '${((received / total) * 100).round()}'.padLeft(3) + '%'; + + @override + String toString() => '$name: $received/$total ($percent).'; +} + +final Map _downloadTrackers = + {}; +void _printDownloadTrackers() { + for (final DownloadTracker tracker in _downloadTrackers.values) { + stdout.write( + '${tracker.name.replaceAll('Android ', '')}: ${tracker.percent} '); + } + + if (_downloadTrackers.values + .every((DownloadTracker tracker) => tracker.received == tracker.total)) { + stdout.writeln(); + print('Downloads complete.'); + } else { + stdout.write('\r'); + } +} + +class ArchiveDownloadResult { + const ArchiveDownloadResult(this.zipFileName, this.checksum); + + static const ArchiveDownloadResult empty = ArchiveDownloadResult(null, null); + + final String zipFileName; + final String checksum; +} + +Future downloadArchive( + List packages, + OptionsRevision revision, + String repositoryBase, + Directory outDirectory, { + OSType osType, + int apiLevel, + String checksumToSkip, +}) async { + AndroidRepositoryRemotePackage package; + for (final AndroidRepositoryRemotePackage p in packages) { + if (apiLevel != null && p is AndroidRepositoryPlatform) { + if (p.apiLevel != apiLevel) { + continue; + } + } + if (p.revision.matches( + revision.major, revision.minor, revision.micro, revision.preview)) { + package = p; + break; + } + } + if (package == null) { + throw StateError('Could not find package matching arguments: ' + '$revision, $osType, $apiLevel'); + } + + final String displayName = package.displayName; + final AndroidRepositoryArchive archive = osType == null + ? package.archives.first + : package.archives.firstWhere( + (AndroidRepositoryArchive archive) => archive.hostOS == osType, + ); + + if (archive.checksum == checksumToSkip) { + print('Skipping $displayName, checksum matches current asset.'); + return ArchiveDownloadResult.empty; + } + + Uri uri = Uri.parse(archive.url); + if (!uri.isAbsolute) { + uri = Uri.parse(repositoryBase + archive.url); + } + + _downloadTrackers[displayName] = DownloadTracker(displayName, archive.size); + final String outFileName = path.join(outDirectory.path, archive.url); + final IOSink tempFileSink = File(outFileName).openWrite(); + + Future _handlePlatformZip(HttpClientResponse response) async { + await for (List data in response) { + _downloadTrackers[displayName].received += data.length; + tempFileSink.add(data); + _printDownloadTrackers(); + } + await tempFileSink.close(); + } + + await httpGet(uri, _handlePlatformZip); + return ArchiveDownloadResult(outFileName, archive.checksum); +} diff --git a/tools/android_sdk_downloader/lib/src/options.dart b/tools/android_sdk_downloader/lib/src/options.dart new file mode 100644 index 0000000000000..73e3a2f210b35 --- /dev/null +++ b/tools/android_sdk_downloader/lib/src/options.dart @@ -0,0 +1,129 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:meta/meta.dart'; + +import 'android_repository.dart'; + +const Map osTypeMap = { + 'windows': OSType.windows, + 'macos': OSType.mac, + 'linux': OSType.linux, +}; + +class OptionsRevision { + const OptionsRevision( + this.raw, [ + this.major = 0, + this.minor = 0, + this.micro = 0, + this.preview = 0, + ]); + + /// Accepted formats: 1.2.3 or 1.2.3.4 + factory OptionsRevision.fromRaw(String raw) { + final List rawParts = raw.split('.'); + if (rawParts == null || (rawParts.length != 3 && rawParts.length != 4)) { + throw ArgumentError('Invalid revision string $raw.'); + } + return OptionsRevision( + raw, + int.parse(rawParts[0]), + int.parse(rawParts[1]), + int.parse(rawParts[2]), + rawParts.length == 4 ? int.parse(rawParts[3]) : 0, + ); + } + + final String raw; + final int major; + final int minor; + final int micro; + final int preview; +} + +class Options { + const Options({ + @required this.platformApiLevel, + @required this.platformRevision, + @required this.repositoryXml, + @required this.repositoryXmlUri, + @required this.buildToolsRevision, + @required this.platformToolsRevision, + @required this.toolsRevision, + @required this.ndkRevision, + @required this.outDirectory, + @required this.repositoryBase, + @required this.osType, + this.acceptLicenses = false, + this.overwrite = false, + }); + + static Options parseAndValidate(List args, ArgParser argParser) { + final ArgResults argResults = argParser.parse(args); + final int platformApiLevel = int.parse(argResults['platform']); + final int platformRevision = int.parse(argResults['platform-revision']); + final Directory outDirectory = Directory(argResults['out']); + + final String rawRepositoryXmlUri = argResults['repository-xml']; + final Uri repositoryXmlUri = Uri.tryParse(rawRepositoryXmlUri); + final int lastSlash = rawRepositoryXmlUri.lastIndexOf('/'); + final String repositoryBase = + rawRepositoryXmlUri.substring(0, lastSlash + 1); + + if (repositoryXmlUri == null) { + throw ArgumentError( + 'Error: could not parse $rawRepositoryXmlUri as a valid URL.'); + } + + String getRawVersion(String argName) { + final String raw = argResults[argName]; + if (raw?.isEmpty == true) { + print('Could not parse required argument $argName.'); + print(argParser.usage); + exit(-1); + } + return raw; + } + + final String rawBuildToolsVersion = getRawVersion('build-tools-version'); + final String rawPlatformToolsVersion = + getRawVersion('platform-tools-version'); + final String rawToolsVersion = getRawVersion('tools-version'); + final String rawNdkVersion = getRawVersion('ndk-version'); + + return Options( + platformApiLevel: platformApiLevel, + platformRevision: platformRevision, + outDirectory: outDirectory, + repositoryXml: rawRepositoryXmlUri, + repositoryXmlUri: repositoryXmlUri, + repositoryBase: repositoryBase, + buildToolsRevision: OptionsRevision.fromRaw(rawBuildToolsVersion), + platformToolsRevision: OptionsRevision.fromRaw(rawPlatformToolsVersion), + toolsRevision: OptionsRevision.fromRaw(rawToolsVersion), + ndkRevision: OptionsRevision.fromRaw(rawNdkVersion), + osType: osTypeMap[argResults['os']], + acceptLicenses: argResults['accept-licenses'], + overwrite: argResults['overwrite'], + ); + } + + final int platformApiLevel; + final String repositoryXml; + final Uri repositoryXmlUri; + final int platformRevision; + final OptionsRevision buildToolsRevision; + final OptionsRevision platformToolsRevision; + final OptionsRevision toolsRevision; + final OptionsRevision ndkRevision; + final String repositoryBase; + final Directory outDirectory; + final OSType osType; + final bool acceptLicenses; + final bool overwrite; +} diff --git a/tools/android_sdk_downloader/lib/src/zip.dart b/tools/android_sdk_downloader/lib/src/zip.dart new file mode 100644 index 0000000000000..488cb9b29b512 --- /dev/null +++ b/tools/android_sdk_downloader/lib/src/zip.dart @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +// TODO(dnfield): if/when a streaming unzip routine is available for Dart, use that instead. + +Future unzipFile(String file, Directory outDir) async { + await outDir.parent.create(recursive: true); + final Directory tempDir = await outDir.parent.createTemp(); + + String command; + List args; + if (Platform.isWindows) { + command = 'powershell.exe -nologo -noprofile -command ' + '"& { ' + 'Add-Type -A \'System.IO.Compression.FileSystem\'; ' + '[IO.Compression.ZipFile]::ExtractToDirectory(\'$file\', \'${tempDir.path}\'); ' + '}"'; + args = []; + } else { + command = 'unzip'; + args = [ + file, + '-d', + tempDir.path, + ]; + } + final ProcessResult result = await Process.run(command, args); + if (result.exitCode != 0) { + throw Exception('Failed to unzip archive!'); + } + final Directory dir = await tempDir.list().first; + if (await outDir.exists()) { + await outDir.delete(recursive: true); + } + await dir.rename(outDir.path); + await tempDir.delete(); +} diff --git a/tools/android_sdk_downloader/pubspec.yaml b/tools/android_sdk_downloader/pubspec.yaml new file mode 100644 index 0000000000000..ef1b304818da9 --- /dev/null +++ b/tools/android_sdk_downloader/pubspec.yaml @@ -0,0 +1,12 @@ +name: 'android_sdk_downloader' +publish_to: none + +environment: + # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. + sdk: ">=2.0.0-dev.68.0 <3.0.0" + +dependencies: + args: ^1.5.1 + meta: ^1.1.6 + path: ^1.6.2 + xml: ^3.2.3