foliage/app/Foliage/CmdBuild.hs

376 lines
16 KiB
Haskell
Raw Normal View History

2022-10-21 11:46:50 +03:00
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE ImportQualifiedPost #-}
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.Bifunctor (second)
import Data.ByteString.Char8 qualified as BS
import Data.ByteString.Lazy.Char8 qualified as BL
import Data.List (sortOn)
import Data.List.NonEmpty qualified as NE
2022-03-29 12:10:19 +03:00
import Data.Maybe (fromMaybe)
import Data.Text qualified as T
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Data.Traversable (for)
import Development.Shake
import Development.Shake.FilePath
2022-05-19 09:51:21 +03:00
import Distribution.Package
2022-09-23 18:46:18 +03:00
import Distribution.Pretty (prettyShow)
import Distribution.Version
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
import Foliage.PreparePackageVersion (PreparedPackageVersion (..), preparePackageVersion)
import Foliage.PrepareSdist (addPrepareSdistRule)
import Foliage.PrepareSource (addPrepareSourceRule)
2022-09-22 19:54:35 +03:00
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
outputDirRoot <- 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 $ pure []
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 $ makePackageVersionPage outputDir
void $
forP packageVersions $ \PreparedPackageVersion {pkgId, cabalFilePath} -> do
let PackageIdentifier {pkgName, pkgVersion} = pkgId
copyFileChanged cabalFilePath (outputDir </> "index" </> prettyShow pkgName </> prettyShow pkgVersion </> prettyShow pkgName <.> "cabal")
2022-10-24 11:55:58 +03:00
2022-10-10 07:14:56 +03:00
cabalEntries <-
foldMap
( \PreparedPackageVersion {pkgId, pkgTimestamp, originalCabalFilePath, cabalFileRevisions} -> do
2022-10-10 10:30:11 +03:00
-- original cabal file, with its timestamp (if specified)
let cabalFileTimestamp = fromMaybe currentTime pkgTimestamp
cf <- prepareIndexPkgCabal pkgId cabalFileTimestamp originalCabalFilePath
2022-10-10 10:30:11 +03:00
-- all revised cabal files, with their timestamp
revcf <- for cabalFileRevisions $ uncurry (prepareIndexPkgCabal pkgId)
2022-09-23 18:46:18 +03:00
-- WARN: So far Foliage allows publishing a package and a cabal file revision with the same timestamp
-- This accidentally works because 1) the following inserts the original cabal file before the revisions
-- AND 2) Data.List.sortOn is stable. The revised cabal file will always be after the original one.
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"
2023-03-10 08:29:22 +03:00
2022-10-10 10:30:11 +03:00
metadataEntries <-
forP packageVersions $ \ppv@PreparedPackageVersion {pkgId, pkgTimestamp} -> do
2022-10-24 11:55:58 +03:00
let PackageIdentifier {pkgName, pkgVersion} = pkgId
targets <- prepareIndexPkgMetadata expiryTime ppv
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
pure $
mkTarEntry
(renderSignedJSON targetKeys targets)
(IndexPkgMetadata pkgId)
(fromMaybe currentTime pkgTimestamp)
2022-09-23 18:46:18 +03:00
let extraEntries = getExtraEntries packageVersions
-- WARN: See note above, the sorting here has to be stable
let tarContents = Tar.write $ sortOn Tar.entryTime (cabalEntries ++ metadataEntries ++ extraEntries)
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 -> [PreparedPackageVersion] -> Action ()
makeMetadataFile outputDir packageVersions = traced "writing metadata" $ do
createDirectoryIfMissing True (outputDir </> "foliage")
Aeson.encodeFile
(outputDir </> "foliage" </> "packages.json")
(map encodePackageVersion packageVersions)
where
encodePackageVersion
PreparedPackageVersion
{ pkgId = PackageIdentifier {pkgName, pkgVersion},
pkgTimestamp,
pkgVersionForce,
pkgVersionSource
} =
Aeson.object
( [ "pkg-name" Aeson..= pkgName,
"pkg-version" Aeson..= pkgVersion,
"url" Aeson..= sourceUrl pkgVersionSource
]
++ ["forced-version" Aeson..= True | pkgVersionForce]
++ (case pkgTimestamp 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 [PreparedPackageVersion]
getPackageVersions inputDir = do
2022-09-22 19:54:35 +03:00
metaFiles <- getDirectoryFiles inputDir ["*/*/meta.toml"]
when (null metaFiles) $ do
error $
2022-09-22 19:54:35 +03:00
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"
]
forP metaFiles $ preparePackageVersion inputDir
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
pure $ mkTarEntry (BL.fromStrict contents) (IndexPkgCabal pkgId) timestamp
2022-09-23 18:46:18 +03:00
prepareIndexPkgMetadata :: Maybe UTCTime -> PreparedPackageVersion -> Action Targets
prepareIndexPkgMetadata expiryTime PreparedPackageVersion {pkgId, sdistPath} = do
2023-03-10 08:29:22 +03:00
targetFileInfo <- liftIO $ computeFileInfoSimple sdistPath
2022-09-23 18:46:18 +03:00
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
-- Currently `extraEntries` are only used for encoding `prefered-versions`.
getExtraEntries :: [PreparedPackageVersion] -> [Tar.Entry]
getExtraEntries packageVersions =
2023-05-15 09:12:22 +03:00
let -- Group all (package) versions by package (name)
groupedPackageVersions :: [NE.NonEmpty PreparedPackageVersion]
groupedPackageVersions = NE.groupWith (pkgName . pkgId) packageVersions
-- All versions of a given package together form a list of entries
-- The list of entries might be empty (in case no version has been deprecated)
generateEntriesForGroup :: NE.NonEmpty PreparedPackageVersion -> [Tar.Entry]
generateEntriesForGroup packageGroup = map createTarEntry effectiveRanges
where
-- Get the package name of the current group.
2023-05-15 09:12:22 +03:00
pn :: PackageName
pn = pkgName $ pkgId $ NE.head packageGroup
2023-05-15 09:12:22 +03:00
-- Collect and sort the deprecation changes for the package group, turning them into a action on VersionRange
deprecationChanges :: [(UTCTime, VersionRange -> VersionRange)]
deprecationChanges = sortOn fst $ foldMap versionDeprecationChanges packageGroup
-- Calculate (by applying them chronologically) the effective `VersionRange` for the package group.
2023-05-15 09:12:22 +03:00
effectiveRanges :: [(UTCTime, VersionRange)]
effectiveRanges = NE.tail $ NE.scanl applyChangeToRange (posixSecondsToUTCTime 0, anyVersion) deprecationChanges
-- Create a `Tar.Entry` for the package group, its computed `VersionRange` and a timestamp.
2023-04-24 17:26:13 +03:00
createTarEntry (ts, effectiveRange) = mkTarEntry (BL.pack $ prettyShow effectiveRange) (IndexPkgPrefs pn) ts
in foldMap generateEntriesForGroup groupedPackageVersions
2023-05-15 09:12:22 +03:00
-- TODO: the functions belows should be moved to Foliage.PreparedPackageVersion
-- Extract deprecation changes for a given `PreparedPackageVersion`.
versionDeprecationChanges :: PreparedPackageVersion -> [(UTCTime, VersionRange -> VersionRange)]
2023-05-15 09:12:22 +03:00
versionDeprecationChanges
PreparedPackageVersion
{ pkgId = PackageIdentifier {pkgVersion},
pkgVersionDeprecationChanges
} =
map (second $ applyDeprecation pkgVersion) pkgVersionDeprecationChanges
-- Apply a given change (`VersionRange -> VersionRange`) to a `VersionRange` and
-- return the simplified the result with a new timestamp.
applyChangeToRange :: (UTCTime, VersionRange) -> (UTCTime, VersionRange -> VersionRange) -> (UTCTime, VersionRange)
applyChangeToRange (_, range) (ts, change) = (ts, simplifyVersionRange $ change range)
-- Exclude (or include) to the `VersionRange` of prefered versions, a given
-- `Version`, if the `Version` is (or not) tagged as "deprecated".
applyDeprecation :: Version -> Bool -> VersionRange -> VersionRange
applyDeprecation pkgVersion deprecated =
if deprecated
then intersectVersionRanges (notThisVersion pkgVersion)
else unionVersionRanges (thisVersion pkgVersion)
mkTarEntry :: BL.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
}
}
2022-09-22 19:54:35 +03:00
where
tarPath = case Tar.toTarPath False indexPath of
Left e -> error $ "Invalid tar path " ++ indexPath ++ "(" ++ e ++ ")"
Right tp -> tp
2022-09-22 19:54:35 +03:00
indexPath = toFilePath $ castRoot $ indexFileToPath hackageIndexLayout indexFile
anchorPath :: Path Absolute -> (RepoLayout -> RepoPath) -> FilePath
anchorPath outputDirRoot p =
toFilePath $ anchorRepoPathLocally outputDirRoot $ p hackageRepoLayout