Skip to content

Commit d82f180

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 628e0c1 commit d82f180

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(
@@ -134,7 +133,7 @@ final case class Config(
134133
throwErrorForInvalidSets: Flag = Flag(),
135134
@arg(
136135
name = "reverse-jpaths-priority",
137-
doc = """If set, reverses the import order of specified jpaths (so that the rightmost wins)"""
136+
doc = """Deprecated compatibility flag; --jpath already uses right-most-wins priority"""
138137
)
139138
reverseJpathsPriority: Flag = Flag(),
140139
@arg(
@@ -159,12 +158,23 @@ final case class Config(
159158
doc = "Print runtime statistics (thunks, calls, imports, timing) to stderr after evaluation"
160159
)
161160
debugStats: Flag = Flag(),
161+
@arg(
162+
name = "version",
163+
doc = "Print version"
164+
)
165+
version: Flag = Flag(),
162166
@arg(
163167
name = "max-stack",
164168
short = 's',
165169
doc = "Number of allowed stack frames (default 500)"
166170
)
167171
maxStack: Int = 500,
172+
@arg(
173+
name = "max-trace",
174+
short = 't',
175+
doc = "Max length of stack trace before cropping (default 20, 0 means unlimited)"
176+
)
177+
maxTrace: Int = 20,
168178
@arg(
169179
name = "profile",
170180
doc =
@@ -182,16 +192,15 @@ final case class Config(
182192
* Returns the sequence of jpaths, combining command-line flags and the JSONNET_PATH environment
183193
* variable.
184194
*
185-
* JSONNET_PATH directories always have lower priority than --jpath flags. Within JSONNET_PATH,
186-
* the left-most entry has the highest priority, matching the behavior of the C++ and Go
187-
* implementations.
195+
* JSONNET_PATH directories always have lower priority than --jpath flags. Command-line --jpath
196+
* flags use right-most-wins priority, while JSONNET_PATH keeps left-most-wins priority, matching
197+
* the behavior of the C++ and Go implementations.
188198
*
189-
* The --reverse-jpaths-priority flag only affects the ordering of --jpath flags (reversing them
190-
* so that the rightmost wins, matching go-jsonnet behavior). JSONNET_PATH entries are always
191-
* appended after the (possibly reversed) --jpath flags.
199+
* The --reverse-jpaths-priority flag is retained for CLI compatibility and no longer changes
200+
* ordering because sjsonnet now matches go-jsonnet by default. JSONNET_PATH entries are always
201+
* appended after --jpath flags.
192202
*
193-
* For example, `JSONNET_PATH=a:b sjsonnet -J c -J d` results in search order: c, d, a, b (default
194-
* mode). With --reverse-jpaths-priority, the order becomes: d, c, a, b.
203+
* For example, `JSONNET_PATH=a:b sjsonnet -J c -J d` results in search order: d, c, a, b.
195204
*
196205
* See [[https://jsonnet-libs.github.io/jsonnet-training-course/lesson2.html#jsonnet_path]] for
197206
* details.
@@ -209,8 +218,7 @@ final case class Config(
209218
def getOrderedJpaths(jsonnetPathEnv: Option[String]): Seq[String] = {
210219
val envValue = jsonnetPathEnv.getOrElse(System.getenv("JSONNET_PATH"))
211220
val envPaths = Config.jsonnetPathEntries(envValue)
212-
val orderedJpaths = if (reverseJpathsPriority.value) jpaths.reverse else jpaths
213-
orderedJpaths ++ envPaths
221+
jpaths.reverse ++ envPaths
214222
}
215223
}
216224

0 commit comments

Comments
 (0)