yarn2nix-0.5.0: SetupNodePackagePaths.hs
{-# LANGUAGE RecordWildCards, NoImplicitPrelude, LambdaCase, FlexibleContexts, NamedFieldPuns, OverloadedStrings #-}
import Protolude
import qualified Data.Text.IO as TIO
import qualified Control.Monad.Except as ExcT
import qualified Control.Exception as Exc
import qualified System.IO.Error as IOErr
import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Lazy as HML
import qualified System.FilePath as FP
import qualified System.Directory as Dir
import qualified System.Posix.Files as PosixFiles
import Options.Applicative
import qualified Distribution.Nodejs.Package as NP
data Args
= Args { argMode :: Mode
, argTargetDir :: FilePath
, argPackageDir :: FilePath }
data Mode = BinMode
args :: Parser Args
args = subparser
( command "bin" (info (modeCommands BinMode)
(progDesc "link package bin files")) )
where
modeCommands mode = Args
<$> pure mode
<*> strOption
( long "to"
<> metavar "LINK_TARGET"
<> help "folder to link to (absolute or relative from package folder)" )
<*> strOption
( long "package"
<> metavar "FOLDER"
<> help "folder with the node package" )
main :: IO ()
main = execParser (info (args <**> helper)
(progDesc "Link various files from npm packages to folders"))
>>= realMain
type ErrorLogger = ExceptT [Char] IO
realMain :: Args -> IO ()
realMain Args{..} = do
let packageJsonPath = argPackageDir FP.</> "package.json"
unlessM (Dir.doesDirectoryExist argTargetDir)
$ die $ argTargetDir <> " is not a directory."
unlessM (Dir.doesFileExist packageJsonPath)
$ die $ packageJsonPath <> " does not exist."
runExceptT
(tryRead packageJsonPath >>= tryDecode packageJsonPath >>= go)
>>= \case
(Left err) -> die $ "ERROR: " <> err
(Right _) -> pass
where
tryIOMsg :: ([Char] -> [Char]) -> IO a -> ErrorLogger a
tryIOMsg errAnn = ExcT.withExceptT (errAnn . Exc.displayException) . tryIO
tryRead :: FilePath -> ErrorLogger BL.ByteString
tryRead fp = tryIOMsg exc $ BL.readFile fp
where exc e = fp <> " cannot be read:\n" <> e
tryDecode :: FilePath -> BL.ByteString -> ExceptT [Char] IO NP.Package
tryDecode fp fileBs = do
(pkg, warnings) <- NP.unLoggingPackage
<$> ExcT.ExceptT (pure $ first exc $ NP.decode fileBs)
warn $ fmap NP.formatWarning warnings
pure pkg
where exc e = fp <> " cannot be decoded\n" <> toS e
qte s = "\"" <> s <> "\""
warn :: [Text] -> ErrorLogger ()
warn ws = for_ ws $ liftIO . TIO.hPutStrLn stderr
go :: NP.Package -> ErrorLogger ()
go NP.Package{bin} = case argMode of
BinMode -> traverse_ linkBin =<< case bin of
-- files with names how they should be linked
(NP.BinFiles bs) -> pure $ HML.toList bs
-- a whole folder where everything should be linked
(NP.BinFolder bf) -> do
dirE <- liftIO $ tryJust
(\e -> guard (IOErr.isDoesNotExistError e ||
IOErr.isPermissionError e))
(Dir.listDirectory bf)
case dirE of
(Left _) -> do
warn ["Binary folder " <> toS (qte bf) <> " could not be accessed."]
pure []
(Right dir) ->
pure $ fmap (\f -> (toS f, bf FP.</> f)) dir
-- | Link a binary file to @targetDir/name@.
-- @relBinPath@ is relative from the package dir.
linkBin :: (Text, FilePath) -> ErrorLogger ()
linkBin (name, relBinPath) = do
let canon fp = tryIOMsg
(\e -> "Couldn’t canonicalize path " <> qte fp <> ": " <> e)
(Dir.canonicalizePath fp)
pkgDir <- canon argPackageDir
binPath <- canon $ argPackageDir FP.</> relBinPath
targetDir <- canon argTargetDir
when (not $ pkgDir `isPrefixOf` binPath)
$ throwError $ mconcat
[ "The link to executable file "
, qte relBinPath
, " lies outside of the package folder!\n"
, "That’s a security risk, aborting." ]
tryIOMsg
(\e -> "Symlink could not be created: " <> e)
(PosixFiles.createSymbolicLink binPath $ targetDir FP.</> toS name)