From a539dcdb08daf10a16d76a21f52f2be2a6d1135f Mon Sep 17 00:00:00 2001 From: Afnan Ahmad <2064669+afnanahmad@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:31:46 -0600 Subject: [PATCH] Allow xcodeproj up to 1.28.1 to keep on-demand resource tags xcodeproj 1.27.0 does not model the assetTagsByRelativePath attribute of PBXFileSystemSynchronizedBuildFileExceptionSet, so any load/save cycle, including a merge through the Kintsugi driver, silently deletes every on-demand resource tag map from the project file. xcodeproj 1.28.1 models the attribute and preserves it. Raise the upper bound of the xcodeproj dependency to 1.28.1 and add an integration spec that runs a project containing such an exception set through a real merge with the driver and asserts the attribute survives. The spec fails with xcodeproj 1.27.0 and passes with 1.28.1. Co-Authored-By: Claude --- kintsugi.gemspec | 2 +- spec/kintsugi_integration_spec.rb | 90 +++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/kintsugi.gemspec b/kintsugi.gemspec index 89c89d4..97e7360 100644 --- a/kintsugi.gemspec +++ b/kintsugi.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency "tty-prompt", "~> 0" - spec.add_dependency "xcodeproj", ">= 1.26.0", "<= 1.27.0" + spec.add_dependency "xcodeproj", ">= 1.26.0", "<= 1.28.1" spec.add_development_dependency "git", "~> 1.11" spec.add_development_dependency "rake", "~> 13.0" diff --git a/spec/kintsugi_integration_spec.rb b/spec/kintsugi_integration_spec.rb index a769df3..fb859a5 100644 --- a/spec/kintsugi_integration_spec.rb +++ b/spec/kintsugi_integration_spec.rb @@ -185,10 +185,100 @@ def create_new_project_at_path(path) project end +def find_exception_set_hash(plist) + plist["objects"].values.find do |object| + object["isa"] == "PBXFileSystemSynchronizedBuildFileExceptionSet" + end +end + +def write_exception_set_asset_tags(project_path, asset_tags) + pbxproj_path = File.join(project_path, "project.pbxproj") + plist = Xcodeproj::Plist.read_from_path(pbxproj_path) + find_exception_set_hash(plist)["assetTagsByRelativePath"] = asset_tags + File.open(pbxproj_path, "w") do |file| + Nanaimo::Writer::PBXProjWriter + .new(Nanaimo::Plist.new(plist, :ascii), pretty: true, output: file, strict: false).write + end +end + +def read_exception_set_asset_tags(project_path) + plist = Xcodeproj::Plist.read_from_path(File.join(project_path, "project.pbxproj")) + exception_set = find_exception_set_hash(plist) + exception_set && exception_set["assetTagsByRelativePath"] +end + describe Kintsugi, :kintsugi do %w[rebase cherry-pick merge].each do |git_command| ["foo.xcodeproj", "foo with space.xcodeproj"].each do |project_name| it_behaves_like("tests", git_command, project_name) end end + + context "when merging a project with on-demand resource asset tags" do + let(:temporary_directories_paths) { [] } + let(:git_directory_path) { Dir.mktmpdir.tap { |path| temporary_directories_paths << path } } + let(:git) { Git.init(git_directory_path) } + + before do + git.config("user.email", "you@example.com") + git.config("user.name", "Your Name") + end + + after do + temporary_directories_paths.each do |directory_path| + FileUtils.remove_entry(directory_path) + end + end + + it "keeps 'assetTagsByRelativePath' of an exception set when resolving with driver" do + git.config("merge.kintsugi.name", "Kintsugi driver") + git.config("merge.kintsugi.driver", "#{__dir__}/../bin/kintsugi driver %O %A %B %P") + File.write(File.join(git_directory_path, ".gitattributes"), "*.pbxproj merge=kintsugi") + + asset_tags = {"OnDemandAssets/Level1.imageset" => ["level1"]} + + project = create_new_project_at_path(File.join(git_directory_path, "foo.xcodeproj")) + target = project.new_target("com.apple.product-type.library.static", "foo", :ios) + group = project.new(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup) + group.source_tree = "" + group.path = "SyncedSources" + exception_set = + project.new(Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet) + exception_set.target = target + exception_set.membership_exceptions = ["Excluded.swift"] + group.exceptions << exception_set + project.main_group.children << group + target.file_system_synchronized_groups << group + project.save + # `assetTagsByRelativePath` is written to the file directly because not all xcodeproj + # versions in the supported range model this attribute, and those that don't drop it when + # a project is saved, which is exactly what this test guards against. + write_exception_set_asset_tags(project.path, asset_tags) + + git.add(all: true) + git.commit("Initial project") + + project = Xcodeproj::Project.open(project.path) + project.new_target("com.apple.product-type.library.static", "bar", :ios) + project.save + write_exception_set_asset_tags(project.path, asset_tags) + git.add(all: true) + git.commit("Add target bar") + first_commit_hash = git.revparse("HEAD") + + git.checkout("HEAD^") + project = Xcodeproj::Project.open(project.path) + project.new_target("com.apple.product-type.library.static", "baz", :ios) + project.save + write_exception_set_asset_tags(project.path, asset_tags) + git.add(all: true) + git.commit("Add target baz") + + `git -C #{git_directory_path} merge #{first_commit_hash} &> /dev/null` + + project = Xcodeproj::Project.open(project.path) + expect(project.targets.map(&:display_name)).to contain_exactly("foo", "bar", "baz") + expect(read_exception_set_asset_tags(project.path)).to eq(asset_tags) + end + end end