Skip to content

Commit 3547b3f

Browse files
committed
feat: align CLI with go-jsonnet
Motivation: Close CLI compatibility gaps with go-jsonnet, including option parsing, output behavior, environment/JPATH handling, and stack accounting semantics. Modification: Update CLI config and main flow, add JVM CLI compatibility tests, and keep tail-call trampolines from consuming the user-facing maxStack budget while preserving maxStack failures for non-tail recursion. Result: The PR branch is represented as one cohesive commit and the JVM CI failure is fixed by aligning tests with the intended tail-recursive stack behavior. References: #1068
1 parent 2d2161e commit 3547b3f

11 files changed

Lines changed: 564 additions & 108 deletions

File tree

build.mill

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,84 @@ object sjsonnet extends VersionFileModule {
382382
def forkArgs =
383383
Seq("-Xss" + stackSize, "--enable-native-access=ALL-UNNAMED")
384384

385+
override def prependShellScript = mill.util.Jvm.universalScript(
386+
shellCommands =
387+
s"""if [ -z "$$JAVA_HOME" ] ; then
388+
| JAVACMD="java"
389+
|else
390+
| JAVACMD="$$JAVA_HOME/bin/java"
391+
|fi
392+
|SJSONNET_JAVA_OPTS=""
393+
|if "$$JAVACMD" --sun-misc-unsafe-memory-access=allow -version >/dev/null 2>&1; then
394+
| SJSONNET_JAVA_OPTS="--sun-misc-unsafe-memory-access=allow"
395+
|fi
396+
|exec "$$JAVACMD" -Xss$stackSize --enable-native-access=ALL-UNNAMED $$SJSONNET_JAVA_OPTS $$JAVA_OPTS -cp "$$0" 'sjsonnet.SjsonnetMain' "$$@"""".stripMargin,
397+
cmdCommands =
398+
s"""set "JAVACMD=java.exe"
399+
|if not "%JAVA_HOME%"=="" set "JAVACMD=%JAVA_HOME%\\bin\\java.exe"
400+
|set "SJSONNET_JAVA_OPTS="
401+
|"%JAVACMD%" --sun-misc-unsafe-memory-access=allow -version >nul 2>&1
402+
|if "%errorlevel%" == "0" set "SJSONNET_JAVA_OPTS=--sun-misc-unsafe-memory-access=allow"
403+
|"%JAVACMD%" -Xss$stackSize --enable-native-access=ALL-UNNAMED %SJSONNET_JAVA_OPTS% %JAVA_OPTS% -cp "%~dpnx0" "sjsonnet.SjsonnetMain" %*""".stripMargin
404+
)
405+
406+
def assemblySmoke = Task {
407+
val jar = assembly().path
408+
val jarCommand =
409+
if (scala.util.Properties.isWin) Seq[os.Shellable](jar)
410+
else
411+
Seq[os.Shellable](
412+
"env",
413+
"-u",
414+
"JDK_JAVA_OPTIONS",
415+
"-u",
416+
"JAVA_TOOL_OPTIONS",
417+
jar
418+
)
419+
def callJar(args: os.Shellable*) =
420+
os.proc((jarCommand ++ args)*).call(
421+
stdout = os.Pipe,
422+
stderr = os.Pipe,
423+
check = false
424+
)
425+
426+
val version = callJar("--version")
427+
assert(
428+
version.exitCode == 0,
429+
s"--version failed with exit ${version.exitCode}: ${version.err.text()}"
430+
)
431+
assert(version.err.text().isEmpty, s"--version stderr was not empty: ${version.err.text()}")
432+
433+
val help = callJar("--help")
434+
val helpOut = help.out.text()
435+
val helpErr = help.err.text()
436+
assert(help.exitCode == 0, s"--help failed with exit ${help.exitCode}: $helpErr")
437+
assert(helpErr.isEmpty, s"--help stderr was not empty: $helpErr")
438+
assert(!helpOut.contains("Missing argument"), helpOut)
439+
assert(!helpOut.contains("Unknown argument"), helpOut)
440+
441+
val stack = callJar(
442+
"--exec",
443+
"--max-stack",
444+
"2",
445+
"local f(n) = if n == 0 then 0 else f(n - 1); f(3)"
446+
)
447+
val stackErr = stack.err.text()
448+
assert(stack.exitCode != 0, s"--max-stack unexpectedly exited ${stack.exitCode}")
449+
assert(stackErr.contains("Max stack frames exceeded."), stackErr)
450+
451+
val defaultStack = callJar(
452+
"--exec",
453+
"local f(n) = if n == 0 then 0 else f(n - 1); f(1000)"
454+
)
455+
val defaultStackErr = defaultStack.err.text()
456+
assert(
457+
defaultStack.exitCode != 0,
458+
s"default max-stack unexpectedly exited ${defaultStack.exitCode}"
459+
)
460+
assert(defaultStackErr.contains("Max stack frames exceeded."), defaultStackErr)
461+
}
462+
385463
def scalacOptions = super.scalacOptions() ++ Seq("-release", "17") ++
386464
(if (JvmWorkerUtil.isScala3(scalaVersion())) Seq("-Yfuture-lazy-vals") else Seq.empty)
387465
def javacOptions = super.javacOptions() ++ Seq("--release", "17")

readme.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ Expected Signature: Sjsonnet 0.7.0
1313
usage: sjsonnet [sjsonnet-options] script-file
1414
-A --tla-str <str> <var>[=<val>] Provide top-level arguments as string. 'If <val>
1515
is omitted, get from environment var <var>
16-
-J --jpath <str> Specify an additional library search dir (left-most wins
17-
unless reverse-jpaths-priority is set)
16+
-J --jpath <str> Specify an additional library search dir (right-most wins,
17+
matching go-jsonnet)
1818
-S --string Expect a string, manifest as plain text
1919
-V --ext-str <str> <var>[=<val>] Provide 'external' variable as string. 'If <val>
2020
is omitted, get from environment var <var>
@@ -44,9 +44,11 @@ usage: sjsonnet [sjsonnet-options] script-file
4444
--profile <str> Profile evaluation and write results to a file. Format:
4545
--profile <file> or --profile <format>:<file> where format is
4646
'text' (default) or 'flamegraph'
47-
--reverse-jpaths-priority If set, reverses the import order of specified jpaths (so that the
48-
rightmost wins)
47+
--reverse-jpaths-priority Deprecated compatibility flag; --jpath already uses
48+
right-most-wins priority
4949
-s --max-stack <int> Number of allowed stack frames (default 500)
50+
-t --max-trace <int> Max length of stack trace before cropping (default 20, 0 means
51+
unlimited)
5052
--strict Enforce some additional syntax limitations
5153
--throw-error-for-invalid-sets Throw an error if a set operation is used on a non-set
5254
--tla-code <str> <var>[=<val>] Provide top-level arguments as Jsonnet code. 'If
@@ -55,6 +57,7 @@ usage: sjsonnet [sjsonnet-options] script-file
5557
code from the file
5658
--tla-str-file <str> <var>=<file> Provide top-level arguments variable as string from
5759
the file
60+
--version Print version
5861
-y --yaml-stream Write output as a YAML stream of JSON documents
5962
--yaml-debug Generate source line comments in the output YAML doc to make it
6063
easier to figure out where values come from.

sjsonnet/src-jvm-native/sjsonnet/Config.scala

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ final case class Config(
77
@arg(
88
name = "jpath",
99
short = 'J',
10-
doc =
11-
"Specify an additional library search dir (left-most wins unless reverse-jpaths-priority is set)"
10+
doc = "Specify an additional library search dir (right-most wins, matching go-jsonnet)"
1211
)
1312
private val jpaths: List[String] = Nil,
1413
@arg(
@@ -140,7 +139,7 @@ final case class Config(
140139
throwErrorForInvalidSets: Flag = Flag(),
141140
@arg(
142141
name = "reverse-jpaths-priority",
143-
doc = """If set, reverses the import order of specified jpaths (so that the rightmost wins)"""
142+
doc = """Deprecated compatibility flag; --jpath already uses right-most-wins priority"""
144143
)
145144
reverseJpathsPriority: Flag = Flag(),
146145
@arg(
@@ -165,12 +164,23 @@ final case class Config(
165164
doc = "Print runtime statistics (thunks, calls, imports, timing) to stderr after evaluation"
166165
)
167166
debugStats: Flag = Flag(),
167+
@arg(
168+
name = "version",
169+
doc = "Print version"
170+
)
171+
version: Flag = Flag(),
168172
@arg(
169173
name = "max-stack",
170174
short = 's',
171175
doc = "Number of allowed stack frames (default 500)"
172176
)
173177
maxStack: Int = 500,
178+
@arg(
179+
name = "max-trace",
180+
short = 't',
181+
doc = "Max length of stack trace before cropping (default 20, 0 means unlimited)"
182+
)
183+
maxTrace: Int = 20,
174184
@arg(
175185
name = "profile",
176186
doc =
@@ -188,16 +198,15 @@ final case class Config(
188198
* Returns the sequence of jpaths, combining command-line flags and the JSONNET_PATH environment
189199
* variable.
190200
*
191-
* JSONNET_PATH directories always have lower priority than --jpath flags. Within JSONNET_PATH,
192-
* the left-most entry has the highest priority, matching the behavior of the C++ and Go
193-
* implementations.
201+
* JSONNET_PATH directories always have lower priority than --jpath flags. Command-line --jpath
202+
* flags use right-most-wins priority, while JSONNET_PATH keeps left-most-wins priority, matching
203+
* the behavior of the C++ and Go implementations.
194204
*
195-
* The --reverse-jpaths-priority flag only affects the ordering of --jpath flags (reversing them
196-
* so that the rightmost wins, matching go-jsonnet behavior). JSONNET_PATH entries are always
197-
* appended after the (possibly reversed) --jpath flags.
205+
* The --reverse-jpaths-priority flag is retained for CLI compatibility and no longer changes
206+
* ordering because sjsonnet now matches go-jsonnet by default. JSONNET_PATH entries are always
207+
* appended after --jpath flags.
198208
*
199-
* For example, `JSONNET_PATH=a:b sjsonnet -J c -J d` results in search order: c, d, a, b (default
200-
* mode). With --reverse-jpaths-priority, the order becomes: d, c, a, b.
209+
* For example, `JSONNET_PATH=a:b sjsonnet -J c -J d` results in search order: d, c, a, b.
201210
*
202211
* See [[https://jsonnet-libs.github.io/jsonnet-training-course/lesson2.html#jsonnet_path]] for
203212
* details.
@@ -215,8 +224,7 @@ final case class Config(
215224
def getOrderedJpaths(jsonnetPathEnv: Option[String]): Seq[String] = {
216225
val envValue = jsonnetPathEnv.getOrElse(System.getenv("JSONNET_PATH"))
217226
val envPaths = Config.jsonnetPathEntries(envValue)
218-
val orderedJpaths = if (reverseJpathsPriority.value) jpaths.reverse else jpaths
219-
orderedJpaths ++ envPaths
227+
jpaths.reverse ++ envPaths
220228
}
221229
}
222230

0 commit comments

Comments
 (0)