[GHC] #13801: Make -main-is work with {thing} from arbitrary installed packages

GHC ghc-devs at haskell.org
Thu Jun 8 09:58:59 UTC 2017


#13801: Make -main-is work with {thing} from arbitrary installed packages
-------------------------------------+-------------------------------------
           Reporter:  SimonHengel    |             Owner:  (none)
               Type:  feature        |            Status:  new
  request                            |
           Priority:  normal         |         Milestone:
          Component:  Compiler       |           Version:  8.0.2
           Keywords:                 |  Operating System:  Unknown/Multiple
       Architecture:                 |   Type of failure:  None/Unknown
  Unknown/Multiple                   |
          Test Case:                 |        Blocked By:
           Blocking:                 |   Related Tickets:
Differential Rev(s):                 |         Wiki Page:
-------------------------------------+-------------------------------------
 = TL;DR
 Conceptually, `-main-is {thing}` is useful when writing unit tests. It
 allows you to test the code in your "Main" module by allowing you to use a
 different name for it.

 But using it that way will always result in double compilation (that is,
 all your code has to be compiled twice, once for your executable and once
 for your test suite).

 The reason for this is that `{thing}` has to be part of the currently
 compiled package (aka the `main` package).  As a consequent, when using
 `-is-main`, it is not possible to define a library that is used by both
 the executable and the test suite.

 I propose that `-main-is` is extended so that `{thing}` can be from any
 ''installed package''.

 I'll give a somewhat detailed motivation below.  Please feel free to fast-
 forward to the last section, which gives a test case (or rather acceptance
 criteria) for this feature request.

 = The full story

 == How to unit test code without the use of {{{-main-is}}}

 This section shows a common way to structure code for an executable, so
 that it is possible to:

 1. write unit tests for all the code
 1. avoid double compilation between the executable and the test suite

 (for the reminder of this text I call these two properties ''desirable
 properties'')

 === Code as a library
 Define all your code outside of `Main`, including your `main` function.


 As an example, let's assume we define our code in
 `src/My/Awesome/Tool.hs`, which defines the module `My.Awesome.Tool` and a
 "main" function named `run` in it:


 {{{#!hs
 -- src/My/Awesome/Tool.hs
 module My.Awesome.Tool where

 run :: IO ()
 run = do
   ...
 }}}
 === Tests that use the library
 It is then possible to write tests for that code by importing the library
 module, e.g.:

 {{{#!hs
 -- test/Main.hs
 module Main where

 imports My.Awesome.Tool

 main = do
   -- unit tests go here
   ...
 }}}
 === Executable as a thin wrapper around the library code

 To compile an actual executable we create a ''driver''.  The driver
 imports the library module and defines a `main` function.  For our example
 this would looks something like this:

 {{{#!hs
 -- driver/Main.hs
 module Main where

 import My.Awesome.Tool (run)

 main = run
 }}}

 '''Note:''' The driver does not define any non-trivial code.  This is to
 retain our first desirable property.

 === Compiling everything with Cabal
 It is then possible to compile everything with Cabal, using a Cabal file
 similar to this one:

 {{{
 -- my-awesome-tool.cabal
 name: my-awesome-tool

 library
   hs-source-dirs: src
   exposed-modules: My.Awesome.Tool

 test-suite test
   type: exitcode-stdio-1.0
   build-depends: my-awesome-tool
   hs-source-dirs: test
   main-is: Main.hs

 executable my-awesome-tool
   build-depends: my-awesome-tool
   hs-source-dirs: driver
   main-is: Main.hs
 }}}

 '''Note:''' Both, the executable and the test suite depend on the library
 component.  This avoids double compilation, one of our desirable
 properties.

 == Removing the need for a driver by using `-main-is`
 It is possible to get rid of the need for a driver by using `-main-is`:
 {{{
 -- my-awesome-tool.cabal

 ...

 executable my-awesome-tool
   hs-source-dirs: src
   main-is: My/Awesome/Tool.hs
   ghc-options: -main-is My.Awesome.Tool.run
 }}}

 But doing so results in double compilation: The executable can no longer
 depend on the library component.

 This is expected behavior, as stated in the documentation:

 > Strictly speaking, `-main-is` is not a link-phase flag at all; it has no
 effect on the link step. The flag must be specified when compiling the
 module containing the specified main function

 == Shortcomings of `-main-is`

 According to the documentation, the purpose of `-main-is` is:

 > When testing, it is often convenient to change which function is the
 “main” one, and the `-main-is` flag allows you to do so.

 It is not very explicit what "when testing" refers to here, but for the
 lack of any other evidence I assume this refers to unit testing.

 As far as I can tell, there is no way to use `-main-is` for unit testing
 without double compilation.

 '''Or in other words:''' If we use `-main-is` for it's stated purpose we
 always loose the second of our desirable properties.

 Please correct me if you think that I'm wrong.

 == How is `-main-is` implemented?
 I haven't looked at any code, but my assumption is that GHC generates a
 driver module, similar to the one we have to write by hand if we don't use
 `-main-is`.  Can somebody confirm (or negate) this?

 == Proposed change
 I propose that GHC always generates the driver when `-main-is {thing}` is
 specified.  GHC should even generate the driver if `{thing}` is not part
 of the currently compiled package (specifically `{thing}` is defined in an
 ''installed package'', not the `main` package).

 == (manual) test case

 This test case uses my `hpack` package (but any package that defines some
 function of type `IO ()` should work):

 {{{
 $ cabal install hpack
 $ ghc -package hpack -main-is Hpack.main -o hpack
 }}}

 === expected result

 An executable named `hpack` is compiled that uses `Hpack.main` from the
 installed package `hpack` as entry point.

 === actual result

 {{{
 ghc: no input files
 Usage: For basic information, try the `--help' option.
 }}}

--
Ticket URL: <http://ghc.haskell.org/trac/ghc/ticket/13801>
GHC <http://www.haskell.org/ghc/>
The Glasgow Haskell Compiler


More information about the ghc-tickets mailing list