当我使用dart语言编写程序时,难免会需要用到一些依赖库,这些依赖库可以是远程开源的、也可以是本地的。flutter可以通过
flutter pub get和flutter pub upgrade命令来拉取,不过其他们本质还是dart语言的pub get和pub 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函数的第一个参数是SolveType:Performs 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 clone和git 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的流程基本走完,其中细节太多。感兴趣的需要自己慢慢啃源码。