foliage/app/Foliage/CmdBuild.hs

329 lines
13 KiB
Haskell
Raw Normal View History

2022-10-10 07:14:56 +03:00
{-# LANGUAGE OverloadedStrings #-}
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
import Control.Monad (unless, when)
2022-10-10 07:14:56 +03:00
import Data.Aeson (object, (.=))
import Data.ByteString.Lazy qualified as BSL
2022-10-10 07:14:56 +03:00
import Data.Foldable (for_)
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
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)
import Foliage.Meta
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)
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)
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,
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-10 10:30:11 +03:00
( \pkgMeta@PackageVersionMeta {pkgId, pkgSpec} -> do
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