Support build files that reference a Swift package product - #143
Ryan Zulkoski (rzulkoski) wants to merge 1 commit into
Conversation
A PBXBuildFile references either a file (fileRef) or a Swift package product (productRef, an XCSwiftPackageProductDependency). add_build_file assumed the former: it warned "Trying to add a build file without any file reference" and returned whenever fileRef was nil, silently dropping any build file that links a package product (e.g. a package framework in a Frameworks build phase). Warn and return only when both fileRef and productRef are absent, and dedup productRef build files by their full product reference (two products from different packages may share a product name). A package product dependency is shared within a target by its packageProductDependencies entry and the productRef of the build file that links it, so reuse an equivalent dependency already present in the same target rather than adding a duplicate (scoped per target, since Xcode keeps a separate dependency object per target). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Noam Freeman (noamfreeman)
left a comment
There was a problem hiding this comment.
Thanks!
nicely implemented, thanks for dealing with the mess productRef adds.
2 small comments.
| existing_dependency = | ||
| existing_package_product_dependency(target, swift_package_product_dependency) | ||
| unless existing_dependency.nil? | ||
| swift_package_product_dependency.remove_from_project |
There was a problem hiding this comment.
hygiene, non blocking.
will it be better to lookup then add, instead of adding then removing?
we can end with unwanted additions with the add then remove
something like this (of coarse, we can keep the usefull comment):
--- a/lib/kintsugi/apply_change_to_project.rb
+++ b/lib/kintsugi/apply_change_to_project.rb
@@ -746,21 +746,16 @@ module Kintsugi
def add_swift_package_product_dependency(containing_component, change, change_path)
- project = containing_component.project
- swift_package_product_dependency =
- project.new(Xcodeproj::Project::XCSwiftPackageProductDependency)
- add_attributes_to_component(swift_package_product_dependency, change, change_path)
-
- # Within a single target, the target's `packageProductDependencies` entry and the `productRef`
- # of the build file that links the product are the same object. The diff adds it from both
- # places, so reuse an equivalent dependency already present in the SAME target rather than
- # adding a duplicate. The reuse is scoped to the target because Xcode keeps a separate
- # dependency object per target.
target = owning_native_target(containing_component)
- existing_dependency =
- existing_package_product_dependency(target, swift_package_product_dependency)
- unless existing_dependency.nil?
- swift_package_product_dependency.remove_from_project
- swift_package_product_dependency = existing_dependency
+ swift_package_product_dependency = existing_package_product_dependency(target, change)
+
+ if swift_package_product_dependency.nil?
+ swift_package_product_dependency =
+ containing_component.project.new(Xcodeproj::Project::XCSwiftPackageProductDependency)
+ add_attributes_to_component(swift_package_product_dependency, change, change_path)
end
case containing_component
@@ -793,13 +788,13 @@ module Kintsugi
# An existing package product dependency of `target` (either in its `packageProductDependencies`
# or referenced by one of its build files) equivalent to `dependency`, or nil if there is none.
- def existing_package_product_dependency(target, dependency)
+ def existing_package_product_dependency(target, change)
return nil if target.nil?
candidates = target.package_product_dependencies.to_a +
target.build_phases.flat_map(&:files).map(&:product_ref).compact
candidates.uniq.find do |candidate|
- !candidate.equal?(dependency) && candidate.to_tree_hash == dependency.to_tree_hash
+ candidate.to_tree_hash == change
end
end| theirs_project.targets[0].package_product_dependencies << dependency | ||
| build_file = theirs_project.new(Xcodeproj::Project::PBXBuildFile) | ||
| build_file.product_ref = dependency | ||
| theirs_project.targets[0].frameworks_build_phase.files << build_file |
There was a problem hiding this comment.
also hygiene, for future agents adding tests. no blocking
can we add a test that also puts the package on rootObject.packageReferences?
i think this is what xcode does when adding a package.
the package on rootObject.packageReferences and the product dependancy point to the same object (and the productRef pointing at the dependancy, of coarse)
What & why
A
PBXBuildFilereferences either a file (fileRef) or a Swift package product (productRef, anXCSwiftPackageProductDependency).add_build_fileassumed the former: it warned "Trying to add a build file without any file reference" and returned wheneverfileRefwas nil, silently dropping any build file that links a package product (e.g. a package framework in a Frameworks build phase) during a merge.This warns and returns only when both
fileRefandproductRefare absent, and:productRefbuild files by their full product reference (two products from different packages may share a product name);packageProductDependenciesentry and the linking build file'sproductRef, but Xcode keeps a separate dependency object per target.Testing
Adds specs for productRef build files, the shared within-target dependency, per-target separation, and same-name-different-package products; full suite green.
Disclosure
Developed with Claude Code, including several rounds of automated adversarial review before submission.
🤖 Generated with Claude Code