Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed
- When `dotnet restore` fails in one .NET project, the projects it may have skipped as up to date are no longer reported as not restored.
- Maven Socket facts now include every dependency edge Maven resolves, as Gradle and sbt facts do, so dependency chains show each path to a package. A dependency Maven puts on the runtime classpath is no longer marked as a dev dependency because Maven kept it under a test or provided dependency.
- With `--ignore-unresolved`, a Maven module with unresolvable version conflicts no longer adds a partial dependency list.

## [1.14.1](https://github.com/SocketDev/socket-cli/releases/tag/v1.14.1) - 2026-11-06

Expand Down

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/commands/manifest/scripts/test/maven-compat/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ graph-failure/target/
descriptor-failure/records.tsv
descriptor-failure/target/
descriptor-failure/*/target/
unsolvable-conflict/records.tsv
unsolvable-conflict/target/
unsolvable-conflict/localrepo/
57 changes: 48 additions & 9 deletions src/commands/manifest/scripts/test/maven-compat/assert-records.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
import sys
rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
tool = None
roots, nodes, files, direct = {}, {}, {}, {}
roots, root_projects, nodes, files, direct, edges = {}, {}, {}, {}, {}, {}
for r in rows:
if r[0] == 'meta': tool = r[1]
elif r[0] == 'root': roots[r[1]] = (r[4] == '1') # rootId -> prod
elif r[0] == 'root':
# rootId to prod and to projectKey.
roots[r[1]] = (r[4] == '1')
root_projects[r[1]] = r[2]
elif r[0] == 'node':
nodes.setdefault(r[2], set()).add(r[1]) # coordId -> {rootId}
if r[8] == '1': direct.setdefault(r[2], set()).add(r[1]) # coordId -> {rootId where direct}
elif r[0] == 'file': files.setdefault(r[2], set()).add(r[3]) # coordId -> {path}
elif r[0] == 'edge':
# (parent, child) to the roots that hold the edge.
edges.setdefault((r[2], r[3]), set()).add(r[1])

errors = []

Expand Down Expand Up @@ -61,15 +67,48 @@ def has_jar(cid): return any(p.endswith('.jar') for p in files.get(cid, ()))
elif not in_prod(lib): errors.append("internal module demo:lib not in app's prod root")
elif not direct.get(lib): errors.append("internal module demo:lib not marked direct")

# Scope conflict: Maven keeps demo.ext:shared under the test parent while its effective
# scope is compile, so it must land in the dev root. Reachability (checked by
# ../assert-reachability.py) additionally proves it kept its parent edge.
shared = coord('demo.ext:shared:jar:')
if not shared: errors.append("missing scope-conflict dep demo.ext:shared")
elif in_prod(shared): errors.append("scope-conflict demo.ext:shared wrongly in a prod root")
def edge_roots(parent, child):
return edges.get((parent, child), set())

def edge_pair(parent, child):
"""Both coordinates, or None after reporting the missing ones."""
found = (coord(parent), coord(child))
missing = [p for p, c in zip((parent, child), found) if not c]
if missing:
errors.append(f"missing {', '.join(m.rstrip(':') for m in missing)}")
return None
return found

# Scope conflict: Maven keeps demo.ext:shared under the test parent but selects compile, the
# scope conflict-prod's path gives it, so it is on the runtime classpath. Its edge from
# conflict-prod, onto the copy Maven kept under the test parent, keeps it reachable in the prod root.
pair = edge_pair('demo.ext:conflict-prod:jar:', 'demo.ext:shared:jar:')
if pair:
if not in_prod(pair[1]): errors.append("scope-conflict demo.ext:shared not in a prod root")
elif not any(roots.get(rid) for rid in edge_roots(*pair)):
errors.append("edge demo.ext:conflict-prod -> demo.ext:shared missing from a prod root")

# An edge onto a dependency Maven kept elsewhere: dup-io's dup-util is the direct one.
pair = edge_pair('demo.ext:dup-io:jar:', 'demo.ext:dup-util:jar:')
if pair and not edge_roots(*pair): errors.append("edge demo.ext:dup-io -> demo.ext:dup-util missing")

# A mediated edge: med-io asks for another med-util than mid declares, and points at mid's.
# coord() fails if the med-util version med-io asked for is also emitted.
pair = edge_pair('demo.ext:med-io:jar:', 'demo.ext:med-util:jar:')
if pair and not any(root_projects.get(rid) == 'demo:mid:1.0' for rid in edge_roots(*pair)):
errors.append("edge demo.ext:med-io -> demo.ext:med-util missing from mid")

# An edge across the prod/dev split: container-lib (compile) onto container-api, which app
# declares provided. The edge is kept and container-api stays dev.
pair = edge_pair('demo.ext:container-lib:jar:', 'demo.ext:container-api:jar:')
if pair:
if in_prod(pair[1]): errors.append("provided demo.ext:container-api wrongly in a prod root")
elif not in_prod(pair[0]): errors.append("demo.ext:container-lib not in a prod root")
elif not edge_roots(*pair):
errors.append("edge demo.ext:container-lib -> demo.ext:container-api missing")

if errors:
print("FAIL:")
for e in errors: print(" -", e)
sys.exit(1)
print(f"PASS: tool=maven; {ext} prod+jar; harness/harness-core dev; internal demo:lib (bare id, direct); {shared} dev")
print(f"PASS: tool=maven; {ext} prod+jar; harness/harness-core dev; internal demo:lib (bare id, direct); demo.ext:shared prod; edges onto kept dependencies")
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@
<artifactId>app</artifactId>
<dependencies>
<!-- internal reactor modules -> inter-module edges, bare-id components. lib's jar comes in
through mid, deeper than lib's test-jar further down, so Maven keeps demo.ext:libdep (with
its compile scope) under the test-jar node and none under the jar node, which the walk
reaches first. -->
through mid, which excludes demo.ext:libdep, so only lib's test-jar further down brings
libdep, and the walk reaches the jar first. -->
<dependency>
<groupId>demo</groupId>
<artifactId>mid</artifactId>
Expand All @@ -31,10 +30,9 @@
<scope>test</scope>
</dependency>
<!-- Scope-conflict pair: both pull demo.ext:shared at the same depth. Because the test
dep is declared first, Maven keeps shared under the test parent but unions the scopes to
compile, so shared's effective scope is prod while its only retained parent is dev. A
scope-based prod/dev split would strand shared in the prod root with no parent edge (an
unreachable, non-direct component); the reachability split keeps it with its parent. -->
dep is declared first, Maven keeps shared under the test parent but selects compile, the
scope conflict-prod's path gives it, so shared is prod and reached through
conflict-prod's edge. -->
<dependency>
<groupId>demo.ext</groupId>
<artifactId>conflict-test</artifactId>
Expand All @@ -46,6 +44,29 @@
<artifactId>conflict-prod</artifactId>
<version>1.0</version>
</dependency>
<!-- dup-io depends on dup-util, which Maven keeps as this direct dependency. -->
<dependency>
<groupId>demo.ext</groupId>
<artifactId>dup-util</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>demo.ext</groupId>
<artifactId>dup-io</artifactId>
<version>1.0</version>
</dependency>
<!-- container-lib (compile) depends on container-api, which Maven keeps as provided. -->
<dependency>
<groupId>demo.ext</groupId>
<artifactId>container-api</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>demo.ext</groupId>
<artifactId>container-lib</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>demo</groupId>
<artifactId>lib</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@
<groupId>demo</groupId>
<artifactId>lib</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>demo.ext</groupId>
<artifactId>libdep</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- med-io asks for med-util 2.0; Maven mediates it to the 1.0 declared here. -->
<dependency>
<groupId>demo.ext</groupId>
<artifactId>med-util</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>demo.ext</groupId>
<artifactId>med-io</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Regression guard: when Maven's conflict resolution throws (hard version ranges no version
# satisfies), Maven resolves no graph for the module. It must be reported unscannable and none of its
# collected dependencies emitted or fetched, however their scopes were left mid-resolution.
#
# Usage: smoke-test-unsolvable-conflict.sh <path-to-mvn> <path-to-extension-jar>
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
MVN="${1:?usage: smoke-test-unsolvable-conflict.sh <mvn> <extension-jar>}"
JAR="${2:?usage: smoke-test-unsolvable-conflict.sh <mvn> <extension-jar>}"
PROJECT="$HERE/unsolvable-conflict"
RECORDS="$PROJECT/records.tsv"
STUBS="$PROJECT/localrepo"
# shellcheck source=SCRIPTDIR/../compat-cache.sh
. "$HERE/../compat-cache.sh"
M2="$SOCKET_COMPAT_CACHE/m2"

bash "$HERE/../make-stub-repo.sh" "$STUBS" \
'demo.unsolvable:left:1.0+demo.unsolvable:shared:[1.0]' \
'demo.unsolvable:right:1.0+demo.unsolvable:shared:[2.0]' \
'demo.unsolvable:harness:1.0+demo.unsolvable:harness-core:1.0' \
'demo.unsolvable:harness-core:1.0' \
'demo.unsolvable:shared:1.0' \
'demo.unsolvable:shared:2.0'
rm -rf "$M2/demo/unsolvable" "$RECORDS"

echo "+ $("$MVN" -v 2>/dev/null | head -1) (unsolvable conflict)"
( cd "$PROJECT" && "$MVN" --batch-mode -q \
"-Dmaven.ext.class.path=$JAR" \
-Dcoana.task=socket-facts \
-Dsocket.withFiles=true \
"-Dmaven.repo.local=$M2" \
"-Dstub.repo.url=file://$STUBS" \
"-Dsocket.recordsFile=$RECORDS" \
validate )

python3 - "$RECORDS" "$M2/demo/unsolvable" <<'PY'
import os, sys
rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
module = 'demo:unsolvable-conflict:1.0'
errors = []
if not any(r[0] == 'unscannable' and r[3] == module for r in rows):
errors.append(f"{module} must be reported unscannable; records={rows}")
nodes = sorted(r[2] for r in rows if r[0] == 'node')
if nodes:
errors.append(f"Maven resolved no graph, so nothing may be emitted; nodes={nodes}")
jars = sorted(f for _, _, fs in os.walk(sys.argv[2]) for f in fs if f.endswith('.jar'))
if jars:
errors.append(f"Maven resolved no graph, so nothing may be fetched; jars={jars}")
if errors:
print("FAIL:")
for e in errors:
print(" -", e)
sys.exit(1)
print(f"PASS (unsolvable conflict): {module} unscannable, nothing emitted or fetched")
PY
17 changes: 13 additions & 4 deletions src/commands/manifest/scripts/test/maven-compat/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
# its transitive demo.ext:harness-core (only in a non-prod root -> the assembler's dev flag);
# - emits the internal reactor module demo:lib by its bare groupId:artifactId:version id (so the
# inter-module edge lines up with its `project` record);
# - resolves a scope conflict so the retained node stays reachable from its parent root;
# - marks a node prod by the scope Maven kept it with, and keeps it reachable in the prod root;
# - records every resolved edge, pointing at the node Maven kept: onto a direct dependency, onto a
# mediated version, and from a prod node onto a provided one;
# - every component is reachable from a direct dependency of its own root (no orphaned components);
# - keeps the dependencies Maven leaves only under a sibling module's test-jar (app reaches
# demo.ext:libdep under lib's test-jar node, after lib's jar node through mid);
# - keeps a dependency only a sibling module's test-jar brings (mid excludes demo.ext:libdep from
# lib, and app reaches lib's jar through mid before lib's test-jar);
# - materializes resolved external jars under -Dsocket.withFiles;
# - scopes that materialization to -Dsocket.populateFilesFor (a newline-delimited GAV file).
#
Expand Down Expand Up @@ -39,7 +41,14 @@ bash "$HERE/../make-stub-repo.sh" "$STUBS" \
'demo.ext:conflict-test:1.0+demo.ext:shared:1.0' \
'demo.ext:conflict-prod:1.0+demo.ext:shared:1.0' \
'demo.ext:shared:1.0' \
'demo.ext:libdep:1.0'
'demo.ext:libdep:1.0' \
'demo.ext:dup-util:1.0' \
'demo.ext:dup-io:1.0+demo.ext:dup-util:1.0' \
'demo.ext:med-util:1.0' \
'demo.ext:med-util:2.0' \
'demo.ext:med-io:1.0+demo.ext:med-util:2.0' \
'demo.ext:container-api:1.0' \
'demo.ext:container-lib:1.0+demo.ext:container-api:1.0'
# Maven's own plugin closure stays cached between runs; the stubs never do, so every run has to
# resolve them from the repo just generated.
rm -rf "$M2/demo" "$RECORDS" "$PROJECT"/*/target "$PROJECT"/target
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>unsolvable-conflict</artifactId>
<version>1.0</version>
<properties>
<stub.repo.url>file://${maven.multiModuleProjectDirectory}/localrepo</stub.repo.url>
</properties>
<repositories>
<repository>
<id>socket-stubs</id>
<url>${stub.repo.url}</url>
<releases>
<enabled>true</enabled>
<checksumPolicy>ignore</checksumPolicy>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<!-- left and right pin demo.unsolvable:shared to [1.0] and [2.0], which no version satisfies. -->
<dependencies>
<dependency>
<groupId>demo.unsolvable</groupId>
<artifactId>left</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>demo.unsolvable</groupId>
<artifactId>right</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>demo.unsolvable</groupId>
<artifactId>harness</artifactId>
<version>1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
1 change: 1 addition & 0 deletions src/commands/manifest/scripts/test/run-compat.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ run_maven() {
bash "$HERE/maven-compat/smoke-test-duplicate-failure.sh" "$dir/bin/mvn" "$jar"
bash "$HERE/maven-compat/smoke-test-graph-failure.sh" "$dir/bin/mvn" "$jar"
bash "$HERE/maven-compat/smoke-test-descriptor-failure.sh" "$dir/bin/mvn" "$jar"
bash "$HERE/maven-compat/smoke-test-unsolvable-conflict.sh" "$dir/bin/mvn" "$jar"
done
}

Expand Down
Loading
Loading