[tool] Add option for Android compile SDK version to update-dependencies command (#5010)

Adds option to `update-dependencies` command to update the compile SDK version of plugins or their example apps.
This commit is contained in:
Camille Simon
2023-10-31 22:15:01 +00:00
committed by GitHub
parent 1ac75cfbc7
commit 64dbd3a303
4 changed files with 484 additions and 92 deletions

View File

@ -142,6 +142,18 @@ class RepositoryPackage {
!isPlatformInterface &&
directory.basename != directory.parent.basename;
/// True if this appears to be an example package, according to package
/// conventions.
bool get isExample {
final RepositoryPackage? enclosingPackage = getEnclosingPackage();
if (enclosingPackage == null) {
// An example package is enclosed in another package.
return false;
}
// Check whether this is one of the enclosing package's examples.
return enclosingPackage.getExamples().any((RepositoryPackage p) => p.path == path);
}
/// Returns the Flutter example packages contained in the package, if any.
Iterable<RepositoryPackage> getExamples() {
final Directory exampleDirectory = directory.childDirectory('example');

View File

@ -42,10 +42,17 @@ class UpdateDependencyCommand extends PackageLoopingCommand {
argParser.addOption(_androidDependency,
help: 'An Android dependency to update.',
allowed: <String>[
'gradle',
_AndroidDepdencyType.gradle,
_AndroidDepdencyType.compileSdk,
_AndroidDepdencyType.compileSdkForExamples,
],
allowedHelp: <String, String>{
'gradle': 'Updates Gradle version used in plugin example apps.',
_AndroidDepdencyType.gradle:
'Updates Gradle version used in plugin example apps.',
_AndroidDepdencyType.compileSdk:
'Updates compileSdk version used to compile plugins.',
_AndroidDepdencyType.compileSdkForExamples:
'Updates compileSdk version used to compile plugin examples.',
});
argParser.addOption(
_versionFlag,
@ -130,7 +137,7 @@ ${response.httpResponse.body}
if (version == null) {
printError('A version must be provided to update this dependency.');
throw ToolExit(_exitNoTargetVersion);
} else if (_targetAndroidDependency == 'gradle') {
} else if (_targetAndroidDependency == _AndroidDepdencyType.gradle) {
final RegExp validGradleVersionPattern = RegExp(r'^\d+(?:\.\d+){1,2}$');
final bool isValidGradleVersion =
validGradleVersionPattern.stringMatch(version) == version;
@ -139,14 +146,24 @@ ${response.httpResponse.body}
'A version with a valid format (maximum 2-3 numbers separated by period) must be provided.');
throw ToolExit(_exitInvalidTargetVersion);
}
_targetVersion = version;
return;
} else if (_targetAndroidDependency == _AndroidDepdencyType.compileSdk ||
_targetAndroidDependency ==
_AndroidDepdencyType.compileSdkForExamples) {
final RegExp validSdkVersion = RegExp(r'^\d{1,2}$');
final bool isValidSdkVersion =
validSdkVersion.stringMatch(version) == version;
if (!isValidSdkVersion) {
printError(
'A valid Android SDK version number (1-2 digit numbers) must be provided.');
throw ToolExit(_exitInvalidTargetVersion);
}
} else {
// TODO(camsim99): Add other supported Android dependencies like the Android SDK and AGP.
// TODO(camsim99): Add other supported Android dependencies like the min/target Android SDK and AGP.
printError(
'Target Android dependency $_targetAndroidDependency is unrecognized.');
throw ToolExit(_exitIncorrectTargetDependency);
}
_targetVersion = version;
}
}
@ -233,61 +250,116 @@ ${response.httpResponse.body}
/// an Android dependency.
Future<PackageResult> _runForAndroidDependency(
RepositoryPackage package) async {
if (_targetAndroidDependency == 'gradle') {
final Iterable<RepositoryPackage> 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(<String>[
'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.');
if (_targetAndroidDependency == _AndroidDepdencyType.compileSdk) {
return _runForCompileSdkVersion(package);
} else if (_targetAndroidDependency == _AndroidDepdencyType.gradle ||
_targetAndroidDependency ==
_AndroidDepdencyType.compileSdkForExamples) {
return _runForAndroidDependencyOnExamples(package);
}
return PackageResult.fail(<String>[
'Target Android dependency $_androidDependency is unrecognized.'
]);
}
Future<PackageResult> _runForAndroidDependencyOnExamples(
RepositoryPackage package) async {
final Iterable<RepositoryPackage> packageExamples = package.getExamples();
bool updateRanForExamples = false;
for (final RepositoryPackage example in packageExamples) {
if (!example.platformDirectory(FlutterPlatform.android).existsSync()) {
continue;
}
updateRanForExamples = true;
Directory androidDirectory =
example.platformDirectory(FlutterPlatform.android);
final File fileToUpdate;
final RegExp dependencyVersionPattern;
final String newDependencyVersionEntry;
if (_targetAndroidDependency == _AndroidDepdencyType.gradle) {
if (androidDirectory
.childDirectory('app')
.childDirectory('gradle')
.existsSync()) {
androidDirectory = androidDirectory.childDirectory('app');
}
fileToUpdate = androidDirectory
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties');
dependencyVersionPattern =
RegExp(r'^\s*distributionUrl\s*=\s*.*\.zip', multiLine: true);
// TODO(camsim99): Validate current AGP version against target Gradle
// version: https://github.com/flutter/flutter/issues/133887.
newDependencyVersionEntry =
'distributionUrl=https\\://services.gradle.org/distributions/gradle-$_targetVersion-all.zip';
} else if (_targetAndroidDependency ==
_AndroidDepdencyType.compileSdkForExamples) {
fileToUpdate =
androidDirectory.childDirectory('app').childFile('build.gradle');
dependencyVersionPattern = RegExp(
r'(compileSdk|compileSdkVersion) (\d{1,2}|flutter.compileSdkVersion)');
newDependencyVersionEntry = 'compileSdk $_targetVersion';
} else {
printError(
'Target Android dependency $_targetAndroidDependency is unrecognized.');
throw ToolExit(_exitIncorrectTargetDependency);
}
final String oldFileToUpdateContents = fileToUpdate.readAsStringSync();
if (!dependencyVersionPattern.hasMatch(oldFileToUpdateContents)) {
return PackageResult.fail(<String>[
'Unable to find a $_targetAndroidDependency version entry to update for ${example.displayName}.'
]);
}
print(
'${indentation}Updating ${getRelativePosixPath(example.directory, from: package.directory)} to "$_targetVersion"');
final String newGradleWrapperPropertiesContents = oldFileToUpdateContents
.replaceFirst(dependencyVersionPattern, newDependencyVersionEntry);
fileToUpdate.writeAsStringSync(newGradleWrapperPropertiesContents);
}
return updateRanForExamples
? PackageResult.success()
: PackageResult.skip('No example apps run on Android.');
}
Future<PackageResult> _runForCompileSdkVersion(
RepositoryPackage package) async {
if (!package.platformDirectory(FlutterPlatform.android).existsSync()) {
return PackageResult.skip(
'Package ${package.displayName} does not run on Android.');
} else if (package.isExample) {
// We skip examples for this command.
return PackageResult.skip(
'Package ${package.displayName} is not a top-level package; run with "compileSdkForExamples" to update.');
}
final File buildConfigurationFile = package
.platformDirectory(FlutterPlatform.android)
.childFile('build.gradle');
final String buildConfigurationContents =
buildConfigurationFile.readAsStringSync();
final RegExp validCompileSdkVersion =
RegExp(r'(compileSdk|compileSdkVersion) \d{1,2}');
if (!validCompileSdkVersion.hasMatch(buildConfigurationContents)) {
return PackageResult.fail(<String>[
'Unable to find a compileSdk version entry to update for ${package.displayName}.'
]);
}
print('${indentation}Updating ${package.directory} to "$_targetVersion"');
final String newBuildConfigurationContents = buildConfigurationContents
.replaceFirst(validCompileSdkVersion, 'compileSdk $_targetVersion');
buildConfigurationFile.writeAsStringSync(newBuildConfigurationContents);
return PackageResult.success();
}
/// Returns information about the current dependency of [package] on
/// the package named [dependencyName], or null if there is no dependency.
_PubDependencyInfo? _getPubDependencyInfo(
@ -414,3 +486,9 @@ class _PubDependencyInfo {
}
enum _PubDependencyType { normal, dev }
class _AndroidDepdencyType {
static const String gradle = 'gradle';
static const String compileSdk = 'compileSdk';
static const String compileSdkForExamples = 'compileSdkForExamples';
}

View File

@ -102,6 +102,7 @@ void main() {
final List<RepositoryPackage> examples = plugin.getExamples().toList();
expect(examples.length, 1);
expect(examples[0].isExample, isTrue);
expect(examples[0].path, getExampleDir(plugin).path);
});
@ -112,6 +113,8 @@ void main() {
final List<RepositoryPackage> examples = plugin.getExamples().toList();
expect(examples.length, 2);
expect(examples[0].isExample, isTrue);
expect(examples[1].isExample, isTrue);
expect(examples[0].path,
getExampleDir(plugin).childDirectory('example1').path);
expect(examples[1].path,
@ -125,6 +128,7 @@ void main() {
final List<RepositoryPackage> examples = package.getExamples().toList();
expect(examples.length, 1);
expect(examples[0].isExample, isTrue);
expect(examples[0].path, getExampleDir(package).path);
});
@ -136,6 +140,8 @@ void main() {
final List<RepositoryPackage> examples = package.getExamples().toList();
expect(examples.length, 2);
expect(examples[0].isExample, isTrue);
expect(examples[1].isExample, isTrue);
expect(examples[0].path,
getExampleDir(package).childDirectory('example1').path);
expect(examples[1].path,
@ -151,6 +157,7 @@ void main() {
expect(plugin.isAppFacing, false);
expect(plugin.isPlatformInterface, false);
expect(plugin.isFederated, false);
expect(plugin.isExample, isFalse);
});
test('handle app-facing packages', () {
@ -160,6 +167,7 @@ void main() {
expect(plugin.isAppFacing, true);
expect(plugin.isPlatformInterface, false);
expect(plugin.isPlatformImplementation, false);
expect(plugin.isExample, isFalse);
});
test('handle platform interface packages', () {
@ -170,6 +178,7 @@ void main() {
expect(plugin.isAppFacing, false);
expect(plugin.isPlatformInterface, true);
expect(plugin.isPlatformImplementation, false);
expect(plugin.isExample, isFalse);
});
test('handle platform implementation packages', () {
@ -181,6 +190,7 @@ void main() {
expect(plugin.isAppFacing, false);
expect(plugin.isPlatformInterface, false);
expect(plugin.isPlatformImplementation, true);
expect(plugin.isExample, isFalse);
});
});

View File

@ -690,7 +690,7 @@ How is it even possible that I didn't specify a Gradle distribution?
output,
containsAllInOrder(<Matcher>[
contains(
'Unable to find a "distributionUrl" entry to update for ${package.displayName}.'),
'Unable to find a gradle version entry to update for ${package.displayName}/example.'),
]),
);
});
@ -779,30 +779,28 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
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: <String>[
'example_1',
'example_2'
], extraFiles: <String>[
'example/example_2/android/app/gradle/wrapper/gradle-wrapper.properties'
]);
const String newGradleVersion = '8.8.8';
test('succeeds if one example app runs on Android and another does not',
() async {
final RepositoryPackage package = createFakePlugin(
'fake_plugin', packagesDir, examples: <String>[
'example_1',
'example_2'
], extraFiles: <String>[
'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');
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'''
gradleWrapperPropertiesFile.writeAsStringSync(r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
@ -810,21 +808,315 @@ zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
''');
await runCapturingPrint(runner, <String>[
'update-dependency',
'--packages',
package.displayName,
'--android-dependency',
'gradle',
'--version',
newGradleVersion,
]);
await runCapturingPrint(runner, <String>[
'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'));
final String updatedGradleWrapperPropertiesContents =
gradleWrapperPropertiesFile.readAsStringSync();
expect(
updatedGradleWrapperPropertiesContents,
contains(
r'distributionUrl=https\://services.gradle.org/distributions/'
'gradle-$newGradleVersion-all.zip'));
});
});
group('compileSdk/compileSdkForExamples', () {
// Tests if the compileSdk version is updated for the provided
// build.gradle file and new compileSdk version to update to.
Future<void> testCompileSdkVersionUpdated(
{required RepositoryPackage package,
required File buildGradleFile,
required String oldCompileSdkVersion,
required String newCompileSdkVersion,
bool runForExamples = false,
bool checkForDeprecatedCompileSdkVersion = false}) async {
buildGradleFile.writeAsStringSync('''
android {
// Conditional for compatibility with AGP <4.2.
if (project.android.hasProperty("namespace")) {
namespace 'io.flutter.plugins.pathprovider'
}
${checkForDeprecatedCompileSdkVersion ? 'compileSdkVersion' : 'compileSdk'} $oldCompileSdkVersion
''');
await runCapturingPrint(runner, <String>[
'update-dependency',
'--packages',
package.displayName,
'--android-dependency',
if (runForExamples) 'compileSdkForExamples' else 'compileSdk',
'--version',
newCompileSdkVersion,
]);
final String updatedBuildGradleContents =
buildGradleFile.readAsStringSync();
// compileSdkVersion is now deprecated, so if the tool finds any
// instances of compileSdk OR compileSdkVersion, it should change it
// to compileSdk. See https://developer.android.com/reference/tools/gradle-api/7.2/com/android/build/api/dsl/CommonExtension#compileSdkVersion(kotlin.Int).
expect(updatedBuildGradleContents,
contains('compileSdk $newCompileSdkVersion'));
}
test('throws if version format is invalid for compileSdk', () async {
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--android-dependency',
'compileSdk',
'--version',
'834',
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'A valid Android SDK version number (1-2 digit numbers) must be provided.'),
]),
);
});
test('throws if version format is invalid for compileSdkForExamples',
() async {
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--android-dependency',
'compileSdkForExamples',
'--version',
'438',
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'A valid Android SDK version number (1-2 digit numbers) must be provided.'),
]),
);
});
test('skips if plugin does not run on Android', () async {
final RepositoryPackage package =
createFakePlugin('fake_plugin', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--packages',
package.displayName,
'--android-dependency',
'compileSdk',
'--version',
'34',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'SKIPPING: Package ${package.displayName} does not run on Android.'),
]),
);
});
test('skips if plugin example does not run on Android', () async {
final RepositoryPackage package =
createFakePlugin('fake_plugin', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--packages',
package.displayName,
'--android-dependency',
'compileSdkForExamples',
'--version',
'34',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING: No example apps run on Android.'),
]),
);
});
test(
'throws if build configuration file does not have compileSdk version with expected format for compileSdk',
() async {
final RepositoryPackage package = createFakePlugin(
'fake_plugin', packagesDir,
extraFiles: <String>['android/build.gradle']);
final File buildGradleFile = package.directory
.childDirectory('android')
.childFile('build.gradle');
buildGradleFile.writeAsStringSync('''
How is it even possible that I didn't specify a compileSdk version?
''');
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--packages',
package.displayName,
'--android-dependency',
'compileSdk',
'--version',
'34',
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Unable to find a compileSdk version entry to update for ${package.displayName}.'),
]),
);
});
test(
'throws if build configuration file does not have compileSdk version with expected format for compileSdkForExamples',
() async {
final RepositoryPackage package = createFakePlugin(
'fake_plugin', packagesDir,
extraFiles: <String>['example/android/app/build.gradle']);
final File buildGradleFile = package.directory
.childDirectory('example')
.childDirectory('android')
.childDirectory('app')
.childFile('build.gradle');
buildGradleFile.writeAsStringSync('''
How is it even possible that I didn't specify a compileSdk version?
''');
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'update-dependency',
'--packages',
package.displayName,
'--android-dependency',
'compileSdkForExamples',
'--version',
'34',
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'Unable to find a compileSdkForExamples version entry to update for ${package.displayName}/example.'),
]),
);
});
test(
'succeeds if plugin runs on Android and valid version is supplied for compileSdkVersion entry',
() async {
final RepositoryPackage package = createFakePlugin(
'fake_plugin', packagesDir, extraFiles: <String>[
'android/build.gradle',
'example/android/app/build.gradle'
]);
final File buildGradleFile = package.directory
.childDirectory('android')
.childFile('build.gradle');
await testCompileSdkVersionUpdated(
package: package,
buildGradleFile: buildGradleFile,
oldCompileSdkVersion: '8',
newCompileSdkVersion: '16',
checkForDeprecatedCompileSdkVersion: true);
});
test(
'succeeds if plugin example runs on Android and valid version is supplied for compileSdkVersion entry',
() async {
final RepositoryPackage package = createFakePlugin(
'fake_plugin', packagesDir, extraFiles: <String>[
'android/build.gradle',
'example/android/app/build.gradle'
]);
final File exampleBuildGradleFile = package.directory
.childDirectory('example')
.childDirectory('android')
.childDirectory('app')
.childFile('build.gradle');
await testCompileSdkVersionUpdated(
package: package,
buildGradleFile: exampleBuildGradleFile,
oldCompileSdkVersion: '8',
newCompileSdkVersion: '16',
runForExamples: true,
checkForDeprecatedCompileSdkVersion: true);
});
test(
'succeeds if plugin runs on Android and valid version is supplied for compileSdk entry',
() async {
final RepositoryPackage package = createFakePlugin(
'fake_plugin', packagesDir, extraFiles: <String>[
'android/build.gradle',
'example/android/app/build.gradle'
]);
final File buildGradleFile = package.directory
.childDirectory('android')
.childFile('build.gradle');
await testCompileSdkVersionUpdated(
package: package,
buildGradleFile: buildGradleFile,
oldCompileSdkVersion: '8',
newCompileSdkVersion: '16');
});
test(
'succeeds if plugin example runs on Android and valid version is supplied for compileSdk entry',
() async {
final RepositoryPackage package = createFakePlugin(
'fake_plugin', packagesDir, extraFiles: <String>[
'android/build.gradle',
'example/android/app/build.gradle'
]);
final File exampleBuildGradleFile = package.directory
.childDirectory('example')
.childDirectory('android')
.childDirectory('app')
.childFile('build.gradle');
await testCompileSdkVersionUpdated(
package: package,
buildGradleFile: exampleBuildGradleFile,
oldCompileSdkVersion: '33',
newCompileSdkVersion: '34',
runForExamples: true);
});
});
});
}