@socketsecurity/cli-with-sentry 1.10.0 → 1.11.0
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 +26 -0
- package/README.md +5 -5
- package/dist/cli.js +255 -128
- 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 +241 -54
- package/dist/manifest-scripts/socket-facts.plugin.scala +224 -38
- package/dist/tsconfig.dts.tsbuildinfo +1 -1
- package/dist/types/commands/ci/handle-ci.d.mts.map +1 -1
- package/dist/types/commands/fix/handle-fix.d.mts.map +1 -1
- package/dist/types/commands/manifest/convert-gradle-to-facts.d.mts +2 -1
- 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 +2 -1
- 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 +2 -1
- package/dist/types/commands/manifest/convert-sbt-to-facts.d.mts.map +1 -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/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 +7 -1
- 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.map +1 -1
- package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts +9 -4
- package/dist/types/commands/manifest/scripts/resolution-report-render.d.mts.map +1 -1
- package/dist/types/commands/manifest/scripts/resolution-report.d.mts +7 -2
- package/dist/types/commands/manifest/scripts/resolution-report.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/perform-reachability-analysis.d.mts +2 -0
- package/dist/types/commands/scan/perform-reachability-analysis.d.mts.map +1 -1
- package/dist/types/commands/scan/reach-advisories.d.mts +7 -0
- package/dist/types/commands/scan/reach-advisories.d.mts.map +1 -0
- package/dist/types/commands/scan/reachability-flags.d.mts.map +1 -1
- package/dist/types/constants.d.mts +1 -1
- package/dist/types/constants.d.mts.map +1 -1
- package/dist/types/utils/advisory-ids.d.mts +14 -0
- package/dist/types/utils/advisory-ids.d.mts.map +1 -0
- package/dist/types/utils/cve-to-ghsa.d.mts +2 -0
- package/dist/types/utils/cve-to-ghsa.d.mts.map +1 -1
- package/dist/utils.js +69 -3
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -63,10 +63,26 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
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,6 +154,7 @@ 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
|
|
|
@@ -194,17 +214,19 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
194
214
|
}.toMap
|
|
195
215
|
}
|
|
196
216
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
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.
|
|
199
219
|
private def runUpdateResilient(
|
|
200
220
|
taskName: String,
|
|
201
221
|
ref: ProjectRef,
|
|
202
222
|
extracted: Extracted,
|
|
203
223
|
state: State,
|
|
224
|
+
lenient: => State,
|
|
204
225
|
matcher: String => Boolean,
|
|
205
226
|
failures: mutable.LinkedHashSet[Failure],
|
|
206
|
-
unscannable: mutable.LinkedHashSet[Unscannable]
|
|
207
|
-
|
|
227
|
+
unscannable: mutable.LinkedHashSet[Unscannable],
|
|
228
|
+
resolver: Resolver
|
|
229
|
+
): Option[(UpdateReport, Set[String], IvyUnfetched, Boolean)] = {
|
|
208
230
|
val key = TaskKey[UpdateReport](taskName)
|
|
209
231
|
|
|
210
232
|
def runOn(st: State): Either[(Seq[ModuleID], String), UpdateReport] =
|
|
@@ -215,14 +237,18 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
215
237
|
}
|
|
216
238
|
|
|
217
239
|
runOn(state) match {
|
|
218
|
-
case Right(rep) => Some((rep, Set.empty[String]))
|
|
219
|
-
case Left((failed, detail)) if failed.isEmpty =>
|
|
220
|
-
unscannable += Unscannable(detail, ref.project)
|
|
221
|
-
None
|
|
240
|
+
case Right(rep) => Some((rep, Set.empty[String], Map.empty: IvyUnfetched, false))
|
|
222
241
|
case Left((failed, detail)) =>
|
|
223
|
-
val
|
|
224
|
-
|
|
225
|
-
|
|
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)) =>
|
|
226
252
|
val confsByGav = mutable.HashMap.empty[String, mutable.LinkedHashSet[String]]
|
|
227
253
|
rep.configurations.foreach { cr =>
|
|
228
254
|
cr.modules.foreach { r =>
|
|
@@ -236,7 +262,7 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
236
262
|
case Some(confs) => confs.filter(matcher).foreach(c => failures += Failure(coordOf(m), detailFor(m, detail), c, ref.project))
|
|
237
263
|
}
|
|
238
264
|
}
|
|
239
|
-
Some((rep, failed.map(gavKey).toSet))
|
|
265
|
+
Some((rep, failed.map(gavKey).toSet, ivyUnfetched, failed.isEmpty))
|
|
240
266
|
case Left(_) =>
|
|
241
267
|
// Even a lenient update failed: the whole project is unresolved, but the modules sbt
|
|
242
268
|
// named are still reported.
|
|
@@ -247,6 +273,83 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
247
273
|
}
|
|
248
274
|
}
|
|
249
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
|
+
|
|
250
353
|
// UpdateConfiguration has `withMissingOk` on sbt 1.x; on 0.13 its only Boolean field is missingOk.
|
|
251
354
|
private def withMissingOk[T](c: T): T = {
|
|
252
355
|
val cls = c.getClass
|
|
@@ -307,7 +410,13 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
307
410
|
scannedConfigs: mutable.LinkedHashSet[String],
|
|
308
411
|
withFiles: Boolean,
|
|
309
412
|
populateScope: Option[Set[String]],
|
|
310
|
-
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]
|
|
311
420
|
): mutable.LinkedHashMap[String, RootTree] = {
|
|
312
421
|
val perRoot = mutable.LinkedHashMap.empty[String, RootTree]
|
|
313
422
|
val rootKey = moduleKey(rootIdOf(extracted, ref))
|
|
@@ -386,10 +495,18 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
386
495
|
cr.modules.foreach { m =>
|
|
387
496
|
if (emittable(m)) {
|
|
388
497
|
val ids = midToIds.getOrElseUpdate(moduleKey(m.module), mutable.LinkedHashSet.empty[String])
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
}
|
|
393
510
|
}
|
|
394
511
|
}
|
|
395
512
|
}
|
|
@@ -419,22 +536,86 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
419
536
|
perRoot
|
|
420
537
|
}
|
|
421
538
|
|
|
422
|
-
private def variantsOf(m: ModuleReport
|
|
539
|
+
private def variantsOf(m: ModuleReport, resolver: Resolver, ivyMissing: Seq[IvyArtifact]): Seq[Variant] = {
|
|
423
540
|
val mid = m.module
|
|
424
541
|
// `mid.revision` is normally set for a resolved module, but normalize null (Coord.id calls
|
|
425
542
|
// `.nonEmpty`, which NPEs on null) to match the other call sites (gavKey, module records).
|
|
426
543
|
val ver = if (mid.revision == null) "" else mid.revision
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
})
|
|
435
576
|
}
|
|
577
|
+
}
|
|
436
578
|
}
|
|
437
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
|
+
|
|
438
619
|
// ---- config selection ---------------------------------------------------
|
|
439
620
|
|
|
440
621
|
// With no includes the default is ALL configurations (captures build/tooling deps).
|
|
@@ -618,8 +799,13 @@ object SocketFactsPlugin extends AutoPlugin {
|
|
|
618
799
|
var project = ""
|
|
619
800
|
// External artifact's resolved jar(s); --with-files only.
|
|
620
801
|
val targets = mutable.TreeSet.empty[String]
|
|
802
|
+
// An expected artifact was not fetched; --with-files only.
|
|
803
|
+
var unfetched = false
|
|
621
804
|
}
|
|
622
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
|
+
|
|
623
809
|
private final class RootTree(
|
|
624
810
|
val projectKey: String,
|
|
625
811
|
val config: String,
|