From 09393f6aaeacccac241f2090465ab527ff892285 Mon Sep 17 00:00:00 2001 From: skymanbp <57272723+skymanbp@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:25:26 -0400 Subject: [PATCH 1/3] ci: add Windows and macOS test lanes The suite passes on Windows since #212; these lanes keep it that way. Runs the cabal.project.ci subset on windows-latest and macos-14 at the ends of the tested GHC range (9.6.7, 9.12.2). --- .github/workflows/ci-windows-macos.yml | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/ci-windows-macos.yml diff --git a/.github/workflows/ci-windows-macos.yml b/.github/workflows/ci-windows-macos.yml new file mode 100644 index 00000000..cc997d13 --- /dev/null +++ b/.github/workflows/ci-windows-macos.yml @@ -0,0 +1,44 @@ +# Windows + macOS lanes over the portable subset in cabal.project.ci +# (skips packages needing libtorch, system C libs, or foreign-library). + +name: CI (Windows + macOS) + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + name: ${{ matrix.os }} / GHC ${{ matrix.ghc }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, macos-14] + ghc: ["9.6.7", "9.12.2"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Set up GHC ${{ matrix.ghc }} + id: setup + uses: haskell-actions/setup@v2 + with: + ghc-version: ${{ matrix.ghc }} + cabal-version: latest + + - name: Freeze + run: cabal freeze --project-file=cabal.project.ci + + - name: Cache cabal store + uses: actions/cache@v4 + with: + path: ${{ steps.setup.outputs.cabal-store }} + key: ${{ runner.os }}-ghc-${{ matrix.ghc }}-${{ hashFiles('cabal.project.ci.freeze') }} + restore-keys: ${{ runner.os }}-ghc-${{ matrix.ghc }}- + + - name: Build (cabal.project.ci subset) + run: cabal build all --project-file=cabal.project.ci + + - name: Test (cabal.project.ci subset) + run: cabal test all --project-file=cabal.project.ci From b3c143bbbcf0ca34c89cf71ea47740802a75160c Mon Sep 17 00:00:00 2001 From: skymanbp <57272723+skymanbp@users.noreply.github.com> Date: Tue, 18 Aug 2026 23:52:11 -0400 Subject: [PATCH 2/3] fix: Windows portability in library and app code Same classes #212 fixed in tests. CSV and HTML writers and the lazy reader used locale handles; pin them to UTF-8. 'start' is a cmd builtin, so launch it through a shell. lazy-bench wrote to /tmp, which Windows lacks; use the system temp dir. --- app/LazyBenchmark.hs | 16 +++++++++------- dataframe-csv/src/DataFrame/IO/CSV.hs | 4 +++- dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs | 3 +++ .../DataFrame/Internal/Parsing.hs | 14 ++++++++++++-- .../src/DataFrame/Display/Web/Plot.hs | 19 ++++++++++++------- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/app/LazyBenchmark.hs b/app/LazyBenchmark.hs index 9cfd43f9..84fefb99 100644 --- a/app/LazyBenchmark.hs +++ b/app/LazyBenchmark.hs @@ -8,7 +8,7 @@ Usage: cabal run lazy-bench [-- [OPTIONS]] (+RTS -s -RTS for heap stats) --rows N rows to generate (default 1_000_000_000) - --file PATH output CSV path (default /tmp/lazy_1b.csv) + --file PATH output CSV path (default: lazy_1b.csv in the system temp dir) --skip-gen reuse the file if it already exists -} module Main where @@ -22,7 +22,7 @@ import qualified DataFrame as D import qualified DataFrame.Lazy as L import DataFrame.Operators import DataFrame.Schema (Schema (..), schemaType) -import System.Directory (doesFileExist, getFileSize) +import System.Directory (doesFileExist, getFileSize, getTemporaryDirectory) import System.Environment (getArgs) import System.Exit (exitFailure) import System.IO ( @@ -42,8 +42,9 @@ import System.Random.Stateful defaultRows :: Int defaultRows = 1_000_000_000 -defaultFile :: FilePath -defaultFile = "/tmp/lazy_1b.csv" +-- /tmp does not exist on Windows. +defaultFile :: IO FilePath +defaultFile = (<> "/lazy_1b.csv") <$> getTemporaryDirectory -- Rows written per Builder flush to disk. chunkSize :: Int @@ -59,8 +60,8 @@ data Opts = Opts , optSkipGen :: Bool } -parseArgs :: [String] -> Either String Opts -parseArgs = go (Opts defaultRows defaultFile False) +parseArgs :: FilePath -> [String] -> Either String Opts +parseArgs defFile = go (Opts defaultRows defFile False) where go opts [] = Right opts go opts ("--rows" : n : rest) = case reads n of @@ -170,7 +171,8 @@ main :: IO () main = do hSetBuffering stdout LineBuffering args <- getArgs - opts <- case parseArgs args of + defFile <- defaultFile + opts <- case parseArgs defFile args of Left err -> putStrLn ("Error: " ++ err) >> exitFailure Right o -> return o diff --git a/dataframe-csv/src/DataFrame/IO/CSV.hs b/dataframe-csv/src/DataFrame/IO/CSV.hs index f9a7db4a..f5fb050e 100644 --- a/dataframe-csv/src/DataFrame/IO/CSV.hs +++ b/dataframe-csv/src/DataFrame/IO/CSV.hs @@ -35,7 +35,9 @@ import qualified Data.ByteString.Lazy as BL import qualified Data.Map.Strict as M import qualified Data.Text as T import qualified Data.Text.Encoding as TE -import qualified Data.Text.IO as TIO + +-- UTF-8 byte mode; locale handles corrupt output on Windows. +import qualified Data.Text.IO.Utf8 as TIO import Control.Exception (SomeException, catch) import Data.Maybe (fromMaybe) diff --git a/dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs b/dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs index c3a24c15..5d33c457 100644 --- a/dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs +++ b/dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs @@ -103,6 +103,8 @@ readSeparated c opts path = do Nothing -> (0, totalRows') Just (start, len'') -> (start, min len'' (totalRows' - rowsRead opts)) withFile path ReadMode $ \handle -> do + -- Decode UTF-8, not the locale. + hSetEncoding handle utf8 firstRow <- fmap T.strip . parseSep c <$> TIO.hGetLine handle let columnNames = if hasHeader opts @@ -256,6 +258,7 @@ openCsvStream :: openCsvStream sep schema path = do handle <- openFile path ReadMode hSetBuffering handle (BlockBuffering (Just (8 * 1024 * 1024))) + hSetEncoding handle utf8 headerLine <- TIO.hGetLine handle let headerCols = fmap (T.filter (/= '"') . T.strip) (parseSep sep headerLine) let schemaMap = elements schema diff --git a/dataframe-parsing/src-internal/DataFrame/Internal/Parsing.hs b/dataframe-parsing/src-internal/DataFrame/Internal/Parsing.hs index a7126247..221ddbe6 100644 --- a/dataframe-parsing/src-internal/DataFrame/Internal/Parsing.hs +++ b/dataframe-parsing/src-internal/DataFrame/Internal/Parsing.hs @@ -17,7 +17,15 @@ import Data.Foldable (fold) import Data.Text.Read (decimal, double, signed) import Data.Time (Day, defaultTimeLocale, parseTimeM) import GHC.Stack (HasCallStack) -import System.IO (Handle, IOMode (..), hIsEOF, hTell, withFile) +import System.IO ( + Handle, + IOMode (..), + hIsEOF, + hSetEncoding, + hTell, + utf8, + withFile, + ) import Prelude hiding (takeWhile) isNullish :: T.Text -> Bool @@ -187,7 +195,9 @@ lineEnd = -- | First pass to count rows for exact allocation. countRows :: Char -> FilePath -> IO Int -countRows c path = withFile path ReadMode $! go 0 "" +countRows c path = withFile path ReadMode $ \h -> + -- Decode UTF-8, not the locale. + hSetEncoding h utf8 >> go 0 "" h where go n input h = do isEOF <- hIsEOF h diff --git a/dataframe-viz/src/DataFrame/Display/Web/Plot.hs b/dataframe-viz/src/DataFrame/Display/Web/Plot.hs index 64dc8075..af68dec9 100644 --- a/dataframe-viz/src/DataFrame/Display/Web/Plot.hs +++ b/dataframe-viz/src/DataFrame/Display/Web/Plot.hs @@ -66,15 +66,19 @@ import Data.Char (chr) import qualified Data.List as L import qualified Data.Maybe import qualified Data.Text as T -import qualified Data.Text.IO as T + +-- UTF-8 byte mode; locale handles corrupt output on Windows. +import qualified Data.Text.IO.Utf8 as T import GHC.Stack (HasCallStack) import Numeric (showFFloat) import System.Directory (getHomeDirectory) import System.Info (os) import System.Process ( + CreateProcess, StdStream (NoStream), createProcess, proc, + shell, std_err, std_in, std_out, @@ -471,15 +475,16 @@ showInDefaultBrowser p = do putStrLn fullPath T.writeFile fullPath (T.pack p) case os of - "mingw32" -> openFileSilently "start" fullPath - "darwin" -> openFileSilently "open" fullPath - _ -> openFileSilently "xdg-open" fullPath + -- 'start' is a cmd builtin; it needs a shell. + "mingw32" -> launchSilently (shell ("start \"\" \"" <> fullPath <> "\"")) + "darwin" -> launchSilently (proc "open" [fullPath]) + _ -> launchSilently (proc "xdg-open" [fullPath]) -openFileSilently :: FilePath -> FilePath -> IO () -openFileSilently program path = do +launchSilently :: CreateProcess -> IO () +launchSilently cp = do (_, _, _, ph) <- createProcess - (proc program [path]) + cp { std_in = NoStream , std_out = NoStream , std_err = NoStream From 82e295b64dee3313586a2b9a2910f4345ebbbb69 Mon Sep 17 00:00:00 2001 From: skymanbp <57272723+skymanbp@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:03:11 -0400 Subject: [PATCH 3/3] Remove redundant comments, move the start note into the haddock --- app/LazyBenchmark.hs | 1 - dataframe-csv/src/DataFrame/IO/CSV.hs | 2 -- dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs | 1 - dataframe-parsing/src-internal/DataFrame/Internal/Parsing.hs | 1 - dataframe-viz/src/DataFrame/Display/Web/Plot.hs | 4 +--- 5 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app/LazyBenchmark.hs b/app/LazyBenchmark.hs index 84fefb99..cd30a60d 100644 --- a/app/LazyBenchmark.hs +++ b/app/LazyBenchmark.hs @@ -42,7 +42,6 @@ import System.Random.Stateful defaultRows :: Int defaultRows = 1_000_000_000 --- /tmp does not exist on Windows. defaultFile :: IO FilePath defaultFile = (<> "/lazy_1b.csv") <$> getTemporaryDirectory diff --git a/dataframe-csv/src/DataFrame/IO/CSV.hs b/dataframe-csv/src/DataFrame/IO/CSV.hs index f5fb050e..547f58d1 100644 --- a/dataframe-csv/src/DataFrame/IO/CSV.hs +++ b/dataframe-csv/src/DataFrame/IO/CSV.hs @@ -35,8 +35,6 @@ import qualified Data.ByteString.Lazy as BL import qualified Data.Map.Strict as M import qualified Data.Text as T import qualified Data.Text.Encoding as TE - --- UTF-8 byte mode; locale handles corrupt output on Windows. import qualified Data.Text.IO.Utf8 as TIO import Control.Exception (SomeException, catch) diff --git a/dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs b/dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs index 5d33c457..d4229302 100644 --- a/dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs +++ b/dataframe-lazy/src/DataFrame/Lazy/IO/CSV.hs @@ -103,7 +103,6 @@ readSeparated c opts path = do Nothing -> (0, totalRows') Just (start, len'') -> (start, min len'' (totalRows' - rowsRead opts)) withFile path ReadMode $ \handle -> do - -- Decode UTF-8, not the locale. hSetEncoding handle utf8 firstRow <- fmap T.strip . parseSep c <$> TIO.hGetLine handle let columnNames = diff --git a/dataframe-parsing/src-internal/DataFrame/Internal/Parsing.hs b/dataframe-parsing/src-internal/DataFrame/Internal/Parsing.hs index 221ddbe6..7b922b39 100644 --- a/dataframe-parsing/src-internal/DataFrame/Internal/Parsing.hs +++ b/dataframe-parsing/src-internal/DataFrame/Internal/Parsing.hs @@ -196,7 +196,6 @@ lineEnd = -- | First pass to count rows for exact allocation. countRows :: Char -> FilePath -> IO Int countRows c path = withFile path ReadMode $ \h -> - -- Decode UTF-8, not the locale. hSetEncoding h utf8 >> go 0 "" h where go n input h = do diff --git a/dataframe-viz/src/DataFrame/Display/Web/Plot.hs b/dataframe-viz/src/DataFrame/Display/Web/Plot.hs index af68dec9..e28717b8 100644 --- a/dataframe-viz/src/DataFrame/Display/Web/Plot.hs +++ b/dataframe-viz/src/DataFrame/Display/Web/Plot.hs @@ -66,8 +66,6 @@ import Data.Char (chr) import qualified Data.List as L import qualified Data.Maybe import qualified Data.Text as T - --- UTF-8 byte mode; locale handles corrupt output on Windows. import qualified Data.Text.IO.Utf8 as T import GHC.Stack (HasCallStack) import Numeric (showFFloat) @@ -462,6 +460,7 @@ plotBoxPlots ys = box (mkBox ys) -- Browser launcher -- --------------------------------------------------------------------------- +-- | Windows launches via a shell: 'start' is a cmd builtin. showInDefaultBrowser :: String -> IO () showInDefaultBrowser p = do plotId <- generateChartId @@ -475,7 +474,6 @@ showInDefaultBrowser p = do putStrLn fullPath T.writeFile fullPath (T.pack p) case os of - -- 'start' is a cmd builtin; it needs a shell. "mingw32" -> launchSilently (shell ("start \"\" \"" <> fullPath <> "\"")) "darwin" -> launchSilently (proc "open" [fullPath]) _ -> launchSilently (proc "xdg-open" [fullPath])