1 {-# LANGUAGE LambdaCase #-}
2 {-# LANGUAGE NamedFieldPuns #-}
3 {-# LANGUAGE OverloadedStrings #-}
4 {-# LANGUAGE RecordWildCards #-}
6 -- | Utilities to help commands with scripts
7 module Distribution
.Client
.ScriptUtils
9 , getScriptCacheDirectory
10 , ensureScriptCacheDirectory
11 , withContextAndSelectors
12 , AcceptNoTargets
(..)
14 , updateContextAndWriteProjectFile
15 , updateContextAndWriteProjectFile
'
16 , fakeProjectSourcePackage
21 import Distribution
.Client
.Compat
.Prelude
hiding (toList
)
24 import Distribution
.Compat
.Lens
25 import qualified Distribution
.Types
.Lens
as L
27 import Distribution
.CabalSpecVersion
28 ( CabalSpecVersion
(..)
31 import Distribution
.Client
.Config
32 ( defaultScriptBuildsDir
34 import Distribution
.Client
.DistDirLayout
38 import Distribution
.Client
.HashValue
43 import Distribution
.Client
.HttpUtils
47 import Distribution
.Client
.NixStyleOptions
50 import Distribution
.Client
.ProjectConfig
53 , ProjectConfigShared
(..)
54 , projectConfigHttpTransport
57 , withProjectOrGlobalConfig
59 import Distribution
.Client
.ProjectConfig
.Legacy
60 ( ProjectConfigSkeleton
61 , instantiateProjectConfigSkeletonFetchingCompiler
62 , parseProjectSkeleton
64 import Distribution
.Client
.ProjectFlags
67 import Distribution
.Client
.ProjectOrchestration
68 import Distribution
.Client
.ProjectPlanning
69 ( ElaboratedConfiguredPackage
(..)
70 , ElaboratedSharedConfig
(..)
73 import Distribution
.Client
.RebuildMonad
76 import Distribution
.Client
.Setup
80 import Distribution
.Client
.TargetSelector
81 ( TargetSelectorProblem
(..)
84 import Distribution
.Client
.Types
85 ( PackageLocation
(..)
86 , PackageSpecifier
(..)
87 , UnresolvedSourcePackage
89 import Distribution
.Compiler
91 , perCompilerFlavorToList
93 import Distribution
.FieldGrammar
97 import Distribution
.Fields
102 import Distribution
.PackageDescription
105 import Distribution
.PackageDescription
.FieldGrammar
106 ( executableFieldGrammar
108 import Distribution
.PackageDescription
.PrettyPrint
109 ( showGenericPackageDescription
111 import Distribution
.Parsec
114 import qualified Distribution
.SPDX
.License
as SPDX
115 import Distribution
.Simple
.Compiler
117 , OptimisationLevel
(..)
120 import Distribution
.Simple
.Flag
124 import Distribution
.Simple
.PackageDescription
127 import Distribution
.Simple
.Setup
130 import Distribution
.Simple
.Utils
131 ( createDirectoryIfMissingVerbose
132 , createTempDirectory
139 import Distribution
.Solver
.Types
.SourcePackage
as SP
142 import Distribution
.System
145 import Distribution
.Types
.BuildInfo
148 import Distribution
.Types
.ComponentId
151 import Distribution
.Types
.CondTree
154 import Distribution
.Types
.Executable
157 import Distribution
.Types
.GenericPackageDescription
as GPD
158 ( GenericPackageDescription
(..)
159 , emptyGenericPackageDescription
161 import Distribution
.Types
.PackageDescription
162 ( PackageDescription
(..)
163 , emptyPackageDescription
165 import Distribution
.Types
.PackageName
.Magic
166 ( fakePackageCabalFileName
169 import Distribution
.Types
.UnitId
172 import Distribution
.Types
.UnqualComponentName
173 ( UnqualComponentName
175 import Distribution
.Utils
.NubList
178 import Distribution
.Verbosity
181 import Language
.Haskell
.Extension
185 import Control
.Concurrent
.MVar
190 import Control
.Exception
193 import qualified Data
.ByteString
.Char8
as BS
194 import Data
.ByteString
.Lazy
()
195 import qualified Data
.Set
as S
196 import Distribution
.Client
.Errors
197 import System
.Directory
200 , getTemporaryDirectory
201 , removeDirectoryRecursive
203 import System
.FilePath
209 import qualified Text
.Parsec
as P
211 -- A note on multi-module script support #6787:
212 -- Multi-module scripts are not supported and support is non-trivial.
213 -- What you want to do is pass the absolute path to the script's directory in hs-source-dirs,
214 -- but hs-source-dirs only accepts relative paths. This leaves you with several options none
215 -- of which are particularly appealing.
216 -- 1) Loosen the requirement that hs-source-dirs take relative paths
217 -- 2) Add a field to BuildInfo that acts like an hs-source-dir, but accepts an absolute path
218 -- 3) Use a path relative to the project root in hs-source-dirs, and pass extra flags to the
219 -- repl to deal with the fact that the repl is relative to the working directory and not
222 -- | Get the hash of a script's absolute path.
224 -- Two hashes will be the same as long as the absolute paths
226 getScriptHash
:: FilePath -> IO String
227 getScriptHash script
=
228 -- Truncation here tries to help with long path issues on Windows.
233 <$> canonicalizePath script
235 -- | Get the directory for caching a script build.
237 -- The only identity of a script is it's absolute path, so append the
238 -- hashed path to the @script-builds@ dir to get the cache directory.
239 getScriptCacheDirectory
:: FilePath -> IO FilePath
240 getScriptCacheDirectory script
= (</>) <$> defaultScriptBuildsDir
<*> getScriptHash script
242 -- | Get the directory for caching a script build and ensure it exists.
244 -- The only identity of a script is it's absolute path, so append the
245 -- hashed path to the @script-builds@ dir to get the cache directory.
246 ensureScriptCacheDirectory
:: Verbosity
-> FilePath -> IO FilePath
247 ensureScriptCacheDirectory verbosity script
= do
248 cacheDir
<- getScriptCacheDirectory script
249 createDirectoryIfMissingVerbose verbosity
True cacheDir
252 -- | What your command should do when no targets are found.
254 = -- | die on 'TargetSelectorNoTargetsInProject'
256 |
-- | return a default 'TargetSelector'
260 -- | Information about the context in which we found the 'TargetSelector's.
262 = -- | The target selectors are part of a project.
264 |
-- | The target selectors are from the global context.
266 |
-- | The target selectors refer to a script. Contains the path to the script and
267 -- the executable metadata parsed from the script
268 ScriptContext
FilePath Executable
271 -- | Determine whether the targets represent regular targets or a script
272 -- and return the proper context and target selectors.
273 -- Die with an error message if selectors are valid as neither regular targets or as a script.
275 -- In the case that the context refers to a temporary directory,
276 -- delete it after the action finishes.
277 withContextAndSelectors
279 -- ^ What your command should do when no targets are found.
280 -> Maybe ComponentKind
283 -- ^ Command line flags
285 -- ^ Target strings or a script and args.
289 -- ^ Current Command (usually for error reporting).
290 -> (TargetContext
-> ProjectBaseContext
-> [TargetSelector
] -> IO b
)
291 -- ^ The body of your command action.
293 withContextAndSelectors noTargets kind flags
@NixStyleFlags
{..} targetStrings globalFlags cmd act
=
294 withTemporaryTempDirectory
$ \mkTmpDir
-> do
295 (tc
, ctx
) <- withProjectOrGlobalConfig verbosity ignoreProject globalConfigFlag withProject
(withoutProject mkTmpDir
)
297 (tc
', ctx
', sels
) <- case targetStrings
of
298 -- Only script targets may contain spaces and or end with ':'.
299 -- Trying to readTargetSelectors such a target leads to a parse error.
300 [target
] |
any (\c
-> isSpace c
) target ||
":" `
isSuffixOf` target
-> do
301 scriptOrError target
[TargetSelectorNoScript
$ TargetString1 target
]
303 -- In the case where a selector is both a valid target and script, assume it is a target,
304 -- because you can disambiguate the script with "./script"
305 readTargetSelectors
(localPackages ctx
) kind targetStrings
>>= \case
306 Left err
@(TargetSelectorNoTargetsInProject
: _
)
307 |
[] <- targetStrings
308 , AcceptNoTargets
<- noTargets
->
309 return (tc
, ctx
, defaultTarget
)
310 |
(script
: _
) <- targetStrings
-> scriptOrError script err
311 Left err
@(TargetSelectorNoSuch t _
: _
)
312 | TargetString1 script
<- t
-> scriptOrError script err
313 Left err
@(TargetSelectorExpected t _ _
: _
)
314 | TargetString1 script
<- t
-> scriptOrError script err
315 Left err
@(MatchingInternalError _ _ _
: _
) -- Handle ':' in middle of script name.
316 |
[script
] <- targetStrings
-> scriptOrError script err
317 Left err
-> reportTargetSelectorProblems verbosity err
318 Right sels
-> return (tc
, ctx
, sels
)
322 verbosity
= fromFlagOrDefault normal
(configVerbosity configFlags
)
323 ignoreProject
= flagIgnoreProject projectFlags
324 cliConfig
= commandLineFlagsToProjectConfig globalFlags flags mempty
325 globalConfigFlag
= projectConfigConfigFile
(projectConfigShared cliConfig
)
326 defaultTarget
= [TargetPackage TargetExplicitNamed
[fakePackageId
] Nothing
]
329 ctx
<- establishProjectBaseContext verbosity cliConfig cmd
330 return (ProjectContext
, ctx
)
331 withoutProject mkTmpDir globalConfig
= do
332 distDirLayout
<- establishDummyDistDirLayout verbosity
(globalConfig
<> cliConfig
) =<< mkTmpDir
333 ctx
<- establishDummyProjectBaseContext verbosity
(globalConfig
<> cliConfig
) distDirLayout
[] cmd
334 return (GlobalContext
, ctx
)
336 scriptBaseCtx script globalConfig
= do
337 let noDistDir
= mempty
{projectConfigShared
= mempty
{projectConfigDistDir
= Flag
""}}
338 let cfg
= noDistDir
<> globalConfig
<> cliConfig
339 rootDir
<- ensureScriptCacheDirectory verbosity script
340 distDirLayout
<- establishDummyDistDirLayout verbosity cfg rootDir
341 establishDummyProjectBaseContext verbosity cfg distDirLayout
[] cmd
343 scriptOrError script err
= do
344 exists
<- doesFileExist script
347 ctx
<- withGlobalConfig verbosity globalConfigFlag
(scriptBaseCtx script
)
349 let projectRoot
= distProjectRootDirectory
$ distDirLayout ctx
350 writeFile (projectRoot
</> "scriptlocation") =<< canonicalizePath script
352 scriptContents
<- BS
.readFile script
353 executable <- readExecutableBlockFromScript verbosity scriptContents
358 (fromNubList
. projectConfigProgPathExtra
$ projectConfigShared cliConfig
)
359 (flagToMaybe
. projectConfigHttpTransport
$ projectConfigBuildOnly cliConfig
)
361 projectCfgSkeleton
<- readProjectBlockFromScript verbosity httpTransport
(distDirLayout ctx
) (takeFileName script
) scriptContents
363 createDirectoryIfMissingVerbose verbosity
True (distProjectCacheDirectory
$ distDirLayout ctx
)
364 (compiler
, platform
@(Platform arch os
), _
) <- runRebuild projectRoot
$ configureCompiler verbosity
(distDirLayout ctx
) (fst (ignoreConditions projectCfgSkeleton
) <> projectConfig ctx
)
366 projectCfg
<- instantiateProjectConfigSkeletonFetchingCompiler
(pure
(os
, arch
, compilerInfo compiler
)) mempty projectCfgSkeleton
368 let ctx
' = ctx
& lProjectConfig
%~
(<> projectCfg
)
370 build_dir
= distBuildDirectory
(distDirLayout ctx
') $ (scriptDistDirParams script
) ctx
' compiler platform
371 exePath
= build_dir
</> "bin" </> scriptExeFileName script
372 exePathRel
= makeRelative projectRoot exePath
376 & L
.buildInfo
. L
.defaultLanguage
%~
maybe (Just Haskell2010
) Just
377 & L
.buildInfo
. L
.options
%~
fmap (setExePath exePathRel
)
379 createDirectoryIfMissingVerbose verbosity
True (takeDirectory exePath
)
381 return (ScriptContext script
executable', ctx
', defaultTarget
)
382 else reportTargetSelectorProblems verbosity err
384 withTemporaryTempDirectory
:: (IO FilePath -> IO a
) -> IO a
385 withTemporaryTempDirectory act
= newEmptyMVar
>>= \m
-> bracket (getMkTmp m
) (rmTmp m
) act
387 -- We return an (IO Filepath) instead of a FilePath for two reasons:
388 -- 1) To give the consumer the discretion to not create the tmpDir,
389 -- but still grantee that it's deleted if they do create it
390 -- 2) Because the path returned by createTempDirectory is not predicable
391 getMkTmp m
= return $ do
392 tmpDir
<- getTemporaryDirectory
>>= flip createTempDirectory
"cabal-repl."
395 rmTmp m _
= tryTakeMVar m
>>= maybe (return ()) (handleDoesNotExist
() . removeDirectoryRecursive
)
397 scriptComponenetName
:: IsString s
=> FilePath -> s
398 scriptComponenetName scriptPath
= fromString cname
400 cname
= "script-" ++ map censor
(takeFileName scriptPath
)
402 | c `S
.member` ccNamecore
= c
405 scriptExeFileName
:: FilePath -> FilePath
406 scriptExeFileName scriptPath
= "cabal-script-" ++ takeFileName scriptPath
408 scriptDistDirParams
:: FilePath -> ProjectBaseContext
-> Compiler
-> Platform
-> DistDirParams
409 scriptDistDirParams scriptPath ctx compiler platform
=
411 { distParamUnitId
= newSimpleUnitId cid
412 , distParamPackageId
= fakePackageId
413 , distParamComponentId
= cid
414 , distParamComponentName
= Just
$ CExeName cn
415 , distParamCompilerId
= compilerId compiler
416 , distParamPlatform
= platform
417 , distParamOptimization
= fromFlagOrDefault NormalOptimisation optimization
420 cn
= scriptComponenetName scriptPath
421 cid
= mkComponentId
$ prettyShow fakePackageId
<> "-inplace-" <> prettyShow cn
422 optimization
= (packageConfigOptimization
. projectConfigLocalPackages
. projectConfig
) ctx
424 setExePath
:: FilePath -> [String] -> [String]
425 setExePath exePath options
426 |
"-o" `
notElem` options
= "-o" : exePath
: options
427 |
otherwise = options
429 -- | Add the 'SourcePackage' to the context and use it to write a .cabal file.
430 updateContextAndWriteProjectFile
' :: ProjectBaseContext
-> SourcePackage
(PackageLocation
(Maybe FilePath)) -> IO ProjectBaseContext
431 updateContextAndWriteProjectFile
' ctx srcPkg
= do
432 let projectRoot
= distProjectRootDirectory
$ distDirLayout ctx
433 packageFile
= projectRoot
</> fakePackageCabalFileName
434 contents
= showGenericPackageDescription
(srcpkgDescription srcPkg
)
435 writePackageFile
= writeUTF8File packageFile contents
436 -- TODO This is here to prevent reconfiguration of cached repl packages.
437 -- It's worth investigating why it's needed in the first place.
438 packageFileExists
<- doesFileExist packageFile
441 cached
<- force
<$> readUTF8File packageFile
445 else writePackageFile
446 return (ctx
& lLocalPackages
%~
(++ [SpecificSourcePackage srcPkg
]))
448 -- | Add add the executable metadata to the context and write a .cabal file.
449 updateContextAndWriteProjectFile
:: ProjectBaseContext
-> FilePath -> Executable
-> IO ProjectBaseContext
450 updateContextAndWriteProjectFile ctx scriptPath scriptExecutable
= do
451 let projectRoot
= distProjectRootDirectory
$ distDirLayout ctx
453 absScript
<- canonicalizePath scriptPath
456 fakeProjectSourcePackage projectRoot
457 & lSrcpkgDescription
. L
.condExecutables
458 .~
[(scriptComponenetName scriptPath
, CondNode
executable (targetBuildDepends
$ buildInfo
executable) [])]
461 & L
.modulePath
.~ absScript
463 updateContextAndWriteProjectFile
' ctx sourcePackage
465 parseScriptBlock
:: BS
.ByteString
-> ParseResult Executable
466 parseScriptBlock str
=
467 case readFields str
of
469 let (fields
, _
) = takeFields fs
470 parseFieldGrammar cabalSpecLatest fields
(executableFieldGrammar
"script")
471 Left perr
-> parseFatalFailure pos
(show perr
)
473 ppos
= P
.errorPos perr
474 pos
= Position
(P
.sourceLine ppos
) (P
.sourceColumn ppos
)
476 readScriptBlock
:: Verbosity
-> BS
.ByteString
-> IO Executable
477 readScriptBlock verbosity
= parseString parseScriptBlock verbosity
"script block"
479 -- | Extract the first encountered executable metadata block started and
480 -- terminated by the below tokens or die.
486 -- Return the metadata.
487 readExecutableBlockFromScript
:: Verbosity
-> BS
.ByteString
-> IO Executable
488 readExecutableBlockFromScript verbosity str
= do
489 str
' <- case extractScriptBlock
"cabal" str
of
490 Left e
-> dieWithException verbosity
$ FailedExtractingScriptBlock e
492 when (BS
.all isSpace str
') $ warn verbosity
"Empty script block"
493 readScriptBlock verbosity str
'
495 -- | Extract the first encountered project metadata block started and
496 -- terminated by the below tokens.
502 -- Return the metadata.
503 readProjectBlockFromScript
:: Verbosity
-> HttpTransport
-> DistDirLayout
-> String -> BS
.ByteString
-> IO ProjectConfigSkeleton
504 readProjectBlockFromScript verbosity httpTransport DistDirLayout
{distDownloadSrcDirectory
} scriptName str
= do
505 case extractScriptBlock
"project" str
of
506 Left _
-> return mempty
508 reportParseResult verbosity
"script" scriptName
509 =<< parseProjectSkeleton distDownloadSrcDirectory httpTransport verbosity
[] scriptName x
511 -- | Extract the first encountered script metadata block started end
512 -- terminated by the tokens
518 -- appearing alone on lines (while tolerating trailing whitespace).
519 -- These tokens are not part of the 'Right' result.
521 -- In case of missing or unterminated blocks a 'Left'-error is
523 extractScriptBlock
:: BS
.ByteString
-> BS
.ByteString
-> Either String BS
.ByteString
524 extractScriptBlock header str
= goPre
(BS
.lines str
)
526 isStartMarker
= (== startMarker
) . stripTrailSpace
527 isEndMarker
= (== endMarker
) . stripTrailSpace
529 stripTrailSpace
= fst . BS
.spanEnd
isSpace
531 -- before start marker
532 goPre ls
= case dropWhile (not . isStartMarker
) ls
of
533 [] -> Left
$ "`" ++ BS
.unpack startMarker
++ "` start marker not found"
534 (_
: ls
') -> goBody
[] ls
'
536 goBody _
[] = Left
$ "`" ++ BS
.unpack endMarker
++ "` end marker not found"
538 | isEndMarker l
= Right
$! BS
.unlines $ reverse acc
539 |
otherwise = goBody
(l
: acc
) ls
541 startMarker
, endMarker
:: BS
.ByteString
542 startMarker
= "{- " <> header
<> ":"
545 -- | The base for making a 'SourcePackage' for a fake project.
546 -- It needs a 'Distribution.Types.Library.Library' or 'Executable' depending on the command.
547 fakeProjectSourcePackage
:: FilePath -> SourcePackage
(PackageLocation loc
)
548 fakeProjectSourcePackage projectRoot
= sourcePackage
552 { srcpkgPackageId
= fakePackageId
553 , srcpkgDescription
= genericPackageDescription
554 , srcpkgSource
= LocalUnpackedPackage projectRoot
555 , srcpkgDescrOverride
= Nothing
557 genericPackageDescription
=
558 emptyGenericPackageDescription
559 { GPD
.packageDescription
= packageDescription
562 emptyPackageDescription
563 { package
= fakePackageId
564 , specVersion
= CabalSpecV2_2
565 , licenseRaw
= Left SPDX
.NONE
568 -- | Find the path of an exe that has been relocated with a "-o" option
569 movedExePath
:: UnqualComponentName
-> DistDirLayout
-> ElaboratedSharedConfig
-> ElaboratedConfiguredPackage
-> Maybe FilePath
570 movedExePath selectedComponent distDirLayout elabShared elabConfigured
= do
571 exe
<- find ((== selectedComponent
) . exeName
) . executables
$ elabPkgDescription elabConfigured
572 let CompilerId flavor _
= (compilerId
. pkgConfigCompiler
) elabShared
573 opts
<- lookup flavor
(perCompilerFlavorToList
. options
$ buildInfo exe
)
574 let projectRoot
= distProjectRootDirectory distDirLayout
575 fmap (projectRoot
</>) . lookup "-o" $ reverse (zip opts
(drop 1 opts
))
579 -- | A lens for the 'srcpkgDescription' field of 'SourcePackage'
580 lSrcpkgDescription
:: Lens
' (SourcePackage loc
) GenericPackageDescription
581 lSrcpkgDescription f s
= fmap (\x
-> s
{srcpkgDescription
= x
}) (f
(srcpkgDescription s
))
582 {-# INLINE lSrcpkgDescription #-}
584 lLocalPackages
:: Lens
' ProjectBaseContext
[PackageSpecifier UnresolvedSourcePackage
]
585 lLocalPackages f s
= fmap (\x
-> s
{localPackages
= x
}) (f
(localPackages s
))
586 {-# INLINE lLocalPackages #-}
588 lProjectConfig
:: Lens
' ProjectBaseContext ProjectConfig
589 lProjectConfig f s
= fmap (\x
-> s
{projectConfig
= x
}) (f
(projectConfig s
))
590 {-# INLINE lProjectConfig #-}
593 -- Transcribed from "templates/Lexer.x"
594 ccSpace
, ccCtrlchar
, ccPrintable
, ccSymbol
', ccParen
, ccNamecore
:: Set
Char
595 ccSpace
= S
.fromList
" "
596 ccCtrlchar
= S
.fromList
$ [chr 0x0 .. chr 0x1f] ++ [chr 0x7f]
597 ccPrintable
= S
.fromList
[chr 0x0 .. chr 0xff] S
.\\ ccCtrlchar
598 ccSymbol
' = S
.fromList
",=<>+*&|!$%^@#?/\\~"
599 ccParen
= S
.fromList
"()[]"
600 ccNamecore
= ccPrintable S
.\\ S
.unions
[ccSpace
, S
.fromList
":\"{}", ccParen
, ccSymbol
']