| // 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 '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 platformLineSep = Platform.isWindows ? '\r\n': '\n'; |
| |
| /// Tests that a plugin A can depend on platform code from a plugin B |
| /// as long as plugin B is defined as a pub dependency of plugin A. |
| /// |
| /// This test fails when `flutter build apk` fails and the stderr from this command |
| /// contains "Unresolved reference: plugin_b". |
| Future<void> main() async { |
| await task(() async { |
| |
| section('Find Java'); |
| |
| final String? javaHome = await findJavaHome(); |
| if (javaHome == null) { |
| return TaskResult.failure('Could not find Java'); |
| } |
| |
| print('\nUsing JAVA_HOME=$javaHome'); |
| |
| final Directory tempDir = Directory.systemTemp.createTempSync('flutter_plugin_dependencies.'); |
| try { |
| |
| section('Create plugin A'); |
| |
| final Directory pluginADirectory = Directory(path.join(tempDir.path, 'plugin_a')); |
| await inDirectory(tempDir, () async { |
| await flutter( |
| 'create', |
| options: <String>[ |
| '--org', |
| 'io.flutter.devicelab.plugin_a', |
| '--template=plugin', |
| '--platforms=android,ios', |
| pluginADirectory.path, |
| ], |
| ); |
| }); |
| |
| section('Create plugin B'); |
| |
| final Directory pluginBDirectory = Directory(path.join(tempDir.path, 'plugin_b')); |
| await inDirectory(tempDir, () async { |
| await flutter( |
| 'create', |
| options: <String>[ |
| '--org', |
| 'io.flutter.devicelab.plugin_b', |
| '--template=plugin', |
| '--platforms=android,ios', |
| pluginBDirectory.path, |
| ], |
| ); |
| }); |
| |
| section('Create plugin C without android/ directory'); |
| |
| final Directory pluginCDirectory = Directory(path.join(tempDir.path, 'plugin_c')); |
| await inDirectory(tempDir, () async { |
| await flutter( |
| 'create', |
| options: <String>[ |
| '--org', |
| 'io.flutter.devicelab.plugin_c', |
| '--template=plugin', |
| '--platforms=ios', |
| pluginCDirectory.path, |
| ], |
| ); |
| }); |
| |
| checkDirectoryNotExists(path.join( |
| pluginCDirectory.path, |
| 'android', |
| )); |
| |
| final File pluginCpubspec = File(path.join(pluginCDirectory.path, 'pubspec.yaml')); |
| await pluginCpubspec.writeAsString(''' |
| name: plugin_c |
| version: 0.0.1 |
| |
| flutter: |
| plugin: |
| platforms: |
| ios: |
| pluginClass: Plugin_cPlugin |
| |
| dependencies: |
| flutter: |
| sdk: flutter |
| |
| environment: |
| sdk: ">=2.0.0-dev.28.0 <3.0.0" |
| flutter: ">=1.5.0" |
| ''', flush: true); |
| |
| section('Create plugin D without ios/ directory'); |
| |
| final Directory pluginDDirectory = Directory(path.join(tempDir.path, 'plugin_d')); |
| await inDirectory(tempDir, () async { |
| await flutter( |
| 'create', |
| options: <String>[ |
| '--org', |
| 'io.flutter.devicelab.plugin_d', |
| '--template=plugin', |
| '--platforms=android', |
| pluginDDirectory.path, |
| ], |
| ); |
| }); |
| |
| checkDirectoryNotExists(path.join( |
| pluginDDirectory.path, |
| 'ios', |
| )); |
| |
| section('Write dummy Kotlin code in plugin B'); |
| |
| final File pluginBKotlinClass = File(path.join( |
| pluginBDirectory.path, |
| 'android', |
| 'src', |
| 'main', |
| 'kotlin', |
| 'DummyPluginBClass.kt', |
| )); |
| |
| await pluginBKotlinClass.writeAsString(''' |
| package io.flutter.devicelab.plugin_b |
| |
| public class DummyPluginBClass { |
| companion object { |
| fun dummyStaticMethod() { |
| } |
| } |
| } |
| ''', flush: true); |
| |
| section('Make plugin A depend on plugin B, C, and D'); |
| |
| final File pluginApubspec = File(path.join(pluginADirectory.path, 'pubspec.yaml')); |
| String pluginApubspecContent = await pluginApubspec.readAsString(); |
| pluginApubspecContent = pluginApubspecContent.replaceFirst( |
| '${platformLineSep}dependencies:$platformLineSep', |
| '${platformLineSep}dependencies:$platformLineSep' |
| ' plugin_b:$platformLineSep' |
| ' path: ${pluginBDirectory.path}$platformLineSep' |
| ' plugin_c:$platformLineSep' |
| ' path: ${pluginCDirectory.path}$platformLineSep' |
| ' plugin_d:$platformLineSep' |
| ' path: ${pluginDDirectory.path}$platformLineSep', |
| ); |
| await pluginApubspec.writeAsString(pluginApubspecContent, flush: true); |
| |
| section('Write Kotlin code in plugin A that references Kotlin code from plugin B'); |
| |
| final File pluginAKotlinClass = File(path.join( |
| pluginADirectory.path, |
| 'android', |
| 'src', |
| 'main', |
| 'kotlin', |
| 'DummyPluginAClass.kt', |
| )); |
| |
| await pluginAKotlinClass.writeAsString(''' |
| package io.flutter.devicelab.plugin_a |
| |
| import io.flutter.devicelab.plugin_b.DummyPluginBClass |
| |
| public class DummyPluginAClass { |
| constructor() { |
| // Call a method from plugin b. |
| DummyPluginBClass.dummyStaticMethod(); |
| } |
| } |
| ''', flush: true); |
| |
| section('Verify .flutter-plugins-dependencies'); |
| |
| final Directory exampleApp = Directory(path.join(pluginADirectory.path, 'example')); |
| |
| await inDirectory(exampleApp, () async { |
| await flutter( |
| 'packages', |
| options: <String>['get'], |
| ); |
| }); |
| |
| final File flutterPluginsDependenciesFile = |
| File(path.join(exampleApp.path, '.flutter-plugins-dependencies')); |
| |
| if (!flutterPluginsDependenciesFile.existsSync()) { |
| return TaskResult.failure("${flutterPluginsDependenciesFile.path} doesn't exist"); |
| } |
| |
| final String flutterPluginsDependenciesFileContent = flutterPluginsDependenciesFile.readAsStringSync(); |
| |
| final Map<String, dynamic> jsonContent = json.decode(flutterPluginsDependenciesFileContent) as Map<String, dynamic>; |
| |
| // Verify the dependencyGraph object is valid. The rest of the contents of this file are not relevant to the |
| // dependency graph and are tested by unit tests. |
| final List<dynamic> dependencyGraph = jsonContent['dependencyGraph'] as List<dynamic>; |
| const String kExpectedPluginsDependenciesContent = |
| '[' |
| '{' |
| '"name":"plugin_a",' |
| '"dependencies":["plugin_b","plugin_c","plugin_d"]' |
| '},' |
| '{' |
| '"name":"plugin_b",' |
| '"dependencies":[]' |
| '},' |
| '{' |
| '"name":"plugin_c",' |
| '"dependencies":[]' |
| '},' |
| '{' |
| '"name":"plugin_d",' |
| '"dependencies":[]' |
| '}' |
| ']'; |
| final String graphString = json.encode(dependencyGraph); |
| if (graphString != kExpectedPluginsDependenciesContent) { |
| return TaskResult.failure( |
| 'Unexpected file content in ${flutterPluginsDependenciesFile.path}: ' |
| 'Found "$graphString" instead of "$kExpectedPluginsDependenciesContent"' |
| ); |
| } |
| |
| section('Build plugin A example Android app'); |
| |
| final StringBuffer stderr = StringBuffer(); |
| await inDirectory(exampleApp, () async { |
| await evalFlutter( |
| 'build', |
| options: <String>['apk', '--target-platform', 'android-arm'], |
| canFail: true, |
| stderr: stderr, |
| ); |
| }); |
| |
| if (stderr.toString().contains('Unresolved reference: plugin_b')) { |
| return TaskResult.failure('plugin_a cannot reference plugin_b'); |
| } |
| |
| final bool pluginAExampleApk = exists(File(path.join( |
| pluginADirectory.path, |
| 'example', |
| 'build', |
| 'app', |
| 'outputs', |
| 'apk', |
| 'release', |
| 'app-release.apk', |
| ))); |
| |
| if (!pluginAExampleApk) { |
| return TaskResult.failure('Failed to build plugin A example APK'); |
| } |
| |
| if (Platform.isMacOS) { |
| section('Build plugin A example iOS app'); |
| |
| await inDirectory(exampleApp, () async { |
| await evalFlutter( |
| 'build', |
| options: <String>[ |
| 'ios', |
| '--no-codesign', |
| ], |
| ); |
| }); |
| |
| final Directory appBundle = Directory(path.join( |
| pluginADirectory.path, |
| 'example', |
| 'build', |
| 'ios', |
| 'iphoneos', |
| 'Runner.app', |
| )); |
| |
| if (!exists(appBundle)) { |
| return TaskResult.failure('Failed to build plugin A example iOS app'); |
| } |
| |
| checkDirectoryExists(path.join( |
| appBundle.path, |
| 'Frameworks', |
| 'plugin_a.framework', |
| )); |
| checkDirectoryExists(path.join( |
| appBundle.path, |
| 'Frameworks', |
| 'plugin_b.framework', |
| )); |
| checkDirectoryExists(path.join( |
| appBundle.path, |
| 'Frameworks', |
| 'plugin_c.framework', |
| )); |
| |
| // Plugin D is Android only and should not be embedded. |
| checkDirectoryNotExists(path.join( |
| appBundle.path, |
| 'Frameworks', |
| 'plugin_d.framework', |
| )); |
| } |
| |
| return TaskResult.success(null); |
| } on TaskResult catch (taskResult) { |
| return taskResult; |
| } catch (e) { |
| return TaskResult.failure(e.toString()); |
| } finally { |
| rmTree(tempDir); |
| } |
| }); |
| } |