dependabot-swift 0.365.0 → 0.366.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dependabot/swift/file_fetcher.rb +54 -5
- data/lib/dependabot/swift/file_parser/xcode_spm_resolver.rb +70 -16
- data/lib/dependabot/swift/file_parser.rb +3 -3
- data/lib/dependabot/swift/file_updater/xcode_lockfile_updater.rb +303 -0
- data/lib/dependabot/swift/file_updater.rb +76 -2
- data/lib/dependabot/swift/metadata_finder.rb +14 -0
- data/lib/dependabot/swift/update_checker/requirements_updater.rb +117 -3
- data/lib/dependabot/swift/update_checker/xcode_version_resolver.rb +124 -0
- data/lib/dependabot/swift/update_checker.rb +84 -1
- data/lib/dependabot/swift/xcode_file_helpers.rb +47 -0
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfecded3f835cd8011a78e5cd10c42eaf1f4a518594428a6467c64300fbe1e2a
|
|
4
|
+
data.tar.gz: a742aa0fab89643edc392546758b904c0e6b8b7a750d7886c470caed4dd46f2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ad87b7b2c883cb85b11d4c4f21d1ee99730b3ceff9b2d38ca6dc62e2640e854ab5036d38545a239bf387df39d9d6f81f4a77756195556bd4f0c74050bd50736
|
|
7
|
+
data.tar.gz: 9bcebc8617174bbeb25bbb92c9ab6710fcada5a749e118898937d535543d6b65dea35f210fe646682dd29bc343377a3040ad9420e2636d98fb481aea807967e3
|
|
@@ -5,20 +5,24 @@ require "sorbet-runtime"
|
|
|
5
5
|
require "dependabot/experiments"
|
|
6
6
|
require "dependabot/file_fetchers"
|
|
7
7
|
require "dependabot/file_fetchers/base"
|
|
8
|
+
require "dependabot/swift/xcode_file_helpers"
|
|
8
9
|
|
|
9
10
|
module Dependabot
|
|
10
11
|
module Swift
|
|
11
12
|
class FileFetcher < Dependabot::FileFetchers::Base
|
|
12
13
|
extend T::Sig
|
|
13
14
|
|
|
15
|
+
XCODEPROJ_SUFFIX = ".xcodeproj"
|
|
16
|
+
XCWORKSPACE_SUFFIX = ".xcworkspace"
|
|
14
17
|
XCODE_SPM_PACKAGE_RESOLVED_PATH = "project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
|
|
18
|
+
XCWORKSPACE_PACKAGE_RESOLVED_PATH = "xcshareddata/swiftpm/Package.resolved"
|
|
15
19
|
|
|
16
20
|
sig { override.params(filenames: T::Array[String]).returns(T::Boolean) }
|
|
17
21
|
def self.required_files_in?(filenames)
|
|
18
22
|
return true if filenames.include?("Package.swift")
|
|
19
23
|
|
|
20
24
|
if Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
|
|
21
|
-
return filenames.any? { |f|
|
|
25
|
+
return filenames.any? { |f| XcodeFileHelpers.xcode_resolved_path?(f) }
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
false
|
|
@@ -28,7 +32,7 @@ module Dependabot
|
|
|
28
32
|
def self.required_files_message
|
|
29
33
|
if Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
|
|
30
34
|
"Repo must contain a Package.swift configuration file or " \
|
|
31
|
-
"an .xcodeproj directory with a Package.resolved file."
|
|
35
|
+
"an .xcodeproj/.xcworkspace directory with a Package.resolved file."
|
|
32
36
|
else
|
|
33
37
|
"Repo must contain a Package.swift configuration file."
|
|
34
38
|
end
|
|
@@ -74,17 +78,62 @@ module Dependabot
|
|
|
74
78
|
resolved = fetch_file_if_present(File.join(xcodeproj_path, XCODE_SPM_PACKAGE_RESOLVED_PATH))
|
|
75
79
|
fetched_files << resolved if resolved
|
|
76
80
|
end
|
|
81
|
+
|
|
82
|
+
xcworkspace_dirs.each do |workspace_path|
|
|
83
|
+
workspace_data = fetch_support_file(File.join(workspace_path, "contents.xcworkspacedata"))
|
|
84
|
+
fetched_files << workspace_data if workspace_data
|
|
85
|
+
|
|
86
|
+
resolved = fetch_file_if_present(File.join(workspace_path, XCWORKSPACE_PACKAGE_RESOLVED_PATH))
|
|
87
|
+
fetched_files << resolved if resolved
|
|
88
|
+
end
|
|
77
89
|
end
|
|
78
90
|
|
|
79
91
|
sig { returns(T::Array[String]) }
|
|
80
92
|
def xcodeproj_dirs
|
|
81
93
|
@xcodeproj_dirs ||= T.let(
|
|
82
|
-
|
|
83
|
-
.select { |entry| entry.type == "dir" && entry.name.end_with?(".xcodeproj") }
|
|
84
|
-
.map(&:name),
|
|
94
|
+
discover_dirs_with_suffix(XCODEPROJ_SUFFIX),
|
|
85
95
|
T.nilable(T::Array[String])
|
|
86
96
|
)
|
|
87
97
|
end
|
|
98
|
+
|
|
99
|
+
sig { returns(T::Array[String]) }
|
|
100
|
+
def xcworkspace_dirs
|
|
101
|
+
@xcworkspace_dirs ||= T.let(
|
|
102
|
+
discover_dirs_with_suffix(XCWORKSPACE_SUFFIX)
|
|
103
|
+
.reject { |path| path.include?("#{XCODEPROJ_SUFFIX}/") },
|
|
104
|
+
T.nilable(T::Array[String])
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { params(suffix: String).returns(T::Array[String]) }
|
|
109
|
+
def discover_dirs_with_suffix(suffix)
|
|
110
|
+
discovered = T.let([], T::Array[String])
|
|
111
|
+
queue = T.let(["."], T::Array[String])
|
|
112
|
+
visited = T.let({}, T::Hash[String, T::Boolean])
|
|
113
|
+
index = T.let(0, Integer)
|
|
114
|
+
|
|
115
|
+
while index < queue.length
|
|
116
|
+
dir = T.must(queue[index])
|
|
117
|
+
index += 1
|
|
118
|
+
next if visited[dir]
|
|
119
|
+
|
|
120
|
+
visited[dir] = true
|
|
121
|
+
|
|
122
|
+
entries = repo_contents(dir: dir, raise_errors: false)
|
|
123
|
+
entries.each do |entry|
|
|
124
|
+
next unless entry.type == "dir"
|
|
125
|
+
|
|
126
|
+
next_dir = dir == "." ? entry.name : File.join(dir, entry.name)
|
|
127
|
+
if entry.name.end_with?(suffix)
|
|
128
|
+
discovered << next_dir
|
|
129
|
+
elsif !entry.name.start_with?(".")
|
|
130
|
+
queue << next_dir
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
discovered.sort
|
|
136
|
+
end
|
|
88
137
|
end
|
|
89
138
|
end
|
|
90
139
|
end
|
|
@@ -7,14 +7,16 @@ require "dependabot/file_parsers/base/dependency_set"
|
|
|
7
7
|
require "dependabot/swift/file_parser"
|
|
8
8
|
require "dependabot/swift/file_parser/package_resolved_parser"
|
|
9
9
|
require "dependabot/swift/file_parser/pbxproj_parser"
|
|
10
|
+
require "dependabot/swift/xcode_file_helpers"
|
|
10
11
|
|
|
11
12
|
module Dependabot
|
|
12
13
|
module Swift
|
|
13
14
|
class FileParser < Dependabot::FileParsers::Base
|
|
14
15
|
# Orchestrates Xcode-managed SwiftPM dependency parsing.
|
|
15
16
|
#
|
|
16
|
-
# Parses Package.resolved JSON files found inside
|
|
17
|
-
#
|
|
17
|
+
# Parses Package.resolved JSON files found inside Xcode project and
|
|
18
|
+
# workspace directories (e.g., .xcodeproj and .xcworkspace), then
|
|
19
|
+
# enriches each dependency with requirement info extracted from the
|
|
18
20
|
# corresponding project.pbxproj files.
|
|
19
21
|
class XcodeSpmResolver
|
|
20
22
|
extend T::Sig
|
|
@@ -38,8 +40,10 @@ module Dependabot
|
|
|
38
40
|
|
|
39
41
|
xcode_resolved_files.each do |resolved_file|
|
|
40
42
|
resolved_deps = PackageResolvedParser.new(resolved_file).parse
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
pbxproj_requirements = requirements_for_resolved_file(
|
|
44
|
+
scoped_requirements: scoped_requirements,
|
|
45
|
+
resolved_file_name: resolved_file.name
|
|
46
|
+
)
|
|
43
47
|
|
|
44
48
|
resolved_deps.each do |dep|
|
|
45
49
|
enriched = enrich_with_pbxproj_requirements(dep, pbxproj_requirements)
|
|
@@ -59,24 +63,67 @@ module Dependabot
|
|
|
59
63
|
attr_reader :pbxproj_files
|
|
60
64
|
|
|
61
65
|
# Collects requirement info from all project.pbxproj support files,
|
|
62
|
-
# keyed by
|
|
63
|
-
# requirements from its
|
|
66
|
+
# keyed by Xcode scope directory so each resolved file can be enriched
|
|
67
|
+
# by requirements from its closest matching Xcode scope.
|
|
64
68
|
sig { returns(T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]) }
|
|
65
69
|
def aggregate_pbxproj_requirements
|
|
66
70
|
scoped = T.let({}, T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]])
|
|
67
71
|
|
|
68
72
|
pbxproj_files.each do |pbxproj_file|
|
|
69
|
-
|
|
70
|
-
scoped[
|
|
73
|
+
xcode_scope_dir = extract_xcode_scope_dir(pbxproj_file.name)
|
|
74
|
+
scoped[xcode_scope_dir] ||= {}
|
|
71
75
|
|
|
72
76
|
PbxprojParser.new(pbxproj_file).parse.each do |name, req_info|
|
|
73
|
-
T.must(scoped[
|
|
77
|
+
T.must(scoped[xcode_scope_dir])[name] = req_info
|
|
74
78
|
end
|
|
75
79
|
end
|
|
76
80
|
|
|
77
81
|
scoped
|
|
78
82
|
end
|
|
79
83
|
|
|
84
|
+
sig do
|
|
85
|
+
params(
|
|
86
|
+
scoped_requirements: T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]]
|
|
87
|
+
).returns(T::Hash[String, T::Hash[Symbol, T.untyped]])
|
|
88
|
+
end
|
|
89
|
+
def merge_scopes(scoped_requirements)
|
|
90
|
+
scoped_requirements.values.each_with_object({}) do |requirements, merged|
|
|
91
|
+
requirements.each { |name, req_info| merged[name] = req_info }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig do
|
|
96
|
+
params(
|
|
97
|
+
scoped_requirements: T::Hash[T.nilable(String), T::Hash[String, T::Hash[Symbol, T.untyped]]],
|
|
98
|
+
resolved_file_name: String
|
|
99
|
+
).returns(T::Hash[String, T::Hash[Symbol, T.untyped]])
|
|
100
|
+
end
|
|
101
|
+
def requirements_for_resolved_file(scoped_requirements:, resolved_file_name:)
|
|
102
|
+
scope_dir = extract_xcode_scope_dir(resolved_file_name)
|
|
103
|
+
return T.must(scoped_requirements[scope_dir]) if scoped_requirements.key?(scope_dir)
|
|
104
|
+
|
|
105
|
+
workspace_root = workspace_root_for_scope(scope_dir)
|
|
106
|
+
return {} unless workspace_root
|
|
107
|
+
|
|
108
|
+
local_scopes = scoped_requirements.select do |candidate_scope, _|
|
|
109
|
+
scope_in_workspace_root?(candidate_scope, workspace_root)
|
|
110
|
+
end
|
|
111
|
+
return {} if local_scopes.empty?
|
|
112
|
+
|
|
113
|
+
merge_scopes(local_scopes)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
sig { params(candidate_scope: T.nilable(String), workspace_root: String).returns(T::Boolean) }
|
|
117
|
+
def scope_in_workspace_root?(candidate_scope, workspace_root)
|
|
118
|
+
return false unless candidate_scope
|
|
119
|
+
|
|
120
|
+
if workspace_root == "."
|
|
121
|
+
!candidate_scope.include?("/")
|
|
122
|
+
else
|
|
123
|
+
candidate_scope.start_with?("#{workspace_root}/")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
80
127
|
# Enriches a dependency parsed from Package.resolved with requirement
|
|
81
128
|
# info from the matching project.pbxproj
|
|
82
129
|
sig do
|
|
@@ -92,6 +139,7 @@ module Dependabot
|
|
|
92
139
|
pbxproj_file = req_info[:file]
|
|
93
140
|
requirement_str = req_info[:requirement]
|
|
94
141
|
requirement_string = req_info[:requirement_string]
|
|
142
|
+
kind = req_info[:kind]
|
|
95
143
|
|
|
96
144
|
new_requirements = dep.requirements.map do |req|
|
|
97
145
|
req.merge(
|
|
@@ -101,7 +149,8 @@ module Dependabot
|
|
|
101
149
|
# declaration_string is not applicable for Xcode-managed SPM
|
|
102
150
|
# (no Package.swift manifest to extract it from)
|
|
103
151
|
declaration_string: nil,
|
|
104
|
-
requirement_string: requirement_string
|
|
152
|
+
requirement_string: requirement_string,
|
|
153
|
+
kind: kind
|
|
105
154
|
}.compact
|
|
106
155
|
)
|
|
107
156
|
end
|
|
@@ -115,13 +164,18 @@ module Dependabot
|
|
|
115
164
|
)
|
|
116
165
|
end
|
|
117
166
|
|
|
118
|
-
# Extracts the
|
|
119
|
-
#
|
|
120
|
-
# e.g. "sub/dir/App.xcodeproj/project.pbxproj" -> "sub/dir/App.xcodeproj"
|
|
167
|
+
# Extracts the Xcode scope directory (.xcodeproj or .xcworkspace)
|
|
168
|
+
# from a file path.
|
|
121
169
|
sig { params(path: String).returns(T.nilable(String)) }
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
170
|
+
def extract_xcode_scope_dir(path)
|
|
171
|
+
XcodeFileHelpers.extract_xcode_scope_dir(path)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
sig { params(scope_dir: T.nilable(String)).returns(T.nilable(String)) }
|
|
175
|
+
def workspace_root_for_scope(scope_dir)
|
|
176
|
+
return nil unless scope_dir&.end_with?(".xcworkspace")
|
|
177
|
+
|
|
178
|
+
File.dirname(scope_dir)
|
|
125
179
|
end
|
|
126
180
|
end
|
|
127
181
|
end
|
|
@@ -10,6 +10,7 @@ require "dependabot/swift/file_parser/manifest_parser"
|
|
|
10
10
|
require "dependabot/swift/file_parser/xcode_spm_resolver"
|
|
11
11
|
require "dependabot/swift/package_manager"
|
|
12
12
|
require "dependabot/swift/language"
|
|
13
|
+
require "dependabot/swift/xcode_file_helpers"
|
|
13
14
|
|
|
14
15
|
module Dependabot
|
|
15
16
|
module Swift
|
|
@@ -115,13 +116,12 @@ module Dependabot
|
|
|
115
116
|
@package_manifest_file ||= T.let(get_original_file("Package.swift"), T.nilable(Dependabot::DependencyFile))
|
|
116
117
|
end
|
|
117
118
|
|
|
118
|
-
# All non-support Package.resolved files from Xcode project directories
|
|
119
|
+
# All non-support Package.resolved files from Xcode project and workspace directories (.xcodeproj, .xcworkspace)
|
|
119
120
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
120
121
|
def xcode_resolved_files
|
|
121
122
|
@xcode_resolved_files ||= T.let(
|
|
122
123
|
dependency_files.select do |f|
|
|
123
|
-
|
|
124
|
-
f.name.include?(".xcodeproj/") &&
|
|
124
|
+
XcodeFileHelpers.xcode_resolved_path?(f.name) &&
|
|
125
125
|
!f.support_file?
|
|
126
126
|
end,
|
|
127
127
|
T.nilable(T::Array[Dependabot::DependencyFile])
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
require "dependabot/dependency"
|
|
7
|
+
require "dependabot/dependency_file"
|
|
8
|
+
require "dependabot/errors"
|
|
9
|
+
require "dependabot/shared_helpers"
|
|
10
|
+
require "dependabot/swift/file_updater"
|
|
11
|
+
require "dependabot/swift/url_helpers"
|
|
12
|
+
require "dependabot/swift/xcode_file_helpers"
|
|
13
|
+
|
|
14
|
+
module Dependabot
|
|
15
|
+
module Swift
|
|
16
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
17
|
+
class XcodeLockfileUpdater
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
20
|
+
SUPPORTED_VERSIONS = T.let([1, 2, 3].freeze, T::Array[Integer])
|
|
21
|
+
|
|
22
|
+
# Maps schema version to the JSON keys used for each pin field
|
|
23
|
+
PIN_KEYS = T.let(
|
|
24
|
+
{
|
|
25
|
+
1 => { url: "repositoryURL", identity: "package", pins_path: %w(object pins) },
|
|
26
|
+
2 => { url: "location", identity: "identity", pins_path: ["pins"] },
|
|
27
|
+
3 => { url: "location", identity: "identity", pins_path: ["pins"] }
|
|
28
|
+
}.freeze,
|
|
29
|
+
T::Hash[Integer, T::Hash[Symbol, T.untyped]]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
sig do
|
|
33
|
+
params(
|
|
34
|
+
resolved_file: Dependabot::DependencyFile,
|
|
35
|
+
dependencies: T::Array[Dependabot::Dependency],
|
|
36
|
+
workspace_files: T::Array[Dependabot::DependencyFile]
|
|
37
|
+
).void
|
|
38
|
+
end
|
|
39
|
+
def initialize(resolved_file:, dependencies:, workspace_files: [])
|
|
40
|
+
@resolved_file = resolved_file
|
|
41
|
+
@dependencies = dependencies
|
|
42
|
+
@workspace_files = workspace_files
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { returns(String) }
|
|
46
|
+
def updated_lockfile_content
|
|
47
|
+
content = resolved_file.content
|
|
48
|
+
unless content
|
|
49
|
+
raise Dependabot::DependencyFileNotParseable.new(
|
|
50
|
+
resolved_file.name,
|
|
51
|
+
"#{resolved_file.name} has no content"
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
parsed = parse_json(content)
|
|
56
|
+
schema_version = detect_schema_version(parsed)
|
|
57
|
+
keys = T.must(PIN_KEYS[schema_version])
|
|
58
|
+
|
|
59
|
+
update_pins(parsed, schema_version, keys)
|
|
60
|
+
|
|
61
|
+
# Use JSON.pretty_generate to match Xcode's output format:
|
|
62
|
+
# - 2-space indentation
|
|
63
|
+
# - space before colon (e.g., "key" : "value")
|
|
64
|
+
JSON.pretty_generate(
|
|
65
|
+
parsed,
|
|
66
|
+
indent: " ",
|
|
67
|
+
space: " ",
|
|
68
|
+
space_before: " ",
|
|
69
|
+
object_nl: "\n",
|
|
70
|
+
array_nl: "\n"
|
|
71
|
+
) + "\n"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { returns(T::Boolean) }
|
|
75
|
+
def lockfile_changed?
|
|
76
|
+
dependencies_for_file.any?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
sig { returns(Dependabot::DependencyFile) }
|
|
82
|
+
attr_reader :resolved_file
|
|
83
|
+
|
|
84
|
+
sig { returns(T::Array[Dependabot::Dependency]) }
|
|
85
|
+
attr_reader :dependencies
|
|
86
|
+
|
|
87
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
88
|
+
attr_reader :workspace_files
|
|
89
|
+
|
|
90
|
+
sig { params(content: String).returns(T::Hash[String, T.untyped]) }
|
|
91
|
+
def parse_json(content)
|
|
92
|
+
JSON.parse(content)
|
|
93
|
+
rescue JSON::ParserError => e
|
|
94
|
+
raise Dependabot::DependencyFileNotParseable.new(
|
|
95
|
+
resolved_file.name,
|
|
96
|
+
"#{resolved_file.name} is not valid JSON: #{e.message}"
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sig { params(parsed: T::Hash[String, T.untyped]).returns(Integer) }
|
|
101
|
+
def detect_schema_version(parsed)
|
|
102
|
+
version = parsed["version"]
|
|
103
|
+
|
|
104
|
+
unless version.is_a?(Integer) && SUPPORTED_VERSIONS.include?(version)
|
|
105
|
+
raise Dependabot::DependencyFileNotParseable.new(
|
|
106
|
+
resolved_file.name,
|
|
107
|
+
"#{resolved_file.name} has unsupported schema version: #{version.inspect}. " \
|
|
108
|
+
"Supported versions: #{SUPPORTED_VERSIONS.join(', ')}"
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
version
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sig do
|
|
116
|
+
params(
|
|
117
|
+
parsed: T::Hash[String, T.untyped],
|
|
118
|
+
schema_version: Integer,
|
|
119
|
+
keys: T::Hash[Symbol, T.untyped]
|
|
120
|
+
).void
|
|
121
|
+
end
|
|
122
|
+
def update_pins(parsed, schema_version, keys)
|
|
123
|
+
pins_path = T.cast(keys[:pins_path], T::Array[String])
|
|
124
|
+
pins = dig_pins(parsed, pins_path)
|
|
125
|
+
|
|
126
|
+
unless pins.is_a?(Array)
|
|
127
|
+
raise Dependabot::DependencyFileNotParseable.new(
|
|
128
|
+
resolved_file.name,
|
|
129
|
+
"#{resolved_file.name} is missing the expected 'pins' array " \
|
|
130
|
+
"(schema version #{schema_version})"
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
dependencies_for_file.each do |dep|
|
|
135
|
+
update_pin_for_dependency(pins, dep, keys, schema_version)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
sig do
|
|
140
|
+
params(
|
|
141
|
+
parsed: T::Hash[String, T.untyped],
|
|
142
|
+
path: T::Array[String]
|
|
143
|
+
).returns(T.untyped)
|
|
144
|
+
end
|
|
145
|
+
def dig_pins(parsed, path)
|
|
146
|
+
# Navigate nested hash using path keys
|
|
147
|
+
# Path is either ["object", "pins"] for v1 or ["pins"] for v2/v3
|
|
148
|
+
current = T.let(parsed, T.untyped)
|
|
149
|
+
path.each do |key|
|
|
150
|
+
break unless current.is_a?(Hash)
|
|
151
|
+
|
|
152
|
+
current = current[key]
|
|
153
|
+
end
|
|
154
|
+
current
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
sig do
|
|
158
|
+
params(
|
|
159
|
+
pins: T::Array[T::Hash[String, T.untyped]],
|
|
160
|
+
dependency: Dependabot::Dependency,
|
|
161
|
+
keys: T::Hash[Symbol, T.untyped],
|
|
162
|
+
schema_version: Integer
|
|
163
|
+
).void
|
|
164
|
+
end
|
|
165
|
+
def update_pin_for_dependency(pins, dependency, keys, schema_version)
|
|
166
|
+
pin = find_pin_for_dependency(pins, dependency, keys, schema_version)
|
|
167
|
+
return unless pin
|
|
168
|
+
|
|
169
|
+
state = pin["state"]
|
|
170
|
+
return unless state.is_a?(Hash)
|
|
171
|
+
|
|
172
|
+
source = dependency.requirements.first&.dig(:source)
|
|
173
|
+
new_version = dependency.version
|
|
174
|
+
new_ref = source&.dig(:ref)
|
|
175
|
+
|
|
176
|
+
if new_version
|
|
177
|
+
state["version"] = new_version
|
|
178
|
+
# When updating to a new version, update revision if provided in source
|
|
179
|
+
# The ref from source is typically the git SHA corresponding to the version tag
|
|
180
|
+
state["revision"] = new_ref if new_ref && looks_like_sha?(new_ref)
|
|
181
|
+
elsif new_ref
|
|
182
|
+
# Revision-only update (no version, just SHA)
|
|
183
|
+
state["revision"] = new_ref
|
|
184
|
+
state.delete("version")
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
sig { params(str: String).returns(T::Boolean) }
|
|
189
|
+
def looks_like_sha?(str)
|
|
190
|
+
str.match?(/\A[0-9a-f]{40}\z/i)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
sig do
|
|
194
|
+
params(
|
|
195
|
+
pins: T::Array[T::Hash[String, T.untyped]],
|
|
196
|
+
dependency: Dependabot::Dependency,
|
|
197
|
+
keys: T::Hash[Symbol, T.untyped],
|
|
198
|
+
schema_version: Integer
|
|
199
|
+
).returns(T.nilable(T::Hash[String, T.untyped]))
|
|
200
|
+
end
|
|
201
|
+
def find_pin_for_dependency(pins, dependency, keys, schema_version)
|
|
202
|
+
identity_key = T.cast(keys[:identity], String)
|
|
203
|
+
url_key = T.cast(keys[:url], String)
|
|
204
|
+
identity = dependency.metadata[:identity]
|
|
205
|
+
|
|
206
|
+
pins.find do |pin|
|
|
207
|
+
pin_identity = pin[identity_key]
|
|
208
|
+
pin_identity = pin_identity&.downcase if schema_version == 1
|
|
209
|
+
|
|
210
|
+
if identity && pin_identity == identity
|
|
211
|
+
true
|
|
212
|
+
else
|
|
213
|
+
# Fall back to URL matching
|
|
214
|
+
pin_url = pin[url_key]
|
|
215
|
+
next false unless pin_url.is_a?(String)
|
|
216
|
+
|
|
217
|
+
normalized_pin_url = SharedHelpers.scp_to_standard(pin_url)
|
|
218
|
+
pin_name = UrlHelpers.normalize_name(normalized_pin_url)
|
|
219
|
+
|
|
220
|
+
pin_name == dependency.name
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
sig { returns(T::Array[Dependabot::Dependency]) }
|
|
226
|
+
def dependencies_for_file
|
|
227
|
+
@dependencies_for_file ||= T.let(
|
|
228
|
+
dependencies.select do |dep|
|
|
229
|
+
dep.requirements.any? do |req|
|
|
230
|
+
req_file_matches_resolved_scope?(req[:file])
|
|
231
|
+
end
|
|
232
|
+
end,
|
|
233
|
+
T.nilable(T::Array[Dependabot::Dependency])
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
sig { params(req_file: T.nilable(String)).returns(T::Boolean) }
|
|
238
|
+
def req_file_matches_resolved_scope?(req_file)
|
|
239
|
+
return false unless req_file
|
|
240
|
+
return true if req_file == resolved_file.name
|
|
241
|
+
return false unless req_file.include?(".xcodeproj/") || req_file.include?(".xcworkspace/")
|
|
242
|
+
|
|
243
|
+
req_scope = extract_xcode_scope_dir(req_file)
|
|
244
|
+
resolved_scope = extract_xcode_scope_dir(resolved_file.name)
|
|
245
|
+
|
|
246
|
+
return true if req_scope && resolved_scope && req_scope == resolved_scope
|
|
247
|
+
|
|
248
|
+
workspace_related_dependency?(req_file)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Extracts the Xcode scope directory (.xcodeproj or .xcworkspace)
|
|
252
|
+
# from a file path.
|
|
253
|
+
sig { params(path: String).returns(T.nilable(String)) }
|
|
254
|
+
def extract_xcode_scope_dir(path)
|
|
255
|
+
XcodeFileHelpers.extract_xcode_scope_dir(path)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
sig { params(req_file: T.nilable(String)).returns(T::Boolean) }
|
|
259
|
+
def workspace_related_dependency?(req_file)
|
|
260
|
+
return false unless req_file
|
|
261
|
+
|
|
262
|
+
workspace_scope = extract_xcode_scope_dir(resolved_file.name)
|
|
263
|
+
return false unless workspace_scope&.end_with?(".xcworkspace")
|
|
264
|
+
return false unless req_file.include?(".xcodeproj/")
|
|
265
|
+
|
|
266
|
+
req_scope = extract_xcode_scope_dir(req_file)
|
|
267
|
+
return false unless req_scope
|
|
268
|
+
|
|
269
|
+
referenced = referenced_project_scopes_for_workspace(workspace_scope)
|
|
270
|
+
return referenced.include?(req_scope) if referenced.any?
|
|
271
|
+
|
|
272
|
+
workspace_root = File.dirname(workspace_scope)
|
|
273
|
+
|
|
274
|
+
if workspace_root == "."
|
|
275
|
+
!req_scope.include?("/")
|
|
276
|
+
else
|
|
277
|
+
req_scope.start_with?("#{workspace_root}/")
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
sig { params(workspace_scope: String).returns(T::Set[String]) }
|
|
282
|
+
def referenced_project_scopes_for_workspace(workspace_scope)
|
|
283
|
+
workspace_data_path = "#{workspace_scope}/contents.xcworkspacedata"
|
|
284
|
+
file = workspace_files.find { |workspace_file| workspace_file.name == workspace_data_path }
|
|
285
|
+
return Set.new unless file&.content
|
|
286
|
+
|
|
287
|
+
project_refs = T.must(file.content).scan(/location\s*=\s*"(?:group:)?([^"\n]+\.xcodeproj)"/).flatten
|
|
288
|
+
workspace_root = File.dirname(workspace_scope)
|
|
289
|
+
|
|
290
|
+
Set.new(
|
|
291
|
+
project_refs.map do |project_ref|
|
|
292
|
+
if workspace_root == "."
|
|
293
|
+
project_ref
|
|
294
|
+
else
|
|
295
|
+
File.join(workspace_root, project_ref)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "dependabot/experiments"
|
|
4
5
|
require "dependabot/file_updaters"
|
|
5
6
|
require "dependabot/file_updaters/base"
|
|
6
7
|
require "dependabot/swift/file_updater/lockfile_updater"
|
|
7
8
|
require "dependabot/swift/file_updater/manifest_updater"
|
|
9
|
+
require "dependabot/swift/file_updater/xcode_lockfile_updater"
|
|
10
|
+
require "dependabot/swift/xcode_file_helpers"
|
|
8
11
|
|
|
9
12
|
module Dependabot
|
|
10
13
|
module Swift
|
|
@@ -13,6 +16,18 @@ module Dependabot
|
|
|
13
16
|
|
|
14
17
|
sig { override.returns(T::Array[Dependabot::DependencyFile]) }
|
|
15
18
|
def updated_dependency_files
|
|
19
|
+
if xcode_spm_mode?
|
|
20
|
+
updated_xcode_spm_files
|
|
21
|
+
else
|
|
22
|
+
updated_classic_spm_files
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Classic SPM update: uses swift CLI to resolve and update
|
|
29
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
30
|
+
def updated_classic_spm_files
|
|
16
31
|
updated_files = T.let([], T::Array[Dependabot::DependencyFile])
|
|
17
32
|
|
|
18
33
|
SharedHelpers.in_a_temporary_repo_directory(T.must(manifest).directory, repo_contents_path) do
|
|
@@ -31,7 +46,35 @@ module Dependabot
|
|
|
31
46
|
updated_files
|
|
32
47
|
end
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
# Xcode SPM update: updates Package.resolved files in-place without CLI
|
|
50
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
51
|
+
def updated_xcode_spm_files
|
|
52
|
+
updated_files = T.let([], T::Array[Dependabot::DependencyFile])
|
|
53
|
+
|
|
54
|
+
xcode_resolved_files.each do |resolved_file|
|
|
55
|
+
updater = XcodeLockfileUpdater.new(
|
|
56
|
+
resolved_file: resolved_file,
|
|
57
|
+
dependencies: dependencies,
|
|
58
|
+
workspace_files: xcode_workspace_files
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
next unless updater.lockfile_changed?
|
|
62
|
+
|
|
63
|
+
updated_content = updater.updated_lockfile_content
|
|
64
|
+
next if updated_content == resolved_file.content
|
|
65
|
+
|
|
66
|
+
updated_files << updated_file(file: resolved_file, content: updated_content)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if updated_files.empty?
|
|
70
|
+
raise Dependabot::DependencyFileNotFound.new(
|
|
71
|
+
nil,
|
|
72
|
+
"No Package.resolved files needed updating for the specified dependencies"
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
updated_files
|
|
77
|
+
end
|
|
35
78
|
|
|
36
79
|
sig { returns(Dependabot::Dependency) }
|
|
37
80
|
def dependency
|
|
@@ -42,7 +85,38 @@ module Dependabot
|
|
|
42
85
|
|
|
43
86
|
sig { override.void }
|
|
44
87
|
def check_required_files
|
|
45
|
-
|
|
88
|
+
return if manifest
|
|
89
|
+
return if xcode_spm_mode? && xcode_resolved_files.any?
|
|
90
|
+
|
|
91
|
+
raise "A Package.swift file or Xcode Package.resolved must be provided!"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig { returns(T::Boolean) }
|
|
95
|
+
def xcode_spm_mode?
|
|
96
|
+
return false unless Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
|
|
97
|
+
|
|
98
|
+
manifest.nil? && xcode_resolved_files.any?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
102
|
+
def xcode_resolved_files
|
|
103
|
+
@xcode_resolved_files ||= T.let(
|
|
104
|
+
dependency_files.select do |f|
|
|
105
|
+
XcodeFileHelpers.xcode_resolved_path?(f.name) &&
|
|
106
|
+
!f.support_file?
|
|
107
|
+
end,
|
|
108
|
+
T.nilable(T::Array[Dependabot::DependencyFile])
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
113
|
+
def xcode_workspace_files
|
|
114
|
+
@xcode_workspace_files ||= T.let(
|
|
115
|
+
dependency_files.select do |f|
|
|
116
|
+
f.name.end_with?("contents.xcworkspacedata") && f.support_file?
|
|
117
|
+
end,
|
|
118
|
+
T.nilable(T::Array[Dependabot::DependencyFile])
|
|
119
|
+
)
|
|
46
120
|
end
|
|
47
121
|
|
|
48
122
|
sig { returns(String) }
|
|
@@ -17,6 +17,11 @@ module Dependabot
|
|
|
17
17
|
case new_source_type
|
|
18
18
|
when "git" then find_source_from_git_url
|
|
19
19
|
when "registry" then find_source_from_registry
|
|
20
|
+
when "default", nil
|
|
21
|
+
# For dependencies without explicit source info (e.g., Xcode-managed
|
|
22
|
+
# SPM dependencies parsed from Package.resolved), attempt to infer
|
|
23
|
+
# source from the dependency name which is typically a normalized URL
|
|
24
|
+
find_source_from_dependency_name
|
|
20
25
|
else raise "Unexpected source type: #{new_source_type}"
|
|
21
26
|
end
|
|
22
27
|
end
|
|
@@ -34,6 +39,15 @@ module Dependabot
|
|
|
34
39
|
Source.from_url(url)
|
|
35
40
|
end
|
|
36
41
|
|
|
42
|
+
sig { returns(T.nilable(Dependabot::Source)) }
|
|
43
|
+
def find_source_from_dependency_name
|
|
44
|
+
name = dependency.name
|
|
45
|
+
return nil unless name.include?("/")
|
|
46
|
+
|
|
47
|
+
url = "https://#{name}"
|
|
48
|
+
Source.from_url(url)
|
|
49
|
+
end
|
|
50
|
+
|
|
37
51
|
sig { returns(T.noreturn) }
|
|
38
52
|
def find_source_from_registry
|
|
39
53
|
raise NotImplementedError
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "dependabot/update_checkers/base"
|
|
@@ -14,11 +14,13 @@ module Dependabot
|
|
|
14
14
|
sig do
|
|
15
15
|
params(
|
|
16
16
|
requirements: T::Array[T::Hash[Symbol, T.untyped]],
|
|
17
|
-
target_version: T.nilable(T.any(String, Gem::Version))
|
|
17
|
+
target_version: T.nilable(T.any(String, Gem::Version)),
|
|
18
|
+
xcode_mode: T::Boolean
|
|
18
19
|
).void
|
|
19
20
|
end
|
|
20
|
-
def initialize(requirements:, target_version:)
|
|
21
|
+
def initialize(requirements:, target_version:, xcode_mode: false)
|
|
21
22
|
@requirements = requirements
|
|
23
|
+
@xcode_mode = xcode_mode
|
|
22
24
|
|
|
23
25
|
return unless target_version && Version.correct?(target_version)
|
|
24
26
|
|
|
@@ -27,6 +29,8 @@ module Dependabot
|
|
|
27
29
|
|
|
28
30
|
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
29
31
|
def updated_requirements
|
|
32
|
+
return updated_xcode_requirements if xcode_mode
|
|
33
|
+
|
|
30
34
|
NativeRequirement.map_requirements(requirements) do |requirement|
|
|
31
35
|
T.must(requirement.update_if_needed(T.must(target_version)))
|
|
32
36
|
end
|
|
@@ -39,6 +43,116 @@ module Dependabot
|
|
|
39
43
|
|
|
40
44
|
sig { returns(T.nilable(Gem::Version)) }
|
|
41
45
|
attr_reader :target_version
|
|
46
|
+
|
|
47
|
+
sig { returns(T::Boolean) }
|
|
48
|
+
attr_reader :xcode_mode
|
|
49
|
+
|
|
50
|
+
# For Xcode projects, we update the version in the requirement while preserving the kind.
|
|
51
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
52
|
+
def updated_xcode_requirements
|
|
53
|
+
requirements.map do |req|
|
|
54
|
+
next req unless target_version
|
|
55
|
+
|
|
56
|
+
updated_req = update_xcode_requirement(req)
|
|
57
|
+
updated_req
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { params(requirement: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
62
|
+
def update_xcode_requirement(requirement)
|
|
63
|
+
metadata = requirement[:metadata] || {}
|
|
64
|
+
requirement_string = metadata[:requirement_string]
|
|
65
|
+
kind = metadata[:kind]
|
|
66
|
+
|
|
67
|
+
new_requirement_string = build_xcode_requirement_string(requirement_string, kind)
|
|
68
|
+
new_requirement = build_xcode_requirement(requirement_string, kind)
|
|
69
|
+
|
|
70
|
+
requirement.merge(
|
|
71
|
+
requirement: new_requirement,
|
|
72
|
+
metadata: metadata.merge(
|
|
73
|
+
requirement_string: new_requirement_string
|
|
74
|
+
).compact
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig do
|
|
79
|
+
params(
|
|
80
|
+
requirement_string: T.nilable(String),
|
|
81
|
+
kind: T.nilable(String)
|
|
82
|
+
).returns(T.nilable(String))
|
|
83
|
+
end
|
|
84
|
+
def build_xcode_requirement_string(requirement_string, kind)
|
|
85
|
+
return requirement_string unless target_version
|
|
86
|
+
|
|
87
|
+
case kind
|
|
88
|
+
when "upToNextMajorVersion"
|
|
89
|
+
"from: \"#{target_version}\""
|
|
90
|
+
when "upToNextMinorVersion"
|
|
91
|
+
".upToNextMinor(from: \"#{target_version}\")"
|
|
92
|
+
when "exactVersion"
|
|
93
|
+
"exact: \"#{target_version}\""
|
|
94
|
+
when "versionRange"
|
|
95
|
+
max = extract_version_range_max(requirement_string)
|
|
96
|
+
"\"#{target_version}\"..<\"#{max}\""
|
|
97
|
+
else
|
|
98
|
+
# Default: update to exact version for unknown kinds
|
|
99
|
+
"exact: \"#{target_version}\""
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sig do
|
|
104
|
+
params(
|
|
105
|
+
requirement_string: T.nilable(String),
|
|
106
|
+
kind: T.nilable(String)
|
|
107
|
+
).returns(T.nilable(String))
|
|
108
|
+
end
|
|
109
|
+
def build_xcode_requirement(requirement_string, kind)
|
|
110
|
+
return nil unless target_version
|
|
111
|
+
|
|
112
|
+
case kind
|
|
113
|
+
when "upToNextMajorVersion"
|
|
114
|
+
max = bump_version(target_version.to_s, :major)
|
|
115
|
+
">= #{target_version}, < #{max}"
|
|
116
|
+
when "upToNextMinorVersion"
|
|
117
|
+
max = bump_version(target_version.to_s, :minor)
|
|
118
|
+
">= #{target_version}, < #{max}"
|
|
119
|
+
when "exactVersion"
|
|
120
|
+
"= #{target_version}"
|
|
121
|
+
when "versionRange"
|
|
122
|
+
max = extract_version_range_max(requirement_string)
|
|
123
|
+
">= #{target_version}, < #{max}"
|
|
124
|
+
else
|
|
125
|
+
# Default: exact version
|
|
126
|
+
"= #{target_version}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Extracts the upper bound from a versionRange requirement string.
|
|
131
|
+
# Format: "min"..<"max" or "min"..."max"
|
|
132
|
+
sig { params(requirement_string: T.nilable(String)).returns(String) }
|
|
133
|
+
def extract_version_range_max(requirement_string)
|
|
134
|
+
return bump_version(target_version.to_s, :major) unless requirement_string
|
|
135
|
+
|
|
136
|
+
# Match patterns like "1.0.0"..<"2.0.0" or "1.0.0"..."2.0.0"
|
|
137
|
+
match = requirement_string.match(/\.{2,3}<?"(\d+\.\d+\.\d+)"/)
|
|
138
|
+
return bump_version(target_version.to_s, :major) unless match
|
|
139
|
+
|
|
140
|
+
match[1].to_s
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
sig { params(version_str: String, bump_type: Symbol).returns(String) }
|
|
144
|
+
def bump_version(version_str, bump_type)
|
|
145
|
+
parts = version_str.split(".").map(&:to_i)
|
|
146
|
+
|
|
147
|
+
case bump_type
|
|
148
|
+
when :major
|
|
149
|
+
[(parts[0] || 0) + 1, 0, 0]
|
|
150
|
+
when :minor
|
|
151
|
+
[parts[0] || 0, (parts[1] || 0) + 1, 0]
|
|
152
|
+
else
|
|
153
|
+
parts
|
|
154
|
+
end.join(".")
|
|
155
|
+
end
|
|
42
156
|
end
|
|
43
157
|
end
|
|
44
158
|
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/git_commit_checker"
|
|
6
|
+
require "dependabot/swift/update_checker"
|
|
7
|
+
require "dependabot/swift/requirement"
|
|
8
|
+
require "dependabot/swift/version"
|
|
9
|
+
require "dependabot/update_checkers/version_filters"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Swift
|
|
13
|
+
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
|
14
|
+
# Resolves versions for Xcode-only SwiftPM projects (no Package.swift).
|
|
15
|
+
#
|
|
16
|
+
# Unlike the classic VersionResolver which relies on `swift package update`,
|
|
17
|
+
# this resolver uses GitCommitChecker to find the latest available version
|
|
18
|
+
# from git tags, since we cannot run the Swift CLI without a manifest.
|
|
19
|
+
class XcodeVersionResolver
|
|
20
|
+
extend T::Sig
|
|
21
|
+
|
|
22
|
+
sig do
|
|
23
|
+
params(
|
|
24
|
+
dependency: Dependabot::Dependency,
|
|
25
|
+
git_commit_checker: Dependabot::GitCommitChecker,
|
|
26
|
+
security_advisories: T::Array[Dependabot::SecurityAdvisory]
|
|
27
|
+
).void
|
|
28
|
+
end
|
|
29
|
+
def initialize(dependency:, git_commit_checker:, security_advisories:)
|
|
30
|
+
@dependency = dependency
|
|
31
|
+
@git_commit_checker = git_commit_checker
|
|
32
|
+
@security_advisories = security_advisories
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
36
|
+
def latest_resolvable_version
|
|
37
|
+
return nil unless version_pinned?
|
|
38
|
+
|
|
39
|
+
tag = git_commit_checker.local_tag_for_latest_version
|
|
40
|
+
return nil unless tag
|
|
41
|
+
|
|
42
|
+
version = tag.fetch(:version)
|
|
43
|
+
return nil unless version_meets_requirements?(version)
|
|
44
|
+
|
|
45
|
+
Version.new(version)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
49
|
+
def lowest_security_fix_version
|
|
50
|
+
return nil unless version_pinned?
|
|
51
|
+
|
|
52
|
+
tags = git_commit_checker.local_tags_for_allowed_versions
|
|
53
|
+
relevant_tags = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(
|
|
54
|
+
tags,
|
|
55
|
+
security_advisories
|
|
56
|
+
)
|
|
57
|
+
relevant_tags = filter_lower_tags(relevant_tags)
|
|
58
|
+
|
|
59
|
+
lowest_tag = relevant_tags.min_by { |tag| tag.fetch(:version) }
|
|
60
|
+
return nil unless lowest_tag
|
|
61
|
+
|
|
62
|
+
Version.new(lowest_tag.fetch(:version))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { returns(T::Boolean) }
|
|
66
|
+
def version_pinned?
|
|
67
|
+
return false unless dependency.version
|
|
68
|
+
|
|
69
|
+
Version.correct?(dependency.version)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
sig { returns(Dependabot::Dependency) }
|
|
75
|
+
attr_reader :dependency
|
|
76
|
+
|
|
77
|
+
sig { returns(Dependabot::GitCommitChecker) }
|
|
78
|
+
attr_reader :git_commit_checker
|
|
79
|
+
|
|
80
|
+
sig { returns(T::Array[Dependabot::SecurityAdvisory]) }
|
|
81
|
+
attr_reader :security_advisories
|
|
82
|
+
|
|
83
|
+
sig { returns(T.nilable(Dependabot::Swift::Requirement)) }
|
|
84
|
+
def dependency_requirement
|
|
85
|
+
req_string = dependency.requirements.first&.dig(:requirement)
|
|
86
|
+
return nil unless req_string
|
|
87
|
+
|
|
88
|
+
Dependabot::Swift::Requirement.new(req_string)
|
|
89
|
+
rescue Gem::Requirement::BadRequirementError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
sig { params(version: T.untyped).returns(T::Boolean) }
|
|
94
|
+
def version_meets_requirements?(version)
|
|
95
|
+
requirement = dependency_requirement
|
|
96
|
+
return true unless requirement
|
|
97
|
+
|
|
98
|
+
requirement.satisfied_by?(version)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig do
|
|
102
|
+
params(
|
|
103
|
+
tags: T::Array[T::Hash[Symbol, T.untyped]]
|
|
104
|
+
).returns(T::Array[T::Hash[Symbol, T.untyped]])
|
|
105
|
+
end
|
|
106
|
+
def filter_lower_tags(tags)
|
|
107
|
+
current = current_version
|
|
108
|
+
return tags unless current
|
|
109
|
+
|
|
110
|
+
tags.select { |tag| tag.fetch(:version) > current }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
114
|
+
def current_version
|
|
115
|
+
return nil unless dependency.version
|
|
116
|
+
|
|
117
|
+
Version.new(dependency.version)
|
|
118
|
+
rescue ArgumentError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/experiments"
|
|
5
6
|
require "dependabot/update_checkers"
|
|
6
7
|
require "dependabot/update_checkers/base"
|
|
7
8
|
require "dependabot/update_checkers/version_filters"
|
|
8
9
|
require "dependabot/git_commit_checker"
|
|
9
10
|
require "dependabot/swift/native_requirement"
|
|
10
11
|
require "dependabot/swift/file_updater/manifest_updater"
|
|
12
|
+
require "dependabot/swift/xcode_file_helpers"
|
|
11
13
|
|
|
12
14
|
module Dependabot
|
|
13
15
|
module Swift
|
|
@@ -17,6 +19,7 @@ module Dependabot
|
|
|
17
19
|
require_relative "update_checker/requirements_updater"
|
|
18
20
|
require_relative "update_checker/version_resolver"
|
|
19
21
|
require_relative "update_checker/latest_version_resolver"
|
|
22
|
+
require_relative "update_checker/xcode_version_resolver"
|
|
20
23
|
|
|
21
24
|
sig { override.returns(T.nilable(Dependabot::Version)) }
|
|
22
25
|
def latest_version
|
|
@@ -50,21 +53,50 @@ module Dependabot
|
|
|
50
53
|
|
|
51
54
|
sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
52
55
|
def updated_requirements
|
|
56
|
+
return updated_xcode_requirements if xcode_spm_mode?
|
|
57
|
+
|
|
58
|
+
# If no target version is available, return old requirements unchanged
|
|
59
|
+
target = preferred_resolvable_version
|
|
60
|
+
return old_requirements unless target
|
|
61
|
+
|
|
53
62
|
RequirementsUpdater.new(
|
|
54
63
|
requirements: old_requirements,
|
|
55
|
-
target_version:
|
|
64
|
+
target_version: target
|
|
56
65
|
).updated_requirements
|
|
57
66
|
end
|
|
58
67
|
|
|
59
68
|
private
|
|
60
69
|
|
|
70
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
71
|
+
def updated_xcode_requirements
|
|
72
|
+
# If no target version is available (e.g., revision-only or branch-pinned
|
|
73
|
+
# dependency), return old requirements unchanged
|
|
74
|
+
target = preferred_resolvable_version
|
|
75
|
+
return old_requirements unless target
|
|
76
|
+
|
|
77
|
+
RequirementsUpdater.new(
|
|
78
|
+
requirements: old_requirements,
|
|
79
|
+
target_version: target,
|
|
80
|
+
xcode_mode: true
|
|
81
|
+
).updated_requirements
|
|
82
|
+
end
|
|
83
|
+
|
|
61
84
|
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
62
85
|
def old_requirements
|
|
63
86
|
dependency.requirements
|
|
64
87
|
end
|
|
65
88
|
|
|
89
|
+
sig { returns(T::Boolean) }
|
|
90
|
+
def xcode_spm_mode?
|
|
91
|
+
return false unless Dependabot::Experiments.enabled?(:enable_swift_xcode_spm)
|
|
92
|
+
|
|
93
|
+
manifest.nil? && xcode_resolved_files.any?
|
|
94
|
+
end
|
|
95
|
+
|
|
66
96
|
sig { returns(T.nilable(Dependabot::Version)) }
|
|
67
97
|
def fetch_latest_version
|
|
98
|
+
return fetch_xcode_latest_version if xcode_spm_mode?
|
|
99
|
+
|
|
68
100
|
return unless git_commit_checker.pinned_ref_looks_like_version? && latest_version_tag
|
|
69
101
|
|
|
70
102
|
tag = latest_version_tag
|
|
@@ -73,8 +105,22 @@ module Dependabot
|
|
|
73
105
|
tag.fetch(:version)
|
|
74
106
|
end
|
|
75
107
|
|
|
108
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
109
|
+
def fetch_xcode_latest_version
|
|
110
|
+
# For branch-pinned or revision-only dependencies, don't report a latest version
|
|
111
|
+
# since they can't be meaningfully updated to version-based pins
|
|
112
|
+
return nil unless xcode_version_resolver.version_pinned?
|
|
113
|
+
|
|
114
|
+
tag = latest_version_tag
|
|
115
|
+
return unless tag
|
|
116
|
+
|
|
117
|
+
tag.fetch(:version)
|
|
118
|
+
end
|
|
119
|
+
|
|
76
120
|
sig { returns(T.nilable(Dependabot::Version)) }
|
|
77
121
|
def fetch_lowest_security_fix_version
|
|
122
|
+
return fetch_xcode_lowest_security_fix_version if xcode_spm_mode?
|
|
123
|
+
|
|
78
124
|
return unless git_commit_checker.pinned_ref_looks_like_version? && latest_version_tag
|
|
79
125
|
|
|
80
126
|
tag = lowest_security_fix_version_tag
|
|
@@ -83,16 +129,30 @@ module Dependabot
|
|
|
83
129
|
tag.fetch(:version)
|
|
84
130
|
end
|
|
85
131
|
|
|
132
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
133
|
+
def fetch_xcode_lowest_security_fix_version
|
|
134
|
+
xcode_version_resolver.lowest_security_fix_version
|
|
135
|
+
end
|
|
136
|
+
|
|
86
137
|
sig { returns(T.nilable(Dependabot::Version)) }
|
|
87
138
|
def fetch_latest_resolvable_version
|
|
139
|
+
return fetch_xcode_latest_resolvable_version if xcode_spm_mode?
|
|
140
|
+
|
|
88
141
|
latest_resolvable_version = version_resolver_for(unlocked_requirements).latest_resolvable_version
|
|
89
142
|
return current_version unless latest_resolvable_version
|
|
90
143
|
|
|
91
144
|
Version.new(latest_resolvable_version)
|
|
92
145
|
end
|
|
93
146
|
|
|
147
|
+
sig { returns(T.nilable(Dependabot::Version)) }
|
|
148
|
+
def fetch_xcode_latest_resolvable_version
|
|
149
|
+
xcode_version_resolver.latest_resolvable_version
|
|
150
|
+
end
|
|
151
|
+
|
|
94
152
|
sig { returns(T.nilable(Dependabot::Version)) }
|
|
95
153
|
def fetch_lowest_resolvable_security_fix_version
|
|
154
|
+
return fetch_xcode_lowest_security_fix_version if xcode_spm_mode?
|
|
155
|
+
|
|
96
156
|
lowest_resolvable_security_fix_version = version_resolver_for(
|
|
97
157
|
force_lowest_security_fix_requirements
|
|
98
158
|
).latest_resolvable_version
|
|
@@ -218,6 +278,29 @@ module Dependabot
|
|
|
218
278
|
tags_array
|
|
219
279
|
.select { |tag| tag.fetch(:version) > current_version }
|
|
220
280
|
end
|
|
281
|
+
|
|
282
|
+
sig { returns(XcodeVersionResolver) }
|
|
283
|
+
def xcode_version_resolver
|
|
284
|
+
@xcode_version_resolver ||= T.let(
|
|
285
|
+
XcodeVersionResolver.new(
|
|
286
|
+
dependency: dependency,
|
|
287
|
+
git_commit_checker: git_commit_checker,
|
|
288
|
+
security_advisories: security_advisories
|
|
289
|
+
),
|
|
290
|
+
T.nilable(XcodeVersionResolver)
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
295
|
+
def xcode_resolved_files
|
|
296
|
+
@xcode_resolved_files ||= T.let(
|
|
297
|
+
dependency_files.select do |f|
|
|
298
|
+
XcodeFileHelpers.xcode_resolved_path?(f.name) &&
|
|
299
|
+
!f.support_file?
|
|
300
|
+
end,
|
|
301
|
+
T.nilable(T::Array[Dependabot::DependencyFile])
|
|
302
|
+
)
|
|
303
|
+
end
|
|
221
304
|
end
|
|
222
305
|
end
|
|
223
306
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module Dependabot
|
|
7
|
+
module Swift
|
|
8
|
+
module XcodeFileHelpers
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
XCODEPROJ_SUFFIX = ".xcodeproj/"
|
|
12
|
+
XCWORKSPACE_SUFFIX = ".xcworkspace/"
|
|
13
|
+
PACKAGE_RESOLVED = "Package.resolved"
|
|
14
|
+
|
|
15
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
16
|
+
def self.xcode_resolved_path?(path)
|
|
17
|
+
return false unless path.end_with?(PACKAGE_RESOLVED)
|
|
18
|
+
|
|
19
|
+
path.include?(XCODEPROJ_SUFFIX) || path.include?(XCWORKSPACE_SUFFIX)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { params(path: String).returns(T.nilable(String)) }
|
|
23
|
+
def self.extract_xcode_scope_dir(path)
|
|
24
|
+
# Find the first occurrence of .xcodeproj/ or .xcworkspace/
|
|
25
|
+
xcodeproj_idx = path.index(XCODEPROJ_SUFFIX)
|
|
26
|
+
xcworkspace_idx = path.index(XCWORKSPACE_SUFFIX)
|
|
27
|
+
|
|
28
|
+
# Determine which match to use (earliest occurrence)
|
|
29
|
+
match_idx = T.let(nil, T.nilable(Integer))
|
|
30
|
+
suffix_len = T.let(0, Integer)
|
|
31
|
+
|
|
32
|
+
if xcodeproj_idx && (xcworkspace_idx.nil? || xcodeproj_idx < xcworkspace_idx)
|
|
33
|
+
match_idx = xcodeproj_idx
|
|
34
|
+
suffix_len = XCODEPROJ_SUFFIX.length
|
|
35
|
+
elsif xcworkspace_idx
|
|
36
|
+
match_idx = xcworkspace_idx
|
|
37
|
+
suffix_len = XCWORKSPACE_SUFFIX.length
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return nil if match_idx.nil?
|
|
41
|
+
|
|
42
|
+
# Return path up to and including the suffix (minus trailing /)
|
|
43
|
+
path[0, match_idx + suffix_len - 1]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-swift
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.366.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.366.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.366.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -253,6 +253,7 @@ files:
|
|
|
253
253
|
- lib/dependabot/swift/file_updater/lockfile_updater.rb
|
|
254
254
|
- lib/dependabot/swift/file_updater/manifest_updater.rb
|
|
255
255
|
- lib/dependabot/swift/file_updater/requirement_replacer.rb
|
|
256
|
+
- lib/dependabot/swift/file_updater/xcode_lockfile_updater.rb
|
|
256
257
|
- lib/dependabot/swift/language.rb
|
|
257
258
|
- lib/dependabot/swift/metadata_finder.rb
|
|
258
259
|
- lib/dependabot/swift/native_requirement.rb
|
|
@@ -263,14 +264,16 @@ files:
|
|
|
263
264
|
- lib/dependabot/swift/update_checker/latest_version_resolver.rb
|
|
264
265
|
- lib/dependabot/swift/update_checker/requirements_updater.rb
|
|
265
266
|
- lib/dependabot/swift/update_checker/version_resolver.rb
|
|
267
|
+
- lib/dependabot/swift/update_checker/xcode_version_resolver.rb
|
|
266
268
|
- lib/dependabot/swift/url_helpers.rb
|
|
267
269
|
- lib/dependabot/swift/version.rb
|
|
270
|
+
- lib/dependabot/swift/xcode_file_helpers.rb
|
|
268
271
|
homepage: https://github.com/dependabot/dependabot-core
|
|
269
272
|
licenses:
|
|
270
273
|
- MIT
|
|
271
274
|
metadata:
|
|
272
275
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
273
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
276
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.366.0
|
|
274
277
|
rdoc_options: []
|
|
275
278
|
require_paths:
|
|
276
279
|
- lib
|