org.pkl.commons.cli.CliCommand.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pkl-commons-cli Show documentation
Show all versions of pkl-commons-cli Show documentation
Internal CLI utilities. NOT A PUBLIC API.
The newest version!
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.commons.cli
import java.nio.file.Files
import java.nio.file.Path
import java.util.regex.Pattern
import kotlin.io.path.isRegularFile
import org.pkl.core.*
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
import org.pkl.core.externalreader.ExternalReaderProcess
import org.pkl.core.http.HttpClient
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.module.ModuleKeyFactory
import org.pkl.core.module.ModulePathResolver
import org.pkl.core.project.Project
import org.pkl.core.resource.ResourceReader
import org.pkl.core.resource.ResourceReaders
import org.pkl.core.settings.PklSettings
import org.pkl.core.util.IoUtils
/** Building block for CLI commands. Configured programmatically to allow for embedding. */
abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
/** Runs this command. */
fun run() {
if (cliOptions.testMode) {
IoUtils.setTestMode()
}
try {
proxyAddress?.let(IoUtils::setSystemProxy)
doRun()
} catch (e: PklException) {
throw CliException(e.message!!)
} catch (e: CliException) {
throw e
} catch (e: Exception) {
throw CliBugException(e)
}
}
/**
* Implements this command. May throw [PklException] or [CliException]. Any other thrown exception
* is treated as a bug.
*/
protected abstract fun doRun()
/** The Pkl settings used by this command. */
@Suppress("MemberVisibilityCanBePrivate")
protected val settings: PklSettings by lazy {
try {
if (cliOptions.normalizedSettingsModule != null) {
PklSettings.load(ModuleSource.uri(cliOptions.normalizedSettingsModule))
} else {
PklSettings.loadFromPklHomeDir()
}
} catch (e: PklException) {
// do not use `errorRenderer` because it depends on `settings`
throw CliException(e.toString())
}
}
/** The Project used by this command. */
protected val project: Project? by lazy {
if (cliOptions.noProject) {
null
} else {
cliOptions.normalizedProjectFile?.let { loadProject(it) }
}
}
protected fun loadProject(projectFile: Path): Project {
val securityManager =
SecurityManagers.standard(
cliOptions.allowedModules ?: SecurityManagers.defaultAllowedModules,
cliOptions.allowedResources ?: SecurityManagers.defaultAllowedResources,
SecurityManagers.defaultTrustLevels,
cliOptions.normalizedRootDir
)
val envVars = cliOptions.environmentVariables ?: System.getenv()
val stackFrameTransformer =
if (IoUtils.isTestMode()) StackFrameTransformers.empty
else StackFrameTransformers.defaultTransformer
return Project.loadFromPath(
projectFile,
securityManager,
cliOptions.timeout,
stackFrameTransformer,
envVars
)
}
private val evaluatorSettings: PklEvaluatorSettings? by lazy {
if (cliOptions.omitProjectSettings) null else project?.evaluatorSettings
}
protected val allowedModules: List by lazy {
cliOptions.allowedModules
?: evaluatorSettings?.allowedModules
?: (SecurityManagers.defaultAllowedModules +
externalModuleReaders.keys.map { Pattern.compile("$it:") }.toList())
}
protected val allowedResources: List by lazy {
cliOptions.allowedResources
?: evaluatorSettings?.allowedResources
?: (SecurityManagers.defaultAllowedResources +
externalResourceReaders.keys.map { Pattern.compile("$it:") }.toList())
}
protected val rootDir: Path? by lazy {
cliOptions.normalizedRootDir ?: evaluatorSettings?.rootDir
}
protected val environmentVariables: Map by lazy {
cliOptions.environmentVariables ?: evaluatorSettings?.env ?: System.getenv()
}
protected val externalProperties: Map by lazy {
cliOptions.externalProperties ?: evaluatorSettings?.externalProperties ?: emptyMap()
}
protected val moduleCacheDir: Path? by lazy {
if (cliOptions.noCache) null
else
cliOptions.normalizedModuleCacheDir
?: evaluatorSettings?.let { settings ->
if (settings.noCache == true) null else settings.moduleCacheDir
}
?: IoUtils.getDefaultModuleCacheDir()
}
protected val modulePath: List by lazy {
cliOptions.normalizedModulePath ?: evaluatorSettings?.modulePath ?: emptyList()
}
protected val stackFrameTransformer: StackFrameTransformer by lazy {
if (cliOptions.testMode) {
StackFrameTransformers.empty
} else {
StackFrameTransformers.createDefault(settings)
}
}
protected val securityManager: SecurityManager by lazy {
SecurityManagers.standard(
allowedModules,
allowedResources,
SecurityManagers.defaultTrustLevels,
rootDir
)
}
protected val useColor: Boolean by lazy { cliOptions.color?.hasColor() ?: false }
private val proxyAddress by lazy {
cliOptions.httpProxy
?: project?.evaluatorSettings?.http?.proxy?.address ?: settings.http?.proxy?.address
}
private val noProxy by lazy {
cliOptions.httpNoProxy
?: project?.evaluatorSettings?.http?.proxy?.noProxy ?: settings.http?.proxy?.noProxy
}
private val externalModuleReaders by lazy {
(project?.evaluatorSettings?.externalModuleReaders
?: emptyMap()) + cliOptions.externalModuleReaders
}
private val externalResourceReaders by lazy {
(project?.evaluatorSettings?.externalResourceReaders
?: emptyMap()) + cliOptions.externalResourceReaders
}
private val externalProcesses by lazy {
// Share ExternalReaderProcess instances between configured external resource/module readers
// with the same spec. This avoids spawning multiple subprocesses if the same reader implements
// both reader types and/or multiple schemes.
(externalModuleReaders + externalResourceReaders).values.toSet().associateWith {
ExternalReaderProcess.of(it)
}
}
private fun HttpClient.Builder.addDefaultCliCertificates() {
val caCertsDir = IoUtils.getPklHomeDir().resolve("cacerts")
var certsAdded = false
if (Files.isDirectory(caCertsDir)) {
Files.list(caCertsDir)
.filter { it.isRegularFile() }
.forEach { cert ->
certsAdded = true
addCertificates(cert)
}
}
if (!certsAdded) {
val defaultCerts =
javaClass.classLoader.getResourceAsStream("org/pkl/commons/cli/PklCARoots.pem")
?: throw CliException("Could not find bundled certificates")
addCertificates(defaultCerts.readAllBytes())
}
}
/**
* The HTTP client used for this command.
*
* To release resources held by the HTTP client in a timely manner, call [HttpClient.close].
*/
val httpClient: HttpClient by lazy {
with(HttpClient.builder()) {
setTestPort(cliOptions.testPort)
if (cliOptions.normalizedCaCertificates.isEmpty()) {
addDefaultCliCertificates()
} else {
for (file in cliOptions.normalizedCaCertificates) addCertificates(file)
}
if ((proxyAddress ?: noProxy) != null) {
setProxy(proxyAddress, noProxy ?: listOf())
}
// Lazy building significantly reduces execution time of commands that do minimal work.
// However, it means that HTTP client initialization errors won't surface until an HTTP
// request is made.
buildLazily()
}
}
protected fun moduleKeyFactories(modulePathResolver: ModulePathResolver): List {
return buildList {
externalModuleReaders.forEach { (key, value) ->
add(ModuleKeyFactories.externalProcess(key, externalProcesses[value]!!))
}
add(ModuleKeyFactories.standardLibrary)
add(ModuleKeyFactories.modulePath(modulePathResolver))
add(ModuleKeyFactories.pkg)
add(ModuleKeyFactories.projectpackage)
addAll(ModuleKeyFactories.fromServiceProviders())
add(ModuleKeyFactories.file)
add(ModuleKeyFactories.http)
add(ModuleKeyFactories.genericUrl)
}
}
private fun resourceReaders(modulePathResolver: ModulePathResolver): List {
return buildList {
add(ResourceReaders.environmentVariable())
add(ResourceReaders.externalProperty())
add(ResourceReaders.modulePath(modulePathResolver))
add(ResourceReaders.pkg())
add(ResourceReaders.projectpackage())
add(ResourceReaders.file())
add(ResourceReaders.http())
add(ResourceReaders.https())
externalResourceReaders.forEach { (key, value) ->
add(ResourceReaders.externalProcess(key, externalProcesses[value]!!))
}
}
}
/**
* Creates an [EvaluatorBuilder] preconfigured according to [cliOptions]. To avoid resource leaks,
* `ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)` must be called once the returned
* builder and evaluators built by it are no longer in use.
*/
protected fun evaluatorBuilder(): EvaluatorBuilder {
// indirectly closed by `ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)`
val modulePathResolver = ModulePathResolver(modulePath)
return EvaluatorBuilder.unconfigured()
.setStackFrameTransformer(stackFrameTransformer)
.apply { project?.let { setProjectDependencies(it.dependencies) } }
.setSecurityManager(securityManager)
.setHttpClient(httpClient)
.setExternalProperties(externalProperties)
.setEnvironmentVariables(environmentVariables)
.addModuleKeyFactories(moduleKeyFactories(modulePathResolver))
.addResourceReaders(resourceReaders(modulePathResolver))
.setColor(useColor)
.setLogger(Loggers.stdErr())
.setTimeout(cliOptions.timeout)
.setModuleCacheDir(moduleCacheDir)
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy