@socketsecurity/cli-with-sentry 1.9.1 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +5 -5
- package/dist/cli.js +379 -127
- package/dist/cli.js.map +1 -1
- package/dist/constants.js +4 -4
- package/dist/constants.js.map +1 -1
- package/dist/manifest-scripts/maven-extension/coana-maven-extension.jar +0 -0
- package/dist/manifest-scripts/socket-facts.init.gradle +244 -56
- package/dist/manifest-scripts/socket-facts.plugin.scala +290 -73
- package/dist/tsconfig.dts.tsbuildinfo +1 -1
- package/dist/types/commands/manifest/cmd-manifest-dynamic-sbom-inference.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts +5 -2
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-maven-to-facts.d.mts +5 -2
- package/dist/types/commands/manifest/convert-maven-to-facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts +5 -2
- package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/enumerate-workspaces.d.mts.map +1 -1
- package/dist/types/commands/manifest/generate-recursive-manifests.d.mts +8 -1
- package/dist/types/commands/manifest/generate-recursive-manifests.d.mts.map +1 -1
- package/dist/types/commands/manifest/generate_auto_manifest.d.mts.map +1 -1
- package/dist/types/commands/manifest/handle-manifest-dynamic-sbom-inference.d.mts +2 -1
- package/dist/types/commands/manifest/handle-manifest-dynamic-sbom-inference.d.mts.map +1 -1
- package/dist/types/commands/manifest/run-manifest-facts.d.mts +4 -1
- package/dist/types/commands/manifest/run-manifest-facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/assemble.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/facts.d.mts +3 -2
- package/dist/types/commands/manifest/scripts/facts.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/records.d.mts +10 -4
- package/dist/types/commands/manifest/scripts/records.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/resolution-report-gradle.d.mts +3 -2
- package/dist/types/commands/manifest/scripts/resolution-report-gradle.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/resolution-report-maven.d.mts +3 -3
- package/dist/types/commands/manifest/scripts/resolution-report-maven.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts +29 -8
- package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/resolution-report.d.mts +14 -3
- package/dist/types/commands/manifest/scripts/resolution-report.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/run.d.mts +2 -1
- package/dist/types/commands/manifest/scripts/run.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/sidecar.d.mts.map +1 -1
- package/dist/types/commands/scan/cmd-scan-create.d.mts.map +1 -1
- package/dist/types/commands/scan/cmd-scan-reach.d.mts.map +1 -1
- package/dist/types/commands/scan/handle-create-new-scan.d.mts.map +1 -1
- package/dist/types/commands/scan/warn-socket-json-only-flags.d.mts +2 -0
- package/dist/types/commands/scan/warn-socket-json-only-flags.d.mts.map +1 -0
- package/dist/types/constants.d.mts +1 -1
- package/dist/utils.js +2 -1
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,7 +4,6 @@ import sbt._
|
|
|
4
4
|
import sbt.Keys._
|
|
5
5
|
|
|
6
6
|
import scala.collection.mutable
|
|
7
|
-
import scala.reflect.ClassTag
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Emits a flat line-protocol RECORDS file at the build root (NOT the final
|
|
@@ -16,7 +15,7 @@ import scala.reflect.ClassTag
|
|
|
16
15
|
*
|
|
17
16
|
* Must compile on Scala 2.10/sbt 0.13 and Scala 2.12/sbt 1.x (compiled by the sbt
|
|
18
17
|
* meta-build), hence string-named TaskKeys and reflection for version-specific
|
|
19
|
-
*
|
|
18
|
+
* UpdateConfiguration / ResolveException / ConfigRef shapes.
|
|
20
19
|
*/
|
|
21
20
|
object SocketFactsPlugin extends AutoPlugin {
|
|
22
21
|
override def trigger = allRequirements
|
|
@@ -57,16 +56,33 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
57
56
|
// One tree per resolution root (project, config); the TS assembler merges them path-sensitively.
|
|
58
57
|
val perSub = mutable.LinkedHashMap.empty[String, RootTree]
|
|
59
58
|
val failures = mutable.LinkedHashSet.empty[Failure]
|
|
59
|
+
val unscannable = mutable.LinkedHashSet.empty[Unscannable]
|
|
60
60
|
val scannedConfigs = mutable.LinkedHashSet.empty[String]
|
|
61
61
|
val matcher = buildConfigMatcher()
|
|
62
62
|
|
|
63
63
|
// Real artifact ext per build module, so an ext-less inter-project dep gets its true coordinate.
|
|
64
64
|
val moduleExts = buildModuleExts(allRefs, extracted)
|
|
65
65
|
|
|
66
|
+
// Every project: `update` also runs each upstream project's update, which would fail strictly.
|
|
67
|
+
lazy val lenientState = extracted.append(allRefs.map(r => updateConfiguration.in(r) ~= (c => withMissingOk(c))), st)
|
|
68
|
+
val pomsByCache = mutable.HashMap.empty[Option[File], PomPackaging]
|
|
69
|
+
|
|
66
70
|
allRefs.foreach { ref =>
|
|
67
71
|
if (!isExcludedRef(ref)) {
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
// Built-in lm-coursier (sbt 1.3+) and the legacy sbt-coursier plugin name their caches differently.
|
|
73
|
+
val legacyCoursierCache = extracted.getOpt(SettingKey[File]("coursierCache").in(ref))
|
|
74
|
+
val csrCache = extracted.getOpt(SettingKey[File]("csrCacheDirectory").in(ref)).orElse(legacyCoursierCache)
|
|
75
|
+
val poms = pomsByCache.getOrElseUpdate(csrCache, new PomPackaging(csrCache))
|
|
76
|
+
// sbt 0.13 has no `useCoursier`: it resolves with Ivy unless the legacy plugin replaces it.
|
|
77
|
+
val ivy = extracted.getOpt(SettingKey[Boolean]("useCoursier").in(ref)) match {
|
|
78
|
+
case Some(useCoursier) => !useCoursier
|
|
79
|
+
case None => legacyCoursierCache.isEmpty
|
|
80
|
+
}
|
|
81
|
+
val resolver = new Resolver(ivy, poms)
|
|
82
|
+
runUpdateResilient(updateTaskName, ref, extracted, st, lenientState, matcher, failures, unscannable, resolver)
|
|
83
|
+
.foreach { case (report, unresolved, ivyUnfetched, updateFailed) =>
|
|
84
|
+
foldReport(report, unresolved, ref, extracted, matcher, scannedConfigs, withFiles, populateScope, moduleExts,
|
|
85
|
+
resolver, ivyUnfetched, updateFailed, failures).foreach {
|
|
70
86
|
case (rootKey, tree) => perSub(rootKey) = tree
|
|
71
87
|
}
|
|
72
88
|
}
|
|
@@ -101,21 +117,24 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
101
117
|
rec("buildRoot", rootCanonPath.toString)
|
|
102
118
|
|
|
103
119
|
// One `project` record per build module (sources/targets only with --with-files). Excluded
|
|
104
|
-
// subprojects
|
|
120
|
+
// subprojects get none (they were also skipped during resolution above), but keep their paths so
|
|
121
|
+
// a kept project depending on one is analysed with its code.
|
|
105
122
|
allRefs.foreach { ref =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
val mid = rootIdOf(extracted, ref)
|
|
124
|
+
val ver = if (mid.revision == null) "" else mid.revision
|
|
125
|
+
if (isExcludedRef(ref)) {
|
|
126
|
+
if (withFiles) rec("excludedProject", ref.project)
|
|
127
|
+
} else {
|
|
109
128
|
val baseDir = extracted.get(baseDirectory.in(ref))
|
|
110
129
|
rec("project", ref.project, mid.organization, mid.name, ver, relOf(baseDir))
|
|
111
130
|
(buildFilesByRef.getOrElse(ref, Nil) ++ baseBuildSbt(baseDir, rootCanonPath).map(relOf)).distinct.sorted
|
|
112
131
|
.foreach(f => rec("projectBuild", ref.project, f))
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
}
|
|
133
|
+
if (withFiles) {
|
|
134
|
+
moduleDirs.get(mid.organization + ":" + mid.name + ":" + ver).foreach {
|
|
135
|
+
case (sources, targets) =>
|
|
136
|
+
sources.foreach(s => rec("projectSrc", ref.project, s))
|
|
137
|
+
targets.foreach(t => rec("projectTgt", ref.project, t))
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
140
|
}
|
|
@@ -135,12 +154,14 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
135
154
|
node.project)
|
|
136
155
|
node.children.foreach(ch => rec("edge", rootId, coordId, ch))
|
|
137
156
|
node.targets.foreach(p => rec("file", rootId, coordId, p))
|
|
157
|
+
if (node.unfetched) rec("unfetched", rootId, coordId)
|
|
138
158
|
}
|
|
139
159
|
}
|
|
140
160
|
|
|
141
161
|
// Scanned configs + raw failures (Coana owns the abort/warn policy and report rendering).
|
|
142
162
|
scannedConfigs.toList.sorted.foreach(c => rec("scanned", c))
|
|
143
|
-
failures.foreach(f => rec("failure", f.coord, f.detail, f.config))
|
|
163
|
+
failures.foreach(f => rec("failure", f.coord, f.detail, f.config, f.project))
|
|
164
|
+
unscannable.foreach(u => rec("unscannable", "", u.detail, u.project))
|
|
144
165
|
|
|
145
166
|
val recordsFile = sys.props.get("socket.recordsFile").filter(_.nonEmpty) match {
|
|
146
167
|
case Some(p) => new File(p)
|
|
@@ -193,38 +214,173 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
193
214
|
}.toMap
|
|
194
215
|
}
|
|
195
216
|
|
|
196
|
-
//
|
|
217
|
+
// A failure naming no module is an artifact that could not be fetched, which only the lenient
|
|
218
|
+
// report's missing artifacts reveal; with neither, the project is unscannable.
|
|
197
219
|
private def runUpdateResilient(
|
|
198
220
|
taskName: String,
|
|
199
221
|
ref: ProjectRef,
|
|
200
222
|
extracted: Extracted,
|
|
201
223
|
state: State,
|
|
202
|
-
|
|
203
|
-
|
|
224
|
+
lenient: => State,
|
|
225
|
+
matcher: String => Boolean,
|
|
226
|
+
failures: mutable.LinkedHashSet[Failure],
|
|
227
|
+
unscannable: mutable.LinkedHashSet[Unscannable],
|
|
228
|
+
resolver: Resolver
|
|
229
|
+
): Option[(UpdateReport, Set[String], IvyUnfetched, Boolean)] = {
|
|
204
230
|
val key = TaskKey[UpdateReport](taskName)
|
|
205
231
|
|
|
206
|
-
def runOn(st: State): Either[Seq[ModuleID], UpdateReport] =
|
|
232
|
+
def runOn(st: State): Either[(Seq[ModuleID], String), UpdateReport] =
|
|
207
233
|
Project.runTask(key.in(ref).scopedKey, st) match {
|
|
208
234
|
case Some((_, sbt.Value(rep))) => Right(rep)
|
|
209
|
-
case Some((_, sbt.Inc(inc))) => Left(extractFailedModules(inc))
|
|
210
|
-
case _ => Left(Nil)
|
|
235
|
+
case Some((_, sbt.Inc(inc))) => Left((extractFailedModules(inc), incompleteMessage(inc)))
|
|
236
|
+
case _ => Left((Nil, s"$taskName did not run"))
|
|
211
237
|
}
|
|
212
238
|
|
|
213
239
|
runOn(state) match {
|
|
214
|
-
case Right(rep) => Some(rep)
|
|
215
|
-
case Left(failed) =>
|
|
216
|
-
|
|
217
|
-
|
|
240
|
+
case Right(rep) => Some((rep, Set.empty[String], Map.empty: IvyUnfetched, false))
|
|
241
|
+
case Left((failed, detail)) =>
|
|
242
|
+
val lenientRun = runOn(lenient).right.map { rep =>
|
|
243
|
+
(rep, if (resolver.ivy) ivyUnfetchedOf(rep, extracted.get(target.in(ref))) else Map.empty: IvyUnfetched)
|
|
244
|
+
}
|
|
245
|
+
lenientRun match {
|
|
246
|
+
case Right((rep, ivyUnfetched))
|
|
247
|
+
if failed.isEmpty && ivyUnfetched.isEmpty &&
|
|
248
|
+
!rep.configurations.exists(_.modules.exists(m => !m.evicted && resolver.missingOf(m).nonEmpty)) =>
|
|
249
|
+
unscannable += Unscannable(detail, ref.project)
|
|
250
|
+
None
|
|
251
|
+
case Right((rep, ivyUnfetched)) =>
|
|
252
|
+
val confsByGav = mutable.HashMap.empty[String, mutable.LinkedHashSet[String]]
|
|
253
|
+
rep.configurations.foreach { cr =>
|
|
254
|
+
cr.modules.foreach { r =>
|
|
255
|
+
if (!r.evicted) confsByGav.getOrElseUpdate(gavKey(r.module), mutable.LinkedHashSet.empty[String]) += confName(cr)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
failed.foreach { m =>
|
|
259
|
+
// Absent from the graph: no configuration. Present only in excluded ones: not reported.
|
|
260
|
+
confsByGav.get(gavKey(m)) match {
|
|
261
|
+
case None => failures += Failure(coordOf(m), detailFor(m, detail), "", ref.project)
|
|
262
|
+
case Some(confs) => confs.filter(matcher).foreach(c => failures += Failure(coordOf(m), detailFor(m, detail), c, ref.project))
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
Some((rep, failed.map(gavKey).toSet, ivyUnfetched, failed.isEmpty))
|
|
266
|
+
case Left(_) =>
|
|
267
|
+
// Even a lenient update failed: the whole project is unresolved, but the modules sbt
|
|
268
|
+
// named are still reported.
|
|
269
|
+
failed.foreach(m => failures += Failure(coordOf(m), detailFor(m, detail), "", ref.project))
|
|
270
|
+
unscannable += Unscannable(detail, ref.project)
|
|
271
|
+
None
|
|
218
272
|
}
|
|
219
|
-
if (failed.isEmpty) None
|
|
220
|
-
else
|
|
221
|
-
runOn(extracted.append(exclusionSettings(extracted, failed), state)) match {
|
|
222
|
-
case Right(rep) => Some(rep)
|
|
223
|
-
case Left(_) => None
|
|
224
|
-
}
|
|
225
273
|
}
|
|
226
274
|
}
|
|
227
275
|
|
|
276
|
+
// config -> "org:name:rev" -> artifacts Ivy failed to download.
|
|
277
|
+
private type IvyUnfetched = Map[String, Map[String, Seq[IvyArtifact]]]
|
|
278
|
+
|
|
279
|
+
private final case class IvyArtifact(ext: String, classifier: String, detail: String)
|
|
280
|
+
|
|
281
|
+
// sbt 0.13's UpdateReport omits artifacts Ivy failed to download; Ivy's own report per configuration,
|
|
282
|
+
// `<resolution cache>/reports/<organisation>-<module>-<conf>.xml`, keeps them.
|
|
283
|
+
private def ivyUnfetchedOf(rep: UpdateReport, projectTarget: File): IvyUnfetched = {
|
|
284
|
+
val descriptor = Option(rep.cachedDescriptor).filter(_.isFile)
|
|
285
|
+
val bound = projectTarget.getCanonicalFile.toPath
|
|
286
|
+
def reportsDirs(f: File): Stream[File] =
|
|
287
|
+
Option(f.getParentFile).filter(_.getCanonicalFile.toPath.startsWith(bound)).toStream
|
|
288
|
+
.flatMap(p => new File(p, "reports") #:: reportsDirs(p))
|
|
289
|
+
(for {
|
|
290
|
+
d <- descriptor.toSeq
|
|
291
|
+
id <- ivyResolveId(d).toSeq
|
|
292
|
+
cr <- rep.configurations
|
|
293
|
+
conf = confName(cr)
|
|
294
|
+
report <- reportsDirs(d).map(new File(_, id + "-" + conf + ".xml")).find(_.isFile).toSeq
|
|
295
|
+
arts = ivyFailedArtifacts(report)
|
|
296
|
+
if arts.nonEmpty
|
|
297
|
+
} yield conf -> arts).toMap
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private def parseXml(f: File): org.w3c.dom.Document = {
|
|
301
|
+
// A POM may declare a DOCTYPE, so only what it could pull in from outside is refused.
|
|
302
|
+
val dbf = javax.xml.parsers.DocumentBuilderFactory.newInstance()
|
|
303
|
+
dbf.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
|
304
|
+
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
|
|
305
|
+
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
|
|
306
|
+
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
|
|
307
|
+
dbf.setXIncludeAware(false)
|
|
308
|
+
dbf.setExpandEntityReferences(false)
|
|
309
|
+
dbf.newDocumentBuilder().parse(f)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Ivy's ResolveOptions.getDefaultResolveId: `<organisation>-<module>` of the resolved descriptor.
|
|
313
|
+
private def ivyResolveId(descriptor: File): Option[String] =
|
|
314
|
+
try {
|
|
315
|
+
val info = parseXml(descriptor).getElementsByTagName("info")
|
|
316
|
+
if (info.getLength == 0) None
|
|
317
|
+
else {
|
|
318
|
+
val e = info.item(0).asInstanceOf[org.w3c.dom.Element]
|
|
319
|
+
Some(e.getAttribute("organisation") + "-" + e.getAttribute("module"))
|
|
320
|
+
}
|
|
321
|
+
} catch { case _: Exception => None }
|
|
322
|
+
|
|
323
|
+
private def ivyFailedArtifacts(report: File): Map[String, Seq[IvyArtifact]] =
|
|
324
|
+
try {
|
|
325
|
+
val modules = parseXml(report).getElementsByTagName("module")
|
|
326
|
+
val out = for {
|
|
327
|
+
mi <- 0 until modules.getLength
|
|
328
|
+
module = modules.item(mi).asInstanceOf[org.w3c.dom.Element]
|
|
329
|
+
revisions = module.getElementsByTagName("revision")
|
|
330
|
+
ri <- 0 until revisions.getLength
|
|
331
|
+
revision = revisions.item(ri).asInstanceOf[org.w3c.dom.Element]
|
|
332
|
+
if revision.getAttribute("evicted").isEmpty
|
|
333
|
+
artifacts = revision.getElementsByTagName("artifact")
|
|
334
|
+
ai <- 0 until artifacts.getLength
|
|
335
|
+
a = artifacts.item(ai).asInstanceOf[org.w3c.dom.Element]
|
|
336
|
+
if a.getAttribute("status") == "failed"
|
|
337
|
+
} yield {
|
|
338
|
+
val (org, name, rev) = (module.getAttribute("organisation"), module.getAttribute("name"), revision.getAttribute("name"))
|
|
339
|
+
val classifier = a.getAttribute("extra-classifier")
|
|
340
|
+
val file = a.getAttribute("name") + (if (classifier.isEmpty) "" else "-" + classifier) + "." + a.getAttribute("ext")
|
|
341
|
+
// An absent artifact is "missing artifact" (empty under sbt 0.13's Ivy); one the resolver chain
|
|
342
|
+
// failed itself has "null"; anything else is a transfer failure's message.
|
|
343
|
+
val details = a.getAttribute("details")
|
|
344
|
+
val cause =
|
|
345
|
+
if (details.isEmpty || details == "missing artifact") "not found"
|
|
346
|
+
else if (details == "null") "download failed"
|
|
347
|
+
else "download failed (" + details + ")"
|
|
348
|
+
(org + ":" + name + ":" + rev) -> IvyArtifact(a.getAttribute("ext"), classifier, cause + ": " + org + "#" + name + ";" + rev + "!" + file)
|
|
349
|
+
}
|
|
350
|
+
out.groupBy(_._1).map { case (gav, pairs) => gav -> pairs.map(_._2).distinct }
|
|
351
|
+
} catch { case _: Exception => Map.empty }
|
|
352
|
+
|
|
353
|
+
// UpdateConfiguration has `withMissingOk` on sbt 1.x; on 0.13 its only Boolean field is missingOk.
|
|
354
|
+
private def withMissingOk[T](c: T): T = {
|
|
355
|
+
val cls = c.getClass
|
|
356
|
+
try cls.getMethod("withMissingOk", classOf[Boolean]).invoke(c, java.lang.Boolean.TRUE).asInstanceOf[T]
|
|
357
|
+
catch {
|
|
358
|
+
case _: NoSuchMethodException =>
|
|
359
|
+
val copy = cls.getMethods.find(_.getName == "copy").get
|
|
360
|
+
val args = copy.getParameterTypes.zipWithIndex.map { case (t, i) =>
|
|
361
|
+
if (t == classOf[Boolean]) java.lang.Boolean.TRUE
|
|
362
|
+
else cls.getMethod("copy$default$" + (i + 1)).invoke(c)
|
|
363
|
+
}
|
|
364
|
+
copy.invoke(c, args: _*).asInstanceOf[T]
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// One module's part of a batch resolve message, so records grow with the failure count, not its
|
|
369
|
+
// square: Coursier writes an indented "Error downloading org:name:rev" block per module, Ivy a line.
|
|
370
|
+
private def detailFor(m: ModuleID, batch: String): String = {
|
|
371
|
+
val lines = batch.split("\n").toList
|
|
372
|
+
val keys = Seq(coordOf(m), s"${orEmpty(m.organization)}#${orEmpty(m.name)};${orEmpty(m.revision)}")
|
|
373
|
+
val start = lines.indexWhere(l => keys.exists(k => l.contains(k)))
|
|
374
|
+
if (start < 0) lines.map(_.trim).find(_.nonEmpty).getOrElse(batch)
|
|
375
|
+
else (lines(start) :: lines.drop(start + 1).takeWhile(l => l.startsWith(" ") || l.startsWith("\t"))).mkString("\n").trim
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private def incompleteMessage(inc: sbt.Incomplete): String = {
|
|
379
|
+
def first(i: sbt.Incomplete): Option[String] =
|
|
380
|
+
i.directCause.flatMap(ex => Option(ex.getMessage)).orElse(i.causes.view.flatMap(first).headOption)
|
|
381
|
+
first(inc).map(_.trim).filter(_.nonEmpty).getOrElse("unresolved dependency")
|
|
382
|
+
}
|
|
383
|
+
|
|
228
384
|
// Reflection: ResolveException's package and the `failed` accessor differ between sbt 0.13 and 1.x.
|
|
229
385
|
private def extractFailedModules(inc: sbt.Incomplete): Seq[ModuleID] = {
|
|
230
386
|
val out = mutable.ListBuffer.empty[ModuleID]
|
|
@@ -244,42 +400,23 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
244
400
|
out.toList.distinct
|
|
245
401
|
}
|
|
246
402
|
|
|
247
|
-
// ExclusionRule constructor shape differs between sbt 0.13 (`sbt.ExclusionRule`) and 1.x (`sbt.SbtExclusionRule`).
|
|
248
|
-
private def exclusionSettings(extracted: Extracted, failed: Seq[ModuleID]): Seq[Setting[_]] = {
|
|
249
|
-
val cls: Class[_] =
|
|
250
|
-
try Class.forName("sbt.SbtExclusionRule")
|
|
251
|
-
catch { case _: ClassNotFoundException => classOf[sbt.ExclusionRule] }
|
|
252
|
-
|
|
253
|
-
def rule(m: ModuleID): Any =
|
|
254
|
-
if (cls.getName.contains("SbtExclusionRule")) {
|
|
255
|
-
cls.getConstructors.find(_.getParameterCount == 5) match {
|
|
256
|
-
case Some(ctor) =>
|
|
257
|
-
ctor.newInstance(m.organization, m.name, "*", Seq(), sbt.CrossVersion.Disabled)
|
|
258
|
-
case None =>
|
|
259
|
-
throw new IllegalStateException("No suitable SbtExclusionRule constructor")
|
|
260
|
-
}
|
|
261
|
-
} else ExclusionRule(organization = m.organization, name = m.name)
|
|
262
|
-
|
|
263
|
-
def castSeq[T](xs: Seq[Any])(implicit ct: ClassTag[T]): Seq[T] = xs.map(_.asInstanceOf[T])
|
|
264
|
-
|
|
265
|
-
extracted.structure.allProjectRefs.map { ref =>
|
|
266
|
-
excludeDependencies.in(ref) := {
|
|
267
|
-
val original = excludeDependencies.in(ref).value
|
|
268
|
-
castSeq(original ++ failed.map(rule))(ClassTag(cls))
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
403
|
// Fold one project's UpdateReport into one tree per configuration (rootKey -> tree), keyed by coordinate.
|
|
274
404
|
private def foldReport(
|
|
275
405
|
report: UpdateReport,
|
|
406
|
+
unresolved: Set[String],
|
|
276
407
|
ref: ProjectRef,
|
|
277
408
|
extracted: Extracted,
|
|
278
409
|
matcher: String => Boolean,
|
|
279
410
|
scannedConfigs: mutable.LinkedHashSet[String],
|
|
280
411
|
withFiles: Boolean,
|
|
281
412
|
populateScope: Option[Set[String]],
|
|
282
|
-
moduleExts: Map[String, String]
|
|
413
|
+
moduleExts: Map[String, String],
|
|
414
|
+
resolver: Resolver,
|
|
415
|
+
ivyUnfetched: IvyUnfetched,
|
|
416
|
+
// sbt's own update failed on artifacts alone, so one it could not fetch is a failure even
|
|
417
|
+
// without files.
|
|
418
|
+
updateFailed: Boolean,
|
|
419
|
+
failures: mutable.LinkedHashSet[Failure]
|
|
283
420
|
): mutable.LinkedHashMap[String, RootTree] = {
|
|
284
421
|
val perRoot = mutable.LinkedHashMap.empty[String, RootTree]
|
|
285
422
|
val rootKey = moduleKey(rootIdOf(extracted, ref))
|
|
@@ -287,7 +424,7 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
287
424
|
// Build modules come from the build structure instead: lm-coursier lists every inter-project
|
|
288
425
|
// dependency in every configuration, and cross-versioning can merge distinct ones.
|
|
289
426
|
def emittable(m: ModuleReport): Boolean =
|
|
290
|
-
!m.evicted && !moduleExts.contains(gavKey(m.module))
|
|
427
|
+
!m.evicted && !moduleExts.contains(gavKey(m.module)) && !unresolved.contains(gavKey(m.module))
|
|
291
428
|
|
|
292
429
|
def confsOf(r: ProjectRef): Seq[Configuration] = extracted.getOpt(ivyConfigurations.in(r)).getOrElse(Nil)
|
|
293
430
|
val buildDeps = extracted.get(buildDependencies)
|
|
@@ -358,10 +495,18 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
358
495
|
cr.modules.foreach { m =>
|
|
359
496
|
if (emittable(m)) {
|
|
360
497
|
val ids = midToIds.getOrElseUpdate(moduleKey(m.module), mutable.LinkedHashSet.empty[String])
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
498
|
+
val ivyMissing =
|
|
499
|
+
if (ivyUnfetched.isEmpty) Nil
|
|
500
|
+
else ivyUnfetched.getOrElse(cfg, Map.empty[String, Seq[IvyArtifact]]).getOrElse(gavKey(m.module), Nil)
|
|
501
|
+
val recordFiles = withFiles && inScope(m.module)
|
|
502
|
+
variantsOf(m, resolver, ivyMissing).foreach { v =>
|
|
503
|
+
val node = nodes.getOrElseUpdate(v.coord.id, new Node(v.coord))
|
|
504
|
+
ids += v.coord.id
|
|
505
|
+
if (recordFiles) v.file.foreach(f => node.targets += f.getAbsolutePath)
|
|
506
|
+
v.unfetched.foreach { url =>
|
|
507
|
+
if (recordFiles) node.unfetched = true
|
|
508
|
+
if (recordFiles || updateFailed) failures += Failure(coordOf(m.module), url, cfg, ref.project)
|
|
509
|
+
}
|
|
365
510
|
}
|
|
366
511
|
}
|
|
367
512
|
}
|
|
@@ -391,22 +536,86 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
391
536
|
perRoot
|
|
392
537
|
}
|
|
393
538
|
|
|
394
|
-
private def variantsOf(m: ModuleReport
|
|
539
|
+
private def variantsOf(m: ModuleReport, resolver: Resolver, ivyMissing: Seq[IvyArtifact]): Seq[Variant] = {
|
|
395
540
|
val mid = m.module
|
|
396
541
|
// `mid.revision` is normally set for a resolved module, but normalize null (Coord.id calls
|
|
397
542
|
// `.nonEmpty`, which NPEs on null) to match the other call sites (gavKey, module records).
|
|
398
543
|
val ver = if (mid.revision == null) "" else mid.revision
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
544
|
+
def coordFor(art: Artifact) = Coord(mid.organization, mid.name, ver, extOf(art), classifierOf(art))
|
|
545
|
+
val fetched = Option(m.artifacts).toList.flatten.map { case (art, file) => Variant(coordFor(art), Option(file), None) }
|
|
546
|
+
val missing = resolver.missingOf(m).map { art =>
|
|
547
|
+
// The update report does not say why: a 401 or a timeout is as likely as an absent file.
|
|
548
|
+
Variant(coordFor(art), None, Some("could not be fetched: " + art.url.map(_.toString).getOrElse(coordFor(art).id)))
|
|
549
|
+
} ++ ivyMissing.map(a => Variant(Coord(mid.organization, mid.name, ver, a.ext, a.classifier), None, Some(a.detail)))
|
|
550
|
+
if (fetched.isEmpty && missing.isEmpty)
|
|
551
|
+
// No artifact expected (a pom-only module): leave it ext-less.
|
|
552
|
+
Seq(Variant(Coord(mid.organization, mid.name, ver, "", ""), None, None))
|
|
553
|
+
else fetched ++ missing
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Artifacts the resolver expected and did not fetch. Under Ivy they come from its resolution report
|
|
557
|
+
// instead (see ivyUnfetchedOf).
|
|
558
|
+
private final class Resolver(val ivy: Boolean, poms: PomPackaging) {
|
|
559
|
+
def missingOf(m: ModuleReport): List[Artifact] =
|
|
560
|
+
if (ivy) Nil else Option(m.missingArtifacts).toList.flatten.filterNot(poms.isUnexpectedDefault)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Coursier requests the default jar of every Maven module and skips it silently when absent unless
|
|
564
|
+
// the POM declares a jar packaging. Only the module's own POM tells whether its artifact is a jar
|
|
565
|
+
// (packaging defaults to jar, so a parent's never applies); an unreadable POM counts as one.
|
|
566
|
+
private final class PomPackaging(csrCacheDir: Option[File]) {
|
|
567
|
+
private val memo = mutable.HashMap.empty[String, Boolean]
|
|
568
|
+
|
|
569
|
+
def isUnexpectedDefault(a: Artifact): Boolean = {
|
|
570
|
+
val ext = extOf(a)
|
|
571
|
+
a.classifier.isEmpty && ext == "jar" && a.url.map(_.toString).exists { url =>
|
|
572
|
+
url.endsWith("." + ext) && memo.getOrElseUpdate(url, {
|
|
573
|
+
val pomUrl = url.dropRight(ext.length) + "pom"
|
|
574
|
+
coursierLocalFiles(pomUrl, csrCacheDir).find(_.isFile).flatMap(pomPackaging).exists(p => !JarPackagings(p))
|
|
575
|
+
})
|
|
407
576
|
}
|
|
577
|
+
}
|
|
408
578
|
}
|
|
409
579
|
|
|
580
|
+
// coursier.maven.MavenAttributes.typeExtensions (2.1.24, which adds ejb): the packagings whose
|
|
581
|
+
// artifact is a `.jar`. Any other packaging (pom, aar, war, ...) publishes its own artifact.
|
|
582
|
+
private val JarPackagings = Set("eclipse-plugin", "maven-plugin", "hk2-jar", "orbit", "scala-jar", "jar", "bundle",
|
|
583
|
+
"doc", "src", "test-jar", "ejb-client", "ejb")
|
|
584
|
+
|
|
585
|
+
// coursier.paths.CachePath.localFile. A repository with credentials is cached under `user@host`,
|
|
586
|
+
// and the update report does not carry the user.
|
|
587
|
+
private def coursierLocalFiles(url: String, csrCacheDir: Option[File]): Seq[File] =
|
|
588
|
+
if (url.startsWith("file:/"))
|
|
589
|
+
try Seq(new File(new java.net.URI(url)))
|
|
590
|
+
catch { case _: Exception => Nil }
|
|
591
|
+
else {
|
|
592
|
+
val i = url.indexOf(':')
|
|
593
|
+
val protocol = if (i < 0) "" else url.substring(0, i)
|
|
594
|
+
val rest = if (i < 0) "" else url.substring(i + 1).dropWhile(_ == '/')
|
|
595
|
+
csrCacheDir.toSeq.flatMap { dir =>
|
|
596
|
+
val protocolDir = new File(dir, coursierEscape(protocol))
|
|
597
|
+
val host = coursierEscape(rest.takeWhile(_ != '/'))
|
|
598
|
+
val path = coursierEscape(rest.dropWhile(_ != '/').drop(1))
|
|
599
|
+
val withUser = Option(protocolDir.listFiles).toSeq.flatten.filter(_.getName.endsWith("%40" + host))
|
|
600
|
+
(new File(protocolDir, host) +: withUser).map(new File(_, path))
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private def coursierEscape(s: String): String =
|
|
605
|
+
s.flatMap { ch =>
|
|
606
|
+
if (ch > 128 || " %$&+,:;=?@<>#".indexOf(ch) >= 0) "%" + "%02X".format(ch.toInt) else ch.toString
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Maven's default packaging is jar.
|
|
610
|
+
private def pomPackaging(pom: File): Option[String] =
|
|
611
|
+
try {
|
|
612
|
+
val kids = parseXml(pom).getDocumentElement.getChildNodes
|
|
613
|
+
val declared = (0 until kids.getLength).map(kids.item).collectFirst {
|
|
614
|
+
case e: org.w3c.dom.Element if e.getNodeName.split(':').last == "packaging" => e.getTextContent.trim
|
|
615
|
+
}
|
|
616
|
+
Some(declared.filter(_.nonEmpty).getOrElse("jar"))
|
|
617
|
+
} catch { case _: Exception => None }
|
|
618
|
+
|
|
410
619
|
// ---- config selection ---------------------------------------------------
|
|
411
620
|
|
|
412
621
|
// With no includes the default is ALL configurations (captures build/tooling deps).
|
|
@@ -568,7 +777,10 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
568
777
|
else v.replace("\\", "\\\\").replace("\t", "\\t").replace("\n", "\\n").replace("\r", "\\r")
|
|
569
778
|
}
|
|
570
779
|
|
|
571
|
-
|
|
780
|
+
// Empty `config`: the failure belongs to no configuration.
|
|
781
|
+
private final case class Failure(coord: String, detail: String, config: String, project: String)
|
|
782
|
+
|
|
783
|
+
private final case class Unscannable(detail: String, project: String)
|
|
572
784
|
|
|
573
785
|
private final case class Coord(
|
|
574
786
|
org: String,
|
|
@@ -587,8 +799,13 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
587
799
|
var project = ""
|
|
588
800
|
// External artifact's resolved jar(s); --with-files only.
|
|
589
801
|
val targets = mutable.TreeSet.empty[String]
|
|
802
|
+
// An expected artifact was not fetched; --with-files only.
|
|
803
|
+
var unfetched = false
|
|
590
804
|
}
|
|
591
805
|
|
|
806
|
+
// `unfetched`: why the expected artifact could not be fetched.
|
|
807
|
+
private final case class Variant(coord: Coord, file: Option[File], unfetched: Option[String])
|
|
808
|
+
|
|
592
809
|
private final class RootTree(
|
|
593
810
|
val projectKey: String,
|
|
594
811
|
val config: String,
|