Skip to content
This repository has been archived by the owner on Apr 1, 2022. It is now read-only.

Adds support for git sources in Podfile.lock tactic #345

Merged
merged 2 commits into from
Aug 27, 2021
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 @@
# Spectrometer Changelog

## v2.15.9

- CocoaPods: Supports git sources in `Podfile.lock` analysis. ([#345](https://github.com/fossas/spectrometer/pull/345))

## v2.15.8

- `fossa analyze --experimental-enable-monorepo` now turns off proprietary language scanning by default, and has this feature controlled by a feature flag ([#343](https://github.com/fossas/spectrometer/pull/343))
Expand Down
9 changes: 8 additions & 1 deletion docs/strategies/ios/cocoapods.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ DEPENDENCIES:
- one (> 4.4)
- three (from `Submodules/subproject/.podspec`)
- "five/+zlib (7.0.0)"
```
```

## Limitations

- Pods sourced from local path are not supported (e.g. `pod 'AFNetworking', :path => '~/Documents/AFNetworking'`).
- Pods sourced from http path are not supported (e.g `pod 'JSONKit', :podspec => 'https://example.com/JSONKit.podspec'`).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would it take to support these? We have the url fetcher that we use with yarn analysis. These look more complex though, correct me if I'm wrong but we would have to download the podspec file during analysis and then parse that file to get the dependency information?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can still create the graph, but we would need to download the file to infer the licence informations. It also may require downloading of additional content based on the the podspec file for that purpose.

It is likely the least used source, I think we can ignore it for now, until there is an ask for it. It may involve a bit of work, on the core backend.

- Pods sourced from subversion, mercurial, and bazaar are not supported.
- Plugins in Podfiles are ignored.
102 changes: 71 additions & 31 deletions src/Strategy/Cocoapods/PodfileLock.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ module Strategy.Cocoapods.PodfileLock (
PodLock (..),
Dep (..),
Pod (..),
Section (..),
toSections,
ExternalSource (..),
ExternalGitSource (..),
) where

import Control.Effect.Diagnostics (
Expand All @@ -14,29 +14,32 @@ import Control.Effect.Diagnostics (
context,
run,
)
import Data.Aeson (FromJSON (parseJSON), (.:))
import Data.Aeson (FromJSON (parseJSON))
import Data.Aeson.Types (FromJSONKey)
import Data.Char qualified as C
import Data.Foldable (traverse_)
import Data.Foldable (asum, traverse_)
import Data.Functor (void)
import Data.HashMap.Lazy qualified as HashMap (toList)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Text (Text)
import Data.Void (Void)
import Data.Yaml ((.:), (.:?))
import Data.Yaml qualified as Yaml
import DepTypes (DepType (PodType), Dependency (..), VerConstraint (CEq))
import DepTypes (DepType (GitType, PodType), Dependency (..), VerConstraint (CEq))
import Effect.Grapher (LabeledGrapher, direct, edge, label, withLabeling)
import Effect.ReadFS (ReadFS, readContentsYaml)
import Graphing (Graphing)
import Options.Applicative (Alternative ((<|>)))
import Path
import Text.Megaparsec (MonadParsec (takeWhileP), Parsec, between, empty, errorBundlePretty, parse, some, takeWhile1P)
import Text.Megaparsec.Char (char)
import Text.Megaparsec.Char.Lexer qualified as L

analyze' :: (Has ReadFS sig m, Has Diagnostics sig m) => Path Abs File -> m (Graphing Dependency)
analyze' file = do
podfileLock <- toSections <$> readContentsYaml file
podfileLock <- readContentsYaml file
context "Building dependency graph" $ pure (buildGraph podfileLock)

newtype PodfilePkg = PodfilePkg {pkgName :: Text}
Expand All @@ -45,42 +48,64 @@ newtype PodfilePkg = PodfilePkg {pkgName :: Text}
type PodfileGrapher = LabeledGrapher PodfilePkg PodfileLabel

newtype PodfileLabel
= PodfileVersion Text
= PodfileVersion (Maybe Text)
deriving (Eq, Ord, Show)

toDependency :: PodfilePkg -> Set PodfileLabel -> Dependency
toDependency pkg = foldr applyLabel start
toDependency :: Map Text ExternalSource -> PodfilePkg -> Set PodfileLabel -> Dependency
toDependency externalSrc pkg = foldr applyLabel start
where
start :: Dependency
start =
Dependency
{ dependencyType = PodType
, dependencyName = pkgName pkg
{ dependencyType = depType
, dependencyName = depName
, dependencyVersion = Nothing
, dependencyLocations = []
, dependencyEnvironments = []
, dependencyTags = Map.empty
}

depType :: DepType
depType = case Map.lookup (pkgName pkg) externalSrc of
Just (ExternalGitType _) -> GitType
_ -> PodType

depName :: Text
depName = case Map.lookup (pkgName pkg) externalSrc of
Just (ExternalGitType gitSrc) -> urlOf gitSrc
_ -> pkgName pkg

applyLabel :: PodfileLabel -> Dependency -> Dependency
applyLabel (PodfileVersion ver) dep = dep{dependencyVersion = Just (CEq ver)}
applyLabel (PodfileVersion ver) dep = dep{dependencyVersion = CEq <$> ver}

buildGraph :: [Section] -> Graphing Dependency
buildGraph sections =
run . withLabeling toDependency $
buildGraph :: PodLock -> Graphing Dependency
buildGraph lockContent =
run . withLabeling (toDependency $ lockExternalSources lockContent) $
traverse_ addSection sections
where
sections :: [Section]
sections = toSections lockContent

addSection :: Has PodfileGrapher sig m => Section -> m ()
addSection (DependencySection deps) = traverse_ (direct . PodfilePkg . depName) deps
addSection (PodSection pods) = traverse_ addSpec pods

addSpec :: Has PodfileGrapher sig m => Pod -> m ()
addSpec pod = do
let pkg = PodfilePkg (podName pod)
let pkgVersion = case Map.lookup (podName pod) (lockExternalSources lockContent) of
Just (ExternalGitType src) ->
asum
[ tagOf src
, commitOf src
, branchOf src
]
_ -> Just $ podVersion pod

-- add edges between spec and specdeps
traverse_ (edge pkg . PodfilePkg . depName) (podSpecs pod)
traverse_ (edge pkg . PodfilePkg . depName) $ podSpecs pod
-- add a label for version
label pkg (PodfileVersion (podVersion pod))
label pkg $ PodfileVersion pkgVersion

type Parser = Parsec Void Text

Expand All @@ -89,9 +114,23 @@ data Section
| DependencySection [Dep]
deriving (Eq, Ord, Show)

data ExternalSource
= ExternalGitType ExternalGitSource
| ExternalOtherType
deriving (Show, Eq, Ord)

data ExternalGitSource = ExternalGitSource
{ urlOf :: Text
, tagOf :: Maybe Text
, commitOf :: Maybe Text
, branchOf :: Maybe Text
}
deriving (Eq, Ord, Show)

data PodLock = PodLock
{ lockPods :: [Pod]
, lockDeps :: [Dep]
, lockExternalSources :: Map Text ExternalSource
}
deriving (Show, Eq, Ord)

Expand All @@ -104,13 +143,7 @@ toSections lockContent =
newtype Dep = Dep
{ depName :: Text
}
deriving (Eq, Ord, Show)

data SourceDep = SourceDep
{ sDepName :: Text
, tags :: Map Text Text
}
deriving (Eq, Ord, Show)
deriving (Eq, Ord, Show, FromJSONKey)

data Pod = Pod
{ podName :: Text
Expand All @@ -119,12 +152,6 @@ data Pod = Pod
}
deriving (Eq, Ord, Show)

data Remote = Remote
{ remoteLocation :: Text
, remoteDeps :: [Dep]
}
deriving (Eq, Ord, Show)

parseName :: Parser Text
parseName = lexeme $ takeWhile1P (Just "dep") (not . C.isSpace)

Expand All @@ -135,9 +162,10 @@ parseNameAndVersion :: Parser (Text, Text)
parseNameAndVersion = (,) <$> parseName <*> parseVersion

instance FromJSON PodLock where
parseJSON = Yaml.withObject "Podfile.lock content" $ \obj -> do
parseJSON = Yaml.withObject "Podfile.lock content" $ \obj ->
PodLock <$> obj .: "PODS"
<*> obj .: "DEPENDENCIES"
<*> obj .: "EXTERNAL SOURCES"

instance FromJSON Pod where
parseJSON (Yaml.String p) = parserPod p Nothing
Expand All @@ -153,6 +181,18 @@ instance FromJSON Dep where
Right name -> pure $ Dep name
parseJSON notSupported = fail $ "Expected string, but received: " <> show notSupported

instance FromJSON ExternalSource where
parseJSON = Yaml.withObject "External source" $ \obj ->
do
ExternalGitType
<$> ( ExternalGitSource
<$> obj .: ":git"
<*> obj .:? ":tag"
<*> obj .:? ":commit"
<*> obj .:? ":branch"
)
<|> pure ExternalOtherType

parserPod :: Text -> Maybe Yaml.Value -> Yaml.Parser Pod
parserPod query deps = case parse parseNameAndVersion "" query of
Left pErr -> fail $ show $ errorBundlePretty pErr
Expand Down
Loading