class Dependabot::Cargo::UpdateChecker::VersionResolver
Constants
- BRANCH_NOT_FOUND_REGEX
- OBJECT_PATTERN
- REF_NOT_FOUND_REGEX
- REVSPEC_PATTERN
- UNABLE_TO_UPDATE
Attributes
credentials[R]
dependency[R]
original_dependency_files[R]
prepared_dependency_files[R]
Public Class Methods
new(dependency:, credentials:, original_dependency_files:, prepared_dependency_files:)
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 23 def initialize(dependency:, credentials:, original_dependency_files:, prepared_dependency_files:) @dependency = dependency @prepared_dependency_files = prepared_dependency_files @original_dependency_files = original_dependency_files @credentials = credentials end
Public Instance Methods
latest_resolvable_version()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 31 def latest_resolvable_version @latest_resolvable_version ||= fetch_latest_resolvable_version end
Private Instance Methods
better_specification_needed?(error)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/AbcSize
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 86 def better_specification_needed?(error) return false if @custom_specification return false unless error.message.match?(/specification .* is ambigu/) spec_options = error.message.gsub(/.*following:\n/m, ""). lines.map(&:strip) ver = if git_dependency? && git_dependency_version git_dependency_version else dependency.version end if spec_options.count { |s| s.end_with?(ver) } == 1 @custom_specification = spec_options.find { |s| s.end_with?(ver) } return true elsif spec_options.count { |s| s.end_with?(ver) } > 1 spec_options.select! { |s| s.end_with?(ver) } end if git_dependency? && git_source_url && spec_options.count { |s| s.include?(git_source_url) } >= 1 spec_options.select! { |s| s.include?(git_source_url) } end @custom_specification = spec_options.first true end
check_rust_workspace_root()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 160 def check_rust_workspace_root cargo_toml = original_dependency_files. select { |f| f.name.end_with?("../Cargo.toml") }. max_by { |f| f.name.length } return unless TomlRB.parse(cargo_toml.content)["workspace"] msg = "This project is part of a Rust workspace but is not the "\ "workspace root."\ if cargo_toml.directory != "/" msg += "Please update your settings so Dependabot points at the "\ "workspace root instead of #{cargo_toml.directory}." end raise Dependabot::DependencyFileNotResolvable, msg end
dependency_spec()
click to toggle source
rubocop:enable Metrics/AbcSize rubocop:enable Metrics/CyclomaticComplexity rubocop:enable Metrics/PerceivedComplexity
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 118 def dependency_spec return @custom_specification if @custom_specification spec = dependency.name if git_dependency? spec += ":#{git_dependency_version}" if git_dependency_version elsif dependency.version spec += ":#{dependency.version}" spec = "https://github.com/rust-lang/crates.io-index#" + spec end spec end
dummy_app_content()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 365 def dummy_app_content %{fn main() {\nprintln!("Hello, world!");\n}} end
fetch_latest_resolvable_version()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 40 def fetch_latest_resolvable_version base_directory = prepared_dependency_files.first.directory SharedHelpers.in_a_temporary_directory(base_directory) do write_temporary_dependency_files SharedHelpers.with_git_configured(credentials: credentials) do # Shell out to Cargo, which handles everything for us, and does # so without doing an install (so it's fast). run_cargo_command("cargo update -p #{dependency_spec} --verbose") end updated_version = fetch_version_from_new_lockfile return if updated_version.nil? return updated_version if git_dependency? version_class.new(updated_version) end rescue SharedHelpers::HelperSubprocessFailed => e retry if better_specification_needed?(e) handle_cargo_errors(e) end
fetch_version_from_new_lockfile()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 63 def fetch_version_from_new_lockfile check_rust_workspace_root unless File.exist?("Cargo.lock") lockfile_content = File.read("Cargo.lock") versions = TomlRB.parse(lockfile_content).fetch("package"). select { |p| p["name"] == dependency.name } updated_version = if dependency.top_level? versions.max_by { |p| version_class.new(p.fetch("version")) } else versions.min_by { |p| version_class.new(p.fetch("version")) } end if git_dependency? updated_version.fetch("source").split("#").last else updated_version.fetch("version") end end
git_dependency?()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 407 def git_dependency? GitCommitChecker.new( dependency: dependency, credentials: credentials ).git_dependency? end
git_dependency_version()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 349 def git_dependency_version return unless lockfile TomlRB.parse(lockfile.content). fetch("package", []). select { |p| p["name"] == dependency.name }. find { |p| p["source"].end_with?(dependency.version) }. fetch("version") end
git_source_url()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 359 def git_source_url dependency.requirements. find { |r| r.dig(:source, :type) == "git" }&. dig(:source, :url) end
handle_cargo_errors(error)
click to toggle source
rubocop:disable Metrics/AbcSize rubocop:disable Metrics/PerceivedComplexity rubocop:disable Metrics/MethodLength
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 179 def handle_cargo_errors(error) if error.message.include?("does not have these features") # TODO: Ideally we should update the declaration not to ask # for the specified features return nil end if error.message.include?("authenticate when downloading repo") || error.message.include?("HTTP 200 response: got 401") # Check all dependencies for reachability (so that we raise a # consistent error) urls = unreachable_git_urls if urls.none? url = error.message.match(UNABLE_TO_UPDATE). named_captures.fetch("url").split(/[#?]/).first raise if reachable_git_urls.include?(url) urls << url end raise Dependabot::GitDependenciesNotReachable, urls end if error.message.match?(BRANCH_NOT_FOUND_REGEX) dependency_url = error.message.match(BRANCH_NOT_FOUND_REGEX). named_captures.fetch("url").split(/[#?]/).first raise Dependabot::GitDependencyReferenceNotFound, dependency_url end if error.message.match?(REF_NOT_FOUND_REGEX) dependency_url = error.message.match(REF_NOT_FOUND_REGEX). named_captures.fetch("url").split(/[#?]/).first raise Dependabot::GitDependencyReferenceNotFound, dependency_url end if workspace_native_library_update_error?(error.message) # This happens when we're updating one part of a workspace which # triggers an update of a subdependency that uses a native library, # whilst leaving another part of the workspace using an older # version. Ideally we would prevent the subdependency update. return nil end if git_dependency? && error.message.include?("no matching package") # This happens when updating a git dependency whose version has # changed from a release to a pre-release version return nil end if error.message.include?("all possible versions conflict") # This happens when a top-level requirement locks us to an old # patch release of a dependency that is a sub-dep of what we're # updating. It's (probably) a Cargo bug. return nil end raise Dependabot::DependencyFileNotResolvable, error.message if resolvability_error?(error.message) raise error end
lockfile()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 397 def lockfile @lockfile ||= prepared_dependency_files. find { |f| f.name == "Cargo.lock" } end
original_manifest_files()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 391 def original_manifest_files @original_manifest_files ||= original_dependency_files. select { |f| f.name.end_with?("Cargo.toml") } end
original_requirements_resolvable?()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 294 def original_requirements_resolvable? base_directory = original_dependency_files.first.directory SharedHelpers.in_a_temporary_directory(base_directory) do write_temporary_dependency_files(prepared: false) SharedHelpers.with_git_configured(credentials: credentials) do run_cargo_command("cargo update -p #{dependency_spec} --verbose") end end true rescue SharedHelpers::HelperSubprocessFailed => e raise unless e.message.include?("no matching version") || e.message.include?("failed to select a version") || e.message.include?("no matching package named") || e.message.include?("failed to parse manifest") || e.message.include?("failed to update submodule") false end
prepared_manifest_files()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 385 def prepared_manifest_files @prepared_manifest_files ||= prepared_dependency_files. select { |f| f.name.end_with?("Cargo.toml") } end
reachable_git_urls()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 277 def reachable_git_urls return @reachable_git_urls if defined?(@reachable_git_urls) unreachable_git_urls @reachable_git_urls end
resolvability_error?(message)
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 284 def resolvability_error?(message) return true if message.include?("failed to parse lock") return true if message.include?("believes it's in a workspace") return true if message.include?("wasn't a root") return true if message.include?("requires a nightly version") return true if message.match?(/feature `[^\`]+` is required/) !original_requirements_resolvable? end
run_cargo_command(command)
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 133 def run_cargo_command(command) start = Time.now command = SharedHelpers.escape_command(command) stdout, process = Open3.capture2e(command) time_taken = Time.now - start # Raise an error with the output from the shell session if Cargo # returns a non-zero status return if process.success? raise SharedHelpers::HelperSubprocessFailed.new( message: stdout, error_context: { command: command, time_taken: time_taken, process_exit_value: process.to_s } ) end
sanitized_manifest_content(content)
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 369 def sanitized_manifest_content(content) object = TomlRB.parse(content) object.delete("bin") object["package"].delete("default-run") if object.dig("package", "default-run") package_name = object.dig("package", "name") return TomlRB.dump(object) unless package_name&.match?(/[\{\}]/) raise "Sanitizing name for pkg with lockfile. Investigate!" if lockfile object["package"]["name"] = "sanitized" TomlRB.dump(object) end
toolchain()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 402 def toolchain @toolchain ||= prepared_dependency_files. find { |f| f.name == "rust-toolchain" } end
unreachable_git_urls()
click to toggle source
rubocop:enable Metrics/AbcSize rubocop:enable Metrics/PerceivedComplexity rubocop:enable Metrics/MethodLength
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 246 def unreachable_git_urls return @unreachable_git_urls if defined?(@unreachable_git_urls) @unreachable_git_urls = [] @reachable_git_urls = [] dependencies = FileParser.new( dependency_files: original_dependency_files, source: nil ).parse dependencies.each do |dep| checker = GitCommitChecker.new( dependency: dep, credentials: credentials ) next unless checker.git_dependency? url = dep.requirements.find { |r| r.dig(:source, :type) == "git" }. fetch(:source).fetch(:url) if checker.git_repo_reachable? @reachable_git_urls << url else @unreachable_git_urls << url end end @unreachable_git_urls end
version_class()
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 421 def version_class Cargo::Version end
virtual_manifest?(file)
click to toggle source
When the package table is not present in a workspace manifest, it is called a virtual manifest: doc.rust-lang.org/cargo/reference/ manifest.html#virtual-manifest
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 417 def virtual_manifest?(file) !file.content.include?("[package]") end
workspace_native_library_update_error?(message)
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 315 def workspace_native_library_update_error?(message) return unless message.include?("native library") library_count = prepared_manifest_files.count do |file| package_name = TomlRB.parse(file.content).dig("package", "name") next false unless package_name message.include?("depended on by `#{package_name} ") end library_count >= 2 end
write_manifest_files(prepared: true)
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 328 def write_manifest_files(prepared: true) manifest_files = if prepared then prepared_manifest_files else original_manifest_files end manifest_files.each do |file| path = file.name dir = Pathname.new(path).dirname FileUtils.mkdir_p(dir) File.write(file.name, sanitized_manifest_content(file.content)) next if virtual_manifest?(file) File.write(File.join(dir, "build.rs"), dummy_app_content) FileUtils.mkdir_p(File.join(dir, "src")) File.write(File.join(dir, "src/lib.rs"), dummy_app_content) File.write(File.join(dir, "src/main.rs"), dummy_app_content) end end
write_temporary_dependency_files(prepared: true)
click to toggle source
# File lib/dependabot/cargo/update_checker/version_resolver.rb, line 153 def write_temporary_dependency_files(prepared: true) write_manifest_files(prepared: prepared) File.write(lockfile.name, lockfile.content) if lockfile File.write(toolchain.name, toolchain.content) if toolchain end