apple/pkl · error · PklException

Cannot substitute output path placeholder `%{moduleDir}` bec

Error message

Cannot substitute output path placeholder `%{moduleDir}` because module `$uri` does not have a file system path.

What it means

When resolving per-module output file paths, the `%{moduleDir}` placeholder can only be substituted for modules that have a filesystem path (e.g. file:// modules). If a module came from a non-filesystem source (HTTP, classpath, virtual/repl module) and its pathStr still contains `%{moduleDir}` after substitution, CliEvaluator throws this error.

Source

Thrown at pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt:135

    return options.base.normalizedSourceModules.associateWith { uri ->
      val moduleDir: String? =
        IoUtils.toPath(uri)?.let {
          IoUtils.relativize(it.parent, workingDir).toString().ifEmpty { "." }
        }
      val moduleKey =
        try {
          moduleResolver.resolve(uri)
        } catch (e: VmException) {
          throw e.toPklException(stackFrameTransformer, options.base.color?.hasColor() ?: false)
        }
      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
    }
  }

  private fun Evaluator.evalOutput(moduleSource: ModuleSource): ByteArray {
    if (options.expression == null) {
      return evaluateOutputBytes(moduleSource)
    }
    return evaluateExpressionString(moduleSource, options.expression)
      .toByteArray(StandardCharsets.UTF_8)
  }

  /** Renders each module's `output.bytes`, writing it to the specified output file. */

View on GitHub (pinned to f3efcbfc9b)

Solutions

  1. Remove `%{moduleDir}` from the output pathSpec or replace it with a fixed directory.
  2. Evaluate only local file-based modules when output paths depend on %{moduleDir}.
  3. Materialize remote modules to disk first, then evaluate them as file modules.
  4. Use `%{moduleName}` instead, which works for modules without a filesystem path.

Example fix

// before
["%{moduleDir}/out.json"] = output // module has no file path
// after
["out/%{moduleName}.json"] = output
Defensive patterns

Strategy: validation

Validate before calling

// Ensure %{moduleDir} is only used for file-based modules
if (pathStr.contains("%{moduleDir}") && !moduleUri.scheme.startsWith("file")) {
  throw CliException("%{moduleDir} unsupported for non-file module $moduleUri")
}

Try / catch

try {
  ev.writeMultipleFileOutput(...)
} catch (e: PklException) {
  if (e.message?.contains("%{moduleDir}") == true) {
    // switch pathSpec to %{moduleName} and retry
  }
}

Prevention

When it happens

Trigger: Using `%{moduleDir}` in a multiple-file-output path option while evaluating modules loaded from jar/classpath, HTTP URLs, or stdin (no moduleDir), e.g. `pkl eval --multiple-file-output-path ... https://.../mod.pkl` with pathSpec "%{moduleDir}/out.json".

Common situations: Evaluating remote modules or modules from stdin while an output.files mapping references %{moduleDir}; reusing a config designed for local files against remote dependencies.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08). Data as JSON: /api/errors/17d3ddfb9f56a3a1. Report an issue: GitHub.