dependabot-core 0.80.1 → 0.81.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,213 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "toml-rb"
4
-
5
- require "dependabot/dependency"
6
- require "dependabot/file_parsers/base"
7
- require "dependabot/utils/rust/requirement"
8
- require "dependabot/utils/rust/version"
9
- require "dependabot/errors"
10
-
11
- # Relevant Cargo docs can be found at:
12
- # - https://doc.rust-lang.org/cargo/reference/manifest.html
13
- # - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
14
- module Dependabot
15
- module FileParsers
16
- module Rust
17
- class Cargo < 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 = Utils::Rust::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
- Utils::Rust::Version
209
- end
210
- end
211
- end
212
- end
213
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "toml-rb"
4
- require "dependabot/git_commit_checker"
5
- require "dependabot/file_updaters/base"
6
- require "dependabot/file_parsers/rust/cargo"
7
- require "dependabot/shared_helpers"
8
-
9
- module Dependabot
10
- module FileUpdaters
11
- module Rust
12
- class Cargo < Dependabot::FileUpdaters::Base
13
- require_relative "cargo/manifest_updater"
14
- require_relative "cargo/lockfile_updater"
15
-
16
- def self.updated_files_regex
17
- [
18
- /^Cargo\.toml$/,
19
- /^Cargo\.lock$/
20
- ]
21
- end
22
-
23
- def updated_dependency_files
24
- # Returns an array of updated files. Only files that have been updated
25
- # should be returned.
26
- updated_files = []
27
-
28
- manifest_files.each do |file|
29
- next unless file_changed?(file)
30
-
31
- updated_files <<
32
- updated_file(
33
- file: file,
34
- content: updated_manifest_content(file)
35
- )
36
- end
37
-
38
- if lockfile && updated_lockfile_content != lockfile.content
39
- updated_files <<
40
- updated_file(file: lockfile, content: updated_lockfile_content)
41
- end
42
-
43
- raise "No files changed!" if updated_files.empty?
44
-
45
- updated_files
46
- end
47
-
48
- private
49
-
50
- def check_required_files
51
- raise "No Cargo.toml!" unless get_original_file("Cargo.toml")
52
- end
53
-
54
- def updated_manifest_content(file)
55
- ManifestUpdater.new(
56
- dependencies: dependencies,
57
- manifest: file
58
- ).updated_manifest_content
59
- end
60
-
61
- def updated_lockfile_content
62
- @updated_lockfile_content ||=
63
- LockfileUpdater.new(
64
- dependencies: dependencies,
65
- dependency_files: dependency_files,
66
- credentials: credentials
67
- ).updated_lockfile_content
68
- end
69
-
70
- def manifest_files
71
- @manifest_files ||=
72
- dependency_files.
73
- select { |f| f.name.end_with?("Cargo.toml") }.
74
- reject { |f| f.type == "path_dependency" }
75
- end
76
-
77
- def lockfile
78
- @lockfile ||= get_original_file("Cargo.lock")
79
- end
80
- end
81
- end
82
- end
83
- end
@@ -1,251 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "toml-rb"
4
- require "dependabot/git_commit_checker"
5
- require "dependabot/file_updaters/rust/cargo"
6
- require "dependabot/file_updaters/rust/cargo/manifest_updater"
7
- require "dependabot/file_parsers/rust/cargo"
8
- require "dependabot/shared_helpers"
9
-
10
- module Dependabot
11
- module FileUpdaters
12
- module Rust
13
- class Cargo
14
- class LockfileUpdater
15
- def initialize(dependencies:, dependency_files:, credentials:)
16
- @dependencies = dependencies
17
- @dependency_files = dependency_files
18
- @credentials = credentials
19
- end
20
-
21
- def updated_lockfile_content
22
- base_directory = dependency_files.first.directory
23
- SharedHelpers.in_a_temporary_directory(base_directory) do
24
- write_temporary_dependency_files
25
-
26
- SharedHelpers.with_git_configured(credentials: credentials) do
27
- # Shell out to Cargo, which handles everything for us, and does
28
- # so without doing an install (so it's fast).
29
- command = "cargo update -p #{dependency_spec}"
30
- run_shell_command(command)
31
- end
32
-
33
- updated_lockfile = File.read("Cargo.lock")
34
- updated_lockfile = post_process_lockfile(updated_lockfile)
35
-
36
- if updated_lockfile.include?(desired_lockfile_content)
37
- next updated_lockfile
38
- end
39
-
40
- raise "Failed to update #{dependency.name}!"
41
- end
42
- rescue Dependabot::SharedHelpers::HelperSubprocessFailed => error
43
- handle_cargo_error(error)
44
- end
45
-
46
- private
47
-
48
- attr_reader :dependencies, :dependency_files, :credentials
49
-
50
- # Currently, there will only be a single updated dependency
51
- def dependency
52
- dependencies.first
53
- end
54
-
55
- def handle_cargo_error(error)
56
- raise unless error.message.include?("no matching version")
57
- raise if error.message.include?("`#{dependency.name}`")
58
-
59
- raise Dependabot::DependencyFileNotResolvable, error.message
60
- end
61
-
62
- def dependency_spec
63
- spec = dependency.name
64
-
65
- if git_dependency?
66
- spec += ":#{git_previous_version}" if git_previous_version
67
- elsif dependency.previous_version
68
- spec += ":#{dependency.previous_version}"
69
- end
70
-
71
- spec
72
- end
73
-
74
- def git_previous_version
75
- TomlRB.parse(lockfile.content).
76
- fetch("package", []).
77
- select { |p| p["name"] == dependency.name }.
78
- find { |p| p["source"].end_with?(dependency.previous_version) }.
79
- fetch("version")
80
- end
81
-
82
- def desired_lockfile_content
83
- return dependency.version if git_dependency?
84
-
85
- %(name = "#{dependency.name}"\nversion = "#{dependency.version}")
86
- end
87
-
88
- def run_shell_command(command)
89
- raw_response = nil
90
- IO.popen(command, err: %i(child out)) do |process|
91
- raw_response = process.read
92
- end
93
-
94
- # Raise an error with the output from the shell session if Cargo
95
- # returns a non-zero status
96
- return if $CHILD_STATUS.success?
97
-
98
- raise SharedHelpers::HelperSubprocessFailed.new(
99
- raw_response,
100
- command
101
- )
102
- end
103
-
104
- def write_temporary_dependency_files
105
- write_temporary_manifest_files
106
- write_temporary_path_dependency_files
107
-
108
- File.write(lockfile.name, lockfile.content)
109
- File.write(toolchain.name, toolchain.content) if toolchain
110
- end
111
-
112
- def write_temporary_manifest_files
113
- manifest_files.each do |file|
114
- path = file.name
115
- dir = Pathname.new(path).dirname
116
- FileUtils.mkdir_p(Pathname.new(path).dirname)
117
- File.write(file.name, prepared_manifest_content(file))
118
-
119
- FileUtils.mkdir_p(File.join(dir, "src"))
120
- File.write(File.join(dir, "src/lib.rs"), dummy_app_content)
121
- File.write(File.join(dir, "src/main.rs"), dummy_app_content)
122
- end
123
- end
124
-
125
- def write_temporary_path_dependency_files
126
- path_dependency_files.each do |file|
127
- path = file.name
128
- dir = Pathname.new(path).dirname
129
- FileUtils.mkdir_p(Pathname.new(path).dirname)
130
- File.write(file.name, prepared_path_dependency_content(file))
131
-
132
- FileUtils.mkdir_p(File.join(dir, "src"))
133
- File.write(File.join(dir, "src/lib.rs"), dummy_app_content)
134
- File.write(File.join(dir, "src/main.rs"), dummy_app_content)
135
- end
136
- end
137
-
138
- def prepared_manifest_content(file)
139
- content = updated_manifest_content(file)
140
- content = pin_version(content) unless git_dependency?
141
- content = replace_ssh_urls(content)
142
- content
143
- end
144
-
145
- def prepared_path_dependency_content(file)
146
- content = file.content.dup
147
- content = replace_ssh_urls(content)
148
- content
149
- end
150
-
151
- def updated_manifest_content(file)
152
- ManifestUpdater.new(
153
- dependencies: dependencies,
154
- manifest: file
155
- ).updated_manifest_content
156
- end
157
-
158
- def pin_version(content)
159
- parsed_manifest = TomlRB.parse(content)
160
-
161
- FileParsers::Rust::Cargo::DEPENDENCY_TYPES.each do |type|
162
- next unless (req = parsed_manifest.dig(type, dependency.name))
163
-
164
- updated_req = "=#{dependency.version}"
165
-
166
- if req.is_a?(Hash)
167
- parsed_manifest[type][dependency.name]["version"] = updated_req
168
- else
169
- parsed_manifest[type][dependency.name] = updated_req
170
- end
171
- end
172
-
173
- TomlRB.dump(parsed_manifest)
174
- end
175
-
176
- def replace_ssh_urls(content)
177
- git_ssh_requirements_to_swap.each do |ssh_url, https_url|
178
- content = content.gsub(ssh_url, https_url)
179
- end
180
- content
181
- end
182
-
183
- def post_process_lockfile(content)
184
- git_ssh_requirements_to_swap.each do |ssh_url, https_url|
185
- content = content.gsub(https_url, ssh_url)
186
- end
187
-
188
- content
189
- end
190
-
191
- def git_ssh_requirements_to_swap
192
- if @git_ssh_requirements_to_swap
193
- return @git_ssh_requirements_to_swap
194
- end
195
-
196
- @git_ssh_requirements_to_swap = {}
197
-
198
- [*manifest_files, *path_dependency_files].each do |manifest|
199
- parsed_manifest = TomlRB.parse(manifest.content)
200
-
201
- FileParsers::Rust::Cargo::DEPENDENCY_TYPES.each do |type|
202
- (parsed_manifest[type] || {}).each do |_, details|
203
- next unless details.is_a?(Hash)
204
- next unless details["git"]&.match?(%r{ssh://git@(.*?)/})
205
-
206
- @git_ssh_requirements_to_swap[details["git"]] =
207
- details["git"].gsub(%r{ssh://git@(.*?)/}, 'https://\1/')
208
- end
209
- end
210
- end
211
-
212
- @git_ssh_requirements_to_swap
213
- end
214
-
215
- def dummy_app_content
216
- %{fn main() {\nprintln!("Hello, world!");\n}}
217
- end
218
-
219
- def git_dependency?
220
- GitCommitChecker.new(
221
- dependency: dependency,
222
- credentials: credentials
223
- ).git_dependency?
224
- end
225
-
226
- def manifest_files
227
- @manifest_files ||=
228
- dependency_files.
229
- select { |f| f.name.end_with?("Cargo.toml") }.
230
- reject { |f| f.type == "path_dependency" }
231
- end
232
-
233
- def path_dependency_files
234
- @path_dependency_files ||=
235
- dependency_files.
236
- select { |f| f.type == "path_dependency" }
237
- end
238
-
239
- def lockfile
240
- @lockfile ||= dependency_files.find { |f| f.name == "Cargo.lock" }
241
- end
242
-
243
- def toolchain
244
- @toolchain ||=
245
- dependency_files.find { |f| f.name == "rust-toolchain" }
246
- end
247
- end
248
- end
249
- end
250
- end
251
- end