美文网首页
修复 pod install 无法正确拉取 pod 代码

修复 pod install 无法正确拉取 pod 代码

作者: 大宝来巡山 | 来源:发表于2022-04-12 15:38 被阅读0次

    发现问题
    如题,在 Podfile 里将 pod 指向了某个版本,但 pod install 却无法正确的下载该版本号的代码。这个问题是最近在打包机上遇到的,Jenkins 打包提交给测试的时候偶尔会出现提交的代码未应用,或者在本地确认无误的代码在打包过程中 xcodebuild 直接编译失败。

    因为场景比较特殊所以很快发现了问题。一开始我们在 Podfile 里将 PodA 指向了版本号 1.0.0,如:

    Pod 'PodA', '1.0.0'
    

    提测过程中,我们从 1.0.0 tag 处拉取了分支 branch_a,并且在该分支上提交了 commitA。然后在 Podfile 里将 PodA 从版本号 1.0.0 改向了分支 branch_a,如:

    Pod 'PodA', :git => 'https://xyz.com/PodA.git', :branch => 'branch_a'
    

    打包提交给测试之后,因为需求变更,在分支 branch_a 上的改动不需要了,所以又将 Podfile 里的 PodA 从分支改回了版本号 1.0.0,如:

    Pod 'PodA', '1.0.0'
    

    这时再打包就发现 Pods 文件夹下的 PodA 的代码依然包含了分支 branch_a 上的 commitA,并未更改为 1.0.0 tag 的代码。

    通过查看 CocoaPods 源码发现,在 pod install 过程中,CocoaPods 会根据 Pods/Manifest.lock 文件比对 pod 是否需要重新下载。从版本号改向分支的时候,CocoaPods 有个 predownloaded 的判读,重新下载了 pod 代码;然而从分支改向版本号的时候,对比了 pod 版本号、名称、checksum 都没变所以代码未更新。以下是 CocoaPods 1.10.2 版本的判断代码:

    def pod_changed?(pod)
      spec = root_spec(pod)
      return true if spec.version != sandbox_version(pod)
      return true if spec.checksum != sandbox_checksum(pod)
      return true if resolved_spec_names(pod) != sandbox_spec_names(pod)
      return true if sandbox.predownloaded?(pod)
      return true if folder_empty?(pod)
      false
    end
    

    源码解读:pod install 对 pod sources 更新逻辑
    现在,从头捋一遍:pod install 过程中这么判断哪些 pod 该更新,哪些 pod 不更新的呢?

    首先打开 installer.rb 文件,找到 def install! 方法,便是 pod install 的入口方法了

    def install!
      prepare
      resolve_dependencies
      download_dependencies
      validate_targets
      if installation_options.skip_pods_project_generation?
        show_skip_pods_project_generation_message
      else
        integrate
      end
      write_lockfiles
      perform_post_install_actions
    end
    

    其中调用了 download_dependencies 方法:

    def download_dependencies
      UI.section 'Downloading dependencies' do
        install_pod_sources
        run_podfile_pre_install_hooks
        clean_pod_sources
      end
    end
    

    其中调用了 install_pod_sources 方法:

    # Downloads, installs the documentation and cleans the sources of the Pods
    # which need to be installed.
    #
    # @return [void]
    #
    def install_pod_sources
      @installed_specs = []
      pods_to_install = sandbox_state.added | sandbox_state.changed
      title_options = { :verbose_prefix => '-> '.green }
      root_specs.sort_by(&:name).each do |spec|
        if pods_to_install.include?(spec.name)
          ... 省略代码逻辑
          UI.titled_section(title.green, title_options) do
            install_source_of_pod(spec.name)
          end
        else
          UI.section("Using #{spec}", title_options[:verbose_prefix]) do
            create_pod_installer(spec.name)
          end
        end
      end
    end
    

    发现 sandbox_state.added 和 sandbox_state.changed 应该是分别对应着 新增、变更 的 pod。全局搜索 sandbox_state 会发现方法:

    # @return [SpecsState] The state of the sandbox returned by the analyzer.
    #
    def sandbox_state
      analysis_result.sandbox_state
    end
    

    全局搜索 analysis_result,看看它是哪里来的,会发现方法:

    # @!group Installation steps
    
    # Performs the analysis.
    #
    # @param  [Analyzer] analyzer the analyzer to use for analysis
    #
    # @return [void]
    #
    def analyze(analyzer = create_analyzer)
      @analysis_result = analyzer.analyze
      @aggregate_targets = @analysis_result.targets
      @pod_targets = @analysis_result.pod_targets
    end
    

    analysis_result 是执行了 analyzer 的 analyze 方法得到的。搜索当前这个 analyze( 方法的调用是在方法:

    # @return [Analyzer] The analyzer used to resolve dependencies
    #
    def resolve_dependencies
      plugin_sources = run_source_provider_hooks
      analyzer = create_analyzer(plugin_sources)
    
      ... 省略代码逻辑
    
      UI.section 'Analyzing dependencies' do
        analyze(analyzer)
        validate_build_configurations
      end
    
      ... 省略代码逻辑
    
      analyzer
    end
    

    以上 resolve_dependencies 方法也是在 install 方法里被调用的,而且是在 download_dependencies 方法之前。根据方法名也不难看出这里的代码逻辑:首先调用 resolve_dependencies 方法进行依赖分析拿到结果,再执行 download_dependencies 方法下载依赖。

    接着搜索 create_analyzer 方法:

    1
    2
    3
    def create_analyzer(plugin_sources = nil)
      Analyzer.new(sandbox, podfile, lockfile, plugin_sources, has_dependencies?, update)
    end
    

    Analyzer 应该就是具体的分析类,这个类里拥有的 analyze 方法正是返回了上述提到的 @analysis_result。打开 analyzer.rb 文件搜索 analyze 试试:

    # Performs the analysis.
    #
    # The Podfile and the Lockfile provide the information necessary to
    # compute which specification should be installed. The manifest of the
    # sandbox returns which specifications are installed.
    #
    # @param  [Bool] allow_fetches
    #         whether external sources may be fetched
    #
    # @return [AnalysisResult]
    #
    def analyze(allow_fetches = true)
      ... 省略代码逻辑
      sandbox_state   = generate_sandbox_state(specifications)
      ... 省略代码逻辑
      @result = AnalysisResult.new(podfile_state, specs_by_target, specs_by_source, specifications, sandbox_state,
                                   aggregate_targets, pod_targets, @podfile_dependency_cache)
    end
    
    # Computes the state of the sandbox respect to the resolved
    # specifications.
    #
    # @return [SpecsState] the representation of the state of the manifest
    #         specifications.
    #
    def generate_sandbox_state(specifications)
      sandbox_state = nil
      UI.section 'Comparing resolved specification to the sandbox manifest' do
        sandbox_analyzer = SandboxAnalyzer.new(sandbox, specifications, update_mode?)
        sandbox_state = sandbox_analyzer.analyze
        sandbox_state.print
      end
      sandbox_state
    end
    

    至此,就找到了前文提到的 def sandbox_state 方法返回的 analysis_result.sandbox_state 正是以上方法里 AnalysisResult 这个类的 sandbox_state 属性,而该属性又是来自 generate_sandbox_state 方法生成的 SandboxAnalyzer 对象,执行 analyze 方法返回的。

    找到 SandboxAnalyzer 类就找到我们此行的最终答案,打开 sandbox_analyzer.rb 文件:

    # Performs the analysis to the detect the state of the sandbox respect
    # to the resolved specifications.
    #
    # @return [void]
    #
    def analyze
      state = SpecsState.new
      if sandbox_manifest
        all_names = (resolved_pods + sandbox_pods).uniq.sort
        all_names.sort.each do |name|
          state.add_name(name, pod_state(name))
        end
      else
        state.added.merge(resolved_pods)
      end
      state
    end
    
    #---------------------------------------------------------------------#
    
    private
    
    # @!group Pod state
    
    # Returns the state of the Pod with the given name.
    #
    # @param  [String] pod
    #         the name of the Pod.
    #
    # @return [Symbol] The state
    #
    def pod_state(pod)
      return :added   if pod_added?(pod)
      return :deleted if pod_deleted?(pod)
      return :changed if pod_changed?(pod)
      :unchanged
    end
    
    # Returns whether the Pod with the given name should be installed.
    #
    # @note   A Pod whose folder doesn't exists is considered added.
    #
    # @param  [String] pod
    #         the name of the Pod.
    #
    # @return [Bool] Whether the Pod is added.
    #
    def pod_added?(pod)
      return true if resolved_pods.include?(pod) && !sandbox_pods.include?(pod)
      return true if !folder_exist?(pod) && !sandbox.local?(pod)
      false
    end
    
    # Returns whether the Pod with the given name should be removed from
    # the installation.
    #
    # @param  [String] pod
    #         the name of the Pod.
    #
    # @return [Bool] Whether the Pod is deleted.
    #
    def pod_deleted?(pod)
      return true if !resolved_pods.include?(pod) && sandbox_pods.include?(pod)
      false
    end
    
    # Returns whether the Pod with the given name should be considered
    # changed and thus should be reinstalled.
    #
    # @note   In update mode, as there is no way to know if a remote source
    #         hash changed the Pods from external
    #         sources are always marked as changed.
    #
    # @note   A Pod whose folder is empty is considered changed.
    #
    # @param  [String] pod
    #         the name of the Pod.
    #
    # @return [Bool] Whether the Pod is changed.
    #
    def pod_changed?(pod)
      spec = root_spec(pod)
      return true if spec.version != sandbox_version(pod)
      return true if spec.checksum != sandbox_checksum(pod)
      return true if resolved_spec_names(pod) != sandbox_spec_names(pod)
      return true if sandbox.predownloaded?(pod)
      return true if folder_empty?(pod)
      false
    end
    

    这段代码就是 pod install 过程中判断得到的哪些 pod 是新增的、哪些 pod 是删除的、哪些 pod 是改变的,而新增的 和 改变的则需要下载新的代码。

    关注 def pod_changed 方法,spec 相当于目标 pod 的 podspec,sandbox_ 相关的则是进行比对的 Pods/Manifest.lock 文件包含的 pod 信息。
    在 Podfile 里将 pod 从分支指向改为 版本号 时,这里的判断的 versionchecksumname 等信息都是相同的,所以该 pod 并未出现在 changed 数组里,Pods 文件夹里该 pod 的代码也就并未更新。

    修复问题: PR #10825

    首先要搞明白在 Podfile 里将 pod 指向分支和指向版本号在 lock 记录里究竟有什么不一样,才好处理究竟要怎么判断。打开 Podfile.lock 文件(和 Pods/Manifest.lock 文件内容一致),搜索 PodA 相关的内容:

    # Pod 'PodA', :git => 'https://xyz.com/PodA.git', :branch => 'branch_a'
    DEPENDENCIES:
      - PodA (from `https://xyz.com/PodA.git`, branch `branch_a`)
    
    # Pod 'PodA', '1.0.0'
    DEPENDENCIES:
      - PodA (= 1.0.0)
    

    由此,只要判断 Podfile 里生成的 DependencyPods/Manifest.lock 文件里记录的 Dependency 不一致则代表 pod changed,需要被更新。

    通过查看 cocoapods-core 代码,Lockfile 类里已经有的 dependencies 数组,收集好了 Dependency 对象,Podfile 类里也已经提供好了 dependencies 数组。只要根据 pod name 从两个数组里分别匹配出 Dependency对象进行比对,如果不同则代表 pod changed,需要被更新。

    思路大概就这样,具体改动可参考 PR 里的 files changed

    private hofix

    因为 PR 里 @dnkoutso 标记了 1.11.0 发布。感觉日常开发中遇到这个 bug 还挺严重的。如果 Jenkins 打包失败了还好,还能发现问题,万一不影响代码编译很有可能将错误代码提交给测试,甚至带到线上去。所以在组内发布了 private hofix 版本。基于当前的最新版本(1.10.2)cherry pick 了相关的 commit,发布了 1.10.2.9.private.hotfix 版本。作为在 1.11.0 出来之前的临时使用版本吧。

    参考与佳期的个人博客:http://gonghonglou.com/2021/08/08/cocoapods-update-pod-sources/

    相关文章

      网友评论

          本文标题:修复 pod install 无法正确拉取 pod 代码

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