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
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
* (same contract as the Gradle/SBT scripts; no JSON/hashing here). Per module: a test root with the
* whole resolution and a compile root with the nodes kept with a prod scope (compile/runtime/system),
* each with the resolved edges between its own nodes. A reactor module becomes a component only
* where another depends on it, by its bare {@code groupId:artifactId:version} id.
* where another depends on one of its artifacts, each of which names the module as its project.
*
* <p>Resolution goes through {@link ProjectDependenciesResolver} — the same component Maven's own
* lifecycle uses to build a project's classpath — so the graph, the scope and management semantics,
Expand Down Expand Up @@ -309,9 +309,8 @@ private void collectModule(
reportUnscannable(unscannable, moduleCoord, "dependencies resolved without the socket-facts graph transformer");
}
if (graph == null) return;
Set<String> visited = new HashSet<>();
for (DependencyNode child : edges.get(graph)) {
String id = visit(child, edges, passingScopes, reactorGavs, unresolved, opts, nodes, visited);
String id = visit(child, edges, passingScopes, reactorGavs, unresolved, opts, nodes);
if (id != null) directIds.add(id);
}
for (String id : unfetched) {
Expand Down Expand Up @@ -410,8 +409,7 @@ private String visit(
Set<String> reactorGavs,
Set<String> unresolved,
Options opts,
Map<String, Node> nodes,
Set<String> visited) {
Map<String, Node> nodes) {
Dependency dep = dn.getDependency();
Artifact artifact = dn.getArtifact();
if (dep == null || artifact == null) return null;
Expand All @@ -423,20 +421,17 @@ private String visit(
boolean internal = reactorGavs.contains(gav);
String type = artifact.getProperty(ARTIFACT_PROPERTY_TYPE, artifact.getExtension());
String classifier = artifact.getClassifier();
String version = artifact.getBaseVersion();
String key = externalId(artifact);
String id = internal ? SocketSupport.bareId(artifact.getGroupId(), artifact.getArtifactId(), version) : key;
String id = externalId(artifact);

// One walk per artifact: a shared subtree is already recorded. Keyed by coordinate, as a sibling's jar
// and test-jar share a node but can collect different children, e.g. when one declaration has exclusions.
if (!visited.add(key)) return id;
// One walk per artifact: a shared subtree is already recorded.
if (nodes.containsKey(id)) return id;

Node node = internal
? upsert(nodes, id, artifact.getGroupId(), artifact.getArtifactId(), "", "", version, gav)
: upsert(nodes, id, artifact.getGroupId(), artifact.getArtifactId(),
type == null ? "" : type, classifier == null ? "" : classifier, version, "");
// A sibling's jar and test-jar share a node, each kept with its own scope.
if (isProd(scope)) node.prod = true;
// A sibling module's artifacts, such as its jar and test-jar, name the module as their project; the
// assembler merges them into its component.
Node node = new Node(id, artifact.getGroupId(), artifact.getArtifactId(),
type == null ? "" : type, classifier == null ? "" : classifier, artifact.getBaseVersion(), internal ? gav : "");
node.prod = isProd(scope);
nodes.put(id, node);

// Maven wrote each accepted node's resolved file back onto the node; a reactor module reports its
// own dirs through its `project` record instead of a `file` record.
Expand All @@ -446,7 +441,7 @@ private String visit(
}

for (DependencyNode child : edges.get(dn)) {
String childId = visit(child, edges, passingScopes, reactorGavs, unresolved, opts, nodes, visited);
String childId = visit(child, edges, passingScopes, reactorGavs, unresolved, opts, nodes);
if (childId != null) node.children.add(childId);
}
return id;
Expand Down Expand Up @@ -476,17 +471,6 @@ private static boolean isProd(String scope) {
return scope.equals("compile") || scope.equals("runtime") || scope.equals("system");
}

private static Node upsert(
Map<String, Node> nodes, String id, String groupId, String artifactId, String type, String classifier, String version,
String project) {
Node node = nodes.get(id);
if (node == null) {
node = new Node(id, groupId, artifactId, type, classifier, version, project);
nodes.put(id, node);
}
return node;
}

// ---- emission ----

// Shaped like Gradle's runtime and test classpaths. An edge from a prod node onto a dev one, such as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@ public static String coordId(String groupId, String artifactId, String type, Str
return sb.toString();
}

/** Bare {@code groupId:artifactId:version} id used for reactor (internal) module components. */
public static String bareId(String groupId, String artifactId, String version) {
return coordId(groupId, artifactId, null, null, version);
}

/**
* Translate a config-name glob to a case-sensitive regex. Supports {@code *}, {@code ?}, and
* {@code [...]} character classes: enumerations ({@code [cC]}), ranges ({@code [a-z]}), and
Expand Down
42 changes: 23 additions & 19 deletions src/commands/manifest/scripts/test/maven-compat/assert-records.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
tool = None
roots, root_projects, nodes, files, direct, edges = {}, {}, {}, {}, {}, {}
roots, root_projects, nodes, projects, files, direct, edges = {}, {}, {}, {}, {}, {}, {}
for r in rows:
if r[0] == 'meta': tool = r[1]
elif r[0] == 'root':
Expand All @@ -16,6 +16,8 @@
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}
# coordId to the build projects its rows resolve it to.
projects.setdefault(r[2], set()).add(r[9] if len(r) > 9 else '')
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.
Expand All @@ -27,26 +29,18 @@
# orphaned component. What remains here is Maven-specific: which coordinate lands in which root,
# and jar materialization.

def coord(prefix, fields=None):
"""The one emitted coordId starting with prefix, or None. Keeps assertions version-agnostic.

fields pins the count of colon-separated fields. Without it a prefix that stops before the
type, as the bare-id lookup below does, would also match the typed form it is meant to rule
out: 'demo:lib:' is a prefix of both 'demo:lib:1.0' and 'demo:lib:jar:1.0'.
"""
hits = sorted(
c for c in set(nodes) | set(files)
if c.startswith(prefix) and (fields is None or c.count(':') + 1 == fields)
)
def coord(prefix):
"""The one emitted coordId starting with prefix, or None. Keeps assertions version-agnostic."""
hits = sorted(c for c in set(nodes) | set(files) if c.startswith(prefix))
if len(hits) > 1:
errors.append(f"expected one coordinate for {prefix!r}, got {hits}")
return hits[0] if hits else None

ext = coord('demo.ext:tool:jar:')
harness = coord('demo.ext:harness:jar:')
harness_core = coord('demo.ext:harness-core:jar:')
# Internal module: bare groupId:artifactId:version, no type field.
lib = coord('demo:lib:', fields=3)
# Sibling module demo:lib: app reaches its jar through mid and declares its test-jar.
lib_jar, lib_tests = coord('demo:lib:jar:'), coord('demo:lib:test-jar:')

if tool != 'maven': errors.append(f"meta tool {tool!r} != 'maven'")

Expand All @@ -63,13 +57,23 @@ def has_jar(cid): return any(p.endswith('.jar') for p in files.get(cid, ()))
if not harness_core: errors.append("missing transitive test dep demo.ext:harness-core")
elif in_prod(harness_core): errors.append("transitive test dep demo.ext:harness-core wrongly in a prod root")

if not lib: errors.append("internal module demo:lib not emitted by its bare id")
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")

def edge_roots(parent, child):
return edges.get((parent, child), set())

def direct_in(cid, project): return any(root_projects.get(rid) == project for rid in direct.get(cid, ()))

if 'demo:lib:1.0' in nodes: errors.append("sibling demo:lib emitted under its bare id")
elif not lib_jar or not lib_tests: errors.append("sibling demo:lib's jar or test-jar missing")
elif projects.get(lib_jar, set()) | projects.get(lib_tests, set()) != {'demo:lib:1.0'}:
errors.append("sibling demo:lib's artifacts do not name demo:lib:1.0 as their project")
elif not any(roots.get(rid) and root_projects.get(rid) == 'demo:app:1.0' for rid in nodes.get(lib_jar, ())):
errors.append("sibling demo:lib's jar not in app's prod root")
elif not any(root_projects.get(rid) == 'demo:app:1.0' for rid in edge_roots(coord('demo:mid:jar:'), lib_jar)):
errors.append("edge demo:mid -> demo:lib's jar missing from app")
elif in_prod(lib_tests): errors.append("sibling demo:lib's test-jar wrongly in a prod root")
elif direct_in(lib_jar, 'demo:app:1.0'): errors.append("sibling demo:lib's jar wrongly direct in app")
elif not direct_in(lib_tests, 'demo:app:1.0'): errors.append("sibling demo:lib's test-jar not direct in app")

def edge_pair(parent, child):
"""Both coordinates, or None after reporting the missing ones."""
found = (coord(parent), coord(child))
Expand Down Expand Up @@ -119,4 +123,4 @@ def edge_pair(parent, child):
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); demo.ext:shared prod; edges onto kept dependencies")
print(f"PASS: tool=maven; {ext} prod+jar; harness/harness-core dev; sibling demo:lib jar prod, test-jar direct; demo.ext:shared prod; edges onto kept dependencies")
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
</dependencies>
</dependencyManagement>
<dependencies>
<!-- internal reactor modules -> inter-module edges, bare-id components. lib's jar comes in
<!-- internal reactor modules -> inter-module edges onto their artifacts. lib's jar comes in
through mid, which excludes demo.ext:libdep, so only lib's test-jar further down brings
libdep, and the walk reaches the jar first. -->
libdep. -->
<dependency>
<groupId>demo</groupId>
<artifactId>mid</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,22 @@ echo "+ $("$MVN" -v 2>/dev/null | head -1) (repo inheritance)"
python3 - "$RECORDS" <<'PY'
import sys
rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
roots, nodes, files, edges, failures = {}, {}, {}, set(), []
roots, nodes, projects, files, edges, failures = {}, {}, {}, {}, set(), []
for r in rows:
if r[0] == 'root': roots[r[1]] = r[2] # rootId -> projectKey
elif r[0] == 'node': nodes.setdefault(r[2], set()).add(r[1]) # coordId -> {rootId}
elif r[0] == 'node':
# coordId to the roots that hold it.
nodes.setdefault(r[2], set()).add(r[1])
# coordId to the build project it resolves to, if any.
if len(r) > 9 and r[9]: projects[r[2]] = r[9]
elif r[0] == 'file': files.setdefault(r[2], set()).add(r[3]) # coordId -> {path}
elif r[0] == 'edge': edges.add((r[1], r[2], r[3])) # (rootId, parent, child)
elif r[0] == 'failure': failures.append(r[1:])

WIDGET = 'demo.scoped:widget:jar:1.0'
LIBA = 'demo:liba:1.0'
# liba's jar, which names liba as its project.
LIBA_JAR = 'demo:liba:jar:1.0'
LIBB = 'demo:libb:1.0'
errors = []

Expand All @@ -70,16 +76,19 @@ for project_key in (LIBA, LIBB):
errors.append(f"{WIDGET} missing from {project_key}'s graph")

# The point of the fixture: libb reaches widget only through the reactor module liba.
if not any(rid in roots_for(LIBB) and p == LIBA and c == WIDGET for rid, p, c in edges):
errors.append(f"no {LIBA} -> {WIDGET} edge in libb's root")
if not any(rid in roots_for(LIBB) and p == LIBA_JAR and c == WIDGET for rid, p, c in edges):
errors.append(f"no {LIBA_JAR} -> {WIDGET} edge in libb's root")

jars = [p for p in files.get(WIDGET, ()) if p.endswith('.jar')]
if not jars:
errors.append(f"{WIDGET} jar not materialized: {files.get(WIDGET)}")

# A reactor sibling reports its dirs through its `project` record, never a `file` record.
if files.get(LIBA):
errors.append(f"reactor module {LIBA} should have no file records, got {files[LIBA]}")
if projects.get(LIBA_JAR) != LIBA:
errors.append(f"{LIBA_JAR} does not name {LIBA} as its project")
liba_files = {p for c, ps in files.items() if c == LIBA_JAR or projects.get(c) == LIBA for p in ps}
if liba_files:
errors.append(f"reactor module {LIBA} should have no file records, got {sorted(liba_files)}")

if errors:
print("FAIL:")
Expand Down
19 changes: 12 additions & 7 deletions src/commands/manifest/scripts/test/maven-compat/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
# tested separately in `nx test utils`). Guards, across the supported Maven range, that the extension:
# - emits the external prod dep demo.ext:tool (in a prod root) and the test dep demo.ext:harness +
# 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);
# - emits a sibling module's jar and test-jar under their own coordinates, each naming the module
# as its project (the assembler merges them into the module's component);
# - marks a node prod by the scope Maven kept it with, also one whose scope dependencyManagement
# sets under a test parent;
# - 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;
# - its records assemble into facts with no orphaned components;
# - 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);
# - keeps a dependency only a sibling module's test-jar brings under that test-jar, not its jar
# (mid excludes demo.ext:libdep from lib);
# - materializes resolved external jars under -Dsocket.withFiles;
# - scopes that materialization to -Dsocket.populateFilesFor (a newline-delimited GAV file).
#
Expand Down Expand Up @@ -71,9 +71,14 @@ python3 - "$RECORDS" <<'PY'
import sys
rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
app = {r[1] for r in rows if r[0] == 'root' and r[2] == 'demo:app:1.0'}
if not any(r[0] == 'node' and r[1] in app and r[2].startswith('demo.ext:libdep:') for r in rows):
print("FAIL: demo.ext:libdep missing from app"); sys.exit(1)
print("PASS: demo.ext:libdep in app through lib's test-jar")
def libdep_edge(parent):
return any(r[0] == 'edge' and r[1] in app and r[2].startswith(parent)
and r[3].startswith('demo.ext:libdep:') for r in rows)
if not libdep_edge('demo:lib:test-jar:'):
print("FAIL: no demo:lib test-jar -> demo.ext:libdep edge in app"); sys.exit(1)
if libdep_edge('demo:lib:jar:'):
print("FAIL: demo:lib jar -> demo.ext:libdep edge in app, though mid excludes it"); sys.exit(1)
print("PASS: demo.ext:libdep in app through lib's test-jar only")
PY
assemble_records "$RECORDS"

Expand Down
Loading
Loading