foliage/app/Foliage/CmdBuild.hs

362 lines
14 KiB
Haskell
Raw Normal View History

2022-10-21 11:46:50 +03:00
{-# LANGUAGE DerivingVia #-}
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
2023-01-29 07:27:04 +03:00
import Control.Monad (unless, void, when)
2023-02-13 11:54:30 +03:00
import Data.Aeson qualified as Aeson
import Data.ByteString qualified as BS
2022-10-24 11:55:58 +03:00
import Data.ByteString.Lazy qualified as BL
import Data.List (sortOn)
2022-03-29 12:10:19 +03:00
import Data.Maybe (fromMaybe)
import Data.Text qualified as T
import Data.Traversable (for)
import Development.Shake
import Development.Shake.FilePath
2022-05-19 09:51:21 +03:00
import Distribution.Package
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 Foliage.HackageSecurity hiding (ToJSON, toJSON)
import Foliage.Meta
2022-10-21 07:30:55 +03:00
import Foliage.Meta.Aeson ()
import Foliage.Options
import Foliage.Pages
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)
import Network.URI (URI (uriPath, uriQuery, uriScheme), nullURI)
import System.Directory (createDirectoryIfMissing)
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,
shakeVerbosity = Verbose,
shakeThreads = buildOptsNumThreads buildOptions
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,
buildOptsWriteMetadata = doWritePackageMeta
2022-03-29 12:10:19 +03:00
} = 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
packageVersions <- getPackageVersions inputDir
2022-09-22 19:54:35 +03:00
2022-10-27 06:56:00 +03:00
makeIndexPage outputDir
2022-10-21 11:46:50 +03:00
2022-10-27 06:56:00 +03:00
makeAllPackagesPage currentTime outputDir packageVersions
makeAllPackageVersionsPage currentTime outputDir packageVersions
2022-10-10 07:14:56 +03:00
when doWritePackageMeta $
makeMetadataFile outputDir packageVersions
2023-02-13 11:54:30 +03:00
void $ forP packageVersions $ uncurry $ makePackageVersionPage inputDir outputDir
void $ forP packageVersions $ \(pkgId, pkgSpec) -> do
2022-10-24 11:55:58 +03:00
let PackageIdentifier {pkgName, pkgVersion} = pkgId
cabalFilePath <- maybe (originalCabalFile pkgId pkgSpec) pure (revisedCabalFile inputDir pkgId pkgSpec)
2022-10-24 11:55:58 +03:00
copyFileChanged cabalFilePath (outputDir </> "index" </> prettyShow pkgName </> prettyShow pkgVersion </> prettyShow pkgName <.> "cabal")
2022-10-10 07:14:56 +03:00
cabalEntries <-
foldMap
( \(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 pkgId pkgSpec
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
)
packageVersions
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 <-
forP packageVersions $ \(pkgId, pkgSpec) -> do
2022-10-24 11:55:58 +03:00
let PackageIdentifier {pkgName, pkgVersion} = pkgId
let PackageVersionSpec {packageVersionTimestamp} = pkgSpec
targets <- prepareIndexPkgMetadata expiryTime pkgId pkgSpec
2022-10-24 11:55:58 +03:00
let path = outputDir </> "index" </> prettyShow pkgName </> prettyShow pkgVersion </> "package.json"
liftIO $ BL.writeFile path $ renderSignedJSON targetKeys targets
2023-03-02 05:48:31 +03:00
mkTarEntry
(renderSignedJSON targetKeys targets)
(IndexPkgMetadata pkgId)
(fromMaybe currentTime packageVersionTimestamp)
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
2023-01-29 07:27:04 +03:00
BL.writeFile (anchorPath outputDirRoot repoLayoutIndexTar) tarContents
BL.writeFile (anchorPath outputDirRoot repoLayoutIndexTarGz) $ GZip.compress tarContents
2022-09-23 18:46:18 +03:00
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
makeMetadataFile :: FilePath -> [(PackageId, PackageVersionSpec)] -> Action ()
makeMetadataFile outputDir packageVersions = traced "writing metadata" $ do
createDirectoryIfMissing True (outputDir </> "foliage")
Aeson.encodeFile
(outputDir </> "foliage" </> "packages.json")
(map encodePackageVersion packageVersions)
where
encodePackageVersion
( PackageIdentifier {pkgName, pkgVersion},
PackageVersionSpec
{ packageVersionSource,
packageVersionForce,
packageVersionTimestamp
}
) =
Aeson.object
( [ "pkg-name" Aeson..= pkgName,
"pkg-version" Aeson..= pkgVersion,
"url" Aeson..= sourceUrl packageVersionSource
]
++ ["forced-version" Aeson..= True | packageVersionForce]
++ (case packageVersionTimestamp of Nothing -> []; Just t -> ["timestamp" Aeson..= t])
)
sourceUrl :: PackageVersionSource -> URI
sourceUrl (TarballSource uri Nothing) = uri
sourceUrl (TarballSource uri (Just subdir)) = uri {uriQuery = "?dir=" ++ subdir}
sourceUrl (GitHubSource repo rev Nothing) =
nullURI
{ uriScheme = "github:",
uriPath = T.unpack (unGitHubRepo repo) </> T.unpack (unGitHubRev rev)
}
sourceUrl (GitHubSource repo rev (Just subdir)) =
nullURI
{ uriScheme = "github:",
uriPath = T.unpack (unGitHubRepo repo) </> T.unpack (unGitHubRev rev),
uriQuery = "?dir=" ++ subdir
}
2023-02-13 11:54:30 +03:00
getPackageVersions :: FilePath -> Action [(PackageId, PackageVersionSpec)]
getPackageVersions inputDir = do
2022-09-22 19:54:35 +03:00
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"
forP metaFiles $ \metaFile -> do
2023-03-02 05:48:31 +03:00
(pkgName, pkgVersion) <- case splitDirectories metaFile of
[pkgName, pkgVersion, _] -> pure (pkgName, pkgVersion)
_else -> fail $ "internal error: I should not be looking at " ++ metaFile
name <- case simpleParsec pkgName of
Nothing -> fail $ "invalid package name: " ++ pkgName
Just name -> pure name
version <- case simpleParsec pkgVersion of
Nothing -> fail $ "invalid package version: " ++ pkgVersion
Just version -> pure version
2022-09-22 19:54:35 +03:00
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
2023-01-29 07:27:04 +03:00
putError $
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"
]
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
2023-01-29 07:27:04 +03:00
putError $
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"
]
fail "invalid package metadata"
2022-09-22 19:54:35 +03:00
meta ->
return meta
2022-10-10 10:30:11 +03:00
return (pkgId, pkgSpec)
2022-10-10 10:30:11 +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 $ BS.readFile filePath
2023-03-02 05:48:31 +03:00
mkTarEntry (BL.fromStrict contents) (IndexPkgCabal pkgId) timestamp
2022-09-23 18:46:18 +03:00
prepareIndexPkgMetadata :: Maybe UTCTime -> PackageId -> PackageVersionSpec -> Action Targets
prepareIndexPkgMetadata expiryTime pkgId pkgSpec = do
2022-10-10 10:30:11 +03:00
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
2022-10-24 11:55:58 +03:00
return
Targets
{ targetsVersion = FileVersion 1,
targetsExpires = FileExpires expiryTime,
targetsTargets = fromList [(TargetPathRepo packagePath, targetFileInfo)],
targetsDelegations = Nothing
}
2022-09-22 19:54:35 +03:00
2023-03-02 05:48:31 +03:00
mkTarEntry :: BL.ByteString -> IndexFile dec -> UTCTime -> Action Tar.Entry
mkTarEntry contents indexFile timestamp = do
tarPath <- case Tar.toTarPath False indexPath of
Left e -> fail $ "Invalid tar path " ++ indexPath ++ "(" ++ e ++ ")"
Right tarPath -> pure tarPath
pure
(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
}
}
2022-09-22 19:54:35 +03:00
where
indexPath = toFilePath $ castRoot $ indexFileToPath hackageIndexLayout indexFile
anchorPath :: Path Absolute -> (RepoLayout -> RepoPath) -> FilePath
anchorPath outputDirRoot p =
toFilePath $ anchorRepoPathLocally outputDirRoot $ p hackageRepoLayout