Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cabal's main-is field (source file) does not alter GHC's -main-is option (module and function name) #6695

Open
andreasabel opened this issue Mar 15, 2025 · 7 comments

Comments

@andreasabel
Copy link

If the module in main-is has a name and there is another module that has no explicit name (no module header), then the other module is picked as main module.

Bar.hs

main = putStrLn "I am the shady main..."

Foo.hs

module Foo where

main = putStrLn "I am the real main!"

wrong-main.cabal

cabal-version: 1.12
name:           wrong-main
version:        0.0.0
build-type:     Simple

executable wrong-main
  main-is: Foo.hs
  other-modules:
      Bar
      Paths_wrong_main
  hs-source-dirs:
      ./
  build-depends:
      base
  default-language: Haskell2010
$ stack run
...                                                                                                       
I am the shady main...

Packaged reproducer: wrong-main.tgz

The wrong result can be reproduced with this vanilla GHC call:

ghc --make Foo.hs Bar.hs

The correct result can be obtained by adding -main-is Foo:

ghc --make -main-is Foo Bar.hs Foo.hs    

I suppose the difficulty is to extract the module name Foo from Foo.hs, since -main-is does not accept a file, just a qualified (module) name.

This issue is shared with cabal:

@mpilgrem
Copy link
Member

mpilgrem commented Mar 15, 2025

@andreasabel, how did you get it to build? I tried to reproduce with a package.yaml:

spec-version: 0.36.0

name: test6695
version: 0.1.0.0

executables:
  test6695:
    main: Main.hs
    source-dirs: app
    dependencies:
    - base

generating Cabal file:

cabal-version: 1.12

-- This file has been generated from package.yaml by hpack version 0.38.0.
--
-- see: https://github.com/sol/hpack

name:           test6695
version:        0.1.0.0
license:        BSD3
license-file:   LICENSE
build-type:     Simple

executable test6695
  main-is: Main.hs
  other-modules:
      NoModuleName
  hs-source-dirs:
      app
  build-depends:
      base
  default-language: Haskell2010

(In my package, I called Bar.hs NoModuleName.hs and Foo.hs Main.hs.)

and stack.yaml:

snapshot: lts-23.14 # GHC 9.8.4

However, stack build fails with:

test6695> build (exe) with ghc-9.8.4
Preprocessing executable 'test6695' for test6695-0.1.0.0..
Building executable 'test6695' for test6695-0.1.0.0..

app\NoModuleName.hs:1:1: error: [GHC-28623]
    File name does not match module name:
    Saw     : ‘Main’
    Expected: ‘NoModuleName’
  |
1 | main :: IO ()
  | ^

@mpilgrem
Copy link
Member

OK, so I can reproduce what you experienced, but only if the modules are in the project root directory. In my example, I had put them in an app directory.

@mpilgrem
Copy link
Member

I think:

ghc Foo.hs Bar.hs

behaves as expected, namely:

[1 of 3] Compiling Foo              ( Foo.hs, Foo.o )
[2 of 3] Compiling Main             ( Bar.hs, Bar.o )
[3 of 3] Linking Bar.exe

Bar.hs is deemed to have header module Main(main) where (Haskell 2010 Language Report, section 5.1). As it is a module named Main with function main, GHC creates Bar.exe. Foo.hs has module Foo with function main, so you would not expect GHC to create an executable.

@andreasabel
Copy link
Author

Yes, so the question is how to communicate the main-is to GHC correctly.
If we just let GHC pick the main module based on the module name, then there is no point in a main-is directive.

@mpilgrem
Copy link
Member

mpilgrem commented Mar 15, 2025

@andreasabel, I may be wrong, but it seems to me that Cabal uses its main-is field to decide which source file to pass to GHC - and does not tell GHC to treat that source file as if it were a Main module.

That is, given main-is: Foo.hs, Cabal commands:

ghc Foo.hs Bar

and not:

ghc -main-is Foo Foo.hs Bar

The Cabal User Guide has for main-is:

The name of the .hs or .lhs file containing the Main module. ... Further, while the name of the file may vary, the module itself must be named Main.

Foo.hs does not meet this requirement, so is not a valid value for the main-is field.

To force GHC to treat your Foo.hs as if its module were named Main, I think you need to specify that expressly:

cabal-version: 1.12
name:           test6695
version:        0.1.0.0
build-type:     Simple

executable test6695
  main-is: Foo.hs
  ghc-options: -main-is Foo -- Required because Foo.hs does not provide a `Main` module
  build-depends:
      base
  default-language: Haskell2010

EDIT: So I think the resolution of this is more complete documentation in the Cabal User Guide.

@andreasabel
Copy link
Author

Your analysis is correct and matches my observation.

it seems to me that Cabal uses its main-is field to decide which source file to pass to GHC - and does not tell GHC to treat that source file as if it were a Main module.

Yes, this is what I could extract from cabal's -v3 output, see the companion issue report: haskell/cabal#10832

The Cabal User Guide has for main-is:

The name of the .hs or .lhs file containing the Main module. ... Further, while the name of the file may vary, the module itself must be named Main.

Foo.hs does not meet this requirement, so is not a valid value for the main-is field.

Cabal aims to be build-tool with a declarative package language, in the spirit of the declarative functional language Haskell. So I am expecting a more declarative treatment of main-is. Currently it behaves in the garbage in - garbage out spirit. That is not at all what Haskell is. In Haskell, when you write garbage, you get an type error, it does not simply produce garbage assembly code from your garbage source.
This defines my expectations to the Haskell build tools.

So, if I write main-is: FILE I want the tool to communicate to GHC that the entrypoint to my executable is found in FILE.

I think this is correct behavior is hampered at the moment only by the extra effort this would cause. The build tool would have to extract the module name of FILE and communicate this to GHC via -main-is.

I see only two principled solutions here:

  1. Fix main-is so that it communicates correctly to GHC.
    (The "easiest" solution here (from the perspective of the build tools) would be to convince the GHC developers to accept a flag like -main-is in which you communicate a file name rather than a module name.)
  2. Deprecate (and finally remove) main-is. Let GHC decide what the main module is based on the module name (Main) or the ghc-options: -main-is ... flag.

@mpilgrem
Copy link
Member

@andreasabel, I've opened a Cabal issue which treats this a gap in the documentation:

but I understand your view that Cabal could also do something differently. I think that is best put directly to the Cabal project at its repository.

@mpilgrem mpilgrem changed the title Another module than given in main-is is picked as main module Cabal's main-is field (source file) does not alter GHC's -main-is option (module and function name) Mar 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants