mirror of
https://github.com/apple/pkl.git
synced 2026-03-20 00:04:05 +01:00
Initial commit
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.util.regex.Pattern
|
||||
import org.pkl.core.module.ProjectDependenciesManager
|
||||
import org.pkl.core.util.IoUtils
|
||||
|
||||
/** Base options shared between CLI commands. */
|
||||
data class CliBaseOptions(
|
||||
/** The source modules to evaluate. Relative URIs are resolved against [workingDir]. */
|
||||
private val sourceModules: List<URI> = listOf(),
|
||||
|
||||
/**
|
||||
* The URI patterns that determine which modules can be loaded and evaluated. Patterns are matched
|
||||
* against the beginning of module URIs. At least one pattern needs to match for a module to be
|
||||
* loadable. Both [sourceModules] and module imports are subject to this check.
|
||||
*/
|
||||
val allowedModules: List<Pattern>? = null,
|
||||
|
||||
/**
|
||||
* The URI patterns that determine which external resources can be read. Patterns are matched
|
||||
* against the beginning of resource URIs. At least one pattern needs to match for a resource to
|
||||
* be readable.
|
||||
*/
|
||||
val allowedResources: List<Pattern>? = null,
|
||||
|
||||
/**
|
||||
* The environment variables to set. Pkl code can read environment variables with
|
||||
* `read("env:<NAME>")`.
|
||||
*/
|
||||
val environmentVariables: Map<String, String>? = null,
|
||||
|
||||
/**
|
||||
* The external properties to set. Pkl code can read external properties with
|
||||
* `read("prop:<name>")`.
|
||||
*/
|
||||
val externalProperties: Map<String, String>? = null,
|
||||
|
||||
/**
|
||||
* The directories, ZIP archives, or JAR archives to search when resolving `modulepath:` URIs.
|
||||
* Relative paths are resolved against [workingDir].
|
||||
*/
|
||||
private val modulePath: List<Path>? = null,
|
||||
|
||||
/**
|
||||
* The base path that relative module paths passed as command-line arguments are resolved against.
|
||||
*/
|
||||
private val workingDir: Path = IoUtils.getCurrentWorkingDir(),
|
||||
|
||||
/**
|
||||
* The root directory for `file:` modules and resources. If non-null, access to file-based modules
|
||||
* and resources is restricted to those located under [rootDir]. Any symlinks are resolved before
|
||||
* this check is performed.
|
||||
*/
|
||||
private val rootDir: Path? = null,
|
||||
|
||||
/**
|
||||
* The Pkl settings file to use. A settings file is a Pkl module amending the `pkl.settings`
|
||||
* standard library module. If `null`, `~/.pkl/settings.pkl` (if present) or the defaults
|
||||
* specified in the `pkl:settings` standard library module are used.
|
||||
*/
|
||||
private val settings: URI? = null,
|
||||
|
||||
/**
|
||||
* The root directory of the project. The directory must contain a `PklProject` that amends the
|
||||
* `pkl.Project` standard library module.
|
||||
*
|
||||
* If `null`, looks for a `PklProject` file in [workingDir], traversing up to [rootDir], or `/` if
|
||||
* [rootDir] is `null`.
|
||||
*
|
||||
* This can be disabled with [noProject].
|
||||
*/
|
||||
private val projectDir: Path? = null,
|
||||
|
||||
/**
|
||||
* The duration after which evaluation of a source module will be timed out. Note that a timeout
|
||||
* is treated the same as a program error in that any subsequent source modules will not be
|
||||
* evaluated.
|
||||
*/
|
||||
val timeout: Duration? = null,
|
||||
|
||||
/** The cache directory for storing packages. */
|
||||
private val moduleCacheDir: Path? = null,
|
||||
|
||||
/** Whether to disable the module cache. */
|
||||
val noCache: Boolean = false,
|
||||
|
||||
/** Ignores any evaluator settings set in the PklProject file. */
|
||||
val omitProjectSettings: Boolean = false,
|
||||
|
||||
/** Disables all behavior related to projects. */
|
||||
val noProject: Boolean = false,
|
||||
|
||||
/** Tells whether to run the CLI in test mode. This is an internal option. */
|
||||
val testMode: Boolean = false,
|
||||
|
||||
/**
|
||||
* [X.509 certificates](https://en.wikipedia.org/wiki/X.509) in PEM format.
|
||||
*
|
||||
* Elements can either be a [Path] or a [java.io.InputStream]. Input streams are closed when
|
||||
* [CliCommand] is initialized.
|
||||
*
|
||||
* If not empty, this determines the CA root certs used for all HTTPS requests. Warning: this
|
||||
* affects the whole Java runtime, not just the Pkl API!
|
||||
*/
|
||||
val caCertificates: List<Any> = emptyList(),
|
||||
) {
|
||||
|
||||
companion object {
|
||||
tailrec fun Path.getProjectFile(rootDir: Path?): Path? {
|
||||
val candidate = resolve(ProjectDependenciesManager.PKL_PROJECT_FILENAME)
|
||||
return when {
|
||||
Files.exists(candidate) -> candidate
|
||||
parent == null -> null
|
||||
rootDir != null && !parent.startsWith(rootDir) -> null
|
||||
else -> parent.getProjectFile(rootDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** [workingDir] after normalization. */
|
||||
val normalizedWorkingDir: Path = IoUtils.getCurrentWorkingDir().resolve(workingDir)
|
||||
|
||||
/** [rootDir] after normalization. */
|
||||
val normalizedRootDir: Path? = rootDir?.let(normalizedWorkingDir::resolve)
|
||||
|
||||
/** [sourceModules] after normalization. */
|
||||
val normalizedSourceModules: List<URI> =
|
||||
sourceModules
|
||||
.map { uri ->
|
||||
if (uri.isAbsolute) uri else IoUtils.resolve(normalizedWorkingDir.toUri(), uri)
|
||||
}
|
||||
// sort modules to make cli output independent of source module order
|
||||
.sorted()
|
||||
|
||||
val normalizedSettingsModule: URI? =
|
||||
settings?.let { uri ->
|
||||
if (uri.isAbsolute) uri else IoUtils.resolve(normalizedWorkingDir.toUri(), uri)
|
||||
}
|
||||
|
||||
/** [modulePath] after normalization. */
|
||||
val normalizedModulePath: List<Path>? = modulePath?.map(normalizedWorkingDir::resolve)
|
||||
|
||||
/** [moduleCacheDir] after normalization. */
|
||||
val normalizedModuleCacheDir: Path? = moduleCacheDir?.let(normalizedWorkingDir::resolve)
|
||||
|
||||
/** The effective project directory, if exists. */
|
||||
val normalizedProjectFile: Path? by lazy {
|
||||
projectDir?.resolve(ProjectDependenciesManager.PKL_PROJECT_FILENAME)
|
||||
?: normalizedWorkingDir.getProjectFile(rootDir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 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.Path
|
||||
import java.util.regex.Pattern
|
||||
import org.pkl.core.*
|
||||
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.runtime.CertificateUtils
|
||||
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) {
|
||||
init {
|
||||
if (cliOptions.caCertificates.isNotEmpty()) {
|
||||
CertificateUtils.setupAllX509CertificatesGlobally(cliOptions.caCertificates)
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs this command. */
|
||||
fun run() {
|
||||
if (cliOptions.testMode) {
|
||||
IoUtils.setTestMode()
|
||||
}
|
||||
try {
|
||||
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) {
|
||||
return@lazy null
|
||||
}
|
||||
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 projectSettings: Project.EvaluatorSettings? by lazy {
|
||||
if (cliOptions.omitProjectSettings) {
|
||||
return@lazy null
|
||||
}
|
||||
project?.settings
|
||||
}
|
||||
|
||||
protected val allowedModules: List<Pattern> by lazy {
|
||||
cliOptions.allowedModules
|
||||
?: projectSettings?.allowedModules ?: SecurityManagers.defaultAllowedModules
|
||||
}
|
||||
|
||||
protected val allowedResources: List<Pattern> by lazy {
|
||||
cliOptions.allowedResources
|
||||
?: projectSettings?.allowedResources ?: SecurityManagers.defaultAllowedResources
|
||||
}
|
||||
|
||||
protected val rootDir: Path? by lazy { cliOptions.normalizedRootDir ?: projectSettings?.rootDir }
|
||||
|
||||
protected val environmentVariables: Map<String, String> by lazy {
|
||||
cliOptions.environmentVariables ?: projectSettings?.env ?: System.getenv()
|
||||
}
|
||||
|
||||
protected val externalProperties: Map<String, String> by lazy {
|
||||
cliOptions.externalProperties ?: projectSettings?.externalProperties ?: emptyMap()
|
||||
}
|
||||
|
||||
protected val moduleCacheDir: Path? by lazy {
|
||||
if (cliOptions.noCache) null
|
||||
else
|
||||
cliOptions.normalizedModuleCacheDir
|
||||
?: projectSettings?.let { settings ->
|
||||
if (settings.isNoCache == true) null else settings.moduleCacheDir
|
||||
}
|
||||
?: IoUtils.getDefaultModuleCacheDir()
|
||||
}
|
||||
|
||||
protected val modulePath: List<Path> by lazy {
|
||||
cliOptions.normalizedModulePath ?: projectSettings?.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 fun moduleKeyFactories(modulePathResolver: ModulePathResolver): List<ModuleKeyFactory> {
|
||||
return buildList {
|
||||
add(ModuleKeyFactories.standardLibrary)
|
||||
add(ModuleKeyFactories.modulePath(modulePathResolver))
|
||||
add(ModuleKeyFactories.pkg)
|
||||
add(ModuleKeyFactories.projectpackage)
|
||||
addAll(ModuleKeyFactories.fromServiceProviders())
|
||||
add(ModuleKeyFactories.file)
|
||||
add(ModuleKeyFactories.genericUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resourceReaders(modulePathResolver: ModulePathResolver): List<ResourceReader> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
.setExternalProperties(externalProperties)
|
||||
.setEnvironmentVariables(environmentVariables)
|
||||
.addModuleKeyFactories(moduleKeyFactories(modulePathResolver))
|
||||
.addResourceReaders(resourceReaders(modulePathResolver))
|
||||
.setLogger(Loggers.stdErr())
|
||||
.setTimeout(cliOptions.timeout)
|
||||
.setModuleCacheDir(moduleCacheDir)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 org.pkl.commons.printStackTraceToString
|
||||
|
||||
/** A CLI error to report back to users. */
|
||||
open class CliException(
|
||||
/**
|
||||
* The error message to report back to CLI users. The message is expected to be displayed as-is
|
||||
* without any further enrichment. As such the message should be comprehensive and designed with
|
||||
* the CLI user in mind.
|
||||
*/
|
||||
message: String,
|
||||
|
||||
/** The process exit code to use. */
|
||||
val exitCode: Int = 1
|
||||
) : RuntimeException(message) {
|
||||
|
||||
override fun toString(): String = message!!
|
||||
}
|
||||
|
||||
/** An unexpected CLI error classified as bug. */
|
||||
class CliBugException(
|
||||
/** The cause for the bug. */
|
||||
private val theCause: Exception,
|
||||
|
||||
/** The process exit code to use. */
|
||||
exitCode: Int = 1
|
||||
) :
|
||||
CliException("An unexpected error has occurred. Would you mind filing a bug report?", exitCode) {
|
||||
|
||||
override fun toString(): String = "$message\n\n${theCause.printStackTraceToString()}"
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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.io.PrintStream
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/** Building block for CLIs. Intended to be called from a `main` method. */
|
||||
fun cliMain(block: () -> Unit) {
|
||||
fun printError(error: Throwable, stream: PrintStream) {
|
||||
val message = error.toString()
|
||||
stream.print(message)
|
||||
// ensure CLI output always ends with newline
|
||||
if (!message.endsWith('\n')) stream.println()
|
||||
}
|
||||
|
||||
try {
|
||||
block()
|
||||
} catch (e: CliTestException) {
|
||||
// no need to print the error, the test results will already do it
|
||||
exitProcess(e.exitCode)
|
||||
} catch (e: CliException) {
|
||||
printError(e, if (e.exitCode == 0) System.out else System.err)
|
||||
exitProcess(e.exitCode)
|
||||
} catch (e: Exception) {
|
||||
printError(CliBugException(e), System.err)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
object CliMain {
|
||||
val compat: String? = System.getProperty("org.pkl.compat")
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
class CliTestException(msg: String) : CliException(msg, 1)
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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.Path
|
||||
|
||||
class CliTestOptions(val junitDir: Path? = null, val overwrite: Boolean = false)
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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.commands
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.parameters.groups.provideDelegate
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import org.pkl.commons.cli.CliException
|
||||
import org.pkl.core.runtime.VmUtils
|
||||
import org.pkl.core.util.IoUtils
|
||||
|
||||
abstract class BaseCommand(name: String, helpLink: String, help: String = "") :
|
||||
CliktCommand(
|
||||
name = name,
|
||||
help = help,
|
||||
epilog = "For more information, visit $helpLink",
|
||||
) {
|
||||
val baseOptions by BaseOptions()
|
||||
|
||||
/**
|
||||
* Parses [moduleName] into a URI. If scheme is not present, we expect that this is a file path
|
||||
* and encode any possibly invalid characters. If a scheme is present, we expect that this is a
|
||||
* valid URI.
|
||||
*/
|
||||
protected fun parseModuleName(moduleName: String): URI =
|
||||
when (moduleName) {
|
||||
"-" -> VmUtils.REPL_TEXT_URI
|
||||
else ->
|
||||
try {
|
||||
IoUtils.toUri(moduleName)
|
||||
} catch (e: URISyntaxException) {
|
||||
val message = buildString {
|
||||
append("Module URI `$moduleName` has invalid syntax (${e.reason}).")
|
||||
if (e.index > -1) {
|
||||
append("\n\n")
|
||||
append(moduleName)
|
||||
append("\n")
|
||||
append(" ".repeat(e.index))
|
||||
append("^")
|
||||
}
|
||||
}
|
||||
throw CliException(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 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.commands
|
||||
|
||||
import com.github.ajalt.clikt.parameters.groups.OptionGroup
|
||||
import com.github.ajalt.clikt.parameters.options.*
|
||||
import com.github.ajalt.clikt.parameters.types.long
|
||||
import com.github.ajalt.clikt.parameters.types.path
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.util.regex.Pattern
|
||||
import java.util.stream.Collectors
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.isRegularFile
|
||||
import org.pkl.commons.cli.CliBaseOptions
|
||||
import org.pkl.core.util.IoUtils
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class BaseOptions : OptionGroup() {
|
||||
companion object {
|
||||
fun includedCARootCerts(): InputStream {
|
||||
return BaseOptions::class.java.getResourceAsStream("IncludedCARoots.pem")!!
|
||||
}
|
||||
}
|
||||
|
||||
private val defaults = CliBaseOptions()
|
||||
|
||||
private val output =
|
||||
arrayOf("json", "jsonnet", "pcf", "properties", "plist", "textproto", "xml", "yaml")
|
||||
|
||||
val allowedModules: List<Pattern> by
|
||||
option(
|
||||
names = arrayOf("--allowed-modules"),
|
||||
help = "URI patterns that determine which modules can be loaded and evaluated."
|
||||
)
|
||||
.convert("<pattern1,pattern2>") { Pattern.compile(it) }
|
||||
.splitAll(",")
|
||||
|
||||
val allowedResources: List<Pattern> by
|
||||
option(
|
||||
names = arrayOf("--allowed-resources"),
|
||||
help = "URI patterns that determine which external resources can be read."
|
||||
)
|
||||
.convert("<pattern1,pattern2>") { Pattern.compile(it) }
|
||||
.splitAll(",")
|
||||
|
||||
val rootDir: Path? by
|
||||
option(
|
||||
names = arrayOf("--root-dir"),
|
||||
help =
|
||||
"Restricts access to file-based modules and resources to those located under the root directory."
|
||||
)
|
||||
.single()
|
||||
.path()
|
||||
|
||||
val cacheDir: Path? by
|
||||
option(names = arrayOf("--cache-dir"), help = "The cache directory for storing packages.")
|
||||
.single()
|
||||
.path()
|
||||
|
||||
val workingDir: Path by
|
||||
option(
|
||||
names = arrayOf("-w", "--working-dir"),
|
||||
help = "Base path that relative module paths are resolved against."
|
||||
)
|
||||
.single()
|
||||
.path()
|
||||
.default(defaults.normalizedWorkingDir)
|
||||
|
||||
val properties: Map<String, String> by
|
||||
option(
|
||||
names = arrayOf("-p", "--property"),
|
||||
metavar = "<name=value>",
|
||||
help = "External property to set (repeatable)."
|
||||
)
|
||||
.associate()
|
||||
|
||||
val noCache: Boolean by
|
||||
option(names = arrayOf("--no-cache"), help = "Disable cacheing of packages")
|
||||
.single()
|
||||
.flag(default = false)
|
||||
|
||||
val format: String? by
|
||||
option(
|
||||
names = arrayOf("-f", "--format"),
|
||||
help = "Output format to generate. <${output.joinToString()}>"
|
||||
)
|
||||
.single()
|
||||
|
||||
val envVars: Map<String, String> by
|
||||
option(
|
||||
names = arrayOf("-e", "--env-var"),
|
||||
metavar = "<name=value>",
|
||||
help = "Environment variable to set (repeatable)."
|
||||
)
|
||||
.associate()
|
||||
|
||||
val modulePath: List<Path> by
|
||||
option(
|
||||
names = arrayOf("--module-path"),
|
||||
metavar = "<path1${File.pathSeparator}path2>",
|
||||
help =
|
||||
"Directories, ZIP archives, or JAR archives to search when resolving `modulepath:` URIs."
|
||||
)
|
||||
.path()
|
||||
.splitAll(File.pathSeparator)
|
||||
|
||||
val settings: URI? by
|
||||
option(names = arrayOf("--settings"), help = "Pkl settings module to use.").single().convert {
|
||||
IoUtils.toUri(it)
|
||||
}
|
||||
|
||||
val timeout: Duration? by
|
||||
option(
|
||||
names = arrayOf("-t", "--timeout"),
|
||||
metavar = "<number>",
|
||||
help = "Duration, in seconds, after which evaluation of a source module will be timed out."
|
||||
)
|
||||
.single()
|
||||
.long()
|
||||
.convert { Duration.ofSeconds(it) }
|
||||
|
||||
val caCertificates: List<Path> by
|
||||
option(
|
||||
names = arrayOf("--ca-certificates"),
|
||||
metavar = "<path>",
|
||||
help = "Replaces the built-in CA certificates with the provided certificate file."
|
||||
)
|
||||
.path()
|
||||
.multiple()
|
||||
|
||||
/**
|
||||
* 1. If `--ca-certificates` option is not empty, use that.
|
||||
* 2. If directory `~/.pkl/cacerts` is not empty, use that.
|
||||
* 3. Use the bundled CA certificates.
|
||||
*/
|
||||
private fun getEffectiveCaCertificates(): List<Any> {
|
||||
return caCertificates
|
||||
.ifEmpty {
|
||||
val home = System.getProperty("user.home")
|
||||
val cacerts = Path.of(home, ".pkl", "cacerts")
|
||||
if (cacerts.exists() && cacerts.isDirectory())
|
||||
Files.list(cacerts).filter(Path::isRegularFile).collect(Collectors.toList())
|
||||
else emptyList()
|
||||
}
|
||||
.ifEmpty { listOf(includedCARootCerts()) }
|
||||
}
|
||||
|
||||
fun baseOptions(
|
||||
modules: List<URI>,
|
||||
projectOptions: ProjectOptions? = null,
|
||||
testMode: Boolean = false
|
||||
): CliBaseOptions {
|
||||
return CliBaseOptions(
|
||||
sourceModules = modules,
|
||||
allowedModules = allowedModules.ifEmpty { null },
|
||||
allowedResources = allowedResources.ifEmpty { null },
|
||||
environmentVariables = envVars.ifEmpty { null },
|
||||
externalProperties = properties.mapValues { it.value.ifBlank { "true" } }.ifEmpty { null },
|
||||
modulePath = modulePath.ifEmpty { null },
|
||||
workingDir = workingDir,
|
||||
settings = settings,
|
||||
rootDir = rootDir,
|
||||
projectDir = projectOptions?.projectDir,
|
||||
timeout = timeout,
|
||||
moduleCacheDir = cacheDir ?: defaults.normalizedModuleCacheDir,
|
||||
noCache = noCache,
|
||||
testMode = testMode,
|
||||
omitProjectSettings = projectOptions?.omitProjectSettings ?: false,
|
||||
noProject = projectOptions?.noProject ?: false,
|
||||
caCertificates = getEffectiveCaCertificates()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.commands
|
||||
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.arguments.convert
|
||||
import com.github.ajalt.clikt.parameters.arguments.multiple
|
||||
import com.github.ajalt.clikt.parameters.groups.provideDelegate
|
||||
import java.net.URI
|
||||
|
||||
abstract class ModulesCommand(name: String, helpLink: String, help: String = "") :
|
||||
BaseCommand(
|
||||
name = name,
|
||||
help = help,
|
||||
helpLink = helpLink,
|
||||
) {
|
||||
open val modules: List<URI> by
|
||||
argument(name = "<modules>", help = "Module paths or URIs to evaluate.")
|
||||
.convert { parseModuleName(it) }
|
||||
.multiple(required = true)
|
||||
|
||||
protected val projectOptions by ProjectOptions()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.commands
|
||||
|
||||
import com.github.ajalt.clikt.parameters.options.NullableOption
|
||||
import com.github.ajalt.clikt.parameters.options.OptionWithValues
|
||||
import com.github.ajalt.clikt.parameters.options.transformAll
|
||||
|
||||
/** Forbid this option from being repeated. */
|
||||
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.single(): NullableOption<EachT, ValueT> {
|
||||
return transformAll {
|
||||
if (it.size > 1) {
|
||||
fail("Option cannot be repeated")
|
||||
}
|
||||
it.lastOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow this option to be repeated and to receive multiple values separated by [separator]. This is
|
||||
* a mix of [split][com.github.ajalt.clikt.parameters.options.split] and
|
||||
* [multiple][com.github.ajalt.clikt.parameters.options.multiple] joined together.
|
||||
*/
|
||||
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.splitAll(
|
||||
separator: String = ",",
|
||||
default: List<ValueT> = emptyList()
|
||||
): OptionWithValues<List<ValueT>, List<ValueT>, ValueT> {
|
||||
return copy(
|
||||
transformValue = transformValue,
|
||||
transformEach = { it },
|
||||
transformAll = { it.flatten().ifEmpty { default } },
|
||||
validator = {},
|
||||
nvalues = 1,
|
||||
valueSplit = Regex.fromLiteral(separator)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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.commands
|
||||
|
||||
import com.github.ajalt.clikt.parameters.groups.OptionGroup
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.github.ajalt.clikt.parameters.types.path
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Options related to projects for CLI commands that are related to normal evaluation (`pkl eval`,
|
||||
* `pkl test`).
|
||||
*/
|
||||
class ProjectOptions : OptionGroup() {
|
||||
val projectDir: Path? by
|
||||
option(
|
||||
names = arrayOf("--project-dir"),
|
||||
metavar = "<path>",
|
||||
help =
|
||||
"The project directory to use for this command. By default, searches up from the working directory for a PklProject file."
|
||||
)
|
||||
.single()
|
||||
.path()
|
||||
|
||||
val omitProjectSettings: Boolean by
|
||||
option(
|
||||
names = arrayOf("--omit-project-settings"),
|
||||
help = "Ignores evaluator settings set in the PklProject file."
|
||||
)
|
||||
.single()
|
||||
.flag(default = false)
|
||||
|
||||
val noProject: Boolean by
|
||||
option(
|
||||
names = arrayOf("--no-project"),
|
||||
help = "Disables loading settings and dependencies from the PklProject file."
|
||||
)
|
||||
.single()
|
||||
.flag(default = false)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.commands
|
||||
|
||||
import com.github.ajalt.clikt.parameters.groups.OptionGroup
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.github.ajalt.clikt.parameters.types.path
|
||||
import java.nio.file.Path
|
||||
import org.pkl.commons.cli.CliTestOptions
|
||||
|
||||
class TestOptions : OptionGroup() {
|
||||
private val junitReportDir: Path? by
|
||||
option(
|
||||
names = arrayOf("--junit-reports"),
|
||||
metavar = "<dir>",
|
||||
help = "Directory where to store JUnit reports."
|
||||
)
|
||||
.path()
|
||||
|
||||
private val overwrite: Boolean by
|
||||
option(names = arrayOf("--overwrite"), help = "Force generation of expected examples.").flag()
|
||||
|
||||
val cliTestOptions: CliTestOptions by lazy { CliTestOptions(junitReportDir, overwrite) }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user