1 {-# LANGUAGE LambdaCase #-}
2 {-# LANGUAGE NamedFieldPuns #-}
3 {-# LANGUAGE OverloadedStrings #-}
4 {-# LANGUAGE RecordWildCards #-}
6 -- | Utilities to help commands with scripts
8 module Distribution
.Client
.ScriptUtils
(
9 getScriptHash
, getScriptCacheDirectory
, ensureScriptCacheDirectory
,
10 withContextAndSelectors
, AcceptNoTargets
(..), TargetContext
(..),
11 updateContextAndWriteProjectFile
, updateContextAndWriteProjectFile
',
12 fakeProjectSourcePackage
, lSrcpkgDescription
16 import Distribution
.Client
.Compat
.Prelude
hiding (toList
)
18 import Distribution
.Compat
.Lens
19 import qualified Distribution
.Types
.Lens
as L
21 import Distribution
.CabalSpecVersion
22 ( CabalSpecVersion
(..), cabalSpecLatest
)
23 import Distribution
.Client
.ProjectOrchestration
24 import Distribution
.Client
.Config
25 ( defaultScriptBuildsDir
)
26 import Distribution
.Client
.DistDirLayout
28 import Distribution
.Client
.HashValue
29 ( hashValue
, showHashValue
)
30 import Distribution
.Client
.HttpUtils
31 ( HttpTransport
, configureTransport
)
32 import Distribution
.Client
.NixStyleOptions
33 ( NixStyleFlags
(..) )
34 import Distribution
.Client
.ProjectConfig
35 ( ProjectConfig
(..), ProjectConfigShared
(..)
36 , reportParseResult
, withProjectOrGlobalConfig
37 , projectConfigHttpTransport
)
38 import Distribution
.Client
.ProjectConfig
.Legacy
39 ( ProjectConfigSkeleton
40 , parseProjectSkeleton
, instantiateProjectConfigSkeletonFetchingCompiler
)
41 import Distribution
.Client
.ProjectFlags
43 import Distribution
.Client
.RebuildMonad
45 import Distribution
.Client
.Setup
46 ( ConfigFlags
(..), GlobalFlags
(..) )
47 import Distribution
.Client
.TargetSelector
48 ( TargetSelectorProblem
(..), TargetString
(..) )
49 import Distribution
.Client
.Types
50 ( PackageLocation
(..), PackageSpecifier
(..), UnresolvedSourcePackage
)
51 import Distribution
.FieldGrammar
52 ( parseFieldGrammar
, takeFields
)
53 import Distribution
.Fields
54 ( ParseResult
, parseFatalFailure
, readFields
)
55 import Distribution
.PackageDescription
57 import Distribution
.PackageDescription
.FieldGrammar
58 ( executableFieldGrammar
)
59 import Distribution
.PackageDescription
.PrettyPrint
60 ( showGenericPackageDescription
)
61 import Distribution
.Parsec
63 import Distribution
.Simple
.Flag
64 ( fromFlagOrDefault
, flagToMaybe
)
65 import Distribution
.Simple
.PackageDescription
67 import Distribution
.Simple
.Setup
69 import Distribution
.Simple
.Compiler
71 import Distribution
.Simple
.Utils
72 ( createDirectoryIfMissingVerbose
, createTempDirectory
, die
', handleDoesNotExist
, readUTF8File
, warn
, writeUTF8File
)
73 import qualified Distribution
.SPDX
.License
as SPDX
74 import Distribution
.Solver
.Types
.SourcePackage
as SP
76 import Distribution
.System
78 import Distribution
.Types
.BuildInfo
80 import Distribution
.Types
.CondTree
82 import Distribution
.Types
.Executable
84 import Distribution
.Types
.GenericPackageDescription
as GPD
85 ( GenericPackageDescription
(..), emptyGenericPackageDescription
)
86 import Distribution
.Types
.PackageDescription
87 ( PackageDescription
(..), emptyPackageDescription
)
88 import Distribution
.Types
.PackageName
.Magic
89 ( fakePackageCabalFileName
, fakePackageId
)
90 import Distribution
.Utils
.NubList
92 import Distribution
.Client
.ProjectPlanning
94 import Distribution
.Verbosity
96 import Language
.Haskell
.Extension
99 import Control
.Concurrent
.MVar
100 ( newEmptyMVar
, putMVar
, tryTakeMVar
)
101 import Control
.Exception
103 import qualified Data
.ByteString
.Char8
as BS
104 import Data
.ByteString
.Lazy
()
105 import qualified Data
.Set
as S
106 import System
.Directory
107 ( canonicalizePath
, doesFileExist, getTemporaryDirectory
, removeDirectoryRecursive
)
108 import System
.FilePath
109 ( (</>), takeFileName
)
110 import qualified Text
.Parsec
as P
112 -- A note on multi-module script support #6787:
113 -- Multi-module scripts are not supported and support is non-trivial.
114 -- What you want to do is pass the absolute path to the script's directory in hs-source-dirs,
115 -- but hs-source-dirs only accepts relative paths. This leaves you with several options none
116 -- of which are particularly appealing.
117 -- 1) Loosen the requirement that hs-source-dirs take relative paths
118 -- 2) Add a field to BuildInfo that acts like an hs-source-dir, but accepts an absolute path
119 -- 3) Use a path relative to the project root in hs-source-dirs, and pass extra flags to the
120 -- repl to deal with the fact that the repl is relative to the working directory and not
123 -- | Get the hash of a script's absolute path)
125 -- Two hashes will be the same as long as the absolute paths
127 getScriptHash
:: FilePath -> IO String
128 getScriptHash script
= showHashValue
. hashValue
. fromString
<$> canonicalizePath script
130 -- | Get the directory for caching a script build.
132 -- The only identity of a script is it's absolute path, so append the
133 -- hashed path to the @script-builds@ dir to get the cache directory.
134 getScriptCacheDirectory
:: FilePath -> IO FilePath
135 getScriptCacheDirectory script
= (</>) <$> defaultScriptBuildsDir
<*> getScriptHash script
137 -- | Get the directory for caching a script build and ensure it exists.
139 -- The only identity of a script is it's absolute path, so append the
140 -- hashed path to the @script-builds@ dir to get the cache directory.
141 ensureScriptCacheDirectory
:: Verbosity
-> FilePath -> IO FilePath
142 ensureScriptCacheDirectory verbosity script
= do
143 cacheDir
<- getScriptCacheDirectory script
144 createDirectoryIfMissingVerbose verbosity
True cacheDir
147 -- | What your command should do when no targets are found.
149 = RejectNoTargets
-- ^ die on 'TargetSelectorNoTargetsInProject'
150 | AcceptNoTargets
-- ^ return a default 'TargetSelector'
153 -- | Information about the context in which we found the 'TargetSelector's.
155 = ProjectContext
-- ^ The target selectors are part of a project.
156 | GlobalContext
-- ^ The target selectors are from the global context.
157 | ScriptContext
FilePath Executable
158 -- ^ The target selectors refer to a script. Contains the path to the script and
159 -- the executable metadata parsed from the script
162 -- | Determine whether the targets represent regular targets or a script
163 -- and return the proper context and target selectors.
164 -- Die with an error message if selectors are valid as neither regular targets or as a script.
166 -- In the case that the context refers to a temporary directory,
167 -- delete it after the action finishes.
168 withContextAndSelectors
169 :: AcceptNoTargets
-- ^ What your command should do when no targets are found.
170 -> Maybe ComponentKind
-- ^ A target filter
171 -> NixStyleFlags a
-- ^ Command line flags
172 -> [String] -- ^ Target strings or a script and args.
173 -> GlobalFlags
-- ^ Global flags.
174 -> CurrentCommand
-- ^ Current Command (usually for error reporting).
175 -> (TargetContext
-> ProjectBaseContext
-> [TargetSelector
] -> IO b
)
176 -- ^ The body of your command action.
178 withContextAndSelectors noTargets kind flags
@NixStyleFlags
{..} targetStrings globalFlags cmd act
179 = withTemporaryTempDirectory
$ \mkTmpDir
-> do
180 (tc
, ctx
) <- withProjectOrGlobalConfig verbosity ignoreProject globalConfigFlag with
(without mkTmpDir
)
182 (tc
', ctx
', sels
) <- case targetStrings
of
183 -- Only script targets may contain spaces and or end with ':'.
184 -- Trying to readTargetSelectors such a target leads to a parse error.
185 [target
] |
any (\c
-> isSpace c
) target ||
":" `
isSuffixOf` target
-> do
186 scriptOrError target
[TargetSelectorNoScript
$ TargetString1 target
]
188 -- In the case where a selector is both a valid target and script, assume it is a target,
189 -- because you can disambiguate the script with "./script"
190 readTargetSelectors
(localPackages ctx
) kind targetStrings
>>= \case
191 Left err
@(TargetSelectorNoTargetsInProject
:_
)
192 |
[] <- targetStrings
193 , AcceptNoTargets
<- noTargets
-> return (tc
, ctx
, defaultTarget
)
194 |
(script
:_
) <- targetStrings
-> scriptOrError script err
195 Left err
@(TargetSelectorNoSuch t _
:_
)
196 | TargetString1 script
<- t
-> scriptOrError script err
197 Left err
@(TargetSelectorExpected t _ _
:_
)
198 | TargetString1 script
<- t
-> scriptOrError script err
199 Left err
@(MatchingInternalError _ _ _
:_
) -- Handle ':' in middle of script name.
200 |
[script
] <- targetStrings
-> scriptOrError script err
201 Left err
-> reportTargetSelectorProblems verbosity err
202 Right sels
-> return (tc
, ctx
, sels
)
206 verbosity
= fromFlagOrDefault normal
(configVerbosity configFlags
)
207 ignoreProject
= flagIgnoreProject projectFlags
208 cliConfig
= commandLineFlagsToProjectConfig globalFlags flags mempty
209 globalConfigFlag
= projectConfigConfigFile
(projectConfigShared cliConfig
)
210 defaultTarget
= [TargetPackage TargetExplicitNamed
[fakePackageId
] Nothing
]
213 ctx
<- establishProjectBaseContext verbosity cliConfig cmd
214 return (ProjectContext
, ctx
)
215 without mkDir globalConfig
= do
216 distDirLayout
<- establishDummyDistDirLayout verbosity
(globalConfig
<> cliConfig
) =<< mkDir
217 ctx
<- establishDummyProjectBaseContext verbosity
(globalConfig
<> cliConfig
) distDirLayout
[] cmd
218 return (GlobalContext
, ctx
)
219 scriptOrError script err
= do
220 exists
<- doesFileExist script
222 -- In the script case we always want a dummy context even when ignoreProject is False
223 let mkCacheDir
= ensureScriptCacheDirectory verbosity script
224 (_
, ctx
) <- withProjectOrGlobalConfig verbosity
(Flag
True) globalConfigFlag with
(without mkCacheDir
)
226 let projectRoot
= distProjectRootDirectory
$ distDirLayout ctx
227 writeFile (projectRoot
</> "scriptlocation") =<< canonicalizePath script
229 scriptContents
<- BS
.readFile script
230 executable <- readExecutableBlockFromScript verbosity scriptContents
233 httpTransport
<- configureTransport verbosity
234 (fromNubList
. projectConfigProgPathExtra
$ projectConfigShared cliConfig
)
235 (flagToMaybe
. projectConfigHttpTransport
$ projectConfigBuildOnly cliConfig
)
237 projectCfgSkeleton
<- readProjectBlockFromScript verbosity httpTransport
(distDirLayout ctx
) (takeFileName script
) scriptContents
239 let fetchCompiler
= do
240 (compiler
, Platform arch os
, _
) <- runRebuild
(distProjectRootDirectory
. distDirLayout
$ ctx
) $ configureCompiler verbosity
(distDirLayout ctx
) ((fst $ ignoreConditions projectCfgSkeleton
) <> projectConfig ctx
)
241 pure
(os
, arch
, compilerInfo compiler
)
243 projectCfg
<- instantiateProjectConfigSkeletonFetchingCompiler fetchCompiler mempty projectCfgSkeleton
245 let executable' = executable & L
.buildInfo
. L
.defaultLanguage
%~
maybe (Just Haskell2010
) Just
246 ctx
' = ctx
& lProjectConfig
%~
(<> projectCfg
)
247 return (ScriptContext script
executable', ctx
', defaultTarget
)
248 else reportTargetSelectorProblems verbosity err
250 withTemporaryTempDirectory
:: (IO FilePath -> IO a
) -> IO a
251 withTemporaryTempDirectory act
= newEmptyMVar
>>= \m
-> bracket (getMkTmp m
) (rmTmp m
) act
253 -- We return an (IO Filepath) instead of a FilePath for two reasons:
254 -- 1) To give the consumer the discretion to not create the tmpDir,
255 -- but still grantee that it's deleted if they do create it
256 -- 2) Because the path returned by createTempDirectory is not predicable
257 getMkTmp m
= return $ do
258 tmpDir
<- getTemporaryDirectory
>>= flip createTempDirectory
"cabal-repl."
261 rmTmp m _
= tryTakeMVar m
>>= maybe (return ()) (handleDoesNotExist
() . removeDirectoryRecursive
)
263 -- | Add the 'SourcePackage' to the context and use it to write a .cabal file.
264 updateContextAndWriteProjectFile
' :: ProjectBaseContext
-> SourcePackage
(PackageLocation
(Maybe FilePath)) -> IO ProjectBaseContext
265 updateContextAndWriteProjectFile
' ctx srcPkg
= do
266 let projectRoot
= distProjectRootDirectory
$ distDirLayout ctx
267 packageFile
= projectRoot
</> fakePackageCabalFileName
268 contents
= showGenericPackageDescription
(srcpkgDescription srcPkg
)
269 writePackageFile
= writeUTF8File packageFile contents
270 -- TODO This is here to prevent reconfiguration of cached repl packages.
271 -- It's worth investigating why it's needed in the first place.
272 packageFileExists
<- doesFileExist packageFile
273 if packageFileExists
then do
274 cached
<- force
<$> readUTF8File packageFile
275 when (cached
/= contents
)
277 else writePackageFile
278 return (ctx
& lLocalPackages
%~
(++ [SpecificSourcePackage srcPkg
]))
280 -- | Add add the executable metadata to the context and write a .cabal file.
281 updateContextAndWriteProjectFile
:: ProjectBaseContext
-> FilePath -> Executable
-> IO ProjectBaseContext
282 updateContextAndWriteProjectFile ctx scriptPath scriptExecutable
= do
283 let projectRoot
= distProjectRootDirectory
$ distDirLayout ctx
285 absScript
<- canonicalizePath scriptPath
287 -- Replace characters which aren't allowed in the executable component name with '_'
288 -- Prefix with "cabal-script-" to make it clear to end users that the name may be mangled
289 scriptExeName
= "cabal-script-" ++ map censor
(takeFileName scriptPath
)
290 censor c | c `S
.member` ccNamecore
= c
293 sourcePackage
= fakeProjectSourcePackage projectRoot
294 & lSrcpkgDescription
. L
.condExecutables
295 .~
[(fromString scriptExeName
, CondNode
executable (targetBuildDepends
$ buildInfo
executable) [])]
296 executable = scriptExecutable
297 & L
.modulePath
.~ absScript
299 updateContextAndWriteProjectFile
' ctx sourcePackage
301 parseScriptBlock
:: BS
.ByteString
-> ParseResult Executable
302 parseScriptBlock str
=
303 case readFields str
of
305 let (fields
, _
) = takeFields fs
306 parseFieldGrammar cabalSpecLatest fields
(executableFieldGrammar
"script")
307 Left perr
-> parseFatalFailure pos
(show perr
) where
308 ppos
= P
.errorPos perr
309 pos
= Position
(P
.sourceLine ppos
) (P
.sourceColumn ppos
)
311 readScriptBlock
:: Verbosity
-> BS
.ByteString
-> IO Executable
312 readScriptBlock verbosity
= parseString parseScriptBlock verbosity
"script block"
314 -- | Extract the first encountered executable metadata block started and
315 -- terminated by the below tokens or die.
321 -- Return the metadata.
322 readExecutableBlockFromScript
:: Verbosity
-> BS
.ByteString
-> IO Executable
323 readExecutableBlockFromScript verbosity str
= do
324 str
' <- case extractScriptBlock
"cabal" str
of
325 Left e
-> die
' verbosity
$ "Failed extracting script block: " ++ e
327 when (BS
.all isSpace str
') $ warn verbosity
"Empty script block"
328 readScriptBlock verbosity str
'
330 -- | Extract the first encountered project metadata block started and
331 -- terminated by the below tokens.
337 -- Return the metadata.
338 readProjectBlockFromScript
:: Verbosity
-> HttpTransport
-> DistDirLayout
-> String -> BS
.ByteString
-> IO ProjectConfigSkeleton
339 readProjectBlockFromScript verbosity httpTransport DistDirLayout
{distDownloadSrcDirectory
} scriptName str
= do
340 case extractScriptBlock
"project" str
of
341 Left _
-> return mempty
342 Right x
-> reportParseResult verbosity
"script" scriptName
343 =<< parseProjectSkeleton distDownloadSrcDirectory httpTransport verbosity
[] scriptName x
345 -- | Extract the first encountered script metadata block started end
346 -- terminated by the tokens
352 -- appearing alone on lines (while tolerating trailing whitespace).
353 -- These tokens are not part of the 'Right' result.
355 -- In case of missing or unterminated blocks a 'Left'-error is
357 extractScriptBlock
:: BS
.ByteString
-> BS
.ByteString
-> Either String BS
.ByteString
358 extractScriptBlock header str
= goPre
(BS
.lines str
)
360 isStartMarker
= (== startMarker
) . stripTrailSpace
361 isEndMarker
= (== endMarker
) . stripTrailSpace
363 stripTrailSpace
= fst . BS
.spanEnd
isSpace
365 -- before start marker
366 goPre ls
= case dropWhile (not . isStartMarker
) ls
of
367 [] -> Left
$ "`" ++ BS
.unpack startMarker
++ "` start marker not found"
368 (_
:ls
') -> goBody
[] ls
'
370 goBody _
[] = Left
$ "`" ++ BS
.unpack endMarker
++ "` end marker not found"
372 | isEndMarker l
= Right
$! BS
.unlines $ reverse acc
373 |
otherwise = goBody
(l
:acc
) ls
375 startMarker
, endMarker
:: BS
.ByteString
376 startMarker
= "{- " <> header
<> ":"
379 -- | The base for making a 'SourcePackage' for a fake project.
380 -- It needs a 'Distribution.Types.Library.Library' or 'Executable' depending on the command.
381 fakeProjectSourcePackage
:: FilePath -> SourcePackage
(PackageLocation loc
)
382 fakeProjectSourcePackage projectRoot
= sourcePackage
384 sourcePackage
= SourcePackage
385 { srcpkgPackageId
= fakePackageId
386 , srcpkgDescription
= genericPackageDescription
387 , srcpkgSource
= LocalUnpackedPackage projectRoot
388 , srcpkgDescrOverride
= Nothing
390 genericPackageDescription
= emptyGenericPackageDescription
391 { GPD
.packageDescription
= packageDescription
}
392 packageDescription
= emptyPackageDescription
393 { package
= fakePackageId
394 , specVersion
= CabalSpecV2_2
395 , licenseRaw
= Left SPDX
.NONE
399 -- | A lens for the 'srcpkgDescription' field of 'SourcePackage'
400 lSrcpkgDescription
:: Lens
' (SourcePackage loc
) GenericPackageDescription
401 lSrcpkgDescription f s
= fmap (\x
-> s
{ srcpkgDescription
= x
}) (f
(srcpkgDescription s
))
402 {-# inline lSrcpkgDescription #-}
404 lLocalPackages
:: Lens
' ProjectBaseContext
[PackageSpecifier UnresolvedSourcePackage
]
405 lLocalPackages f s
= fmap (\x
-> s
{ localPackages
= x
}) (f
(localPackages s
))
406 {-# inline lLocalPackages #-}
408 lProjectConfig
:: Lens
' ProjectBaseContext ProjectConfig
409 lProjectConfig f s
= fmap (\x
-> s
{ projectConfig
= x
}) (f
(projectConfig s
))
410 {-# inline lProjectConfig #-}
413 -- Transcribed from "templates/Lexer.x"
414 ccSpace
, ccCtrlchar
, ccPrintable
, ccSymbol
', ccParen
, ccNamecore
:: Set
Char
415 ccSpace
= S
.fromList
" "
416 ccCtrlchar
= S
.fromList
$ [chr 0x0 .. chr 0x1f] ++ [chr 0x7f]
417 ccPrintable
= S
.fromList
[chr 0x0 .. chr 0xff] S
.\\ ccCtrlchar
418 ccSymbol
' = S
.fromList
",=<>+*&|!$%^@#?/\\~"
419 ccParen
= S
.fromList
"()[]"
420 ccNamecore
= ccPrintable S
.\\ S
.unions
[ccSpace
, S
.fromList
":\"{}", ccParen
, ccSymbol
']