├── test ├── data │ ├── LICENSE │ ├── ChangeLog.md │ ├── docs │ │ └── stuff │ ├── cabal-init-minimal.cabal.yaml │ ├── cabal-init-with-dot.cabal.yaml │ ├── cabal-init-with-executables.cabal.yaml │ ├── quoted-options.cabal.yaml │ ├── cabal-init-with-tested-with.cabal.yaml │ ├── cabal-init-with-conditionals.cabal.yaml │ ├── cabal-init-with-conditionals-buildable4.cabal.yaml │ ├── cabal-init-with-conditionals-buildable.cabal.yaml │ ├── cabal-init-minimal.cabal │ ├── cabal-init-with-dot.cabal │ ├── cabal-init-with-tests.cabal.yaml │ ├── cabal-init-with-conditionals-buildable5.cabal.yaml │ ├── cabal-init-with-executables.cabal │ ├── cabal-init-with-conditionals-and-else.cabal.yaml │ ├── quoted-options.cabal │ ├── cabal-init-with-conditionals.cabal │ ├── cabal-init-with-tested-with.cabal │ ├── cabal-init-with-benchmarks.cabal.yaml │ ├── cabal-init-with-conditionals-buildable.cabal │ ├── cabal-init-with-conditionals-and-else.cabal │ ├── cabal-init-with-conditionals-buildable4.cabal │ ├── cabal-init-with-conditionals-buildable5.cabal │ ├── cabal-init-with-tests.cabal │ ├── cabal-init-with-conditionals-buildable2.cabal │ ├── cabal-init-with-conditionals-buildable3.cabal.yaml │ ├── cabal-init-with-conditionals-buildable2.cabal.yaml │ ├── cabal-init-with-conditionals-buildable3.cabal │ ├── cabal-init-with-benchmarks.cabal │ ├── getopt-generics.cabal.yaml │ ├── hpack.cabal.yaml │ ├── getopt-generics.cabal │ └── hpack.cabal ├── Spec.hs ├── Hpack │ ├── GenericsUtilSpec.hs │ ├── HaskellSpec.hs │ ├── FormattingHintsSpec.hs │ ├── RenderSpec.hs │ ├── UtilSpec.hs │ ├── RunSpec.hs │ ├── ConvertSpec.hs │ └── ConfigSpec.hs ├── Helper.hs └── HpackSpec.hs ├── .dockerignore ├── .gitignore ├── stack.yaml ├── Setup.lhs ├── .ghci ├── driver └── Main.hs ├── stack-5.yaml ├── dockerfiles ├── ghc8.dockerfile ├── ghc7-10.dockerfile └── ghc7-8.dockerfile ├── appveyor.yml ├── src ├── Hpack │ ├── Yaml.hs │ ├── Haskell.hs │ ├── GenericsUtil.hs │ ├── Convert │ │ └── Run.hs │ ├── FormattingHints.hs │ ├── Util.hs │ ├── Render.hs │ ├── Run.hs │ └── Convert.hs └── Hpack.hs ├── .travis-setup.sh ├── LICENSE ├── package.yaml ├── README.md ├── Makefile ├── CHANGELOG.md ├── .travis.yml └── hpack-convert.cabal /test/data/LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/data/ChangeLog.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/data/docs/stuff: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .stack-work 2 | hpack-convert 3 | x86* 4 | dist 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | .stack-work 3 | x86_64-linux 4 | x86_64-osx 5 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | flags: {} 2 | packages: 3 | - '.' 4 | extra-deps: [] 5 | resolver: lts-8.13 6 | -------------------------------------------------------------------------------- /Setup.lhs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env runhaskell 2 | > import Distribution.Simple 3 | > main = defaultMain 4 | -------------------------------------------------------------------------------- /.ghci: -------------------------------------------------------------------------------- 1 | :set -isrc -itest -idist/build/autogen -DTEST -optP-include -optPdist/build/autogen/cabal_macros.h 2 | -------------------------------------------------------------------------------- /driver/Main.hs: -------------------------------------------------------------------------------- 1 | module Main (main) where 2 | 3 | import qualified Hpack 4 | 5 | main :: IO () 6 | main = Hpack.main 7 | -------------------------------------------------------------------------------- /stack-5.yaml: -------------------------------------------------------------------------------- 1 | flags: {} 2 | packages: 3 | - '.' 4 | extra-deps: 5 | - aeson-0.11.0.0 6 | - unordered-containers-0.2.7.1 7 | resolver: lts-5.3 8 | -------------------------------------------------------------------------------- /dockerfiles/ghc8.dockerfile: -------------------------------------------------------------------------------- 1 | FROM haskell:8 2 | ADD ./hpack-convert.cabal /hpack-convert/hpack-convert.cabal 3 | WORKDIR /hpack-convert 4 | RUN cabal update 5 | RUN cabal sandbox init 6 | RUN cabal install --only-dep -j 7 | ADD . /hpack-convert 8 | RUN cabal configure 9 | RUN cabal build 10 | -------------------------------------------------------------------------------- /dockerfiles/ghc7-10.dockerfile: -------------------------------------------------------------------------------- 1 | FROM haskell:8 2 | ADD ./hpack-convert.cabal /hpack-convert/hpack-convert.cabal 3 | WORKDIR /hpack-convert 4 | RUN cabal update 5 | RUN cabal sandbox init 6 | RUN cabal install --only-dep -j 7 | ADD . /hpack-convert 8 | RUN cabal configure 9 | RUN cabal build 10 | -------------------------------------------------------------------------------- /dockerfiles/ghc7-8.dockerfile: -------------------------------------------------------------------------------- 1 | FROM haskell:7.8.4 2 | ADD ./hpack-convert.cabal /hpack-convert/hpack-convert.cabal 3 | WORKDIR /hpack-convert 4 | RUN cabal update 5 | RUN cabal sandbox init 6 | RUN cabal install --only-dep -j 7 | ADD . /hpack-convert 8 | RUN cabal configure 9 | RUN cabal build 10 | -------------------------------------------------------------------------------- /test/data/cabal-init-minimal.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | library: 11 | source-dirs: src 12 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-dot.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | library: 11 | source-dirs: ./. 12 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-executables.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | -------------------------------------------------------------------------------- /test/data/quoted-options.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | ghc-options: 9 | - -rtsopts 10 | - ! '"-with-rtsopts=-N15 -H4G"' 11 | dependencies: 12 | - base >=4.8 && <4.9 13 | library: 14 | source-dirs: src 15 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-tested-with.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | tested-with: GHC==8.0.1 GHC==7.10.3 GHC==7.8.4 GHC==7.6.3 GHC==7.4.2 7 | extra-source-files: 8 | - ChangeLog.md 9 | dependencies: 10 | - base >=4.8 && <4.9 11 | library: 12 | source-dirs: src 13 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | when: 15 | - condition: os(osx) 16 | buildable: false 17 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable4.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | when: 15 | - condition: os(linux) 16 | buildable: false 17 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | when: 15 | - condition: ! '!(os(osx))' 16 | buildable: false 17 | -------------------------------------------------------------------------------- /test/data/cabal-init-minimal.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | library 11 | build-depends: base >=4.8 && <4.9 12 | hs-source-dirs: src 13 | default-language: Haskell2010 14 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-dot.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | library 11 | build-depends: base >=4.8 && <4.9 12 | hs-source-dirs: . 13 | default-language: Haskell2010 14 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-tests.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | tests: 15 | hello-world-spec: 16 | main: Spec.hs 17 | source-dirs: 18 | - src 19 | - test 20 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable5.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | buildable: false 15 | when: 16 | - condition: os(osx) 17 | buildable: true 18 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-executables.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | executable hello-world 11 | main-is: HelloWorld.hs 12 | build-depends: base >=4.8 && <4.9 13 | hs-source-dirs: src 14 | default-language: Haskell2010 15 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-and-else.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | when: 15 | - condition: os(osx) 16 | then: 17 | source-dirs: osx 18 | else: 19 | source-dirs: notosx 20 | -------------------------------------------------------------------------------- /test/data/quoted-options.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | library 11 | build-depends: base >=4.8 && <4.9 12 | hs-source-dirs: src 13 | default-language: Haskell2010 14 | ghc-options: -rtsopts "-with-rtsopts=-N15 -H4G" 15 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | executable hello-world 11 | main-is: HelloWorld.hs 12 | build-depends: base >=4.8 && <4.9 13 | hs-source-dirs: src 14 | default-language: Haskell2010 15 | 16 | if os(darwin) 17 | buildable: False 18 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-tested-with.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | tested-with: GHC==8.0.1, GHC==7.10.3, GHC==7.8.4, GHC==7.6.3, GHC==7.4.2 10 | 11 | library 12 | build-depends: base >=4.8 && <4.9 13 | hs-source-dirs: src 14 | default-language: Haskell2010 15 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Disabled cache in hope of improving reliability of AppVeyor builds 2 | #cache: 3 | #- "c:\\sr" # stack root, short paths == fewer problems 4 | 5 | build: off 6 | 7 | before_test: 8 | - curl -ostack.zip -L --insecure http://www.stackage.org/stack/windows-i386 9 | - 7z x stack.zip stack.exe 10 | 11 | clone_folder: "c:\\stack" 12 | environment: 13 | global: 14 | STACK_ROOT: "c:\\sr" 15 | 16 | test_script: 17 | - stack setup > nul 18 | # The ugly echo "" hack is to avoid complaints about 0 being an invalid file 19 | # descriptor 20 | - echo "" | stack --no-terminal test 21 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-benchmarks.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | tests: 15 | hello-world-spec: 16 | main: Spec.hs 17 | source-dirs: 18 | - src 19 | - test 20 | benchmarks: 21 | hello-world-benchmark: 22 | main: Bench.hs 23 | source-dirs: 24 | - src 25 | - benchmarks 26 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | executable hello-world 11 | main-is: HelloWorld.hs 12 | build-depends: base >=4.8 && <4.9 13 | hs-source-dirs: src 14 | default-language: Haskell2010 15 | 16 | if os(darwin) 17 | buildable: True 18 | else 19 | buildable: False 20 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-and-else.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | executable hello-world 11 | main-is: HelloWorld.hs 12 | build-depends: base >=4.8 && <4.9 13 | hs-source-dirs: src 14 | default-language: Haskell2010 15 | 16 | if os(darwin) 17 | hs-source-dirs: osx 18 | else 19 | hs-source-dirs: notosx 20 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable4.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | executable hello-world 11 | main-is: HelloWorld.hs 12 | build-depends: base >=4.8 && <4.9 13 | hs-source-dirs: src 14 | default-language: Haskell2010 15 | 16 | if os(darwin) 17 | buildable: True 18 | if os(linux) 19 | buildable: False 20 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable5.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | executable hello-world 11 | main-is: HelloWorld.hs 12 | build-depends: base >=4.8 && <4.9 13 | hs-source-dirs: src 14 | default-language: Haskell2010 15 | buildable: False 16 | 17 | if os(darwin) 18 | buildable: True 19 | if os(linux) 20 | buildable: False 21 | -------------------------------------------------------------------------------- /src/Hpack/Yaml.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RecordWildCards #-} 2 | module Hpack.Yaml where 3 | 4 | import Data.Yaml 5 | 6 | decodeYaml :: FromJSON a => FilePath -> IO (Either String a) 7 | decodeYaml file = do 8 | result <- decodeFileEither file 9 | return $ either (Left . errToString) Right result 10 | where 11 | errToString err = file ++ case err of 12 | AesonException e -> ": " ++ e 13 | InvalidYaml (Just (YamlException s)) -> ": " ++ s 14 | InvalidYaml (Just (YamlParseException{..})) -> ":" ++ show yamlLine ++ ":" ++ show yamlColumn ++ ": " ++ yamlProblem ++ " " ++ yamlContext 15 | where YamlMark{..} = yamlProblemMark 16 | _ -> ": " ++ show err 17 | -------------------------------------------------------------------------------- /test/Hpack/GenericsUtilSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric #-} 2 | module Hpack.GenericsUtilSpec (spec) where 3 | 4 | import Test.Hspec 5 | 6 | import Data.Proxy 7 | import GHC.Generics 8 | 9 | import Hpack.GenericsUtil 10 | 11 | data Person = Person { 12 | _personName :: String 13 | , _personAge :: Int 14 | } deriving Generic 15 | 16 | spec :: Spec 17 | spec = do 18 | describe "selectors" $ do 19 | it "returns a list of record selectors" $ do 20 | selectors (Proxy :: Proxy Person) `shouldBe` ["_personName", "_personAge"] 21 | 22 | describe "typeName" $ do 23 | it "gets datatype name" $ do 24 | typeName (Proxy :: Proxy Person) `shouldBe` "Person" 25 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-tests.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | executable hello-world 11 | main-is: HelloWorld.hs 12 | build-depends: base >=4.8 && <4.9 13 | hs-source-dirs: src 14 | default-language: Haskell2010 15 | 16 | test-suite hello-world-spec 17 | main-is: Spec.hs 18 | type: exitcode-stdio-1.0 19 | build-depends: base >=4.8 && <4.9 20 | hs-source-dirs: src 21 | , test 22 | default-language: Haskell2010 -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable2.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | Flag Something 11 | default: False 12 | 13 | Flag SomethingElse 14 | default: False 15 | 16 | executable hello-world 17 | main-is: HelloWorld.hs 18 | build-depends: base >=4.8 && <4.9 19 | hs-source-dirs: src 20 | default-language: Haskell2010 21 | 22 | if os(darwin) 23 | buildable: True 24 | if flag(Something) 25 | buildable: True 26 | if flag(SomethingElse) 27 | buildable: False 28 | else 29 | buildable: False 30 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable3.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | buildable: false 15 | when: 16 | - condition: os(osx) 17 | buildable: true 18 | when: 19 | - condition: flag(something) 20 | when: 21 | - condition: flag(somethingelse) 22 | buildable: false 23 | flags: 24 | somethingelse: 25 | description: null 26 | manual: false 27 | default: false 28 | something: 29 | description: null 30 | manual: false 31 | default: false 32 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable2.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: '0.1.0.0' 3 | author: Pedro Tacla Yamada 4 | maintainer: tacla.yamada@gmail.com 5 | license: PublicDomain 6 | extra-source-files: 7 | - ChangeLog.md 8 | dependencies: 9 | - base >=4.8 && <4.9 10 | executables: 11 | hello-world: 12 | main: HelloWorld.hs 13 | source-dirs: src 14 | when: 15 | - condition: os(osx) 16 | then: 17 | when: 18 | - condition: flag(something) 19 | when: 20 | - condition: flag(somethingelse) 21 | buildable: false 22 | else: 23 | buildable: false 24 | flags: 25 | somethingelse: 26 | description: null 27 | manual: false 28 | default: false 29 | something: 30 | description: null 31 | manual: false 32 | default: false 33 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-conditionals-buildable3.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | Flag Something 11 | default: False 12 | 13 | Flag SomethingElse 14 | default: False 15 | 16 | executable hello-world 17 | main-is: HelloWorld.hs 18 | build-depends: base >=4.8 && <4.9 19 | hs-source-dirs: src 20 | default-language: Haskell2010 21 | buildable: False 22 | 23 | if os(darwin) 24 | buildable: True 25 | if flag(Something) 26 | buildable: True 27 | if flag(SomethingElse) 28 | buildable: False 29 | else 30 | buildable: False 31 | -------------------------------------------------------------------------------- /test/Helper.hs: -------------------------------------------------------------------------------- 1 | module Helper ( 2 | module Test.Hspec 3 | , module Test.Mockery.Directory 4 | , module Control.Applicative 5 | , withTempDirectory 6 | , module System.FilePath 7 | , withCurrentDirectory 8 | ) where 9 | 10 | import Test.Hspec 11 | import Test.Mockery.Directory 12 | import Control.Applicative 13 | import System.Directory (getCurrentDirectory, setCurrentDirectory, canonicalizePath) 14 | import Control.Exception 15 | import qualified System.IO.Temp as Temp 16 | import System.FilePath 17 | 18 | withCurrentDirectory :: FilePath -> IO a -> IO a 19 | withCurrentDirectory dir action = do 20 | bracket getCurrentDirectory setCurrentDirectory $ \ _ -> do 21 | setCurrentDirectory dir 22 | action 23 | 24 | withTempDirectory :: (FilePath -> IO a) -> IO a 25 | withTempDirectory action = Temp.withSystemTempDirectory "hspec" $ \dir -> do 26 | canonicalizePath dir >>= action 27 | -------------------------------------------------------------------------------- /test/data/cabal-init-with-benchmarks.cabal: -------------------------------------------------------------------------------- 1 | name: cabal-init-minimal 2 | version: 0.1.0.0 3 | license: PublicDomain 4 | author: Pedro Tacla Yamada 5 | maintainer: tacla.yamada@gmail.com 6 | build-type: Simple 7 | extra-source-files: ChangeLog.md 8 | cabal-version: >=1.10 9 | 10 | executable hello-world 11 | main-is: HelloWorld.hs 12 | build-depends: base >=4.8 && <4.9 13 | hs-source-dirs: src 14 | default-language: Haskell2010 15 | 16 | test-suite hello-world-spec 17 | main-is: Spec.hs 18 | type: exitcode-stdio-1.0 19 | build-depends: base >=4.8 && <4.9 20 | hs-source-dirs: src 21 | , test 22 | default-language: Haskell2010 23 | 24 | benchmark hello-world-benchmark 25 | main-is: Bench.hs 26 | type: exitcode-stdio-1.0 27 | build-depends: base >=4.8 && <4.9 28 | hs-source-dirs: src 29 | , benchmarks 30 | default-language: Haskell2010 -------------------------------------------------------------------------------- /test/Hpack/HaskellSpec.hs: -------------------------------------------------------------------------------- 1 | module Hpack.HaskellSpec (spec) where 2 | 3 | import Test.Hspec 4 | 5 | import Hpack.Haskell 6 | 7 | spec :: Spec 8 | spec = do 9 | describe "isModule" $ do 10 | it "accepts module names" $ do 11 | isModule ["Foo", "Bar"] `shouldBe` True 12 | 13 | it "rejects the empty list" $ do 14 | isModule [] `shouldBe` False 15 | 16 | describe "isQualifiedIdentifier" $ do 17 | it "accepts qualified Haskell identifiers" $ do 18 | isQualifiedIdentifier ["Foo", "Bar", "baz"] `shouldBe` True 19 | 20 | it "rejects invalid input" $ do 21 | isQualifiedIdentifier ["Foo", "Bar", "Baz"] `shouldBe` False 22 | 23 | describe "isIdentifier" $ do 24 | it "accepts Haskell identifiers" $ do 25 | isIdentifier "foo" `shouldBe` True 26 | 27 | it "rejects reserved keywords" $ do 28 | isIdentifier "case" `shouldBe` False 29 | 30 | it "rejects invalid input" $ do 31 | isIdentifier "Foo" `shouldBe` False 32 | -------------------------------------------------------------------------------- /.travis-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | travis_retry() { 6 | cmd=$* 7 | $cmd || (sleep 2 && $cmd) || (sleep 10 && $cmd) 8 | } 9 | 10 | fetch_stack_osx() { 11 | curl -skL https://www.stackage.org/stack/osx-x86_64 | tar xz --strip-components=1 --include '*/stack' -C ~/.local/bin; 12 | } 13 | 14 | fetch_stack_linux() { 15 | curl -sL https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'; 16 | } 17 | 18 | fetch_tinc() { 19 | curl -sL https://zalora-public.s3.amazonaws.com/tinc > $HOME/.local/bin/tinc 20 | } 21 | 22 | mkdir -p $HOME/.local/bin 23 | 24 | case "$BUILD" in 25 | stack) 26 | if [ `uname` = "Darwin" ]; then 27 | travis_retry fetch_stack_osx 28 | else 29 | travis_retry fetch_stack_linux 30 | fi; 31 | 32 | travis_retry stack --no-terminal setup; 33 | ;; 34 | tinc) 35 | travis_retry fetch_tinc 36 | chmod +x $HOME/.local/bin/tinc 37 | travis_retry cabal update 38 | sed -i 's/^jobs:/-- jobs:/' $HOME/.cabal/config 39 | ;; 40 | esac 41 | -------------------------------------------------------------------------------- /test/data/getopt-generics.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: getopt-generics 2 | version: '0.13' 3 | synopsis: Create command line interfaces with ease 4 | description: Create command line interfaces with ease 5 | category: Console, System 6 | author: 7 | - Linh Nguyen 8 | - Sönke Hahn 9 | maintainer: 10 | - linh.nguyen@zalora.com 11 | - soenkehahn@gmail.com 12 | copyright: Zalora South East Asia Pte Ltd 13 | license: BSD3 14 | github: soenkehahn/getopt-generics 15 | extra-source-files: 16 | - docs/stuff 17 | ghc-options: 18 | - -Wall 19 | - -fno-warn-name-shadowing 20 | dependencies: 21 | - base ==4.* 22 | - base-compat >=0.8 23 | - base-orphans 24 | - generics-sop >=0.1 && <0.3 25 | - tagged 26 | library: 27 | source-dirs: src 28 | exposed-modules: 29 | - WithCli 30 | - WithCli.Pure 31 | tests: 32 | spec: 33 | main: Spec.hs 34 | source-dirs: 35 | - src 36 | - test 37 | - docs 38 | ghc-options: 39 | - -threaded 40 | - -O0 41 | dependencies: 42 | - hspec >=2.1.8 43 | - QuickCheck 44 | - silently 45 | - filepath 46 | - safe 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016 Simon Hengel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/data/hpack.cabal.yaml: -------------------------------------------------------------------------------- 1 | name: hpack 2 | version: '0.14.1' 3 | synopsis: An alternative format for Haskell packages 4 | category: Development 5 | maintainer: Simon Hengel 6 | license: MIT 7 | github: sol/hpack 8 | dependencies: 9 | - base >=4.7 && <5 10 | - base-compat >=0.8 11 | - Cabal 12 | - pretty 13 | - deepseq 14 | - directory 15 | - filepath 16 | - Glob 17 | - text 18 | - containers 19 | - unordered-containers 20 | - yaml 21 | - bytestring 22 | - vector 23 | library: 24 | source-dirs: src 25 | ghc-options: -Wall 26 | exposed-modules: 27 | - Hpack 28 | - Hpack.Config 29 | - Hpack.Run 30 | - Hpack.Yaml 31 | dependencies: 32 | - aeson >=0.8 33 | executables: 34 | hpack: 35 | main: Main.hs 36 | source-dirs: driver 37 | ghc-options: -Wall 38 | dependencies: 39 | - hpack 40 | - aeson >=0.8 41 | tests: 42 | spec: 43 | main: Spec.hs 44 | source-dirs: 45 | - test 46 | - src 47 | ghc-options: -Wall 48 | cpp-options: -DTEST 49 | dependencies: 50 | - hspec ==2.* 51 | - QuickCheck 52 | - temporary 53 | - mockery >=0.3 54 | - interpolate 55 | - aeson-qq 56 | - aeson >=0.10 57 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: hpack-convert 2 | version: '1.0.1' 3 | synopsis: Convert Cabal manifests into hpack's package.yamls 4 | maintainer: Pedro Tacla Yamada 5 | license: MIT 6 | github: yamadapc/hpack-convert 7 | category: Development 8 | 9 | ghc-options: -Wall -fcontext-stack=100 10 | extra-source-files: 11 | - ./test/data/**/* 12 | 13 | dependencies: 14 | - base >= 4.7 && < 5 15 | - base-compat >= 0.8 16 | - Cabal >= 1.22 17 | - pretty 18 | - deepseq 19 | - directory 20 | - filepath 21 | - Glob 22 | - text 23 | - containers 24 | - unordered-containers >= 0.2.7.1 25 | - yaml 26 | - bytestring 27 | - vector 28 | - aeson 29 | - split 30 | 31 | library: 32 | source-dirs: src 33 | dependencies: 34 | exposed-modules: 35 | - Hpack.Convert 36 | 37 | executables: 38 | hpack-convert: 39 | main: Main.hs 40 | source-dirs: 41 | - driver 42 | - src 43 | 44 | tests: 45 | spec: 46 | cpp-options: -DTEST 47 | main: Spec.hs 48 | source-dirs: 49 | - test 50 | - src 51 | dependencies: 52 | - hspec == 2.* 53 | - QuickCheck 54 | - temporary 55 | - mockery >= 0.3 56 | - interpolate 57 | - aeson-qq 58 | -------------------------------------------------------------------------------- /src/Hpack/Haskell.hs: -------------------------------------------------------------------------------- 1 | module Hpack.Haskell ( 2 | isModule 3 | , isQualifiedIdentifier 4 | , isIdentifier 5 | ) where 6 | 7 | import Data.Char 8 | 9 | isModule :: [String] -> Bool 10 | isModule name = (not . null) name && all isModuleName name 11 | 12 | isModuleName :: String -> Bool 13 | isModuleName name = case name of 14 | x : xs -> isUpper x && all isIdChar xs 15 | _ -> False 16 | 17 | isQualifiedIdentifier :: [String] -> Bool 18 | isQualifiedIdentifier name = case reverse name of 19 | x : xs -> isIdentifier x && isModule xs 20 | _ -> False 21 | 22 | isIdentifier :: String -> Bool 23 | isIdentifier name = case name of 24 | x : xs -> isLower x && all isIdChar xs && name `notElem` reserved 25 | _ -> False 26 | 27 | reserved :: [String] 28 | reserved = [ 29 | "case" 30 | , "class" 31 | , "data" 32 | , "default" 33 | , "deriving" 34 | , "do" 35 | , "else" 36 | , "foreign" 37 | , "if" 38 | , "import" 39 | , "in" 40 | , "infix" 41 | , "infixl" 42 | , "infixr" 43 | , "instance" 44 | , "let" 45 | , "module" 46 | , "newtype" 47 | , "of" 48 | , "then" 49 | , "type" 50 | , "where" 51 | , "_" 52 | ] 53 | 54 | isIdChar :: Char -> Bool 55 | isIdChar c = isAlphaNum c || c == '_' || c == '\'' 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hpack-convert 2 | [![Build Status](https://travis-ci.org/yamadapc/hpack-convert.svg?branch=hpack-convert)](https://travis-ci.org/yamadapc/hpack-convert) 3 | - - - 4 | Convert Cabal manifests into [hpack's package.yamls](https://github.com/sol/hpack). 5 | 6 | ## Installing from source 7 | ``` 8 | git clone https://github.com/yamadapc/hpack-convert 9 | cd hpack-convert 10 | stack install 11 | ``` 12 | 13 | ## Download a pre-built binary 14 | - [OSX](https://github.com/yamadapc/hpack-convert/releases/download/v0.14.4/hpack-convert_x86_64-osx.tar.gz) 15 | - [Linux 64-bits (_Requires **libgmp**_)](https://github.com/yamadapc/hpack-convert/releases/download/v0.14.4/hpack-convert_x86_64-linux.tar.gz) 16 | 17 | ## Usage 18 | ```bash 19 | # Inside a directory with a .cabal file, run: 20 | hpack-convert 21 | # This will convert your .cabal file into a `package.yaml` 22 | ``` 23 | 24 | ## Using the web-service without installing anything 25 | There's a simple web-service running `hpack-convert` on a free Heroku dyno, if 26 | it's awake, this command should convert your cabal file: 27 | ```bash 28 | curl -F "cabalfile=@./`echo *.cabal`" https://hpack-convert.herokuapp.com 29 | ``` 30 | _Source-code at https://github.com/yamadapc/hpack-convert-api_ 31 | 32 | ## License 33 | MIT 34 | -------------------------------------------------------------------------------- /src/Hpack/GenericsUtil.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE PolyKinds #-} 2 | {-# LANGUAGE TypeOperators #-} 3 | {-# LANGUAGE FlexibleInstances #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | {-# LANGUAGE FlexibleContexts #-} 6 | {-# LANGUAGE TypeFamilies #-} 7 | {-# LANGUAGE UndecidableInstances #-} 8 | module Hpack.GenericsUtil ( 9 | HasTypeName 10 | , typeName 11 | , Selectors 12 | , selectors 13 | ) where 14 | 15 | import Data.Proxy 16 | import GHC.Generics 17 | 18 | class HasTypeName a where 19 | typeName :: Proxy a -> String 20 | 21 | instance (Datatype d, Generic a, Rep a ~ M1 D d m) => HasTypeName a where 22 | typeName _ = datatypeName (undefined :: M1 D d x y) 23 | 24 | selectors :: (Selectors (Rep a)) => Proxy a -> [String] 25 | selectors = f 26 | where 27 | f :: forall a. (Selectors (Rep a)) => Proxy a -> [String] 28 | f _ = selNames (Proxy :: Proxy (Rep a)) 29 | 30 | class Selectors a where 31 | selNames :: Proxy a -> [String] 32 | 33 | instance Selectors f => Selectors (M1 D x f) where 34 | selNames _ = selNames (Proxy :: Proxy f) 35 | 36 | instance Selectors f => Selectors (M1 C x f) where 37 | selNames _ = selNames (Proxy :: Proxy f) 38 | 39 | instance Selector s => Selectors (M1 S s (K1 R t)) where 40 | selNames _ = [selName (undefined :: M1 S s (K1 R t) ())] 41 | 42 | instance (Selectors a, Selectors b) => Selectors (a :*: b) where 43 | selNames _ = selNames (Proxy :: Proxy a) ++ selNames (Proxy :: Proxy b) 44 | 45 | instance Selectors U1 where 46 | selNames _ = [] 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | version=$(shell cat package.yaml | yaml2json | jq -r '.version') 2 | 3 | test: FORCE 4 | stack build --stack-yaml ./stack-5.yaml 5 | stack test --stack-yaml ./stack-5.yaml 6 | stack build 7 | stack test 8 | 9 | docker-build: 10 | for dockerfile in dockerfiles/*; do docker build -f $$dockerfile . ; done 11 | 12 | all: 13 | make x86_64-osx/hpack-convert 14 | make x86_64-linux/hpack-convert 15 | 16 | make upload-osx 17 | make upload-linux-64 18 | 19 | upload-osx: x86_64-osx/hpack-convert 20 | github-release upload --name hpack-convert_x86_64-osx.tar.gz --label "Pre-built binary for OSX" -u yamadapc -s $$GITHUB_API_TOKEN -r hpack-convert -t v$(version) -f ./x86_64-osx/hpack-convert_x86_64-osx.tar.gz 21 | 22 | upload-linux-64: x86_64-linux/hpack-convert 23 | github-release upload --name hpack-convert_x86_64-linux.tar.gz --label "Pre-built binary for Linux 64-bits" -u yamadapc -s $$GITHUB_API_TOKEN -r hpack-convert -t v$(version) -f ./x86_64-linux/hpack-convert_x86_64-linux.tar.gz 24 | 25 | x86_64-osx/hpack-convert: FORCE 26 | stack build 27 | rm -rf ./x86_64-osx 28 | mkdir -p x86_64-osx 29 | cp `stack path --dist-dir`/build/hpack-convert/hpack-convert ./x86_64-osx/ 30 | cd ./x86_64-osx; tar -zcvf hpack-convert_x86_64-osx.tar.gz * 31 | 32 | x86_64-linux/hpack-convert: FORCE 33 | stack docker pull 34 | stack build --docker 35 | rm -rf ./x86_64-linux 36 | mkdir -p x86_64-linux 37 | cp `stack path --docker --dist-dir`/build/hpack-convert/hpack-convert ./x86_64-linux/ 38 | cd ./x86_64-linux; tar -zcvf hpack-convert_x86_64-linux.tar.gz * 39 | 40 | FORCE: 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.0.0](https://github.com/yamadapc/hpack-convert/tree/v1.0.0) (2017-05-12) 4 | [Full Changelog](https://github.com/yamadapc/hpack-convert/compare/v0.14.7...v1.0.0) 5 | 6 | **Closed issues:** 7 | 8 | - Do not expose library component [\#4](https://github.com/yamadapc/hpack-convert/issues/4) 9 | 10 | ## [v0.14.7](https://github.com/yamadapc/hpack-convert/tree/v0.14.7) (2017-05-12) 11 | [Full Changelog](https://github.com/yamadapc/hpack-convert/compare/0.17.0...v0.14.7) 12 | 13 | **Closed issues:** 14 | 15 | - Cannot use with aeson 1 \(lts-8.5\) [\#5](https://github.com/yamadapc/hpack-convert/issues/5) 16 | 17 | ## [v0.14.6](https://github.com/yamadapc/hpack-convert/tree/v0.14.6) (2016-08-24) 18 | [Full Changelog](https://github.com/yamadapc/hpack-convert/compare/v0.14.5...v0.14.6) 19 | 20 | **Closed issues:** 21 | 22 | - tested-with is broken? [\#3](https://github.com/yamadapc/hpack-convert/issues/3) 23 | 24 | ## [v0.14.5](https://github.com/yamadapc/hpack-convert/tree/v0.14.5) (2016-08-18) 25 | [Full Changelog](https://github.com/yamadapc/hpack-convert/compare/v0.14.4...v0.14.5) 26 | 27 | **Closed issues:** 28 | 29 | - tested-with should be on top of block fields [\#2](https://github.com/yamadapc/hpack-convert/issues/2) 30 | 31 | ## [v0.14.4](https://github.com/yamadapc/hpack-convert/tree/v0.14.4) (2016-08-11) 32 | [Full Changelog](https://github.com/yamadapc/hpack-convert/compare/0.14.3...v0.14.4) 33 | 34 | **Closed issues:** 35 | 36 | - Test suite failure [\#1](https://github.com/yamadapc/hpack-convert/issues/1) 37 | 38 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: c 4 | 5 | matrix: 6 | include: 7 | - env: BUILD=stack GHCVER=7.10.3 STACK_YAML=stack.yaml 8 | compiler: ": #stack 7.10.3" 9 | addons: {apt: {packages: [ghc-7.10.3,happy-1.19.5], sources: [hvr-ghc]}} 10 | - env: BUILD=stack GHCVER=7.10.3 STACK_YAML=stack-5.yaml 11 | compiler: ": #stack 7.10.3" 12 | addons: {apt: {packages: [ghc-7.10.3,happy-1.19.5], sources: [hvr-ghc]}} 13 | - env: BUILD=stack STACK_YAML=stack.yaml 14 | compiler: ": #stack 7.10.3 osx" 15 | os: osx 16 | - env: BUILD=stack STACK_YAML=stack.yaml 17 | compiler: ": #stack 8.0.2" 18 | addons: {apt: {packages: [cabal-install-1.22,ghc-8.0.2,happy-1.19.5], sources: [hvr-ghc]}} 19 | - env: BUILD=cabal STACK_YAML=stack.yaml 20 | compiler: ": #stack 8.0.2" 21 | addons: {apt: {packages: [cabal-install-1.22,ghc-8.0.2,happy-1.19.5], sources: [hvr-ghc]}} 22 | 23 | before_install: 24 | - unset CC 25 | - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:/opt/happy/1.19.5/bin:$PATH 26 | - ./.travis-setup.sh 27 | - ghc --version || stack exec -- ghc --version 28 | - happy --version || true 29 | - cabal --version || true 30 | - stack --version || true 31 | - tick --version || true 32 | 33 | install: 34 | - case "$BUILD" in 35 | stack) 36 | stack --no-terminal test --only-dependencies;; 37 | cabal) 38 | cabal install --only-dependencies -j 39 | esac 40 | 41 | script: 42 | - case "$BUILD" in 43 | stack) 44 | stack --no-terminal test --haddock --no-haddock-deps;; 45 | cabal) 46 | cabal test 47 | esac 48 | 49 | cache: 50 | directories: 51 | - $HOME/.stack 52 | -------------------------------------------------------------------------------- /src/Hpack/Convert/Run.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TupleSections #-} 2 | module Hpack.Convert.Run 3 | ( runConvert 4 | ) 5 | where 6 | 7 | import Prelude () 8 | import Prelude.Compat 9 | 10 | import Control.Monad 11 | import Data.List 12 | import Data.Maybe 13 | import System.Directory 14 | import System.Environment 15 | import System.Exit.Compat 16 | import System.FilePath 17 | import System.FilePath.Glob 18 | 19 | import Hpack.Config 20 | import Hpack.Convert 21 | 22 | runConvert :: IO () 23 | runConvert = do 24 | as <- getArgs 25 | (dir, cabalFileFP) <- case as of 26 | (dir:_) -> do 27 | isFile <- doesFileExist dir 28 | if takeExtension dir == ".cabal" && isFile 29 | then return (takeDirectory dir, dir) 30 | else (dir,) <$> findCabalFileFP dir 31 | _ -> do 32 | cwd <- getCurrentDirectory 33 | cabalFileFP <- findCabalFileFP cwd 34 | return (cwd, cabalFileFP) 35 | pkg <- runConvert' dir cabalFileFP 36 | writePackage (dir "package.yaml") pkg 37 | putStrLn $ "generated package.yaml based on " ++ cabalFileFP 38 | 39 | findCabalFileFP :: FilePath -> IO FilePath 40 | findCabalFileFP dir = do 41 | mcabalFileFP <- listToMaybe <$> globDir1 (compile "*.cabal") dir 42 | case mcabalFileFP of 43 | Nothing -> die "No cabal file in the current directory" 44 | Just cabalFileFP -> return cabalFileFP 45 | 46 | runConvert' :: FilePath -> FilePath -> IO Package 47 | runConvert' dir cabalFileFP = do 48 | mpackageYaml <- find (== "package.yaml") <$> getDirectoryContents dir 49 | 50 | when (isJust mpackageYaml) $ 51 | die $ (dir "package.yaml") ++ " already exists" 52 | 53 | old <- readFile cabalFileFP 54 | case fromPackageDescriptionString old of 55 | Left err -> die (show err) 56 | Right pkg -> return pkg 57 | -------------------------------------------------------------------------------- /test/HpackSpec.hs: -------------------------------------------------------------------------------- 1 | module HpackSpec (spec) where 2 | 3 | import Prelude () 4 | import Prelude.Compat 5 | 6 | import Control.Monad.Compat 7 | import Control.DeepSeq 8 | import Data.Version (Version (..), showVersion) 9 | 10 | import Test.Hspec 11 | import Test.Mockery.Directory 12 | import Test.QuickCheck 13 | 14 | import Hpack 15 | import Hpack.Config () 16 | import Hpack.Convert () 17 | 18 | makeVersion :: [Int] -> Version 19 | makeVersion v = Version v [] 20 | 21 | spec :: Spec 22 | spec = do 23 | describe "extractVersion" $ do 24 | it "extracts Hpack version from a cabal file" $ do 25 | let cabalFile = ["-- This file has been generated from package.yaml by hpack version 0.10.0."] 26 | extractVersion cabalFile `shouldBe` Just (Version [0, 10, 0] []) 27 | 28 | it "is total" $ do 29 | let cabalFile = ["-- This file has been generated from package.yaml by hpack version "] 30 | extractVersion cabalFile `shouldBe` Nothing 31 | 32 | describe "parseVersion" $ do 33 | it "is inverse to showVersion" $ do 34 | let positive = getPositive <$> arbitrary 35 | forAll (replicateM 3 positive) $ \xs -> do 36 | let v = Version xs [] 37 | parseVersion (showVersion v) `shouldBe` Just v 38 | 39 | describe "hpackWithVersion" $ do 40 | context "when only the hpack version in the cabal file header changed" $ do 41 | it "does not write a new cabal file" $ do 42 | inTempDirectory $ do 43 | writeFile "package.yaml" "name: foo" 44 | hpackWithVersion (makeVersion [0,8,0]) "." False 45 | old <- readFile "foo.cabal" >>= (return $!!) 46 | hpackWithVersion (makeVersion [0,10,0]) "." False 47 | readFile "foo.cabal" `shouldReturn` old 48 | 49 | context "when exsting cabal file was generated with a newer version of hpack" $ do 50 | it "does not re-generate" $ do 51 | inTempDirectory $ do 52 | writeFile "package.yaml" $ unlines [ 53 | "name: foo" 54 | , "version: 0.1.0" 55 | ] 56 | hpackWithVersion (makeVersion [0,10,0]) "." False 57 | old <- readFile "foo.cabal" >>= (return $!!) 58 | 59 | writeFile "package.yaml" $ unlines [ 60 | "name: foo" 61 | , "version: 0.2.0" 62 | ] 63 | 64 | hpackWithVersion (makeVersion [0,8,0]) "." False 65 | readFile "foo.cabal" `shouldReturn` old 66 | -------------------------------------------------------------------------------- /test/data/getopt-generics.cabal: -------------------------------------------------------------------------------- 1 | -- This file has been generated from package.yaml by hpack version 0.5.4. 2 | -- 3 | -- see: https://github.com/sol/hpack 4 | 5 | name: getopt-generics 6 | version: 0.13 7 | synopsis: Create command line interfaces with ease 8 | description: Create command line interfaces with ease 9 | category: Console, System 10 | homepage: https://github.com/soenkehahn/getopt-generics#readme 11 | bug-reports: https://github.com/soenkehahn/getopt-generics/issues 12 | author: Linh Nguyen, Sönke Hahn 13 | maintainer: linh.nguyen@zalora.com, soenkehahn@gmail.com 14 | copyright: Zalora South East Asia Pte Ltd 15 | license: BSD3 16 | license-file: LICENSE 17 | build-type: Simple 18 | cabal-version: >=1.10 19 | 20 | extra-source-files: 21 | docs/stuff 22 | 23 | source-repository head 24 | type: git 25 | location: https://github.com/soenkehahn/getopt-generics 26 | 27 | library 28 | hs-source-dirs: 29 | src 30 | ghc-options: -Wall -fno-warn-name-shadowing 31 | build-depends: 32 | base ==4.* 33 | , base-compat >=0.8 34 | , base-orphans 35 | , generics-sop >=0.1 && <0.3 36 | , tagged 37 | exposed-modules: 38 | WithCli 39 | WithCli.Pure 40 | other-modules: 41 | WithCli.Argument 42 | WithCli.Flag 43 | WithCli.HasArguments 44 | WithCli.Modifier 45 | WithCli.Modifier.Types 46 | WithCli.Normalize 47 | WithCli.Parser 48 | WithCli.Pure.Internal 49 | WithCli.Result 50 | default-language: Haskell2010 51 | 52 | test-suite spec 53 | type: exitcode-stdio-1.0 54 | main-is: Spec.hs 55 | hs-source-dirs: 56 | src 57 | , test 58 | , docs 59 | ghc-options: -Wall -fno-warn-name-shadowing -threaded -O0 60 | build-depends: 61 | base ==4.* 62 | , base-compat >=0.8 63 | , base-orphans 64 | , generics-sop >=0.1 && <0.3 65 | , tagged 66 | , hspec >=2.1.8 67 | , QuickCheck 68 | , silently 69 | , filepath 70 | , safe 71 | other-modules: 72 | WithCli 73 | WithCli.Argument 74 | WithCli.Flag 75 | WithCli.HasArguments 76 | WithCli.Modifier 77 | WithCli.Modifier.Types 78 | WithCli.Normalize 79 | WithCli.Parser 80 | WithCli.Pure 81 | WithCli.Pure.Internal 82 | WithCli.Result 83 | DocsSpec 84 | ModifiersSpec 85 | ModifiersSpec.RenameOptionsSpec 86 | ModifiersSpec.UseForPositionalArgumentsSpec 87 | ShellProtocol 88 | Util 89 | WithCli.ArgumentSpec 90 | WithCli.HasArgumentsSpec 91 | WithCli.ModifierSpec 92 | WithCli.NormalizeSpec 93 | WithCli.ParserSpec 94 | WithCli.Pure.RecordSpec 95 | WithCli.PureSpec 96 | WithCli.ResultSpec 97 | WithCliSpec 98 | CustomOption 99 | CustomOptionRecord 100 | RecordType 101 | Simple 102 | Test01 103 | Test02 104 | Test03 105 | Test04 106 | default-language: Haskell2010 107 | -------------------------------------------------------------------------------- /test/data/hpack.cabal: -------------------------------------------------------------------------------- 1 | -- This file has been generated from package.yaml by hpack version 0.14.0. 2 | -- 3 | -- see: https://github.com/sol/hpack 4 | 5 | name: hpack 6 | version: 0.14.1 7 | synopsis: An alternative format for Haskell packages 8 | category: Development 9 | homepage: https://github.com/sol/hpack#readme 10 | bug-reports: https://github.com/sol/hpack/issues 11 | maintainer: Simon Hengel 12 | license: MIT 13 | license-file: LICENSE 14 | build-type: Simple 15 | cabal-version: >= 1.10 16 | 17 | source-repository head 18 | type: git 19 | location: https://github.com/sol/hpack 20 | 21 | library 22 | hs-source-dirs: 23 | src 24 | ghc-options: -Wall 25 | build-depends: 26 | base >= 4.7 && < 5 27 | , base-compat >= 0.8 28 | , Cabal 29 | , pretty 30 | , deepseq 31 | , directory 32 | , filepath 33 | , Glob 34 | , text 35 | , containers 36 | , unordered-containers 37 | , yaml 38 | , bytestring 39 | , vector 40 | , aeson >= 0.8 41 | exposed-modules: 42 | Hpack 43 | Hpack.Config 44 | Hpack.Run 45 | Hpack.Yaml 46 | other-modules: 47 | Hpack.Convert 48 | Hpack.Convert.Run 49 | Hpack.FormattingHints 50 | Hpack.GenericsUtil 51 | Hpack.Haskell 52 | Hpack.Render 53 | Hpack.Util 54 | Paths_hpack 55 | default-language: Haskell2010 56 | 57 | executable hpack 58 | main-is: Main.hs 59 | hs-source-dirs: 60 | driver 61 | ghc-options: -Wall 62 | build-depends: 63 | base >= 4.7 && < 5 64 | , base-compat >= 0.8 65 | , Cabal 66 | , pretty 67 | , deepseq 68 | , directory 69 | , filepath 70 | , Glob 71 | , text 72 | , containers 73 | , unordered-containers 74 | , yaml 75 | , bytestring 76 | , vector 77 | , hpack 78 | , aeson >= 0.8 79 | default-language: Haskell2010 80 | 81 | test-suite spec 82 | type: exitcode-stdio-1.0 83 | main-is: Spec.hs 84 | hs-source-dirs: 85 | test 86 | , src 87 | ghc-options: -Wall 88 | cpp-options: -DTEST 89 | build-depends: 90 | base >= 4.7 && < 5 91 | , base-compat >= 0.8 92 | , Cabal 93 | , pretty 94 | , deepseq 95 | , directory 96 | , filepath 97 | , Glob 98 | , text 99 | , containers 100 | , unordered-containers 101 | , yaml 102 | , bytestring 103 | , vector 104 | , hspec == 2.* 105 | , QuickCheck 106 | , temporary 107 | , mockery >= 0.3 108 | , interpolate 109 | , aeson-qq 110 | , aeson >= 0.10 111 | other-modules: 112 | Helper 113 | Hpack.ConfigSpec 114 | Hpack.ConvertSpec 115 | Hpack.FormattingHintsSpec 116 | Hpack.GenericsUtilSpec 117 | Hpack.HaskellSpec 118 | Hpack.RenderSpec 119 | Hpack.RunSpec 120 | Hpack.UtilSpec 121 | HpackSpec 122 | Hpack 123 | Hpack.Config 124 | Hpack.Convert 125 | Hpack.Convert.Run 126 | Hpack.FormattingHints 127 | Hpack.GenericsUtil 128 | Hpack.Haskell 129 | Hpack.Render 130 | Hpack.Run 131 | Hpack.Util 132 | Hpack.Yaml 133 | default-language: Haskell2010 134 | -------------------------------------------------------------------------------- /src/Hpack.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | module Hpack ( 3 | hpack 4 | , hpackResult 5 | , Result(..) 6 | , Status(..) 7 | , version 8 | , main 9 | #ifdef TEST 10 | , hpackWithVersion 11 | , extractVersion 12 | , parseVersion 13 | #endif 14 | ) where 15 | 16 | import Prelude () 17 | import Prelude.Compat 18 | 19 | import Control.DeepSeq 20 | import Control.Exception 21 | import Control.Monad.Compat 22 | import Data.List.Compat 23 | import Data.Maybe 24 | import Data.Version (Version) 25 | import qualified Data.Version as Version 26 | import System.Environment 27 | import System.IO 28 | import System.IO.Error 29 | import Text.ParserCombinators.ReadP 30 | 31 | import Paths_hpack_convert (version) 32 | import Hpack.Config 33 | import Hpack.Convert.Run 34 | import Hpack.Run 35 | 36 | programVersion :: Version -> String 37 | programVersion v = "hpack version " ++ Version.showVersion v 38 | 39 | header :: Version -> String 40 | header v = unlines [ 41 | "-- This file has been generated from " ++ packageConfig ++ " by " ++ programVersion v ++ "." 42 | , "--" 43 | , "-- see: https://github.com/sol/hpack" 44 | , "" 45 | ] 46 | 47 | main :: IO () 48 | main = do 49 | args <- getArgs 50 | case args of 51 | ["--version"] -> putStrLn (programVersion version) 52 | ["--help"] -> printHelp 53 | _ -> runConvert 54 | 55 | printHelp :: IO () 56 | printHelp = do 57 | hPutStr stderr $ unlines [ 58 | "Usage: hpack-convert [ dir | cabalfile ]" 59 | , " hpack-convert --version" 60 | , " hpack-convert --help" 61 | ] 62 | 63 | safeInit :: [a] -> [a] 64 | safeInit [] = [] 65 | safeInit xs = init xs 66 | 67 | extractVersion :: [String] -> Maybe Version 68 | extractVersion = listToMaybe . mapMaybe (stripPrefix prefix >=> parseVersion . safeInit) 69 | where 70 | prefix = "-- This file has been generated from package.yaml by hpack version " 71 | 72 | parseVersion :: String -> Maybe Version 73 | parseVersion xs = case [v | (v, "") <- readP_to_S Version.parseVersion xs] of 74 | [v] -> Just v 75 | _ -> Nothing 76 | 77 | hpack :: FilePath -> Bool -> IO () 78 | hpack = hpackWithVersion version 79 | 80 | hpackResult :: FilePath -> IO Result 81 | hpackResult = hpackWithVersionResult version 82 | 83 | data Result = Result { 84 | resultWarnings :: [String] 85 | , resultCabalFile :: String 86 | , resultStatus :: Status 87 | } 88 | 89 | data Status = Generated | AlreadyGeneratedByNewerHpack | OutputUnchanged 90 | 91 | hpackWithVersion :: Version -> FilePath -> Bool -> IO () 92 | hpackWithVersion v dir verbose = do 93 | r <- hpackWithVersionResult v dir 94 | forM_ (resultWarnings r) $ \warning -> hPutStrLn stderr ("WARNING: " ++ warning) 95 | when verbose $ putStrLn $ 96 | case resultStatus r of 97 | Generated -> "generated " ++ resultCabalFile r 98 | OutputUnchanged -> resultCabalFile r ++ " is up-to-date" 99 | AlreadyGeneratedByNewerHpack -> resultCabalFile r ++ " was generated with a newer version of hpack, please upgrade and try again." 100 | 101 | hpackWithVersionResult :: Version -> FilePath -> IO Result 102 | hpackWithVersionResult v dir = do 103 | (warnings, cabalFile, new) <- run dir 104 | old <- either (const Nothing) (Just . splitHeader) <$> tryJust (guard . isDoesNotExistError) (readFile cabalFile >>= (return $!!)) 105 | let oldVersion = fmap fst old >>= extractVersion 106 | status <- 107 | if (oldVersion <= Just v) then 108 | if (fmap snd old == Just (lines new)) then 109 | return OutputUnchanged 110 | else do 111 | writeFile cabalFile $ header v ++ new 112 | return Generated 113 | else 114 | return AlreadyGeneratedByNewerHpack 115 | return Result 116 | { resultWarnings = warnings 117 | , resultCabalFile = cabalFile 118 | , resultStatus = status 119 | } 120 | where 121 | splitHeader :: String -> ([String], [String]) 122 | splitHeader = fmap (dropWhile null) . span ("--" `isPrefixOf`) . lines 123 | -------------------------------------------------------------------------------- /src/Hpack/FormattingHints.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE ViewPatterns #-} 3 | module Hpack.FormattingHints ( 4 | FormattingHints (..) 5 | , sniffFormattingHints 6 | #ifdef TEST 7 | , extractFieldOrder 8 | , extractSectionsFieldOrder 9 | , breakLines 10 | , unindent 11 | , sniffAlignment 12 | , splitField 13 | , sniffIndentation 14 | , sniffCommaStyle 15 | #endif 16 | ) where 17 | 18 | import Prelude () 19 | import Prelude.Compat 20 | 21 | import Data.Char 22 | import Data.Maybe 23 | import Data.List.Compat 24 | import Control.Applicative 25 | 26 | import Hpack.Render 27 | -- import Hpack.Util 28 | 29 | data FormattingHints = FormattingHints { 30 | formattingHintsFieldOrder :: [String] 31 | , formattingHintsSectionsFieldOrder :: [(String, [String])] 32 | , formattingHintsAlignment :: Maybe Alignment 33 | , formattingHintsRenderSettings :: RenderSettings 34 | } deriving (Eq, Show) 35 | 36 | sniffFormattingHints :: String -> FormattingHints 37 | sniffFormattingHints (breakLines -> input) = FormattingHints { 38 | formattingHintsFieldOrder = extractFieldOrder input 39 | , formattingHintsSectionsFieldOrder = extractSectionsFieldOrder input 40 | , formattingHintsAlignment = sniffAlignment input 41 | , formattingHintsRenderSettings = sniffRenderSettings input 42 | } 43 | 44 | breakLines :: String -> [String] 45 | breakLines = filter (not . null) . map (reverse . dropWhile isSpace . reverse) . lines 46 | 47 | extractFieldOrder :: [String] -> [String] 48 | extractFieldOrder = map fst . catMaybes . map splitField 49 | 50 | extractSectionsFieldOrder :: [String] -> [(String, [String])] 51 | extractSectionsFieldOrder = map (fmap extractFieldOrder) . splitSections 52 | where 53 | splitSections input = case break startsWithSpace input of 54 | ([], []) -> [] 55 | (xs, ys) -> case span startsWithSpace ys of 56 | (fields, zs) -> case reverse xs of 57 | name : _ -> (name, unindent fields) : splitSections zs 58 | _ -> splitSections zs 59 | 60 | startsWithSpace :: String -> Bool 61 | startsWithSpace xs = case xs of 62 | y : _ -> isSpace y 63 | _ -> False 64 | 65 | unindent :: [String] -> [String] 66 | unindent input = map (drop indentation) input 67 | where 68 | indentation = minimum $ map (length . takeWhile isSpace) input 69 | 70 | sniffAlignment :: [String] -> Maybe Alignment 71 | sniffAlignment input = case nub . catMaybes . map indentation . catMaybes . map splitField $ input of 72 | [n] -> Just (Alignment n) 73 | _ -> Nothing 74 | where 75 | 76 | indentation :: (String, String) -> Maybe Int 77 | indentation (name, value) = case span isSpace value of 78 | (_, "") -> Nothing 79 | (xs, _) -> (Just . succ . length $ name ++ xs) 80 | 81 | splitField :: String -> Maybe (String, String) 82 | splitField field = case span isNameChar field of 83 | (xs, ':':ys) -> Just (xs, ys) 84 | _ -> Nothing 85 | where 86 | isNameChar = (`elem` nameChars) 87 | nameChars = ['a'..'z'] ++ ['A'..'Z'] ++ "-" 88 | 89 | sniffIndentation :: [String] -> Maybe Int 90 | sniffIndentation input = sniffFrom "library" <|> sniffFrom "executable" 91 | where 92 | sniffFrom :: String -> Maybe Int 93 | sniffFrom section = case findSection . removeEmptyLines $ input of 94 | _ : x : _ -> Just . length $ takeWhile isSpace x 95 | _ -> Nothing 96 | where 97 | findSection = dropWhile (not . isPrefixOf section) 98 | 99 | removeEmptyLines :: [String] -> [String] 100 | removeEmptyLines = filter $ any (not . isSpace) 101 | 102 | sniffCommaStyle :: [String] -> Maybe CommaStyle 103 | sniffCommaStyle input 104 | | any startsWithComma input = Just LeadingCommas 105 | | any (startsWithComma . reverse) input = Just TrailingCommas 106 | | otherwise = Nothing 107 | where 108 | startsWithComma = isPrefixOf "," . dropWhile isSpace 109 | 110 | sniffRenderSettings :: [String] -> RenderSettings 111 | sniffRenderSettings input = RenderSettings indentation fieldAlignment commaStyle 112 | where 113 | indentation = fromMaybe (renderSettingsIndentation defaultRenderSettings) (sniffIndentation input) 114 | fieldAlignment = renderSettingsFieldAlignment defaultRenderSettings 115 | commaStyle = fromMaybe (renderSettingsCommaStyle defaultRenderSettings) (sniffCommaStyle input) 116 | -------------------------------------------------------------------------------- /src/Hpack/Util.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | module Hpack.Util ( 3 | List(..) 4 | , GhcOption 5 | , GhcProfOption 6 | , CppOption 7 | , CCOption 8 | , LdOption 9 | , parseMain 10 | , toModule 11 | , getFilesRecursive 12 | , tryReadFile 13 | , expandGlobs 14 | , sort 15 | , lexicographically 16 | ) where 17 | 18 | import Prelude () 19 | import Prelude.Compat 20 | 21 | import Control.Applicative 22 | import Control.DeepSeq 23 | import Control.Exception 24 | import Control.Monad.Compat 25 | import Data.Aeson.Types 26 | import Data.Char 27 | import Data.Data 28 | import Data.List.Compat hiding (sort) 29 | import Data.Ord 30 | import System.Directory 31 | import System.FilePath 32 | import qualified System.FilePath.Posix as Posix 33 | import System.FilePath.Glob 34 | 35 | import Hpack.Haskell 36 | 37 | sort :: [String] -> [String] 38 | sort = sortBy (comparing lexicographically) 39 | 40 | lexicographically :: String -> (String, String) 41 | lexicographically x = (map toLower x, x) 42 | 43 | newtype List a = List {fromList :: [a]} 44 | deriving (Eq, Show, Data, Typeable) 45 | 46 | instance FromJSON a => FromJSON (List a) where 47 | parseJSON v = List <$> case v of 48 | Array _ -> parseJSON v 49 | _ -> return <$> parseJSON v 50 | 51 | type GhcOption = String 52 | type GhcProfOption = String 53 | type CppOption = String 54 | type CCOption = String 55 | type LdOption = String 56 | 57 | parseMain :: String -> (FilePath, [GhcOption]) 58 | parseMain main = case reverse name of 59 | x : _ | isQualifiedIdentifier name && x `notElem` ["hs", "lhs"] -> (intercalate "/" (init name) ++ ".hs", ["-main-is " ++ main]) 60 | _ | isModule name -> (intercalate "/" name ++ ".hs", ["-main-is " ++ main]) 61 | _ -> (main, []) 62 | where 63 | name = splitOn '.' main 64 | 65 | splitOn :: Char -> String -> [String] 66 | splitOn c = go 67 | where 68 | go xs = case break (== c) xs of 69 | (ys, "") -> [ys] 70 | (ys, _:zs) -> ys : go zs 71 | 72 | toModule :: [FilePath] -> Maybe String 73 | toModule path = case reverse path of 74 | [] -> Nothing 75 | x : xs -> do 76 | m <- stripSuffix ".hs" x <|> stripSuffix ".lhs" x <|> stripSuffix ".hsc" x 77 | let name = reverse (m : xs) 78 | guard (isModule name) >> return (intercalate "." name) 79 | where 80 | stripSuffix :: String -> String -> Maybe String 81 | stripSuffix suffix x = reverse <$> stripPrefix (reverse suffix) (reverse x) 82 | 83 | getFilesRecursive :: FilePath -> IO [[String]] 84 | getFilesRecursive baseDir = go [] 85 | where 86 | go :: [FilePath] -> IO [[FilePath]] 87 | go dir = do 88 | c <- map ((dir ++) . return) . filter (`notElem` [".", ".."]) <$> getDirectoryContents (pathTo dir) 89 | subdirsFiles <- filterM (doesDirectoryExist . pathTo) c >>= mapM go 90 | files <- filterM (doesFileExist . pathTo) c 91 | return (files ++ concat subdirsFiles) 92 | where 93 | pathTo :: [FilePath] -> FilePath 94 | pathTo p = baseDir joinPath p 95 | 96 | tryReadFile :: FilePath -> IO (Maybe String) 97 | tryReadFile file = do 98 | r <- try (readFile file) :: IO (Either IOException String) 99 | return $!! either (const Nothing) Just r 100 | 101 | toPosixFilePath :: FilePath -> FilePath 102 | toPosixFilePath = Posix.joinPath . splitDirectories 103 | 104 | expandGlobs :: FilePath -> [String] -> IO ([String], [FilePath]) 105 | expandGlobs dir patterns = do 106 | files <- (fst <$> globDir compiledPatterns dir) >>= mapM removeDirectories 107 | let warnings = [warn pattern | ([], pattern) <- zip files patterns] 108 | return (warnings, combineResults files) 109 | where 110 | combineResults = nub . sort . map (toPosixFilePath . makeRelative dir) . concat 111 | warn pattern = "Specified pattern " ++ show pattern ++ " for extra-source-files does not match any files" 112 | compiledPatterns = map (compileWith options) patterns 113 | removeDirectories = filterM doesFileExist 114 | options = CompOptions { 115 | characterClasses = False 116 | , characterRanges = False 117 | , numberRanges = False 118 | , wildcards = True 119 | , recursiveWildcards = True 120 | , pathSepInRanges = False 121 | , errorRecovery = True 122 | } 123 | -------------------------------------------------------------------------------- /src/Hpack/Render.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 4 | module Hpack.Render ( 5 | -- * AST 6 | Element (..) 7 | , Value (..) 8 | 9 | -- * Render 10 | , RenderSettings (..) 11 | , CommaStyle (..) 12 | , defaultRenderSettings 13 | , Alignment (..) 14 | , Nesting 15 | , render 16 | 17 | -- * Utils 18 | , sortFieldsBy 19 | 20 | #ifdef TEST 21 | , Lines (..) 22 | , renderValue 23 | , addSortKey 24 | #endif 25 | ) where 26 | 27 | import Prelude () 28 | import Prelude.Compat 29 | 30 | import Data.String 31 | import Data.List.Compat 32 | 33 | data Value = 34 | Literal String 35 | | CommaSeparatedList [String] 36 | | LineSeparatedList [String] 37 | | WordList [String] 38 | deriving (Eq, Show) 39 | 40 | data Element = Stanza String [Element] | Group Element Element | Field String Value 41 | deriving (Eq, Show) 42 | 43 | data Lines = SingleLine String | MultipleLines [String] 44 | deriving (Eq, Show) 45 | 46 | data CommaStyle = LeadingCommas | TrailingCommas 47 | deriving (Eq, Show) 48 | 49 | newtype Nesting = Nesting Int 50 | deriving (Eq, Show, Num, Enum) 51 | 52 | newtype Alignment = Alignment Int 53 | deriving (Eq, Show, Num) 54 | 55 | data RenderSettings = RenderSettings { 56 | renderSettingsIndentation :: Int 57 | , renderSettingsFieldAlignment :: Alignment 58 | , renderSettingsCommaStyle :: CommaStyle 59 | } deriving (Eq, Show) 60 | 61 | defaultRenderSettings :: RenderSettings 62 | defaultRenderSettings = RenderSettings 2 0 LeadingCommas 63 | 64 | render :: RenderSettings -> Nesting -> Element -> [String] 65 | render settings nesting (Stanza name elements) = indent settings nesting name : renderElements settings (succ nesting) elements 66 | render settings nesting (Group a b) = render settings nesting a ++ render settings nesting b 67 | render settings nesting (Field name value) = renderField settings nesting name value 68 | 69 | renderElements :: RenderSettings -> Nesting -> [Element] -> [String] 70 | renderElements settings nesting = concatMap (render settings nesting) 71 | 72 | renderField :: RenderSettings -> Nesting -> String -> Value -> [String] 73 | renderField settings@RenderSettings{..} nesting name value = case renderValue settings value of 74 | SingleLine "" -> [] 75 | SingleLine x -> [indent settings nesting (name ++ ": " ++ padding ++ x)] 76 | MultipleLines [] -> [] 77 | MultipleLines xs -> (indent settings nesting name ++ ":") : map (indent settings $ succ nesting) xs 78 | where 79 | Alignment fieldAlignment = renderSettingsFieldAlignment 80 | padding = replicate (fieldAlignment - length name - 2) ' ' 81 | 82 | renderValue :: RenderSettings -> Value -> Lines 83 | renderValue RenderSettings{..} v = case v of 84 | Literal s -> SingleLine s 85 | WordList ws -> SingleLine $ unwords ws 86 | LineSeparatedList xs -> renderLineSeparatedList renderSettingsCommaStyle xs 87 | CommaSeparatedList xs -> renderCommaSeparatedList renderSettingsCommaStyle xs 88 | 89 | renderLineSeparatedList :: CommaStyle -> [String] -> Lines 90 | renderLineSeparatedList style = MultipleLines . map (padding ++) 91 | where 92 | padding = case style of 93 | LeadingCommas -> " " 94 | TrailingCommas -> "" 95 | 96 | renderCommaSeparatedList :: CommaStyle -> [String] -> Lines 97 | renderCommaSeparatedList style = MultipleLines . case style of 98 | LeadingCommas -> map renderLeadingComma . zip (True : repeat False) 99 | TrailingCommas -> map renderTrailingComma . reverse . zip (True : repeat False) . reverse 100 | where 101 | renderLeadingComma :: (Bool, String) -> String 102 | renderLeadingComma (isFirst, x) 103 | | isFirst = " " ++ x 104 | | otherwise = ", " ++ x 105 | 106 | renderTrailingComma :: (Bool, String) -> String 107 | renderTrailingComma (isLast, x) 108 | | isLast = x 109 | | otherwise = x ++ "," 110 | 111 | instance IsString Value where 112 | fromString = Literal 113 | 114 | indent :: RenderSettings -> Nesting -> String -> String 115 | indent RenderSettings{..} (Nesting nesting) s = replicate (nesting * renderSettingsIndentation) ' ' ++ s 116 | 117 | sortFieldsBy :: [String] -> [Element] -> [Element] 118 | sortFieldsBy existingFieldOrder = 119 | map snd 120 | . sortOn fst 121 | . addSortKey 122 | . map (\a -> (existingIndex a, a)) 123 | where 124 | existingIndex :: Element -> Maybe Int 125 | existingIndex (Field name _) = name `elemIndex` existingFieldOrder 126 | existingIndex _ = Nothing 127 | 128 | addSortKey :: [(Maybe Int, a)] -> [((Int, Int), a)] 129 | addSortKey = go (-1) . zip [0..] 130 | where 131 | go :: Int -> [(Int, (Maybe Int, a))] -> [((Int, Int), a)] 132 | go n xs = case xs of 133 | [] -> [] 134 | (x, (Just y, a)) : ys -> ((y, x), a) : go y ys 135 | (x, (Nothing, a)) : ys -> ((n, x), a) : go n ys 136 | -------------------------------------------------------------------------------- /test/Hpack/FormattingHintsSpec.hs: -------------------------------------------------------------------------------- 1 | module Hpack.FormattingHintsSpec (spec) where 2 | 3 | import Test.Hspec 4 | 5 | import Hpack.FormattingHints 6 | import Hpack.Render 7 | 8 | spec :: Spec 9 | spec = do 10 | describe "extractFieldOrder" $ do 11 | it "extracts field order hints" $ do 12 | let input = [ 13 | "name: cabalize" 14 | , "version: 0.0.0" 15 | , "license:" 16 | , "license-file: " 17 | , "build-type: Simple" 18 | , "cabal-version: >= 1.10" 19 | ] 20 | extractFieldOrder input `shouldBe` [ 21 | "name" 22 | , "version" 23 | , "license" 24 | , "license-file" 25 | , "build-type" 26 | , "cabal-version" 27 | ] 28 | 29 | describe "extractSectionsFieldOrder" $ do 30 | it "splits input into sections" $ do 31 | let input = [ 32 | "name: cabalize" 33 | , "version: 0.0.0" 34 | , "" 35 | , "library" 36 | , " foo: 23" 37 | , " bar: 42" 38 | , "" 39 | , "executable foo" 40 | , " bar: 23" 41 | , " baz: 42" 42 | ] 43 | extractSectionsFieldOrder input `shouldBe` [("library", ["foo", "bar"]), ("executable foo", ["bar", "baz"])] 44 | 45 | describe "breakLines" $ do 46 | it "breaks input into lines" $ do 47 | let input = unlines [ 48 | "foo" 49 | , "" 50 | , " " 51 | , " bar " 52 | , " baz" 53 | ] 54 | breakLines input `shouldBe` [ 55 | "foo" 56 | , " bar" 57 | , " baz" 58 | ] 59 | 60 | describe "unindent" $ do 61 | it "unindents" $ do 62 | let input = [ 63 | " foo" 64 | , " bar" 65 | , " baz" 66 | ] 67 | unindent input `shouldBe` [ 68 | " foo" 69 | , "bar" 70 | , " baz" 71 | ] 72 | 73 | describe "sniffAlignment" $ do 74 | it "sniffs field alignment from given cabal file" $ do 75 | let input = [ 76 | "name: cabalize" 77 | , "version: 0.0.0" 78 | , "license: MIT" 79 | , "license-file: LICENSE" 80 | , "build-type: Simple" 81 | , "cabal-version: >= 1.10" 82 | ] 83 | sniffAlignment input `shouldBe` Just 16 84 | 85 | it "ignores fields without a value on the same line" $ do 86 | let input = [ 87 | "name: cabalize" 88 | , "version: 0.0.0" 89 | , "description: " 90 | , " foo" 91 | , " bar" 92 | ] 93 | sniffAlignment input `shouldBe` Just 16 94 | 95 | describe "splitField" $ do 96 | it "splits fields" $ do 97 | splitField "foo: bar" `shouldBe` Just ("foo", " bar") 98 | 99 | it "accepts fields names with dashes" $ do 100 | splitField "foo-bar: baz" `shouldBe` Just ("foo-bar", " baz") 101 | 102 | it "rejects fields names with spaces" $ do 103 | splitField "foo bar: baz" `shouldBe` Nothing 104 | 105 | it "rejects invalid fields" $ do 106 | splitField "foo bar" `shouldBe` Nothing 107 | 108 | describe "sniffIndentation" $ do 109 | it "sniff alignment from executable section" $ do 110 | let input = [ 111 | "name: foo" 112 | , "version: 0.0.0" 113 | , "" 114 | , "executable foo" 115 | , " build-depends: bar" 116 | ] 117 | sniffIndentation input `shouldBe` Just 4 118 | 119 | it "sniff alignment from library section" $ do 120 | let input = [ 121 | "name: foo" 122 | , "version: 0.0.0" 123 | , "" 124 | , "library" 125 | , " build-depends: bar" 126 | ] 127 | sniffIndentation input `shouldBe` Just 4 128 | 129 | it "ignores empty lines" $ do 130 | let input = [ 131 | "executable foo" 132 | , "" 133 | , " build-depends: bar" 134 | ] 135 | sniffIndentation input `shouldBe` Just 4 136 | 137 | it "ignores whitespace lines" $ do 138 | let input = [ 139 | "executable foo" 140 | , " " 141 | , " build-depends: bar" 142 | ] 143 | sniffIndentation input `shouldBe` Just 4 144 | 145 | describe "sniffCommaStyle" $ do 146 | it "detects leading commas" $ do 147 | let input = [ 148 | "executable foo" 149 | , " build-depends:" 150 | , " bar" 151 | , " , baz" 152 | ] 153 | sniffCommaStyle input `shouldBe` Just LeadingCommas 154 | 155 | it "detects trailing commas" $ do 156 | let input = [ 157 | "executable foo" 158 | , " build-depends:" 159 | , " bar, " 160 | , " baz" 161 | ] 162 | sniffCommaStyle input `shouldBe` Just TrailingCommas 163 | 164 | context "when detection fails" $ do 165 | it "returns Nothing" $ do 166 | sniffCommaStyle [] `shouldBe` Nothing 167 | -------------------------------------------------------------------------------- /hpack-convert.cabal: -------------------------------------------------------------------------------- 1 | -- This file has been generated from package.yaml by hpack version 0.17.0. 2 | -- 3 | -- see: https://github.com/sol/hpack 4 | 5 | name: hpack-convert 6 | version: 1.0.1 7 | synopsis: Convert Cabal manifests into hpack's package.yamls 8 | category: Development 9 | homepage: https://github.com/yamadapc/hpack-convert#readme 10 | bug-reports: https://github.com/yamadapc/hpack-convert/issues 11 | maintainer: Pedro Tacla Yamada 12 | license: MIT 13 | license-file: LICENSE 14 | build-type: Simple 15 | cabal-version: >= 1.10 16 | 17 | extra-source-files: 18 | ./test/data/cabal-init-minimal.cabal 19 | ./test/data/cabal-init-minimal.cabal.yaml 20 | ./test/data/cabal-init-with-benchmarks.cabal 21 | ./test/data/cabal-init-with-benchmarks.cabal.yaml 22 | ./test/data/cabal-init-with-conditionals-and-else.cabal 23 | ./test/data/cabal-init-with-conditionals-and-else.cabal.yaml 24 | ./test/data/cabal-init-with-conditionals-buildable.cabal 25 | ./test/data/cabal-init-with-conditionals-buildable.cabal.yaml 26 | ./test/data/cabal-init-with-conditionals-buildable2.cabal 27 | ./test/data/cabal-init-with-conditionals-buildable2.cabal.yaml 28 | ./test/data/cabal-init-with-conditionals-buildable3.cabal 29 | ./test/data/cabal-init-with-conditionals-buildable3.cabal.yaml 30 | ./test/data/cabal-init-with-conditionals-buildable4.cabal 31 | ./test/data/cabal-init-with-conditionals-buildable4.cabal.yaml 32 | ./test/data/cabal-init-with-conditionals-buildable5.cabal 33 | ./test/data/cabal-init-with-conditionals-buildable5.cabal.yaml 34 | ./test/data/cabal-init-with-conditionals.cabal 35 | ./test/data/cabal-init-with-conditionals.cabal.yaml 36 | ./test/data/cabal-init-with-dot.cabal 37 | ./test/data/cabal-init-with-dot.cabal.yaml 38 | ./test/data/cabal-init-with-executables.cabal 39 | ./test/data/cabal-init-with-executables.cabal.yaml 40 | ./test/data/cabal-init-with-tested-with.cabal 41 | ./test/data/cabal-init-with-tested-with.cabal.yaml 42 | ./test/data/cabal-init-with-tests.cabal 43 | ./test/data/cabal-init-with-tests.cabal.yaml 44 | ./test/data/ChangeLog.md 45 | ./test/data/docs/stuff 46 | ./test/data/getopt-generics.cabal 47 | ./test/data/getopt-generics.cabal.yaml 48 | ./test/data/hpack.cabal 49 | ./test/data/hpack.cabal.yaml 50 | ./test/data/LICENSE 51 | ./test/data/quoted-options.cabal 52 | ./test/data/quoted-options.cabal.yaml 53 | 54 | source-repository head 55 | type: git 56 | location: https://github.com/yamadapc/hpack-convert 57 | 58 | library 59 | hs-source-dirs: 60 | src 61 | ghc-options: -Wall -fcontext-stack=100 62 | build-depends: 63 | base >= 4.7 && < 5 64 | , base-compat >= 0.8 65 | , Cabal >= 1.22 66 | , pretty 67 | , deepseq 68 | , directory 69 | , filepath 70 | , Glob 71 | , text 72 | , containers 73 | , unordered-containers >= 0.2.7.1 74 | , yaml 75 | , bytestring 76 | , vector 77 | , aeson 78 | , split 79 | exposed-modules: 80 | Hpack.Convert 81 | other-modules: 82 | Hpack 83 | Hpack.Config 84 | Hpack.Convert.Run 85 | Hpack.FormattingHints 86 | Hpack.GenericsUtil 87 | Hpack.Haskell 88 | Hpack.Render 89 | Hpack.Run 90 | Hpack.Util 91 | Hpack.Yaml 92 | Paths_hpack_convert 93 | default-language: Haskell2010 94 | 95 | executable hpack-convert 96 | main-is: Main.hs 97 | hs-source-dirs: 98 | driver 99 | src 100 | ghc-options: -Wall -fcontext-stack=100 101 | build-depends: 102 | base >= 4.7 && < 5 103 | , base-compat >= 0.8 104 | , Cabal >= 1.22 105 | , pretty 106 | , deepseq 107 | , directory 108 | , filepath 109 | , Glob 110 | , text 111 | , containers 112 | , unordered-containers >= 0.2.7.1 113 | , yaml 114 | , bytestring 115 | , vector 116 | , aeson 117 | , split 118 | other-modules: 119 | Hpack 120 | Hpack.Config 121 | Hpack.Convert 122 | Hpack.Convert.Run 123 | Hpack.FormattingHints 124 | Hpack.GenericsUtil 125 | Hpack.Haskell 126 | Hpack.Render 127 | Hpack.Run 128 | Hpack.Util 129 | Hpack.Yaml 130 | default-language: Haskell2010 131 | 132 | test-suite spec 133 | type: exitcode-stdio-1.0 134 | main-is: Spec.hs 135 | hs-source-dirs: 136 | test 137 | src 138 | ghc-options: -Wall -fcontext-stack=100 139 | cpp-options: -DTEST 140 | build-depends: 141 | base >= 4.7 && < 5 142 | , base-compat >= 0.8 143 | , Cabal >= 1.22 144 | , pretty 145 | , deepseq 146 | , directory 147 | , filepath 148 | , Glob 149 | , text 150 | , containers 151 | , unordered-containers >= 0.2.7.1 152 | , yaml 153 | , bytestring 154 | , vector 155 | , aeson 156 | , split 157 | , hspec == 2.* 158 | , QuickCheck 159 | , temporary 160 | , mockery >= 0.3 161 | , interpolate 162 | , aeson-qq 163 | other-modules: 164 | Helper 165 | Hpack.ConfigSpec 166 | Hpack.ConvertSpec 167 | Hpack.FormattingHintsSpec 168 | Hpack.GenericsUtilSpec 169 | Hpack.HaskellSpec 170 | Hpack.RenderSpec 171 | Hpack.RunSpec 172 | Hpack.UtilSpec 173 | HpackSpec 174 | Hpack 175 | Hpack.Config 176 | Hpack.Convert 177 | Hpack.Convert.Run 178 | Hpack.FormattingHints 179 | Hpack.GenericsUtil 180 | Hpack.Haskell 181 | Hpack.Render 182 | Hpack.Run 183 | Hpack.Util 184 | Hpack.Yaml 185 | default-language: Haskell2010 186 | -------------------------------------------------------------------------------- /test/Hpack/RenderSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Hpack.RenderSpec where 3 | 4 | import Prelude () 5 | import Prelude.Compat 6 | 7 | import Test.Hspec 8 | import Test.QuickCheck 9 | import Data.List.Compat 10 | import Data.Maybe 11 | 12 | import Hpack.Render 13 | 14 | spec :: Spec 15 | spec = do 16 | describe "render" $ do 17 | let render_ = render defaultRenderSettings 0 18 | context "when rendering a Stanza" $ do 19 | it "renders stanza" $ do 20 | let stanza = Stanza "foo" [ 21 | Field "bar" "23" 22 | , Field "baz" "42" 23 | ] 24 | render_ stanza `shouldBe` [ 25 | "foo" 26 | , " bar: 23" 27 | , " baz: 42" 28 | ] 29 | 30 | it "omits empty fields" $ do 31 | let stanza = Stanza "foo" [ 32 | Field "bar" "23" 33 | , Field "baz" (WordList []) 34 | ] 35 | render_ stanza `shouldBe` [ 36 | "foo" 37 | , " bar: 23" 38 | ] 39 | 40 | it "allows to customize indentation" $ do 41 | let stanza = Stanza "foo" [ 42 | Field "bar" "23" 43 | , Field "baz" "42" 44 | ] 45 | render defaultRenderSettings{renderSettingsIndentation = 4} 0 stanza `shouldBe` [ 46 | "foo" 47 | , " bar: 23" 48 | , " baz: 42" 49 | ] 50 | 51 | it "renders nested stanzas" $ do 52 | let input = Stanza "foo" [Field "bar" "23", Stanza "baz" [Field "qux" "42"]] 53 | render_ input `shouldBe` [ 54 | "foo" 55 | , " bar: 23" 56 | , " baz" 57 | , " qux: 42" 58 | ] 59 | 60 | context "when rendering a Field" $ do 61 | context "when rendering a MultipleLines value" $ do 62 | it "takes nesting into account" $ do 63 | let field = Field "foo" (CommaSeparatedList ["bar", "baz"]) 64 | render defaultRenderSettings 1 field `shouldBe` [ 65 | " foo:" 66 | , " bar" 67 | , " , baz" 68 | ] 69 | 70 | context "when value is empty" $ do 71 | it "returns an empty list" $ do 72 | let field = Field "foo" (CommaSeparatedList []) 73 | render_ field `shouldBe` [] 74 | 75 | context "when rendering a SingleLine value" $ do 76 | it "returns a single line" $ do 77 | let field = Field "foo" (Literal "bar") 78 | render_ field `shouldBe` ["foo: bar"] 79 | 80 | it "takes nesting into account" $ do 81 | let field = Field "foo" (Literal "bar") 82 | render defaultRenderSettings 2 field `shouldBe` [" foo: bar"] 83 | 84 | it "takes alignment into account" $ do 85 | let field = Field "foo" (Literal "bar") 86 | render defaultRenderSettings {renderSettingsFieldAlignment = 10} 0 field `shouldBe` ["foo: bar"] 87 | 88 | context "when value is empty" $ do 89 | it "returns an empty list" $ do 90 | let field = Field "foo" (Literal "") 91 | render_ field `shouldBe` [] 92 | 93 | describe "renderValue" $ do 94 | it "renders WordList" $ do 95 | renderValue defaultRenderSettings (WordList ["foo", "bar", "baz"]) `shouldBe` SingleLine "foo bar baz" 96 | 97 | it "renders CommaSeparatedList" $ do 98 | renderValue defaultRenderSettings (CommaSeparatedList ["foo", "bar", "baz"]) `shouldBe` MultipleLines [ 99 | " foo" 100 | , ", bar" 101 | , ", baz" 102 | ] 103 | 104 | it "renders LineSeparatedList" $ do 105 | renderValue defaultRenderSettings (LineSeparatedList ["foo", "bar", "baz"]) `shouldBe` MultipleLines [ 106 | " foo" 107 | , " bar" 108 | , " baz" 109 | ] 110 | 111 | context "when renderSettingsCommaStyle is TrailingCommas" $ do 112 | let settings = defaultRenderSettings{renderSettingsCommaStyle = TrailingCommas} 113 | 114 | it "renders CommaSeparatedList with trailing commas" $ do 115 | renderValue settings (CommaSeparatedList ["foo", "bar", "baz"]) `shouldBe` MultipleLines [ 116 | "foo," 117 | , "bar," 118 | , "baz" 119 | ] 120 | 121 | it "renders LineSeparatedList without padding" $ do 122 | renderValue settings (LineSeparatedList ["foo", "bar", "baz"]) `shouldBe` MultipleLines [ 123 | "foo" 124 | , "bar" 125 | , "baz" 126 | ] 127 | 128 | describe "sortFieldsBy" $ do 129 | let 130 | field name = Field name (Literal $ name ++ " value") 131 | arbitraryFieldNames = sublistOf ["foo", "bar", "baz", "qux", "foobar", "foobaz"] >>= shuffle 132 | 133 | it "sorts fields" $ do 134 | let fields = map field ["baz", "bar", "foo"] 135 | sortFieldsBy ["foo", "bar", "baz"] fields `shouldBe` map field ["foo", "bar", "baz"] 136 | 137 | it "keeps existing field order" $ do 138 | forAll (map field <$> arbitraryFieldNames) $ \fields -> do 139 | forAll arbitraryFieldNames $ \existingFieldOrder -> do 140 | let 141 | existingIndex :: Element -> Maybe Int 142 | existingIndex (Field name _) = name `elemIndex` existingFieldOrder 143 | existingIndex _ = Nothing 144 | 145 | indexes :: [Int] 146 | indexes = mapMaybe existingIndex (sortFieldsBy existingFieldOrder fields) 147 | 148 | sort indexes `shouldBe` indexes 149 | 150 | it "is stable" $ do 151 | forAll arbitraryFieldNames $ \fieldNames -> do 152 | forAll (elements $ subsequences fieldNames) $ \existingFieldOrder -> do 153 | let fields = map field fieldNames 154 | sortFieldsBy existingFieldOrder fields `shouldBe` fields 155 | 156 | describe "addSortKey" $ do 157 | it "adds sort key" $ do 158 | addSortKey [(Nothing, "foo"), (Just 3, "bar"), (Nothing, "baz")] `shouldBe` [((-1, 0), "foo"), ((3, 1), "bar"), ((3, 2), "baz" :: String)] 159 | -------------------------------------------------------------------------------- /test/Hpack/UtilSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE QuasiQuotes #-} 3 | module Hpack.UtilSpec (main, spec) where 4 | 5 | import Data.Aeson 6 | import Data.Aeson.QQ 7 | import Data.Aeson.Types 8 | import Helper 9 | import System.Directory 10 | 11 | import Hpack.Config 12 | import Hpack.Util 13 | 14 | main :: IO () 15 | main = hspec spec 16 | 17 | spec :: Spec 18 | spec = do 19 | describe "sort" $ do 20 | it "sorts lexicographically" $ do 21 | sort ["foo", "Foo"] `shouldBe` ["Foo", "foo" :: String] 22 | 23 | describe "parseMain" $ do 24 | it "accepts source file" $ do 25 | parseMain "Main.hs" `shouldBe` ("Main.hs", []) 26 | 27 | it "accepts literate source file" $ do 28 | parseMain "Main.lhs" `shouldBe` ("Main.lhs", []) 29 | 30 | it "accepts module" $ do 31 | parseMain "Foo" `shouldBe` ("Foo.hs", ["-main-is Foo"]) 32 | 33 | it "accepts hierarchical module" $ do 34 | parseMain "Foo.Bar.Baz" `shouldBe` ("Foo/Bar/Baz.hs", ["-main-is Foo.Bar.Baz"]) 35 | 36 | it "accepts qualified identifier" $ do 37 | parseMain "Foo.bar" `shouldBe` ("Foo.hs", ["-main-is Foo.bar"]) 38 | 39 | describe "toModule" $ do 40 | it "maps .hs paths to module names" $ do 41 | toModule ["Foo", "Bar", "Baz.hs"] `shouldBe` Just "Foo.Bar.Baz" 42 | 43 | it "maps .lhs paths to module names" $ do 44 | toModule ["Foo", "Bar", "Baz.lhs"] `shouldBe` Just "Foo.Bar.Baz" 45 | 46 | it "maps .hsc paths to module names" $ do 47 | toModule ["Foo", "Bar", "Baz.hsc"] `shouldBe` Just "Foo.Bar.Baz" 48 | 49 | it "rejects invalid module names" $ do 50 | toModule ["resources", "hello.hs"] `shouldBe` Nothing 51 | 52 | describe "getFilesRecursive" $ do 53 | it "gets all files from given directory and all its subdirectories" $ do 54 | inTempDirectoryNamed "test" $ do 55 | touch "foo/bar" 56 | touch "foo/baz" 57 | touch "foo/foobar/baz" 58 | actual <- getFilesRecursive "foo" 59 | actual `shouldMatchList` [ 60 | ["bar"] 61 | , ["baz"] 62 | , ["foobar", "baz"] 63 | ] 64 | 65 | describe "List" $ do 66 | let invalid = [aesonQQ|{ 67 | name: "hpack", 68 | gi: "sol/hpack", 69 | ref: "master" 70 | }|] 71 | parseError :: String -> Either String (List Dependency) 72 | parseError prefix = Left (prefix ++ ": neither key \"git\" nor key \"github\" present") 73 | context "when parsing single values" $ do 74 | it "returns the value in a singleton list" $ do 75 | fromJSON (toJSON $ Number 23) `shouldBe` Success (List [23 :: Int]) 76 | 77 | it "returns error messages from element parsing" $ do 78 | parseEither parseJSON invalid `shouldBe` parseError "Error in $" 79 | 80 | context "when parsing a list of values" $ do 81 | it "returns the list" $ do 82 | fromJSON (toJSON [Number 23, Number 42]) `shouldBe` Success (List [23, 42 :: Int]) 83 | 84 | it "propagates parse error messages of invalid elements" $ do 85 | parseEither parseJSON (toJSON [String "foo", invalid]) `shouldBe` parseError "Error in $[1]" 86 | 87 | describe "tryReadFile" $ do 88 | it "reads file" $ do 89 | inTempDirectory $ do 90 | writeFile "foo" "bar" 91 | tryReadFile "foo" `shouldReturn` Just "bar" 92 | 93 | it "returns Nothing if file does not exist" $ do 94 | inTempDirectory $ do 95 | tryReadFile "foo" `shouldReturn` Nothing 96 | 97 | describe "expandGlobs" $ around withTempDirectory $ do 98 | it "accepts simple files" $ \dir -> do 99 | touch (dir "foo.js") 100 | expandGlobs dir ["foo.js"] `shouldReturn` ([], ["foo.js"]) 101 | 102 | it "removes duplicates" $ \dir -> do 103 | touch (dir "foo.js") 104 | expandGlobs dir ["foo.js", "*.js"] `shouldReturn` ([], ["foo.js"]) 105 | 106 | it "rejects directories" $ \dir -> do 107 | touch (dir "foo") 108 | createDirectory (dir "bar") 109 | expandGlobs dir ["*"] `shouldReturn` ([], ["foo"]) 110 | 111 | it "rejects character ranges" $ \dir -> do 112 | touch (dir "foo1") 113 | touch (dir "foo2") 114 | touch (dir "foo[1,2]") 115 | expandGlobs dir ["foo[1,2]"] `shouldReturn` ([], ["foo[1,2]"]) 116 | 117 | context "when expanding *" $ do 118 | it "expands by extension" $ \dir -> do 119 | let files = [ 120 | "files/foo.js" 121 | , "files/bar.js" 122 | , "files/baz.js"] 123 | mapM_ (touch . (dir )) files 124 | touch (dir "files/foo.hs") 125 | expandGlobs dir ["files/*.js"] `shouldReturn` ([], sort files) 126 | 127 | it "rejects dot-files" $ \dir -> do 128 | touch (dir "foo/bar") 129 | touch (dir "foo/.baz") 130 | expandGlobs dir ["foo/*"] `shouldReturn` ([], ["foo/bar"]) 131 | 132 | it "accepts dot-files when explicitly asked to" $ \dir -> do 133 | touch (dir "foo/bar") 134 | touch (dir "foo/.baz") 135 | expandGlobs dir ["foo/.*"] `shouldReturn` ([], ["foo/.baz"]) 136 | 137 | it "matches at most one directory component" $ \dir -> do 138 | touch (dir "foo/bar/baz.js") 139 | touch (dir "foo/bar.js") 140 | expandGlobs dir ["*/*.js"] `shouldReturn` ([], ["foo/bar.js"]) 141 | 142 | context "when expanding **" $ do 143 | it "matches arbitrary many directory components" $ \dir -> do 144 | let file = "foo/bar/baz.js" 145 | touch (dir file) 146 | expandGlobs dir ["**/*.js"] `shouldReturn` ([], [file]) 147 | 148 | context "when a pattern does not match anything" $ do 149 | it "warns" $ \dir -> do 150 | expandGlobs dir ["foo"] `shouldReturn` 151 | (["Specified pattern \"foo\" for extra-source-files does not match any files"], []) 152 | 153 | context "when a pattern only matches a directory" $ do 154 | it "warns" $ \dir -> do 155 | createDirectory (dir "foo") 156 | expandGlobs dir ["foo"] `shouldReturn` 157 | (["Specified pattern \"foo\" for extra-source-files does not match any files"], []) 158 | -------------------------------------------------------------------------------- /src/Hpack/Run.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE QuasiQuotes #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | {-# LANGUAGE ViewPatterns #-} 5 | {-# LANGUAGE CPP #-} 6 | module Hpack.Run ( 7 | run 8 | , renderPackage 9 | , RenderSettings(..) 10 | , Alignment(..) 11 | , CommaStyle(..) 12 | , defaultRenderSettings 13 | #ifdef TEST 14 | , renderConditional 15 | , renderFlag 16 | , renderSourceRepository 17 | , formatDescription 18 | #endif 19 | ) where 20 | 21 | import Prelude () 22 | import Prelude.Compat 23 | 24 | import Control.Monad 25 | import Data.Char 26 | import Data.Maybe 27 | import Data.List.Compat 28 | import System.Exit.Compat 29 | import System.FilePath 30 | 31 | import Hpack.Util 32 | import Hpack.Config 33 | import Hpack.Render 34 | import Hpack.FormattingHints 35 | 36 | run :: FilePath -> IO ([String], FilePath, String) 37 | run dir = do 38 | mPackage <- readPackageConfig (dir packageConfig) 39 | case mPackage of 40 | Right (warnings, pkg) -> do 41 | let cabalFile = dir (packageName pkg ++ ".cabal") 42 | 43 | old <- tryReadFile cabalFile 44 | 45 | let 46 | FormattingHints{..} = sniffFormattingHints (fromMaybe "" old) 47 | alignment = fromMaybe 16 formattingHintsAlignment 48 | settings = formattingHintsRenderSettings 49 | 50 | output = renderPackage settings alignment formattingHintsFieldOrder formattingHintsSectionsFieldOrder pkg 51 | 52 | return (warnings, cabalFile, output) 53 | Left err -> die err 54 | 55 | renderPackage :: RenderSettings -> Alignment -> [String] -> [(String, [String])] -> Package -> String 56 | renderPackage settings alignment existingFieldOrder sectionsFieldOrder Package{..} = intercalate "\n" (unlines header : chunks) 57 | where 58 | chunks :: [String] 59 | chunks = map unlines . filter (not . null) . map (render settings 0) $ sortSectionFields sectionsFieldOrder stanzas 60 | 61 | header :: [String] 62 | header = concatMap (render settings {renderSettingsFieldAlignment = alignment} 0) fields 63 | 64 | extraSourceFiles :: Element 65 | extraSourceFiles = Field "extra-source-files" (LineSeparatedList packageExtraSourceFiles) 66 | 67 | dataFiles :: Element 68 | dataFiles = Field "data-files" (LineSeparatedList packageDataFiles) 69 | 70 | sourceRepository = maybe [] (return . renderSourceRepository) packageSourceRepository 71 | 72 | library = maybe [] (return . renderLibrary) packageLibrary 73 | 74 | stanzas :: [Element] 75 | stanzas = 76 | extraSourceFiles 77 | : dataFiles 78 | : sourceRepository 79 | ++ concat [ 80 | map renderFlag packageFlags 81 | , library 82 | , renderExecutables packageExecutables 83 | , renderTests packageTests 84 | , renderBenchmarks packageBenchmarks 85 | ] 86 | 87 | fields :: [Element] 88 | fields = sortFieldsBy existingFieldOrder . mapMaybe (\(name, value) -> Field name . Literal <$> value) $ [ 89 | ("name", Just packageName) 90 | , ("version", Just packageVersion) 91 | , ("synopsis", packageSynopsis) 92 | , ("description", (formatDescription alignment <$> packageDescription)) 93 | , ("category", packageCategory) 94 | , ("stability", packageStability) 95 | , ("homepage", packageHomepage) 96 | , ("bug-reports", packageBugReports) 97 | , ("author", formatList packageAuthor) 98 | , ("maintainer", formatList packageMaintainer) 99 | , ("copyright", formatList packageCopyright) 100 | , ("license", packageLicense) 101 | , ("license-file", packageLicenseFile) 102 | , ("tested-with", packageTestedWith) 103 | , ("build-type", Just "Simple") 104 | , ("cabal-version", cabalVersion) 105 | ] 106 | 107 | formatList :: [String] -> Maybe String 108 | formatList xs = guard (not $ null xs) >> (Just $ intercalate separator xs) 109 | where 110 | separator = let Alignment n = alignment in ",\n" ++ replicate n ' ' 111 | 112 | cabalVersion :: Maybe String 113 | cabalVersion = maximum [ 114 | Just ">= 1.10" 115 | , packageLibrary >>= libCabalVersion 116 | ] 117 | where 118 | libCabalVersion :: Section Library -> Maybe String 119 | libCabalVersion sect = ">= 1.21" <$ guard (hasReexportedModules sect) 120 | 121 | hasReexportedModules :: Section Library -> Bool 122 | hasReexportedModules = not . null . libraryReexportedModules . sectionData 123 | 124 | sortSectionFields :: [(String, [String])] -> [Element] -> [Element] 125 | sortSectionFields sectionsFieldOrder = go 126 | where 127 | go sections = case sections of 128 | [] -> [] 129 | Stanza name fields : xs | Just fieldOrder <- lookup name sectionsFieldOrder -> Stanza name (sortFieldsBy fieldOrder fields) : go xs 130 | x : xs -> x : go xs 131 | 132 | formatDescription :: Alignment -> String -> String 133 | formatDescription (Alignment alignment) description = case map emptyLineToDot $ lines description of 134 | x : xs -> intercalate "\n" (x : map (indentation ++) xs) 135 | [] -> "" 136 | where 137 | n = max alignment (length ("description: " :: String)) 138 | indentation = replicate n ' ' 139 | 140 | emptyLineToDot xs 141 | | isEmptyLine xs = "." 142 | | otherwise = xs 143 | 144 | isEmptyLine = all isSpace 145 | 146 | renderSourceRepository :: SourceRepository -> Element 147 | renderSourceRepository SourceRepository{..} = Stanza "source-repository head" [ 148 | Field "type" "git" 149 | , Field "location" (Literal sourceRepositoryUrl) 150 | , Field "subdir" (maybe "" Literal sourceRepositorySubdir) 151 | ] 152 | 153 | renderFlag :: Flag -> Element 154 | renderFlag Flag {..} = Stanza ("flag " ++ flagName) $ description ++ [ 155 | Field "manual" (Literal $ show flagManual) 156 | , Field "default" (Literal $ show flagDefault) 157 | ] 158 | where 159 | description = maybe [] (return . Field "description" . Literal) flagDescription 160 | 161 | renderExecutables :: [Section Executable] -> [Element] 162 | renderExecutables = map renderExecutable 163 | 164 | renderExecutable :: Section Executable -> Element 165 | renderExecutable sect@(sectionData -> Executable{..}) = 166 | Stanza ("executable " ++ executableName) (renderExecutableSection sect) 167 | 168 | renderTests :: [Section Executable] -> [Element] 169 | renderTests = map renderTest 170 | 171 | renderTest :: Section Executable -> Element 172 | renderTest sect@(sectionData -> Executable{..}) = 173 | Stanza ("test-suite " ++ executableName) 174 | (Field "type" "exitcode-stdio-1.0" : renderExecutableSection sect) 175 | 176 | renderBenchmarks :: [Section Executable] -> [Element] 177 | renderBenchmarks = map renderBenchmark 178 | 179 | renderBenchmark :: Section Executable -> Element 180 | renderBenchmark sect@(sectionData -> Executable{..}) = 181 | Stanza ("benchmark " ++ executableName) 182 | (Field "type" "exitcode-stdio-1.0" : renderExecutableSection sect) 183 | 184 | renderExecutableSection :: Section Executable -> [Element] 185 | renderExecutableSection sect@(sectionData -> Executable{..}) = 186 | mainIs : renderSection sect ++ [otherModules, defaultLanguage] 187 | where 188 | mainIs = Field "main-is" (Literal executableMain) 189 | otherModules = renderOtherModules executableOtherModules 190 | 191 | renderLibrary :: Section Library -> Element 192 | renderLibrary sect@(sectionData -> Library{..}) = Stanza "library" $ 193 | renderSection sect ++ 194 | maybe [] (return . renderExposed) libraryExposed ++ [ 195 | renderExposedModules libraryExposedModules 196 | , renderOtherModules libraryOtherModules 197 | , renderReexportedModules libraryReexportedModules 198 | , defaultLanguage 199 | ] 200 | 201 | renderExposed :: Bool -> Element 202 | renderExposed = Field "exposed" . Literal . show 203 | 204 | renderSection :: Section a -> [Element] 205 | renderSection Section{..} = [ 206 | renderSourceDirs sectionSourceDirs 207 | , renderDefaultExtensions sectionDefaultExtensions 208 | , renderOtherExtensions sectionOtherExtensions 209 | , renderGhcOptions sectionGhcOptions 210 | , renderGhcProfOptions sectionGhcProfOptions 211 | , renderCppOptions sectionCppOptions 212 | , renderCCOptions sectionCCOptions 213 | , Field "include-dirs" (LineSeparatedList sectionIncludeDirs) 214 | , Field "install-includes" (LineSeparatedList sectionInstallIncludes) 215 | , Field "c-sources" (LineSeparatedList sectionCSources) 216 | , Field "extra-lib-dirs" (LineSeparatedList sectionExtraLibDirs) 217 | , Field "extra-libraries" (LineSeparatedList sectionExtraLibraries) 218 | , renderLdOptions sectionLdOptions 219 | , renderDependencies sectionDependencies 220 | , renderBuildTools sectionBuildTools 221 | ] 222 | ++ maybe [] (return . renderBuildable) sectionBuildable 223 | ++ map renderConditional sectionConditionals 224 | 225 | renderConditional :: Conditional -> Element 226 | renderConditional (Conditional condition sect mElse) = case mElse of 227 | Nothing -> if_ 228 | Just else_ -> Group if_ (Stanza "else" $ renderSection else_) 229 | where 230 | if_ = Stanza ("if " ++ condition) (renderSection sect) 231 | 232 | defaultLanguage :: Element 233 | defaultLanguage = Field "default-language" "Haskell2010" 234 | 235 | renderSourceDirs :: [String] -> Element 236 | renderSourceDirs = Field "hs-source-dirs" . CommaSeparatedList 237 | 238 | renderExposedModules :: [String] -> Element 239 | renderExposedModules = Field "exposed-modules" . LineSeparatedList 240 | 241 | renderOtherModules :: [String] -> Element 242 | renderOtherModules = Field "other-modules" . LineSeparatedList 243 | 244 | renderReexportedModules :: [String] -> Element 245 | renderReexportedModules = Field "reexported-modules" . LineSeparatedList 246 | 247 | renderDependencies :: [Dependency] -> Element 248 | renderDependencies = Field "build-depends" . CommaSeparatedList . map dependencyName 249 | 250 | renderGhcOptions :: [GhcOption] -> Element 251 | renderGhcOptions = Field "ghc-options" . WordList 252 | 253 | renderGhcProfOptions :: [GhcProfOption] -> Element 254 | renderGhcProfOptions = Field "ghc-prof-options" . WordList 255 | 256 | renderCppOptions :: [CppOption] -> Element 257 | renderCppOptions = Field "cpp-options" . WordList 258 | 259 | renderCCOptions :: [CCOption] -> Element 260 | renderCCOptions = Field "cc-options" . WordList 261 | 262 | renderLdOptions :: [LdOption] -> Element 263 | renderLdOptions = Field "ld-options" . WordList 264 | 265 | renderBuildable :: Bool -> Element 266 | renderBuildable = Field "buildable" . Literal . show 267 | 268 | renderDefaultExtensions :: [String] -> Element 269 | renderDefaultExtensions = Field "default-extensions" . WordList 270 | 271 | renderOtherExtensions :: [String] -> Element 272 | renderOtherExtensions = Field "other-extensions" . WordList 273 | 274 | renderBuildTools :: [Dependency] -> Element 275 | renderBuildTools = Field "build-tools" . CommaSeparatedList . map dependencyName 276 | -------------------------------------------------------------------------------- /test/Hpack/RunSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Hpack.RunSpec (spec) where 3 | 4 | import Test.Hspec 5 | import Data.List.Compat 6 | 7 | import Hpack.ConfigSpec hiding (spec) 8 | import Hpack.Config hiding (package) 9 | import Hpack.Render 10 | import Hpack.Run 11 | 12 | spec :: Spec 13 | spec = do 14 | describe "renderPackage" $ do 15 | let renderPackage_ = renderPackage defaultRenderSettings 0 [] [] 16 | it "renders a package" $ do 17 | renderPackage_ package `shouldBe` unlines [ 18 | "name: foo" 19 | , "version: 0.0.0" 20 | , "build-type: Simple" 21 | , "cabal-version: >= 1.10" 22 | ] 23 | 24 | it "aligns fields" $ do 25 | renderPackage defaultRenderSettings 16 [] [] package `shouldBe` unlines [ 26 | "name: foo" 27 | , "version: 0.0.0" 28 | , "build-type: Simple" 29 | , "cabal-version: >= 1.10" 30 | ] 31 | 32 | it "includes description" $ do 33 | renderPackage_ package {packageDescription = Just "foo\n\nbar\n"} `shouldBe` unlines [ 34 | "name: foo" 35 | , "version: 0.0.0" 36 | , "description: foo" 37 | , " ." 38 | , " bar" 39 | , "build-type: Simple" 40 | , "cabal-version: >= 1.10" 41 | ] 42 | 43 | it "aligns description" $ do 44 | renderPackage defaultRenderSettings 16 [] [] package {packageDescription = Just "foo\n\nbar\n"} `shouldBe` unlines [ 45 | "name: foo" 46 | , "version: 0.0.0" 47 | , "description: foo" 48 | , " ." 49 | , " bar" 50 | , "build-type: Simple" 51 | , "cabal-version: >= 1.10" 52 | ] 53 | 54 | it "includes stability" $ do 55 | renderPackage_ package {packageStability = Just "experimental"} `shouldBe` unlines [ 56 | "name: foo" 57 | , "version: 0.0.0" 58 | , "stability: experimental" 59 | , "build-type: Simple" 60 | , "cabal-version: >= 1.10" 61 | ] 62 | 63 | it "includes copyright holder" $ do 64 | renderPackage_ package {packageCopyright = ["(c) 2015 Simon Hengel"]} `shouldBe` unlines [ 65 | "name: foo" 66 | , "version: 0.0.0" 67 | , "copyright: (c) 2015 Simon Hengel" 68 | , "build-type: Simple" 69 | , "cabal-version: >= 1.10" 70 | ] 71 | 72 | it "aligns copyright holders" $ do 73 | renderPackage defaultRenderSettings 16 [] [] package {packageCopyright = ["(c) 2015 Foo", "(c) 2015 Bar"]} `shouldBe` unlines [ 74 | "name: foo" 75 | , "version: 0.0.0" 76 | , "copyright: (c) 2015 Foo," 77 | , " (c) 2015 Bar" 78 | , "build-type: Simple" 79 | , "cabal-version: >= 1.10" 80 | ] 81 | 82 | it "includes extra-source-files" $ do 83 | renderPackage_ package {packageExtraSourceFiles = ["foo", "bar"]} `shouldBe` unlines [ 84 | "name: foo" 85 | , "version: 0.0.0" 86 | , "build-type: Simple" 87 | , "cabal-version: >= 1.10" 88 | , "" 89 | , "extra-source-files:" 90 | , " foo" 91 | , " bar" 92 | ] 93 | 94 | it "includes buildable" $ do 95 | renderPackage_ package {packageLibrary = Just (section library){sectionBuildable = Just False}} `shouldBe` unlines [ 96 | "name: foo" 97 | , "version: 0.0.0" 98 | , "build-type: Simple" 99 | , "cabal-version: >= 1.10" 100 | , "" 101 | , "library" 102 | , " buildable: False" 103 | , " default-language: Haskell2010" 104 | ] 105 | 106 | context "when rendering library section" $ do 107 | it "renders library section" $ do 108 | renderPackage_ package {packageLibrary = Just $ section library} `shouldBe` unlines [ 109 | "name: foo" 110 | , "version: 0.0.0" 111 | , "build-type: Simple" 112 | , "cabal-version: >= 1.10" 113 | , "" 114 | , "library" 115 | , " default-language: Haskell2010" 116 | ] 117 | 118 | it "includes exposed-modules" $ do 119 | renderPackage_ package {packageLibrary = Just (section library{libraryExposedModules = ["Foo"]})} `shouldBe` unlines [ 120 | "name: foo" 121 | , "version: 0.0.0" 122 | , "build-type: Simple" 123 | , "cabal-version: >= 1.10" 124 | , "" 125 | , "library" 126 | , " exposed-modules:" 127 | , " Foo" 128 | , " default-language: Haskell2010" 129 | ] 130 | 131 | it "includes other-modules" $ do 132 | renderPackage_ package {packageLibrary = Just (section library{libraryOtherModules = ["Bar"]})} `shouldBe` unlines [ 133 | "name: foo" 134 | , "version: 0.0.0" 135 | , "build-type: Simple" 136 | , "cabal-version: >= 1.10" 137 | , "" 138 | , "library" 139 | , " other-modules:" 140 | , " Bar" 141 | , " default-language: Haskell2010" 142 | ] 143 | 144 | it "includes reexported-modules and bumps cabal version" $ do 145 | renderPackage_ package {packageLibrary = Just (section library{libraryReexportedModules = ["Baz"]})} `shouldBe` unlines [ 146 | "name: foo" 147 | , "version: 0.0.0" 148 | , "build-type: Simple" 149 | , "cabal-version: >= 1.21" 150 | , "" 151 | , "library" 152 | , " reexported-modules:" 153 | , " Baz" 154 | , " default-language: Haskell2010" 155 | ] 156 | 157 | context "when given list of existing fields" $ do 158 | it "retains field order" $ do 159 | renderPackage defaultRenderSettings 16 ["cabal-version", "version", "name", "build-type"] [] package `shouldBe` unlines [ 160 | "cabal-version: >= 1.10" 161 | , "version: 0.0.0" 162 | , "name: foo" 163 | , "build-type: Simple" 164 | ] 165 | 166 | it "uses default field order for new fields" $ do 167 | renderPackage defaultRenderSettings 16 ["name", "version", "cabal-version"] [] package `shouldBe` unlines [ 168 | "name: foo" 169 | , "version: 0.0.0" 170 | , "build-type: Simple" 171 | , "cabal-version: >= 1.10" 172 | ] 173 | 174 | it "retains section field order" $ do 175 | renderPackage defaultRenderSettings 0 [] [("executable foo", ["default-language", "main-is", "ghc-options"])] package {packageExecutables = [(section $ executable "foo" "Main.hs") {sectionGhcOptions = ["-Wall"]}]} `shouldBe` unlines [ 176 | "name: foo" 177 | , "version: 0.0.0" 178 | , "build-type: Simple" 179 | , "cabal-version: >= 1.10" 180 | , "" 181 | , "executable foo" 182 | , " default-language: Haskell2010" 183 | , " main-is: Main.hs" 184 | , " ghc-options: -Wall" 185 | ] 186 | 187 | 188 | context "when rendering executable section" $ do 189 | it "includes dependencies" $ do 190 | renderPackage_ package {packageExecutables = [(section $ executable "foo" "Main.hs") {sectionDependencies = ["foo", "bar", "foo", "baz"]}]} `shouldBe` unlines [ 191 | "name: foo" 192 | , "version: 0.0.0" 193 | , "build-type: Simple" 194 | , "cabal-version: >= 1.10" 195 | , "" 196 | , "executable foo" 197 | , " main-is: Main.hs" 198 | , " build-depends:" 199 | , " foo" 200 | , " , bar" 201 | , " , foo" 202 | , " , baz" 203 | , " default-language: Haskell2010" 204 | ] 205 | 206 | it "includes GHC options" $ do 207 | renderPackage_ package {packageExecutables = [(section $ executable "foo" "Main.hs") {sectionGhcOptions = ["-Wall"]}]} `shouldBe` unlines [ 208 | "name: foo" 209 | , "version: 0.0.0" 210 | , "build-type: Simple" 211 | , "cabal-version: >= 1.10" 212 | , "" 213 | , "executable foo" 214 | , " main-is: Main.hs" 215 | , " ghc-options: -Wall" 216 | , " default-language: Haskell2010" 217 | ] 218 | 219 | it "includes GHC profiling options" $ do 220 | renderPackage_ package {packageExecutables = [(section $ executable "foo" "Main.hs") {sectionGhcProfOptions = ["-fprof-auto", "-rtsopts"]}]} `shouldBe` unlines [ 221 | "name: foo" 222 | , "version: 0.0.0" 223 | , "build-type: Simple" 224 | , "cabal-version: >= 1.10" 225 | , "" 226 | , "executable foo" 227 | , " main-is: Main.hs" 228 | , " ghc-prof-options: -fprof-auto -rtsopts" 229 | , " default-language: Haskell2010" 230 | ] 231 | describe "renderConditional" $ do 232 | it "renders conditionals" $ do 233 | let conditional = Conditional "os(windows)" (section ()) {sectionDependencies = ["Win32"]} Nothing 234 | render defaultRenderSettings 0 (renderConditional conditional) `shouldBe` [ 235 | "if os(windows)" 236 | , " build-depends:" 237 | , " Win32" 238 | ] 239 | 240 | it "renders conditionals with else-branch" $ do 241 | let conditional = Conditional "os(windows)" (section ()) {sectionDependencies = ["Win32"]} (Just $ (section ()) {sectionDependencies = ["unix"]}) 242 | render defaultRenderSettings 0 (renderConditional conditional) `shouldBe` [ 243 | "if os(windows)" 244 | , " build-depends:" 245 | , " Win32" 246 | , "else" 247 | , " build-depends:" 248 | , " unix" 249 | ] 250 | 251 | it "renders nested conditionals" $ do 252 | let conditional = Conditional "arch(i386)" (section ()) {sectionGhcOptions = ["-threaded"], sectionConditionals = [innerConditional]} Nothing 253 | innerConditional = Conditional "os(windows)" (section ()) {sectionDependencies = ["Win32"]} Nothing 254 | render defaultRenderSettings 0 (renderConditional conditional) `shouldBe` [ 255 | "if arch(i386)" 256 | , " ghc-options: -threaded" 257 | , " if os(windows)" 258 | , " build-depends:" 259 | , " Win32" 260 | ] 261 | 262 | describe "renderFlag" $ do 263 | it "renders flags" $ do 264 | let flag = (Flag "foo" (Just "some flag") True False) 265 | render defaultRenderSettings 0 (renderFlag flag) `shouldBe` [ 266 | "flag foo" 267 | , " description: some flag" 268 | , " manual: True" 269 | , " default: False" 270 | ] 271 | 272 | describe "formatDescription" $ do 273 | it "formats description" $ do 274 | let description = unlines [ 275 | "foo" 276 | , "bar" 277 | ] 278 | "description: " ++ formatDescription 0 description `shouldBe` intercalate "\n" [ 279 | "description: foo" 280 | , " bar" 281 | ] 282 | 283 | it "takes specified alignment into account" $ do 284 | let description = unlines [ 285 | "foo" 286 | , "bar" 287 | , "baz" 288 | ] 289 | "description: " ++ formatDescription 15 description `shouldBe` intercalate "\n" [ 290 | "description: foo" 291 | , " bar" 292 | , " baz" 293 | ] 294 | 295 | it "formats empty lines" $ do 296 | let description = unlines [ 297 | "foo" 298 | , " " 299 | , "bar" 300 | ] 301 | "description: " ++ formatDescription 0 description `shouldBe` intercalate "\n" [ 302 | "description: foo" 303 | , " ." 304 | , " bar" 305 | ] 306 | 307 | describe "renderSourceRepository" $ do 308 | it "renders source-repository without subdir correctly" $ do 309 | let repository = SourceRepository "https://github.com/hspec/hspec" Nothing 310 | (render defaultRenderSettings 0 $ renderSourceRepository repository) 311 | `shouldBe` [ 312 | "source-repository head" 313 | , " type: git" 314 | , " location: https://github.com/hspec/hspec" 315 | ] 316 | 317 | it "renders source-repository with subdir" $ do 318 | let repository = SourceRepository "https://github.com/hspec/hspec" (Just "hspec-core") 319 | (render defaultRenderSettings 0 $ renderSourceRepository repository) 320 | `shouldBe` [ 321 | "source-repository head" 322 | , " type: git" 323 | , " location: https://github.com/hspec/hspec" 324 | , " subdir: hspec-core" 325 | ] 326 | -------------------------------------------------------------------------------- /src/Hpack/Convert.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NamedFieldPuns #-} 2 | {-# LANGUAGE RankNTypes #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | module Hpack.Convert where 5 | 6 | import Data.Char 7 | import Data.Monoid 8 | import Prelude () 9 | import Prelude.Compat 10 | 11 | import Data.List.Split (splitOn) 12 | import Data.Maybe 13 | import qualified Data.Version as Version 14 | import qualified Distribution.Compiler as Compiler 15 | import qualified Distribution.InstalledPackageInfo as Cabal 16 | import qualified Distribution.Package as Cabal 17 | import qualified Distribution.PackageDescription as Cabal 18 | import qualified Distribution.PackageDescription.Parse as Cabal 19 | import qualified Distribution.Text as Cabal 20 | import qualified Distribution.Version as Cabal 21 | import Hpack.Config hiding (package) 22 | import Text.PrettyPrint (fsep, (<+>)) 23 | 24 | -- * Public API 25 | 26 | -- | Reads a 'Package' from cabal's 'GenericPackageDescription' representation 27 | -- of a @.cabal@ file 28 | fromPackageDescription :: Cabal.GenericPackageDescription -> Package 29 | fromPackageDescription Cabal.GenericPackageDescription{..} = 30 | let Cabal.PackageDescription{..} = packageDescription 31 | in 32 | Package { packageName = Cabal.unPackageName (Cabal.pkgName package) 33 | , packageVersion = Version.showVersion (Cabal.pkgVersion package) 34 | , packageSynopsis = nullNothing synopsis 35 | , packageDescription = nullNothing description 36 | , packageHomepage = nullNothing homepage 37 | , packageBugReports = nullNothing bugReports 38 | , packageCategory = nullNothing category 39 | , packageStability = nullNothing stability 40 | , packageAuthor = maybe [] parseCommaSep (nullNothing author) 41 | , packageMaintainer = maybe [] parseCommaSep (nullNothing maintainer) 42 | , packageCopyright = maybe [] parseCommaSep (nullNothing copyright) 43 | , packageLicense = Just (show (Cabal.disp license)) 44 | , packageLicenseFile = listToMaybe licenseFiles 45 | , packageTestedWith = 46 | map toUpper . 47 | show . 48 | fsep . map (\(f, vr) -> Cabal.disp f <> Cabal.disp vr ) <$> 49 | nullNothing testedWith 50 | , packageFlags = 51 | map (\Cabal.MkFlag{..} -> 52 | let Cabal.FlagName fn = flagName 53 | in Flag { flagName = fn 54 | , flagDescription = 55 | nullNothing flagDescription 56 | , flagManual = flagManual 57 | , flagDefault = flagDefault 58 | }) 59 | genPackageFlags 60 | , packageExtraSourceFiles = extraSrcFiles 61 | , packageDataFiles = dataFiles 62 | , packageSourceRepository = fromSourceRepos sourceRepos 63 | , packageLibrary = fromCondLibrary condLibrary 64 | , packageExecutables = fromCondExecutables condExecutables 65 | , packageTests = fromCondTestSuites condTestSuites 66 | , packageBenchmarks = fromCondBenchmarks condBenchmarks 67 | } 68 | 69 | -- | Reads a 'Package' from a @.cabal@ manifest string 70 | fromPackageDescriptionString :: String -> Either ConvertError Package 71 | fromPackageDescriptionString pkgStr = 72 | case Cabal.parsePackageDescription pkgStr of 73 | Cabal.ParseFailed e -> Left (ConvertCabalParseError e) 74 | Cabal.ParseOk _ gpkg -> Right (fromPackageDescription gpkg) 75 | 76 | data ConvertError = ConvertCabalParseError Cabal.PError 77 | deriving(Show, Eq) 78 | 79 | -- data ConvertWarning = CWIgnoreSection String 80 | -- | CWIgnoreCondition String 81 | -- | CWIgnoreSourceRepo Cabal.SourceRepo 82 | -- | CWSourceRepoWithoutUrl Cabal.SourceRepo 83 | 84 | -- * Private functions for converting each section 85 | 86 | fromSourceRepos :: [Cabal.SourceRepo] -> Maybe SourceRepository 87 | fromSourceRepos [] = Nothing 88 | fromSourceRepos (_repo@Cabal.SourceRepo{..}:_more) = 89 | -- ( 90 | Just SourceRepository { sourceRepositoryUrl = fromMaybe "" repoLocation 91 | -- TODO - this is broken (?) 92 | , sourceRepositorySubdir = fmap processDir repoSubdir 93 | } 94 | -- TODO - Warnings 95 | -- , case repoLocation of 96 | -- Nothing -> [CWSourceRepoWithoutUrl repo] 97 | -- _ -> [] 98 | -- ++ map CWIgnoreSourceRepo more 99 | -- ) 100 | 101 | fromDependency :: Cabal.Dependency -> Dependency 102 | fromDependency (Cabal.Dependency pn vr) | vr == Cabal.anyVersion = 103 | Dependency (show (Cabal.disp pn)) Nothing 104 | fromDependency (Cabal.Dependency pn vr) = 105 | Dependency (show (Cabal.disp pn <+> Cabal.disp vr)) Nothing 106 | 107 | fromCondLibrary :: Maybe (Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.Library) -> Maybe (Section Library) 108 | fromCondLibrary mcondLibrary = do 109 | condLibrary@(Cabal.CondNode Cabal.Library{libBuildInfo} _ components) <- mcondLibrary 110 | l <- libFromCondLibrary condLibrary 111 | return (sectionWithBuildInfo l libBuildInfo) 112 | { sectionConditionals = map fromCondComponentHasBuildInfo components 113 | } 114 | 115 | fromCondExecutables :: [(String, Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.Executable)] -> [Section Executable] 116 | fromCondExecutables = map fromCondExecutableTup 117 | 118 | fromCondTestSuites :: [(String, Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.TestSuite)] -> [Section Executable] 119 | fromCondTestSuites = mapMaybe fromCondTestSuiteTup 120 | 121 | fromCondBenchmarks :: [(String, Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.Benchmark)] -> [Section Executable] 122 | fromCondBenchmarks = mapMaybe fromCondBenchmarkTup 123 | 124 | fromCondExecutableTup :: (String, Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.Executable) -> Section Executable 125 | fromCondExecutableTup etup@(_, Cabal.CondNode Cabal.Executable{buildInfo} _ components) = 126 | let e = exeFromCondExecutableTup etup 127 | in (sectionWithBuildInfo e buildInfo) 128 | { sectionConditionals = map fromCondComponentHasBuildInfo components 129 | } 130 | 131 | fromCondTestSuiteTup :: (String, Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.TestSuite) -> Maybe (Section Executable) 132 | fromCondTestSuiteTup ttup@(_, Cabal.CondNode Cabal.TestSuite{testBuildInfo} _ components) = do 133 | te <- testExeFromCondExecutableTup ttup 134 | return (sectionWithBuildInfo te testBuildInfo) 135 | { sectionConditionals = map fromCondComponentHasBuildInfo components 136 | } 137 | 138 | fromCondBenchmarkTup :: (String, Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.Benchmark) -> Maybe (Section Executable) 139 | fromCondBenchmarkTup btup@(_, Cabal.CondNode Cabal.Benchmark{benchmarkBuildInfo} _ components) = do 140 | be <- benchExeFromCondExecutableTup btup 141 | return (sectionWithBuildInfo be benchmarkBuildInfo) 142 | { sectionConditionals = map fromCondComponentHasBuildInfo components 143 | } 144 | 145 | -- * Conditional Mapping 146 | class HasBuildInfo a where 147 | getBuildInfo :: a -> Cabal.BuildInfo 148 | 149 | instance HasBuildInfo Cabal.Library where 150 | getBuildInfo Cabal.Library{libBuildInfo} = libBuildInfo 151 | 152 | instance HasBuildInfo Cabal.Executable where 153 | getBuildInfo Cabal.Executable{buildInfo} = buildInfo 154 | 155 | instance HasBuildInfo Cabal.TestSuite where 156 | getBuildInfo Cabal.TestSuite{testBuildInfo} = testBuildInfo 157 | 158 | instance HasBuildInfo Cabal.Benchmark where 159 | getBuildInfo Cabal.Benchmark{benchmarkBuildInfo} = benchmarkBuildInfo 160 | 161 | fromCondHasBuildInfo :: HasBuildInfo a => Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] a -> Section () 162 | fromCondHasBuildInfo (Cabal.CondNode hbi _ components) = 163 | let bi = getBuildInfo hbi 164 | in (sectionWithBuildInfo () bi) 165 | { sectionConditionals = map fromCondComponentHasBuildInfo components 166 | } 167 | 168 | fromCondComponentHasBuildInfo :: (HasBuildInfo a) 169 | => ( Cabal.Condition Cabal.ConfVar 170 | , Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] a 171 | , Maybe (Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] a) 172 | ) 173 | -> Conditional 174 | fromCondComponentHasBuildInfo (cond, ifTree, elseTree) = 175 | Conditional { conditionalCondition = fromCondition cond 176 | , conditionalThen = fromCondHasBuildInfo ifTree 177 | , conditionalElse = fromCondHasBuildInfo <$> elseTree 178 | } 179 | 180 | fromCondition :: Cabal.Condition Cabal.ConfVar -> String 181 | fromCondition (Cabal.Var c) = case c of 182 | Cabal.OS os -> "os(" ++ show (Cabal.disp os) ++ ")" 183 | Cabal.Flag (Cabal.FlagName fl) -> "flag(" ++ fl ++ ")" 184 | Cabal.Arch ar -> "arch(" ++ show (Cabal.disp ar) ++ ")" 185 | Cabal.Impl cc vr -> "impl(" ++ show (Cabal.disp cc <+> Cabal.disp vr) ++ ")" 186 | fromCondition (Cabal.CNot c) = "!(" ++ fromCondition c ++ ")" 187 | fromCondition (Cabal.COr c1 c2) = "(" ++ fromCondition c1 ++ ") || (" ++ fromCondition c2 ++ ")" 188 | fromCondition (Cabal.CAnd c1 c2) = "(" ++ fromCondition c1 ++ ") && (" ++ fromCondition c2 ++ ")" 189 | fromCondition (Cabal.Lit b) = show b 190 | 191 | 192 | -- * Private helpers 193 | 194 | -- | Builds a 'Package' 'Section' from a data entity and a 'BuildInfo' entity 195 | sectionWithBuildInfo :: a -> Cabal.BuildInfo -> Section a 196 | sectionWithBuildInfo d Cabal.BuildInfo{..} = 197 | Section { sectionData = d 198 | , sectionSourceDirs = processDirs hsSourceDirs 199 | , sectionDependencies = map fromDependency targetBuildDepends 200 | , sectionDefaultExtensions = map (show . Cabal.disp) 201 | defaultExtensions 202 | , sectionOtherExtensions = map (show . Cabal.disp) otherExtensions 203 | , sectionGhcOptions = map (\l -> case words l of 204 | [] -> "" 205 | [x] -> x 206 | _ -> ensureQuoted l) $ 207 | fromMaybe [] $ 208 | lookup Compiler.GHC options 209 | , sectionGhcProfOptions = fromMaybe [] $ 210 | lookup Compiler.GHC profOptions 211 | , sectionCppOptions = cppOptions 212 | , sectionCCOptions = ccOptions 213 | , sectionCSources = cSources 214 | , sectionExtraLibDirs = processDirs extraLibDirs 215 | , sectionExtraLibraries = extraLibs 216 | , sectionIncludeDirs = processDirs includeDirs 217 | , sectionInstallIncludes = installIncludes 218 | , sectionLdOptions = ldOptions 219 | , sectionBuildable = Just buildable 220 | -- TODO ^^ ???? 221 | , sectionConditionals = [] 222 | -- TODO ^^ ???? 223 | , sectionBuildTools = map fromDependency buildTools 224 | } 225 | 226 | libFromCondLibrary :: Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.Library -> Maybe Library 227 | libFromCondLibrary (Cabal.CondNode (Cabal.Library{..}) _ _) = do 228 | let Cabal.BuildInfo{..} = libBuildInfo 229 | return Library { libraryExposed = Just libExposed 230 | , libraryExposedModules = map (show . Cabal.disp) 231 | exposedModules 232 | , libraryOtherModules = map (show . Cabal.disp) otherModules 233 | , libraryReexportedModules = map (show . Cabal.disp) 234 | reexportedModules 235 | } 236 | 237 | exeFromCondExecutableTup :: (String, Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.Executable) -> Executable 238 | exeFromCondExecutableTup (name, Cabal.CondNode Cabal.Executable{..} _ _) = 239 | Executable { executableName = name 240 | , executableMain = modulePath 241 | , executableOtherModules = map (show . Cabal.disp) 242 | (Cabal.otherModules buildInfo) 243 | } 244 | 245 | testExeFromCondExecutableTup :: (String, Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.TestSuite) -> Maybe Executable 246 | testExeFromCondExecutableTup (name, Cabal.CondNode Cabal.TestSuite{..} _ _) = 247 | case testInterface of 248 | Cabal.TestSuiteExeV10 _ mainIs -> Just 249 | Executable { executableName = name 250 | , executableMain = mainIs 251 | , executableOtherModules = map (show . Cabal.disp) 252 | (Cabal.otherModules testBuildInfo) 253 | } 254 | _ -> Nothing 255 | 256 | benchExeFromCondExecutableTup :: (String, Cabal.CondTree Cabal.ConfVar [Cabal.Dependency] Cabal.Benchmark) -> Maybe Executable 257 | benchExeFromCondExecutableTup (name, Cabal.CondNode Cabal.Benchmark{..} _ _) = 258 | case benchmarkInterface of 259 | Cabal.BenchmarkExeV10 _ mainIs -> Just 260 | Executable { executableName = name 261 | , executableMain = mainIs 262 | , executableOtherModules = map (show . Cabal.disp) 263 | (Cabal.otherModules benchmarkBuildInfo) 264 | } 265 | _ -> Nothing 266 | 267 | -- | Returns Nothing if a list is empty and Just the list otherwise 268 | -- 269 | -- >>> nullNothing [] 270 | -- Nothing 271 | -- >>> nullNothing [1, 2, 3] 272 | -- Just [1, 2, 3] 273 | nullNothing :: [a] -> Maybe [a] 274 | nullNothing s = const s <$> listToMaybe s 275 | 276 | processDirs :: [String] -> [String] 277 | processDirs = map processDir 278 | 279 | -- | Replaces @.@ with @./.@ 280 | -- 281 | -- See https://github.com/sol/hpack/issues/119 282 | processDir :: String -> String 283 | processDir "." = "./." 284 | processDir d = d 285 | 286 | -- | Parse comma separated list, stripping whitespace 287 | parseCommaSep :: String -> [String] 288 | parseCommaSep s = 289 | -- separate on commas 290 | -- strip leading and trailing whitespace 291 | fmap trimEnds (splitOn "," s) 292 | 293 | -- | Trim leading and trailing whitespace 294 | trimEnds :: String -> String 295 | trimEnds = f . f 296 | where f = reverse . dropWhile isSpace 297 | 298 | ensureQuoted :: String -> String 299 | ensureQuoted l = 300 | if isQuoted l 301 | then l 302 | else "\"" <> l <> "\"" 303 | 304 | isQuoted :: String -> Bool 305 | isQuoted s = testQuote '\'' || testQuote '\"' 306 | where 307 | testQuote q = head s == q && last s == q 308 | -------------------------------------------------------------------------------- /test/Hpack/ConvertSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Hpack.ConvertSpec (spec) where 3 | 4 | import Prelude () 5 | import Prelude.Compat 6 | 7 | import Control.Monad 8 | import qualified Data.ByteString as ByteString hiding (pack, unpack) 9 | import qualified Data.ByteString.Char8 as ByteString (unpack) 10 | import System.Directory 11 | import System.FilePath 12 | import Test.Hspec 13 | 14 | import Hpack.Config 15 | import Hpack.Convert 16 | 17 | spec :: Spec 18 | spec = 19 | describe "fromPackageDescriptionString" $ do 20 | describe "generated from ./test/data/*.{cabal,yaml}" $ do 21 | cabalFiles <- runIO $ do 22 | fs <- getDirectoryContents "./test/data" 23 | return $ map ("./test/data" ) 24 | (filter ((== ".cabal") . takeExtension) fs) 25 | 26 | forM_ cabalFiles $ \cabalFp -> do 27 | let expectationFp = cabalFp ++ ".yaml" 28 | readPackageFromCabal fp = do 29 | cc <- readFile fp 30 | let Right pkg = fromPackageDescriptionString cc 31 | return pkg 32 | expectationExists <- runIO $ doesFileExist expectationFp 33 | 34 | if not expectationExists 35 | then do 36 | it ("parses " ++ cabalFp) 37 | (pendingWith ("no expected output file at " ++ expectationFp)) 38 | it ("produces the same output from " ++ cabalFp ++ " as from " ++ expectationFp) 39 | (pendingWith ("no expected output file at " ++ expectationFp)) 40 | else do 41 | it ("parses " ++ cabalFp) $ do 42 | pkg <- readPackageFromCabal cabalFp 43 | ecc <- ByteString.readFile expectationFp 44 | -- ByteString.writeFile (cabalFp ++ ".yaml.out") bpkg 45 | encodePackage pkg `shouldBe` ecc 46 | 47 | it ("produces the same output from " ++ cabalFp ++ " as from " ++ expectationFp) $ do 48 | pkg <- readPackageFromCabal cabalFp 49 | Right (_, pkg') <- readPackageConfig expectationFp 50 | -- ByteString.writeFile (cabalFp ++ ".yaml.out.2") (encodePackage pkg') 51 | 52 | -- This is here to make sure encoding is consistent with the cabal 53 | -- file encoding 54 | ByteString.unpack (encodePackage pkg') `shouldBe` ByteString.unpack (encodePackage pkg) 55 | 56 | describe "simple generated cabal file" $ do 57 | it "cabal init -m" $ do 58 | pkgDescription <- readFile "./test/data/cabal-init-minimal.cabal" 59 | let pkg = fromPackageDescriptionString pkgDescription 60 | pkg `shouldBe` Right Package { packageName = "cabal-init-minimal" 61 | , packageVersion = "0.1.0.0" 62 | , packageSynopsis = Nothing 63 | , packageDescription = Nothing 64 | , packageHomepage = Nothing 65 | , packageBugReports = Nothing 66 | , packageCategory = Nothing 67 | , packageStability = Nothing 68 | , packageAuthor = [ "Pedro Tacla Yamada" ] 69 | , packageMaintainer = [ "tacla.yamada@gmail.com" ] 70 | , packageCopyright = [] 71 | , packageLicense = Just "PublicDomain" 72 | , packageLicenseFile = Nothing 73 | , packageTestedWith = Nothing 74 | , packageFlags = [] 75 | , packageExtraSourceFiles = ["ChangeLog.md"] 76 | , packageDataFiles = [] 77 | , packageSourceRepository = Nothing 78 | , packageLibrary = Just Section { sectionData = Library { libraryExposed = Just True 79 | , libraryExposedModules = [] 80 | , libraryOtherModules = [] 81 | , libraryReexportedModules = [] 82 | } 83 | , sectionSourceDirs = ["src"] 84 | , sectionDependencies = [ Dependency "base >=4.8 && <4.9" Nothing ] 85 | , sectionDefaultExtensions = [] 86 | , sectionOtherExtensions = [] 87 | , sectionGhcOptions = [] 88 | , sectionGhcProfOptions = [] 89 | , sectionCppOptions = [] 90 | , sectionCCOptions = [] 91 | , sectionCSources = [] 92 | , sectionExtraLibDirs = [] 93 | , sectionExtraLibraries = [] 94 | , sectionIncludeDirs = [] 95 | , sectionInstallIncludes = [] 96 | , sectionLdOptions = [] 97 | , sectionBuildable = Just True 98 | , sectionConditionals = [] 99 | , sectionBuildTools = [] 100 | } 101 | , packageExecutables = [] 102 | , packageTests = [] 103 | , packageBenchmarks = [] 104 | } 105 | 106 | it "cabal init -m with executables" $ do 107 | pkgDescription <- readFile "./test/data/cabal-init-with-executables.cabal" 108 | let pkg = fromPackageDescriptionString pkgDescription 109 | pkg `shouldBe` Right Package { packageName = "cabal-init-minimal" 110 | , packageVersion = "0.1.0.0" 111 | , packageSynopsis = Nothing 112 | , packageDescription = Nothing 113 | , packageHomepage = Nothing 114 | , packageBugReports = Nothing 115 | , packageCategory = Nothing 116 | , packageStability = Nothing 117 | , packageAuthor = [ "Pedro Tacla Yamada" ] 118 | , packageMaintainer = [ "tacla.yamada@gmail.com" ] 119 | , packageCopyright = [] 120 | , packageLicense = Just "PublicDomain" 121 | , packageLicenseFile = Nothing 122 | , packageTestedWith = Nothing 123 | , packageFlags = [] 124 | , packageExtraSourceFiles = ["ChangeLog.md"] 125 | , packageDataFiles = [] 126 | , packageSourceRepository = Nothing 127 | , packageLibrary = Nothing 128 | , packageExecutables = [ 129 | Section { sectionData = Executable { executableName = "hello-world" 130 | , executableMain = "HelloWorld.hs" 131 | , executableOtherModules = [] 132 | } 133 | , sectionSourceDirs = [ "src" ] 134 | , sectionDependencies = [ Dependency "base >=4.8 && <4.9" Nothing ] 135 | , sectionDefaultExtensions = [] 136 | , sectionOtherExtensions = [] 137 | , sectionGhcOptions = [] 138 | , sectionGhcProfOptions = [] 139 | , sectionCppOptions = [] 140 | , sectionCCOptions = [] 141 | , sectionCSources = [] 142 | , sectionExtraLibDirs = [] 143 | , sectionExtraLibraries = [] 144 | , sectionIncludeDirs = [] 145 | , sectionInstallIncludes = [] 146 | , sectionLdOptions = [] 147 | , sectionBuildable = Just True 148 | , sectionConditionals = [] 149 | , sectionBuildTools = [] 150 | } 151 | ] 152 | , packageTests = [] 153 | , packageBenchmarks = [] 154 | } 155 | 156 | it "cabal init -m with test-suites" $ do 157 | pkgDescription <- readFile "./test/data/cabal-init-with-tests.cabal" 158 | let pkg = fromPackageDescriptionString pkgDescription 159 | pkg `shouldBe` Right Package { packageName = "cabal-init-minimal" 160 | , packageVersion = "0.1.0.0" 161 | , packageSynopsis = Nothing 162 | , packageDescription = Nothing 163 | , packageHomepage = Nothing 164 | , packageBugReports = Nothing 165 | , packageCategory = Nothing 166 | , packageStability = Nothing 167 | , packageAuthor = [ "Pedro Tacla Yamada" ] 168 | , packageMaintainer = [ "tacla.yamada@gmail.com" ] 169 | , packageCopyright = [] 170 | , packageLicense = Just "PublicDomain" 171 | , packageLicenseFile = Nothing 172 | , packageTestedWith = Nothing 173 | , packageFlags = [] 174 | , packageExtraSourceFiles = ["ChangeLog.md"] 175 | , packageDataFiles = [] 176 | , packageSourceRepository = Nothing 177 | , packageLibrary = Nothing 178 | , packageExecutables = [ 179 | Section { sectionData = Executable { executableName = "hello-world" 180 | , executableMain = "HelloWorld.hs" 181 | , executableOtherModules = [] 182 | } 183 | , sectionSourceDirs = [ "src" ] 184 | , sectionDependencies = [ Dependency "base >=4.8 && <4.9" Nothing ] 185 | , sectionDefaultExtensions = [] 186 | , sectionOtherExtensions = [] 187 | , sectionGhcOptions = [] 188 | , sectionGhcProfOptions = [] 189 | , sectionCppOptions = [] 190 | , sectionCCOptions = [] 191 | , sectionCSources = [] 192 | , sectionExtraLibDirs = [] 193 | , sectionExtraLibraries = [] 194 | , sectionIncludeDirs = [] 195 | , sectionInstallIncludes = [] 196 | , sectionLdOptions = [] 197 | , sectionBuildable = Just True 198 | , sectionConditionals = [] 199 | , sectionBuildTools = [] 200 | } 201 | ] 202 | , packageTests = [ 203 | Section { sectionData = Executable { executableName = "hello-world-spec" 204 | , executableMain = "Spec.hs" 205 | , executableOtherModules = [] 206 | } 207 | , sectionSourceDirs = [ "src", "test" ] 208 | , sectionDependencies = [ Dependency "base >=4.8 && <4.9" Nothing ] 209 | , sectionDefaultExtensions = [] 210 | , sectionOtherExtensions = [] 211 | , sectionGhcOptions = [] 212 | , sectionGhcProfOptions = [] 213 | , sectionCppOptions = [] 214 | , sectionCCOptions = [] 215 | , sectionCSources = [] 216 | , sectionExtraLibDirs = [] 217 | , sectionExtraLibraries = [] 218 | , sectionIncludeDirs = [] 219 | , sectionInstallIncludes = [] 220 | , sectionLdOptions = [] 221 | , sectionBuildable = Just True 222 | , sectionConditionals = [] 223 | , sectionBuildTools = [] 224 | } 225 | ] 226 | , packageBenchmarks = [] 227 | } 228 | 229 | it "cabal init -m with benchmarks" $ do 230 | pkgDescription <- readFile "./test/data/cabal-init-with-benchmarks.cabal" 231 | let pkg = fromPackageDescriptionString pkgDescription 232 | pkg `shouldBe` Right Package { packageName = "cabal-init-minimal" 233 | , packageVersion = "0.1.0.0" 234 | , packageSynopsis = Nothing 235 | , packageDescription = Nothing 236 | , packageHomepage = Nothing 237 | , packageBugReports = Nothing 238 | , packageCategory = Nothing 239 | , packageStability = Nothing 240 | , packageAuthor = [ "Pedro Tacla Yamada" ] 241 | , packageMaintainer = [ "tacla.yamada@gmail.com" ] 242 | , packageCopyright = [] 243 | , packageLicense = Just "PublicDomain" 244 | , packageLicenseFile = Nothing 245 | , packageTestedWith = Nothing 246 | , packageFlags = [] 247 | , packageExtraSourceFiles = ["ChangeLog.md"] 248 | , packageDataFiles = [] 249 | , packageSourceRepository = Nothing 250 | , packageLibrary = Nothing 251 | , packageExecutables = [ 252 | Section { sectionData = Executable { executableName = "hello-world" 253 | , executableMain = "HelloWorld.hs" 254 | , executableOtherModules = [] 255 | } 256 | , sectionSourceDirs = [ "src" ] 257 | , sectionDependencies = [ Dependency "base >=4.8 && <4.9" Nothing ] 258 | , sectionDefaultExtensions = [] 259 | , sectionOtherExtensions = [] 260 | , sectionGhcOptions = [] 261 | , sectionGhcProfOptions = [] 262 | , sectionCppOptions = [] 263 | , sectionCCOptions = [] 264 | , sectionCSources = [] 265 | , sectionExtraLibDirs = [] 266 | , sectionExtraLibraries = [] 267 | , sectionIncludeDirs = [] 268 | , sectionInstallIncludes = [] 269 | , sectionLdOptions = [] 270 | , sectionBuildable = Just True 271 | , sectionConditionals = [] 272 | , sectionBuildTools = [] 273 | } 274 | ] 275 | , packageTests = [ 276 | Section { sectionData = Executable { executableName = "hello-world-spec" 277 | , executableMain = "Spec.hs" 278 | , executableOtherModules = [] 279 | } 280 | , sectionSourceDirs = [ "src", "test" ] 281 | , sectionDependencies = [ Dependency "base >=4.8 && <4.9" Nothing ] 282 | , sectionDefaultExtensions = [] 283 | , sectionOtherExtensions = [] 284 | , sectionGhcOptions = [] 285 | , sectionGhcProfOptions = [] 286 | , sectionCppOptions = [] 287 | , sectionCCOptions = [] 288 | , sectionCSources = [] 289 | , sectionExtraLibDirs = [] 290 | , sectionExtraLibraries = [] 291 | , sectionIncludeDirs = [] 292 | , sectionInstallIncludes = [] 293 | , sectionLdOptions = [] 294 | , sectionBuildable = Just True 295 | , sectionConditionals = [] 296 | , sectionBuildTools = [] 297 | } 298 | ] 299 | , packageBenchmarks = [ 300 | Section { sectionData = Executable { executableName = "hello-world-benchmark" 301 | , executableMain = "Bench.hs" 302 | , executableOtherModules = [] 303 | } 304 | , sectionSourceDirs = [ "src", "benchmarks" ] 305 | , sectionDependencies = [ Dependency "base >=4.8 && <4.9" Nothing ] 306 | , sectionDefaultExtensions = [] 307 | , sectionOtherExtensions = [] 308 | , sectionGhcOptions = [] 309 | , sectionGhcProfOptions = [] 310 | , sectionCppOptions = [] 311 | , sectionCCOptions = [] 312 | , sectionCSources = [] 313 | , sectionExtraLibDirs = [] 314 | , sectionExtraLibraries = [] 315 | , sectionIncludeDirs = [] 316 | , sectionInstallIncludes = [] 317 | , sectionLdOptions = [] 318 | , sectionBuildable = Just True 319 | , sectionConditionals = [] 320 | , sectionBuildTools = [] 321 | } 322 | ] 323 | } 324 | 325 | it "cabal init -m with conditionals" $ do 326 | pkgDescription <- readFile "./test/data/cabal-init-with-conditionals.cabal" 327 | let pkg = fromPackageDescriptionString pkgDescription 328 | pkg `shouldBe` Right Package { packageName = "cabal-init-minimal" 329 | , packageVersion = "0.1.0.0" 330 | , packageSynopsis = Nothing 331 | , packageDescription = Nothing 332 | , packageHomepage = Nothing 333 | , packageBugReports = Nothing 334 | , packageCategory = Nothing 335 | , packageStability = Nothing 336 | , packageAuthor = [ "Pedro Tacla Yamada" ] 337 | , packageMaintainer = [ "tacla.yamada@gmail.com" ] 338 | , packageCopyright = [] 339 | , packageLicense = Just "PublicDomain" 340 | , packageLicenseFile = Nothing 341 | , packageTestedWith = Nothing 342 | , packageFlags = [] 343 | , packageExtraSourceFiles = ["ChangeLog.md"] 344 | , packageDataFiles = [] 345 | , packageSourceRepository = Nothing 346 | , packageLibrary = Nothing 347 | , packageExecutables = [ 348 | Section { sectionData = Executable { executableName = "hello-world" 349 | , executableMain = "HelloWorld.hs" 350 | , executableOtherModules = [] 351 | } 352 | , sectionSourceDirs = [ "src" ] 353 | , sectionDependencies = [ Dependency "base >=4.8 && <4.9" Nothing ] 354 | , sectionDefaultExtensions = [] 355 | , sectionOtherExtensions = [] 356 | , sectionGhcOptions = [] 357 | , sectionGhcProfOptions = [] 358 | , sectionCppOptions = [] 359 | , sectionCCOptions = [] 360 | , sectionCSources = [] 361 | , sectionExtraLibDirs = [] 362 | , sectionExtraLibraries = [] 363 | , sectionIncludeDirs = [] 364 | , sectionInstallIncludes = [] 365 | , sectionLdOptions = [] 366 | , sectionBuildable = Just True 367 | , sectionConditionals = [ Conditional "os(osx)" (emptySection {sectionBuildable = Just False}) Nothing 368 | ] 369 | , sectionBuildTools = [] 370 | } 371 | ] 372 | , packageTests = [] 373 | , packageBenchmarks = [] 374 | } 375 | 376 | it "cabal init -m with conditionals and an else branch" $ do 377 | pkgDescription <- readFile "./test/data/cabal-init-with-conditionals-and-else.cabal" 378 | let pkg = fromPackageDescriptionString pkgDescription 379 | pkg `shouldBe` Right Package { packageName = "cabal-init-minimal" 380 | , packageVersion = "0.1.0.0" 381 | , packageSynopsis = Nothing 382 | , packageDescription = Nothing 383 | , packageHomepage = Nothing 384 | , packageBugReports = Nothing 385 | , packageCategory = Nothing 386 | , packageStability = Nothing 387 | , packageAuthor = [ "Pedro Tacla Yamada" ] 388 | , packageMaintainer = [ "tacla.yamada@gmail.com" ] 389 | , packageCopyright = [] 390 | , packageLicense = Just "PublicDomain" 391 | , packageLicenseFile = Nothing 392 | , packageTestedWith = Nothing 393 | , packageFlags = [] 394 | , packageExtraSourceFiles = ["ChangeLog.md"] 395 | , packageDataFiles = [] 396 | , packageSourceRepository = Nothing 397 | , packageLibrary = Nothing 398 | , packageExecutables = [ 399 | Section { sectionData = Executable { executableName = "hello-world" 400 | , executableMain = "HelloWorld.hs" 401 | , executableOtherModules = [] 402 | } 403 | , sectionSourceDirs = [ "src" ] 404 | , sectionDependencies = [ Dependency "base >=4.8 && <4.9" Nothing ] 405 | , sectionDefaultExtensions = [] 406 | , sectionOtherExtensions = [] 407 | , sectionGhcOptions = [] 408 | , sectionGhcProfOptions = [] 409 | , sectionCppOptions = [] 410 | , sectionCCOptions = [] 411 | , sectionCSources = [] 412 | , sectionExtraLibDirs = [] 413 | , sectionExtraLibraries = [] 414 | , sectionIncludeDirs = [] 415 | , sectionInstallIncludes = [] 416 | , sectionLdOptions = [] 417 | , sectionBuildable = Just True 418 | , sectionConditionals = [ 419 | Conditional "os(osx)" 420 | (emptySection {sectionSourceDirs = ["osx"]}) 421 | (Just emptySection {sectionSourceDirs = ["notosx"]}) 422 | ] 423 | , sectionBuildTools = [] 424 | } 425 | ] 426 | , packageTests = [] 427 | , packageBenchmarks = [] 428 | } 429 | 430 | emptySection :: Section () 431 | emptySection = Section { sectionData = () 432 | , sectionSourceDirs = [] 433 | , sectionDependencies = [] 434 | , sectionDefaultExtensions = [] 435 | , sectionOtherExtensions = [] 436 | , sectionGhcOptions = [] 437 | , sectionGhcProfOptions = [] 438 | , sectionCppOptions = [] 439 | , sectionCCOptions = [] 440 | , sectionCSources = [] 441 | , sectionExtraLibDirs = [] 442 | , sectionExtraLibraries = [] 443 | , sectionIncludeDirs = [] 444 | , sectionInstallIncludes = [] 445 | , sectionLdOptions = [] 446 | , sectionBuildable = Just True 447 | , sectionConditionals = [] 448 | , sectionBuildTools = [] 449 | } 450 | -------------------------------------------------------------------------------- /test/Hpack/ConfigSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedLists #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE QuasiQuotes #-} 4 | module Hpack.ConfigSpec ( 5 | spec 6 | 7 | , package 8 | , executable 9 | , library 10 | ) where 11 | 12 | import Helper 13 | 14 | import Data.Aeson.QQ 15 | import Data.Aeson.Types 16 | import Data.String.Interpolate.IsString 17 | import Control.Arrow 18 | import System.Directory (createDirectory) 19 | import Data.Yaml 20 | import Data.Either.Compat 21 | 22 | import Hpack.Util 23 | import Hpack.Config hiding (package) 24 | import qualified Hpack.Config as Config 25 | 26 | package :: Package 27 | package = Config.package "foo" "0.0.0" 28 | 29 | executable :: String -> String -> Executable 30 | executable name main_ = Executable name main_ [] 31 | 32 | library :: Library 33 | library = Library Nothing [] [] [] 34 | 35 | withPackage :: String -> IO () -> (([String], Package) -> Expectation) -> Expectation 36 | withPackage content beforeAction expectation = withTempDirectory $ \dir_ -> do 37 | let dir = dir_ "foo" 38 | createDirectory dir 39 | writeFile (dir "package.yaml") content 40 | withCurrentDirectory dir beforeAction 41 | r <- readPackageConfig (dir "package.yaml") 42 | either expectationFailure expectation r 43 | 44 | withPackageConfig :: String -> IO () -> (Package -> Expectation) -> Expectation 45 | withPackageConfig content beforeAction expectation = withPackage content beforeAction (expectation . snd) 46 | 47 | withPackageConfig_ :: String -> (Package -> Expectation) -> Expectation 48 | withPackageConfig_ content = withPackageConfig content (return ()) 49 | 50 | withPackageWarnings :: String -> IO () -> ([String] -> Expectation) -> Expectation 51 | withPackageWarnings content beforeAction expectation = withPackage content beforeAction (expectation . fst) 52 | 53 | withPackageWarnings_ :: String -> ([String] -> Expectation) -> Expectation 54 | withPackageWarnings_ content = withPackageWarnings content (return ()) 55 | 56 | spec :: Spec 57 | spec = do 58 | describe "renamePackage" $ do 59 | it "renames a package" $ do 60 | renamePackage "bar" package `shouldBe` package {packageName = "bar"} 61 | 62 | it "renames dependencies on self" $ do 63 | let packageWithExecutable dependencies = package {packageExecutables = [(section $ executable "main" "Main.hs") {sectionDependencies = dependencies}]} 64 | renamePackage "bar" (packageWithExecutable ["foo"]) `shouldBe` (packageWithExecutable ["bar"]) {packageName = "bar"} 65 | 66 | describe "renameDependencies" $ do 67 | let sectionWithDeps dependencies = (section ()) {sectionDependencies = dependencies} 68 | 69 | it "renames dependencies" $ do 70 | renameDependencies "bar" "baz" (sectionWithDeps ["foo", "bar"]) `shouldBe` sectionWithDeps ["foo", "baz"] 71 | 72 | it "renames dependency in conditionals" $ do 73 | let sectionWithConditional dependencies = (section ()) { 74 | sectionConditionals = [ 75 | Conditional { 76 | conditionalCondition = "some condition" 77 | , conditionalThen = sectionWithDeps dependencies 78 | , conditionalElse = Just (sectionWithDeps dependencies) 79 | } 80 | ] 81 | } 82 | renameDependencies "bar" "baz" (sectionWithConditional ["foo", "bar"]) `shouldBe` sectionWithConditional ["foo", "baz"] 83 | 84 | describe "parseJSON" $ do 85 | context "when parsing (CaptureUnknownFields Section a)" $ do 86 | it "accepts dependencies" $ do 87 | let input = [i| 88 | dependencies: hpack 89 | |] 90 | captureUnknownFieldsValue <$> decodeEither input 91 | `shouldBe` Right (section Empty){sectionDependencies = ["hpack"]} 92 | 93 | it "accepts includes-dirs" $ do 94 | let input = [i| 95 | include-dirs: 96 | - foo 97 | - bar 98 | |] 99 | captureUnknownFieldsValue <$> decodeEither input 100 | `shouldBe` Right (section Empty){sectionIncludeDirs = ["foo", "bar"]} 101 | 102 | it "accepts install-includes" $ do 103 | let input = [i| 104 | install-includes: 105 | - foo.h 106 | - bar.h 107 | |] 108 | captureUnknownFieldsValue <$> decodeEither input 109 | `shouldBe` Right (section Empty){sectionInstallIncludes = ["foo.h", "bar.h"]} 110 | 111 | it "accepts c-sources" $ do 112 | let input = [i| 113 | c-sources: 114 | - foo.c 115 | - bar.c 116 | |] 117 | captureUnknownFieldsValue <$> decodeEither input 118 | `shouldBe` Right (section Empty){sectionCSources = ["foo.c", "bar.c"]} 119 | 120 | it "accepts extra-lib-dirs" $ do 121 | let input = [i| 122 | extra-lib-dirs: 123 | - foo 124 | - bar 125 | |] 126 | captureUnknownFieldsValue <$> decodeEither input 127 | `shouldBe` Right (section Empty){sectionExtraLibDirs = ["foo", "bar"]} 128 | 129 | it "accepts extra-libraries" $ do 130 | let input = [i| 131 | extra-libraries: 132 | - foo 133 | - bar 134 | |] 135 | captureUnknownFieldsValue <$> decodeEither input 136 | `shouldBe` Right (section Empty){sectionExtraLibraries = ["foo", "bar"]} 137 | 138 | context "when parsing conditionals" $ do 139 | it "accepts conditionals" $ do 140 | let input = [i| 141 | when: 142 | condition: os(windows) 143 | dependencies: Win32 144 | |] 145 | conditionals = [ 146 | Conditional "os(windows)" 147 | (section ()){sectionDependencies = ["Win32"]} 148 | Nothing 149 | ] 150 | captureUnknownFieldsValue <$> decodeEither input 151 | `shouldBe` Right (section Empty){sectionConditionals = conditionals} 152 | 153 | it "warns on unknown fields" $ do 154 | let input = [i| 155 | foo: 23 156 | when: 157 | - condition: os(windows) 158 | bar: 23 159 | when: 160 | condition: os(windows) 161 | bar2: 23 162 | - condition: os(windows) 163 | baz: 23 164 | |] 165 | captureUnknownFieldsFields <$> (decodeEither input :: Either String (CaptureUnknownFields (Section Empty))) 166 | `shouldBe` Right ["foo", "bar", "bar2", "baz"] 167 | 168 | context "when parsing conditionals with else-branch" $ do 169 | it "accepts conditionals with else-branch" $ do 170 | let input = [i| 171 | when: 172 | condition: os(windows) 173 | then: 174 | dependencies: Win32 175 | else: 176 | dependencies: unix 177 | |] 178 | conditionals = [ 179 | Conditional "os(windows)" 180 | (section ()){sectionDependencies = ["Win32"]} 181 | (Just (section ()){sectionDependencies = ["unix"]}) 182 | ] 183 | r :: Either String (Section Empty) 184 | r = captureUnknownFieldsValue <$> decodeEither input 185 | sectionConditionals <$> r `shouldBe` Right conditionals 186 | 187 | it "rejects invalid conditionals" $ do 188 | let input = [i| 189 | when: 190 | condition: os(windows) 191 | then: 192 | dependencies: Win32 193 | else: null 194 | |] 195 | 196 | r :: Either String (Section Empty) 197 | r = captureUnknownFieldsValue <$> decodeEither input 198 | sectionConditionals <$> r `shouldSatisfy` isLeft 199 | 200 | it "warns on unknown fields" $ do 201 | let input = [i| 202 | when: 203 | condition: os(windows) 204 | foo: null 205 | then: 206 | bar: null 207 | else: 208 | baz: null 209 | |] 210 | captureUnknownFieldsFields <$> (decodeEither input :: Either String (CaptureUnknownFields (Section Empty))) 211 | `shouldBe` Right ["foo", "bar", "baz"] 212 | 213 | context "when parsing a Dependency" $ do 214 | it "accepts simple dependencies" $ do 215 | parseEither parseJSON "hpack" `shouldBe` Right (Dependency "hpack" Nothing) 216 | 217 | it "accepts git dependencies" $ do 218 | let value = [aesonQQ|{ 219 | name: "hpack", 220 | git: "https://github.com/sol/hpack", 221 | ref: "master" 222 | }|] 223 | source = GitRef "https://github.com/sol/hpack" "master" Nothing 224 | parseEither parseJSON value `shouldBe` Right (Dependency "hpack" (Just source)) 225 | 226 | it "accepts github dependencies" $ do 227 | let value = [aesonQQ|{ 228 | name: "hpack", 229 | github: "sol/hpack", 230 | ref: "master" 231 | }|] 232 | source = GitRef "https://github.com/sol/hpack" "master" Nothing 233 | parseEither parseJSON value `shouldBe` Right (Dependency "hpack" (Just source)) 234 | 235 | it "accepts an optional subdirectory for git dependencies" $ do 236 | let value = [aesonQQ|{ 237 | name: "warp", 238 | github: "yesodweb/wai", 239 | ref: "master", 240 | subdir: "warp" 241 | }|] 242 | source = GitRef "https://github.com/yesodweb/wai" "master" (Just "warp") 243 | parseEither parseJSON value `shouldBe` Right (Dependency "warp" (Just source)) 244 | 245 | it "accepts local dependencies" $ do 246 | let value = [aesonQQ|{ 247 | name: "hpack", 248 | path: "../hpack" 249 | }|] 250 | source = Local "../hpack" 251 | parseEither parseJSON value `shouldBe` Right (Dependency "hpack" (Just source)) 252 | 253 | context "when parsing fails" $ do 254 | it "returns an error message" $ do 255 | let value = Number 23 256 | parseEither parseJSON value `shouldBe` (Left "Error in $: expected String or an Object, encountered Number" :: Either String Dependency) 257 | 258 | context "when ref is missing" $ do 259 | it "produces accurate error messages" $ do 260 | let value = [aesonQQ|{ 261 | name: "hpack", 262 | git: "sol/hpack", 263 | ef: "master" 264 | }|] 265 | parseEither parseJSON value `shouldBe` (Left "Error in $: key \"ref\" not present" :: Either String Dependency) 266 | 267 | context "when both git and github are missing" $ do 268 | it "produces accurate error messages" $ do 269 | let value = [aesonQQ|{ 270 | name: "hpack", 271 | gi: "sol/hpack", 272 | ref: "master" 273 | }|] 274 | parseEither parseJSON value `shouldBe` (Left "Error in $: neither key \"git\" nor key \"github\" present" :: Either String Dependency) 275 | 276 | describe "getModules" $ around withTempDirectory $ do 277 | it "returns Haskell modules in specified source directory" $ \dir -> do 278 | touch (dir "src/Foo.hs") 279 | touch (dir "src/Bar/Baz.hs") 280 | touch (dir "src/Setup.hs") 281 | getModules dir "src" >>= (`shouldMatchList` ["Foo", "Bar.Baz", "Setup"]) 282 | 283 | context "when source directory is '.'" $ do 284 | it "ignores Setup" $ \dir -> do 285 | touch (dir "Foo.hs") 286 | touch (dir "Setup.hs") 287 | getModules dir "." `shouldReturn` ["Foo"] 288 | 289 | context "when source directory is './.'" $ do 290 | it "ignores Setup" $ \dir -> do 291 | touch (dir "Foo.hs") 292 | touch (dir "Setup.hs") 293 | getModules dir "./." `shouldReturn` ["Foo"] 294 | 295 | describe "determineModules" $ do 296 | it "adds the Paths_* module to the other-modules" $ do 297 | determineModules "foo" [] (Just $ List ["Foo"]) Nothing `shouldBe` (["Foo"], ["Paths_foo"]) 298 | 299 | it "replaces dashes with underscores in Paths_*" $ do 300 | determineModules "foo-bar" [] (Just $ List ["Foo"]) Nothing `shouldBe` (["Foo"], ["Paths_foo_bar"]) 301 | 302 | context "when the Paths_* module is part of the exposed-modules" $ do 303 | it "does not add the Paths_* module to the other-modules" $ do 304 | determineModules "foo" [] (Just $ List ["Foo", "Paths_foo"]) Nothing `shouldBe` (["Foo", "Paths_foo"], []) 305 | 306 | describe "readPackageConfig" $ do 307 | it "warns on unknown fields" $ do 308 | withPackageWarnings_ [i| 309 | bar: 23 310 | baz: 42 311 | |] 312 | (`shouldBe` [ 313 | "Ignoring unknown field \"bar\" in package description" 314 | , "Ignoring unknown field \"baz\" in package description" 315 | ] 316 | ) 317 | 318 | it "warns on unknown fields in when block, list" $ do 319 | withPackageWarnings_ [i| 320 | when: 321 | - condition: impl(ghc) 322 | bar: 23 323 | baz: 42 324 | |] 325 | (`shouldBe` [ 326 | "Ignoring unknown field \"bar\" in package description" 327 | , "Ignoring unknown field \"baz\" in package description" 328 | ] 329 | ) 330 | 331 | it "warns on unknown fields in when block, single" $ do 332 | withPackageWarnings_ [i| 333 | when: 334 | condition: impl(ghc) 335 | github: foo/bar 336 | dependencies: ghc-prim 337 | baz: 42 338 | |] 339 | (`shouldBe` [ 340 | "Ignoring unknown field \"baz\" in package description" 341 | , "Ignoring unknown field \"github\" in package description" 342 | ] 343 | ) 344 | 345 | it "accepts name" $ do 346 | withPackageConfig_ [i| 347 | name: bar 348 | |] 349 | (packageName >>> (`shouldBe` "bar")) 350 | 351 | it "accepts version" $ do 352 | withPackageConfig_ [i| 353 | version: 0.1.0 354 | |] 355 | (packageVersion >>> (`shouldBe` "0.1.0")) 356 | 357 | it "accepts synopsis" $ do 358 | withPackageConfig_ [i| 359 | synopsis: some synopsis 360 | |] 361 | (packageSynopsis >>> (`shouldBe` Just "some synopsis")) 362 | 363 | it "accepts description" $ do 364 | withPackageConfig_ [i| 365 | description: some description 366 | |] 367 | (packageDescription >>> (`shouldBe` Just "some description")) 368 | 369 | it "accepts category" $ do 370 | withPackageConfig_ [i| 371 | category: Data 372 | |] 373 | (`shouldBe` package {packageCategory = Just "Data"}) 374 | 375 | it "accepts author" $ do 376 | withPackageConfig_ [i| 377 | author: John Doe 378 | |] 379 | (`shouldBe` package {packageAuthor = ["John Doe"]}) 380 | 381 | it "accepts maintainer" $ do 382 | withPackageConfig_ [i| 383 | maintainer: John Doe 384 | |] 385 | (`shouldBe` package {packageMaintainer = ["John Doe "]}) 386 | 387 | it "accepts copyright" $ do 388 | withPackageConfig_ [i| 389 | copyright: (c) 2015 John Doe 390 | |] 391 | (`shouldBe` package {packageCopyright = ["(c) 2015 John Doe"]}) 392 | 393 | it "accepts stability" $ do 394 | withPackageConfig_ [i| 395 | stability: experimental 396 | |] 397 | (packageStability >>> (`shouldBe` Just "experimental")) 398 | 399 | it "accepts homepage URL" $ do 400 | withPackageConfig_ [i| 401 | github: hspec/hspec 402 | homepage: https://example.com/ 403 | |] 404 | (packageHomepage >>> (`shouldBe` Just "https://example.com/")) 405 | 406 | it "infers homepage URL from github" $ do 407 | withPackageConfig_ [i| 408 | github: hspec/hspec 409 | |] 410 | (packageHomepage >>> (`shouldBe` Just "https://github.com/hspec/hspec#readme")) 411 | 412 | it "omits homepage URL if it is null" $ do 413 | withPackageConfig_ [i| 414 | github: hspec/hspec 415 | homepage: null 416 | |] 417 | (packageHomepage >>> (`shouldBe` Nothing)) 418 | 419 | it "accepts bug-reports URL" $ do 420 | withPackageConfig_ [i| 421 | github: hspec/hspec 422 | bug-reports: https://example.com/issues 423 | |] 424 | (packageBugReports >>> (`shouldBe` Just "https://example.com/issues")) 425 | 426 | it "infers bug-reports URL from github" $ do 427 | withPackageConfig_ [i| 428 | github: hspec/hspec 429 | |] 430 | (packageBugReports >>> (`shouldBe` Just "https://github.com/hspec/hspec/issues")) 431 | 432 | it "omits bug-reports URL if it is null" $ do 433 | withPackageConfig_ [i| 434 | github: hspec/hspec 435 | bug-reports: null 436 | |] 437 | (packageBugReports >>> (`shouldBe` Nothing)) 438 | 439 | it "accepts license" $ do 440 | withPackageConfig_ [i| 441 | license: MIT 442 | |] 443 | (`shouldBe` package {packageLicense = Just "MIT"}) 444 | 445 | it "infers license file" $ do 446 | withPackageConfig [i| 447 | name: foo 448 | |] 449 | (do 450 | touch "LICENSE" 451 | ) 452 | (packageLicenseFile >>> (`shouldBe` Just "LICENSE")) 453 | 454 | it "accepts license file" $ do 455 | withPackageConfig_ [i| 456 | license-file: FOO 457 | |] 458 | (packageLicenseFile >>> (`shouldBe` Just "FOO")) 459 | 460 | it "accepts flags" $ do 461 | withPackageConfig_ [i| 462 | flags: 463 | integration-tests: 464 | description: Run the integration test suite 465 | manual: yes 466 | default: no 467 | |] 468 | (packageFlags >>> (`shouldBe` [Flag "integration-tests" (Just "Run the integration test suite") True False])) 469 | 470 | it "warns on unknown fields in flag sections" $ do 471 | withPackageWarnings_ [i| 472 | flags: 473 | integration-tests: 474 | description: Run the integration test suite 475 | manual: yes 476 | default: no 477 | foo: 23 478 | |] 479 | (`shouldBe` [ 480 | "Ignoring unknown field \"foo\" for flag \"integration-tests\"" 481 | ] 482 | ) 483 | 484 | it "accepts extra-source-files" $ do 485 | withPackageConfig [i| 486 | extra-source-files: 487 | - CHANGES.markdown 488 | - README.markdown 489 | |] 490 | (do 491 | touch "CHANGES.markdown" 492 | touch "README.markdown" 493 | ) 494 | (packageExtraSourceFiles >>> (`shouldBe` ["CHANGES.markdown", "README.markdown"])) 495 | 496 | it "accepts data-files" $ do 497 | withPackageConfig [i| 498 | data-files: 499 | - data/**/*.html 500 | |] 501 | (do 502 | touch "data/foo/index.html" 503 | touch "data/bar/index.html" 504 | ) 505 | (packageDataFiles >>> (`shouldMatchList` ["data/foo/index.html", "data/bar/index.html"])) 506 | 507 | it "accepts github" $ do 508 | withPackageConfig_ [i| 509 | github: hspec/hspec 510 | |] 511 | (packageSourceRepository >>> (`shouldBe` Just (SourceRepository "https://github.com/hspec/hspec" Nothing))) 512 | 513 | it "accepts third part of github URL as subdir" $ do 514 | withPackageConfig_ [i| 515 | github: hspec/hspec/hspec-core 516 | |] 517 | (packageSourceRepository >>> (`shouldBe` Just (SourceRepository "https://github.com/hspec/hspec" (Just "hspec-core")))) 518 | 519 | it "accepts arbitrary git URLs as source repository" $ do 520 | withPackageConfig_ [i| 521 | git: https://gitlab.com/gitlab-org/gitlab-ce.git 522 | |] 523 | (packageSourceRepository >>> (`shouldBe` Just (SourceRepository "https://gitlab.com/gitlab-org/gitlab-ce.git" Nothing))) 524 | 525 | it "accepts CPP options" $ do 526 | withPackageConfig_ [i| 527 | cpp-options: -DFOO 528 | library: 529 | cpp-options: -DLIB 530 | 531 | executables: 532 | foo: 533 | main: Main.hs 534 | cpp-options: -DFOO 535 | 536 | 537 | tests: 538 | spec: 539 | main: Spec.hs 540 | cpp-options: -DTEST 541 | |] 542 | (`shouldBe` package { 543 | packageLibrary = Just (section library) {sectionCppOptions = ["-DFOO", "-DLIB"]} 544 | , packageExecutables = [(section $ executable "foo" "Main.hs") {sectionCppOptions = ["-DFOO", "-DFOO"]}] 545 | , packageTests = [(section $ executable "spec" "Spec.hs") {sectionCppOptions = ["-DFOO", "-DTEST"]}] 546 | } 547 | ) 548 | 549 | it "accepts cc-options" $ do 550 | withPackageConfig_ [i| 551 | cc-options: -Wall 552 | library: 553 | cc-options: -fLIB 554 | 555 | executables: 556 | foo: 557 | main: Main.hs 558 | cc-options: -O2 559 | 560 | 561 | tests: 562 | spec: 563 | main: Spec.hs 564 | cc-options: -O0 565 | |] 566 | (`shouldBe` package { 567 | packageLibrary = Just (section library) {sectionCCOptions = ["-Wall", "-fLIB"]} 568 | , packageExecutables = [(section $ executable "foo" "Main.hs") {sectionCCOptions = ["-Wall", "-O2"]}] 569 | , packageTests = [(section $ executable "spec" "Spec.hs") {sectionCCOptions = ["-Wall", "-O0"]}] 570 | } 571 | ) 572 | 573 | it "accepts ld-options" $ do 574 | withPackageConfig_ [i| 575 | library: 576 | ld-options: -static 577 | |] 578 | (`shouldBe` package { 579 | packageLibrary = Just (section library) {sectionLdOptions = ["-static"]} 580 | } 581 | ) 582 | 583 | it "accepts buildable" $ do 584 | withPackageConfig_ [i| 585 | buildable: no 586 | library: 587 | buildable: yes 588 | 589 | executables: 590 | foo: 591 | main: Main.hs 592 | |] 593 | (`shouldBe` package { 594 | packageLibrary = Just (section library) {sectionBuildable = Just True} 595 | , packageExecutables = [(section $ executable "foo" "Main.hs") {sectionBuildable = Just False}] 596 | } 597 | ) 598 | 599 | context "when reading library section" $ do 600 | it "warns on unknown fields" $ do 601 | withPackageWarnings_ [i| 602 | library: 603 | bar: 23 604 | baz: 42 605 | |] 606 | (`shouldBe` [ 607 | "Ignoring unknown field \"bar\" in library section" 608 | , "Ignoring unknown field \"baz\" in library section" 609 | ] 610 | ) 611 | 612 | it "accepts source-dirs" $ do 613 | withPackageConfig_ [i| 614 | library: 615 | source-dirs: 616 | - foo 617 | - bar 618 | |] 619 | (packageLibrary >>> (`shouldBe` Just (section library) {sectionSourceDirs = ["foo", "bar"]})) 620 | 621 | it "accepts build-tools" $ do 622 | withPackageConfig_ [i| 623 | library: 624 | build-tools: 625 | - alex 626 | - happy 627 | |] 628 | (packageLibrary >>> (`shouldBe` Just (section library) {sectionBuildTools = ["alex", "happy"]})) 629 | 630 | it "accepts default-extensions" $ do 631 | withPackageConfig_ [i| 632 | library: 633 | default-extensions: 634 | - Foo 635 | - Bar 636 | |] 637 | (packageLibrary >>> (`shouldBe` Just (section library) {sectionDefaultExtensions = ["Foo", "Bar"]})) 638 | 639 | it "accepts global default-extensions" $ do 640 | withPackageConfig_ [i| 641 | default-extensions: 642 | - Foo 643 | - Bar 644 | library: {} 645 | |] 646 | (packageLibrary >>> (`shouldBe` Just (section library) {sectionDefaultExtensions = ["Foo", "Bar"]})) 647 | 648 | it "accepts global source-dirs" $ do 649 | withPackageConfig_ [i| 650 | source-dirs: 651 | - foo 652 | - bar 653 | library: {} 654 | |] 655 | (packageLibrary >>> (`shouldBe` Just (section library) {sectionSourceDirs = ["foo", "bar"]})) 656 | 657 | it "accepts global build-tools" $ do 658 | withPackageConfig_ [i| 659 | build-tools: 660 | - alex 661 | - happy 662 | library: {} 663 | |] 664 | (packageLibrary >>> (`shouldBe` Just (section library) {sectionBuildTools = ["alex", "happy"]})) 665 | 666 | it "allows to specify exposed" $ do 667 | withPackageConfig_ [i| 668 | library: 669 | exposed: no 670 | |] 671 | (packageLibrary >>> (`shouldBe` Just (section library{libraryExposed = Just False}))) 672 | 673 | it "allows to specify exposed-modules" $ do 674 | withPackageConfig [i| 675 | library: 676 | source-dirs: src 677 | exposed-modules: Foo 678 | |] 679 | (do 680 | touch "src/Foo.hs" 681 | touch "src/Bar.hs" 682 | ) 683 | (packageLibrary >>> (`shouldBe` Just (section library{libraryExposedModules = ["Foo"], libraryOtherModules = ["Bar", "Paths_foo"]}) {sectionSourceDirs = ["src"]})) 684 | 685 | it "allows to specify other-modules" $ do 686 | withPackageConfig [i| 687 | library: 688 | source-dirs: src 689 | other-modules: Bar 690 | |] 691 | (do 692 | touch "src/Foo.hs" 693 | touch "src/Bar.hs" 694 | ) 695 | (packageLibrary >>> (`shouldBe` Just (section library{libraryExposedModules = ["Foo"], libraryOtherModules = ["Bar"]}) {sectionSourceDirs = ["src"]})) 696 | 697 | it "allows to specify reexported-modules" $ do 698 | withPackageConfig_ [i| 699 | library: 700 | reexported-modules: Baz 701 | |] 702 | (packageLibrary >>> (`shouldBe` Just (section library{libraryReexportedModules = ["Baz"]}))) 703 | 704 | it "allows to specify both exposed-modules and other-modules" $ do 705 | withPackageConfig [i| 706 | library: 707 | source-dirs: src 708 | exposed-modules: Foo 709 | other-modules: Bar 710 | |] 711 | (do 712 | touch "src/Baz.hs" 713 | ) 714 | (packageLibrary >>> (`shouldBe` Just (section library{libraryExposedModules = ["Foo"], libraryOtherModules = ["Bar"]}) {sectionSourceDirs = ["src"]})) 715 | 716 | context "when neither exposed-modules nor other-modules are specified" $ do 717 | it "exposes all modules" $ do 718 | withPackageConfig [i| 719 | library: 720 | source-dirs: src 721 | |] 722 | (do 723 | touch "src/Foo.hs" 724 | touch "src/Bar.hs" 725 | ) 726 | (packageLibrary >>> (`shouldBe` Just (section library{libraryExposedModules = ["Bar", "Foo"]}) {sectionSourceDirs = ["src"]})) 727 | 728 | context "when reading executable section" $ do 729 | it "warns on unknown fields" $ do 730 | withPackageWarnings_ [i| 731 | executables: 732 | foo: 733 | main: Main.hs 734 | bar: 42 735 | baz: 23 736 | |] 737 | (`shouldBe` [ 738 | "Ignoring unknown field \"bar\" in executable section \"foo\"" 739 | , "Ignoring unknown field \"baz\" in executable section \"foo\"" 740 | ] 741 | ) 742 | 743 | it "reads executable section" $ do 744 | withPackageConfig_ [i| 745 | executables: 746 | foo: 747 | main: driver/Main.hs 748 | |] 749 | (packageExecutables >>> (`shouldBe` [section $ executable "foo" "driver/Main.hs"])) 750 | 751 | it "accepts arbitrary entry points as main" $ do 752 | withPackageConfig_ [i| 753 | executables: 754 | foo: 755 | main: Foo 756 | |] 757 | (packageExecutables >>> (`shouldBe` [ 758 | (section $ executable "foo" "Foo.hs") {sectionGhcOptions = ["-main-is Foo"]} 759 | ] 760 | )) 761 | 762 | it "accepts source-dirs" $ do 763 | withPackageConfig_ [i| 764 | executables: 765 | foo: 766 | main: Main.hs 767 | source-dirs: 768 | - foo 769 | - bar 770 | |] 771 | (packageExecutables >>> (`shouldBe` [(section $ executable "foo" "Main.hs") {sectionSourceDirs = ["foo", "bar"]}])) 772 | 773 | it "accepts build-tools" $ do 774 | withPackageConfig_ [i| 775 | executables: 776 | foo: 777 | main: Main.hs 778 | build-tools: 779 | - alex 780 | - happy 781 | |] 782 | (packageExecutables >>> (`shouldBe` [(section $ executable "foo" "Main.hs") {sectionBuildTools = ["alex", "happy"]}])) 783 | 784 | it "accepts global source-dirs" $ do 785 | withPackageConfig_ [i| 786 | source-dirs: 787 | - foo 788 | - bar 789 | executables: 790 | foo: 791 | main: Main.hs 792 | |] 793 | (packageExecutables >>> (`shouldBe` [(section $ executable "foo" "Main.hs") {sectionSourceDirs = ["foo", "bar"]}])) 794 | 795 | it "accepts global build-tools" $ do 796 | withPackageConfig_ [i| 797 | build-tools: 798 | - alex 799 | - happy 800 | executables: 801 | foo: 802 | main: Main.hs 803 | |] 804 | (packageExecutables >>> (`shouldBe` [(section $ executable "foo" "Main.hs") {sectionBuildTools = ["alex", "happy"]}])) 805 | 806 | it "infers other-modules" $ do 807 | withPackageConfig [i| 808 | executables: 809 | foo: 810 | main: Main.hs 811 | source-dirs: src 812 | |] 813 | (do 814 | touch "src/Main.hs" 815 | touch "src/Foo.hs" 816 | ) 817 | (map (executableOtherModules . sectionData) . packageExecutables >>> (`shouldBe` [["Foo"]])) 818 | 819 | it "allows to specify other-modules" $ do 820 | withPackageConfig [i| 821 | executables: 822 | foo: 823 | main: Main.hs 824 | source-dirs: src 825 | other-modules: Baz 826 | |] 827 | (do 828 | touch "src/Foo.hs" 829 | touch "src/Bar.hs" 830 | ) 831 | (map (executableOtherModules . sectionData) . packageExecutables >>> (`shouldBe` [["Baz"]])) 832 | 833 | it "accepts default-extensions" $ do 834 | withPackageConfig_ [i| 835 | executables: 836 | foo: 837 | main: driver/Main.hs 838 | default-extensions: 839 | - Foo 840 | - Bar 841 | |] 842 | (packageExecutables >>> (`shouldBe` [(section $ executable "foo" "driver/Main.hs") {sectionDefaultExtensions = ["Foo", "Bar"]}])) 843 | 844 | it "accepts global default-extensions" $ do 845 | withPackageConfig_ [i| 846 | default-extensions: 847 | - Foo 848 | - Bar 849 | executables: 850 | foo: 851 | main: driver/Main.hs 852 | |] 853 | (packageExecutables >>> (`shouldBe` [(section $ executable "foo" "driver/Main.hs") {sectionDefaultExtensions = ["Foo", "Bar"]}])) 854 | 855 | it "accepts GHC options" $ do 856 | withPackageConfig_ [i| 857 | executables: 858 | foo: 859 | main: driver/Main.hs 860 | ghc-options: -Wall 861 | |] 862 | (`shouldBe` package {packageExecutables = [(section $ executable "foo" "driver/Main.hs") {sectionGhcOptions = ["-Wall"]}]}) 863 | 864 | it "accepts global GHC options" $ do 865 | withPackageConfig_ [i| 866 | ghc-options: -Wall 867 | executables: 868 | foo: 869 | main: driver/Main.hs 870 | |] 871 | (`shouldBe` package {packageExecutables = [(section $ executable "foo" "driver/Main.hs") {sectionGhcOptions = ["-Wall"]}]}) 872 | 873 | it "accepts GHC profiling options" $ do 874 | withPackageConfig_ [i| 875 | executables: 876 | foo: 877 | main: driver/Main.hs 878 | ghc-prof-options: -fprof-auto 879 | |] 880 | (`shouldBe` package {packageExecutables = [(section $ executable "foo" "driver/Main.hs") {sectionGhcProfOptions = ["-fprof-auto"]}]}) 881 | 882 | it "accepts global GHC profiling options" $ do 883 | withPackageConfig_ [i| 884 | ghc-prof-options: -fprof-auto 885 | executables: 886 | foo: 887 | main: driver/Main.hs 888 | |] 889 | (`shouldBe` package {packageExecutables = [(section $ executable "foo" "driver/Main.hs") {sectionGhcProfOptions = ["-fprof-auto"]}]}) 890 | 891 | 892 | context "when reading test section" $ do 893 | it "warns on unknown fields" $ do 894 | withPackageWarnings_ [i| 895 | tests: 896 | foo: 897 | main: Main.hs 898 | bar: 42 899 | baz: 23 900 | |] 901 | (`shouldBe` [ 902 | "Ignoring unknown field \"bar\" in test section \"foo\"" 903 | , "Ignoring unknown field \"baz\" in test section \"foo\"" 904 | ] 905 | ) 906 | 907 | it "reads test section" $ do 908 | withPackageConfig_ [i| 909 | tests: 910 | spec: 911 | main: test/Spec.hs 912 | |] 913 | (`shouldBe` package {packageTests = [section $ executable "spec" "test/Spec.hs"]}) 914 | 915 | it "accepts single dependency" $ do 916 | withPackageConfig_ [i| 917 | tests: 918 | spec: 919 | main: test/Spec.hs 920 | dependencies: hspec 921 | |] 922 | (`shouldBe` package {packageTests = [(section $ executable "spec" "test/Spec.hs") {sectionDependencies = ["hspec"]}]}) 923 | 924 | it "accepts list of dependencies" $ do 925 | withPackageConfig_ [i| 926 | tests: 927 | spec: 928 | main: test/Spec.hs 929 | dependencies: 930 | - hspec 931 | - QuickCheck 932 | |] 933 | (`shouldBe` package {packageTests = [(section $ executable "spec" "test/Spec.hs") {sectionDependencies = ["hspec", "QuickCheck"]}]}) 934 | 935 | context "when both global and section specific dependencies are specified" $ do 936 | it "combines dependencies" $ do 937 | withPackageConfig_ [i| 938 | dependencies: 939 | - base 940 | 941 | tests: 942 | spec: 943 | main: test/Spec.hs 944 | dependencies: hspec 945 | |] 946 | (`shouldBe` package {packageTests = [(section $ executable "spec" "test/Spec.hs") {sectionDependencies = ["base", "hspec"]}]}) 947 | 948 | context "when a specified source directory does not exist" $ do 949 | it "warns" $ do 950 | withPackageWarnings [i| 951 | source-dirs: 952 | - some-dir 953 | - some-existing-dir 954 | library: 955 | source-dirs: some-lib-dir 956 | executables: 957 | main: 958 | main: Main.hs 959 | source-dirs: some-exec-dir 960 | tests: 961 | spec: 962 | main: Main.hs 963 | source-dirs: some-test-dir 964 | |] 965 | (do 966 | touch "some-existing-dir/foo" 967 | ) 968 | (`shouldBe` [ 969 | "Specified source-dir \"some-dir\" does not exist" 970 | , "Specified source-dir \"some-exec-dir\" does not exist" 971 | , "Specified source-dir \"some-lib-dir\" does not exist" 972 | , "Specified source-dir \"some-test-dir\" does not exist" 973 | ] 974 | ) 975 | 976 | around withTempDirectory $ do 977 | context "when package.yaml can not be parsed" $ do 978 | it "returns an error" $ \dir -> do 979 | let file = dir "package.yaml" 980 | writeFile file [i| 981 | foo: bar 982 | foo baz 983 | |] 984 | readPackageConfig file `shouldReturn` Left (file ++ ":3:12: could not find expected ':' while scanning a simple key") 985 | 986 | context "when package.yaml is invalid" $ do 987 | it "returns an error" $ \dir -> do 988 | let file = dir "package.yaml" 989 | writeFile file [i| 990 | executables: 991 | foo: 992 | ain: driver/Main.hs 993 | |] 994 | readPackageConfig file >>= (`shouldSatisfy` isLeft) 995 | 996 | context "when package.yaml does not exist" $ do 997 | it "returns an error" $ \dir -> do 998 | let file = dir "package.yaml" 999 | readPackageConfig file `shouldReturn` Left [i|#{file}: Yaml file not found: #{file}|] 1000 | 1001 | context "toJSON :: Conditinal -> Value" $ do 1002 | it "serializes conditionals properly" $ do 1003 | let s = emptySection { sectionBuildable = Just False } 1004 | toJSON (Conditional "os(darwin)" s Nothing) 1005 | `shouldBe` object [ "condition" .= String "os(darwin)" 1006 | , "buildable" .= Bool False 1007 | ] 1008 | 1009 | it "serializes conditionals with an else branch properly" $ do 1010 | let s = emptySection { sectionBuildable = Just False } 1011 | toJSON (Conditional "os(darwin)" s (Just s)) 1012 | `shouldBe` object [ "condition" .= String "os(darwin)" 1013 | , "then" .= object [ "buildable" .= Bool False 1014 | ] 1015 | , "else" .= object [ "buildable" .= Bool False 1016 | ] 1017 | ] 1018 | 1019 | 1020 | context "toJSON :: Section a -> Value" $ 1021 | it "serializes conditionals properly" $ do 1022 | let s = emptySection { sectionConditionals = [ Conditional "os(darwin)" emptySection { sectionBuildable = Just False } Nothing] 1023 | } 1024 | toJSON s 1025 | `shouldBe` object [ "when" .= array [ object [ "condition" .= String "os(darwin)" 1026 | , "buildable" .= Bool False 1027 | ] 1028 | ] 1029 | ] 1030 | 1031 | emptySection :: Section () 1032 | emptySection = Section { sectionData = () 1033 | , sectionSourceDirs = [] 1034 | , sectionDependencies = [] 1035 | , sectionDefaultExtensions = [] 1036 | , sectionOtherExtensions = [] 1037 | , sectionGhcOptions = [] 1038 | , sectionGhcProfOptions = [] 1039 | , sectionCppOptions = [] 1040 | , sectionCCOptions = [] 1041 | , sectionCSources = [] 1042 | , sectionExtraLibDirs = [] 1043 | , sectionExtraLibraries = [] 1044 | , sectionIncludeDirs = [] 1045 | , sectionInstallIncludes = [] 1046 | , sectionLdOptions = [] 1047 | , sectionBuildable = Nothing 1048 | , sectionConditionals = [] 1049 | , sectionBuildTools = [] 1050 | } 1051 | --------------------------------------------------------------------------------