Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions packages/react-native/scripts/cocoapods/__tests__/spm-test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

require "test/unit"
require "fileutils"
require "cocoapods"
require_relative "../spm.rb"

# These tests exercise the real `Pod::Project` UUID machinery (via the
# `xcodeproj`/`cocoapods` gems) because the bug they guard against is emergent
# from how `Pod::Project` hands out UUIDs, and cannot be observed against a mock.
class SPMTests < Test::Unit::TestCase
PodSpecStub = Struct.new(:name)
InstallerStub = Struct.new(:pods_project)

POD_NAME = "ReactNativeEnrichedMarkdown"
TMP_DIR = File.join(Dir.tmpdir, "rn-spm-test")

def setup
FileUtils.rm_rf(TMP_DIR)
FileUtils.mkdir_p(TMP_DIR)
end

def teardown
FileUtils.rm_rf(TMP_DIR)
end

def build_project(num_pods)
path = File.join(TMP_DIR, "Pods.xcodeproj")
FileUtils.mkdir_p(path)
project = Pod::Project.new(path)
num_pods.times { |i| project.new_target(:static_library, "Pod#{i}", :ios) }
project.new_target(:static_library, POD_NAME, :ios)
project.save
project
end

def inject_spm(project)
manager = SPMManager.new
manager.dependency(
PodSpecStub.new(POD_NAME),
url: "https://github.com/software-mansion-labs/RaTeX.git",
requirement: { kind: "upToNextMajorVersion", minimumVersion: "0.1.0" },
products: ["RaTeX"]
)
manager.apply_on_post_install(InstallerStub.new(project))
project.save
end

# Simulates the state after an on-disk reload / incremental `pod install`:
# existing objects keep their counter-based UUIDs, but the generator counters
# reset to zero. This is what makes `Pod::Project#generate_uuid` hand back a
# UUID (`<prefix>00000000`) that already belongs to the root object.
def simulate_reload(project)
project.instance_variable_set(:@generated_uuids, [])
project.instance_variable_set(:@available_uuids, [])
end

def assert_loadable_project(path)
reopened = nil
assert_nothing_raised("Pods project must reload cleanly after SPM injection") do
reopened = Xcodeproj::Project.open(path)
end
root_uuid = reopened.root_object.uuid
assert(
reopened.objects_by_uuid[root_uuid].is_a?(Xcodeproj::Project::Object::PBXProject),
"rootObject UUID must resolve to a PBXProject"
)
package_uuids = reopened.root_object.package_references.map(&:uuid)
assert(
package_uuids.none? { |uuid| uuid == root_uuid },
"injected package reference must not collide with the root object UUID"
)
reopened
end

def test_spm_injection_on_freshly_generated_project_reloads_cleanly
project = build_project(88)
inject_spm(project)
assert_loadable_project(project.path)
end

def test_spm_injection_after_project_reload_does_not_collide_with_root_object
project = build_project(88)
simulate_reload(project)
inject_spm(project)
assert_loadable_project(project.path)
end

def test_injected_uuids_are_unique_across_all_objects
project = build_project(88)
simulate_reload(project)
inject_spm(project)
reopened = assert_loadable_project(project.path)
uuids = reopened.objects.map(&:uuid)
assert_equal(uuids.length, uuids.uniq.length, "all object UUIDs must be unique")
end
end
29 changes: 26 additions & 3 deletions packages/react-native/scripts/cocoapods/spm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,29 @@ def apply_on_post_install(installer)

private

# Creates a new object in the project with a UUID guaranteed not to collide
# with any UUID already present in the project.
#
# `Pod::Project` overrides `generate_available_uuid_list` with a fast,
# counter-based scheme (`<sha prefix><counter>0`) that deliberately skips
# collision checks, on the assumption that the whole Pods project is generated
# in a single pass. That assumption does not hold here: we run in a
# `post_install` hook, and the generator's counter can be out of sync with the
# UUIDs already assigned to existing objects (e.g. when the project has been
# reloaded from disk during an incremental install, the counter restarts at 0
# while the root object still occupies `<prefix>00000000`). Using `project.new`
# directly can therefore hand back a UUID that is already in use and overwrite
# an existing object (notably the root `PBXProject`), producing a Pods project
# Xcode refuses to load. We keep the deterministic scheme but probe forward
# until we find a UUID that is actually free.
def new_object(project, klass)
uuid = project.generate_uuid
uuid = project.generate_uuid while project.objects_by_uuid.key?(uuid)
object = klass.new(project, uuid)
object.initialize_defaults
object
end

def log(msg)
::Pod::UI.puts "[SPM] #{msg}"
end
Expand All @@ -76,7 +99,7 @@ def add_spm_to_target(project, target, url, requirement, products)
pkg_class = Xcodeproj::Project::Object::XCLocalSwiftPackageReference
pkg = project.root_object.package_references.find { |p| p.class == pkg_class && p.relative_path == url }
if !pkg
pkg = project.new(pkg_class)
pkg = new_object(project, pkg_class)
pkg.relative_path = url
log(" Adding local package to workspace: #{pkg.inspect}")
project.root_object.package_references << pkg
Expand All @@ -85,7 +108,7 @@ def add_spm_to_target(project, target, url, requirement, products)
pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference
pkg = project.root_object.package_references.find { |p| p.class == pkg_class && p.repositoryURL == url }
if !pkg
pkg = project.new(pkg_class)
pkg = new_object(project, pkg_class)
pkg.repositoryURL = url
pkg.requirement = requirement
log(" Adding remote package to workspace: #{pkg.inspect}")
Expand All @@ -101,7 +124,7 @@ def add_spm_to_target(project, target, url, requirement, products)
next if ref

log(" Adding product dependency #{product_name} to #{target.name}")
ref = project.new(ref_class)
ref = new_object(project, ref_class)
ref.package = pkg
ref.product_name = product_name
target.package_product_dependencies << ref
Expand Down
Loading