2022-10-10 07:14:56 +03:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
2022-03-28 07:53:39 +03:00
|
|
|
module Foliage.CmdBuild (cmdBuild) where
|
|
|
|
|
|
|
|
import Codec.Archive.Tar qualified as Tar
|
|
|
|
import Codec.Archive.Tar.Entry qualified as Tar
|
|
|
|
import Codec.Compression.GZip qualified as GZip
|
2022-03-30 07:56:17 +03:00
|
|
|
import Control.Monad (unless, when)
|
2022-10-10 07:14:56 +03:00
|
|
|
import Data.Aeson (object, (.=))
|
2022-03-28 07:53:39 +03:00
|
|
|
import Data.ByteString.Lazy qualified as BSL
|
2022-10-10 07:14:56 +03:00
|
|
|
import Data.Foldable (for_)
|
2022-09-22 14:32:05 +03:00
|
|
|
import Data.List (sortOn)
|
2022-03-29 12:10:19 +03:00
|
|
|
import Data.Maybe (fromMaybe)
|
2022-10-10 07:14:56 +03:00
|
|
|
import Data.Text.Lazy.IO qualified as TL
|
2022-03-28 07:53:39 +03:00
|
|
|
import Data.Traversable (for)
|
|
|
|
import Development.Shake
|
|
|
|
import Development.Shake.FilePath
|
2022-10-10 07:14:56 +03:00
|
|
|
import Distribution.Aeson
|
2022-05-19 09:51:21 +03:00
|
|
|
import Distribution.Package
|
2022-10-10 10:30:11 +03:00
|
|
|
import Distribution.PackageDescription (GenericPackageDescription)
|
2022-05-18 07:12:20 +03:00
|
|
|
import Distribution.Parsec (simpleParsec)
|
2022-09-23 18:46:18 +03:00
|
|
|
import Distribution.Pretty (prettyShow)
|
2022-10-10 07:14:56 +03:00
|
|
|
import Distribution.Simple.PackageDescription (readGenericPackageDescription)
|
|
|
|
import Distribution.Verbosity qualified as Verbosity
|
2022-10-10 07:14:56 +03:00
|
|
|
import Foliage.HackageSecurity hiding (ToJSON, toJSON)
|
2022-03-28 07:53:39 +03:00
|
|
|
import Foliage.Meta
|
2022-10-21 07:30:55 +03:00
|
|
|
import Foliage.Meta.Aeson ()
|
2022-03-28 07:53:39 +03:00
|
|
|
import Foliage.Options
|
2022-10-10 10:30:11 +03:00
|
|
|
import Foliage.Pages (packageVersionPageTemplate)
|
2022-10-05 10:11:48 +03:00
|
|
|
import Foliage.PrepareSdist
|
2022-09-22 19:54:35 +03:00
|
|
|
import Foliage.PrepareSource (addPrepareSourceRule, prepareSource)
|
|
|
|
import Foliage.RemoteAsset (addFetchRemoteAssetRule)
|
2022-03-28 07:53:39 +03:00
|
|
|
import Foliage.Shake
|
|
|
|
import Foliage.Time qualified as Time
|
2022-09-22 19:54:35 +03:00
|
|
|
import Hackage.Security.Util.Path (castRoot, toFilePath)
|
2022-10-10 10:30:11 +03:00
|
|
|
import System.Directory qualified as IO
|
|
|
|
import Text.Mustache (Template, renderMustache)
|
2022-03-28 07:53:39 +03:00
|
|
|
|
|
|
|
cmdBuild :: BuildOptions -> IO ()
|
2022-09-22 19:54:35 +03:00
|
|
|
cmdBuild buildOptions = do
|
2022-09-23 18:46:18 +03:00
|
|
|
outputDirRoot <- liftIO $ makeAbsolute (fromFilePath (buildOptsOutputDir buildOptions))
|
2022-09-22 19:54:35 +03:00
|
|
|
shake opts $
|
|
|
|
do
|
|
|
|
addFetchRemoteAssetRule cacheDir
|
|
|
|
addPrepareSourceRule (buildOptsInputDir buildOptions) cacheDir
|
2022-09-23 18:46:18 +03:00
|
|
|
addPrepareSdistRule outputDirRoot
|
2022-09-22 19:54:35 +03:00
|
|
|
phony "buildAction" (buildAction buildOptions)
|
|
|
|
want ["buildAction"]
|
|
|
|
where
|
|
|
|
cacheDir = "_cache"
|
|
|
|
opts =
|
|
|
|
shakeOptions
|
2022-09-23 18:46:18 +03:00
|
|
|
{ shakeFiles = cacheDir,
|
2022-10-05 11:24:06 +03:00
|
|
|
shakeVerbosity = Verbose
|
2022-09-22 19:54:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
buildAction :: BuildOptions -> Action ()
|
|
|
|
buildAction
|
2022-03-29 12:10:19 +03:00
|
|
|
BuildOptions
|
2022-09-19 16:33:50 +03:00
|
|
|
{ buildOptsSignOpts = signOpts,
|
2022-03-29 12:10:19 +03:00
|
|
|
buildOptsCurrentTime = mCurrentTime,
|
2022-06-08 05:39:46 +03:00
|
|
|
buildOptsExpireSignaturesOn = mExpireSignaturesOn,
|
2022-03-29 12:10:19 +03:00
|
|
|
buildOptsInputDir = inputDir,
|
|
|
|
buildOptsOutputDir = outputDir
|
|
|
|
} = do
|
2022-09-22 19:54:35 +03:00
|
|
|
outputDirRoot <- liftIO $ makeAbsolute (fromFilePath outputDir)
|
2022-09-23 13:00:43 +03:00
|
|
|
|
|
|
|
maybeReadKeysAt <- case signOpts of
|
2022-09-19 16:33:50 +03:00
|
|
|
SignOptsSignWithKeys keysPath -> do
|
2022-09-22 19:54:35 +03:00
|
|
|
ks <- doesDirectoryExist keysPath
|
2022-09-19 16:33:50 +03:00
|
|
|
unless ks $ do
|
2022-09-22 19:54:35 +03:00
|
|
|
putWarn $ "You don't seem to have created a set of TUF keys. I will create one in " <> keysPath
|
|
|
|
liftIO $ createKeys keysPath
|
2022-09-23 13:00:43 +03:00
|
|
|
return $ \name -> readKeysAt (keysPath </> name)
|
|
|
|
SignOptsDon'tSign ->
|
|
|
|
return $ const $ return []
|
2022-09-22 12:19:29 +03:00
|
|
|
|
2022-09-22 19:54:35 +03:00
|
|
|
expiryTime <-
|
|
|
|
for mExpireSignaturesOn $ \expireSignaturesOn -> do
|
|
|
|
putInfo $ "Expiry time set to " <> Time.iso8601Show expireSignaturesOn
|
|
|
|
return expireSignaturesOn
|
|
|
|
|
|
|
|
currentTime <- case mCurrentTime of
|
|
|
|
Nothing -> do
|
|
|
|
t <- Time.truncateSeconds <$> liftIO Time.getCurrentTime
|
|
|
|
putInfo $ "Current time set to " <> Time.iso8601Show t <> ". You can set a fixed time using the --current-time option."
|
|
|
|
return t
|
|
|
|
Just t -> do
|
|
|
|
putInfo $ "Current time set to " <> Time.iso8601Show t <> "."
|
|
|
|
return t
|
|
|
|
|
|
|
|
packages <- getPackages inputDir
|
|
|
|
|
2022-10-10 10:30:11 +03:00
|
|
|
for_ packages $ makePackageVersionPage inputDir outputDir packageVersionPageTemplate
|
2022-10-10 07:14:56 +03:00
|
|
|
|
2022-10-10 07:14:56 +03:00
|
|
|
cabalEntries <-
|
|
|
|
foldMap
|
2022-10-21 07:30:55 +03:00
|
|
|
( \pkgMeta@PackageVersionMeta{pkgId, pkgSpec} -> do
|
2022-10-10 10:30:11 +03:00
|
|
|
let PackageVersionSpec {packageVersionTimestamp, packageVersionRevisions} = pkgSpec
|
|
|
|
|
|
|
|
-- original cabal file, with its timestamp (if specified)
|
|
|
|
cabalFilePath <- originalCabalFile pkgMeta
|
2022-10-10 07:14:56 +03:00
|
|
|
let cabalFileTimestamp = fromMaybe currentTime packageVersionTimestamp
|
|
|
|
cf <- prepareIndexPkgCabal pkgId cabalFileTimestamp cabalFilePath
|
2022-10-10 10:30:11 +03:00
|
|
|
|
|
|
|
-- all revised cabal files, with their timestamp
|
2022-10-10 07:14:56 +03:00
|
|
|
revcf <-
|
|
|
|
for packageVersionRevisions $
|
2022-10-10 10:30:11 +03:00
|
|
|
\RevisionSpec {revisionTimestamp, revisionNumber} ->
|
2022-10-10 07:14:56 +03:00
|
|
|
prepareIndexPkgCabal
|
|
|
|
pkgId
|
|
|
|
revisionTimestamp
|
|
|
|
(cabalFileRevisionPath inputDir pkgId revisionNumber)
|
2022-09-23 18:46:18 +03:00
|
|
|
|
2022-10-10 07:14:56 +03:00
|
|
|
return $ cf : revcf
|
|
|
|
)
|
|
|
|
packages
|
2022-09-23 18:46:18 +03:00
|
|
|
|
2022-09-22 19:54:35 +03:00
|
|
|
targetKeys <- maybeReadKeysAt "target"
|
2022-10-10 10:30:11 +03:00
|
|
|
metadataEntries <-
|
|
|
|
for packages $
|
|
|
|
prepareIndexPkgMetadata currentTime expiryTime targetKeys
|
2022-09-23 18:46:18 +03:00
|
|
|
|
2022-10-10 07:14:56 +03:00
|
|
|
let tarContents = Tar.write $ sortOn Tar.entryTime (cabalEntries ++ metadataEntries)
|
2022-09-23 18:46:18 +03:00
|
|
|
traced "Writing index" $ do
|
|
|
|
BSL.writeFile (anchorPath outputDirRoot repoLayoutIndexTar) tarContents
|
|
|
|
BSL.writeFile (anchorPath outputDirRoot repoLayoutIndexTarGz) $ GZip.compress tarContents
|
|
|
|
|
|
|
|
privateKeysRoot <- maybeReadKeysAt "root"
|
|
|
|
privateKeysTarget <- maybeReadKeysAt "target"
|
|
|
|
privateKeysSnapshot <- maybeReadKeysAt "snapshot"
|
|
|
|
privateKeysTimestamp <- maybeReadKeysAt "timestamp"
|
|
|
|
privateKeysMirrors <- maybeReadKeysAt "mirrors"
|
|
|
|
|
|
|
|
liftIO $
|
|
|
|
writeSignedJSON outputDirRoot repoLayoutMirrors privateKeysMirrors $
|
|
|
|
Mirrors
|
|
|
|
{ mirrorsVersion = FileVersion 1,
|
|
|
|
mirrorsExpires = FileExpires expiryTime,
|
|
|
|
mirrorsMirrors = []
|
|
|
|
}
|
|
|
|
|
|
|
|
liftIO $
|
|
|
|
writeSignedJSON outputDirRoot repoLayoutRoot privateKeysRoot $
|
|
|
|
Root
|
|
|
|
{ rootVersion = FileVersion 1,
|
|
|
|
rootExpires = FileExpires expiryTime,
|
|
|
|
rootKeys =
|
|
|
|
fromKeys $
|
|
|
|
concat
|
|
|
|
[ privateKeysRoot,
|
|
|
|
privateKeysTarget,
|
|
|
|
privateKeysSnapshot,
|
|
|
|
privateKeysTimestamp,
|
|
|
|
privateKeysMirrors
|
|
|
|
],
|
|
|
|
rootRoles =
|
|
|
|
RootRoles
|
|
|
|
{ rootRolesRoot =
|
|
|
|
RoleSpec
|
|
|
|
{ roleSpecKeys = map somePublicKey privateKeysRoot,
|
|
|
|
roleSpecThreshold = KeyThreshold 2
|
|
|
|
},
|
|
|
|
rootRolesSnapshot =
|
|
|
|
RoleSpec
|
|
|
|
{ roleSpecKeys = map somePublicKey privateKeysSnapshot,
|
|
|
|
roleSpecThreshold = KeyThreshold 1
|
|
|
|
},
|
|
|
|
rootRolesTargets =
|
|
|
|
RoleSpec
|
|
|
|
{ roleSpecKeys = map somePublicKey privateKeysTarget,
|
|
|
|
roleSpecThreshold = KeyThreshold 1
|
|
|
|
},
|
|
|
|
rootRolesTimestamp =
|
|
|
|
RoleSpec
|
|
|
|
{ roleSpecKeys = map somePublicKey privateKeysTimestamp,
|
|
|
|
roleSpecThreshold = KeyThreshold 1
|
|
|
|
},
|
|
|
|
rootRolesMirrors =
|
|
|
|
RoleSpec
|
|
|
|
{ roleSpecKeys = map somePublicKey privateKeysMirrors,
|
|
|
|
roleSpecThreshold = KeyThreshold 1
|
2022-03-29 12:10:19 +03:00
|
|
|
}
|
|
|
|
}
|
2022-09-23 18:46:18 +03:00
|
|
|
}
|
2022-09-22 19:54:35 +03:00
|
|
|
|
2022-09-23 18:46:18 +03:00
|
|
|
rootInfo <- computeFileInfoSimple' (anchorPath outputDirRoot repoLayoutRoot)
|
|
|
|
mirrorsInfo <- computeFileInfoSimple' (anchorPath outputDirRoot repoLayoutMirrors)
|
|
|
|
tarInfo <- computeFileInfoSimple' (anchorPath outputDirRoot repoLayoutIndexTar)
|
|
|
|
tarGzInfo <- computeFileInfoSimple' (anchorPath outputDirRoot repoLayoutIndexTarGz)
|
|
|
|
|
|
|
|
liftIO $
|
|
|
|
writeSignedJSON outputDirRoot repoLayoutSnapshot privateKeysSnapshot $
|
|
|
|
Snapshot
|
|
|
|
{ snapshotVersion = FileVersion 1,
|
|
|
|
snapshotExpires = FileExpires expiryTime,
|
|
|
|
snapshotInfoRoot = rootInfo,
|
|
|
|
snapshotInfoMirrors = mirrorsInfo,
|
|
|
|
snapshotInfoTar = Just tarInfo,
|
|
|
|
snapshotInfoTarGz = tarGzInfo
|
|
|
|
}
|
2022-09-22 19:54:35 +03:00
|
|
|
|
2022-09-23 18:46:18 +03:00
|
|
|
snapshotInfo <- computeFileInfoSimple' (anchorPath outputDirRoot repoLayoutSnapshot)
|
|
|
|
liftIO $
|
|
|
|
writeSignedJSON outputDirRoot repoLayoutTimestamp privateKeysTimestamp $
|
|
|
|
Timestamp
|
|
|
|
{ timestampVersion = FileVersion 1,
|
|
|
|
timestampExpires = FileExpires expiryTime,
|
|
|
|
timestampInfoSnapshot = snapshotInfo
|
|
|
|
}
|
2022-09-22 19:54:35 +03:00
|
|
|
|
2022-10-10 10:30:11 +03:00
|
|
|
makePackageVersionPage :: FilePath -> FilePath -> Template -> PackageVersionMeta -> Action ()
|
|
|
|
makePackageVersionPage inputDir outputDir pageTemplate pkgMeta@PackageVersionMeta {pkgId, pkgSpec} = do
|
|
|
|
cabalFilePath <- maybe (originalCabalFile pkgMeta) pure (revisedCabalFile inputDir pkgMeta)
|
|
|
|
pkgDesc <- readGenericPackageDescription' cabalFilePath
|
|
|
|
liftIO $ do
|
|
|
|
IO.createDirectoryIfMissing True (outputDir </> "package" </> prettyShow pkgId)
|
|
|
|
TL.writeFile (outputDir </> "package" </> prettyShow pkgId </> "index.html") $
|
|
|
|
renderMustache pageTemplate $
|
|
|
|
object
|
|
|
|
[ "pkgSpec" .= pkgSpec,
|
|
|
|
"pkgDesc" .= jsonGenericPackageDescription pkgDesc
|
|
|
|
]
|
|
|
|
|
|
|
|
getPackages :: FilePath -> Action [PackageVersionMeta]
|
2022-09-22 19:54:35 +03:00
|
|
|
getPackages inputDir = do
|
|
|
|
metaFiles <- getDirectoryFiles inputDir ["*/*/meta.toml"]
|
|
|
|
|
|
|
|
when (null metaFiles) $ do
|
|
|
|
putError $
|
|
|
|
unlines
|
|
|
|
[ "We could not find any package metadata file (i.e. _sources/<name>/<version>/meta.toml)",
|
|
|
|
"Make sure you are passing the right input directory. The default input directory is _sources"
|
|
|
|
]
|
|
|
|
fail "no package metadata found"
|
|
|
|
|
|
|
|
for metaFiles $ \metaFile -> do
|
|
|
|
let [pkgName, pkgVersion, _] = splitDirectories metaFile
|
|
|
|
let Just name = simpleParsec pkgName
|
|
|
|
let Just version = simpleParsec pkgVersion
|
|
|
|
let pkgId = PackageIdentifier name version
|
|
|
|
|
2022-10-10 10:30:11 +03:00
|
|
|
pkgSpec <-
|
|
|
|
readPackageVersionSpec' (inputDir </> metaFile) >>= \case
|
|
|
|
PackageVersionSpec {packageVersionRevisions, packageVersionTimestamp = Nothing}
|
2022-09-22 19:54:35 +03:00
|
|
|
| not (null packageVersionRevisions) -> do
|
|
|
|
putError $
|
2022-10-10 10:30:11 +03:00
|
|
|
unlines
|
|
|
|
[ inputDir </> metaFile <> " has cabal file revisions but the original package has no timestamp.",
|
|
|
|
"This combination doesn't make sense. Either add a timestamp on the original package or remove the revisions"
|
|
|
|
]
|
2022-09-22 19:54:35 +03:00
|
|
|
fail "invalid package metadata"
|
2022-10-10 10:30:11 +03:00
|
|
|
PackageVersionSpec {packageVersionRevisions, packageVersionTimestamp = Just pkgTs}
|
2022-09-22 19:54:35 +03:00
|
|
|
| any ((< pkgTs) . revisionTimestamp) packageVersionRevisions -> do
|
|
|
|
putError $
|
2022-10-10 10:30:11 +03:00
|
|
|
unlines
|
|
|
|
[ inputDir </> metaFile <> " has a revision with timestamp earlier than the package itself.",
|
|
|
|
"Adjust the timestamps so that all revisions come after the original package"
|
|
|
|
]
|
2022-09-22 19:54:35 +03:00
|
|
|
fail "invalid package metadata"
|
|
|
|
meta ->
|
|
|
|
return meta
|
2022-10-10 10:30:11 +03:00
|
|
|
|
|
|
|
return $ PackageVersionMeta pkgId pkgSpec
|
|
|
|
|
|
|
|
readGenericPackageDescription' :: FilePath -> Action GenericPackageDescription
|
|
|
|
readGenericPackageDescription' fp = do
|
|
|
|
need [fp]
|
|
|
|
liftIO $ readGenericPackageDescription Verbosity.silent fp
|
2022-09-22 19:54:35 +03:00
|
|
|
|
2022-09-23 18:46:18 +03:00
|
|
|
prepareIndexPkgCabal :: PackageId -> UTCTime -> FilePath -> Action Tar.Entry
|
|
|
|
prepareIndexPkgCabal pkgId timestamp filePath = do
|
|
|
|
need [filePath]
|
|
|
|
contents <- liftIO $ BSL.readFile filePath
|
|
|
|
return $ mkTarEntry contents (IndexPkgCabal pkgId) timestamp
|
|
|
|
|
2022-10-10 10:30:11 +03:00
|
|
|
prepareIndexPkgMetadata :: UTCTime -> Maybe UTCTime -> [Some Key] -> PackageVersionMeta -> Action Tar.Entry
|
|
|
|
prepareIndexPkgMetadata currentTime expiryTime keys PackageVersionMeta {pkgId, pkgSpec} = do
|
|
|
|
let PackageVersionSpec {packageVersionTimestamp} = pkgSpec
|
|
|
|
srcDir <- prepareSource pkgId pkgSpec
|
2022-10-05 11:24:06 +03:00
|
|
|
sdist <- prepareSdist srcDir
|
2022-09-23 18:46:18 +03:00
|
|
|
targetFileInfo <- computeFileInfoSimple' sdist
|
|
|
|
let packagePath = repoLayoutPkgTarGz hackageRepoLayout pkgId
|
|
|
|
let targets =
|
|
|
|
Targets
|
|
|
|
{ targetsVersion = FileVersion 1,
|
|
|
|
targetsExpires = FileExpires expiryTime,
|
|
|
|
targetsTargets = fromList [(TargetPathRepo packagePath, targetFileInfo)],
|
|
|
|
targetsDelegations = Nothing
|
|
|
|
}
|
2022-09-22 19:54:35 +03:00
|
|
|
|
2022-09-23 18:46:18 +03:00
|
|
|
return $
|
|
|
|
mkTarEntry
|
|
|
|
(renderSignedJSON keys targets)
|
|
|
|
(IndexPkgMetadata pkgId)
|
|
|
|
(fromMaybe currentTime packageVersionTimestamp)
|
2022-09-22 19:54:35 +03:00
|
|
|
|
|
|
|
mkTarEntry :: BSL.ByteString -> IndexFile dec -> UTCTime -> Tar.Entry
|
|
|
|
mkTarEntry contents indexFile timestamp =
|
|
|
|
(Tar.fileEntry tarPath contents)
|
|
|
|
{ Tar.entryTime = floor $ Time.utcTimeToPOSIXSeconds timestamp,
|
|
|
|
Tar.entryOwnership =
|
|
|
|
Tar.Ownership
|
|
|
|
{ Tar.ownerName = "foliage",
|
|
|
|
Tar.groupName = "foliage",
|
|
|
|
Tar.ownerId = 0,
|
|
|
|
Tar.groupId = 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
where
|
|
|
|
indexPath = toFilePath $ castRoot $ indexFileToPath hackageIndexLayout indexFile
|
|
|
|
Right tarPath = Tar.toTarPath False indexPath
|
|
|
|
|
|
|
|
anchorPath :: Path Absolute -> (RepoLayout -> RepoPath) -> FilePath
|
|
|
|
anchorPath outputDirRoot p =
|
|
|
|
toFilePath $ anchorRepoPathLocally outputDirRoot $ p hackageRepoLayout
|
2022-09-23 18:46:18 +03:00
|
|
|
|
|
|
|
cabalFileRevisionPath :: FilePath -> PackageIdentifier -> Int -> FilePath
|
|
|
|
cabalFileRevisionPath inputDir PackageIdentifier {pkgName, pkgVersion} revisionNumber =
|
|
|
|
inputDir </> unPackageName pkgName </> prettyShow pkgVersion </> "revisions" </> show revisionNumber <.> "cabal"
|
2022-10-10 10:30:11 +03:00
|
|
|
|
|
|
|
originalCabalFile :: PackageVersionMeta -> Action FilePath
|
|
|
|
originalCabalFile PackageVersionMeta {pkgId, pkgSpec} = do
|
|
|
|
srcDir <- prepareSource pkgId pkgSpec
|
|
|
|
return $ srcDir </> unPackageName (pkgName pkgId) <.> "cabal"
|
|
|
|
|
|
|
|
revisedCabalFile :: FilePath -> PackageVersionMeta -> Maybe FilePath
|
|
|
|
revisedCabalFile inputDir PackageVersionMeta {pkgId, pkgSpec} = do
|
|
|
|
cabalFileRevisionPath inputDir pkgId <$> latestRevisionNumber pkgSpec
|