Module: Pindo::KeystoreHelper

Defined in:
lib/pindo/module/android/keystore_helper.rb

Constant Summary collapse

ENV_RELEASE_KEYSTORE_PATH =

与工程约定一致:Gradle 内用 RELEASE_* 环境变量 + 本地 ?: 回退;pindo 拉取 JPS 后写入当前进程 ENV(不落盘明文)

"RELEASE_KEYSTORE_PATH"
ENV_RELEASE_KEYSTORE_PASSWORD =
"RELEASE_KEYSTORE_PASSWORD"
ENV_RELEASE_KEY_ALIAS =
"RELEASE_KEY_ALIAS"
ENV_RELEASE_KEY_PASSWORD =
"RELEASE_KEY_PASSWORD"
FIELD_CRYPTO_ALGORITHM =
"aes-128-gcm"
FIELD_CRYPTO_KEY =
"WNldU35bhG!8TQtg"
FIELD_CRYPTO_IV_LENGTH =
12
FIELD_CRYPTO_TAG_LENGTH =
16

Class Method Summary collapse

Class Method Details

.apply_keystore_config(project_dir, build_type = "debug", bundle_id:) ⇒ Boolean

将签名配置应用到 Android 工程

默认(与工程手写的 RELEASE_* 约定一致):只拉取 JPS、拷贝 jks 到项目 signing/,并设置当前进程RELEASE_KEYSTORE_PATH / RELEASE_KEYSTORE_PASSWORD / RELEASE_KEY_ALIAS / RELEASE_KEY_PASSWORD,不修改 build.gradle。RELEASE_KEYSTORE_PATH 使用 jks 的绝对路径,便于 app 子模块内 ‘file(System.getenv(…))` 引用。

Parameters:

  • project_dir (String)

    项目目录

  • build_type (String) (defaults to: "debug")

    构建类型 “debug” 或 “release”

Returns:

  • (Boolean)

    是否成功

Raises:

  • (ArgumentError)


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/pindo/module/android/keystore_helper.rb', line 156

def apply_keystore_config(project_dir, build_type = "debug", bundle_id:)
  raise ArgumentError, "项目目录不能为空" if project_dir.nil?
  raise ArgumentError, "build_type 必须是 debug 或 release" unless ["debug", "release"].include?(build_type)
  raise ArgumentError, "bundle_id 不能为空" if bundle_id.blank?

  reset_managed_signing_paths!
  reset_managed_gradle_backups!
  reset_managed_env_keys!

  main_module = Pindo::AndroidProjectHelper.get_main_module(project_dir)
  unless main_module
    Funlog.error("无法找到主模块")
    return false
  end

  gradle_file = find_gradle_file(main_module)
  unless gradle_file
    Funlog.error("无法找到 build.gradle 文件")
    return false
  end

  sign_config = get_android_sign_config(bundle_id: bundle_id)
  copy_keystore_to_project(project_dir, build_type, sign_config[build_type])

  # JPS 下载的临时 jks(系统临时目录下 pindo_jks),拷贝进工程后即可删除
  tmp_jks = sign_config[build_type]["storeFile"].to_s
  if File.exist?(tmp_jks)
    tmp_exp = File.expand_path(tmp_jks)
    tmp_root_exp = File.expand_path(File.join(Dir.tmpdir, "pindo_jks"))
    under_pindo_tmp = tmp_exp == tmp_root_exp || tmp_exp.start_with?(tmp_root_exp + File::SEPARATOR)
    FileUtils.rm_f(tmp_jks) if under_pindo_tmp
  end

  cfg = sign_config[build_type]
  rel_plain = cfg["relative_store_file"].to_s.sub(%r{\A\$rootDir/?}, "").delete_prefix("/")
  if rel_plain.empty?
    raise "JPS keystore 未拷贝到工程 signing/(无法解析路径),Gradle 将回退到 build.gradle 中的本地 jks;请检查 JPS 证书是否下载成功"
  end

  if gradle_file.end_with?(".kts")
    ensure_keystore_config_kts(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
  else
    ensure_keystore_config_groovy(gradle_file, project_dir, build_type, sign_config, bundle_id: bundle_id)
  end
  # 注意:部分 Unity 导出工程/多模块工程的 Gradle rootProject 目录可能不是 project_dir,
  # 相对路径(如 signing/xxx.jks)会被解析到错误的模块目录下(如 launcher/signing/...)。
  # 这里改为导出绝对路径,确保与 keystore 创建/拷贝目录保持一致。
  keystore_abs = File.expand_path(File.join(project_dir, rel_plain))
  export_jps_release_signing_env!(cfg, keystore_path_for_env: keystore_abs)
  true
end

.cleanup_managed_signing_env!Object

清理本次写入到进程 ENV 的签名变量(避免后续任务误继承)



140
141
142
143
144
145
# File 'lib/pindo/module/android/keystore_helper.rb', line 140

def cleanup_managed_signing_env!
  keys = @pindo_managed_env_keys || []
  keys.each { |k| ENV.delete(k.to_s) if k }
ensure
  @pindo_managed_env_keys = []
end

.cleanup_managed_signing_paths!Object

打包结束后由 AndroidBuildHelper.ensure 调用,删除本次落到工程 signing/ 下的 JKS



118
119
120
121
122
123
# File 'lib/pindo/module/android/keystore_helper.rb', line 118

def cleanup_managed_signing_paths!
  (@pindo_managed_signing_paths || []).each do |p|
    FileUtils.rm_f(p) if p && File.exist?(p)
  end
  @pindo_managed_signing_paths = []
end

.get_android_sign_config(bundle_id:) ⇒ Hash

从 JPS 获取 Android 签名配置(必须成功,否则抛出异常)

Parameters:

  • bundle_id (String)

    Android 包名(Application ID)

Returns:

  • (Hash)

    签名配置哈希,包含 debug 和 release 配置(共用同一个 JKS)

Raises:

  • (ArgumentError)


30
31
32
33
34
35
36
# File 'lib/pindo/module/android/keystore_helper.rb', line 30

def get_android_sign_config(bundle_id:)
  raise ArgumentError, "bundle_id 不能为空" if bundle_id.blank?

  cfg = fetch_jks_from_jps(bundle_id: bundle_id)
  # debug 和 release 使用同一个 JKS
  { "debug" => cfg, "release" => cfg }
end

.get_keystore_config_from_project(project_path, debug = false, workflow_build_type: nil) ⇒ Hash?

从工程的 build.gradle 中读取已有的 keystore 配置

Parameters:

  • project_path (String)

    项目路径

  • debug (Boolean) (defaults to: false)

    是否为 debug 构建(仅当未传 workflow_build_type 时用于选择 debug/release)

  • workflow_build_type (String, nil) (defaults to: nil)

    Workflow 对应 Gradle buildType 名(如 jps);若提供则**仅**从 buildTypes.<name> 解析,不回退 debug/release

Returns:

  • (Hash, nil)

    keystore 配置哈希(workflow 模式下解析失败会 raise,不返回 nil)



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/pindo/module/android/keystore_helper.rb', line 98

def get_keystore_config_from_project(project_path, debug = false, workflow_build_type: nil)
  main_module = Pindo::AndroidProjectHelper.get_main_module(project_path)
  return unless main_module

  gradle_kts_path = File.join(main_module, "build.gradle.kts")
  gradle_path     = File.join(main_module, "build.gradle")

  if File.exist?(gradle_kts_path)
    puts "KTS 项目,读取 #{File.basename(gradle_kts_path)} 文件"
    get_keystore_config_kts(gradle_kts_path, project_path, debug, workflow_build_type: workflow_build_type)
  elsif File.exist?(gradle_path)
    puts "Groovy 项目,读取 #{File.basename(gradle_path)} 文件"
    get_keystore_config_groovy(gradle_path, project_path, debug, workflow_build_type: workflow_build_type)
  else
    puts "未找到 build.gradle 或 build.gradle.kts 文件"
    nil
  end
end

.resolve_keystore_paths(config) ⇒ Hash

解析 keystore 文件路径

Parameters:

  • config (Hash)

    签名配置

Returns:

  • (Hash)

    解析后的配置



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/pindo/module/android/keystore_helper.rb', line 68

def resolve_keystore_paths(config)
  # 使用 PindoConfig 获取配置目录
  pindo_config = Pindoconfig.instance
  pindo_common_config_dir = pindo_config.pindo_common_configdir

  resolved_config = {}

  ["debug", "release"].each do |build_type|
    next if config[build_type].nil?

    cfg = config[build_type].dup
    store_file = cfg["storeFile"]

    # 如果是相对路径,则基于 pindo_common_config 目录解析
    cfg["storeFile"] = File.join(pindo_common_config_dir, store_file) unless store_file.start_with?("/")

    # 验证文件是否存在
    Funlog.warning("Keystore 文件不存在: #{cfg["storeFile"]}") unless File.exist?(cfg["storeFile"])

    resolved_config[build_type] = cfg
  end

  resolved_config
end

.restore_managed_signing_config!Object

打包结束后恢复本次被修改的 Gradle 签名配置(仅恢复被本次任务改动过的文件)



126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/pindo/module/android/keystore_helper.rb', line 126

def restore_managed_signing_config!
  backups = @pindo_managed_gradle_backups || {}
  backups.each do |path, original_content|
    next if path.nil? || path.to_s.empty?
    next unless original_content.is_a?(String)
    next unless File.exist?(path)

    File.write(path, original_content)
  end
ensure
  @pindo_managed_gradle_backups = {}
end

.validate_sign_config(config) ⇒ Boolean

验证签名配置格式

Parameters:

  • config (Hash)

    签名配置

Returns:

  • (Boolean)

    是否有效



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/pindo/module/android/keystore_helper.rb', line 41

def validate_sign_config(config)
  return false unless config.is_a?(Hash)

  # 至少需要有 debug 或 release 配置
  return false if config["debug"].nil? && config["release"].nil?

  # 验证每个配置的必需字段
  ["debug", "release"].each do |build_type|
    next if config[build_type].nil?

    cfg = config[build_type]
    required_fields = %w[storeFile storePassword keyAlias keyPassword]

    required_fields.each do |field|
      if cfg[field].nil? || cfg[field].to_s.empty?
        Funlog.error("#{build_type} 配置缺少必需字段: #{field}")
        return false
      end
    end
  end

  true
end