Skip to content

jvm: use argfiles for JVM and JVM-compiler process arguments - #23620

Open
robertpi wants to merge 4 commits into
pantsbuild:mainfrom
robertpi:fix/classpath-length-issue
Open

jvm: use argfiles for JVM and JVM-compiler process arguments#23620
robertpi wants to merge 4 commits into
pantsbuild:mainfrom
robertpi:fix/classpath-length-issue

Conversation

@robertpi

Copy link
Copy Markdown
Contributor

Problem

JVM processes (javac, scalac, kotlinc, and the java binary itself) are invoked with their full classpath and argument list passed directly on the command line via argv. For components with many dependencies or long classpaths, this can exceed OS-level command-line length limits (e.g. ARG_MAX on Linux/macOS), causing compilation to fail with an "Argument list too long" style error.

Fix

  • For Java 9+ JDKs, JvmProcess now writes the java invocation's arguments (classpath plus JVM options and program args) to a @argfile (__jvm_args.txt) instead of passing them on the command line, per the java command-line argument files support added in JDK 9. Since Java 9+ argfiles supersede the classpath-length problem nailgun was also working around, nailgun is disabled in that path in favor of the argfile.
  • Extract a shared jvm_argfile_content helper for building a correctly quoted/escaped argfile from a list of arguments.
  • Apply the same argfile approach to the javac, scalac, and kotlinc compiler invocations themselves (-d <dir>, classpath, source file lists, etc.), which independently suffer from the same command-line length limits regardless of JDK version.

Test plan

  • src/python/pants/jvm/jdk_rules_test.py (new/updated coverage for argfile generation, escaping, and the Java 9+ nailgun-vs-argfile branch)
  • ./pants fmt lint on all changed files

The Java 9+ argfile optimization moved the classpath and program
arguments into an `@__jvm_args.txt` file to avoid OS-level "Argument
list too long" errors. This broke `pants run` and deploy-jar execution
against a Java 9+ JDK: those code paths embed a literal `{chroot}`
placeholder in classpath entries (substituted by the engine only within
a process's argv at spawn time, never within file contents), and the
InteractiveProcess run in the workspace doesn't have its cwd set to the
sandbox, so the bare `@__jvm_args.txt` reference was also unresolvable.

Skip the argfile and fall back to inline argv whenever a `{chroot}`
placeholder is present in the classpath or argv, which is exactly the
set of callers (run.py, run_deploy_jar.py) relying on that
substitution. All other callers, which don't use `{chroot}`, keep using
the argfile.

Also updates a stale OpenAPI generator test that asserted on argv
content that now lives in the argfile, and adds a release note.

@jgranstrom jgranstrom left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be able to expand on the cases where you hit this? Given where these limits generally sit it would have to be quite extreme to hit it for classpaths. Essentially on the order of thousands of classpath entries on a single target.

This PR effectively disables nailgun for JDK 9+, and nailgun is what gives warm JVMs, so that is a significant and unconditional cost on things like compile. But this isn't a problem when running nailgun in the first place, because then the potentially long arg list isn't passed via command arguments at all. So this disables nailgun, making it a universal problem, then applies the fix, and applies the file-indirection to every jvm_process whether or not it's needed. For anyone without the extreme classpath case that is strictly a regression.

The launcher args, the ones incompatible with nailgun, are also the ones that are bounded for anything nailgun-able. Under nailgun the only command line is the server spawn, carrying just the tool classpath. Everything that varies per invocation goes over the socket. So they are only unbounded for things that will not run under nailgun anyway, like tests.

The compile-side argfiles are nailgun-compatible, though also unnecessary under nailgun, since those args aren't passed via the command line there either.

So both are only needed when nailgun isn't used. The compile-side ones pay for that with a small unconditional overhead while the jvm_process one pays for it by disabling nailgun for everyone.

There is also a nailgun implementation that supports all modern JDK versions which Pants can be configured to use, so this would entirely disable the option of using that.

I would not like to see the blanket disabling of nailgun, but I can imagine other directions

  • Only do this when nailgun is not used, which is not immediately known here. You would essentially need to predict if nailgun will be used, which needs to combine several pieces of state. This can easily drift.
  • Doing it only if use_nailgun=False would allow for the fix when you know nailgun won't be used, but it being True only means it might.
  • Possibly rewriting the args at a later stage when you know how it will execute (nailgun or not) and would also know the actual argv length

Comment thread src/python/pants/jvm/jdk_rules.py Outdated
Comment on lines +463 to +465
use_argfile = jdk.jre_major_version >= 9 and not contains_chroot_placeholder
use_nailgun = []
if request.use_nailgun:
if request.use_nailgun and not use_argfile:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effectively disables nailgun, where the fix is not needed since the long args are not passed on command line already.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — see the full reply above and the new commit; gated the argfile on not request.use_nailgun instead of just the JDK version, so nailgun stays enabled here unless the caller already isn't going to use it.

Nailgun sends the classpath and program arguments to the already-running
server over a socket rather than via argv, so it was never subject to the
OS argument-length limit the argfile works around, and gets no benefit
from routing through it. Gating the argfile on JDK version alone
unconditionally disabled nailgun for every Java 9+ process, including the
common case (e.g. compilation) where nailgun was requested and the
argfile wasn't needed.

Gate on `not request.use_nailgun` instead, so the argfile is only used
when the caller already isn't going to use nailgun (as JUnit/ScalaTest
test execution and `pants run`/deploy-jar already do), rather than
disabling nailgun universally to make room for it.

Verified with `PY=python3.14 ./pants test` on:
- src/python/pants/jvm/jdk_rules_test.py
- src/python/pants/jvm/run_integration_test.py
- src/python/pants/backend/openapi/util_rules/generator_process_test.py
- src/python/pants/backend/java/compile/javac_test.py
- src/python/pants/backend/scala/compile/scalac_test.py
- src/python/pants/backend/kotlin/compile/kotlinc_test.py
- src/python/pants/jvm/test/junit_test.py
- src/python/pants/backend/scala/test/scalatest_test.py

and `PY=python3.14 ./pants fmt lint check` on all changed files.
@robertpi

Copy link
Copy Markdown
Contributor Author

Good catch — you're right this disables nailgun for every JDK 9+ process, not just the ones that need the argfile, and that nailgun already keeps the classpath off the command line entirely, so the fix bought nothing there and only cost something. Pushed a fix: the gate is now use_argfile = jdk.jre_major_version >= 9 and not request.use_nailgun and not contains_chroot_placeholder, so we only route through the argfile when the caller already isn't going to use nailgun, matching your second suggestion. Compilation (which doesn't override use_nailgun, so defaults to True) goes back to using nailgun on Java 9+ as before this PR.

On the concrete case: it's pants test on a subproject with a JUnit 5 test classpath of roughly 250 jars (Spark + Hadoop + Cassandra + Scala + Netty native bundles). Colon-joined for java -cp, that exceeds Linux's MAX_ARG_STRLEN (128 KiB per argv element), and the process launch fails with Os { code: 7, kind: ArgumentListTooLong, ... }. Worth noting: JUnit test runs already set use_nailgun=False (junit.py), so this case was never nailgun-eligible in the first place, which is exactly why gating on use_nailgun rather than JDK version is the right fix, not just a safer one.

Verified pants test on the JVM compile (javac/scalac/kotlinc), JUnit/ScalaTest, and pants run/deploy-jar paths still pass with this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants