diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 0f5086709da2f..533bf64dd9218 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -151,6 +151,7 @@ dependencies { implementation(libs.clikt) implementation(libs.hikari) implementation(libs.jacksonModuleKotlin) + implementation(libs.koinCore) implementation(libs.kotlinxCoroutines) implementation(libs.kotlinxSerialization) implementation(libs.log4jApiToSlf4j) diff --git a/cli/src/main/kotlin/OrtMain.kt b/cli/src/main/kotlin/OrtMain.kt index 7c5816d7c46cb..938642d7bb85b 100644 --- a/cli/src/main/kotlin/OrtMain.kt +++ b/cli/src/main/kotlin/OrtMain.kt @@ -41,9 +41,11 @@ import java.io.File import kotlin.system.exitProcess +import org.koin.core.context.GlobalContext.startKoin +import org.koin.dsl.module + import org.ossreviewtoolkit.cli.commands.* import org.ossreviewtoolkit.cli.utils.logger -import org.ossreviewtoolkit.model.config.LicenseFilenamePatterns import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.utils.common.Os import org.ossreviewtoolkit.utils.common.expandTilde @@ -70,7 +72,6 @@ sealed interface GroupTypes { * Helper class for collecting options that can be passed to subcommands. */ data class GlobalOptions( - val config: OrtConfiguration, val forceOverwrite: Boolean ) @@ -179,13 +180,21 @@ class OrtMain : CliktCommand(name = ORT_NAME, invokeWithoutSubcommand = true) { logger.debug { "Used command line arguments: ${currentContext.originalArgv}" } + val ortConfigModule = module { + single { + OrtConfiguration.load(configArguments, configFile) + } + } + + startKoin { + modules(ortConfigModule) + } + // Make the parameter globally available. printStackTrace = stacktrace // Make options available to subcommands and apply static configuration. - val ortConfiguration = OrtConfiguration.load(configArguments, configFile) - currentContext.findOrSetObject { GlobalOptions(ortConfiguration, forceOverwrite) } - LicenseFilenamePatterns.configure(ortConfiguration.licenseFilePatterns) + currentContext.findOrSetObject { GlobalOptions(forceOverwrite) } if (helpAll) { registeredSubcommands().forEach { diff --git a/cli/src/main/kotlin/commands/AdvisorCommand.kt b/cli/src/main/kotlin/commands/AdvisorCommand.kt index 20c0ee01ec60d..f8d8c96338232 100644 --- a/cli/src/main/kotlin/commands/AdvisorCommand.kt +++ b/cli/src/main/kotlin/commands/AdvisorCommand.kt @@ -34,6 +34,9 @@ import com.github.ajalt.clikt.parameters.options.split import com.github.ajalt.clikt.parameters.types.enum import com.github.ajalt.clikt.parameters.types.file +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.advisor.Advisor import org.ossreviewtoolkit.cli.GlobalOptions import org.ossreviewtoolkit.cli.utils.SeverityStats @@ -42,6 +45,7 @@ import org.ossreviewtoolkit.cli.utils.outputGroup import org.ossreviewtoolkit.cli.utils.readOrtResult import org.ossreviewtoolkit.cli.utils.writeOrtResult import org.ossreviewtoolkit.model.FileFormat +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.utils.DefaultResolutionProvider import org.ossreviewtoolkit.model.utils.mergeLabels import org.ossreviewtoolkit.utils.common.expandTilde @@ -49,7 +53,10 @@ import org.ossreviewtoolkit.utils.common.safeMkdirs import org.ossreviewtoolkit.utils.ort.ORT_RESOLUTIONS_FILENAME import org.ossreviewtoolkit.utils.ort.ortConfigDirectory -class AdvisorCommand : CliktCommand(name = "advise", help = "Check dependencies for security vulnerabilities.") { +class AdvisorCommand : KoinComponent, CliktCommand( + name = "advise", + help = "Check dependencies for security vulnerabilities." +) { private val ortFile by option( "--ort-file", "-i", help = "An ORT result file with an analyzer result to use." @@ -100,6 +107,7 @@ class AdvisorCommand : CliktCommand(name = "advise", help = "Check dependencies ).flag() private val globalOptionsForSubcommands by requireObject() + private val ortConfig by inject() override fun run() { val outputFiles = outputFormats.mapTo(mutableSetOf()) { format -> @@ -117,8 +125,7 @@ class AdvisorCommand : CliktCommand(name = "advise", help = "Check dependencies println("The following advisors are activated:") println("\t" + distinctProviders.joinToString()) - val config = globalOptionsForSubcommands.config - val advisor = Advisor(distinctProviders, config.advisor) + val advisor = Advisor(distinctProviders, ortConfig.advisor) val ortResultInput = readOrtResult(ortFile) val ortResultOutput = advisor.retrieveFindings(ortResultInput, skipExcluded).mergeLabels(labels) @@ -138,6 +145,6 @@ class AdvisorCommand : CliktCommand(name = "advise", help = "Check dependencies advisorResults.collectIssues().flatMap { it.value }.partition { resolutionProvider.isResolved(it) } val severityStats = SeverityStats.createFromIssues(resolvedIssues, unresolvedIssues) - severityStats.print().conclude(config.severeIssueThreshold, 2) + severityStats.print().conclude(ortConfig.severeIssueThreshold, 2) } } diff --git a/cli/src/main/kotlin/commands/AnalyzerCommand.kt b/cli/src/main/kotlin/commands/AnalyzerCommand.kt index eab9e59111990..ad6feb23513b2 100644 --- a/cli/src/main/kotlin/commands/AnalyzerCommand.kt +++ b/cli/src/main/kotlin/commands/AnalyzerCommand.kt @@ -35,6 +35,9 @@ import com.github.ajalt.clikt.parameters.options.split import com.github.ajalt.clikt.parameters.types.enum import com.github.ajalt.clikt.parameters.types.file +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.analyzer.Analyzer import org.ossreviewtoolkit.analyzer.PackageManager import org.ossreviewtoolkit.analyzer.PackageManagerFactory @@ -54,6 +57,7 @@ import org.ossreviewtoolkit.cli.utils.outputGroup import org.ossreviewtoolkit.cli.utils.writeOrtResult import org.ossreviewtoolkit.model.FileFormat import org.ossreviewtoolkit.model.config.AnalyzerConfiguration +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.config.RepositoryConfiguration import org.ossreviewtoolkit.model.readValueOrNull import org.ossreviewtoolkit.model.utils.DefaultResolutionProvider @@ -66,7 +70,10 @@ import org.ossreviewtoolkit.utils.ort.ORT_REPO_CONFIG_FILENAME import org.ossreviewtoolkit.utils.ort.ORT_RESOLUTIONS_FILENAME import org.ossreviewtoolkit.utils.ort.ortConfigDirectory -class AnalyzerCommand : CliktCommand(name = "analyze", help = "Determine dependencies of a software project.") { +class AnalyzerCommand : KoinComponent, CliktCommand( + name = "analyze", + help = "Determine dependencies of a software project." +) { private val inputDir by option( "--input-dir", "-i", help = "The project directory to analyze. As a special case, if only one package manager is enabled, this " + @@ -177,6 +184,7 @@ class AnalyzerCommand : CliktCommand(name = "analyze", help = "Determine depende ) private val globalOptionsForSubcommands by requireObject() + private val ortConfig by inject() override fun run() { val outputFiles = outputFormats.mapTo(mutableSetOf()) { format -> @@ -209,12 +217,10 @@ class AnalyzerCommand : CliktCommand(name = "analyze", help = "Determine depende println("Looking for analyzer-specific configuration in the following files and directories:") println("\t" + configurationInfo) - val config = globalOptionsForSubcommands.config - val enabledPackageManagers = if (enabledPackageManagers != null || disabledPackageManagers != null) { (enabledPackageManagers ?: PackageManager.ALL.values).toSet() - disabledPackageManagers.orEmpty().toSet() } else { - config.analyzer.determineEnabledPackageManagers() + ortConfig.analyzer.determineEnabledPackageManagers() } println("The following package managers are enabled:") @@ -226,7 +232,7 @@ class AnalyzerCommand : CliktCommand(name = "analyze", help = "Determine depende ?: RepositoryConfiguration() val analyzerConfiguration = - repositoryConfiguration.analyzer?.let { config.analyzer.merge(it) } ?: config.analyzer + repositoryConfiguration.analyzer?.let { ortConfig.analyzer.merge(it) } ?: ortConfig.analyzer val analyzer = Analyzer(analyzerConfiguration, labels) @@ -237,7 +243,7 @@ class AnalyzerCommand : CliktCommand(name = "analyze", help = "Determine depende val repositoryPackageCurations = repositoryConfiguration.curations.packages - if (config.enableRepositoryPackageCurations) { + if (ortConfig.enableRepositoryPackageCurations) { add(SimplePackageCurationProvider(repositoryPackageCurations)) } else if (repositoryPackageCurations.isNotEmpty()) { logger.warn { @@ -249,7 +255,7 @@ class AnalyzerCommand : CliktCommand(name = "analyze", help = "Determine depende val curationProviders = listOfNotNull( CompositePackageCurationProvider(defaultCurationProviders), - config.analyzer.sw360Configuration?.let { + ortConfig.analyzer.sw360Configuration?.let { Sw360PackageCurationProvider(it).takeIf { useSw360Curations } }, ClearlyDefinedPackageCurationProvider().takeIf { useClearlyDefinedCurations } @@ -301,7 +307,7 @@ class AnalyzerCommand : CliktCommand(name = "analyze", help = "Determine depende analyzerResult.collectIssues().flatMap { it.value }.partition { resolutionProvider.isResolved(it) } val severityStats = SeverityStats.createFromIssues(resolvedIssues, unresolvedIssues) - severityStats.print().conclude(config.severeIssueThreshold, 2) + severityStats.print().conclude(ortConfig.severeIssueThreshold, 2) } } diff --git a/cli/src/main/kotlin/commands/ConfigCommand.kt b/cli/src/main/kotlin/commands/ConfigCommand.kt index 44d3d33343c60..1ee43d87f8eaa 100644 --- a/cli/src/main/kotlin/commands/ConfigCommand.kt +++ b/cli/src/main/kotlin/commands/ConfigCommand.kt @@ -23,16 +23,20 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option -import org.ossreviewtoolkit.cli.GlobalOptions +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.config.OrtConfigurationWrapper import org.ossreviewtoolkit.model.config.REFERENCE_CONFIG_FILENAME -class ConfigCommand : CliktCommand(name = "config", help = "Show different ORT configurations") { +class ConfigCommand : KoinComponent, CliktCommand( + name = "config", + help = "Show different ORT configurations" +) { private val showDefault by option( "--show-default", help = "Show the default configuration used when no custom configuration is present." @@ -49,7 +53,7 @@ class ConfigCommand : CliktCommand(name = "config", help = "Show different ORT c "example entries for all supported configuration options." ).flag() - private val globalOptionsForSubcommands by requireObject() + private val ortConfig by inject() private val mapper = YAMLMapper().apply { registerKotlinModule() @@ -68,7 +72,7 @@ class ConfigCommand : CliktCommand(name = "config", help = "Show different ORT c if (showActive) { println("The active configuration is:") println() - println(globalOptionsForSubcommands.config.renderYaml()) + println(ortConfig.renderYaml()) } if (showReference) { diff --git a/cli/src/main/kotlin/commands/DownloaderCommand.kt b/cli/src/main/kotlin/commands/DownloaderCommand.kt index 6defe7191ca9e..d31035b0e0e5d 100644 --- a/cli/src/main/kotlin/commands/DownloaderCommand.kt +++ b/cli/src/main/kotlin/commands/DownloaderCommand.kt @@ -21,7 +21,6 @@ package org.ossreviewtoolkit.cli.commands import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.ProgramResult -import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.parameters.groups.default import com.github.ajalt.clikt.parameters.groups.mutuallyExclusiveOptions import com.github.ajalt.clikt.parameters.groups.required @@ -37,7 +36,9 @@ import com.github.ajalt.clikt.parameters.types.file import java.io.File -import org.ossreviewtoolkit.cli.GlobalOptions +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.cli.GroupTypes.FileType import org.ossreviewtoolkit.cli.GroupTypes.StringType import org.ossreviewtoolkit.cli.utils.OPTION_GROUP_INPUT @@ -56,6 +57,7 @@ import org.ossreviewtoolkit.model.PackageType import org.ossreviewtoolkit.model.RemoteArtifact import org.ossreviewtoolkit.model.VcsInfo import org.ossreviewtoolkit.model.VcsType +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.licenses.LicenseCategorization import org.ossreviewtoolkit.model.licenses.LicenseClassifications import org.ossreviewtoolkit.model.licenses.LicenseInfoResolver @@ -75,7 +77,10 @@ import org.ossreviewtoolkit.utils.ort.ortConfigDirectory import org.ossreviewtoolkit.utils.ort.showStackTrace import org.ossreviewtoolkit.utils.spdx.model.SpdxLicenseChoice -class DownloaderCommand : CliktCommand(name = "download", help = "Fetch source code from a remote location.") { +class DownloaderCommand : KoinComponent, CliktCommand( + name = "download", + help = "Fetch source code from a remote location." +) { private val input by mutuallyExclusiveOptions( option( "--ort-file", "-i", @@ -176,7 +181,7 @@ class DownloaderCommand : CliktCommand(name = "download", help = "Fetch source c "result to limit downloads to. If not specified, all packages are downloaded." ).split(",") - private val globalOptionsForSubcommands by requireObject() + private val ortConfig by inject() override fun run() { val failureMessages = mutableListOf() @@ -237,7 +242,7 @@ class DownloaderCommand : CliktCommand(name = "download", help = "Fetch source c } } - val includedLicenseCategories = globalOptionsForSubcommands.config.downloader.includedLicenseCategories + val includedLicenseCategories = ortConfig.downloader.includedLicenseCategories if (includedLicenseCategories.isNotEmpty() && licenseClassificationsFile.isFile) { val originalCount = packages.size @@ -268,7 +273,7 @@ class DownloaderCommand : CliktCommand(name = "download", help = "Fetch source c packageDownloadDirs.forEach { (pkg, dir) -> try { - Downloader(globalOptionsForSubcommands.config.downloader).download(pkg, dir) + Downloader(ortConfig.downloader).download(pkg, dir) if (archiveMode == ArchiveMode.ENTITY) { val zipFile = outputDir.resolve("${pkg.id.toPath("-")}.zip") @@ -369,7 +374,7 @@ class DownloaderCommand : CliktCommand(name = "download", help = "Fetch source c // Always allow moving revisions when directly downloading a single project only. This is for // convenience as often the latest revision (referred to by some VCS-specific symbolic name) of a // project needs to be downloaded. - val config = globalOptionsForSubcommands.config.downloader.copy(allowMovingRevisions = true) + val config = ortConfig.downloader.copy(allowMovingRevisions = true) val provenance = Downloader(config).download(dummyPackage, outputDir) println("Successfully downloaded $provenance.") }.onFailure { diff --git a/cli/src/main/kotlin/commands/EvaluatorCommand.kt b/cli/src/main/kotlin/commands/EvaluatorCommand.kt index 9dfb8ab6180d4..b997b17464977 100644 --- a/cli/src/main/kotlin/commands/EvaluatorCommand.kt +++ b/cli/src/main/kotlin/commands/EvaluatorCommand.kt @@ -39,6 +39,9 @@ import java.io.File import kotlin.time.measureTimedValue +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.analyzer.curation.FilePackageCurationProvider import org.ossreviewtoolkit.cli.GlobalOptions import org.ossreviewtoolkit.cli.GroupTypes.FileType @@ -58,7 +61,7 @@ import org.ossreviewtoolkit.evaluator.Evaluator import org.ossreviewtoolkit.model.FileFormat import org.ossreviewtoolkit.model.RuleViolation import org.ossreviewtoolkit.model.config.CopyrightGarbage -import org.ossreviewtoolkit.model.config.LicenseFilenamePatterns +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.config.RepositoryConfiguration import org.ossreviewtoolkit.model.config.createFileArchiver import org.ossreviewtoolkit.model.config.orEmpty @@ -81,7 +84,10 @@ import org.ossreviewtoolkit.utils.ort.ORT_REPO_CONFIG_FILENAME import org.ossreviewtoolkit.utils.ort.ORT_RESOLUTIONS_FILENAME import org.ossreviewtoolkit.utils.ort.ortConfigDirectory -class EvaluatorCommand : CliktCommand(name = "evaluate", help = "Evaluate ORT result files against policy rules.") { +class EvaluatorCommand : KoinComponent, CliktCommand( + name = "evaluate", + help = "Evaluate ORT result files against policy rules." +) { private val ortFile by option( "--ort-file", "-i", help = "The ORT result file to read as input." @@ -204,6 +210,7 @@ class EvaluatorCommand : CliktCommand(name = "evaluate", help = "Evaluate ORT re ).flag() private val globalOptionsForSubcommands by requireObject() + private val ortConfig by inject() override fun run() { val configurationFiles = listOfNotNull( @@ -272,9 +279,7 @@ class EvaluatorCommand : CliktCommand(name = "evaluate", help = "Evaluate ORT re ortResultInput = ortResultInput.replacePackageCurations(curations) } - val config = globalOptionsForSubcommands.config - - val packageConfigurationProvider = if (config.enableRepositoryPackageConfigurations) { + val packageConfigurationProvider = if (ortConfig.enableRepositoryPackageConfigurations) { CompositePackageConfigurationProvider( SimplePackageConfigurationProvider(ortResultInput.repository.config.packageConfigurations), packageConfigurationOption.createProvider() @@ -292,9 +297,9 @@ class EvaluatorCommand : CliktCommand(name = "evaluate", help = "Evaluate ORT re val licenseInfoResolver = LicenseInfoResolver( provider = DefaultLicenseInfoProvider(ortResultInput, packageConfigurationProvider), copyrightGarbage = copyrightGarbage, - addAuthorsToCopyrights = config.addAuthorsToCopyrights, - archiver = config.scanner.archive.createFileArchiver(), - licenseFilenamePatterns = LicenseFilenamePatterns.getInstance() + addAuthorsToCopyrights = ortConfig.addAuthorsToCopyrights, + archiver = ortConfig.scanner.archive.createFileArchiver(), + licenseFilenamePatterns = ortConfig.licenseFilePatterns ) val resolutionProvider = DefaultResolutionProvider.create(ortResultInput, resolutionsFile) @@ -322,7 +327,7 @@ class EvaluatorCommand : CliktCommand(name = "evaluate", help = "Evaluate ORT re evaluatorRun.violations.partition { resolutionProvider.isResolved(it) } val severityStats = SeverityStats.createFromRuleViolations(resolvedViolations, unresolvedViolations) - severityStats.print().conclude(config.severeRuleViolationThreshold, 2) + severityStats.print().conclude(ortConfig.severeRuleViolationThreshold, 2) } } diff --git a/cli/src/main/kotlin/commands/NotifierCommand.kt b/cli/src/main/kotlin/commands/NotifierCommand.kt index e6a7f88dd03d3..17ec83e3cb0a7 100644 --- a/cli/src/main/kotlin/commands/NotifierCommand.kt +++ b/cli/src/main/kotlin/commands/NotifierCommand.kt @@ -21,7 +21,6 @@ package org.ossreviewtoolkit.cli.commands import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.UsageError -import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.parameters.options.associate import com.github.ajalt.clikt.parameters.options.convert import com.github.ajalt.clikt.parameters.options.default @@ -29,7 +28,6 @@ import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.required import com.github.ajalt.clikt.parameters.types.file -import org.ossreviewtoolkit.cli.GlobalOptions import org.ossreviewtoolkit.cli.utils.configurationGroup import org.ossreviewtoolkit.cli.utils.inputGroup import org.ossreviewtoolkit.cli.utils.readOrtResult @@ -74,16 +72,10 @@ class NotifierCommand : CliktCommand(name = "notify", help = "Create notificatio "same name. Can be used multiple times. For example: --label distribution=external" ).associate() - private val globalOptionsForSubcommands by requireObject() - override fun run() { - val script = notificationsFile?.readText() ?: readDefaultNotificationsFile() - val ortResult = readOrtResult(ortFile).mergeLabels(labels) - - val config = globalOptionsForSubcommands.config.notifier - - val notifier = Notifier(ortResult, config, DefaultResolutionProvider.create(ortResult, resolutionsFile)) + val notifier = Notifier(ortResult, DefaultResolutionProvider.create(ortResult, resolutionsFile)) + val script = notificationsFile?.readText() ?: readDefaultNotificationsFile() notifier.run(script) } diff --git a/cli/src/main/kotlin/commands/ReporterCommand.kt b/cli/src/main/kotlin/commands/ReporterCommand.kt index 2601d1a1eb01f..3201582ce1f51 100644 --- a/cli/src/main/kotlin/commands/ReporterCommand.kt +++ b/cli/src/main/kotlin/commands/ReporterCommand.kt @@ -22,7 +22,6 @@ package org.ossreviewtoolkit.cli.commands import com.github.ajalt.clikt.core.BadParameterValue import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.ProgramResult -import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.parameters.groups.mutuallyExclusiveOptions import com.github.ajalt.clikt.parameters.groups.single import com.github.ajalt.clikt.parameters.options.convert @@ -41,7 +40,9 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking -import org.ossreviewtoolkit.cli.GlobalOptions +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.cli.utils.OPTION_GROUP_CONFIGURATION import org.ossreviewtoolkit.cli.utils.PackageConfigurationOption import org.ossreviewtoolkit.cli.utils.configurationGroup @@ -51,7 +52,7 @@ import org.ossreviewtoolkit.cli.utils.logger import org.ossreviewtoolkit.cli.utils.outputGroup import org.ossreviewtoolkit.cli.utils.readOrtResult import org.ossreviewtoolkit.model.config.CopyrightGarbage -import org.ossreviewtoolkit.model.config.LicenseFilenamePatterns +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.config.RepositoryConfiguration import org.ossreviewtoolkit.model.config.createFileArchiver import org.ossreviewtoolkit.model.config.orEmpty @@ -80,7 +81,7 @@ import org.ossreviewtoolkit.utils.ort.ORT_RESOLUTIONS_FILENAME import org.ossreviewtoolkit.utils.ort.ortConfigDirectory import org.ossreviewtoolkit.utils.ort.showStackTrace -class ReporterCommand : CliktCommand( +class ReporterCommand : KoinComponent, CliktCommand( name = "report", help = "Present Analyzer, Scanner and Evaluator results in various formats." ) { @@ -198,7 +199,7 @@ class ReporterCommand : CliktCommand( format to Pair(option.substringBefore("="), option.substringAfter("=", "")) }.multiple() - private val globalOptionsForSubcommands by requireObject() + private val ortConfig by inject() override fun run() { var ortResult = readOrtResult(ortFile) @@ -212,9 +213,7 @@ class ReporterCommand : CliktCommand( val licenseTextDirectories = listOfNotNull(customLicenseTextsDir.takeIf { it.isDirectory }) - val config = globalOptionsForSubcommands.config - - val packageConfigurationProvider = if (config.enableRepositoryPackageConfigurations) { + val packageConfigurationProvider = if (ortConfig.enableRepositoryPackageConfigurations) { CompositePackageConfigurationProvider( SimplePackageConfigurationProvider(ortResult.repository.config.packageConfigurations), packageConfigurationOption.createProvider() @@ -232,9 +231,9 @@ class ReporterCommand : CliktCommand( val licenseInfoResolver = LicenseInfoResolver( provider = DefaultLicenseInfoProvider(ortResult, packageConfigurationProvider), copyrightGarbage = copyrightGarbage, - addAuthorsToCopyrights = config.addAuthorsToCopyrights, - archiver = config.scanner.archive.createFileArchiver(), - licenseFilenamePatterns = LicenseFilenamePatterns.getInstance() + addAuthorsToCopyrights = ortConfig.addAuthorsToCopyrights, + archiver = ortConfig.scanner.archive.createFileArchiver(), + licenseFilenamePatterns = ortConfig.licenseFilePatterns ) val licenseClassifications = @@ -248,7 +247,7 @@ class ReporterCommand : CliktCommand( val input = ReporterInput( ortResult, - globalOptionsForSubcommands.config, + ortConfig, packageConfigurationProvider, resolutionProvider, DefaultLicenseTextProvider(licenseTextDirectories), @@ -260,7 +259,7 @@ class ReporterCommand : CliktCommand( val reportOptionsMap = sortedMapOf>(String.CASE_INSENSITIVE_ORDER) - config.reporter.options?.forEach { (reporterName, option) -> + ortConfig.reporter.options?.forEach { (reporterName, option) -> val reportSpecificOptionsMap = reportOptionsMap.getOrPut(reporterName) { mutableMapOf() } reportSpecificOptionsMap += option } diff --git a/cli/src/main/kotlin/commands/ScannerCommand.kt b/cli/src/main/kotlin/commands/ScannerCommand.kt index 78651de6a112e..d704501f60c62 100644 --- a/cli/src/main/kotlin/commands/ScannerCommand.kt +++ b/cli/src/main/kotlin/commands/ScannerCommand.kt @@ -40,6 +40,9 @@ import com.github.ajalt.clikt.parameters.types.file import kotlinx.coroutines.runBlocking +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.cli.GlobalOptions import org.ossreviewtoolkit.cli.utils.OPTION_GROUP_INPUT import org.ossreviewtoolkit.cli.utils.SeverityStats @@ -67,7 +70,10 @@ import org.ossreviewtoolkit.utils.common.safeMkdirs import org.ossreviewtoolkit.utils.ort.ORT_RESOLUTIONS_FILENAME import org.ossreviewtoolkit.utils.ort.ortConfigDirectory -class ScannerCommand : CliktCommand(name = "scan", help = "Run external license / copyright scanners.") { +class ScannerCommand : KoinComponent, CliktCommand( + name = "scan", + help = "Run external license / copyright scanners." +) { private val input by mutuallyExclusiveOptions( option( "--ort-file", "-i", @@ -137,6 +143,7 @@ class ScannerCommand : CliktCommand(name = "scan", help = "Run external license .configurationGroup() private val globalOptionsForSubcommands by requireObject() + private val ortConfig by inject() override fun run() { val outputFiles = outputFormats.mapTo(mutableSetOf()) { format -> @@ -150,9 +157,7 @@ class ScannerCommand : CliktCommand(name = "scan", help = "Run external license } } - val config = globalOptionsForSubcommands.config - - val ortResult = runScanners(scanners, projectScanners ?: scanners, config).mergeLabels(labels) + val ortResult = runScanners(scanners, projectScanners ?: scanners, ortConfig).mergeLabels(labels) // Write the result. outputDir.safeMkdirs() @@ -171,7 +176,7 @@ class ScannerCommand : CliktCommand(name = "scan", help = "Run external license } val severityStats = SeverityStats.createFromIssues(resolvedIssues, unresolvedIssues) - severityStats.print().conclude(config.severeIssueThreshold, 2) + severityStats.print().conclude(ortConfig.severeIssueThreshold, 2) } private fun runScanners( diff --git a/cli/src/main/kotlin/commands/UploadResultToPostgresCommand.kt b/cli/src/main/kotlin/commands/UploadResultToPostgresCommand.kt index ced01108d86a0..6459b61eb3008 100644 --- a/cli/src/main/kotlin/commands/UploadResultToPostgresCommand.kt +++ b/cli/src/main/kotlin/commands/UploadResultToPostgresCommand.kt @@ -20,7 +20,6 @@ package org.ossreviewtoolkit.cli.commands import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.parameters.options.convert import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option @@ -37,10 +36,13 @@ import org.jetbrains.exposed.sql.SchemaUtils.withDataBaseLock import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.transactions.transaction -import org.ossreviewtoolkit.cli.GlobalOptions +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.cli.utils.inputGroup import org.ossreviewtoolkit.cli.utils.readOrtResult import org.ossreviewtoolkit.model.OrtResult +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.config.PostgresStorageConfiguration import org.ossreviewtoolkit.model.utils.DatabaseUtils import org.ossreviewtoolkit.model.utils.DatabaseUtils.checkDatabaseEncoding @@ -50,7 +52,7 @@ import org.ossreviewtoolkit.utils.common.collectMessages import org.ossreviewtoolkit.utils.common.expandTilde import org.ossreviewtoolkit.utils.ort.showStackTrace -class UploadResultToPostgresCommand : CliktCommand( +class UploadResultToPostgresCommand : KoinComponent, CliktCommand( name = "upload-result-to-postgres", help = "Upload an ORT result to a PostgreSQL database.", epilog = "EXPERIMENTAL: The command is still in development and usage will likely change in the near future. The " + @@ -80,12 +82,12 @@ class UploadResultToPostgresCommand : CliktCommand( help = "Create the table if it does not exist." ).flag() - private val globalOptionsForSubcommands by requireObject() + private val ortConfig by inject() override fun run() { val ortResult = readOrtResult(ortFile) - val postgresConfig = globalOptionsForSubcommands.config.scanner.storages?.values + val postgresConfig = ortConfig.scanner.storages?.values ?.filterIsInstance()?.let { configs -> if (configs.size > 1) { val config = configs.first() diff --git a/cli/src/main/kotlin/commands/UploadResultToSw360Command.kt b/cli/src/main/kotlin/commands/UploadResultToSw360Command.kt index 11e963e1dfb5d..03cd30bbbbbf4 100644 --- a/cli/src/main/kotlin/commands/UploadResultToSw360Command.kt +++ b/cli/src/main/kotlin/commands/UploadResultToSw360Command.kt @@ -20,7 +20,6 @@ package org.ossreviewtoolkit.cli.commands import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.requireObject import com.github.ajalt.clikt.parameters.options.convert import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option @@ -36,7 +35,9 @@ import org.eclipse.sw360.clients.rest.resource.projects.SW360Project import org.eclipse.sw360.clients.rest.resource.releases.SW360Release import org.eclipse.sw360.clients.utils.SW360ClientException -import org.ossreviewtoolkit.cli.GlobalOptions +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.cli.utils.inputGroup import org.ossreviewtoolkit.cli.utils.logger import org.ossreviewtoolkit.cli.utils.readOrtResult @@ -45,6 +46,7 @@ import org.ossreviewtoolkit.model.Identifier import org.ossreviewtoolkit.model.OrtResult import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.Project +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.config.Sw360StorageConfiguration import org.ossreviewtoolkit.model.utils.toPurl import org.ossreviewtoolkit.scanner.storages.Sw360Storage @@ -54,7 +56,7 @@ import org.ossreviewtoolkit.utils.common.packZip import org.ossreviewtoolkit.utils.common.safeDeleteRecursively import org.ossreviewtoolkit.utils.ort.createOrtTempDir -class UploadResultToSw360Command : CliktCommand( +class UploadResultToSw360Command : KoinComponent, CliktCommand( name = "upload-result-to-sw360", help = "Upload an ORT result to SW360.", epilog = "EXPERIMENTAL: The command is still in development and usage will likely change in the near future. The " + @@ -74,12 +76,12 @@ class UploadResultToSw360Command : CliktCommand( help = "Download sources of packages and upload them as attachments to SW360 releases." ).flag() - private val globalOptionsForSubcommands by requireObject() + private val ortConfig by inject() override fun run() { val ortResult = readOrtResult(ortFile) - val sw360Config = globalOptionsForSubcommands.config.scanner.storages?.values + val sw360Config = ortConfig.scanner.storages?.values ?.filterIsInstance()?.singleOrNull() requireNotNull(sw360Config) { @@ -89,7 +91,7 @@ class UploadResultToSw360Command : CliktCommand( val sw360Connection = Sw360Storage.createConnection(sw360Config) val sw360ReleaseClient = sw360Connection.releaseAdapter val sw360ProjectClient = sw360Connection.projectAdapter - val downloader = Downloader(globalOptionsForSubcommands.config.downloader) + val downloader = Downloader(ortConfig.downloader) getProjectWithPackages(ortResult).forEach { (project, pkgList) -> val linkedReleases = pkgList.mapNotNull { pkg -> diff --git a/downloader/build.gradle.kts b/downloader/build.gradle.kts index f527cc822e9cf..5a72691cf5e8f 100644 --- a/downloader/build.gradle.kts +++ b/downloader/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(libs.jgit) implementation(libs.jgitJsch) implementation(libs.jschAgentProxy) + implementation(libs.koinCore) implementation(libs.svnkit) testImplementation(libs.mockk) diff --git a/downloader/src/main/kotlin/VersionControlSystem.kt b/downloader/src/main/kotlin/VersionControlSystem.kt index 24c3fa759ff7a..38da5e792b6b6 100644 --- a/downloader/src/main/kotlin/VersionControlSystem.kt +++ b/downloader/src/main/kotlin/VersionControlSystem.kt @@ -27,10 +27,13 @@ import java.util.ServiceLoader import org.apache.logging.log4j.kotlin.Logging +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.VcsInfo import org.ossreviewtoolkit.model.VcsType -import org.ossreviewtoolkit.model.config.LicenseFilenamePatterns +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.orEmpty import org.ossreviewtoolkit.utils.common.CommandLineTool import org.ossreviewtoolkit.utils.common.collectMessages @@ -39,7 +42,7 @@ import org.ossreviewtoolkit.utils.ort.ORT_REPO_CONFIG_FILENAME import org.ossreviewtoolkit.utils.ort.showStackTrace abstract class VersionControlSystem { - companion object : Logging { + companion object : KoinComponent, Logging { private val LOADER = ServiceLoader.load(VersionControlSystem::class.java) /** @@ -151,8 +154,9 @@ abstract class VersionControlSystem { */ internal fun getSparseCheckoutGlobPatterns(): List { val globPatterns = mutableListOf("*$ORT_REPO_CONFIG_FILENAME") - val licensePatterns = LicenseFilenamePatterns.getInstance() - return licensePatterns.allLicenseFilenames.generateCapitalizationVariants().mapTo(globPatterns) { "**/$it" } + val ortConfig by inject() + return ortConfig.licenseFilePatterns.allLicenseFilenames.generateCapitalizationVariants() + .mapTo(globPatterns) { "**/$it" } } private fun Collection.generateCapitalizationVariants() = diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 18999f3fa08f5..1db7ffa348588 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,7 @@ jruby = "9.3.9.0" jschAgentProxy = "0.0.9" jslt = "0.1.13" jsonSchemaValidator = "1.0.73" +koin = "3.2.2" kotest = "5.5.3" kotlinxCoroutines = "1.6.4" kotlinxHtml = "0.8.0" @@ -118,6 +119,8 @@ jruby = { module = "org.jruby:jruby-complete", version.ref = "jruby" } jschAgentProxy = { module = "com.jcraft:jsch.agentproxy.jsch", version.ref = "jschAgentProxy" } jslt = { module = "com.schibsted.spt.data:jslt", version.ref = "jslt" } jsonSchemaValidator = { module = "com.networknt:json-schema-validator", version.ref = "jsonSchemaValidator" } +koinCore = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koinTest = { module = "io.insert-koin:koin-test", version.ref = "koin" } kotestAssertionsCore = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } kotestAssertionsJson = { module = "io.kotest:kotest-assertions-json", version.ref = "kotest" } kotestExtensionsJunitXml = { module = "io.kotest:kotest-extensions-junitxml", version.ref = "kotest" } diff --git a/model/build.gradle.kts b/model/build.gradle.kts index 048cf0c9340b2..f70a8d028700f 100644 --- a/model/build.gradle.kts +++ b/model/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { api(libs.jacksonDatabind) api(libs.jacksonDataformatXml) api(libs.jacksonDataformatYaml) + api(libs.koinCore) implementation(libs.bundles.exposed) implementation(libs.bundles.hoplite) diff --git a/model/src/main/kotlin/ScanSummary.kt b/model/src/main/kotlin/ScanSummary.kt index 56cae16ab4ec8..ed0a23a21f70e 100644 --- a/model/src/main/kotlin/ScanSummary.kt +++ b/model/src/main/kotlin/ScanSummary.kt @@ -27,7 +27,10 @@ import com.fasterxml.jackson.annotation.JsonProperty import java.time.Instant import java.util.SortedSet -import org.ossreviewtoolkit.model.config.LicenseFilenamePatterns +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.utils.RootLicenseMatcher import org.ossreviewtoolkit.utils.common.FileMatcher import org.ossreviewtoolkit.utils.spdx.SpdxExpression @@ -73,7 +76,7 @@ data class ScanSummary( */ @JsonInclude(JsonInclude.Include.NON_EMPTY) val issues: List = emptyList() -) { +) : KoinComponent { companion object { /** * A constant for a [ScannerRun] where all properties are empty. @@ -98,7 +101,8 @@ data class ScanSummary( fun filterByPath(path: String): ScanSummary { if (path.isBlank()) return this - val rootLicenseMatcher = RootLicenseMatcher(LicenseFilenamePatterns.getInstance()) + val ortConfig by inject() + val rootLicenseMatcher = RootLicenseMatcher(ortConfig.licenseFilePatterns) val applicableLicenseFiles = rootLicenseMatcher.getApplicableRootLicenseFindingsForDirectories( licenseFindings = licenseFindings, directories = listOf(path) diff --git a/model/src/main/kotlin/config/FileArchiverConfiguration.kt b/model/src/main/kotlin/config/FileArchiverConfiguration.kt index 0c90149c4ed6b..a36c7e1842348 100644 --- a/model/src/main/kotlin/config/FileArchiverConfiguration.kt +++ b/model/src/main/kotlin/config/FileArchiverConfiguration.kt @@ -21,6 +21,8 @@ package org.ossreviewtoolkit.model.config import org.apache.logging.log4j.kotlin.Logging +import org.koin.core.context.GlobalContext + import org.ossreviewtoolkit.model.utils.DatabaseUtils import org.ossreviewtoolkit.model.utils.FileArchiver import org.ossreviewtoolkit.model.utils.FileArchiverFileStorage @@ -80,7 +82,9 @@ fun FileArchiverConfiguration?.createFileArchiver(): FileArchiver? { else -> FileArchiverFileStorage(LocalFileStorage(FileArchiver.DEFAULT_ARCHIVE_DIR)) } - val patterns = LicenseFilenamePatterns.getInstance().allLicenseFilenames + // The nullable receiver class of this extension function prevents the use of inject() as a delegate, so access the + // global context directly. + val ortConfig = GlobalContext.get().get() - return FileArchiver(patterns, storage) + return FileArchiver(ortConfig.licenseFilePatterns.allLicenseFilenames, storage) } diff --git a/model/src/main/kotlin/config/LicenseFilenamePatterns.kt b/model/src/main/kotlin/config/LicenseFilenamePatterns.kt index fcccd052dfbc9..5204daaaa1d7a 100644 --- a/model/src/main/kotlin/config/LicenseFilenamePatterns.kt +++ b/model/src/main/kotlin/config/LicenseFilenamePatterns.kt @@ -36,12 +36,6 @@ data class LicenseFilenamePatterns( */ val rootLicenseFilenames: List ) { - /** - * A list of globs that match all kind of license file names, equaling the union of [licenseFilenames], - * [patentFilenames] and [rootLicenseFilenames]. The patterns are supposed to be used case-insensitively. - */ - val allLicenseFilenames = (licenseFilenames + patentFilenames + rootLicenseFilenames).distinct() - companion object { val DEFAULT = LicenseFilenamePatterns( licenseFilenames = listOf( @@ -61,15 +55,11 @@ data class LicenseFilenamePatterns( "readme*" ) ) - - private var instance: LicenseFilenamePatterns = DEFAULT - - @Synchronized - fun configure(patterns: LicenseFilenamePatterns) { - instance = patterns - } - - @Synchronized - fun getInstance(): LicenseFilenamePatterns = instance } + + /** + * A list of globs that match all kind of license file names, equaling the union of [licenseFilenames], + * [patentFilenames] and [rootLicenseFilenames]. The patterns are supposed to be used case-insensitively. + */ + val allLicenseFilenames = (licenseFilenames + patentFilenames + rootLicenseFilenames).distinct() } diff --git a/model/src/main/kotlin/utils/OrtResultExtensions.kt b/model/src/main/kotlin/utils/OrtResultExtensions.kt index bd1437c8716b2..7d1462507d6b6 100644 --- a/model/src/main/kotlin/utils/OrtResultExtensions.kt +++ b/model/src/main/kotlin/utils/OrtResultExtensions.kt @@ -19,10 +19,12 @@ package org.ossreviewtoolkit.model.utils +import org.koin.core.context.GlobalContext + import org.ossreviewtoolkit.model.OrtResult import org.ossreviewtoolkit.model.RepositoryProvenance import org.ossreviewtoolkit.model.config.CopyrightGarbage -import org.ossreviewtoolkit.model.config.LicenseFilenamePatterns +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.licenses.DefaultLicenseInfoProvider import org.ossreviewtoolkit.model.licenses.LicenseInfoResolver @@ -35,13 +37,18 @@ fun OrtResult.createLicenseInfoResolver( copyrightGarbage: CopyrightGarbage = CopyrightGarbage(), addAuthorsToCopyrights: Boolean = false, archiver: FileArchiver? = null -) = LicenseInfoResolver( +): LicenseInfoResolver { + // Avoid the need for OrtResult to implement KoinComponent by accessing the global context directly. + val ortConfig = GlobalContext.get().get() + + return LicenseInfoResolver( DefaultLicenseInfoProvider(this, packageConfigurationProvider), copyrightGarbage, addAuthorsToCopyrights, archiver, - LicenseFilenamePatterns.getInstance() + ortConfig.licenseFilePatterns ) +} /** * Return the path where the repository given by [provenance] is linked into the source tree. diff --git a/notifier/src/main/kotlin/Notifier.kt b/notifier/src/main/kotlin/Notifier.kt index 1e492529f41c7..4d7d726cd7305 100644 --- a/notifier/src/main/kotlin/Notifier.kt +++ b/notifier/src/main/kotlin/Notifier.kt @@ -28,9 +28,12 @@ import kotlin.script.experimental.api.providedProperties import kotlin.script.experimental.api.scriptsInstancesSharing import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + import org.ossreviewtoolkit.model.NotifierRun import org.ossreviewtoolkit.model.OrtResult -import org.ossreviewtoolkit.model.config.NotifierConfiguration +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.utils.DefaultResolutionProvider import org.ossreviewtoolkit.model.utils.ResolutionProvider import org.ossreviewtoolkit.notifier.modules.JiraNotifier @@ -39,12 +42,15 @@ import org.ossreviewtoolkit.utils.scripting.ScriptRunner class Notifier( ortResult: OrtResult = OrtResult.EMPTY, - config: NotifierConfiguration = NotifierConfiguration(), resolutionProvider: ResolutionProvider = DefaultResolutionProvider() -) : ScriptRunner() { +) : ScriptRunner(), KoinComponent { + private val ortConfig by inject() + private val customProperties = buildMap { - config.mail?.let { put("mailClient", MailNotifier(it)) } - config.jira?.let { put("jiraClient", JiraNotifier(it)) } + with(ortConfig.notifier) { + mail?.let { put("mailClient", MailNotifier(it)) } + jira?.let { put("jiraClient", JiraNotifier(it)) } + } put("resolutionProvider", resolutionProvider) } diff --git a/notifier/src/test/kotlin/NotifierTest.kt b/notifier/src/test/kotlin/NotifierTest.kt index 946c91723d48b..5062316a39427 100644 --- a/notifier/src/test/kotlin/NotifierTest.kt +++ b/notifier/src/test/kotlin/NotifierTest.kt @@ -27,15 +27,36 @@ import io.kotest.matchers.types.shouldBeInstanceOf import kotlin.script.experimental.api.ResultValue +import org.koin.core.context.GlobalContext.startKoin +import org.koin.core.context.loadKoinModules +import org.koin.core.context.stopKoin +import org.koin.dsl.koinApplication +import org.koin.dsl.module + import org.ossreviewtoolkit.model.config.JiraConfiguration import org.ossreviewtoolkit.model.config.NotifierConfiguration +import org.ossreviewtoolkit.model.config.OrtConfiguration import org.ossreviewtoolkit.model.config.SendMailConfiguration class NotifierTest : WordSpec({ + beforeSpec { + startKoin { } + } + + afterSpec { + stopKoin() + } + "Client properties" should { "only be available if configured" { - val mailNotifier = Notifier(config = NotifierConfiguration(mail = SendMailConfiguration())) - val jiraNotifier = Notifier(config = NotifierConfiguration(jira = JiraConfiguration())) + val sendMailConfigModule = module { + single { + OrtConfiguration(notifier = NotifierConfiguration(mail = SendMailConfiguration())) + } + } + + loadKoinModules(sendMailConfigModule) + val mailNotifier = Notifier() assertSoftly { shouldNotThrow { @@ -53,6 +74,15 @@ class NotifierTest : WordSpec({ } } + val jiraConfigModule = module { + single { + OrtConfiguration(notifier = NotifierConfiguration(jira = JiraConfiguration())) + } + } + + loadKoinModules(jiraConfigModule) + val jiraNotifier = Notifier() + assertSoftly { shouldThrow { jiraNotifier.runScript("mailClient")