Skip to content

Commit 96d9571

Browse files
committed
Updating Gradle binaries support
1 parent fa5522f commit 96d9571

22 files changed

Lines changed: 415 additions & 92 deletions

.github/workflows/smoke.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ concurrency:
1515
env:
1616
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1717
SMOKE_TEST_REPO: gmazzo/dependabot-smoke-tests
18-
SMOKE_TEST_BRANCH: gradle-wrapper-support
18+
SMOKE_TEST_BRANCH: gradle-wrapper-support-binaries
1919
jobs:
2020
discover:
2121
runs-on: ubuntu-latest

gradle/lib/dependabot/gradle/file_fetcher.rb

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ 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
26+
SUPPORTED_WRAPPER_FILES_PATH = %w(
27+
gradlew
28+
gradlew.bat
29+
gradle/wrapper/gradle-wrapper.jar
30+
gradle/wrapper/gradle-wrapper.properties
31+
).freeze
2832

2933
# For now Gradle only supports library .toml files in the main gradle folder
3034
SUPPORTED_VERSION_CATALOG_FILE_PATH =
@@ -69,13 +73,9 @@ def fetch_files
6973

7074
sig { params(root_dir: String).returns(T::Array[DependencyFile]) }
7175
def all_buildfiles_in_build(root_dir)
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
76+
files = [buildfile(root_dir), settings_file(root_dir), version_catalog_file(root_dir), lockfile(root_dir)]
77+
.compact
78+
files += wrapper_files(root_dir)
7979
files += subproject_buildfiles(root_dir)
8080
files += subproject_lockfiles(root_dir)
8181
files += dependency_script_plugins(root_dir)
@@ -147,14 +147,23 @@ def subproject_buildfiles(root_dir)
147147
end
148148
end
149149

150-
sig { params(root_dir: String).returns(T.nilable(DependencyFile)) }
151-
def wrapper_properties_file(root_dir)
152-
return nil unless Experiments.enabled?(:gradle_wrapper_updater)
150+
sig { params(dir: String).returns(T::Array[DependencyFile]) }
151+
def wrapper_files(dir)
152+
return [] unless Experiments.enabled?(:gradle_wrapper_updater)
153153

154-
gradle_wrapper_properties_file(root_dir)
155-
rescue Dependabot::DependencyFileNotFound
156-
# Wrapper file is optional for Gradle
157-
nil
154+
SUPPORTED_WRAPPER_FILES_PATH.filter_map do |filename|
155+
file = fetch_file_if_present(File.join(dir, filename))
156+
next unless file
157+
158+
if file.name.end_with?(".jar")
159+
file.content = Base64.encode64(T.must(file.content)) if file.content
160+
file.content_encoding = DependencyFile::ContentEncoding::BASE64
161+
end
162+
file
163+
rescue Dependabot::DependencyFileNotFound
164+
# Gradle itself doesn't worry about missing subprojects, so we don't
165+
nil
166+
end
158167
end
159168

160169
sig { params(root_dir: String).returns(T.nilable(DependencyFile)) }
@@ -209,11 +218,6 @@ def buildfile(dir)
209218
file
210219
end
211220

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-
217221
sig { params(dir: String).returns(T.nilable(DependencyFile)) }
218222
def gradle_toml_file(dir)
219223
find_first(dir, SUPPORTED_VERSION_CATALOG_FILE_PATH)

gradle/lib/dependabot/gradle/file_updater.rb

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class FileUpdater < Dependabot::FileUpdaters::Base
1515
require_relative "file_updater/dependency_set_updater"
1616
require_relative "file_updater/property_value_updater"
1717
require_relative "file_updater/lockfile_updater"
18+
require_relative "file_updater/wrapper_updater"
1819

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

@@ -23,8 +24,9 @@ def self.updated_files_regex
2324
[
2425
# Matches build.gradle or build.gradle.kts in root directory
2526
%r{(^|.*/)build\.gradle(\.kts)?$},
26-
# Matches gradle/wrapper/gradle-wrapper.properties in root or any subdirectory
27-
%r{(^|.*/)?gradle/wrapper/gradle-wrapper.properties$},
27+
# Matches gradle-wrapper files in root or any subdirectory
28+
%r{(^|.*/)?gradlew(\.bat)?$},
29+
%r{(^|.*/)?gradle/wrapper/gradle-wrapper\.(properties|jar)$},
2830
# Matches gradle/libs.versions.toml in root or any subdirectory
2931
%r{(^|.*/)?gradle/libs\.versions\.toml$},
3032
# Matches settings.gradle or settings.gradle.kts in root or any subdirectory
@@ -81,6 +83,10 @@ def original_file
8183
def update_buildfiles_for_dependency(buildfiles:, dependency:)
8284
files = buildfiles.dup
8385

86+
# dependencies may have multiple requirements targeting the same file or build dir
87+
# we keep the last one by path to later run its native helpers
88+
buildfiles_processed = T.let({}, T::Hash[String, Dependabot::DependencyFile])
89+
8490
# The UpdateChecker ensures the order of requirements is preserved
8591
# when updating, so we can zip them together in new/old pairs.
8692
reqs = dependency.requirements.zip(T.must(dependency.previous_requirements))
@@ -110,16 +116,20 @@ def update_buildfiles_for_dependency(buildfiles:, dependency:)
110116
files[T.must(files.index(buildfile))] = update_version_in_buildfile(dependency, buildfile, old_req, new_req)
111117
end
112118

113-
next unless Dependabot::Experiments.enabled?(:gradle_lockfile_updater)
119+
buildfiles_processed[buildfile.name] = buildfile
120+
end
121+
122+
# runs native updaters (e.g. wrapper, lockfile) on relevant build files updated
123+
updaters = native_updaters(files, dependency)
124+
buildfiles_processed.each do |_, buildfile|
125+
updated_files = updaters.flat_map { |updater| updater.update_files(buildfile) }
114126

115-
lockfile_updater = LockfileUpdater.new(dependency_files: files)
116-
lockfiles = lockfile_updater.update_lockfiles(buildfile)
117-
lockfiles.each do |lockfile|
118-
existing_file = files.find { |f| f.name == lockfile.name && f.directory == lockfile.directory }
127+
updated_files.each do |file|
128+
existing_file = files.find { |f| f.name == file.name && f.directory == file.directory }
119129
if existing_file.nil?
120-
files << lockfile
130+
files << file
121131
else
122-
files[T.must(files.index(existing_file))] = lockfile
132+
files[T.must(files.index(existing_file))] = file
123133
end
124134
end
125135
end
@@ -130,6 +140,18 @@ def update_buildfiles_for_dependency(buildfiles:, dependency:)
130140
# rubocop:enable Metrics/CyclomaticComplexity
131141
# rubocop:enable Metrics/AbcSize
132142

143+
sig do
144+
params(files: T::Array[Dependabot::DependencyFile],
145+
dependency: Dependabot::Dependency).returns(T::Array[GradleUpdaterBase])
146+
end
147+
def native_updaters(files, dependency)
148+
updaters = T.let([], T::Array[GradleUpdaterBase])
149+
updaters << LockfileUpdater.new(dependency_files: files) if Experiments.enabled?(:gradle_lockfile_updater)
150+
updaters << WrapperUpdater.new(dependency_files: files, dependency: dependency) if
151+
Experiments.enabled?(:gradle_wrapper_updater)
152+
updaters
153+
end
154+
133155
sig do
134156
params(
135157
buildfiles: T::Array[Dependabot::DependencyFile],

gradle/lib/dependabot/gradle/file_updater/gradle_updater_base.rb

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,72 +10,88 @@
1010
module Dependabot
1111
module Gradle
1212
class FileUpdater
13-
class LockfileUpdater
13+
class GradleUpdaterBase
1414
extend T::Sig
1515

1616
sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
1717
def initialize(dependency_files:)
1818
@dependency_files = dependency_files
1919
end
2020

21+
sig { params(_file: Dependabot::DependencyFile).returns(T::Boolean) }
22+
def target_file?(_file)
23+
raise NotImplementedError
24+
end
25+
26+
sig { returns(T::Array[String]) }
27+
def command_args
28+
raise NotImplementedError
29+
end
30+
31+
sig { params(build_file: Dependabot::DependencyFile).returns(String) }
32+
def base_path(build_file)
33+
File.dirname(File.join(build_file.directory, build_file.name))
34+
end
35+
2136
sig { params(build_file: Dependabot::DependencyFile).returns(T::Array[Dependabot::DependencyFile]) }
22-
def update_lockfiles(build_file)
23-
local_lockfiles = dependency_files.select do |file|
24-
file.directory == build_file.directory && file.name.end_with?(".lockfile")
37+
def update_files(build_file)
38+
local_files = dependency_files.select do |file|
39+
file.directory == build_file.directory && target_file?(file)
2540
end
2641

27-
# If we don't have any lockfiles in the build files don't generate one
28-
return dependency_files unless local_lockfiles.any?
42+
# If we don't have any files in the build files don't generate one
43+
return dependency_files unless local_files.any?
2944

3045
updated_files = dependency_files.dup
3146
SharedHelpers.in_a_temporary_directory do |temp_dir|
3247
populate_temp_directory(temp_dir)
33-
cwd = File.join(temp_dir, build_file.directory, build_file.name)
34-
cwd = File.dirname(cwd)
48+
cwd = File.join(temp_dir, base_path(build_file))
3549

3650
# Create gradle.properties file with proxy settings
3751
# Would prefer to use command line arguments, but they don't work.
3852
properties_filename = File.join(temp_dir, build_file.directory, "gradle.properties")
3953
write_properties_file(properties_filename)
4054

41-
command_parts = [
42-
"gradle",
43-
"dependencies",
44-
"--no-daemon",
45-
"--write-locks"
46-
]
55+
command_parts = %w(gradle --no-daemon --stacktrace) + command_args
4756
command = Shellwords.join(command_parts)
4857

4958
Dir.chdir(cwd) do
5059
SharedHelpers.run_shell_command(command, cwd: cwd)
51-
update_lockfiles_content(temp_dir, local_lockfiles, updated_files)
60+
update_files_content(temp_dir, local_files, updated_files)
5261
rescue SharedHelpers::HelperSubprocessFailed => e
53-
puts "Failed to update lockfiles: #{e.message}"
62+
puts "Failed to update files: #{e.message}"
5463
return updated_files
5564
end
5665
end
5766
updated_files
5867
end
5968

69+
private
70+
6071
sig do
6172
params(
6273
temp_dir: T.any(Pathname, String),
63-
local_lockfiles: T::Array[Dependabot::DependencyFile],
64-
updated_lockfiles: T::Array[Dependabot::DependencyFile]
74+
local_files: T::Array[Dependabot::DependencyFile],
75+
updated_files: T::Array[Dependabot::DependencyFile]
6576
).void
6677
end
67-
def update_lockfiles_content(temp_dir, local_lockfiles, updated_lockfiles)
68-
local_lockfiles.each do |file|
78+
def update_files_content(temp_dir, local_files, updated_files)
79+
local_files.each do |file|
6980
f_content = File.read(File.join(temp_dir, file.directory, file.name))
7081
tmp_file = file.dup
7182
tmp_file.content = f_content
72-
updated_lockfiles[T.must(updated_lockfiles.index(file))] = tmp_file
83+
updated_files[T.must(updated_files.index(file))] = tmp_file
7384
end
7485
end
7586

87+
sig { returns(T::Array[Dependabot::DependencyFile]) }
88+
def files_to_populate
89+
@dependency_files
90+
end
91+
7692
sig { params(temp_dir: T.any(Pathname, String)).void }
7793
def populate_temp_directory(temp_dir)
78-
@dependency_files.each do |file|
94+
files_to_populate.each do |file|
7995
in_path_name = File.join(temp_dir, file.directory, file.name)
8096
FileUtils.mkdir_p(File.dirname(in_path_name))
8197
File.write(in_path_name, file.content)
@@ -100,8 +116,6 @@ def write_properties_file(file_name) # rubocop:disable Metrics/PerceivedComplexi
100116
File.write(file_name, properties_content)
101117
end
102118

103-
private
104-
105119
sig { returns(T::Array[Dependabot::DependencyFile]) }
106120
attr_reader :dependency_files
107121
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# typed: strong
2+
# frozen_string_literal: true
3+
4+
require "sorbet-runtime"
5+
require "shellwords"
6+
7+
require "dependabot/gradle/file_updater/gradle_updater_base"
8+
9+
module Dependabot
10+
module Gradle
11+
class FileUpdater
12+
class LockfileUpdater < GradleUpdaterBase
13+
extend T::Sig
14+
15+
sig { override.params(file: Dependabot::DependencyFile).returns(T::Boolean) }
16+
def target_file?(file)
17+
file.name.end_with?(".lockfile")
18+
end
19+
20+
sig { override.returns(T::Array[String]) }
21+
def command_args
22+
%w(dependencies --write-locks)
23+
end
24+
end
25+
end
26+
end
27+
end
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# typed: strong
2+
# frozen_string_literal: true
3+
4+
require "sorbet-runtime"
5+
require "shellwords"
6+
7+
require "dependabot/gradle/distributions"
8+
require "dependabot/gradle/file_updater/gradle_updater_base"
9+
10+
module Dependabot
11+
module Gradle
12+
class FileUpdater
13+
class WrapperUpdater < GradleUpdaterBase
14+
extend T::Sig
15+
include Dependabot::Gradle::Distributions
16+
17+
sig { params(dependency_files: T::Array[Dependabot::DependencyFile], dependency: Dependabot::Dependency).void }
18+
def initialize(dependency_files:, dependency:)
19+
super(dependency_files: dependency_files)
20+
@dependency = dependency
21+
@target_files = T.let(%w(
22+
/gradlew
23+
/gradlew.bat
24+
/gradle/wrapper/gradle-wrapper.properties
25+
/gradle/wrapper/gradle-wrapper.jar
26+
), T::Array[String])
27+
@build_files = T.let(%w(
28+
build.gradle
29+
build.gradle.kts
30+
settings.gradle
31+
settings.gradle.kts
32+
), T::Array[String])
33+
end
34+
35+
sig { override.params(file: Dependabot::DependencyFile).returns(T::Boolean) }
36+
def target_file?(file)
37+
@target_files.any? { |r| "/#{file.name}".end_with?(r) }
38+
end
39+
40+
sig { override.returns(T::Array[String]) }
41+
def command_args
42+
version = T.let(dependency.requirements[0]&.[](:requirement), String)
43+
checksum = T.let(dependency.requirements[1]&.[](:requirement), String) if dependency.requirements.size > 1
44+
45+
args = %W(wrapper --gradle-version #{version} --no-validate-url)
46+
args += %W(--gradle-distribution-sha256-sum #{checksum}) if checksum
47+
args
48+
end
49+
50+
sig { params(build_file: Dependabot::DependencyFile).returns(String) }
51+
def base_path(build_file)
52+
super.delete_suffix("/gradle/wrapper")
53+
end
54+
55+
private
56+
57+
# Gradle builds can be complex, to maximize the chances of a successful we just keep related wrapper files
58+
# and produce a minimal build for it to run (losing any customisations of the `wrapper` task in the process)
59+
sig { override.returns(T::Array[Dependabot::DependencyFile]) }
60+
def files_to_populate
61+
@dependency_files.filter_map do |f|
62+
next f if target_file?(f)
63+
next Dependabot::DependencyFile.new(directory: f.directory, name: f.name, content: "") if build_file?(f)
64+
end
65+
end
66+
67+
sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) }
68+
def build_file?(file)
69+
@build_files.include?(File.basename(file.name))
70+
end
71+
72+
sig { returns(Dependabot::Dependency) }
73+
attr_reader :dependency
74+
end
75+
end
76+
end
77+
end

0 commit comments

Comments
 (0)