この記事はJX通信社アドベントカレンダーの19日目です。
sakebookです。最近はServer Side Kotlinをやってますが、Flutterも少し触ってます。
去年はKotlin で CLI のネタを書いたので今年はそれのDart版を書こうと思います。
全体の流れ
- Dartとプロジェクトのセットアップ
- CLIでの動作確認
- GitHub Actionsで配布
Dartのインストール
Flutterを使っていればbundleでインストールされていますが、standalone版が必要(後述)なのでHomebrewでいれます。
$ brew tap dart-lang/dart $ brew install dart
筆者の環境は次の通りです
$ dart --version Dart VM version: 2.7.0 (Fri Dec 6 16:26:51 2019 +0100) on "macos_x64"
CLIツールのテンプレート作成
Stagehandでプロジェクトテンプレートを作成します(関係ないですが画像が好み)。
まずはStagehandを有効化します。
$ pub global activate stagehand
Stagehandにはいくつかのテンプレートが用意されていますが、今回はCLIツールを作りたいので console-full
を選択します。
$ mkdir dart-cli-sample $ cd dart-cli-sample $ stagehand console-full
作成したテンプレートは次のような構成になっています。pub packageに則した構成になっています。
.
├── CHANGELOG.md
├── README.md
├── analysis_options.yaml
├── bin
│ └── main.dart
├── lib
│ └── dart_cli_sample.dart
├── pubspec.yaml
└── test
└── dart_cli_sample_test.dart
dartファイルを見ていきます。
- bin/main.dart
import 'package:dart_cli_sample/dart_cli_sample.dart' as dart_cli_sample; void main(List<String> arguments) { print('Hello world: ${dart_cli_sample.calculate()}!'); }
- lib/dart_cli_sample.dart
int calculate() { return 6 * 7; }
mainはbinにあり、実装はlibに置くような構成になっています。
テストも作成されます。
- test/dart_cli_sample_test.dart
import 'package:dart_cli_sample/dart_cli_sample.dart'; import 'package:test/test.dart'; void main() { test('calculate', () { expect(calculate(), 42); }); }
動かしてみる
初回は依存ライブラリのDLが必要です。
$ pub get
$ dart bin/main.dart
Hello world: 42!
この状態だと、Dartコードを実行しただけです。
Dartコードを変換してネイティブコードにします。
ネイティブコードの作成
dart2native
コマンドを実行して作成します。このコマンドはFlutterにbundleされているDartには含まれていません。
$ dart2native bin/main.dart -o main
Generated: /YOUR_PATH/dart-cli-sample/main
作成した main
を実行してみます。
$ ./main
Hello world: 42!
無事実行できました。
制約
どこでも実行可能なものができたと思いきや、現状はホストOS用のネイティブコードしかコンパイルされません。なので、macOS, Windows, Linuxとそれぞれでコンパイルしないとダメです。
現状の制約は辛いですが、その辛さを和らげる方法があります。
GitHub Actions
GitHub Actionsでは、マトリクスビルドをサポートしています。これを利用して、OSごとに実行してそれぞれに対応したネイティブコードを作成します。
コードの修正
各OSで動かしていることがわかるように、コードを変更します。
- bin/main.dart
import 'dart:io'; import 'package:dart_cli_sample/dart_cli_sample.dart' as dart_cli_sample; void main(List<String> arguments) { stdout.writeln('Hello ${dart_cli_sample.system()}!'); exitCode = 0; }
- lib/dart_cli_sample.dart
import 'dart:io'; String system() { return Platform.operatingSystem; }
動作環境のOS名を返すコードです。
Actionを定義
.github/workflows/
にyamlファイルを置きます。
先に完成したものを貼っておきます。
name: Cross compile on: [push] jobs: build: name: Compile runs-on: ${{ matrix.os }} strategy: fail-fast: false # https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/virtual-environments-for-github-hosted-runners#supported-runners-and-hardware-resources matrix: os: [windows-latest, ubuntu-latest, macos-latest] include: - os: windows-latest file-name: windows.exe - os: ubuntu-latest file-name: ubuntu - os: macos-latest file-name: macos steps: - name: Checkout uses: actions/checkout@v1 # https://dart.dev/get-dart - name: Install Dart(windows) if: matrix.os == 'windows-latest' run: | choco install dart-sdk - name: Install Dart(ubuntu) if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install apt-transport-https sudo sh -c 'wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' sudo sh -c 'wget -qO- https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list' sudo apt-get update sudo apt-get install dart - name: Install Dart(macos) if: matrix.os == 'macos-latest' run: | brew tap dart-lang/dart brew install dart - name: Build(windows) if: matrix.os == 'windows-latest' run: | $env:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.." Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" refreshenv pub get dart2native bin/main.dart -o bin/${{ matrix.file-name }} - name: Build(ubuntu) if: matrix.os == 'ubuntu-latest' run: | echo 'export PATH="$PATH:/usr/lib/dart/bin"' >> ~/.profile source ~/.profile pub get dart2native bin/main.dart -o bin/${{ matrix.file-name }} - name: Build(macos) if: matrix.os == 'macos-latest' run: | pub get dart2native bin/main.dart -o bin/${{ matrix.file-name }} - name: Upload artifact uses: actions/upload-artifact@v1 with: name: bin path: bin execute: name: Run artifact needs: build runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [windows-latest, ubuntu-latest, macos-latest] include: - os: windows-latest file-name: windows.exe - os: ubuntu-latest file-name: ubuntu - os: macos-latest file-name: macos steps: - name: Download artifact uses: actions/download-artifact@v1 with: name: bin - name: Run(windows) if: matrix.os == 'windows-latest' run: | cd bin .\${{ matrix.file-name }} - name: Run(ubuntu) if: matrix.os == 'ubuntu-latest' run: | cd bin chmod 755 ${{ matrix.file-name }} ./${{ matrix.file-name }} - name: Run(macos) if: matrix.os == 'macos-latest' run: | cd bin chmod 755 ${{ matrix.file-name }} ./${{ matrix.file-name }}
Dartをインストール
ホストOSで実行させたいので、JavaScriptアクションを使います。MarketPlaceにDartのJavaScriptアクションが見当たらなかったので、愚直にDartをインストールしました。
コンパイル
インストールしたDartへのPATHを通してコンパイルします。同一フォルダに出力してartifactとしたかったので、ファイル名をincludeでそれぞれ定義しています。
実行
それぞれのOSで生成したネイティブコードをそれぞれのOSで実行してみた結果です。
- windows
- ubuntu
- macos
しっかりOS名が出力されています。
手元でそれぞれ動かしてみたい方はこちらからartifactをDLできます。
各OSでしか実行できないことがわかると思います。
まとめ
テンプレート作成だったり、今回は触れませんでしたが引数をパースするライブラリも提供されており、DartでCLIツールは作りやすいです。
まだ制約もありますが、今回の様なやり方で制約を緩和することができます。
今回動作確認したリポジトリはこちらです。
参考
GitHub - dart-lang/stagehand: Dart project generator - web apps, console apps, servers, and more.
Write command-line apps | Dart