Dart的包管理命令解析-pub get

3,713 阅读10分钟

当我使用dart语言编写程序时,难免会需要用到一些依赖库,这些依赖库可以是远程开源的、也可以是本地的。flutter可以通过flutter pub getflutter pub upgrade命令来拉取,不过其他们本质还是dart语言的pub getpub upgrade。今天主要就是看看pub get这个命令。

1. dcat小应用测试

下面参考一个dart官网的demo:dcat

该程序的功效:是你指定一个文件,它将文件内容输出在控制台。

import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';

const lineNumber = 'line-number';

ArgResults argResults;

void main(List<String> arguments) {
  exitCode = 0; // presume success
  final parser = ArgParser()
    ..addFlag(lineNumber, negatable: false, abbr: 'n');

  argResults = parser.parse(arguments);
  final paths = argResults.rest;

  dcat(paths, argResults[lineNumber] as bool);
}

Future dcat(List<String> paths, bool showLineNumbers) async {
  if (paths.isEmpty) {
    // No files provided as arguments. Read from stdin and print each line.
    await stdin.pipe(stdout);
  } else {
    for (var path in paths) {
      var lineNumber = 1;
      final lines = utf8.decoder
          .bind(File(path).openRead())
          .transform(const LineSplitter());
      try {
        await for (var line in lines) {
          if (showLineNumbers) {
            stdout.write('${lineNumber++} ');
          }
          stdout.writeln(line);
        }
      } catch (_) {
        await _handleError(path);
      }
    }
  }
}

Future _handleError(String path) async {
  if (await FileSystemEntity.isDirectory(path)) {
    stderr.writeln('error: $path is a directory');
  } else {
    exitCode = 2;
  }
}

此时我们直接运行dart dcat.dart,会报错: 因为我们代码中使用了args/args.dart文件。但是编译时没有找到。此时我们就需要通过pub get来拉取args的源码。 直接执行pub get试试。 我们看到,会提示在当前目录下找不到pubspec.yaml文件。

我们手动加一个pubspec.yaml文件,指定args版本。

  1 name : dcat
  2
  3 environment :
  4   sdk : '>=2.6.0 <3.0.0'
  5
  6 dependencies :
  7   args : ^1.5.0

**重点:**我们看下当前的目录结构:

现在我们执行 pub get来拉取依赖。 拉取成功后,我们发现当前目录下多了些文件:

我们先来运行下dcat.dart看看能不能运行成功了:dart dcat.dart -n pubspec.lock 我们直接打印刚生成的pubspec.lock文件:

通过拉取需要的依赖文件,我们现在能成功运行程序了。但是在这过程中我们发现,pub get会根据pubspec.yaml文件来拉取依赖,同时:如果是第一拉取,还会生成.package、pubspec.lock文件以及.dart_tool目录。


2. pub 的辅助文件

2.1 pubspec.lock

  1 # Generated by pub
  2 # See https://dart.dev/tools/pub/glossary#lockfile
  3 packages:
  4   args:
  5     dependency: "direct main"
  6     description:
  7       name: args
  8       url: "https://pub.flutter-io.cn"
  9     source: hosted
 10     version: "1.6.0"
 11 sdks:
 12   dart: ">=2.6.0 <3.0.0"

在开发时,我们自己编辑了个pubspec.yaml文件,为啥pub get又帮我们生成了一个pubspec.lock文件呢???

我们看到.pubspec中也定义了个args,不过他的版本是:1.6.0。这是因为我们在pubspec.yaml中指定args版本时,前面加了^符号,有这个符号在第一次拉取的时候就会拉取最新版本。

args : ^1.5.0

我们看到最新版本确实是1.6.0。

而其他参数 name和url其实就是真实路径。你可以发现这些东西拼接在一起有点像下面的网址:

pubspec.lock文件中描述的就是你各种依赖包的真实版本和下载地址。当然还有lock的作用,当你下次刷新项目时,pub get会根据lock中的描述去获取依赖包。即使有新的版本也不会跟新到。此时可以通过 pub upgrade来更新。

2.2 .package

看下之前例子生成的.package文件。

  1 # Generated by pub on 2020-08-02 21:40:40.461932.
  2 args:file:///Users/frc/.pub-cache/hosted/pub.flutter-io.cn/args-1.6.0/lib/
  3 dcat:lib/

看上面的内容,很容易理解。这个文件就是保存了:包名和包源码真实存储地址的映射关系。 比如说args这个包,他通过pub get拉取后,存储到了.pub-cache/hosted/pub.flutter-io.cn/args-1.6.0/lib/这个位置。所以我们在源码中引入args其实是指向了本地缓存的地址:比如

import 'package:args/args.dart';

其实就是

import 'file:///Users/frc/.pub-cache/hosted/pub.flutter-io.cn/args-1.6.0/lib/args.dart';

我们可以进入这个实际目录看下,确实是存在args.dart文件:

所以说:.package文件是一个包名和包具体存储地址的映射文件,通过定义的包名key可以找到真实的包源码,从而实现链接。

2.3 .dart_tool

.dart_tool是一个目录,里面有个package_config.json文件

  1 {
  2   "configVersion": 2,
  3   "packages": [
  4     {
  5       "name": "args",
  6       "rootUri": "file:///Users/frc/.pub-cache/hosted/pub.flutter-io.cn/args    -1.6.0",
  7       "packageUri": "lib/",
  8       "languageVersion": "2.3"
  9     },
 10     {
 11       "name": "dcat",
 12       "rootUri": "../",
 13       "packageUri": "lib/",
 14       "languageVersion": "2.6"
 15     }
 16   ],
 17   "generated": "2020-08-02T13:40:40.469361Z",
 18   "generator": "pub",
 19   "generatorVersion": "2.8.4"
 20 }
 


3. 源码分析

3.1 pub.dart.snapshot

我们可以先看下我们在执行pub get时会怎样呢?由于pub 其实属于dart的命令,所以我先看下我的dart的path路径是什么: 进去这个路径查看目录下有什么: ,可以猜测pub是个shell脚本 打开pub文件看下:


 #!/usr/bin/env bash
  2 # Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
  3 # for details. All rights reserved. Use of this source code is governed by a
  4 # BSD-style license that can be found in the LICENSE file.
  5
  6 # Run pub.dart on the Dart VM. This script assumes the Dart SDK's directory
  7 # structure.
  8
  9 function follow_links() {
 10   file="$1"
 11   while [ -h "$file" ]; do
 12     # On Mac OS, readlink -f doesn't work.
 13     file="$(readlink "$file")"
 14   done
 15   echo "$file"
 16 }
 17
 18 function array_contains() {
 19   local needle="$1"
 20   local element
 21   shift
 22   for element; do [ "$element" = "$needle" ] && return 0; done
 23   return 1
 24 }
 25
 26 # Unlike $0, $BASH_SOURCE points to the absolute path of this file.
 27 PROG_NAME="$(follow_links "$BASH_SOURCE")"
 28
 29 # Handle the case where dart-sdk/bin has been symlinked to.
 30 BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
 31
 32
 33 unset VM_OPTIONS
 34 declare -a VM_OPTIONS
 35
 36 # Allow extra VM options to be passed in through an environment variable.
 37 if [[ $DART_VM_OPTIONS ]]; then
 38   read -a OPTIONS <<< "$DART_VM_OPTIONS"
 39   VM_OPTIONS+=("${OPTIONS[@]}")
 40 fi
 41
 42 # Run the pub snapshot.
 43 DART="$BIN_DIR/dart"
 44 if array_contains "--no-preview-dart-2" "${VM_OPTIONS[@]}"; then
 45   echo "Pub no longer supports Dart 1"
 46   exit -1
 47 else
 48   SNAPSHOT="$BIN_DIR/snapshots/pub.dart.snapshot"
 49   exec "$DART" "${VM_OPTIONS[@]}" "$SNAPSHOT" "$@"
 50 fi

总结一下:pub get其实可以转换成dart 你的FlutterRoot目录/flutter/bin/cache/dart-sdk/bin/snapshots/pub.dart.snapshot get。 所以关键是这个pub.dart.snapshot文件了:

这里面应该是包含了所有pub相关的逻辑,不过pub的详细源码不在flutter源码中。需要通过这个地址去下载。 我通过git自己下载了一份:

我在pub/bin/目录下找到个pub.dart文件: 打开一看,错不了了,就是他了:

 1 // Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
  2 // for details. All rights reserved. Use of this source code is governed by a
  3 // BSD-style license that can be found in the LICENSE file.
  4
  5 import 'package:pub/src/command_runner.dart';
  6
  7 void main(List<String> arguments) {
  8   PubCommandRunner().run(arguments);
  9 }

根据以往经验xxxCommandRunner基本就是dart 命令的实现。

这里有个需要注意的:import 'package:pub/src/command_runner.dart';command_runner文件路径不是在pub/src/下。这个之前说过,应该是去找.package文件然后拿pub作为key获取value: 在文件结尾处果然有个pub:lib/ 所以command_runner.dart的路径应该是 你的dart源码路径/lib/src/

3.2 PubCommandRunner.dart


... 

class PubCommandRunner extends CommandRunner {
  ...
  
  PubCommandRunner()
      : super('pub', 'Pub is a package manager for Dart.',
            usageLineLength: lineLength) {
    argParser.addFlag('version', negatable: false, help: 'Print pub version.');
    argParser.addFlag('trace',
        help: 'Print debugging information when an error occurs.');
    argParser
        .addOption('verbosity', help: 'Control output verbosity.', allowed: [
      'error',
      'warning',
      'normal',
      'io',
      'solver',
      'all'
    ], allowedHelp: {
      'error': 'Show only errors.',
      'warning': 'Show only errors and warnings.',
      'normal': 'Show errors, warnings, and user messages.',
      'io': 'Also show IO operations.',
      'solver': 'Show steps during version resolution.',
      'all': 'Show all output including internal tracing messages.'
    });
    argParser.addFlag('verbose',
        abbr: 'v', negatable: false, help: 'Shortcut for "--verbosity=all".');

    addCommand(BuildCommand());
    addCommand(CacheCommand());
    addCommand(DepsCommand());
    addCommand(DowngradeCommand());
    addCommand(GlobalCommand());
    addCommand(GetCommand());
    addCommand(ListPackageDirsCommand());
    addCommand(LishCommand());
    addCommand(OutdatedCommand());
    addCommand(RunCommand());
    addCommand(ServeCommand());
    addCommand(UpgradeCommand());
    addCommand(UploaderCommand());
    addCommand(LogoutCommand());
    addCommand(VersionCommand());
  }
..

这里有个 addCommand(GetCommand());,可以猜测pub get的实现应该就在GetCommand:

3.3 GetCommand.dart

 路径:lib/src/command/get.dart
 
import 'dart:async';

import '../command.dart';
import '../log.dart' as log;
import '../solver.dart';

/// Handles the `get` pub command.
class GetCommand extends PubCommand {
  @override
  String get name => 'get';
  @override
  String get description => "Get the current package's dependencies.";
  @override
  String get invocation => 'pub get';
  @override
  String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-get';
  @override
  bool get isOffline => argResults['offline'];

  GetCommand() {
    argParser.addFlag('offline',
        help: 'Use cached packages instead of accessing the network.');

    argParser.addFlag('dry-run',
        abbr: 'n',
        negatable: false,
        help: "Report what dependencies would change but don't change any.");

    argParser.addFlag('precompile',
        help: 'Precompile executables in immediate dependencies.');

    argParser.addFlag('packages-dir', hide: true);
  }

  @override
  Future run() {
    if (argResults.wasParsed('packages-dir')) {
      log.warning(log.yellow(
          'The --packages-dir flag is no longer used and does nothing.'));
    }
    return entrypoint.acquireDependencies(SolveType.GET,
        dryRun: argResults['dry-run'], precompile: argResults['precompile']);
  }
}


从它的类注释:Handles the get pub command.也验证了我的猜测。确实是用来处理pub的get命令的。

而这个类的核心就在run函数中:

entrypoint.acquireDependencies(SolveType.GET,
        dryRun: argResults['dry-run'], precompile: argResults['precompile']);

3.4 Entrypoint.dart

文件地址:lib/src/entrypoint.dart

class Entrypoint {

...
  /// Gets all dependencies of the [root] package.
  ///
  /// Performs version resolution according to [SolveType].
  ///
  /// [useLatest], if provided, defines a list of packages that will be
  /// unlocked and forced to their latest versions. If [upgradeAll] is
  /// true, the previous lockfile is ignored and all packages are re-resolved
  /// from scratch. Otherwise, it will attempt to preserve the versions of all
  /// previously locked packages.
  ///
  /// Shows a report of the changes made relative to the previous lockfile. If
  /// this is an upgrade or downgrade, all transitive dependencies are shown in
  /// the report. Otherwise, only dependencies that were changed are shown. If
  /// [dryRun] is `true`, no physical changes are made.
  ///
  /// If [precompile] is `true` (the default), this snapshots dependencies'
  /// executables.
  ///
  /// Updates [lockFile] and [packageRoot] accordingly.
  Future acquireDependencies(SolveType type,
      {List<String> useLatest,
      bool dryRun = false,
      bool precompile = false}) async {
    var result = await log.progress(
      'Resolving dependencies',
      () => resolveVersions(
        type,
        cache,
        root,
        lockFile: lockFile,
        useLatest: useLatest,
      ),
    );

    // Log once about all overridden packages.
    if (warnAboutPreReleaseSdkOverrides && result.pubspecs != null) {
      var overriddenPackages = (result.pubspecs.values
              .where((pubspec) => pubspec.dartSdkWasOverridden)
              .map((pubspec) => pubspec.name)
              .toList()
                ..sort())
          .join(', ');
      if (overriddenPackages.isNotEmpty) {
        log.message(log.yellow(
            'Overriding the upper bound Dart SDK constraint to <=${sdk.version} '
            'for the following packages:\n\n$overriddenPackages\n\n'
            'To disable this you can set the PUB_ALLOW_PRERELEASE_SDK system '
            'environment variable to `false`, or you can silence this message '
            'by setting it to `quiet`.'));
      }
    }

    result.showReport(type);

    if (dryRun) {
      result.summarizeChanges(type, dryRun: dryRun);
      return;
    }

    await Future.wait(result.packages.map(_get));
    
    _saveLockFile(result);

    result.summarizeChanges(type, dryRun: dryRun);

    /// Build a package graph from the version solver results so we don't
    /// have to reload and reparse all the pubspecs.
    _packageGraph = PackageGraph.fromSolveResult(this, result);

    await writePackagesFiles();

    try {
      if (precompile) {
        await precompileExecutables(changed: result.changedPackages);
      } else {
        _deleteExecutableSnapshots(changed: result.changedPackages);
      }
    } catch (error, stackTrace) {
      // Just log exceptions here. Since the method is just about acquiring
      // dependencies, it shouldn't fail unless that fails.
      log.exception(error, stackTrace);
    }
  }

...

}

我们可以看到acquireDependencies是用来获取[root]包的所有依赖项。有意思的是acquireDependencies函数的第一个参数是SolveTypePerforms version resolution according to [SolveType].-->根据[SolveType]执行版本解析。而我们当前的值是SolveType.GET

3.4.1 SolveType.dart

文件地址:lib/src/solver/type.dart

class SolveType {
  ///尽可能少地更改锁定文件,以使其与pubspec保持一致。
  static const GET = SolveType._('get');

   ///将所有软件包或特定软件包升级到可能的最高版本,
  ///无论锁定文件如何。 
  static const UPGRADE = SolveType._('upgrade');

   ///将所有软件包或特定软件包降级到最低版本,
  ///无论锁定文件如何。
  static const DOWNGRADE = SolveType._('downgrade');

  final String _name;

  const SolveType._(this._name);

  @override
  String toString() => _name;
}

看上面的注释我可以大胆猜测pug upgrade执行的也是这个方法,只不过参数是SolveType.UPGRADE

文件路径:lib/src/command/upgrade.dart

...
/// Handles the `upgrade` pub command.
class UpgradeCommand extends PubCommand {
 ...
  @override
  Future run() async {
    if (argResults.wasParsed('packages-dir')) {
      log.warning(log.yellow(
          'The --packages-dir flag is no longer used and does nothing.'));
    }
    await entrypoint.acquireDependencies(SolveType.UPGRADE,
        useLatest: argResults.rest,
        dryRun: argResults['dry-run'],
        precompile: argResults['precompile']);

    if (isOffline) {
      log.warning('Warning: Upgrading when offline may not update you to the '
          'latest versions of your dependencies.');
    }
  }
}


果然,UpgradeCommand的核心也是EntryPoint的acquireDependencies函数,不过第一个参数是:SolveType.UPGRADE

3.4.2 获取所有依赖包的最加版本:VersionSolver

acquireDependencies函数的第一步:就是获取所有依赖包的最加版本,因为只有知道具体版本才能去准确加载。

 var result = await log.progress(
      'Resolving dependencies',
      () => resolveVersions(
        type,
        cache,
        root,
        lockFile: lockFile,
        useLatest: useLatest,
      ),
    );
    
///路径: lib/src/solver.dart    
Future<SolveResult> resolveVersions(
    SolveType type, SystemCache cache, Package root,
    {LockFile lockFile, Iterable<String> useLatest}) {
  return VersionSolver(
    type,
    cache,
    root,
    lockFile ?? LockFile.empty(),
    useLatest ?? const [],
  ).solve();
}    

最终会调VersionSolver的solve函数,这个获取最新版本低额逻辑有点复杂,想深入了解的可以查看这个地址:github.com/dart-lang/p… VersionSolver的地址是lib/src/solver/version_solver.dart。

我们可以直接看返回的对象SolveResult,这里面应该包含我们下载需要的所以信息。

3.4.3 下载依赖

acquireDependencies函数第二个核心步骤是:根据SolveResult对象信息,下载依赖包。

 await Future.wait(result.packages.map(_get));

我们看下packages是什么:

路径:lib/src/solver/result.dart

/// 版本解析成功的结果
class SolveResult {
 /// 可从根目录访问的每个软件包的具体软件包版本列表。
  final List<PackageId> packages;
  ...
  }

其中PackageId是我们每个包下载的详细信息:

路径: lib/src/package_name.dart


/// A reference to a specific version of a package.
///
/// A package ID contains enough information to correctly get the package.
///
/// It's possible for multiple distinct package IDs to point to different
/// packages that have identical contents. For example, the same package may be
/// available from multiple sources. As far as Pub is concerned, those packages
/// are different.
///
/// Note that a package ID's [description] field has a different structure than
/// the [PackageRef.description] or [PackageRange.description] fields for some
/// sources. For example, the `git` source adds revision information to the
/// description to ensure that the same ID always points to the same source.
class PackageId extends PackageName {
  /// The package's version.
  final Version version;

  /// Creates an ID for a package with the given [name], [source], [version],
  /// and [description].
  ///
  /// Since an ID's description is an implementation detail of its source, this
  /// should generally not be called outside of [Source] subclasses.
  PackageId(String name, Source source, this.version, description)
      : super._(name, source, description);

  /// Creates an ID for the given root package.
  PackageId.root(Package package)
      : version = package.version,
        super._(package.name, null, package.name);

  @override
  int get hashCode => super.hashCode ^ version.hashCode;

  @override
  bool operator ==(other) =>
      other is PackageId && samePackage(other) && other.version == version;

  /// Returns a [PackageRange] that allows only [version] of this package.
  PackageRange toRange() => withConstraint(version);

  @override
  String toString([PackageDetail detail]) {
    detail ??= PackageDetail.defaults;

    var buffer = StringBuffer(name);
    if (detail.showVersion ?? !isRoot) buffer.write(' $version');

    if (!isRoot && (detail.showSource ?? source is! HostedSource)) {
      buffer.write(' from $source');
      if (detail.showDescription) {
        buffer.write(' ${source.formatDescription(description)}');
      }
    }

    return buffer.toString();
  }
}

有了PackageId我们就可以执行_get函数了

	/// Makes sure the package at [id] is locally available.
  ///
  /// This automatically downloads the package to the system-wide cache as well
  /// if it requires network access to retrieve (specifically, if the package's
  /// source is a [CachedSource]).
  Future _get(PackageId id) {
    return http.withDependencyType(root.dependencyType(id.name), () async {
      if (id.isRoot) return;

      var source = cache.source(id.source);
      if (source is CachedSource) await source.downloadToSystemCache(id);
    });
  }


具体的下载执行是await source.downloadToSystemCache(id); 这里的source是CachedSource,但是CachedSource是抽象类,downloadToSystemCache函数也是抽象未实现的逻辑。所以我们要找到他的具体实现类。找了一圈就发现一个BoundGitSource是他的实现类。 所以说这里的downloadToSystemCache实际上是BoundGitSource的downloadToSystemCache实现逻辑。

路径: lib/src/source/git.dart

  /// Clones a Git repo to the local filesystem.
  ///
  /// The Git cache directory is a little idiosyncratic. At the top level, it
  /// contains a directory for each commit of each repository, named `<package
  /// name>-<commit hash>`. These are the canonical package directories that are
  /// linked to from the `packages/` directory.
  ///
  /// In addition, the Git system cache contains a subdirectory named `cache/`
  /// which contains a directory for each separate repository URL, named
  /// `<package name>-<url hash>`. These are used to check out the repository
  /// itself; each of the commit-specific directories are clones of a directory
  /// in `cache/`.
  @override
  Future<Package> downloadToSystemCache(PackageId id) async {
    return await _pool.withResource(() async {
      var ref = id.toRef();
      if (!git.isInstalled) {
        fail("Cannot get ${id.name} from Git (${ref.description['url']}).\n"
            'Please ensure Git is correctly installed.');
      }

      ensureDir(p.join(systemCacheRoot, 'cache'));
      await _ensureRevision(ref, id.description['resolved-ref']);

      var revisionCachePath = _revisionCachePath(id);
      await _revisionCacheClones.putIfAbsent(revisionCachePath, () async {
        if (!entryExists(revisionCachePath)) {
          await _clone(_repoCachePath(ref), revisionCachePath);
          await _checkOut(revisionCachePath, id.description['resolved-ref']);
          _writePackageList(revisionCachePath, [id.description['path']]);
        } else {
          _updatePackageList(revisionCachePath, id.description['path']);
        }
      });

      return Package.load(
          id.name,
          p.join(revisionCachePath, id.description['path']),
          systemCache.sources);
    });
  }

这里细节很多没法讲了,就看两步:

  • await _clone(_repoCachePath(ref), revisionCachePath);
  • await _checkOut(revisionCachePath, id.description['resolved-ref']);

熟悉git的同学都了解git clonegit checkout 。通过这两步可以下载git管理的源码,并切换到所需的位置。

可以看_clone和_checkout两个函数的实现都是依赖git.run

 Future _clone(String from, String to,
      {bool mirror = false, bool shallow = false}) {
    return Future.sync(() {
      // Git on Windows does not seem to automatically create the destination
      // directory.
      ensureDir(to);
      var args = ['clone', from, to];

      if (mirror) args.insert(1, '--mirror');
      if (shallow) args.insertAll(1, ['--depth', '1']);

      return git.run(args);
    }).then((result) => null);
  }

  /// Checks out the reference [ref] in [repoPath].
  Future _checkOut(String repoPath, String ref) {
    return git
        .run(['checkout', ref], workingDir: repoPath).then((result) => null);
  }

3.4.4 编辑pubspec.lock文件

下载完包之后会生成或者修改pubspec.lock文件: _saveLockFile(result);

路径: lib/src/entrypoint.dart


  ///将具体的软件包版本列表保存到`pubspec.lock`文件中。
  ///如果存在`pubspec.lock`,将使用Windows行尾(`\ r \ n`),并使用它。
  void _saveLockFile(SolveResult result) {
    _lockFile = result.lockFile;

    final windowsLineEndings = fileExists(lockFilePath) &&
        detectWindowsLineEndings(readTextFile(lockFilePath));

    final serialized = _lockFile.serialize(root.dir);
    writeTextFile(lockFilePath,
        windowsLineEndings ? serialized.replaceAll('\n', '\r\n') : serialized);
  }


3.4.5编辑.packages和.dart_tool/package_config.json

await writePackagesFiles();

 /// Writes .packages and .dart_tool/package_config.json
  Future<void> writePackagesFiles() async {
    writeTextFile(packagesFile, lockFile.packagesFile(cache, root.name));
    ensureDir(p.dirname(packageConfigFile));
    writeTextFile(
        packageConfigFile,
        await lockFile.packageConfigFile(
          cache,
          entrypoint: root.name,
          entrypointSdkConstraint: root.pubspec.sdkConstraints[sdk.identifier],
        ));
  }

至此pub get的流程基本走完,其中细节太多。感兴趣的需要自己慢慢啃源码。