Skip to content

Commit ba37efc

Browse files
committed
Added Gradle Wrapper support
1 parent b12ec08 commit ba37efc

27 files changed

+8948
-2
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "dependabot/registry_client"
5+
6+
module Dependabot
7+
module Gradle
8+
module Distributions
9+
extend T::Sig
10+
11+
DISTRIBUTIONS_URL = "https://services.gradle.org"
12+
DISTRIBUTION_DEPENDENCY_TYPE = "gradle-distribution"
13+
14+
sig { params(requirements: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Boolean) }
15+
def self.distribution_requirements?(requirements)
16+
requirements.any? do |req|
17+
req.dig(:source, :type) == DISTRIBUTION_DEPENDENCY_TYPE
18+
end
19+
end
20+
end
21+
end
22+
end

gradle/lib/dependabot/gradle/file_fetcher.rb

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class FileFetcher < Dependabot::FileFetchers::Base
2323
SUPPORTED_SETTINGS_FILE_NAMES =
2424
T.let(%w(settings.gradle settings.gradle.kts).freeze, T::Array[String])
2525

26+
SUPPORTED_WRAPPER_PROPERTIES_FILE_PATH =
27+
%w(/gradle/wrapper/gradle-wrapper.properties).freeze
28+
2629
# For now Gradle only supports library .toml files in the main gradle folder
2730
SUPPORTED_VERSION_CATALOG_FILE_PATH =
2831
T.let(%w(/gradle/libs.versions.toml).freeze, T::Array[String])
@@ -66,8 +69,13 @@ def fetch_files
6669

6770
sig { params(root_dir: String).returns(T::Array[DependencyFile]) }
6871
def all_buildfiles_in_build(root_dir)
69-
files = [buildfile(root_dir), settings_file(root_dir), version_catalog_file(root_dir), lockfile(root_dir)]
70-
.compact
72+
files = [
73+
buildfile(root_dir),
74+
settings_file(root_dir),
75+
wrapper_properties_file(root_dir),
76+
version_catalog_file(root_dir),
77+
lockfile(root_dir)
78+
].compact
7179
files += subproject_buildfiles(root_dir)
7280
files += subproject_lockfiles(root_dir)
7381
files += dependency_script_plugins(root_dir)
@@ -139,6 +147,16 @@ def subproject_buildfiles(root_dir)
139147
end
140148
end
141149

150+
sig { params(root_dir: String).returns(T.nilable(DependencyFile)) }
151+
def wrapper_properties_file(root_dir)
152+
return nil unless root_dir == "."
153+
154+
gradle_wrapper_properties_file(root_dir)
155+
rescue Dependabot::DependencyFileNotFound
156+
# Wrapper file is optional for Gradle
157+
nil
158+
end
159+
142160
sig { params(root_dir: String).returns(T.nilable(DependencyFile)) }
143161
def version_catalog_file(root_dir)
144162
return nil unless root_dir == "."
@@ -191,6 +209,11 @@ def buildfile(dir)
191209
file
192210
end
193211

212+
sig { params(dir: String).returns(T.nilable(DependencyFile)) }
213+
def gradle_wrapper_properties_file(dir)
214+
find_first(dir, SUPPORTED_WRAPPER_PROPERTIES_FILE_PATH)
215+
end
216+
194217
sig { params(dir: String).returns(T.nilable(DependencyFile)) }
195218
def gradle_toml_file(dir)
196219
find_first(dir, SUPPORTED_VERSION_CATALOG_FILE_PATH)

gradle/lib/dependabot/gradle/file_parser.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class FileParser < Dependabot::FileParsers::Base # rubocop:disable Metrics/Class
2525
extend T::Sig
2626

2727
require "dependabot/file_parsers/base/dependency_set"
28+
require_relative "file_parser/distributions_finder.rb"
2829
require_relative "file_parser/property_value_finder"
2930

3031
SUPPORTED_BUILD_FILE_NAMES = T.let(%w(build.gradle build.gradle.kts settings.gradle settings.gradle.kts).freeze,
@@ -57,6 +58,9 @@ def parse
5758
script_plugin_files.each do |plugin_file|
5859
dependency_set += buildfile_dependencies(plugin_file)
5960
end
61+
wrapper_properties_file.each do |properties_file|
62+
dependency_set += wrapper_properties_dependencies(properties_file)
63+
end
6064
version_catalog_file.each do |toml_file|
6165
dependency_set += version_catalog_dependencies(toml_file)
6266
end
@@ -114,6 +118,14 @@ def language
114118
end, T.nilable(Dependabot::Gradle::Language))
115119
end
116120

121+
sig { params(properties_file: Dependabot::DependencyFile).returns(DependencySet) }
122+
def wrapper_properties_dependencies(properties_file)
123+
dependency_set = DependencySet.new
124+
dependency = DistributionsFinder.resolve_dependency(properties_file)
125+
dependency_set << dependency if dependency
126+
dependency_set
127+
end
128+
117129
sig { params(toml_file: Dependabot::DependencyFile).returns(DependencySet) }
118130
def version_catalog_dependencies(toml_file)
119131
dependency_set = DependencySet.new
@@ -534,6 +546,14 @@ def buildfiles
534546
)
535547
end
536548

549+
sig { returns(T::Array[Dependabot::DependencyFile]) }
550+
def wrapper_properties_file
551+
@wrapper_properties_file ||= T.let(
552+
dependency_files.select { |f| f.name.end_with?("gradle-wrapper.properties") },
553+
T.nilable(T::Array[Dependabot::DependencyFile])
554+
)
555+
end
556+
537557
sig { returns(T::Array[Dependabot::DependencyFile]) }
538558
def version_catalog_file
539559
@version_catalog_file ||= T.let(
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# typed: strong
2+
# frozen_string_literal: true
3+
4+
require "dependabot/gradle/file_parser"
5+
require "dependabot/gradle/distributions"
6+
7+
module Dependabot
8+
module Gradle
9+
class FileParser
10+
class DistributionsFinder
11+
extend T::Sig
12+
include Dependabot::Gradle::Distributions
13+
14+
DISTRIBUTION_URL_REGEX = %r{^#{Regexp.escape(DISTRIBUTIONS_URL)}/distributions/gradle-(?<version>[\d.]+)-(?<distribution_type>bin|all)\.zip$} # rubocop:disable Layout/LineLength
15+
16+
sig { params(properties_file: DependencyFile).returns(T.nilable(Dependency)) }
17+
def self.resolve_dependency(properties_file)
18+
content = properties_file.content
19+
return nil unless content
20+
21+
distribution_url, checksum = load_properties(content)
22+
match = distribution_url&.match(DISTRIBUTION_URL_REGEX)&.named_captures
23+
return nil unless match
24+
25+
version = match.fetch("version")
26+
27+
requirements = T.let([{
28+
requirement: version,
29+
file: properties_file.name,
30+
source: {
31+
type: DISTRIBUTION_DEPENDENCY_TYPE,
32+
url: distribution_url,
33+
property: "distributionUrl"
34+
},
35+
groups: []
36+
}], T::Array[T::Hash[Symbol, T.untyped]])
37+
38+
if checksum
39+
requirements << {
40+
requirement: checksum,
41+
file: properties_file.name,
42+
source: {
43+
type: DISTRIBUTION_DEPENDENCY_TYPE,
44+
url: "#{distribution_url}.sha256",
45+
property: "distributionSha256Sum"
46+
},
47+
groups: []
48+
}
49+
end
50+
51+
Dependency.new(
52+
name: "gradle-wrapper",
53+
version: version,
54+
requirements: requirements,
55+
package_manager: "gradle"
56+
)
57+
end
58+
59+
sig { params(properties_content: String).returns(T::Array[T.nilable(String)]) }
60+
def self.load_properties(properties_content)
61+
distribution_url = T.let(nil, T.nilable(String))
62+
checksum = T.let(nil, T.nilable(String))
63+
64+
properties_content.lines.each do |line|
65+
(key, value) = line.split("=", 2).map(&:strip)
66+
next unless key && value
67+
68+
case key
69+
when "distributionUrl"
70+
distribution_url = value.gsub("\\:", ":")
71+
when "distributionSha256Sum"
72+
checksum = value
73+
else
74+
next
75+
end
76+
break if distribution_url && checksum
77+
end
78+
79+
[distribution_url, checksum]
80+
end
81+
82+
private_class_method :load_properties
83+
end
84+
end
85+
end
86+
end

gradle/lib/dependabot/gradle/file_updater.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def self.updated_files_regex
2323
[
2424
# Matches build.gradle or build.gradle.kts in root directory
2525
%r{(^|.*/)build\.gradle(\.kts)?$},
26+
# Matches gradle/wrapper/gradle-wrapper.properties in root or any subdirectory
27+
%r{(^|.*/)?gradle/wrapper/gradle-wrapper.properties$},
2628
# Matches gradle/libs.versions.toml in root or any subdirectory
2729
%r{(^|.*/)?gradle/libs\.versions\.toml$},
2830
# Matches settings.gradle or settings.gradle.kts in root or any subdirectory
@@ -202,6 +204,7 @@ def update_version_in_buildfile(dependency, buildfile, previous_req,
202204
end
203205

204206
# rubocop:disable Metrics/AbcSize
207+
# rubocop:disable Metrics/PerceivedComplexity
205208
sig do
206209
params(
207210
dependency: Dependabot::Dependency,
@@ -220,6 +223,10 @@ def original_buildfile_declarations(dependency, requirement)
220223
if dependency.name.include?(":")
221224
dep_parts = dependency.name.split(":")
222225
next false unless line.include?(T.must(dep_parts.first)) || line.include?(T.must(dep_parts.last))
226+
elsif T.let(requirement.fetch(:file), String).end_with?(".properties")
227+
property = T.let(requirement, T::Hash[Symbol, T.nilable(T::Hash[Symbol, T.nilable(String)])])
228+
.dig(:source, :property)
229+
next false unless !property.nil? && line.start_with?(property)
223230
elsif T.let(requirement.fetch(:file), String).end_with?(".toml")
224231
next false unless line.include?(dependency.name)
225232
else
@@ -232,6 +239,7 @@ def original_buildfile_declarations(dependency, requirement)
232239
end
233240
end
234241
# rubocop:enable Metrics/AbcSize
242+
# rubocop:enable Metrics/PerceivedComplexity
235243

236244
sig { params(string: String, buildfile: Dependabot::DependencyFile).returns(String) }
237245
def evaluate_properties(string, buildfile)

gradle/lib/dependabot/gradle/metadata_finder.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "sorbet-runtime"
66

77
require "dependabot/file_fetchers/base"
8+
require "dependabot/gradle/distributions"
89
require "dependabot/gradle/file_fetcher"
910
require "dependabot/gradle/file_parser/repositories_finder"
1011
require "dependabot/maven/utils/auth_headers_finder"
@@ -25,6 +26,8 @@ class MetadataFinder < Dependabot::MetadataFinders::Base
2526

2627
sig { override.returns(T.nilable(Dependabot::Source)) }
2728
def look_up_source
29+
return nil if Distributions.distribution_requirements?(dependency.requirements)
30+
2831
tmp_source = look_up_source_in_pom(dependency_pom_file)
2932
return tmp_source if tmp_source
3033

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# typed: strong
2+
# frozen_string_literal: true
3+
4+
require "dependabot/gradle/distributions"
5+
require "dependabot/gradle/version"
6+
7+
module Dependabot
8+
module Gradle
9+
class UpdateChecker
10+
class DistributionsFinder
11+
extend T::Sig
12+
include Dependabot::Gradle::Distributions
13+
14+
@available_versions = T.let([], T::Array[T::Hash[String, T.untyped]])
15+
@distributions_checksums = T.let({}, T::Hash[String, T::Array[String]])
16+
17+
sig { returns(T.any(T::Array[T::Hash[String, T.untyped]], T::Array[T::Hash[Symbol, T.untyped]])) }
18+
def self.available_versions
19+
return @available_versions if @available_versions.any?
20+
21+
response = Dependabot::RegistryClient.get(url: "#{DISTRIBUTIONS_URL}/versions/all")
22+
versions = T.let(JSON.parse(T.let(response.body, String),
23+
object_class: OpenStruct), T::Array[OpenStruct])
24+
@available_versions += versions
25+
.select { |v| release_version?(version: v) }
26+
.map { |v| T.let(v["version"], String) }
27+
.uniq
28+
.select { |v| Gradle::Version.correct?(v) }
29+
.map { |v| Gradle::Version.new(v) }
30+
.sort
31+
.map { |version| { version: version, source_url: DISTRIBUTIONS_URL } }
32+
end
33+
34+
sig { params(version: OpenStruct).returns(T::Boolean) }
35+
def self.release_version?(version:)
36+
T.let(version[:broken], T::Boolean) == false &&
37+
T.let(version[:snapshot], T::Boolean) == false &&
38+
T.let(version[:rcFor], String) == "" &&
39+
T.let(version[:milestoneFor], String) == "" &&
40+
/.*-(rc|milestone)-.*/.match?(T.let(version[:version], String)) == false
41+
end
42+
43+
sig { params(distribution_url: String).returns(T.nilable(T::Array[String])) }
44+
def self.resolve_checksum(distribution_url)
45+
cached = @distributions_checksums[distribution_url]
46+
return cached if cached
47+
48+
checksum_url = "#{distribution_url}.sha256"
49+
checksum = T.let(Dependabot::RegistryClient.get(url: checksum_url).body, String).strip
50+
return nil unless checksum.match?(/\A[a-f0-9]{64}\z/)
51+
52+
@distributions_checksums[distribution_url] = [checksum_url, checksum]
53+
end
54+
55+
private_class_method :release_version?
56+
end
57+
end
58+
end
59+
end

gradle/lib/dependabot/gradle/update_checker/requirements_updater.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require "sorbet-runtime"
1010

1111
require "dependabot/requirements_updater/base"
12+
require "dependabot/gradle/distributions"
1213
require "dependabot/gradle/update_checker"
1314
require "dependabot/gradle/version"
1415
require "dependabot/gradle/requirement"
@@ -42,11 +43,13 @@ def initialize(requirements:, latest_version:, source_url:,
4243
return unless latest_version
4344

4445
@latest_version = T.let(version_class.new(latest_version), Version)
46+
@is_distribution = T.let(Distributions.distribution_requirements?(requirements), T::Boolean)
4547
end
4648

4749
sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
4850
def updated_requirements
4951
return requirements unless latest_version
52+
return updated_distribution_requirements if @is_distribution
5053

5154
# NOTE: Order is important here. The FileUpdater needs the updated
5255
# requirement at index `i` to correspond to the previous requirement
@@ -110,6 +113,31 @@ def update_dynamic_requirement(req_string)
110113
end
111114
end
112115

116+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
117+
def updated_distribution_requirements
118+
req_version = T.must(requirements[0])
119+
120+
requirement = req_version[:requirement]
121+
updated_requirement = update_exact_requirement(requirement)
122+
123+
distribution_url = req_version[:source][:url]
124+
updated_distribution_url = distribution_url.gsub(requirement, updated_requirement)
125+
126+
req_version = req_version.merge(
127+
requirement: updated_requirement,
128+
source: req_version[:source].merge(url: updated_distribution_url)
129+
)
130+
return [req_version] unless requirements.size > 1
131+
132+
req_checksum = T.must(requirements[1])
133+
checksum_url, checksum = DistributionsFinder.resolve_checksum(updated_distribution_url)
134+
req_checksum = req_checksum.merge(
135+
requirement: checksum,
136+
source: req_checksum[:source].merge(url: checksum_url)
137+
)
138+
[req_version, req_checksum]
139+
end
140+
113141
sig { override.returns(T::Class[Version]) }
114142
def version_class
115143
Gradle::Version

0 commit comments

Comments
 (0)