kintsugi 0.7.9 → 0.7.10
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 +4 -4
- data/kintsugi.gemspec +1 -1
- data/lib/kintsugi/apply_change_to_project.rb +280 -10
- data/lib/kintsugi/version.rb +1 -1
- data/lib/kintsugi/xcodeproj_extensions.rb +63 -0
- data/spec/kintsugi_apply_change_to_project_spec.rb +587 -0
- data/spec/kintsugi_integration_spec.rb +45 -0
- metadata +191 -169
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: feebfac564140617fc5d55b773eadb7bc12b90ce0a10c1498827fd445319c36c
|
|
4
|
+
data.tar.gz: 364efec483aa9be794146d57d22d3ce0093bdeadc29c5cc16bc837bf42147088
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b12c34ac0ba743b080e81cc006cfbea332335493d66f5703bf40bc9ae84e7b2bec7d352d67fd2dcc8f1bd401c75d6d438516ba23e5d0bc6c68c42d1cad270d44
|
|
7
|
+
data.tar.gz: 012a869ace7be6b2869cb060181d7aebb2ee231c27a811f17b85e9a4b7904fd0046e44aa9cd647db05e77ea4d8775b8ccee7acdb99b9abda0d3ba54c308f8817
|
data/kintsugi.gemspec
CHANGED
|
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
spec.require_paths = ["lib"]
|
|
24
24
|
|
|
25
25
|
spec.add_dependency "tty-prompt", "~> 0"
|
|
26
|
-
spec.add_dependency "xcodeproj", ">= 1.
|
|
26
|
+
spec.add_dependency "xcodeproj", ">= 1.26.0", "<= 1.27.0"
|
|
27
27
|
|
|
28
28
|
spec.add_development_dependency "git", "~> 1.11"
|
|
29
29
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
@@ -24,6 +24,12 @@ end
|
|
|
24
24
|
|
|
25
25
|
module Kintsugi
|
|
26
26
|
class << self
|
|
27
|
+
# Isas of components whose additions and removals are handled by the main group pipeline
|
|
28
|
+
# (`apply_group_additions`/`apply_group_removals`). `PBXFileSystemSynchronizedRootGroup` is the
|
|
29
|
+
# Xcode 16 "buildable folder" object, which lives in the group tree like a group.
|
|
30
|
+
GROUP_PIPELINE_ISAS =
|
|
31
|
+
%w[PBXGroup PBXVariantGroup PBXFileSystemSynchronizedRootGroup].freeze
|
|
32
|
+
|
|
27
33
|
# Applies the change specified by `change` to `project`.
|
|
28
34
|
#
|
|
29
35
|
# @param [Xcodeproj::Project] project
|
|
@@ -45,6 +51,7 @@ module Kintsugi
|
|
|
45
51
|
@change_source_project = change_source_project
|
|
46
52
|
@ignored_components_group_paths = []
|
|
47
53
|
@created_components_group_paths = []
|
|
54
|
+
@pending_exception_references = []
|
|
48
55
|
|
|
49
56
|
# We iterate over the main group and project references first because they might create file
|
|
50
57
|
# or project references that are referenced in other parts.
|
|
@@ -65,6 +72,11 @@ module Kintsugi
|
|
|
65
72
|
change["rootObject"].reject { |key|
|
|
66
73
|
%w[mainGroup projectReferences].include?(key)
|
|
67
74
|
}, "")
|
|
75
|
+
|
|
76
|
+
# Exception sets reference targets and build phases that may only be created later in the
|
|
77
|
+
# change (and groups are linked to their targets only in the rootObject pass above), so their
|
|
78
|
+
# references are resolved here, once everything exists.
|
|
79
|
+
resolve_pending_exception_references
|
|
68
80
|
end
|
|
69
81
|
|
|
70
82
|
private
|
|
@@ -107,7 +119,7 @@ module Kintsugi
|
|
|
107
119
|
|
|
108
120
|
def apply_group_additions(project, additions, force_create_containing_group: false)
|
|
109
121
|
additions.each do |change, path|
|
|
110
|
-
next unless
|
|
122
|
+
next unless GROUP_PIPELINE_ISAS.include?(change["isa"])
|
|
111
123
|
|
|
112
124
|
group_type = Module.const_get("Xcodeproj::Project::#{change["isa"]}")
|
|
113
125
|
containing_group = project.group_or_file_at_path(path)
|
|
@@ -244,7 +256,8 @@ module Kintsugi
|
|
|
244
256
|
when Xcodeproj::Project::PBXFileReference
|
|
245
257
|
apply_file_changes(project, [[component_change, containing_group_path]], [],
|
|
246
258
|
force_create_containing_group: true)
|
|
247
|
-
when Xcodeproj::Project::PBXGroup
|
|
259
|
+
when Xcodeproj::Project::PBXGroup,
|
|
260
|
+
Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup
|
|
248
261
|
apply_group_additions(project, [[component_change, containing_group_path]],
|
|
249
262
|
force_create_containing_group: true)
|
|
250
263
|
else
|
|
@@ -256,14 +269,16 @@ module Kintsugi
|
|
|
256
269
|
|
|
257
270
|
def apply_group_removals(project, removals)
|
|
258
271
|
removals.sort_by(&:last).reverse.each do |change, path|
|
|
259
|
-
next unless
|
|
272
|
+
next unless GROUP_PIPELINE_ISAS.include?(change["isa"])
|
|
260
273
|
|
|
261
274
|
group_path = join_path(path, change["displayName"])
|
|
262
275
|
|
|
263
276
|
# by now we've deleted all of this group's children in the project, so we need to adapt the
|
|
264
277
|
# change to the expected current state of the group, that is, without any children.
|
|
278
|
+
# `PBXFileSystemSynchronizedRootGroup` has no `children` attribute, so we must not inject an
|
|
279
|
+
# empty one, otherwise `remove_component`'s tree hash comparison would never match.
|
|
265
280
|
change_without_children = change.dup
|
|
266
|
-
change_without_children["children"] = []
|
|
281
|
+
change_without_children["children"] = [] if change.key?("children")
|
|
267
282
|
|
|
268
283
|
remove_component(project[group_path], change_without_children)
|
|
269
284
|
end
|
|
@@ -291,12 +306,7 @@ module Kintsugi
|
|
|
291
306
|
if change[:removed].is_a?(Hash)
|
|
292
307
|
remove_component(component, change[:removed])
|
|
293
308
|
elsif change[:removed].is_a?(Array)
|
|
294
|
-
unless component.nil?
|
|
295
|
-
(change[:removed]).each do |removed_change|
|
|
296
|
-
child = child_component_of_object_list(component, removed_change["displayName"])
|
|
297
|
-
remove_component(child, removed_change)
|
|
298
|
-
end
|
|
299
|
-
end
|
|
309
|
+
remove_children_from_object_list(component, change[:removed]) unless component.nil?
|
|
300
310
|
elsif !change[:removed].nil?
|
|
301
311
|
raise MergeError, "Unsupported removed change type for #{change[:removed]}"
|
|
302
312
|
end
|
|
@@ -323,6 +333,22 @@ module Kintsugi
|
|
|
323
333
|
end
|
|
324
334
|
end
|
|
325
335
|
|
|
336
|
+
def remove_children_from_object_list(object_list, removed_changes)
|
|
337
|
+
removed_changes.each do |removed_change|
|
|
338
|
+
child = child_component_of_object_list(object_list, removed_change["displayName"])
|
|
339
|
+
if removed_change["isa"] == "PBXFileSystemSynchronizedRootGroup"
|
|
340
|
+
# A target references the buildable folder object; unlinking it must detach the reference,
|
|
341
|
+
# not unconditionally delete the shared object. `ObjectList#delete` is reference-counted:
|
|
342
|
+
# it removes the object only when this was its last referrer, so a folder still referenced
|
|
343
|
+
# by the main group or another target survives. (Full deletion of the folder itself is
|
|
344
|
+
# driven separately by its main group entry, via `apply_group_removals`.)
|
|
345
|
+
object_list.delete(child) unless child.nil?
|
|
346
|
+
else
|
|
347
|
+
remove_component(child, removed_change)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
326
352
|
def resolve_nonexistent_component(parent_component, change_path)
|
|
327
353
|
source_project_component = component_at_path(@change_source_project, change_path)
|
|
328
354
|
group_path = group_path_of_group_based_component(source_project_component)
|
|
@@ -353,6 +379,10 @@ module Kintsugi
|
|
|
353
379
|
elsif component.is_a?(Xcodeproj::Project::PBXFileReference) ||
|
|
354
380
|
component.is_a?(Xcodeproj::Project::PBXGroup)
|
|
355
381
|
component.hierarchy_path.delete_prefix("/")
|
|
382
|
+
elsif component.is_a?(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup)
|
|
383
|
+
# A synchronized root group is not a `PBXGroup`, so it has no `hierarchy_path` instance
|
|
384
|
+
# method; the groupable helper computes the same path from its parent chain.
|
|
385
|
+
Xcodeproj::Project::Object::GroupableHelper.hierarchy_path(component).delete_prefix("/")
|
|
356
386
|
end
|
|
357
387
|
end
|
|
358
388
|
|
|
@@ -652,6 +682,13 @@ module Kintsugi
|
|
|
652
682
|
add_file_reference(component, change, change_path)
|
|
653
683
|
when "PBXGroup"
|
|
654
684
|
add_group(component, change, change_path)
|
|
685
|
+
when "PBXFileSystemSynchronizedRootGroup"
|
|
686
|
+
add_file_system_synchronized_root_group(component, change, change_path)
|
|
687
|
+
when "PBXFileSystemSynchronizedBuildFileExceptionSet"
|
|
688
|
+
add_file_system_synchronized_build_file_exception_set(component, change, change_path)
|
|
689
|
+
when "PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet"
|
|
690
|
+
add_file_system_synchronized_group_build_phase_membership_exception_set(component, change,
|
|
691
|
+
change_path)
|
|
655
692
|
when "PBXContainerItemProxy"
|
|
656
693
|
add_container_item_proxy(component, change, change_path)
|
|
657
694
|
when "PBXTargetDependency"
|
|
@@ -768,6 +805,239 @@ module Kintsugi
|
|
|
768
805
|
end
|
|
769
806
|
end
|
|
770
807
|
|
|
808
|
+
# Known limitation: buildable folders have no stable identity in a project diff (they're
|
|
809
|
+
# compared by value), so relocating a folder between parent groups while it stays linked to a
|
|
810
|
+
# target reads as remove-from-A + add-to-B. The old object (and its target link) is removed, and
|
|
811
|
+
# the new object is created but not re-linked to the target, since the target's own change is
|
|
812
|
+
# empty. Such a merge can drop the folder's target membership; re-verify after relocating.
|
|
813
|
+
def add_file_system_synchronized_root_group(containing_component, change, change_path)
|
|
814
|
+
case containing_component
|
|
815
|
+
when Xcodeproj::Project::PBXNativeTarget
|
|
816
|
+
# The group was already created in the group tree by the main group pass. Resolve it by its
|
|
817
|
+
# position in the tree so we reuse the exact same object, even when another buildable folder
|
|
818
|
+
# shares its name in a different parent group.
|
|
819
|
+
group = resolve_file_system_synchronized_root_group(containing_component, change)
|
|
820
|
+
if group.nil?
|
|
821
|
+
raise MergeError, "No file system synchronized root group matching #{change} was " \
|
|
822
|
+
"found in the group tree. Change path: #{change_path}"
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Dedup by object identity, not display name: one target can legitimately link several
|
|
826
|
+
# same-named folders from different parent groups, each a distinct object.
|
|
827
|
+
already_linked = containing_component.file_system_synchronized_groups.any? do |linked|
|
|
828
|
+
linked.uuid == group.uuid
|
|
829
|
+
end
|
|
830
|
+
return if !Settings.allow_duplicates && already_linked
|
|
831
|
+
|
|
832
|
+
containing_component.file_system_synchronized_groups << group
|
|
833
|
+
else
|
|
834
|
+
raise MergeError, "Trying to add file system synchronized root group to an unsupported " \
|
|
835
|
+
"component type #{containing_component.isa}. Change is: #{change}"
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def resolve_file_system_synchronized_root_group(target, change)
|
|
840
|
+
candidates = target.project.objects.select do |object|
|
|
841
|
+
object.isa == "PBXFileSystemSynchronizedRootGroup" &&
|
|
842
|
+
object.display_name == change["displayName"]
|
|
843
|
+
end
|
|
844
|
+
return candidates.first if candidates.length <= 1
|
|
845
|
+
|
|
846
|
+
# More than one buildable folder shares this name (in different parent groups). Match against
|
|
847
|
+
# the folders the change's target links in the source project, comparing hierarchy paths as
|
|
848
|
+
# strings. A folder's own `path`/`display_name` may itself contain "/", so this must not be
|
|
849
|
+
# split into segments, so path-walking helpers like `group_or_file_at_path` can't be used.
|
|
850
|
+
source_hierarchies = source_synchronized_root_group_hierarchies(target, change)
|
|
851
|
+
source_matching = candidates.select do |candidate|
|
|
852
|
+
source_hierarchies.include?(synchronized_root_group_hierarchy_path(candidate))
|
|
853
|
+
end
|
|
854
|
+
return candidates.first if source_matching.empty?
|
|
855
|
+
|
|
856
|
+
# Prefer a folder not already linked to this target, so a target linking two same-named
|
|
857
|
+
# folders resolves each addition to a distinct object rather than dropping the second.
|
|
858
|
+
linked_uuids = target.file_system_synchronized_groups.map(&:uuid)
|
|
859
|
+
source_matching.find { |candidate| !linked_uuids.include?(candidate.uuid) } ||
|
|
860
|
+
source_matching.first
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
# Hierarchy paths (in source order) of the buildable folders the change's target links that
|
|
864
|
+
# match `change`. A target may link several same-named folders from different parents; their
|
|
865
|
+
# identity lets us map each addition to the right destination object.
|
|
866
|
+
def source_synchronized_root_group_hierarchies(target, change)
|
|
867
|
+
source_target = find_target(@change_source_project, target.display_name)
|
|
868
|
+
(source_target&.file_system_synchronized_groups || [])
|
|
869
|
+
.select { |group| group.to_tree_hash == change }
|
|
870
|
+
.map { |group| synchronized_root_group_hierarchy_path(group) }
|
|
871
|
+
.compact
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Groupable helper raises a `RuntimeError` consistency error (rather than returning nil) for an
|
|
875
|
+
# object with no parent group; treat that as "no hierarchy" so a stray unparented candidate
|
|
876
|
+
# can't abort the merge. Narrowly rescued so unrelated errors still surface.
|
|
877
|
+
def synchronized_root_group_hierarchy_path(group)
|
|
878
|
+
Xcodeproj::Project::Object::GroupableHelper.hierarchy_path(group)
|
|
879
|
+
rescue RuntimeError
|
|
880
|
+
nil
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def add_file_system_synchronized_build_file_exception_set(containing_component, change,
|
|
884
|
+
change_path)
|
|
885
|
+
unless containing_component.is_a?(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup)
|
|
886
|
+
raise MergeError, "Trying to add file system synchronized build file exception set to an " \
|
|
887
|
+
"unsupported component type #{containing_component.isa}. Change is: " \
|
|
888
|
+
"#{change}"
|
|
889
|
+
end
|
|
890
|
+
return if exception_set_already_exists?(containing_component, change)
|
|
891
|
+
|
|
892
|
+
exception_set = containing_component.project.new(
|
|
893
|
+
Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet
|
|
894
|
+
)
|
|
895
|
+
containing_component.exceptions << exception_set
|
|
896
|
+
add_attributes_to_component(exception_set, change, change_path, ignore_keys: ["target"])
|
|
897
|
+
resolve_or_defer_exception_reference(exception_set, :target, change["target"])
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def add_file_system_synchronized_group_build_phase_membership_exception_set(
|
|
901
|
+
containing_component, change, change_path
|
|
902
|
+
)
|
|
903
|
+
unless containing_component.is_a?(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup)
|
|
904
|
+
raise MergeError, "Trying to add file system synchronized group build phase membership " \
|
|
905
|
+
"exception set to an unsupported component type " \
|
|
906
|
+
"#{containing_component.isa}. Change is: #{change}"
|
|
907
|
+
end
|
|
908
|
+
return if exception_set_already_exists?(containing_component, change)
|
|
909
|
+
|
|
910
|
+
exception_set = containing_component.project.new(
|
|
911
|
+
Xcodeproj::Project::PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet
|
|
912
|
+
)
|
|
913
|
+
containing_component.exceptions << exception_set
|
|
914
|
+
add_attributes_to_component(exception_set, change, change_path, ignore_keys: ["buildPhase"])
|
|
915
|
+
resolve_or_defer_exception_reference(exception_set, :build_phase, change["buildPhase"])
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
# `true` if an equivalent exception set already exists on `group`. This is intentionally NOT
|
|
919
|
+
# gated on `Settings.allow_duplicates`: a synchronized root group is referenced from both the
|
|
920
|
+
# main group and its target(s), so the same exception addition is visited through several graph
|
|
921
|
+
# paths, and two exception sets identical in target/build phase + membership are meaningless.
|
|
922
|
+
def exception_set_already_exists?(group, change)
|
|
923
|
+
group.exceptions.any? { |exception_set| exception_set_matches_change?(exception_set, change) }
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def exception_set_matches_change?(exception_set, change)
|
|
927
|
+
signature = exception_set.to_tree_hash
|
|
928
|
+
pending = @pending_exception_references.find { |set, _, _| set.equal?(exception_set) }
|
|
929
|
+
unless pending.nil?
|
|
930
|
+
_, kind, reference = pending
|
|
931
|
+
signature = signature.merge(exception_reference_key(kind) => reference)
|
|
932
|
+
end
|
|
933
|
+
# `displayName` is derived from the (possibly still-unresolved) reference and the folder name,
|
|
934
|
+
# so it carries no information the reference key and simple attributes don't already carry;
|
|
935
|
+
# excluding it avoids a spurious mismatch when the reference is still pending (nil).
|
|
936
|
+
signature.reject { |key, _| key == "displayName" } ==
|
|
937
|
+
change.reject { |key, _| key == "displayName" }
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
def exception_reference_key(kind)
|
|
941
|
+
kind == :target ? "target" : "buildPhase"
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
# Resolves the target/build phase an exception set references. The target may be created, and
|
|
945
|
+
# the group linked to its targets, only later, so unresolvable references are recorded and
|
|
946
|
+
# retried by `resolve_pending_exception_references` after the whole change is applied. Build
|
|
947
|
+
# phase resolution always defers: it needs the group already linked to a target.
|
|
948
|
+
def resolve_or_defer_exception_reference(exception_set, kind, reference)
|
|
949
|
+
# Only the target reference is resolved eagerly (it just needs the target to exist). The build
|
|
950
|
+
# phase reference is always deferred, because the phase it points to might itself be created
|
|
951
|
+
# later in the same change (e.g. a target and its build phases added together).
|
|
952
|
+
if kind == :target
|
|
953
|
+
resolved = resolve_exception_reference(exception_set, kind, reference)
|
|
954
|
+
unless resolved.nil?
|
|
955
|
+
assign_exception_reference(exception_set, kind, resolved)
|
|
956
|
+
return
|
|
957
|
+
end
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
@pending_exception_references << [exception_set, kind, reference]
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def resolve_pending_exception_references
|
|
964
|
+
@pending_exception_references.each do |exception_set, kind, reference|
|
|
965
|
+
resolved = resolve_exception_reference(exception_set, kind, reference)
|
|
966
|
+
if resolved.nil?
|
|
967
|
+
puts "Warning: Couldn't resolve #{kind} reference #{reference.inspect} for exception " \
|
|
968
|
+
"set '#{exception_set.display_name}'."
|
|
969
|
+
next
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
assign_exception_reference(exception_set, kind, resolved)
|
|
973
|
+
end
|
|
974
|
+
@pending_exception_references = []
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
def resolve_exception_reference(exception_set, kind, reference)
|
|
978
|
+
case kind
|
|
979
|
+
when :target
|
|
980
|
+
find_target(exception_set.project, reference)
|
|
981
|
+
when :build_phase
|
|
982
|
+
find_synchronized_group_build_phase(exception_set, reference)
|
|
983
|
+
end
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
def assign_exception_reference(exception_set, kind, resolved)
|
|
987
|
+
case kind
|
|
988
|
+
when :target then exception_set.target = resolved
|
|
989
|
+
when :build_phase then exception_set.build_phase = resolved
|
|
990
|
+
end
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
# Finds the build phase an exception set references. The reference carries its owning target,
|
|
994
|
+
# so we resolve that target by name (unique) then its build phase by name. This disambiguates
|
|
995
|
+
# same-named phases (e.g. "Sources") when the group is shared by several targets. Returns nil
|
|
996
|
+
# when the target isn't created yet, so the caller can defer.
|
|
997
|
+
def find_synchronized_group_build_phase(exception_set, reference)
|
|
998
|
+
project = exception_set.project
|
|
999
|
+
target_name = reference.is_a?(Hash) ? reference["target"] : nil
|
|
1000
|
+
phase_name = reference.is_a?(Hash) ? reference["name"] : reference
|
|
1001
|
+
|
|
1002
|
+
candidate_targets =
|
|
1003
|
+
if target_name.nil?
|
|
1004
|
+
synchronized_group_referencing_targets(exception_set)
|
|
1005
|
+
else
|
|
1006
|
+
target = find_target(project, target_name)
|
|
1007
|
+
return nil if target.nil?
|
|
1008
|
+
|
|
1009
|
+
[target]
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
matching_build_phases = candidate_targets.flat_map(&:build_phases).select do |build_phase|
|
|
1013
|
+
build_phase.display_name == phase_name
|
|
1014
|
+
end
|
|
1015
|
+
if matching_build_phases.length > 1
|
|
1016
|
+
puts "Debug: Multiple build phases named '#{phase_name}'. Using the first one."
|
|
1017
|
+
end
|
|
1018
|
+
matching_build_phases.first
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
# The targets that reference the exception set's owning synchronized root group. Used as a
|
|
1022
|
+
# fallback when a build phase reference doesn't carry its owning target. The groupable helper
|
|
1023
|
+
# raises `RuntimeError` for an unparented set (e.g. its group was removed earlier in the same
|
|
1024
|
+
# change); fall back to all native targets rather than aborting the merge.
|
|
1025
|
+
def synchronized_group_referencing_targets(exception_set)
|
|
1026
|
+
project = exception_set.project
|
|
1027
|
+
group =
|
|
1028
|
+
begin
|
|
1029
|
+
Xcodeproj::Project::Object::GroupableHelper.parent(exception_set)
|
|
1030
|
+
rescue RuntimeError
|
|
1031
|
+
nil
|
|
1032
|
+
end
|
|
1033
|
+
return project.native_targets if group.nil?
|
|
1034
|
+
|
|
1035
|
+
referencing_targets = project.native_targets.select do |target|
|
|
1036
|
+
target.file_system_synchronized_groups.any? { |linked| linked.uuid == group.uuid }
|
|
1037
|
+
end
|
|
1038
|
+
referencing_targets.empty? ? project.native_targets : referencing_targets
|
|
1039
|
+
end
|
|
1040
|
+
|
|
771
1041
|
def add_build_rule(target, change, change_path)
|
|
772
1042
|
build_rule = target.project.new(Xcodeproj::Project::PBXBuildRule)
|
|
773
1043
|
target.build_rules << build_rule
|
data/lib/kintsugi/version.rb
CHANGED
|
@@ -126,6 +126,69 @@ module Xcodeproj
|
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# Modifies `PBXFileSystemSynchronizedBuildFileExceptionSet`'s `to_tree_hash` to serialize its
|
|
130
|
+
# `target` as a reference (by display name) instead of recursing into it. Without this, the
|
|
131
|
+
# target expands into a hash that contains the synchronized root group owning this exception
|
|
132
|
+
# set, which owns this exception set, causing infinite recursion.
|
|
133
|
+
class PBXFileSystemSynchronizedBuildFileExceptionSet
|
|
134
|
+
# xcodeproj 1.27.0's `display_name` interpolates `#{target.name}`; guard against a nil target
|
|
135
|
+
# (which occurs transiently while the target reference is resolved) to avoid `NoMethodError`
|
|
136
|
+
# during serialization or tree hashing. `GroupableHelper.parent` raises (and re-interpolates
|
|
137
|
+
# `display_name`, recursing to a stack overflow) when the set has no referrers, so only call
|
|
138
|
+
# it when a parent actually exists.
|
|
139
|
+
def display_name
|
|
140
|
+
folder_name = referrers.empty? ? nil : GroupableHelper.parent(self)&.display_name
|
|
141
|
+
"Exceptions for \"#{folder_name}\" folder in \"#{target&.name}\" target"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def to_tree_hash
|
|
145
|
+
hash = { 'displayName' => display_name, 'isa' => isa }
|
|
146
|
+
self.class.simple_attributes.each do |attribute|
|
|
147
|
+
value = attribute.get_value(self)
|
|
148
|
+
hash[attribute.plist_name] = value unless value.nil?
|
|
149
|
+
end
|
|
150
|
+
hash['target'] = target.display_name if target
|
|
151
|
+
hash
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Same fix as `PBXFileSystemSynchronizedBuildFileExceptionSet`, for the build phase membership
|
|
156
|
+
# variant, whose recursing reference is `build_phase`.
|
|
157
|
+
class PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet
|
|
158
|
+
# xcodeproj 1.27.0's `display_name` calls `build_phase.name`, which build phases don't
|
|
159
|
+
# implement, raising `NoMethodError` on any serialization or tree hash of this object. Use
|
|
160
|
+
# the build phase's `display_name` (e.g. "Sources") instead. Also guard the parent lookup,
|
|
161
|
+
# which otherwise raises and recurses to a stack overflow when the set has no referrers.
|
|
162
|
+
def display_name
|
|
163
|
+
folder_name = referrers.empty? ? nil : GroupableHelper.parent(self)&.display_name
|
|
164
|
+
"Exceptions for \"#{folder_name}\" folder in \"#{build_phase&.display_name}\" build phase"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def to_tree_hash
|
|
168
|
+
hash = { 'displayName' => display_name, 'isa' => isa }
|
|
169
|
+
self.class.simple_attributes.each do |attribute|
|
|
170
|
+
value = attribute.get_value(self)
|
|
171
|
+
hash[attribute.plist_name] = value unless value.nil?
|
|
172
|
+
end
|
|
173
|
+
hash['buildPhase'] = build_phase_reference if build_phase
|
|
174
|
+
hash
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Serializes `build_phase` as a reference that also carries its owning target, so it can be
|
|
178
|
+
# resolved unambiguously even when the owning group is shared by multiple targets that each
|
|
179
|
+
# have a build phase with the same name (e.g. "Sources").
|
|
180
|
+
def build_phase_reference
|
|
181
|
+
reference = { 'name' => build_phase.display_name }
|
|
182
|
+
# Match by UUID: xcodeproj compares build phases by value, so `include?` would match a
|
|
183
|
+
# same-named empty phase on an unrelated target.
|
|
184
|
+
owning_target = build_phase.project.native_targets.find do |target|
|
|
185
|
+
target.build_phases.any? { |phase| phase.uuid == build_phase.uuid }
|
|
186
|
+
end
|
|
187
|
+
reference['target'] = owning_target.display_name unless owning_target.nil?
|
|
188
|
+
reference
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
129
192
|
# By default, for this type, the `display_name` is used when calling `ascii_plist_annotation` (which is used
|
|
130
193
|
# to serialize the project to disk). In the case where the `display_name` contains a "plugin:" prefix, which
|
|
131
194
|
# means that the package is a plugin, the prefix is ommitted so just the package name is used.
|