{-# LANGUAGE ScopedTypeVariables #-} module Main where import Control.Monad.State import Data.Digest.Pure.SHA import Data.List import Data.Maybe import Data.Semigroup import Data.Set (Set) import qualified Data.Set as Set import Data.Traversable import Network.HTTP.Conduit import Network.HTTP.Types.Status import System.Environment import Text.XML.Light -- | Takes multiple maven package descriptions as command line arguments -- and finds the dependencies of those maven packages. -- Package descriptions should be of the form @groupid:artifactid:version@ main :: IO () main = do mavens <- getArgs mgr <- newManager tlsManagerSettings (_, deps) <- flip runStateT Set.empty $ traverse (recurseDependencies mgr) $ parseMaven <$> mavens putStrLn $ "[" <> (concatMap toNix deps) <> "]" data Maven = Maven { _maven_groupId :: String , _maven_artifactId :: String , _maven_version :: String } deriving (Show, Read, Eq, Ord) data MavenNix = MavenNix { _mavenNix_maven :: Maven , _mavenNix_jarSha256 :: Maybe (Digest SHA256State) , _mavenNix_pomSha256 :: Maybe (Digest SHA256State) } deriving (Show, Eq, Ord) -- | Parses strings of the form @groupid:artifactid:version@, -- e.g., @com.android.build.tools:gradle:2.3.0@ parseMaven :: String -> Maven parseMaven s = let (groupId, _:rest) = break (==':') s (artifactId, _:version) = break (==':') rest in Maven groupId artifactId version -- | Create a nix record for a hashed maven package toNix :: MavenNix -> String toNix m = let mvn = _mavenNix_maven m showHash h = fromMaybe "null" $ (\x -> "\"" <> x <> "\"") . showDigest <$> h in unlines [ " { artifactId = \"" <> _maven_artifactId mvn <> "\";" , " groupId = \"" <> _maven_groupId mvn <> "\";" , " version = \"" <> _maven_version mvn <> "\";" , " jarSha256 = " <> showHash (_mavenNix_jarSha256 m) <> ";" , " pomSha256 = " <> showHash (_mavenNix_pomSha256 m) <> "; }" ] -- | Maven repositories in which to look for packages mirrors :: [String] mirrors = [ "https://repo1.maven.org/maven2/" , "https://jcenter.bintray.com/" , "http://central.maven.org/maven2/" ] -- | Hash a particular maven package's .pom and .jar files and parse the .pom file as xml fetch :: [String] -> Manager -> Maven -> IO (MavenNix, Element) fetch (mirror:fallbacks) mgr mvn = do let version = takeWhile (/= ',') $ dropWhile (\c -> c == '[' || c == '(') $ _maven_version mvn url ext = mconcat [ mirror , (\x -> if x == '.' then '/' else x) <$> _maven_groupId mvn , "/" , _maven_artifactId mvn , "/" , version , "/" , _maven_artifactId mvn , "-" , version , ext ] hash rsp = if responseStatus rsp == status200 then Just $ sha256 $ responseBody rsp else Nothing reqPom <- parseRequest $ url ".pom" reqJar <- parseRequest $ url ".jar" pom <- httpLbs reqPom mgr jar <- httpLbs reqJar mgr if responseStatus jar /= status200 && responseStatus pom /= status200 then fetch fallbacks mgr mvn else return $ let Just e = parseXMLDoc $ responseBody pom mvnNix = MavenNix { _mavenNix_maven = mvn , _mavenNix_jarSha256 = hash jar , _mavenNix_pomSha256 = hash pom } in (mvnNix, e) fetch [] _ mvn = error $ mconcat [ "Error: Could not find " , _maven_groupId mvn , ":" , _maven_artifactId mvn , ":" , _maven_version mvn , " in any mirror." ] -- | Extract the dependencies from a package's pom xml getDepsFor :: Element -> [Maven] getDepsFor x = do deps <- findChildrenByTagName "dependencies" x dep <- findChildrenByTagName "dependency" deps groupId <- findChildrenByTagName "groupId" dep artifactId <- findChildrenByTagName "artifactId" dep version <- findChildrenByTagName "version" dep let optional = case findChildrenByTagName "optional" dep of [] -> False xs -> any ((=="true") . strContent) xs guard (not ("$" `isPrefixOf` strContent version) && not optional) return $ Maven { _maven_groupId = strContent groupId , _maven_artifactId = strContent artifactId , _maven_version = strContent version } -- | Given a starting maven package, retrieve its dependencies recursively recurseDependencies :: Manager -> Maven -> StateT (Set MavenNix) IO () recurseDependencies mgr mvn = do s <- get when (not $ any (\mvnNix -> _mavenNix_maven mvnNix == mvn) s) $ do (mvnNix, e) <- liftIO $ fetch mirrors mgr mvn modify $ Set.insert mvnNix void $ traverse (recurseDependencies mgr) $ getDepsFor e -- | Retrieve an XML Element's children by tag name findChildrenByTagName :: String -> Element -> [Element] findChildrenByTagName n = filterChildren (\a -> qName (elName a) == n)