Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions sjsonnet/src-jvm-native/sjsonnet/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -164,21 +164,55 @@ final case class Config(
) {

/**
* Returns the sequence of jpaths specified on the command line, ordered according to the flags.
* Returns the sequence of jpaths, combining command-line flags and the JSONNET_PATH environment
* variable.
*
* Historically, sjsonnet evaluated jpaths in left-to-right order, which is also the order of
* evaluation in the core. However, in gojsonnet, the arguments are prioritized right to left, and
* the reverse-jpaths-priority flag was introduced for possible consistency across the two
* JSONNET_PATH directories always have lower priority than --jpath flags. Within JSONNET_PATH,
* the left-most entry has the highest priority, matching the behavior of the C++ and Go
* implementations.
*
* The --reverse-jpaths-priority flag only affects the ordering of --jpath flags (reversing them
* so that the rightmost wins, matching go-jsonnet behavior). JSONNET_PATH entries are always
* appended after the (possibly reversed) --jpath flags.
*
* For example, `JSONNET_PATH=a:b sjsonnet -J c -J d` results in search order: c, d, a, b (default
* mode). With --reverse-jpaths-priority, the order becomes: d, c, a, b.
*
* See [[https://jsonnet-libs.github.io/jsonnet-training-course/lesson2.html#jsonnet_path]] for
* details.
*/
def getOrderedJpaths: Seq[String] = {
if (reverseJpathsPriority.value) {
jpaths.reverse
} else {
jpaths
def getOrderedJpaths: Seq[String] = getOrderedJpaths(jsonnetPathEnv = None)

/**
* Returns the sequence of jpaths, combining command-line flags and the JSONNET_PATH environment
* variable.
*
* @param jsonnetPathEnv
* If Some(value), use the given value instead of reading from the JSONNET_PATH environment
* variable. If None, read from System.getenv("JSONNET_PATH").
*/
def getOrderedJpaths(jsonnetPathEnv: Option[String]): Seq[String] = {
val envValue = jsonnetPathEnv.getOrElse(System.getenv("JSONNET_PATH"))
val envPaths = Config.jsonnetPathEntries(envValue)
val orderedJpaths = if (reverseJpathsPriority.value) jpaths.reverse else jpaths
orderedJpaths ++ envPaths
}
}

object Config {

/**
* Parses the JSONNET_PATH value into a sequence of directory paths. Entries are kept in their
* original order so that, with sjsonnet's default left-to-right search, the left-most entry in
* the environment variable has the highest priority among the JSONNET_PATH entries, matching the
* behavior of the C++ and Go implementations.
*
* The separator is colon on Unix and semicolon on Windows ({@code java.io.File.pathSeparator}).
*/
private[sjsonnet] def jsonnetPathEntries(envValue: String): Seq[String] = {
if (envValue == null || envValue.isEmpty) Nil
else {
envValue.split(java.io.File.pathSeparator).filter(_.nonEmpty).toSeq
}
}
}
59 changes: 50 additions & 9 deletions sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,33 @@ object SjsonnetMainBase {
}
}

/**
* Java-compatible overload that omits `jsonnetPathEnv` (defaults to `None`). Keeps source
* compatibility for callers that were compiled against the pre-JSONNET_PATH signature.
*/
def main0(
args: Array[String],
parseCache: ParseCache,
stdin: InputStream,
stdout: PrintStream,
stderr: PrintStream,
wd: os.Path,
allowedInputs: Option[Set[os.Path]],
importer: Option[Importer],
std: Val.Obj): Int =
main0(
args,
parseCache,
stdin,
stdout,
stderr,
wd,
allowedInputs,
importer,
std,
jsonnetPathEnv = None
)

def main0(
args: Array[String],
parseCache: ParseCache,
Expand All @@ -73,7 +100,8 @@ object SjsonnetMainBase {
wd: os.Path,
allowedInputs: Option[Set[os.Path]] = None,
importer: Option[Importer] = None,
std: Val.Obj = sjsonnet.stdlib.StdLibModule.Default.module): Int = {
std: Val.Obj = sjsonnet.stdlib.StdLibModule.Default.module,
jsonnetPathEnv: Option[String] = None): Int = {

var hasWarnings = false
def warn(isTrace: Boolean, msg: String): Unit = {
Expand All @@ -87,14 +115,27 @@ object SjsonnetMainBase {
val parser = mainargs.ParserForClass[Config]
val name = s"Sjsonnet ${sjsonnet.Version.version}"
val doc = "usage: sjsonnet [sjsonnet-options] script-file"
val envVarsDoc =
"""
|Environment variables:
| JSONNET_PATH is a colon (semicolon on Windows) separated list of directories
| added in reverse order before the paths specified by --jpath (i.e. left-most
| wins). E.g. these are equivalent:
| JSONNET_PATH=a:b sjsonnet -J c -J d
| JSONNET_PATH=d:c:a:b sjsonnet
| sjsonnet -J b -J a -J c -J d""".stripMargin

val result = for {
config <- parser.constructEither(
args.toIndexedSeq,
allowRepeats = true,
customName = name,
customDoc = doc,
autoPrintHelpAndExit = None
)
config <- parser
.constructEither(
args.toIndexedSeq,
allowRepeats = true,
customName = name,
customDoc = doc,
autoPrintHelpAndExit = None
)
.left
.map(_ + envVarsDoc)
_ <- {
if (config.noTrailingNewline.value && config.yamlStream.value)
Left("error: cannot use --no-trailing-newline with --yaml-stream")
Expand All @@ -115,7 +156,7 @@ object SjsonnetMainBase {
wd,
importer.getOrElse {
new SimpleImporter(
config.getOrderedJpaths.map(p => OsPath(os.Path(p, wd))),
config.getOrderedJpaths(jsonnetPathEnv).map(p => OsPath(os.Path(p, wd))),
allowedInputs,
debugImporter = config.debugImporter.value
)
Expand Down
84 changes: 84 additions & 0 deletions sjsonnet/test/src-jvm-native/sjsonnet/ConfigTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package sjsonnet

import utest.*

object ConfigTests extends TestSuite {
private val sep = java.io.File.pathSeparator

val tests: Tests = Tests {
test("jsonnetPathEntries") {
test("null") {
assert(Config.jsonnetPathEntries(null) == Nil)
}
test("empty") {
assert(Config.jsonnetPathEntries("") == Nil)
}
test("single") {
// Single path should be returned as-is
assert(Config.jsonnetPathEntries("/foo") == Seq("/foo"))
}
test("multiple") {
// JSONNET_PATH=a<sep>b should produce Seq("a", "b") (original order, left-most wins)
val result = Config.jsonnetPathEntries(s"/a${sep}/b${sep}/c")
assert(result == Seq("/a", "/b", "/c"))
}
test("emptyEntries") {
// Empty entries between separators should be filtered out
val result = Config.jsonnetPathEntries(s"/a$sep$sep/b")
assert(result == Seq("/a", "/b"))
}
test("trailingSeparator") {
val result = Config.jsonnetPathEntries(s"/a$sep/b$sep")
assert(result == Seq("/a", "/b"))
}
}

test("getOrderedJpaths") {
test("jpathsOnlyDefaultOrder") {
// Without JSONNET_PATH env set, getOrderedJpaths should return jpaths in original order
val config = Config(jpaths = List("/x", "/y", "/z"), file = "test.jsonnet")
val result = config.getOrderedJpaths(jsonnetPathEnv = Some(""))
assert(result == Seq("/x", "/y", "/z"))
}
test("jpathsOnlyReversed") {
import mainargs.Flag
val config = Config(
jpaths = List("/x", "/y", "/z"),
reverseJpathsPriority = Flag(true),
file = "test.jsonnet"
)
val result = config.getOrderedJpaths(jsonnetPathEnv = Some(""))
assert(result == Seq("/z", "/y", "/x"))
}
test("envPathsAppendedAfterJpaths") {
val config = Config(jpaths = List("/c", "/d"), file = "test.jsonnet")
val result = config.getOrderedJpaths(jsonnetPathEnv = Some(s"/a$sep/b"))
// -J paths first, then JSONNET_PATH entries in original order
assert(result == Seq("/c", "/d", "/a", "/b"))
}
test("envPathsWithReverse") {
import mainargs.Flag
val config = Config(
jpaths = List("/c", "/d"),
reverseJpathsPriority = Flag(true),
file = "test.jsonnet"
)
val result = config.getOrderedJpaths(jsonnetPathEnv = Some(s"/a$sep/b"))
// reversed -J paths first, then JSONNET_PATH entries in original order
assert(result == Seq("/d", "/c", "/a", "/b"))
}
test("envPathsOnlyNoJpaths") {
val config = Config(file = "test.jsonnet")
val result = config.getOrderedJpaths(jsonnetPathEnv = Some(s"/a$sep/b"))
assert(result == Seq("/a", "/b"))
}
test("noArgsDefaultFallback") {
// Use the injected overload with an empty env string so the test is
// deterministic even when the real JSONNET_PATH env var is set.
val config = Config(jpaths = List("/x"), file = "test.jsonnet")
val result = config.getOrderedJpaths(jsonnetPathEnv = Some(""))
assert(result == Seq("/x"))
}
}
}
}
3 changes: 2 additions & 1 deletion sjsonnet/test/src-jvm/sjsonnet/Example.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public void example(){
os.package$.MODULE$.pwd(),
scala.None$.empty(),
scala.None$.empty(),
new sjsonnet.stdlib.StdLibModule(Map$.MODULE$.empty(), Map$.MODULE$.empty()).module()
new sjsonnet.stdlib.StdLibModule(Map$.MODULE$.empty(), Map$.MODULE$.empty()).module(),
scala.None$.empty()
);
}
}
98 changes: 96 additions & 2 deletions sjsonnet/test/src-jvm/sjsonnet/MainTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,102 @@ object MainTests extends TestSuite {
|- 2""".stripMargin
assert((res, out, err) == ((0, expectedYaml, "")))
}

test("jsonnetPath") {
// Create temp directories with library files to test JSONNET_PATH resolution
val libDir = os.temp.dir()
os.write(libDir / "mylib.libsonnet", """{ x: 42 }""")

val mainFile = os.temp(suffix = ".jsonnet")
os.write.over(mainFile, """local lib = import 'mylib.libsonnet'; lib.x""")

// Without JSONNET_PATH or -J, the import should fail
val (res1, _, err1) = runMain(mainFile)
assert(res1 == 1)
assert(err1.contains("Couldn't import"))

// With -J pointing to the lib directory, the import should succeed
val (res2, out2, _) = runMain("-J", libDir, mainFile)
assert(res2 == 0)
assert(out2.trim == "42")

// With JSONNET_PATH pointing to the lib directory, the import should succeed
val (res3, out3, _) =
runMainWithEnv(jsonnetPathEnv = libDir.toString, mainFile)
assert(res3 == 0)
assert(out3.trim == "42")
}

test("jsonnetPathMultipleDirs") {
// Test that JSONNET_PATH=a:b results in left-most winning (a has priority over b)
val libDirA = os.temp.dir()
val libDirB = os.temp.dir()

// Both dirs have mylib.libsonnet but with different values
os.write(libDirA / "mylib.libsonnet", """{ x: "from_a" }""")
os.write(libDirB / "mylib.libsonnet", """{ x: "from_b" }""")

val mainFile = os.temp(suffix = ".jsonnet")
os.write.over(mainFile, """local lib = import 'mylib.libsonnet'; lib.x""")

// JSONNET_PATH=a:b → left-most (a) wins
val sep = java.io.File.pathSeparator
val (res, out, _) =
runMainWithEnv(jsonnetPathEnv = s"$libDirA$sep$libDirB", mainFile)
assert(res == 0)
assert(out.trim == "\"from_a\"")
}

test("jsonnetPathJpathPriority") {
// -J flags should take priority over JSONNET_PATH
val libDirEnv = os.temp.dir()
val libDirJ = os.temp.dir()

os.write(libDirEnv / "mylib.libsonnet", """{ x: "from_env" }""")
os.write(libDirJ / "mylib.libsonnet", """{ x: "from_jflag" }""")

val mainFile = os.temp(suffix = ".jsonnet")
os.write.over(mainFile, """local lib = import 'mylib.libsonnet'; lib.x""")

// -J flag should win over JSONNET_PATH
val (res, out, _) =
runMainWithEnv(jsonnetPathEnv = libDirEnv.toString, "-J", libDirJ, mainFile)
assert(res == 0)
assert(out.trim == "\"from_jflag\"")
}

test("jsonnetPathReverseJpathsPriority") {
// With --reverse-jpaths-priority, rightmost -J wins, but -J still beats JSONNET_PATH
val libDirEnv = os.temp.dir()
val libDirC = os.temp.dir()
val libDirD = os.temp.dir()

os.write(libDirEnv / "mylib.libsonnet", """{ x: "from_env" }""")
os.write(libDirC / "mylib.libsonnet", """{ x: "from_c" }""")
os.write(libDirD / "mylib.libsonnet", """{ x: "from_d" }""")

val mainFile = os.temp(suffix = ".jsonnet")
os.write.over(mainFile, """local lib = import 'mylib.libsonnet'; lib.x""")

// With --reverse-jpaths-priority, -J d (rightmost) should win over -J c and JSONNET_PATH
val (res, out, _) = runMainWithEnv(
jsonnetPathEnv = libDirEnv.toString,
"--reverse-jpaths-priority",
"-J",
libDirC,
"-J",
libDirD,
mainFile
)
assert(res == 0)
assert(out.trim == "\"from_d\"")
}
}

def runMain(args: os.Shellable*): (Int, String, String) = {
def runMain(args: os.Shellable*): (Int, String, String) =
runMainWithEnv(jsonnetPathEnv = "", args*)

def runMainWithEnv(jsonnetPathEnv: String, args: os.Shellable*): (Int, String, String) = {
val err = new ByteArrayOutputStream()
val perr = new PrintStream(err, true, "UTF-8")
val out = new ByteArrayOutputStream()
Expand All @@ -321,7 +414,8 @@ object MainTests extends TestSuite {
pout,
perr,
workspaceRoot,
None
None,
jsonnetPathEnv = Some(jsonnetPathEnv)
)
(res, new String(out.toByteArray, "UTF-8"), new String(err.toByteArray, "UTF-8"))
}
Expand Down
Loading