Skip to content

Commit bd48d80

Browse files
committed
Merge remote-tracking branch 'origin/master' into fix-sbt-dependency-tree-html-routing
# Conflicts: # Changelog.md
2 parents 36a9927 + 52c2ae8 commit bd48d80

16 files changed

Lines changed: 354 additions & 57 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Changelog.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,33 @@
44

55
- 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))
66

7+
## 3.17.11
8+
9+
- Conan: Handle list-valued license in make_fossa_deps_conan ([#1719](https://github.com/fossas/fossa-cli/pull/1719)).
10+
- Strip non printable characters from locators ([#1720](https://github.com/fossas/fossa-cli/pull/1720)).
11+
- Container scanning: Support scanning /var/lib/dpkg/status.d directory ([#1721](https://github.com/fossas/fossa-cli/pull/1721)).
12+
13+
## 3.17.10
14+
15+
- Licensing: Fix bad SPL matches ([#1717](https://github.com/fossas/fossa-cli/pull/1717)).
16+
17+
## 3.17.9
18+
19+
- Cargo: Fix transitive dev-dependency classification. Dependencies reachable only through dev-dep or build-dep roots are now correctly labeled as Development instead of Production ([#1692](https://github.com/fossas/fossa-cli/pull/1692)).
20+
21+
## 3.17.8
22+
23+
- Vendored dependencies: archive uploads with an absolute `path` (as produced by the meta-fossa Yocto layer) no longer crash with a `permission denied` error while writing the tarball. ([#1713](https://github.com/fossas/fossa-cli/pull/1713))
24+
25+
## 3.17.7
26+
27+
- NuGet: PackageReference discovery now analyzes every `.csproj`/`.xproj`/`.vbproj`/`.dbproj`/`.fsproj` in a directory. Previously only the first match returned by the directory listing was analyzed, so sibling project files were silently dropped. ([#1712](https://github.com/fossas/fossa-cli/pull/1712))
28+
729
## 3.17.6
830

931
- Config: `paths.only` and `paths.exclude` in `.fossa.yml` now accept glob patterns. ([#1703](https://github.com/fossas/fossa-cli/pull/1703))
1032
- Licensing - Fix two bad GPL matches [No PR]
1133

12-
1334
## 3.17.5
1435

1536
- Vendetta: Debug bundles now include per-file component match data from Vendetta scans, making it easier to diagnose why a vendored dependency was or wasn't detected. ([#1706](https://github.com/fossas/fossa-cli/pull/1706))
@@ -30,8 +51,7 @@
3051
- Poetry: Support PEP 621 `[project].dependencies` for Poetry 2.x projects. Production dependencies declared in the standard `[project]` section are now correctly detected alongside legacy `[tool.poetry.dependencies]`. ([#1683](https://github.com/fossas/fossa-cli/pull/1683))
3152

3253
## 3.17.1
33-
34-
- Node.js: Yarn and npm workspace packages now appear as individual build targets (e.g. `yarn@./:my-package`, `npm@./:my-package`), enabling per-package dependency scoping via `.fossa.yml`.
54+
- Node.js: Yarn and npm workspace packages now appear as individual build targets (e.g. `yarn@./:my-package`, `npm@./:my-package`), enabling per-package dependency scoping via `.fossa.yml`. ([#1643](https://github.com/fossas/fossa-cli/pull/1643))
3555
- Project edit: Fix 500 error when running `fossa project edit --policy` on existing projects ([#1688](https://github.com/fossas/fossa-cli/pull/1688))
3656
- UV: Add `directory` source type to uv.lock parser, fixing parse failures on projects with local directory dependencies ([#1690](https://github.com/fossas/fossa-cli/pull/1690))
3757

docs/walkthroughs/make_fossa_deps_conan.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,29 @@ def name_version_of(label: str) -> Tuple[str, str]:
107107
name, version = label.split("/", 1)
108108
return name, version
109109

110+
# Conan recipes may declare `license` as a single string ("MIT") or as a list/tuple of
111+
# strings (["MIT", "Apache-2.0"]). The fossa-deps `license` field must be a single string,
112+
# so a list is joined into one SPDX expression. We use " AND " (every license's obligations
113+
# apply) as the conservative default; change MULTI_LICENSE_JOINER to " OR " if your packages
114+
# are dual-licensed (consumer's choice).
115+
MULTI_LICENSE_JOINER = " AND "
116+
117+
# fossa-deps requires a license string for every custom dependency. When a Conan recipe
118+
# declares no license, fall back to the SPDX "NOASSERTION" marker so the file stays valid;
119+
# emitting a bare `license: null` triggers: expected String, but encountered Null.
120+
NO_LICENSE = "NOASSERTION"
121+
110122
def license_of(node: dict) -> Optional[str]:
111-
return node.get("license")
123+
raw = node.get("license")
124+
if raw is None:
125+
return None
126+
if isinstance(raw, str):
127+
return raw or None
128+
if isinstance(raw, (list, tuple)):
129+
parts = [str(item).strip() for item in raw if item is not None and str(item).strip()]
130+
return MULTI_LICENSE_JOINER.join(parts) if parts else None
131+
# Unexpected shape (number, dict, ...): coerce to a string so fossa-deps stays valid.
132+
return str(raw)
112133

113134
def homepage_of(node: dict) -> Optional[str]:
114135
candidate = node.get("homepage")
@@ -158,7 +179,7 @@ def mk_fossa_deps(graph):
158179
vendored_deps.append(FossaVendorDep(name, version, src_dir))
159180
else:
160181
logging.info(f"could not find source code in disk for: {label}, using this as vendored dependency for fossa-deps")
161-
custom_deps.append(FossaCustomDep(name, version, license, FossaCustomDepMetadata(homepage, description)))
182+
custom_deps.append(FossaCustomDep(name, version, license or NO_LICENSE, FossaCustomDepMetadata(homepage, description)))
162183

163184
fossa_dep_yml = FossaDep(vendored_deps, custom_deps)
164185
fossa_dep_yml.dump()

integration-test/Analysis/NugetSpec.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ testServiceStackForPkgReferences =
4646
aroundAll (withAnalysisOf NonStrict $ serviceStack NuGet.discover) $ do
4747
describe "ServiceStack" $ do
4848
it "should find targets" $ \(result, _) -> do
49-
length result `shouldBe` 64
49+
length result `shouldBe` 92
5050

5151
testServiceStackForPkgConfig :: Spec
5252
testServiceStackForPkgConfig =

spectrometer.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@ test-suite unit-tests
692692
Node.PackageLockSpec
693693
Node.PackageLockV3Spec
694694
NuGet.DirectoryPackagesPropsSpec
695+
NuGet.NuGetSpec
695696
NuGet.NuspecSpec
696697
NuGet.PackageReferenceSpec
697698
NuGet.PackagesConfigSpec

src/App/Fossa/VendoredDependency.hs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module App.Fossa.VendoredDependency (
77
vendoredDepToLocator,
88
forceVendoredToArchive,
99
compressFile,
10+
safeSeparators,
1011
hashFile,
1112
hashBs,
1213
dedupVendoredDeps,
@@ -50,7 +51,8 @@ import Fossa.API.Types (
5051
import Path (Abs, Dir, Path)
5152
import Prettyprinter (Pretty (pretty), vsep)
5253
import Srclib.Types (Locator (..), ProvidedPackageLabel)
53-
import System.FilePath.Posix (splitDirectories, (</>))
54+
import System.FilePath (dropDrive, splitDirectories)
55+
import System.FilePath.Posix ((</>))
5456

5557
data VendoredDependency = VendoredDependency
5658
{ vendoredName :: Text
@@ -197,8 +199,11 @@ hashFile fileToHash = do
197199
fileContent <- BS.readFile fileToHash
198200
pure . toText . show $ md5 fileContent
199201

202+
-- Flatten a path into a single filename component. We use `dropDrive` to
203+
-- ensure the result is relative, since callers join it with `(</>)`, which
204+
-- discards the LHS when the RHS is absolute.
200205
safeSeparators :: FilePath -> FilePath
201-
safeSeparators = intercalate "_" . splitDirectories
206+
safeSeparators = intercalate "_" . splitDirectories . dropDrive
202207

203208
skippedDepsDebugLog :: NeedScanningDeps -> SkippableDeps -> VendoredDependencyScanMode -> SkippedDepsLogMsg
204209
skippedDepsDebugLog needScanningDeps skippedDeps scanMode =

src/Srclib/Types.hs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module Srclib.Types (
4141
) where
4242

4343
import Data.Aeson
44+
import Data.Char qualified as Char
4445
import Data.List.NonEmpty (NonEmpty ((:|)))
4546
import Data.List.NonEmpty qualified as NE
4647
import Data.Map (Map)
@@ -497,7 +498,10 @@ instance ToText Locator where
497498

498499
renderLocator :: Locator -> Text
499500
renderLocator Locator{..} =
500-
locatorFetcher <> "+" <> locatorProject <> "$" <> fromMaybe "" locatorRevision
501+
stripNonPrintable $
502+
locatorFetcher <> "+" <> locatorProject <> "$" <> fromMaybe "" locatorRevision
503+
where
504+
stripNonPrintable = Text.filter Char.isPrint
501505

502506
-- The projectId is the full locator of the project. E.g. custom+123/someProject (<fetcher>+<orgId>/<project-name>)
503507
projectId :: Locator -> Text

src/Strategy/Cargo.hs

Lines changed: 79 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ import Data.Foldable (for_, traverse_)
5252
import Data.Functor (void)
5353
import Data.List.NonEmpty qualified as NonEmpty
5454
import Data.Map.Strict qualified as Map
55-
import Data.Maybe (catMaybes, fromMaybe, isJust)
55+
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
5656
import Data.Set (Set)
57+
import Data.Set qualified as Set
5758
import Data.String.Conversion (toString, toText)
5859
import Data.Text (Text, breakOn)
5960
import Data.Text qualified as Text
@@ -74,7 +75,6 @@ import Effect.Exec (
7475
execThrow,
7576
)
7677
import Effect.Grapher (
77-
LabeledGrapher,
7878
direct,
7979
edge,
8080
label,
@@ -485,36 +485,88 @@ toDependency emitGitBackedLocators sourceMap pkg =
485485
extractGitCommitHash sourceUrl
486486
_ -> Nothing
487487

488-
-- Possible values here are "build", "dev", and null.
489-
-- Null refers to productions, while dev and build refer to development-time dependencies
490-
-- Cargo does not differentiate test dependencies and dev dependencies,
491-
-- so we just simplify it to Development.
492-
kindToLabel :: Maybe Text.Text -> CargoLabel
493-
kindToLabel (Just _) = CargoDepKind EnvDevelopment
494-
kindToLabel Nothing = CargoDepKind EnvProduction
495-
496-
addLabel :: Has (LabeledGrapher PackageId CargoLabel) sig m => NodeDependency -> m ()
497-
addLabel dep = do
498-
let packageId = nodePkg dep
499-
traverse_ (label packageId . kindToLabel . nodeDepKind) $ nodeDepKinds dep
500-
501-
addEdge :: Has (LabeledGrapher PackageId CargoLabel) sig m => ResolveNode -> m ()
502-
addEdge node = do
503-
let parentId = resolveNodeId node
504-
for_ (resolveNodeDeps node) $ \dep -> do
505-
addLabel dep
506-
edge parentId $ nodePkg dep
507-
488+
-- A Cargo edge's kind ("build", "dev", or null) reflects the parent's manifest
489+
-- declaration, not the path taken to reach the parent. We classify each package
490+
-- by which workspace-rooted paths can reach it:
491+
--
492+
-- * Production: reachable from a workspace member via a path of null-kind
493+
-- edges only. These packages are linked into the release artifact.
494+
--
495+
-- * Development: any package reachable (via any edge) from the target of a
496+
-- non-null-kind edge. A "build" or "dev" edge marks the start of a subtree
497+
-- that never ships in the release binary, and every descendant of that
498+
-- subtree inherits Development.
499+
--
500+
-- A package can carry both labels when it's reachable by both kinds of paths.
501+
--
502+
-- We do not need a separate "dev-deps of prod-deps" case: Cargo only resolves
503+
-- dev-dependencies for workspace members, so non-workspace edges with kind
504+
-- "dev" do not appear in 'cargo metadata' output. The only non-null kind we
505+
-- see on a non-workspace edge is "build".
506+
--
507+
-- Cargo is the only strategy with per-edge kinds; others (pnpm, yarn, poetry)
508+
-- label nodes and propagate with 'hydrateDepEnvs'. That helper walks from a
509+
-- labeled node to every dependency it declares, regardless of edge kind, so
510+
-- a Production label on a workspace member would flow through a "dev" or
511+
-- "build" edge and mislabel the dev/build subtree as Production. We roll our
512+
-- own edge-filtered reachability here rather than generalize the shared helper.
508513
buildGraph :: Bool -> CargoMetadata -> Graphing Dependency
509-
-- By construction, workspace members are the root nodes in the graph.
510-
-- Use shrinkRoots to remove them and promote their direct dependencies to the
511-
-- direct dependencies we report for the project.
512514
buildGraph emitGitBackedLocators meta = shrinkRoots $
513515
run . withLabeling (toDependency emitGitBackedLocators sourceMap) $ do
514-
traverse_ direct $ metadataWorkspaceMembers meta
515-
traverse_ addEdge $ resolvedNodes $ metadataResolve meta
516+
traverse_ direct (metadataWorkspaceMembers meta)
517+
for_ nodes $ \node ->
518+
for_ (resolveNodeDeps node) $ \dep ->
519+
edge (resolveNodeId node) (nodePkg dep)
520+
for_ (Set.toList prodReachable) $ \pkg ->
521+
label pkg (CargoDepKind EnvProduction)
522+
for_ (Set.toList devReachable) $ \pkg ->
523+
label pkg (CargoDepKind EnvDevelopment)
516524
where
517525
sourceMap = buildPackageSourceMap $ metadataPackages meta
526+
nodes = resolvedNodes (metadataResolve meta)
527+
workspaceMembers = Set.fromList (metadataWorkspaceMembers meta)
528+
529+
-- These predicates are not mutually exclusive: a dep declared in both
530+
-- [dependencies] and [dev-dependencies] on the same parent carries both
531+
-- a null and a non-null kind, so the edge feeds prodAdj *and* devSeeds.
532+
isProdEdge dep = any (isNothing . nodeDepKind) (nodeDepKinds dep)
533+
isDevEdge dep = any (isJust . nodeDepKind) (nodeDepKinds dep)
534+
535+
-- Adjacency containing only edges whose parent declares the child as a
536+
-- normal dependency (at least one kind is null). Production reachability
537+
-- must only traverse these — a build or dev edge breaks the release chain.
538+
prodAdj =
539+
Map.fromList $
540+
map
541+
(\node -> (resolveNodeId node, map nodePkg . filter isProdEdge . resolveNodeDeps $ node))
542+
nodes
543+
544+
-- Every edge in the metadata graph, for Development reachability.
545+
allAdj =
546+
Map.fromList $
547+
map (\node -> (resolveNodeId node, map nodePkg (resolveNodeDeps node))) nodes
548+
549+
-- Targets of any non-null-kind edge. Each seeds a Development subtree:
550+
-- the target and all its transitive descendants are never linked into
551+
-- a release build.
552+
devSeeds =
553+
Set.fromList $
554+
map nodePkg $
555+
concatMap (filter isDevEdge . resolveNodeDeps) nodes
556+
557+
prodReachable = reachable prodAdj workspaceMembers
558+
devReachable = reachable allAdj devSeeds
559+
560+
reachable :: Map.Map PackageId [PackageId] -> Set PackageId -> Set PackageId
561+
reachable adj = go Set.empty . Set.toList
562+
where
563+
go visited [] = visited
564+
go visited (x : xs) =
565+
if Set.member x visited
566+
then go visited xs
567+
else
568+
let children = fromMaybe [] (Map.lookup x adj)
569+
in go (Set.insert x visited) (children ++ xs)
518570

519571
-- | Custom Parsec type alias
520572
type PkgSpecParser a = Parsec Void Text a

src/Strategy/Dpkg.hs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Container.OsRelease (OsInfo)
99
import Control.Effect.Diagnostics (Diagnostics)
1010
import Control.Effect.Reader (Reader)
1111
import Data.Aeson (ToJSON)
12+
import Data.Foldable (find)
1213
import Data.String.Conversion (toText)
1314
import Data.Text qualified as Text
1415
import Discovery.Filters (AllFilters)
@@ -18,7 +19,7 @@ import Discovery.Walk (
1819
findFileNamed,
1920
walkWithFilters',
2021
)
21-
import Effect.ReadFS (Has, ReadFS)
22+
import Effect.ReadFS (Has, ReadFS, listDir)
2223
import GHC.Generics (Generic)
2324
import Path (Abs, Dir, File, Path, toFilePath)
2425
import Strategy.Dpkg.Database (analyze)
@@ -59,13 +60,19 @@ findProjects ::
5960
OsInfo ->
6061
Path Abs Dir ->
6162
m [DpkgDatabase]
62-
findProjects osInfo = walkWithFilters' $ \dir _ files -> do
63-
case findFileNamed "status" files of
64-
Nothing -> pure ([], WalkContinue)
65-
Just file -> do
66-
if (Text.isInfixOf "var/lib/dpkg/" $ toText . toFilePath $ file)
67-
then pure ([DpkgDatabase dir file osInfo], WalkContinue)
68-
else pure ([], WalkContinue)
63+
findProjects osInfo = walkWithFilters' $ \dir dirs files -> do
64+
let standardDBs = case findFileNamed "status" files of
65+
Just file ->
66+
if Text.isInfixOf "var/lib/dpkg/" (toText . toFilePath $ file)
67+
then [DpkgDatabase dir file osInfo]
68+
else []
69+
Nothing -> []
70+
statusD_DBs <- case find (\f -> toFilePath f == "var/lib/dpkg/status.d/") dirs of
71+
Just dir' -> do
72+
(_, filesInDir) <- listDir dir'
73+
pure $ map (\file -> DpkgDatabase dir' file osInfo) (filter (not . Text.isSuffixOf ".md5sums" . toText) filesInDir)
74+
Nothing -> pure []
75+
pure (standardDBs ++ statusD_DBs, WalkContinue)
6976

7077
mkProject :: DpkgDatabase -> DiscoveredProject DpkgDatabase
7178
mkProject project =

0 commit comments

Comments
 (0)