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

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

问题描述

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

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

1
2
3
4
5
6
$ /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 方法来控制临时输出目录的位置。

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

1
2
3
4
5
6
7
8
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

相似之处

  • 两者都允许在运行时修改既有行为。
  • 都可以用于扩展或修改现有类的功能。
  • 在调试和添加功能时都非常有用。

不同之处:

  • 作用范围
    Ruby 的 Monkey Patching 可以添加新方法、修改现有方法,甚至修改核心类。
    Objective-C 的方法交换主要用于交换现有方法的实现,不能直接添加新方法。

  • 实现方式
    Ruby 直接在类定义中重新定义方法。
    Objective-C 使用运行时函数如 method_exchangeImplementations 来交换方法实现。

  • 灵活性
    Ruby 的 Monkey Patching 更加灵活,可以轻松修改任何类的行为。
    Objective-C 的方法交换相对受限,主要用于修改自己的类或分类中的方法。

  • 使用场景
    Ruby 的 Monkey Patching 常用于扩展库功能、打补丁、调试等。
    Objective-C 的方法交换常用于 AOP(面向切面编程)、为现有方法添加额外功能等。

  • 风险
    Ruby 的 Monkey Patching 可能导致难以预料的副作用,特别是在修改核心类时。
    Objective-C 的方法交换风险相对较小,但不当使用仍可能导致意外行为。

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

解决方案:应用 Monkey Patching

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

以下是 gym_patches.rb 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 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 中添加一行导入即可:

1
require_relative './gym_patches'

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

优势

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

结语

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

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

Happy coding!