diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt index 012a690396..11c82737e1 100644 --- a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt @@ -69,9 +69,9 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { protected val settings: PklSettings by lazy { try { if (cliOptions.normalizedSettingsModule != null) { - PklSettings.load(ModuleSource.uri(cliOptions.normalizedSettingsModule)) + PklSettings.load(ModuleSource.uri(cliOptions.normalizedSettingsModule), cliModuleCacheDir) } else { - PklSettings.loadFromSystem() + PklSettings.loadFromSystem(cliModuleCacheDir) } } catch (e: PklException) { // do not use `errorRenderer` because it depends on `settings` @@ -107,9 +107,19 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) { stackFrameTransformer, envVars, cliOptions.powerAssertionsEnabled, + cliModuleCacheDir, ) } + /** + * The module cache dir as determined by CLI options alone. Used to evaluate the settings and + * project files, which are loaded before project evaluator settings are known. + */ + private val cliModuleCacheDir: Path? by lazy { + if (cliOptions.noCache) null + else cliOptions.normalizedModuleCacheDir ?: IoUtils.getSystemModuleCacheDir() + } + private val evaluatorSettings: PklEvaluatorSettings? by lazy { @Suppress("PklCliDirectProjectEvaluatorSettingsAccess") if (cliOptions.omitProjectSettings) null else project?.resolvedEvaluatorSettings diff --git a/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/CliCommandTest.kt b/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/CliCommandTest.kt index cc07879a97..85d3cb60b0 100644 --- a/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/CliCommandTest.kt +++ b/pkl-commons-cli/src/test/kotlin/org/pkl/commons/cli/CliCommandTest.kt @@ -26,6 +26,7 @@ import org.junit.jupiter.api.condition.JRE import org.junit.jupiter.api.io.TempDir import org.pkl.commons.cli.commands.BaseCommand import org.pkl.commons.cli.commands.ProjectOptions +import org.pkl.commons.test.PackageServer import org.pkl.commons.writeString import org.pkl.core.SecurityManagers import org.pkl.core.evaluatorSettings.PklEvaluatorSettings @@ -134,7 +135,9 @@ class CliCommandTest { @DisabledOnJre(JRE.JAVA_22, JRE.JAVA_23, JRE.JAVA_24) fun `test that --omit-project-settings actually omits project settings`(@TempDir tempDir: Path) { val project = tempDir.resolve("PklProject").writeString(projectWithAllEvaluatorSettings) - cmd.parse(arrayOf("--working-dir=$tempDir", "--omit-project-settings")) + cmd.parse( + arrayOf("--working-dir=$tempDir", "--omit-project-settings", "--settings=pkl:settings") + ) val opts = cmd.baseOptions.baseOptions(listOf(project.toUri()), cmd.projectOptions, testMode = true) val cliTest = CliTest(opts) @@ -200,4 +203,31 @@ class CliCommandTest { assertThat(cliTest.myExternalModuleReaders) .isEqualTo(mapOf("bar" to PklEvaluatorSettings.ExternalReader("bar", listOf(), null))) } + + @Test + fun `--cache-dir is used when loading the project`(@TempDir tempDir: Path) { + val cacheDir = tempDir.resolve("cache").also(PackageServer::populateCacheDir) + tempDir + .resolve("PklProject") + .writeString( + // language=pkl + """ + amends "pkl:Project" + + import "package://localhost:0/fruit@1.0.5#/catalog/apple.pkl" + + evaluatorSettings { + externalProperties { + ["fruit"] = apple.name + } + } + """ + .trimIndent() + ) + cmd.parse(arrayOf("--working-dir=$tempDir", "--cache-dir=$cacheDir")) + val opts = cmd.baseOptions.baseOptions(emptyList(), cmd.projectOptions, testMode = true) + val cliTest = CliTest(opts) + assertThat(cliTest.myProjectEvaluatorSettings!!.externalProperties) + .containsEntry("fruit", "Apple") + } } diff --git a/pkl-core/src/main/java/org/pkl/core/project/Project.java b/pkl-core/src/main/java/org/pkl/core/project/Project.java index 4757fb4626..ce1518ea32 100644 --- a/pkl-core/src/main/java/org/pkl/core/project/Project.java +++ b/pkl-core/src/main/java/org/pkl/core/project/Project.java @@ -84,6 +84,34 @@ public static Project loadFromPath( StackFrameTransformer stackFrameTransformer, Map envVars, boolean powerAssertionsEnabled) { + return loadFromPath( + path, + securityManager, + timeout, + stackFrameTransformer, + envVars, + powerAssertionsEnabled, + IoUtils.getSystemModuleCacheDir()); + } + + /** + * Loads Project data from the given {@link Path}. + * + *

Evaluates a module's {@code output.value} to allow for embedding a project within a + * template. + * + * @param moduleCacheDir the directory where packages imported by the project file are cached; + * {@code null} disables caching + * @throws PklException if an error occurred while evaluating the project file. + */ + public static Project loadFromPath( + Path path, + SecurityManager securityManager, + java.time.@Nullable Duration timeout, + StackFrameTransformer stackFrameTransformer, + Map envVars, + boolean powerAssertionsEnabled, + @Nullable Path moduleCacheDir) { try (var evaluator = EvaluatorBuilder.unconfigured() .setSecurityManager(securityManager) @@ -97,6 +125,7 @@ public static Project loadFromPath( .addEnvironmentVariables(envVars) .setTimeout(timeout) .setPowerAssertionsEnabled(powerAssertionsEnabled) + .setModuleCacheDir(moduleCacheDir) .build()) { return load(evaluator, ModuleSource.path(path)); } diff --git a/pkl-core/src/main/java/org/pkl/core/service/ExecutorSpiImpl.java b/pkl-core/src/main/java/org/pkl/core/service/ExecutorSpiImpl.java index 84d5029c27..4f58d1ea4d 100644 --- a/pkl-core/src/main/java/org/pkl/core/service/ExecutorSpiImpl.java +++ b/pkl-core/src/main/java/org/pkl/core/service/ExecutorSpiImpl.java @@ -125,7 +125,9 @@ public String evaluatePath(Path modulePath, ExecutorSpiOptions options) { securityManager, null, transformer, - options.getEnvironmentVariables()); + options.getEnvironmentVariables(), + false, + options.getModuleCacheDir()); builder.setProjectDependencies(project.getDependencies()); } diff --git a/pkl-core/src/main/java/org/pkl/core/settings/PklSettings.java b/pkl-core/src/main/java/org/pkl/core/settings/PklSettings.java index 35bd717ba0..0ff1e68f56 100644 --- a/pkl-core/src/main/java/org/pkl/core/settings/PklSettings.java +++ b/pkl-core/src/main/java/org/pkl/core/settings/PklSettings.java @@ -53,10 +53,20 @@ public record PklSettings(Editor editor, PklEvaluatorSettings.@Nullable Http htt *

If neither file exists, returns default settings defined by module {@code pkl.settings}. */ public static PklSettings loadFromSystem() throws VmEvalException { + return loadFromSystem(IoUtils.getSystemModuleCacheDir()); + } + + /** + * Loads the user settings file, using the given module cache directory. + * + * @param moduleCacheDir the directory where packages are cached; {@code null} disables caching + * @see #loadFromSystem() + */ + public static PklSettings loadFromSystem(@Nullable Path moduleCacheDir) throws VmEvalException { var file = IoUtils.getSystemSettingsFile(); if (Files.exists(file)) { DebugLogger.log("Loading settings file from " + file.normalize().toAbsolutePath()); - return load(ModuleSource.path(file)); + return load(ModuleSource.path(file), moduleCacheDir); } return defaultInstance; } @@ -89,6 +99,16 @@ private static PklSettings loadFromSettingsFile(Path path) throws VmEvalExceptio /** Loads a settings file from the given path. */ public static PklSettings load(ModuleSource moduleSource) throws VmEvalException { + return load(moduleSource, IoUtils.getSystemModuleCacheDir()); + } + + /** + * Loads a settings file from the given path, using the given module cache directory. + * + * @param moduleCacheDir the directory where packages are cached; {@code null} disables caching + */ + public static PklSettings load(ModuleSource moduleSource, @Nullable Path moduleCacheDir) + throws VmEvalException { try (var evaluator = EvaluatorBuilder.unconfigured() .setSecurityManager( @@ -100,6 +120,7 @@ public static PklSettings load(ModuleSource moduleSource) throws VmEvalException .addResourceReader(ResourceReaders.environmentVariable()) .addResourceReader(ResourceReaders.file()) .addEnvironmentVariables(System.getenv()) + .setModuleCacheDir(moduleCacheDir) .build()) { var module = evaluator.evaluateOutputValueAs(moduleSource, PClassInfo.Settings); return parseSettings(module, moduleSource); diff --git a/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt index b37c0d09ce..f5ad6ec19a 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt @@ -362,4 +362,56 @@ class ProjectTest { assertThat(project.evaluatorSettings.externalModuleReaders?.get("foo")!!.executable()) .isEqualTo("my-command") } + + @Test + fun `loadFromPath uses the given module cache dir`(@TempDir tempDir: Path) { + val cacheDir = tempDir.resolve("cache").also(PackageServer::populateCacheDir) + val pklProject = tempDir.resolve("PklProject").writeString(PROJECT_IMPORTING_PACKAGE) + val project = + Project.loadFromPath( + pklProject, + SecurityManagers.defaultManager, + null, + StackFrameTransformers.empty, + mapOf(), + false, + cacheDir, + ) + assertThat(project.evaluatorSettings.externalProperties).containsEntry("fruit", "Apple") + } + + @Test + fun `loadFromPath does not use a cache if module cache dir is null`(@TempDir tempDir: Path) { + val pklProject = tempDir.resolve("PklProject").writeString(PROJECT_IMPORTING_PACKAGE) + assertThrows { + Project.loadFromPath( + pklProject, + SecurityManagers.defaultManager, + null, + StackFrameTransformers.empty, + mapOf(), + false, + null, + ) + } + } + + companion object { + // `localhost:0` is never reachable without a test HTTP client, so this package can only be + // resolved from a pre-populated cache dir. + private val PROJECT_IMPORTING_PACKAGE = + // language=pkl + """ + amends "pkl:Project" + + import "package://localhost:0/fruit@1.0.5#/catalog/apple.pkl" + + evaluatorSettings { + externalProperties { + ["fruit"] = apple.name + } + } + """ + .trimIndent() + } }