Initial commit

This commit is contained in:
Peter Niederwieser
2016-01-19 14:51:19 +01:00
committed by Dan Chao
commit ecad035dca
2972 changed files with 211653 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
/**
* 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.cli.svm;
import org.graalvm.nativeimage.hosted.Feature;
import org.pkl.core.runtime.BaseModule;
/**
* This class is registered with native-image via a CLI option (see Gradle task `nativeExecutable`).
*/
@SuppressWarnings({"unused", "ResultOfMethodCallIgnored"})
public final class InitFeature implements Feature {
/**
* Enforce that {@link BaseModule#getModule()}'s static initializer completes before depending
* static initializers are invoked. This is necessary to avoid deadlocks in native-image's
* multi-threaded execution of static initializers. It's not clear at this point if multi-threaded
* initialization on the JVM could also deadlock, i.e., if this is a Pkl bug.
*/
public void duringSetup(DuringSetupAccess access) {
BaseModule.getModule();
}
}

View File

@@ -0,0 +1,34 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.TargetClass;
/**
* native-image can't determine how calls to {@link sun.misc.Unsafe#arrayBaseOffset(Class)} affect
* static fields in msgpack's {@code MessageBuffer}.
*
* <p>This informs the compiler which field to re-compute.
*/
@SuppressWarnings("unused")
@TargetClass(className = "org.msgpack.core.buffer.MessageBuffer")
final class MessagePackRecomputations {
@Alias
@RecomputeFieldValue(kind = RecomputeFieldValue.Kind.ArrayBaseOffset, declClass = byte[].class)
static int ARRAY_BYTE_BASE_OFFSET;
}

View File

@@ -0,0 +1,52 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.truffle.TruffleFeature;
import java.util.Map;
/**
* Workaround to prevent the native-image build error "Detected a started Thread in the image
* heap.". The cause of this error is the use of {@link org.graalvm.polyglot.Context} in the
* (intentionally) statically reachable class {@link org.pkl.core.runtime.StdLibModule}.
*
* <p>A cleaner solution would be to have a separate {@link org.pkl.core.ast.builder.AstBuilder} for
* stdlib modules that produces a fully initialized module object without executing any Truffle
* nodes.
*
* <p>This class is automatically discovered by native-image; no registration is required.
*/
@SuppressWarnings({"unused", "ClassName"})
@TargetClass(
className = "com.oracle.truffle.polyglot.PolyglotContextImpl",
onlyWith = {TruffleFeature.IsEnabled.class})
public final class PolyglotContextImplTarget {
@Alias
@RecomputeFieldValue(kind = Kind.NewInstance, declClassName = "java.util.HashMap")
public Map<?, ?> threads;
@Alias
@RecomputeFieldValue(kind = Kind.Reset)
public WeakAssumedValueTarget singleThreadValue;
@Alias
@RecomputeFieldValue(kind = Kind.Reset)
public PolyglotThreadInfoTarget cachedThreadInfo;
}

View File

@@ -0,0 +1,25 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.truffle.TruffleFeature;
/** Makes non-public class PolyglotThreadInfo usable above. */
@TargetClass(
className = "com.oracle.truffle.polyglot.PolyglotThreadInfo",
onlyWith = {TruffleFeature.IsEnabled.class})
public final class PolyglotThreadInfoTarget {}

View File

@@ -0,0 +1,33 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.truffle.TruffleFeature;
import java.util.Map;
@SuppressWarnings("unused")
@TargetClass(
className = "com.oracle.truffle.api.impl.ThreadLocalHandshake",
onlyWith = {TruffleFeature.IsEnabled.class})
public final class ThreadLocalHandshakeTarget {
@Alias
@RecomputeFieldValue(kind = Kind.NewInstance, declClassName = "java.util.HashMap")
static Map<?, ?> SAFEPOINTS;
}

View File

@@ -0,0 +1,25 @@
/**
* 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.cli.svm;
import com.oracle.svm.core.annotate.TargetClass;
import com.oracle.svm.truffle.TruffleFeature;
/** Makes non-public class WeakAssumedValue usable. */
@TargetClass(
className = "com.oracle.truffle.polyglot.WeakAssumedValue",
onlyWith = {TruffleFeature.IsEnabled.class})
public final class WeakAssumedValueTarget {}

View File

@@ -0,0 +1,48 @@
/**
* 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.cli
import java.nio.file.Files
import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliBaseOptions.Companion.getProjectFile
import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliException
import org.pkl.core.module.ProjectDependenciesManager.PKL_PROJECT_FILENAME
abstract class CliAbstractProjectCommand(
cliOptions: CliBaseOptions,
private val projectDirs: List<Path>
) : CliCommand(cliOptions) {
protected val normalizedProjectFiles: List<Path> by lazy {
if (projectDirs.isEmpty()) {
val projectFile =
cliOptions.normalizedWorkingDir.getProjectFile(cliOptions.normalizedRootDir)
?: throw CliException(
"No project visible to the working directory. Ensure there is a PklProject file in the workspace, or provide an explicit project directory as an argument."
)
return@lazy listOf(projectFile.normalize())
}
projectDirs.map { dir ->
val projectFile = dir.resolve(PKL_PROJECT_FILENAME)
if (!Files.exists(projectFile)) {
throw CliException("Directory $dir does not contain a PklProject file.")
}
projectFile.normalize()
}
}
}

View File

@@ -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.cli
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliException
import org.pkl.core.packages.PackageResolver
import org.pkl.core.packages.PackageUri
class CliDownloadPackageCommand(
baseOptions: CliBaseOptions,
private val packageUris: List<PackageUri>,
private val noTranstive: Boolean
) : CliCommand(baseOptions) {
override fun doRun() {
if (moduleCacheDir == null) {
throw CliException("Cannot download packages because no cache directory is specified.")
}
val packageResolver = PackageResolver.getInstance(securityManager, moduleCacheDir)
val errors = mutableMapOf<PackageUri, Throwable>()
for (pkg in packageUris) {
try {
packageResolver.downloadPackage(pkg, pkg.checksums, noTranstive)
} catch (e: Throwable) {
errors[pkg] = e
}
}
when (errors.size) {
0 -> return
1 -> throw CliException(errors.values.single().message!!)
else ->
throw CliException(
buildString {
appendLine("Failed to download some packages.")
for ((uri, error) in errors) {
appendLine()
appendLine("Failed to download $uri because:")
appendLine("${error.message}")
}
}
)
}
}
}

View File

@@ -0,0 +1,236 @@
/**
* 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.cli
import java.io.File
import java.io.Reader
import java.io.Writer
import java.net.URI
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliException
import org.pkl.commons.createParentDirectories
import org.pkl.commons.currentWorkingDir
import org.pkl.commons.writeString
import org.pkl.core.EvaluatorBuilder
import org.pkl.core.ModuleSource
import org.pkl.core.PklException
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.module.ModulePathResolver
import org.pkl.core.runtime.ModuleResolver
import org.pkl.core.runtime.VmException
import org.pkl.core.runtime.VmUtils
import org.pkl.core.util.IoUtils
private data class OutputFile(val pathSpec: String, val moduleUri: URI)
/** API equivalent of the Pkl command-line evaluator. */
class CliEvaluator
@JvmOverloads
constructor(
private val options: CliEvaluatorOptions,
// use System.{in,out}() rather than System.console()
// because the latter returns null when output is sent through a unix pipe
private val consoleReader: Reader = System.`in`.reader(),
private val consoleWriter: Writer = System.out.writer(),
) : CliCommand(options.base) {
/**
* Output files for the modules to be evaluated. Returns `null` if `options.outputPath` is `null`.
* Multiple modules may be mapped to the same output file, in which case their outputs are
* concatenated with [CliEvaluatorOptions.moduleOutputSeparator].
*/
@Suppress("MemberVisibilityCanBePrivate")
val outputFiles: Set<File>? by lazy {
fileOutputPaths?.values?.mapTo(mutableSetOf(), Path::toFile)
}
/**
* Output directories for the modules to be evaluated. Returns `null` if
* `options.multipleFileOutputPath` is `null`.
*/
@Suppress("MemberVisibilityCanBePrivate")
val outputDirectories: Set<File>? by lazy {
directoryOutputPaths?.values?.mapTo(mutableSetOf(), Path::toFile)
}
/** The file output path */
val fileOutputPaths: Map<URI, Path>? by lazy {
if (options.multipleFileOutputPath != null) return@lazy null
options.outputPath?.let { resolveOutputPaths(it) }
}
private val directoryOutputPaths: Map<URI, Path>? by lazy {
options.multipleFileOutputPath?.let { resolveOutputPaths(it) }
}
/**
* Evaluates source modules according to [options].
*
* If [CliEvaluatorOptions.outputPath] is set, each module's `output.text` is written to the
* module's [output file][outputFiles]. If [CliEvaluatorOptions.multipleFileOutputPath] is set,
* each module's `output.files` are written to the module's [output directory][outputDirectories].
* Otherwise, each module's `output.text` is written to [consoleWriter] (which defaults to
* standard out).
*
* Throws [CliException] in case of an error.
*/
override fun doRun() {
val builder = evaluatorBuilder()
try {
if (options.multipleFileOutputPath != null) {
writeMultipleFileOutput(builder)
} else {
writeOutput(builder)
}
} finally {
ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)
}
}
private fun resolveOutputPaths(pathStr: String): Map<URI, Path> {
val moduleUris = options.base.normalizedSourceModules
val workingDir = options.base.normalizedWorkingDir
// used just to resolve the `%{moduleName}` placeholder
val moduleResolver = ModuleResolver(moduleKeyFactories(ModulePathResolver.empty()))
return moduleUris.associateWith { uri ->
val moduleDir: String? =
IoUtils.toPath(uri)?.let { workingDir.relativize(it.parent).toString().ifEmpty { "." } }
val moduleKey =
try {
moduleResolver.resolve(uri)
} catch (e: VmException) {
throw e.toPklException(stackFrameTransformer)
}
val substituted =
pathStr
.replace("%{moduleName}", IoUtils.inferModuleName(moduleKey))
.replace("%{outputFormat}", options.outputFormat ?: "%{outputFormat}")
.replace("%{moduleDir}", moduleDir ?: "%{moduleDir}")
if (substituted.contains("%{moduleDir}")) {
throw PklException(
"Cannot substitute output path placeholder `%{moduleDir}` " +
"because module `$uri` does not have a file system path."
)
}
val absolutePath = workingDir.resolve(substituted).normalize()
absolutePath
}
}
/** Renders each module's `output.text`, writing it to the specified output file. */
private fun writeOutput(builder: EvaluatorBuilder) {
val evaluator = builder.setOutputFormat(options.outputFormat).build()
evaluator.use {
val outputFiles = fileOutputPaths
if (outputFiles != null) {
// files that we've written non-empty output to
// YamlRenderer produces empty output if `isStream` is true and `output.value` is empty
// collection
val writtenFiles = mutableSetOf<Path>()
for ((moduleUri, outputFile) in outputFiles) {
val moduleSource = toModuleSource(moduleUri, consoleReader)
val output = evaluator.evaluateExpressionString(moduleSource, options.expression)
outputFile.createParentDirectories()
if (!writtenFiles.contains(outputFile)) {
// write file even if output is empty to overwrite output from previous runs
outputFile.writeString(output)
if (output.isNotEmpty()) {
writtenFiles.add(outputFile)
}
} else {
if (output.isNotEmpty()) {
outputFile.writeString(
options.moduleOutputSeparator + IoUtils.getLineSeparator(),
Charsets.UTF_8,
StandardOpenOption.WRITE,
StandardOpenOption.APPEND
)
outputFile.writeString(
output,
Charsets.UTF_8,
StandardOpenOption.WRITE,
StandardOpenOption.APPEND
)
}
}
}
} else {
var outputWritten = false
for (moduleUri in options.base.normalizedSourceModules) {
val moduleSource = toModuleSource(moduleUri, consoleReader)
val output = evaluator.evaluateExpressionString(moduleSource, options.expression)
if (output.isNotEmpty()) {
if (outputWritten) consoleWriter.appendLine(options.moduleOutputSeparator)
consoleWriter.write(output)
consoleWriter.flush()
outputWritten = true
}
}
}
}
}
private fun toModuleSource(uri: URI, reader: Reader) =
if (uri == VmUtils.REPL_TEXT_URI) ModuleSource.create(uri, reader.readText())
else ModuleSource.uri(uri)
/**
* Renders each module's `output.files`, writing each entry as a file into the specified output
* directory.
*/
private fun writeMultipleFileOutput(builder: EvaluatorBuilder) {
val outputDirs = directoryOutputPaths!!
val writtenFiles = mutableMapOf<Path, OutputFile>()
for ((moduleUri, outputDir) in outputDirs) {
val evaluator = builder.setOutputFormat(options.outputFormat).build()
if (outputDir.exists() && !outputDir.isDirectory()) {
throw CliException("Output path `$outputDir` exists and is not a directory.")
}
val moduleSource = toModuleSource(moduleUri, consoleReader)
val output = evaluator.evaluateOutputFiles(moduleSource)
for ((pathSpec, fileOutput) in output) {
val resolvedPath = outputDir.resolve(pathSpec).normalize()
val realPath = if (resolvedPath.exists()) resolvedPath.toRealPath() else resolvedPath
if (!realPath.startsWith(outputDir)) {
throw CliException(
"Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is outside output directory `$outputDir`."
)
}
val previousOutput = writtenFiles[realPath]
if (previousOutput != null) {
throw CliException(
"Output file conflict: `output.files` entries `\"${previousOutput.pathSpec}\"` in module `${previousOutput.moduleUri}` and `\"$pathSpec\"` in module `$moduleUri` resolve to the same file path `$realPath`."
)
}
if (realPath.isDirectory()) {
throw CliException(
"Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is a directory."
)
}
writtenFiles[realPath] = OutputFile(pathSpec, moduleUri)
realPath.createParentDirectories()
realPath.writeString(fileOutput.text)
consoleWriter.write(currentWorkingDir.relativize(resolvedPath).toString() + "\n")
consoleWriter.flush()
}
}
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.cli
import org.pkl.commons.cli.CliBaseOptions
/** Configuration options for [CliEvaluator]. */
data class CliEvaluatorOptions(
/** Base options shared between CLI commands. */
val base: CliBaseOptions,
/**
* The file path where the output file is placed. If multiple source modules are given,
* placeholders can be used to map them to different output files. If multiple modules are mapped
* to the same output file, their outputs are concatenated. Currently, the only available
* concatenation strategy is to separate outputs with `---`, as in a YAML stream.
*
* The following placeholders are supported:
* - `%{moduleDir}` The directory path of the module, relative to the working directory. Only
* available when evaluating file-based modules.
* - `%{moduleName}` The simple module name as inferred from the module URI. For hierarchical
* URIs, this is the last path segment without file extension.
* - `%{outputFormat}` The requested output format. Only available if `outputFormat` is non-null.
*
* If [CliBaseOptions.workingDir] corresponds to a file system path, relative output paths are
* resolved against that path. Otherwise, relative output paths are not allowed.
*
* If `null`, output is written to the console.
*/
val outputPath: String? = null,
/**
* The output format to generate.
*
* The default output renderer for a module supports the following formats:
* - `"json"`
* - `"jsonnet"`
* - `"pcf"` (default)
* - `"plist"`
* - `"properties"`
* - `"textproto"`
* - `"xml"`
* - `"yaml"`
*/
val outputFormat: String? = null,
/** The separator to use when multiple module outputs are written to the same location. */
val moduleOutputSeparator: String = "---",
/**
* The directory where a module's output files are placed.
*
* Setting this option causes Pkl to evaluate `output.files` instead of `output.text`, and write
* files using each entry's key as the file path relative to [multipleFileOutputPath], and each
* value's `text` property as the file's contents.
*/
val multipleFileOutputPath: String? = null,
/**
* The expression to evaluate within the module.
*
* If set, the said expression is evaluated under the context of the enclosing module.
*
* If unset, the module's `output.text` property evaluated.
*/
val expression: String = "output.text",
) {
companion object {
val defaults = CliEvaluatorOptions(CliBaseOptions())
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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.cli
import java.io.Writer
import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
import org.pkl.commons.cli.CliTestException
import org.pkl.commons.cli.CliTestOptions
import org.pkl.core.project.Project
import org.pkl.core.project.ProjectPackager
import org.pkl.core.util.ErrorMessages
class CliProjectPackager(
baseOptions: CliBaseOptions,
projectDirs: List<Path>,
private val testOptions: CliTestOptions,
private val outputPath: String,
private val skipPublishCheck: Boolean,
private val consoleWriter: Writer = System.out.writer(),
private val errWriter: Writer = System.err.writer()
) : CliAbstractProjectCommand(baseOptions, projectDirs) {
private fun runApiTests(project: Project) {
val apiTests = project.`package`!!.apiTests
if (apiTests.isEmpty()) return
val normalizeApiTests = apiTests.map { project.projectDir.resolve(it).toUri() }
val testRunner =
CliTestRunner(
cliOptions.copy(sourceModules = normalizeApiTests, projectDir = project.projectDir),
testOptions = testOptions,
consoleWriter = consoleWriter,
errWriter = errWriter,
)
try {
testRunner.run()
} catch (e: CliTestException) {
throw CliException(ErrorMessages.create("packageTestsFailed", project.`package`!!.uri))
}
}
override fun doRun() {
val projects = buildList {
for (projectFile in normalizedProjectFiles) {
val project = loadProject(projectFile)
project.`package`
?: throw CliException(
ErrorMessages.create("noPackageDefinedByProject", project.projectFileUri)
)
runApiTests(project)
add(project)
}
}
// Require that all local projects are included
projects.forEach { proj ->
proj.dependencies.localDependencies.values.forEach { localDep ->
val projectDir = Path.of(localDep.projectFileUri).parent
if (projects.none { it.projectDir == projectDir }) {
throw CliException(
ErrorMessages.create("missingProjectInPackageCommand", proj.projectDir, projectDir)
)
}
}
}
ProjectPackager(
projects,
cliOptions.normalizedWorkingDir,
outputPath,
stackFrameTransformer,
securityManager,
skipPublishCheck,
consoleWriter
)
.createPackages()
}
}

View File

@@ -0,0 +1,53 @@
/**
* 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.cli
import java.io.Writer
import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.core.SecurityManagers
import org.pkl.core.module.ProjectDependenciesManager
import org.pkl.core.packages.PackageResolver
import org.pkl.core.project.ProjectDependenciesResolver
class CliProjectResolver(
baseOptions: CliBaseOptions,
projectDirs: List<Path>,
private val consoleWriter: Writer = System.out.writer(),
private val errWriter: Writer = System.err.writer()
) : CliAbstractProjectCommand(baseOptions, projectDirs) {
override fun doRun() {
for (projectFile in normalizedProjectFiles) {
val project = loadProject(projectFile)
val packageResolver =
PackageResolver.getInstance(
SecurityManagers.standard(
allowedModules,
allowedResources,
SecurityManagers.defaultTrustLevels,
rootDir
),
moduleCacheDir
)
val dependencies = ProjectDependenciesResolver(project, packageResolver, errWriter).resolve()
val depsFile =
projectFile.parent.resolve(ProjectDependenciesManager.PKL_PROJECT_DEPS_FILENAME).toFile()
depsFile.outputStream().use { dependencies.writeTo(it) }
consoleWriter.appendLine(depsFile.toString())
consoleWriter.flush()
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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.cli
import org.pkl.cli.repl.Repl
import org.pkl.commons.cli.CliCommand
import org.pkl.core.Loggers
import org.pkl.core.SecurityManagers
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.module.ModulePathResolver
import org.pkl.core.repl.ReplServer
import org.pkl.core.resource.ResourceReaders
internal class CliRepl(private val options: CliEvaluatorOptions) : CliCommand(options.base) {
override fun doRun() {
ModulePathResolver(modulePath).use { modulePathResolver ->
// TODO: send options as command
val server =
ReplServer(
SecurityManagers.standard(
allowedModules,
allowedResources,
SecurityManagers.defaultTrustLevels,
rootDir
),
Loggers.stdErr(),
listOf(
ModuleKeyFactories.standardLibrary,
ModuleKeyFactories.modulePath(modulePathResolver)
) +
ModuleKeyFactories.fromServiceProviders() +
listOf(
ModuleKeyFactories.file,
ModuleKeyFactories.pkg,
ModuleKeyFactories.projectpackage,
ModuleKeyFactories.genericUrl
),
listOf(
ResourceReaders.environmentVariable(),
ResourceReaders.externalProperty(),
ResourceReaders.modulePath(modulePathResolver),
ResourceReaders.file(),
ResourceReaders.http(),
ResourceReaders.https(),
ResourceReaders.pkg(),
ResourceReaders.projectpackage()
),
environmentVariables,
externalProperties,
moduleCacheDir,
project?.dependencies,
options.outputFormat,
options.base.normalizedWorkingDir,
stackFrameTransformer
)
Repl(options.base.normalizedWorkingDir, server).run()
}
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.cli
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliException
import org.pkl.server.MessageTransports
import org.pkl.server.ProtocolException
import org.pkl.server.Server
class CliServer(options: CliBaseOptions) : CliCommand(options) {
override fun doRun() =
try {
val server = Server(MessageTransports.stream(System.`in`, System.out))
server.use { it.start() }
} catch (e: ProtocolException) {
throw CliException(e.message!!)
}
}

View File

@@ -0,0 +1,104 @@
/**
* 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.cli
import java.io.Writer
import org.pkl.commons.cli.*
import org.pkl.core.EvaluatorBuilder
import org.pkl.core.ModuleSource.uri
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.stdlib.test.report.JUnitReport
import org.pkl.core.stdlib.test.report.SimpleReport
import org.pkl.core.util.ErrorMessages
class CliTestRunner
@JvmOverloads
constructor(
private val options: CliBaseOptions,
private val testOptions: CliTestOptions,
private val consoleWriter: Writer = System.out.writer(),
private val errWriter: Writer = System.err.writer()
) : CliCommand(options) {
override fun doRun() {
val builder = evaluatorBuilder()
try {
evalTest(builder)
} finally {
ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)
}
}
private fun evalTest(builder: EvaluatorBuilder) {
val sources =
options.normalizedSourceModules.ifEmpty { project?.tests?.map { it.toUri() } }
?:
// keep in sync with error message thrown by clikt
throw CliException(
"""
Usage: pkl test [OPTIONS] <modules>...
Error: Missing argument "<modules>"
"""
.trimIndent()
)
val evaluator = builder.build()
evaluator.use {
var failed = false
val moduleNames = mutableSetOf<String>()
for (moduleUri in sources) {
try {
val results = evaluator.evaluateTest(uri(moduleUri), testOptions.overwrite)
if (!failed) {
failed = results.failed()
}
SimpleReport().report(results, consoleWriter)
consoleWriter.flush()
val junitDir = testOptions.junitDir
if (junitDir != null) {
junitDir.toFile().mkdirs()
val moduleName = "${results.moduleName}.xml"
if (moduleName in moduleNames) {
throw RuntimeException(
"""
Cannot generate JUnit report for $moduleUri.
A report with the same name was already generated.
To fix, provide a different name for this module by adding a module header.
"""
.trimIndent()
)
}
moduleNames += moduleName
JUnitReport().reportToPath(results, junitDir.resolve(moduleName))
}
} catch (ex: Exception) {
errWriter.appendLine("Error evaluating module ${moduleUri.path}:")
errWriter.write(ex.message ?: "")
if (moduleUri != sources.last()) {
errWriter.appendLine()
}
errWriter.flush()
failed = true
}
}
if (failed) {
throw CliTestException(ErrorMessages.create("testsFailed"))
}
}
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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.
*/
@file:JvmName("Main")
package org.pkl.cli
import com.github.ajalt.clikt.core.subcommands
import org.pkl.cli.commands.*
import org.pkl.commons.cli.CliMain
import org.pkl.commons.cli.cliMain
import org.pkl.core.Release
/** Main method of the Pkl CLI (command-line evaluator and REPL). */
internal fun main(args: Array<String>) {
val version = Release.current().versionInfo()
val helpLink = "${Release.current().documentation().homepage()}pkl-cli/index.html#usage"
val commands =
arrayOf(
EvalCommand(helpLink),
ReplCommand(helpLink),
ServerCommand(helpLink),
TestCommand(helpLink),
ProjectCommand(helpLink),
DownloadPackageCommand(helpLink)
)
val cmd = RootCommand("pkl", version, helpLink).subcommands(*commands)
cliMain {
if (CliMain.compat == "alpine") {
// Alpine's main thread has a prohibitively small stack size by default;
// https://github.com/oracle/graal/issues/3398
var throwable: Throwable? = null
Thread(null, { cmd.main(args) }, "alpineMain", 10000000).apply {
setUncaughtExceptionHandler { _, t -> throwable = t }
start()
join()
}
throwable?.let { throw it }
} else {
cmd.main(args)
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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.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 com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import org.pkl.cli.CliDownloadPackageCommand
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.ProjectOptions
import org.pkl.commons.cli.commands.single
import org.pkl.core.packages.PackageUri
class DownloadPackageCommand(helpLink: String) :
BaseCommand(
name = "download-package",
helpLink = helpLink,
help =
"""
Download package(s)
This command downloads the specified packages to the cache directory.
If the package already exists in the cache directory, this command is a no-op.
Examples:
```
# Download two packages
$ pkl download-package package://example.com/package1@1.0.0 package://example.com/package2@1.0.0
```
"""
.trimIndent()
) {
private val projectOptions by ProjectOptions()
private val packageUris: List<PackageUri> by
argument("<package>", "The package URIs to download")
.convert { PackageUri(it) }
.multiple(required = true)
private val noTransitive: Boolean by
option(
names = arrayOf("--no-transitive"),
help = "Skip downloading transitive dependencies of a package"
)
.single()
.flag()
override fun run() {
CliDownloadPackageCommand(
baseOptions.baseOptions(emptyList(), projectOptions),
packageUris,
noTransitive
)
.run()
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.validate
import org.pkl.cli.CliEvaluator
import org.pkl.cli.CliEvaluatorOptions
import org.pkl.commons.cli.commands.ModulesCommand
import org.pkl.commons.cli.commands.single
class EvalCommand(helpLink: String) :
ModulesCommand(
name = "eval",
help = "Render pkl module(s)",
helpLink = helpLink,
) {
private val outputPath: String? by
option(
names = arrayOf("-o", "--output-path"),
metavar = "<path>",
help = "File path where the output file is placed."
)
.single()
private val moduleOutputSeparator: String by
option(
names = arrayOf("--module-output-separator"),
metavar = "<string>",
help =
"Separator to use when multiple module outputs are written to the same file. (default: ---)"
)
.single()
.default("---")
private val expression: String? by
option(
names = arrayOf("-x", "--expression"),
metavar = "<expression>",
help = "Expression to be evaluated within the module."
)
.single()
private val multipleFileOutputPath: String? by
option(
names = arrayOf("-m", "--multiple-file-output-path"),
metavar = "<path>",
help = "Directory where a module's multiple file output is placed."
)
.single()
.validate {
if (outputPath != null || expression != null) {
fail("Option is mutually exclusive with -o, --output-path and -x, --expression.")
}
}
// hidden option used by the native tests
private val testMode: Boolean by
option(names = arrayOf("--test-mode"), help = "Internal test mode", hidden = true).flag()
override fun run() {
val options =
CliEvaluatorOptions(
base = baseOptions.baseOptions(modules, projectOptions, testMode = testMode),
outputPath = outputPath,
outputFormat = baseOptions.format,
moduleOutputSeparator = moduleOutputSeparator,
multipleFileOutputPath = multipleFileOutputPath,
expression = expression ?: CliEvaluatorOptions.defaults.expression
)
CliEvaluator(options).run()
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.default
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.cli.CliProjectPackager
import org.pkl.cli.CliProjectResolver
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.TestOptions
import org.pkl.commons.cli.commands.single
class ProjectCommand(helpLink: String) :
NoOpCliktCommand(
name = "project",
help = "Run commands related to projects",
epilog = "For more information, visit $helpLink"
) {
init {
subcommands(ResolveCommand(helpLink), PackageCommand(helpLink))
}
companion object {
class ResolveCommand(helpLink: String) :
BaseCommand(
name = "resolve",
helpLink = helpLink,
help =
"""
Resolve dependencies for project(s)
This command takes the `dependencies` of `PklProject`s, and writes the
resolved versions to `PklProject.deps.json` files.
Examples:
```
# Search the current working directory for a project, and resolve its dependencies.
$ pkl project resolve
# Resolve dependencies for all projects within the `packages/` directory.
$ pkl project resolve packages/*/
```
""",
) {
private val projectDirs: List<Path> by
argument("<dir>", "The project directories to resolve dependencies for").path().multiple()
override fun run() {
CliProjectResolver(baseOptions.baseOptions(emptyList()), projectDirs).run()
}
}
private const val NEWLINE = '\u0085'
class PackageCommand(helpLink: String) :
BaseCommand(
name = "package",
helpLink = helpLink,
help =
"""
Verify package(s), and prepare package artifacts to be published.
This command runs a project's api tests, as defined by `apiTests` in `PklProject`.
Additionally, it verifies that all imports resolve to paths that are local to the project.
Finally, this command writes the folowing artifacts into the output directory specified by the output path.
- `name@version` - dependency metadata$NEWLINE
- `name@version.sha256` - dependency metadata's SHA-256 checksum$NEWLINE
- `name@version.zip` - package archive$NEWLINE
- `name@version.zip.sha256` - package archive's SHA-256 checksum
The output path option accepts the following placeholders:
- %{name}: The display name of the package$NEWLINE
- %{version}: The version of the package
If a project has local project dependencies, the depended upon project directories must also
be included as arguments to this command.
Examples:
```
# Search the current working directory for a project, and package it.
$ pkl project package
# Package all projects within the `packages/` directory.
$ pkl project package packages/*/
```
"""
.trimIndent(),
) {
private val testOptions by TestOptions()
private val projectDirs: List<Path> by
argument("<dir>", "The project directories to package").path().multiple()
private val outputPath: String by
option(
names = arrayOf("--output-path"),
help = "The directory to write artifacts to",
metavar = "<path>"
)
.single()
.default(".out/%{name}@%{version}")
private val skipPublishCheck: Boolean by
option(
names = arrayOf("--skip-publish-check"),
help = "Skip checking if a package has already been published with different contents",
)
.single()
.flag()
override fun run() {
CliProjectPackager(
baseOptions.baseOptions(emptyList()),
projectDirs,
testOptions.cliTestOptions,
outputPath,
skipPublishCheck
)
.run()
}
}
}
}

View File

@@ -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.cli.commands
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import org.pkl.cli.CliEvaluatorOptions
import org.pkl.cli.CliRepl
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.ProjectOptions
class ReplCommand(helpLink: String) :
BaseCommand(
name = "repl",
help = "Start a REPL session",
helpLink = helpLink,
) {
private val projectOptions by ProjectOptions()
override fun run() {
val options = CliEvaluatorOptions(base = baseOptions.baseOptions(emptyList(), projectOptions))
CliRepl(options).run()
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.context
import com.github.ajalt.clikt.parameters.options.versionOption
class RootCommand(name: String, version: String, helpLink: String) :
NoOpCliktCommand(
name = name,
printHelpOnEmptyArgs = true,
epilog = "For more information, visit $helpLink",
) {
init {
versionOption(version, names = setOf("-v", "--version"), message = { it })
context {
correctionSuggestor = { given, possible ->
if (!given.startsWith("-")) {
registeredSubcommands().map { it.commandName }
} else possible
}
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.cli.commands
import com.github.ajalt.clikt.core.CliktCommand
import org.pkl.cli.CliServer
import org.pkl.commons.cli.CliBaseOptions
class ServerCommand(helpLink: String) :
CliktCommand(
name = "server",
help = "Run as a server that communicates over standard input/output",
epilog = "For more information, visit $helpLink"
) {
override fun run() {
CliServer(CliBaseOptions()).run()
}
}

View File

@@ -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.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
import org.pkl.cli.CliTestRunner
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.ProjectOptions
import org.pkl.commons.cli.commands.TestOptions
class TestCommand(helpLink: String) :
BaseCommand(name = "test", help = "Run tests within the given module(s)", helpLink = helpLink) {
val modules: List<URI> by
argument(name = "<modules>", help = "Module paths or URIs to evaluate.")
.convert { parseModuleName(it) }
.multiple()
private val projectOptions by ProjectOptions()
private val testOptions by TestOptions()
override fun run() {
CliTestRunner(
options = baseOptions.baseOptions(modules, projectOptions),
testOptions = testOptions.cliTestOptions
)
.run()
}
}

View File

@@ -0,0 +1,219 @@
/**
* 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.cli.repl
import java.io.IOException
import java.net.URI
import java.nio.file.Path
import kotlin.io.path.deleteIfExists
import org.fusesource.jansi.Ansi
import org.jline.reader.EndOfFileException
import org.jline.reader.LineReader.Option
import org.jline.reader.LineReaderBuilder
import org.jline.reader.UserInterruptException
import org.jline.reader.impl.completer.AggregateCompleter
import org.jline.reader.impl.history.DefaultHistory
import org.jline.terminal.TerminalBuilder
import org.jline.utils.InfoCmp
import org.pkl.core.repl.ReplRequest
import org.pkl.core.repl.ReplResponse
import org.pkl.core.repl.ReplServer
import org.pkl.core.util.IoUtils
internal class Repl(workingDir: Path, private val server: ReplServer) {
private val terminal = TerminalBuilder.builder().apply { jansi(true) }.build()
private val history = DefaultHistory()
private val reader =
LineReaderBuilder.builder()
.apply {
history(history)
terminal(terminal)
completer(AggregateCompleter(CommandCompleter, FileCompleter(workingDir)))
option(Option.DISABLE_EVENT_EXPANSION, true)
variable(
org.jline.reader.LineReader.HISTORY_FILE,
(IoUtils.getPklHomeDir().resolve("repl-history"))
)
}
.build()
private var continuation = false
private var quit = false
private var nextRequestId = 0
fun run() {
// JLine 2 history file is incompatible with JLine 3
IoUtils.getPklHomeDir().resolve("repl-history.bin").deleteIfExists()
println(ReplMessages.welcome)
println()
var inputBuffer = ""
try {
while (!quit) {
val line =
try {
if (continuation) {
nextRequestId -= 1
reader.readLine(" ".repeat("pkl$nextRequestId> ".length))
} else {
reader.readLine("pkl$nextRequestId> ")
}
} catch (e: UserInterruptException) {
":quit"
} catch (e: EndOfFileException) {
":quit"
}
val input = line.trim()
if (input.isEmpty()) continue
if (continuation) {
inputBuffer = (inputBuffer + "\n" + input).trim()
continuation = false
} else {
inputBuffer = input
}
if (inputBuffer.startsWith(":")) {
executeCommand(inputBuffer)
} else {
evaluate(inputBuffer)
}
}
} finally {
try {
history.save()
} catch (ignored: IOException) {}
try {
terminal.close()
} catch (ignored: IOException) {}
}
}
private fun executeCommand(inputBuffer: String) {
val candidates = getMatchingCommands(inputBuffer)
when {
candidates.isEmpty() -> {
println("Unknown command: `${inputBuffer.drop(1)}`")
}
candidates.size > 1 -> {
print("Which of the following did you mean? ")
println(candidates.joinToString(separator = " ") { "`:${it.type}`" })
}
else -> {
doExecuteCommand(candidates.single())
}
}
}
private fun doExecuteCommand(command: ParsedCommand) {
when (command.type) {
Command.Clear -> clear()
Command.Examples -> examples()
Command.Force -> force(command)
Command.Help -> help()
Command.Load -> load(command)
Command.Quit -> quit()
Command.Reset -> reset()
}
}
private fun clear() {
terminal.puts(InfoCmp.Capability.clear_screen)
terminal.flush()
}
private fun examples() {
println(ReplMessages.examples)
}
private fun help() {
println(ReplMessages.help)
}
private fun quit() {
quit = true
}
private fun reset() {
server.handleRequest(ReplRequest.Reset(nextRequestId()))
clear()
nextRequestId = 0
}
private fun evaluate(inputBuffer: String) {
handleEvalRequest(ReplRequest.Eval(nextRequestId(), inputBuffer, false, false))
}
private fun loadModule(uri: URI) {
handleEvalRequest(ReplRequest.Load(nextRequestId(), uri))
}
private fun force(command: ParsedCommand) {
handleEvalRequest(ReplRequest.Eval(nextRequestId(), command.arg, false, true))
}
private fun load(command: ParsedCommand) {
loadModule(IoUtils.toUri(command.arg))
}
private fun handleEvalRequest(request: ReplRequest) {
val responses = server.handleRequest(request)
for (response in responses) {
when (response) {
is ReplResponse.EvalSuccess -> {
println(response.result)
}
is ReplResponse.EvalError -> {
println(response.message)
}
is ReplResponse.InternalError -> {
throw response.cause
}
is ReplResponse.IncompleteInput -> {
assert(responses.size == 1)
continuation = true
}
else -> throw IllegalStateException("Unexpected response: $response")
}
}
}
private fun nextRequestId(): String = "pkl$nextRequestId".apply { nextRequestId += 1 }
private fun print(msg: String) {
terminal.writer().print(highlight(msg))
}
private fun println(msg: String = "") {
terminal.writer().println(highlight(msg))
}
private fun highlight(str: String): String {
val ansi = Ansi.ansi()
var normal = true
for (part in str.split("`", "```")) {
ansi.a(part)
normal = !normal
if (!normal) ansi.bold() else ansi.boldOff()
}
ansi.reset()
return ansi.toString()
}
}

View File

@@ -0,0 +1,45 @@
/**
* 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.cli.repl
private val cmdRegex = Regex(":(\\p{Alpha}*)(\\p{Space}*)(.*)", RegexOption.DOT_MATCHES_ALL)
internal fun getMatchingCommands(input: String): List<ParsedCommand> {
val match = cmdRegex.matchEntire(input) ?: return listOf()
val (cmd, ws, arg) = match.destructured
return Command.values()
.filter { it.toString().lowercase().startsWith(cmd) }
.map { ParsedCommand(it, cmd, ws, arg) }
}
internal data class ParsedCommand(
val type: Command,
val cmd: String,
val ws: String,
val arg: String
)
internal enum class Command {
Clear,
Examples,
Force,
Help,
Load,
Quit,
Reset;
override fun toString() = name.lowercase()
}

View File

@@ -0,0 +1,182 @@
/**
* 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.cli.repl
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import org.jline.reader.Candidate
import org.jline.reader.Completer
import org.jline.reader.LineReader
import org.jline.reader.ParsedLine
import org.jline.terminal.Terminal
import org.jline.utils.AttributedStringBuilder
import org.jline.utils.OSUtils
import org.jline.utils.StyleResolver
/**
* Originally copied from:
* https://github.com/jline/jline3/blob/jline-parent-3.21.0/builtins/src/main/java/org/jline/builtins/Completers.java
*
* Reasons for copying this class instead of adding jline-builtins dependency:
* - Adding the dependency breaks native-image build (at least when using build-time initialization,
* might work with some config).
* - Completers.FileNameCompleter is the only class we currently use.
*/
internal abstract class JLineFileNameCompleter : Completer {
override fun complete(
reader: LineReader,
commandLine: ParsedLine,
candidates: MutableList<Candidate>
) {
val buffer = commandLine.word().substring(0, commandLine.wordCursor())
val current: Path
val curBuf: String
val sep = getSeparator(reader.isSet(LineReader.Option.USE_FORWARD_SLASH))
val lastSep = buffer.lastIndexOf(sep)
try {
if (lastSep >= 0) {
curBuf = buffer.substring(0, lastSep + 1)
current =
if (curBuf.startsWith("~")) {
if (curBuf.startsWith("~$sep")) {
userHome.resolve(curBuf.substring(2))
} else {
userHome.parent.resolve(curBuf.substring(1))
}
} else {
userDir.resolve(curBuf)
}
} else {
curBuf = ""
current = userDir
}
try {
Files.newDirectoryStream(current) { accept(it) }
.use { directory ->
directory.forEach { path ->
val value = curBuf + path.fileName.toString()
if (Files.isDirectory(path)) {
candidates.add(
Candidate(
value + if (reader.isSet(LineReader.Option.AUTO_PARAM_SLASH)) sep else "",
getDisplay(reader.terminal, path, resolver, sep),
null,
null,
if (reader.isSet(LineReader.Option.AUTO_REMOVE_SLASH)) sep else null,
null,
false
)
)
} else {
candidates.add(
Candidate(
value,
getDisplay(reader.terminal, path, resolver, sep),
null,
null,
null,
null,
true
)
)
}
}
}
} catch (ignored: IOException) {}
} catch (ignored: Exception) {}
}
protected open fun accept(path: Path): Boolean {
return try {
!Files.isHidden(path)
} catch (e: IOException) {
false
}
}
protected open val userDir: Path
get() = Path.of(System.getProperty("user.dir"))
private val userHome: Path
get() = Path.of(System.getProperty("user.home"))
private fun getSeparator(useForwardSlash: Boolean): String {
return if (useForwardSlash) "/" else userDir.fileSystem.separator
}
private fun getDisplay(
terminal: Terminal,
path: Path,
resolver: StyleResolver,
separator: String
): String {
val builder = AttributedStringBuilder()
val name = path.fileName.toString()
val index = name.lastIndexOf(".")
val type = if (index != -1) ".*" + name.substring(index) else null
if (Files.isSymbolicLink(path)) {
builder.styled(resolver.resolve(".ln"), name).append("@")
} else if (Files.isDirectory(path)) {
builder.styled(resolver.resolve(".di"), name).append(separator)
} else if (Files.isExecutable(path) && !OSUtils.IS_WINDOWS) {
builder.styled(resolver.resolve(".ex"), name).append("*")
} else if (type != null && resolver.resolve(type).style != 0L) {
builder.styled(resolver.resolve(type), name)
} else if (Files.isRegularFile(path)) {
builder.styled(resolver.resolve(".fi"), name)
} else {
builder.append(name)
}
return builder.toAnsi(terminal)
}
companion object {
private val resolver = StyleResolver { name ->
when (name) {
// imitate org.jline.builtins.Styles.DEFAULT_LS_COLORS
"di" -> "1;91"
"ex" -> "1;92"
"ln" -> "1;96"
"fi" -> null
else -> null
}
}
}
}
internal class FileCompleter(override val userDir: Path) : JLineFileNameCompleter() {
override fun complete(
reader: LineReader,
commandLine: ParsedLine,
candidates: MutableList<Candidate>
) {
val loadCmd =
getMatchingCommands(commandLine.line()).find { it.type == Command.Load && it.ws.isNotEmpty() }
if (loadCmd != null) {
super.complete(reader, commandLine, candidates)
}
}
}
internal object CommandCompleter : Completer {
private val commandCandidates: List<Candidate> =
Command.values().map { Candidate(":" + it.toString().lowercase()) }
override fun complete(reader: LineReader, line: ParsedLine, candidates: MutableList<Candidate>) {
if (line.wordIndex() == 0) candidates.addAll(commandCandidates)
}
}

View File

@@ -0,0 +1,104 @@
/**
* 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.cli.repl
import org.pkl.core.Release
internal object ReplMessages {
val welcome =
"""
Welcome to Pkl ${Release.current().version()}.
Type an expression to have it evaluated.
Type `:help` or `:examples` for more information.
"""
.trimIndent()
val help =
"""
`<expr>` Evaluate <expr> and print the result. `1 + 3`
`<name> = <expr>` Evaluate <expr> and assign the result to property <name>. `msg = "howdy"`
`:clear` Clear the screen.
`:examples` Show code examples (use copy and paste to run them).
`:force <expr>` Force eager evaluation of a value.
`:help` Show this help.
`:load <file>` Load <file> from local file system. `:load path/to/config.pkl`
`:quit` Quit this program.
`:reset` Reset the environment to its initial state.
Tips:
* Commands can be abbreviated. `:h`
* Commands can be completed. `:<TAB>`
* File paths can be completed. `:load <TAB>`
* Expressions can be completed. `"hello".re<TAB>`
* Multiple declarations and expressions can be evaluated at once. `a = 1; b = a + 2`
* Incomplete input will be continued on the next line.
* Multi-line programs can be copy-pasted into the REPL.
"""
.trimIndent()
val examples: String =
"""
Expressions:
`2 + 3 * 4`
Strings:
`"Hello, " + "World!"`
Properties:
`timeout = 5.min; timeout`
Objects:
```pigeon {
name = "Pigeon"
fullName = "\(name) Bird"
age = 42
address {
street = "Landers St."
}
}
pigeon.fullName
hobbies {
"Swimming"
"Dancing"
"Surfing"
}
hobbies[1]
prices {
["Apple"] = 1.5
["Orange"] = 5
["Banana"] = 2
}
prices["Banana"]```
Inheritance:
```parrot = (pigeon) {
name = "Parrot"
age = 41
}
:force parrot```
For more examples, see the Language Reference${if (isMacOs()) " (Command+Double-click the link below)" else ""}:
${Release.current().documentation().homepage()}language-reference/
"""
.trimIndent()
private fun isMacOs() = System.getProperty("os.name").equals("Mac OS X", ignoreCase = true)
}

View File

@@ -0,0 +1,34 @@
/**
* This package contains source code from:
*
* <p>https://github.com/jline/jline3
*
* <p>Original license:
*
* <p>Copyright (c) 2002-2018, the original author or authors. All rights reserved.
*
* <p>https://opensource.org/licenses/BSD-3-Clause
*
* <p>Redistribution and use in source and binary forms, with or without modification, are permitted
* provided that the following conditions are met:
*
* <p>Redistributions of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer.
*
* <p>Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials provided with
* the distribution.
*
* <p>Neither the name of JLine nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* <p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.pkl.cli.repl;