dependabot-maven 0.391.0 → 0.392.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e291e2478cd805665628ff741f4bf0b36a80c9297c20aa4219080f32155f30a3
4
- data.tar.gz: f155a760998f79499829ce17fecea84c3a288f96ee302e70774d2dee66824b64
3
+ metadata.gz: d7384e20ca998f4af49001eb34de080505f3abff092e4faa921424ad6b65299e
4
+ data.tar.gz: 3c7f12c0289d6a8e811d0d51c6d24af81a319e59ba722fe773d623433ddb6ab9
5
5
  SHA512:
6
- metadata.gz: a3249ddd53286a0e01e1f88a3ab7e07baada7d595a0888f4138d410503847b1a0306e6f575e426c55fb5630d3851e63db59744ee7c97ab70e8e27178d996a175
7
- data.tar.gz: d54dcd4d6c97f470cd22f871e4b8532cdf9cfc47ada811ffb2d98854e0f7867d2422310e8f5097dc573102b628e4d8b666c8c9a024376b76f60e63c2794d714a
6
+ metadata.gz: f8da50ec13cdb3fead27bd2e0c20e019650a8f38a9817be6fb17f8ec5ceef8b7ffb19b7e6805a7f40787de1c4d029d7692e35149a1fc10901d2f48d043c4a02b
7
+ data.tar.gz: f6f258473e1bff627f213be788e692c6ddfaced18172fdc6910307f504c12554ed88b5447163932e4d909b9be856aaa5921669e1f572de0e81c3ff6d865781a5
@@ -0,0 +1,31 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/dependency_requirement"
5
+
6
+ module Dependabot
7
+ module Maven
8
+ module Distributions
9
+ extend T::Sig
10
+
11
+ # Used to distinguish wrapper requirements (which live in maven-wrapper.properties)
12
+ # from regular POM requirements (which live in pom.xml)
13
+ DISTRIBUTION_DEPENDENCY_TYPE = "maven-distribution"
14
+
15
+ # Maven and the maven-wrapper plugin release independently with separate cadences.
16
+ # Tracking them as distinct dependencies allows users to update each on their own
17
+ # schedule. Users who prefer batched updates can use grouped updates.
18
+
19
+ MAVEN_DISTRIBUTION_PACKAGE = "org.apache.maven:apache-maven"
20
+ MAVEN_WRAPPER_PACKAGE = "org.apache.maven.wrapper:maven-wrapper"
21
+
22
+ sig { params(requirements: T::Array[Dependabot::DependencyRequirement]).returns(T::Boolean) }
23
+ def self.distribution_requirements?(requirements)
24
+ # Returns true if any requirement came from a maven-wrapper.properties
25
+ # file rather than a pom.xml. Used as the primary guard throughout the
26
+ # updater pipeline to short-circuit non-wrapper paths.
27
+ requirements.any? { |req| req.source_string("type") == DISTRIBUTION_DEPENDENCY_TYPE }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,12 +1,15 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "base64"
4
5
  require "nokogiri"
5
6
  require "sorbet-runtime"
6
7
 
7
8
  require "dependabot/file_fetchers"
8
9
  require "dependabot/file_fetchers/base"
9
10
  require "dependabot/file_filtering"
11
+ require "dependabot/experiments"
12
+ require "dependabot/maven/file_parser/wrapper_mojo"
10
13
 
11
14
  module Dependabot
12
15
  module Maven
@@ -17,6 +20,14 @@ module Dependabot
17
20
  MODULE_SELECTOR = "project > modules > module, " \
18
21
  "profile > modules > module"
19
22
 
23
+ WRAPPER_PROPERTIES_RELATIVE = ".mvn/wrapper/maven-wrapper.properties"
24
+ WRAPPER_JAR_RELATIVE = ".mvn/wrapper/maven-wrapper.jar"
25
+ WRAPPER_DOWNLOADER_RELATIVE = ".mvn/wrapper/MavenWrapperDownloader.java"
26
+
27
+ WRAPPER_UNIX_SCRIPTS = %w(mvnw mvnwDebug).freeze
28
+ WRAPPER_WINDOWS_SCRIPTS = %w(mvnw.cmd mvnwDebug.cmd).freeze
29
+ WRAPPER_ALL_SCRIPTS = T.let((WRAPPER_UNIX_SCRIPTS + WRAPPER_WINDOWS_SCRIPTS).freeze, T::Array[String])
30
+
20
31
  sig { override.params(filenames: T::Array[String]).returns(T::Boolean) }
21
32
  def self.required_files_in?(filenames)
22
33
  filenames.include?("pom.xml")
@@ -31,10 +42,13 @@ module Dependabot
31
42
  def fetch_files
32
43
  fetched_files = []
33
44
  fetched_files << pom
34
- fetched_files += child_poms
45
+ poms = child_poms
46
+ fetched_files += poms
35
47
  fetched_files += relative_path_parents(fetched_files)
36
48
  fetched_files += targetfiles
37
49
  fetched_files << extensions if extensions
50
+ # Pass already-fetched poms so all_wrapper_files does not re-fetch them.
51
+ fetched_files += all_wrapper_files([T.must(pom)] + poms)
38
52
 
39
53
  # Filter excluded files from final collection
40
54
  filtered_files = fetched_files.uniq.reject do |file|
@@ -46,6 +60,63 @@ module Dependabot
46
60
 
47
61
  private
48
62
 
63
+ sig { params(dir: String).returns(T::Array[DependencyFile]) }
64
+ def wrapper_files_for_dir(dir)
65
+ return [] unless Dependabot::Experiments.enabled?(:maven_wrapper_updater)
66
+
67
+ # Strip leading "./" from root-level paths
68
+ properties_path = File.join(dir, WRAPPER_PROPERTIES_RELATIVE).delete_prefix("./")
69
+ properties = fetch_file_if_present(properties_path)
70
+ return [] unless properties
71
+
72
+ files = T.let([properties], T::Array[DependencyFile])
73
+ WRAPPER_ALL_SCRIPTS.each do |script|
74
+ script_path = dir == "." ? script : File.join(dir, script)
75
+ f = fetch_file_if_present(script_path)
76
+ f.mode = DependencyFile::Mode::EXECUTABLE if f && WRAPPER_UNIX_SCRIPTS.include?(script)
77
+ files << f if f
78
+ end
79
+
80
+ content = T.must(properties.content)
81
+ wrapper_url = FileParser::WrapperMojo.get_property_value(content, "wrapperUrl")
82
+ dist_type = FileParser::WrapperMojo.resolve_distribution_type(content, wrapper_url)
83
+ files + fetch_wrapper_artifact_files(dir, dist_type)
84
+ rescue Dependabot::DependencyFileNotFound
85
+ []
86
+ end
87
+
88
+ sig { params(dir: String, dist_type: String).returns(T::Array[DependencyFile]) }
89
+ def fetch_wrapper_artifact_files(dir, dist_type)
90
+ case dist_type
91
+ when "bin", "script"
92
+ jar_path = File.join(dir, WRAPPER_JAR_RELATIVE).delete_prefix("./")
93
+ jar = fetch_file_if_present(jar_path)
94
+ return [] unless jar
95
+
96
+ jar.content = Base64.encode64(T.must(jar.content)) if jar.content
97
+ jar.content_encoding = DependencyFile::ContentEncoding::BASE64
98
+ [jar]
99
+ when "source"
100
+ dl_path = File.join(dir, WRAPPER_DOWNLOADER_RELATIVE).delete_prefix("./")
101
+ downloader = fetch_file_if_present(dl_path)
102
+ downloader ? [downloader] : []
103
+ else
104
+ []
105
+ end
106
+ end
107
+
108
+ sig { params(poms: T::Array[DependencyFile]).returns(T::Array[DependencyFile]) }
109
+ def all_wrapper_files(poms)
110
+ seen_dirs = T.let(Set.new, T::Set[String])
111
+ poms.filter_map do |pom_file|
112
+ dir = File.dirname(pom_file.name)
113
+ next if seen_dirs.include?(dir)
114
+
115
+ seen_dirs << dir
116
+ wrapper_files_for_dir(dir)
117
+ end.flatten
118
+ end
119
+
49
120
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
50
121
  def pom
51
122
  @pom ||= T.let(fetch_file_from_host("pom.xml"), T.nilable(Dependabot::DependencyFile))
@@ -0,0 +1,407 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ # Parses maven-wrapper.properties and emits Dependency objects for the two
5
+ # tracked Maven coordinates: org.apache.maven:apache-maven (the maven distribution)
6
+ # and org.apache.maven.wrapper:maven-wrapper (the wrapper plugin).
7
+ require "dependabot/dependency_requirement"
8
+ require "dependabot/maven/file_parser"
9
+ require "dependabot/maven/distributions"
10
+
11
+ module Dependabot
12
+ module Maven
13
+ class FileParser
14
+ # Model of the Maven Wrapper plugin's WrapperMojo goal. Mirrors the
15
+ # properties recognized by the upstream plugin:
16
+ # https://github.com/apache/maven-wrapper/blob/master/maven-wrapper-plugin/src/main/java/org/apache/maven/plugins/wrapper/WrapperMojo.java
17
+ class WrapperMojo
18
+ extend T::Sig
19
+
20
+ class WrapperProperties < T::Struct
21
+ # Resolved distributionUrl value (the raw value from the properties file).
22
+ # This value is mandatory
23
+ const :distribution_url, String
24
+ # The Maven Version extracted from the distributionUrl, e.g. "3.9.9"
25
+ const :distribution_version, String
26
+
27
+ # Value of distributionSha256Sum, or nil when the property is absent.
28
+ # Checksum verification is not mandatory
29
+ # Tracked as a second requirement on the Apache Maven dependency so the
30
+ # checksum is updated atomically with the version.
31
+ const :distribution_sha256_sum, T.nilable(String)
32
+
33
+ # Value of wrapperSha256Sum, or nil when the property is absent.
34
+ # Used to verify the wrapper JAR download
35
+ const :wrapper_sha256_sum, T.nilable(String)
36
+
37
+ # Version of the Maven Wrapper plugin, e.g. "3.3.4".
38
+ # Sourced from the first strategy that succeeds:
39
+ # - wrapperVersion property (>=3.3.1),
40
+ # - version segment of wrapperUrl JAR filename (<3.3.0), or
41
+ # - comment in the mvnw script body (3.3.0 only, see MWRAPPER-120 and MWRAPPER-134).
42
+ # This field is mandatory and raises if none of the sources yield a version.
43
+ const :wrapper_version, String
44
+
45
+ # The full JAR URL from the wrapperUrl property
46
+ # (e.g. "https://.../maven-wrapper-3.3.3.jar"), used to compute the
47
+ # new download URL for wrapperSha256Sum recomputation.
48
+ # Present in both old-format (wrapperUrl only) and new-format (wrapperVersion + wrapperUrl) files.
49
+ # nil only when the version was read from the mvnw script body (very old wrappers).
50
+ const :wrapper_url, T.nilable(String)
51
+
52
+ # Value of distributionType, controlling which binary artifacts are committed
53
+ # alongside the wrapper scripts:
54
+ # - "bin" (JAR present),
55
+ # - "script" (JAR present),
56
+ # - "only-script" (no JAR),
57
+ # - "source" (MavenWrapperDownloader.java present).
58
+ # Defaults to "bin" when the property is absent, matching pre-3.3.0 behavior.
59
+ const :distribution_type, String
60
+ end
61
+
62
+ # Extracts the version from a distributionUrl value (the resolved URL,
63
+ # not the raw properties line). Match the standard artifact filename so
64
+ # custom mirrors do not need to preserve Maven Central's directory layout.
65
+ DIST_URL_VERSION_REGEX = %r{
66
+ /apache-maven-(?<version>[^/?#]+)-(?:bin|src)\.(?:zip|tar\.gz)(?:[?#].*)?\z
67
+ }x
68
+ class UnparseableDistributionUrl < RuntimeError; end
69
+
70
+ sig do
71
+ params(
72
+ properties_file: DependencyFile,
73
+ script_files: T::Array[DependencyFile]
74
+ ).returns(T::Array[Dependency])
75
+ end
76
+ def self.resolve_dependencies(properties_file, script_files: [])
77
+ content = properties_file.content
78
+ return [] unless content
79
+
80
+ distribution_url = get_property_value(content, "distributionUrl")
81
+ if distribution_url&.include?("mvnd")
82
+ Dependabot.logger.warn("Maven daemon (mvnd) distribution is not supported, skipping wrapper update")
83
+ return []
84
+ end
85
+
86
+ begin
87
+ props = load_properties(content, script_files: script_files)
88
+ rescue UnparseableDistributionUrl => e
89
+ Dependabot.logger.warn("#{e.message}, skipping wrapper update")
90
+ return []
91
+ end
92
+
93
+ if props.wrapper_url&.include?("takari")
94
+ Dependabot.logger.warn("The Takari distribution is not supported, skipping wrapper update")
95
+ return []
96
+ end
97
+
98
+ file_name = properties_file.name
99
+ has_debug_scripts = debug_scripts?(script_files)
100
+ deps = [build_distribution_dependency(file_name, props, props.distribution_version, has_debug_scripts)]
101
+ deps << build_wrapper_dependency(
102
+ file_name, props, props.distribution_version, props.wrapper_version, has_debug_scripts
103
+ )
104
+ deps.compact
105
+ end
106
+
107
+ sig do
108
+ params(
109
+ file_name: String,
110
+ props: WrapperProperties,
111
+ dist_version: String,
112
+ has_debug_scripts: T::Boolean
113
+ ).returns(Dependency)
114
+ end
115
+ def self.build_distribution_dependency(file_name, props, dist_version, has_debug_scripts)
116
+ Dependency.new(
117
+ name: Distributions::MAVEN_DISTRIBUTION_PACKAGE,
118
+ version: dist_version,
119
+ requirements: build_distribution_requirements(file_name, props, dist_version, has_debug_scripts),
120
+ package_manager: "maven"
121
+ )
122
+ end
123
+
124
+ sig do
125
+ params(
126
+ file_name: String,
127
+ props: WrapperProperties,
128
+ dist_version: String,
129
+ has_debug_scripts: T::Boolean
130
+ ).returns(T::Array[Dependabot::DependencyRequirement])
131
+ end
132
+ def self.build_distribution_requirements(file_name, props, dist_version, has_debug_scripts)
133
+ metadata = T.let(
134
+ {
135
+ packaging_type: "pom",
136
+ wrapper_version: props.wrapper_version,
137
+ distribution_type: props.distribution_type,
138
+ distribution_version: dist_version,
139
+ include_debug_script: has_debug_scripts
140
+ },
141
+ Dependabot::DependencyRequirement::ObjectHash
142
+ )
143
+ metadata[:distribution_sha256_sum] = props.distribution_sha256_sum if props.distribution_sha256_sum
144
+
145
+ main_req = Dependabot::DependencyRequirement.create(
146
+ {
147
+ requirement: dist_version,
148
+ file: file_name,
149
+ source: {
150
+ type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
151
+ url: props.distribution_url,
152
+ property: "distributionUrl"
153
+ },
154
+ groups: [],
155
+ # The Apache Maven distribution is not a JAR, but a POM is available
156
+ # We can use this POM to query for new versions using the existing update checker for maven
157
+ metadata: metadata
158
+ }
159
+ )
160
+
161
+ requirements = T.let([main_req], T::Array[Dependabot::DependencyRequirement])
162
+ requirements.concat(build_wrapper_url_requirement(file_name, props))
163
+ requirements
164
+ end
165
+
166
+ sig do
167
+ params(
168
+ file_name: String,
169
+ props: WrapperProperties
170
+ ).returns(T::Array[Dependabot::DependencyRequirement])
171
+ end
172
+ def self.build_wrapper_url_requirement(file_name, props)
173
+ return [] unless props.wrapper_url
174
+
175
+ Dependabot.logger.debug "wrapperUrl is present #{props.wrapper_url} version=#{props.wrapper_version}"
176
+ metadata = T.let({}, Dependabot::DependencyRequirement::ObjectHash)
177
+ metadata[:wrapper_sha256_sum] = props.wrapper_sha256_sum if props.wrapper_sha256_sum
178
+
179
+ req = Dependabot::DependencyRequirement.create(
180
+ {
181
+ requirement: props.wrapper_version,
182
+ file: file_name,
183
+ source: {
184
+ type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
185
+ property: "wrapperUrl",
186
+ url: props.wrapper_url
187
+ },
188
+ groups: []
189
+ }
190
+ )
191
+ req[:metadata] = metadata if metadata.any?
192
+ [req]
193
+ end
194
+
195
+ sig do
196
+ params(
197
+ file_name: String,
198
+ props: WrapperProperties,
199
+ dist_version: String,
200
+ wrapper_version: String,
201
+ has_debug_scripts: T::Boolean
202
+ ).returns(Dependency)
203
+ end
204
+ def self.build_wrapper_dependency(file_name, props, dist_version, wrapper_version, has_debug_scripts)
205
+ metadata = T.let(
206
+ {
207
+ packaging_type: "pom",
208
+ distribution_version: dist_version,
209
+ wrapper_version: wrapper_version,
210
+ distribution_type: props.distribution_type,
211
+ include_debug_script: has_debug_scripts
212
+ },
213
+ Dependabot::DependencyRequirement::ObjectHash
214
+ )
215
+ Dependency.new(
216
+ name: Distributions::MAVEN_WRAPPER_PACKAGE,
217
+ version: props.wrapper_version,
218
+ requirements: [
219
+ Dependabot::DependencyRequirement.create(
220
+ {
221
+ requirement: props.wrapper_version,
222
+ file: file_name,
223
+ source: {
224
+ type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
225
+ property: "wrapperVersion"
226
+ },
227
+ groups: [],
228
+ metadata: metadata
229
+ }
230
+ )
231
+ ],
232
+ package_manager: "maven"
233
+ )
234
+ end
235
+
236
+ # Resolves the distribution type, matching the maven-wrapper-plugin's own precedence:
237
+ # an explicit distributionType wins; otherwise the plugin defaults to "only-script" (since
238
+ # 3.2.0). Legacy pre-3.3.0 files omit distributionType but carry a wrapperUrl pointing at the
239
+ # maven-wrapper JAR, which means a JAR-based ("bin") setup, so we keep that for them.
240
+ # https://maven.apache.org/tools/wrapper/maven-wrapper-plugin/wrapper-mojo.html
241
+ sig { params(content: String, wrapper_url: T.nilable(String)).returns(String) }
242
+ def self.resolve_distribution_type(content, wrapper_url)
243
+ explicit = get_property_value(content, "distributionType")
244
+ return explicit if explicit
245
+
246
+ wrapper_url ? "bin" : "only-script"
247
+ end
248
+
249
+ sig { params(script_files: T::Array[DependencyFile]).returns(T::Boolean) }
250
+ def self.debug_scripts?(script_files)
251
+ debug_scripts = %w(mvnwDebug mvnwDebug.cmd)
252
+ script_files.any? { |f| debug_scripts.any? { |s| f.name.end_with?(s) } }
253
+ end
254
+
255
+ sig { params(content: String, script_files: T::Array[DependencyFile]).returns(WrapperProperties) }
256
+ def self.load_properties(content, script_files: [])
257
+ distribution_url = get_property_value!(content, "distributionUrl")
258
+ distribution_version = extract_distribution_version(distribution_url)
259
+ distribution_sha256_sum = get_property_value(content, "distributionSha256Sum")
260
+ wrapper_url = get_property_value(content, "wrapperUrl")
261
+ wrapper_sha256_sum = get_property_value(content, "wrapperSha256Sum")
262
+ distribution_type = resolve_distribution_type(content, wrapper_url)
263
+ wrapper_version = resolve_wrapper_version(content, wrapper_url, script_files)
264
+
265
+ WrapperProperties.new(
266
+ distribution_url: distribution_url,
267
+ distribution_version: distribution_version,
268
+ distribution_sha256_sum: distribution_sha256_sum,
269
+ wrapper_sha256_sum: wrapper_sha256_sum,
270
+ wrapper_version: wrapper_version,
271
+ wrapper_url: wrapper_url,
272
+ distribution_type: distribution_type
273
+ )
274
+ end
275
+
276
+ sig { params(content: String).returns(String) }
277
+ def self.extract_distribution_version(content)
278
+ match = content.match(DIST_URL_VERSION_REGEX)
279
+ unless match && match[:version]
280
+ raise UnparseableDistributionUrl, "Could not extract Maven version from distributionUrl"
281
+ end
282
+
283
+ T.must(match[:version])
284
+ end
285
+
286
+ sig { params(content: String, target_key: String).returns(T.nilable(String)) }
287
+ def self.get_property_value(content, target_key)
288
+ # 1. Handle Java line continuations (the backslash edge case)
289
+ # We join lines ending in \ before splitting into an array
290
+ normalized_content = content.gsub(/\\\n\s*/, "")
291
+
292
+ # 2. Escape the key for Regex safety
293
+ escaped_key = Regexp.escape(target_key)
294
+
295
+ # Start of line -> Key -> optional space -> delimiter (= or :) -> value.
296
+ # Bounded to spaces/tabs (not \s) so the per-line match can't backtrack polynomially.
297
+ pattern = /^[ \t]*#{escaped_key}[ \t]*[=:][ \t]*(.*)$/
298
+
299
+ normalized_content.lines.each do |line|
300
+ next if line.start_with?("#", "!") # Skip Java comments
301
+
302
+ if (match = line.match(pattern))
303
+ return decode_property_value(T.must(match[1]).strip)
304
+ end
305
+ end
306
+ nil
307
+ end
308
+
309
+ # Decodes Java properties escapes so URLs are usable by URI parsing, registry matching, and
310
+ # HTTP clients. Maven commonly writes URL schemes as `https\://` in legacy wrapper files.
311
+ sig { params(value: String).returns(String) }
312
+ def self.decode_property_value(value)
313
+ value.gsub(/\\(?:u(?<unicode>[0-9a-fA-F]{4})|(?<escaped>.))/) do
314
+ unicode = Regexp.last_match(:unicode)
315
+ next [unicode.hex].pack("U") if unicode
316
+
317
+ case Regexp.last_match(:escaped)
318
+ when "t" then "\t"
319
+ when "n" then "\n"
320
+ when "r" then "\r"
321
+ when "f" then "\f"
322
+ else Regexp.last_match(:escaped)
323
+ end
324
+ end
325
+ end
326
+
327
+ sig { params(content: String, target_key: String).returns(String) }
328
+ def self.get_property_value!(content, target_key)
329
+ value = get_property_value(content, target_key)
330
+
331
+ raise "Missing mandatory property: #{target_key}" if value.nil?
332
+
333
+ value
334
+ end
335
+
336
+ private_class_method :build_distribution_dependency
337
+ private_class_method :build_wrapper_dependency
338
+ private_class_method :build_distribution_requirements
339
+ private_class_method :build_wrapper_url_requirement
340
+ private_class_method :decode_property_value
341
+
342
+ # Matches the human-readable banner embedded in mvnw / mvnw.cmd, e.g.:
343
+ # "Apache Maven Wrapper startup script, version 3.3.2"
344
+ # "Apache Maven Wrapper startup batch script, version 3.3.2"
345
+ SCRIPT_VERSION_REGEX = /
346
+ Apache \s Maven \s Wrapper \s startup \s (?:batch \s)?
347
+ script, \s version \s
348
+ (?<version>\d+\.\d+(?:\.\d+)?)
349
+ /x
350
+
351
+ sig do
352
+ params(
353
+ content: String,
354
+ wrapper_url: T.nilable(String),
355
+ script_files: T::Array[DependencyFile]
356
+ ).returns(String)
357
+ end
358
+ def self.resolve_wrapper_version(content, wrapper_url, script_files)
359
+ version = get_property_value(content, "wrapperVersion")
360
+ return version if version
361
+
362
+ version = parse_version_from_wrapper_url(wrapper_url) if wrapper_url
363
+ return version if version
364
+
365
+ if script_files.any?
366
+ Dependabot.logger.warn "Maven Wrapper with no wrapperVersion or wrapperUrl in properties file"
367
+ version = load_wrapper_version_from_scripts(script_files)
368
+ end
369
+
370
+ if version.nil?
371
+ raise "Could not determine Maven Wrapper version from wrapperVersion, wrapperUrl, or script files"
372
+ end
373
+
374
+ version
375
+ end
376
+
377
+ sig { params(url: String).returns(T.nilable(String)) }
378
+ def self.parse_version_from_wrapper_url(url)
379
+ match = url.match(/-(?<version>\d+\.\d+(?:\.\d+)?(?:-\w+)*)(?:-bin)?\.jar/)
380
+ match&.[](:version)
381
+ end
382
+
383
+ # Extracts the Maven Wrapper version declared in the mvnw / mvnw.cmd
384
+ # shell scripts. Unix scripts (mvnw) are checked before Windows scripts (mvnw.cmd)
385
+ # if neither contains the banner, nil is returned and the caller falls back to other sources.
386
+ sig { params(script_files: T::Array[DependencyFile]).returns(T.nilable(String)) }
387
+ def self.load_wrapper_version_from_scripts(script_files)
388
+ # Preferred order: Unix script first, Windows script as fallback.
389
+ windows_scripts, unix_scripts = script_files.partition { |f| f.name.end_with?(".cmd") }
390
+ unix_scripts.chain(windows_scripts).each do |file|
391
+ next unless file.content
392
+
393
+ T.must(file.content).each_line do |line|
394
+ m = line.match(SCRIPT_VERSION_REGEX)
395
+ return m[:version] if m
396
+ end
397
+ end
398
+ nil
399
+ end
400
+
401
+ private_class_method :resolve_wrapper_version
402
+ private_class_method :parse_version_from_wrapper_url
403
+ private_class_method :load_wrapper_version_from_scripts
404
+ end
405
+ end
406
+ end
407
+ end