Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions gradle/lib/dependabot/gradle/distributions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# typed: strict
# frozen_string_literal: true

module Dependabot
module Gradle
module Distributions
extend T::Sig

DISTRIBUTION_DEPENDENCY_TYPE = "gradle-distribution"

sig { params(requirements: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Boolean) }
def self.distribution_requirements?(requirements)
requirements.any? do |req|
req.dig(:source, :type) == DISTRIBUTION_DEPENDENCY_TYPE
end
end
end
end
end
27 changes: 27 additions & 0 deletions gradle/lib/dependabot/gradle/file_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ class FileFetcher < Dependabot::FileFetchers::Base
SUPPORTED_SETTINGS_FILE_NAMES =
T.let(%w(settings.gradle settings.gradle.kts).freeze, T::Array[String])

SUPPORTED_WRAPPER_FILES_PATH = %w(
gradlew
gradlew.bat
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties
).freeze

# For now Gradle only supports library .toml files in the main gradle folder
SUPPORTED_VERSION_CATALOG_FILE_PATH =
T.let(%w(/gradle/libs.versions.toml).freeze, T::Array[String])
Expand Down Expand Up @@ -76,6 +83,7 @@ def fetch_files
def all_buildfiles_in_build(root_dir)
files = [buildfile(root_dir), settings_file(root_dir), version_catalog_file(root_dir), lockfile(root_dir)]
.compact
files += wrapper_files(root_dir)
files += subproject_buildfiles(root_dir)
files += subproject_lockfiles(root_dir)
files += dependency_script_plugins(root_dir)
Expand Down Expand Up @@ -172,6 +180,25 @@ def subproject_buildfiles(root_dir)
end
end

sig { params(dir: String).returns(T::Array[DependencyFile]) }
def wrapper_files(dir)
return [] unless Experiments.enabled?(:gradle_wrapper_updater)

SUPPORTED_WRAPPER_FILES_PATH.filter_map do |filename|
file = fetch_file_if_present(File.join(dir, filename))
next unless file

if file.name.end_with?(".jar")
file.content = Base64.encode64(T.must(file.content)) if file.content
file.content_encoding = DependencyFile::ContentEncoding::BASE64
end
file
rescue Dependabot::DependencyFileNotFound
# Gradle itself doesn't worry about missing subprojects, so we don't
nil
end
end

sig { params(root_dir: String).returns(T.nilable(DependencyFile)) }
def version_catalog_file(root_dir)
return nil unless root_dir == "."
Expand Down
20 changes: 20 additions & 0 deletions gradle/lib/dependabot/gradle/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class FileParser < Dependabot::FileParsers::Base # rubocop:disable Metrics/Class
extend T::Sig

require "dependabot/file_parsers/base/dependency_set"
require_relative "file_parser/distributions_finder"
require_relative "file_parser/property_value_finder"

SUPPORTED_BUILD_FILE_NAMES = T.let(
Expand Down Expand Up @@ -59,6 +60,9 @@ def parse
script_plugin_files.each do |plugin_file|
dependency_set += buildfile_dependencies(plugin_file)
end
wrapper_properties_file.each do |properties_file|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing feature flag check?

dependency_set += wrapper_properties_dependencies(properties_file)
end
version_catalog_file.each do |toml_file|
dependency_set += version_catalog_dependencies(toml_file)
end
Expand Down Expand Up @@ -119,6 +123,14 @@ def language
)
end

sig { params(properties_file: Dependabot::DependencyFile).returns(DependencySet) }
def wrapper_properties_dependencies(properties_file)
dependency_set = DependencySet.new
dependency = DistributionsFinder.resolve_dependency(properties_file)
dependency_set << dependency if dependency
dependency_set
end

sig { params(toml_file: Dependabot::DependencyFile).returns(DependencySet) }
def version_catalog_dependencies(toml_file)
dependency_set = DependencySet.new
Expand Down Expand Up @@ -544,6 +556,14 @@ def buildfiles
)
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def wrapper_properties_file
@wrapper_properties_file ||= T.let(
dependency_files.select { |f| f.name.end_with?("gradle-wrapper.properties") },
T.nilable(T::Array[Dependabot::DependencyFile])
)
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def version_catalog_file
@version_catalog_file ||= T.let(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# typed: strong
# frozen_string_literal: true

require "dependabot/gradle/file_parser"
require "dependabot/gradle/distributions"
require "sorbet-runtime"

module Dependabot
module Gradle
class FileParser
class DistributionsFinder
extend T::Sig

DISTRIBUTION_URL_REGEX =
/.*?(?<version>(\d+(?:\.\d+){1,3}(?:-(?!bin|all)\w++)*(?:\+\w++)*))(?:-bin|-all)?.*?/

sig { params(properties_file: DependencyFile).returns(T.nilable(Dependency)) }
def self.resolve_dependency(properties_file) # rubocop:disable Metrics/MethodLength
content = properties_file.content
return nil unless content

distribution_url, checksum = load_properties(content)
match = distribution_url&.match(DISTRIBUTION_URL_REGEX)&.named_captures
return nil unless match

version = match.fetch("version")

requirements = T.let(
[{
requirement: version,
file: properties_file.name,
source: {
type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
url: distribution_url,
property: "distributionUrl"
},
groups: []
}],
T::Array[T::Hash[Symbol, T.untyped]]
)

if checksum
requirements << {
requirement: checksum,
file: properties_file.name,
source: {
type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
url: "#{distribution_url}.sha256",
property: "distributionSha256Sum"
},
groups: []
}
end

Dependency.new(
name: "gradle-wrapper",
version: version,
requirements: requirements,
package_manager: "gradle"
)
end

sig { params(properties_content: String).returns(T::Array[T.nilable(String)]) }
def self.load_properties(properties_content)
distribution_url = T.let(nil, T.nilable(String))
checksum = T.let(nil, T.nilable(String))

properties_content.lines.each do |line|
(key, value) = line.split("=", 2).map(&:strip)
next unless key && value

case key
when "distributionUrl"
distribution_url = value.gsub("\\:", ":")
when "distributionSha256Sum"
checksum = value
else
next
end
break if distribution_url && checksum
end

[distribution_url, checksum]
end

private_class_method :load_properties
end
end
end
end
43 changes: 36 additions & 7 deletions gradle/lib/dependabot/gradle/file_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class FileUpdater < Dependabot::FileUpdaters::Base
require_relative "file_updater/dependency_set_updater"
require_relative "file_updater/property_value_updater"
require_relative "file_updater/lockfile_updater"
require_relative "file_updater/wrapper_updater"

SUPPORTED_BUILD_FILE_NAMES = %w(build.gradle build.gradle.kts gradle.lockfile).freeze

Expand Down Expand Up @@ -64,6 +65,10 @@ def original_file
def update_buildfiles_for_dependency(buildfiles:, dependency:)
files = buildfiles.dup

# dependencies may have multiple requirements targeting the same file or build dir
# we keep the last one by path to later run its native helpers
buildfiles_processed = T.let({}, T::Hash[String, Dependabot::DependencyFile])

# The UpdateChecker ensures the order of requirements is preserved
# when updating, so we can zip them together in new/old pairs.
reqs = dependency.requirements.zip(T.must(dependency.previous_requirements))
Expand Down Expand Up @@ -93,16 +98,20 @@ def update_buildfiles_for_dependency(buildfiles:, dependency:)
files[T.must(files.index(buildfile))] = update_version_in_buildfile(dependency, buildfile, old_req, new_req)
end

next unless Dependabot::Experiments.enabled?(:gradle_lockfile_updater)
buildfiles_processed[buildfile.name] = buildfile
end

# runs native updaters (e.g. wrapper, lockfile) on relevant build files updated
updaters = native_updaters(files, dependency)
buildfiles_processed.each do |_, buildfile|
updated_files = updaters.flat_map { |updater| updater.update_files(buildfile) }

lockfile_updater = LockfileUpdater.new(dependency_files: files)
lockfiles = lockfile_updater.update_lockfiles(buildfile)
lockfiles.each do |lockfile|
existing_file = files.find { |f| f.name == lockfile.name && f.directory == lockfile.directory }
updated_files.each do |file|
existing_file = files.find { |f| f.name == file.name && f.directory == file.directory }
if existing_file.nil?
files << lockfile
files << file
else
files[T.must(files.index(existing_file))] = lockfile
files[T.must(files.index(existing_file))] = file
end
end
end
Expand All @@ -113,6 +122,20 @@ def update_buildfiles_for_dependency(buildfiles:, dependency:)
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize

sig do
params(
files: T::Array[Dependabot::DependencyFile],
dependency: Dependabot::Dependency
).returns(T::Array[GradleUpdaterBase])
end
def native_updaters(files, dependency)
updaters = T.let([], T::Array[GradleUpdaterBase])
updaters << LockfileUpdater.new(dependency_files: files) if Experiments.enabled?(:gradle_lockfile_updater)
updaters << WrapperUpdater.new(dependency_files: files, dependency: dependency) if
Experiments.enabled?(:gradle_wrapper_updater)
updaters
end

sig do
params(
buildfiles: T::Array[Dependabot::DependencyFile],
Expand Down Expand Up @@ -191,6 +214,7 @@ def update_version_in_buildfile(
end

# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/PerceivedComplexity
sig do
params(
dependency: Dependabot::Dependency,
Expand All @@ -209,6 +233,10 @@ def original_buildfile_declarations(dependency, requirement)
if dependency.name.include?(":")
dep_parts = dependency.name.split(":")
next false unless line.include?(T.must(dep_parts.first)) || line.include?(T.must(dep_parts.last))
elsif T.let(requirement.fetch(:file), String).end_with?(".properties")
property = T.let(requirement, T::Hash[Symbol, T.nilable(T::Hash[Symbol, T.nilable(String)])])
.dig(:source, :property)
next false unless !property.nil? && line.start_with?(property)
elsif T.let(requirement.fetch(:file), String).end_with?(".toml")
next false unless line.include?(dependency.name)
else
Expand All @@ -221,6 +249,7 @@ def original_buildfile_declarations(dependency, requirement)
end
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/PerceivedComplexity

sig { params(string: String, buildfile: Dependabot::DependencyFile).returns(String) }
def evaluate_properties(string, buildfile)
Expand Down
Loading
Loading