// 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); } }); }