Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import platform
import re
from typing import Optional

Expand All @@ -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)

Expand All @@ -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:
Expand All @@ -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),
)
Expand All @@ -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 []
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading