美文网首页
2023-02-14 flutter pub publish核心

2023-02-14 flutter pub publish核心

作者: 我是小胡胡分胡 | 来源:发表于2023-02-13 15:30 被阅读0次

    acquireDependencies->_validate->_publish ->_authorize

    1. 解析 pubspec.yaml 文件,获取包名和版本号,并检查包名和版本号的合法性。

    2. 构建上传包的压缩文件,包括 lib、bin、example 等文件夹中的文件以及 pubspec.yaml 文件。

    3. 对上传包进行验证,包括检查上传包的大小、检查是否包含非法字符等。

    4. 通过 OAuth2 或 Bearer 身份验证机制登录 pub.dev 等服务器。

    5. 上传压缩文件到服务器,并等待服务器响应结果。

    6. 如果上传成功,更新本地缓存并显示上传成功的信息;如果上传失败,抛出相应的异常并显示上传失败的信息。

    上述步骤都是通过调用 pub 包中的函数实现的。而 flutter pub publish 命令本身只是一个包装器,它调用 pub 包中的函数来完成实际的操作。

    Resolving dependencies

    下面一段代码实现了 Pub 的核心功能之一:解析依赖关系。主要流程如下:

    • 从 pubspec.yaml 中读取当前 package 的信息,并检查其对 Dart SDK 的约束;
      根据指定的解析类型(如 get 或 upgrade),解析当前 package 依赖的所有 packages 的版本信息;
    • 根据 pubspec.lock 文件,检查当前 package 是否已经锁定某些 packages 的版本号,如果是,则保持不变,否则更新版本号;
    • 根据解析结果,将 packages 下载到本地缓存;
    • 如果需要,生成新的 pubspec.lock 文件,并显示出解析报告;
    • 根据 --dry-run 和 --enforce-lockfile 等参数,更新 pubspec.lock 文件和 package graph,快照可执行文件等。
    /// 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);
          }
        }
      } 
    

    主流程

    下面的代码是一个异步函数 runProtected(),它的执行流程如下:

    • 检查命令行参数 --server 是否已解析,如果已解析,则输出一条警告信息,表明此选项已过时,应该使用 pubspec.yaml 文件中的 publish_to 字段或设置 $PUB_HOSTED_URL 环境变量代替。

    • 检查命令行参数 --force 和 --dry-run 是否同时存在,如果是,则抛出一个使用异常,表示这两个选项不能同时使用。

    • 检查当前包是否为私有包,如果是,则输出一条错误信息,表示私有包不能被发布,应该通过更改 pubspec.yaml 文件中的 publish_to 字段来启用。

    • 调用 entrypoint.acquireDependencies() 函数获取包依赖项。

    • 获取当前包的文件列表,打印一条消息,说明正在打包和发布当前包,并展示包含的文件列表。

    • 创建并压缩当前包的文件,并获取压缩包的字节数组。

    • 验证当前包是否有效,如果无效,则设置退出码并返回,否则继续执行下一步。

    • 如果是 --dry-run 模式,则打印一条消息,表示服务器可能会执行额外的检查,然后返回,否则继续执行下一步。

    • 调用 _publish() 函数,将压缩包上传到指定的服务器,并等待上传完成。

    @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函数

    • 创建一个包含官方 pub 服务器地址和测试用的本地服务器地址的集合。

    • 判断当前服务器地址是否属于官方 pub 服务器地址集合中的地址,如果是,则检查本地缓存中是否有对应服务器地址的令牌,如果没有,则使用 OAuth2 身份验证客户端(oauth2.withClient)来进行身份验证,并调用 _publishUsingClient() 函数上传压缩包;如果有,则使用 Bearer 身份验证客户端(withAuthenticatedClient)来进行身份验证,并调用 _publishUsingClient() 函数上传压缩包。

    • 如果当前服务器地址不属于官方 pub 服务器地址集合中的地址,则使用 Bearer 身份验证客户端来进行身份验证,并调用 _publishUsingClient() 函数上传压缩包。

    • 如果上传过程中出现 PubHttpResponseException 异常,则获取请求的 URL,判断该 URL 是否与当前服务器地址相同,如果相同,则调用 handleJsonError() 函数处理错误;如果不同,则将异常抛出。

    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 函数:

    • 创建一个AuthorizationCodeGrant对象,该对象代表了使用OAuth2协议的授权码授权流程。

    • 使用AuthorizationCodeGrant对象的getAuthorizationUrl方法获取授权URL,该URL将重定向到pub.dev的授权页面,以便用户授权Pub作为客户端访问pub.dev的API。

    • 启动一个本地的HTTP服务器,并使用该URL响应任何传入的HTTP请求。服务器将绑定到本地地址localhost和随机端口,以接收来自pub.dev授权服务器的响应。

    • 在浏览器中打开授权URL,用户将在pub.dev网站上看到授权请求。用户需要登录并授权Pub访问API。

    • 授权服务器将重定向到之前启动的本地HTTP服务器,并将授权码作为查询参数传递。HTTP服务器收到授权码后,使用Completer将授权码传递给Future。

    • 使用授权码调用AuthorizationCodeGrant对象的handleAuthorizationResponse方法来获取访问令牌。

    • 本地HTTP服务器响应一个重定向,将用户重定向回pub.dev/authorized,以便用户了解授权已成功完成。

    • 关闭本地HTTP服务器,并返回已授权的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;
    }
    
    

    相关文章

      网友评论

          本文标题:2023-02-14 flutter pub publish核心

          本文链接:https://www.haomeiwen.com/subject/sqtskdtx.html