From 176ad544b711d3314a00b9059811ba8e19c6d5d6 Mon Sep 17 00:00:00 2001 From: "omer.roth" Date: Thu, 9 Jul 2026 14:13:33 +0300 Subject: [PATCH] CM-68462 added support for gradlew --- .../sca/maven/restore_gradle_dependencies.py | 22 +++++-- .../maven/test_restore_gradle_dependencies.py | 58 +++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py b/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py index d2687bf6..ea91a8de 100644 --- a/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +++ b/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py @@ -1,4 +1,5 @@ import os +import platform import re from typing import Optional @@ -12,19 +13,32 @@ BUILD_GRADLE_FILE_NAME = 'build.gradle' BUILD_GRADLE_KTS_FILE_NAME = 'build.gradle.kts' BUILD_GRADLE_DEP_TREE_FILE_NAME = 'gradle-dependencies-generated.txt' -BUILD_GRADLE_ALL_PROJECTS_COMMAND = ['gradle', 'projects'] ALL_PROJECTS_REGEX = r"[+-]{3} Project '(.*?)'" +GRADLE_EXECUTABLE = 'gradle' +GRADLEW_FILE_NAME = 'gradlew' +GRADLEW_BAT_FILE_NAME = 'gradlew.bat' + class RestoreGradleDependencies(BaseRestoreDependencies): def __init__( self, ctx: typer.Context, is_git_diff: bool, command_timeout: int, projects: Optional[set[str]] = None ) -> None: super().__init__(ctx, is_git_diff, command_timeout, create_output_file_manually=True) + self.gradle_executable = self._resolve_gradle_executable() if projects is None: projects = set() self.projects = self.get_all_projects() if self.is_gradle_sub_projects() else projects + def _resolve_gradle_executable(self) -> str: + scan_root = get_path_from_context(self.ctx) + if scan_root: + wrapper_name = GRADLEW_BAT_FILE_NAME if platform.system() == 'Windows' else GRADLEW_FILE_NAME + wrapper_path = os.path.join(scan_root, wrapper_name) + if os.path.isfile(wrapper_path): + return wrapper_path + return GRADLE_EXECUTABLE + def is_gradle_sub_projects(self) -> bool: return self.ctx.obj.get('gradle_all_sub_projects', False) @@ -35,7 +49,7 @@ def get_commands(self, manifest_file_path: str) -> list[list[str]]: return ( self.get_commands_for_sub_projects(manifest_file_path) if self.is_gradle_sub_projects() - else [['gradle', 'dependencies', '-b', manifest_file_path, '-q', '--console', 'plain']] + else [[self.gradle_executable, 'dependencies', '-b', manifest_file_path, '-q', '--console', 'plain']] ) def get_lock_file_name(self) -> str: @@ -49,7 +63,7 @@ def get_working_directory(self, document: Document) -> Optional[str]: def get_all_projects(self) -> set[str]: output = shell( - command=BUILD_GRADLE_ALL_PROJECTS_COMMAND, + command=[self.gradle_executable, 'projects'], timeout=self.command_timeout, working_directory=get_path_from_context(self.ctx), ) @@ -62,7 +76,7 @@ def get_commands_for_sub_projects(self, manifest_file_path: str) -> list[list[st project_name = os.path.basename(os.path.dirname(manifest_file_path)) project_name = f':{project_name}' return ( - [['gradle', f'{project_name}:dependencies', '-q', '--console', 'plain']] + [[self.gradle_executable, f'{project_name}:dependencies', '-q', '--console', 'plain']] if project_name in self.projects else [] ) diff --git a/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py b/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py index 72ca8a7d..43d34e30 100644 --- a/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py +++ b/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py @@ -9,11 +9,15 @@ BUILD_GRADLE_DEP_TREE_FILE_NAME, BUILD_GRADLE_FILE_NAME, BUILD_GRADLE_KTS_FILE_NAME, + GRADLE_EXECUTABLE, + GRADLEW_BAT_FILE_NAME, + GRADLEW_FILE_NAME, RestoreGradleDependencies, ) from cycode.cli.models import Document _BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' +_GRADLE_MODULE = 'cycode.cli.files_collector.sca.maven.restore_gradle_dependencies' @pytest.fixture @@ -47,6 +51,60 @@ def test_settings_gradle_does_not_match(self, restore_gradle: RestoreGradleDepen assert restore_gradle.is_project(doc) is False +class TestResolveGradleExecutable: + def test_falls_back_to_gradle_when_no_wrapper(self, restore_gradle: RestoreGradleDependencies) -> None: + assert restore_gradle.gradle_executable == GRADLE_EXECUTABLE + + def test_prefers_gradlew_wrapper_on_posix(self, mock_ctx: typer.Context, tmp_path: Path) -> None: + wrapper = tmp_path / GRADLEW_FILE_NAME + wrapper.write_text('#!/bin/sh\n') + with patch(f'{_GRADLE_MODULE}.platform.system', return_value='Linux'): + restore = RestoreGradleDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + assert restore.gradle_executable == str(wrapper) + + def test_prefers_gradlew_bat_on_windows(self, mock_ctx: typer.Context, tmp_path: Path) -> None: + wrapper = tmp_path / GRADLEW_BAT_FILE_NAME + wrapper.write_text('@echo off\n') + with patch(f'{_GRADLE_MODULE}.platform.system', return_value='Windows'): + restore = RestoreGradleDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + assert restore.gradle_executable == str(wrapper) + + def test_posix_ignores_bat_wrapper(self, mock_ctx: typer.Context, tmp_path: Path) -> None: + (tmp_path / GRADLEW_BAT_FILE_NAME).write_text('@echo off\n') + with patch(f'{_GRADLE_MODULE}.platform.system', return_value='Linux'): + restore = RestoreGradleDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + assert restore.gradle_executable == GRADLE_EXECUTABLE + + def test_wrapper_is_threaded_into_get_commands(self, mock_ctx: typer.Context, tmp_path: Path) -> None: + wrapper = tmp_path / GRADLEW_FILE_NAME + wrapper.write_text('#!/bin/sh\n') + with patch(f'{_GRADLE_MODULE}.platform.system', return_value='Linux'): + restore = RestoreGradleDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + commands = restore.get_commands(str(tmp_path / BUILD_GRADLE_FILE_NAME)) + assert commands == [ + [str(wrapper), 'dependencies', '-b', str(tmp_path / BUILD_GRADLE_FILE_NAME), '-q', '--console', 'plain'] + ] + + def test_wrapper_is_threaded_into_sub_project_commands(self, tmp_path: Path) -> None: + wrapper = tmp_path / GRADLEW_FILE_NAME + wrapper.write_text('#!/bin/sh\n') + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': False, 'gradle_all_sub_projects': True} + ctx.params = {'path': str(tmp_path)} + module_project = tmp_path / 'module-a' + module_project.mkdir() + manifest = module_project / BUILD_GRADLE_FILE_NAME + + with ( + patch(f'{_GRADLE_MODULE}.platform.system', return_value='Linux'), + patch.object(RestoreGradleDependencies, 'get_all_projects', return_value={':module-a'}), + ): + restore = RestoreGradleDependencies(ctx, is_git_diff=False, command_timeout=30) + + commands = restore.get_commands_for_sub_projects(str(manifest)) + assert commands == [[str(wrapper), ':module-a:dependencies', '-q', '--console', 'plain']] + + class TestCleanup: def test_generated_dep_tree_file_is_deleted_after_restore( self, restore_gradle: RestoreGradleDependencies, tmp_path: Path