Setting the file. One moment.
Upgrade Bom · Azure Upgrade · microsoft/azure-skills · Skills Docs
ContentsBack to the top of the page 279
def _is_kotlin_dsl
— line 279
This file
Number 28.18
Position 18 of 30
Type Python
Size 20 KB
Lines 527 references/languages/java/scripts/ upgrade_bom.py
Python · 527 lines · 20 KB
16
17 Usage:
18 python3 upgrade_bom.py <project_dir> [latest] [options]
19 python3 upgrade_bom.py --get-latest-version
20
21 Arguments:
22 project_dir Path to the project root (must contain pom.xml or build.gradle).
23 latest Optional compatibility token. Explicit version pins are rejected.
24
25 Options:
26 --mvn <cmd> Maven command override.
27 --gradle <cmd> Gradle command override.
28 """
29
30 from __future__ import annotations
31
32 import argparse
33 import os
34 import re
35 import stat
36 import subprocess
37 import sys
38 import textwrap
39 import urllib.error
40 import urllib.request
41 import xml.etree.ElementTree as ET
42
43 GROUP_ID = "com.azure"
44 ARTIFACT_ID = "azure-sdk-bom"
45 BOM_POM_URL = "https://raw.githubusercontent.com/Azure/azure-sdk-for-java/main/sdk/boms/azure-sdk-bom/pom.xml"
46 POM_NAMESPACE = { "m" : "http://maven.apache.org/POM/4.0.0" }
47 MIN_BOM_VERSION = "1.3.0"
48 LATEST_VERSION_SENTINEL = "latest"
49 HTTP_TIMEOUT_SECONDS = 30
50 STABLE_SEMVER_RE = re.compile( r " ^(\d + ) \. (\d + ) \. (\d + )$ " )
51
52 # Maven constants
53 MVN_REWRITE_PLUGIN = "org.openrewrite.maven:rewrite-maven-plugin"
54 MVN_REWRITE_ARTIFACT_COORDS = "org.openrewrite:rewrite-maven"
55 MVN_UPGRADE_RECIPE = "org.openrewrite.maven.UpgradeDependencyVersion"
56 MVN_ADD_MANAGED_RECIPE = "org.openrewrite.maven.AddManagedDependency"
57 MVN_REMOVE_REDUNDANT_RECIPE = "org.openrewrite.maven.RemoveRedundantDependencyVersions"
58
59 # Gradle constants
60 GRADLE_UPGRADE_RECIPE = "org.openrewrite.gradle.UpgradeDependencyVersion"
61 GRADLE_ADD_PLATFORM_RECIPE = "org.openrewrite.gradle.AddPlatformDependency"
62 GRADLE_REMOVE_REDUNDANT_RECIPE = "org.openrewrite.gradle.RemoveRedundantDependencyVersions"
63 REWRITE_YML_NAME = "rewrite.yml"
64 GRADLE_PLUGIN_MARKER = "// --- openrewrite-upgrade-bom-plugin (auto-added, safe to remove) ---"
65
66 # ---------------------------------------------------------------------------
67 # Build-system detection
68 # ---------------------------------------------------------------------------
69
70 def _detect_build_system (project_dir: str ) -> str :
71 """Return 'maven' or 'gradle' depending on which build file is present."""
72 if os.path.isfile(os.path.join(project_dir, "pom.xml" )):
73 return "maven"
74 for name in ( "build.gradle" , "build.gradle.kts" ):
75 if os.path.isfile(os.path.join(project_dir, name)):
76 return "gradle"
77 return "unknown"
78
79
80 def _get_latest_bom_version () -> str :
81 try :
82 with urllib.request.urlopen( BOM_POM_URL , timeout = HTTP_TIMEOUT_SECONDS ) as response:
83 pom_xml = response.read()
84 except urllib.error.URLError as exc:
85 raise SystemExit ( f "Failed to download { BOM_POM_URL } : { exc } " ) from exc
86
87 try :
88 root = ET .fromstring(pom_xml)
89 except ET .ParseError as exc:
90 raise SystemExit ( f "Failed to parse BOM pom.xml: { exc } " ) from exc
91
92 version = root.findtext( "m:version" , namespaces = POM_NAMESPACE )
93 if not version:
94 raise SystemExit ( "Failed to find the azure-sdk-bom <version> in pom.xml" )
95
96 return version.strip()
97
98
99 def _parse_stable_semver (version: str ) -> tuple[ int , int , int ]:
100 match = STABLE_SEMVER_RE .fullmatch(version.strip())
101 if not match:
102 raise ValueError (
103 f "Invalid azure-sdk-bom version ' { version } '. Expected stable MAJOR.MINOR.PATCH."
104 )
105 return tuple ( int (part) for part in match.groups())
106
107
108 def _validate_minimum_bom_version (version: str ) -> None :
109 parsed = _parse_stable_semver(version)
110 minimum = _parse_stable_semver( MIN_BOM_VERSION )
111 if parsed < minimum:
112 raise ValueError (
113 f "azure-sdk-bom { version } is below the minimum supported version "
114 f " { MIN_BOM_VERSION } ; resolve the latest stable version from { BOM_POM_URL } ."
115 )
116
117
118 def _resolve_latest_bom_version (requested_version: str | None ) -> str :
119 if requested_version and requested_version.strip().lower() != LATEST_VERSION_SENTINEL :
120 raise ValueError (
121 "Explicit azure-sdk-bom version pins are not allowed in this migration flow. "
122 f "Use no version argument, or use ' { LATEST_VERSION_SENTINEL } ' for compatibility."
123 )
124
125 latest = _get_latest_bom_version()
126 _validate_minimum_bom_version(latest)
127 print ( f "[upgrade_bom] Resolved latest azure-sdk-bom version: { latest } " )
128 print ( f "[upgrade_bom] Source: { BOM_POM_URL } " )
129 return latest
130
131
132 # ---------------------------------------------------------------------------
133 # Maven helpers
134 # ---------------------------------------------------------------------------
135
136 def _detect_maven (project_dir: str ) -> str :
137 if sys.platform == "win32" :
138 wrapper = os.path.join(project_dir, "mvnw.cmd" )
139 # .cmd files on Windows are invoked by the shell; no executable bit needed.
140 if os.path.isfile(wrapper):
141 return wrapper
142 else :
143 wrapper = os.path.join(project_dir, "mvnw" )
144 if os.path.isfile(wrapper):
145 if not os.access(wrapper, os. X_OK ):
146 # Wrapper exists but isn't executable (common after fresh clones
147 # on filesystems that don't preserve the +x bit). Try to fix it.
148 try :
149 mode = os.stat(wrapper).st_mode
150 os.chmod(wrapper, mode | stat. S_IXUSR | stat. S_IXGRP | stat. S_IXOTH )
151 print ( f "[upgrade_bom] Added executable bit to { wrapper } ." )
152 except OSError as exc:
153 print (
154 f "[upgrade_bom] WARNING: mvnw exists at { wrapper } but is not "
155 f "executable and chmod failed ( { exc } ); falling back to 'mvn'." ,
156 file = sys.stderr,
157 )
158 return "mvn"
159 if os.access(wrapper, os. X_OK ):
160 return wrapper
161 return "mvn"
162
163
164 def _has_maven_bom_entry (pom_path: str ) -> bool :
165 try :
166 tree = ET .parse(pom_path)
167 except ET .ParseError:
168 return False
169 ns = { "m" : "http://maven.apache.org/POM/4.0.0" }
170 for dep in tree.findall( ".//m:dependencyManagement/m:dependencies/m:dependency" , ns):
171 gid = dep.find( "m:groupId" , ns)
172 aid = dep.find( "m:artifactId" , ns)
173 if gid is not None and aid is not None :
174 if gid.text == GROUP_ID and aid.text == ARTIFACT_ID :
175 return True
176 for dep in tree.findall( ".//dependencyManagement/dependencies/dependency" ):
177 gid = dep.find( "groupId" )
178 aid = dep.find( "artifactId" )
179 if gid is not None and aid is not None :
180 if gid.text == GROUP_ID and aid.text == ARTIFACT_ID :
181 return True
182 return False
183
184
185 def _run_maven_recipe (mvn_cmd: str , project_dir: str , recipe: str , options: str ) -> int :
186 """Run an OpenRewrite recipe via the rewrite-maven-plugin."""
187 cmd = [
188 mvn_cmd, "-U" ,
189 f " { MVN_REWRITE_PLUGIN } :run" ,
190 f "-Drewrite.recipeArtifactCoordinates= { MVN_REWRITE_ARTIFACT_COORDS } " ,
191 f "-Drewrite.activeRecipes= { recipe } " ,
192 f "-Drewrite.options= { options } " ,
193 ]
194 print ( f "[upgrade_bom] Running: { ' ' .join(cmd) } " )
195 return subprocess.run(cmd, cwd = project_dir).returncode
196
197
198 def _handle_maven (project_dir: str , bom_version: str , mvn_cmd: str | None ) -> int :
199 pom_path = os.path.join(project_dir, "pom.xml" )
200 mvn = mvn_cmd or _detect_maven(project_dir)
201
202 # Step 1: Add or upgrade the BOM
203 if not _has_maven_bom_entry(pom_path):
204 print ( "[upgrade_bom] No existing azure-sdk-bom entry found — adding via AddManagedDependency." )
205 options = "," .join([
206 f "groupId= { GROUP_ID } " ,
207 f "artifactId= { ARTIFACT_ID } " ,
208 f "version= { bom_version } " ,
209 "type=pom" ,
210 "scope=import" ,
211 ])
212 rc = _run_maven_recipe(mvn, project_dir, MVN_ADD_MANAGED_RECIPE , options)
213 if rc != 0 :
214 print ( f "[upgrade_bom] ERROR: AddManagedDependency exited with code { rc } " , file = sys.stderr)
215 return rc
216 print ( f "[upgrade_bom] azure-sdk-bom { bom_version } added successfully." )
217 else :
218 print ( f "[upgrade_bom] Existing azure-sdk-bom entry found — upgrading to { bom_version } ." )
219 options = "," .join([
220 f "groupId= { GROUP_ID } " ,
221 f "artifactId= { ARTIFACT_ID } " ,
222 f "newVersion= { bom_version } " ,
223 "overrideManagedVersion=true" ,
224 ])
225 rc = _run_maven_recipe(mvn, project_dir, MVN_UPGRADE_RECIPE , options)
226 if rc != 0 :
227 print ( f "[upgrade_bom] ERROR: UpgradeDependencyVersion exited with code { rc } " , file = sys.stderr)
228 return rc
229 print ( f "[upgrade_bom] azure-sdk-bom upgraded to { bom_version } successfully." )
230
231 # Step 2: Remove explicit versions from Azure deps managed by the BOM
232 print ( "[upgrade_bom] Removing redundant explicit versions for Azure dependencies..." )
233 options = f "groupPattern= { GROUP_ID } *,onlyIfManagedVersionIs=GTE"
234 rc = _run_maven_recipe(mvn, project_dir, MVN_REMOVE_REDUNDANT_RECIPE , options)
235 if rc != 0 :
236 print ( f "[upgrade_bom] WARNING: RemoveRedundantDependencyVersions exited with code { rc } " , file = sys.stderr)
237 else :
238 print ( "[upgrade_bom] Redundant explicit versions removed successfully." )
239 return rc
240
241
242 # ---------------------------------------------------------------------------
243 # Gradle helpers
244 # ---------------------------------------------------------------------------
245
246 def _detect_gradle (project_dir: str ) -> str :
247 if sys.platform == "win32" :
248 wrapper = os.path.join(project_dir, "gradlew.bat" )
249 if os.path.isfile(wrapper):
250 return wrapper
251 else :
252 wrapper = os.path.join(project_dir, "gradlew" )
253 if os.path.isfile(wrapper):
254 if not os.access(wrapper, os. X_OK ):
255 try :
256 mode = os.stat(wrapper).st_mode
257 os.chmod(wrapper, mode | stat. S_IXUSR | stat. S_IXGRP | stat. S_IXOTH )
258 print ( f "[upgrade_bom] Added executable bit to { wrapper } ." )
259 except OSError as exc:
260 print (
261 f "[upgrade_bom] WARNING: gradlew exists at { wrapper } but is not "
262 f "executable and chmod failed ( { exc } ); falling back to 'gradle'." ,
263 file = sys.stderr,
264 )
265 return "gradle"
266 if os.access(wrapper, os. X_OK ):
267 return wrapper
268 return "gradle"
269
270
271 def _find_gradle_build_file (project_dir: str ) -> str | None :
272 for name in ( "build.gradle" , "build.gradle.kts" ):
273 path = os.path.join(project_dir, name)
274 if os.path.isfile(path):
275 return path
276 return None
277
278
279 def _is_kotlin_dsl (build_file: str ) -> bool :
280 return build_file.endswith( ".kts" )
281
282
283 def _has_gradle_bom_entry (build_file: str ) -> bool :
284 """Check whether build.gradle already references azure-sdk-bom."""
285 try :
286 with open (build_file, "r" , encoding = "utf-8" ) as f:
287 content = f.read()
288 except OSError :
289 return False
290 return f " { GROUP_ID } : { ARTIFACT_ID } " in content
291
292
293 def _create_rewrite_yml (project_dir: str , bom_version: str , has_bom: bool ) -> str :
294 """Create a temporary rewrite.yml with the appropriate OpenRewrite recipes.
295
296 When has_bom is True, uses UpgradeDependencyVersion to upgrade the existing BOM.
297 When has_bom is False, uses AddPlatformDependency to add a new enforcedPlatform BOM.
298 Always includes RemoveRedundantDependencyVersions as a final step.
299 """
300 yml_path = os.path.join(project_dir, REWRITE_YML_NAME )
301 recipes: list[ str ] = []
302
303 if has_bom:
304 recipes.append(textwrap.dedent( f """ \
305 - org.openrewrite.gradle.UpgradeDependencyVersion:
306 groupId: { GROUP_ID }
307 artifactId: { ARTIFACT_ID }
308 newVersion: { bom_version } """ ))
309 else :
310 recipes.append(textwrap.dedent( f """ \
311 - org.openrewrite.gradle.AddPlatformDependency:
312 groupId: { GROUP_ID }
313 artifactId: { ARTIFACT_ID }
314 version: { bom_version }
315 configuration: implementation
316 enforced: true""" ))
317
318 recipes.append(textwrap.dedent( f """ \
319 - org.openrewrite.gradle.RemoveRedundantDependencyVersions:
320 groupPattern: { GROUP_ID } *
321 onlyIfManagedVersionIs: GTE""" ))
322
323 yml_content = textwrap.dedent( """ \
324 ---
325 type: specs.openrewrite.org/v1beta/recipe
326 name: com.azure.UpgradeBom
327 displayName: Upgrade azure-sdk-bom and remove redundant versions
328 recipeList:
329 """ ) + " \n " .join(recipes) + " \n "
330
331 with open (yml_path, "w" , encoding = "utf-8" ) as f:
332 f.write(yml_content)
333 print ( f "[upgrade_bom] Created { yml_path } " )
334 return yml_path
335
336
337 def _inject_gradle_rewrite_plugin (build_file: str ) -> bool :
338 """Temporarily add the OpenRewrite plugin to build.gradle if not present.
339
340 Returns True if the plugin block was injected (and should be cleaned up).
341 """
342 with open (build_file, "r" , encoding = "utf-8" ) as f:
343 content = f.read()
344
345 if "org.openrewrite.rewrite" in content:
346 return False
347
348 kotlin = _is_kotlin_dsl(build_file)
349 if kotlin:
350 plugin_line = ' id("org.openrewrite.rewrite") version "latest.release"'
351 else :
352 plugin_line = ' id "org.openrewrite.rewrite" version "latest.release"'
353
354 rewrite_block_kt = textwrap.dedent( """ \
355
356 rewrite {
357 activeRecipe("com.azure.UpgradeBom")
358 }
359
360 repositories {
361 mavenCentral()
362 }
363 """ )
364 rewrite_block_groovy = rewrite_block_kt # same syntax for both DSLs here
365
366 plugins_pattern = re.compile( r " ( plugins \s * \{ ) " , re. MULTILINE )
367 match = plugins_pattern.search(content)
368 if match:
369 insert_pos = match.end()
370 # content[insert_pos:] already starts with the newline that follows
371 # `plugins {`, so don't add another one before the marker.
372 content = (
373 content[:insert_pos]
374 + " \n "
375 + GRADLE_PLUGIN_MARKER
376 + " \n "
377 + plugin_line
378 + content[insert_pos:]
379 )
380 else :
381 # No plugins block — prepend one
382 content = (
383 "plugins { \n "
384 + GRADLE_PLUGIN_MARKER
385 + " \n "
386 + plugin_line
387 + " \n } \n\n "
388 + content
389 )
390
391 content += GRADLE_PLUGIN_MARKER + " \n "
392 content += rewrite_block_kt if kotlin else rewrite_block_groovy
393
394 with open (build_file, "w" , encoding = "utf-8" ) as f:
395 f.write(content)
396 print ( f "[upgrade_bom] Injected OpenRewrite plugin into { build_file } " )
397 return True
398
399
400 def _remove_gradle_rewrite_plugin (build_file: str ) -> None :
401 """Remove the temporarily injected OpenRewrite plugin and config blocks."""
402 with open (build_file, "r" , encoding = "utf-8" ) as f:
403 lines = f.readlines()
404
405 cleaned: list[ str ] = []
406 marker_count = 0
407 i = 0
408 while i < len (lines):
409 line = lines[i]
410 if GRADLE_PLUGIN_MARKER in line:
411 marker_count += 1
412 if marker_count == 1 :
413 # First marker (inside plugins {}): skip the marker line and
414 # the following injected plugin id line.
415 i += 2
416 continue
417 else :
418 # Second marker (at end of file): skip the marker and every
419 # remaining line — they're the injected rewrite {} and
420 # repositories {} blocks.
421 break
422 cleaned.append(line)
423 i += 1
424
425 with open (build_file, "w" , encoding = "utf-8" ) as f:
426 f.writelines(cleaned)
427 print ( f "[upgrade_bom] Cleaned up OpenRewrite plugin from { build_file } " )
428
429
430 def _run_gradle_openrewrite (gradle_cmd: str , project_dir: str ) -> int :
431 cmd = [gradle_cmd, "rewriteRun" ]
432 print ( f "[upgrade_bom] Running: { ' ' .join(cmd) } " )
433 return subprocess.run(cmd, cwd = project_dir).returncode
434
435
436 def _handle_gradle (project_dir: str , bom_version: str , gradle_cmd: str | None ) -> int :
437 build_file = _find_gradle_build_file(project_dir)
438 if build_file is None :
439 print ( "[upgrade_bom] ERROR: no build.gradle or build.gradle.kts found" , file = sys.stderr)
440 return 1
441
442 gradle = gradle_cmd or _detect_gradle(project_dir)
443 has_bom = _has_gradle_bom_entry(build_file)
444
445 if has_bom:
446 print ( f "[upgrade_bom] Existing azure-sdk-bom entry found — upgrading to { bom_version } ." )
447 else :
448 print ( "[upgrade_bom] No existing azure-sdk-bom entry found — adding via AddPlatformDependency." )
449
450 yml_path = None
451 injected = False
452
453 try :
454 # Set up OpenRewrite: create rewrite.yml + inject plugin temporarily
455 yml_path = _create_rewrite_yml(project_dir, bom_version, has_bom = has_bom)
456 injected = _inject_gradle_rewrite_plugin(build_file)
457 rc = _run_gradle_openrewrite(gradle, project_dir)
458 finally :
459 if yml_path and os.path.isfile(yml_path):
460 os.remove(yml_path)
461 print ( f "[upgrade_bom] Removed { yml_path } " )
462 if injected:
463 _remove_gradle_rewrite_plugin(build_file)
464
465 if rc != 0 :
466 print ( f "[upgrade_bom] ERROR: OpenRewrite exited with code { rc } " , file = sys.stderr)
467 else :
468 print ( f "[upgrade_bom] BOM set to { bom_version } and redundant versions removed successfully." )
469 return rc
470
471
472 # ---------------------------------------------------------------------------
473 # Main
474 # ---------------------------------------------------------------------------
475
476 def main (argv: list[ str ] | None = None ) -> int :
477 parser = argparse.ArgumentParser(
478 description = "Upgrade azure-sdk-bom version in a Maven or Gradle project using OpenRewrite."
479 )
480 parser.add_argument( "project_dir" , nargs = "?" , help = "Path to the project root." )
481 parser.add_argument(
482 "bom_version" ,
483 nargs = "?" ,
484 help = "Optional compatibility token. Only 'latest' is accepted; explicit versions are rejected." ,
485 )
486 parser.add_argument( "--mvn" , default = None , help = "Maven command override." )
487 parser.add_argument( "--gradle" , default = None , help = "Gradle command override." )
488 parser.add_argument(
489 "--get-latest-version" ,
490 action = "store_true" ,
491 help = "Print the latest azure-sdk-bom version from the Azure SDK for Java BOM pom.xml." ,
492 )
493 args = parser.parse_args(argv)
494
495 if args.get_latest_version:
496 if args.project_dir or args.bom_version:
497 parser.error( "--get-latest-version does not accept project_dir or bom_version." )
498 print (_get_latest_bom_version())
499 return 0
500
501 if not args.project_dir:
502 parser.error( "project_dir is required unless --get-latest-version is used." )
503
504 try :
505 bom_version = _resolve_latest_bom_version(args.bom_version)
506 except ValueError as exc:
507 raise SystemExit ( f "[upgrade_bom] ERROR: { exc } " ) from exc
508
509 project_dir = os.path.abspath(args.project_dir)
510 build_system = _detect_build_system(project_dir)
511
512 if build_system == "maven" :
513 print ( "[upgrade_bom] Detected Maven project." )
514 return _handle_maven(project_dir, bom_version, args.mvn)
515 elif build_system == "gradle" :
516 print ( "[upgrade_bom] Detected Gradle project." )
517 return _handle_gradle(project_dir, bom_version, args.gradle)
518 else :
519 print (
520 f "[upgrade_bom] ERROR: No pom.xml or build.gradle found in { project_dir } " ,
521 file = sys.stderr,
522 )
523 return 1
524
525
526 if __name__ == "__main__" :
527 sys.exit(main())