mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

PR to pave the way for https://github.com/flutter/engine/pull/53001 to re-land Summary: - Enforces use of Kotlin >= `1.7.0` (please see below note) - Fixes ci failures that prevented the above PR from landing. Details: Because it landed initially, we are able to fake the roll in this PR to fix all the tests ([see my comment](https://github.com/flutter/flutter/pull/149204#discussion_r1617924772)). Fixes all the tests that failed: 1. `module_test` failing on multiple platforms (3/9 of the failures). Failure is ``` > Android resource linking failed ERROR:/b/s/w/ir/x/t/flutter_module_test.KECMXW/hello/.android/plugins_build_output/device_info/intermediates/merged_res/release/values/values.xml:194: AAPT: error: resource android:attr/lStar not found. ``` This is a rather unhelpful error message but some [folks online suggest](https://stackoverflow.com/a/69050529) that upgrading your `compileSdk` version fixes this. These resolve when I remove the dependency on the long discontinued [package_info](https://pub.dev/packages/package_info) and [device_info](https://pub.dev/packages/device_info) packages, perhaps because they are transitively pulling in low `compileSdk` versions? This is unclear to me. 2. `module_custom_host_app_name_test` was failing for the same reason (another 3/9, or cumulative 6/9). 3. `tool_integration_tests_3_4` was a flake ð (7/9) 4. `framework_tests_slow` needed a newer version of the Kotlin Gradle plugin (the flutter tool tells us this, so I just upgraded as suggested) and it resolved (8/9) 5.`android_preview_tool_integration_tests` needed newer AGP and KGP versions. I also refactored the tests, and bumped our error versions, fixing https://github.com/flutter/flutter/issues/142653. **Note that the bump to KGP is not in line with our policy** - we didn't warn for `1.5.0-1.6.x` for a release (or at all) before dropping support. But I think it might still be justified: - The bump to our androidx libraries unblocks ongoing Scribe work, and also includes a fix for a [memory leak](https://github.com/flutter/flutter/issues/129307#issuecomment-1601636959) and a [crash on folding phones](https://github.com/flutter/flutter/issues/114868#issuecomment-2133226962), among many other bug fixes. - Gradle [doesn't test on half of that range](https://docs.gradle.org/current/userguide/compatibility.html#kotlin), and so we implicitly can't claim to support it either. More generally, our Java and Kotlin support ranges should probably strictly fall within what Gradle tests.
456 lines
14 KiB
Dart
456 lines
14 KiB
Dart
// Copyright 2014 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 'dart:typed_data';
|
|
|
|
import 'package:archive/archive.dart';
|
|
import 'package:flutter_devicelab/framework/apk_utils.dart';
|
|
import 'package:flutter_devicelab/framework/framework.dart';
|
|
import 'package:flutter_devicelab/framework/task_result.dart';
|
|
import 'package:flutter_devicelab/framework/utils.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew';
|
|
final String gradlewExecutable =
|
|
Platform.isWindows ? '.\\$gradlew' : './$gradlew';
|
|
final String fileReadWriteMode = Platform.isWindows ? 'rw-rw-rw-' : 'rw-r--r--';
|
|
final String platformLineSep = Platform.isWindows ? '\r\n' : '\n';
|
|
|
|
/// Combines several TaskFunctions with trivial success value into one.
|
|
TaskFunction combine(List<TaskFunction> tasks) {
|
|
return () async {
|
|
for (final TaskFunction task in tasks) {
|
|
final TaskResult result = await task();
|
|
if (result.failed) {
|
|
return result;
|
|
}
|
|
}
|
|
return TaskResult.success(null);
|
|
};
|
|
}
|
|
|
|
/// Tests that the Flutter module project template works and supports
|
|
/// adding Flutter to an existing Android app.
|
|
class ModuleTest {
|
|
ModuleTest(
|
|
this.buildTarget, {
|
|
this.gradleVersion = '7.6.3',
|
|
});
|
|
|
|
final String buildTarget;
|
|
final String gradleVersion;
|
|
|
|
Future<TaskResult> call() async {
|
|
section('Running: $buildTarget');
|
|
section('Find Java');
|
|
|
|
final String? javaHome = await findJavaHome();
|
|
if (javaHome == null) {
|
|
return TaskResult.failure('Could not find Java');
|
|
}
|
|
print('\nUsing JAVA_HOME=$javaHome');
|
|
|
|
section('Create Flutter module project');
|
|
|
|
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
|
|
final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
|
|
try {
|
|
await inDirectory(tempDir, () async {
|
|
await flutter(
|
|
'create',
|
|
options: <String>['--org', 'io.flutter.devicelab', '--template=module', 'hello'],
|
|
);
|
|
});
|
|
|
|
section('Create package with native assets');
|
|
|
|
await flutter(
|
|
'config',
|
|
options: <String>['--enable-native-assets'],
|
|
);
|
|
|
|
const String ffiPackageName = 'ffi_package';
|
|
await createFfiPackage(ffiPackageName, tempDir);
|
|
|
|
section('Add FFI package');
|
|
|
|
final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
|
|
String content = await pubspec.readAsString();
|
|
content = content.replaceFirst(
|
|
'dependencies:$platformLineSep',
|
|
'dependencies:$platformLineSep $ffiPackageName:$platformLineSep path: ..${Platform.pathSeparator}$ffiPackageName$platformLineSep',
|
|
);
|
|
await pubspec.writeAsString(content, flush: true);
|
|
await inDirectory(projectDir, () async {
|
|
await flutter(
|
|
'packages',
|
|
options: <String>['get'],
|
|
);
|
|
});
|
|
|
|
section('Add read-only asset');
|
|
|
|
final File readonlyTxtAssetFile = await File(path.join(
|
|
projectDir.path,
|
|
'assets',
|
|
'read-only.txt'
|
|
))
|
|
.create(recursive: true);
|
|
|
|
if (!exists(readonlyTxtAssetFile)) {
|
|
return TaskResult.failure('Failed to create read-only asset');
|
|
}
|
|
|
|
if (!Platform.isWindows) {
|
|
await exec('chmod', <String>[
|
|
'444',
|
|
readonlyTxtAssetFile.path,
|
|
]);
|
|
}
|
|
|
|
content = content.replaceFirst(
|
|
'$platformLineSep # assets:$platformLineSep',
|
|
'$platformLineSep assets:$platformLineSep - assets/read-only.txt$platformLineSep',
|
|
);
|
|
await pubspec.writeAsString(content, flush: true);
|
|
|
|
section('Add plugins');
|
|
|
|
content = content.replaceFirst(
|
|
'${platformLineSep}dependencies:$platformLineSep',
|
|
'${platformLineSep}dependencies:$platformLineSep',
|
|
);
|
|
await pubspec.writeAsString(content, flush: true);
|
|
await inDirectory(projectDir, () async {
|
|
await flutter(
|
|
'packages',
|
|
options: <String>['get'],
|
|
);
|
|
});
|
|
|
|
// TODO(dacoharkes): Implement Add2app. https://github.com/flutter/flutter/issues/129757
|
|
|
|
section('Build Flutter module library archive');
|
|
|
|
await inDirectory(Directory(path.join(projectDir.path, '.android')), () async {
|
|
await exec(
|
|
gradlewExecutable,
|
|
<String>['flutter:assembleDebug'],
|
|
environment: <String, String>{ 'JAVA_HOME': javaHome },
|
|
);
|
|
});
|
|
|
|
final bool aarBuilt = exists(File(path.join(
|
|
projectDir.path,
|
|
'.android',
|
|
'Flutter',
|
|
'build',
|
|
'outputs',
|
|
'aar',
|
|
'flutter-debug.aar',
|
|
)));
|
|
|
|
if (!aarBuilt) {
|
|
return TaskResult.failure('Failed to build .aar');
|
|
}
|
|
|
|
section('Build ephemeral host app');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter(
|
|
'build',
|
|
options: <String>['apk'],
|
|
);
|
|
});
|
|
|
|
final bool ephemeralHostApkBuilt = exists(File(path.join(
|
|
projectDir.path,
|
|
'build',
|
|
'host',
|
|
'outputs',
|
|
'apk',
|
|
'release',
|
|
'app-release.apk',
|
|
)));
|
|
|
|
if (!ephemeralHostApkBuilt) {
|
|
return TaskResult.failure('Failed to build ephemeral host .apk');
|
|
}
|
|
|
|
section('Clean build');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter('clean');
|
|
});
|
|
|
|
section('Make Android host app editable');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter(
|
|
'make-host-app-editable',
|
|
options: <String>['android'],
|
|
);
|
|
});
|
|
|
|
section('Build editable host app');
|
|
|
|
await inDirectory(projectDir, () async {
|
|
await flutter(
|
|
'build',
|
|
options: <String>['apk'],
|
|
);
|
|
});
|
|
|
|
final bool editableHostApkBuilt = exists(File(path.join(
|
|
projectDir.path,
|
|
'build',
|
|
'host',
|
|
'outputs',
|
|
'apk',
|
|
'release',
|
|
'app-release.apk',
|
|
)));
|
|
|
|
if (!editableHostApkBuilt) {
|
|
return TaskResult.failure('Failed to build editable host .apk');
|
|
}
|
|
|
|
section('Add to existing Android app');
|
|
|
|
final Directory hostApp = Directory(path.join(tempDir.path, 'hello_host_app'));
|
|
mkdir(hostApp);
|
|
recursiveCopy(
|
|
Directory(
|
|
path.join(
|
|
flutterDirectory.path,
|
|
'dev',
|
|
'integration_tests',
|
|
'android_host_app_v2_embedding',
|
|
),
|
|
),
|
|
hostApp,
|
|
);
|
|
copy(
|
|
File(path.join(projectDir.path, '.android', gradlew)),
|
|
hostApp,
|
|
);
|
|
copy(
|
|
File(path.join(projectDir.path, '.android', 'gradle', 'wrapper',
|
|
'gradle-wrapper.jar')),
|
|
Directory(path.join(hostApp.path, 'gradle', 'wrapper')),
|
|
);
|
|
|
|
// Modify gradle version to passed in version.
|
|
// This is somehow the wrong file.
|
|
final File gradleWrapperProperties = File(path.join(
|
|
hostApp.path, 'gradle', 'wrapper', 'gradle-wrapper.properties'));
|
|
String propertyContent = await gradleWrapperProperties.readAsString();
|
|
propertyContent = propertyContent.replaceFirst(
|
|
'REPLACEME',
|
|
gradleVersion,
|
|
);
|
|
section(propertyContent);
|
|
await gradleWrapperProperties.writeAsString(propertyContent, flush: true);
|
|
|
|
final File analyticsOutputFile =
|
|
File(path.join(tempDir.path, 'analytics.log'));
|
|
|
|
section('Build debug host APK');
|
|
|
|
await inDirectory(hostApp, () async {
|
|
if (!Platform.isWindows) {
|
|
await exec('chmod', <String>['+x', 'gradlew']);
|
|
}
|
|
await exec(gradlewExecutable,
|
|
<String>['app:assembleDebug'],
|
|
environment: <String, String>{
|
|
'JAVA_HOME': javaHome,
|
|
'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path,
|
|
},
|
|
);
|
|
});
|
|
|
|
section('Check debug APK exists');
|
|
|
|
final String debugHostApk = path.join(
|
|
hostApp.path,
|
|
'app',
|
|
'build',
|
|
'outputs',
|
|
'apk',
|
|
'debug',
|
|
'app-debug.apk',
|
|
);
|
|
if (!exists(File(debugHostApk))) {
|
|
return TaskResult.failure('Failed to build debug host APK');
|
|
}
|
|
|
|
section('Check files in debug APK');
|
|
|
|
checkCollectionContains<String>(<String>[
|
|
...flutterAssets,
|
|
...debugAssets,
|
|
...baseApkFiles,
|
|
'lib/arm64-v8a/lib$ffiPackageName.so',
|
|
'lib/armeabi-v7a/lib$ffiPackageName.so',
|
|
], await getFilesInApk(debugHostApk));
|
|
|
|
section('Check debug AndroidManifest.xml');
|
|
|
|
final String androidManifestDebug = await getAndroidManifest(debugHostApk);
|
|
if (!androidManifestDebug.contains('''
|
|
<meta-data
|
|
android:name="flutterProjectType"
|
|
android:value="module" />''')
|
|
) {
|
|
return TaskResult.failure("Debug host APK doesn't contain metadata: flutterProjectType = module ");
|
|
}
|
|
|
|
final String analyticsOutput = analyticsOutputFile.readAsStringSync();
|
|
if (!analyticsOutput.contains('cd24: android')
|
|
|| !analyticsOutput.contains('cd25: true')
|
|
|| !analyticsOutput.contains('viewName: assemble')) {
|
|
return TaskResult.failure(
|
|
'Building outer app produced the following analytics: "$analyticsOutput" '
|
|
'but not the expected strings: "cd24: android", "cd25: true" and '
|
|
'"viewName: assemble"'
|
|
);
|
|
}
|
|
|
|
section('Check file access modes for read-only asset from Flutter module');
|
|
|
|
final String readonlyDebugAssetFilePath = path.joinAll(<String>[
|
|
hostApp.path,
|
|
'app',
|
|
'build',
|
|
'intermediates',
|
|
'assets',
|
|
'debug',
|
|
'flutter_assets',
|
|
'assets',
|
|
'read-only.txt',
|
|
]);
|
|
final File readonlyDebugAssetFile = File(readonlyDebugAssetFilePath);
|
|
if (!exists(readonlyDebugAssetFile)) {
|
|
return TaskResult.failure('Failed to copy read-only asset file');
|
|
}
|
|
|
|
String modes = readonlyDebugAssetFile.statSync().modeString();
|
|
print('\nread-only.txt file access modes = $modes');
|
|
if (modes.compareTo(fileReadWriteMode) != 0) {
|
|
return TaskResult.failure('Failed to make assets user-readable and writable');
|
|
}
|
|
|
|
section('Build release host APK');
|
|
|
|
await inDirectory(hostApp, () async {
|
|
await exec(gradlewExecutable,
|
|
<String>['app:assembleRelease'],
|
|
environment: <String, String>{
|
|
'JAVA_HOME': javaHome,
|
|
'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path,
|
|
},
|
|
);
|
|
});
|
|
|
|
final String releaseHostApk = path.join(
|
|
hostApp.path,
|
|
'app',
|
|
'build',
|
|
'outputs',
|
|
'apk',
|
|
'release',
|
|
'app-release-unsigned.apk',
|
|
);
|
|
if (!exists(File(releaseHostApk))) {
|
|
return TaskResult.failure('Failed to build release host APK');
|
|
}
|
|
|
|
section('Check files in release APK');
|
|
|
|
checkCollectionContains<String>(<String>[
|
|
...flutterAssets,
|
|
...baseApkFiles,
|
|
'lib/arm64-v8a/lib$ffiPackageName.so',
|
|
'lib/arm64-v8a/libapp.so',
|
|
'lib/arm64-v8a/libflutter.so',
|
|
'lib/armeabi-v7a/lib$ffiPackageName.so',
|
|
'lib/armeabi-v7a/libapp.so',
|
|
'lib/armeabi-v7a/libflutter.so',
|
|
], await getFilesInApk(releaseHostApk));
|
|
|
|
section('Check the NOTICE file is correct');
|
|
|
|
await inDirectory(hostApp, () async {
|
|
final File apkFile = File(releaseHostApk);
|
|
final Archive apk = ZipDecoder().decodeBytes(apkFile.readAsBytesSync());
|
|
// Shouldn't be missing since we already checked it exists above.
|
|
final ArchiveFile? noticesFile = apk.findFile('assets/flutter_assets/NOTICES.Z');
|
|
|
|
final Uint8List? licenseData = noticesFile?.content as Uint8List?;
|
|
if (licenseData == null) {
|
|
return TaskResult.failure('Invalid license file.');
|
|
}
|
|
final String licenseString = utf8.decode(gzip.decode(licenseData));
|
|
if (!licenseString.contains('skia') || !licenseString.contains('Flutter Authors')) {
|
|
return TaskResult.failure('License content missing.');
|
|
}
|
|
});
|
|
|
|
section('Check release AndroidManifest.xml');
|
|
|
|
final String androidManifestRelease = await getAndroidManifest(debugHostApk);
|
|
if (!androidManifestRelease.contains('''
|
|
<meta-data
|
|
android:name="flutterProjectType"
|
|
android:value="module" />''')
|
|
) {
|
|
return TaskResult.failure("Release host APK doesn't contain metadata: flutterProjectType = module ");
|
|
}
|
|
|
|
section('Check file access modes for read-only asset from Flutter module');
|
|
|
|
final String readonlyReleaseAssetFilePath = path.joinAll(<String>[
|
|
hostApp.path,
|
|
'app',
|
|
'build',
|
|
'intermediates',
|
|
'assets',
|
|
'release',
|
|
'flutter_assets',
|
|
'assets',
|
|
'read-only.txt',
|
|
]);
|
|
final File readonlyReleaseAssetFile = File(readonlyReleaseAssetFilePath);
|
|
if (!exists(readonlyReleaseAssetFile)) {
|
|
return TaskResult.failure('Failed to copy read-only asset file');
|
|
}
|
|
|
|
modes = readonlyReleaseAssetFile.statSync().modeString();
|
|
print('\nread-only.txt file access modes = $modes');
|
|
if (modes.compareTo(fileReadWriteMode) != 0) {
|
|
return TaskResult.failure('Failed to make assets user-readable and writable');
|
|
}
|
|
|
|
return TaskResult.success(null);
|
|
} on TaskResult catch (taskResult) {
|
|
return taskResult;
|
|
} catch (e) {
|
|
return TaskResult.failure(e.toString());
|
|
} finally {
|
|
rmTree(tempDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> main() async {
|
|
await task(combine(<TaskFunction>[
|
|
// ignore: avoid_redundant_argument_values
|
|
ModuleTest('module-gradle-7.6', gradleVersion: '7.6.3').call,
|
|
ModuleTest('module-gradle-7.6', gradleVersion: '7.6-rc-2').call,
|
|
]));
|
|
}
|