Module: AnnFlavorCocoapods::PodfileHelper

Defined in:
lib/ann_flavor_cocoapods/podfile_helper.rb

Class Method Summary collapse

Class Method Details

.bundle_id(flavor_key, project_root: Dir.pwd) ⇒ Object

Returns the bundle ID for a given flavor key, merging defaults.



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/ann_flavor_cocoapods/podfile_helper.rb', line 69

def self.bundle_id(flavor_key, project_root: Dir.pwd)
  spec    = SpecLoader.load(project_root)
  flavors = SpecLoader.ios_flavors(spec)
  default = SpecLoader.ios_default(spec)

  flavor_cfg = flavors[flavor_key.to_s] || {}
  merged     = default.merge(flavor_cfg)

  base   = merged['id'] || default['id'] || ''
  suffix = flavor_cfg['id_suffix'] || ''
  "#{base}#{suffix}"
end

.bundle_id_for_build_type(flavor_key, build_type, project_root: Dir.pwd) ⇒ Object

Returns the bundle ID for a given flavor key and build type (e.g. "debug"), applying build_types.id_suffix from either flavor or default.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/ann_flavor_cocoapods/podfile_helper.rb', line 84

def self.bundle_id_for_build_type(flavor_key, build_type, project_root: Dir.pwd)
  spec    = SpecLoader.load(project_root)
  flavors = SpecLoader.ios_flavors(spec)
  default = SpecLoader.ios_default(spec)

  flavor_cfg = flavors[flavor_key.to_s] || {}
  merged     = default.merge(flavor_cfg)

  # Merge build_types maps: flavor overrides default per key
  default_build_types = default.dig('build_types') || {}
  flavor_build_types  = flavor_cfg.dig('build_types') || {}
  build_types         = default_build_types.merge(flavor_build_types)

  base        = merged['id'] || default['id'] || ''
  id_suffix   = flavor_cfg['id_suffix'] || ''
  bt_suffix   = build_types.dig(build_type.to_s, 'id_suffix') || ''
  "#{base}#{id_suffix}#{bt_suffix}"
end

.configure_firebase(installer, project_root: Dir.pwd) ⇒ Object

Call inside your Podfile's post_install hook to wire per-flavor Firebase GoogleService-Info.plist into the Xcode build.

Example Podfile usage:

require 'ann_flavor_cocoapods'
post_install do |installer|
AnnFlavorCocoapods::PodfileHelper.configure_firebase(installer)
end

For each iOS flavor that has config_file set, this injects two build phases into the Runner target (or the matching umbrella target):

- A pre-build shell script that copies the correct plist to
${SRCROOT}/Runner/GoogleService-Info.plist at build time.
- A post-build shell script that removes the copied file so the workspace
stays clean and a mis-matched plist cannot accidentally survive.

Gate: only runs when integrations.firebase is true in annspec.yaml.



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
# File 'lib/ann_flavor_cocoapods/podfile_helper.rb', line 23

def self.configure_firebase(installer, project_root: Dir.pwd)
  spec = SpecLoader.load(project_root)

  # integrations.firebase must be explicitly enabled (REQ-FIRE-00010).
  return unless spec.dig('app', 'integrations', 'firebase') == true

  flavors = SpecLoader.ios_flavors(spec)
  default = SpecLoader.ios_default(spec)

  # Build a map: configuration_name → config_file path.
  # Xcode configuration names are capitalized: Release-<flavor>, Debug-<flavor>.
  config_map = {}
  flavors.each do |flavor_key, flavor_cfg|
    merged = default.merge(flavor_cfg || {})

    default_build_types = default.dig('build_types') || {}
    flavor_build_types  = (flavor_cfg || {}).dig('build_types') || {}
    build_types         = default_build_types.merge(flavor_build_types)

    %w[release debug].each do |bt|
      bt_fb      = build_types.dig(bt, 'firebase') || {}
      fb_file    = bt_fb['config_file']
      project_id = bt_fb['project_id']

      if fb_file.nil? && project_id
        # project_id mode — derive plist from stable generated path.
        fb_file = "lib/generated/firebase/GoogleService-Info-#{flavor_key}-#{bt}.plist"
      end

      next unless fb_file

      # Map Xcode config name (e.g. "Release-ledger_in") → plist path
      xcode_config = "#{bt.capitalize}-#{flavor_key}"
      config_map[xcode_config] = fb_file

      warn_if_static_plist(project_root, xcode_config, fb_file)
    end
  end

  return if config_map.empty?
  return if installer.nil?

  inject_build_phases(installer, project_root, config_map)
end

.inject_build_phases(installer, project_root, config_map) ⇒ Object

Injects pre-build and post-build shell script phases into the Runner (or umbrella) target for every native target that matches the project.



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
169
170
171
172
# File 'lib/ann_flavor_cocoapods/podfile_helper.rb', line 119

def self.inject_build_phases(installer, project_root, config_map)
  # Build the per-config shell that selects the right source file.
  # Uses a case statement keyed on $CONFIGURATION (Xcode build setting).
  case_body = config_map.map do |xcode_config, plist_path|
    "  #{xcode_config}) SRC_PLIST=\"${SRCROOT}/../#{plist_path}\" ;;"
  end.join("\n")

  pre_build_script = <<~SHELL
    # Injected by ann-flavor-cocoapods: copy per-flavor GoogleService-Info.plist
    case "$CONFIGURATION" in
    #{case_body}
      *) echo "ann-flavor-cocoapods: no Firebase plist for $CONFIGURATION"; exit 0 ;;
    esac
    DEST="${SRCROOT}/Runner/GoogleService-Info.plist"
    if [ ! -f "$SRC_PLIST" ]; then
      echo "ann-flavor-cocoapods ERROR: Firebase plist not found: $SRC_PLIST"
      exit 1
    fi
    cp "$SRC_PLIST" "$DEST"
    echo "ann-flavor-cocoapods: copied $SRC_PLIST -> $DEST"
  SHELL

  post_build_script = <<~SHELL
    # Injected by ann-flavor-cocoapods: remove copied GoogleService-Info.plist after build
    DEST="${SRCROOT}/Runner/GoogleService-Info.plist"
    rm -f "$DEST"
    echo "ann-flavor-cocoapods: removed $DEST"
  SHELL

  installer.pods_project.targets.each do |target|
    next unless target.name == 'Pods-Runner'

    # Avoid duplicate injection on repeated pod install runs.
    next if target.build_phases.any? { |p| p.respond_to?(:shell_script) &&
              p.shell_script.include?('ann-flavor-cocoapods: copy per-flavor') }

    pre  = target.project.new(Xcodeproj::Project::Object::PBXShellScriptBuildPhase)
    pre.name             = '[ann-flavor] Copy Firebase plist'
    pre.shell_path       = '/bin/sh'
    pre.shell_script     = pre_build_script
    pre.show_env_vars_in_log = '0'
    target.build_phases.unshift(pre)

    post = target.project.new(Xcodeproj::Project::Object::PBXShellScriptBuildPhase)
    post.name            = '[ann-flavor] Remove Firebase plist'
    post.shell_path      = '/bin/sh'
    post.shell_script    = post_build_script
    post.show_env_vars_in_log = '0'
    target.build_phases << post

    puts "ann-flavor-cocoapods: injected Firebase build phases into #{target.name} " \
         "(#{config_map.size} configuration(s))"
  end
end

.warn_if_static_plist(project_root, xcode_config, config_file) ⇒ Object

Warn if a static GoogleService-Info.plist already exists in the project; a higher-priority static file would shadow the one we copy at build time.



107
108
109
110
111
112
113
114
115
# File 'lib/ann_flavor_cocoapods/podfile_helper.rb', line 107

def self.warn_if_static_plist(project_root, xcode_config, config_file)
  static_path = File.join(project_root, 'ios', 'Runner', 'GoogleService-Info.plist')
  if File.exist?(static_path)
    puts "ann-flavor-cocoapods WARNING: A static GoogleService-Info.plist exists at " \
         "ios/Runner/GoogleService-Info.plist. It will be overwritten at build time " \
         "for configuration '#{xcode_config}' (source: #{config_file}). " \
         "Remove the static file to silence this warning."
  end
end