Skip to content

Commit 9d0faf3

Browse files
ctruedenclaude
andcommitted
Fix cache key to include classifier and packaging
The cache key previously used Component (G:A:V only), causing artifacts with different classifiers to incorrectly share the same cache directory. For example: - org.lwjgl:lwjgl:3.3.1:jar:natives-linux (Linux natives) - org.lwjgl:lwjgl:3.3.1:jar:natives-windows (Windows natives) These have completely different JARs but were sharing the same cache. Changes: - Changed _cache_key() to use list[Dependency] instead of list[Component] - Dependency includes full Artifact coordinates (G:A:V:C:P) - Added _coordinates_to_dependencies() to preserve coordinate information - Added _components_to_dependencies() for backward compatibility - Updated from_components() to accept optional coordinates parameter - Updated from_spec() to preserve parsed coordinates for cache key - Added comprehensive tests in tests/test_cache_key.py Benefits: - Different classifiers now create different caches (fixes LWJGL bug) - Different packaging types now create different caches - Cache key ready for exclusions (Dependency includes exclusions field) Impact: - Cache directory structure changes (old caches won't be reused) - This is intentional - old caches with classifiers were incorrect 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent ebe1510 commit 9d0faf3

File tree

3 files changed

+257
-11
lines changed

3 files changed

+257
-11
lines changed

src/jgo/env/builder.py

Lines changed: 149 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from .lockfile import LockedDependency, LockFile, compute_sha256, compute_spec_hash
2727

2828
if TYPE_CHECKING:
29-
from ..maven import Component, MavenContext
29+
from ..maven import Component, Dependency, MavenContext
3030
from .spec import EnvironmentSpec
3131

3232
_log = logging.getLogger(__name__)
@@ -282,12 +282,25 @@ def from_components(
282282
components: list[Component],
283283
update: bool = False,
284284
main_class: str | None = None,
285+
coordinates: list["Coordinate"] | None = None,
285286
) -> Environment:
286287
"""
287288
Build an environment from a list of components (ad-hoc mode).
289+
290+
Args:
291+
components: Maven components to include
292+
update: Force update even if cached
293+
main_class: Optional main class override
294+
coordinates: Original coordinates (preserves classifier/packaging for cache key).
295+
If not provided, defaults are used (empty classifier, jar packaging).
288296
"""
289-
# Generate cache key
290-
cache_key = self._cache_key(components)
297+
# Generate cache key from coordinates (if provided) or components
298+
if coordinates:
299+
dependencies = self._coordinates_to_dependencies(coordinates)
300+
else:
301+
dependencies = self._components_to_dependencies(components)
302+
303+
cache_key = self._cache_key(dependencies)
291304

292305
# Environment path (always hierarchical for ad-hoc mode)
293306
primary = components[0]
@@ -365,7 +378,9 @@ def from_spec(
365378
Environment instance
366379
"""
367380
# Parse coordinates into components
381+
# Keep coordinates for cache key generation (preserves classifier/packaging)
368382
components = []
383+
coordinates = []
369384
for coord_str in spec.coordinates:
370385
coord = Coordinate.parse(coord_str)
371386
version = coord.version or "RELEASE"
@@ -374,6 +389,7 @@ def from_spec(
374389
coord.groupId, coord.artifactId
375390
).at_version(version)
376391
components.append(component)
392+
coordinates.append(coord)
377393

378394
# Use spec's cache_dir if specified, otherwise use builder's default
379395
cache_dir = (
@@ -388,7 +404,9 @@ def from_spec(
388404
workspace_path = cache_dir
389405
else:
390406
# Hierarchical structure for ad-hoc mode
391-
cache_key = self._cache_key(components)
407+
# Use coordinates for cache key to preserve classifier/packaging
408+
dependencies = self._coordinates_to_dependencies(coordinates)
409+
cache_key = self._cache_key(dependencies)
392410
primary = components[0]
393411
workspace_path = (
394412
cache_dir / "envs" / primary.project.path_prefix / cache_key
@@ -583,13 +601,134 @@ def _is_environment_valid(
583601

584602
return True
585603

586-
def _cache_key(self, components: list[Component]) -> str:
587-
"""Generate a stable hash for a set of components."""
604+
def _components_to_dependencies(
605+
self, components: list[Component]
606+
) -> list["Dependency"]:
607+
"""
608+
Convert Component objects to Dependency objects with default classifier/packaging.
609+
610+
This is a compatibility helper for code that still uses Components.
611+
Uses empty classifier and 'jar' packaging by default.
612+
613+
Args:
614+
components: List of Component objects
615+
616+
Returns:
617+
List of Dependency objects with default artifact settings
618+
"""
619+
from jgo.maven.core import Dependency
620+
621+
dependencies = []
622+
for component in components:
623+
# Create default artifact (empty classifier, jar packaging)
624+
artifact = component.artifact(classifier="", packaging="jar")
625+
626+
# Create dependency with defaults
627+
dependency = Dependency(
628+
artifact=artifact,
629+
scope="compile",
630+
optional=False,
631+
exclusions=[],
632+
)
633+
dependencies.append(dependency)
634+
635+
return dependencies
636+
637+
def _coordinates_to_dependencies(
638+
self, coordinates: list["Coordinate"]
639+
) -> list["Dependency"]:
640+
"""
641+
Convert Coordinate objects to Dependency objects for cache key generation.
642+
643+
This preserves classifier, packaging, and exclusion information from the
644+
original coordinates.
645+
646+
Args:
647+
coordinates: List of parsed Coordinate objects with full coordinate info
648+
649+
Returns:
650+
List of Dependency objects
651+
"""
652+
from jgo.maven.core import Dependency
653+
654+
dependencies = []
655+
for coord in coordinates:
656+
# Get version (default to RELEASE if not specified)
657+
version = coord.version or "RELEASE"
658+
659+
# Create Component (G:A:V)
660+
component = self.context.project(
661+
coord.groupId, coord.artifactId
662+
).at_version(version)
663+
664+
# Create Artifact with classifier and packaging (G:A:V:C:P)
665+
classifier = coord.classifier or ""
666+
packaging = coord.packaging or "jar"
667+
artifact = component.artifact(classifier=classifier, packaging=packaging)
668+
669+
# Create Dependency with scope and exclusions
670+
# TODO: Parse exclusions from coord when exclusion syntax is implemented
671+
# For now, use empty exclusions list
672+
scope = coord.scope or "compile"
673+
optional = coord.optional or False
674+
exclusions: list = [] # Will be populated when exclusion parsing is added
675+
676+
dependency = Dependency(
677+
artifact=artifact,
678+
scope=scope,
679+
optional=optional,
680+
exclusions=exclusions,
681+
)
682+
dependencies.append(dependency)
683+
684+
return dependencies
685+
686+
def _cache_key(self, dependencies: list["Dependency"]) -> str:
687+
"""
688+
Generate a stable hash for a set of dependencies.
689+
690+
Uses full artifact coordinates (G:A:V:C:P) plus exclusions to ensure:
691+
- Different classifiers get different caches (e.g., natives-linux vs natives-windows)
692+
- Different packaging types get different caches
693+
- Different exclusions get different caches (different dependency trees)
694+
695+
Args:
696+
dependencies: List of Dependency objects with full coordinate info and exclusions
697+
698+
Returns:
699+
16-character hex hash string
700+
"""
588701
# Sort to ensure stable ordering
589-
# Use resolved_version to ensure RELEASE/LATEST resolve to consistent cache keys
590-
coord_strings = sorted(
591-
[f"{c.groupId}:{c.artifactId}:{c.resolved_version}" for c in components]
592-
)
702+
# Use resolved version to ensure RELEASE/LATEST resolve to consistent cache keys
703+
coord_strings = []
704+
for dep in sorted(
705+
dependencies,
706+
key=lambda d: (
707+
d.artifact.groupId,
708+
d.artifact.artifactId,
709+
d.artifact.version, # This resolves RELEASE/LATEST
710+
d.artifact.classifier,
711+
d.artifact.packaging,
712+
),
713+
):
714+
# Include full artifact coordinates: G:A:V:C:P
715+
coord_str = (
716+
f"{dep.artifact.groupId}:"
717+
f"{dep.artifact.artifactId}:"
718+
f"{dep.artifact.version}:" # Resolved version
719+
f"{dep.artifact.classifier}:"
720+
f"{dep.artifact.packaging}"
721+
)
722+
723+
# Include exclusions for this dependency
724+
if dep.exclusions:
725+
excl_strs = sorted(
726+
[f"{e.groupId}:{e.artifactId}" for e in dep.exclusions]
727+
)
728+
coord_str += f":excl={','.join(excl_strs)}"
729+
730+
coord_strings.append(coord_str)
731+
593732
combined = "+".join(coord_strings)
594733

595734
# Include optional_depth in cache key

tests/test_cache_key.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Test that cache key includes classifier and packaging."""
2+
3+
from jgo.env.builder import EnvironmentBuilder
4+
from jgo.maven.core import MavenContext
5+
from jgo.parse.coordinate import Coordinate
6+
7+
8+
def test_cache_key_includes_classifier():
9+
"""Test that different classifiers produce different cache keys."""
10+
context = MavenContext()
11+
builder = EnvironmentBuilder(context)
12+
13+
# Create coordinates with different classifiers
14+
coord1 = Coordinate(
15+
groupId="org.lwjgl",
16+
artifactId="lwjgl",
17+
version="3.3.1",
18+
classifier="natives-linux",
19+
packaging="jar",
20+
)
21+
22+
coord2 = Coordinate(
23+
groupId="org.lwjgl",
24+
artifactId="lwjgl",
25+
version="3.3.1",
26+
classifier="natives-windows",
27+
packaging="jar",
28+
)
29+
30+
# Convert to dependencies
31+
deps1 = builder._coordinates_to_dependencies([coord1])
32+
deps2 = builder._coordinates_to_dependencies([coord2])
33+
34+
# Generate cache keys
35+
key1 = builder._cache_key(deps1)
36+
key2 = builder._cache_key(deps2)
37+
38+
# Different classifiers should produce different keys
39+
assert key1 != key2, f"Expected different cache keys but got: {key1} == {key2}"
40+
41+
42+
def test_cache_key_includes_packaging():
43+
"""Test that different packaging types produce different cache keys."""
44+
context = MavenContext()
45+
builder = EnvironmentBuilder(context)
46+
47+
# Create coordinates with different packaging
48+
coord1 = Coordinate(
49+
groupId="org.example",
50+
artifactId="myapp",
51+
version="1.0.0",
52+
classifier="",
53+
packaging="jar",
54+
)
55+
56+
coord2 = Coordinate(
57+
groupId="org.example",
58+
artifactId="myapp",
59+
version="1.0.0",
60+
classifier="",
61+
packaging="war",
62+
)
63+
64+
# Convert to dependencies
65+
deps1 = builder._coordinates_to_dependencies([coord1])
66+
deps2 = builder._coordinates_to_dependencies([coord2])
67+
68+
# Generate cache keys
69+
key1 = builder._cache_key(deps1)
70+
key2 = builder._cache_key(deps2)
71+
72+
# Different packaging should produce different keys
73+
assert key1 != key2, f"Expected different cache keys but got: {key1} == {key2}"
74+
75+
76+
def test_cache_key_same_coordinates():
77+
"""Test that identical coordinates produce the same cache key."""
78+
context = MavenContext()
79+
builder = EnvironmentBuilder(context)
80+
81+
coord1 = Coordinate(
82+
groupId="org.example",
83+
artifactId="myapp",
84+
version="1.0.0",
85+
classifier="linux",
86+
packaging="jar",
87+
)
88+
89+
coord2 = Coordinate(
90+
groupId="org.example",
91+
artifactId="myapp",
92+
version="1.0.0",
93+
classifier="linux",
94+
packaging="jar",
95+
)
96+
97+
deps1 = builder._coordinates_to_dependencies([coord1])
98+
deps2 = builder._coordinates_to_dependencies([coord2])
99+
100+
key1 = builder._cache_key(deps1)
101+
key2 = builder._cache_key(deps2)
102+
103+
# Same coordinates should produce same key
104+
assert key1 == key2, f"Expected same cache keys but got: {key1} != {key2}"

tests/test_env.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ def test_cache_key_generation():
9494
component2 = project2.at_version("2.0.0")
9595

9696
components = [component1, component2]
97-
key = builder._cache_key(components)
97+
98+
# Convert components to dependencies for cache key
99+
dependencies = builder._components_to_dependencies(components)
100+
key = builder._cache_key(dependencies)
98101

99102
# Should generate a hash
100103
assert isinstance(key, str)

0 commit comments

Comments
 (0)