pub global activateでインストールしたDartコマンドは起動が速いね、snapshot起動だからね

AngularDartで開発していた際、ふと`webdev`コマンドが（*.dartのソースコードではなく）pre-compileされた`snapshot`としてインストールされている事に気付きました。
ちょうど、「Dart2になってから(DartVM用の)コマンド起動ちょっと遅くなったし、大きいパッケージをimportすると更に遅いな・・」と思っていたところだったので、調べてみました。

なおDartのsnapshotの話は、@takyamさんの以下の翻訳記事が参考になります。
https://qiita.com/takyam/items/99ee4500f01fa7cd1d27

## pubやgit(hub)からActivateしたDartコマンドはsnapshotにpre-compileされる

例えばですが、前述の`webdev`コマンドは`pub global activate webdev`を実行することでActivate（インストール/アップデート）します。
Activateの際には、パッケージの取得元を`-s`(`--source`)で指定することが出来ます。

```
~$ pub global activate -h
Make a package's executables globally available.

Usage: pub global activate <package...>
-h, --help              Print this usage information.
-s, --source            The source used to find the package.
                        [git, hosted (default), path] ★ ココ
(以下略)
```

詳細は https://www.dartlang.org/tools/pub/cmd/pub-global#activating-a-package を参照下さい。
取得元として、`hosted`（デフォルト）、`git`、`path`が指定できます。

さて本節の題名の通り、`pub global activate`を行うと、executableなDartパッケージはpre-compileされたsnapshotとしてインストールされます（なお後述しますが、**path指定ではpre-compileしてくれません**）。
ちょっと試してみましょう。

### helloコマンドを作成する

```dart:bin/hello.dart
void main(List<String> args) {
  print('hello.');
}
```

```yaml:pubspec.yaml
name: hello_dart
description: A sample command-line application.
version: 0.0.1
environment:
  sdk: '>=2.1.0 <3.0.0'
executables: # ★ ココが重要
  hello:
```

上記の様な、簡単なDartVM用コマンドを作成します（`pubspec.yaml`に`executables:`とコマンド名を書きます）。
で、GitHubの「hello_dart」リポジトリとしてアップします。

### helloコマンドをGitHubからActivteする

次に、sourceを`git`指定でActivateします。

```shell
~$ pub global activate -s git git@github.com:takutaro/hello_dart.git
Resolving dependencies... (2.4s)
+ hello_dart 0.0.1 from git git@github.com:takutaro/hello_dart.git at 7f70b3
Precompiling executables... 
Precompiled hello_dart:hello. ★ pub global activateのタイミングで事前コンパイルされる！
Installed executable hello.
Activated hello_dart 0.0.1 from Git repository "git@github.com:takutaro/hello_dart.git".
```

```shell
~$ tree -a .pub-cache/bin/
.pub-cache/bin/
└── hello
```

`~/.pub-cache/bin`配下にhelloコマンドがインストールされました（予め`~/.pub-cache/bin`にはパスが通っている前提です）。
`Precompiled hello_dart:hello`の出力も見えます。
では`hello`コマンドの中身を覗いてみます。

### コマンド（スクリプト）の中身

中身は以下の様になっています。
snapshotが存在する場合はそれを利用していますね。これでほぼノータイムでDartコマンドが起動できます！

```shell:~/.pub-cache/bin/hello 
#!/usr/bin/env sh
# This file was created by pub v2.2.0-dev.0.0.
# Package: hello_dart
# Version: 0.0.1
# Executable: hello
# Script: hello

# ★ snapshotが存在するので、snapshotで起動。速い！
dart "/home/taku/.pub-cache/global_packages/hello_dart/bin/hello.dart.snapshot.dart2" "$@"

# The VM exits with code 253 if the snapshot version is out-of-date.
# If it is, we need to delete it and run "pub global" manually.
exit_code=$?
if [ $exit_code != 253 ]; then
  exit $exit_code
fi

# ★ snapshotが古かったりした場合は、それを使わずに起動
pub global run hello_dart:hello "$@"
```

ちなみにディレクトリ/ファイル構成は以下のようになっていますね。

```sh
.pub-cache/global_packages/hello_dart/
├── .packages
├── bin
│   └── hello.dart.snapshot.dart2
└── pubspec.lock

.pub-cache/git/hello_dart-7f70b3b25b4895dbef84910ba1781be5907a236f/bin
└── hello.dart
```

## なぜpath指定ではsnapshotにpre-compileしてくれないのか？

分かりません。ローカルにあるsourceはいつ消えてもおかしくないので、信用できないという事ですかね？

```dart:global_packages.dart
class GlobalPackages {
  final SystemCache cache;
  String get _directory => p.join(cache.rootDir, "global_packages");
  String get _binStubDir => p.join(cache.rootDir, "bin");

  GlobalPackages(this.cache);

  Future activateGit(String repo, List<String> executables,
      {Map<String, FeatureDependency> features, bool overwriteBinStubs}) async {
    var name = await cache.git.getPackageNameFromRepo(repo);
    _describeActive(name);
    await _installInCache( // ★ snapshotに事前コンパイルされる
        cache.git.source
            .refFor(name, repo)
            .withConstraint(VersionConstraint.any)
            .withFeatures(features ?? const {}),
        executables,
        overwriteBinStubs: overwriteBinStubs);
  }

  Future activateHosted(
      String name, VersionConstraint constraint, List<String> executables,
      {Map<String, FeatureDependency> features, bool overwriteBinStubs}) async {
    _describeActive(name);
    await _installInCache( // ★ 〃
        cache.hosted.source
            .refFor(name)
            .withConstraint(constraint)
            .withFeatures(features ?? const {}),
        executables,
        overwriteBinStubs: overwriteBinStubs);
  }

  Future activatePath(String path, List<String> executables,
      {bool overwriteBinStubs}) async {
    // ★ ローカルパス指定でのactivateではsnapshotを作ってくれない
    var entrypoint = Entrypoint(path, cache);
    await entrypoint.acquireDependencies(SolveType.GET);
    var name = entrypoint.root.name;
    _describeActive(name);
    var fullPath = canonicalize(entrypoint.root.dir);
    var id = cache.path.source.idFor(name, entrypoint.root.version, fullPath);
    _writeLockFile(name, LockFile([id]));
    var binDir = p.join(_directory, name, 'bin');
    if (dirExists(binDir)) deleteEntry(binDir);
    _updateBinStubs(entrypoint.root, executables,
        overwriteBinStubs: overwriteBinStubs);
    log.message('Activated ${_formatPackage(id)}.');
  }

```

## おまけ

ActivateでインストールしたDartコマンドのアップデートって面倒（＆忘れがち）ですよね。
問答無用で一気にアップデートするスクリプトを載せておきます。

```bash:pub_activate.sh
#!/usr/bin/env bash

pub global list | awk '{print $1}' | xargs -n1 pub global activate
```