Skip to content

Make -main-is work with {thing} from arbitrary installed packages

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
  2. 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:

-- 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.:

-- 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:

-- 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.
Trac metadata
Trac field Value
Version 8.0.2
Type FeatureRequest
TypeOfFailure OtherFailure
Priority normal
Resolution Unresolved
Component Compiler
Test case
Differential revisions
BlockedBy
Related
Blocking
CC
Operating system
Architecture
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information