Class: Pindo::Android::WorkflowGradleInjector

Inherits:
Object
  • Object
show all
Defined in:
lib/pindo/module/android/workflow_gradle_injector.rb

Constant Summary collapse

TMP_ROOT_RELATIVE =
".pindo_tmp".freeze
LOCK_RELATIVE_PATH =
File.join(TMP_ROOT_RELATIVE, "locks", "gradle_inject.lock").freeze
RUNS_RELATIVE_DIR =
File.join(TMP_ROOT_RELATIVE, "gradle_inject").freeze
MARK_BEGIN =
"PINDO_WORKFLOW_GRADLE_INJECTOR_BEGIN".freeze
MARK_END =
"PINDO_WORKFLOW_GRADLE_INJECTOR_END".freeze
RELEASE_SIGNING_ENV_VARS =
%w[
  RELEASE_KEYSTORE_PATH
  RELEASE_KEYSTORE_PASSWORD
  RELEASE_KEY_ALIAS
  RELEASE_KEY_PASSWORD
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.bin_copy(src, dst) ⇒ Object



804
805
806
807
808
809
810
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 804

def self.bin_copy(src, dst)
  File.open(src, "rb") do |r|
    File.open(dst, "wb") do |w|
      IO.copy_stream(r, w)
    end
  end
end

.remove_project_pindo_tmp_dir!(project_dir) ⇒ Object

删除工程根下打包过程生成的 ‘.pindo_tmp`(锁文件、gradle_inject 等)。须在 `cleanup!` 释放文件锁之后调用。



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 43

def remove_project_pindo_tmp_dir!(project_dir)
  return if project_dir.to_s.empty?

  base = File.expand_path(project_dir)
  return unless File.directory?(base)

  target = File.expand_path(File.join(base, TMP_ROOT_RELATIVE))
  return unless File.dirname(target) == base

  FileUtils.rm_rf(target) if File.exist?(target)
end

.restore_from_manifest_data!(project_dir, data) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 250

def self.restore_from_manifest_data!(project_dir, data)
  backups = data["backups"] || []
  backups.each do |item|
    path = File.join(project_dir, item.fetch("path"))
    backup_path = File.join(project_dir, item.fetch("backup_path"))

    if item["type"] == "dir"
      next unless File.directory?(backup_path)
      FileUtils.rm_rf(path) if File.exist?(path)
      FileUtils.mkdir_p(File.dirname(path))
      FileUtils.cp_r(backup_path, path)
    else
      next unless File.file?(backup_path)
      FileUtils.mkdir_p(File.dirname(path))
      bin_copy(backup_path, path)
    end
  end

  # 临时 PAD 默认会清理 created_dirs(通常是 *_pack)。
  # 若显式开启 PINDO_PAD_KEEP_REFS=1,则保留 pack 目录与引用,避免构建结束后被外部基线覆盖而丢引用。
  unless ENV.fetch("PINDO_PAD_KEEP_REFS", "").to_s.strip.downcase.match?(/\A(1|true|yes|on)\z/)
    (data["created_dirs"] || []).each do |rel|
      abs = File.join(project_dir, rel)
      FileUtils.rm_rf(abs) if abs.start_with?(project_dir.to_s)
    end
  end
end

.with_injection(project_dir:, workflow_name:, workflow_build_type: nil, enable_pad: true, main_module: nil) ⇒ Object

仅执行 prepare! 并 yield(context)。不在此自动 cleanup!:Gradle 注入与 JKS 的清理时机由调用方在「AAB→APK(universal.apk)成功产出后」统一处理;构建失败时调用方应在 ensure 中执行 cleanup! 以恢复 gradle,但不应删除尚未完成转换流程所需的 JKS。



30
31
32
33
34
35
36
37
38
39
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 30

def with_injection(project_dir:, workflow_name:, workflow_build_type: nil, enable_pad: true, main_module: nil)
  injector = new
  injector.prepare!(
    project_dir: project_dir,
    workflow_name: workflow_name,
    workflow_build_type: workflow_build_type,
    enable_pad: enable_pad,
    main_module: main_module
  ).tap { |context| yield(context) }
end

Instance Method Details

#cleanup!(context) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 170

def cleanup!(context)
  project_dir = context[:project_dir]
  tmp_run_dir = context[:tmp_run_dir]

  restore_from_manifest!(context)

  FileUtils.rm_f(context[:marker_path]) if context[:marker_path]
  FileUtils.rm_rf(tmp_run_dir) if tmp_run_dir && tmp_run_dir.start_with?(project_dir.to_s)
ensure
  begin
    if (lock_io = context[:lock_io])
      lock_io.flock(File::LOCK_UN)
      lock_io.close
    end
  rescue
    # ignore
  end
end

#prepare!(project_dir:, workflow_name:, workflow_build_type: nil, enable_pad: true, main_module: nil) ⇒ Object

Raises:

  • (ArgumentError)


56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 56

def prepare!(project_dir:, workflow_name:, workflow_build_type: nil, enable_pad: true, main_module: nil)
  raise ArgumentError, "project_dir 不能为空" if project_dir.to_s.empty?
  raise ArgumentError, "项目目录不存在: #{project_dir}" unless File.directory?(project_dir)
  raise ArgumentError, "workflow_name 不能为空" if workflow_name.to_s.empty?

  run_id = SecureRandom.uuid
  workflow_build_type ||= sanitize_workflow_build_type(workflow_name)
  signing_config_name = workflow_build_type

  tmp_run_dir = File.join(project_dir, RUNS_RELATIVE_DIR, run_id)
  backup_dir = File.join(tmp_run_dir, "backup")
  marker_path = File.join(tmp_run_dir, "marker")
  manifest_path = File.join(tmp_run_dir, "manifest.json")

  FileUtils.mkdir_p(backup_dir)

  lock_file = File.join(project_dir, LOCK_RELATIVE_PATH)
  FileUtils.mkdir_p(File.dirname(lock_file))

  lock_io = File.open(lock_file, File::RDWR | File::CREAT, 0o644)
  lock_io.flock(File::LOCK_EX)

  begin
    self_check_restore!(project_dir)

    context = {
      project_dir: project_dir,
      run_id: run_id,
      workflow_name: workflow_name,
      workflow_build_type: workflow_build_type,
      signing_config_name: signing_config_name,
      tmp_run_dir: tmp_run_dir,
      backup_dir: backup_dir,
      marker_path: marker_path,
      manifest_path: manifest_path,
      lock_file: lock_file,
      lock_io: lock_io,
      backups: [],
      created_dirs: [],
    }

    # 1) 先备份/再 PAD(PAD 会改 settings.gradle/launcher build.gradle/新增 pack 目录/移动 assets)
    if enable_pad
      pad_backup!(context)
      run_pad!(context)

      # 临时 PAD 默认会在 cleanup! 中恢复备份并删除 created_dirs(即 *_pack),以实现回滚。
      # 但某些工程会在构建结束后用“基线文件/外部备份”覆盖当前 build.gradle,
      # 若该基线不包含 assetPacks/include,会导致引用丢失。
      #
      # 当显式开启 PINDO_PAD_KEEP_REFS=1 时:
      # - 将 PAD 写入后的 settings.gradle / launcher/build.gradle 同步进备份快照,
      #   以确保 cleanup! 恢复后仍保留 assetPacks/include;
      # - 同时 cleanup! 将跳过删除 created_dirs,保留 *_pack 目录,保持工程可 sync。
      sync_pad_refs_to_backups!(context) if pad_keep_refs_enabled?
    end

    # 2) 注入 buildType/signingConfig(对所有 Android modules)
    gradle_files = discover_gradle_module_files(project_dir)
    main_module_gradle = resolve_main_module_gradle_file(project_dir, gradle_files, main_module: main_module)

    # 需要改写的文件清单(幂等:已包含标记则不会重复注入)
    to_inject = gradle_files.dup
    to_inject << main_module_gradle if main_module_gradle && !to_inject.include?(main_module_gradle)
    to_inject.compact!
    to_inject.uniq!

    to_inject.each { |path| backup_file!(context, path) }

    File.write(marker_path, JSON.pretty_generate({
      run_id: run_id,
      workflow_name: workflow_name,
      workflow_build_type: workflow_build_type,
      signing_config_name: signing_config_name,
      started_at: Time.now.to_i,
    }))

    to_inject.each do |gradle_path|
      inject_into_gradle_file!(
        gradle_path: gradle_path,
        workflow_name: workflow_name,
        workflow_build_type: workflow_build_type,
        signing_config_name: signing_config_name,
        is_main_module: (gradle_path == main_module_gradle)
      )
    end

    File.write(manifest_path, JSON.pretty_generate({
      run_id: run_id,
      marker_path: marker_path,
      backups: context[:backups],
      created_dirs: context[:created_dirs],
    }))

    context
  rescue
    # 如果 prepare! 中途失败,也尽可能清理掉 marker/恢复备份,避免污染后续运行
    begin
      cleanup!({
        project_dir: project_dir,
        tmp_run_dir: tmp_run_dir,
        marker_path: marker_path,
        manifest_path: manifest_path,
        lock_io: lock_io,
        backups: [],
        created_dirs: [],
      })
    rescue
      # ignore
    end
    raise
  end
end

#sanitize_workflow_build_type(workflow_name) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 189

def sanitize_workflow_build_type(workflow_name)
  raw = workflow_name.to_s

  parts = raw.gsub(/[^A-Za-z0-9_]+/, " ").strip.split(/\s+/)
  # 去掉开头的 "test" 词:AGP 禁止 BuildType 名以 test 开头;如 "test demo" 应生成 demo 而非 testDemo
  while parts.first && parts.first.casecmp("test").zero?
    parts.shift
  end

  base = if parts.empty?
           "workflow"
         else
           first = parts.first.downcase
           rest = parts.drop(1).map { |p| p[0].to_s.upcase + p[1..].to_s.downcase }
           ([first] + rest).join
         end

  base = base.gsub(/[^A-Za-z0-9_]/, "")
  base = "w_#{base}" unless base.match?(/\A[A-Za-z]/)
  # AGP:BuildType 名称不能以 test 开头(保留字),否则报 BuildType names cannot start with 'test'
  base = "wf_#{base}" if base.match?(/\Atest/i)

  max_len = 40
  if base.length > max_len
    digest = Digest::SHA256.hexdigest(raw)[0, 8]
    base = "#{base[0, max_len - 9]}_#{digest}"
  end

  base
end