From d5df676df7a9bea716fdbdaff455fa1ab57073ac Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Tue, 3 Oct 2023 18:53:41 +0200 Subject: [PATCH] Add module with an info file parser --- Rakefile | 61 ------------- app/Main.hs | 21 ++++- app/SlackBuilder/CommandLine.hs | 11 +-- lib/SlackBuilder/Info.hs | 156 ++++++++++++++++++++++++++++++++ lib/up2date.rb | 14 --- slackbuilder.cabal | 10 +- tests/SlackBuilder/InfoSpec.hs | 139 ++++++++++++++++++++++++++++ 7 files changed, 325 insertions(+), 87 deletions(-) create mode 100644 lib/SlackBuilder/Info.hs create mode 100644 tests/SlackBuilder/InfoSpec.hs diff --git a/Rakefile b/Rakefile index 29f9aaf..4912b59 100644 --- a/Rakefile +++ b/Rakefile @@ -92,61 +92,6 @@ module SlackBuilder end end - class UniversalCtags < Updater - def update(version) - package = create_package version - - uri = "https://github.com/universal-ctags/ctags/archive/#{version}/ctags-#{version}.tar.gz" - tarball = "slackbuilds/development/universal-ctags/ctags-#{version}.tar.gz" - checksum = SlackBuilder.download URI(uri), tarball - download = "https://download.dlackware.com/hosted-sources/universal-ctags/ctags-#{version}.tar.gz" - - write_info package, downloads: [Download.new(download, checksum)] - update_slackbuild_version 'development/universal-ctags', version - sh 'scp', tarball, "#{CONFIG[:remote_path]}/universal-ctags" - - commit 'development/universal-ctags', version - end - - private - - def create_package(version) - Package.new 'development/universal-ctags', - version: version, - homepage: 'https://ctags.io/', - requires: ['%README%'] - end - end - - class Composer < Updater - def update(version) - package = Package.new 'development/composer', - version: version, - homepage: 'https://getcomposer.org/' - - uri = "https://getcomposer.org/download/#{version}/composer.phar" - checksum = SlackBuilder.download URI(uri), 'slackbuilds/development/composer/composer.phar' - write_info package, downloads: [Download.new(uri, checksum)] - update_slackbuild_version 'development/composer', version - - commit 'development/composer', version - end - end - - class JitsiMeetDesktop < Updater - def update(version) - package = Package.new 'network/jitsi-meet-desktop', - version: version, - homepage: 'https://jitsi.org/' - uri = "https://github.com/jitsi/jitsi-meet-electron/releases/download/v#{version}/jitsi-meet-x86_64.AppImage" - checksum = SlackBuilder.download URI(uri), 'slackbuilds/network/jitsi-meet-desktop/jitsi-meet-x86_64.AppImage' - - write_info package, downloads: [Download.new(uri, checksum, is64: true)] - update_slackbuild_version 'network/jitsi-meet-desktop', version - commit 'network/jitsi-meet-desktop', version - end - end - class PHP < Updater def update(version) package = Package.new 'development/php82', @@ -167,15 +112,9 @@ module SlackBuilder end AUTO_UPDATABLE = { - 'universal-ctags' => [SlackBuilder::GitHub.new('universal-ctags', 'ctags'), SlackBuilder::UniversalCtags.new], - 'composer' => [SlackBuilder::Packagist.new('composer', 'composer'), SlackBuilder::Composer.new], 'php82' => [SlackBuilder::GitHub.new('php', 'php-src', 'php'), SlackBuilder::PHP.new], 'rdiff-backup' => [SlackBuilder::GitHub.new('rdiff-backup', 'rdiff-backup', 'rdiff-backup')], 'librsync' => [SlackBuilder::GitHub.new('librsync', 'librsync')], - 'jitsi-meet-desktop' => [ - SlackBuilder::GitHub.new('jitsi', 'jitsi-meet-electron'), - SlackBuilder::JitsiMeetDesktop.new - ], 'dmd' => [SlackBuilder::LatestText.new('https://downloads.dlang.org/releases/LATEST')] }.freeze diff --git a/app/Main.hs b/app/Main.hs index c82a4e9..e8cd335 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -70,6 +70,25 @@ autoUpdatable = , requires = mempty , reupload = False } + , Package + { latest = + let ghArguments = GhArguments + { owner = "jitsi" + , name = "jitsi-meet-electron" + , transform = Nothing + } + latest' = latestGitHub ghArguments $ Text.stripPrefix "v" + template = Package.DownloadTemplate + $ Package.StaticPlaceholder "https://github.com/jitsi/jitsi-meet-electron/releases/download/v" + :| Package.VersionPlaceholder + : [Package.StaticPlaceholder "/jitsi-meet-x86_64.AppImage"] + in Package.Updater latest' template + , category = "network" + , name = "jitsi-meet-desktop" + , homepage = Just [uri|https://jitsi.org/|] + , requires = mempty + , reupload = False + } ] up2Date :: SlackBuilderT () @@ -134,8 +153,6 @@ main = do Text.IO.putStrLn $ fromMaybe "" latestVersion where executeCommand = \case - PackagistCommand packagistArguments -> - latestPackagist packagistArguments TextCommand textArguments -> latestText textArguments GhCommand ghArguments@GhArguments{ transform } -> latestGitHub ghArguments $ chooseTransformFunction transform diff --git a/app/SlackBuilder/CommandLine.hs b/app/SlackBuilder/CommandLine.hs index 48881e2..8624cb8 100644 --- a/app/SlackBuilder/CommandLine.hs +++ b/app/SlackBuilder/CommandLine.hs @@ -20,8 +20,7 @@ import Options.Applicative ) data SlackBuilderCommand - = PackagistCommand PackagistArguments - | TextCommand TextArguments + = TextCommand TextArguments | GhCommand GhArguments | SlackBuildCommand Text Text | CommitCommand Text Text @@ -47,11 +46,6 @@ data GhArguments = GhArguments newtype TextArguments = TextArguments Text deriving (Eq, Show) -packagistArguments :: Parser PackagistArguments -packagistArguments = PackagistArguments - <$> argument str (metavar "VENDOR") - <*> argument str (metavar"NAME") - textArguments :: Parser TextArguments textArguments = TextArguments <$> argument str (metavar "URL") @@ -66,8 +60,7 @@ slackBuilderParser = info slackBuilderCommand fullDesc slackBuilderCommand :: Parser SlackBuilderCommand slackBuilderCommand = subparser - $ command "packagist" (info (PackagistCommand <$> packagistArguments) mempty) - <> command "text" (info (TextCommand <$> textArguments) mempty) + $ command "text" (info (TextCommand <$> textArguments) mempty) <> command "github" (info (GhCommand <$> ghArguments) mempty) <> command "slackbuild" (info slackBuildCommand mempty) <> command "commit" (info commitCommand mempty) diff --git a/lib/SlackBuilder/Info.hs b/lib/SlackBuilder/Info.hs new file mode 100644 index 0000000..bedbc48 --- /dev/null +++ b/lib/SlackBuilder/Info.hs @@ -0,0 +1,156 @@ +module SlackBuilder.Info + ( PackageInfo(..) + , generate + , parseInfoFile + , update + , updateDownloadVersion + ) where + +import Control.Monad.Combinators (sepBy) +import qualified Data.ByteArray as ByteArray +import Data.ByteString (ByteString) +import qualified Data.ByteString as ByteString +import qualified Data.ByteString.Char8 as Char8 +import qualified Data.List.NonEmpty as NonEmpty +import Data.Maybe (mapMaybe) +import Data.Text (Text) +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text +import qualified Data.Text.Lazy as Lazy.Text +import qualified Data.Text.Lazy.Builder as Text.Builder +import qualified Data.Text.Lazy.Builder as Text (Builder) +import Crypto.Hash (Digest, MD5, digestFromByteString) +import Data.Void (Void) +import Data.Word (Word8) +import Numeric (readHex, showHex) +import Text.Megaparsec (Parsec, count, eof, takeWhile1P) +import Text.Megaparsec.Byte (space, string, hexDigitChar) +import Text.URI + ( Authority(..) + , URI(..) + , mkPathPiece + , parserBs + , render + , unRText + ) + +type GenParser = Parsec Void ByteString + +data PackageInfo = PackageInfo + { pkgname :: String + , version :: Text + , homepage :: Text + , downloads :: [URI] + , checksums :: [Digest MD5] + } deriving (Eq, Show) + +variableEntry :: ByteString -> GenParser ByteString +variableEntry variable = string (Char8.append variable "=\"") + *> takeWhile1P Nothing (0x22 /=) + <* string "\"\n" + +variableSeparator :: GenParser () +variableSeparator = string " \\" *> space + +packageDownloads :: GenParser [URI] +packageDownloads = string "DOWNLOAD=\"" + *> sepBy parserBs variableSeparator + <* string "\"\n" + +hexDigit :: GenParser Word8 +hexDigit = + let digitPair = count 2 hexDigitChar + in fst . head . readHex . fmap (toEnum . fromIntegral) <$> digitPair + +packageChecksum :: GenParser ByteString +packageChecksum = ByteString.pack <$> count 16 hexDigit + +packageChecksums :: GenParser [ByteString] +packageChecksums = string "MD5SUM=\"" + *> sepBy packageChecksum variableSeparator + <* string "\"\n" + +parseInfoFile :: GenParser PackageInfo +parseInfoFile = PackageInfo + <$> (Char8.unpack <$> variableEntry "PKGNAM") + <*> (Text.decodeUtf8 <$> variableEntry "VERSION") + <*> (Text.decodeUtf8 <$> variableEntry "HOMEPAGE") + <*> packageDownloads + <*> (mapMaybe digestFromByteString <$> packageChecksums) + <* eof + +updateDownloadVersion :: PackageInfo -> Text -> Maybe String -> [URI] +updateDownloadVersion package toVersion gnomeVersion + = updateDownload (version package) toVersion gnomeVersion + <$> downloads package + +updateDownload :: Text -> Text -> Maybe String -> URI -> URI +updateDownload fromVersion toVersion gnomeVersion + = updateCoreVersion fromVersion toVersion gnomeVersion + . updatePackageVersion fromVersion toVersion gnomeVersion + +updatePackageVersion :: Text -> Text -> Maybe String -> URI -> URI +updatePackageVersion fromVersion toVersion _gnomeVersion download = download + { uriPath = uriPath download >>= traverse (traverse updatePathPiece) + } + where + updatePathPiece = mkPathPiece + . Text.replace fromMajor toMajor + . Text.replace fromVersion toVersion + . unRText + fromMajor = major fromVersion + toMajor = major toVersion + +major :: Text -> Text +major = Text.intercalate "." . take 2 . Text.splitOn "." + +updateCoreVersion :: Text -> Text -> Maybe String -> URI -> URI +updateCoreVersion _fromVersion _toVersion (Just gnomeVersion) download + | Just (False, pathPieces) <- uriPath download + , (beforeCore, afterCore) <- NonEmpty.break (comparePathPiece "core") pathPieces + , _ : _ : _ : sources : afterSources <- afterCore + , comparePathPiece "sources" sources && not (null afterSources) + , Right Authority{..} <- uriAuthority download + , ".gnome.org" `Text.isSuffixOf` unRText authHost + , Nothing <- authPort = + download { uriPath = buildPath beforeCore afterSources } + where + comparePathPiece this that = Just that == mkPathPiece this + buildPath beforeCore afterSources = do + core <- mkPathPiece "core" + let textGnomeVersion = Text.pack gnomeVersion + minorGnomeVersion <- mkPathPiece $ major textGnomeVersion + patchGnomeVersion <- mkPathPiece textGnomeVersion + sources <- mkPathPiece "sources" + let afterCore = core : minorGnomeVersion : patchGnomeVersion : sources : afterSources + (False,) <$> NonEmpty.nonEmpty (beforeCore ++ afterCore) +updateCoreVersion _ _ _ download = download + +update :: PackageInfo -> Text -> [URI] -> [Digest MD5] -> PackageInfo +update old toVersion downloads' checksums' = old + { version = toVersion + , downloads = downloads' + , checksums = checksums' + } + +generate :: PackageInfo -> Text +generate pkg = Lazy.Text.toStrict $ Text.Builder.toLazyText builder + where + digestToText = Text.pack . foldr hexAppender "" . ByteArray.unpack + hexAppender x acc + | x > 15 = showHex x acc + | otherwise = '0' : showHex x acc + builder = "PKGNAM=\"" <> Text.Builder.fromString (pkgname pkg) <> "\"\n" + <> "VERSION=\"" <> Text.Builder.fromText (version pkg) <> "\"\n" + <> "HOMEPAGE=\"" <> Text.Builder.fromText (homepage pkg) <> "\"\n" + <> generateMultiEntry "DOWNLOAD" (render <$> downloads pkg) + <> generateMultiEntry "MD5SUM" (digestToText <$> checksums pkg) + +generateMultiEntry :: Text -> [Text] -> Text.Builder +generateMultiEntry name entries = Text.Builder.fromText name + <> "=\"" + <> Text.Builder.fromText (Text.intercalate separator entries) + <> "\"\n" + where + padLength = Text.length name + 2 + separator = " \\\n" <> Text.replicate padLength " " diff --git a/lib/up2date.rb b/lib/up2date.rb index d9eab15..5d63da3 100644 --- a/lib/up2date.rb +++ b/lib/up2date.rb @@ -35,20 +35,6 @@ module SlackBuilder end end - # Request the latest version from the packagist API. - class Packagist < Repository - def initialize(vendor, name) - super() - - @vendor = vendor - @name = name - end - - def latest - `./bin/slackbuilder packagist #{@vendor} #{@name}`.strip - end - end - # Reads a remote LATEST file. class LatestText < Repository def initialize(latest_url) diff --git a/slackbuilder.cabal b/slackbuilder.cabal index b56c39d..0a7cdb5 100644 --- a/slackbuilder.cabal +++ b/slackbuilder.cabal @@ -18,9 +18,13 @@ extra-source-files: CHANGELOG.md common dependencies build-depends: base ^>= 4.16.4.0, + bytestring ^>= 0.11.0, cryptonite >= 0.30, filepath ^>= 1.4.2, + megaparsec ^>= 9.5, modern-uri ^>= 0.3.6, + memory ^>= 0.18, + parser-combinators ^>= 1.3, text ^>= 2.0, tomland ^>= 1.3.3, transformers ^>= 0.5.6 @@ -28,18 +32,21 @@ common dependencies default-extensions: DataKinds DuplicateRecordFields + ExplicitForAll LambdaCase NamedFieldPuns OverloadedStrings RecordWildCards QuasiQuotes TemplateHaskell + TupleSections TypeApplications library import: dependencies exposed-modules: SlackBuilder.Config + SlackBuilder.Info SlackBuilder.Package SlackBuilder.Trans hs-source-dirs: lib @@ -58,7 +65,6 @@ executable slackbuilder SlackBuilder.Updater build-depends: aeson ^>= 2.2.0, - bytestring ^>= 0.11.0, conduit ^>= 1.3.5, http-client ^>= 0.7, optparse-applicative ^>= 0.18.1, @@ -77,10 +83,12 @@ test-suite slackbuilder-test main-is: Spec.hs other-modules: + SlackBuilder.InfoSpec SlackBuilder.PackageSpec hs-source-dirs: tests build-depends: hspec >= 2.10.9 && < 2.12, + hspec-megaparsec ^>= 2.2, slackbuilder ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall diff --git a/tests/SlackBuilder/InfoSpec.hs b/tests/SlackBuilder/InfoSpec.hs new file mode 100644 index 0000000..28167b5 --- /dev/null +++ b/tests/SlackBuilder/InfoSpec.hs @@ -0,0 +1,139 @@ +module SlackBuilder.InfoSpec + ( spec + ) where + +import Crypto.Hash (Digest, MD5, digestFromByteString) +import qualified Data.ByteString as ByteString +import Data.ByteString.Char8 (ByteString) +import Data.Maybe (maybeToList) +import qualified Data.Text.Encoding as Text +import Data.Void (Void) +import SlackBuilder.Info +import Test.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec.Megaparsec (parseSatisfies, shouldSucceedOn) +import Text.Megaparsec (parse) +import Text.Megaparsec.Error (ParseErrorBundle) +import Text.URI (mkURI) + +parseInfoFile' + :: ByteString + -> Either (ParseErrorBundle ByteString Void) PackageInfo +parseInfoFile' = parse parseInfoFile "" + +infoDownload0 :: ByteString +infoDownload0 = "PKGNAM=\"pkgnam\"\n\ + \VERSION=\"1.2.3\"\n\ + \HOMEPAGE=\"homepage\"\n\ + \DOWNLOAD=\"\"\n\ + \MD5SUM=\"\"\n" + +infoDownload1 :: ByteString +infoDownload1 = "PKGNAM=\"pkgnam\"\n\ + \VERSION=\"1.2.3\"\n\ + \HOMEPAGE=\"homepage\"\n\ + \DOWNLOAD=\"https://dlackware.com/download.tar.gz\"\n\ + \MD5SUM=\"0102030405060708090a0b0c0d0e0f10\"\n" + +maybeToDoubleList :: forall a. Maybe a -> [a] +maybeToDoubleList xs = [y | x <- maybeToList xs, y <- [x, x]] + +checksumSample :: [Digest MD5] +checksumSample = maybeToList $ digestFromByteString (ByteString.pack [1 .. 16]) + +spec :: Spec +spec = do + describe "parseInfoFile" $ do + it "returns package on a valid input" $ + parseInfoFile' `shouldSucceedOn` infoDownload1 + + it "returns an array with one element if one download is given" $ + let condition = (== 1) . length . checksums + in parseInfoFile' infoDownload1 `parseSatisfies` condition + + it "translates checksum characters into the binary format" $ + let expected = "0102030405060708090a0b0c0d0e0f10" + condition = (== expected) . show . head . checksums + in parseInfoFile' infoDownload1 `parseSatisfies` condition + + it "accepts an empty downloads list" $ + parseInfoFile' `shouldSucceedOn` infoDownload0 + + describe "generate" $ do + it "generates an .info file without downloads" $ + let given = PackageInfo "pkgnam" "1.2.3" "homepage" [] [] + in generate given `shouldBe` Text.decodeUtf8 infoDownload0 + + it "splits multiple downloads into multiple lines" $ + let downloads' = maybeToDoubleList + $ mkURI "https://dlackware.com/download.tar.gz" + checksums' = maybeToDoubleList + $ digestFromByteString (ByteString.pack [1.. 16]) + given = PackageInfo + "pkgnam" "1.2.3" "homepage" downloads' checksums' + expected = "PKGNAM=\"pkgnam\"\n\ + \VERSION=\"1.2.3\"\n\ + \HOMEPAGE=\"homepage\"\n\ + \DOWNLOAD=\"https://dlackware.com/download.tar.gz \\\n\ + \ https://dlackware.com/download.tar.gz\"\n\ + \MD5SUM=\"0102030405060708090a0b0c0d0e0f10 \\\n\ + \ 0102030405060708090a0b0c0d0e0f10\"\n" + in generate given `shouldBe` expected + + it "prints the checksum as a sequence of hexadecimal numbers" $ + let downloads' = maybeToList + $ mkURI "https://dlackware.com/download.tar.gz" + given = PackageInfo + "pkgnam" "1.2.3" "homepage" downloads' checksumSample + in generate given `shouldBe` Text.decodeUtf8 infoDownload1 + + describe "updateDownloadVersion" $ do + it "replaces the version" $ + let downloads' = maybeToList + $ mkURI "https://dlackware.com/download-1.2.3.tar.gz" + testPackage = PackageInfo + "pkgnam" "1.2.3" "homepage" downloads' checksumSample + expected = maybeToList + $ mkURI "https://dlackware.com/download-2.3.4.tar.gz" + actual = updateDownloadVersion testPackage "2.3.4" Nothing + in actual `shouldBe` expected + + it "updates the major version" $ + let downloads' = maybeToList + $ mkURI "https://dlackware.com/1.2/download.tar.gz" + testPackage = PackageInfo + "pkgnam" "1.2.3" "homepage" downloads' checksumSample + expected = maybeToList + $ mkURI "https://dlackware.com/2.3/download.tar.gz" + actual = updateDownloadVersion testPackage "2.3.4" Nothing + in actual `shouldBe` expected + + it "updates gnome version" $ + let downloads' = maybeToList + $ mkURI "https://download.gnome.org/core/3.36/3.36.0/sources/gnome-calendar-3.36.0.tar.xz" + testPackage = PackageInfo + "gnome-calendar" "3.36.0" "https://wiki.gnome.org/Core/Calendar" downloads' checksumSample + expected = maybeToList + $ mkURI "https://download.gnome.org/core/3.36/3.36.4/sources/gnome-calendar-3.36.2.tar.xz" + actual = updateDownloadVersion testPackage "3.36.2" $ Just "3.36.4" + in actual `shouldBe` expected + + it "updates versions without a patch number" $ + let downloads' = maybeToList + $ mkURI "https://dlackware.com/gnome-contacts-3.36.tar.xz" + testPackage = PackageInfo + "gnome-contacts" "3.36" "homepage" downloads' checksumSample + expected = maybeToList + $ mkURI "https://dlackware.com/gnome-contacts-3.36.2.tar.xz" + actual = updateDownloadVersion testPackage "3.36.2" Nothing + in actual `shouldBe` expected + + describe "update" $ + it "replaces the version" $ + let downloads' = maybeToList + $ mkURI "https://dlackware.com/1.2/download.tar.gz" + testPackage = PackageInfo + "pkgnam" "1.2.3" "homepage" downloads' checksumSample + expected = PackageInfo + "pkgnam" "2.3.4" "homepage" downloads' checksumSample + given = update testPackage "2.3.4" downloads' checksumSample + in given `shouldBe` expected