This proposal specifies a dependency exclusion approach for jgo, allowing users to exclude specific transitive dependencies from the classpath when running Java applications.
Motivation
Maven supports <exclusions> blocks within <dependency> elements to exclude transitive dependencies. Users need equivalent functionality in jgo without creating custom pom.xml files.
Common use cases:
- Logging conflicts: Exclude commons-logging when using slf4j
- Version conflicts: Exclude older component versions when explicitly including newer ones with different G:A
- Unwanted dependencies: Remove unused transitive dependencies that cause classpath bloat
Design Summary
Three Ways to Specify Exclusions
-
Endpoint syntax (inline): Exclusions specified directly in coordinate strings
coord(x) - Mark this coordinate AS a global exclusion
coord(x:excl1,x:excl2) - This coordinate HAS per-dependency exclusions
-
jgo.toml (project mode): Exclusions in project configuration files
- Global exclusions:
exclusions = ["g:a", ...]
- Per-coordinate: Use inline syntax in coordinate strings
-
jgo exclude (management): CLI command to add/remove exclusions from jgo.toml
jgo exclude add g:a
jgo exclude remove g:a
jgo exclude list
Design Rationale
Why inline syntax?
- Exclusions modify the dependency graph and must affect the cache key
- Users may need to include exclusions in shortcuts (which expand to endpoints, not CLI flags)
- Enables per-coordinate exclusions: exclude X from coordinate A but not from coordinate B
Why no --exclude CLI flag?
- Redundant with inline syntax:
+coord(x) achieves same result
- Simplicity: One way to specify exclusions (Python philosophy)
- Consistency: Other coordinate modifiers (placement, raw) use inline syntax
Why jgo exclude command?
- Convenience for managing jgo.toml without manual editing
- Validates exclusion coordinates before writing
- Prevents syntax errors in TOML files
- Symmetry with
jgo add and jgo remove for coordinates themselves
Detailed Specification
1. Endpoint Syntax
All coordinate modifiers use a unified parenthetical with comma-separated values:
Grammar
coordinate := G:A[:V][:C][:P] [modifiers] [!] [@mainClass]
modifiers := '(' modifier [',' modifier]* ')'
modifier := placement | exclusion-marker | exclusion
placement := 'c' | 'cp' | 'm' | 'mp' | 'p'
exclusion-marker := 'x' # this coord IS an exclusion
exclusion := 'x:' groupId ':' artifactId # this coord HAS this exclusion
Key simplification: Each exclusion is a separate modifier with its own x: prefix. This eliminates comma ambiguity - commas only separate modifiers, never parts within a modifier.
Two Meanings of (x)
1. Exclusion Marker: coord(x) - this coordinate IS a global exclusion
# Include httpclient, but globally exclude commons-logging and log4j
jgo httpclient+commons-logging:commons-logging(x)+log4j:log4j(x)
The marked coordinates are excluded from ALL dependencies in the dependency graph.
2. Exclusion List: coord(x:excl1,x:excl2) - this coordinate HAS per-dependency exclusions
# Include httpclient, but exclude commons-logging only from httpclient's dependency tree
jgo httpclient(x:commons-logging:commons-logging)
The exclusions apply only to the specified coordinate's transitive dependencies.
Multiple Exclusions
Specify multiple exclusions by repeating the x: modifier:
# Exclude multiple dependencies from httpclient
jgo httpclient(x:commons-logging:commons-logging,x:commons-codec:commons-codec)
Wildcard Support
Maven-style wildcards (*) are supported for groupId or artifactId:
# Exclude all artifacts from org.slf4j group
jgo spring-web(x:org.slf4j:*)
# Exclude commons-logging from any group
jgo httpclient(x:*:commons-logging)
Combining Modifiers
Multiple modifiers can coexist in one parenthetical (order independent):
# Placement + exclusions
jgo httpclient(c,x:commons-logging:commons-logging)
jgo httpclient(x:commons-logging:commons-logging,c) # same thing
# Multiple exclusions + placement + raw flag
jgo httpclient:4.5.14(c,x:commons-logging:commons-logging,x:commons-codec:commons-codec)!
# Module-path + exclusions
jgo lwjgl:3.3.1(m,x:lwjgl-opengl:lwjgl-opengl)
Key properties:
- Single parenthetical - no ambiguity about ordering
- Comma-separated - readable and familiar
- Order independent -
(c,x:...) equals (x:...,c)
- Extensible - easy to add new modifiers
- Raw flag
! always last - logical termination symbol
Examples
Basic global exclusion:
jgo httpclient+commons-logging:commons-logging(x)
Per-coordinate exclusion:
jgo httpclient(x:commons-logging:commons-logging)+spring-web
Multiple per-coordinate exclusions:
jgo spring-web(x:commons-logging:commons-logging,x:log4j:log4j)
Combined with placement modifiers:
jgo lwjgl:3.3.1(m,x:lwjgl-opengl:lwjgl-opengl)
Complex scenario with main class:
jgo 'httpclient(x:commons-logging:commons-logging)+lwjgl:3.3.1(m)@MyApp'
Shortcuts with Exclusions
Shortcuts can include exclusions since they're part of the coordinate syntax:
# ~/.config/jgo.conf
[shortcuts]
# Per-coordinate exclusions
httpclient-clean = httpclient:4.5.14(x:commons-logging:commons-logging,x:commons-codec:commons-codec)
spring-clean = spring-web(x:commons-logging:commons-logging,x:log4j:log4j)
# Exclusions + placement
lwjgl-modular = lwjgl:3.3.1(m,x:lwjgl-opengl:lwjgl-opengl)
# Global exclusion markers
no-logging = httpclient+commons-logging:commons-logging(x)+log4j:log4j(x)
Usage:
jgo httpclient-clean
jgo spring-clean@MyApp
jgo lwjgl-modular
2. jgo.toml Integration
Global Exclusions
Global exclusions apply to all dependencies in the environment:
[environment]
name = "my-app"
[dependencies]
coordinates = [
"org.springframework:spring-web:5.3.20",
"org.apache.httpcomponents:httpclient:4.5.13"
]
# Global exclusions - applied to ALL dependencies above
exclusions = [
"commons-logging:commons-logging",
"log4j:log4j"
]
Per-Coordinate Exclusions
Use inline endpoint syntax directly in coordinate strings:
[dependencies]
coordinates = [
# Exclude commons-logging only from httpclient
"org.apache:httpclient:4.5.14(x:commons-logging:commons-logging)",
# Exclude multiple dependencies from spring-web
"org.springframework:spring-web:5.3.20(x:commons-logging:commons-logging,x:log4j:log4j)",
# Regular coordinate without exclusions
"org.slf4j:slf4j-api:1.7.32"
]
Rationale for inline syntax:
- Reuses existing endpoint parsing logic
- No special TOML schema needed
- Clear and self-documenting
- Consistent with CLI usage
Complete Example
[environment]
name = "web-app"
[dependencies]
# Mix of global and per-coordinate exclusions
coordinates = [
# This has its own exclusions
"org.apache:httpclient:4.5.14(x:commons-codec:commons-codec)",
# This one too
"org.springframework:spring-web:5.3.20(x:log4j:log4j)",
# This gets global exclusions only
"org.slf4j:slf4j-api:1.7.32"
]
# These apply to ALL coordinates
exclusions = [
"commons-logging:commons-logging"
]
[entrypoints]
default = "com.example.WebApp"
Effective exclusions:
- httpclient: excludes commons-logging (global) + commons-codec (per-coord)
- spring-web: excludes commons-logging (global) + log4j (per-coord)
- slf4j-api: excludes commons-logging (global only)
3. jgo exclude Command
Manage exclusions in jgo.toml files via CLI.
Command Syntax
# Add global exclusion
jgo exclude add <groupId>:<artifactId>
# Remove global exclusion
jgo exclude remove <groupId>:<artifactId>
# List all exclusions
jgo exclude list
# Add exclusion to specific coordinate (optional feature)
jgo exclude add --coordinate <coordinate> <exclusion>
Options
--file FILE or -f FILE: Path to jgo.toml (default: ./jgo.toml)
--coordinate <coordinate>: Coordinate to modify with the exclusion
Examples
Add global exclusion:
jgo exclude add commons-logging:commons-logging
# Adds to [dependencies] exclusions array in jgo.toml
Remove global exclusion:
jgo exclude remove commons-logging:commons-logging
# Removes from [dependencies] exclusions array
List all exclusions:
jgo exclude list
# Output:
# Global exclusions:
# - commons-logging:commons-logging
# - log4j:log4j
#
# Per-coordinate exclusions:
# org.apache:httpclient:4.5.14
# - commons-codec:commons-codec
# org.springframework:spring-web:5.3.20
# - log4j:log4j
Add per-coordinate exclusion (optional):
jgo exclude add --coordinate httpclient:4.5.14 commons-logging:commons-logging
# Modifies the coordinate string in jgo.toml to add x:commons-logging:commons-logging modifier
Behavior
Adding exclusions:
- Validate exclusion coordinate format (groupId:artifactId)
- Load existing jgo.toml
- Check if exclusion already exists (idempotent)
- Add to
[dependencies] exclusions array
- Write updated jgo.toml (preserve formatting where possible)
- Print confirmation message
Removing exclusions:
- Validate exclusion coordinate format
- Load existing jgo.toml
- Remove from exclusions array (no error if not present)
- Write updated jgo.toml
- Print confirmation message
Listing exclusions:
- Load jgo.toml
- Parse global exclusions from
[dependencies] exclusions
- Parse per-coordinate exclusions from coordinate strings
- Display grouped by type (global vs per-coordinate)
Error handling:
- No jgo.toml exists: Create new file with minimal structure
- Invalid coordinate format: Print error and exit
- TOML syntax error: Print error and suggest manual fix
- File not writable: Print error with permission details
Implementation Notes
The jgo exclude command modifies jgo.toml programmatically using a TOML library (e.g., tomli/tomli-w).
For global exclusions:
- Simple array manipulation in
[dependencies] exclusions
For per-coordinate exclusions (optional feature):
- Parse existing coordinate string
- Add/remove exclusion from
(x:...) modifier
- Reconstruct coordinate string
- Replace in coordinates array
Preservation of comments:
- Best-effort preservation using TOML library features
- Document that some formatting may be lost during automated edits
Technical Implementation
1. Parsing
Extend coordinate parsing in src/jgo/parse/endpoint.py:
def parse_coordinate_modifiers(coordinate: str) -> tuple[str, dict]:
"""
Parse unified parenthetical modifiers from a coordinate string.
Returns: (coordinate_without_modifiers, modifiers_dict)
Example:
>>> parse_coordinate_modifiers("httpclient:4.5.14(c,x:commons-logging:commons-logging,x:log4j:log4j)!")
("httpclient:4.5.14", {
'placement': 'class-path',
'exclusions': [
Project(groupId='commons-logging', artifactId='commons-logging'),
Project(groupId='log4j', artifactId='log4j')
],
'is_exclusion': False,
'raw': True
})
"""
modifiers = {
'placement': None, # 'class-path' | 'module-path' | None
'exclusions': [], # list[Project]
'is_exclusion': False, # bool (this coord IS an exclusion)
'raw': False # bool (disable dependency management)
}
# 1. Check for raw flag (always at end)
if coordinate.endswith('!'):
modifiers['raw'] = True
coordinate = coordinate[:-1]
# 2. Extract single parenthetical (if present)
if '(' in coordinate:
match = re.search(r'\(([^)]+)\)$', coordinate)
if match:
modifier_str = match.group(1)
coordinate = coordinate[:match.start()]
# 3. Parse comma-separated modifiers
for modifier in modifier_str.split(','):
modifier = modifier.strip()
# Placement modifiers
if modifier in ('c', 'cp'):
modifiers['placement'] = 'class-path'
elif modifier in ('m', 'mp', 'p'):
modifiers['placement'] = 'module-path'
# Exclusion marker (this coord IS an exclusion)
elif modifier == 'x':
modifiers['is_exclusion'] = True
# Exclusion (this coord HAS this exclusion)
elif modifier.startswith('x:'):
excl_str = modifier[2:] # Strip 'x:'
# Parse groupId:artifactId
if ':' in excl_str:
parts = excl_str.split(':', 1)
if len(parts) == 2:
groupId, artifactId = parts
modifiers['exclusions'].append(
Project(groupId=groupId, artifactId=artifactId)
)
return coordinate, modifiers
Parsing strategy:
- Strip
! raw flag (always last)
- Extract single
(...) parenthetical
- Simple comma-split to get modifiers (no special cases needed!)
- Parse each modifier: placement (
c, m, etc.), exclusion marker (x), or exclusion (x:g:a)
- Return cleaned coordinate + modifiers dict
Key simplification: No need for complex state machines or special splitting logic. Each x:groupId:artifactId is a complete, self-contained modifier.
2. Cache Key Integration ✅ IMPLEMENTED
Exclusions must affect the cache key since they modify the dependency graph.
Implemented design: Changed to use list[Dependency] which includes exclusions, classifier, and packaging.
def _cache_key(self, dependencies: list[Dependency]) -> str:
"""
Generate a stable hash for a set of dependencies.
Uses full artifact coordinates (G:A:V:C:P) plus exclusions to ensure:
- Different classifiers get different caches (e.g., natives-linux vs natives-windows)
- Different packaging types get different caches
- Different exclusions get different caches (different dependency trees)
Args:
dependencies: List of Dependency objects with full coordinate info and exclusions
Returns:
16-character hex hash string
"""
# Sort to ensure stable ordering
# Use resolved version to ensure RELEASE/LATEST resolve to consistent cache keys
coord_strings = []
for dep in sorted(
dependencies,
key=lambda d: (
d.artifact.groupId,
d.artifact.artifactId,
d.artifact.version, # Resolved version
d.artifact.classifier,
d.artifact.packaging,
),
):
# Include full artifact coordinates: G:A:V:C:P
coord_str = (
f"{dep.artifact.groupId}:"
f"{dep.artifact.artifactId}:"
f"{dep.artifact.version}:"
f"{dep.artifact.classifier}:"
f"{dep.artifact.packaging}"
)
# Include exclusions for this dependency
if dep.exclusions:
excl_strs = sorted(
[f"{e.groupId}:{e.artifactId}" for e in dep.exclusions]
)
coord_str += f":excl={','.join(excl_strs)}"
coord_strings.append(coord_str)
combined = "+".join(coord_strings)
# Include optional_depth in cache key
# This ensures different optional_depth values get separate cache directories
combined += f":optional_depth={self.optional_depth}"
return hashlib.sha256(combined.encode()).hexdigest()[:16]
Helper methods for conversion:
def _coordinates_to_dependencies(
self, coordinates: list[Coordinate]
) -> list[Dependency]:
"""
Convert Coordinate objects to Dependency objects for cache key generation.
Preserves classifier, packaging, and exclusion information.
"""
dependencies = []
for coord in coordinates:
version = coord.version or "RELEASE"
component = self.context.project(coord.groupId, coord.artifactId).at_version(version)
# Create Artifact with classifier and packaging (G:A:V:C:P)
classifier = coord.classifier or ""
packaging = coord.packaging or "jar"
artifact = component.artifact(classifier=classifier, packaging=packaging)
# Create Dependency with scope and exclusions
# TODO: Parse exclusions from coord when exclusion syntax is implemented
scope = coord.scope or "compile"
optional = coord.optional or False
exclusions = [] # Will be populated when exclusion parsing is added
dependency = Dependency(
artifact=artifact,
scope=scope,
optional=optional,
exclusions=exclusions,
)
dependencies.append(dependency)
return dependencies
def _components_to_dependencies(
self, components: list[Component]
) -> list[Dependency]:
"""
Convert Component objects to Dependency objects with default classifier/packaging.
Backward compatibility helper for code that still uses Components.
"""
dependencies = []
for component in components:
artifact = component.artifact(classifier="", packaging="jar")
dependency = Dependency(
artifact=artifact,
scope="compile",
optional=False,
exclusions=[],
)
dependencies.append(dependency)
return dependencies
Key improvements:
- ✅ Exclusions baked into Dependency objects (no separate parameters needed)
- ✅ Classifier and packaging included (fixes LWJGL natives bug)
- ✅ Single source of truth for cache key components
- ✅ Ready for exclusion feature (just need to populate
exclusions field)
3. Dependency Resolution Integration
Pass exclusions to Maven resolver when creating synthetic POM:
def create_pom_with_exclusions(
components: list[Component],
boms: list[Component] | None,
global_exclusions: list[Project] | None = None,
per_coord_exclusions: dict[Component, list[Project]] | None = None
) -> POM:
"""
Create a synthetic wrapper POM with dependency exclusions.
Args:
components: Components to include as dependencies
boms: BOM imports for dependencyManagement
global_exclusions: Exclusions applied to all dependencies
per_coord_exclusions: Per-component exclusions
Returns:
POM object representing the synthetic wrapper
"""
dependencies = []
for comp in components:
artifact = comp.artifact()
# Merge global and per-coordinate exclusions
exclusions = (global_exclusions or []).copy()
if per_coord_exclusions and comp in per_coord_exclusions:
exclusions.extend(per_coord_exclusions[comp])
# Remove duplicates while preserving order
seen = set()
unique_exclusions = []
for excl in exclusions:
key = (excl.groupId, excl.artifactId)
if key not in seen:
seen.add(key)
unique_exclusions.append(excl)
dep = Dependency(
artifact=artifact,
scope="compile",
exclusions=tuple(unique_exclusions)
)
dependencies.append(dep)
# Create wrapper POM with dependencies
return create_wrapper_pom(dependencies, boms)
4. EnvironmentBuilder Updates
Extend EnvironmentBuilder to handle exclusions:
class EnvironmentBuilder:
def __init__(
self,
context: MavenContext,
cache_dir: Path | None = None,
link_strategy: LinkStrategy = LinkStrategy.AUTO,
optional_depth: int = 0,
global_exclusions: list[Project] | None = None,
per_coord_exclusions: dict[Component, list[Project]] | None = None,
):
self.context = context
self.link_strategy = link_strategy
self.optional_depth = optional_depth
self.global_exclusions = global_exclusions or []
self.per_coord_exclusions = per_coord_exclusions or {}
# ... rest of initialization
Complete Examples
Example 1: Exclude Commons Logging Globally
Problem: Multiple dependencies pull in commons-logging, but you want to use slf4j.
Solution:
# Using inline syntax
jgo httpclient+spring-web+commons-logging:commons-logging(x)
Example 2: Per-Coordinate Exclusions
Problem: Want different exclusions for different coordinates.
Solution:
# Exclude commons-logging from httpclient, log4j from spring-web
jgo httpclient(x:commons-logging:commons-logging)+spring-web(x:log4j:log4j)
Example 3: Project with jgo.toml
Setup:
# jgo.toml
[environment]
name = "web-app"
[dependencies]
coordinates = [
"org.springframework:spring-web:5.3.20(x:log4j:log4j)",
"org.apache:httpclient:4.5.14",
]
# Global exclusion for commons-logging
exclusions = [
"commons-logging:commons-logging"
]
[entrypoints]
default = "com.example.WebApp"
Usage:
# Initialize environment
jgo sync
# Run default entrypoint
jgo run
# Add another global exclusion
jgo exclude add org.slf4j:slf4j-log4j12
# List all exclusions
jgo exclude list
Example 4: Shortcuts with Exclusions
Setup:
# ~/.config/jgo.conf
[shortcuts]
httpclient-clean = httpclient:4.5.14(x:commons-logging:commons-logging,x:commons-codec:commons-codec)
spring-minimal = spring-web:5.3.20(x:commons-logging:commons-logging,x:log4j:log4j)
Usage:
jgo httpclient-clean@org.example.HttpDemo
jgo spring-minimal+slf4j-simple@org.example.WebDemo
Example 5: Complex Multi-Coordinate Setup
Scenario: Spring web app with LWJGL on module-path, excluding problematic logging libraries.
jgo 'spring-web:5.3.20(c,x:commons-logging:commons-logging,x:log4j:log4j)+lwjgl:3.3.1(m)+commons-logging:commons-logging(x)@WebApp'
Breakdown:
spring-web:5.3.20(c,x:commons-logging:commons-logging,x:log4j:log4j) - Spring on classpath, excludes its commons-logging and log4j
lwjgl:3.3.1(m) - LWJGL on module-path
commons-logging:commons-logging(x) - Global exclusion marker for commons-logging
@WebApp - Main class
Benefits
- Resolves dependency conflicts: Exclude problematic transitive dependencies
- Consistent with Maven: Uses same exclusion semantics as Maven POMs
- Cache-aware: Different exclusions create separate cached environments
- Composable: Works seamlessly with existing jgo features (+, @, !, placement modifiers)
- Flexible: Supports both global and per-coordinate exclusions
- Shortcut-compatible: Exclusions can be embedded in shortcuts
- Simple: One primary syntax (inline), with convenience command for jgo.toml
Open Questions
1. Validation
Question: Should we validate that excluded dependencies actually exist in the resolved tree?
Decision: No validation (following Maven's approach).
Rationale:
- Maven doesn't validate exclusions
- Allows "defensive" exclusions that may not always be present
- Simpler implementation
- Avoids coupling exclusion spec to resolution results
2. Wildcard Exclusion Scope
Question: Should wildcards in per-coordinate exclusions (coord(x:org.slf4j:*)) apply transitively?
Decision: Yes, follow Maven semantics (wildcards apply transitively).
Example:
A depends on B depends on slf4j-simple
A(x:org.slf4j:*) excludes slf4j-simple even though it's transitive
3. jgo exclude --coordinate Feature
Question: Should jgo exclude support per-coordinate exclusions or only global?
Options:
- Option A: Only global exclusions (simpler)
- Option B: Support per-coordinate via
--coordinate flag (more features)
Recommendation: Start with Option A (global only). Users can manually edit jgo.toml for per-coordinate exclusions. Add Option B later if demand exists.
Implementation Checklist
Phase 1: Core Parsing and Syntax
Phase 2: Exclusion Integration
Phase 3: jgo.toml Support
Phase 4: jgo exclude Command
Phase 5: Testing
Phase 6: Documentation
Alternatives Considered
CLI Flags Only
Proposal: Use only --exclude flags without inline syntax.
jgo --exclude commons-logging:commons-logging httpclient
Rejected because:
- Cannot include exclusions in shortcuts (deal-breaker)
- Less flexible for per-coordinate exclusions
- Doesn't affect cache key naturally (needs special handling)
- Verbose when same exclusions needed repeatedly
Special Character Syntax
Proposal: Use symbols like ~, ^, or / instead of (x).
jgo httpclient~commons-logging:commons-logging # tilde
jgo httpclient^commons-logging:commons-logging # caret
jgo httpclient/commons-logging:commons-logging # slash
Rejected because:
~ and ^ have version semantics in poetry
- Less self-documenting than
(x)
- Harder to distinguish from coordinate body
- Shell escaping issues
- Inconsistent with existing
(...) placement modifiers
Multiple Separate Parentheticals
Proposal: Allow multiple parentheticals instead of unified syntax.
jgo httpclient(c)(x:excl1)(x:excl2)!
Rejected because:
- Order ambiguity (does
(c) come before or after (x:...)?)
- More verbose
- Harder to parse (multiple passes)
- Less readable
Configuration File Only
Proposal: Require jgo.toml for all exclusions.
Rejected because:
- Too heavyweight for quick one-off runs
- No support for shortcuts with exclusions
- Inconsistent with other jgo features
- Doesn't solve ad-hoc use cases
Summary
This design provides a comprehensive, coherent exclusion system for jgo:
- One primary syntax: Inline
(x) and (x:...) modifiers
- Three access methods: Direct inline, jgo.toml, jgo exclude command
- Two exclusion scopes: Global (apply everywhere) and per-coordinate (apply to one dependency)
- Cache-aware: Exclusions properly affect cache keys
- Maven-compatible: Semantics match Maven's
<exclusions> blocks
The system is simple, composable, and consistent with jgo's existing design principles.
This proposal specifies a dependency exclusion approach for jgo, allowing users to exclude specific transitive dependencies from the classpath when running Java applications.
Motivation
Maven supports
<exclusions>blocks within<dependency>elements to exclude transitive dependencies. Users need equivalent functionality in jgo without creating custom pom.xml files.Common use cases:
Design Summary
Three Ways to Specify Exclusions
Endpoint syntax (inline): Exclusions specified directly in coordinate strings
coord(x)- Mark this coordinate AS a global exclusioncoord(x:excl1,x:excl2)- This coordinate HAS per-dependency exclusionsjgo.toml (project mode): Exclusions in project configuration files
exclusions = ["g:a", ...]jgo exclude (management): CLI command to add/remove exclusions from jgo.toml
jgo exclude add g:ajgo exclude remove g:ajgo exclude listDesign Rationale
Why inline syntax?
Why no
--excludeCLI flag?+coord(x)achieves same resultWhy
jgo excludecommand?jgo addandjgo removefor coordinates themselvesDetailed Specification
1. Endpoint Syntax
All coordinate modifiers use a unified parenthetical with comma-separated values:
Grammar
Key simplification: Each exclusion is a separate modifier with its own
x:prefix. This eliminates comma ambiguity - commas only separate modifiers, never parts within a modifier.Two Meanings of
(x)1. Exclusion Marker:
coord(x)- this coordinate IS a global exclusion# Include httpclient, but globally exclude commons-logging and log4j jgo httpclient+commons-logging:commons-logging(x)+log4j:log4j(x)The marked coordinates are excluded from ALL dependencies in the dependency graph.
2. Exclusion List:
coord(x:excl1,x:excl2)- this coordinate HAS per-dependency exclusions# Include httpclient, but exclude commons-logging only from httpclient's dependency tree jgo httpclient(x:commons-logging:commons-logging)The exclusions apply only to the specified coordinate's transitive dependencies.
Multiple Exclusions
Specify multiple exclusions by repeating the
x:modifier:# Exclude multiple dependencies from httpclient jgo httpclient(x:commons-logging:commons-logging,x:commons-codec:commons-codec)Wildcard Support
Maven-style wildcards (
*) are supported for groupId or artifactId:Combining Modifiers
Multiple modifiers can coexist in one parenthetical (order independent):
Key properties:
(c,x:...)equals(x:...,c)!always last - logical termination symbolExamples
Basic global exclusion:
Per-coordinate exclusion:
Multiple per-coordinate exclusions:
Combined with placement modifiers:
Complex scenario with main class:
jgo 'httpclient(x:commons-logging:commons-logging)+lwjgl:3.3.1(m)@MyApp'Shortcuts with Exclusions
Shortcuts can include exclusions since they're part of the coordinate syntax:
Usage:
2. jgo.toml Integration
Global Exclusions
Global exclusions apply to all dependencies in the environment:
Per-Coordinate Exclusions
Use inline endpoint syntax directly in coordinate strings:
Rationale for inline syntax:
Complete Example
Effective exclusions:
3. jgo exclude Command
Manage exclusions in jgo.toml files via CLI.
Command Syntax
Options
--file FILEor-f FILE: Path to jgo.toml (default: ./jgo.toml)--coordinate <coordinate>: Coordinate to modify with the exclusionExamples
Add global exclusion:
jgo exclude add commons-logging:commons-logging # Adds to [dependencies] exclusions array in jgo.tomlRemove global exclusion:
jgo exclude remove commons-logging:commons-logging # Removes from [dependencies] exclusions arrayList all exclusions:
Add per-coordinate exclusion (optional):
jgo exclude add --coordinate httpclient:4.5.14 commons-logging:commons-logging # Modifies the coordinate string in jgo.toml to add x:commons-logging:commons-logging modifierBehavior
Adding exclusions:
[dependencies] exclusionsarrayRemoving exclusions:
Listing exclusions:
[dependencies] exclusionsError handling:
Implementation Notes
The
jgo excludecommand modifies jgo.toml programmatically using a TOML library (e.g.,tomli/tomli-w).For global exclusions:
[dependencies] exclusionsFor per-coordinate exclusions (optional feature):
(x:...)modifierPreservation of comments:
Technical Implementation
1. Parsing
Extend coordinate parsing in
src/jgo/parse/endpoint.py:Parsing strategy:
!raw flag (always last)(...)parentheticalc,m, etc.), exclusion marker (x), or exclusion (x:g:a)Key simplification: No need for complex state machines or special splitting logic. Each
x:groupId:artifactIdis a complete, self-contained modifier.2. Cache Key Integration ✅ IMPLEMENTED
Exclusions must affect the cache key since they modify the dependency graph.
Implemented design: Changed to use
list[Dependency]which includes exclusions, classifier, and packaging.Helper methods for conversion:
Key improvements:
exclusionsfield)3. Dependency Resolution Integration
Pass exclusions to Maven resolver when creating synthetic POM:
4. EnvironmentBuilder Updates
Extend
EnvironmentBuilderto handle exclusions:Complete Examples
Example 1: Exclude Commons Logging Globally
Problem: Multiple dependencies pull in commons-logging, but you want to use slf4j.
Solution:
# Using inline syntax jgo httpclient+spring-web+commons-logging:commons-logging(x)Example 2: Per-Coordinate Exclusions
Problem: Want different exclusions for different coordinates.
Solution:
# Exclude commons-logging from httpclient, log4j from spring-web jgo httpclient(x:commons-logging:commons-logging)+spring-web(x:log4j:log4j)Example 3: Project with jgo.toml
Setup:
Usage:
Example 4: Shortcuts with Exclusions
Setup:
Usage:
Example 5: Complex Multi-Coordinate Setup
Scenario: Spring web app with LWJGL on module-path, excluding problematic logging libraries.
jgo 'spring-web:5.3.20(c,x:commons-logging:commons-logging,x:log4j:log4j)+lwjgl:3.3.1(m)+commons-logging:commons-logging(x)@WebApp'Breakdown:
spring-web:5.3.20(c,x:commons-logging:commons-logging,x:log4j:log4j)- Spring on classpath, excludes its commons-logging and log4jlwjgl:3.3.1(m)- LWJGL on module-pathcommons-logging:commons-logging(x)- Global exclusion marker for commons-logging@WebApp- Main classBenefits
Open Questions
1. Validation
Question: Should we validate that excluded dependencies actually exist in the resolved tree?
Decision: No validation (following Maven's approach).
Rationale:
2. Wildcard Exclusion Scope
Question: Should wildcards in per-coordinate exclusions (
coord(x:org.slf4j:*)) apply transitively?Decision: Yes, follow Maven semantics (wildcards apply transitively).
Example:
3. jgo exclude --coordinate Feature
Question: Should
jgo excludesupport per-coordinate exclusions or only global?Options:
--coordinateflag (more features)Recommendation: Start with Option A (global only). Users can manually edit jgo.toml for per-coordinate exclusions. Add Option B later if demand exists.
Implementation Checklist
Phase 1: Core Parsing and Syntax
parse_coordinate_modifiers()with exclusion support*for groupId/artifactId)!raw flag processed after parentheticalx:modifiers work correctlyPhase 2: Exclusion Integration
_cache_key()to include exclusions (uses Dependency objects)_cache_key()to include classifier and packaging (fixes LWJGL bug)_coordinates_to_dependencies()helper method_components_to_dependencies()compatibility helperfrom_components()to accept optional coordinates parameterfrom_spec()to preserve coordinates for cache keyEnvironmentBuilderto accept global and per-coord exclusionscreate_pom()to propagate exclusions to dependencies<exclusions>blocksPhase 3: jgo.toml Support
exclusionsfield to TOML schema[dependencies] exclusionsjgo syncto handle exclusionsPhase 4: jgo exclude Command
jgo exclude addsubcommandjgo exclude removesubcommandjgo exclude listsubcommand--fileoption for custom jgo.toml pathsPhase 5: Testing
(x)marker (global exclusion)x:modifiers:(x:excl1,x:excl2)(c,x:...,x:...)jgo excludecommand operationsPhase 6: Documentation
jgo excludecommandAlternatives Considered
CLI Flags Only
Proposal: Use only
--excludeflags without inline syntax.Rejected because:
Special Character Syntax
Proposal: Use symbols like
~,^, or/instead of(x).Rejected because:
~and^have version semantics in poetry(x)(...)placement modifiersMultiple Separate Parentheticals
Proposal: Allow multiple parentheticals instead of unified syntax.
jgo httpclient(c)(x:excl1)(x:excl2)!Rejected because:
(c)come before or after(x:...)?)Configuration File Only
Proposal: Require jgo.toml for all exclusions.
Rejected because:
Summary
This design provides a comprehensive, coherent exclusion system for jgo:
(x)and(x:...)modifiers<exclusions>blocksThe system is simple, composable, and consistent with jgo's existing design principles.