diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt index ccecd07a..9cc526fa 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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. @@ -263,13 +263,15 @@ constructor( } val moduleSource = toModuleSource(moduleUri, inputStream) val output = evaluator.evaluateOutputFiles(moduleSource) + val realOutputDir = if (outputDir.exists()) outputDir.toRealPath() else outputDir + for ((pathSpec, fileOutput) in output) { checkPathSpec(pathSpec) - val resolvedPath = outputDir.resolve(pathSpec).normalize() + val resolvedPath = realOutputDir.resolve(pathSpec).normalize() val realPath = if (resolvedPath.exists()) resolvedPath.toRealPath() else resolvedPath - if (!realPath.startsWith(outputDir)) { + if (!realPath.startsWith(realOutputDir)) { throw CliException( - "Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is outside output directory `$outputDir`." + "Output file conflict: `output.files` entry `\"$pathSpec\"` in module `$moduleUri` resolves to file path `$realPath`, which is outside output directory `$realOutputDir`." ) } val previousOutput = writtenFiles[realPath] diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCommand.kt index 5409413f..739691f2 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCommand.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCommand.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-2026 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. diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt index 89dbaf9d..2cf8416e 100644 --- a/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliEvaluatorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2024-2026 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. @@ -1634,6 +1634,95 @@ result = someLib.x ) } + @Test + @DisabledOnOs(OS.WINDOWS) + fun `multiple file output works with symlinked output directory`() { + val realOutputDir = tempDir.resolve("real-output").createDirectories() + + val symlinkOutputDir = + Files.createSymbolicLink(tempDir.resolve("symlink-output"), realOutputDir) + + val sourceFile = + writePklFile( + "test.pkl", + """ + pigeon { + name = "Pigeon" + diet = "Seeds" + } + parrot { + name = "Parrot" + diet = "Seeds" + } + output { + files { + ["pigeon.json"] { + value = pigeon + renderer = new JsonRenderer {} + } + ["birds/parrot.yaml"] { + value = parrot + renderer = new YamlRenderer {} + } + } + } + """ + .trimIndent(), + ) + + val options = + CliEvaluatorOptions( + CliBaseOptions(sourceModules = listOf(sourceFile), workingDir = tempDir), + multipleFileOutputPath = symlinkOutputDir.toString(), + ) + + CliEvaluator(options).run() + + checkOutputFile( + realOutputDir.resolve("pigeon.json"), + "pigeon.json", + """ + { + "name": "Pigeon", + "diet": "Seeds" + } + """ + .trimIndent(), + ) + + checkOutputFile( + realOutputDir.resolve("birds/parrot.yaml"), + "parrot.yaml", + """ + name: Parrot + diet: Seeds + """ + .trimIndent(), + ) + + checkOutputFile( + symlinkOutputDir.resolve("pigeon.json"), + "pigeon.json", + """ + { + "name": "Pigeon", + "diet": "Seeds" + } + """ + .trimIndent(), + ) + + checkOutputFile( + symlinkOutputDir.resolve("birds/parrot.yaml"), + "parrot.yaml", + """ + name: Parrot + diet: Seeds + """ + .trimIndent(), + ) + } + private fun evalModuleThatImportsPackage(certsFile: Path?, testPort: Int = -1) { val moduleUri = writePklFile(