diff --git a/.github/actions/check-release-builds/action.yml b/.github/actions/check-release-builds/action.yml new file mode 100644 index 000000000..fc996ec5e --- /dev/null +++ b/.github/actions/check-release-builds/action.yml @@ -0,0 +1,59 @@ +name: "Check release builds" +description: "Check release builds for public libraries in local packages" +inputs: + ghc-version: + required: true + description: "Version of GHC" + cabal-version: + required: true + description: "Version of cabal" + hackage-index-state: + required: true + description: "Timestamp for Hackage index" + cli-args: + required: true + description: "Command-line arguments for the check-release-builds script" +runs: + using: composite + steps: + - name: 🛠️ Install Haskell + uses: haskell-actions/setup@v2 + id: setup-haskell + with: + ghc-version: ${{ inputs.ghc-version }} + cabal-version: ${{ inputs.cabal-version }} + + - name: 💾 Restore Cabal dependencies + uses: actions/cache/restore@v4 + if: ${{ !env.ACT }} + id: cache-cabal + with: + path: ${{ steps.setup-haskell.outputs.cabal-store }} + key: check-release-builds-${{ runner.os }}-ghc-${{ steps.setup-haskell.outputs.ghc-version }}-cabal-${{ steps.setup-haskell.outputs.cabal-version }}-input-state-${{ inputs.hackage-index-state }} + + - name: 🛠️ Build Cabal dependencies + shell: sh + run: | + cabal build ./scripts/check-release-builds.hs \ + ${{ inputs.hackage-index-state && format('--index-state={0}', inputs.hackage-index-state) }} \ + --only-dependencies + + - name: 💾 Save Cabal dependencies + uses: actions/cache/save@v4 + if: ${{ !env.ACT && steps.cache-cabal.outputs.cache-hit != 'true' }} + with: + path: ${{ steps.setup-haskell.outputs.cabal-store }} + key: ${{ steps.cache-cabal.outputs.cache-primary-key }} + + - name: 🏗️ Build + shell: sh + run: | + cabal build ./scripts/check-release-builds.hs \ + ${{ inputs.hackage-index-state && format('--index-state={0}', inputs.hackage-index-state) }} + + - name: 🛠️ Check release builds + shell: sh + run: | + cabal run ./scripts/check-release-builds.hs \ + ${{ inputs.hackage-index-state && format('--index-state={0}', inputs.hackage-index-state) }} \ + -- ${{ inputs.cli-args }} diff --git a/.github/workflows/check-release-builds.yml b/.github/workflows/check-release-builds.yml new file mode 100644 index 000000000..5ed98624a --- /dev/null +++ b/.github/workflows/check-release-builds.yml @@ -0,0 +1,123 @@ +name: Check release builds + +on: + push: + branches: + - "main" + pull_request: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +# Set the default shell on all platforms. +defaults: + run: + shell: sh + +jobs: + ################################################################################ + # Build + ################################################################################ + build: + name: | + ${{ format( + 'Check release build on {0}{1}{2}', + startsWith(matrix.os, 'ubuntu-') && 'Linux' || startsWith(matrix.os, 'macOS-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows', + matrix.ghc-version && format(' with GHC {0}', matrix.ghc-version), + matrix.cabal-version && format(' and Cabal {0}', matrix.cabal-version) + ) + }} + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + + env: + full-run: ${{ github.event_name == 'workflow_dispatch' || matrix.ghc-version == '9.6' }} + + steps: + - name: 📥 Checkout repository + if: ${{ env.full-run }} + uses: actions/checkout@v4 + + - name: 🗄️ Print Job info + if: ${{ env.full-run }} + run: | + echo 'matrix.os: ${{ matrix.os }}' + echo 'matrix.ghc-version: ${{ matrix.ghc-version }}' + echo 'matrix.cabal-version: ${{ matrix.cabal-version }}' + echo 'toJSON(matrix): ${{ toJSON(matrix) }}' + + - name: 🛠️ Setup Haskell + if: ${{ env.full-run }} + id: setup-haskell + uses: haskell-actions/setup@v2 + with: + ghc-version: ${{ matrix.ghc-version }} + cabal-version: ${{ matrix.cabal-version }} + + - name: 🛠️ Setup system dependencies (Linux) + if: ${{ runner.os == 'Linux' && env.full-run }} + run: sudo apt-get update && sudo apt-get -y install liburing-dev librocksdb-dev + env: + DEBIAN_FRONTEND: "noninteractive" + + - name: 🛠️ Configure + if: ${{ env.full-run }} + run: | + echo "packages: ./blockio/blockio.cabal ./lsm-tree.cabal" > cabal.project.temp + cabal configure \ + --project-file="cabal.project.temp" \ + --disable-tests \ + --disable-benchmarks \ + --index-state=HEAD \ + --ghc-options="-Werror" \ + cat "cabal.project.temp.local" + + - name: 💾 Generate Cabal plan + if: ${{ env.full-run }} + run: | + cabal build all \ + --project-file="cabal.project.temp" \ + --dry-run + + - name: 💾 Restore Cabal dependencies + uses: actions/cache/restore@v4 + if: ${{ !env.ACT && env.full-run }} + id: cache-cabal + env: + key: check-release-build-${{ runner.os }}-ghc-${{ steps.setup-haskell.outputs.ghc-version }}-cabal-${{ steps.setup-haskell.outputs.cabal-version }} + with: + path: ${{ steps.setup-haskell.outputs.cabal-store }} + key: ${{ env.key }}-plan-${{ hashFiles('dist-newstyle/cache/plan.json') }} + restore-keys: ${{ env.key }}- + + - name: 🛠️ Build Cabal dependencies + if: ${{ env.full-run }} + run: | + cabal build all \ + --project-file="cabal.project.temp" \ + --only-dependencies + + - name: 💾 Save Cabal dependencies + uses: actions/cache/save@v4 + if: ${{ !env.ACT && steps.cache-cabal.outputs.cache-hit != 'true' && env.full-run }} + with: + path: ${{ steps.setup-haskell.outputs.cabal-store }} + key: ${{ steps.cache-cabal.outputs.cache-primary-key }} + + - name: 🏗️ Build + if: ${{ env.full-run }} + run: | + cabal build all \ + --project-file="cabal.project.temp" + + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "macOS-latest", "windows-latest"] + ghc-version: ["9.2", "9.4", "9.6", "9.8", "9.10", "9.12"] + cabal-version: ["3.12"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99c42eda4..5aac35cb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -342,6 +342,26 @@ jobs: - name: 🎗️ Lint with ShellCheck run: ./scripts/lint-shellcheck.sh + ################################################################################ + # Check release builds + ################################################################################ + check-release-builds: + name: Check release builds + runs-on: ubuntu-latest + steps: + - name: 📥 Checkout repository + uses: actions/checkout@v4 + + - name: 🛠️ Check release builds + uses: ./.github/actions/check-release-builds + with: + ghc-version: ${{ env.DEFAULT_GHC_VERSION }} + cabal-version: ${{ env.DEFAULT_CABAL_VERSION }} + hackage-index-state: "2025-06-16T00:00:00Z" + # Run without supplying any cabal files just to verify whether the + # script runs without problems. + cli-args: "Default" + ################################################################################ # Publish documentation ################################################################################ diff --git a/bloomfilter/examples/spell.hs b/bloomfilter/examples/spell.hs index a1cf356a4..91886534f 100644 --- a/bloomfilter/examples/spell.hs +++ b/bloomfilter/examples/spell.hs @@ -1,7 +1,7 @@ {-# LANGUAGE BangPatterns #-} module Main (main) where -import Control.Monad (forM_, when) +import Control.Monad (forM_, unless, when) import System.Environment (getArgs) import qualified Data.BloomFilter as B @@ -9,8 +9,9 @@ import qualified Data.BloomFilter as B main :: IO () main = do files <- getArgs - dictionary <- readFile "/usr/share/dict/words" - let !bloom = B.fromList (B.policyForFPR 0.01) (words dictionary) - forM_ files $ \file -> - putStrLn . unlines . filter (`B.notElem` bloom) . words - =<< readFile file + unless (null files) $ do + dictionary <- readFile "/usr/share/dict/words" + let !bloom = B.fromList (B.policyForFPR 0.01) (words dictionary) + forM_ files $ \file -> + putStrLn . unlines . filter (`B.notElem` bloom) . words + =<< readFile file diff --git a/bloomfilter/tests/fpr-calc.hs b/bloomfilter/tests/fpr-calc.hs index 8776eb99c..cf9acfc64 100644 --- a/bloomfilter/tests/fpr-calc.hs +++ b/bloomfilter/tests/fpr-calc.hs @@ -12,7 +12,6 @@ import qualified Data.IntSet as IntSet import Data.List (unfoldr) import Math.Regression.Simple import System.Environment (getArgs) -import System.Exit (exitFailure) import System.IO import System.Random @@ -29,7 +28,8 @@ main = do ["Regression"] -> main_regression _ -> do putStrLn "Usage: bloomfilter-fpr-calc [Generate|Regression]" - exitFailure + -- NOTE: we expicitly don't use exitFailure here because + -- @bloomfilter-fpr-calc@ is a cabal @test-suite@. main_regression :: IO () main_regression = do diff --git a/lsm-tree.cabal b/lsm-tree.cabal index 0ae17ab31..eaef8b500 100644 --- a/lsm-tree.cabal +++ b/lsm-tree.cabal @@ -692,9 +692,14 @@ benchmark bloomfilter-bench , lsm-tree:bloomfilter , random -executable bloomfilter-fpr-calc +-- TODO: ideally this should be an executable because it's not really testing +-- anything, but if we make it an executable component then the solver takes the +-- build-depends into account when building the public library, which is not +-- desirable. Maybe these executables should be moved out into a separate +-- package? +test-suite bloomfilter-fpr-calc import: language, warnings - scope: private + type: exitcode-stdio-1.0 hs-source-dirs: bloomfilter/tests main-is: fpr-calc.hs build-depends: @@ -707,9 +712,14 @@ executable bloomfilter-fpr-calc ghc-options: -threaded -executable bloomfilter-spell +-- TODO: ideally this should be an executable because it's not really testing +-- anything, but if we make it an executable component then the solver takes the +-- build-depends into account when building the public library, which is not +-- desirable. Maybe these executables should be moved out into a separate +-- package? +test-suite bloomfilter-spell import: language - scope: private + type: exitcode-stdio-1.0 hs-source-dirs: bloomfilter/examples main-is: spell.hs build-depends: diff --git a/scripts/check-release-builds.hs b/scripts/check-release-builds.hs new file mode 100755 index 000000000..9370cc0e3 --- /dev/null +++ b/scripts/check-release-builds.hs @@ -0,0 +1,123 @@ +#!/usr/bin/env cabal +{- cabal: +build-depends: + , base >=4.16 && <4.22 + , directory ^>=1.3 + , filepath ^>=1.5 + , process ^>=1.6 + +ghc-options: + -Wall -Wcompat -Wincomplete-uni-patterns + -Wincomplete-record-updates -Wpartial-fields -Widentities + -Wredundant-constraints -Wmissing-export-lists + -Wno-unticked-promoted-constructors -Wunused-packages + +ghc-options: -Werror=missing-deriving-strategies +-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GHC2021 #-} +{-# LANGUAGE LambdaCase #-} + +import Control.Exception +import Control.Monad +import System.Directory +import System.Environment +import System.Exit +import System.FilePath +import System.Process +import Text.Printf + +main :: IO () +main = do + args <- getArgs + case args of + ghcVersions : cabalFiles + | Just vs <- case ghcVersions of + "Default" -> Just [Default] + "All" -> Just allGhcVersions + _ -> Nothing + -> do + printf "Checking release builds for %s with ghc versions %s...\n" (show cabalFiles) (show $ fmap ghcVersionExecutableName vs) + + forM_ vs findGhcExecutable + + withTempProjectFile cabalFiles $ \projectFile -> + forM_ cabalFiles $ \cabalFile -> do + let component = (dropExtension $ takeFileName cabalFile) + forM_ vs $ buildComponentWith projectFile component + + printf "All release builds successful for GHC versions: %s" (unwords $ fmap ghcVersionExecutableName vs) + _ -> do + putStrLn "Usage: [Default|All] FILES" + +-- TODO: I wanted to use the --ignore-project cabal option, which should be +-- a globally configurable value according to the cabal user guide, but for +-- some reason cabal thinks it's a valid option. So, I'm creating a +-- temporary project file instead. +tempProjectFilePath :: FilePath +tempProjectFilePath = "cabal.project.temp" + +withTempProjectFile :: [FilePath] -> (FilePath -> IO ()) -> IO () +withTempProjectFile cabalFiles k = do + let contents = unwords ("packages:" : cabalFiles) + printf "Creating temporary project file at %s with contents %s...\n" tempProjectFilePath contents + bracket + (do writeFile tempProjectFilePath contents + pure tempProjectFilePath) + (\_ -> removeFile tempProjectFilePath) + k + +data GhcVersion = Default | Ghc9_2 | Ghc9_4 | Ghc9_6 | Ghc9_8 | Ghc9_10 | Ghc9_12 + deriving stock (Enum, Bounded) + +allGhcVersions :: [GhcVersion] +allGhcVersions = [Ghc9_2 .. maxBound] + +ghcVersionExecutableName :: GhcVersion -> String +ghcVersionExecutableName = \case + Default -> "ghc" + Ghc9_2 -> "ghc-9.2" + Ghc9_4 -> "ghc-9.4" + Ghc9_6 -> "ghc-9.6" + Ghc9_8 -> "ghc-9.8" + Ghc9_10 -> "ghc-9.10" + Ghc9_12 -> "ghc-9.12" + +findGhcExecutable :: GhcVersion -> IO () +findGhcExecutable v = do + let exe = ghcVersionExecutableName v + printf "Finding executable for %s...\n" exe + ec <- readProcessWithExitCode' "which" [exe] "" + unless (ec == ExitSuccess) $ do + printf "Could not find executable for %s...\n" exe + exitWith ec + +buildComponentWith :: String -> FilePath -> GhcVersion -> IO () +buildComponentWith projectFile component v = do + let ghcExe = ghcVersionExecutableName v + printf "Building %s with %s...\n" component ghcExe + let args = [ + "--project-file="++projectFile + , "--disable-tests" + , "--disable-benchmarks" + , "--index-state=HEAD" + , "--with-compiler=" ++ ghcExe + ] + ec <- readProcessWithExitCode' "cabal" ("build" : component : args) "" + unless (ec == ExitSuccess) $ do + printf "Failure during building %s with %s...\n" component ghcExe + exitWith ec + +readProcessWithExitCode' :: FilePath -> [String] -> String -> IO ExitCode +readProcessWithExitCode' cmd args inp = do + printf "Running '%s %s%s'...\n" cmd (unwords args) inp + (ec, sStdout, sStderr) <- readProcessWithExitCode cmd args inp + + -- TODO: ideally stdout and stderr would be streamed to the terminal that + -- the script was called from, but for now it only prints both when the + -- build process has terminated. + putStrLn "Printing stdout..." + putStr sStdout + putStrLn "Printing stderr..." + putStr sStderr + pure ec