Skip to content
Open
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
1 change: 1 addition & 0 deletions CLI_ARGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
| `--sonar-scanner-arch`, `-Dsonar.scanner.arch` | Architecture on which the scanner will be running |
| `--sonar-scanner-cloud-url`, `-Dsonar.scanner.cloudUrl` | SonarQube Cloud base URL, https://sonarcloud.io for example |
| `--sonar-scanner-connect-timeout`, `-Dsonar.scanner.connectTimeout` | Time period to establish connections with the server (in seconds) |
| `--sonar-scanner-engine-jar-path`, `-Dsonar.scanner.engineJarPath` | Path to a local scanner engine JAR. If set, the scanner engine will not be downloaded from the server |
| `--sonar-scanner-internal-dump-to-file`, `-Dsonar.scanner.internal.dumpToFile` | Filename where the input to the scanner engine will be dumped. Useful for debugging |
| `--sonar-scanner-internal-sq-version`, `-Dsonar.scanner.internal.sqVersion` | Emulate the result of the call to get SQ server version. Useful for debugging with --sonar-scanner-internal-dump-to-file |
| `--sonar-scanner-java-exe-path`, `-Dsonar.scanner.javaExePath` | If defined, the scanner engine will be run with this JRE |
Expand Down
25 changes: 24 additions & 1 deletion src/pysonar_scanner/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#

import logging
import os
import pathlib
from typing import Any
from pysonar_scanner import app_logging
from pysonar_scanner import cache
Expand All @@ -36,6 +38,7 @@
SONAR_SCANNER_OS,
SONAR_SCANNER_ARCH,
SONAR_SCANNER_DRY_RUN,
SONAR_SCANNER_ENGINE_JAR_PATH,
SONAR_PROJECT_BASE_DIR,
SONAR_PYTHON_COVERAGE_REPORT_PATHS,
)
Expand Down Expand Up @@ -116,14 +119,34 @@ def update_config_with_api_urls(config, base_urls: BaseUrls):


def create_scanner_engine(api, cache_manager, config):
configured_scanner_engine_path = get_configured_scanner_engine_path(config)
jre_path = create_jre(api, cache_manager, config)
config[SONAR_SCANNER_JAVA_EXE_PATH] = str(jre_path.path)
logging.debug(f"JRE path: {jre_path.path}")
scanner_engine_path = ScannerEngineProvisioner(api, cache_manager).provision()
scanner_engine_path = configured_scanner_engine_path or ScannerEngineProvisioner(api, cache_manager).provision()
scanner = ScannerEngine(jre_path, scanner_engine_path)
return scanner


def get_configured_scanner_engine_path(config: dict[str, Any]) -> pathlib.Path | None:
if not config.get(SONAR_SCANNER_ENGINE_JAR_PATH):
return None

scanner_engine_path = pathlib.Path(config[SONAR_SCANNER_ENGINE_JAR_PATH])
if not scanner_engine_path.is_file():
raise exceptions.InconsistentConfiguration(
f"Configured scanner engine JAR does not exist or is not a file: {scanner_engine_path}. "
f"Please check the '{SONAR_SCANNER_ENGINE_JAR_PATH}' property."
)
if not os.access(scanner_engine_path, os.R_OK):
raise exceptions.InconsistentConfiguration(
f"Configured scanner engine JAR is not readable: {scanner_engine_path}. "
f"Please check file permissions for the '{SONAR_SCANNER_ENGINE_JAR_PATH}' property."
)
logging.debug(f"Using local scanner engine JAR: {scanner_engine_path}")
return scanner_engine_path


def create_jre(api, cache, config: dict[str, Any]) -> JREResolvedPath:
jre_provisioner = JREProvisioner(api, cache, config[SONAR_SCANNER_OS], config[SONAR_SCANNER_ARCH])
jre_resolver = JREResolver(JREResolverConfiguration.from_dict(config), jre_provisioner)
Expand Down
6 changes: 6 additions & 0 deletions src/pysonar_scanner/configuration/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,12 @@ def __create_parser(cls):
type=str,
help="Arguments specifies the heap size provided to the JVM when running the scanner",
)
jvm_group.add_argument(
"--sonar-scanner-engine-jar-path",
"-Dsonar.scanner.engineJarPath",
type=str,
help="Path to a local scanner engine JAR. If set, the scanner engine will not be downloaded from the server",
)

truststore_group = parser.add_argument_group("Truststore arguments")
truststore_group.add_argument(
Expand Down
6 changes: 6 additions & 0 deletions src/pysonar_scanner/configuration/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
SONAR_SCANNER_METADATA_FILEPATH: Key = "sonar.scanner.metadataFilePath"
SONAR_SCANNER_JAVA_OPTS: Key = "sonar.scanner.javaOpts"
SONAR_SCANNER_JAVA_HEAP_SIZE: Key = "sonar.scanner.javaHeapSize"
SONAR_SCANNER_ENGINE_JAR_PATH: Key = "sonar.scanner.engineJarPath"
SONAR_PROJECT_BASE_DIR: Key = "sonar.projectBaseDir"
SONAR_PROJECT_KEY: Key = "sonar.projectKey"
SONAR_PROJECT_NAME: Key = "sonar.projectName"
Expand Down Expand Up @@ -315,6 +316,11 @@ def env_variable_name(self) -> str:
default_value=None,
cli_getter=lambda args: args.sonar_scanner_java_heap_size
),
Property(
name=SONAR_SCANNER_ENGINE_JAR_PATH,
default_value=None,
cli_getter=lambda args: args.sonar_scanner_engine_jar_path
),
Property(
name=SONAR_SCANNER_METADATA_FILEPATH,
default_value=None,
Expand Down
5 changes: 4 additions & 1 deletion tests/test_environment_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
SONAR_HOST_URL,
SONAR_REGION,
SONAR_SCANNER_ARCH,
SONAR_SCANNER_ENGINE_JAR_PATH,
SONAR_SCANNER_JAVA_OPTS,
SONAR_SCANNER_OS,
SONAR_TOKEN,
Expand All @@ -51,6 +52,7 @@ def test__environment_variables(self):
"SONAR_HOST_URL": "https://sonarqube.example.com",
"SONAR_USER_HOME": "/custom/sonar/home",
"SONAR_SCANNER_JAVA_OPTS": "-Xmx1024m -XX:MaxPermSize=256m",
"SONAR_SCANNER_ENGINE_JAR_PATH": "/path/to/scanner-engine.jar",
"SONAR_REGION": "us",
}
with patch.dict("os.environ", env, clear=True):
Expand All @@ -60,9 +62,10 @@ def test__environment_variables(self):
SONAR_HOST_URL: "https://sonarqube.example.com",
SONAR_USER_HOME: "/custom/sonar/home",
SONAR_SCANNER_JAVA_OPTS: "-Xmx1024m -XX:MaxPermSize=256m",
SONAR_SCANNER_ENGINE_JAR_PATH: "/path/to/scanner-engine.jar",
SONAR_REGION: "us",
}
self.assertEqual(len(properties), 5)
self.assertEqual(len(properties), 6)
self.assertDictEqual(properties, expected_properties)

def test_irrelevant_environment_variables(self):
Expand Down
3 changes: 3 additions & 0 deletions tests/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
SONAR_SCANNER_SOCKET_TIMEOUT,
SONAR_SCANNER_PROXY_PASSWORD,
SONAR_SCANNER_INTERNAL_DUMP_TO_FILE,
SONAR_SCANNER_ENGINE_JAR_PATH,
SONAR_PROJECT_KEY,
SONAR_PROJECT_BASE_DIR,
PROPERTIES,
Expand All @@ -47,6 +48,7 @@ def test_python_name_conversion(self):
(SONAR_SCANNER_APP_VERSION, "sonar.scanner.app-version"),
(SONAR_SCANNER_SOCKET_TIMEOUT, "sonar.scanner.socket-timeout"),
(SONAR_SCANNER_PROXY_PASSWORD, "sonar.scanner.proxy-password"),
(SONAR_SCANNER_ENGINE_JAR_PATH, "sonar.scanner.engine-jar-path"),
# Complex properties
(SONAR_SCANNER_INTERNAL_DUMP_TO_FILE, "sonar.scanner.internal.dump-to-file"),
(SONAR_PROJECT_KEY, "sonar.project-key"),
Expand Down Expand Up @@ -82,6 +84,7 @@ def test_env_variable_name_conversion(self):
(SONAR_SCANNER_APP_VERSION, "SONAR_SCANNER_APP_VERSION"),
(SONAR_SCANNER_SOCKET_TIMEOUT, "SONAR_SCANNER_SOCKET_TIMEOUT"),
(SONAR_SCANNER_PROXY_PASSWORD, "SONAR_SCANNER_PROXY_PASSWORD"),
(SONAR_SCANNER_ENGINE_JAR_PATH, "SONAR_SCANNER_ENGINE_JAR_PATH"),
# Complex properties
(SONAR_SCANNER_INTERNAL_DUMP_TO_FILE, "SONAR_SCANNER_INTERNAL_DUMP_TO_FILE"),
(SONAR_PROJECT_KEY, "SONAR_PROJECT_KEY"),
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/test_configuration_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
SONAR_SCANNER_API_BASE_URL,
SONAR_SCANNER_ARCH,
SONAR_SCANNER_CONNECT_TIMEOUT,
SONAR_SCANNER_ENGINE_JAR_PATH,
SONAR_SCANNER_INTERNAL_DUMP_TO_FILE,
SONAR_SCANNER_INTERNAL_SQ_VERSION,
SONAR_SCANNER_JAVA_EXE_PATH,
Expand Down Expand Up @@ -130,6 +131,7 @@
SONAR_SCANNER_JAVA_EXE_PATH: "mySonarScannerJavaExePath",
SONAR_SCANNER_JAVA_OPTS: "mySonarScannerJavaOpts",
SONAR_SCANNER_JAVA_HEAP_SIZE: "8000Mb",
SONAR_SCANNER_ENGINE_JAR_PATH: "mySonarScannerEngineJarPath",
SONAR_SCANNER_METADATA_FILEPATH: "myMetadataFilepath",
SONAR_REGION: "us",
SONAR_ORGANIZATION: "mySonarOrganization",
Expand Down Expand Up @@ -343,6 +345,8 @@ def test_impossible_os_choice(self):
"mySonarScannerJavaExePath",
"--sonar-scanner-java-opts",
"mySonarScannerJavaOpts",
"--sonar-scanner-engine-jar-path",
"mySonarScannerEngineJarPath",
"--sonar-scanner-metadata-filepath",
"myMetadataFilepath",
"--sonar-scanner-internal-dump-to-file",
Expand Down Expand Up @@ -480,6 +484,7 @@ def test_all_cli_args(self):
"-Dsonar.scanner.skipJreProvisioning",
"-Dsonar.scanner.javaExePath=mySonarScannerJavaExePath",
"-Dsonar.scanner.javaOpts=mySonarScannerJavaOpts",
"-Dsonar.scanner.engineJarPath=mySonarScannerEngineJarPath",
"-Dsonar.scanner.metadataFilepath=myMetadataFilepath",
"-Dsonar.region=us",
"-Dsonar.organization=mySonarOrganization",
Expand Down
37 changes: 35 additions & 2 deletions tests/unit/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
import pathlib
import tempfile
from unittest.mock import patch, Mock, call

from pyfakefs import fake_filesystem_unittest as pyfakefs

from pysonar_scanner.__main__ import scan, main, check_version, create_jre
from pysonar_scanner.__main__ import scan, main, check_version, create_jre, create_scanner_engine
from pysonar_scanner.api import SQVersion, SonarQubeApi
from pysonar_scanner.cache import Cache
from pysonar_scanner.configuration.configuration_loader import ConfigurationLoader
Expand All @@ -35,9 +36,10 @@
SONAR_SCANNER_PROXY_PORT,
SONAR_SCANNER_OS,
SONAR_SCANNER_ARCH,
SONAR_SCANNER_ENGINE_JAR_PATH,
SONAR_SCANNER_JAVA_EXE_PATH,
)
from pysonar_scanner.exceptions import SQTooOldException
from pysonar_scanner.exceptions import InconsistentConfiguration, SQTooOldException
from pysonar_scanner.jre import JREResolvedPath, JREResolver
from pysonar_scanner.scannerengine import ScannerEngine, ScannerEngineProvisioner
from tests.unit import sq_api_utils
Expand Down Expand Up @@ -137,3 +139,34 @@ def test_get_jre(self, resolve_jre_mock, cmd_executor_mock):
api = SonarQubeApi(Mock(), Mock())
cache = Cache(Mock())
create_jre(api, cache, {SONAR_SCANNER_OS: "linux", SONAR_SCANNER_ARCH: "x64"})

@patch.object(ScannerEngineProvisioner, "provision")
@patch("pysonar_scanner.__main__.create_jre", return_value=JREResolvedPath(pathlib.Path("jre/bin/java")))
def test_create_scanner_engine_uses_local_engine_jar_path(self, create_jre_mock, provision_mock):
with tempfile.NamedTemporaryFile(suffix=".jar") as engine_jar:
config = {
SONAR_SCANNER_OS: "linux",
SONAR_SCANNER_ARCH: "x64",
SONAR_SCANNER_ENGINE_JAR_PATH: engine_jar.name,
}

scanner = create_scanner_engine(Mock(), Mock(), config)

self.assertEqual(scanner.scanner_engine_path, pathlib.Path(engine_jar.name))
self.assertEqual(pathlib.Path(config[SONAR_SCANNER_JAVA_EXE_PATH]), pathlib.Path("jre/bin/java"))
provision_mock.assert_not_called()

@patch.object(ScannerEngineProvisioner, "provision")
@patch("pysonar_scanner.__main__.create_jre")
def test_create_scanner_engine_fails_when_local_engine_jar_path_is_missing(self, create_jre_mock, provision_mock):
config = {
SONAR_SCANNER_OS: "linux",
SONAR_SCANNER_ARCH: "x64",
SONAR_SCANNER_ENGINE_JAR_PATH: "/path/to/missing-scanner-engine.jar",
}

with self.assertRaisesRegex(InconsistentConfiguration, "Configured scanner engine JAR does not exist"):
create_scanner_engine(Mock(), Mock(), config)

create_jre_mock.assert_not_called()
provision_mock.assert_not_called()
Loading