Skip to content

Commit 6e2b86b

Browse files
Handle Backpack signature files in component discovery
Treat declared .hsig files as Backpack signatures instead of unknown custom-preprocessor candidates. Track declared signature files for rebuilds, keep signature modules out of ordinary module resolution, and emit a Backpack-specific warning when a local .hsig file is not listed in the component's signatures field. Keep the existing custom-preprocessor warning for other unknown extensions.
1 parent 68405b1 commit 6e2b86b

15 files changed

Lines changed: 188 additions & 26 deletions

File tree

src/Stack/ComponentFile.hs

Lines changed: 87 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ import qualified Distribution.Utils.Path as Cabal
4141
import GHC.Records ( HasField )
4242
import qualified HiFileParser as Iface
4343
import Path
44-
( (</>), filename, isProperPrefixOf, parent, parseRelDir
45-
, stripProperPrefix
44+
( (</>), fileExtension, filename, isProperPrefixOf, parent
45+
, parseRelDir, stripProperPrefix
4646
)
4747
import Path.Extra
4848
( forgivingResolveDir, forgivingResolveFile
@@ -84,7 +84,7 @@ stackBenchmarkFiles ::
8484
StackBenchmark
8585
-> RIO GetPackageFileContext (NamedComponent, ComponentFile)
8686
stackBenchmarkFiles bench =
87-
resolveComponentFiles (CBench bench.name) build names
87+
resolveComponentFiles (CBench bench.name) build names []
8888
where
8989
names :: [DotCabalDescriptor]
9090
names = bnames <> exposed
@@ -106,7 +106,7 @@ stackTestSuiteFiles ::
106106
StackTestSuite
107107
-> RIO GetPackageFileContext (NamedComponent, ComponentFile)
108108
stackTestSuiteFiles test =
109-
resolveComponentFiles (CTest test.name) build names
109+
resolveComponentFiles (CTest test.name) build names []
110110
where
111111
names :: [DotCabalDescriptor]
112112
names = bnames <> exposed
@@ -129,7 +129,7 @@ stackExecutableFiles ::
129129
StackExecutable
130130
-> RIO GetPackageFileContext (NamedComponent, ComponentFile)
131131
stackExecutableFiles exe =
132-
resolveComponentFiles (CExe exe.name) build names
132+
resolveComponentFiles (CExe exe.name) build names []
133133
where
134134
build :: StackBuildInfo
135135
build = exe.buildInfo
@@ -144,7 +144,7 @@ stackLibraryFiles ::
144144
StackLibrary
145145
-> RIO GetPackageFileContext (NamedComponent, ComponentFile)
146146
stackLibraryFiles lib =
147-
resolveComponentFiles componentName build names
147+
resolveComponentFiles componentName build names lib.signatures
148148
where
149149
componentRawName :: StackUnqualCompName
150150
componentRawName = lib.name
@@ -174,18 +174,22 @@ resolveComponentFiles ::
174174
=> NamedComponent
175175
-> rec
176176
-> [DotCabalDescriptor]
177+
-> [ModuleName]
177178
-> RIO GetPackageFileContext (NamedComponent, ComponentFile)
178-
resolveComponentFiles component build names = do
179+
resolveComponentFiles component build names signatureNames = do
179180
dirs <- mapMaybeM (resolveDirOrWarn . getSymbolicPath) build.hsSourceDirs
180181
dir <- asks (parent . (.file))
181182
agdirs <- autogenDirs
183+
let allDirs = (if null dirs then [dir] else dirs) ++ agdirs
182184
(modules,files,warnings) <-
183185
resolveFilesAndDeps
184186
component
185-
((if null dirs then [dir] else dirs) ++ agdirs)
187+
allDirs
186188
names
189+
(S.fromList signatureNames)
190+
sigFiles <- resolveSignatureFiles allDirs signatureNames
187191
cfiles <- buildOtherSources build
188-
pure (component, ComponentFile modules (files <> cfiles) warnings)
192+
pure (component, ComponentFile modules (files <> sigFiles <> cfiles) warnings)
189193
where
190194
autogenDirs :: RIO GetPackageFileContext [Path Abs Dir]
191195
autogenDirs = do
@@ -201,11 +205,13 @@ resolveFilesAndDeps ::
201205
NamedComponent -- ^ Package component name
202206
-> [Path Abs Dir] -- ^ Directories to look in.
203207
-> [DotCabalDescriptor] -- ^ Base names.
208+
-> Set ModuleName -- ^ Backpack signatures already accounted for.
204209
-> RIO
205210
GetPackageFileContext
206211
(Map ModuleName (Path Abs File), [DotCabalPath], [PackageWarning])
207-
resolveFilesAndDeps component dirs names0 = do
208-
(dotCabalPaths, foundModules, missingModules, _) <- loop names0 S.empty M.empty
212+
resolveFilesAndDeps component dirs names0 signatureModules = do
213+
(dotCabalPaths, foundModules, missingModules, _) <-
214+
loop names0 signatureModules M.empty
209215
warnings <-
210216
liftM2 (++) (warnUnlisted foundModules) (warnMissing missingModules)
211217
pure (foundModules, dotCabalPaths, warnings)
@@ -224,7 +230,7 @@ resolveFilesAndDeps component dirs names0 = do
224230
)
225231
loop [] _ _ = pure ([], M.empty, [], M.empty)
226232
loop names doneModules0 knownUsages = do
227-
resolved <- resolveFiles dirs names
233+
resolved <- resolveFiles dirs signatureModules names
228234
let foundFiles = mapMaybe snd resolved
229235
foundModules = mapMaybe toResolvedModule resolved
230236
missingModules = mapMaybe toMissingModule resolved
@@ -289,6 +295,22 @@ resolveFilesAndDeps component dirs names0 = do
289295
toMissingModule _ =
290296
Nothing
291297

298+
-- | Resolve Backpack signature files declared by a library component. Signature
299+
-- files are tracked for rebuilds, but they are not ordinary implementation
300+
-- modules and should not feed unlisted-module warnings.
301+
resolveSignatureFiles ::
302+
[Path Abs Dir]
303+
-> [ModuleName]
304+
-> RIO GetPackageFileContext [DotCabalPath]
305+
resolveSignatureFiles dirs =
306+
fmap concat . mapM resolveSignatureFile
307+
where
308+
resolveSignatureFile mn = do
309+
let relFile = Cabal.toFilePath mn ++ ".hsig"
310+
matches <- fmap (nubOrd . catMaybes) $
311+
mapM (\dir -> resolveDirFile dir relFile) dirs
312+
pure $ map DotCabalFilePath matches
313+
292314
-- | Get the dependencies of a Haskell module file.
293315
getDependencies ::
294316
Map FilePath (Path Abs File)
@@ -385,28 +407,33 @@ componentOutputDir namedComponent distDir =
385407
-- extensions.
386408
resolveFiles ::
387409
[Path Abs Dir] -- ^ Directories to look in.
410+
-> Set ModuleName -- ^ Backpack signatures declared by the component.
388411
-> [DotCabalDescriptor] -- ^ Base names.
389412
-> RIO GetPackageFileContext [(DotCabalDescriptor, Maybe DotCabalPath)]
390-
resolveFiles dirs names =
391-
forM names (\name -> fmap (name, ) (findCandidate dirs name))
413+
resolveFiles dirs signatureModules names =
414+
forM names (\name -> fmap (name, ) (findCandidate dirs signatureModules name))
392415

393416
-- | Find a candidate for the given module-or-filename from the list
394417
-- of directories and given extensions.
395418
findCandidate ::
396419
[Path Abs Dir]
420+
-> Set ModuleName
397421
-> DotCabalDescriptor
398422
-> RIO GetPackageFileContext (Maybe DotCabalPath)
399-
findCandidate dirs name = do
423+
findCandidate dirs signatureModules name = do
400424
pkg <- asks (.file) >>= parsePackageNameFromFilePath
401425
customPreprocessorExts <- view $ configL . to (.customPreprocessorExts)
402426
let haskellPreprocessorExts =
403-
haskellDefaultPreprocessorExts ++ customPreprocessorExts
427+
filter
428+
(not . isBackpackSignatureExt)
429+
(haskellDefaultPreprocessorExts ++ customPreprocessorExts)
404430
liftIO (makeNameCandidates haskellPreprocessorExts) >>= \case
405431
[candidate] -> pure (Just (cons candidate))
406432
[] -> do
407433
case name of
408434
DotCabalModule mn
409-
| display mn /= paths_pkg pkg -> logPossibilities dirs mn
435+
| display mn /= paths_pkg pkg ->
436+
logPossibilities dirs signatureModules mn
410437
_ -> pure ()
411438
pure Nothing
412439
(candidate:rest) -> do
@@ -451,22 +478,39 @@ findCandidate dirs name = do
451478
(xs, ys) -> xs ++ ys
452479
resolveCandidate dir = fmap maybeToList . resolveDirFile dir
453480

454-
-- | Log that we couldn't find a candidate, but there are
455-
-- possibilities for custom preprocessor extensions.
481+
isBackpackSignatureExt :: Text -> Bool
482+
isBackpackSignatureExt ext =
483+
T.toLower (fromMaybe ext $ T.stripPrefix "." ext) == "hsig"
484+
485+
isBackpackSignatureFile :: Path b File -> Bool
486+
isBackpackSignatureFile file =
487+
maybe False (isBackpackSignatureExt . T.pack) $ fileExtension file
488+
489+
-- | Log that we couldn't find a candidate, but there are possibilities for
490+
-- custom preprocessor extensions or an undeclared Backpack signature.
456491
--
457492
-- For example: .erb for a Ruby file might exist in one of the
458493
-- directories.
459-
logPossibilities :: HasTerm env => [Path Abs Dir] -> ModuleName -> RIO env ()
460-
logPossibilities dirs mn = do
494+
logPossibilities ::
495+
HasTerm env
496+
=> [Path Abs Dir]
497+
-> Set ModuleName
498+
-> ModuleName
499+
-> RIO env ()
500+
logPossibilities dirs signatureModules mn = do
461501
possibilities <- concat <$> makePossibilities
462-
unless (null possibilities) $ prettyWarn $
502+
let nonSignaturePossibilities =
503+
filter (not . isBackpackSignatureFile) possibilities
504+
signaturePossibilities =
505+
filter isBackpackSignatureFile possibilities
506+
unless (null nonSignaturePossibilities) $ prettyWarn $
463507
fillSep
464508
[ flow "Unable to find a known candidate for the Cabal entry"
465509
, (style Module . fromString $ display mn) <> ","
466510
, flow "but did find:"
467511
]
468512
<> line
469-
<> bulletedList (map pretty possibilities)
513+
<> bulletedList (map pretty nonSignaturePossibilities)
470514
<> blankLine
471515
<> fillSep
472516
[ flow "If you are using a custom preprocessor for this module with \
@@ -476,6 +520,26 @@ logPossibilities dirs mn = do
476520
, flow "key in Stack's project-level configuration file"
477521
, "(" <> style File "stack.yaml" <> ")."
478522
]
523+
when
524+
(mn `S.notMember` signatureModules && not (null signaturePossibilities))
525+
$ prettyWarn
526+
$ fillSep
527+
[ flow "Found Backpack signature file for Cabal entry"
528+
, (style Module . fromString $ display mn) <> ","
529+
, flow "but that module is not listed in the component's"
530+
, style Shell "signatures"
531+
, flow "field:"
532+
]
533+
<> line
534+
<> bulletedList (map pretty signaturePossibilities)
535+
<> blankLine
536+
<> fillSep
537+
[ flow "If this file is meant to be a Backpack signature, add"
538+
, style Module (fromString $ display mn)
539+
, flow "to the"
540+
, style Shell "signatures"
541+
, flow "field in the package description."
542+
]
479543
where
480544
makePossibilities = mapM makePossibility dirs
481545

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- Stack should explain likely Backpack signature mistakes with a Backpack
2+
-- warning, not the custom-preprocessor warning.
3+
4+
import Control.Monad ( unless, when )
5+
import Data.List ( isInfixOf )
6+
import StackTest
7+
8+
main :: IO ()
9+
main =
10+
stackErrStderr ["build"] $ \err -> do
11+
expect err "Found Backpack signature file for Cabal entry"
12+
expect err "Logger"
13+
expect err "not listed in the component's"
14+
expect err "signatures"
15+
when ("custom-preprocessor-extensions" `isInfixOf` err) $
16+
error $
17+
"Expected no custom-preprocessor warning for Logger.hsig, got: "
18+
++ show err
19+
20+
expect :: String -> String -> IO ()
21+
expect err msg =
22+
unless (msg `isInfixOf` err) $
23+
error $ "Expected " ++ show msg ++ " in stderr, got: " ++ show err
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hsig-warning.cabal
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
spec-version: 0.36.0
2+
3+
name: hsig-warning
4+
5+
dependencies:
6+
- base
7+
8+
library:
9+
source-dirs: src
10+
exposed-modules: Lib
11+
other-modules: Logger
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module Lib where
2+
3+
answer :: Int
4+
answer = 42
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
signature Logger where
2+
3+
logMessage :: String -> String
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
snapshot: ghc-9.10.3

tests/integration/tests/backpack-x-pkg-transitive/Main.hs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
-- sig: Logger) depends on str-sig (indefinite, sig: Str). When consumer mixes
33
-- in logger-sig, both Logger and Str holes must be filled transitively.
44

5-
import Control.Monad ( unless )
5+
import Control.Monad ( unless, when )
66
import Data.List ( isInfixOf )
77
import StackTest
88

@@ -15,19 +15,31 @@ main = do
1515
-- 4. logger-sig CLib (indefinite, typecheck-only, inherits Str hole)
1616
-- 5. logger-sig CInst (fills BOTH Logger and Str holes)
1717
-- 6. consumer-pkg CLib + CExe
18-
stack ["build"]
18+
stackCheckStderr ["build"] expectNoCandidateWarning
1919

2020
-- Verify the consumer executable calls through the transitive chain
2121
stackCheckStdout ["exec", "consumer-demo"] $ \out ->
2222
unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $
2323
error $ "Expected '[LOG] Hello from transitive chain' in output, got: "
2424
++ show out
2525

26+
appendFile "logger-sig/src/Logger.hsig" "\nloggerName :: String\n"
27+
2628
-- Rebuild should succeed (no stale CInst state)
27-
stack ["build"]
29+
stackCheckStderr ["build"] $ \err -> do
30+
expectNoCandidateWarning err
31+
unless ("Compiling Logger[sig]" `isInfixOf` err) $
32+
error $
33+
"Expected Logger.hsig change to rebuild Logger[sig], got stderr: "
34+
++ show err
2835

2936
-- Verify output still correct after rebuild
3037
stackCheckStdout ["exec", "consumer-demo"] $ \out ->
3138
unless ("[LOG] Hello from transitive chain" `isInfixOf` out) $
3239
error $ "Expected '[LOG] Hello from transitive chain' after rebuild, got: "
3340
++ show out
41+
42+
expectNoCandidateWarning :: String -> IO ()
43+
expectNoCandidateWarning err =
44+
when ("Unable to find a known candidate for the Cabal entry" `isInfixOf` err) $
45+
error $ "Unexpected known candidate warning in stderr: " ++ show err

tests/integration/tests/backpack-x-pkg-transitive/files/impl-pkg/src/Logger.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ module Logger where
22

33
logMessage :: String -> String
44
logMessage msg = "[LOG] " ++ msg
5+
6+
loggerName :: String
7+
loggerName = "transitive logger"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- Stack should still suggest custom-preprocessor-extensions for unknown
2+
-- non-Haskell module file extensions.
3+
4+
import Control.Monad ( unless )
5+
import Data.List ( isInfixOf )
6+
import StackTest
7+
8+
main :: IO ()
9+
main =
10+
stackErrStderr ["build"] $ \err -> do
11+
expect err "Unable to find a known candidate for the Cabal entry"
12+
expect err "Generated"
13+
expect err "Generated.foo"
14+
expect err "custom-preprocessor-extensions"
15+
16+
expect :: String -> String -> IO ()
17+
expect err msg =
18+
unless (msg `isInfixOf` err) $
19+
error $ "Expected " ++ show msg ++ " in stderr, got: " ++ show err

0 commit comments

Comments
 (0)