Flutterのビルドコマンドを追ってみよう

はじめに

この記事はFlutter #2 Advent Calendar 2018 3日目の記事です。

毎日flutter doctorを叩いてしまう、そんなエンジニアって多いと思います。
……flutter doctorを叩かずとも、flutter runflutter build apkflutter build iosは叩いてますよね。

そんなわけでflutter buildコマンドを追ってみたいと思います。

確認環境

確認日:2018.12.1

env ver
Mac OS 10.14.1
flutter v0.11.13-beta
Dart 2.1.0

コマンドファイルの場所を探そう

flutter SDKを展開したら、下図のcommandディレクトリを探します。 packages/flutter_tools/lib/src/commandsですね。 Githubで見たい場合はこちらからどうぞ。

├── bin
├── dev
├── examples
├── flutter
└── packages
    ├── flutter
    ├── flutter_driver
    ├── flutter_goldens
    ├── flutter_goldens_client
    ├── flutter_localizations
    ├── flutter_test
    ├── flutter_tools
    │   ├── bin
    │   ├── doc
    │   ├── gradle
    │   ├── ide_templates
    │   ├── lib
    │   │   └── src
    │   │       ├── android
    │   │       ├── base
    │   │       ├── commands <=
    │   │       ├── dart
    │   │       ├── fuchsia
    │   │       ├── intellij
    │   │       ├── ios
    │   │       ├── runner
    │   │       ├── test
    │   │       ├── tester
    │   │       └── vscode
    │   ├── schema
    │   ├── templates
    │   ├── test
    │   └── tool
    └── fuchsia_remote_debug_protocol

commandsディレクトリ内を見てみると、見慣れたコマンドが並んでいます。

├── analyze.dart
├── analyze_base.dart
├── analyze_continuously.dart
├── analyze_once.dart
├── attach.dart
├── build.dart
├── build_aot.dart
├── build_apk.dart
├── build_bundle.dart
├── build_flx.dart
├── build_ios.dart
├── channel.dart
├── clean.dart
├── config.dart
├── create.dart
├── daemon.dart
├── devices.dart
├── doctor.dart
├── drive.dart
├── emulators.dart
├── format.dart
├── ide_config.dart
├── inject_plugins.dart
├── install.dart
├── logs.dart
├── make_host_app_editable.dart
├── packages.dart
├── precache.dart
├── run.dart
├── screenshot.dart
├── shell_completion.dart
├── stop.dart
├── test.dart
├── trace.dart
├── update_packages.dart
└── upgrade.dart

buildコマンドを見てみる

今回はbuild系のコマンドを追ってみたいので、まずベースとなるbuild.dartを読んでみます。

// Copyright 2016 The Chromium 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:meta/meta.dart';

import '../base/file_system.dart';
import '../base/utils.dart';
import '../globals.dart';
import '../runner/flutter_command.dart';
import 'build_aot.dart';
import 'build_apk.dart';
import 'build_bundle.dart';
import 'build_flx.dart';
import 'build_ios.dart';

class BuildCommand extends FlutterCommand {
  BuildCommand({bool verboseHelp = false}) {
    addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildAotCommand());
    addSubcommand(BuildIOSCommand());
    addSubcommand(BuildFlxCommand());
    addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
  }

  @override
  final String name = 'build';

  @override
  final String description = 'Flutter build commands.';

  @override
  Future<FlutterCommandResult> runCommand() async => null;
}

abstract class BuildSubCommand extends FlutterCommand {
  BuildSubCommand() {
    requiresPubspecYaml();
  }

  @override
  @mustCallSuper
  Future<FlutterCommandResult> runCommand() async {
    if (isRunningOnBot) {
      final File dotPackages = fs.file('.packages');
      printStatus('Contents of .packages:');
      if (dotPackages.existsSync())
        printStatus(dotPackages.readAsStringSync());
      else
        printError('File not found: ${dotPackages.absolute.path}');

      final File pubspecLock = fs.file('pubspec.lock');
      printStatus('Contents of pubspec.lock:');
      if (pubspecLock.existsSync())
        printStatus(pubspecLock.readAsStringSync());
      else
        printError('File not found: ${pubspecLock.absolute.path}');
    }
    return null;
  }
}

build.dartファイルには BuildCommandがクラスとして、BuildSubCommandアブストラクトクラスとして定義されています。どちらもFlutterCommandクラスを、FlutterCommandクラスがCommandクラスを継承しています。

Commandクラスはpackages/flutter_tools/lib/src/runner/command_runner.dartに記述されています。コマンドの説明や結果で表示される文章もCommandクラスが生成しているので、flutter buildコマンドの例を取ってみるとおおよその関係が見えてきます。

Flutter build commands.                         // description反映

Usage: flutter build <subcommand> [arguments]   // name + SubCommand反映
-h, --help    Print this usage information.

Available subcommands:                          // SubCommand反映
  aot      Build an ahead-of-time compiled snapshot of your app's Dart code.
  apk      Build an Android APK file from your app.
  bundle   Build the Flutter assets directory from your app.
  flx      Deprecated
  ios      Build an iOS application bundle (Mac OS X host only).

BuildSubCommandは下記2つの確認を行うだけでのアブストラクトクラスになっています。

  1. pubspec.yamlがコマンドを叩いたカレントディレクトリに存在するか
  2. コマンドが実行された環境がCI環境か、そうではないか

BuildCommandクラスにてaddSubcommandされているので、BuildSubCommandはSubCommandとして扱われているだけなので、基本的にBuildCommandBuildSubCommandに差がなさそうです。

build apkコマンドをみてみる

筆者が得意なOSがAndroidのため、flutter build apkコマンドを追ってみることにします。 コマンドのコードはbuild_apk.dartになります。

// Copyright 2015 The Chromium 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 '../android/apk.dart';
import '../project.dart';
import '../runner/flutter_command.dart' show FlutterCommandResult;
import 'build.dart';

class BuildApkCommand extends BuildSubCommand {
  BuildApkCommand({bool verboseHelp = false}) {
    usesTargetOption();         // ビルド対象のpath指定
    addBuildModeFlags();        // releset/debug指定
    usesFlavorOption();         // flavor指定
    usesPubOption();            // flutter packages getをビルドコマンドの前に走らせるか
    usesBuildNumberOption();    // VersionCodeを指定
    usesBuildNameOption();      // VersionNameを指定

    argParser
      ..addFlag('track-widget-creation', negatable: false, hide: !verboseHelp)
      ..addFlag('build-shared-library',
        negatable: false,
        help: 'Whether to prefer compiling to a *.so file (android only).',
      )
      ..addOption('target-platform',
        defaultsTo: 'android-arm',
        allowed: <String>['android-arm', 'android-arm64']);
  }

  @override
  final String name = 'apk';

  @override
  final String description = 'Build an Android APK file from your app.\n\n'
    'This command can build debug and release versions of your application. \'debug\' builds support '
    'debugging and a quick development cycle. \'release\' builds don\'t support debugging and are '
    'suitable for deploying to app stores.';

  @override
  Future<FlutterCommandResult> runCommand() async {
    await super.runCommand();
    await buildApk(
      project: await FlutterProject.current(),
      target: targetFile,
      buildInfo: getBuildInfo(),
    );
    return null;
  }
}

BuildApkCommandのオプション処理はコメントに書いた通りの内容になります。実装はpackages/flutter_tools/lib/src//android/flutter_command.dartです。ビルドのModeなど、 Androidのビルドオプションに親しんでいる人ならば、コメントからおおよそ対応している項目は類推できる印象ですね。

runCommandメソッドはbuildApkコマンドを利用しています。packages/flutter_tools/lib/src//android/apk.dartを開くとわかるのですが、ほぼGradleによるbuildへの橋渡しをしているだけです。ただ、Android関連のbuildはGradleで完結するため、過不足ない記述になっています。

感想

FlutterがどうやってAndroid(Gradle)に処理を渡しているのか気になっていたので、簡単ではありますがコードベースで処理を追ってみました。
一番感動したのは、Flutterのビルド処理が全てDartで完結していたことです。Flutterを書く時にもDart、Flutterのビルドコマンドを書く時にもDartを利用することで、Dartが読めれば上から下まで理解できる仕組みになっているなと感じました。

ビルド中にログ出力をしたいや、CI上でより細かな処理をしたい場合などに、今回の記事が参考になれば幸いです。

最後までお付き合いいただき、ありがとうございました。