elm2nix (empty) → 0.1.0
raw patch · 11 files changed
+651/−0 lines, 11 filesdep +aesondep +ansi-wl-pprintdep +asyncsetup-changed
Dependencies added: aeson, ansi-wl-pprint, async, base, binary, bytestring, containers, data-default, directory, elm2nix, filepath, here, mtl, optparse-applicative, process, req, text, transformers, unordered-containers
Files
- ChangeLog.md +5/−0
- LICENSE +30/−0
- README.md +90/−0
- Setup.hs +2/−0
- data/default.nix +42/−0
- elm2nix.cabal +85/−0
- elm2nix/Main.hs +60/−0
- src/Elm2Nix.hs +142/−0
- src/Elm2Nix/FixedOutput.hs +54/−0
- src/Elm2Nix/PackagesSnapshot.hs +139/−0
- test/Spec.hs +2/−0
+ ChangeLog.md view
@@ -0,0 +1,5 @@+# Changelog for elm2nix++## 0.1.0 (2018-12-28)++- Initial release (@domenkozar)
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Domen Kožar (c) 2017-2019++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Domen Kožar nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,90 @@+# elm2nix++[](https://travis-ci.org/domenkozar/elm2nix)+[](https://hackage.haskell.org/package/elm2nix)++Convert an [Elm](http://elm-lang.org/) project into+[Nix](https://nixos.org/nix/) expressions.++It consists of multiple commands:+- `elm2nix convert`: Given `elm.json` in current directory, all dependencies are+ parsed and their sha256sum calculated+- `elm2nix snapshot`: Downloads snapshot of http://package.elm-lang.org into `versions.dat`+- `elm2nix init`: Generates `default.nix` that glues everything together++## Assumptions++Supports Elm 0.19.x++## Installation++ $ nix-shell -p stack --run "stack install --nix"++## Usage++ $ git clone https://github.com/evancz/elm-todomvc.git+ $ cd elm-todomvc+ $ ~/.local/bin/elm2nix init > default.nix+ $ ~/.local/bin/elm2nix convert > elm-srcs.nix+ $ ~/.local/bin/elm2nix snapshot > versions.dat+ $ nix-build+ $ chromium ./result/index.html++## Running tests (as per CI)++ $ ./scripts/tests.sh++## FAQ++### Why is mkDerivation inlined into `default.nix`?++As it's considered experimental, it's generated for now. Might change in the future.++### How to use with ParcelJS and Yarn?++Instead of running `elm2nix init`, use something like:++```nix+{ pkgs ? import <nixpkgs> {}+}:++let+ yarnPkg = pkgs.yarn2nix.mkYarnPackage {+ name = "myproject-node-packages";+ packageJSON = ./package.json;+ unpackPhase = ":";+ src = null;+ yarnLock = ./yarn.lock;+ publishBinsFor = ["parcel-bundler"];+ };+in pkgs.stdenv.mkDerivation {+ name = "myproject-frontend";+ src = pkgs.lib.cleanSource ./.;++ buildInputs = with pkgs.elmPackages; [+ elm+ elm-format+ yarnPkg+ pkgs.yarn+ ];++ patchPhase = ''+ rm -rf elm-stuff+ ln -sf ${yarnPkg}/node_modules .+ '';++ shellHook = ''+ ln -fs ${yarnPkg}/node_modules .+ '';++ configurePhase = pkgs.elmPackages.fetchElmDeps {+ elmPackages = import ./elm-srcs.nix;+ versionsDat = ./versions.dat;+ };++ installPhase = ''+ mkdir -p $out+ parcel build -d $out index.html+ '';+}+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ data/default.nix view
@@ -0,0 +1,42 @@+{ nixpkgs ? <nixpkgs>+, config ? {}+}:++with (import nixpkgs config);++let+ mkDerivation =+ { srcs ? ./elm-srcs.nix+ , src+ , name+ , srcdir ? "./src"+ , targets ? []+ , versionsDat ? ./versions.dat+ }:+ stdenv.mkDerivation {+ inherit name src;++ buildInputs = [ elmPackages.elm ];++ buildPhase = pkgs.elmPackages.fetchElmDeps {+ elmPackages = import srcs;+ inherit versionsDat;+ };++ installPhase = let+ elmfile = module: "\${srcdir}/\${builtins.replaceStrings ["."] ["/"] module}.elm";+ in ''+ mkdir -p \$out/share/doc+ \${lib.concatStrings (map (module: ''+ echo "compiling \${elmfile module}"+ elm make \${elmfile module} --output \$out/\${module}.html --docs \$out/share/doc/\${module}.json+ '') targets)}+ '';+ };+in mkDerivation {+ name = "${name}";+ srcs = ./elm-srcs.nix;+ src = ./.;+ targets = ["Main"];+ srcdir = "${srcdir}";+}
+ elm2nix.cabal view
@@ -0,0 +1,85 @@+-- This file has been generated from package.yaml by hpack version 0.18.1.+--+-- see: https://github.com/sol/hpack++name: elm2nix+version: 0.1.0+synopsis: Turn your Elm project into buildable Nix project+description: Please see the README on Github at <https://github.com/domenkozar/elm2nix#readme>+homepage: https://github.com/domenkozar/elm2nix#readme+bug-reports: https://github.com/domenkozar/elm2nix/issues+author: Domen Kožar+maintainer: domen@dev.si+copyright: 2017-2019 Domen Kožar+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: 2.0++extra-source-files:+ ChangeLog.md+ README.md+ data/default.nix++source-repository head+ type: git+ location: https://github.com/domenkozar/elm2nix++library+ hs-source-dirs:+ src+ build-depends:+ base >= 4.7 && < 5+ , aeson+ , async+ , bytestring+ , binary+ , containers+ , data-default+ , directory+ , filepath+ , here+ , mtl+ , process+ , req+ , text+ , transformers+ , unordered-containers+ exposed-modules:+ Elm2Nix+ Elm2Nix.FixedOutput+ Elm2Nix.PackagesSnapshot+ other-modules:+ Paths_elm2nix+ autogen-modules:+ Paths_elm2nix+ default-language: Haskell2010++executable elm2nix+ main-is: Main.hs+ hs-source-dirs:+ elm2nix+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ autogen-modules:+ Paths_elm2nix+ other-modules:+ Paths_elm2nix+ build-depends:+ base >= 4.7 && < 5+ , elm2nix+ , directory+ , optparse-applicative+ , here+ , ansi-wl-pprint+ default-language: Haskell2010++test-suite elm2nix-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >= 4.7 && < 5+ , elm2nix+ default-language: Haskell2010
+ elm2nix/Main.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE QuasiQuotes #-}+module Main+ ( main+ ) where++import Data.Semigroup ((<>))+import Data.Version (showVersion)+import Data.String.Here (hereLit)+import Options.Applicative+import System.IO+import System.Directory (getCurrentDirectory)+import qualified Elm2Nix+import qualified Paths_elm2nix as This+import qualified Text.PrettyPrint.ANSI.Leijen as PP+++data Command+ = Init+ | Convert+ | Snapshot++main :: IO ()+main = do+ hSetBuffering stdout LineBuffering+ hSetBuffering stderr LineBuffering+ cmd <- getOpts+ cwd <- getCurrentDirectory+ case cmd of+ Convert -> Elm2Nix.convert+ Init -> Elm2Nix.initialize+ Snapshot -> Elm2Nix.snapshot cwd++getOpts :: IO Command+getOpts = customExecParser p (infoH opts rest)+ where+ infoH a = info (helper <*> a)+ rest = fullDesc+ <> progDesc "Convert Elm project to Nix expressions"+ <> header ("elm2nix " ++ showVersion This.version)+ <> footerDoc (Just $ PP.string exampleText)+ p = prefs showHelpOnEmpty++ exampleText :: String+ exampleText = [hereLit|++ Usage:++ $ elm2nix init > default.nix+ $ elm2nix convert > elm-srcs.nix+ $ nix-build++ Note: You have to run elm2nix from top-level directory of an Elm project.+ |]++ opts :: Parser Command+ opts = subparser+ ( command "init" (infoH (pure Init) (progDesc "Generate default.nix (printed to stdout)"))+ <> command "convert" (infoH (pure Convert) (progDesc "Generate Nix expressions for elm.json using nix-prefetch-url"))+ <> command "snapshot" (infoH (pure Snapshot) (progDesc "Generate versions.dat"))+ )
+ src/Elm2Nix.hs view
@@ -0,0 +1,142 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Elm2Nix+ ( convert+ , initialize+ , snapshot+ ) where++import Control.Concurrent.Async (mapConcurrently)+import Control.Monad (liftM2)+import Control.Monad.Except (liftIO, MonadIO)+import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)+import Data.Aeson (Value(..))+import Data.List (intercalate)+import Data.HashMap.Strict (HashMap)+import Data.String.Here+import Data.Text (Text)+import System.Exit (exitFailure)+import System.IO (hPutStrLn, stderr)++import qualified Data.HashMap.Strict as HM+import qualified Data.ByteString.Lazy as LBS+import qualified Data.Aeson as Json+import qualified Data.Text as Text++import Elm2Nix.FixedOutput (FixedDerivation(..), prefetch)+import Elm2Nix.PackagesSnapshot (snapshot)+++newtype Elm2Nix a = Elm2Nix { runElm2Nix_ :: ExceptT Elm2NixError IO a }+ deriving (Functor, Applicative, Monad, MonadIO)++type Dep = (String, String)++data Elm2NixError =+ ElmJsonReadError String+ | UnexpectedValue Value+ | KeyNotFound Text+ deriving Show++runElm2Nix :: Elm2Nix a -> IO (Either Elm2NixError a)+runElm2Nix = runExceptT . runElm2Nix_++throwErr :: Elm2NixError -> Elm2Nix a+throwErr e = Elm2Nix (throwE e)++parseElmJsonDeps :: Value -> Either Elm2NixError [Dep]+parseElmJsonDeps obj =+ case obj of+ Object hm -> do+ deps <- tryLookup hm "dependencies"+ case deps of+ Object dhm -> do+ direct <- tryLookup dhm "direct"+ indirect <- tryLookup dhm "indirect"+ liftM2 (++) (parseDeps direct) (parseDeps indirect)+ v -> Left (UnexpectedValue v)+ v -> Left (UnexpectedValue v)+ where+ parseDep :: Text -> Value -> Either Elm2NixError Dep+ parseDep name (String ver) = Right (Text.unpack name, Text.unpack ver)+ parseDep _ v = Left (UnexpectedValue v)++ parseDeps :: Value -> Either Elm2NixError [Dep]+ parseDeps (Object hm) = mapM (uncurry parseDep) (HM.toList hm)+ parseDeps v = Left (UnexpectedValue v)++ maybeToRight :: b -> Maybe a -> Either b a+ maybeToRight _ (Just x) = Right x+ maybeToRight y Nothing = Left y++ tryLookup :: HashMap Text Value -> Text -> Either Elm2NixError Value+ tryLookup hm key =+ maybeToRight (KeyNotFound key) (HM.lookup key hm)++-- CMDs++convert :: IO ()+convert = runCLI $ do+ liftIO (hPutStrLn stderr "Resolving elm.json dependencies into Nix ...")+ res <- liftIO (fmap Json.eitherDecode (LBS.readFile "elm.json"))+ elmJson <- either (throwErr . ElmJsonReadError) return res++ deps <- either throwErr return (parseElmJsonDeps elmJson)+ liftIO (hPutStrLn stderr "Prefetching tarballs and computing sha256 hashes ...")++ sources <- liftIO (mapConcurrently (uncurry prefetch) deps)+ liftIO (putStrLn (generateNixSources sources))++initialize :: IO ()+initialize = runCLI $+ liftIO (putStrLn [template|data/default.nix|])+ where+ -- | Converts Package.Name to Nix friendly name+ baseName :: Text+ baseName = "elm-app"+ version :: Text+ version = "0.1.0"+ toNixName :: Text -> Text+ toNixName = Text.replace "/" "-"+ name :: String+ name = Text.unpack (toNixName baseName <> "-" <> version)+ srcdir :: String+ srcdir = "./src" -- TODO: get from elm.json++-- Utils++runCLI :: Elm2Nix a -> IO a+runCLI m = do+ result <- runElm2Nix m+ case result of+ Right a ->+ return a+ Left err -> do+ depErrToStderr err+ exitFailure++depErrToStderr :: Elm2NixError -> IO ()+depErrToStderr err =+ hPutStrLn stderr $+ case err of+ UnexpectedValue v -> "Unexpected Value: \n" ++ show v+ ElmJsonReadError s -> "Error reading json: " ++ s+ KeyNotFound key -> "Key not found in json: " ++ Text.unpack key++generateNixSources :: [FixedDerivation] -> String+generateNixSources dss =+ [iTrim|+{+${intercalate "\n" (map f dss)}+}+ |]+ where+ f :: FixedDerivation -> String+ f ds =+ [i|+ "${drvName ds}" = {+ sha256 = "${drvHash ds}";+ version = "${drvVersion ds}";+ };|]
+ src/Elm2Nix/FixedOutput.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedStrings #-}+module Elm2Nix.FixedOutput+ ( FixedDerivation(..)+ , prefetch+ ) where++import System.Exit+import System.Process+import qualified Data.ByteString.Lazy.Char8 as BS+++-- fixed output derivation metadata+data FixedDerivation = FixedDerivation+ { drvHash :: String -- ^ Computed sha256 hash+ , drvPath :: String -- ^ Nix store path of the derivation+ , drvUrl :: String -- ^ URL to the tarball+ , drvName :: String+ , drvVersion :: String+ } deriving (Show, Eq)++-- | Use nix-prefetch-url to obtain resulting path and it's hash+-- | Partially taken from cabal2nix/src/Distribution/Nixpkgs/Fetch.hs+prefetch :: String -> String -> IO FixedDerivation+prefetch name version = do+ (Nothing, Just stdoutH, _, processH) <-+ createProcess (proc "nix-prefetch-url" args) { env = Nothing+ , std_in = Inherit+ , std_err = Inherit+ , std_out = CreatePipe+ }+ exitCode <- waitForProcess processH+ case exitCode of+ ExitFailure _ -> error "nix-prefetch-url exited with non-zero"+ ExitSuccess -> do+ buf <- BS.hGetContents stdoutH+ let ls = BS.lines buf+ case length ls of+ 0 -> error "unknown nix-prefetch-url output"+ 2 -> return $ FixedDerivation (BS.unpack $ head ls)+ (BS.unpack $ head $ tail ls)+ url+ name+ version+ _ -> error "unknown nix-prefetch-url output"+ where+ url :: String+ url =+ "https://github.com/"+ ++ name+ ++ "/archive/"+ ++ version+ ++ ".tar.gz"+ args :: [String]+ args = ["--print-path", url]
+ src/Elm2Nix/PackagesSnapshot.hs view
@@ -0,0 +1,139 @@+{- Downloads binary serialized https://package.elm-lang.org/all-packages+ as Elm compiler expects it to parse.++ Takes Elm upstream code from:+ - https://github.com/elm/compiler/blob/master/builder/src/Deps/Cache.hs+ - https://github.com/elm/compiler/blob/master/builder/src/Deps/Website.hs+ - https://github.com/elm/compiler/blob/master/compiler/src/Elm/Package.hs++-}+{-# LANGUAGE OverloadedStrings #-}+module Elm2Nix.PackagesSnapshot+ ( snapshot+ ) where++import Control.Monad (liftM2, liftM3)+import qualified Data.Aeson as Aeson+import qualified Data.Binary as Binary+import Data.Binary (Binary, put, get, putWord8, getWord8)+import qualified Data.Map as Map+import Data.Default (def)+import Data.Map (Map)+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Word (Word16)+import qualified Network.HTTP.Req as Req+import System.FilePath ((</>))+++data Name =+ Name+ { _author :: !Text+ , _project :: !Text+ }+ deriving (Eq, Ord)++data Package =+ Package+ { _name :: !Name+ , _version :: !Version+ }+ deriving (Eq, Ord)++data Version =+ Version+ { _major :: {-# UNPACK #-} !Word16+ , _minor :: {-# UNPACK #-} !Word16+ , _patch :: {-# UNPACK #-} !Word16+ }+ deriving (Eq, Ord)++data PackageRegistry =+ PackageRegistry Int (Map Name [Version])++instance Binary Name where+ get =+ liftM2 Name get get++ put (Name author project) =+ do put author+ put project++instance Binary Package where+ get =+ liftM2 Package get get++ put (Package name version) =+ do put name+ put version++instance Binary Version where+ get =+ do word <- getWord8+ if word == 0+ then liftM3 Version get get get+ else+ do minor <- fmap fromIntegral getWord8+ patch <- fmap fromIntegral getWord8+ return (Version (fromIntegral word) minor patch)++ put (Version major minor patch) =+ if major < 256 && minor < 256 && patch < 256 then+ do putWord8 (fromIntegral major)+ putWord8 (fromIntegral minor)+ putWord8 (fromIntegral patch)+ else+ do putWord8 0+ put major+ put minor+ put patch++instance Binary PackageRegistry where+ get = liftM2 PackageRegistry get get+ put (PackageRegistry a b) = put a >> put b++snapshot :: String -> IO ()+snapshot dir = do+ r <- Req.runReq def $+ Req.req+ Req.POST+ (Req.https "package.elm-lang.org" Req./: "all-packages")+ Req.NoReqBody+ Req.jsonResponse+ mempty+ let packages = unwrap $ case Aeson.fromJSON (Req.responseBody r) of+ Aeson.Error s -> error s+ Aeson.Success val -> val+ size = Map.foldr ((+) . length) 0 packages+ registry = PackageRegistry size packages+ Binary.encodeFile (dir </> "versions.dat") registry++newtype Packages = Packages { unwrap :: Map.Map Name [Version] }++instance Aeson.FromJSON Packages where+ parseJSON v = Packages <$> Aeson.parseJSON v++instance Aeson.FromJSON Version where+ parseJSON = Aeson.withText "string" $ \x ->+ case Text.splitOn "." x of+ [major, minor, patch] ->+ return $ Version+ (read (Text.unpack major))+ (read (Text.unpack minor))+ (read (Text.unpack patch))+ _ ->+ fail "failure parsing version"+++instance Aeson.FromJSON Name where+ parseJSON = Aeson.withText "string" $ \x ->+ case Text.splitOn "/" x of+ [author, package] -> return $ Name author package+ lst -> fail $ "wrong package name: " <> show lst+++instance Aeson.FromJSONKey Name where+ fromJSONKey = Aeson.FromJSONKeyTextParser $ \x ->+ case Text.splitOn "/" x of+ [author, package] -> return $ Name author package+ lst -> fail $ "wrong package name: " <> show lst
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"