slackbuilder/src/Main.hs

522 lines
22 KiB
Haskell
Raw Normal View History

2023-12-23 22:15:10 +01:00
{- This Source Code Form is subject to the terms of the Mozilla Public License,
v. 2.0. If a copy of the MPL was not distributed with this file, You can
obtain one at https://mozilla.org/MPL/2.0/. -}
module Main
( main
) where
import Data.Char (isNumber)
import Control.Applicative (Applicative(liftA2))
2023-09-03 10:26:43 +02:00
import Data.List.NonEmpty (NonEmpty(..))
2023-10-01 17:19:06 +02:00
import qualified Data.List.NonEmpty as NonEmpty
2023-12-11 08:14:55 +01:00
import Control.Monad.Catch (MonadThrow(..))
2023-09-03 10:26:43 +02:00
import Control.Monad.IO.Class (MonadIO(..))
import Data.Maybe (fromJust, fromMaybe)
import qualified Data.Map as Map
import Options.Applicative (execParser)
import SlackBuilder.CommandLine
2023-08-09 20:59:42 +02:00
import SlackBuilder.Config
2023-08-15 10:33:19 +02:00
import SlackBuilder.Trans
import SlackBuilder.LatestVersionCheck
2023-08-09 20:59:42 +02:00
import qualified Toml
2023-10-04 22:36:19 +02:00
import qualified Data.ByteString as ByteString
2023-09-03 10:26:43 +02:00
import Data.Text (Text)
2023-08-09 20:59:42 +02:00
import qualified Data.Text as Text
2023-09-03 10:26:43 +02:00
import qualified Data.Text.IO as Text.IO
import Control.Monad.Trans.Reader (ReaderT(..), asks)
2023-08-15 10:33:19 +02:00
import SlackBuilder.Download
import SlackBuilder.Package (PackageDescription(..), PackageUpdateData(..))
2023-09-03 10:26:43 +02:00
import qualified SlackBuilder.Package as Package
import Text.URI (URI(..), mkURI)
import Text.URI.QQ (uri)
import Data.Foldable (Foldable(..), for_, find)
2023-09-03 10:26:43 +02:00
import qualified Text.URI as URI
import System.FilePath
( (</>)
, (<.>)
, dropExtension
, takeBaseName
, splitFileName
, takeDirectory
, takeFileName
, dropTrailingPathSeparator
)
2023-10-04 22:36:19 +02:00
import SlackBuilder.Info
import Text.Megaparsec (parse, errorBundlePretty)
2023-10-08 12:28:46 +02:00
import GHC.Records (HasField(..))
import System.Process
( CmdSpec(..)
, CreateProcess(..)
, StdStream(..)
, withCreateProcess
, waitForProcess
)
2023-10-20 19:23:21 +02:00
import System.Console.ANSI
( setSGR
, SGR(..)
, ColorIntensity(..)
, Color(..)
, ConsoleLayer(..)
)
import System.Directory (listDirectory, doesDirectoryExist, withCurrentDirectory, removeDirectoryRecursive)
import Control.Monad (filterM, void)
2023-12-12 18:51:44 +01:00
import Data.List (isPrefixOf, isSuffixOf, partition)
import Conduit (runConduitRes, (.|), yield)
import Data.Functor ((<&>))
import Data.Bifunctor (Bifunctor(..))
import Data.Conduit.Tar (tarFilePath)
import qualified Data.Conduit.Lzma as Lzma
2023-09-03 10:26:43 +02:00
autoUpdatable :: [PackageDescription]
2023-09-03 10:26:43 +02:00
autoUpdatable =
[ PackageDescription
2023-09-03 10:26:43 +02:00
{ latest =
let ghArguments = PackageOwner{ owner = "universal-ctags", name = "ctags" }
2023-09-03 10:26:43 +02:00
templateTail =
[ Package.VersionPlaceholder
, Package.StaticPlaceholder "/ctags-"
2023-09-03 10:26:43 +02:00
, Package.VersionPlaceholder
, Package.StaticPlaceholder ".tar.gz"
]
template = Package.DownloadTemplate
2023-10-01 17:19:06 +02:00
$ Package.StaticPlaceholder "https://github.com/universal-ctags/ctags/archive/"
:| templateTail
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestGitHub ghArguments stableTagTransform
, getVersion = reuploadWithTemplate template []
, is64 = False
}
2023-09-03 10:26:43 +02:00
, name = "universal-ctags"
, downloaders = mempty
2023-10-01 17:19:06 +02:00
}
, PackageDescription
2023-10-01 17:19:06 +02:00
{ latest =
let packagistArguments = PackageOwner{ owner = "composer", name = "composer" }
2023-10-01 17:19:06 +02:00
template = Package.DownloadTemplate
$ Package.StaticPlaceholder "https://getcomposer.org/download/"
:| [Package.VersionPlaceholder, Package.StaticPlaceholder "/composer.phar"]
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestPackagist packagistArguments
, getVersion = downloadWithTemplate template
, is64 = False
}
2023-10-01 17:19:06 +02:00
, name = "composer"
, downloaders = mempty
2023-09-03 10:26:43 +02:00
}
, PackageDescription
2023-10-03 18:53:41 +02:00
{ latest =
let ghArguments = PackageOwner
2023-10-03 18:53:41 +02:00
{ owner = "jitsi"
, name = "jitsi-meet-electron"
}
template = Package.DownloadTemplate
$ Package.StaticPlaceholder "https://github.com/jitsi/jitsi-meet-electron/releases/download/v"
:| Package.VersionPlaceholder
: [Package.StaticPlaceholder "/jitsi-meet-x86_64.AppImage"]
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestGitHub ghArguments $ Text.stripPrefix "v"
, getVersion = downloadWithTemplate template
, is64 = True
}
2023-10-03 18:53:41 +02:00
, name = "jitsi-meet-desktop"
, downloaders = mempty
2023-10-03 18:53:41 +02:00
}
, PackageDescription
2023-10-04 22:36:19 +02:00
{ latest =
let ghArguments = PackageOwner
2023-10-04 22:36:19 +02:00
{ owner = "php"
, name = "php-src"
}
checkVersion x
2023-10-13 19:34:02 +02:00
| not $ Text.isInfixOf "RC" x
, Text.isPrefixOf "php-8.2." x = Text.stripPrefix "php-" x
2023-10-04 22:36:19 +02:00
| otherwise = Nothing
template = Package.DownloadTemplate
$ Package.StaticPlaceholder "https://www.php.net/distributions/php-"
:| Package.VersionPlaceholder
: [Package.StaticPlaceholder ".tar.xz"]
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestGitHub ghArguments checkVersion
, getVersion = downloadWithTemplate template
, is64 = False
}
2023-10-04 22:36:19 +02:00
, name = "php82"
, downloaders = mempty
2023-10-08 12:28:46 +02:00
}
, PackageDescription
2023-10-08 12:28:46 +02:00
{ latest =
let ghArguments = PackageOwner
2023-10-08 12:28:46 +02:00
{ owner = "kovidgoyal"
, name = "kitty"
}
templateTail =
[ Package.StaticPlaceholder "/kitty-"
, Package.VersionPlaceholder
, Package.StaticPlaceholder ".tar.xz"
]
template = Package.DownloadTemplate
$ Package.StaticPlaceholder "https://github.com/kovidgoyal/kitty/releases/download/v"
:| Package.VersionPlaceholder
: templateTail
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestGitHub ghArguments stableTagTransform
, getVersion = reuploadWithTemplate template [RawCommand "go" ["mod", "vendor"]]
, is64 = False
}
2023-10-08 12:28:46 +02:00
, name = "kitty"
, downloaders = mempty
2023-10-04 22:36:19 +02:00
}
, PackageDescription
2023-10-13 19:34:02 +02:00
{ latest =
let ghArguments = PackageOwner
2023-10-13 19:34:02 +02:00
{ owner = "rdiff-backup"
, name = "rdiff-backup"
}
template = Package.DownloadTemplate
$ Package.StaticPlaceholder "https://github.com/rdiff-backup/rdiff-backup/releases/download/v"
:| Package.VersionPlaceholder
: Package.StaticPlaceholder "/rdiff-backup-"
: Package.VersionPlaceholder
: [Package.StaticPlaceholder ".tar.gz"]
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestGitHub ghArguments $ Text.stripPrefix "v"
, getVersion = reuploadWithTemplate template []
, is64 = False
}
2023-10-13 19:34:02 +02:00
, name = "rdiff-backup"
, downloaders = mempty
2023-10-13 19:34:02 +02:00
}
, PackageDescription
{ latest =
let needle = "Linux—"
textArguments = TextArguments
{ textURL = "https://help.webex.com/en-us/article/mqkve8/Webex-App-%7C-Release-notes"
, versionPicker = Text.takeWhile (liftA2 (||) (== '.') isNumber)
. Text.drop (Text.length needle)
. snd
. Text.breakOn needle
}
template = Package.DownloadTemplate $ pure
$ Package.StaticPlaceholder
"https://binaries.webex.com/WebexDesktop-Ubuntu-Official-Package/Webex.deb"
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestText textArguments
, getVersion = downloadWithTemplate template
, is64 = True
}
, name = "webex"
, downloaders = mempty
}
, PackageDescription
2023-10-20 19:23:21 +02:00
{ latest =
let ghArguments = PackageOwner
2023-10-20 19:23:21 +02:00
{ owner = "librsync"
, name = "librsync"
}
template = Package.DownloadTemplate
$ Package.StaticPlaceholder "https://github.com/librsync/librsync/archive/v"
:| Package.VersionPlaceholder
: Package.StaticPlaceholder "/librsync-"
: Package.VersionPlaceholder
: [Package.StaticPlaceholder ".tar.gz"]
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestGitHub ghArguments $ Text.stripPrefix "v"
, getVersion = reuploadWithTemplate template []
, is64 = True
}
2023-10-20 19:23:21 +02:00
, name = "librsync"
, downloaders = mempty
2023-10-20 19:23:21 +02:00
}
, PackageDescription
2023-10-20 19:23:21 +02:00
{ latest =
let textArguments = TextArguments
{ textURL = "https://downloads.dlang.org/releases/LATEST"
, versionPicker = Text.strip
}
template = Package.DownloadTemplate
$ Package.StaticPlaceholder "https://downloads.dlang.org/releases/2.x/"
:| Package.VersionPlaceholder
: Package.StaticPlaceholder "/dmd."
: Package.VersionPlaceholder
: [Package.StaticPlaceholder ".linux.tar.xz"]
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestText textArguments
, getVersion = downloadWithTemplate template
, is64 = False
}
2023-10-20 19:23:21 +02:00
, name = "dmd"
, downloaders = mempty
}
, PackageDescription
{ latest =
let textArguments = TextArguments
{ textURL = "https://downloads.dlang.org/releases/LATEST"
, versionPicker = Text.strip
}
template = Package.DownloadTemplate
$ Package.StaticPlaceholder "https://codeload.github.com/dlang/tools/tar.gz/v"
:| [Package.VersionPlaceholder]
2024-01-19 09:57:58 +01:00
in Package.Updater
{ detectLatest = latestText textArguments
, getVersion = reuploadWithTemplate template []
, is64 = False
}
, name = "d-tools"
, downloaders =
let dubArguments = PackageOwner{ owner = "dlang", name = "dub" }
dscannerArguments = PackageOwner{ owner = "dlang-community", name = "D-Scanner" }
dcdArguments = PackageOwner{ owner = "dlang-community", name = "DCD" }
2024-01-19 09:57:58 +01:00
latestDub = Package.Updater
{ detectLatest = latestGitHub dubArguments stableTagTransform
, getVersion = downloadWithTemplate dubTemplate
, is64 = False
}
latestDscanner = Package.Updater
{ detectLatest = latestGitHub dscannerArguments stableTagTransform
, getVersion = cloneFromGit dscannerURI "v"
, is64 = False
}
dcdURI = [uri|https://github.com/dlang-community/DCD.git|]
latestDcd = Package.Updater
{ detectLatest = latestGitHub dcdArguments stableTagTransform
, getVersion = cloneFromGit dcdURI "v"
, is64 = False
}
dubTemplate = Package.DownloadTemplate
$ Package.StaticPlaceholder "https://codeload.github.com/dlang/dub/tar.gz/v"
:| [Package.VersionPlaceholder]
dscannerURI = [uri|https://github.com/dlang-community/D-Scanner.git|]
in Map.fromList
2024-01-19 09:57:58 +01:00
[ ("DUB", latestDub)
, ("DSCANNER", latestDscanner)
2024-01-19 09:57:58 +01:00
, ("DCD", latestDcd)
]
2023-10-20 19:23:21 +02:00
}
2023-09-03 10:26:43 +02:00
]
2023-12-11 08:14:55 +01:00
up2Date :: Maybe Text -> SlackBuilderT ()
up2Date = \case
Nothing -> for_ autoUpdatable go
Just packageName
| Just foundPackage <- find ((packageName ==) . getField @"name") autoUpdatable ->
go foundPackage
| otherwise -> throwM $ UpdaterNotFound packageName
2023-09-03 10:26:43 +02:00
where
2023-10-20 19:23:21 +02:00
go package = getAndLogLatest package
>>= mapM_ updatePackageIfRequired
2023-10-20 19:23:21 +02:00
>> liftIO (putStrLn "")
2023-09-03 10:26:43 +02:00
check :: SlackBuilderT ()
check = for_ autoUpdatable go
where
go package = getAndLogLatest package
>>= mapM_ checkUpdateAvailability
>> liftIO (putStrLn "")
getAndLogLatest :: PackageDescription -> SlackBuilderT (Maybe PackageUpdateData)
getAndLogLatest description = do
let PackageDescription{ latest = Package.Updater{ detectLatest }, name } = description
liftIO (putStrLn $ Text.unpack name <> ": Retreiving the latest version.")
detectedVersion <- detectLatest
category <- fmap Text.pack
<$> findCategory (Text.unpack name)
pure $ PackageUpdateData description
<$> category
<*> detectedVersion
checkUpdateAvailability :: PackageUpdateData -> SlackBuilderT (Maybe PackageInfo)
checkUpdateAvailability PackageUpdateData{..} = do
let name' = Text.unpack $ getField @"name" description
packagePath = Text.unpack category </> name' </> (name' <.> "info")
2023-10-04 22:36:19 +02:00
repository' <- SlackBuilderT $ asks repository
infoContents <- liftIO $ ByteString.readFile $ repository' </> packagePath
case parse parseInfoFile packagePath infoContents of
2023-10-08 12:28:46 +02:00
Right parsedInfoFile
| version == getField @"version" parsedInfoFile ->
2023-10-20 19:23:21 +02:00
liftIO $ do
setSGR [SetColor Foreground Dull Green]
Text.IO.putStrLn
$ getField @"name" description <> " is up to date (Version " <> version <> ")."
2023-10-20 19:23:21 +02:00
setSGR [Reset]
pure Nothing
| otherwise ->
liftIO $ do
setSGR [SetColor Foreground Dull Yellow]
Text.IO.putStr
$ "A new version of "
<> getField @"name" description
<> " " <> getField @"version" parsedInfoFile
<> " is available (" <> version <> ")."
setSGR [Reset]
putStrLn ""
pure $ Just parsedInfoFile
Left errorBundle -> liftIO (putStr $ errorBundlePretty errorBundle)
>> pure Nothing
updatePackageIfRequired :: PackageUpdateData -> SlackBuilderT ()
updatePackageIfRequired updateData
= checkUpdateAvailability updateData
>>= mapM_ (updatePackage updateData)
2023-10-04 22:36:19 +02:00
2024-01-19 09:57:58 +01:00
data DownloadUpdated = DownloadUpdated
{ result :: Package.Download
, version :: Text
, is64 :: Bool
} deriving (Eq, Show)
updateDownload :: Text -> Package.Updater -> SlackBuilderT DownloadUpdated
updateDownload packagePath Package.Updater{..} = do
latestDownloadVersion <- fromJust <$> detectLatest
2024-01-19 09:57:58 +01:00
result <- getVersion packagePath latestDownloadVersion
pure $ DownloadUpdated
{ result = result
, version = latestDownloadVersion
, is64 = is64
}
2023-10-28 04:04:52 +02:00
cloneFromGit :: URI -> Text -> Text -> Text -> SlackBuilderT Package.Download
2023-10-28 04:04:52 +02:00
cloneFromGit repo tagPrefix packagePath version = do
let downloadFileName = URI.unRText
$ NonEmpty.last $ snd $ fromJust $ URI.uriPath repo
relativeTarball = Text.unpack packagePath
</> (dropExtension (Text.unpack downloadFileName) <> "-" <> Text.unpack version)
(uri', checksum) <- cloneAndUpload (URI.render repo) relativeTarball tagPrefix
pure $ Package.Download
{ md5sum = checksum
, download = uri'
}
2023-09-03 10:26:43 +02:00
2024-01-19 09:57:58 +01:00
downloadWithTemplate :: Package.DownloadTemplate -> Text -> Text -> SlackBuilderT Package.Download
downloadWithTemplate downloadTemplate packagePath version = do
repository' <- SlackBuilderT $ asks repository
2023-09-03 10:26:43 +02:00
uri' <- liftIO $ Package.renderDownloadWithVersion downloadTemplate version
2024-03-05 23:06:32 +01:00
checksum <- download uri' $ repository' </> Text.unpack packagePath
pure $ Package.Download uri' $ snd checksum
reuploadWithTemplate :: Package.DownloadTemplate -> [CmdSpec] -> Text -> Text -> SlackBuilderT Package.Download
reuploadWithTemplate downloadTemplate commands packagePath version = do
2024-01-24 14:34:58 +01:00
repository' <- SlackBuilderT $ asks repository
uri' <- liftIO $ Package.renderDownloadWithVersion downloadTemplate version
let downloadFileName = Text.unpack
$ URI.unRText
$ NonEmpty.last $ snd $ fromJust $ URI.uriPath uri'
packagePathRelativeToCurrent = repository' </> Text.unpack packagePath
(relativeTarball', checksum) <- case commands of
[] -> do
2024-03-05 23:06:32 +01:00
(downloadedFileName, checksum) <- download uri' packagePathRelativeToCurrent
pure (packagePathRelativeToCurrent </> downloadedFileName, checksum)
_ -> do
changedArchiveRootName <- extractRemote uri' packagePathRelativeToCurrent
let relativeTarball = packagePathRelativeToCurrent
</> fromMaybe downloadFileName changedArchiveRootName
prepareSource relativeTarball
2023-09-03 10:26:43 +02:00
download' <- handleReupload relativeTarball' downloadFileName
2024-01-19 09:57:58 +01:00
pure $ Package.Download download' checksum
2023-10-01 17:19:06 +02:00
where
name' = Text.pack $ takeBaseName $ Text.unpack packagePath
prepareSource tarballPath =
liftIO (traverse (defaultCreateProcess tarballPath) commands)
>> liftIO (tarCompress tarballPath)
<* liftIO (removeDirectoryRecursive tarballPath)
tarCompress tarballPath =
let archiveBaseFilename = takeFileName tarballPath
appendTarExtension = (<.> "tar.xz")
in fmap (appendTarExtension tarballPath,)
$ withCurrentDirectory (takeDirectory tarballPath)
$ runConduitRes $ yield archiveBaseFilename
.| void tarFilePath
.| Lzma.compress Nothing
.| sinkFileAndHash (appendTarExtension archiveBaseFilename)
handleReupload relativeTarball downloadFileName = do
downloadURL' <- SlackBuilderT $ asks downloadURL
liftIO $ putStrLn $ "Upload the source tarball " <> relativeTarball
uploadCommand relativeTarball ("/" <> name')
liftIO $ mkURI $ downloadURL' <> "/" <> name' <> "/" <> Text.pack downloadFileName
2023-10-08 12:28:46 +02:00
defaultCreateProcess cwd' cmdSpec
= flip withCreateProcess (const . const . const waitForProcess)
$ CreateProcess
{ use_process_jobs = False
, std_out = Inherit
, std_in = NoStream
, std_err = Inherit
, new_session = False
, env = Nothing
, detach_console = False
, delegate_ctlc = False
, cwd = Just cwd'
, create_new_console = False
, create_group = False
, cmdspec = cmdSpec
, close_fds = True
, child_user = Nothing
, child_group = Nothing
}
updatePackage :: PackageUpdateData -> PackageInfo -> SlackBuilderT ()
updatePackage PackageUpdateData{..} info = do
let packagePath = category <> "/" <> getField @"name" description
latest' = getField @"latest" description
repository' <- SlackBuilderT $ asks repository
mainDownload <- (, getField @"is64" latest')
<$> getField @"getVersion" latest' packagePath version
moreDownloads <- traverse (updateDownload packagePath)
$ getField @"downloaders" description
2024-01-19 09:57:58 +01:00
let (downloads64, allDownloads) = partition snd
$ mainDownload
: (liftA2 (,) (getField @"result") (getField @"is64") <$> toList moreDownloads)
let infoFilePath = repository' </> Text.unpack packagePath
</> (Text.unpack (getField @"name" description) <.> "info")
package' = info
{ version = version
2024-01-19 09:57:58 +01:00
, downloads = getField @"download" . fst <$> allDownloads
, checksums = getField @"md5sum" . fst <$> allDownloads
, downloadX64 = getField @"download" . fst <$> downloads64
, checksumX64 = getField @"md5sum" . fst <$> downloads64
}
liftIO $ Text.IO.writeFile infoFilePath $ generate package'
2024-01-19 09:57:58 +01:00
updateSlackBuildVersion packagePath version
$ getField @"version" <$> moreDownloads
commit packagePath version
2023-10-01 17:19:06 +02:00
findCategory :: FilePath -> SlackBuilderT (Maybe FilePath)
findCategory packageName = do
repository' <- SlackBuilderT $ asks repository
go repository' [] "" <&> fmap fst . find ((packageName ==) . snd)
where
go currentDirectory found accumulatedDirectory = do
let fullDirectory = currentDirectory </> accumulatedDirectory
contents <- liftIO $ listDirectory fullDirectory
case find (isSuffixOf ".info") contents of
Just _ ->
let result = first dropTrailingPathSeparator
$ splitFileName accumulatedDirectory
in pure $ result : found
Nothing ->
let accumulatedDirectories = (accumulatedDirectory </>)
<$> filter (not . isPrefixOf ".") contents
directoryFilter = liftIO . doesDirectoryExist
. (currentDirectory </>)
in filterM directoryFilter accumulatedDirectories
>>= traverse (go currentDirectory found) <&> concat
2023-10-20 19:23:21 +02:00
main :: IO ()
main = do
programCommand <- execParser slackBuilderParser
2023-08-09 20:59:42 +02:00
settings <- Toml.decodeFile settingsCodec "config/config.toml"
2023-08-15 10:33:19 +02:00
latestVersion <- flip runReaderT settings
$ runSlackBuilderT
$ executeCommand programCommand
2023-10-20 19:23:21 +02:00
maybe (pure ()) Text.IO.putStrLn latestVersion
2023-08-09 20:59:42 +02:00
where
2023-08-15 10:33:19 +02:00
executeCommand = \case
CheckCommand -> check >> pure Nothing
2023-12-11 08:14:55 +01:00
Up2DateCommand packageName -> up2Date packageName >> pure Nothing