| // Copyright 2013 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:async'; | |
| import 'package:file/file.dart'; | |
| import 'package:platform/platform.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| import 'common/core.dart'; | |
| import 'common/package_looping_command.dart'; | |
| import 'common/plugin_utils.dart'; | |
| import 'common/process_runner.dart'; | |
| import 'common/repository_package.dart'; | |
| /// Key for APK. | |
| const String _platformFlagApk = 'apk'; | |
| const String _pluginToolsConfigFileName = '.pluginToolsConfig.yaml'; | |
| const String _pluginToolsConfigBuildFlagsKey = 'buildFlags'; | |
| const String _pluginToolsConfigGlobalKey = 'global'; | |
| const String _pluginToolsConfigExample = ''' | |
| $_pluginToolsConfigBuildFlagsKey: | |
| $_pluginToolsConfigGlobalKey: | |
| - "--no-tree-shake-icons" | |
| - "--dart-define=buildmode=testing" | |
| '''; | |
| const int _exitNoPlatformFlags = 3; | |
| const int _exitInvalidPluginToolsConfig = 4; | |
| // Flutter build types. These are the values passed to `flutter build <foo>`. | |
| const String _flutterBuildTypeAndroid = 'apk'; | |
| const String _flutterBuildTypeIOS = 'ios'; | |
| const String _flutterBuildTypeLinux = 'linux'; | |
| const String _flutterBuildTypeMacOS = 'macos'; | |
| const String _flutterBuildTypeWeb = 'web'; | |
| const String _flutterBuildTypeWindows = 'windows'; | |
| /// A command to build the example applications for packages. | |
| class BuildExamplesCommand extends PackageLoopingCommand { | |
| /// Creates an instance of the build command. | |
| BuildExamplesCommand( | |
| Directory packagesDir, { | |
| ProcessRunner processRunner = const ProcessRunner(), | |
| Platform platform = const LocalPlatform(), | |
| }) : super(packagesDir, processRunner: processRunner, platform: platform) { | |
| argParser.addFlag(platformLinux); | |
| argParser.addFlag(platformMacOS); | |
| argParser.addFlag(platformWeb); | |
| argParser.addFlag(platformWindows); | |
| argParser.addFlag(platformIOS); | |
| argParser.addFlag(_platformFlagApk); | |
| argParser.addOption( | |
| kEnableExperiment, | |
| defaultsTo: '', | |
| help: 'Enables the given Dart SDK experiments.', | |
| ); | |
| } | |
| // Maps the switch this command uses to identify a platform to information | |
| // about it. | |
| static final Map<String, _PlatformDetails> _platforms = | |
| <String, _PlatformDetails>{ | |
| _platformFlagApk: const _PlatformDetails( | |
| 'Android', | |
| pluginPlatform: platformAndroid, | |
| flutterBuildType: _flutterBuildTypeAndroid, | |
| ), | |
| platformIOS: const _PlatformDetails( | |
| 'iOS', | |
| pluginPlatform: platformIOS, | |
| flutterBuildType: _flutterBuildTypeIOS, | |
| extraBuildFlags: <String>['--no-codesign'], | |
| ), | |
| platformLinux: const _PlatformDetails( | |
| 'Linux', | |
| pluginPlatform: platformLinux, | |
| flutterBuildType: _flutterBuildTypeLinux, | |
| ), | |
| platformMacOS: const _PlatformDetails( | |
| 'macOS', | |
| pluginPlatform: platformMacOS, | |
| flutterBuildType: _flutterBuildTypeMacOS, | |
| ), | |
| platformWeb: const _PlatformDetails( | |
| 'web', | |
| pluginPlatform: platformWeb, | |
| flutterBuildType: _flutterBuildTypeWeb, | |
| ), | |
| platformWindows: const _PlatformDetails( | |
| 'Windows', | |
| pluginPlatform: platformWindows, | |
| flutterBuildType: _flutterBuildTypeWindows, | |
| ), | |
| }; | |
| @override | |
| final String name = 'build-examples'; | |
| @override | |
| final String description = | |
| 'Builds all example apps (IPA for iOS and APK for Android).\n\n' | |
| 'This command requires "flutter" to be in your path.\n\n' | |
| 'A $_pluginToolsConfigFileName file can be placed in an example app ' | |
| 'directory to specify additional build arguments. It should be a YAML ' | |
| 'file with a top-level map containing a single key ' | |
| '"$_pluginToolsConfigBuildFlagsKey" containing a map containing a ' | |
| 'single key "$_pluginToolsConfigGlobalKey" containing a list of build ' | |
| 'arguments.'; | |
| @override | |
| Future<void> initializeRun() async { | |
| final List<String> platformFlags = _platforms.keys.toList(); | |
| platformFlags.sort(); | |
| if (!platformFlags.any((String platform) => getBoolArg(platform))) { | |
| printError( | |
| 'None of ${platformFlags.map((String platform) => '--$platform').join(', ')} ' | |
| 'were specified. At least one platform must be provided.'); | |
| throw ToolExit(_exitNoPlatformFlags); | |
| } | |
| } | |
| @override | |
| Future<PackageResult> runForPackage(RepositoryPackage package) async { | |
| final List<String> errors = <String>[]; | |
| final bool isPlugin = isFlutterPlugin(package); | |
| final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries | |
| .where( | |
| (MapEntry<String, _PlatformDetails> entry) => getBoolArg(entry.key)) | |
| .map((MapEntry<String, _PlatformDetails> entry) => entry.value); | |
| // Platform support is checked at the package level for plugins; there is | |
| // no package-level platform information for non-plugin packages. | |
| final Set<_PlatformDetails> buildPlatforms = isPlugin | |
| ? requestedPlatforms | |
| .where((_PlatformDetails platform) => | |
| pluginSupportsPlatform(platform.pluginPlatform, package)) | |
| .toSet() | |
| : requestedPlatforms.toSet(); | |
| String platformDisplayList(Iterable<_PlatformDetails> platforms) { | |
| return platforms.map((_PlatformDetails p) => p.label).join(', '); | |
| } | |
| if (buildPlatforms.isEmpty) { | |
| final String unsupported = requestedPlatforms.length == 1 | |
| ? '${requestedPlatforms.first.label} is not supported' | |
| : 'None of [${platformDisplayList(requestedPlatforms)}] are supported'; | |
| return PackageResult.skip('$unsupported by this plugin'); | |
| } | |
| print('Building for: ${platformDisplayList(buildPlatforms)}'); | |
| final Set<_PlatformDetails> unsupportedPlatforms = | |
| requestedPlatforms.toSet().difference(buildPlatforms); | |
| if (unsupportedPlatforms.isNotEmpty) { | |
| final List<String> skippedPlatforms = unsupportedPlatforms | |
| .map((_PlatformDetails platform) => platform.label) | |
| .toList(); | |
| skippedPlatforms.sort(); | |
| print('Skipping unsupported platform(s): ' | |
| '${skippedPlatforms.join(', ')}'); | |
| } | |
| print(''); | |
| bool builtSomething = false; | |
| for (final RepositoryPackage example in package.getExamples()) { | |
| final String packageName = | |
| getRelativePosixPath(example.directory, from: packagesDir); | |
| for (final _PlatformDetails platform in buildPlatforms) { | |
| // Repo policy is that a plugin must have examples configured for all | |
| // supported platforms. For packages, just log and skip any requested | |
| // platform that a package doesn't have set up. | |
| if (!isPlugin && | |
| !example.directory | |
| .childDirectory(platform.flutterPlatformDirectory) | |
| .existsSync()) { | |
| print('Skipping ${platform.label} for $packageName; not supported.'); | |
| continue; | |
| } | |
| builtSomething = true; | |
| String buildPlatform = platform.label; | |
| if (platform.label.toLowerCase() != platform.flutterBuildType) { | |
| buildPlatform += ' (${platform.flutterBuildType})'; | |
| } | |
| print('\nBUILDING $packageName for $buildPlatform'); | |
| if (!await _buildExample(example, platform.flutterBuildType, | |
| extraBuildFlags: platform.extraBuildFlags)) { | |
| errors.add('$packageName (${platform.label})'); | |
| } | |
| } | |
| } | |
| if (!builtSomething) { | |
| if (isPlugin) { | |
| errors.add('No examples found'); | |
| } else { | |
| return PackageResult.skip( | |
| 'No examples found supporting requested platform(s).'); | |
| } | |
| } | |
| return errors.isEmpty | |
| ? PackageResult.success() | |
| : PackageResult.fail(errors); | |
| } | |
| Iterable<String> _readExtraBuildFlagsConfiguration( | |
| Directory directory) sync* { | |
| final File pluginToolsConfig = | |
| directory.childFile(_pluginToolsConfigFileName); | |
| if (pluginToolsConfig.existsSync()) { | |
| final Object? configuration = | |
| loadYaml(pluginToolsConfig.readAsStringSync()); | |
| if (configuration is! YamlMap) { | |
| printError('The $_pluginToolsConfigFileName file must be a YAML map.'); | |
| printError( | |
| 'Currently, the key "$_pluginToolsConfigBuildFlagsKey" is the only one that has an effect.'); | |
| printError( | |
| 'It must itself be a map. Currently, in that map only the key "$_pluginToolsConfigGlobalKey"'); | |
| printError( | |
| 'has any effect; it must contain a list of arguments to pass to the'); | |
| printError('flutter tool.'); | |
| printError(_pluginToolsConfigExample); | |
| throw ToolExit(_exitInvalidPluginToolsConfig); | |
| } | |
| if (configuration.containsKey(_pluginToolsConfigBuildFlagsKey)) { | |
| final Object? buildFlagsConfiguration = | |
| configuration[_pluginToolsConfigBuildFlagsKey]; | |
| if (buildFlagsConfiguration is! YamlMap) { | |
| printError( | |
| 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map.'); | |
| printError( | |
| 'Currently, in that map only the key "$_pluginToolsConfigGlobalKey" has any effect; it must '); | |
| printError( | |
| 'contain a list of arguments to pass to the flutter tool.'); | |
| printError(_pluginToolsConfigExample); | |
| throw ToolExit(_exitInvalidPluginToolsConfig); | |
| } | |
| if (buildFlagsConfiguration.containsKey(_pluginToolsConfigGlobalKey)) { | |
| final Object? globalBuildFlagsConfiguration = | |
| buildFlagsConfiguration[_pluginToolsConfigGlobalKey]; | |
| if (globalBuildFlagsConfiguration is! YamlList) { | |
| printError( | |
| 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map'); | |
| printError('whose "$_pluginToolsConfigGlobalKey" key is a list.'); | |
| printError( | |
| 'That list must contain a list of arguments to pass to the flutter tool.'); | |
| printError( | |
| 'For example, the $_pluginToolsConfigFileName file could look like:'); | |
| printError(_pluginToolsConfigExample); | |
| throw ToolExit(_exitInvalidPluginToolsConfig); | |
| } | |
| yield* globalBuildFlagsConfiguration.cast<String>(); | |
| } | |
| } | |
| } | |
| } | |
| Future<bool> _buildExample( | |
| RepositoryPackage example, | |
| String flutterBuildType, { | |
| List<String> extraBuildFlags = const <String>[], | |
| }) async { | |
| final String enableExperiment = getStringArg(kEnableExperiment); | |
| final int exitCode = await processRunner.runAndStream( | |
| flutterCommand, | |
| <String>[ | |
| 'build', | |
| flutterBuildType, | |
| ...extraBuildFlags, | |
| ..._readExtraBuildFlagsConfiguration(example.directory), | |
| if (enableExperiment.isNotEmpty) | |
| '--enable-experiment=$enableExperiment', | |
| ], | |
| workingDir: example.directory, | |
| ); | |
| return exitCode == 0; | |
| } | |
| } | |
| /// A collection of information related to a specific platform. | |
| class _PlatformDetails { | |
| const _PlatformDetails( | |
| this.label, { | |
| required this.pluginPlatform, | |
| required this.flutterBuildType, | |
| this.extraBuildFlags = const <String>[], | |
| }); | |
| /// The name to use in output. | |
| final String label; | |
| /// The key in a pubspec's platform: entry. | |
| final String pluginPlatform; | |
| /// The `flutter build` build type. | |
| final String flutterBuildType; | |
| /// The Flutter platform directory name. | |
| // In practice, this is the same as the plugin platform key for all platforms. | |
| // If that changes, this can be adjusted. | |
| String get flutterPlatformDirectory => pluginPlatform; | |
| /// Any extra flags to pass to `flutter build`. | |
| final List<String> extraBuildFlags; | |
| } |