diff --git a/.gitignore b/.gitignore index 62d89f7..e84f893 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ tags TAGS codex.tags .vim + + +## test specific ignores +test/testdata/**/stack.yaml diff --git a/app/Main.hs b/app/Main.hs index 361e09e..902ba1e 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} module Main where @@ -14,24 +15,38 @@ import Hie.Locate import Hie.Yaml import System.Directory import System.Directory.Internal -import System.FilePath.Posix +import System.FilePath +import Hie.CabalHelper +import Distribution.Helper (Ex (..)) main :: IO () main = do pwd <- getCurrentDirectory - files <- listDirectory pwd - let name = - if | any (("dist-newstyle" ==) . takeFileName) files -> "cabal" - | any ((".stack-work" ==) . takeFileName) files -> "stack" - | any (("cabal.project" ==) . takeFileName) files -> "cabal" - | any (("stack.yaml" ==) . takeFileName) files -> "stack" - | otherwise -> "cabal" - cfs <- runMaybeT $ case name of - "cabal" -> cabalPkgs pwd - _ -> stackYamlPkgs pwd - when (null cfs) $ error $ - "No .cabal files found under" - <> pwd - <> "\n You may need to run stack build." - pkgs <- catMaybes <$> mapM (nestedPkg pwd) (concat cfs) - putStr <$> hieYaml name $ fmtPkgs name pkgs + projM <- findCabalHelperEntryPoint $ pwd </> "Foo.hs" + case projM of + Nothing -> error "could not find project context" + Just proj@(Ex p) -> do + env <- initialiseEnvironment p + packages <- loadPackages env + pkgs <- toHieYaml env packages + let name = if | isCabalProject proj -> "cabal" + | isStackProject proj -> "stack" + | otherwise -> error "Neither stack nor cabal project" + putStr $ hieYaml name $ fmtPkgs name pkgs + + -- files <- listDirectory pwd + -- let name = + -- if | any (("dist-newstyle" ==) . takeFileName) files -> "cabal" + -- | any ((".stack-work" ==) . takeFileName) files -> "stack" + -- | any (("cabal.project" ==) . takeFileName) files -> "cabal" + -- | any (("stack.yaml" ==) . takeFileName) files -> "stack" + -- | otherwise -> "cabal" + -- cfs <- runMaybeT $ case name of + -- "cabal" -> cabalPkgs pwd + -- _ -> stackYamlPkgs pwd + -- when (null cfs) $ error $ + -- "No .cabal files found under" + -- <> pwd + -- <> "\n You may need to run stack build." + -- pkgs <- catMaybes <$> mapM (nestedPkg pwd) (concat cfs) + -- putStr <$> hieYaml name $ fmtPkgs name pkgs diff --git a/hie.yaml b/hie.yaml index 683f137..86e1067 100644 --- a/hie.yaml +++ b/hie.yaml @@ -8,3 +8,9 @@ cradle: - path: "test" component: "implicit-hie:test:implicit-hie-test" + + - path: test/utils + component: "implicit-hie:test:unit-tests" + + - path: test/unit + component: "implicit-hie:test:unit-tests" diff --git a/implicit-hie.cabal b/implicit-hie.cabal index 0c495fb..770f9f9 100644 --- a/implicit-hie.cabal +++ b/implicit-hie.cabal @@ -1,11 +1,4 @@ -cabal-version: 2.0 - --- This file has been generated from package.yaml by hpack version 0.33.0. --- --- see: https://github.com/sol/hpack --- --- hash: 18f92037a7863d121ac45e847f7dc6177adf0ebc7951dfa1588340f86e64456b - +cabal-version: 2.4 name: implicit-hie version: 0.1.0.0 description: Auto generate a stack or cabal multi component hie.yaml file @@ -16,41 +9,68 @@ bug-reports: https://github.com/Avi-D-coder/implicit-hie/issues author: Avi Dessauer maintainer: avi.the.coder@gmail.com copyright: 2020 -license: BSD3 +license: BSD-3-Clause license-file: LICENSE build-type: Simple extra-source-files: README.md ChangeLog.md + test/testdata/cabal-helper/**/*.hs + test/testdata/cabal-helper/**/*.cabal + test/testdata/cabal-helper/**/*.project source-repository head type: git location: https://github.com/Avi-D-coder/implicit-hie +flag cabalHelper + Description: Enable Cabal-Helper dependencies + Default: True + Manual: True + +common cabal-helper + if flag(cabalHelper) + cpp-options: -DCABAL_HELPER_SUPPORT + library + import: cabal-helper exposed-modules: Hie.Cabal.Parser Hie.Locate + Hie.Logger Hie.Yaml + + if flag(cabalHelper) + exposed-modules: + Hie.CabalHelper + build-depends: + cabal-helper >= 1.1 && <1.2 + other-modules: Paths_implicit_hie autogen-modules: Paths_implicit_hie hs-source-dirs: src - ghc-options: -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns -fno-warn-unused-imports -fno-warn-unused-binds -fno-warn-name-shadowing -fwarn-redundant-constraints + ghc-options: -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns -fwarn-unused-imports -fwarn-unused-binds -fwarn-name-shadowing -fwarn-redundant-constraints build-depends: - attoparsec >= 0.13 + aeson + , attoparsec >= 0.13 , base >=4.7 && <5 + , containers , directory >= 1.3 , filepath >= 1.4 , filepattern >= 0.1 + , hie-bios >=0.5 && <0.6 + , hslogger , text >= 1.2 , transformers >= 0.5 + , process , yaml >= 0.5 default-language: Haskell2010 executable gen-hie + import: cabal-helper main-is: Main.hs other-modules: Paths_implicit_hie @@ -62,16 +82,19 @@ executable gen-hie build-depends: attoparsec , base >=4.7 && <5 + , cabal-helper , directory , filepath , filepattern , implicit-hie + , optparse-applicative , text , transformers , yaml default-language: Haskell2010 test-suite implicit-hie-test + import: cabal-helper type: exitcode-stdio-1.0 main-is: Spec.hs other-modules: @@ -94,3 +117,27 @@ test-suite implicit-hie-test , transformers , yaml default-language: Haskell2010 + +test-suite unit-tests + import: cabal-helper + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + Paths_implicit_hie, TestUtils + if flag(cabalHelper) + other-modules: + CabalHelperSpec + hs-source-dirs: + test/unit, test/utils + ghc-options: -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns -fno-warn-unused-imports -fno-warn-unused-binds -fno-warn-name-shadowing -fwarn-redundant-constraints -threaded -rtsopts -with-rtsopts=-N + build-depends: + aeson + , base + , directory + , filepath + , hie-bios + , hspec + , implicit-hie + , text + , yaml + default-language: Haskell2010 diff --git a/src/Hie/Cabal/Parser.hs b/src/Hie/Cabal/Parser.hs index 92633f0..4766294 100644 --- a/src/Hie/Cabal/Parser.hs +++ b/src/Hie/Cabal/Parser.hs @@ -20,7 +20,7 @@ type Indent = Int data Package = Package Name [Component] deriving (Show, Eq, Ord) -data CompType = Lib | Exe | Test | Bench +data CompType = Lib | Exe | Test | Bench | FLib deriving (Show, Eq, Ord) data Component diff --git a/src/Hie/CabalHelper.hs b/src/Hie/CabalHelper.hs new file mode 100644 index 0000000..763cb8d --- /dev/null +++ b/src/Hie/CabalHelper.hs @@ -0,0 +1,864 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Hie.CabalHelper + ( CabalHelper (..), + isStackCradle, + isCabalCradle, + cabalHelperCradle, + findCabalHelperEntryPoint, + isCabalProject, + isStackProject, + initialiseEnvironment, + loadAllUnits, + toHieYaml, + loadPackages, + ) +where + +import Control.Exception +import Control.Monad (forM) +import Data.Foldable (toList) +import Data.Function ((&)) +import Data.List (find, intercalate, isPrefixOf, sortOn) +import qualified Data.List.NonEmpty as NonEmpty +import Data.List.NonEmpty (NonEmpty) +import qualified Data.Map as Map +import Data.Maybe (isJust, listToMaybe, mapMaybe) +import Data.Ord (Down (..)) +import qualified Data.Text as T +import Distribution.Helper + ( ChComponentInfo (..), + ChComponentName (..), + ChLibraryName(..), + ChEntrypoint (..), + Ex (..), + Package, + ProjLoc (..), + QueryEnv, + Unit, + UnitInfo (..), + mkQueryEnv, + pPackageName, + pSourceDir, + pUnits, + projectPackages, + runQuery, + uiComponents, + unChModuleName, + unitInfo, + ) +import Distribution.Helper.Discover (findProjects, getDefaultDistDir) +import HIE.Bios as Bios +import HIE.Bios.Types (CradleAction (..)) +import qualified HIE.Bios.Types as Bios +import Hie.Logger +import qualified Hie.Cabal.Parser as CP +import System.Directory (canonicalizePath, findExecutable, getCurrentDirectory, makeRelativeToCurrentDirectory) +import System.Exit +import System.FilePath + +-- | Check if the given cradle is a stack cradle. +-- This might be used to determine the GHC version to use on the project. +-- If it is a stack-cradle, we have to use @"stack path --compiler-exe"@ +-- otherwise we may ask `ghc` directly what version it is. +isStackCradle :: Cradle CabalHelper -> Bool +isStackCradle = + (`elem` [Bios.Other Stack, Bios.Other StackNone]) + . Bios.actionName + . Bios.cradleOptsProg + +-- | Check if the given cradle is a cabal cradle. +-- This might be used to determine the GHC version to use on the project. +-- If it is a stack-cradle, we have to use @"stack path --compiler-exe"@ +-- otherwise we may ask @ghc@ directly what version it is. +isCabalCradle :: Cradle CabalHelper -> Bool +isCabalCradle = + (`elem` [Bios.Other CabalV2, Bios.Other CabalNone]) + . Bios.actionName + . Bios.cradleOptsProg + +data CabalHelper + = Stack + | StackNone + | CabalV2 + | CabalNone + deriving (Show, Eq, Ord) + +-- | Finds a Cabal v2-project, Cabal v1-project or a Stack project +-- relative to the given FilePath. +-- Cabal v2-project and Stack have priority over Cabal v1-project. +-- This entails that if a Cabal v1-project can be identified, it is +-- first checked whether there are Stack projects or Cabal v2-projects +-- before it is concluded that this is the project root. +-- Cabal v2-projects and Stack projects are equally important. +-- Due to the lack of user-input we have to guess which project it +-- should rather be. +-- This guessing has no guarantees and may change at any time. +-- +-- === Example: +-- +-- Assume the following project structure: +-- +-- @ +-- / +-- └── Foo/ +-- ├── Foo.cabal +-- ├── stack.yaml +-- ├── cabal.project +-- ├── src +-- │ └── Lib.hs +-- └── B/ +-- ├── B.cabal +-- └── src/ +-- └── Lib2.hs +-- @ +-- +-- Assume the call @findCabalHelperEntryPoint "\/Foo\/B\/src\/Lib2.hs"@. +-- We now want to know to which project "\/Foo\/B\/src\/Lib2.hs" belongs to +-- and what the projects root is. If we only do a naive search to find the +-- first occurrence of either "B.cabal", "stack.yaml", "cabal.project" +-- or "Foo.cabal", we might assume that the location of "B.cabal" marks +-- the project's root directory of which "\/Foo\/B\/src\/Lib2.hs" is part of. +-- However, there is also a "cabal.project" and "stack.yaml" in the parent +-- directory, which add the package @B@ as a package. +-- So, the compilation of the package @B@, and the file "src\/Lib2.hs" in it, +-- does not only depend on the definitions in "B.cabal", but also +-- on "stack.yaml" and "cabal.project". +-- The project root is therefore "\/Foo\/". +-- Only if there is no "stack.yaml" or "cabal.project" in any of the ancestor +-- directories, it is safe to assume that "B.cabal" marks the root of the project. +-- +-- Thus: +-- +-- >>> findCabalHelperEntryPoint "/Foo/B/src/Lib2.hs +-- Just (Ex (ProjLocStackYaml { plStackYaml = "/Foo/"})) +-- +-- or +-- +-- >>> findCabalHelperEntryPoint "/Foo/B/src/Lib2.hs" +-- Just (Ex (ProjLocV2File { plProjectDirV2 = "/Foo/"})) +-- +-- In the given example, it is not guaranteed which project type is found, +-- it is only guaranteed that it will not identify the project +-- as a cabal v1-project. Note that with cabal-helper version (1.0), +-- by default a *.cabal file is identified as a 'ProjLocV2Dir' project. +-- The same issue as before exists and we look for a 'ProjLocV2File' or +-- 'ProjLocStackYaml' before deciding that 'ProjLocV2Dir' marks the project root. +-- +-- Note that this will not return any project types for which the corresponding +-- build tool is not on the PATH. This is "stack" and "cabal" for stack and cabal +-- (both v1 and v2) projects respectively. +findCabalHelperEntryPoint :: FilePath -> IO (Maybe (Ex ProjLoc)) +findCabalHelperEntryPoint fp = do + allProjs <- concat <$> mapM findProjects (ancestors (takeDirectory fp)) + debugm $ "Cabal-Helper found these projects: " ++ show (map (\(Ex x) -> show x) allProjs) + -- We only want to return projects that we have the build tools installed for + isStackInstalled <- isJust <$> findExecutable "stack" + isCabalInstalled <- isJust <$> findExecutable "cabal" + let supportedProjs = filter (\x -> supported x isStackInstalled isCabalInstalled) allProjs + debugm $ "These projects have the build tools installed: " ++ show (map (\(Ex x) -> show x) supportedProjs) + case filter (\p -> isCabalV2FileProject p || isStackProject p) supportedProjs of + (x : _) -> return $ Just x + [] -> case filter isCabalProject supportedProjs of + (x : _) -> return $ Just x + [] -> return Nothing + where + supported :: Ex ProjLoc -> Bool -> Bool -> Bool + supported (Ex ProjLocStackYaml {}) stackInstalled _ = stackInstalled + supported (Ex ProjLocV2Dir {}) _ cabalInstalled = cabalInstalled + supported (Ex ProjLocV2File {}) _ cabalInstalled = cabalInstalled + supported (Ex ProjLocV1Dir {}) _ cabalInstalled = cabalInstalled + supported (Ex ProjLocV1CabalFile {}) _ cabalInstalled = cabalInstalled + +isStackProject :: Ex ProjLoc -> Bool +isStackProject (Ex ProjLocStackYaml {}) = True +isStackProject _ = False + +isCabalV2FileProject :: Ex ProjLoc -> Bool +isCabalV2FileProject (Ex ProjLocV2File {}) = True +isCabalV2FileProject _ = False + +isCabalProject :: Ex ProjLoc -> Bool +isCabalProject (Ex ProjLocV1CabalFile {}) = True +isCabalProject (Ex ProjLocV1Dir {}) = True +isCabalProject (Ex ProjLocV2File {}) = True +isCabalProject (Ex ProjLocV2Dir {}) = True +isCabalProject _ = False + +-- | Given a FilePath, find the cradle the FilePath belongs to. +-- +-- Finds the Cabal Package the FilePath is most likely a part of +-- and creates a cradle whose root directory is the directory +-- of the package the File belongs to. +-- +-- It is not required that the FilePath given actually exists. If it does not +-- exist or is not part of any of the packages in the project, a "None"-cradle is +-- produced. +-- See <https://github.com/mpickering/hie-bios> for what a "None"-cradle is. +-- The "None"-cradle can still be used to query for basic information, such as +-- the GHC version used to build the project. However, it can not be used to +-- load any of the files in the project. +-- +-- == General Approach +-- +-- Given a FilePath that we want to load, we need to create a cradle +-- that can compile and load the given FilePath. +-- In Cabal-Helper, there is no notion of a cradle, but a project +-- consists of multiple packages that contain multiple units. +-- Each unit may consist of multiple components. +-- A unit is the smallest part of code that Cabal (the library) can compile. +-- Examples are executables, libraries, tests or benchmarks are all units. +-- Each of this units has a name that is unique within a build-plan, +-- such as "exe:hie" which represents the executable of the Haskell IDE Engine. +-- +-- In principle, a unit is what hie-bios considers to be a cradle. +-- However, to find out to which unit a FilePath belongs, we have to initialise +-- the unit, e.g. configure its dependencies and so on. When discovering a cradle +-- we do not want to pay for this upfront, but rather when we actually want to +-- load a Module in the project. Therefore, we only identify the package the +-- FilePath is part of and decide which unit to load when 'runCradle' is executed. +-- +-- Thus, to find the options required to compile and load the given FilePath, +-- we have to do the following: +-- +-- 1. Find the project type of the project. +-- Happens in 'cabalHelperCradle' +-- 2. Identify the package that contains the FilePath (should be unique). +-- Happens in 'cabalHelperAction' +-- 3. Find the unit that that contains the FilePath (May be non-unique). +-- Happens in 'cabalHelperAction' +-- 4. Find the component that exposes the FilePath (May be non-unique). +-- Happens in 'cabalHelperAction' +-- +-- === Find the project type of the project. +-- +-- The function 'cabalHelperCradle' does the first step only. +-- It starts by querying Cabal-Helper to find the project's root. +-- See 'findCabalHelperEntryPoint' for details how this is done. +-- +-- === Identify the package that contains the FilePath +-- +-- Once the root of the project is defined, we query Cabal-Helper for all packages +-- that are defined in the project and match by the packages source directory +-- which package the given FilePath is most likely to be a part of. +-- E.g. if the source directory of the package is the most concrete +-- prefix of the FilePath, the FilePath is in that package. +-- After the package is identified, we create a cradle where cradle's root +-- directory is set to the package's source directory. This is necessary, +-- because compiler options obtained from a component, are relative +-- to the source directory of the package the component is part of. +-- +-- === Find the unit that that contains the FilePath +-- +-- In 'cabalHelperAction' we want to load a given FilePath, already knowing +-- which package the FilePath is part of. Now we obtain all Units that are part +-- of the package and match by the source directories (plural is intentional), +-- to which unit the given FilePath most likely belongs to. If no unit can be +-- obtained, e.g. for every unit, no source directory is a prefix of the FilePath, +-- we return an error code, since this is not allowed to happen. +-- If there are multiple matches, which is possible, we check whether any of the +-- components defined in the unit exposes or defines the given FilePath as a module. +-- +-- === Find the component that exposes the FilePath +-- +-- A component defines the options that are necessary to compile a FilePath that +-- is in the component. It also defines which modules are in the component. +-- Therefore, we translate the given FilePath into a module name, relative to +-- the unit's source directory, and check if the module name is exposed by the +-- component. There is a special case, executables define a FilePath, for the +-- file that contains the 'main'-function, that is relative to the unit's source +-- directory. +-- +-- After the component has been identified, we can actually retrieve the options +-- required to load and compile the given file. +-- +-- == Examples +-- +-- === Mono-Repo +-- +-- Assume the project structure: +-- +-- @ +-- / +-- └── Mono/ +-- ├── cabal.project +-- ├── stack.yaml +-- ├── A/ +-- │ ├── A.cabal +-- │ └── Lib.hs +-- └── B/ +-- ├── B.cabal +-- └── Exe.hs +-- @ +-- +-- Currently, Haskell IDE Engine needs to know on startup which GHC version is +-- needed to compile the project. This information is needed to show warnings to +-- the user if the GHC version on the project does not agree with the GHC version +-- that was used to compile Haskell IDE Engine. +-- +-- Therefore, the function 'findLocalCradle' is invoked with a dummy FilePath, +-- such as "\/Mono\/Lib.hs". Since there will be no package that contains this +-- dummy FilePath, the result will be a None-cradle. +-- +-- Either +-- +-- >>> findLocalCradle "/Mono/Lib.hs" +-- Cradle { cradleRootDir = "/Mono/", CradleAction { actionName = "Cabal-Helper-Stack-None", ..} } +-- +-- or +-- +-- >>> findLocalCradle "/Mono/Lib.hs" +-- Cradle { cradleRootDir = "/Mono/", CradleAction { actionName = "Cabal-Helper-Cabal-V2-None", ..} } +-- +-- The cradle result of this invocation is only used to obtain the GHC version, +-- which is safe, since it only checks if the cradle is a 'stack' project or +-- a 'cabal' project. +-- +-- +-- If we are trying to load the executable: +-- +-- >>> findLocalCradle "/Mono/B/Exe.hs" +-- Cradle { cradleRootDir = "/Mono/", CradleAction { actionName = "Cabal-Helper-Cabal-V2", ..} } +-- +-- we will detect correctly the compiler options, by first finding the appropriate +-- package, followed by traversing the units in the package and finding the +-- component that exposes the executable by FilePath. +-- +-- === No explicit executable folder +-- +-- Assume the project structure: +-- +-- @ +-- / +-- └── Library/ +-- ├── cabal.project +-- ├── stack.yaml +-- ├── Library.cabal +-- └── src +-- ├── Lib.hs +-- └── Exe.hs +-- @ +-- +-- There are different dependencies for the library "Lib.hs" and the +-- executable "Exe.hs". If we are trying to load the executable "src\/Exe.hs" +-- we will correctly identify the executable unit, and correctly initialise +-- dependencies of "exe:Library". +-- It will be correct even if we load the unit "lib:Library" before +-- the "exe:Library" because the unit "lib:Library" does not expose +-- a module @"Exe"@. +-- +-- === Sub package +-- +-- Assume the project structure: +-- +-- @ +-- / +-- └── Repo/ +-- ├── cabal.project +-- ├── stack.yaml +-- ├── Library.cabal +-- ├── src +-- | └── Lib.hs +-- └── SubRepo +-- ├── SubRepo.cabal +-- └── Lib2.hs +-- @ +-- +-- When we try to load "\/Repo\/SubRepo\/Lib2.hs", we need to identify root +-- of the project, which is "\/Repo\/" but set the root directory of the cradle +-- responsible to load "\/Repo\/SubRepo\/Lib2.hs" to "\/Repo\/SubRepo", since +-- the compiler options obtained from Cabal-Helper are relative to the package +-- source directory, which is "\/Repo\/SubRepo". +cabalHelperCradle :: FilePath -> IO (Cradle CabalHelper) +cabalHelperCradle file = do + projM <- findCabalHelperEntryPoint file + case projM of + Nothing -> do + errorm $ "Could not find a Project for file: " ++ file + cwd <- getCurrentDirectory + return + Cradle + { cradleRootDir = cwd, + cradleOptsProg = + CradleAction + { actionName = Bios.Direct, + runCradle = \_ _ -> + return $ + CradleSuccess + ComponentOptions + { componentOptions = [file, fixImportDirs cwd "-i."], + componentRoot = cwd, + componentDependencies = [] + } + } + } + Just (Ex proj) -> do + logm $ "Cabal-Helper decided to use: " ++ show proj + -- Find the root of the project based on project type. + let root = projectRootDir proj + -- Create a suffix for the cradle name. + -- Purpose is mainly for easier debugging. + let actionNameSuffix = projectType proj + env <- initialiseEnvironment proj + return + Cradle + { cradleRootDir = root, + cradleOptsProg = + CradleAction + { actionName = Bios.Other actionNameSuffix, + runCradle = \_ fp -> + cabalHelperAction + (Ex proj) + env + fp + } + } + +-- | Cradle Action to query for the ComponentOptions that are needed +-- to load the given FilePath. +-- This Function is not supposed to throw any exceptions and use +-- 'CradleLoadResult' to indicate errors. +cabalHelperAction :: + -- | Project location, can be used + -- to present build-tool + -- agnostic error messages. + Ex ProjLoc -> + -- | Query Env created by 'mkQueryEnv' + -- with the appropriate 'distdir' + QueryEnv v -> + -- | FilePath to load, expected to be an absolute path. + FilePath -> + IO (CradleLoadResult ComponentOptions) +cabalHelperAction proj env fp = do + -- This builds all packages in the project. + packages <- runQuery projectPackages env + -- Find the package the given file may belong to. + -- If it does not belong to any package, fail the loading process + case packages `findPackageFor` fp of + Nothing -> do + debugm $ "Failed to find a package for: " ++ fp + return $ CradleFail $ + CradleError + (ExitFailure 1) + [ "Failed to find a package for: " ++ fp, + "No Prefix matched.", + "Following packages were searched: " + ++ intercalate + "; " + ( map + (\p -> pPackageName p ++ "(" ++ pSourceDir p ++ ")") + $ NonEmpty.toList packages + ) + ] + Just package -> do + debugm $ "Cabal-Helper cradle package: " ++ show package + -- Field `pSourceDir` often has the form `<cwd>/./plugin` + -- but we only want `<cwd>/plugin` + packageRoot <- canonicalizePath $ pSourceDir package + debugm $ + "Cabal-Helper normalisedPackageLocation: " + ++ packageRoot + -- Get all unit infos the given FilePath may belong to + let units = pUnits package + -- make the FilePath to load relative to the root of the cradle. + let relativeFp = makeRelative packageRoot fp + debugm $ "Relative Module FilePath: " ++ relativeFp + getComponent proj env (toList units) relativeFp + >>= \case + Right comp -> do + let fs = getFlags comp + let targets = getTargets comp relativeFp + let ghcOptions = removeRTS (fs ++ targets) + debugm $ "Flags for \"" ++ fp ++ "\": " ++ show ghcOptions + debugm $ "Component Infos: " ++ show comp + return $ + CradleSuccess + ComponentOptions + { componentOptions = ghcOptions, + componentRoot = packageRoot, + componentDependencies = [] + } + Left err -> + return + $ CradleFail + $ CradleError + (ExitFailure 2) + err + where + removeRTS :: [String] -> [String] + removeRTS ("+RTS" : xs) = + case dropWhile (/= "-RTS") xs of + [] -> [] + (_ : ys) -> removeRTS ys + removeRTS (y : ys) = y : removeRTS ys + removeRTS [] = [] + +-- | Fix occurrences of "-i." to "-i<cradle-root-dir>" +-- Flags obtained from cabal-helper are relative to the package +-- source directory. This is less resilient to using absolute paths, +-- thus, we fix it here. +fixImportDirs :: FilePath -> String -> String +fixImportDirs base_dir arg = + if "-i" `isPrefixOf` arg + then + let dir = drop 2 arg + in -- the flag "-i" has special meaning. + if not (null dir) && isRelative dir + then ("-i" ++ base_dir </> dir) + else arg + else arg + +-- | Get the component the given FilePath most likely belongs to. +-- Lazily ask units whether the given FilePath is part of one of their +-- component's. +-- If a Module belongs to multiple components, it is not specified which +-- component will be loaded. +-- The given FilePath must be relative to the Root of the project +-- the given units belong to. +getComponent :: + forall pt. Ex ProjLoc -> QueryEnv pt -> [Unit pt] -> FilePath -> IO (Either [String] ChComponentInfo) +getComponent proj env unitCandidates fp = getComponent' [] [] unitCandidates + >>= \case + (tried, failed, Nothing) -> return (Left $ buildErrorMsg tried failed) + (_, _, Just comp) -> return (Right comp) + where + getComponent' :: [UnitInfo] -> [(Unit pt, IOException)] -> [Unit pt] -> IO ([UnitInfo], [(Unit pt, IOException)], Maybe ChComponentInfo) + getComponent' triedUnits failedUnits [] = return (triedUnits, failedUnits, Nothing) + getComponent' triedUnits failedUnits (unit : units) = + try (runQuery (unitInfo unit) env) >>= \case + Left (e :: IOException) -> do + warningm $ "Catching and swallowing an IOException: " ++ show e + warningm $ + "The Exception was thrown in the context of finding" + ++ " a component for \"" + ++ fp + ++ "\" in the unit: " + ++ show unit + getComponent' triedUnits ((unit, e) : failedUnits) units + Right ui -> do + let components = Map.elems (uiComponents ui) + debugm $ "Unit Info: " ++ show ui + case find (fp `partOfComponent`) components of + Nothing -> getComponent' (ui : triedUnits) failedUnits units + comp -> return (triedUnits, failedUnits, comp) + buildErrorMsg :: [UnitInfo] -> [(Unit pt, IOException)] -> [String] + buildErrorMsg triedUnits failedUnits = + concat + [ [ "Could not obtain flags for: \"" ++ fp ++ "\".", + "" + ], + concat + [ concat + [ [ "This module was not part of any component we are aware of.", + "" + ], + concatMap ppShowUnitInfo triedUnits, + [ "", + "" + ], + if isStackProject proj + then stackSpecificInstructions + else cabalSpecificInstructions + ] + | not (null triedUnits) + ], + concat + [ [ "We could not build all components.", + "If one of these components exposes this Module, make sure they compile.", + "You can try to invoke the commands yourself.", + "The following commands failed:" + ] + ++ concatMap (ppShowIOException . snd) failedUnits + | not (null failedUnits) + ] + ] + stackSpecificInstructions :: [String] + stackSpecificInstructions = + [ "To expose a module, refer to:", + "https://docs.haskellstack.org/en/stable/GUIDE/", + "If you are using `package.yaml` then you don't have to manually expose modules.", + "Maybe you didn't set the source directories for your project correctly." + ] + cabalSpecificInstructions :: [String] + cabalSpecificInstructions = + [ "To expose a module, refer to:", + "https://www.haskell.org/cabal/users-guide/developing-packages.html", + "" + ] + ppShowUnitInfo :: UnitInfo -> [String] + ppShowUnitInfo u = + u + & uiComponents + & Map.toList + & map + ( \(name, info) -> + "Component: " ++ show name ++ " with source directory: " ++ show (ciSourceDirs info) + ) + ppShowIOException :: IOException -> [String] + ppShowIOException e = + [ "", + show e + ] + +-- ---------------------------------------------------------------------------- + +loadPackages :: QueryEnv pt -> IO [Package pt] +loadPackages = fmap toList . runQuery projectPackages + +-- | Eagerly load all given units. +loadAllUnits :: QueryEnv pt -> [Unit pt] -> IO ([UnitInfo], [(Unit pt, IOException)]) +loadAllUnits env units = + loadAllUnits' env [] [] units + +loadAllUnits' :: QueryEnv pt -> [UnitInfo] -> [(Unit pt, IOException)] -> [Unit pt] -> IO ([UnitInfo], [(Unit pt, IOException)]) +loadAllUnits' _ triedUnits failedUnits [] = return (triedUnits, failedUnits) +loadAllUnits' env triedUnits failedUnits (unit : units) = + try (runQuery (unitInfo unit) env) >>= \case + Left (e :: IOException) -> do + warningm $ "Catching and swallowing an IOException: " ++ show e + loadAllUnits' env triedUnits ((unit, e) : failedUnits) units + Right ui -> do + debugm $ "Unit Info: " ++ show ui + loadAllUnits' env (ui : triedUnits) failedUnits units + +initialiseEnvironment :: ProjLoc pt -> IO (QueryEnv pt) +initialiseEnvironment proj = do + -- Create a suffix for the cradle name. + -- Purpose is mainly for easier debugging. + let dist_dir = getDefaultDistDir proj + mkQueryEnv proj dist_dir + +toHieYaml :: QueryEnv pt -> [Package pt] -> IO [CP.Package] +toHieYaml env packages = forM packages $ \package -> do + let units = NonEmpty.toList $ pUnits package + (unitInfos, _failed) <- loadAllUnits env units + let yamlComponents :: [(FilePath, CP.CompType, CP.Name)] + yamlComponents = concatMap unitToComp unitInfos + finalisePackage (pPackageName package) (pSourceDir package) yamlComponents + where + finalisePackage :: String -> FilePath -> [(FilePath, CP.CompType, CP.Name)] -> IO CP.Package + finalisePackage pName pRoot yamlUnits = do + pRoot' <- makeRelativeToCurrentDirectory pRoot + let yamlUnits' = map (\(fp, compType, name) -> CP.Comp compType name (T.pack $ addTrailingPathSeparator $ normalise $ pRoot' </> fp)) yamlUnits + pure $ CP.Package (T.pack pName ) yamlUnits' + + toCompType :: ChComponentInfo -> [(FilePath, CP.CompType, CP.Name)] + toCompType chComp = + let comps = case ciComponentName chComp of + ChLibName ChMainLibName -> [(CP.Lib, T.pack "")] + ChLibName (ChSubLibName name) -> [(CP.Lib, T.pack name)] + ChFLibName name -> [(CP.FLib, T.pack name)] + ChExeName name -> [(CP.Exe, T.pack name)] + ChTestName name -> [(CP.Test, T.pack name)] + ChBenchName name -> [(CP.Bench, T.pack name)] + ChSetupHsName -> [] -- TODO: this could be a none cradle + in (\path (compType, name) -> (path, compType, name)) <$> ciSourceDirs chComp <*> comps + + unitToComp :: UnitInfo -> [(FilePath, CP.CompType, CP.Name)] + unitToComp info = concatMap toCompType (Map.elems $ uiComponents info) + +-- ---------------------------------------------------------------------------- + +-- | Check whether the given FilePath is part of the Component. +-- A FilePath is part of the Component if and only if: +-- +-- * One Component's 'ciSourceDirs' is a prefix of the FilePath +-- * The FilePath, after converted to a module name, +-- is a in the Component's Targets, or the FilePath is +-- the executable in the component. +-- +-- The latter is achieved by making the FilePath relative to the 'ciSourceDirs' +-- and then replacing Path separators with ".". +-- To check whether the given FilePath is the executable of the Component, +-- we have to check whether the FilePath, including 'ciSourceDirs', +-- is part of the targets in the Component. +partOfComponent :: + -- | FilePath relative to the package root. + FilePath -> + -- | Component to check whether the given FilePath is part of it. + ChComponentInfo -> + Bool +partOfComponent fp' comp = + inTargets (ciSourceDirs comp) fp' (getTargets comp fp') + where + -- Check if the FilePath is in an executable or setup's main-is field + inMainIs :: FilePath -> Bool + inMainIs fp + | ChExeEntrypoint mainIs _ <- ciEntrypoints comp = mainIs == fp + | ChSetupEntrypoint mainIs <- ciEntrypoints comp = mainIs == fp + | otherwise = False + inTargets :: [FilePath] -> FilePath -> [String] -> Bool + inTargets sourceDirs fp targets = + let candidates = relativeTo fp sourceDirs + in any (existsInTargets targets fp) candidates + existsInTargets :: [String] -> FilePath -> FilePath -> Bool + existsInTargets targets absFp relFp = + or + [ any (`elem` targets) [getModuleName relFp, absFp], + inMainIs relFp + ] + getModuleName :: FilePath -> String + getModuleName fp = + map + ( \c -> + if isPathSeparator c + then '.' + else c + ) + (dropExtension fp) + +-- | Get the flags necessary to compile the given component. +getFlags :: ChComponentInfo -> [String] +getFlags = ciGhcOptions + +-- | Get all Targets of a Component, since we want to load all components. +-- FilePath is needed for the special case that the Component is an Exe. +-- The Exe contains a Path to the Main which is relative to some entry +-- in 'ciSourceDirs'. +-- We monkey-patch this by supplying the FilePath we want to load, +-- which is part of this component, and select the 'ciSourceDir' we actually want. +-- See the Documentation of 'ciSourceDir' to why this contains multiple entries. +getTargets :: ChComponentInfo -> FilePath -> [String] +getTargets comp fp = case ciEntrypoints comp of + ChSetupEntrypoint {} -> [] + ChLibEntrypoint {chExposedModules, chOtherModules} -> + map unChModuleName (chExposedModules ++ chOtherModules) + ChExeEntrypoint {chMainIs, chOtherModules} -> + [sourceDir </> chMainIs | Just sourceDir <- [sourceDirs]] + ++ map unChModuleName chOtherModules + where + sourceDirs = find (`isFilePathPrefixOf` fp) (ciSourceDirs comp) + +-- | For all packages in a project, find the project the given FilePath +-- belongs to most likely. +findPackageFor :: NonEmpty (Package pt) -> FilePath -> Maybe (Package pt) +findPackageFor packages fp = + packages + & NonEmpty.toList + & sortOn (Down . pSourceDir) + & filter (\p -> pSourceDir p `isFilePathPrefixOf` fp) + & listToMaybe + +projectRootDir :: ProjLoc qt -> FilePath +projectRootDir ProjLocV1CabalFile {plProjectDirV1} = plProjectDirV1 +projectRootDir ProjLocV1Dir {plProjectDirV1} = plProjectDirV1 +projectRootDir ProjLocV2File {plProjectDirV2} = plProjectDirV2 +projectRootDir ProjLocV2Dir {plProjectDirV2} = plProjectDirV2 +projectRootDir ProjLocStackYaml {plStackYaml} = takeDirectory plStackYaml + +projectType :: ProjLoc qt -> CabalHelper +projectType ProjLocV1CabalFile {} = CabalV2 +projectType ProjLocV1Dir {} = CabalV2 +projectType ProjLocV2File {} = CabalV2 +projectType ProjLocV2Dir {} = CabalV2 +projectType ProjLocStackYaml {} = Stack + +-- ---------------------------------------------------------------------------- +-- +-- Utility functions to manipulate FilePath's +-- +-- ---------------------------------------------------------------------------- + +-- | Helper function to make sure that both FilePaths are normalised. +-- Checks whether the first FilePath is a Prefix of the second FilePath. +-- Intended usage: +-- +-- >>> isFilePathPrefixOf "./src/" "./src/File.hs" +-- True +-- +-- >>> isFilePathPrefixOf "./src" "./src/File.hs" +-- True +-- +-- >>> isFilePathPrefixOf "./src/././" "./src/File.hs" +-- True +-- +-- >>> isFilePathPrefixOf "./src" "./src-dir/File.hs" +-- False +isFilePathPrefixOf :: FilePath -> FilePath -> Bool +isFilePathPrefixOf dir fp = isJust $ stripFilePath dir fp + +-- | Strip the given directory from the filepath if and only if +-- the given directory is a prefix of the filepath. +-- +-- >>> stripFilePath "app" "app/File.hs" +-- Just "File.hs" +-- +-- >>> stripFilePath "src" "app/File.hs" +-- Nothing +-- +-- >>> stripFilePath "src" "src-dir/File.hs" +-- Nothing +-- +-- >>> stripFilePath "." "src/File.hs" +-- Just "src/File.hs" +-- +-- >>> stripFilePath "app/" "./app/Lib/File.hs" +-- Just "Lib/File.hs" +-- +-- >>> stripFilePath "/app/" "./app/Lib/File.hs" +-- Nothing -- Nothing since '/app/' is absolute +-- +-- >>> stripFilePath "/app" "/app/Lib/File.hs" +-- Just "Lib/File.hs" +stripFilePath :: FilePath -> FilePath -> Maybe FilePath +stripFilePath "." fp + | isRelative fp = Just fp + | otherwise = Nothing +stripFilePath dir' fp' + | Just relativeFpParts <- splitDir `stripPrefix` splitFp = Just (joinPath relativeFpParts) + | otherwise = Nothing + where + dir = normalise dir' + fp = normalise fp' + splitFp = splitPath fp + splitDir = splitPath dir + stripPrefix (x : xs) (y : ys) + | x `equalFilePath` y = stripPrefix xs ys + | otherwise = Nothing + stripPrefix [] ys = Just ys + stripPrefix _ [] = Nothing + +-- | Obtain all ancestors from a given directory. +-- +-- >>> ancestors "a/b/c/d/e" +-- [ "a/b/c/d/e", "a/b/c/d", "a/b/c", "a/b", "a", "." ] +-- +-- >>> ancestors "/a/b/c/d/e" +-- [ "/a/b/c/d/e", "/a/b/c/d", "/a/b/c", "/a/b", "/a", "/" ] +-- +-- >>> ancestors "/a/b.hs" +-- [ "/a/b.hs", "/a", "/" ] +-- +-- >>> ancestors "a/b.hs" +-- [ "a/b.hs", "a", "." ] +-- +-- >>> ancestors "a/b/" +-- [ "a/b" ] +ancestors :: FilePath -> [FilePath] +ancestors dir + | subdir `equalFilePath` dir = [dir] + | otherwise = dir : ancestors subdir + where + subdir = takeDirectory dir + +-- | Assuming a FilePath @"src\/Lib\/Lib.hs"@ and a list of directories +-- such as @["src", "app"]@, returns the given FilePath +-- with a matching directory stripped away. +-- If there are multiple matches, e.g. multiple directories are a prefix +-- of the given FilePath we return all matches. +-- Returns an empty list if no prefix matches the given FilePath. +-- +-- >>> relativeTo "src/Lib/Lib.hs" ["src"] +-- ["Lib/Lib.hs"] +-- +-- >>> relativeTo "src/Lib/Lib.hs" ["app"] +-- [] +-- +-- >>> relativeTo "src/Lib/Lib.hs" ["src", "src/Lib"] +-- ["Lib/Lib.hs", "Lib.hs"] +relativeTo :: FilePath -> [FilePath] -> [FilePath] +relativeTo file sourceDirs = + mapMaybe (`stripFilePath` file) sourceDirs \ No newline at end of file diff --git a/src/Hie/Logger.hs b/src/Hie/Logger.hs new file mode 100644 index 0000000..1ccd970 --- /dev/null +++ b/src/Hie/Logger.hs @@ -0,0 +1,30 @@ +module Hie.Logger where + +import Control.Monad.IO.Class +import qualified Data.Text as T +import System.Log.Logger + +-- --------------------------------------------------------------------- + +setupLogger :: MonadIO m => Priority -> m () +setupLogger p = liftIO $ updateGlobalLogger loggerName + (setLevel p) + +loggerName :: String +loggerName = "gen-hie" + +-- --------------------------------------------------------------------- + +logm :: MonadIO m => String -> m () +logm s = liftIO $ infoM loggerName s + +debugm :: MonadIO m => String -> m () +debugm s = liftIO $ debugM loggerName s + +warningm :: MonadIO m => String -> m () +warningm s = liftIO $ warningM loggerName s + +errorm :: MonadIO m => String -> m () +errorm s = liftIO $ errorM loggerName s + +-- --------------------------------------------------------------------- diff --git a/test/Spec.hs b/test/Spec.hs index ed378ee..6323182 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -49,7 +49,9 @@ spec = do "implicit-hie" [ Comp Lib "" "src", Comp Exe "gen-hie" "app/Main.hs", - Comp Test "implicit-hie-test" "test" + Comp Test "implicit-hie-test" "test", + Comp Test "unit-tests" "test/unit", + Comp Test "unit-tests" "test/utils" ] describe "Should Succeed" $ it diff --git a/test/stackHie.yaml b/test/stackHie.yaml index f0ed5ca..8d03553 100644 --- a/test/stackHie.yaml +++ b/test/stackHie.yaml @@ -8,3 +8,9 @@ cradle: - path: "./test" component: "implicit-hie:test:implicit-hie-test" + + - path: "./test/unit" + component: "implicit-hie:test:unit-tests" + + - path: "./test/utils" + component: "implicit-hie:test:unit-tests" diff --git a/test/testdata/cabal-helper/implicit-exe/Setup.hs b/test/testdata/cabal-helper/implicit-exe/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/test/testdata/cabal-helper/implicit-exe/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/test/testdata/cabal-helper/implicit-exe/cabal.project b/test/testdata/cabal-helper/implicit-exe/cabal.project new file mode 100644 index 0000000..bfe6289 --- /dev/null +++ b/test/testdata/cabal-helper/implicit-exe/cabal.project @@ -0,0 +1 @@ +packages: ./ \ No newline at end of file diff --git a/test/testdata/cabal-helper/implicit-exe/implicit-exe.cabal b/test/testdata/cabal-helper/implicit-exe/implicit-exe.cabal new file mode 100644 index 0000000..3aca1b4 --- /dev/null +++ b/test/testdata/cabal-helper/implicit-exe/implicit-exe.cabal @@ -0,0 +1,17 @@ +cabal-version: >=1.10 +name: implicit-exe +version: 0.1.0.0 +license-file: LICENSE +build-type: Simple + +library + exposed-modules: Lib + hs-source-dirs: src + build-depends: base + default-language: Haskell2010 + + +executable implicit-exe + main-is: src/Exe.hs + build-depends: base, implicit-exe + default-language: Haskell2010 \ No newline at end of file diff --git a/test/testdata/cabal-helper/implicit-exe/src/Exe.hs b/test/testdata/cabal-helper/implicit-exe/src/Exe.hs new file mode 100644 index 0000000..ed41929 --- /dev/null +++ b/test/testdata/cabal-helper/implicit-exe/src/Exe.hs @@ -0,0 +1,4 @@ + +import Lib (someFunc) + +main = someFunc \ No newline at end of file diff --git a/test/testdata/cabal-helper/implicit-exe/src/Lib.hs b/test/testdata/cabal-helper/implicit-exe/src/Lib.hs new file mode 100644 index 0000000..f51af83 --- /dev/null +++ b/test/testdata/cabal-helper/implicit-exe/src/Lib.hs @@ -0,0 +1,4 @@ +module Lib (someFunc) where + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/test/testdata/cabal-helper/mono-repo/A/A.cabal b/test/testdata/cabal-helper/mono-repo/A/A.cabal new file mode 100644 index 0000000..e70b43f --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/A/A.cabal @@ -0,0 +1,15 @@ +cabal-version: >=1.10 +name: A +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: MyLib + build-depends: base + default-language: Haskell2010 + +executable A + main-is: Main.hs + other-modules: MyLib + build-depends: base, A + default-language: Haskell2010 diff --git a/test/testdata/cabal-helper/mono-repo/A/Main.hs b/test/testdata/cabal-helper/mono-repo/A/Main.hs new file mode 100644 index 0000000..60d904e --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/A/Main.hs @@ -0,0 +1,8 @@ +module Main where + +import qualified MyLib (someFunc) + +main :: IO () +main = do + putStrLn "Hello, Haskell!" + MyLib.someFunc diff --git a/test/testdata/cabal-helper/mono-repo/A/MyLib.hs b/test/testdata/cabal-helper/mono-repo/A/MyLib.hs new file mode 100644 index 0000000..e657c44 --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/A/MyLib.hs @@ -0,0 +1,4 @@ +module MyLib (someFunc) where + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/test/testdata/cabal-helper/mono-repo/A/Setup.hs b/test/testdata/cabal-helper/mono-repo/A/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/A/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/test/testdata/cabal-helper/mono-repo/B/B.cabal b/test/testdata/cabal-helper/mono-repo/B/B.cabal new file mode 100644 index 0000000..4093e1d --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/B/B.cabal @@ -0,0 +1,15 @@ +cabal-version: >=1.10 +name: B +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: MyLib + build-depends: base + default-language: Haskell2010 + +executable B + main-is: Main.hs + other-modules: MyLib + build-depends: base, B + default-language: Haskell2010 diff --git a/test/testdata/cabal-helper/mono-repo/B/Main.hs b/test/testdata/cabal-helper/mono-repo/B/Main.hs new file mode 100644 index 0000000..60d904e --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/B/Main.hs @@ -0,0 +1,8 @@ +module Main where + +import qualified MyLib (someFunc) + +main :: IO () +main = do + putStrLn "Hello, Haskell!" + MyLib.someFunc diff --git a/test/testdata/cabal-helper/mono-repo/B/MyLib.hs b/test/testdata/cabal-helper/mono-repo/B/MyLib.hs new file mode 100644 index 0000000..e657c44 --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/B/MyLib.hs @@ -0,0 +1,4 @@ +module MyLib (someFunc) where + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/test/testdata/cabal-helper/mono-repo/B/Setup.hs b/test/testdata/cabal-helper/mono-repo/B/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/B/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/test/testdata/cabal-helper/mono-repo/C/C.cabal b/test/testdata/cabal-helper/mono-repo/C/C.cabal new file mode 100644 index 0000000..db5e380 --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/C/C.cabal @@ -0,0 +1,9 @@ +cabal-version: >=1.10 +name: C +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: MyLib + build-depends: base + default-language: Haskell2010 diff --git a/test/testdata/cabal-helper/mono-repo/C/MyLib.hs b/test/testdata/cabal-helper/mono-repo/C/MyLib.hs new file mode 100644 index 0000000..e657c44 --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/C/MyLib.hs @@ -0,0 +1,4 @@ +module MyLib (someFunc) where + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/test/testdata/cabal-helper/mono-repo/C/Setup.hs b/test/testdata/cabal-helper/mono-repo/C/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/C/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/test/testdata/cabal-helper/mono-repo/cabal.project b/test/testdata/cabal-helper/mono-repo/cabal.project new file mode 100644 index 0000000..cf2eab3 --- /dev/null +++ b/test/testdata/cabal-helper/mono-repo/cabal.project @@ -0,0 +1,4 @@ +packages: + ./A/ + ./B/ + ./C/ \ No newline at end of file diff --git a/test/testdata/cabal-helper/multi-source-dirs/Setup.hs b/test/testdata/cabal-helper/multi-source-dirs/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/test/testdata/cabal-helper/multi-source-dirs/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/test/testdata/cabal-helper/multi-source-dirs/multi-source-dirs.cabal b/test/testdata/cabal-helper/multi-source-dirs/multi-source-dirs.cabal new file mode 100644 index 0000000..5856868 --- /dev/null +++ b/test/testdata/cabal-helper/multi-source-dirs/multi-source-dirs.cabal @@ -0,0 +1,11 @@ +cabal-version: >=1.10 +name: multi-source-dirs +version: 0.1.0.0 +license-file: LICENSE +build-type: Simple + +library + exposed-modules: Lib, BetterLib + hs-source-dirs: src, src/input + build-depends: base + default-language: Haskell2010 diff --git a/test/testdata/cabal-helper/multi-source-dirs/src/BetterLib.hs b/test/testdata/cabal-helper/multi-source-dirs/src/BetterLib.hs new file mode 100644 index 0000000..0784c76 --- /dev/null +++ b/test/testdata/cabal-helper/multi-source-dirs/src/BetterLib.hs @@ -0,0 +1,5 @@ +module BetterLib where + + +foo = 3 +bar = "String" \ No newline at end of file diff --git a/test/testdata/cabal-helper/multi-source-dirs/src/input/Lib.hs b/test/testdata/cabal-helper/multi-source-dirs/src/input/Lib.hs new file mode 100644 index 0000000..6c37234 --- /dev/null +++ b/test/testdata/cabal-helper/multi-source-dirs/src/input/Lib.hs @@ -0,0 +1,6 @@ +module Lib where + +foobar = 15 + +fizbuzz :: Int -> String +fizbuzz n = "Fizz" \ No newline at end of file diff --git a/test/testdata/cabal-helper/simple-cabal/MyLib.hs b/test/testdata/cabal-helper/simple-cabal/MyLib.hs new file mode 100644 index 0000000..e657c44 --- /dev/null +++ b/test/testdata/cabal-helper/simple-cabal/MyLib.hs @@ -0,0 +1,4 @@ +module MyLib (someFunc) where + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/test/testdata/cabal-helper/simple-cabal/Setup.hs b/test/testdata/cabal-helper/simple-cabal/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/test/testdata/cabal-helper/simple-cabal/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/test/testdata/cabal-helper/simple-cabal/simple-cabal-test.cabal b/test/testdata/cabal-helper/simple-cabal/simple-cabal-test.cabal new file mode 100644 index 0000000..3c8be5d --- /dev/null +++ b/test/testdata/cabal-helper/simple-cabal/simple-cabal-test.cabal @@ -0,0 +1,10 @@ +cabal-version: >=1.10 +name: simple-cabal-test +version: 0.1.0.0 +license-file: LICENSE +build-type: Simple + +library + exposed-modules: MyLib + build-depends: base + default-language: Haskell2010 diff --git a/test/testdata/cabal-helper/simple-stack/MyLib.hs b/test/testdata/cabal-helper/simple-stack/MyLib.hs new file mode 100644 index 0000000..e657c44 --- /dev/null +++ b/test/testdata/cabal-helper/simple-stack/MyLib.hs @@ -0,0 +1,4 @@ +module MyLib (someFunc) where + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/test/testdata/cabal-helper/simple-stack/Setup.hs b/test/testdata/cabal-helper/simple-stack/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/test/testdata/cabal-helper/simple-stack/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/test/testdata/cabal-helper/simple-stack/simple-stack-test.cabal b/test/testdata/cabal-helper/simple-stack/simple-stack-test.cabal new file mode 100644 index 0000000..264baeb --- /dev/null +++ b/test/testdata/cabal-helper/simple-stack/simple-stack-test.cabal @@ -0,0 +1,10 @@ +cabal-version: >=1.10 +name: simple-stack-test +version: 0.1.0.0 +license-file: LICENSE +build-type: Simple + +library + exposed-modules: MyLib + build-depends: base + default-language: Haskell2010 diff --git a/test/testdata/cabal-helper/sub-package/Setup.hs b/test/testdata/cabal-helper/sub-package/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/test/testdata/cabal-helper/sub-package/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/test/testdata/cabal-helper/sub-package/app/Main.hs b/test/testdata/cabal-helper/sub-package/app/Main.hs new file mode 100644 index 0000000..60d904e --- /dev/null +++ b/test/testdata/cabal-helper/sub-package/app/Main.hs @@ -0,0 +1,8 @@ +module Main where + +import qualified MyLib (someFunc) + +main :: IO () +main = do + putStrLn "Hello, Haskell!" + MyLib.someFunc diff --git a/test/testdata/cabal-helper/sub-package/plugins-api/PluginLib.hs b/test/testdata/cabal-helper/sub-package/plugins-api/PluginLib.hs new file mode 100644 index 0000000..55a7098 --- /dev/null +++ b/test/testdata/cabal-helper/sub-package/plugins-api/PluginLib.hs @@ -0,0 +1,4 @@ +module PluginLib (someFunc) where + +someFunc :: IO () +someFunc = putStrLn "someFunc" diff --git a/test/testdata/cabal-helper/sub-package/plugins-api/Setup.hs b/test/testdata/cabal-helper/sub-package/plugins-api/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/test/testdata/cabal-helper/sub-package/plugins-api/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/test/testdata/cabal-helper/sub-package/plugins-api/plugins-api.cabal b/test/testdata/cabal-helper/sub-package/plugins-api/plugins-api.cabal new file mode 100644 index 0000000..223fa73 --- /dev/null +++ b/test/testdata/cabal-helper/sub-package/plugins-api/plugins-api.cabal @@ -0,0 +1,10 @@ +cabal-version: >=1.10 +name: plugins-api +version: 0.1.0.0 +license-file: LICENSE +build-type: Simple + +library + exposed-modules: PluginLib + build-depends: base + default-language: Haskell2010 diff --git a/test/testdata/cabal-helper/sub-package/src/MyLib.hs b/test/testdata/cabal-helper/sub-package/src/MyLib.hs new file mode 100644 index 0000000..53ea5c6 --- /dev/null +++ b/test/testdata/cabal-helper/sub-package/src/MyLib.hs @@ -0,0 +1,6 @@ +module MyLib (someFunc) where + +import qualified PluginLib as L + +someFunc :: IO () +someFunc = L.someFunc diff --git a/test/testdata/cabal-helper/sub-package/sub-package.cabal b/test/testdata/cabal-helper/sub-package/sub-package.cabal new file mode 100644 index 0000000..ba36f1b --- /dev/null +++ b/test/testdata/cabal-helper/sub-package/sub-package.cabal @@ -0,0 +1,17 @@ +cabal-version: >=1.10 +name: sub-package +version: 0.1.0.0 +license-file: LICENSE +build-type: Simple + +library + exposed-modules: MyLib + build-depends: base, plugins-api + hs-source-dirs: src + default-language: Haskell2010 + +executable sub-package + main-is: Main.hs + build-depends: base, sub-package + hs-source-dirs: app + default-language: Haskell2010 diff --git a/test/unit/CabalHelperSpec.hs b/test/unit/CabalHelperSpec.hs new file mode 100644 index 0000000..f9c6d1f --- /dev/null +++ b/test/unit/CabalHelperSpec.hs @@ -0,0 +1,236 @@ +{-# LANGUAGE OverloadedStrings #-} + +module CabalHelperSpec where + +import Data.Maybe (isJust) +import HIE.Bios.Types (Cradle, CradleLoadResult (..), cradleOptsProg, runCradle) +import Hie.CabalHelper +import System.Directory (findExecutable, getCurrentDirectory, removeFile) +import System.FilePath +import Test.Hspec +import TestUtils + +rootPath :: FilePath -> FilePath +rootPath cwd = cwd </> "test" </> "testdata" </> "cabal-helper" + +implicitExePath :: FilePath -> FilePath +implicitExePath cwd = rootPath cwd </> "implicit-exe" + +monoRepoPath :: FilePath -> FilePath +monoRepoPath cwd = rootPath cwd </> "mono-repo" + +subPackagePath :: FilePath -> FilePath +subPackagePath cwd = rootPath cwd </> "sub-package" + +simpleCabalPath :: FilePath -> FilePath +simpleCabalPath cwd = rootPath cwd </> "simple-cabal" + +simpleStackPath :: FilePath -> FilePath +simpleStackPath cwd = rootPath cwd </> "simple-stack" + +multiSourceDirsPath :: FilePath -> FilePath +multiSourceDirsPath cwd = rootPath cwd </> "multi-source-dirs" + +spec :: Spec +spec = beforeAll_ setupStackFiles $ do + describe "stack and cabal executables should be accesible" $ do + it "cabal is accesible" $ do + stack <- findExecutable "cabal" + stack `shouldSatisfy` isJust + it "stack is accesible" $ do + cabal <- findExecutable "stack" + cabal `shouldSatisfy` isJust + describe "cabal-helper spec" $ do + describe "find entry point" findCabalHelperEntryPointSpec + describe "cradle discovery and loading" cabalHelperCradleSpec + +cabalHelperCradleSpec :: Spec +cabalHelperCradleSpec = do + cwd <- runIO getCurrentDirectory + describe "dummy filepath, finds none-cradle" $ do + it "implicit exe" $ do + crdl <- cabalHelperCradle (implicitExePath cwd </> "File.hs") + crdl `shouldSatisfy` isCabalCradle + it "mono repo" $ do + crdl <- cabalHelperCradle (monoRepoPath cwd </> "File.hs") + crdl `shouldSatisfy` isCabalCradle + it "stack repo" $ do + crdl <- cabalHelperCradle (simpleStackPath cwd </> "File.hs") + crdl `shouldSatisfy` isStackCradle + it "cabal repo" $ + pendingWith "Can not work because of global `cabal.project`" + -- crdl <- cabalHelperCradle (simpleCabalPath cwd </> "File.hs") + -- crdl `shouldSatisfy` isCabalCradle + it "sub package" $ do + crdl <- cabalHelperCradle (subPackagePath cwd </> "File.hs") + crdl `shouldSatisfy` isStackCradle + it "multi-source-dirs" $ do + crdl <- cabalHelperCradle (multiSourceDirsPath cwd </> "File.hs") + crdl `shouldSatisfy` isStackCradle + describe "existing projects" $ do + it "implicit exe" $ do + let fp = implicitExePath cwd </> "src" </> "Exe.hs" + componentTest fp isCabalCradle + it "mono repo" $ do + let fp = monoRepoPath cwd </> "A" </> "Main.hs" + componentTest fp isCabalCradle + it "stack repo" $ do + let fp = simpleStackPath cwd </> "MyLib.hs" + componentTest fp isStackCradle + it "cabal repo" $ + pendingWith "Can not work because of global `cabal.project`" + -- let fp = (simpleCabalPath cwd </> "MyLib.hs") + -- componentTest fp isStackCradle + it "sub package" $ do + let fp = subPackagePath cwd </> "plugins-api" </> "PluginLib.hs" + componentTest fp isStackCradle + it "multi-source-dirs, nested dir" $ do + let fp = multiSourceDirsPath cwd </> "src" </> "input" </> "Lib.hs" + componentTest fp isStackCradle + it "multi-source-dirs" $ do + let fp = multiSourceDirsPath cwd </> "src" </> "BetterLib.hs" + componentTest fp isStackCradle + +componentTest :: FilePath -> (Cradle CabalHelper -> Bool) -> Expectation +componentTest fp testCradleType = do + crdl <- cabalHelperCradle fp + crdl `shouldSatisfy` testCradleType + +-- TODO: this works but CI crashes +-- loadComponent crdl fp + +loadComponent :: Cradle CabalHelper -> FilePath -> Expectation +loadComponent crdl fp = do + result <- runCradle (cradleOptsProg crdl) (\_ -> return ()) fp + case result of + CradleFail err -> expectationFailure $ "Loading should not have failed: " ++ show err + _ -> return () + return () + +findCabalHelperEntryPointSpec :: Spec +findCabalHelperEntryPointSpec = do + cwd <- runIO getCurrentDirectory + describe "implicit exe" $ do + it "dummy filepath" $ do + let dummyFile = implicitExePath cwd </> "File.hs" + cabalTest dummyFile + it "source component" $ do + let libFile = implicitExePath cwd </> "src" </> "Lib.hs" + cabalTest libFile + it "executable component" $ do + let mainFile = implicitExePath cwd </> "src" </> "Exe.hs" + cabalTest mainFile + describe "mono repo" $ do + it "dummy filepath" $ do + let dummyFile = monoRepoPath cwd </> "File.hs" + cabalTest dummyFile + it "existing executable" $ do + let mainFile = monoRepoPath cwd </> "A" </> "Main.hs" + cabalTest mainFile + describe "sub package repo" $ do + it "dummy filepath" $ do + let dummyFile = subPackagePath cwd </> "File.hs" + stackTest dummyFile + it "existing executable" $ do + let mainFile = subPackagePath cwd </> "plugins-api" </> "PluginLib.hs" + stackTest mainFile + describe "stack repo" $ do + it "dummy filepath" $ do + let dummyFile = simpleStackPath cwd </> "File.hs" + stackTest dummyFile + it "real filepath" $ do + let dummyFile = simpleStackPath cwd </> "MyLib.hs" + stackTest dummyFile + describe "multi-source-dirs" $ do + it "dummy filepath" $ do + let dummyFile = multiSourceDirsPath cwd </> "File.hs" + stackTest dummyFile + it "real filepath" $ do + let dummyFile = multiSourceDirsPath cwd </> "src" </> "BetterLib.hs" + stackTest dummyFile + it "nested filpath" $ do + let dummyFile = multiSourceDirsPath cwd </> "src" </> "input" </> "Lib.hs" + stackTest dummyFile + describe "simple cabal repo" + $ it "Find project root with dummy filepath" + $ pendingWith "Change test-setup, we will always find `cabal.project` in root dir" + +-- ------------------------------------------------------------- + +cabalTest :: FilePath -> IO () +cabalTest fp = do + entryPoint <- findCabalHelperEntryPoint fp + let Just proj = entryPoint + isCabal = isCabalProject proj + shouldBe isCabal True + +stackTest :: FilePath -> IO () +stackTest fp = do + entryPoint <- findCabalHelperEntryPoint fp + let Just proj = entryPoint + isStack = isStackProject proj + shouldBe isStack True + +-- ------------------------------------------------------------- + +setupStackFiles :: IO () +setupStackFiles = do + resolver <- readResolver + cwd <- getCurrentDirectory + writeFile (implicitExePath cwd </> "stack.yaml") (standardStackYaml resolver) + writeFile (monoRepoPath cwd </> "stack.yaml") (monoRepoStackYaml resolver) + writeFile (subPackagePath cwd </> "stack.yaml") (subPackageStackYaml resolver) + writeFile (simpleStackPath cwd </> "stack.yaml") (standardStackYaml resolver) + writeFile + (multiSourceDirsPath cwd </> "stack.yaml") + (standardStackYaml resolver) + +cleanupStackFiles :: IO () +cleanupStackFiles = do + cwd <- getCurrentDirectory + removeFile (implicitExePath cwd </> "stack.yaml") + removeFile (monoRepoPath cwd </> "stack.yaml") + removeFile (subPackagePath cwd </> "stack.yaml") + removeFile (simpleStackPath cwd </> "stack.yaml") + removeFile (multiSourceDirsPath cwd </> "stack.yaml") + +-- ------------------------------------------------------------- + +standardStackYaml :: String -> String +standardStackYaml resolver = + unlines + [ "# WARNING: THIS FILE IS AUTOGENERATED IN test/utils/CabalHelperSpec. IT WILL BE OVERWRITTEN ON EVERY TEST RUN", + "resolver: " ++ resolver, + "packages:", + "- '.'", + "extra-deps: []", + "flags: {}", + "extra-package-dbs: []" + ] + +monoRepoStackYaml :: String -> String +monoRepoStackYaml resolver = + unlines + [ "# WARNING: THIS FILE IS AUTOGENERATED IN test/utils/CabalHelperSpec. IT WILL BE OVERWRITTEN ON EVERY TEST RUN", + "resolver: " ++ resolver, + "packages:", + "- 'A'", + "- 'B'", + "- 'C'", + "extra-deps: []", + "flags: {}", + "extra-package-dbs: []" + ] + +subPackageStackYaml :: String -> String +subPackageStackYaml resolver = + unlines + [ "# WARNING: THIS FILE IS AUTOGENERATED IN test/unit/CabalHelperSpec. IT WILL BE OVERWRITTEN ON EVERY TEST RUN", + "resolver: " ++ resolver, + "packages:", + "- '.'", + "- 'plugins-api'", + "extra-deps: []", + "flags: {}", + "extra-package-dbs: []" + ] diff --git a/test/unit/Spec.hs b/test/unit/Spec.hs new file mode 100644 index 0000000..5d7c833 --- /dev/null +++ b/test/unit/Spec.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE CPP #-} + +#ifdef CABAL_HELPER_SUPPORT +import qualified CabalHelperSpec +#endif +import Test.Hspec + +main :: IO () +main = hspec spec + +spec :: Spec +spec = +#ifdef CABAL_HELPER_SUPPORT + CabalHelperSpec.spec +#else + pure () +#endif diff --git a/test/utils/TestUtils.hs b/test/utils/TestUtils.hs new file mode 100644 index 0000000..d6f546d --- /dev/null +++ b/test/utils/TestUtils.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE CPP, OverloadedStrings, NamedFieldPuns #-} +module TestUtils where + +import Data.Aeson.Types (typeMismatch) +import Data.Yaml +import Data.Text +import Data.Typeable + +-- |Choose a resolver based on the current compiler, otherwise HaRe/ghc-mod will +-- not be able to load the files +readResolver :: IO String +readResolver = readResolverFrom stackYaml + +stackYaml :: FilePath +stackYaml = "stack.yaml" + +newtype StackResolver = StackResolver String + +instance FromJSON StackResolver where + parseJSON (Object x) = StackResolver <$> x .: pack "resolver" + parseJSON invalid = typeMismatch "StackResolver" invalid + +readResolverFrom :: FilePath -> IO String +readResolverFrom yamlPath = do + result <- decodeFileEither yamlPath + case result of + Left err -> error $ yamlPath ++ " parsing failed: " ++ show err + Right (StackResolver res) -> return res