Skip to content

Commit 2ce7374

Browse files
committed
fix(manifest): report the sbt jars that could not be fetched
Coursier silently skips a default jar it cannot find unless the POM declares jar packaging, and sbt 0.13's update report omits the artifacts Ivy failed to download, so both looked like POM-only modules. The plugin now tells them apart by the cached POM's packaging and by Ivy's own resolution report, reports them under their full coordinate and marks them unfetched.
1 parent 1aaff8b commit 2ce7374

7 files changed

Lines changed: 408 additions & 41 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1717
- A Gradle dependency whose variant is ambiguous in every configuration of its project now fails the run instead of quietly dropping out of the scan.
1818
- Gradle dependencies that fail dependency verification, or whose jars cannot be fetched, are reported instead of passed on as healthy.
1919
- Failed Gradle project dependencies are reported instead of silently dropping everything they pull in, and failures name the version that actually failed.
20+
- sbt dependencies whose jars coursier skips or Ivy fails to download are reported instead of passed off as POM-only modules, and POM-only, war and aar modules keep their real shape.
2021

2122
## [1.10.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.10.0) - 2026-10-22
2223

‎src/commands/manifest/scripts/socket-facts.plugin.scala‎

Lines changed: 204 additions & 28 deletions
Large diffs are not rendered by default.

‎src/commands/manifest/scripts/test/run-compat.sh‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ GRADLE_MATRIX=("1.12 8" "2.14.1 8" "3.3 8" "6.2.2 8" "8.10.2 17" "9.2.1 21")
2929
# 3.2.5 is the oldest, 4.x the newest. Resolution goes through Maven's own
3030
# ProjectDependenciesResolver, so both ends are load-bearing, not decorative.
3131
MAVEN_MATRIX=("3.2.5 8" "3.6.3 11" "3.8.8 17" "3.9.9 17" "4.0.0-rc-6 21")
32-
SBT_MATRIX=("0.13.18 8 2.10.7" "1.4.9 11 2.12.20" "1.6.2 17 2.12.20" "1.9.9 17 2.12.20")
32+
# The `ivy` row runs sbt 1.x with `useCoursier := false`, the other resolver path; failure suite only.
33+
SBT_MATRIX=("0.13.18 8 2.10.7" "1.4.9 11 2.12.20" "1.6.2 17 2.12.20" "1.9.9 17 2.12.20" "1.9.9 17 2.12.20 ivy")
3334

3435
# Select a JDK for the given Java major: use $JDK<major> if set, else current java.
3536
use_jdk() {
@@ -103,12 +104,14 @@ run_sbt() {
103104
for row in "${SBT_MATRIX[@]}"; do
104105
# shellcheck disable=SC2086
105106
set -- $row
106-
local ver="$1" java="$2" scala="$3"
107-
echo "== sbt $ver / scala $scala (wants JDK $java) =="
107+
local ver="$1" java="$2" scala="$3" resolver="${4:-}"
108+
echo "== sbt $ver / scala $scala${resolver:+ / $resolver} (wants JDK $java) =="
108109
use_jdk "$java"
109-
bash "$HERE/sbt-compat/smoke-test.sh" "$ver" "$scala"
110-
bash "$HERE/sbt-compat/smoke-test-workspaces.sh" "$ver" "$scala"
111-
bash "$HERE/sbt-compat/smoke-test-failure.sh" "$ver" "$scala"
110+
if [ -z "$resolver" ]; then
111+
bash "$HERE/sbt-compat/smoke-test.sh" "$ver" "$scala"
112+
bash "$HERE/sbt-compat/smoke-test-workspaces.sh" "$ver" "$scala"
113+
fi
114+
bash "$HERE/sbt-compat/smoke-test-failure.sh" "$ver" "$scala" ${resolver:+"$resolver"}
112115
done
113116
}
114117

‎src/commands/manifest/scripts/test/sbt-compat/.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ failure/target/
1212
failure/project/
1313
failure/scala-version.sbt
1414
failure/records.tsv
15+
artifact-failure/target/
16+
artifact-failure/*/target/
17+
artifact-failure/project/
18+
artifact-failure/scala-version.sbt
19+
artifact-failure/records.tsv
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Artifact-failure fixture for ../../../socket-facts.plugin.scala: every module resolves, but ok has no
2+
// tests-classifier jar, so the only failure is an artifact that names no module. app depends on core,
3+
// whose update sbt also runs for app. Reuses ../failure/localrepo, generated by ../smoke-test-failure.sh.
4+
organization in ThisBuild := "demo"
5+
version in ThisBuild := "0.1.0"
6+
7+
lazy val stubs = Seq(
8+
resolvers := Seq(
9+
"Socket Stubs" at s"file://${(baseDirectory in ThisBuild).value.getAbsolutePath}/../failure/localrepo"
10+
) ++ resolvers.value
11+
)
12+
13+
lazy val core = (project in file("core"))
14+
.settings(stubs)
15+
.settings(
16+
libraryDependencies += "demo.fail" % "ok" % "1.0",
17+
libraryDependencies += "demo.fail" % "ok" % "1.0" classifier "tests"
18+
)
19+
20+
lazy val app = (project in file("app")).settings(stubs).dependsOn(core)

‎src/commands/manifest/scripts/test/sbt-compat/failure/build.sbt‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Failure fixture for ../../../socket-facts.plugin.scala: ok resolves; lib and testlib name a parent POM
2-
// that does not exist; ghost does not exist at all. Generated into `localrepo` by
3-
// ../smoke-test-failure.sh. Uses the `in ThisBuild` form so it parses on sbt 0.13 and 1.x.
2+
// that does not exist; ghost does not exist at all; nojar has a POM but no jar; pomonly is
3+
// `<packaging>pom</packaging>`; pkwar is a war with no jar; ok has no tests-classifier jar. Generated
4+
// into `localrepo` by ../smoke-test-failure.sh. Uses the `in ThisBuild` form so it parses on sbt 0.13
5+
// and 1.x.
46
organization in ThisBuild := "demo"
57
version in ThisBuild := "0.1.0"
68

@@ -15,5 +17,9 @@ lazy val root = (project in file("."))
1517
libraryDependencies += "demo.fail" % "ok" % "1.0",
1618
libraryDependencies += "demo.fail" % "lib" % "1.0",
1719
libraryDependencies += "demo.fail" % "testlib" % "1.0" % Test,
18-
libraryDependencies += "demo.fail" % "ghost" % "1.0"
20+
libraryDependencies += "demo.fail" % "ghost" % "1.0",
21+
libraryDependencies += "demo.fail" % "nojar" % "1.0",
22+
libraryDependencies += "demo.fail" % "pomonly" % "1.0",
23+
libraryDependencies += "demo.fail" % "pkwar" % "1.0",
24+
libraryDependencies += "demo.fail" % "ok" % "1.0" % Test classifier "tests"
1925
)

‎src/commands/manifest/scripts/test/sbt-compat/smoke-test-failure.sh‎

Lines changed: 160 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,22 @@
55
# the declared dependencies whose parent POM is missing (lib, testlib), coursier (sbt 1.3+) names the
66
# missing parent POM itself; both name ghost. Each failure's detail is its own module's message.
77
#
8-
# Usage: smoke-test-failure.sh <sbt-version> <scala-version>
8+
# A second run with files: a jar that cannot be fetched (nojar; lib/testlib's under coursier; ok's
9+
# tests classifier, test-scoped) must be reported and marked `unfetched` under its full coordinate,
10+
# in the configurations that requested it, while pomonly stays an artifactless module and pkwar keeps
11+
# its war. Coursier skips a missing jar silently and lists one for every Maven module whatever its
12+
# packaging; Ivy's own resolution report names what it failed to download.
13+
#
14+
# Then ../artifact-failure, whose only failure is ok's missing tests jar, without and with files: an
15+
# artifact failure names no module, yet sbt's update fails on it, so it must be reported in both runs
16+
# and must not make core or its dependent app unscannable.
17+
#
18+
# Usage: smoke-test-failure.sh <sbt-version> <scala-version> [coursier|ivy] (ivy: sbt 1.x only)
919
set -euo pipefail
1020
HERE="$(cd "$(dirname "$0")" && pwd)"
1121
SBT_VERSION="${1:?usage: smoke-test-failure.sh <sbt-version> <scala-version>}"
1222
SCALA_VERSION="${2:?usage: smoke-test-failure.sh <sbt-version> <scala-version>}"
23+
RESOLVER="${3:-coursier}"
1324
PLUGIN="$HERE/../../socket-facts.plugin.scala"
1425
PROJECT="$HERE/failure"
1526
RECORDS="$PROJECT/records.tsv"
@@ -34,6 +45,34 @@ for lib in lib testlib; do
3445
</project>
3546
POM
3647
done
48+
mkdir -p "$REPO/demo/fail/nojar/1.0" "$REPO/demo/fail/pomonly/1.0" "$REPO/demo/fail/pkwar/1.0"
49+
cat > "$REPO/demo/fail/pkwar/1.0/pkwar-1.0.pom" <<'POM'
50+
<project xmlns="http://maven.apache.org/POM/4.0.0">
51+
<modelVersion>4.0.0</modelVersion>
52+
<groupId>demo.fail</groupId>
53+
<artifactId>pkwar</artifactId>
54+
<version>1.0</version>
55+
<packaging>war</packaging>
56+
</project>
57+
POM
58+
cp "$REPO/demo/fail/ok/1.0/ok-1.0.jar" "$REPO/demo/fail/pkwar/1.0/pkwar-1.0.war"
59+
cat > "$REPO/demo/fail/nojar/1.0/nojar-1.0.pom" <<'POM'
60+
<project xmlns="http://maven.apache.org/POM/4.0.0">
61+
<modelVersion>4.0.0</modelVersion>
62+
<groupId>demo.fail</groupId>
63+
<artifactId>nojar</artifactId>
64+
<version>1.0</version>
65+
</project>
66+
POM
67+
cat > "$REPO/demo/fail/pomonly/1.0/pomonly-1.0.pom" <<'POM'
68+
<project xmlns="http://maven.apache.org/POM/4.0.0">
69+
<modelVersion>4.0.0</modelVersion>
70+
<groupId>demo.fail</groupId>
71+
<artifactId>pomonly</artifactId>
72+
<version>1.0</version>
73+
<packaging>pom</packaging>
74+
</project>
75+
POM
3776
rm -rf "$IVY/cache/demo.fail"
3877

3978
GB="$(mktemp -d)/global-base"
@@ -43,12 +82,20 @@ cp "$PLUGIN" "$GB/plugins/SocketFactsPlugin.scala"
4382
mkdir -p "$PROJECT/project"
4483
echo "sbt.version=$SBT_VERSION" > "$PROJECT/project/build.properties"
4584
echo "scalaVersion in ThisBuild := \"$SCALA_VERSION\"" > "$PROJECT/scala-version.sbt"
85+
if [ "$RESOLVER" = ivy ]; then
86+
echo 'useCoursier in ThisBuild := false' >> "$PROJECT/scala-version.sbt"
87+
fi
4688
rm -rf "$RECORDS" "$PROJECT/target" "$PROJECT/project/target"
4789

90+
run() {
91+
local dir="$1"
92+
shift
93+
( cd "$dir" && sbt -Dsbt.global.base="$GB" -Dsbt.server.autostart=false \
94+
-Dsbt.ivy.home="$IVY" "$@" --batch socketFacts )
95+
}
96+
4897
echo "+ sbt $SBT_VERSION (scala $SCALA_VERSION) (failure)"
49-
( cd "$PROJECT" && sbt -Dsbt.global.base="$GB" -Dsbt.server.autostart=false \
50-
-Dsbt.ivy.home="$IVY" \
51-
-Dsocket.recordsFile="$RECORDS" --batch socketFacts )
98+
run "$PROJECT" -Dsocket.recordsFile="$RECORDS"
5299

53100
python3 - "$RECORDS" <<'PY'
54101
import sys
@@ -84,3 +131,112 @@ if errors:
84131
sys.exit(1)
85132
print(f"PASS (failure): reported {sorted(failed)}; ok still emitted")
86133
PY
134+
135+
rm -rf "$RECORDS" "$PROJECT/target" "$PROJECT/project/target"
136+
echo "+ sbt $SBT_VERSION (scala $SCALA_VERSION) (failure, with files)"
137+
run "$PROJECT" -Dsocket.withFiles=true -Dsocket.recordsFile="$RECORDS"
138+
139+
python3 - "$RECORDS" <<'PY'
140+
import sys
141+
rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
142+
nodes = {r[2]: r for r in rows if r[0] == 'node'}
143+
unfetched = {r[2] for r in rows if r[0] == 'unfetched'}
144+
failed = {r[1] for r in rows if r[0] == 'failure'}
145+
config_of = {r[1]: r[3] for r in rows if r[0] == 'root'}
146+
unfetched_configs = {(r[2], config_of[r[1]]) for r in rows if r[0] == 'unfetched'}
147+
files = {r[2] for r in rows if r[0] == 'file'}
148+
errors = []
149+
150+
if 'demo.fail:pomonly:1.0' not in nodes:
151+
errors.append(f"pomonly is pom-packaged, so it must be emitted without an artifact; nodes={sorted(nodes)}")
152+
if any('pomonly' in u for u in unfetched) or 'demo.fail:pomonly:1.0' in failed:
153+
errors.append(f"pomonly has no jar to fetch; unfetched={sorted(unfetched)} failures={sorted(failed)}")
154+
for name in ('nojar', 'ok'):
155+
if f'demo.fail:{name}:1.0' not in failed:
156+
errors.append(f"{name}'s jar cannot be fetched, so it must be reported; failures={sorted(failed)}")
157+
if 'demo.fail:nojar:jar:1.0' not in unfetched:
158+
errors.append(f"nojar must be emitted as an unfetched jar; unfetched={sorted(unfetched)}")
159+
if 'demo.fail:ok:jar:1.0' not in files:
160+
errors.append("ok's own jar resolved, so its missing tests jar must not cost it its file")
161+
tests_configs = {c for (u, c) in unfetched_configs if u == 'demo.fail:ok:jar:tests:1.0'}
162+
if not tests_configs or any('test' not in c.lower() for c in tests_configs):
163+
errors.append(f"ok's tests jar is requested only in Test, so only test configs may miss it: {sorted(tests_configs)}")
164+
for name in ('nojar', 'lib', 'testlib'):
165+
if f'demo.fail:{name}:1.0' in nodes:
166+
errors.append(f"{name} has a jar-packaged POM, so it must not look artifactless")
167+
if f'demo.fail:{name}:jar:1.0' in nodes and f'demo.fail:{name}:jar:1.0' not in unfetched:
168+
errors.append(f"{name} is emitted without its jar, so it must be marked unfetched; unfetched={sorted(unfetched)}")
169+
if 'demo.fail:pkwar:war:1.0' not in files:
170+
errors.append(f"pkwar's war resolved, so it must keep its file; nodes={sorted(n for n in nodes if 'pkwar' in n)}")
171+
if any('pkwar' in f for f in failed):
172+
errors.append(f"pkwar's packaging is war, so a missing jar is expected; failures={sorted(failed)}")
173+
details = [r[2] for r in rows if r[0] == 'failure' and r[1] == 'demo.fail:nojar:1.0']
174+
if len(set(details)) != 1 or 'not found' not in details[0]:
175+
errors.append(f"nojar's jar is absent, so it must be reported once as not found: {sorted(set(details))}")
176+
stray = unfetched - {f'demo.fail:{n}:jar:1.0' for n in ('nojar', 'lib', 'testlib')} - {'demo.fail:ok:jar:tests:1.0'}
177+
if stray:
178+
errors.append(f"only the jars that are missing may be unfetched: {sorted(stray)}")
179+
180+
if errors:
181+
print("FAIL:")
182+
for e in errors:
183+
print(" -", e)
184+
sys.exit(1)
185+
print(f"PASS (failure, with files): unfetched {sorted(unfetched)}; pomonly artifactless")
186+
PY
187+
188+
ARTIFACT="$HERE/artifact-failure"
189+
mkdir -p "$ARTIFACT/project"
190+
echo "sbt.version=$SBT_VERSION" > "$ARTIFACT/project/build.properties"
191+
cp "$PROJECT/scala-version.sbt" "$ARTIFACT/scala-version.sbt"
192+
rm -rf "$ARTIFACT/records.tsv" "$ARTIFACT/target" "$ARTIFACT"/*/target "$ARTIFACT/project/target"
193+
echo "+ sbt $SBT_VERSION (scala $SCALA_VERSION) (artifact failure)"
194+
run "$ARTIFACT" -Dsocket.recordsFile="$ARTIFACT/records.tsv"
195+
196+
python3 - "$ARTIFACT/records.tsv" <<'PY'
197+
import sys
198+
rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
199+
unscannable = [r[1:] for r in rows if r[0] == 'unscannable']
200+
failed = {r[1] for r in rows if r[0] == 'failure'}
201+
errors = []
202+
if unscannable:
203+
errors.append(f"a missing artifact names no module, but must not make a project unscannable: {unscannable}")
204+
if failed != {'demo.fail:ok:1.0'}:
205+
errors.append(f"sbt's update fails on ok's missing tests jar, so it must be reported without files too; failures={sorted(failed)}")
206+
if errors:
207+
print("FAIL:")
208+
for e in errors:
209+
print(" -", e)
210+
sys.exit(1)
211+
print("PASS (artifact failure): ok's missing tests jar reported without files")
212+
PY
213+
214+
rm -rf "$ARTIFACT/records.tsv"
215+
echo "+ sbt $SBT_VERSION (scala $SCALA_VERSION) (artifact failure, with files)"
216+
run "$ARTIFACT" -Dsocket.withFiles=true -Dsocket.recordsFile="$ARTIFACT/records.tsv"
217+
218+
python3 - "$ARTIFACT/records.tsv" <<'PY'
219+
import sys
220+
rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
221+
unscannable = [r[1:] for r in rows if r[0] == 'unscannable']
222+
project_of = {r[1]: r[2] for r in rows if r[0] == 'root'}
223+
unfetched_by = {}
224+
for r in rows:
225+
if r[0] == 'unfetched':
226+
unfetched_by.setdefault(project_of[r[1]], set()).add(r[2])
227+
failed = {r[1] for r in rows if r[0] == 'failure'}
228+
errors = []
229+
if unscannable:
230+
errors.append(f"a missing artifact names no module, but must not make a project unscannable: {unscannable}")
231+
for p in ('core', 'app'):
232+
if unfetched_by.get(p) != {'demo.fail:ok:jar:tests:1.0'}:
233+
errors.append(f"{p} reaches ok's missing tests jar, so only it may be unfetched: {sorted(unfetched_by.get(p, ()))}")
234+
if failed != {'demo.fail:ok:1.0'}:
235+
errors.append(f"only ok's tests jar is missing; failures={sorted(failed)}")
236+
if errors:
237+
print("FAIL:")
238+
for e in errors:
239+
print(" -", e)
240+
sys.exit(1)
241+
print("PASS (artifact failure, with files): ok's tests jar unfetched in core and app; nothing unscannable")
242+
PY

0 commit comments

Comments
 (0)