77from __future__ import annotations
88
99import hashlib
10+ import logging
1011from pathlib import Path
1112from typing import TYPE_CHECKING
1213
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
3197def 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
0 commit comments