acquireDependencies->_validate->_publish ->_authorize
解析 pubspec.yaml 文件,獲取包名和版本號,并檢查包名和版本號的合法性。
構(gòu)建上傳包的壓縮文件,包括 lib、bin、example 等文件夾中的文件以及 pubspec.yaml 文件。
對上傳包進行驗證,包括檢查上傳包的大小、檢查是否包含非法字符等。
通過 OAuth2 或 Bearer 身份驗證機制登錄 pub.dev 等服務(wù)器。
上傳壓縮文件到服務(wù)器,并等待服務(wù)器響應(yīng)結(jié)果。
如果上傳成功,更新本地緩存并顯示上傳成功的信息;如果上傳失敗,拋出相應(yīng)的異常并顯示上傳失敗的信息。
上述步驟都是通過調(diào)用 pub 包中的函數(shù)實現(xiàn)的。而 flutter pub publish 命令本身只是一個包裝器,它調(diào)用 pub 包中的函數(shù)來完成實際的操作。
Resolving dependencies
下面一段代碼實現(xiàn)了 Pub 的核心功能之一:解析依賴關(guān)系。主要流程如下:
- 從 pubspec.yaml 中讀取當前 package 的信息,并檢查其對 Dart SDK 的約束;
根據(jù)指定的解析類型(如 get 或 upgrade),解析當前 package 依賴的所有 packages 的版本信息; - 根據(jù) pubspec.lock 文件,檢查當前 package 是否已經(jīng)鎖定某些 packages 的版本號,如果是,則保持不變,否則更新版本號;
- 根據(jù)解析結(jié)果,將 packages 下載到本地緩存;
- 如果需要,生成新的 pubspec.lock 文件,并顯示出解析報告;
- 根據(jù) --dry-run 和 --enforce-lockfile 等參數(shù),更新 pubspec.lock 文件和 package graph,快照可執(zhí)行文件等。
/// Gets all dependencies of the [root] package.
///
/// Performs version resolution according to [SolveType].
///
/// The iterable [unlock] specifies the list of packages whose versions can be
/// changed even if they are locked in the pubspec.lock file.
///
/// [analytics] holds the information needed for the embedded pub command to
/// send analytics.
///
/// 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.
///
/// if [summaryOnly] is `true` only success or failure will be
/// shown --- in case of failure, a reproduction command is shown.
///
/// Updates [lockFile] and [packageRoot] accordingly.
///
/// If [enforceLockfile] is true no changes to the current lockfile are
/// allowed. Instead the existing lockfile is loaded, verified against
/// pubspec.yaml and all dependencies downloaded.
Future<void> acquireDependencies(
SolveType type, {
Iterable<String>? unlock,
bool dryRun = false,
bool precompile = false,
required PubAnalytics? analytics,
bool summaryOnly = false,
bool enforceLockfile = false,
}) async {
summaryOnly = summaryOnly || _summaryOnlyEnvironment;
final suffix = root.isInMemory || root.dir == '.' ? '' : ' in ${root.dir}';
String forDetails() {
if (!summaryOnly) return '';
final enforceLockfileOption =
enforceLockfile ? ' --enforce-lockfile' : '';
final directoryOption =
root.isInMemory || root.dir == '.' ? '' : ' --directory ${root.dir}';
return ' For details run `$topLevelProgram pub ${type.toString()}$directoryOption$enforceLockfileOption`';
}
if (enforceLockfile && !fileExists(lockFilePath)) {
throw ApplicationException('''
Retrieving dependencies failed$suffix.
Cannot do `--enforce-lockfile` without an existing `pubspec.lock`.
Try running `$topLevelProgram pub get` to create `$lockFilePath`.''');
}
SolveResult result;
try {
result = await log.progress('Resolving dependencies$suffix', () async {
_checkSdkConstraint(root.pubspec);
return resolveVersions(
type,
cache,
root,
lockFile: lockFile,
unlock: unlock ?? [],
);
});
} catch (e) {
if (summaryOnly && (e is ApplicationException)) {
throw ApplicationException(
'Resolving dependencies$suffix failed.${forDetails()}',
);
} else {
rethrow;
}
}
// We have to download files also with --dry-run to ensure we know the
// archive hashes for downloaded files.
final newLockFile = await result.downloadCachedPackages(cache);
final report = SolveReport(
type,
root,
lockFile,
newLockFile,
result.availableVersions,
cache,
dryRun: dryRun,
enforceLockfile: enforceLockfile,
quiet: summaryOnly,
);
final hasChanges = await report.show();
await report.summarize();
if (enforceLockfile && hasChanges) {
var suggestion = summaryOnly
? ''
: '''
\n\nTo update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
`--enforce-lockfile`.''';
dataError('''
Unable to satisfy `$pubspecPath` using `$lockFilePath`$suffix.${forDetails()}$suggestion''');
}
if (!(dryRun || enforceLockfile)) {
newLockFile.writeToFile(lockFilePath, cache);
}
_lockFile = newLockFile;
if (!dryRun) {
if (analytics != null) {
result.sendAnalytics(analytics);
}
/// 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 writePackageConfigFile();
try {
if (precompile) {
await precompileExecutables();
} 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);
}
}
}
主流程
下面的代碼是一個異步函數(shù) runProtected(),它的執(zhí)行流程如下:
檢查命令行參數(shù) --server 是否已解析,如果已解析,則輸出一條警告信息,表明此選項已過時,應(yīng)該使用 pubspec.yaml 文件中的 publish_to 字段或設(shè)置 $PUB_HOSTED_URL 環(huán)境變量代替。
檢查命令行參數(shù) --force 和 --dry-run 是否同時存在,如果是,則拋出一個使用異常,表示這兩個選項不能同時使用。
檢查當前包是否為私有包,如果是,則輸出一條錯誤信息,表示私有包不能被發(fā)布,應(yīng)該通過更改 pubspec.yaml 文件中的 publish_to 字段來啟用。
調(diào)用 entrypoint.acquireDependencies() 函數(shù)獲取包依賴項。
獲取當前包的文件列表,打印一條消息,說明正在打包和發(fā)布當前包,并展示包含的文件列表。
創(chuàng)建并壓縮當前包的文件,并獲取壓縮包的字節(jié)數(shù)組。
驗證當前包是否有效,如果無效,則設(shè)置退出碼并返回,否則繼續(xù)執(zhí)行下一步。
如果是 --dry-run 模式,則打印一條消息,表示服務(wù)器可能會執(zhí)行額外的檢查,然后返回,否則繼續(xù)執(zhí)行下一步。
調(diào)用 _publish() 函數(shù),將壓縮包上傳到指定的服務(wù)器,并等待上傳完成。
@override
Future runProtected() async {
if (argResults.wasParsed('server')) {
await log.errorsOnlyUnlessTerminal(() {
log.message(
'''
The --server option is deprecated. Use `publish_to` in your pubspec.yaml or set
the \$PUB_HOSTED_URL environment variable.''',
);
});
}
if (force && dryRun) {
usageException('Cannot use both --force and --dry-run.');
}
if (entrypoint.root.pubspec.isPrivate) {
dataError('A private package cannot be published.\n'
'You can enable this by changing the "publish_to" field in your '
'pubspec.');
}
await entrypoint.acquireDependencies(SolveType.get, analytics: analytics);
var files = entrypoint.root.listFiles();
log.fine('Archiving and publishing ${entrypoint.root.name}.');
// Show the package contents so the user can verify they look OK.
var package = entrypoint.root;
log.message(
'Publishing ${package.name} ${package.version} to $host:\n'
'${tree.fromFiles(files, baseDir: entrypoint.root.dir, showFileSizes: true)}',
);
var packageBytesFuture =
createTarGz(files, baseDir: entrypoint.root.dir).toBytes();
// Validate the package.
var isValid = await _validate(
packageBytesFuture.then((bytes) => bytes.length),
files,
);
if (!isValid) {
overrideExitCode(exit_codes.DATA);
return;
} else if (dryRun) {
log.message('The server may enforce additional checks.');
return;
} else {
await _publish(await packageBytesFuture);
}
}
_publish函數(shù)
創(chuàng)建一個包含官方 pub 服務(wù)器地址和測試用的本地服務(wù)器地址的集合。
判斷當前服務(wù)器地址是否屬于官方 pub 服務(wù)器地址集合中的地址,如果是,則檢查本地緩存中是否有對應(yīng)服務(wù)器地址的令牌,如果沒有,則使用 OAuth2 身份驗證客戶端(oauth2.withClient)來進行身份驗證,并調(diào)用 _publishUsingClient() 函數(shù)上傳壓縮包;如果有,則使用 Bearer 身份驗證客戶端(withAuthenticatedClient)來進行身份驗證,并調(diào)用 _publishUsingClient() 函數(shù)上傳壓縮包。
如果當前服務(wù)器地址不屬于官方 pub 服務(wù)器地址集合中的地址,則使用 Bearer 身份驗證客戶端來進行身份驗證,并調(diào)用 _publishUsingClient() 函數(shù)上傳壓縮包。
如果上傳過程中出現(xiàn) PubHttpResponseException 異常,則獲取請求的 URL,判斷該 URL 是否與當前服務(wù)器地址相同,如果相同,則調(diào)用 handleJsonError() 函數(shù)處理錯誤;如果不同,則將異常拋出。
Future<void> _publish(List<int> packageBytes) async {
try {
final officialPubServers = {
'https://pub.dev',
// [validateAndNormalizeHostedUrl] normalizes https://pub.dartlang.org
// to https://pub.dev, so we don't need to do allow that here.
// Pub uses oauth2 credentials only for authenticating official pub
// servers for security purposes (to not expose pub.dev access token to
// 3rd party servers).
// For testing publish command we're using mock servers hosted on
// localhost address which is not a known pub server address. So we
// explicitly have to define mock servers as official server to test
// publish command with oauth2 credentials.
if (runningFromTest &&
Platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL'))
Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'],
};
// Using OAuth2 authentication client for the official pub servers
final isOfficialServer = officialPubServers.contains(host.toString());
if (isOfficialServer && !cache.tokenStore.hasCredential(host)) {
// Using OAuth2 authentication client for the official pub servers, when
// we don't have an explicit token from [TokenStore] to use instead.
//
// This allows us to use `dart pub token add` to inject a token for use
// with the official servers.
await oauth2.withClient(cache, (client) {
return _publishUsingClient(packageBytes, client);
});
} else {
// For third party servers using bearer authentication client
await withAuthenticatedClient(cache, host, (client) {
return _publishUsingClient(packageBytes, client);
});
}
} on PubHttpResponseException catch (error) {
var url = error.response.request!.url;
if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
handleJsonError(error.response);
} else {
rethrow;
}
}
}
_authorize 函數(shù):
創(chuàng)建一個AuthorizationCodeGrant對象,該對象代表了使用OAuth2協(xié)議的授權(quán)碼授權(quán)流程。
使用AuthorizationCodeGrant對象的getAuthorizationUrl方法獲取授權(quán)URL,該URL將重定向到pub.dev的授權(quán)頁面,以便用戶授權(quán)Pub作為客戶端訪問pub.dev的API。
啟動一個本地的HTTP服務(wù)器,并使用該URL響應(yīng)任何傳入的HTTP請求。服務(wù)器將綁定到本地地址localhost和隨機端口,以接收來自pub.dev授權(quán)服務(wù)器的響應(yīng)。
在瀏覽器中打開授權(quán)URL,用戶將在pub.dev網(wǎng)站上看到授權(quán)請求。用戶需要登錄并授權(quán)Pub訪問API。
授權(quán)服務(wù)器將重定向到之前啟動的本地HTTP服務(wù)器,并將授權(quán)碼作為查詢參數(shù)傳遞。HTTP服務(wù)器收到授權(quán)碼后,使用Completer將授權(quán)碼傳遞給Future。
使用授權(quán)碼調(diào)用AuthorizationCodeGrant對象的handleAuthorizationResponse方法來獲取訪問令牌。
本地HTTP服務(wù)器響應(yīng)一個重定向,將用戶重定向回pub.dev/authorized,以便用戶了解授權(quán)已成功完成。
關(guān)閉本地HTTP服務(wù)器,并返回已授權(quán)的HTTP Client對象,以便Pub可以使用OAuth2訪問pub.dev API。
/// Gets the user to authorize pub as a client of pub.dev via oauth2.
///
/// Returns a Future that completes to a fully-authorized [Client].
Future<Client> _authorize() async {
var grant = AuthorizationCodeGrant(
_identifier, _authorizationEndpoint, tokenEndpoint,
secret: _secret,
// Google's OAuth2 API doesn't support basic auth.
basicAuth: false,
httpClient: _retryHttpClient,
);
// Spin up a one-shot HTTP server to receive the authorization code from the
// Google OAuth2 server via redirect. This server will close itself as soon as
// the code is received.
var completer = Completer();
var server = await bindServer('localhost', 0);
shelf_io.serveRequests(server, (request) {
if (request.url.path.isNotEmpty) {
return shelf.Response.notFound('Invalid URI.');
}
log.message('Authorization received, processing...');
var queryString = request.url.query;
// Closing the server here is safe, since it will wait until the response
// is sent to actually shut down.
server.close();
completer
.complete(grant.handleAuthorizationResponse(queryToMap(queryString)));
return shelf.Response.found('https://pub.dev/authorized');
});
var authUrl = grant.getAuthorizationUrl(
Uri.parse('http://localhost:${server.port}'),
scopes: _scopes,
);
log.message(
'Pub needs your authorization to upload packages on your behalf.\n'
'In a web browser, go to $authUrl\n'
'Then click "Allow access".\n\n'
'Waiting for your authorization...');
var client = await completer.future;
log.message('Successfully authorized.\n');
return client;
}