diff --git a/script/tool/lib/src/update_dependency_command.dart b/script/tool/lib/src/update_dependency_command.dart index 7f7c3c6d4efc..185abb57fad6 100644 --- a/script/tool/lib/src/update_dependency_command.dart +++ b/script/tool/lib/src/update_dependency_command.dart @@ -19,6 +19,7 @@ import 'common/repository_package.dart'; const int _exitIncorrectTargetDependency = 3; const int _exitNoTargetVersion = 4; +const int _exitInvalidTargetVersion = 5; /// A command to update a dependency in packages. /// @@ -38,6 +39,14 @@ class UpdateDependencyCommand extends PackageLoopingCommand { _pubPackageFlag, help: 'A pub package to update.', ); + argParser.addOption(_androidDependency, + help: 'An Android dependency to update.', + allowed: [ + 'gradle', + ], + allowedHelp: { + 'gradle': 'Updates Gradle version used in plugin example apps.', + }); argParser.addOption( _versionFlag, help: 'The version to update to.\n\n' @@ -45,16 +54,19 @@ class UpdateDependencyCommand extends PackageLoopingCommand { 'provided. This can be any constraint that pubspec.yaml allows; a ' 'specific version will be treated as the exact version for ' 'dependencies that are alread pinned, or a ^ range for those that ' - 'are unpinned.', + 'are unpinned.\n' + '- For Android dependencies, a version must be provided.', ); } static const String _pubPackageFlag = 'pub-package'; + static const String _androidDependency = 'android-dependency'; static const String _versionFlag = 'version'; final PubVersionFinder _pubVersionFinder; late final String? _targetPubPackage; + late final String? _targetAndroidDependency; late final String _targetVersion; @override @@ -72,7 +84,10 @@ class UpdateDependencyCommand extends PackageLoopingCommand { @override Future initializeRun() async { - const Set targetFlags = {_pubPackageFlag}; + const Set targetFlags = { + _pubPackageFlag, + _androidDependency + }; final Set passedTargetFlags = targetFlags.where((String flag) => argResults![flag] != null).toSet(); if (passedTargetFlags.length != 1) { @@ -80,6 +95,8 @@ class UpdateDependencyCommand extends PackageLoopingCommand { 'Exactly one of the target flags must be provided: (${targetFlags.join(', ')})'); throw ToolExit(_exitIncorrectTargetDependency); } + + // Setup for updating pub dependency. _targetPubPackage = getNullableStringArg(_pubPackageFlag); if (_targetPubPackage != null) { final String? version = getNullableStringArg(_versionFlag); @@ -102,6 +119,33 @@ ${response.httpResponse.body} } } else { _targetVersion = version; + return; + } + } + + // Setup for updating Android dependency. + _targetAndroidDependency = getNullableStringArg(_androidDependency); + if (_targetAndroidDependency != null) { + final String? version = getNullableStringArg(_versionFlag); + if (version == null) { + printError('A version must be provided to update this dependency.'); + throw ToolExit(_exitNoTargetVersion); + } else if (_targetAndroidDependency == 'gradle') { + final RegExp validGradleVersionPattern = RegExp(r'^\d+(?:\.\d+){1,2}$'); + final bool isValidGradleVersion = + validGradleVersionPattern.stringMatch(version) == version; + if (!isValidGradleVersion) { + printError( + 'A version with a valid format (maximum 2-3 numbers separated by period) must be provided.'); + throw ToolExit(_exitInvalidTargetVersion); + } + _targetVersion = version; + return; + } else { + // TODO(camsim99): Add other supported Android dependencies like the Android SDK and AGP. + printError( + 'Target Android dependency $_targetAndroidDependency is unrecognized.'); + throw ToolExit(_exitIncorrectTargetDependency); } } } @@ -116,7 +160,11 @@ ${response.httpResponse.body} if (_targetPubPackage != null) { return _runForPubDependency(package, _targetPubPackage!); } - // TODO(stuartmorgan): Add othe dependency types here (e.g., maven). + if (_targetAndroidDependency != null) { + return _runForAndroidDependency(package); + } + + // TODO(stuartmorgan): Add other dependency types here (e.g., maven). return PackageResult.fail(); } @@ -181,6 +229,65 @@ ${response.httpResponse.body} return PackageResult.success(); } + /// Handles all of the updates for [package] when the target dependency is + /// an Android dependency. + Future _runForAndroidDependency( + RepositoryPackage package) async { + if (_targetAndroidDependency == 'gradle') { + final Iterable packageExamples = package.getExamples(); + bool updateRanForExamples = false; + for (final RepositoryPackage example in packageExamples) { + if (!example.platformDirectory(FlutterPlatform.android).existsSync()) { + continue; + } + + updateRanForExamples = true; + Directory gradleWrapperPropertiesDirectory = + example.platformDirectory(FlutterPlatform.android); + if (gradleWrapperPropertiesDirectory + .childDirectory('app') + .childDirectory('gradle') + .existsSync()) { + gradleWrapperPropertiesDirectory = + gradleWrapperPropertiesDirectory.childDirectory('app'); + } + final File gradleWrapperPropertiesFile = + gradleWrapperPropertiesDirectory + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties'); + + final String gradleWrapperPropertiesContents = + gradleWrapperPropertiesFile.readAsStringSync(); + final RegExp validGradleDistributionUrl = + RegExp(r'^\s*distributionUrl\s*=\s*.*\.zip', multiLine: true); + if (!validGradleDistributionUrl + .hasMatch(gradleWrapperPropertiesContents)) { + return PackageResult.fail([ + 'Unable to find a "distributionUrl" entry to update for ${package.displayName}.' + ]); + } + + print( + '${indentation}Updating ${getRelativePosixPath(example.directory, from: package.directory)} to "$_targetVersion"'); + final String newGradleWrapperPropertiesContents = + gradleWrapperPropertiesContents.replaceFirst( + validGradleDistributionUrl, + 'distributionUrl=https\\://services.gradle.org/distributions/gradle-$_targetVersion-all.zip'); + // TODO(camsim99): Validate current AGP version against target Gradle + // version: https://github.com/flutter/flutter/issues/133887. + gradleWrapperPropertiesFile + .writeAsStringSync(newGradleWrapperPropertiesContents); + } + return updateRanForExamples + ? PackageResult.success() + : PackageResult.skip('No example apps run on Android.'); + } + return PackageResult.fail([ + 'Target Android dependency $_androidDependency is unrecognized.' + ]); + } + /// Returns information about the current dependency of [package] on /// the package named [dependencyName], or null if there is no dependency. _PubDependencyInfo? _getPubDependencyInfo( diff --git a/script/tool/test/update_dependency_command_test.dart b/script/tool/test/update_dependency_command_test.dart index e27b44468b09..0fd3969210f6 100644 --- a/script/tool/test/update_dependency_command_test.dart +++ b/script/tool/test/update_dependency_command_test.dart @@ -79,6 +79,27 @@ dev_dependencies: ); }); + test('throws if multiple dependencies specified', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'update-dependency', + '--pub-package', + 'target_package', + '--android-dependency', + 'gradle' + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Exactly one of the target flags must be provided:'), + ]), + ); + }); + group('pub dependencies', () { test('throws if no version is given for an unpublished target', () async { mockHttpResponse = (http.Request request) async { @@ -584,4 +605,226 @@ dev_dependencies: ); }); }); + + group('Android dependencies', () { + group('gradle', () { + test('throws if version format is invalid', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'update-dependency', + '--android-dependency', + 'gradle', + '--version', + '83', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'A version with a valid format (maximum 2-3 numbers separated by period) must be provided.'), + ]), + ); + }); + + test('skips if example app does not run on Android', () async { + final RepositoryPackage package = + createFakePlugin('fake_plugin', packagesDir); + + final List output = await runCapturingPrint(runner, [ + 'update-dependency', + '--packages', + package.displayName, + '--android-dependency', + 'gradle', + '--version', + '8.8.8', + ]); + + expect( + output, + containsAllInOrder([ + contains('SKIPPING: No example apps run on Android.'), + ]), + ); + }); + + test( + 'throws if wrapper does not have distribution URL with expected format', + () async { + final RepositoryPackage package = createFakePlugin( + 'fake_plugin', packagesDir, extraFiles: [ + 'example/android/app/gradle/wrapper/gradle-wrapper.properties' + ]); + + final File gradleWrapperPropertiesFile = package.directory + .childDirectory('example') + .childDirectory('android') + .childDirectory('app') + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties'); + + gradleWrapperPropertiesFile.writeAsStringSync(''' +How is it even possible that I didn't specify a Gradle distribution? +'''); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'update-dependency', + '--packages', + package.displayName, + '--android-dependency', + 'gradle', + '--version', + '8.8.8', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to find a "distributionUrl" entry to update for ${package.displayName}.'), + ]), + ); + }); + + test('succeeds if example app has android/app/gradle directory structure', + () async { + final RepositoryPackage package = createFakePlugin( + 'fake_plugin', packagesDir, extraFiles: [ + 'example/android/app/gradle/wrapper/gradle-wrapper.properties' + ]); + const String newGradleVersion = '8.8.8'; + + final File gradleWrapperPropertiesFile = package.directory + .childDirectory('example') + .childDirectory('android') + .childDirectory('app') + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties'); + + gradleWrapperPropertiesFile.writeAsStringSync(r''' +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +'''); + + await runCapturingPrint(runner, [ + 'update-dependency', + '--packages', + package.displayName, + '--android-dependency', + 'gradle', + '--version', + newGradleVersion, + ]); + + final String updatedGradleWrapperPropertiesContents = + gradleWrapperPropertiesFile.readAsStringSync(); + expect( + updatedGradleWrapperPropertiesContents, + contains( + r'distributionUrl=https\://services.gradle.org/distributions/' + 'gradle-$newGradleVersion-all.zip')); + }); + + test('succeeds if example app has android/gradle directory structure', + () async { + final RepositoryPackage package = createFakePlugin( + 'fake_plugin', packagesDir, extraFiles: [ + 'example/android/gradle/wrapper/gradle-wrapper.properties' + ]); + const String newGradleVersion = '9.9'; + + final File gradleWrapperPropertiesFile = package.directory + .childDirectory('example') + .childDirectory('android') + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties'); + + gradleWrapperPropertiesFile.writeAsStringSync(r''' +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +'''); + + await runCapturingPrint(runner, [ + 'update-dependency', + '--packages', + package.displayName, + '--android-dependency', + 'gradle', + '--version', + newGradleVersion, + ]); + + final String updatedGradleWrapperPropertiesContents = + gradleWrapperPropertiesFile.readAsStringSync(); + expect( + updatedGradleWrapperPropertiesContents, + contains( + r'distributionUrl=https\://services.gradle.org/distributions/' + 'gradle-$newGradleVersion-all.zip')); + }); + }); + }); + + test('succeeds if one example app runs on Android and another does not', + () async { + final RepositoryPackage package = createFakePlugin( + 'fake_plugin', packagesDir, examples: [ + 'example_1', + 'example_2' + ], extraFiles: [ + 'example/example_2/android/app/gradle/wrapper/gradle-wrapper.properties' + ]); + const String newGradleVersion = '8.8.8'; + + final File gradleWrapperPropertiesFile = package.directory + .childDirectory('example') + .childDirectory('example_2') + .childDirectory('android') + .childDirectory('app') + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties'); + + gradleWrapperPropertiesFile.writeAsStringSync(r''' +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +'''); + + await runCapturingPrint(runner, [ + 'update-dependency', + '--packages', + package.displayName, + '--android-dependency', + 'gradle', + '--version', + newGradleVersion, + ]); + + final String updatedGradleWrapperPropertiesContents = + gradleWrapperPropertiesFile.readAsStringSync(); + expect( + updatedGradleWrapperPropertiesContents, + contains(r'distributionUrl=https\://services.gradle.org/distributions/' + 'gradle-$newGradleVersion-all.zip')); + }); }