module NixFromNpm.ConvertToNix where
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as H
import Data.Map (Map)
import qualified Data.Map as M
import Data.Text (Text)
import qualified Data.Text as T
import NixFromNpm.Common
import Nix.Types
import Nix.Parser
import Nix.Pretty (prettyNix)
import NixFromNpm.Options
import NixFromNpm.NpmTypes
import NixFromNpm.SemVer
import NixFromNpm.Parsers.SemVer
import NixFromNpm.NpmLookup (getPkg, FullyDefinedPackage(..), concatDots,
PackageMap, mapPM, PreExistingPackage(..))
_startingSrc :: String
_startingSrc = "\
\{nixpkgs ? import <nixpkgs> {}}: \
\let \
\ inherit (nixpkgs.lib) attrValues foldl; \
\ joinSets = foldl (a: b: a // b) {}; \
\ joinedExtensions = joinSets (attrValues extensions); \
\ allPkgs = nixpkgs // nodePkgs // joinedExtensions // \
\ {inherit (nixpkgs.nodePackages)buildNodePackage;}; \
\ callPackage = nixpkgs.lib.callPackageWith allPkgs; \
\ nodePkgs = joinedExtensions // byVersion // defaults; \
\in \
\nodePkgs"
_startingExpr :: NExpr
_startingExpr = case parseNixString _startingSrc of
Success e -> e
Failure e -> error $ unlines ["FATAL: Starting source failed to parse:",
show e]
callPackage :: NExpr -> NExpr
callPackage = callPackageWith []
callPackageWith :: [Binding NExpr] -> NExpr -> NExpr
callPackageWith args e = mkApp (mkApp (mkSym "callPackage") e)
(mkNonRecSet args)
callPackageWithRec :: [Binding NExpr] -> NExpr -> NExpr
callPackageWithRec args e = mkApp (mkApp (mkSym "callPackage") e)
(mkRecSet args)
fixName :: Name -> Name
fixName = T.replace "." "-"
toDepName :: Name -> SemVer -> Name
toDepName name (a, b, c) = concat [fixName name, "_", pack $
intercalate "-" $ map show [a, b, c]]
toDotNix :: SemVer -> Text
toDotNix v = concatDots v <> ".nix"
str :: Text -> NExpr
str = mkStr DoubleQuoted
distInfoToNix :: DistInfo -> NExpr
distInfoToNix DistInfo{..} = mkApp (mkSym "fetchurl") $ mkNonRecSet
[ "url" `bindTo` str diUrl,
"sha1" `bindTo` str diShasum ]
metaNotEmpty :: PackageMeta -> Bool
metaNotEmpty PackageMeta{..} = isJust pmDescription
metaToNix :: PackageMeta -> NExpr
metaToNix PackageMeta{..} = case pmDescription of
Nothing -> mkNonRecSet []
Just d -> mkNonRecSet ["description" `bindTo` str d]
resolvedPkgToNix :: ResolvedPkg -> NExpr
resolvedPkgToNix ResolvedPkg{..} = do
let
deps = map (mkSym . uncurry toDepName) $ H.toList rpDependencies
_funcParams = map (uncurry toDepName) (H.toList rpDependencies)
<> ["buildNodePackage", "fetchurl"]
funcParams = mkFormalSet $ map (\x -> (x, Nothing)) _funcParams
let args = mkNonRecSet $ catMaybes [
Just $ "name" `bindTo` str rpName,
Just $ "version" `bindTo` (str $ renderSV rpVersion),
Just $ "src" `bindTo` distInfoToNix rpDistInfo,
maybeIf (length deps > 0) $ "deps" `bindTo` mkList deps,
maybeIf (metaNotEmpty rpMeta) $ "meta" `bindTo` metaToNix rpMeta
]
mkFunction funcParams $ mkApp (mkSym "buildNodePackage") args
mkDefaultNix :: Record [SemVer]
-> Record Path
-> NExpr
mkDefaultNix versionMap extensionMap = do
let mkPath' = mkPath False . unpack
toPath name ver = mkPath' $ concat ["./", name, "/", toDotNix ver]
extensionsSet = mkNonRecSet $
flip map (H.toList extensionMap) $ \(name, path) ->
name `bindTo` (mkApp
(mkApp (mkSym "import") (mkPath False (unpack path)))
(mkNonRecSet [Inherit Nothing [mkSelector "nixpkgs"]]))
mkBinding name ver = toDepName name ver
`bindTo` callPackage (toPath name ver)
mkBindings name vers = map (mkBinding name) vers
mkDefVer name vers = case vers of
[] -> errorC ["FATAL: no versions generated for package ", name]
_ -> fixName name `bindTo` mkSym (toDepName name $ maximum vers)
versOnly = sortOn fst $ H.toList versionMap
byVersion = mkNonRecSet $ concatMap (uncurry mkBindings) versOnly
defaults = mkWith (mkSym "byVersion") $
mkNonRecSet $ map (uncurry mkDefVer) versOnly
newBindings = ["extensions" `bindTo` extensionsSet,
"byVersion" `bindTo` byVersion,
"defaults" `bindTo` defaults]
modifyFunctionBody (appendBindings newBindings) _startingExpr
takeNewPackages :: PackageMap FullyDefinedPackage
-> (PackageMap ResolvedPkg, PackageMap NExpr)
takeNewPackages startingRec = do
let isNew (NewPackage rpkg) = Just rpkg
isNew _ = Nothing
exists (FromExistingInOutput expr) = Just expr
exists _ = Nothing
newPkgs = H.map (modifyMap isNew) startingRec
existingPkgs = H.map (modifyMap exists) startingRec
removeEmpties = H.filter (not . H.null)
(removeEmpties newPkgs, removeEmpties existingPkgs)
dumpPkgs :: MonadIO m
=> String
-> PackageMap ResolvedPkg
-> PackageMap NExpr
-> Record Path
-> m ()
dumpPkgs path newPackages existingPackages extensions = liftIO $ do
let _path = pack path
if H.null newPackages
then putStrLn "No new packages created." >> return ()
else do
putStrsLn ["Creating new packages at ", _path]
createDirectoryIfMissing True path
withDir path $ do
forM_ (H.toList newPackages) $ \(pkgName, pkgVers) -> do
let subdir = path </> unpack pkgName
createDirectoryIfMissing False subdir
withDir subdir $ forM_ (H.toList pkgVers) $ \(ver, rpkg) -> do
let expr = resolvedPkgToNix rpkg
fullPath = subdir </> unpack (toDotNix ver)
putStrsLn ["Writing package file at ", pack fullPath]
writeFile (unpack $ toDotNix ver) $ show $ prettyNix expr
let versionMap = map H.keys newPackages <> map H.keys existingPackages
defaultNix = mkDefaultNix versionMap extensions
writeFile "default.nix" $ show $ prettyNix defaultNix
parseVersion :: Name -> Path -> IO (Maybe (SemVer, NExpr))
parseVersion pkgName path = do
let pth = unpack path
versionTxt = pack $ dropSuffix ".nix" $ takeBaseName pth
case parseSemVer versionTxt of
Left _ -> return Nothing
Right version -> parseNixString . pack <$> readFile pth >>= \case
Failure err -> do
putStrsLn ["Warning: expression for ", pkgName, " version ",
versionTxt, " failed to parse:\n", pack $ show err]
return Nothing
Success expr -> return $ Just (version, expr)
findExisting :: Maybe Name
-> Path
-> IO (PackageMap PreExistingPackage)
findExisting maybeName path = do
doesDirectoryExist (unpack path) >>= \case
False -> case maybeName of
Just name -> errorC ["Extension ", name, " at path ", path,
" does not exist."]
Nothing -> return mempty
True -> withDir (unpack path) $ do
let wrapper :: NExpr -> PreExistingPackage
wrapper = case maybeName of Nothing -> FromOutput
Just name -> FromExtension name
putStrsLn ["Searching for existing expressions in ", path, "..."]
contents <- getDirectoryContents "."
verMaps <- forM contents $ \dir -> do
exprs <- doesDirectoryExist dir >>= \case
True -> withDir dir $ do
contents <- getDirectoryContents "."
let files = pack <$> filter (endswith ".nix") contents
catMaybes <$> mapM (parseVersion $ pack dir) files
False -> do
return mempty
case exprs of
[] -> return Nothing
vs -> return $ Just (pack dir, H.map wrapper $ H.fromList exprs)
let total = sum $ map (H.size . snd) $ catMaybes verMaps
putStrsLn ["Found ", render total, " existing expressions"]
return $ H.fromList $ catMaybes verMaps
preloadPackages :: Bool
-> Path
-> Record Path
-> IO (PackageMap PreExistingPackage)
preloadPackages noExistCheck path toExtend = do
existing <- if noExistCheck then pure mempty
else findExisting Nothing path
libraries <- fmap concat $ forM (H.toList toExtend) $ \(name, path) -> do
findExisting (Just name) path
return (existing <> libraries)
dumpPkgNamed :: Text
-> Path
-> PackageMap PreExistingPackage
-> Record Path
-> Maybe Text
-> IO ()
dumpPkgNamed name path existing extensions token = do
pwd <- getCurrentDirectory
packages <- getPkg name existing token
let (new, existing) = takeNewPackages packages
dumpPkgs (pwd </> unpack path) new existing extensions
getExtensions :: [Text] -> Record Path
getExtensions = foldl' step mempty where
step :: Record Path -> Text -> Record Path
step exts nameEqPath = case T.split (== '=') nameEqPath of
[name, path] -> append name path
[path] -> append (pack $ takeBaseName (unpack path)) path
_ -> errorC ["Extensions must be of the form NAME=PATH (in argument ",
nameEqPath, ")"]
where
append name path = case H.lookup name exts of
Nothing -> H.insert name path exts
Just path' -> errorC ["Extension ", name, " is mapped to both path ",
path, " and path ", path']
dumpPkgFromOptions :: NixFromNpmOptions -> IO ()
dumpPkgFromOptions NixFromNpmOptions{..} = do
forM_ nfnoPkgNames $ \name -> do
let extensions = getExtensions nfnoExtendPaths
existing <- preloadPackages nfnoNoCache nfnoOutputPath extensions
dumpPkgNamed name nfnoOutputPath existing extensions nfnoGithubToken