diff --git a/swift/lib/dependabot/swift/file_fetcher.rb b/swift/lib/dependabot/swift/file_fetcher.rb
index 01b48130b52..354f2367dda 100644
--- a/swift/lib/dependabot/swift/file_fetcher.rb
+++ b/swift/lib/dependabot/swift/file_fetcher.rb
@@ -5,20 +5,24 @@
require "dependabot/experiments"
require "dependabot/file_fetchers"
require "dependabot/file_fetchers/base"
+require "dependabot/swift/xcode_file_helpers"
module Dependabot
module Swift
class FileFetcher < Dependabot::FileFetchers::Base
extend T::Sig
+ XCODEPROJ_SUFFIX = ".xcodeproj"
+ XCWORKSPACE_SUFFIX = ".xcworkspace"
XCODE_SPM_PACKAGE_RESOLVED_PATH = "project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
+ XCWORKSPACE_PACKAGE_RESOLVED_PATH = "xcshareddata/swiftpm/Package.resolved"
sig { override.params(filenames: T::Array[String]).returns(T::Boolean) }
def self.required_files_in?(filenames)
return true if filenames.include?("Package.swift")
if Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
- return filenames.any? { |f| f.end_with?("Package.resolved") }
+ return filenames.any? { |f| XcodeFileHelpers.xcode_resolved_path?(f) }
end
false
@@ -28,7 +32,7 @@ def self.required_files_in?(filenames)
def self.required_files_message
if Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
"Repo must contain a Package.swift configuration file or " \
- "an .xcodeproj directory with a Package.resolved file."
+ "an .xcodeproj/.xcworkspace directory with a Package.resolved file."
else
"Repo must contain a Package.swift configuration file."
end
@@ -74,17 +78,62 @@ def fetch_xcode_spm_files(fetched_files)
resolved = fetch_file_if_present(File.join(xcodeproj_path, XCODE_SPM_PACKAGE_RESOLVED_PATH))
fetched_files << resolved if resolved
end
+
+ xcworkspace_dirs.each do |workspace_path|
+ workspace_data = fetch_support_file(File.join(workspace_path, "contents.xcworkspacedata"))
+ fetched_files << workspace_data if workspace_data
+
+ resolved = fetch_file_if_present(File.join(workspace_path, XCWORKSPACE_PACKAGE_RESOLVED_PATH))
+ fetched_files << resolved if resolved
+ end
end
sig { returns(T::Array[String]) }
def xcodeproj_dirs
@xcodeproj_dirs ||= T.let(
- repo_contents(dir: ".", raise_errors: false)
- .select { |entry| entry.type == "dir" && entry.name.end_with?(".xcodeproj") }
- .map(&:name),
+ discover_dirs_with_suffix(XCODEPROJ_SUFFIX),
T.nilable(T::Array[String])
)
end
+
+ sig { returns(T::Array[String]) }
+ def xcworkspace_dirs
+ @xcworkspace_dirs ||= T.let(
+ discover_dirs_with_suffix(XCWORKSPACE_SUFFIX)
+ .reject { |path| path.include?("#{XCODEPROJ_SUFFIX}/") },
+ T.nilable(T::Array[String])
+ )
+ end
+
+ sig { params(suffix: String).returns(T::Array[String]) }
+ def discover_dirs_with_suffix(suffix)
+ discovered = T.let([], T::Array[String])
+ queue = T.let(["."], T::Array[String])
+ visited = T.let({}, T::Hash[String, T::Boolean])
+ index = T.let(0, Integer)
+
+ while index < queue.length
+ dir = T.must(queue[index])
+ index += 1
+ next if visited[dir]
+
+ visited[dir] = true
+
+ entries = repo_contents(dir: dir, raise_errors: false)
+ entries.each do |entry|
+ next unless entry.type == "dir"
+
+ next_dir = dir == "." ? entry.name : File.join(dir, entry.name)
+ if entry.name.end_with?(suffix)
+ discovered << next_dir
+ elsif !entry.name.start_with?(".")
+ queue << next_dir
+ end
+ end
+ end
+
+ discovered.sort
+ end
end
end
end
diff --git a/swift/lib/dependabot/swift/file_parser.rb b/swift/lib/dependabot/swift/file_parser.rb
index cca6c20a34f..3815ba53dff 100644
--- a/swift/lib/dependabot/swift/file_parser.rb
+++ b/swift/lib/dependabot/swift/file_parser.rb
@@ -10,6 +10,7 @@
require "dependabot/swift/file_parser/xcode_spm_resolver"
require "dependabot/swift/package_manager"
require "dependabot/swift/language"
+require "dependabot/swift/xcode_file_helpers"
module Dependabot
module Swift
@@ -115,13 +116,12 @@ def package_manifest_file
@package_manifest_file ||= T.let(get_original_file("Package.swift"), T.nilable(Dependabot::DependencyFile))
end
- # All non-support Package.resolved files from Xcode project directories
+ # All non-support Package.resolved files from Xcode project and workspace directories (.xcodeproj, .xcworkspace)
sig { returns(T::Array[Dependabot::DependencyFile]) }
def xcode_resolved_files
@xcode_resolved_files ||= T.let(
dependency_files.select do |f|
- f.name.end_with?("Package.resolved") &&
- f.name.include?(".xcodeproj/") &&
+ XcodeFileHelpers.xcode_resolved_path?(f.name) &&
!f.support_file?
end,
T.nilable(T::Array[Dependabot::DependencyFile])
diff --git a/swift/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb b/swift/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb
index e753e984c95..aafa68c597d 100644
--- a/swift/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb
+++ b/swift/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb
@@ -7,14 +7,16 @@
require "dependabot/swift/file_parser"
require "dependabot/swift/file_parser/package_resolved_parser"
require "dependabot/swift/file_parser/pbxproj_parser"
+require "dependabot/swift/xcode_file_helpers"
module Dependabot
module Swift
class FileParser < Dependabot::FileParsers::Base
# Orchestrates Xcode-managed SwiftPM dependency parsing.
#
- # Parses Package.resolved JSON files found inside .xcodeproj directories,
- # then enriches each dependency with requirement info extracted from the
+ # Parses Package.resolved JSON files found inside Xcode project and
+ # workspace directories (e.g., .xcodeproj and .xcworkspace), then
+ # enriches each dependency with requirement info extracted from the
# corresponding project.pbxproj files.
class XcodeSpmResolver
extend T::Sig
@@ -38,8 +40,10 @@ def parse
xcode_resolved_files.each do |resolved_file|
resolved_deps = PackageResolvedParser.new(resolved_file).parse
- xcodeproj_dir = extract_xcodeproj_dir(resolved_file.name)
- pbxproj_requirements = scoped_requirements.fetch(xcodeproj_dir, {})
+ pbxproj_requirements = requirements_for_resolved_file(
+ scoped_requirements: scoped_requirements,
+ resolved_file_name: resolved_file.name
+ )
resolved_deps.each do |dep|
enriched = enrich_with_pbxproj_requirements(dep, pbxproj_requirements)
@@ -59,24 +63,67 @@ def parse
attr_reader :pbxproj_files
# Collects requirement info from all project.pbxproj support files,
- # keyed by xcodeproj directory so each resolved file only sees
- # requirements from its own Xcode project.
+ # keyed by Xcode scope directory so each resolved file can be enriched
+ # by requirements from its closest matching Xcode scope.
sig { returns(T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) }
def aggregate_pbxproj_requirements
scoped = T.let({}, T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]])
pbxproj_files.each do |pbxproj_file|
- xcodeproj_dir = extract_xcodeproj_dir(pbxproj_file.name)
- scoped[xcodeproj_dir] ||= {}
+ xcode_scope_dir = extract_xcode_scope_dir(pbxproj_file.name)
+ scoped[xcode_scope_dir] ||= {}
PbxprojParser.new(pbxproj_file).parse.each do |name, req_info|
- T.must(scoped[xcodeproj_dir])[name] = req_info
+ T.must(scoped[xcode_scope_dir])[name] = req_info
end
end
scoped
end
+ sig do
+ params(
+ scoped_requirements: T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]
+ ).returns(T::Hash[String, T::Hash[Symbol, T.untyped]])
+ end
+ def merge_scopes(scoped_requirements)
+ scoped_requirements.values.each_with_object({}) do |requirements, merged|
+ requirements.each { |name, req_info| merged[name] = req_info }
+ end
+ end
+
+ sig do
+ params(
+ scoped_requirements: T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]],
+ resolved_file_name: String
+ ).returns(T::Hash[String, T::Hash[Symbol, T.untyped]])
+ end
+ def requirements_for_resolved_file(scoped_requirements:, resolved_file_name:)
+ scope_dir = extract_xcode_scope_dir(resolved_file_name)
+ return T.must(scoped_requirements[scope_dir]) if scoped_requirements.key?(scope_dir)
+
+ workspace_root = workspace_root_for_scope(scope_dir)
+ return {} unless workspace_root
+
+ local_scopes = scoped_requirements.select do |candidate_scope, _|
+ scope_in_workspace_root?(candidate_scope, workspace_root)
+ end
+ return {} if local_scopes.empty?
+
+ merge_scopes(local_scopes)
+ end
+
+ sig { params(candidate_scope: T.nilable(String), workspace_root: String).returns(T::Boolean) }
+ def scope_in_workspace_root?(candidate_scope, workspace_root)
+ return false unless candidate_scope
+
+ if workspace_root == "."
+ !candidate_scope.include?("/")
+ else
+ candidate_scope.start_with?("#{workspace_root}/")
+ end
+ end
+
# Enriches a dependency parsed from Package.resolved with requirement
# info from the matching project.pbxproj
sig do
@@ -117,13 +164,18 @@ def enrich_with_pbxproj_requirements(dep, pbxproj_requirements)
)
end
- # Extracts the .xcodeproj directory name from a file path.
- # e.g. "MyApp.xcodeproj/project.xcworkspace/.../Package.resolved" -> "MyApp.xcodeproj"
- # e.g. "sub/dir/App.xcodeproj/project.pbxproj" -> "sub/dir/App.xcodeproj"
+ # Extracts the Xcode scope directory (.xcodeproj or .xcworkspace)
+ # from a file path.
sig { params(path: String).returns(T.nilable(String)) }
- def extract_xcodeproj_dir(path)
- match = path.match(%r{^(.*?\.xcodeproj)/})
- match&.captures&.first
+ def extract_xcode_scope_dir(path)
+ XcodeFileHelpers.extract_xcode_scope_dir(path)
+ end
+
+ sig { params(scope_dir: T.nilable(String)).returns(T.nilable(String)) }
+ def workspace_root_for_scope(scope_dir)
+ return nil unless scope_dir&.end_with?(".xcworkspace")
+
+ File.dirname(scope_dir)
end
end
end
diff --git a/swift/lib/dependabot/swift/file_updater.rb b/swift/lib/dependabot/swift/file_updater.rb
index 52ca6343414..2c49e05f72f 100644
--- a/swift/lib/dependabot/swift/file_updater.rb
+++ b/swift/lib/dependabot/swift/file_updater.rb
@@ -7,6 +7,7 @@
require "dependabot/swift/file_updater/lockfile_updater"
require "dependabot/swift/file_updater/manifest_updater"
require "dependabot/swift/file_updater/xcode_lockfile_updater"
+require "dependabot/swift/xcode_file_helpers"
module Dependabot
module Swift
@@ -53,7 +54,8 @@ def updated_xcode_spm_files
xcode_resolved_files.each do |resolved_file|
updater = XcodeLockfileUpdater.new(
resolved_file: resolved_file,
- dependencies: dependencies
+ dependencies: dependencies,
+ workspace_files: xcode_workspace_files
)
next unless updater.lockfile_changed?
@@ -100,14 +102,23 @@ def xcode_spm_mode?
def xcode_resolved_files
@xcode_resolved_files ||= T.let(
dependency_files.select do |f|
- f.name.end_with?("Package.resolved") &&
- f.name.include?(".xcodeproj/") &&
+ XcodeFileHelpers.xcode_resolved_path?(f.name) &&
!f.support_file?
end,
T.nilable(T::Array[Dependabot::DependencyFile])
)
end
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
+ def xcode_workspace_files
+ @xcode_workspace_files ||= T.let(
+ dependency_files.select do |f|
+ f.name.end_with?("contents.xcworkspacedata") && f.support_file?
+ end,
+ T.nilable(T::Array[Dependabot::DependencyFile])
+ )
+ end
+
sig { returns(String) }
def updated_manifest_content
ManifestUpdater.new(
diff --git a/swift/lib/dependabot/swift/file_updater/xcode_lockfile_updater.rb b/swift/lib/dependabot/swift/file_updater/xcode_lockfile_updater.rb
index ed8b847c2ef..d097c450295 100644
--- a/swift/lib/dependabot/swift/file_updater/xcode_lockfile_updater.rb
+++ b/swift/lib/dependabot/swift/file_updater/xcode_lockfile_updater.rb
@@ -7,9 +7,9 @@
require "dependabot/dependency_file"
require "dependabot/errors"
require "dependabot/shared_helpers"
-require "dependabot/file_updaters"
-require "dependabot/file_updaters/base"
+require "dependabot/swift/file_updater"
require "dependabot/swift/url_helpers"
+require "dependabot/swift/xcode_file_helpers"
module Dependabot
module Swift
@@ -32,12 +32,14 @@ class XcodeLockfileUpdater
sig do
params(
resolved_file: Dependabot::DependencyFile,
- dependencies: T::Array[Dependabot::Dependency]
+ dependencies: T::Array[Dependabot::Dependency],
+ workspace_files: T::Array[Dependabot::DependencyFile]
).void
end
- def initialize(resolved_file:, dependencies:)
+ def initialize(resolved_file:, dependencies:, workspace_files: [])
@resolved_file = resolved_file
@dependencies = dependencies
+ @workspace_files = workspace_files
end
sig { returns(String) }
@@ -82,6 +84,9 @@ def lockfile_changed?
sig { returns(T::Array[Dependabot::Dependency]) }
attr_reader :dependencies
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
+ attr_reader :workspace_files
+
sig { params(content: String).returns(T::Hash[String, T.untyped]) }
def parse_json(content)
JSON.parse(content)
@@ -222,29 +227,75 @@ def dependencies_for_file
@dependencies_for_file ||= T.let(
dependencies.select do |dep|
dep.requirements.any? do |req|
- req_file = req[:file]
- if req_file == resolved_file.name
- true
- elsif req_file&.include?(".xcodeproj/")
- # Extract the xcodeproj dir from both files and compare
- req_xcodeproj = extract_xcodeproj_dir(req_file)
- resolved_xcodeproj = extract_xcodeproj_dir(resolved_file.name)
- req_xcodeproj && req_xcodeproj == resolved_xcodeproj
- else
- false
- end
+ req_file_matches_resolved_scope?(req[:file])
end
end,
T.nilable(T::Array[Dependabot::Dependency])
)
end
- # Extracts the .xcodeproj directory from a file path.
- # e.g. "MyApp.xcodeproj/project.xcworkspace/.../Package.resolved" -> "MyApp.xcodeproj"
+ sig { params(req_file: T.nilable(String)).returns(T::Boolean) }
+ def req_file_matches_resolved_scope?(req_file)
+ return false unless req_file
+ return true if req_file == resolved_file.name
+ return false unless req_file.include?(".xcodeproj/") || req_file.include?(".xcworkspace/")
+
+ req_scope = extract_xcode_scope_dir(req_file)
+ resolved_scope = extract_xcode_scope_dir(resolved_file.name)
+
+ return true if req_scope && resolved_scope && req_scope == resolved_scope
+
+ workspace_related_dependency?(req_file)
+ end
+
+ # Extracts the Xcode scope directory (.xcodeproj or .xcworkspace)
+ # from a file path.
sig { params(path: String).returns(T.nilable(String)) }
- def extract_xcodeproj_dir(path)
- match = path.match(%r{^(.*?\.xcodeproj)/})
- match&.captures&.first
+ def extract_xcode_scope_dir(path)
+ XcodeFileHelpers.extract_xcode_scope_dir(path)
+ end
+
+ sig { params(req_file: T.nilable(String)).returns(T::Boolean) }
+ def workspace_related_dependency?(req_file)
+ return false unless req_file
+
+ workspace_scope = extract_xcode_scope_dir(resolved_file.name)
+ return false unless workspace_scope&.end_with?(".xcworkspace")
+ return false unless req_file.include?(".xcodeproj/")
+
+ req_scope = extract_xcode_scope_dir(req_file)
+ return false unless req_scope
+
+ referenced = referenced_project_scopes_for_workspace(workspace_scope)
+ return referenced.include?(req_scope) if referenced.any?
+
+ workspace_root = File.dirname(workspace_scope)
+
+ if workspace_root == "."
+ !req_scope.include?("/")
+ else
+ req_scope.start_with?("#{workspace_root}/")
+ end
+ end
+
+ sig { params(workspace_scope: String).returns(T::Set[String]) }
+ def referenced_project_scopes_for_workspace(workspace_scope)
+ workspace_data_path = "#{workspace_scope}/contents.xcworkspacedata"
+ file = workspace_files.find { |workspace_file| workspace_file.name == workspace_data_path }
+ return Set.new unless file&.content
+
+ project_refs = T.must(file.content).scan(/location\s*=\s*"(?:group:)?([^"\n]+\.xcodeproj)"/).flatten
+ workspace_root = File.dirname(workspace_scope)
+
+ Set.new(
+ project_refs.map do |project_ref|
+ if workspace_root == "."
+ project_ref
+ else
+ File.join(workspace_root, project_ref)
+ end
+ end
+ )
end
end
end
diff --git a/swift/lib/dependabot/swift/update_checker.rb b/swift/lib/dependabot/swift/update_checker.rb
index 3da88375049..3f3cf666dc9 100644
--- a/swift/lib/dependabot/swift/update_checker.rb
+++ b/swift/lib/dependabot/swift/update_checker.rb
@@ -9,6 +9,7 @@
require "dependabot/git_commit_checker"
require "dependabot/swift/native_requirement"
require "dependabot/swift/file_updater/manifest_updater"
+require "dependabot/swift/xcode_file_helpers"
module Dependabot
module Swift
@@ -294,8 +295,7 @@ def xcode_version_resolver
def xcode_resolved_files
@xcode_resolved_files ||= T.let(
dependency_files.select do |f|
- f.name.end_with?("Package.resolved") &&
- f.name.include?(".xcodeproj/") &&
+ XcodeFileHelpers.xcode_resolved_path?(f.name) &&
!f.support_file?
end,
T.nilable(T::Array[Dependabot::DependencyFile])
diff --git a/swift/lib/dependabot/swift/xcode_file_helpers.rb b/swift/lib/dependabot/swift/xcode_file_helpers.rb
new file mode 100644
index 00000000000..19df736102e
--- /dev/null
+++ b/swift/lib/dependabot/swift/xcode_file_helpers.rb
@@ -0,0 +1,47 @@
+# typed: strong
+# frozen_string_literal: true
+
+require "sorbet-runtime"
+
+module Dependabot
+ module Swift
+ module XcodeFileHelpers
+ extend T::Sig
+
+ XCODEPROJ_SUFFIX = ".xcodeproj/"
+ XCWORKSPACE_SUFFIX = ".xcworkspace/"
+ PACKAGE_RESOLVED = "Package.resolved"
+
+ sig { params(path: String).returns(T::Boolean) }
+ def self.xcode_resolved_path?(path)
+ return false unless path.end_with?(PACKAGE_RESOLVED)
+
+ path.include?(XCODEPROJ_SUFFIX) || path.include?(XCWORKSPACE_SUFFIX)
+ end
+
+ sig { params(path: String).returns(T.nilable(String)) }
+ def self.extract_xcode_scope_dir(path)
+ # Find the first occurrence of .xcodeproj/ or .xcworkspace/
+ xcodeproj_idx = path.index(XCODEPROJ_SUFFIX)
+ xcworkspace_idx = path.index(XCWORKSPACE_SUFFIX)
+
+ # Determine which match to use (earliest occurrence)
+ match_idx = T.let(nil, T.nilable(Integer))
+ suffix_len = T.let(0, Integer)
+
+ if xcodeproj_idx && (xcworkspace_idx.nil? || xcodeproj_idx < xcworkspace_idx)
+ match_idx = xcodeproj_idx
+ suffix_len = XCODEPROJ_SUFFIX.length
+ elsif xcworkspace_idx
+ match_idx = xcworkspace_idx
+ suffix_len = XCWORKSPACE_SUFFIX.length
+ end
+
+ return nil if match_idx.nil?
+
+ # Return path up to and including the suffix (minus trailing /)
+ path[0, match_idx + suffix_len - 1]
+ end
+ end
+ end
+end
diff --git a/swift/spec/dependabot/swift/file_fetcher_spec.rb b/swift/spec/dependabot/swift/file_fetcher_spec.rb
index 51aa607167a..5ccc0616fd3 100644
--- a/swift/spec/dependabot/swift/file_fetcher_spec.rb
+++ b/swift/spec/dependabot/swift/file_fetcher_spec.rb
@@ -97,6 +97,40 @@
end
end
+ context "with a .xcworkspace and sibling .xcodeproj" do
+ let(:project_name) { "xcode_workspace" }
+ let(:directory) { "/" }
+
+ it "fetches workspace lockfile/support file and project support files" do
+ files = file_fetcher_instance.files
+ names = files.map(&:name)
+
+ expect(names).to include(
+ "MyApp.xcworkspace/contents.xcworkspacedata",
+ "MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved",
+ "AppA.xcodeproj/project.pbxproj",
+ "AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
+ )
+ end
+ end
+
+ context "with nested .xcworkspace under subdirectory" do
+ let(:project_name) { "xcode_workspace_nested" }
+ let(:directory) { "/" }
+
+ it "discovers workspace and project files recursively" do
+ files = file_fetcher_instance.files
+ names = files.map(&:name)
+
+ expect(names).to include(
+ "ios/MyApp.xcworkspace/contents.xcworkspacedata",
+ "ios/MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved",
+ "ios/AppA.xcodeproj/project.pbxproj",
+ "ios/AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
+ )
+ end
+ end
+
context "with both Package.swift and .xcodeproj present" do
let(:project_name) { "xcode_project_with_manifest" }
let(:directory) { "/" }
diff --git a/swift/spec/dependabot/swift/file_parser/xcode_spm_resolver_spec.rb b/swift/spec/dependabot/swift/file_parser/xcode_spm_resolver_spec.rb
index 262e83744f6..0a7e5afbef3 100644
--- a/swift/spec/dependabot/swift/file_parser/xcode_spm_resolver_spec.rb
+++ b/swift/spec/dependabot/swift/file_parser/xcode_spm_resolver_spec.rb
@@ -137,6 +137,78 @@
end
end
+ context "with workspace-scoped Package.resolved and sibling project" do
+ let(:project_name) { "xcode_workspace" }
+ let(:xcode_resolved_files) do
+ [
+ Dependabot::DependencyFile.new(
+ name: "MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved",
+ content: fixture(
+ "projects",
+ project_name,
+ "MyApp.xcworkspace",
+ "xcshareddata",
+ "swiftpm",
+ "Package.resolved"
+ )
+ )
+ ]
+ end
+ let(:pbxproj_files) do
+ [
+ Dependabot::DependencyFile.new(
+ name: "AppA.xcodeproj/project.pbxproj",
+ content: fixture("projects", project_name, "AppA.xcodeproj", "project.pbxproj"),
+ support_file: true
+ )
+ ]
+ end
+
+ it "parses and enriches dependencies using available pbxproj requirements" do
+ dep = resolver.parse.first
+
+ expect(dep.name).to eq("github.com/apple/swift-nio")
+ expect(dep.requirements.first[:requirement]).to eq(">= 2.54.0, < 3.0.0")
+ expect(dep.requirements.first[:file]).to eq("AppA.xcodeproj/project.pbxproj")
+ end
+ end
+
+ context "with workspace-scoped Package.resolved and unrelated project scope" do
+ let(:project_name) { "xcode_workspace" }
+ let(:xcode_resolved_files) do
+ [
+ Dependabot::DependencyFile.new(
+ name: "MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved",
+ content: fixture(
+ "projects",
+ project_name,
+ "MyApp.xcworkspace",
+ "xcshareddata",
+ "swiftpm",
+ "Package.resolved"
+ )
+ )
+ ]
+ end
+ let(:pbxproj_files) do
+ [
+ Dependabot::DependencyFile.new(
+ name: "other/OtherApp.xcodeproj/project.pbxproj",
+ content: fixture("projects", "xcode_project_multiple", "AppA.xcodeproj", "project.pbxproj"),
+ support_file: true
+ )
+ ]
+ end
+
+ it "does not enrich requirements from unrelated scopes" do
+ dep = resolver.parse.first
+ req = dep.requirements.first
+
+ expect(req[:requirement]).to eq("= 2.54.0")
+ expect(req[:file]).to eq("MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved")
+ end
+ end
+
context "with multiple dependencies and requirement types" do
let(:project_name) { "xcode_project_multi_req" }
let(:xcode_resolved_files) do
diff --git a/swift/spec/dependabot/swift/file_updater/xcode_lockfile_updater_spec.rb b/swift/spec/dependabot/swift/file_updater/xcode_lockfile_updater_spec.rb
index 32d69153f62..19465699e85 100644
--- a/swift/spec/dependabot/swift/file_updater/xcode_lockfile_updater_spec.rb
+++ b/swift/spec/dependabot/swift/file_updater/xcode_lockfile_updater_spec.rb
@@ -11,10 +11,13 @@
subject(:updater) do
described_class.new(
resolved_file: resolved_file,
- dependencies: dependencies
+ dependencies: dependencies,
+ workspace_files: workspace_files
)
end
+ let(:workspace_files) { [] }
+
describe "#updated_lockfile_content" do
subject(:updated_content) { updater.updated_lockfile_content }
@@ -321,5 +324,95 @@
it { is_expected.to be(false) }
end
+
+ context "when resolved file is workspace-scoped and dependency comes from sibling project" do
+ let(:resolved_file) do
+ Dependabot::DependencyFile.new(
+ name: "MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved",
+ content: fixture(
+ "projects",
+ "xcode_workspace",
+ "MyApp.xcworkspace",
+ "xcshareddata",
+ "swiftpm",
+ "Package.resolved"
+ )
+ )
+ end
+
+ let(:workspace_files) do
+ [
+ Dependabot::DependencyFile.new(
+ name: "MyApp.xcworkspace/contents.xcworkspacedata",
+ content: fixture("projects", "xcode_workspace", "MyApp.xcworkspace", "contents.xcworkspacedata"),
+ support_file: true
+ )
+ ]
+ end
+
+ let(:dependencies) do
+ [
+ Dependabot::Dependency.new(
+ name: "github.com/apple/swift-nio",
+ version: "2.55.0",
+ requirements: [{
+ requirement: ">= 2.55.0, < 3.0.0",
+ groups: ["dependencies"],
+ file: "AppA.xcodeproj/project.pbxproj",
+ source: { type: "git", url: "https://github.com/apple/swift-nio.git", ref: "2.55.0" }
+ }],
+ package_manager: "swift",
+ metadata: { identity: "swift-nio" }
+ )
+ ]
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context "when workspace data does not reference the dependency project" do
+ let(:resolved_file) do
+ Dependabot::DependencyFile.new(
+ name: "MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved",
+ content: fixture(
+ "projects",
+ "xcode_workspace",
+ "MyApp.xcworkspace",
+ "xcshareddata",
+ "swiftpm",
+ "Package.resolved"
+ )
+ )
+ end
+
+ let(:workspace_files) do
+ [
+ Dependabot::DependencyFile.new(
+ name: "MyApp.xcworkspace/contents.xcworkspacedata",
+ content: fixture("projects", "xcode_workspace", "MyApp.xcworkspace", "contents.xcworkspacedata"),
+ support_file: true
+ )
+ ]
+ end
+
+ let(:dependencies) do
+ [
+ Dependabot::Dependency.new(
+ name: "github.com/apple/swift-nio",
+ version: "2.55.0",
+ requirements: [{
+ requirement: ">= 2.55.0, < 3.0.0",
+ groups: ["dependencies"],
+ file: "OtherApp.xcodeproj/project.pbxproj",
+ source: { type: "git", url: "https://github.com/apple/swift-nio.git", ref: "2.55.0" }
+ }],
+ package_manager: "swift",
+ metadata: { identity: "swift-nio" }
+ )
+ ]
+ end
+
+ it { is_expected.to be(false) }
+ end
end
end
diff --git a/swift/spec/dependabot/swift/file_updater_spec.rb b/swift/spec/dependabot/swift/file_updater_spec.rb
index d783b396986..dd7a511946e 100644
--- a/swift/spec/dependabot/swift/file_updater_spec.rb
+++ b/swift/spec/dependabot/swift/file_updater_spec.rb
@@ -256,6 +256,83 @@
end
end
+ context "with workspace-scoped Package.resolved" do
+ let(:project_name) { "xcode_workspace" }
+ let(:files) do
+ [
+ Dependabot::DependencyFile.new(
+ name: "MyApp.xcworkspace/contents.xcworkspacedata",
+ content: fixture("projects", project_name, "MyApp.xcworkspace", "contents.xcworkspacedata"),
+ support_file: true
+ ),
+ Dependabot::DependencyFile.new(
+ name: "AppA.xcodeproj/project.pbxproj",
+ content: fixture("projects", project_name, "AppA.xcodeproj", "project.pbxproj"),
+ support_file: true
+ ),
+ Dependabot::DependencyFile.new(
+ name: "MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved",
+ content: fixture(
+ "projects",
+ project_name,
+ "MyApp.xcworkspace",
+ "xcshareddata",
+ "swiftpm",
+ "Package.resolved"
+ )
+ )
+ ]
+ end
+
+ let(:dependencies) do
+ [
+ Dependabot::Dependency.new(
+ name: "github.com/apple/swift-nio",
+ version: "2.55.0",
+ previous_version: "2.54.0",
+ requirements: [{
+ requirement: ">= 2.55.0, < 3.0.0",
+ groups: ["dependencies"],
+ file: "AppA.xcodeproj/project.pbxproj",
+ source: {
+ type: "git",
+ url: "https://github.com/apple/swift-nio.git",
+ ref: "abc123newrevision",
+ branch: nil
+ },
+ metadata: {
+ requirement_string: "from: \"2.55.0\""
+ }
+ }],
+ previous_requirements: [{
+ requirement: ">= 2.54.0, < 3.0.0",
+ groups: ["dependencies"],
+ file: "AppA.xcodeproj/project.pbxproj",
+ source: {
+ type: "git",
+ url: "https://github.com/apple/swift-nio.git",
+ ref: "1234567890abcdef1234567890abcdef12345678",
+ branch: nil
+ },
+ metadata: {
+ requirement_string: "from: \"2.54.0\""
+ }
+ }],
+ package_manager: "swift",
+ metadata: { identity: "swift-nio" }
+ )
+ ]
+ end
+
+ it "updates workspace Package.resolved" do
+ expect(updated_dependency_files.length).to eq(1)
+ resolved = updated_dependency_files.first
+
+ expect(resolved.name).to eq("MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved")
+ expect(resolved.content).to include('"version" : "2.55.0"')
+ end
+ end
+
context "with multiple Xcode projects" do
let(:project_name) { "xcode_project_multiple" }
let(:files) do
diff --git a/swift/spec/fixtures/projects/xcode_workspace/AppA.xcodeproj/project.pbxproj b/swift/spec/fixtures/projects/xcode_workspace/AppA.xcodeproj/project.pbxproj
new file mode 100644
index 00000000000..4f1a4b117b8
--- /dev/null
+++ b/swift/spec/fixtures/projects/xcode_workspace/AppA.xcodeproj/project.pbxproj
@@ -0,0 +1,22 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin XCRemoteSwiftPackageReference section */
+ A1B2C3D4E5F60001 /* XCRemoteSwiftPackageReference "swift-nio" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/apple/swift-nio.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 2.54.0;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
+ };
+ rootObject = A1B2C3D4E5F60000 /* Project object */;
+}
diff --git a/swift/spec/fixtures/projects/xcode_workspace/AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_workspace/AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000000..e5d008a69c2
--- /dev/null
+++ b/swift/spec/fixtures/projects/xcode_workspace/AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-nio",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio.git",
+ "state" : {
+ "revision" : "1234567890abcdef1234567890abcdef12345678",
+ "version" : "2.54.0"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/swift/spec/fixtures/projects/xcode_workspace/MyApp.xcworkspace/contents.xcworkspacedata b/swift/spec/fixtures/projects/xcode_workspace/MyApp.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000000..58abfcf3e93
--- /dev/null
+++ b/swift/spec/fixtures/projects/xcode_workspace/MyApp.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/swift/spec/fixtures/projects/xcode_workspace/MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_workspace/MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000000..e5d008a69c2
--- /dev/null
+++ b/swift/spec/fixtures/projects/xcode_workspace/MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-nio",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio.git",
+ "state" : {
+ "revision" : "1234567890abcdef1234567890abcdef12345678",
+ "version" : "2.54.0"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/swift/spec/fixtures/projects/xcode_workspace_nested/ios/AppA.xcodeproj/project.pbxproj b/swift/spec/fixtures/projects/xcode_workspace_nested/ios/AppA.xcodeproj/project.pbxproj
new file mode 100644
index 00000000000..4f1a4b117b8
--- /dev/null
+++ b/swift/spec/fixtures/projects/xcode_workspace_nested/ios/AppA.xcodeproj/project.pbxproj
@@ -0,0 +1,22 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin XCRemoteSwiftPackageReference section */
+ A1B2C3D4E5F60001 /* XCRemoteSwiftPackageReference "swift-nio" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/apple/swift-nio.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 2.54.0;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
+ };
+ rootObject = A1B2C3D4E5F60000 /* Project object */;
+}
diff --git a/swift/spec/fixtures/projects/xcode_workspace_nested/ios/AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_workspace_nested/ios/AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000000..e5d008a69c2
--- /dev/null
+++ b/swift/spec/fixtures/projects/xcode_workspace_nested/ios/AppA.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-nio",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio.git",
+ "state" : {
+ "revision" : "1234567890abcdef1234567890abcdef12345678",
+ "version" : "2.54.0"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/swift/spec/fixtures/projects/xcode_workspace_nested/ios/MyApp.xcworkspace/contents.xcworkspacedata b/swift/spec/fixtures/projects/xcode_workspace_nested/ios/MyApp.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000000..58abfcf3e93
--- /dev/null
+++ b/swift/spec/fixtures/projects/xcode_workspace_nested/ios/MyApp.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/swift/spec/fixtures/projects/xcode_workspace_nested/ios/MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved b/swift/spec/fixtures/projects/xcode_workspace_nested/ios/MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000000..e5d008a69c2
--- /dev/null
+++ b/swift/spec/fixtures/projects/xcode_workspace_nested/ios/MyApp.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-nio",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio.git",
+ "state" : {
+ "revision" : "1234567890abcdef1234567890abcdef12345678",
+ "version" : "2.54.0"
+ }
+ }
+ ],
+ "version" : 2
+}