Skip to content

Ruby 魔法:用 Monkey Patching 解决 Fastlane Gym 的清理困境

轩辕十四
Published date:

在 iOS 开发中,自动化构建和打包流程是提高效率的关键。Fastlane 的 Gym 工具为我们提供了强大的自动化能力,但有时也会带来一些令人头疼的问题。今天,我们就来探讨一个我遇到的 Gym 打包问题,并分享一个巧妙的解决方案。

Table of contents

Open Table of contents

问题描述

在使用 Fastlane 的 Gym 工具进行批量 iOS 打包时,我们可能会遇到一个棘手的问题。特别是当我们将 Gym 的 clean 参数设置为 true 时,可能会导致构建过程失败。

在执行打包命令时,Gym 会运行类似这样的命令:

$ /usr/bin/xcrun \
    /opt/homebrew/lib/ruby/gems/3.3.0/gems/fastlane-2.222.0/gym/lib/assets/wrap_xcodebuild/xcbuild-safe.sh \
    -exportArchive \
    -exportOptionsPlist '/var/folders/64/8v_9fcln0_g76s0ry0j6pwt40000gn/T/gym_config20240929-78449-4vb1zz.plist' \
    -archivePath build/beta/zestbuy_beta.xcarchive \
    -exportPath '/var/folders/64/8v_9fcln0_g76s0ry0j6pwt40000gn/T/gym_output20240929-78449-k1ep6q'

问题出现在这里:当 clean 参数设置为 true 时,Gym 会在构建过程中清理临时文件和目录。这可能导致 exportOptionsPlist 指定的文件路径被删除,从而引发 “文件或目录不存在”的错误,最终导致整个打包过程失败。

更具体地说,问题的根源在于:

  1. Gym 使用系统的临时目录来存储 exportOptionsPlist 文件。
  2. clean 参数为 true 时,这些临时文件可能在需要使用之前就被清理掉了。
  3. Gym 没有提供直接的方法让我们自定义这个文件的存储路径。

这个问题特别棘手,因为它只在特定配置下出现(即 clean: true),而这个配置在某些情况下是必要的,比如为了确保每次构建都是从一个干净的状态开始。

深入分析

经过对 Gym 源码分析,我们发现问题的根源主要在 Gym 的 PackageCommandGeneratorXcode7 类中的 config_path 方法。这个方法决定了 exportOptionsPlist 文件的生成位置。同时,为了进一步提高可靠性,我们也可以修改 temporary_output_path 方法来控制临时输出目录的位置。

让我们看看这两个方法的原始实现:

def temporary_output_path
  Gym.cache[:temporary_output_path] ||= Dir.mktmpdir('gym_output')
end

def config_path
  Gym.cache[:config_path] ||= "#{Tempfile.new('gym_config').path}.plist"
  return Gym.cache[:config_path]
end

config_path 方法使用 Tempfile 创建一个临时文件,这就是导致 exportOptionsPlist 文件可能找不到的主要原因。而 temporary_output_path 方法虽然不是直接导致问题的根源,但它使用 Dir.mktmpdir 创建临时目录,也可能在某些情况下引发类似的问题。

Ruby 的 Monkey Patching 原理

在介绍具体解决方案之前,让我们深入了解 Ruby 的 “Monkey Patching” 原理,这是我们解决问题的关键技术。 Monkey Patching 是 Ruby 中一个强大而独特的特性,它允许开发者在运行时修改现有类的行为。其核心原理包括:

  1. 开放类(Open Classes): Ruby 允许在任何时候重新打开并修改已定义的类,包括内置类和第三方库中的类。

  2. 方法查找机制: 当调用一个对象的方法时,Ruby 会沿着方法查找路径(也称为祖先链)向上搜索,直到找到第一个匹配的方法定义。

  3. 方法重定义: 当重新定义一个已存在的方法时,Ruby 会更新该类的方法表,使新的定义覆盖旧的。

  4. 动态性: 由于 Ruby 的动态特性,这些修改是即时生效的,影响所有后续的方法调用。

Monkey Patching 的工作原理可以概括为以下步骤:

  1. 重新打开目标类。
  2. 定义新的方法或重写现有方法。
  3. Ruby 更新类的方法表。
  4. 后续的方法调用将使用新的实现。

对于做过 iOS 开发的朋友来说,这是不是听起来有点熟悉?没错,这与 Objective-C 中的方法交换(Method Swizzling)有些相似之处。让我们来比较一下这两种技术:

Monkey Patching vs. Method Swizzling

相似之处

不同之处:

在我们的 Fastlane Gym 问题中,选择使用 Ruby 的 Monkey Patching 是因为我们需要修改第三方库的行为,而且不仅要修改现有方法的实现,还要改变其逻辑和返回值。Ruby 的动态特性允许我们在不修改原始源代码的情况下实现这些改变,这正是解决我们问题的理想方法。

解决方案:应用 Monkey Patching

了解了 Monkey Patching 的原理,我们现在可以应用这种技术来解决 Gym 的问题。我们将创建一个 gym_patches.rb 文件,使用 Monkey Patching 方式来修改 PackageCommandGeneratorXcode7 类的关键方法。

以下是 gym_patches.rb 的源码:

# frozen_string_literal: true

require 'gym'
require 'fileutils'

module Gym
  class PackageCommandGeneratorXcode7
    class << self
      # 生成配置文件的路径
      #
      # @return [String] 配置文件的完整路径
      # @note 这个方法会在 fastlane 目录下的 configs 子目录中创建一个新的配置文件
      def config_path
        begin
          Gym.cache[:config_path] ||= begin
            config_dir = File.join(File.dirname(__FILE__), 'configs')
            FileUtils.mkdir_p(config_dir)

            timestamp = Time.now.strftime('%Y%m%d%H%M%S')
            project_name = File.basename(Dir.pwd)
            filename = "#{project_name}_gym_config_#{timestamp}.plist"

            File.join(config_dir, filename)
          end
        rescue NoMethodError => e
          UI.error "Error accessing Gym.cache: #{e.message}"
        end
      end

      # 生成临时输出目录的路径
      #
      # @return [String] 临时输出目录的完整路径
      # @note 这个方法会在项目的 build 目录下创建一个带时间戳的 gym_output 子目录
      def temporary_output_path
        begin
          Gym.cache[:temporary_output_path] ||= begin
            output_dir = File.join(File.dirname(__FILE__), '..', 'build', 'gym_output')

            FileUtils.mkdir_p(output_dir)

            timestamp = Time.now.strftime('%Y%m%d%H%M%S')
            timestamped_dir = File.join(output_dir, timestamp)
            FileUtils.mkdir_p(timestamped_dir)

            timestamped_dir
          end
        rescue NoMethodError => e
          UI.error "Error accessing Gym.cache: #{e.message}"
        end
      end
    end
  end
end

这段代码重新定义了 PackageCommandGeneratorXcode7 类中的 config_pathtemporary_output_path 方法。新的实现将文件和目录路径改为项目内的固定位置,而不是使用系统的临时目录。

要使用这个解决方案,只需要在 Fastfile 中添加一行导入即可:

require_relative './gym_patches'

就这么简单!现在运行 Gym 时,它会使用我们指定的路径,而不是 Gym 实现的临时目录。

优势

  1. 可靠性提升:通过使用项目内的固定路径,大大减少了因临时文件丢失导致的构建失败。
  2. 更好的可追踪性:时间戳的添加使得每次构建的输出都能被轻松识别和追踪。
  3. 便于调试:当出现问题时,你可以轻松找到并检查相关的配置文件和输出。
  4. 更好的版本控制:你可以选择将这些文件纳入版本控制,方便团队协作和问题复现。
  5. 非侵入式修改:通过 Monkey Patching,我们修改了 Gym 的行为而无需改动其源代码,保持了原有代码的完整性。

结语

通过运用 Ruby 的 Monkey Patching 技术,我们巧妙地解决了 Fastlane Gym 在打包过程中可能遇到的文件路径问题。这个解决方案不仅修复了当前的问题,还提高了整个打包过程的可靠性和可追踪性。

记住,当你遇到框架限制时,深入研究源码并运用语言特性往往能找到解决方案。希望这个技巧能帮助到遇到类似问题的开发者。如果你有任何问题或改进建议,欢迎在评论区留言讨论。

Happy coding!

Previous
算法:两数之和
Next
如何使用 fastlane 读取 iOS 证书信息