Class: Pindo::XcodeBuildConfig

Inherits:
Object
  • Object
show all
Defined in:
lib/pindo/module/xcode/xcodebuildconfig.rb

Class Method Summary collapse

Class Method Details

.add_single_scheme(plist_dict, scheme_value) ⇒ Boolean

添加单个scheme到plist字典中

Parameters:

  • plist_dict (Hash)

    Info.plist字典

  • scheme_value (String)

    scheme值

Returns:

  • (Boolean)

    是否添加了新scheme



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/pindo/module/xcode/xcodebuildconfig.rb', line 252

def self.add_single_scheme(plist_dict, scheme_value)
    return false if scheme_value.nil? || scheme_value.empty?

    # 检查是否已存在
    return false if plist_dict["CFBundleURLTypes"].any? { |item| item["CFBundleURLName"] == scheme_value }

    # 创建新的URL Type
    url_type = {
        "CFBundleTypeRole" => "Editor",
        "CFBundleURLName" => scheme_value,
        "CFBundleURLSchemes" => [scheme_value]
    }

    plist_dict["CFBundleURLTypes"] << url_type
    return true
end

.add_url_schemes(project_dir: nil, scheme_name: nil) ⇒ Boolean

添加URL Schemes到iOS工程的Info.plist

Parameters:

  • project_dir (String) (defaults to: nil)

    iOS项目目录路径

  • scheme_name (String) (defaults to: nil)

    要添加的scheme名称(可选)

Returns:

  • (Boolean)

    是否成功添加

Raises:

  • (ArgumentError)


196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/pindo/module/xcode/xcodebuildconfig.rb', line 196

def self.add_url_schemes(project_dir: nil, scheme_name: nil)
    raise ArgumentError, "项目目录不能为空" if project_dir.nil?

    project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
    return false if project_fullname.nil?

    info_plist_path = nil
    bundleid_scheme_name = nil

    project_obj = Xcodeproj::Project.open(project_fullname)
    project_obj.targets.each do |target|
        if target.product_type.to_s.eql?("com.apple.product-type.application")
            temp_info_file = target.build_configurations.first.build_settings['INFOPLIST_FILE']
            if temp_info_file && !temp_info_file.empty?
                info_plist_path = File.join(project_dir, temp_info_file)
            end
            bundleid_scheme_name = target.build_configurations.first.build_settings['PRODUCT_BUNDLE_IDENTIFIER']
            break  # 找到第一个application target即可
        end
    end

    return false unless info_plist_path && File.exist?(info_plist_path)

    info_plist_dict = Xcodeproj::Plist.read_from_path(info_plist_path)
    info_plist_dict["CFBundleURLTypes"] ||= []
    schemes_added = []

    # 添加基于项目名的scheme
    if scheme_name && !scheme_name.empty?
        scheme_value = scheme_name.to_s.gsub(/[^a-zA-Z0-9]/, '').downcase
        if add_single_scheme(info_plist_dict, scheme_value)
            schemes_added << scheme_value
        end
    end

    # 添加基于Bundle ID的scheme(从PRODUCT_BUNDLE_IDENTIFIER中读取)
    if bundleid_scheme_name && !bundleid_scheme_name.empty?
        bundleid_scheme_value = bundleid_scheme_name.gsub(/[^a-zA-Z0-9]/, '').downcase
        if add_single_scheme(info_plist_dict, bundleid_scheme_value)
            schemes_added << bundleid_scheme_value
        end
    end

    # 保存修改
    if schemes_added.any?
        Xcodeproj::Plist.write_to_path(info_plist_dict, info_plist_path)
        schemes_added.each { |s| puts "  ✓ 已添加URL Scheme: #{s}" }
    end

    return true
end

.download_and_replace_icon_from_url(project_dir: nil, icon_url: nil) ⇒ Boolean

从URL下载Icon并替换iOS工程的Icon

Parameters:

  • project_dir (String) (defaults to: nil)

    iOS项目目录路径

  • icon_url (String) (defaults to: nil)

    Icon的下载URL地址

Returns:

  • (Boolean)

    是否成功下载并替换Icon



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
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/pindo/module/xcode/xcodebuildconfig.rb', line 428

def self.download_and_replace_icon_from_url(project_dir: nil, icon_url: nil)
    # 参数校验 - 不抛出异常,返回 false
    if project_dir.nil?
        Funlog.instance.fancyinfo_error("Icon 替换失败: 项目目录不能为空")
        return false
    end

    if icon_url.nil? || icon_url.empty?
        Funlog.instance.fancyinfo_error("Icon 替换失败: Icon URL不能为空")
        return false
    end

    puts "\n检测到项目 Icon URL: #{icon_url}"

    # 创建临时目录(所有 icon 处理都在此目录内完成)
    temp_icon_dir = File.join(project_dir, ".pindo_temp_icon")
    icon_download_path = nil
    replace_success = false

    begin
        FileUtils.mkdir_p(temp_icon_dir) unless File.exist?(temp_icon_dir)
        icon_download_path = File.join(temp_icon_dir, "downloaded_icon.png")

        # 下载 icon
        Funlog.instance.fancyinfo_start("正在从 JPS 下载项目 Icon...")
        URI.open(icon_url) do |file|
            File.binwrite(icon_download_path, file.read)
        end
        Funlog.instance.fancyinfo_success("Icon 下载成功!")

        # 验证文件已下载
        unless File.exist?(icon_download_path)
            Funlog.instance.fancyinfo_error("Icon 下载失败: 文件未创建")
            return false
        end

        # 生成新的 icon 目录(放在临时目录内部)
        new_icon_dir = File.join(temp_icon_dir, "generated_icons")

        # 创建各种尺寸的 icon
        Funlog.instance.fancyinfo_start("正在创建 icon...")
        XcodeResHelper.create_icons(
            icon_name: icon_download_path,
            new_icon_dir: new_icon_dir
        )
        Funlog.instance.fancyinfo_success("创建 icon 完成!")

        # 替换工程中的 icon
        Funlog.instance.fancyinfo_start("正在替换 icon...")
        XcodeResHelper.install_icon(
            proj_dir: project_dir,
            new_icon_dir: new_icon_dir
        )
        Funlog.instance.fancyinfo_success("替换 icon 完成!")
        replace_success = true

    rescue => e
        Funlog.instance.fancyinfo_error("Icon 处理失败: #{e.message}")
        puts e.backtrace
        replace_success = false
    ensure
        # 清理临时文件(无论成功失败都清理)
        # FileUtils.rm_rf(temp_icon_dir) if temp_icon_dir && File.exist?(temp_icon_dir)
    end

    return replace_success
end

.update_entitlements_config(project_dir: nil, config_file: nil) ⇒ Boolean

更新entitlements配置根据entitlements文件内容,同步更新config.json中的配置如果entitlements中没有icloud或app group,则从config.json中移除对应配置

Parameters:

  • project_dir (String) (defaults to: nil)

    项目目录路径

  • config_file (String) (defaults to: nil)

    config.json文件路径

Returns:

  • (Boolean)

    是否成功更新

Raises:

  • (ArgumentError)


368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/pindo/module/xcode/xcodebuildconfig.rb', line 368

def self.update_entitlements_config(project_dir: nil, config_file: nil)
    raise ArgumentError, "项目目录不能为空" if project_dir.nil?
    raise ArgumentError, "配置文件路径不能为空" if config_file.nil?

    project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
    return false if project_fullname.nil?

    entitlements_plist_path = nil
    project_obj = Xcodeproj::Project.open(project_fullname)

    project_obj.targets.each do |target|
        if target.product_type.to_s.eql?("com.apple.product-type.application")
            temp_entitlements_file = target.build_configurations.first.build_settings['CODE_SIGN_ENTITLEMENTS']
            if temp_entitlements_file && !temp_entitlements_file.empty?
                entitlements_plist_path = File.join(project_dir, temp_entitlements_file)
            end
            break # 找到第一个application target即可
        end
    end

    # 处理entitlements配置
    if entitlements_plist_path && File.exist?(entitlements_plist_path)
        config_json = nil
        if File.exist?(config_file)
            config_json = JSON.parse(File.read(config_file))
        end

        return false if config_json.nil?

        entitlements_plist_dict = Xcodeproj::Plist.read_from_path(entitlements_plist_path)

        # 如果entitlements中没有icloud配置,从config.json中移除
        if entitlements_plist_dict["com.apple.developer.icloud-container-identifiers"].nil?
            if config_json["app_info"] && config_json["app_info"]['app_icloud_id']
                config_json["app_info"].delete('app_icloud_id')
            end
        end

        # 如果entitlements中没有app group配置,从config.json中移除
        if entitlements_plist_dict["com.apple.security.application-groups"].nil?
            if config_json["app_info"] && config_json["app_info"]['app_group_id']
                config_json["app_info"].delete('app_group_id')
            end
        end

        # 保存更新后的config.json
        File.open(config_file, "w") do |f|
            f.write(JSON.pretty_generate(config_json))
        end

        return true
    end

    return false
end

.update_ios_project_version(project_dir: nil, version: nil, build_number: nil) ⇒ Boolean

更新iOS工程版本号

Parameters:

  • project_dir (String) (defaults to: nil)

    iOS项目目录路径

  • version (String) (defaults to: nil)

    版本号

  • build_number (Integer) (defaults to: nil)

    Build号

Returns:

  • (Boolean)

    是否成功更新

Raises:

  • (ArgumentError)


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
304
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
331
332
333
# File 'lib/pindo/module/xcode/xcodebuildconfig.rb', line 274

def self.update_ios_project_version(project_dir: nil, version: nil, build_number: nil)
    raise ArgumentError, "项目目录不能为空" if project_dir.nil?
    raise ArgumentError, "版本号不能为空" if version.nil?
    raise ArgumentError, "Build号不能为空" if build_number.nil?

    Funlog.instance.fancyinfo_start("正在更新iOS工程版本信息...")

    begin
        # 查找.xcodeproj文件
        xcodeproj_path = find_xcodeproj(project_dir)

        if xcodeproj_path.nil?
            Funlog.instance.fancyinfo_error("未找到Xcode项目文件")
            return false
        end

        # 打开Xcode项目
        project = Xcodeproj::Project.open(xcodeproj_path)

        # 更新所有application类型target的版本号
        updated_targets = []
        project.targets.each do |target|
            # 只处理application类型的target
            if target.product_type.to_s.eql?("com.apple.product-type.application")
                target.build_configurations.each do |config|
                    # 更新版本号
                    config.build_settings['MARKETING_VERSION'] = version
                    config.build_settings['CURRENT_PROJECT_VERSION'] = build_number.to_s

                    # 兼容旧的设置方式,通过Info.plist更新
                    if config.build_settings['INFOPLIST_FILE']
                        info_plist_path = File.join(project_dir, config.build_settings['INFOPLIST_FILE'])
                        if File.exist?(info_plist_path)
                            update_info_plist(info_plist_path, version, build_number.to_s)
                        end
                    end
                end
                updated_targets << target.name
            end
        end

        # 保存项目
        project.save

        if updated_targets.empty?
            Funlog.instance.fancyinfo_error("未找到需要更新的application target")
            return false
        else
            Funlog.instance.fancyinfo_success("iOS版本更新完成!")
            puts "  ✓ 版本号已更新: #{version}"
            puts "  ✓ Build号已更新: #{build_number}"
            puts "  ✓ 更新的Target: #{updated_targets.join(', ')}"
            return true
        end

    rescue StandardError => e
        Funlog.instance.fancyinfo_error("更新iOS版本失败: #{e.message}")
        return false
    end
end

.update_project_with_packagename(project_dir: nil, package_name: nil, project_id: nil) ⇒ Boolean

使用package_name一次性更新Display Name、Bundle ID和URL Schemes(基于package_name)优化版本:只读写一次plist文件,提高性能注意:此函数只添加基于package_name的URL Scheme,基于实际Bundle ID的URL Scheme

将在update_url_schemes_with_bundleid函数中统一添加(在证书配置之后)

Parameters:

  • project_dir (String) (defaults to: nil)

    iOS项目目录路径

  • package_name (String) (defaults to: nil)

    工作流的package_name(如:“Test Demo”)

  • project_id (String) (defaults to: nil)

    JPS项目ID(可选,用于添加快捷操作)

Returns:

  • (Boolean)

    是否成功更新

Raises:

  • (ArgumentError)


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
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
113
114
115
116
117
118
119
120
# File 'lib/pindo/module/xcode/xcodebuildconfig.rb', line 19

def self.update_project_with_packagename(project_dir: nil, package_name: nil, project_id: nil)
    raise ArgumentError, "项目目录不能为空" if project_dir.nil?
    raise ArgumentError, "Package Name不能为空" if package_name.nil?

    # 生成各种值
    display_name = package_name.gsub(/[^a-zA-Z0-9\s]/, '').gsub(/\s+/, '')
    bundle_id_suffix = package_name.gsub(/[^a-zA-Z0-9]/, '').downcase
    final_bundle_id = "com.heroneverdie101.#{bundle_id_suffix}"
    package_scheme = package_name.gsub(/[^a-zA-Z0-9]/, '').downcase

    project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
    return false if project_fullname.nil?

    info_plist_path = nil
    app_target = nil

    project_obj = Xcodeproj::Project.open(project_fullname)
    project_obj.targets.each do |target|
        if target.product_type.to_s.eql?("com.apple.product-type.application")
            temp_info_file = target.build_configurations.first.build_settings['INFOPLIST_FILE']
            if temp_info_file && !temp_info_file.empty?
                info_plist_path = File.join(project_dir, temp_info_file)
            end
            app_target = target
            break  # 找到第一个application target即可
        end
    end

    return false unless info_plist_path && File.exist?(info_plist_path)

    # 一次性读取plist
    info_plist_dict = Xcodeproj::Plist.read_from_path(info_plist_path)

    # 1. 更新Info Plist Display Name
    info_plist_dict["CFBundleDisplayName"] = display_name

    
    # 2. 更新 Bundle ID(plist)
    info_plist_dict["CFBundleIdentifier"] = final_bundle_id

    # 3. 添加 URL Schemes
    info_plist_dict["CFBundleURLTypes"] ||= []

    # 添加基于 package_name 的 URL Scheme
    # 注意:基于 Bundle ID 的 URL Scheme 将在 update_url_schemes_with_bundleid 函数中统一添加
    if add_single_scheme(info_plist_dict, package_scheme)
        puts "  ✓ 已添加URL Scheme: #{package_scheme} (基于 Package Name)"
    end

    # 4. 添加 JPS 快捷操作(UIApplicationShortcutItems)
    if project_id && !project_id.to_s.empty?
        # 创建或获取 UIApplicationShortcutItems 数组
        info_plist_dict["UIApplicationShortcutItems"] ||= []

        # 构建快捷操作类型
        shortcut_type = "jps_project?#{project_id}"
        jps_title = "访问 JPS 详情"

        # 先查找是否存在标题为"访问 JPS 详情"的快捷操作(无论其 type 是什么)
        existing_shortcut = info_plist_dict["UIApplicationShortcutItems"].find do |item|
            item["UIApplicationShortcutItemTitle"] == jps_title
        end

        if existing_shortcut
            # 如果存在,检查 type 是否一致
            if existing_shortcut["UIApplicationShortcutItemType"] == shortcut_type
                puts "  ✓ JPS 快捷操作已存在: #{shortcut_type}"
            else
                # type 不一致,更新为新的 type
                old_type = existing_shortcut["UIApplicationShortcutItemType"]
                existing_shortcut["UIApplicationShortcutItemType"] = shortcut_type
                existing_shortcut["UIApplicationShortcutItemIconType"] = "UIApplicationShortcutIconTypeBookmark"
                puts "  ✓ 已更新 JPS 快捷操作: #{old_type} -> #{shortcut_type}"
            end
        else
            # 不存在,添加新的快捷操作
            jps_shortcut = {
                "UIApplicationShortcutItemType" => shortcut_type,
                "UIApplicationShortcutItemTitle" => jps_title,
                "UIApplicationShortcutItemIconType" => "UIApplicationShortcutIconTypeBookmark"
            }
            info_plist_dict["UIApplicationShortcutItems"] << jps_shortcut
            puts "  ✓ 已添加 JPS 快捷操作: #{shortcut_type}"
        end
    end

    # 一次性写入plist
    Xcodeproj::Plist.write_to_path(info_plist_dict, info_plist_path)

    # 5. 更新 Xcode 项目中的 PRODUCT_BUNDLE_IDENTIFIER
    if app_target
        app_target.build_configurations.each do |config|
            config.build_settings['INFOPLIST_KEY_CFBundleDisplayName'] = display_name
            config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = final_bundle_id
        end
        project_obj.save
    end

    puts "  ✓ Display Name 已更新为: #{display_name}"
    puts "  ✓ Bundle ID 已更新为: #{final_bundle_id}"
    return true
end

.update_url_schemes_with_bundleid(project_dir: nil, package_name: nil) ⇒ Boolean

根据当前项目中的实际 Bundle ID 重新更新 URL Schemes 此函数用于在证书配置后更新 URL Schemes,确保与最终的 Bundle ID 匹配

Parameters:

  • project_dir (String) (defaults to: nil)

    iOS项目目录路径

  • package_name (String) (defaults to: nil)

    工作流的package_name(如:“Test Demo”)

Returns:

  • (Boolean)

    是否成功更新

Raises:

  • (ArgumentError)


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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/pindo/module/xcode/xcodebuildconfig.rb', line 127

def self.update_url_schemes_with_bundleid(project_dir: nil, package_name: nil)
    raise ArgumentError, "项目目录不能为空" if project_dir.nil?

    project_fullname = Dir.glob(File.join(project_dir, "/*.xcodeproj")).max_by {|f| File.mtime(f)}
    return false if project_fullname.nil?

    info_plist_path = nil
    current_bundle_id = nil

    project_obj = Xcodeproj::Project.open(project_fullname)
    project_obj.targets.each do |target|
        if target.product_type.to_s.eql?("com.apple.product-type.application")
            temp_info_file = target.build_configurations.first.build_settings['INFOPLIST_FILE']
            if temp_info_file && !temp_info_file.empty?
                info_plist_path = File.join(project_dir, temp_info_file)
            end
            # 获取当前实际的 Bundle ID
            current_bundle_id = target.build_configurations.first.build_settings['PRODUCT_BUNDLE_IDENTIFIER']
            break  # 找到第一个application target即可
        end
    end

    return false unless info_plist_path && File.exist?(info_plist_path)
    return false unless current_bundle_id && !current_bundle_id.empty?

    puts "\n根据更新后的 Bundle ID 重新配置 URL Schemes:"
    puts "  当前 Bundle ID: #{current_bundle_id}"

    # 读取 plist
    info_plist_dict = Xcodeproj::Plist.read_from_path(info_plist_path)
    info_plist_dict["CFBundleURLTypes"] ||= []

    # 生成 schemes
    package_scheme = package_name ? package_name.gsub(/[^a-zA-Z0-9]/, '').downcase : nil
    bundle_scheme = current_bundle_id.gsub(/[^a-zA-Z0-9]/, '').downcase

    schemes_updated = []

    # 添加基于 package_name 的 URL Scheme(如果提供了 package_name)
    if package_scheme && !package_scheme.empty?
        if add_single_scheme(info_plist_dict, package_scheme)
            schemes_updated << package_scheme
            puts "  ✓ 已更新 URL Scheme: #{package_scheme} (基于 Package Name)"
        else
            puts "  ✓ URL Scheme 已存在: #{package_scheme} (基于 Package Name)"
        end
    end

    # 添加基于实际 Bundle ID 的 URL Scheme
    if add_single_scheme(info_plist_dict, bundle_scheme)
        schemes_updated << bundle_scheme
        puts "  ✓ 已更新 URL Scheme: #{bundle_scheme} (基于更新后的 Bundle ID)"
    else
        puts "  ✓ URL Scheme 已存在: #{bundle_scheme} (基于更新后的 Bundle ID)"
    end

    # 写入 plist
    if schemes_updated.any?
        Xcodeproj::Plist.write_to_path(info_plist_dict, info_plist_path)
        puts "  ✓ URL Schemes 更新完成"
    end

    return true
end