dependabot-cargo 0.81.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/dependabot/cargo.rb +11 -0
- data/lib/dependabot/cargo/file_fetcher.rb +241 -0
- data/lib/dependabot/cargo/file_parser.rb +214 -0
- data/lib/dependabot/cargo/file_updater.rb +83 -0
- data/lib/dependabot/cargo/file_updater/lockfile_updater.rb +247 -0
- data/lib/dependabot/cargo/file_updater/manifest_updater.rb +158 -0
- data/lib/dependabot/cargo/metadata_finder.rb +62 -0
- data/lib/dependabot/cargo/requirement.rb +108 -0
- data/lib/dependabot/cargo/update_checker.rb +283 -0
- data/lib/dependabot/cargo/update_checker/file_preparer.rb +200 -0
- data/lib/dependabot/cargo/update_checker/requirements_updater.rb +173 -0
- data/lib/dependabot/cargo/update_checker/version_resolver.rb +239 -0
- data/lib/dependabot/cargo/version.rb +34 -0
- metadata +183 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9a15f5a5286fc161d2f722711b599016486490abb64c446631eeed673a9a7742
|
4
|
+
data.tar.gz: d01689805661704379ea8b2ccd1795dd674fcf7c5c6a8c0de8680d64dfd08a31
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e3bcff1df9f77bc8b94230cff603352c84dab68e7c4f2648c952e32b02ad2a186a2052e4cd5d862c7e59cb19c247e1772b720e3ccf69c81dbfd94943406151b0
|
7
|
+
data.tar.gz: cf7de24ba36776e1b0377caf4a73f99eeb172f432c32c2f9ae18dad0c04b08ea8b3652aee5319f4a2a70f12bc630ea7419ac40b75349c2217260a6bac0d46244
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# These all need to be required so the various classes can be registered in a
|
4
|
+
# lookup table of package manager names to concrete classes.
|
5
|
+
require "dependabot/cargo/file_fetcher"
|
6
|
+
require "dependabot/cargo/file_parser"
|
7
|
+
require "dependabot/cargo/update_checker"
|
8
|
+
require "dependabot/cargo/file_updater"
|
9
|
+
require "dependabot/cargo/metadata_finder"
|
10
|
+
require "dependabot/cargo/requirement"
|
11
|
+
require "dependabot/cargo/version"
|
@@ -0,0 +1,241 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "toml-rb"
|
5
|
+
|
6
|
+
require "dependabot/file_fetchers"
|
7
|
+
require "dependabot/file_fetchers/base"
|
8
|
+
require "dependabot/cargo/file_parser"
|
9
|
+
|
10
|
+
# Docs on Cargo workspaces:
|
11
|
+
# https://doc.rust-lang.org/cargo/reference/manifest.html#the-workspace-section
|
12
|
+
module Dependabot
|
13
|
+
module Cargo
|
14
|
+
class FileFetcher < Dependabot::FileFetchers::Base
|
15
|
+
def self.required_files_in?(filenames)
|
16
|
+
filenames.include?("Cargo.toml")
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.required_files_message
|
20
|
+
"Repo must contain a Cargo.toml."
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def fetch_files
|
26
|
+
fetched_files = []
|
27
|
+
fetched_files << cargo_toml
|
28
|
+
fetched_files << cargo_lock if cargo_lock
|
29
|
+
fetched_files << rust_toolchain if rust_toolchain
|
30
|
+
fetched_files += workspace_files
|
31
|
+
fetched_files += path_dependency_files
|
32
|
+
fetched_files
|
33
|
+
end
|
34
|
+
|
35
|
+
def workspace_files
|
36
|
+
@workspace_files ||=
|
37
|
+
fetch_workspace_files(
|
38
|
+
file: cargo_toml,
|
39
|
+
previously_fetched_files: []
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def path_dependency_files
|
44
|
+
@path_dependency_files ||=
|
45
|
+
begin
|
46
|
+
fetched_path_dependency_files = []
|
47
|
+
[cargo_toml, *workspace_files].each do |file|
|
48
|
+
fetched_path_dependency_files +=
|
49
|
+
fetch_path_dependency_files(
|
50
|
+
file: file,
|
51
|
+
previously_fetched_files: [cargo_toml, *workspace_files] +
|
52
|
+
fetched_path_dependency_files
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
fetched_path_dependency_files
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch_workspace_files(file:, previously_fetched_files:)
|
61
|
+
current_dir = file.name.split("/")[0..-2].join("/")
|
62
|
+
current_dir = nil if current_dir == ""
|
63
|
+
|
64
|
+
workspace_dependency_paths_from_file(file).flat_map do |path|
|
65
|
+
path = File.join(current_dir, path) unless current_dir.nil?
|
66
|
+
path = Pathname.new(path).cleanpath.to_path
|
67
|
+
|
68
|
+
next if previously_fetched_files.map(&:name).include?(path)
|
69
|
+
next if file.name == path
|
70
|
+
|
71
|
+
fetched_file = fetch_file_from_host(path)
|
72
|
+
previously_fetched_files << fetched_file
|
73
|
+
grandchild_requirement_files =
|
74
|
+
fetch_workspace_files(
|
75
|
+
file: fetched_file,
|
76
|
+
previously_fetched_files: previously_fetched_files
|
77
|
+
)
|
78
|
+
[fetched_file, *grandchild_requirement_files]
|
79
|
+
end.compact
|
80
|
+
end
|
81
|
+
|
82
|
+
def fetch_path_dependency_files(
|
83
|
+
file:,
|
84
|
+
previously_fetched_files:
|
85
|
+
)
|
86
|
+
current_dir = file.name.split("/")[0..-2].join("/")
|
87
|
+
current_dir = nil if current_dir == ""
|
88
|
+
|
89
|
+
path_dependency_paths_from_file(file).flat_map do |path|
|
90
|
+
path = File.join(current_dir, path) unless current_dir.nil?
|
91
|
+
path = Pathname.new(path).cleanpath.to_path
|
92
|
+
|
93
|
+
next if previously_fetched_files.map(&:name).include?(path)
|
94
|
+
next if file.name == path
|
95
|
+
|
96
|
+
fetched_file = fetch_file_from_host(path, type: "path_dependency").
|
97
|
+
tap { |f| f.support_file = true }
|
98
|
+
previously_fetched_files << fetched_file
|
99
|
+
grandchild_requirement_files =
|
100
|
+
fetch_path_dependency_files(
|
101
|
+
file: fetched_file,
|
102
|
+
previously_fetched_files: previously_fetched_files
|
103
|
+
)
|
104
|
+
[fetched_file, *grandchild_requirement_files]
|
105
|
+
rescue Dependabot::DependencyFileNotFound
|
106
|
+
raise if required_path?(file, path)
|
107
|
+
end.compact
|
108
|
+
end
|
109
|
+
|
110
|
+
def path_dependency_paths_from_file(file)
|
111
|
+
paths = []
|
112
|
+
|
113
|
+
# Paths specified in dependency declaration
|
114
|
+
Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
|
115
|
+
parsed_file(file).fetch(type, {}).each do |_, details|
|
116
|
+
next unless details.is_a?(Hash)
|
117
|
+
next unless details["path"]
|
118
|
+
|
119
|
+
paths << File.join(details["path"], "Cargo.toml")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Paths specified for target-specific dependencies
|
124
|
+
parsed_file(file).fetch("target", {}).each do |_, t_details|
|
125
|
+
Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
|
126
|
+
t_details.fetch(type, {}).each do |_, details|
|
127
|
+
next unless details.is_a?(Hash)
|
128
|
+
next unless details["path"]
|
129
|
+
|
130
|
+
paths << File.join(details["path"], "Cargo.toml")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Paths specified as replacements
|
136
|
+
parsed_file(file).fetch("replace", {}).each do |_, details|
|
137
|
+
next unless details.is_a?(Hash)
|
138
|
+
next unless details["path"]
|
139
|
+
|
140
|
+
paths << File.join(details["path"], "Cargo.toml")
|
141
|
+
end
|
142
|
+
|
143
|
+
paths
|
144
|
+
end
|
145
|
+
|
146
|
+
def workspace_dependency_paths_from_file(file)
|
147
|
+
workspace_paths = parsed_file(file).dig("workspace", "members")
|
148
|
+
return [] unless workspace_paths&.any?
|
149
|
+
|
150
|
+
# Expand any workspace paths that specify a `*`
|
151
|
+
workspace_paths = workspace_paths.flat_map do |path|
|
152
|
+
path.end_with?("*") ? expand_workspaces(path) : [path]
|
153
|
+
end
|
154
|
+
|
155
|
+
# Excluded paths, to be subtracted for the workspaces array
|
156
|
+
excluded_paths = parsed_file(file).dig("workspace", "excluded_paths")
|
157
|
+
|
158
|
+
(workspace_paths - (excluded_paths || [])).map do |path|
|
159
|
+
File.join(path, "Cargo.toml")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Check whether a path is required or not. It will not be required if
|
164
|
+
# an alternative source (i.e., a git source) is also specified
|
165
|
+
# rubocop:disable Metrics/AbcSize
|
166
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
167
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
168
|
+
def required_path?(file, path)
|
169
|
+
# Paths specified in dependency declaration
|
170
|
+
Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
|
171
|
+
parsed_file(file).fetch(type, {}).each do |_, details|
|
172
|
+
next unless details.is_a?(Hash)
|
173
|
+
next unless details["path"]
|
174
|
+
next unless path == File.join(details["path"], "Cargo.toml")
|
175
|
+
|
176
|
+
return true if details["git"].nil?
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Paths specified for target-specific dependencies
|
181
|
+
parsed_file(file).fetch("target", {}).each do |_, t_details|
|
182
|
+
Cargo::FileParser::DEPENDENCY_TYPES.each do |type|
|
183
|
+
t_details.fetch(type, {}).each do |_, details|
|
184
|
+
next unless details.is_a?(Hash)
|
185
|
+
next unless details["path"]
|
186
|
+
next unless path == File.join(details["path"], "Cargo.toml")
|
187
|
+
|
188
|
+
return true if details["git"].nil?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Paths specified as replacements
|
194
|
+
parsed_file(file).fetch("replace", {}).each do |_, details|
|
195
|
+
next unless details.is_a?(Hash)
|
196
|
+
next unless details["path"]
|
197
|
+
next unless path == File.join(details["path"], "Cargo.toml")
|
198
|
+
|
199
|
+
return true if details["git"].nil?
|
200
|
+
end
|
201
|
+
|
202
|
+
false
|
203
|
+
end
|
204
|
+
# rubocop:enable Metrics/AbcSize
|
205
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
206
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
207
|
+
|
208
|
+
def expand_workspaces(path)
|
209
|
+
path = Pathname.new(path).cleanpath.to_path
|
210
|
+
dir = directory.gsub(%r{(^/|/$)}, "")
|
211
|
+
unglobbed_path = path.split("*").first.gsub(%r{(?<=/)[^/]*$}, "")
|
212
|
+
|
213
|
+
repo_contents(dir: unglobbed_path, raise_errors: false).
|
214
|
+
select { |file| file.type == "dir" }.
|
215
|
+
map { |f| f.path.gsub(%r{^/?#{Regexp.escape(dir)}/?}, "") }.
|
216
|
+
select { |filename| File.fnmatch?(path, filename) }
|
217
|
+
end
|
218
|
+
|
219
|
+
def parsed_file(file)
|
220
|
+
TomlRB.parse(file.content)
|
221
|
+
rescue TomlRB::ParseError
|
222
|
+
raise Dependabot::DependencyFileNotParseable, file.path
|
223
|
+
end
|
224
|
+
|
225
|
+
def cargo_toml
|
226
|
+
@cargo_toml ||= fetch_file_from_host("Cargo.toml")
|
227
|
+
end
|
228
|
+
|
229
|
+
def cargo_lock
|
230
|
+
@cargo_lock ||= fetch_file_if_present("Cargo.lock")
|
231
|
+
end
|
232
|
+
|
233
|
+
def rust_toolchain
|
234
|
+
@rust_toolchain ||= fetch_file_if_present("rust-toolchain")&.
|
235
|
+
tap { |f| f.support_file = true }
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
Dependabot::FileFetchers.register("cargo", Dependabot::Cargo::FileFetcher)
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "toml-rb"
|
4
|
+
|
5
|
+
require "dependabot/dependency"
|
6
|
+
require "dependabot/file_parsers"
|
7
|
+
require "dependabot/file_parsers/base"
|
8
|
+
require "dependabot/cargo/requirement"
|
9
|
+
require "dependabot/cargo/version"
|
10
|
+
require "dependabot/errors"
|
11
|
+
|
12
|
+
# Relevant Cargo docs can be found at:
|
13
|
+
# - https://doc.rust-lang.org/cargo/reference/manifest.html
|
14
|
+
# - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
|
15
|
+
module Dependabot
|
16
|
+
module Cargo
|
17
|
+
class FileParser < Dependabot::FileParsers::Base
|
18
|
+
require "dependabot/file_parsers/base/dependency_set"
|
19
|
+
|
20
|
+
DEPENDENCY_TYPES =
|
21
|
+
%w(dependencies dev-dependencies build-dependencies).freeze
|
22
|
+
|
23
|
+
def parse
|
24
|
+
check_rust_workspace_root
|
25
|
+
|
26
|
+
dependency_set = DependencySet.new
|
27
|
+
dependency_set += manifest_dependencies
|
28
|
+
dependency_set += lockfile_dependencies if lockfile
|
29
|
+
|
30
|
+
dependencies = dependency_set.dependencies
|
31
|
+
|
32
|
+
# TODO: Handle patched dependencies
|
33
|
+
dependencies.reject! { |d| patched_dependencies.include?(d.name) }
|
34
|
+
|
35
|
+
# TODO: Currently, Dependabot can't handle dependencies that have
|
36
|
+
# multiple source types. Fix that!
|
37
|
+
dependencies.reject do |dep|
|
38
|
+
dep.requirements.map { |r| r.dig(:source, :type) }.uniq.count > 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def check_rust_workspace_root
|
45
|
+
cargo_toml = dependency_files.find { |f| f.name == "Cargo.toml" }
|
46
|
+
workspace_root = parsed_file(cargo_toml).dig("package", "workspace")
|
47
|
+
return unless workspace_root
|
48
|
+
|
49
|
+
msg = "This project is part of a Rust workspace but is not the "\
|
50
|
+
"workspace root."\
|
51
|
+
|
52
|
+
if cargo_toml.directory != "/"
|
53
|
+
msg += "Please update your settings so Dependabot points at the "\
|
54
|
+
"workspace root instead of #{cargo_toml.directory}."
|
55
|
+
end
|
56
|
+
raise Dependabot::DependencyFileNotEvaluatable, msg
|
57
|
+
end
|
58
|
+
|
59
|
+
def manifest_dependencies
|
60
|
+
dependency_set = DependencySet.new
|
61
|
+
|
62
|
+
DEPENDENCY_TYPES.each do |type|
|
63
|
+
manifest_files.each do |file|
|
64
|
+
parsed_file(file).fetch(type, {}).each do |name, requirement|
|
65
|
+
next if lockfile && !version_from_lockfile(name, requirement)
|
66
|
+
|
67
|
+
dependency_set << Dependency.new(
|
68
|
+
name: name,
|
69
|
+
version: version_from_lockfile(name, requirement),
|
70
|
+
package_manager: "cargo",
|
71
|
+
requirements: [{
|
72
|
+
requirement: requirement_from_declaration(requirement),
|
73
|
+
file: file.name,
|
74
|
+
groups: [type],
|
75
|
+
source: source_from_declaration(requirement)
|
76
|
+
}]
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
dependency_set
|
83
|
+
end
|
84
|
+
|
85
|
+
def lockfile_dependencies
|
86
|
+
dependency_set = DependencySet.new
|
87
|
+
return dependency_set unless lockfile
|
88
|
+
|
89
|
+
parsed_file(lockfile).fetch("package", []).each do |package_details|
|
90
|
+
next unless package_details["source"]
|
91
|
+
|
92
|
+
# TODO: This isn't quite right, as it will only give us one
|
93
|
+
# version of each dependency (when in fact there are many)
|
94
|
+
dependency_set << Dependency.new(
|
95
|
+
name: package_details["name"],
|
96
|
+
version: version_from_lockfile_details(package_details),
|
97
|
+
package_manager: "cargo",
|
98
|
+
requirements: []
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
dependency_set
|
103
|
+
end
|
104
|
+
|
105
|
+
def patched_dependencies
|
106
|
+
root_manifest = manifest_files.find { |f| f.name == "Cargo.toml" }
|
107
|
+
return [] unless parsed_file(root_manifest)["patch"]
|
108
|
+
|
109
|
+
parsed_file(root_manifest)["patch"].values.flat_map(&:keys)
|
110
|
+
end
|
111
|
+
|
112
|
+
def requirement_from_declaration(declaration)
|
113
|
+
if declaration.is_a?(String)
|
114
|
+
return declaration == "" ? nil : declaration
|
115
|
+
end
|
116
|
+
unless declaration.is_a?(Hash)
|
117
|
+
raise "Unexpected dependency declaration: #{declaration}"
|
118
|
+
end
|
119
|
+
return declaration["version"] if declaration["version"]
|
120
|
+
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
|
124
|
+
def source_from_declaration(declaration)
|
125
|
+
return if declaration.is_a?(String)
|
126
|
+
unless declaration.is_a?(Hash)
|
127
|
+
raise "Unexpected dependency declaration: #{declaration}"
|
128
|
+
end
|
129
|
+
|
130
|
+
return git_source_details(declaration) if declaration["git"]
|
131
|
+
return { type: "path" } if declaration["path"]
|
132
|
+
end
|
133
|
+
|
134
|
+
def version_from_lockfile(name, declaration)
|
135
|
+
return unless lockfile
|
136
|
+
|
137
|
+
candidate_packages =
|
138
|
+
parsed_file(lockfile).fetch("package", []).
|
139
|
+
select { |p| p["name"] == name }
|
140
|
+
|
141
|
+
if (req = requirement_from_declaration(declaration))
|
142
|
+
req = Cargo::Requirement.new(req)
|
143
|
+
|
144
|
+
candidate_packages =
|
145
|
+
candidate_packages.
|
146
|
+
select { |p| req.satisfied_by?(version_class.new(p["version"])) }
|
147
|
+
end
|
148
|
+
|
149
|
+
candidate_packages =
|
150
|
+
candidate_packages.
|
151
|
+
select do |p|
|
152
|
+
git_req?(declaration) ^ !p["source"]&.start_with?("git+")
|
153
|
+
end
|
154
|
+
|
155
|
+
package =
|
156
|
+
candidate_packages.
|
157
|
+
max_by { |p| version_class.new(p["version"]) }
|
158
|
+
|
159
|
+
return unless package
|
160
|
+
|
161
|
+
version_from_lockfile_details(package)
|
162
|
+
end
|
163
|
+
|
164
|
+
def git_req?(declaration)
|
165
|
+
source_from_declaration(declaration)&.fetch(:type, nil) == "git"
|
166
|
+
end
|
167
|
+
|
168
|
+
def git_source_details(declaration)
|
169
|
+
{
|
170
|
+
type: "git",
|
171
|
+
url: declaration["git"],
|
172
|
+
branch: declaration["branch"],
|
173
|
+
ref: declaration["tag"] || declaration["rev"]
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
def version_from_lockfile_details(package_details)
|
178
|
+
unless package_details["source"]&.start_with?("git+")
|
179
|
+
return package_details["version"]
|
180
|
+
end
|
181
|
+
|
182
|
+
package_details["source"].split("#").last
|
183
|
+
end
|
184
|
+
|
185
|
+
def check_required_files
|
186
|
+
raise "No Cargo.toml!" unless get_original_file("Cargo.toml")
|
187
|
+
end
|
188
|
+
|
189
|
+
def parsed_file(file)
|
190
|
+
@parsed_file ||= {}
|
191
|
+
@parsed_file[file.name] ||= TomlRB.parse(file.content)
|
192
|
+
rescue TomlRB::ParseError
|
193
|
+
raise Dependabot::DependencyFileNotParseable, file.path
|
194
|
+
end
|
195
|
+
|
196
|
+
def manifest_files
|
197
|
+
@manifest_files ||=
|
198
|
+
dependency_files.
|
199
|
+
select { |f| f.name.end_with?("Cargo.toml") }.
|
200
|
+
reject { |f| f.type == "path_dependency" }
|
201
|
+
end
|
202
|
+
|
203
|
+
def lockfile
|
204
|
+
@lockfile ||= get_original_file("Cargo.lock")
|
205
|
+
end
|
206
|
+
|
207
|
+
def version_class
|
208
|
+
Cargo::Version
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
Dependabot::FileParsers.register("cargo", Dependabot::Cargo::FileParser)
|