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
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# FOSSA CLI Changelog

## Unreleased

- Scala/sbt: Route projects using `addDependencyTreePlugin` (sbt 1.4+) to the correct `dependencyBrowseTreeHTML` task, restoring deep dependencies. ([#1711](https://github.com/fossas/fossa-cli/pull/1711))

## 3.17.11

- Conan: Handle list-valued license in make_fossa_deps_conan ([#1719](https://github.com/fossas/fossa-cli/pull/1719)).
Expand Down
22 changes: 13 additions & 9 deletions src/Strategy/Scala.hs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import Strategy.Maven.Pom.PomFile (RawPom (rawPomArtifact, rawPomGroup, rawPomVe
import Strategy.Maven.Pom.Resolver (buildGlobalClosure)
import Strategy.Scala.Common (mkSbtCommand)
import Strategy.Scala.Errors (FailedToListProjects (FailedToListProjects), MaybeWithoutDependencyTreeTask (..), MissingFullDependencyPlugin (..), sbtDepsGraphPluginUrl, scalaFossaDocUrl)
import Strategy.Scala.Plugin (genTreeJson, hasDependencyPlugins)
import Strategy.Scala.Plugin (DependencyPluginsDetected (..), genTreeJson, hasDependencyPlugins)
import Strategy.Scala.SbtDependencyTree (SbtArtifact (SbtArtifact), analyze, sbtDepTreeCmd)
import Strategy.Scala.SbtDependencyTreeJson qualified as TreeJson
import Types (
Expand Down Expand Up @@ -161,10 +161,10 @@ findProjects = walkWithFilters' $ \dir _ files -> do
. context ("Listing sbt projects at " <> pathToText dir)
$ genPoms dir

(miniDepPlugin, depPlugin) <- hasDependencyPlugins dir
case (projectsRes, miniDepPlugin, depPlugin) of
DependencyPluginsDetected{hasMiniDependencyTreePlugin, dependencyTreePlugin} <- hasDependencyPlugins dir
case (projectsRes, hasMiniDependencyTreePlugin, dependencyTreePlugin) of
(Nothing, _, _) -> pure ([], WalkSkipAll)
(Just projects, False, False) -> pure ([SbtTargets Nothing [] projects], WalkSkipAll)
(Just projects, False, Nothing) -> pure ([SbtTargets Nothing [] projects], WalkSkipAll)
(Just projects, True, _) -> do
-- project is using miniature dependency tree plugin,
-- which is included by default with sbt 1.4+
Expand All @@ -184,9 +184,13 @@ findProjects = walkWithFilters' $ \dir _ files -> do
(True, _) -> pure ([SbtTargets Nothing [] projects], WalkSkipAll)
(_, Just _) -> pure ([SbtTargets depTreeStdOut [] projects], WalkSkipAll)
(_, _) -> pure ([], WalkSkipAll)
(Just projects, False, True) -> do
-- project is explicitly configured to use dependency-tree-plugin
treeJSONs <- recover $ genTreeJson dir
(Just projects, False, Just pluginKind) -> do
-- project is explicitly configured to use dependency-tree-plugin.
-- The casing of the dependencyBrowseTree task differs between the
-- modern (sbt 1.4+) DependencyTreePlugin and the legacy
-- net.virtualvoid sbt-dependency-graph plugin; pluginKind selects
-- the right one. See TKT-15490 / ANE-2718.
treeJSONs <- recover $ genTreeJson pluginKind dir
pure ([SbtTargets Nothing (fromMaybe [] treeJSONs) projects], WalkSkipAll)

analyzeWithPoms :: (Has Diagnostics sig m) => ScalaProject -> m DependencyResults
Expand All @@ -199,13 +203,13 @@ analyzeWithPoms (ScalaProject _ _ closure) = context "Analyzing sbt dependencies
}

analyzeWithDepTreeJson :: (Has ReadFS sig m, Has Diagnostics sig m) => ScalaProject -> m DependencyResults
analyzeWithDepTreeJson (ScalaProject _ treeJson closure) = context "Analyzing sbt dependencies using dependencyBrowseTreeHTML" $ do
analyzeWithDepTreeJson (ScalaProject _ treeJson closure) = context "Analyzing sbt dependencies using dependency tree JSON" $ do
treeJson' <-
errCtx MissingFullDependencyPluginCtx $
errHelp MissingFullDependencyPluginHelp $
errDoc sbtDepsGraphPluginUrl $
errDoc scalaFossaDocUrl $
fromMaybeText "Could not retrieve output from sbt dependencyBrowseTreeHTML" treeJson
fromMaybeText "Could not retrieve dependency tree JSON output from sbt" treeJson
projectGraph <- TreeJson.analyze treeJson'
pure $
DependencyResults
Expand Down
120 changes: 95 additions & 25 deletions src/Strategy/Scala/Plugin.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ module Strategy.Scala.Plugin (
hasDependencyPlugins,
detectDependencyPlugins,
genTreeJson,
DependencyTreePluginKind (..),
DependencyPluginsDetected (..),
) where

import Control.Effect.Diagnostics (Diagnostics, fatalText)
import Control.Effect.Stack (context)
import Data.List (find)
import Data.Maybe (mapMaybe)
import Data.String.Conversion (ConvertUtf8 (decodeUtf8), toString)
import Data.Text (Text)
Expand All @@ -22,45 +25,112 @@ import Effect.Exec (
import Path (Abs, Dir, File, Path, mkRelFile, parent, parseAbsFile, (</>))
import Strategy.Scala.Common (mkSbtCommand)

-- | Which non-mini dependency-tree plugin (if any) the project has installed.
--
-- The two plugins differ in the casing of their @dependencyBrowseTree@ task.
-- See 'mkDependencyBrowseTreeCmd' for the command names.
data DependencyTreePluginKind
= -- | @sbt.plugins.DependencyTreePlugin@. Built into sbt 1.4+ and enabled
-- explicitly via @addDependencyTreePlugin@ in @plugins.sbt@. Provides the
-- uppercase @dependencyBrowseTreeHTML@ task.
ModernDependencyTreePlugin
| -- | @net.virtualvoid.sbt.graph.DependencyGraphPlugin@. The third-party
-- @sbt-dependency-graph@ plugin used on sbt < 1.4. Provides the lowercase
-- @dependencyBrowseTreeHtml@ task.
LegacyDependencyGraphPlugin
deriving (Eq, Ord, Show)

-- | What the @sbt plugins@ output told us about dependency-tree plugins.
data DependencyPluginsDetected = DependencyPluginsDetected
{ hasMiniDependencyTreePlugin :: Bool
, dependencyTreePlugin :: Maybe DependencyTreePluginKind
}
deriving (Eq, Ord, Show)

-- | Returns list of plugins used by sbt.
-- Ref: https://www.scala-sbt.org/1.x/docs/Plugins.html
getPlugins :: Command
getPlugins = mkSbtCommand "plugins"

-- | Returns (hasMiniDependencyTreePlugin, hasDependencyTreePlugin) by running sbt plugins.
hasDependencyPlugins :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m (Bool, Bool)
-- | Detect which dependency-tree plugins are loaded by running @sbt plugins@.
hasDependencyPlugins :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m DependencyPluginsDetected
hasDependencyPlugins projectDir = do
stdoutText <- (TextLazy.toStrict . decodeUtf8) <$> context "Identifying plugins" (execThrow projectDir getPlugins)
pure $ detectDependencyPlugins stdoutText

-- | Detect dependency plugins from sbt plugins output.
-- Returns (hasMiniDependencyTreePlugin, hasDependencyTreePlugin).
detectDependencyPlugins :: Text -> (Bool, Bool)
-- | Classify dependency-tree plugins from the @sbt plugins@ output.
--
-- The plugin names mapped here:
--
-- * @sbt.plugins.MiniDependencyTreePlugin@ — bundled with sbt 1.4+, gives
-- us the @dependencyTree@ task used by 'Strategy.Scala.SbtDependencyTree'.
-- * @sbt.plugins.DependencyTreePlugin@ — opt-in on sbt 1.4+ via
-- @addDependencyTreePlugin@. Provides the uppercase
-- @dependencyBrowseTreeHTML@ task.
-- * @net.virtualvoid.sbt.graph.DependencyGraphPlugin@ — third-party plugin
-- used on sbt < 1.4. Provides the lowercase @dependencyBrowseTreeHtml@
-- task.
--
-- @sbt plugins@ groups its output into per-project @Enabled plugins in
-- \<project\>:@ sections, listing one bare plugin FQCN per indented line,
-- followed by a trailing @Plugins that are loaded to the build but not enabled
-- in any subprojects:@ section. A plugin the user disabled via
-- @disablePlugins(...)@ moves into that trailing section. We therefore search
-- only the text *before* that marker, so a disabled (loaded-but-not-enabled)
-- plugin is not mistaken for an active one.
--
-- The plugin FQCN appears on its own line with no @: enabled in@ suffix. An
-- earlier attempt to anchor detection on such a suffix matched nothing in real
-- sbt output, which silently dropped deep dependencies and fell back to poms
-- (regressed TKT-15490). See @test/Scala/PluginSpec.hs@ for fixtures captured
-- from actual @sbt -batch -no-colors plugins@ runs.
--
-- When both modern and legacy non-mini plugins are present we prefer the
-- modern one (sbt 1.4+ wins) since legacy plugin presence on a modern sbt
-- typically means the user has both kinds of declarations in their build.
detectDependencyPlugins :: Text -> DependencyPluginsDetected
detectDependencyPlugins stdoutText =
( Text.count ".MiniDependencyTreePlugin" stdoutText > 0
, Text.count ".DependencyTreePlugin" stdoutText > 0
|| Text.count "net.virtualvoid.sbt.graph.DependencyGraphPlugin" stdoutText > 0 -- sbt < 1.4
)
DependencyPluginsDetected
{ hasMiniDependencyTreePlugin = enabled "sbt.plugins.MiniDependencyTreePlugin"
, dependencyTreePlugin = snd <$> find (enabled . fst) treePlugins
}
where
enabledSection = fst $ Text.breakOn notEnabledMarker stdoutText
notEnabledMarker = "Plugins that are loaded to the build but not enabled"
enabled name = name `Text.isInfixOf` enabledSection
treePlugins =
[ ("sbt.plugins.DependencyTreePlugin", ModernDependencyTreePlugin)
, ("net.virtualvoid.sbt.graph.DependencyGraphPlugin", LegacyDependencyGraphPlugin)
]

-- | Generates Dependency Trees.
-- Ref: https://github.com/sbt/sbt/blob/master/main/src/main/scala/sbt/plugins/DependencyTreeSettings.scala#L101
--
-- This command unlike 'dependencyBrowseTree', does not open
-- the browser when executed.
-- | The sbt task that writes @tree.html@/@tree.json@ alongside its dependency
-- output. Plugin name vs task casing:
--
-- It writes following files in target directory:
-- ./tree.json
-- ./tree.html
-- ./tree.data.js
-- * 'ModernDependencyTreePlugin' (sbt 1.4+, @addDependencyTreePlugin@) →
-- @dependencyBrowseTreeHTML@.
-- * 'LegacyDependencyGraphPlugin' (sbt < 1.4, @sbt-dependency-graph@) →
-- @dependencyBrowseTreeHtml@.
--
-- This command is only used when the plugin is installed explicitly, i.e. sbt < 1.4.
-- Newer versions of sbt will use the built-in dependency graph plugin.
mkDependencyBrowseTreeHTMLCmd :: Command
mkDependencyBrowseTreeHTMLCmd = mkSbtCommand "dependencyBrowseTreeHtml"
-- Picking the wrong casing produces an sbt error like
-- @[error] Not a valid command: dependencyBrowseTreeHTML@ which surfaces to
-- the user as "Could not retrieve output from sbt dependencyBrowseTreeHTML".
-- That regression (CLI 3.8.30) is tracked under TKT-15490 / ANE-2718.
mkDependencyBrowseTreeCmd :: DependencyTreePluginKind -> Command
mkDependencyBrowseTreeCmd ModernDependencyTreePlugin = mkSbtCommand "dependencyBrowseTreeHTML"
mkDependencyBrowseTreeCmd LegacyDependencyGraphPlugin = mkSbtCommand "dependencyBrowseTreeHtml"

genTreeJson :: (Has Exec sig m, Has Diagnostics sig m) => Path Abs Dir -> m [Path Abs File]
genTreeJson projectDir = do
stdoutBL <- context "Generating dependency tree html" $ execThrow projectDir mkDependencyBrowseTreeHTMLCmd
-- | Generates dependency trees by invoking the appropriate
-- @dependencyBrowseTree*@ task. Unlike @dependencyBrowseTree@, this does not
-- open a browser when executed.
--
-- It writes the following files in the target directory:
--
-- * @./tree.json@
-- * @./tree.html@
-- * @./tree.data.js@
genTreeJson :: (Has Exec sig m, Has Diagnostics sig m) => DependencyTreePluginKind -> Path Abs Dir -> m [Path Abs File]
genTreeJson pluginKind projectDir = do
stdoutBL <- context "Generating dependency tree html" $ execThrow projectDir (mkDependencyBrowseTreeCmd pluginKind)

-- stdout for "sbt dependencyBrowseTreeHTML" looks like:
-- -
Expand Down
Loading
Loading