Skip to content

Support build files that reference a Swift package product - #143

Open
Ryan Zulkoski (rzulkoski) wants to merge 2 commits into
Lightricks:mainfrom
hallow-inc:fix/swift-package-product-build-files
Open

Ryan Zulkoski (rzulkoski) wants to merge 2 commits into
Lightricks:mainfrom
hallow-inc:fix/swift-package-product-build-files

Conversation

@rzulkoski

Copy link
Copy Markdown
Contributor

What & why

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) during a merge.

This warns and returns only when both fileRef and productRef are absent, and:

  • dedups productRef build files by their full product reference (two products from different packages may share a product name);
  • reuses an equivalent package product dependency already present in the same target rather than duplicating it — a dependency is shared within a target by its packageProductDependencies entry and the linking build file's productRef, 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

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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!
nicely implemented, thanks for dealing with the mess productRef adds.
2 small comments.

Comment thread lib/kintsugi/apply_change_to_project.rb Outdated
existing_dependency =
existing_package_product_dependency(target, swift_package_product_dependency)
unless existing_dependency.nil?
swift_package_product_dependency.remove_from_project

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — done. It now looks up an equivalent dependency before creating one, so nothing is added-then-removed. existing_package_product_dependency takes the diff hash directly and matches on to_tree_hash == change (the round-trip holds, so it's equivalent to the old comparison). Thanks!

Comment on lines +663 to +666
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test for that shape — and it surfaced that the package reference itself was being duplicated (two value-identical XCRemoteSwiftPackageReference objects), a pre-existing issue in add_remote_swift_package_reference rather than something specific to productRef. Since you flagged the shared-object expectation, I folded in a fix: remote package references are now deduped project-wide, so rootObject.packageReferences and the dependency's package point at the same object, as Xcode does. The new test asserts that sharing.

…add-then-remove

Restructure add_swift_package_product_dependency to look up an equivalent
package product dependency before creating one, rather than creating a
candidate and removing it on a hit (per review feedback) -- no throwaway
object is added. existing_package_product_dependency now matches the diff
hash directly (to_tree_hash == change).

Also dedup remote package references project-wide: rootObject.packageReferences
and a product dependency's `package` share one XCRemoteSwiftPackageReference,
as Xcode writes it, rather than two value-identical objects. Add a spec for
that shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants