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] 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