Skip to content

Commit 62230b3

Browse files
ctruedenclaude
andcommitted
Further improve classpath/module-path detection
Previously, it would use a jar executable on the system path, which caused different behavior depending on whether the jar executable was from OpenJDK 9+ versus pre-JPMS versions of Java. Now, for consistency, jgo fetches a baseline version of OpenJDK 11 so that its jar tool can be used for consistent JAR classification. Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent bf0b7c7 commit 62230b3

File tree

3 files changed

+126
-89
lines changed

3 files changed

+126
-89
lines changed

src/jgo/cli/output.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ def print_jars(environment: Environment) -> None:
118118
print(jar_path)
119119
else:
120120
_console.print("[yellow]No classpath JARs[/]")
121-
_console.print("[dim]TIP: Use 'jgo info modulepath' to see module-path JARs[/]")
122121

123122
# Print module-path JARs
124123
if mp_jars:
@@ -128,6 +127,8 @@ def print_jars(environment: Environment) -> None:
128127
_console.print("[bold cyan]Module-path:[/]")
129128
for jar_path in mp_jars:
130129
print(jar_path)
130+
else:
131+
_console.print("[yellow]No module-path JARs[/]")
131132

132133

133134
def print_main_classes(environment: Environment) -> None:

src/jgo/env/builder.py

Lines changed: 96 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import hashlib
10+
import logging
1011
from pathlib import Path
1112
from typing import TYPE_CHECKING
1213

@@ -27,6 +28,71 @@
2728
from ..maven import Component, MavenContext
2829
from .spec import EnvironmentSpec
2930

31+
_log = logging.getLogger(__name__)
32+
33+
# Cached baseline JDK path for JAR classification
34+
_baseline_jar_tool: Path | None = None
35+
36+
37+
def get_baseline_jar_tool() -> Path | None:
38+
"""
39+
Get a consistent baseline `jar` tool for JAR classification using JavaLocator.
40+
41+
This ensures that JAR module classification is deterministic across all systems,
42+
regardless of which Java version happens to be on the system PATH.
43+
44+
Uses a cached OpenJDK 11 installation obtained via JavaLocator/cjdk. This guarantees:
45+
- Consistent environment structure across all systems
46+
- Reproducible builds (same endpoint → same jars/ vs modules/ placement)
47+
- Reliable CI/local parity
48+
49+
Returns:
50+
Path to the `jar` executable, or None if unavailable
51+
"""
52+
global _baseline_jar_tool
53+
54+
# Return cached value if already resolved
55+
if _baseline_jar_tool is not None:
56+
return _baseline_jar_tool
57+
58+
try:
59+
# Use JavaLocator to get a baseline Java 11 (LTS version with module support)
60+
# This is independent of the target environment's Java version
61+
# Use AUTO mode to always fetch via cjdk (not system Java)
62+
from ..exec.java_source import JavaLocator, JavaSource
63+
64+
_log.debug("Obtaining baseline Java 11 for JAR classification...")
65+
locator = JavaLocator(
66+
java_source=JavaSource.AUTO,
67+
java_version=11,
68+
verbose=False,
69+
)
70+
java_path = locator.locate()
71+
72+
# Derive jar tool path from java executable
73+
# java is at $JAVA_HOME/bin/java, jar is at $JAVA_HOME/bin/jar
74+
import sys
75+
76+
jar_exe = "jar.exe" if sys.platform == "win32" else "jar"
77+
jar_tool = java_path.parent / jar_exe
78+
79+
if jar_tool.exists():
80+
_log.debug(f"Using baseline jar tool: {jar_tool}")
81+
_baseline_jar_tool = jar_tool
82+
return jar_tool
83+
else:
84+
_log.warning(f"jar tool not found at {jar_tool}")
85+
_baseline_jar_tool = None # Cache the failure to avoid repeated attempts
86+
return None
87+
88+
except Exception as e:
89+
_log.warning(
90+
f"Failed to obtain baseline JDK for JAR classification: {e}. "
91+
"Falling back to simple module detection."
92+
)
93+
_baseline_jar_tool = None # Cache the failure
94+
return None
95+
3096

3197
def filter_managed_components(
3298
components: list[Component], coordinates: list[Coordinate]
@@ -632,42 +698,28 @@ def _build_environment(
632698
if version:
633699
min_java_version = max(min_java_version or 0, version)
634700

635-
# Get JDK for module classification (only if Java 9+)
636-
jar_tool = None
637-
if min_java_version and min_java_version >= 9:
638-
try:
639-
from ..exec.java_source import JavaLocator, JavaSource
640-
641-
locator = JavaLocator(java_source=JavaSource.AUTO, verbose=False)
642-
java_path = locator.locate(min_version=min_java_version)
643-
jar_tool = java_path.parent / "jar"
644-
if not jar_tool.exists():
645-
# No jar tool available - fall back to simple detection
646-
jar_tool = None
647-
except Exception:
648-
# Failed to get JDK - fall back to simple detection
649-
jar_tool = None
650-
651-
# First, link the components themselves
652-
for component in components:
653-
artifact = component.artifact()
654-
source_path = artifact.resolve()
701+
# Get baseline JDK for module classification
702+
# This uses a consistent Java 11 via cjdk, ensuring deterministic builds
703+
# regardless of what Java version is on the system PATH
704+
jar_tool = get_baseline_jar_tool()
655705

706+
# Helper function to classify and link a JAR artifact
707+
def process_artifact(artifact, source_path):
708+
"""Classify JAR, link it to the appropriate directory, and create locked dependency."""
656709
# Classify JAR for module compatibility
710+
# Note: min_java_version is about bytecode level, not module support.
711+
# JARs compiled for Java 8 can still have Automatic-Module-Name and
712+
# be used on the module-path with Java 9+.
657713
if jar_tool:
658-
# Java 9+ with jar tool - use precise classification
714+
# Baseline jar tool available - use precise classification
659715
jar_type = classify_jar(source_path, jar_tool)
660716
# Types 1/2/3 are modularizable, type 4 is not
661717
target_dir = modules_dir if jar_type in (1, 2, 3) else jars_dir
662-
elif min_java_version and min_java_version >= 9:
663-
# Java 9+ but no jar tool - use simple module detection
718+
else:
719+
# No jar tool available - use simple module detection
664720
module_info = detect_module_info(source_path)
665721
target_dir = modules_dir if module_info.is_modular else jars_dir
666722
jar_type = None # Not classified
667-
else:
668-
# Java 8 or below - no module support, everything goes to jars/
669-
target_dir = jars_dir
670-
jar_type = None # Not classified
671723

672724
dest_path = target_dir / artifact.filename
673725

@@ -679,20 +731,24 @@ def _build_environment(
679731

680732
# Create locked dependency with module info and classification
681733
sha256 = compute_sha256(source_path) if source_path.exists() else None
682-
locked_deps.append(
683-
LockedDependency(
684-
groupId=artifact.groupId,
685-
artifactId=artifact.artifactId,
686-
version=artifact.version,
687-
packaging=artifact.packaging,
688-
classifier=artifact.classifier,
689-
sha256=sha256,
690-
is_modular=module_info.is_modular,
691-
module_name=module_info.module_name,
692-
jar_type=jar_type,
693-
)
734+
return LockedDependency(
735+
groupId=artifact.groupId,
736+
artifactId=artifact.artifactId,
737+
version=artifact.version,
738+
packaging=artifact.packaging,
739+
classifier=artifact.classifier,
740+
sha256=sha256,
741+
is_modular=module_info.is_modular,
742+
module_name=module_info.module_name,
743+
jar_type=jar_type,
694744
)
695745

746+
# First, link the components themselves
747+
for component in components:
748+
artifact = component.artifact()
749+
source_path = artifact.resolve()
750+
locked_deps.append(process_artifact(artifact, source_path))
751+
696752
# Track which artifacts we've already processed (from components)
697753
processed = {(c.groupId, c.artifactId, c.version) for c in components}
698754

@@ -708,46 +764,7 @@ def _build_environment(
708764
processed.add(key)
709765

710766
source_path = artifact.resolve()
711-
712-
# Classify JAR for module compatibility
713-
if jar_tool:
714-
# Java 9+ with jar tool - use precise classification
715-
jar_type = classify_jar(source_path, jar_tool)
716-
# Types 1/2/3 are modularizable, type 4 is not
717-
target_dir = modules_dir if jar_type in (1, 2, 3) else jars_dir
718-
elif min_java_version and min_java_version >= 9:
719-
# Java 9+ but no jar tool - use simple module detection
720-
module_info = detect_module_info(source_path)
721-
target_dir = modules_dir if module_info.is_modular else jars_dir
722-
jar_type = None # Not classified
723-
else:
724-
# Java 8 or below - no module support, everything goes to jars/
725-
target_dir = jars_dir
726-
jar_type = None # Not classified
727-
728-
dest_path = target_dir / artifact.filename
729-
730-
if not dest_path.exists():
731-
link_file(source_path, dest_path, self.link_strategy)
732-
733-
# For lockfile, we still need module_info for backward compat
734-
module_info = detect_module_info(source_path)
735-
736-
# Create locked dependency with module info and classification
737-
sha256 = compute_sha256(source_path) if source_path.exists() else None
738-
locked_deps.append(
739-
LockedDependency(
740-
groupId=artifact.groupId,
741-
artifactId=artifact.artifactId,
742-
version=artifact.version,
743-
packaging=artifact.packaging,
744-
classifier=artifact.classifier,
745-
sha256=sha256,
746-
is_modular=module_info.is_modular,
747-
module_name=module_info.module_name,
748-
jar_type=jar_type,
749-
)
750-
)
767+
locked_deps.append(process_artifact(artifact, source_path))
751768

752769
# Return locked dependencies for lock file generation
753770
return locked_deps

tests/cli/info.t

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,32 @@ Test info classpath with no endpoint.
3939
Test info classpath with endpoint.
4040

4141
$ jgo info classpath com.google.guava:guava:33.0.0-jre
42-
*/jars/checker-qual-3.41.0.jar (glob)
43-
*/jars/error_prone_annotations-2.23.0.jar (glob)
44-
*/jars/failureaccess-1.0.2.jar (glob)
45-
*/jars/guava-33.0.0-jre.jar (glob)
46-
*/jars/j2objc-annotations-2.8.jar (glob)
47-
*/jars/jsr305-3.0.2.jar (glob)
48-
*/jars/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar (glob)
42+
No JARs on classpath
43+
TIP: Use 'jgo info module-path' to see module-path JARs
44+
45+
Test info modulepath with endpoint.
46+
47+
$ jgo info modulepath com.google.guava:guava:33.0.0-jre
48+
*/modules/checker-qual-3.41.0.jar (glob)
49+
*/modules/error_prone_annotations-2.23.0.jar (glob)
50+
*/modules/failureaccess-1.0.2.jar (glob)
51+
*/modules/guava-33.0.0-jre.jar (glob)
52+
*/modules/j2objc-annotations-2.8.jar (glob)
53+
*/modules/jsr305-3.0.2.jar (glob)
54+
*/modules/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar (glob)
55+
56+
Test info jars with endpoint.
57+
58+
$ jgo info jars com.google.guava:guava:33.0.0-jre
59+
No classpath JARs
60+
Module-path:
61+
*/modules/checker-qual-3.41.0.jar (glob)
62+
*/modules/error_prone_annotations-2.23.0.jar (glob)
63+
*/modules/failureaccess-1.0.2.jar (glob)
64+
*/modules/guava-33.0.0-jre.jar (glob)
65+
*/modules/j2objc-annotations-2.8.jar (glob)
66+
*/modules/jsr305-3.0.2.jar (glob)
67+
*/modules/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar (glob)
4968

5069
Test info deptree with no endpoint.
5170

@@ -105,8 +124,8 @@ Test info javainfo with endpoint.
105124

106125
$ jgo info javainfo com.google.guava:guava:33.0.0-jre
107126

108-
Environment: /*/.cache/jgo/com/google/guava/guava/* (glob)
109-
Class-path JARs: 7
127+
Environment: */.cache/jgo/com/google/guava/guava/* (glob)
128+
Module-path JARs: 7
110129
Total JARs: 7
111130

112131
╭───────────────────────── Java Version Requirements ──────────────────────────╮

0 commit comments

Comments
 (0)