ann-flavor-cocoapods 0.4.4 → 0.4.6

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: 26c819f56bb3a959737651233fbdbed8e0bd887ddcdfbf0fd0c157c26b99a011
4
- data.tar.gz: d322f020327b3b62f3eb02922a01d1af90040a2b5595614a37795eacf311a2cf
3
+ metadata.gz: 0753376f0ddae76a1894acb795e3e1cac3f6a975a79b799e0cccc79c9db2760b
4
+ data.tar.gz: cc9c6297e39a2702dc119975168fc92926eb2db5bdfc9c23ff44eb9076b8b568
5
5
  SHA512:
6
- metadata.gz: 85564c5fcc276b9f448381fc8cb3d334ca65d9bedbccd2b40fe25b46e43775afc50f66b7a2c6394ab032a118ecb8e3b1335b596c89f0c5dc79d7945bc52bc7ac
7
- data.tar.gz: e2e76a0da4568f8bc5a040d1f3f0422a8844b1da28b0b34b6324fda2c56d7c47eceec963ae644c82044f266b20193fe29d9572cce60a7dbf59933ac8df9a2221
6
+ metadata.gz: 2b56c73fc8ced1dbd22e58e2780aded019fc5cf87e76ed6df5fac6ec6ab4650f4391e3707543147095df39b1342fa892a9c6699604cad1ce75daec906b7a37cb
7
+ data.tar.gz: 2c1dcff237aded83a87e68fe1b1d4869bd901d61c5703749307df2dd02ba91105fefdc62363fcc5d7b2dbb095bb09d491d5410b64456eced522e82a2255b9322
data/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.6
4
+
5
+ ### Changed
6
+ - **Info.plist and entitlements merging moved from a build-time
7
+ `PBXShellScriptBuildPhase` to plain Ruby code that runs during
8
+ CocoaPods' `post_integrate` hook (pod-install time).** 0.4.1-0.4.5
9
+ each narrowed the conditions under which the "Cycle inside Runner"
10
+ failure occurred, but deep research (Apple Developer Forums,
11
+ CocoaPods/CocoaPods#11133, several flutter/flutter issues, and the
12
+ `swift-build` source) confirmed the real cause: *any*
13
+ `PBXShellScriptBuildPhase` with declared `output_paths` feeding
14
+ `INFOPLIST_FILE`/`CODE_SIGN_ENTITLEMENTS` becomes a graph node XCBuild
15
+ must order relative to a shared `ModuleVerifierTaskProducer` gate that
16
+ framework-embed phases (CocoaPods and SPM alike) are also gated
17
+ behind — no `build_phases` position or `input_paths` declaration could
18
+ avoid it. The values these phases merged (flavor name, files already on
19
+ disk) are fully known at `pod install` time, so there is no need for a
20
+ build-time script phase at all — the same pattern CocoaPods' own
21
+ `Pods-Runner-Info.plist` already uses.
22
+ - `[ann-flavor] Merge Info.plist` and `[ann-flavor] Merge Entitlements`
23
+ build phases are no longer injected, at all, for any project (new or
24
+ upgrading from 0.4.1-0.4.5's phases, which are removed).
25
+ - Merged output is now written directly to
26
+ `ios/ann/Info/merged/<flavor>-<buildType>.plist` and
27
+ `ios/ann/Entitlements/merged/<flavor>-<buildType>.entitlements` — one
28
+ file per flavor **and build type** (previously one merged file per
29
+ flavor, always built from the `release`-level cascade regardless of
30
+ build type — see the matching `ann-flavor-flutter` 1.6.0 changelog
31
+ entry for the generator-side half of this fix).
32
+ - `INFOPLIST_FILE`/`CODE_SIGN_ENTITLEMENTS` build settings now point
33
+ directly at these per-build-type merged files under `$(SRCROOT)`,
34
+ instead of a `$(DERIVED_FILE_DIR)/...-merged...` path produced by a
35
+ script phase.
36
+
37
+ ### Fixed
38
+ - A stale merged entitlements file is now deleted when a flavor/build-type
39
+ combination that previously had entitlements to sign no longer does
40
+ (previously only the build setting was cleared; the stale file was left
41
+ on disk).
42
+
43
+ ## 0.4.5
44
+
45
+ ### Fixed
46
+ - **0.4.4 stopped the cycle for a *fresh* install, but any project already
47
+ on 0.4.1-0.4.3 kept hitting it after upgrading.** 0.4.4 stopped *setting*
48
+ `input_paths` on `[ann-flavor] Merge Entitlements`/`Merge Info.plist`, but
49
+ never explicitly *cleared* it. `existing = target.build_phases.find { ... }`
50
+ reuses the same Xcodeproj object across runs, and `pod install` never
51
+ touches an attribute it doesn't assign — so a project that had already run
52
+ 0.4.1-0.4.3 (which did set `input_paths` on that same, reused object) kept
53
+ the stale value forever, silently reintroducing the exact "Cycle inside
54
+ Runner" failure 0.4.4 was meant to remove. Confirmed live against `ledger`:
55
+ the cycle came back, now involving whichever framework happened to be on
56
+ the other side of the shared `ModuleVerifierTaskProducer` gate (in this
57
+ case an SPM-resolved `AppLovinAdapter.xcframework`, following the
58
+ consumer's separate SPM plugin migration — same underlying mechanism, not
59
+ a new bug). Fixed by explicitly setting `phase.input_paths = []` on both
60
+ phases, so a stale value is genuinely wiped on the next `pod install`, not
61
+ just left unset going forward.
62
+
3
63
  ## 0.4.4
4
64
 
5
65
  ### Fixed
@@ -2,9 +2,14 @@ require_relative 'spec_loader'
2
2
  require 'ann_flavor_core'
3
3
  require 'xcodeproj'
4
4
  require 'stringio'
5
+ require 'json'
6
+ require 'fileutils'
5
7
 
6
8
  module AnnFlavorCocoapods
7
9
  module PodfileHelper
10
+ # System `plutil` tool -- used to read plist files as JSON (see
11
+ # read_plist below); ships with Xcode/macOS.
12
+ PLUTIL = '/usr/bin/plutil'
8
13
  # Call inside your Podfile's post_integrate hook to wire per-flavor Firebase
9
14
  # GoogleService-Info.plist into the Xcode build. Must be post_integrate, not
10
15
  # post_install (DEF-061, #61): post_install fires before CocoaPods has
@@ -155,7 +160,6 @@ module AnnFlavorCocoapods
155
160
  user_project.native_targets.each do |target|
156
161
  if target.symbol_type == :application
157
162
  find_or_create_icon_catalog(target, user_project)
158
- target_has_entitlements = false
159
163
 
160
164
  ios.flavors.each_key do |flavor_key|
161
165
  flavor = ios.flavors[flavor_key]
@@ -179,29 +183,21 @@ module AnnFlavorCocoapods
179
183
 
180
184
  assert_icon_generated!(user_project, flavor_key, icon_name)
181
185
 
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
186
+ # Resolved per build type -- info_plist:/entitlements: are
187
+ # both valid at build_types.<bt> level (REQ-FLVR-00110/
188
+ # REQ-FLVR-00120), so each (flavor, build type) pair needs
189
+ # its own merge, not one shared per flavor. Writes
190
+ # ann/Info/merged/<flavor>-<bt>.plist and (when there's
191
+ # anything to sign with) ann/Entitlements/merged/<flavor>-<bt>
192
+ # .entitlements to disk now, at pod-install time -- see
193
+ # merge_info_plist's doc comment for why this moved off a
194
+ # build-time script phase.
195
+ info_plist_path = merge_info_plist(user_project.project_dir.to_s, flavor_key, bt)
196
+ entitlements_path = merge_entitlements(user_project.project_dir.to_s, flavor_key, bt)
201
197
 
202
198
  configure_build_configuration(
203
199
  target, user_project, xcode_config, xcconfig_path,
204
- icon_name, entitlements_path
200
+ icon_name, entitlements_path, info_plist_path
205
201
  )
206
202
  end
207
203
 
@@ -211,19 +207,6 @@ module AnnFlavorCocoapods
211
207
  if print_build_info
212
208
  inject_build_info_phase(target)
213
209
  end
214
-
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
227
210
  end
228
211
 
229
212
  # Fixes a real pod-install failure: PBXNativeTarget#resolved_build_setting
@@ -620,7 +603,7 @@ module AnnFlavorCocoapods
620
603
  CODE_SIGN_ENTITLEMENTS
621
604
  ].freeze
622
605
 
623
- def self.configure_build_configuration(target, user_project, xcode_config, xcconfig_path, icon_name, entitlements_path = nil)
606
+ def self.configure_build_configuration(target, user_project, xcode_config, xcconfig_path, icon_name, entitlements_path = nil, info_plist_path = nil)
624
607
  config_list = target.build_configuration_list
625
608
  config = config_list[xcode_config]
626
609
 
@@ -646,12 +629,16 @@ module AnnFlavorCocoapods
646
629
  user_project.main_group.new_reference(xcconfig_path)
647
630
  config.base_configuration_reference = xcconfig_ref
648
631
 
649
- # Points at the [ann-flavor] Merge Info.plist run-script phase's output
650
- # (plan 038), not directly at ann/Info/Info-<flavor>.plist -- that file
651
- # now holds only the flavor's info_plist: cascade delta, not a complete
652
- # plist; the merge phase composes it with Runner/Info.plist (base) at
653
- # build time and writes the result here.
654
- config.build_settings['INFOPLIST_FILE'] = '$(DERIVED_FILE_DIR)/Info-merged.plist'
632
+ # Points at merge_info_plist's output (ann/Info/merged/<flavor>.plist),
633
+ # not directly at ann/Info/Info-<flavor>.plist -- that file holds only
634
+ # the flavor's info_plist: cascade delta, not a complete plist;
635
+ # merge_info_plist composes it with Runner/Info.plist (base) at
636
+ # pod-install time and writes the result here. `info_plist_path` is
637
+ # relative to `user_project.project_dir` (== $(SRCROOT) at build time,
638
+ # confirmed via STEP-2.11 real-project verification -- see the comment
639
+ # on xcconfig_path above), so relative-to-$(SRCROOT) is what
640
+ # merge_info_plist/merge_entitlements are expected to return.
641
+ config.build_settings['INFOPLIST_FILE'] = "$(SRCROOT)/#{info_plist_path}"
655
642
  config.build_settings['ASSETCATALOG_COMPILER_APPICON_NAME'] = icon_name
656
643
  # entitlements: is opt-in (unlike INFOPLIST_FILE/the icon catalog, which
657
644
  # every flavor always has) -- only set CODE_SIGN_ENTITLEMENTS when the
@@ -662,7 +649,7 @@ module AnnFlavorCocoapods
662
649
  # longer exists on disk (a build-breaking dangling reference, not
663
650
  # merely a stale value like most other settings here).
664
651
  if entitlements_path
665
- config.build_settings['CODE_SIGN_ENTITLEMENTS'] = entitlements_path
652
+ config.build_settings['CODE_SIGN_ENTITLEMENTS'] = "$(SRCROOT)/#{entitlements_path}"
666
653
  else
667
654
  config.build_settings.delete('CODE_SIGN_ENTITLEMENTS')
668
655
  end
@@ -808,138 +795,53 @@ module AnnFlavorCocoapods
808
795
  # flavor-specific" and gave nothing else (e.g. a CocoaPods plugin like
809
796
  # ann_ads) any way to contribute Info.plist keys, since INFOPLIST_FILE
810
797
  # pointed at the fully-resolved flavor file and Runner/Info.plist was
811
- # never read again. This phase moves that composition to build time
812
- # instead, so Runner/Info.plist stays a live, always-consulted base layer.
798
+ # never read again. This composition used to happen as a build-time
799
+ # PBXShellScriptBuildPhase (plan 038/039); plan 040-follow-up moved it
800
+ # here, to pod-install time, because every value it needs (the flavor
801
+ # name, both source files) is already fully known at post_integrate time
802
+ # -- there is no genuine build-time input. A script phase producing the
803
+ # file CODE_SIGN_ENTITLEMENTS/INFOPLIST_FILE point at is what caused a
804
+ # real, unfixable "Cycle inside Runner" failure in Xcode's new build
805
+ # system: any PBXShellScriptBuildPhase with declared output_paths becomes
806
+ # a graph node XCBuild must order relative to a shared per-target
807
+ # "ModuleVerifierTaskProducer" gate that framework-embed phases (Pods,
808
+ # SPM packages alike) are also gated behind -- no position in
809
+ # build_phases avoids it, confirmed live against ledger across several
810
+ # failed attempts (0.4.1-0.4.5). Writing a plain static file at
811
+ # post_integrate time, before Xcode ever opens the project to build,
812
+ # removes the script phase (and its graph node) entirely -- the same
813
+ # pattern CocoaPods' own Pods-Runner-Info.plist already uses.
813
814
  #
814
- # Uses PlistBuddy (/usr/libexec/PlistBuddy), not python3/ruby -- PlistBuddy
815
- # ships as part of Xcode itself, so this phase has no dependency on any
816
- # scripting runtime beyond what Xcode already requires (OQ-1,
817
- # docs/03-planning/features/038-info-plist-build-time-merge.md).
815
+ # Returns the merged file's path (relative to `user_project.project_dir`,
816
+ # suitable for a build setting like `$(SRCROOT)/<path>` -- callers pass
817
+ # the absolute path through as INFOPLIST_FILE, see configure_build_settings).
818
818
  #
819
- # Runs before_compile, ahead of Xcode's own Info.plist processing --
820
- # INFOPLIST_FILE (set in configure_build_configuration) points at this
821
- # phase's output, not at either source file directly.
822
- def self.inject_merge_info_plist_phase(target)
823
- script = <<~SHELL
824
- # Injected by ann-flavor-cocoapods: compose the final Info.plist from
825
- # two layers -- Runner/Info.plist (generic/base) and the flavor-
826
- # specific ann/Info/Info-<flavor>.plist (annspec.yaml's info_plist:
827
- # cascade) -- with the flavor layer winning on any key collision.
828
- # See docs/03-planning/features/038-info-plist-build-time-merge.md.
829
- PLISTBUDDY=/usr/libexec/PlistBuddy
830
- BASE="${SRCROOT}/Runner/Info.plist"
831
- FLAVOR_NAME="${CONFIGURATION#*-}"
832
- FLAVOR_PLIST="${SRCROOT}/ann/Info/Info-${FLAVOR_NAME}.plist"
833
- OUT="${DERIVED_FILE_DIR}/Info-merged.plist"
834
-
835
- if [ ! -f "$BASE" ]; then
836
- echo "error: [ann-flavor] Runner/Info.plist not found at $BASE"
837
- exit 1
838
- fi
819
+ # `build_type` (e.g. "debug"/"release"/a custom build_types key) picks
820
+ # which of ios_generator.dart's per-flavor-per-build-type delta files to
821
+ # read -- annspec.yaml's info_plist: cascade is valid at build_types.<bt>
822
+ # level (REQ-FLVR-00110), so this must resolve per build type, not once
823
+ # per flavor, or a build-type-scoped override is silently dropped.
824
+ def self.merge_info_plist(project_dir, flavor_key, build_type)
825
+ base_path = File.join(project_dir, 'Runner', 'Info.plist')
826
+ flavor_path = File.join(project_dir, 'ann', 'Info', "Info-#{flavor_key}-#{build_type}.plist")
827
+ relative_out_path = File.join('ann', 'Info', 'merged', "#{flavor_key}-#{build_type}.plist")
828
+ out_path = File.join(project_dir, relative_out_path)
829
+
830
+ unless File.exist?(base_path)
831
+ raise "[ann-flavor-cocoapods] Runner/Info.plist not found at #{base_path}"
832
+ end
839
833
 
840
- # Start from a full copy of the base layer -- every key
841
- # Runner/Info.plist already has is correct by default; only the
842
- # flavor delta's keys need explicit merging in below.
843
- cp "$BASE" "$OUT"
834
+ merged = read_plist(base_path)
844
835
 
845
- if [ ! -f "$FLAVOR_PLIST" ]; then
846
- echo "warning: [ann-flavor] no flavor Info.plist found for '$FLAVOR_NAME' at $FLAVOR_PLIST -- using Runner/Info.plist unmerged."
847
- exit 0
848
- fi
849
-
850
- # Enumerate the flavor delta's top-level keys. PlistBuddy "Print" with
851
- # no key path dumps the whole dict; grep the top-level "Key = " lines
852
- # (four leading spaces = depth 1 in PlistBuddy's indented dump format).
853
- FLAVOR_KEYS=$("$PLISTBUDDY" -c "Print" "$FLAVOR_PLIST" | grep -E '^ [A-Za-z]' | sed 's/ =.*//' | sed 's/^ *//')
854
-
855
- for KEY in $FLAVOR_KEYS; do
856
- # OQ-2: warn (non-fatal) only on a genuine collision -- Runner/Info.plist
857
- # already had this key, with a different value, before the flavor delta
858
- # is applied. A net-new key (not previously in Runner/Info.plist) is not
859
- # a collision and stays silent -- that's the expected, common case.
860
- # Compared via PlistBuddy's human-readable Print output, which is
861
- # good enough for equality-detection purposes even though it can't
862
- # reliably tell types apart (see the VALUE_XML extraction below,
863
- # which is what actually determines the merged value/type).
864
- BASE_VALUE=$("$PLISTBUDDY" -c "Print :$KEY" "$OUT" 2>/dev/null)
865
- BASE_HAD_KEY=$?
866
- FLAVOR_VALUE=$("$PLISTBUDDY" -c "Print :$KEY" "$FLAVOR_PLIST")
867
-
868
- if [ "$BASE_HAD_KEY" -eq 0 ] && [ "$BASE_VALUE" != "$FLAVOR_VALUE" ]; then
869
- echo "[ann-flavor] Info.plist: '$KEY' overridden by flavor value ($FLAVOR_NAME) -- Runner/Info.plist's value ignored for this build."
870
- fi
871
-
872
- # Extract the flavor value's own XML fragment (e.g. <string>x</string>,
873
- # <true/>, <integer>42</integer>, <array>...</array>) via PlistBuddy's
874
- # -x (XML) output, scoped to just this key, stripping the <plist
875
- # version="1.0">...</plist> wrapper that -x always adds. This
876
- # preserves the flavor delta's real plist type -- Print's
877
- # human-readable output alone can't distinguish an integer 42 from
878
- # the string "42", or a bool true from the string "true", so a
879
- # scalar-vs-composite TYPE guess from Print was unreliable and has
880
- # been replaced with this approach (verified against a real
881
- # PlistBuddy: bool/int/real/string/array/dict all round-trip
882
- # correctly this way, plan 038 STEP-02).
883
- VALUE_XML=$("$PLISTBUDDY" -x -c "Print :$KEY" "$FLAVOR_PLIST" | sed -n '/<plist version/,/<\\/plist>/p' | sed '1d;$d')
884
-
885
- "$PLISTBUDDY" -c "Delete :$KEY" "$OUT" >/dev/null 2>&1
886
- FRAGMENT=$(mktemp /tmp/ann-flavor-plist-fragment.XXXXXX)
887
- cat > "$FRAGMENT" <<XMLEOF
888
- <?xml version="1.0" encoding="UTF-8"?>
889
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
890
- <plist version="1.0">
891
- <dict>
892
- <key>$KEY</key>
893
- $VALUE_XML
894
- </dict>
895
- </plist>
896
- XMLEOF
897
- "$PLISTBUDDY" -c "Merge \\"$FRAGMENT\\"" "$OUT"
898
- rm -f "$FRAGMENT"
899
- done
900
- SHELL
836
+ if File.exist?(flavor_path)
837
+ merge_plist_layer!(merged, read_plist(flavor_path), "Info.plist", "#{flavor_key}-#{build_type}")
838
+ else
839
+ puts "ann-flavor-cocoapods: no flavor Info.plist found for '#{flavor_key}' (#{build_type}) at #{flavor_path} -- using Runner/Info.plist unmerged."
840
+ end
901
841
 
902
- # Find-or-update in place, rather than delete+recreate -- same
903
- # idempotency rationale as inject_build_info_phase above.
904
- existing = target.build_phases.find { |p| p.respond_to?(:name) && p.name == '[ann-flavor] Merge Info.plist' }
905
- phase = existing || target.project.new(Xcodeproj::Project::Object::PBXShellScriptBuildPhase)
906
- phase.name = '[ann-flavor] Merge Info.plist'
907
- phase.shell_path = '/bin/sh'
908
- phase.shell_script = script
909
- phase.show_env_vars_in_log = '0'
910
- # Declares this phase's output so Xcode's build system understands the
911
- # dependency (same technique already used for the Firebase-copy phase's
912
- # output_paths) -- suppresses "input file not found" for INFOPLIST_FILE
913
- # referencing a path nothing else produces until this phase runs.
914
- phase.output_paths = ['$(DERIVED_FILE_DIR)/Info-merged.plist']
915
- # NOT declaring input_paths on Check Pods Manifest.lock's output here
916
- # (tried in v0.4.1) -- see the identical, fuller explanation on
917
- # inject_merge_entitlements_phase below for why that direction was
918
- # actually the cause of the real "Cycle inside Runner" failure, not a
919
- # (partial) fix for it.
920
-
921
- # Appended at the END of build_phases, not unshifted to the front --
922
- # see the identical, fuller explanation on inject_merge_entitlements_phase
923
- # below. This phase's output (INFOPLIST_FILE) is only consumed at
924
- # packaging/signing time, which is late; it has no reason to race
925
- # framework/resource-copy phases early in the build.
926
- #
927
- # Repositions unconditionally, even when `existing` -- see the identical
928
- # note on inject_merge_entitlements_phase for why (a pre-v0.4.2 project
929
- # would otherwise never actually get repositioned on re-`pod install`).
930
- #
931
- # Uses << , NOT push -- Xcodeproj::Project::ObjectList (build_phases'
932
- # real class) overrides <</insert/unshift/delete to also update the
933
- # object's reference count/registration in the project's global
934
- # objects table, but does NOT override push. Array#push silently falls
935
- # through to the plain Array behavior, leaving the phase referenced in
936
- # buildPhases but never actually registered as a project object --
937
- # confirmed live: this produced a real, silently-corrupt
938
- # project.pbxproj (a dangling buildPhases reference to a UUID with no
939
- # matching object), which then failed at Xcode build time with "Build
940
- # input file cannot be found" for this phase's declared output.
941
- target.build_phases.delete(phase) if existing
942
- target.build_phases << phase
842
+ FileUtils.mkdir_p(File.dirname(out_path))
843
+ Xcodeproj::Plist.write_to_path(merged, out_path)
844
+ relative_out_path
943
845
  end
944
846
 
945
847
  # Composes the final .entitlements file Xcode actually signs with, from
@@ -947,154 +849,78 @@ module AnnFlavorCocoapods
947
849
  # not exist at all) and the flavor-specific ann/Entitlements/<flavor>
948
850
  # .entitlements delta (annspec.yaml's entitlements: cascade, opt-in --
949
851
  # may also not exist) -- with the flavor layer winning on any key
950
- # collision. Plan 039: mirrors inject_merge_info_plist_phase (plan 038)
951
- # exactly -- same PlistBuddy-based merge (entitlements files and Info.plist
952
- # are both plain Apple XML property lists; PlistBuddy has no notion of
953
- # file "kind" beyond that), same find-or-update idempotency, same
954
- # collision-warning behavior. The one real difference: unlike
955
- # Runner/Info.plist (every Xcode project has one), most projects have no
956
- # Runner.entitlements at all -- a missing base file here is a normal,
957
- # non-fatal case (starts from an empty plist dict), not the fatal error
958
- # Merge Info.plist treats a missing Runner/Info.plist as.
959
- def self.inject_merge_entitlements_phase(target)
960
- script = <<~SHELL
961
- # Injected by ann-flavor-cocoapods: compose the final .entitlements file
962
- # from two layers -- Runner.entitlements (generic/base, may not exist)
963
- # and the flavor-specific ann/Entitlements/<flavor>.entitlements delta
964
- # (annspec.yaml's entitlements: cascade, may also not exist) -- with the
965
- # flavor layer winning on any key collision. Mirrors [ann-flavor] Merge
966
- # Info.plist (plan 038) -- see plan 039 for why entitlements needed the
967
- # same redesign.
968
- PLISTBUDDY=/usr/libexec/PlistBuddy
969
- BASE="${SRCROOT}/Runner/Runner.entitlements"
970
- FLAVOR_NAME="${CONFIGURATION#*-}"
971
- FLAVOR_FILE="${SRCROOT}/ann/Entitlements/${FLAVOR_NAME}.entitlements"
972
- OUT="${DERIVED_FILE_DIR}/Entitlements-merged.entitlements"
973
-
974
- if [ -f "$BASE" ]; then
975
- cp "$BASE" "$OUT"
976
- else
977
- # No stock Runner.entitlements -- start from an empty plist dict
978
- # rather than failing; the flavor delta (if any) is still applied
979
- # below.
980
- cat > "$OUT" <<PLISTEOF
981
- <?xml version="1.0" encoding="UTF-8"?>
982
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
983
- <plist version="1.0">
984
- <dict/>
985
- </plist>
986
- PLISTEOF
987
- fi
852
+ # collision. Mirrors merge_info_plist exactly -- see its comment for why
853
+ # this moved from a build-time script phase to pod-install time. The one
854
+ # real difference: unlike Runner/Info.plist (every Xcode project has
855
+ # one), most projects have no Runner.entitlements at all -- a missing
856
+ # base file here is a normal, non-fatal case (starts from an empty plist
857
+ # dict), not the fatal error merge_info_plist treats a missing
858
+ # Runner/Info.plist as.
859
+ #
860
+ # Returns the merged file's path, or nil when there's nothing to sign
861
+ # with at all (no stock Runner.entitlements AND no flavor delta) --
862
+ # callers use a nil return to decide whether to set CODE_SIGN_ENTITLEMENTS.
863
+ #
864
+ # `build_type` resolves per build type -- see the identical note on
865
+ # merge_info_plist for why (entitlements: is valid at build_types.<bt>
866
+ # level too, REQ-FLVR-00120).
867
+ def self.merge_entitlements(project_dir, flavor_key, build_type)
868
+ base_path = File.join(project_dir, 'Runner', 'Runner.entitlements')
869
+ flavor_path = File.join(project_dir, 'ann', 'Entitlements', "#{flavor_key}-#{build_type}.entitlements")
870
+ relative_out_path = File.join('ann', 'Entitlements', 'merged', "#{flavor_key}-#{build_type}.entitlements")
871
+ out_path = File.join(project_dir, relative_out_path)
872
+
873
+ has_base = File.exist?(base_path)
874
+ has_flavor = File.exist?(flavor_path)
875
+ unless has_base || has_flavor
876
+ # Nothing to sign with -- delete a previously-written merged file
877
+ # rather than leaving it stale on disk (CODE_SIGN_ENTITLEMENTS is
878
+ # correctly cleared by the caller either way, but an orphaned file
879
+ # here is still generated-output drift the same "disposable, always
880
+ # regenerated" convention ios/ann/ uses elsewhere is meant to avoid).
881
+ FileUtils.rm_f(out_path)
882
+ return nil
883
+ end
988
884
 
989
- if [ ! -f "$FLAVOR_FILE" ]; then
990
- echo "[ann-flavor] Entitlements: no flavor delta found for '$FLAVOR_NAME' at $FLAVOR_FILE -- using Runner.entitlements unmerged."
991
- exit 0
992
- fi
885
+ merged = has_base ? read_plist(base_path) : {}
886
+ if has_flavor
887
+ merge_plist_layer!(merged, read_plist(flavor_path), "Entitlements", "#{flavor_key}-#{build_type}")
888
+ elsif has_base
889
+ puts "ann-flavor-cocoapods: no flavor delta found for '#{flavor_key}' (#{build_type}) at #{flavor_path} -- using Runner.entitlements unmerged."
890
+ end
993
891
 
994
- FLAVOR_KEYS=$("$PLISTBUDDY" -c "Print" "$FLAVOR_FILE" | grep -E '^ [A-Za-z]' | sed 's/ =.*//' | sed 's/^ *//')
995
-
996
- for KEY in $FLAVOR_KEYS; do
997
- BASE_VALUE=$("$PLISTBUDDY" -c "Print :$KEY" "$OUT" 2>/dev/null)
998
- BASE_HAD_KEY=$?
999
- FLAVOR_VALUE=$("$PLISTBUDDY" -c "Print :$KEY" "$FLAVOR_FILE")
1000
-
1001
- if [ "$BASE_HAD_KEY" -eq 0 ] && [ "$BASE_VALUE" != "$FLAVOR_VALUE" ]; then
1002
- echo "[ann-flavor] Entitlements: '$KEY' overridden by flavor value ($FLAVOR_NAME) -- Runner.entitlements' value ignored for this build."
1003
- fi
1004
-
1005
- VALUE_XML=$("$PLISTBUDDY" -x -c "Print :$KEY" "$FLAVOR_FILE" | sed -n '/<plist version/,/<\\/plist>/p' | sed '1d;$d')
1006
-
1007
- "$PLISTBUDDY" -c "Delete :$KEY" "$OUT" >/dev/null 2>&1
1008
- FRAGMENT=$(mktemp /tmp/ann-flavor-entitlements-fragment.XXXXXX)
1009
- cat > "$FRAGMENT" <<XMLEOF
1010
- <?xml version="1.0" encoding="UTF-8"?>
1011
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1012
- <plist version="1.0">
1013
- <dict>
1014
- <key>$KEY</key>
1015
- $VALUE_XML
1016
- </dict>
1017
- </plist>
1018
- XMLEOF
1019
- "$PLISTBUDDY" -c "Merge \\"$FRAGMENT\\"" "$OUT"
1020
- rm -f "$FRAGMENT"
1021
- done
1022
- SHELL
892
+ FileUtils.mkdir_p(File.dirname(out_path))
893
+ Xcodeproj::Plist.write_to_path(merged, out_path)
894
+ relative_out_path
895
+ end
1023
896
 
1024
- existing = target.build_phases.find { |p| p.respond_to?(:name) && p.name == '[ann-flavor] Merge Entitlements' }
1025
- phase = existing || target.project.new(Xcodeproj::Project::Object::PBXShellScriptBuildPhase)
1026
- phase.name = '[ann-flavor] Merge Entitlements'
1027
- phase.shell_path = '/bin/sh'
1028
- phase.shell_script = script
1029
- phase.show_env_vars_in_log = '0'
1030
- phase.output_paths = ['$(DERIVED_FILE_DIR)/Entitlements-merged.entitlements']
1031
- # NOT declaring input_paths on Check Pods Manifest.lock's own output
1032
- # here (tried in v0.4.1/0.4.2/0.4.3 as "belt-and-suspenders"). Confirmed
1033
- # live (yet again, against ledger) that this input_paths declaration is
1034
- # what CAUSES the real "Cycle inside Runner" failure, not a partial fix
1035
- # for it: this phase's output feeds ProcessProductPackaging (signing),
1036
- # which Xcode's new build system gates behind a chain of task producers
1037
- # (ModuleVerifierTaskProducer et al) — declaring Check Pods Manifest
1038
- # .lock's result file as this phase's OWN input routes this phase
1039
- # through that SAME gate chain from the other direction, since Check
1040
- # Pods Manifest.lock's output is itself gated behind
1041
- # ModuleVerifierTaskProducer too. The Firebase framework-copy phases
1042
- # are gated behind that identical chain — closing a genuine cycle no
1043
- # matter where this phase sits in build_phases. Xcode's own error
1044
- # message ("moving Check Pods Manifest.lock so it runs before the
1045
- # phase that depends on its outputs") is describing exactly this
1046
- # self-inflicted dependency, not a real requirement — this phase never
1047
- # actually needed Check Pods Manifest.lock's result as an input;
1048
- # output_paths + running after the Pods/framework-copy phases (via
1049
- # position, below) is sufficient on its own.
1050
- #
1051
- # Appended at the END of build_phases, not unshifted to the front.
1052
- # v0.4.1 tried fixing this phase's real "Cycle inside Runner" Xcode
1053
- # build failure (confirmed live against ledger, a project with real
1054
- # CocoaPods frameworks like Firebase) purely by declaring input_paths
1055
- # above -- that alone was NOT sufficient. The actual cycle: this
1056
- # phase's output feeds ProcessProductPackaging (entitlements signing,
1057
- # gated behind a chain of task producers Xcode's new build system
1058
- # shares with the Firebase framework-copy phases), and unshifting this
1059
- # phase to the very FRONT of build_phases put it, and its declared
1060
- # output dependency, on the same task-producer chain the framework
1061
- # copy phases are gated behind on the other side -- a genuine cycle
1062
- # through shared gates, not resolvable by declaring one more input.
1063
- # CODE_SIGN_ENTITLEMENTS/INFOPLIST_FILE are only consumed at
1064
- # packaging/signing time, which is late in the build -- this phase has
1065
- # no real reason to run before Sources/Frameworks/Resources/Embed
1066
- # Frameworks at all. Appending it at the end removes it from that
1067
- # shared gate entirely.
1068
- #
1069
- # Repositions unconditionally, even when `existing` (a project that
1070
- # already has this phase from a pre-v0.4.2 install, sitting at the
1071
- # front from the old unshift) -- otherwise re-running `pod install`
1072
- # after upgrading the gem would never actually move it, and the real
1073
- # bug would persist for every already-affected consumer.
1074
- #
1075
- # Uses << , NOT push -- see the identical, fuller explanation on
1076
- # inject_merge_info_plist_phase above. Array#push silently bypasses
1077
- # ObjectList's reference-count/registration bookkeeping, producing a
1078
- # real, silently-corrupt project.pbxproj (buildPhases referencing a
1079
- # UUID with no matching object) that fails at Xcode build time with
1080
- # "Build input file cannot be found" -- confirmed live against ledger.
1081
- target.build_phases.delete(phase) if existing
1082
- target.build_phases << phase
897
+ # Reads a plist file as a plain Ruby Hash. Uses `plutil -convert json`
898
+ # (a system tool, ships with Xcode/macOS), NOT Xcodeproj::Plist
899
+ # .read_from_path -- that method's pure-Ruby CFPropertyList fallback path
900
+ # is broken under Ruby 3.4 in this environment (missing stdlib 'kconv',
901
+ # confirmed live) -- same workaround already established in this repo's
902
+ # own spec suite (merge_entitlements_script_spec.rb/
903
+ # merge_info_plist_script_spec.rb).
904
+ def self.read_plist(path)
905
+ json = `#{PLUTIL} -convert json -o - "#{path}"`
906
+ raise "[ann-flavor-cocoapods] plutil failed to read #{path}" unless $?.success?
907
+
908
+ JSON.parse(json)
1083
909
  end
1084
910
 
1085
- # Removes a previously-injected Merge Entitlements phase when no flavor
1086
- # needs entitlements anymore (annspec.yaml's entitlements: cascade was
1087
- # removed, and no stock Runner.entitlements exists either) -- entitlements
1088
- # are opt-in overall, so a stale phase with nothing to sign would be dead
1089
- # weight left in project.pbxproj. Uses ObjectList#delete (not plain
1090
- # Array#reject!/select), same rationale as the stale "[ann-flavor] Remove
1091
- # Firebase plist" cleanup above -- plain array removal only drops the
1092
- # phase from target.build_phases, leaving the object itself permanently
1093
- # orphaned in the project's global objects table.
1094
- def self.remove_merge_entitlements_phase(target)
1095
- target.build_phases.select do |p|
1096
- p.respond_to?(:name) && p.name == '[ann-flavor] Merge Entitlements'
1097
- end.each { |p| target.build_phases.delete(p) }
911
+ # Merges `layer` into `base` in place, one top-level key at a time --
912
+ # `layer`'s value wins on any key both define, printing a one-line notice
913
+ # so an unintentional override is easy to spot (same collision-warning
914
+ # behavior the build-time script phases had). A net-new key (not
915
+ # previously in `base`) is not a collision and stays silent -- the
916
+ # expected, common case.
917
+ def self.merge_plist_layer!(base, layer, kind, flavor_key)
918
+ layer.each do |key, value|
919
+ if base.key?(key) && base[key] != value
920
+ puts "ann-flavor-cocoapods: [#{kind}] '#{key}' overridden by flavor value (#{flavor_key}) -- base value ignored for this build."
921
+ end
922
+ base[key] = value
923
+ end
1098
924
  end
1099
925
  end
1100
926
  end
@@ -1,3 +1,3 @@
1
1
  module AnnFlavorCocoapods
2
- VERSION = "0.4.4"
2
+ VERSION = "0.4.6"
3
3
  end
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.4.4
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - ANN Solutions