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



692
693
694
695
696
697
698
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 692

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



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 240

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"))
    next unless File.file?(backup_path)

    FileUtils.mkdir_p(File.dirname(path))
    bin_copy(backup_path, path)
  end

  (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

.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



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 160

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:



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
# 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)
    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



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
207
208
# File 'lib/pindo/module/android/workflow_gradle_injector.rb', line 179

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