ann-flavor-cocoapods 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 203077b214e8b6dcbf87c8792ea6df6055d34b4b6ea36af00c1c28a94207aab9
4
- data.tar.gz: fcdbfba3ba5e54184fb3d05ace52259c10476947259d550cc4b134009d83e560
3
+ metadata.gz: e23222a95fea37f9217a01fd582557f9310c254e54958ed9414c1a27a67cf3b8
4
+ data.tar.gz: 610b16041b64c3ae03482638436ebd1e965ffc756c29276316d003692086afd5
5
5
  SHA512:
6
- metadata.gz: 1968396623737c7d68439d68cb2fb4d8b8ac6d1da7bba9c32722bc9a0408e3d73fc2b6a4087fbff63231d0d8bd1403949d62e46bf2382a7d9361bd33a7d9fbd1
7
- data.tar.gz: 5f6f3d65b87f5f8c41d52f6201c622fb379da14d81cf7d6338edf5a76bde7168213c2722f067495d0e8571f0ce35415f67e37538f47f4b5d266e297887672cee
6
+ metadata.gz: 02cf0c93b06b3ed7d461a49e3e6582c857ce79b0f6f6da198d23f7d1ba82c3929ac000599c7ab991bf3ae56ff4f5eb45d39f54a3f382468872e8a6197b678821
7
+ data.tar.gz: 9667804d92e78f14267f30c997f100dd22eb80d543693d8863e75efeefd4580e1e979f5371f3ca296c27fda7aadb267ac7ff6d3b619996452498a1b4a7f95180
data/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Fixed
6
+ - **`pod install`/Xcode build failed with "Cycle inside Runner; building
7
+ could produce unreliable results"** against a real project with real
8
+ CocoaPods frameworks (confirmed live: Firebase). The `[ann-flavor] Merge
9
+ Entitlements` and `[ann-flavor] Merge Info.plist` build phases (0.3.0/
10
+ 0.4.0) declared `output_paths` but no `input_paths`, so nothing told
11
+ Xcode's new build system these phases must run after CocoaPods' own
12
+ `[CP] Check Pods Manifest.lock` phase — `build_phases.unshift` only
13
+ controls list order, which CocoaPods' own installer reorders after this
14
+ Podfile hook runs anyway. Fixed by declaring `input_paths =
15
+ ['$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt']` on both
16
+ phases, matching Check Pods Manifest.lock's own declared output — Xcode
17
+ now has an explicit dependency to resolve the ordering, rather than a
18
+ cycle its scheduler was resolving incorrectly.
19
+
20
+ ## 0.4.0
21
+
22
+ ### Added
23
+ - **New `[ann-flavor] Merge Entitlements` build phase composes the final
24
+ `.entitlements` file Xcode signs with at build time**, not sync time: copies
25
+ `ios/Runner/Runner.entitlements` (the generic/base layer, developer-owned —
26
+ if present; unlike `Runner/Info.plist`, most projects have no stock
27
+ entitlements file at all, so a missing base file here starts from an empty
28
+ plist rather than failing), then merges in every key from the current
29
+ flavor's `ann/Entitlements/<flavor>.entitlements` delta (see
30
+ `ann_flutter_flavor` 1.3.0 — that file now holds only the flavor's
31
+ `entitlements:` cascade, not a full copy of `Runner.entitlements`), with the
32
+ flavor's value winning on any key both define. Uses `PlistBuddy`, mirroring
33
+ `[ann-flavor] Merge Info.plist` (0.3.0) exactly — entitlements files and
34
+ Info.plist are both plain Apple XML property lists, so the same
35
+ type-preserving merge logic applies unchanged. Injected only when at least
36
+ one flavor actually needs entitlements (its own cascade, or a stock
37
+ `Runner.entitlements`); removed automatically once nothing needs it anymore.
38
+
39
+ ### Changed
40
+ - **`CODE_SIGN_ENTITLEMENTS` now points at
41
+ `$(DERIVED_FILE_DIR)/Entitlements-merged.entitlements`** (the Merge
42
+ Entitlements phase's output above) instead of directly at
43
+ `ann/Entitlements/<flavor>.entitlements`, whenever anything needs signing —
44
+ extends the same `INFOPLIST_FILE` rewiring from 0.3.0 to entitlements. See
45
+ [plan 039](../../docs/03-planning/features/039-entitlements-build-time-merge.md)
46
+ and the [Entitlements](../../docs/04-userguide/ios/overview.md#entitlements)
47
+ section of the iOS userguide for the full design.
48
+
3
49
  ## 0.3.0
4
50
 
5
51
  ### Added
@@ -155,6 +155,7 @@ module AnnFlavorCocoapods
155
155
  user_project.native_targets.each do |target|
156
156
  if target.symbol_type == :application
157
157
  find_or_create_icon_catalog(target, user_project)
158
+ target_has_entitlements = false
158
159
 
159
160
  ios.flavors.each_key do |flavor_key|
160
161
  flavor = ios.flavors[flavor_key]
@@ -178,16 +179,25 @@ module AnnFlavorCocoapods
178
179
 
179
180
  assert_icon_generated!(user_project, flavor_key, icon_name)
180
181
 
181
- # Unlike the icon catalog and Info.plist, entitlements: is
182
- # opt-in -- ios_generator.dart's _generateEntitlements skips
183
- # creating ios/ann/Entitlements/ entirely when no annspec.yaml
184
- # entitlements: block and no stock Runner.entitlements exist
185
- # anywhere. Only set CODE_SIGN_ENTITLEMENTS when the file
186
- # actually exists on disk; leaving it unset is correct (no
187
- # entitlements to sign with), not an error condition.
188
- entitlements_path = "ann/Entitlements/#{flavor_key}.entitlements"
189
- entitlements_file = File.join(user_project.project_dir.to_s, 'ann', 'Entitlements', "#{flavor_key}.entitlements")
190
- entitlements_path = nil unless File.exist?(entitlements_file)
182
+ # Extended opt-in check (plan 039): CODE_SIGN_ENTITLEMENTS is
183
+ # wired only when there is something to sign with at all --
184
+ # either this flavor's entitlements: cascade, or a stock
185
+ # Runner.entitlements to fall back to. Mirrors
186
+ # ios_generator.dart's _anyFlavorHasEntitlements OR-condition,
187
+ # but evaluated here instead of at sync time -- the actual
188
+ # merge now happens at this (pod install / build time) layer,
189
+ # not sync time, so this is where "is there anything to sign
190
+ # with" needs to be answered from. The Merge Entitlements
191
+ # phase itself is injected once per target below (like Merge
192
+ # Info.plist), not per flavor/build-type here -- it derives
193
+ # $FLAVOR_NAME from $CONFIGURATION at build time, so one
194
+ # shared phase correctly serves every flavor, whether or not
195
+ # that specific flavor has its own entitlements: cascade.
196
+ flavor_entitlements_file = File.join(user_project.project_dir.to_s, 'ann', 'Entitlements', "#{flavor_key}.entitlements")
197
+ stock_entitlements_file = File.join(user_project.project_dir.to_s, 'Runner', 'Runner.entitlements')
198
+ has_entitlements = File.exist?(flavor_entitlements_file) || File.exist?(stock_entitlements_file)
199
+ entitlements_path = has_entitlements ? '$(DERIVED_FILE_DIR)/Entitlements-merged.entitlements' : nil
200
+ target_has_entitlements ||= has_entitlements
191
201
 
192
202
  configure_build_configuration(
193
203
  target, user_project, xcode_config, xcconfig_path,
@@ -203,6 +213,17 @@ module AnnFlavorCocoapods
203
213
  end
204
214
 
205
215
  inject_merge_info_plist_phase(target)
216
+
217
+ # Unlike Merge Info.plist (unconditional -- every flavor always
218
+ # has an Info.plist), Merge Entitlements is only injected when at
219
+ # least one flavor actually needs it (its own cascade, or a
220
+ # stock Runner.entitlements) -- entitlements are opt-in overall,
221
+ # and a phase with nothing to sign would just be dead weight.
222
+ if target_has_entitlements
223
+ inject_merge_entitlements_phase(target)
224
+ else
225
+ remove_merge_entitlements_phase(target)
226
+ end
206
227
  end
207
228
 
208
229
  # Fixes a real pod-install failure: PBXNativeTarget#resolved_build_setting
@@ -891,8 +912,133 @@ module AnnFlavorCocoapods
891
912
  # output_paths) -- suppresses "input file not found" for INFOPLIST_FILE
892
913
  # referencing a path nothing else produces until this phase runs.
893
914
  phase.output_paths = ['$(DERIVED_FILE_DIR)/Info-merged.plist']
915
+ # Same explicit-ordering fix as inject_merge_entitlements_phase below --
916
+ # without this, Xcode's new build system can report a "Cycle inside
917
+ # Runner" against a real project with real Pod frameworks, since
918
+ # output_paths alone isn't enough to order this phase after CocoaPods'
919
+ # own "[CP] Check Pods Manifest.lock" phase.
920
+ phase.input_paths = ['$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt']
894
921
 
895
922
  target.build_phases.unshift(phase) unless existing
896
923
  end
924
+
925
+ # Composes the final .entitlements file Xcode actually signs with, from
926
+ # two layers -- Runner.entitlements (generic/base, developer-owned, may
927
+ # not exist at all) and the flavor-specific ann/Entitlements/<flavor>
928
+ # .entitlements delta (annspec.yaml's entitlements: cascade, opt-in --
929
+ # may also not exist) -- with the flavor layer winning on any key
930
+ # collision. Plan 039: mirrors inject_merge_info_plist_phase (plan 038)
931
+ # exactly -- same PlistBuddy-based merge (entitlements files and Info.plist
932
+ # are both plain Apple XML property lists; PlistBuddy has no notion of
933
+ # file "kind" beyond that), same find-or-update idempotency, same
934
+ # collision-warning behavior. The one real difference: unlike
935
+ # Runner/Info.plist (every Xcode project has one), most projects have no
936
+ # Runner.entitlements at all -- a missing base file here is a normal,
937
+ # non-fatal case (starts from an empty plist dict), not the fatal error
938
+ # Merge Info.plist treats a missing Runner/Info.plist as.
939
+ def self.inject_merge_entitlements_phase(target)
940
+ script = <<~SHELL
941
+ # Injected by ann-flavor-cocoapods: compose the final .entitlements file
942
+ # from two layers -- Runner.entitlements (generic/base, may not exist)
943
+ # and the flavor-specific ann/Entitlements/<flavor>.entitlements delta
944
+ # (annspec.yaml's entitlements: cascade, may also not exist) -- with the
945
+ # flavor layer winning on any key collision. Mirrors [ann-flavor] Merge
946
+ # Info.plist (plan 038) -- see plan 039 for why entitlements needed the
947
+ # same redesign.
948
+ PLISTBUDDY=/usr/libexec/PlistBuddy
949
+ BASE="${SRCROOT}/Runner/Runner.entitlements"
950
+ FLAVOR_NAME="${CONFIGURATION#*-}"
951
+ FLAVOR_FILE="${SRCROOT}/ann/Entitlements/${FLAVOR_NAME}.entitlements"
952
+ OUT="${DERIVED_FILE_DIR}/Entitlements-merged.entitlements"
953
+
954
+ if [ -f "$BASE" ]; then
955
+ cp "$BASE" "$OUT"
956
+ else
957
+ # No stock Runner.entitlements -- start from an empty plist dict
958
+ # rather than failing; the flavor delta (if any) is still applied
959
+ # below.
960
+ cat > "$OUT" <<PLISTEOF
961
+ <?xml version="1.0" encoding="UTF-8"?>
962
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
963
+ <plist version="1.0">
964
+ <dict/>
965
+ </plist>
966
+ PLISTEOF
967
+ fi
968
+
969
+ if [ ! -f "$FLAVOR_FILE" ]; then
970
+ echo "[ann-flavor] Entitlements: no flavor delta found for '$FLAVOR_NAME' at $FLAVOR_FILE -- using Runner.entitlements unmerged."
971
+ exit 0
972
+ fi
973
+
974
+ FLAVOR_KEYS=$("$PLISTBUDDY" -c "Print" "$FLAVOR_FILE" | grep -E '^ [A-Za-z]' | sed 's/ =.*//' | sed 's/^ *//')
975
+
976
+ for KEY in $FLAVOR_KEYS; do
977
+ BASE_VALUE=$("$PLISTBUDDY" -c "Print :$KEY" "$OUT" 2>/dev/null)
978
+ BASE_HAD_KEY=$?
979
+ FLAVOR_VALUE=$("$PLISTBUDDY" -c "Print :$KEY" "$FLAVOR_FILE")
980
+
981
+ if [ "$BASE_HAD_KEY" -eq 0 ] && [ "$BASE_VALUE" != "$FLAVOR_VALUE" ]; then
982
+ echo "[ann-flavor] Entitlements: '$KEY' overridden by flavor value ($FLAVOR_NAME) -- Runner.entitlements' value ignored for this build."
983
+ fi
984
+
985
+ VALUE_XML=$("$PLISTBUDDY" -x -c "Print :$KEY" "$FLAVOR_FILE" | sed -n '/<plist version/,/<\\/plist>/p' | sed '1d;$d')
986
+
987
+ "$PLISTBUDDY" -c "Delete :$KEY" "$OUT" >/dev/null 2>&1
988
+ FRAGMENT=$(mktemp /tmp/ann-flavor-entitlements-fragment.XXXXXX)
989
+ cat > "$FRAGMENT" <<XMLEOF
990
+ <?xml version="1.0" encoding="UTF-8"?>
991
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
992
+ <plist version="1.0">
993
+ <dict>
994
+ <key>$KEY</key>
995
+ $VALUE_XML
996
+ </dict>
997
+ </plist>
998
+ XMLEOF
999
+ "$PLISTBUDDY" -c "Merge \\"$FRAGMENT\\"" "$OUT"
1000
+ rm -f "$FRAGMENT"
1001
+ done
1002
+ SHELL
1003
+
1004
+ existing = target.build_phases.find { |p| p.respond_to?(:name) && p.name == '[ann-flavor] Merge Entitlements' }
1005
+ phase = existing || target.project.new(Xcodeproj::Project::Object::PBXShellScriptBuildPhase)
1006
+ phase.name = '[ann-flavor] Merge Entitlements'
1007
+ phase.shell_path = '/bin/sh'
1008
+ phase.shell_script = script
1009
+ phase.show_env_vars_in_log = '0'
1010
+ phase.output_paths = ['$(DERIVED_FILE_DIR)/Entitlements-merged.entitlements']
1011
+ # Declaring this phase's own output_paths (above) is not enough for
1012
+ # Xcode's new build system to order it correctly relative to CocoaPods'
1013
+ # own "[CP] Check Pods Manifest.lock" phase -- unshift only affects
1014
+ # build_phases list order, which CocoaPods' own installer can (and,
1015
+ # against a real project with real Pod frameworks like Firebase, does)
1016
+ # re-order after this hook runs. Without an explicit input_paths
1017
+ # dependency, Xcode's dependency-cycle detector can flag a real cycle
1018
+ # ("Cycle inside Runner") between this phase's output (consumed by
1019
+ # ProcessProductPackaging for the merged entitlements) and Check Pods
1020
+ # Manifest.lock's own output, even though no true cycle exists --
1021
+ # explicitly declaring Check Pods Manifest.lock's result file as an
1022
+ # input here removes the ambiguity Xcode's scheduler was resolving
1023
+ # incorrectly. Confirmed live against ledger (real Firebase Pods).
1024
+ phase.input_paths = ['$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt']
1025
+
1026
+ target.build_phases.unshift(phase) unless existing
1027
+ end
1028
+
1029
+ # Removes a previously-injected Merge Entitlements phase when no flavor
1030
+ # needs entitlements anymore (annspec.yaml's entitlements: cascade was
1031
+ # removed, and no stock Runner.entitlements exists either) -- entitlements
1032
+ # are opt-in overall, so a stale phase with nothing to sign would be dead
1033
+ # weight left in project.pbxproj. Uses ObjectList#delete (not plain
1034
+ # Array#reject!/select), same rationale as the stale "[ann-flavor] Remove
1035
+ # Firebase plist" cleanup above -- plain array removal only drops the
1036
+ # phase from target.build_phases, leaving the object itself permanently
1037
+ # orphaned in the project's global objects table.
1038
+ def self.remove_merge_entitlements_phase(target)
1039
+ target.build_phases.select do |p|
1040
+ p.respond_to?(:name) && p.name == '[ann-flavor] Merge Entitlements'
1041
+ end.each { |p| target.build_phases.delete(p) }
1042
+ end
897
1043
  end
898
1044
  end
@@ -1,3 +1,3 @@
1
1
  module AnnFlavorCocoapods
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.1"
3
3
  end
@@ -86,18 +86,18 @@ module AnnFlavorCore
86
86
 
87
87
  IosDefault = Struct.new(
88
88
  :id, :name, :version_name, :version_code, :main_file,
89
- :admob, :icon, :credentials, :firebase, :build_types, :custom, :dart_defines, :info_plist, :entitlements, :sdk,
89
+ :admob, :icon, :credentials, :firebase, :build_types, :custom, :dart_defines, :env, :info_plist, :entitlements, :sdk,
90
90
  keyword_init: true
91
91
  ) do
92
- def self.defaults = new(build_types: {}, custom: {}, dart_defines: DartDefines.defaults)
92
+ def self.defaults = new(build_types: {}, custom: {}, dart_defines: DartDefines.defaults, env: {})
93
93
  end
94
94
 
95
95
  IosFlavor = Struct.new(
96
96
  :id, :id_suffix, :name, :name_suffix, :version_name, :version_code,
97
- :main_file, :admob, :icon, :firebase, :stores, :credentials, :build_types, :custom, :dart_defines, :info_plist, :entitlements,
97
+ :main_file, :admob, :icon, :firebase, :stores, :credentials, :build_types, :custom, :dart_defines, :env, :info_plist, :entitlements,
98
98
  keyword_init: true
99
99
  ) do
100
- def self.defaults = new(id_suffix: '', name_suffix: '', build_types: {}, custom: {}, dart_defines: DartDefines.defaults)
100
+ def self.defaults = new(id_suffix: '', name_suffix: '', build_types: {}, custom: {}, dart_defines: DartDefines.defaults, env: {})
101
101
  end
102
102
 
103
103
  IosCredentials = Struct.new(:signing, :app_store, keyword_init: true)
@@ -160,10 +160,10 @@ module AnnFlavorCore
160
160
  :id_suffix, :name_suffix,
161
161
  :firebase, :auth, :admob,
162
162
  :minify_enabled, :shrink_resources, :lint_check_release_builds,
163
- :ndk_version, :ndk_debug_symbol_level, :ndk_abi_filters, :custom, :dart_defines, :info_plist, :entitlements,
163
+ :ndk_version, :ndk_debug_symbol_level, :ndk_abi_filters, :custom, :dart_defines, :env, :info_plist, :entitlements,
164
164
  keyword_init: true
165
165
  ) do
166
- def self.defaults = new(id_suffix: '', name_suffix: '', ndk_abi_filters: [], custom: {}, dart_defines: DartDefines.defaults)
166
+ def self.defaults = new(id_suffix: '', name_suffix: '', ndk_abi_filters: [], custom: {}, dart_defines: DartDefines.defaults, env: {})
167
167
  end
168
168
 
169
169
  FirebaseConfig = Struct.new(:project_id, :config_file, :service_account, :target, keyword_init: true)
@@ -43,6 +43,14 @@ module AnnFlavorCore
43
43
  end
44
44
  end
45
45
 
46
+ # `env:` — a flat string key-value map, cascading the same as `custom`/
47
+ # `dart_defines` but with no nested groups: generic native build-setting
48
+ # overrides (e.g. iOS xcconfig), not the Dart-only `custom` mechanism.
49
+ def self.parse_env(m)
50
+ return {} unless m.is_a?(Hash)
51
+ m.transform_values { |v| v.nil? ? '' : v.to_s }
52
+ end
53
+
46
54
  def self.normalize_custom_value(v)
47
55
  return v.map { |e| e.nil? ? '' : e.to_s } if v.is_a?(Array)
48
56
  v
@@ -162,6 +170,7 @@ module AnnFlavorCore
162
170
  build_types: parse_build_type_configs(h(m, 'build_types')),
163
171
  custom: parse_custom_config(h(m, 'custom')),
164
172
  dart_defines: parse_dart_defines(h(m, 'dart_defines')),
173
+ env: parse_env(h(m, 'env')),
165
174
  info_plist: m['info_plist'],
166
175
  entitlements: m['entitlements'],
167
176
  sdk: (parse_ios_sdk(h(m, 'sdk')) unless h(m, 'sdk').empty?),
@@ -189,6 +198,7 @@ module AnnFlavorCore
189
198
  build_types: parse_build_type_configs(h(m, 'build_types')),
190
199
  custom: parse_custom_config(h(m, 'custom')),
191
200
  dart_defines: parse_dart_defines(h(m, 'dart_defines')),
201
+ env: parse_env(h(m, 'env')),
192
202
  info_plist: m['info_plist'],
193
203
  entitlements: m['entitlements'],
194
204
  )
@@ -322,6 +332,7 @@ module AnnFlavorCore
322
332
  ndk_abi_filters: (v['ndkAbiFilters'].is_a?(Array) ? v['ndkAbiFilters'] : []),
323
333
  custom: parse_custom_config(h(v, 'custom')),
324
334
  dart_defines: parse_dart_defines(h(v, 'dart_defines')),
335
+ env: parse_env(h(v, 'env')),
325
336
  info_plist: v['info_plist'],
326
337
  entitlements: v['entitlements'],
327
338
  )
@@ -27,7 +27,10 @@ module AnnFlavorCore
27
27
  # fields -- same free-form treatment as custom/dart_defines, and deliberately
28
28
  # NOT in RECURSION_TARGETS since there is no single fixed struct type to
29
29
  # recurse into.
30
- FREE_FORM_FIELDS = %i[custom dart_defines info_plist entitlements].freeze
30
+ #
31
+ # env is a flat string Hash of arbitrary, user-declared native
32
+ # build-setting keys (e.g. REVERSED_CLIENT_ID) -- same free-form treatment.
33
+ FREE_FORM_FIELDS = %i[custom dart_defines env info_plist entitlements].freeze
31
34
 
32
35
  # (containing_struct, field_name) => target_struct — only the "does this field
33
36
  # recurse, and into what" relationship; the valid-keys-within-that-struct question
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ann-flavor-cocoapods
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ANN Solutions