Skip to content

Commit

Permalink
Swift: Support v3 file format, downgrade version mismatch to warning …
Browse files Browse the repository at this point in the history
…in the future (#1424)
  • Loading branch information
jssblck authored May 3, 2024
1 parent 2e2b874 commit 87635a7
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 19 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# FOSSA CLI Changelog

## v3.9.16
- Adds support for SwiftPM v3 files ([#1424](https://github.com/fossas/fossa-cli/pull/1424)).
Future SwiftPM file formats will be accepted automatically if they remain backwards compatible with the current parser.
- Updates parallel embedded binary extractions to be more properly isolated ([#1425](https://github.com/fossas/fossa-cli/pull/1425)).

## v3.9.15
Expand Down
69 changes: 57 additions & 12 deletions src/Strategy/Swift/PackageResolved.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ module Strategy.Swift.PackageResolved (
SwiftPackageResolvedFile (..),
SwiftResolvedPackage (..),
resolvedDependenciesOf,
parsePackageResolved,
) where

import Control.Carrier.Diagnostics (Diagnostics, Has, context, warn)
import Control.Monad (when)
import Data.Aeson (
FromJSON (parseJSON),
Key,
Expand All @@ -14,8 +17,24 @@ import Data.Aeson (
)
import Data.Aeson.Types (Parser)
import Data.Foldable (asum)
import Data.List (find)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import DepTypes (DepType (GitType), Dependency (..), VerConstraint (CEq))
import Effect.ReadFS (ReadFS, readContentsJson)
import Path (Abs, File, Path)

parsePackageResolved :: (Has ReadFS sig m, Has Diagnostics sig m) => Path Abs File -> m SwiftPackageResolvedFile
parsePackageResolved path = context "read and parse Package.resolved" $ do
packageResolved <- readContentsJson path

let fileVersion = version packageResolved
let parsedVersion = parserVersion $ parserForVersion fileVersion
when (parsedVersion /= fileVersion) $ warn $ "Package.resolved file is version '" <> show fileVersion <> "', but was parsed as version '" <> show parsedVersion <> "'"

pure packageResolved

data SwiftPackageResolvedFile = SwiftPackageResolvedFile
{ version :: Integer
Expand All @@ -35,10 +54,7 @@ data SwiftResolvedPackage = SwiftResolvedPackage
instance FromJSON SwiftPackageResolvedFile where
parseJSON = withObject "Package.resolved content" $ \obj -> do
version :: Integer <- obj .: "version"
case version of
1 -> SwiftPackageResolvedFile version <$> ((obj .: "object" |> "pins") >>= traverse (withObject "Package.resolved v1 pin" parseV1Pin))
2 -> SwiftPackageResolvedFile version <$> ((obj .: "pins") >>= traverse (withObject "Package.resolved v2 pin" parseV2Pin))
_ -> fail $ "unknown Package.resolved version: " <> show version
(parserFunction $ parserForVersion version) version obj

(|>) :: FromJSON a => Parser Object -> Key -> Parser a
(|>) parser key = do
Expand All @@ -52,14 +68,31 @@ instance FromJSON SwiftPackageResolvedFile where
Nothing -> pure Nothing
Just o -> o .:? key

parseV1Pin :: Object -> Parser SwiftResolvedPackage
parseV1Pin obj =
SwiftResolvedPackage
<$> obj .: "package"
<*> obj .: "repositoryURL"
<*> (obj .:? "state" |?> "branch")
<*> (obj .:? "state" |?> "revision")
<*> (obj .:? "state" |?> "version")
data PackageResolvedParser = PackageResolvedParser
{ parserVersion :: Integer
, parserFunction :: (Integer -> Object -> Parser SwiftPackageResolvedFile)
}

-- | Select the appropriate parser based on the file version.
-- Defaults to the parser with the latest version if none is specified.
parserForVersion :: Integer -> PackageResolvedParser
parserForVersion version = fromMaybe (NE.head allParsers) findVersion
where
findVersion :: Maybe PackageResolvedParser
findVersion = find (\p -> version == parserVersion p) $ NE.toList allParsers

-- Rather than paying the cost of sorting at runtime, the parent function assumes the "default" parser is the first in the list.
-- Ensure when adding new parsers that the new parser is added in the appropriate place based on this.
allParsers :: NonEmpty PackageResolvedParser
allParsers = PackageResolvedParser 3 parseV3 :| [PackageResolvedParser 2 parseV2, PackageResolvedParser 1 parseV1]

-- | From the Swift Package Manager source code, the pins did not change in v3:
-- https://github.com/apple/swift-package-manager/blob/9aa348e8eecc44fb6f93e1ef46e6dbd29947f4e7/Sources/PackageGraph/PinsStore.swift#L470
parseV3 :: Integer -> Object -> Parser SwiftPackageResolvedFile
parseV3 = parseV2

parseV2 :: Integer -> Object -> Parser SwiftPackageResolvedFile
parseV2 version obj = SwiftPackageResolvedFile version <$> ((obj .: "pins") >>= traverse (withObject "Package.resolved v2 pin" parseV2Pin))

parseV2Pin :: Object -> Parser SwiftResolvedPackage
parseV2Pin obj =
Expand All @@ -70,6 +103,18 @@ parseV2Pin obj =
<*> (obj .:? "state" |?> "revision")
<*> (obj .:? "state" |?> "version")

parseV1 :: Integer -> Object -> Parser SwiftPackageResolvedFile
parseV1 version obj = SwiftPackageResolvedFile version <$> ((obj .: "object" |> "pins") >>= traverse (withObject "Package.resolved v1 pin" parseV1Pin))

parseV1Pin :: Object -> Parser SwiftResolvedPackage
parseV1Pin obj =
SwiftResolvedPackage
<$> obj .: "package"
<*> obj .: "repositoryURL"
<*> (obj .:? "state" |?> "branch")
<*> (obj .:? "state" |?> "revision")
<*> (obj .:? "state" |?> "version")

instance FromJSON SwiftResolvedPackage where
parseJSON = withObject "Package.resolved pinned object" $ \obj ->
SwiftResolvedPackage
Expand Down
14 changes: 7 additions & 7 deletions src/Strategy/Swift/Xcode/Pbxproj.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Data.String.Conversion (toText)
import Data.Text (Text)
import DepTypes (DepType (GitType, SwiftType), Dependency (..))
import Diag.Common (MissingDeepDeps (MissingDeepDeps))
import Effect.ReadFS (Has, ReadFS, readContentsJson, readContentsParser)
import Effect.ReadFS (Has, ReadFS, readContentsParser)
import Errata (Errata (..))
import Graphing (Graphing, deeps, directs, promoteToDirect)
import Path
Expand All @@ -28,7 +28,7 @@ import Strategy.Swift.Errors (
swiftPackageResolvedRef,
xcodeCoordinatePkgVersion,
)
import Strategy.Swift.PackageResolved (SwiftPackageResolvedFile, resolvedDependenciesOf)
import Strategy.Swift.PackageResolved (SwiftPackageResolvedFile, parsePackageResolved, resolvedDependenciesOf)
import Strategy.Swift.PackageSwift (
SwiftPackageGitDepRequirement (..),
isGitRefConstraint,
Expand Down Expand Up @@ -119,7 +119,7 @@ hasSomeSwiftDeps projFile = do
analyzeXcodeProjForSwiftPkg :: (Has ReadFS sig m, Has Diagnostics sig m) => Path Abs File -> Maybe (Path Abs File) -> m (Graphing.Graphing Dependency)
analyzeXcodeProjForSwiftPkg xcodeProjFile resolvedFile = do
xCodeProjContent <-
context "Identifying swift package references in xcode project file" $
context "Identify swift package references in xcode project file" $
readContentsParser parsePbxProj xcodeProjFile

packageResolvedContent <- case resolvedFile of
Expand All @@ -133,8 +133,8 @@ analyzeXcodeProjForSwiftPkg xcodeProjFile resolvedFile = do
. errDoc swiftPackageResolvedRef
. errDoc xcodeCoordinatePkgVersion
$ fatalText "Package.resolved file was not discovered"
Just packageResolved ->
context "Identifying dependencies in Package.resolved" $
readContentsJson packageResolved
Just file ->
context "Identify dependencies in Package.resolved" $
Just <$> parsePackageResolved file

context "Building dependency graph" $ pure $ buildGraph xCodeProjContent packageResolvedContent
context "Build dependency graph" $ pure $ buildGraph xCodeProjContent packageResolvedContent
43 changes: 43 additions & 0 deletions test/Swift/PackageResolvedSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,55 @@ expectedV2ResolvedContent =
]
}

expectedV3ResolvedContent :: SwiftPackageResolvedFile
expectedV3ResolvedContent =
SwiftPackageResolvedFile
{ version = 3
, pinnedPackages =
[ SwiftResolvedPackage
{ package = "vonage-client-sdk-video"
, repositoryURL = "https://github.com/opentok/vonage-client-sdk-video.git"
, repositoryBranch = Nothing
, repositoryRevision = Just "e4b1af1808067f0c0a66fa85aaf99915b542a579"
, repositoryVersion = Just "2.27.2"
}
]
}

-- | Not a true v4; we'll need to rename this test in the future.
-- The point here is just to test "a version that is not explicitly supported but still parses".
expectedHypotheticalV4ResolvedContent :: SwiftPackageResolvedFile
expectedHypotheticalV4ResolvedContent =
SwiftPackageResolvedFile
{ version = 4
, pinnedPackages =
[ SwiftResolvedPackage
{ package = "vonage-client-sdk-video"
, repositoryURL = "https://github.com/opentok/vonage-client-sdk-video.git"
, repositoryBranch = Nothing
, repositoryRevision = Just "e4b1af1808067f0c0a66fa85aaf99915b542a579"
, repositoryVersion = Just "2.27.2"
}
]
}

spec :: Spec
spec = do
describe "parse Package.resolved file" $ do
it "should parse v1 content correctly" $ do
resolvedFile <- decodeFileStrict' "test/Swift/testdata/v1/Package.resolved"
resolvedFile `shouldBe` Just expectedV1ResolvedContent

it "should parse v2 content correctly" $ do
resolvedFile <- decodeFileStrict' "test/Swift/testdata/v2/Package.resolved"
resolvedFile `shouldBe` Just expectedV2ResolvedContent

it "should parse v3 content correctly" $ do
resolvedFile <- decodeFileStrict' "test/Swift/testdata/v3/Package.resolved"
resolvedFile `shouldBe` Just expectedV3ResolvedContent

-- Not a true v4; we'll need to rename this test in the future.
-- The point here is just to test "a version that is not explicitly supported but still parses".
it "should parse hypothetical v4 content correctly" $ do
resolvedFile <- decodeFileStrict' "test/Swift/testdata/v4/Package.resolved"
resolvedFile `shouldBe` Just expectedHypotheticalV4ResolvedContent
15 changes: 15 additions & 0 deletions test/Swift/testdata/v3/Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"originHash" : "fbfc6b659421b42ead4ad2cfb780bcd9992922da94fe0df8024fd7086598dc08",
"pins" : [
{
"identity" : "vonage-client-sdk-video",
"kind" : "remoteSourceControl",
"location" : "https://github.com/opentok/vonage-client-sdk-video.git",
"state" : {
"revision" : "e4b1af1808067f0c0a66fa85aaf99915b542a579",
"version" : "2.27.2"
}
}
],
"version" : 3
}
16 changes: 16 additions & 0 deletions test/Swift/testdata/v4/Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"originHash" : "fbfc6b659421b42ead4ad2cfb780bcd9992922da94fe0df8024fd7086598dc08",
"pins" : [
{
"identity" : "vonage-client-sdk-video",
"kind" : "remoteSourceControl",
"location" : "https://github.com/opentok/vonage-client-sdk-video.git",
"state" : {
"revision" : "e4b1af1808067f0c0a66fa85aaf99915b542a579",
"version" : "2.27.2"
},
"extra": "some extra field that didn't exist in v3"
}
],
"version" : 4
}

0 comments on commit 87635a7

Please sign in to comment.