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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ test/fixtures/commands/fix/e2e-test-py-temp-*
/src/commands/manifest/scripts/test/sbt-compat/project/localrepo/
/src/commands/manifest/scripts/test/sbt-compat/project/records.tsv
/src/commands/manifest/scripts/test/sbt-compat/project/target/
/src/commands/manifest/scripts/test/sbt-compat/relocation/*/target/
/src/commands/manifest/scripts/test/sbt-compat/relocation/localrepo/
/src/commands/manifest/scripts/test/sbt-compat/relocation/project/
/src/commands/manifest/scripts/test/sbt-compat/relocation/records.tsv
/src/commands/manifest/scripts/test/sbt-compat/relocation/scala-version.sbt
/src/commands/manifest/scripts/test/sbt-compat/relocation/target/

# Generated when the dynamic SBOM inference tests walk their fixtures
# (test/fixtures/commands/manifest/dynamic-sbom-inference). Those fixtures are
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `--exclude-paths` patterns are now matched relative to the scan root in every build root. They now also exclude modules a build reaches outside its own directory (Maven `../` modules, relocated Gradle projects) and modules matched by wildcards that span a build-root boundary, such as `*/core`.
- `--exclude-paths` patterns written with Windows `\` separators now exclude JVM build modules in single-root runs too.
- Maven SBOMs keep the dependencies a module reaches through a sibling module's test-jar.
- sbt SBOMs now generate for builds with relocated Maven dependencies, such as `mysql:mysql-connector-java`.

## [1.12.2](https://github.com/SocketDev/socket-cli/releases/tag/v1.12.2) - 2026-10-30

Expand Down
242 changes: 204 additions & 38 deletions src/commands/manifest/scripts/socket-facts.plugin.scala

Large diffs are not rendered by default.

53 changes: 51 additions & 2 deletions src/commands/manifest/scripts/test/make-stub-repo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
# Usage: make-stub-repo.sh <repo-dir> <spec>...
# spec = group:artifact:version[+dep-group:dep-artifact:dep-version[,...]]
# Deps listed after `+` are written as compile-scope dependencies of that stub, so a consumer that
# depends on the stub also pulls them transitively.
# depends on the stub also pulls them transitively; `dep@scope` writes that scope instead.
# spec = group:artifact:version>new-group:new-artifact:new-version writes a POM that relocates the
# stub to the new coordinate, and no jar.
set -euo pipefail

REPO="${1:?usage: make-stub-repo.sh <repo-dir> <group:artifact:version[+dep,...]>...}"
Expand All @@ -28,6 +30,32 @@ EMPTY="$(mktemp -d)"
trap 'rm -rf "$EMPTY"' EXIT

for spec in "$@"; do
if [[ "$spec" == *">"* ]]; then
IFS=: read -r group art ver <<<"${spec%%>*}"
IFS=: read -r ngroup nart nver <<<"${spec#*>}"
if [ -z "${group:-}" ] || [ -z "${art:-}" ] || [ -z "${ver:-}" ] || [ -z "${ngroup:-}" ] || [ -z "${nart:-}" ] || [ -z "${nver:-}" ]; then
echo "make-stub-repo.sh: malformed relocation '$spec' (want group:artifact:version>group:artifact:version)" >&2
exit 1
fi
dir="$REPO/${group//.//}/$art/$ver"
mkdir -p "$dir"
cat >"$dir/$art-$ver.pom" <<POM
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>$group</groupId>
<artifactId>$art</artifactId>
<version>$ver</version>
<distributionManagement>
<relocation>
<groupId>$ngroup</groupId>
<artifactId>$nart</artifactId>
<version>$nver</version>
</relocation>
</distributionManagement>
</project>
POM
continue
fi
coord="${spec%%+*}"
deps=""
[ "$coord" = "$spec" ] || deps="${spec#*+}"
Expand All @@ -51,7 +79,9 @@ for spec in "$@"; do
echo ' <dependencies>'
IFS=, read -r -a dep_list <<<"$deps"
for dep in "${dep_list[@]}"; do
IFS=: read -r dgroup dart dver <<<"$dep"
dscope=""
[ "${dep%@*}" = "$dep" ] || dscope="${dep#*@}"
IFS=: read -r dgroup dart dver <<<"${dep%@*}"
if [ -z "${dgroup:-}" ] || [ -z "${dart:-}" ] || [ -z "${dver:-}" ]; then
echo "make-stub-repo.sh: malformed dependency '$dep' in spec '$spec'" >&2
exit 1
Expand All @@ -60,6 +90,7 @@ for spec in "$@"; do
echo " <groupId>$dgroup</groupId>"
echo " <artifactId>$dart</artifactId>"
echo " <version>$dver</version>"
[ -z "$dscope" ] || echo " <scope>$dscope</scope>"
echo ' </dependency>'
done
echo ' </dependencies>'
Expand All @@ -71,4 +102,22 @@ for spec in "$@"; do
( cd "$EMPTY" && jar cf "$dir/$art-$ver.jar" . )
done

# A version list per module, so a version range can resolve against the repo.
find "$REPO" -name '*.pom' | while read -r pom; do
dir="$(dirname "$(dirname "$pom")")"
art="$(basename "$dir")"
group="$(dirname "${dir#"$REPO"/}")"
{
echo '<metadata>'
echo " <groupId>${group//\//.}</groupId>"
echo " <artifactId>$art</artifactId>"
echo ' <versioning>'
echo ' <versions>'
for v in "$dir"/*/; do echo " <version>$(basename "$v")</version>"; done
echo ' </versions>'
echo ' </versioning>'
echo '</metadata>'
} >"$dir/maven-metadata.xml"
done

echo "built stub repo at $REPO"
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 @@ -115,6 +115,7 @@ run_sbt() {
bash "$HERE/sbt-compat/smoke-test-exclude-base.sh" "$ver" "$scala"
fi
bash "$HERE/sbt-compat/smoke-test-failure.sh" "$ver" "$scala" ${resolver:+"$resolver"}
bash "$HERE/sbt-compat/smoke-test-relocation.sh" "$ver" "$scala" ${resolver:+"$resolver"}
done
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Minimal cross-sbt smoke project for ../../socket-facts.plugin.scala. Resolves one prod dep and one
// test dep so the smoke test can assert the prod/dev split and --with-files materialization. Both are
// Minimal cross-sbt smoke project for ../../socket-facts.plugin.scala. Resolves prod deps (two through
// a Maven relocation) and one test dep so the smoke test can assert the prod/dev split, relocations
// and --with-files materialization. All are
// stub artifacts generated into `localrepo` at test time (../../make-stub-repo.sh): plain Java
// artifacts needing no Scala cross-version, never compiled against, so they can never age into a CVE
// alert or a version bump. Uses the `in ThisBuild` setting form (not the `/` slash form) so it parses
Expand All @@ -20,5 +21,7 @@ lazy val root = (project in file("."))
"Maven Central" at "https://repo1.maven.org/maven2/"
) ++ resolvers.value,
libraryDependencies += "demo.ext" % "tool" % "1.0",
// Relocated to demo.ext:moved; tool depends on demo.old:legacy, relocated to demo.ext:legacy.
libraryDependencies += "demo.old" % "moved" % "1.0",
libraryDependencies += "demo.ext" % "harness" % "1.0" % Test
)
21 changes: 21 additions & 0 deletions src/commands/manifest/scripts/test/sbt-compat/relocation/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Relocation fixture for ../../socket-facts.plugin.scala. core declares demo.old:moved, which
// relocates to demo.ext:moved, by a version range; app gets demo.ext:moved only through core, and
// overrides its version to one the relocation does not name. The
// demo.ext:wrapper POM declares demo.old:moved with the test scope, which it does not pass on, so it
// must not become demo.ext:moved's parent. Stub artifacts come from `localrepo`
// (../../make-stub-repo.sh). Uses the `in ThisBuild` setting form so it parses on sbt 0.13 and 1.x.
organization in ThisBuild := "demo"
version in ThisBuild := "0.1.0"

lazy val stubs = Seq(
resolvers := Seq(
"Socket Stubs" at s"file://${(baseDirectory in ThisBuild).value.getAbsolutePath}/localrepo",
"Maven Central" at "https://repo1.maven.org/maven2/"
) ++ resolvers.value
)

lazy val core = (project in file("core")).settings(stubs, libraryDependencies += "demo.old" % "moved" % "[1.0,2.0)")

lazy val app = (project in file("app"))
.dependsOn(core)
.settings(stubs, libraryDependencies += "demo.ext" % "wrapper" % "1.0", dependencyOverrides += "demo.ext" % "moved" % "1.1")
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Run socket-facts.plugin.scala on the relocation fixture and assert a Maven relocation's target keeps
# the parent that declared the relocating module: the project itself (direct) or a build project it
# depends on, never a library that declares it with a scope it does not pass on. Coursier reports the
# target with a caller it leaves out of the report, and gives that caller its declared revision, here
# a version range; an override can move the target off the version the relocation names.
#
# Usage: smoke-test-relocation.sh <sbt-version> <scala-version> [coursier|ivy] (ivy: sbt 1.x only)
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
SBT_VERSION="${1:?usage: smoke-test-relocation.sh <sbt-version> <scala-version>}"
SCALA_VERSION="${2:?usage: smoke-test-relocation.sh <sbt-version> <scala-version>}"
RESOLVER="${3:-coursier}"
PLUGIN="$HERE/../../socket-facts.plugin.scala"
PROJECT="$HERE/relocation"
RECORDS="$PROJECT/records.tsv"
# shellcheck source=SCRIPTDIR/../compat-cache.sh
. "$HERE/../compat-cache.sh"
IVY="$SOCKET_COMPAT_CACHE/ivy2"
export COURSIER_CACHE="$SOCKET_COMPAT_CACHE/coursier"

bash "$HERE/../make-stub-repo.sh" "$PROJECT/localrepo" \
'demo.old:moved:1.0>demo.ext:moved:1.0' \
'demo.ext:moved:1.0' \
'demo.ext:moved:1.1' \
'demo.ext:wrapper:1.0+demo.old:moved:1.0@test'
rm -rf "$IVY/cache/demo.ext" "$IVY/cache/demo.old"

GB="$(mktemp -d)/global-base"
mkdir -p "$GB/plugins"
cp "$PLUGIN" "$GB/plugins/SocketFactsPlugin.scala"
mkdir -p "$PROJECT/project"
echo "sbt.version=$SBT_VERSION" > "$PROJECT/project/build.properties"
echo "scalaVersion in ThisBuild := \"$SCALA_VERSION\"" > "$PROJECT/scala-version.sbt"
if [ "$RESOLVER" = ivy ]; then
echo 'useCoursier in ThisBuild := false' >> "$PROJECT/scala-version.sbt"
fi
rm -rf "$RECORDS" "$PROJECT/target" "$PROJECT/project/target" "$PROJECT"/*/target

echo "+ sbt $SBT_VERSION (scala $SCALA_VERSION) (relocation, $RESOLVER)"
( cd "$PROJECT" && sbt -Dsbt.global.base="$GB" -Dsbt.server.autostart=false \
-Dsbt.ivy.home="$IVY" -Dsocket.recordsFile="$RECORDS" --batch socketFacts )

python3 - "$RECORDS" <<'PY'
import sys
rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
root_of = {r[1]: (r[2], r[3]) for r in rows if r[0] == 'root'}
# Coursier leaves the relocating module out, so the target hangs under its declarer; Ivy keeps it as
# an artifact-less node between the two, which counts as the declarer's edge.
moved = {'core': 'demo.ext:moved:jar:1.0', 'app': 'demo.ext:moved:jar:1.1'}
relocating, wrapper = 'demo.old:moved:1.0', 'demo.ext:wrapper:jar:1.0'
errors = []
for rid, (project, config) in root_of.items():
nodes = {r[2]: r for r in rows if r[0] == 'node' and r[1] == rid}
target = moved.get(project)
if target not in nodes:
continue
parents_of = lambda c: {r[2] for r in rows if r[0] == 'edge' and r[1] == rid and r[3] == c}
parents, direct = parents_of(target), nodes[target][8] == '1'
if relocating in parents:
parents = (parents - {relocating}) | parents_of(relocating)
direct = direct or nodes[relocating][8] == '1'
if project == 'core' and not direct:
errors.append(f"core/{config}: {target} not direct")
if project == 'app' and parents != {'core'}:
errors.append(f"app/{config}: {target} parents {sorted(parents)} != ['core']")
if wrapper in parents:
errors.append(f"{project}/{config}: {wrapper} made a parent of {target}")
for project, target in moved.items():
if not any(r[0] == 'node' and r[2] == target and root_of[r[1]][0] == project for r in rows):
errors.append(f"{target} missing from {project}")
if errors:
print("FAIL:")
for e in errors: print(" -", e)
sys.exit(1)
print(f"PASS (relocation): demo.ext:moved direct in core, under core in app (overridden), never under {wrapper}")
PY

python3 "$HERE/../assert-reachability.py" "$RECORDS"
13 changes: 12 additions & 1 deletion src/commands/manifest/scripts/test/sbt-compat/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
# - tool appears in a prod root, harness only in non-prod roots -> the assembler's dev flag;
# - every component is reachable from a direct dependency of its own root (no orphaned components);
# - both get an on-disk jar `file` record under -Dsocket.withFiles;
# - a Maven relocation's target, declared directly (demo.old:moved) or through a dependency
# (demo.old:legacy, from tool), stays in the graph although coursier leaves its caller out of the report;
# - a source in the base dir is a `projectSrc` file, never the base dir itself.
#
# Both deps are stubs generated into project/localrepo at test time (../make-stub-repo.sh, shared with
Expand All @@ -27,7 +29,13 @@ RECORDS="$PROJECT/records.tsv"
IVY="$SOCKET_COMPAT_CACHE/ivy2"
export COURSIER_CACHE="$SOCKET_COMPAT_CACHE/coursier"

bash "$HERE/../make-stub-repo.sh" "$PROJECT/localrepo" 'demo.ext:tool:1.0' 'demo.ext:harness:1.0'
bash "$HERE/../make-stub-repo.sh" "$PROJECT/localrepo" \
'demo.ext:tool:1.0+demo.old:legacy:1.0' \
'demo.old:legacy:1.0>demo.ext:legacy:1.0' \
'demo.ext:legacy:1.0' \
'demo.old:moved:1.0>demo.ext:moved:1.0' \
'demo.ext:moved:1.0' \
'demo.ext:harness:1.0'
# scala-library and friends stay cached between runs; the stubs never do, so every run has to
# resolve them from the repo just generated. Only Ivy caches them — coursier reads a `file:`
# repository in place.
Expand Down Expand Up @@ -72,6 +80,9 @@ if harness in nodes and any(roots.get(rid) for rid in nodes[harness]):
def has_jar(cid): return any(p.endswith('.jar') for p in files.get(cid, ()))
for cid in (tool, harness):
if not has_jar(cid): errors.append(f"{cid} missing materialized jar under --with-files: {files.get(cid)}")
for cid in ('demo.ext:legacy:jar:1.0', 'demo.ext:moved:jar:1.0'):
if cid not in nodes: errors.append(f"relocation target missing: {cid}")
elif not any(roots.get(rid) for rid in nodes[cid]): errors.append(f"relocation target {cid} not in a prod root")
if os.path.join(base, 'Base.scala') not in srcs: errors.append(f"base-dir source Base.scala not a projectSrc: {sorted(srcs)}")
if base in srcs: errors.append("base dir emitted as a projectSrc root")

Expand Down
Loading