Class: Pindo::AndroidProjectHelper

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

Class Method Summary collapse

Class Method Details

.add_unity_namespace(project_path) ⇒ Object



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/pindo/module/android/android_project_helper.rb', line 233

def add_unity_namespace(project_path)
  # 在 unityLibrary/build.gradle 中添加 namespace(AGP 7.x+ 需要)
  unity_build_gradle = File.join(project_path, "unityLibrary/build.gradle")
  return unless File.exist?(unity_build_gradle)

  content = File.read(unity_build_gradle)

  # 检查是否已经有 namespace
  if content =~ /namespace\s+['"][\w.]+['"]/
    puts "  ✓ unityLibrary 已配置 namespace"
    return
  end

  # 在 android { 块的开始位置添加 namespace
  if content =~ /(android\s*\{)/
    # Unity 默认使用 com.unity3d.player 作为 namespace
    namespace_line = "\n    namespace 'com.unity3d.player'\n"
    content.sub!(/(android\s*\{)/, "\\1#{namespace_line}")

    File.write(unity_build_gradle, content)
    puts "  ✓ 已添加 namespace 到 unityLibrary/build.gradle"
  else
    puts "  ⚠ 无法在 unityLibrary/build.gradle 中找到 android 块"
  end
end

.check_main_local_properties(project_path) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/pindo/module/android/android_project_helper.rb', line 259

def check_main_local_properties(project_path)
  # 检查并配置主工程的 local.properties 文件
  main_local_properties = File.join(project_path, "local.properties")

  # 获取期望的 SDK 路径(从 Unity 模块、环境变量或默认路径)
  expected_sdk_dir = get_expected_sdk_dir(project_path)

  unless expected_sdk_dir
    puts "  ⚠ 无法找到有效的 Android SDK 路径"
    return
  end

  if File.exist?(main_local_properties)
    # 文件存在,检查 sdk.dir 是否正确
    puts "  检查主工程 local.properties..."
    content = File.read(main_local_properties)

    if content =~ /sdk\.dir\s*=\s*(.+)/
      current_sdk_dir = $1.strip

      if current_sdk_dir == expected_sdk_dir
        puts "  ✓ SDK 路径正确: #{current_sdk_dir}"
      elsif File.directory?(current_sdk_dir)
        puts "  ✓ SDK 路径有效: #{current_sdk_dir}"
      else
        # SDK 路径无效,更新为期望的路径
        puts "  ⚠ SDK 路径无效: #{current_sdk_dir}"
        puts "  更新为: #{expected_sdk_dir}"
        content.gsub!(/sdk\.dir\s*=\s*.+/, "sdk.dir=#{expected_sdk_dir}")
        File.write(main_local_properties, content)
        puts "  ✓ 已更新 SDK 路径"
      end
    else
      # 文件存在但没有 sdk.dir,添加它
      puts "  ⚠ local.properties 中未找到 sdk.dir"
      File.write(main_local_properties, "#{content}\nsdk.dir=#{expected_sdk_dir}\n")
      puts "  ✓ 已添加 SDK 路径: #{expected_sdk_dir}"
    end
  else
    # 文件不存在,创建它
    puts "  创建主工程 local.properties..."
    File.write(main_local_properties, "sdk.dir=#{expected_sdk_dir}\n")
    puts "  ✓ 已创建 local.properties,SDK 路径: #{expected_sdk_dir}"
  end
end

.ensure_export_has_firebase_unity_aars!(unity_root_path:, export_path:) ⇒ Object

兼容旧调用名(实现已泛化为「按 Gradle 声明补齐 libs」)。



115
116
117
# File 'lib/pindo/module/android/android_project_helper.rb', line 115

def ensure_export_has_firebase_unity_aars!(unity_root_path:, export_path:)
  ensure_export_unity_library_aars_from_gradle!(unity_root_path: unity_root_path, export_path: export_path)
end

.ensure_export_unity_library_aars_from_gradle!(unity_root_path:, export_path:) ⇒ Array<String>

根据导出工程 ‘unityLibrary/build.gradle` 中**实际声明**的本地 AAR 依赖,校验并补齐 `unityLibrary/libs`。

EDM4U 常将部分 AAR 解析到 ‘Assets/GeneratedLocalRepo/**/m2repository`,导出后 Gradle 仍按 `name + ext:aar` 、`libs/某.aar` 或 `fileTree(dir: ’libs’, include: [‘*.aar’, …])‘ 引用;若只拷贝导出目录会缺文件。声明来源:`AndroidResolverDependencies.xml` + `GeneratedLocalRepo`(`fileTree` 模式会合并二者中的 .aar 清单)。

Returns:

  • (Array<String>)

    已在 libs 中存在或本次拷贝的 .aar 文件名

Raises:

  • (ArgumentError)


51
52
53
54
55
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
# File 'lib/pindo/module/android/android_project_helper.rb', line 51

def ensure_export_unity_library_aars_from_gradle!(unity_root_path:, export_path:)
  raise ArgumentError, "unity_root_path 不能为空" if unity_root_path.to_s.empty?
  raise ArgumentError, "export_path 不能为空" if export_path.to_s.empty?
  raise ArgumentError, "Unity 工程目录不存在: #{unity_root_path}" unless File.directory?(unity_root_path)
  raise ArgumentError, "导出目录不存在: #{export_path}" unless File.directory?(export_path)

  unity_library = File.join(export_path, "unityLibrary")
  gradle_path = %w[build.gradle build.gradle.kts].map { |n| File.join(unity_library, n) }.find { |p| File.file?(p) }
  libs_dir = File.join(unity_library, "libs")
  FileUtils.mkdir_p(libs_dir)

  unless gradle_path
    return []
  end

  gradle_content = File.read(gradle_path, encoding: "UTF-8")
  resolver_xml = File.join(unity_root_path, "ProjectSettings", "AndroidResolverDependencies.xml")
  aar_index = build_android_resolver_aar_index(unity_root_path, resolver_xml)

  required_basenames =
    if gradle_declares_libs_file_tree_with_aar?(gradle_content)
      merged_aar_basenames_for_libs_file_tree(aar_index, unity_root_path)
    else
      extract_required_aar_basenames_from_unity_library_gradle(gradle_content)
    end

  return [] if required_basenames.empty?

  satisfied = []
  missing_after_copy = []

  required_basenames.each do |base|
    dst = File.join(libs_dir, base)
    if file_readable_nonbroken?(dst)
      satisfied << base
      next
    end

    src = resolve_aar_source_for_basename(base, aar_index, unity_root_path)
    unless src
      missing_after_copy << base
      next
    end

    FileUtils.cp(src, dst)
    satisfied << base
  end

  unless missing_after_copy.empty?
    raise Informative, <<~MSG
      Unity unityLibrary/libs 缺少 Gradle 已声明的 AAR,且无法在 Unity 工程内找到源文件:
      #{missing_after_copy.sort.join(', ')}
      Unity 工程: #{unity_root_path}
      导出目录: #{export_path}
      Gradle: #{gradle_path}
      请检查: #{resolver_xml}
      并在 Unity 中执行:External Dependency Manager → Android Resolver → Force Resolve 后重新导出。
    MSG
  end

  satisfied.uniq
end

.ensure_unity_il2cpp_jni_merge_depends_on!(project_path) ⇒ Boolean

永久修复 Unity IL2CPP 工程:将 mergeDebug/mergeReleaseJniLibFolders 对 BuildIl2CppTask 的依赖从“硬编码 buildType”改为“匹配所有 merge*JniLibFolders 变体”,避免自定义 buildType(如 workflow)或 Unity/AGP 版本差异导致漏掉依赖关系。

仅修改 Unity 导出工程(包含 unityLibrary/build.gradle)中的 unityLibrary 模块 Gradle 文件。幂等:已注入则不会重复写入。

Returns:

  • (Boolean)

    是否发生了写入变更

Raises:

  • (ArgumentError)


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
# File 'lib/pindo/module/android/android_project_helper.rb', line 16

def ensure_unity_il2cpp_jni_merge_depends_on!(project_path)
  raise ArgumentError, "project_path 不能为空" if project_path.to_s.empty?
  raise ArgumentError, "项目目录不存在: #{project_path}" unless File.directory?(project_path)

  unity_library = File.join(project_path, "unityLibrary")
  return false unless File.directory?(unity_library)

  gradle_path = File.join(unity_library, "build.gradle")
  kts_path = File.join(unity_library, "build.gradle.kts")
  target_file = File.file?(kts_path) ? kts_path : (File.file?(gradle_path) ? gradle_path : nil)
  return false unless target_file

  original = File.read(target_file, encoding: "UTF-8")
  # 仅在明显是 Unity IL2CPP 工程时介入
  return false unless original.include?("BuildIl2CppTask")

  dsl = target_file.end_with?(".kts") ? :kts : :groovy
  updated = normalize_unity_il2cpp_jni_merge_depends_on_text(original)
  updated = migrate_unity_il2cpp_jni_merge_depends_on_marker_text(updated)
  updated = ensure_unity_il2cpp_jni_merge_depends_on_text(updated, dsl: dsl)

  return false if updated == original

  File.write(target_file, updated, encoding: "UTF-8")
  true
end

.find_android_subproject(project_path) ⇒ Object



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/pindo/module/android/android_project_helper.rb', line 392

def find_android_subproject(project_path)
  android_dir = File.join(project_path, "Unity")
  return nil unless File.directory?(android_dir)

  main_module = get_main_module(android_dir)
  return nil unless main_module

  src_main = File.join(main_module, "src/main")
  return nil unless File.directory?(src_main)

  manifest = File.join(src_main, "AndroidManifest.xml")
  return nil unless File.exist?(manifest)

  android_dir
end

.get_build_toolsObject



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
207
208
209
210
211
212
213
# File 'lib/pindo/module/android/android_project_helper.rb', line 159

def get_build_tools
  # 获取 gem 资源文件路径
  pindo_dir ||= File.expand_path(ENV['PINDO_DIR'] || '~/.pindo')
  pindo_common_configdir ||= File.join(pindo_dir, "pindo_common_config")
  tools_dir = File.join(pindo_common_configdir, 'android_tools')

  # 检查工具目录是否存在
  unless File.directory?(tools_dir)
    Funlog.error("Android 构建工具目录不存在: #{tools_dir}")
    Funlog.error("请执行以下命令更新 Pindo 配置:")
    Funlog.error("  pindo setup")
    Funlog.error("或手动克隆配置仓库到 ~/.pindo/pindo_common_config")
    return nil
  end

  # 定义必要的工具
  required_tools = {
    bundle_tool: 'bundletool.jar',
    gradlew: 'gradlew',
    gradle_wrapper: 'gradle-wrapper.jar'
  }

  tools = {}
  missing_tools = []

  # 检查每个工具是否存在
  required_tools.each do |key, filename|
    path = File.join(tools_dir, filename)
    if File.exist?(path)
      tools[key] = path
    else
      missing_tools << filename
    end
  end

  # 如果有缺失的工具,提供友好的错误信息
  unless missing_tools.empty?
    Funlog.error("缺少以下 Android 构建工具:")
    missing_tools.each do |tool|
      Funlog.error("  - #{tool}")
    end
    Funlog.error("")
    Funlog.error("解决方案:")
    Funlog.error("  1. 执行 'pindo setup' 更新配置")
    Funlog.error("  2. 或手动下载缺失的工具到: #{tools_dir}")
    Funlog.error("")
    Funlog.error("如果问题持续存在,请检查:")
    Funlog.error("  - 网络连接是否正常")
    Funlog.error("  - Git 仓库是否可访问")
    Funlog.error("  - 目录权限是否正确")
    return nil
  end

  tools
end

.get_expected_sdk_dir(project_path) ⇒ Object



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/pindo/module/android/android_project_helper.rb', line 305

def get_expected_sdk_dir(project_path)
  # 优先使用 Android Studio 的 SDK 路径(主工程使用)
  android_studio_sdk = File.expand_path("~/Library/Android/sdk")
  return android_studio_sdk if File.directory?(android_studio_sdk)

  # 尝试从环境变量获取
  sdk_dir = ENV['ANDROID_HOME'] || ENV['ANDROID_SDK_ROOT']
  return sdk_dir if sdk_dir && File.directory?(sdk_dir)

  # 尝试从 Unity 模块的 local.properties 获取 SDK 路径
  unity_local_properties = File.join(project_path, "Unity/local.properties")

  if File.exist?(unity_local_properties)
    unity_content = File.read(unity_local_properties)
    if unity_content =~ /sdk\.dir\s*=\s*(.+)/
      sdk_dir = $1.strip
      return sdk_dir if File.directory?(sdk_dir)
    end
  end

  # 最后尝试 Unity 内置的 SDK 路径
  unity_sdk = "/Applications/Unity/Hub/Editor/2022.3.61f1/PlaybackEngines/AndroidPlayer/SDK"
  return unity_sdk if File.directory?(unity_sdk)

  nil
end

.get_main_module(project_path) ⇒ Object



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/pindo/module/android/android_project_helper.rb', line 332

def get_main_module(project_path)
  settings_gradle_path = File.join(project_path, "settings.gradle")
  settings_gradle_kts_path = File.join(project_path, "settings.gradle.kts")

  # 优先使用 settings.gradle.kts,如果不存在则使用 settings.gradle
  if File.exist?(settings_gradle_kts_path)
    settings_gradle_path = settings_gradle_kts_path
  elsif !File.exist?(settings_gradle_path)
    return nil
  end

  content = File.read(settings_gradle_path)
  modules = extract_modules_from_settings(content)
  project_dir_map = extract_module_project_dirs(content)

  main_module = modules.find do |m|
    module_name = m.split(':').last
    module_rel_path = project_dir_map[m] || module_name
    module_dir = File.join(project_path, module_rel_path)
    gradle_path = File.join(module_dir, "build.gradle")
    gradle_kts_path = File.join(module_dir, "build.gradle.kts")

    gradle_file = if File.exist?(gradle_kts_path)
      gradle_kts_path
    elsif File.exist?(gradle_path)
      gradle_path
    end

    next false unless gradle_file

    gradle_content = File.read(gradle_file)
    android_application_module?(gradle_content)
  end

  # 兜底:一些工程未声明标准 application 插件,优先尝试常见主模块名
  if main_module.nil?
    %w[app launcher application].each do |candidate|
      candidate_module = modules.find { |m| m.split(':').last == candidate }
      next unless candidate_module

      module_rel_path = project_dir_map[candidate_module] || candidate
      module_dir = File.join(project_path, module_rel_path)
      has_gradle = File.exist?(File.join(module_dir, "build.gradle")) || File.exist?(File.join(module_dir, "build.gradle.kts"))
      has_manifest = File.exist?(File.join(module_dir, "src/main/AndroidManifest.xml"))
      if has_gradle && has_manifest
        main_module = candidate_module
        break
      end
    end
  end
  return nil unless main_module

  module_name = main_module.split(':').last
  module_rel_path = project_dir_map[main_module] || module_name
  File.join(project_path, module_rel_path)
end

.modify_il2cpp_config(project_path) ⇒ Object



215
216
217
218
219
220
221
222
223
224
# File 'lib/pindo/module/android/android_project_helper.rb', line 215

def modify_il2cpp_config(project_path)
  # 设置Il2CppOutputProject可执行权限
  system("chmod", "-R", "777",
  File.join(project_path, "unityLibrary/src/main/Il2CppOutputProject"))

  il2cpp_config_path = File.join(project_path, "unityLibrary/src/main/Il2CppOutputProject/IL2CPP/libil2cpp/il2cpp-config.h")
  content = File.read(il2cpp_config_path)
  content.gsub!("il2cpp::vm::Exception::Raise", "//il2cpp::vm::Exception::Raise")
  File.write(il2cpp_config_path, content)
end

.remove_desktop_google_service(project_path) ⇒ Object



226
227
228
229
230
231
# File 'lib/pindo/module/android/android_project_helper.rb', line 226

def remove_desktop_google_service(project_path)
  # 删除google-services-desktop.json
  desktop_google_service_path = File.join(project_path,
    "unityLibrary/src/main/assets/google-services-desktop.json")
  File.delete(desktop_google_service_path) if File.exist?(desktop_google_service_path)
end

.sync_gradle_properties_from_unity_to_main(project_path) ⇒ Object

在 Unity 作为 lib 的工程中,将 Unity 根目录下 gradle.properties 中的关键配置同步到主工程的 gradle.properties 中当前仅同步以下键:

  • android.aapt2FromMavenOverride

  • org.gradle.java.home



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/pindo/module/android/android_project_helper.rb', line 413

def sync_gradle_properties_from_unity_to_main(project_path)
  return unless unity_as_lib_android_project?(project_path)

  unity_gradle_properties = File.join(project_path, "Unity", "gradle.properties")
  return unless File.exist?(unity_gradle_properties)

  keys_to_sync = [
    "android.aapt2FromMavenOverride",
    "org.gradle.java.home",
  ]

  unity_values = {}
  File.read(unity_gradle_properties).each_line do |line|
    stripped = line.strip
    next if stripped.empty? || stripped.start_with?("#", "!")

    keys_to_sync.each do |key|
      # 兼容前后有空格的 "key = value" 写法
      if stripped =~ /^#{Regexp.escape(key)}\s*=\s*(.+)$/
        unity_values[key] = Regexp.last_match(1).strip
      end
    end
  end

  return if unity_values.empty?

  main_gradle_properties = File.join(project_path, "gradle.properties")
  main_content = File.exist?(main_gradle_properties) ? File.read(main_gradle_properties) : ""
  original_content = main_content.dup

  keys_to_sync.each do |key|
    value = unity_values[key]
    next unless value

    key_regex = /^#{Regexp.escape(key)}\s*=.*$/
    if main_content =~ key_regex
      # 替换已存在的配置行
      main_content = main_content.gsub(key_regex, "#{key}=#{value}")
    else
      # 追加新的配置行
      main_content << "\n" unless main_content.empty? || main_content.end_with?("\n")
      main_content << "#{key}=#{value}\n"
    end
  end

  return if main_content == original_content

  File.write(main_gradle_properties, main_content)
end

.unity_android_project?(project_path) ⇒ Boolean

Returns:

  • (Boolean)


119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/pindo/module/android/android_project_helper.rb', line 119

def unity_android_project?(project_path)
  # 检查 unityLibrary 模块是否存在
  unity_library_path = File.join(project_path, "unityLibrary")
  return false unless File.directory?(unity_library_path)

  # 检查 unityLibrary 的 build.gradle 或 build.gradle.kts 是否存在
  unity_gradle_path = File.join(unity_library_path, "build.gradle")
  unity_gradle_kts_path = File.join(unity_library_path, "build.gradle.kts")
  if File.exist?(unity_gradle_kts_path)
    unity_gradle_path = unity_gradle_kts_path
  elsif !File.exist?(unity_gradle_path)
    return false
  end

  # 检查 build.gradle 中是否包含 Unity 特有的配置
  content = File.read(unity_gradle_path)
  content.include?("com.android.library") && content.include?("BuildIl2Cpp")
end

.unity_as_lib_android_project?(project_path) ⇒ Boolean

Returns:

  • (Boolean)


138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/pindo/module/android/android_project_helper.rb', line 138

def unity_as_lib_android_project?(project_path)
  # Unity 作为 lib 的判断条件简化版:
  # 1. 不是独立的 Unity 导出工程
  return false if unity_android_project?(project_path)

  # 2. 检查是否存在 Unity 目录
  unity_dir = File.join(project_path, "Unity")
  return false unless File.directory?(unity_dir)

  # 3. 检查 Unity 目录下是否有 unityLibrary 目录
  unity_library_dir = File.join(unity_dir, "unityLibrary")
  return false unless File.directory?(unity_library_dir)

  # 4. 检查 Unity/unityLibrary 目录下是否有 build.gradle 或 build.gradle.kts
  gradle_path = File.join(unity_library_dir, "build.gradle")
  gradle_kts_path = File.join(unity_library_dir, "build.gradle.kts")

  File.exist?(gradle_path) || File.exist?(gradle_kts_path)
end