From 6da8b929816887df7ea772b9690e59a81b3952f3 Mon Sep 17 00:00:00 2001 From: Tommy Bidne Date: Mon, 16 Jun 2025 16:59:06 +1200 Subject: [PATCH 1/6] Add --cabal-path option Allows usage with custom cabal path. Additionally, adds general --cabal-options argument, for passing through arbitrary cabal options. The latter subsumes the --jobs and --cabal-verbosity args, hence they are removed. The previous behavior can be achieved with e.g. clc-stackage --cabal-options='--jobs=semaphore --verbose=1' --- clc-stackage.cabal | 10 +- src/builder/CLC/Stackage/Builder/Env.hs | 40 +------- src/builder/CLC/Stackage/Builder/Process.hs | 7 +- src/runner/CLC/Stackage/Runner/Args.hs | 107 +++++++------------- src/runner/CLC/Stackage/Runner/Env.hs | 31 +++--- src/utils/CLC/Stackage/Utils/Exception.hs | 18 +++- test/unit/Unit/Prelude.hs | 2 + 7 files changed, 85 insertions(+), 130 deletions(-) diff --git a/clc-stackage.cabal b/clc-stackage.cabal index 2e4d2a1..2e83ba1 100644 --- a/clc-stackage.cabal +++ b/clc-stackage.cabal @@ -43,7 +43,7 @@ library utils , bytestring >=0.10.12.0 && <0.13 , directory ^>=1.3.5.0 , file-io ^>=0.1.0.0 - , filepath >=1.4.2.1 && <1.6 + , filepath >=1.4.100.0 && <1.6 , pretty-terminal ^>=0.1.0.0 , text >=1.2.3.2 && <2.2 , time >=1.9.3 && <1.15 @@ -130,9 +130,7 @@ executable clc-stackage library test-utils import: common-lang - exposed-modules: - Test.Utils - + exposed-modules: Test.Utils build-depends: , base , tasty >=1.1.0.3 && <1.6 @@ -157,7 +155,7 @@ test-suite unit , runner , tasty , tasty-golden - , tasty-hunit >=0.9 && <0.11 + , tasty-hunit >=0.9 && <0.11 , test-utils , time , utils @@ -177,9 +175,9 @@ test-suite functional , env-guard ^>=0.2 , filepath , runner - , test-utils , tasty , tasty-golden + , test-utils , text , time , utils diff --git a/src/builder/CLC/Stackage/Builder/Env.hs b/src/builder/CLC/Stackage/Builder/Env.hs index 3fb5475..200e8e7 100644 --- a/src/builder/CLC/Stackage/Builder/Env.hs +++ b/src/builder/CLC/Stackage/Builder/Env.hs @@ -1,10 +1,6 @@ -- | Provides the environment for building. module CLC.Stackage.Builder.Env ( BuildEnv (..), - CabalVerbosity (..), - cabalVerbosityToArg, - Jobs (..), - jobsToArg, Progress (..), WriteLogs (..), ) @@ -15,40 +11,6 @@ import CLC.Stackage.Utils.Logging qualified as Logging import Data.IORef (IORef) import Data.List.NonEmpty (NonEmpty) import Data.Set (Set) -import Data.Word (Word8) - --- | Cabal's --verbose flag -data CabalVerbosity - = -- | V0 - CabalVerbosity0 - | -- | V1 - CabalVerbosity1 - | -- | V2 - CabalVerbosity2 - | -- | V3 - CabalVerbosity3 - deriving stock (Eq, Show) - -cabalVerbosityToArg :: CabalVerbosity -> String -cabalVerbosityToArg CabalVerbosity0 = "--verbose=0" -cabalVerbosityToArg CabalVerbosity1 = "--verbose=1" -cabalVerbosityToArg CabalVerbosity2 = "--verbose=2" -cabalVerbosityToArg CabalVerbosity3 = "--verbose=3" - --- | Number of build jobs. -data Jobs - = -- | Literal number of jobs. - JobsN Word8 - | -- | String "$ncpus" - JobsNCpus - | -- | Job semaphore. Requires GHC 9.8 and Cabal 3.12 - JobsSemaphore - deriving stock (Eq, Show) - -jobsToArg :: Jobs -> String -jobsToArg (JobsN n) = "--jobs=" ++ show n -jobsToArg JobsNCpus = "--jobs=$ncpus" -jobsToArg JobsSemaphore = "--semaphore" data Progress = MkProgress { -- | Dependencies that built successfully. @@ -74,6 +36,8 @@ data BuildEnv = MkBuildEnv batch :: Maybe Int, -- | Build arguments for cabal. buildArgs :: [String], + -- | Optional path to cabal executable. + cabalPath :: FilePath, -- | If true, colors logs. colorLogs :: Bool, -- | If true, the first group that fails to completely build stops diff --git a/src/builder/CLC/Stackage/Builder/Process.hs b/src/builder/CLC/Stackage/Builder/Process.hs index 9bf88c3..7791093 100644 --- a/src/builder/CLC/Stackage/Builder/Process.hs +++ b/src/builder/CLC/Stackage/Builder/Process.hs @@ -8,7 +8,8 @@ where import CLC.Stackage.Builder.Batch (PackageGroup (unPackageGroup)) import CLC.Stackage.Builder.Env ( BuildEnv - ( colorLogs, + ( cabalPath, + colorLogs, groupFailFast, hLogger, progress, @@ -45,7 +46,7 @@ buildProject env idx pkgs = do let buildNoLogs :: IO ExitCode buildNoLogs = withGeneratedDir $ - (\(ec, _, _) -> ec) <$> P.readProcessWithExitCode "cabal" env.buildArgs "" + (\(ec, _, _) -> ec) <$> P.readProcessWithExitCode env.cabalPath env.buildArgs "" buildLogs :: Bool -> IO ExitCode buildLogs saveFailures = do @@ -53,7 +54,7 @@ buildProject env idx pkgs = do IO.withBinaryFileWriteMode stdoutPath $ \stdoutHandle -> IO.withBinaryFileWriteMode stderrPath $ \stderrHandle -> do - let createProc = P.proc "cabal" env.buildArgs + let createProc = P.proc env.cabalPath env.buildArgs createProc' = createProc { P.std_out = P.UseHandle stdoutHandle, diff --git a/src/runner/CLC/Stackage/Runner/Args.hs b/src/runner/CLC/Stackage/Runner/Args.hs index 4c0cf81..d99fcf1 100644 --- a/src/runner/CLC/Stackage/Runner/Args.hs +++ b/src/runner/CLC/Stackage/Runner/Args.hs @@ -6,15 +6,9 @@ module CLC.Stackage.Runner.Args where import CLC.Stackage.Builder.Env - ( CabalVerbosity - ( CabalVerbosity0, - CabalVerbosity1, - CabalVerbosity2, - CabalVerbosity3 - ), - Jobs (JobsN, JobsNCpus, JobsSemaphore), - WriteLogs (WriteLogsCurrent, WriteLogsNone, WriteLogsSaveFailures), + ( WriteLogs (WriteLogsCurrent, WriteLogsNone, WriteLogsSaveFailures), ) +import Data.String qualified as Str import Options.Applicative ( Mod, Parser, @@ -35,7 +29,8 @@ import Options.Applicative.Help.Chunk (Chunk (Chunk)) import Options.Applicative.Help.Chunk qualified as Chunk import Options.Applicative.Help.Pretty qualified as Pretty import Options.Applicative.Types (ArgPolicy (Intersperse)) -import Text.Read qualified as TR +import System.OsPath (OsPath) +import System.OsPath qualified as OsP -- | Log coloring option. data ColorLogs @@ -49,16 +44,16 @@ data Args = MkArgs { -- | If given, batches packages together so we build more than one. -- Defaults to batching everything together in the same group. batch :: Maybe Int, - -- | Cabal's --verbosity flag. - cabalVerbosity :: Maybe CabalVerbosity, + -- | Options to pass to cabal e.g. --semaphore. + cabalOpts :: [String], + -- | Optional path to cabal executable. + cabalPath :: Maybe OsPath, -- | Determines if we color the logs. If 'Nothing', attempts to detect -- if colors are supported. colorLogs :: ColorLogs, -- | If true, the first group that fails to completely build stops -- clc-stackage. groupFailFast :: Bool, - -- | Number of build jobs. - jobs :: Maybe Jobs, -- | Disables the cache, which otherwise saves the outcome of a run in a -- json file. The cache is used for resuming a run that was interrupted. noCache :: Bool, @@ -119,10 +114,10 @@ parseCliArgs :: Parser Args parseCliArgs = ( do batch <- parseBatch - cabalVerbosity <- parseCabalVerbosity + cabalOpts <- parseCabalOpts + cabalPath <- parseCabalPath colorLogs <- parseColorLogs groupFailFast <- parseGroupFailFast - jobs <- parseJobs noCache <- parseNoCache noCleanup <- parseNoCleanup packageFailFast <- parsePackageFailFast @@ -132,10 +127,10 @@ parseCliArgs = pure $ MkArgs { batch, - cabalVerbosity, + cabalOpts, + cabalPath, colorLogs, groupFailFast, - jobs, noCache, noCleanup, packageFailFast, @@ -164,27 +159,37 @@ parseBatch = ] ) -parseCabalVerbosity :: Parser (Maybe CabalVerbosity) -parseCabalVerbosity = +parseCabalOpts :: Parser [String] +parseCabalOpts = + OA.option + readOpts + ( mconcat + [ OA.long "cabal-options", + OA.metavar "ARGS...", + OA.value [], + mkHelp "Quoted arguments to pass to cabal e.g. '--semaphore --verbose=1'" + ] + ) + where + readOpts = Str.words <$> OA.str + +parseCabalPath :: Parser (Maybe OsPath) +parseCabalPath = OA.optional $ OA.option - readCabalVerbosity + readOsPath ( mconcat - [ OA.long "cabal-verbosity", - OA.metavar "(0 | 1 | 2 | 3)", - mkHelp - "Cabal's --verbose flag. Uses cabal's default if not given (1)." + [ OA.long "cabal-path", + OA.metavar "PATH", + mkHelp "Optional path to cabal executable." ] ) where - readCabalVerbosity = - OA.str >>= \case - "0" -> pure CabalVerbosity0 - "1" -> pure CabalVerbosity1 - "2" -> pure CabalVerbosity2 - "3" -> pure CabalVerbosity3 - other -> - fail $ "Expected one of (0 | 1 | 2 | 3), received: " ++ other + readOsPath = do + fp <- OA.str + case OsP.encodeUtf fp of + Just osp -> pure osp + Nothing -> fail $ "Failed encoding to ospath: " ++ fp parseColorLogs :: Parser ColorLogs parseColorLogs = @@ -220,44 +225,6 @@ parseGroupFailFast = "clc-stackage." ] -parseJobs :: Parser (Maybe Jobs) -parseJobs = - OA.optional $ - OA.option - readJobs - ( mconcat - [ OA.short 'j', - OA.long "jobs", - OA.metavar "(NAT | $ncpus | semaphore)", - mkHelp $ - mconcat - [ "Controls the number of build jobs i.e. the flag passed to ", - "cabal's --jobs option. Can be a natural number in [1, 255] ", - "or the literal string '$ncpus', meaning all cpus. The ", - "literal 'semaphore' will instead use cabal's --semaphore ", - "option. This requires GHC 9.8+ and Cabal 3.12+. No option ", - "uses cabal's default i.e. $ncpus." - ] - ] - ) - where - readJobs = - OA.str >>= \case - "$ncpus" -> pure JobsNCpus - "semaphore" -> pure JobsSemaphore - other -> case TR.readMaybe @Int other of - Just n -> - if n > 0 && n < 256 - then pure $ JobsN $ fromIntegral n - else fail $ "Expected NAT in [1, 255], received: " ++ other - Nothing -> - fail $ - mconcat - [ "Expected one of (NAT in [1, 255] | $ncpus | semaphore), ", - "received: ", - other - ] - parseNoCache :: Parser Bool parseNoCache = OA.switch diff --git a/src/runner/CLC/Stackage/Runner/Env.hs b/src/runner/CLC/Stackage/Runner/Env.hs index 2a52541..b4d27e3 100644 --- a/src/runner/CLC/Stackage/Runner/Env.hs +++ b/src/runner/CLC/Stackage/Runner/Env.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE QuasiQuotes #-} + module CLC.Stackage.Runner.Env ( RunnerEnv (..), setup, @@ -40,6 +42,7 @@ import CLC.Stackage.Runner.Args import CLC.Stackage.Runner.Args qualified as Args import CLC.Stackage.Runner.Report (Results (MkResults)) import CLC.Stackage.Runner.Report qualified as Report +import CLC.Stackage.Utils.Exception qualified as Ex import CLC.Stackage.Utils.IO qualified as IO import CLC.Stackage.Utils.Logging qualified as Logging import CLC.Stackage.Utils.Paths qualified as Paths @@ -49,7 +52,7 @@ import Data.Bool (Bool (False, True), not) import Data.Foldable (Foldable (foldl')) import Data.IORef (newIORef, readIORef) import Data.List.NonEmpty (NonEmpty ((:|))) -import Data.Maybe (Maybe (Just, Nothing), fromMaybe, maybe) +import Data.Maybe (Maybe (Just, Nothing), fromMaybe) import Data.Set (Set) import Data.Set qualified as Set import Data.Text qualified as T @@ -57,7 +60,9 @@ import Data.Time (LocalTime) import System.Console.Pretty (supportsPretty) import System.Directory.OsPath qualified as Dir import System.Exit (ExitCode (ExitSuccess)) -import Prelude (IO, mconcat, pure, show, ($), (++), (.), (<$>), (<>)) +import System.OsPath (osp) +import System.OsPath qualified as OsP +import Prelude (IO, Monad ((>>=)), mconcat, pure, show, ($), (++), (.), (<$>), (<>)) -- | Args used for building all packages. data RunnerEnv = MkRunnerEnv @@ -90,18 +95,22 @@ setup hLogger modifyPackages = do -- Set up build args for cabal, filling in missing defaults let buildArgs = - ["build"] - ++ cabalVerboseArg - ++ jobsArg - ++ keepGoingArg - - cabalVerboseArg = toArgs Builder.Env.cabalVerbosityToArg cliArgs.cabalVerbosity - jobsArg = toArgs Builder.Env.jobsToArg cliArgs.jobs + "build" + : keepGoingArg + ++ cliArgs.cabalOpts -- when packageFailFast is false, add keep-going so that we build as many -- packages in the group. keepGoingArg = ["--keep-going" | not cliArgs.packageFailFast] + let cabalPathRaw = fromMaybe [osp|cabal|] cliArgs.cabalPath + cabalPath <- + Dir.findExecutable cabalPathRaw >>= \case + -- TODO: It would be nice to avoid the decode here and keep everything + -- in OsPath, though that is blocked until process support OsPath. + Just p -> OsP.decodeUtf p + Nothing -> Ex.throwText "Cabal not found" + successesRef <- newIORef Set.empty failuresRef <- newIORef Set.empty @@ -158,6 +167,7 @@ setup hLogger modifyPackages = do MkBuildEnv { batch = cliArgs.batch, buildArgs, + cabalPath, colorLogs, groupFailFast = cliArgs.groupFailFast, hLogger, @@ -187,9 +197,6 @@ setup hLogger modifyPackages = do version = p.version } - toArgs :: (a -> b) -> Maybe a -> [b] - toArgs f = maybe [] (\x -> [f x]) - -- | Prints summary and writes results to disk. teardown :: RunnerEnv -> IO () teardown env = do diff --git a/src/utils/CLC/Stackage/Utils/Exception.hs b/src/utils/CLC/Stackage/Utils/Exception.hs index 2db6c4f..fb9c05e 100644 --- a/src/utils/CLC/Stackage/Utils/Exception.hs +++ b/src/utils/CLC/Stackage/Utils/Exception.hs @@ -1,9 +1,14 @@ -- | Provides utils for exceptions. module CLC.Stackage.Utils.Exception - ( try, + ( -- * Utilities + try, tryAny, throwLeft, mapThrowLeft, + + -- * Types + TextException (..), + throwText, ) where @@ -15,6 +20,8 @@ import Control.Exception ) import Control.Exception qualified as Ex import Data.Bifunctor (first) +import Data.Text (Text) +import Data.Text qualified as T mapThrowLeft :: (Exception e2) => (e1 -> e2) -> Either e1 a -> IO a mapThrowLeft f = throwLeft . first f @@ -42,3 +49,12 @@ isSyncException e = case fromException (toException e) of Just (SomeAsyncException _) -> False Nothing -> True + +newtype TextException = MkTextException Text + deriving stock (Eq, Show) + +instance Exception TextException where + displayException (MkTextException t) = T.unpack t + +throwText :: Text -> IO a +throwText = throwIO . MkTextException diff --git a/test/unit/Unit/Prelude.hs b/test/unit/Unit/Prelude.hs index 585d9b2..f168366 100644 --- a/test/unit/Unit/Prelude.hs +++ b/test/unit/Unit/Prelude.hs @@ -10,6 +10,7 @@ import CLC.Stackage.Builder.Env ( MkBuildEnv, batch, buildArgs, + cabalPath, colorLogs, groupFailFast, hLogger, @@ -65,6 +66,7 @@ mkBuildEnv = do MkBuildEnv { batch = Nothing, buildArgs = [], + cabalPath = "cabal", colorLogs = True, groupFailFast = False, hLogger = From 7779e9785a8712639ebba7629be3543a5e361c04 Mon Sep 17 00:00:00 2001 From: Tommy Bidne Date: Tue, 17 Jun 2025 15:50:10 +1200 Subject: [PATCH 2/6] Add stackage cabal.config fallback When the default stackage json endpoint fails, fallback to trying the /cabal.config endpoint. The latter seems more reliable. This also allows us to easily add a --snapshot-path CLI arg, giving users the ability to pass in a snapshot file manually. --- .github/workflows/ci.yaml | 5 + README.md | 37 +++++- app/Main.hs | 17 +-- clc-stackage.cabal | 14 +- dev.md | 6 +- example_output.png | Bin 161001 -> 0 bytes src/builder/CLC/Stackage/Builder/Env.hs | 2 - src/builder/CLC/Stackage/Builder/Process.hs | 5 +- src/parser/CLC/Stackage/Parser.hs | 39 ++++-- src/parser/CLC/Stackage/Parser/API.hs | 61 ++++++--- .../CLC/Stackage/Parser/API/CabalConfig.hs | 124 ++++++++++++++++++ .../Parser/{Query.hs => API/Common.hs} | 79 +++++------ src/parser/CLC/Stackage/Parser/API/JSON.hs | 105 +++++++++++++++ .../CLC/Stackage/Parser/Data/Response.hs | 40 ------ src/runner/CLC/Stackage/Runner/Args.hs | 66 ++++++++-- src/runner/CLC/Stackage/Runner/Env.hs | 43 +++--- src/runner/CLC/Stackage/Runner/Report.hs | 8 +- src/utils/CLC/Stackage/Utils/Logging.hs | 52 ++++++-- test/functional/Main.hs | 22 +++- .../testSmallSnapshotPath_posix.golden | 12 ++ .../testSmallSnapshotPath_windows.golden | 12 ++ test/functional/snapshot.txt | 14 ++ test/unit/Main.hs | 10 +- test/unit/Unit/CLC/Stackage/Parser/API.hs | 40 ++++++ test/unit/Unit/CLC/Stackage/Runner/Env.hs | 2 +- test/unit/Unit/CLC/Stackage/Runner/Report.hs | 2 +- test/unit/Unit/Prelude.hs | 5 +- 27 files changed, 627 insertions(+), 195 deletions(-) delete mode 100644 example_output.png create mode 100644 src/parser/CLC/Stackage/Parser/API/CabalConfig.hs rename src/parser/CLC/Stackage/Parser/{Query.hs => API/Common.hs} (61%) create mode 100644 src/parser/CLC/Stackage/Parser/API/JSON.hs delete mode 100644 src/parser/CLC/Stackage/Parser/Data/Response.hs create mode 100644 test/functional/goldens/testSmallSnapshotPath_posix.golden create mode 100644 test/functional/goldens/testSmallSnapshotPath_windows.golden create mode 100644 test/functional/snapshot.txt create mode 100644 test/unit/Unit/CLC/Stackage/Parser/API.hs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4bb0c3f..ff7718a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,6 +43,11 @@ jobs: - name: Functional Tests id: functional + # We want to run these tests even if the unit tests fail, because + # it is useful to know if e.g. the unit tests fail due to one + # stackage endpoint failing, but the functional tests pass due to + # a backup working. + if: always() shell: bash run: NO_CLEANUP=1 cabal test functional diff --git a/README.md b/README.md index 4ee0b2f..75a143e 100644 --- a/README.md +++ b/README.md @@ -61,15 +61,42 @@ The procedure is as follows: ### The clc-stackage exe -Previously, this project was just a single (massive) cabal file that had to be manually updated. Usage was fairly simple: `cabal build clc-stackage --keep-going` to build the project, `--keep-going` so that as many packages as possible are built. +`clc-stackage` is an executable that will: -This has been updated so that `clc-stackage` is now an executable that will automatically generate the desired cabal file based on the results of querying stackage directly. This streamlines updates, provides a more flexible build process, and potentially has prettier output (with `--batch` arg): +1. Download the stackage snapshot from the stackage server. +2. Divide the snapshot into groups (determined by `--batch` argument). +3. For each group, generate a cabal file and attempt to build it. -![demo](example_output.png) +#### Querying stackage -In particular, the `clc-stackage` exe allows for splitting the entire package set into subset groups of size `N` with the `--batch N` option. Each group is then built sequentially. Not only can this be useful for situations where building the entire package set in one go is infeasible, but it also provides a "cache" functionality, that allows us to interrupt the program at any point (e.g. `CTRL-C`), and pick up where we left off. For example: +By default, `clc-stackage` queries https://www.stackage.org/ for snapshot information. In situations where this is not desirable (e.g. the server is not working, or we want to test a custom snapshot), the snapshot can be overridden: +```sh +$ ./bin/clc-stackage --snapshot-path=path/to/snapshot ``` + +This snapshot should be formatted similar to the `cabal.config` endpoint on the stackage server (e.g. https://www.stackage.org/nightly/cabal.config). That is, package lines should be formatted ` ==`: + +``` +abstract-deque ==0.3 +abstract-deque-tests ==0.3 +abstract-par ==0.3.3 +AC-Angle ==1.0 +acc ==0.2.0.3 +... +``` + +The stackage config itself is valid, so trailing commas and other extraneous lines are allowed (and ignored). + +#### Investigating failures + +By default (`--write-logs save-failures`), the build logs are saved to the `./output/logs/` directory, with `./output/logs/current-build/` streaming the current build logs. + +#### Group batching + +The `clc-stackage` exe allows for splitting the entire package set into subset groups of size `N` with the `--batch N` option. Each group is then built sequentially. Not only can this be useful for situations where building the entire package set in one go is infeasible, but it also provides a "cache" functionality, that allows us to interrupt the program at any point (e.g. `CTRL-C`), and pick up where we left off. For example: + +```sh $ ./bin/clc-stackage --batch 100 ``` @@ -77,7 +104,7 @@ This will split the entire downloaded package set into groups of size 100. Each See `./bin/clc-stackage --help` for more info. -#### Optimal performance +##### Optimal performance On the one hand, splitting the entire package set into `--batch` groups makes the output easier to understand and offers a nice workflow for interrupting/restarting the build. On the other hand, there is a question of what the best value of `N` is for `--batch N`, with respect to performance. diff --git a/app/Main.hs b/app/Main.hs index d4f375b..4f00334 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -2,26 +2,15 @@ module Main (main) where import CLC.Stackage.Runner qualified as Runner import CLC.Stackage.Utils.Logging qualified as Logging -import Data.Text qualified as T -import Data.Time.LocalTime qualified as Local import System.Console.Terminal.Size qualified as TermSize -import System.IO (hPutStrLn, stderr) main :: IO () main = do mWidth <- (fmap . fmap) TermSize.width TermSize.size + let hLogger = Logging.mkDefaultLogger case mWidth of - Just w -> Runner.run $ mkLogger w + Just w -> Runner.run $ hLogger {Logging.terminalWidth = w} Nothing -> do - let hLogger = mkLogger 80 - Logging.putTimeInfoStr hLogger False "Failed detecting terminal width" + Logging.putTimeInfoStr hLogger "Failed detecting terminal width" Runner.run hLogger - where - mkLogger w = - Logging.MkHandle - { Logging.getLocalTime = Local.zonedTimeToLocalTime <$> Local.getZonedTime, - Logging.logStrErrLn = hPutStrLn stderr . T.unpack, - Logging.logStrLn = putStrLn . T.unpack, - Logging.terminalWidth = w - } diff --git a/clc-stackage.cabal b/clc-stackage.cabal index 2e83ba1..6130f2f 100644 --- a/clc-stackage.cabal +++ b/clc-stackage.cabal @@ -55,13 +55,15 @@ library parser exposed-modules: CLC.Stackage.Parser CLC.Stackage.Parser.API - CLC.Stackage.Parser.Data.Response - CLC.Stackage.Parser.Query + CLC.Stackage.Parser.API.CabalConfig + CLC.Stackage.Parser.API.Common + CLC.Stackage.Parser.API.JSON build-depends: , aeson , bytestring , containers >=0.6.3.1 && <0.9 + , deepseq >=1.4.6.0 && <1.6 , filepath , http-client >=0.5.9 && <0.8 , http-client-tls ^>=0.3 @@ -121,8 +123,6 @@ executable clc-stackage build-depends: , runner , terminal-size ^>=0.3.4 - , text - , time , utils hs-source-dirs: ./app @@ -143,6 +143,7 @@ test-suite unit type: exitcode-stdio-1.0 main-is: Main.hs other-modules: + Unit.CLC.Stackage.Parser.API Unit.CLC.Stackage.Runner.Env Unit.CLC.Stackage.Runner.Report Unit.Prelude @@ -151,11 +152,14 @@ test-suite unit , base , builder , containers + , deepseq , filepath + , http-client-tls + , parser , runner , tasty , tasty-golden - , tasty-hunit >=0.9 && <0.11 + , tasty-hunit >=0.9 && <0.11 , test-utils , time , utils diff --git a/dev.md b/dev.md index b718132..cd87162 100644 --- a/dev.md +++ b/dev.md @@ -96,9 +96,11 @@ The executable that actually runs. This is a very thin wrapper over `runner`, wh 1. `ghc-version` in [.github/workflows/ci.yaml](.github/workflows/ci.yaml). 2. [README.md](README.md). -5. Optional: Update `clc-stackage.cabal`'s dependencies (i.e. `cabal outdated`). +5. Update functional tests as needed i.e. exact package versions in `*golden` and `test/functional/snapshot.txt`. -6. Optional: Update nix inputs (`nix flake update`). +6. Optional: Update `clc-stackage.cabal`'s dependencies (i.e. `cabal outdated`). + +7. Optional: Update nix inputs (`nix flake update`). ## Testing diff --git a/example_output.png b/example_output.png deleted file mode 100644 index 9b6f8fed59f3a5cfdf38ecdce855d44159591f08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161001 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

3j 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

!Bg_A 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 diff --git a/src/builder/CLC/Stackage/Builder/Env.hs b/src/builder/CLC/Stackage/Builder/Env.hs index 200e8e7..48ba823 100644 --- a/src/builder/CLC/Stackage/Builder/Env.hs +++ b/src/builder/CLC/Stackage/Builder/Env.hs @@ -38,8 +38,6 @@ data BuildEnv = MkBuildEnv buildArgs :: [String], -- | Optional path to cabal executable. cabalPath :: FilePath, - -- | If true, colors logs. - colorLogs :: Bool, -- | If true, the first group that fails to completely build stops -- clc-stackage. Defaults to false. groupFailFast :: Bool, diff --git a/src/builder/CLC/Stackage/Builder/Process.hs b/src/builder/CLC/Stackage/Builder/Process.hs index 7791093..9eb7d1f 100644 --- a/src/builder/CLC/Stackage/Builder/Process.hs +++ b/src/builder/CLC/Stackage/Builder/Process.hs @@ -9,7 +9,6 @@ import CLC.Stackage.Builder.Batch (PackageGroup (unPackageGroup)) import CLC.Stackage.Builder.Env ( BuildEnv ( cabalPath, - colorLogs, groupFailFast, hLogger, progress, @@ -101,11 +100,11 @@ buildProject env idx pkgs = do ExitSuccess -> do -- save results modifyIORef' env.progress.successesRef addPackages - Logging.putTimeSuccessStr env.hLogger env.colorLogs msg + Logging.putTimeSuccessStr env.hLogger msg ExitFailure _ -> do -- save results modifyIORef' env.progress.failuresRef addPackages - Logging.putTimeErrStr env.hLogger env.colorLogs msg + Logging.putTimeErrStr env.hLogger msg -- throw error if fail fast when env.groupFailFast $ throwIO exitCode diff --git a/src/parser/CLC/Stackage/Parser.hs b/src/parser/CLC/Stackage/Parser.hs index 4c021c7..241b02a 100644 --- a/src/parser/CLC/Stackage/Parser.hs +++ b/src/parser/CLC/Stackage/Parser.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE CPP #-} {-# LANGUAGE QuasiQuotes #-} module CLC.Stackage.Parser @@ -11,15 +10,18 @@ module CLC.Stackage.Parser ) where -import CLC.Stackage.Parser.Data.Response +import CLC.Stackage.Parser.API ( PackageResponse (name, version), StackageResponse (packages), ) -import CLC.Stackage.Parser.Query qualified as Query +import CLC.Stackage.Parser.API qualified as API +import CLC.Stackage.Parser.API.CabalConfig qualified as CabalConfig import CLC.Stackage.Utils.IO qualified as IO import CLC.Stackage.Utils.JSON qualified as JSON +import CLC.Stackage.Utils.Logging qualified as Logging import CLC.Stackage.Utils.OS (Os (Linux, Osx, Windows)) import CLC.Stackage.Utils.OS qualified as OS +import Control.Monad (when) import Data.Aeson (FromJSON, ToJSON) import Data.Foldable (for_) import Data.Set (Set) @@ -27,12 +29,13 @@ import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic) -import System.OsPath (osp) +import System.OsPath (OsPath, osp) -- | Retrieves the list of packages, based on -- 'CLC.Stackage.Parser.API.stackageUrl'. -getPackageList :: IO [PackageResponse] -getPackageList = getPackageListByOs OS.currentOs +getPackageList :: Logging.Handle -> Maybe OsPath -> IO [PackageResponse] +getPackageList hLogger msnapshotPath = + getPackageListByOs hLogger msnapshotPath OS.currentOs -- | Prints the package list to a file. printPackageList :: Bool -> Maybe Os -> IO () @@ -52,7 +55,9 @@ printPackageList incVers mOs = do -- | Retrieves the package list formatted to text. getPackageListByOsFmt :: Bool -> Os -> IO [Text] -getPackageListByOsFmt incVers = (fmap . fmap) toText . getPackageListByOs +getPackageListByOsFmt incVers = + (fmap . fmap) toText + . getPackageListByOs Logging.mkDefaultLogger Nothing where toText r = if incVers @@ -60,12 +65,26 @@ getPackageListByOsFmt incVers = (fmap . fmap) toText . getPackageListByOs else r.name -- | Helper in case we want to see what the package set for a given OS is. -getPackageListByOs :: Os -> IO [PackageResponse] -getPackageListByOs os = do +getPackageListByOs :: Logging.Handle -> Maybe OsPath -> Os -> IO [PackageResponse] +getPackageListByOs hLogger msnapshotPath os = do excludedPkgs <- getExcludedPkgs os let filterExcluded = flip Set.notMember excludedPkgs . (.name) - response <- Query.getStackage + response <- case msnapshotPath of + Nothing -> API.getStackage hLogger + Just snapshotPath -> + CabalConfig.parseCabalConfig + <$> IO.readFileUtf8 snapshotPath + + let numPackages = length response.packages + when (numPackages < 2000) $ do + let msg = + mconcat + [ "Only found ", + T.pack $ show numPackages, + " packages. Is that right?" + ] + Logging.putTimeWarnStr hLogger msg let packages = filter filterExcluded response.packages diff --git a/src/parser/CLC/Stackage/Parser/API.hs b/src/parser/CLC/Stackage/Parser/API.hs index b8c0f24..cba4125 100644 --- a/src/parser/CLC/Stackage/Parser/API.hs +++ b/src/parser/CLC/Stackage/Parser/API.hs @@ -1,35 +1,54 @@ -- | REST API for stackage.org. module CLC.Stackage.Parser.API - ( withResponse, + ( -- * Querying stackage + StackageResponse (..), + PackageResponse (..), + getStackage, + + -- ** Exceptions + StackageException (..), + ExceptionReason (..), + + -- * Misc stackageSnapshot, ) where -import Network.HTTP.Client (BodyReader, Request, Response) -import Network.HTTP.Client qualified as HttpClient +import CLC.Stackage.Parser.API.CabalConfig qualified as CabalConfig +import CLC.Stackage.Parser.API.Common + ( ExceptionReason + ( ReasonDecodeJson, + ReasonDecodeUtf8, + ReasonReadBody, + ReasonStatus + ), + PackageResponse (name, version), + StackageException (MkStackageException), + StackageResponse (MkStackageResponse, packages), + ) +import CLC.Stackage.Parser.API.JSON qualified as JSON +import CLC.Stackage.Utils.Exception qualified as Ex +import CLC.Stackage.Utils.Logging qualified as Logging +import Control.Exception (Exception (displayException)) +import Data.Text qualified as T import Network.HTTP.Client.TLS qualified as TLS --- | Hits the stackage endpoint, invoking the callback on the result. -withResponse :: (Response BodyReader -> IO a) -> IO a -withResponse onResponse = do +-- | Returns the 'StackageResponse' corresponding to the given snapshot. +getStackage :: Logging.Handle -> IO StackageResponse +getStackage hLogger = do manager <- TLS.newTlsManager - req <- getRequest - HttpClient.withResponse req manager onResponse + Ex.tryAny (JSON.getStackage manager stackageSnapshot) >>= \case + Right r1 -> pure $ r1 + Left jsonEx -> do + let msg = + mconcat + [ "Json endpoint failed. Trying cabal config next: ", + T.pack $ displayException jsonEx + ] -getRequest :: IO Request -getRequest = updateReq <$> mkReq - where - mkReq = HttpClient.parseRequest stackageUrl - updateReq r = - r - { HttpClient.requestHeaders = - [ ("Accept", "application/json;charset=utf-8,application/json") - ] - } + Logging.putTimeWarnStr hLogger msg --- | Url for the stackage snapshot. -stackageUrl :: String -stackageUrl = "https://stackage.org/" <> stackageSnapshot + CabalConfig.getStackage manager stackageSnapshot -- | Stackage snapshot. Note that picking a "good" snapshot is something of -- an art i.e. not all valid snapshots return json output at the diff --git a/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs b/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs new file mode 100644 index 0000000..5d82b67 --- /dev/null +++ b/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs @@ -0,0 +1,124 @@ +module CLC.Stackage.Parser.API.CabalConfig + ( -- * Primary + getStackage, + + -- * Misc + parseCabalConfig, + ) +where + +import CLC.Stackage.Parser.API.Common + ( ExceptionReason + ( ReasonDecodeUtf8, + ReasonReadBody, + ReasonStatus + ), + PackageResponse + ( MkPackageResponse, + name, + version + ), + StackageException (MkStackageException), + StackageResponse (MkStackageResponse), + getStatusCode, + ) +import CLC.Stackage.Utils.Exception qualified as Ex +import Control.Exception (throwIO) +import Control.Monad (when) +import Data.List qualified as L +import Data.Maybe (catMaybes) +import Data.Text (Text) +import Data.Text qualified as T +import Data.Text.Encoding qualified as TEnc +import Network.HTTP.Client (BodyReader, Manager, Request, Response) +import Network.HTTP.Client qualified as HttpClient + +-- | Given http manager and snapshot string, queries the cabal config +-- endpoint. This is intended as a backup, for when the primary endpoint fails. +getStackage :: Manager -> String -> IO StackageResponse +getStackage manager stackageSnapshot = do + req <- getRequest + HttpClient.withResponse req manager readStackageResponse + where + readStackageResponse :: Response BodyReader -> IO StackageResponse + readStackageResponse res = do + let bodyReader = HttpClient.responseBody res + status = HttpClient.responseStatus res + statusCode = getStatusCode res + mkEx = MkStackageException stackageSnapshot + + when (statusCode /= 200) $ + throwIO $ + mkEx (ReasonStatus status) + + bodyBs <- + Ex.mapThrowLeft + (mkEx . ReasonReadBody) + =<< Ex.tryAny (mconcat <$> HttpClient.brConsume bodyReader) + + bodyTxt <- + Ex.mapThrowLeft + (mkEx . ReasonDecodeUtf8 bodyBs) + $ TEnc.decodeUtf8' bodyBs + + pure $ parseCabalConfig bodyTxt + + getRequest :: IO Request + getRequest = HttpClient.parseRequest stackageUrl + + -- Url for the stackage snapshot. + stackageUrl :: String + stackageUrl = + "https://stackage.org/" + <> stackageSnapshot + <> "/cabal.config" + +parseCabalConfig :: Text -> StackageResponse +parseCabalConfig = + MkStackageResponse + . catMaybes + . fmap parseCabalConfigLine + . T.lines + +-- | Parses a line like ' =='. This does not currently handle +-- "installed" packages e.g. 'mtl installed'. This probably isn't a big deal, +-- since all such libs will be built transitively anyway. That said, if +-- we wanted to fix it, we would probably want to change PackageResponse's +-- +-- version :: Text +-- +-- field to +-- +-- version :: Maybe Text +-- +-- and parse "installed" to Nothing. Then, when we go to write the generated +-- cabal file, Nothing will correspond to writing no version number. +-- (CLC.Stackage.Builder.Package.toText). +parseCabalConfigLine :: Text -> Maybe PackageResponse +-- splitOn rather than breakOn since the former drops the delim, which is +-- convenient. +parseCabalConfigLine txt = case T.splitOn delim txt of + [nameRaw, versRaw] -> do + (v, c) <- T.unsnoc versRaw + -- Strip trailing comma if it exists. Otherwise take everything. + let version = if c == ',' then v else T.snoc v c + + -- This line handles prefixes e.g. whitespace or a stanza e.g. + -- + -- constraints: abstract-deque ==0.3, + -- abstract-deque-tests ==0.3, + -- ... + -- + -- We split pre-delim on whitespace, and take the last word. + (_, name) <- L.unsnoc $ T.words nameRaw + + -- T.strip as trailing characters can cause problems e.g. windows can + -- pick up \r. + Just $ + MkPackageResponse + { name = T.strip name, + version = T.strip version + } + _ -> Nothing + where + delim = " ==" diff --git a/src/parser/CLC/Stackage/Parser/Query.hs b/src/parser/CLC/Stackage/Parser/API/Common.hs similarity index 61% rename from src/parser/CLC/Stackage/Parser/Query.hs rename to src/parser/CLC/Stackage/Parser/API/Common.hs index 7343caa..d6ef42e 100644 --- a/src/parser/CLC/Stackage/Parser/Query.hs +++ b/src/parser/CLC/Stackage/Parser/API/Common.hs @@ -1,55 +1,46 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE QuasiQuotes #-} +-- | Types and functions common to stackage JSON and CabalConfig APIs. +module CLC.Stackage.Parser.API.Common + ( -- * Types + StackageResponse (..), + PackageResponse (..), -module CLC.Stackage.Parser.Query - ( -- * Querying stackage - getStackage, - - -- ** Exceptions + -- * Exception StackageException (..), ExceptionReason (..), + + -- * Misc + getStatusCode, ) where -import CLC.Stackage.Parser.API - ( stackageSnapshot, - withResponse, - ) -import CLC.Stackage.Parser.Data.Response (StackageResponse) -import CLC.Stackage.Utils.Exception qualified as Ex -import CLC.Stackage.Utils.JSON qualified as JSON +import Control.DeepSeq (NFData) import Control.Exception ( Exception (displayException), SomeException, - throwIO, ) -import Control.Monad (when) import Data.ByteString (ByteString) +import Data.Text (Text) +import Data.Text.Encoding.Error (UnicodeException) +import GHC.Generics (Generic) import Network.HTTP.Client (Response) import Network.HTTP.Client qualified as HttpClient import Network.HTTP.Types.Status (Status) import Network.HTTP.Types.Status qualified as Status --- | Returns the 'StackageResponse' corresponding to the given snapshot. -getStackage :: IO StackageResponse -getStackage = withResponse $ \res -> do - let bodyReader = HttpClient.responseBody res - status = HttpClient.responseStatus res - statusCode = getStatusCode res - mkEx = MkStackageException stackageSnapshot - - when (statusCode /= 200) $ - throwIO $ - mkEx (ReasonStatus status) - - bodyBs <- - Ex.mapThrowLeft - (mkEx . ReasonReadBody) - =<< Ex.tryAny (mconcat <$> HttpClient.brConsume bodyReader) +-- | Stackage response. This type unifies different stackage responses. +newtype StackageResponse = MkStackageResponse + { packages :: [PackageResponse] + } + deriving stock (Eq, Generic, Show) + deriving anyclass (NFData) - Ex.mapThrowLeft - (mkEx . ReasonDecodeJson bodyBs) - (JSON.decode bodyBs) +-- | Package in a stackage snapshot. +data PackageResponse = MkPackageResponse + { name :: Text, + version :: Text + } + deriving stock (Eq, Generic, Show) + deriving anyclass (NFData) -- | Exception reason. data ExceptionReason @@ -60,6 +51,9 @@ data ExceptionReason | -- | Exception decoding JSON. The first string is the json we attempted -- to decode. The second is the error message. ReasonDecodeJson ByteString String + | -- | Exception decoding JSON. The first string is the bytestring we + -- attempted to decode. The second is the error message. + ReasonDecodeUtf8 ByteString UnicodeException deriving stock (Show) -- | General network exception. @@ -99,10 +93,17 @@ instance Exception StackageException where ] ReasonDecodeJson jsonBs err -> mconcat - [ "Could not decode JSON:\n\n", - show jsonBs, - "\n\nError: ", - err + [ "Could not decode JSON: ", + err, + "This is likely due to the endpoint returning HTML, not JSON. Bytes: ", + show jsonBs + ] + ReasonDecodeUtf8 bs err -> + mconcat + [ "Could not decode UTF-8: ", + displayException err, + ". Bytes: ", + show bs ] where snapshot = ex.snapshot diff --git a/src/parser/CLC/Stackage/Parser/API/JSON.hs b/src/parser/CLC/Stackage/Parser/API/JSON.hs new file mode 100644 index 0000000..dd03658 --- /dev/null +++ b/src/parser/CLC/Stackage/Parser/API/JSON.hs @@ -0,0 +1,105 @@ +module CLC.Stackage.Parser.API.JSON + ( -- * Query json endpoint + getStackage, + ) +where + +import CLC.Stackage.Parser.API.Common + ( ExceptionReason (ReasonDecodeJson, ReasonReadBody, ReasonStatus), + StackageException (MkStackageException), + getStatusCode, + ) +import CLC.Stackage.Parser.API.Common qualified as Common +import CLC.Stackage.Utils.Exception qualified as Ex +import CLC.Stackage.Utils.JSON qualified as JSON +import Control.Exception (throwIO) +import Control.Monad (when) +import Data.Aeson (FromJSON, ToJSON) +import Data.Text (Text) +import GHC.Generics (Generic) +import Network.HTTP.Client (BodyReader, Manager, Request, Response) +import Network.HTTP.Client qualified as HttpClient + +-- | Given http manager and snapshot string, queries the primary json +-- endpoint. +getStackage :: Manager -> String -> IO Common.StackageResponse +getStackage manager stackageSnapshot = do + req <- getRequest + HttpClient.withResponse req manager readStackageResponse + where + readStackageResponse :: Response BodyReader -> IO Common.StackageResponse + readStackageResponse res = do + let bodyReader = HttpClient.responseBody res + status = HttpClient.responseStatus res + statusCode = getStatusCode res + mkEx = MkStackageException stackageSnapshot + + when (statusCode /= 200) $ + throwIO $ + mkEx (ReasonStatus status) + + bodyBs <- + Ex.mapThrowLeft + (mkEx . ReasonReadBody) + =<< Ex.tryAny (mconcat <$> HttpClient.brConsume bodyReader) + + Ex.mapThrowLeft + (mkEx . ReasonDecodeJson bodyBs) + (toSnapshotCommon <$> JSON.decode bodyBs) + + getRequest :: IO Request + getRequest = updateReq <$> mkReq + where + mkReq = HttpClient.parseRequest stackageUrl + updateReq r = + r + { HttpClient.requestHeaders = + [ ("Accept", "application/json;charset=utf-8,application/json") + ] + } + + -- Url for the stackage snapshot. + stackageUrl :: String + stackageUrl = "https://stackage.org/" <> stackageSnapshot + +toSnapshotCommon :: StackageResponse -> Common.StackageResponse +toSnapshotCommon (MkStackageResponse _ pkgs) = + Common.MkStackageResponse + { packages = toPackageCommon <$> pkgs + } + +toPackageCommon :: PackageResponse -> Common.PackageResponse +toPackageCommon pr = + Common.MkPackageResponse + { name = pr.name, + version = pr.version + } + +-- | Response returned by primary stackage endpoint e.g. +-- @stackage.org\/lts-20.14@. +data StackageResponse = MkStackageResponse + { snapshot :: SnapshotResponse, + packages :: [PackageResponse] + } + deriving stock (Eq, Generic, Show) + deriving anyclass (FromJSON, ToJSON) + +-- | Stackage snapshot data. +data SnapshotResponse = MkSnapshotResponse + { ghc :: Text, + created :: Text, + name :: Text, + compiler :: Text + } + deriving stock (Eq, Generic, Show) + deriving anyclass (FromJSON, ToJSON) + +-- | Package in a stackage snapshot. +data PackageResponse = MkPackageResponse + { origin :: Text, + name :: Text, + version :: Text, + synopsis :: Text + } + deriving stock (Eq, Generic, Show) + deriving anyclass (FromJSON, ToJSON) diff --git a/src/parser/CLC/Stackage/Parser/Data/Response.hs b/src/parser/CLC/Stackage/Parser/Data/Response.hs deleted file mode 100644 index f270e44..0000000 --- a/src/parser/CLC/Stackage/Parser/Data/Response.hs +++ /dev/null @@ -1,40 +0,0 @@ --- | Types returned by stackage API. -module CLC.Stackage.Parser.Data.Response - ( StackageResponse (..), - SnapshotResponse (..), - PackageResponse (..), - ) -where - -import Data.Aeson (FromJSON, ToJSON) -import Data.Text (Text) -import GHC.Generics (Generic) - --- | Response returned by primary stackage endpoint e.g. --- @stackage.org\/lts-20.14@. -data StackageResponse = MkStackageResponse - { snapshot :: SnapshotResponse, - packages :: [PackageResponse] - } - deriving stock (Eq, Generic, Show) - deriving anyclass (FromJSON, ToJSON) - --- | Stackage snapshot data. -data SnapshotResponse = MkSnapshotResponse - { ghc :: Text, - created :: Text, - name :: Text, - compiler :: Text - } - deriving stock (Eq, Generic, Show) - deriving anyclass (FromJSON, ToJSON) - --- | Package in a stackage snapshot. -data PackageResponse = MkPackageResponse - { origin :: Text, - name :: Text, - version :: Text, - synopsis :: Text - } - deriving stock (Eq, Generic, Show) - deriving anyclass (FromJSON, ToJSON) diff --git a/src/runner/CLC/Stackage/Runner/Args.hs b/src/runner/CLC/Stackage/Runner/Args.hs index d99fcf1..b8449ce 100644 --- a/src/runner/CLC/Stackage/Runner/Args.hs +++ b/src/runner/CLC/Stackage/Runner/Args.hs @@ -25,10 +25,11 @@ import Options.Applicative (<**>), ) import Options.Applicative qualified as OA +import Options.Applicative.Help (Doc) import Options.Applicative.Help.Chunk (Chunk (Chunk)) import Options.Applicative.Help.Chunk qualified as Chunk import Options.Applicative.Help.Pretty qualified as Pretty -import Options.Applicative.Types (ArgPolicy (Intersperse)) +import Options.Applicative.Types (ArgPolicy (Intersperse), ReadM) import System.OsPath (OsPath) import System.OsPath qualified as OsP @@ -64,6 +65,9 @@ data Args = MkArgs packageFailFast :: Bool, -- | Whether to retry packages that failed. retryFailures :: Bool, + -- | Optional path to snapshot file. If given, we use the file's contents + -- as the package set, rather than the stackage server. + snapshotPath :: Maybe OsPath, -- | Determines what logs to write. writeLogs :: Maybe WriteLogs } @@ -107,8 +111,29 @@ getArgs = OA.execParser parserInfoArgs mconcat [ "This will build everything in one package group, and pass ", "--keep-going to cabal." - ] + ], + Chunk.paragraph "Examples:", + mkExample + [ "# Basic example", + "$ clc-stackage" + ], + mkExample + [ "# Batch with groups of 100 and some cabal options", + "$ clc-stackage --batch 100 --cabal-options='--semaphore --verbose=1'" + ], + mkExample + [ "# Run with custom cabal", + "$ clc-stackage --cabal-path=path/to/cabal --cabal-options='--store-dir=path/to/store'" + ], + mkExample + [ "# Run with custom snapshot", + "$ clc-stackage --snapshot-path=path/to/snapshot-file" + ] ] + mkExample :: [String] -> Chunk Doc + mkExample = + Chunk.vcatChunks + . fmap (fmap (Pretty.indent 2) . Chunk.stringChunk) parseCliArgs :: Parser Args parseCliArgs = @@ -122,6 +147,7 @@ parseCliArgs = noCleanup <- parseNoCleanup packageFailFast <- parsePackageFailFast retryFailures <- parseRetryFailures + snapshotPath <- parseSnapshotPath writeLogs <- parseWriteLogs pure $ @@ -135,6 +161,7 @@ parseCliArgs = noCleanup, packageFailFast, retryFailures, + snapshotPath, writeLogs } ) @@ -184,12 +211,6 @@ parseCabalPath = mkHelp "Optional path to cabal executable." ] ) - where - readOsPath = do - fp <- OA.str - case OsP.encodeUtf fp of - Just osp -> pure osp - Nothing -> fail $ "Failed encoding to ospath: " ++ fp parseColorLogs :: Parser ColorLogs parseColorLogs = @@ -275,6 +296,28 @@ parseRetryFailures = ] ) +parseSnapshotPath :: Parser (Maybe OsPath) +parseSnapshotPath = + OA.optional $ + OA.option + readOsPath + ( mconcat + [ OA.long "snapshot-path", + OA.metavar "PATH", + mkHelp $ + mconcat + [ "Optional path to snapshot file. If given, this overrides ", + "the stackage snapshot; that is, we use the file's contents, ", + "rather than the stackage server. The file should be ", + "formatted similar to ", + "https://www.stackage.org//cabal.config i.e. each ", + "line should be ' ==' e.g. 'lens ==5.3.4'. Note ", + "that the snapshot is still filtered according to ", + "excluded_pkgs.json." + ] + ] + ) + parseWriteLogs :: Parser (Maybe WriteLogs) parseWriteLogs = OA.optional $ @@ -307,6 +350,13 @@ parseWriteLogs = other ] +readOsPath :: ReadM OsPath +readOsPath = do + fp <- OA.str + case OsP.encodeUtf fp of + Just osp -> pure osp + Nothing -> fail $ "Failed encoding to ospath: " ++ fp + mkHelp :: String -> Mod f a mkHelp = OA.helpDoc diff --git a/src/runner/CLC/Stackage/Runner/Env.hs b/src/runner/CLC/Stackage/Runner/Env.hs index b4d27e3..d4055f4 100644 --- a/src/runner/CLC/Stackage/Runner/Env.hs +++ b/src/runner/CLC/Stackage/Runner/Env.hs @@ -15,7 +15,6 @@ import CLC.Stackage.Builder.Env ( BuildEnv ( MkBuildEnv, batch, - colorLogs, groupFailFast, hLogger, packagesToBuild, @@ -31,9 +30,10 @@ import CLC.Stackage.Builder.Env import CLC.Stackage.Builder.Env qualified as Builder.Env import CLC.Stackage.Builder.Package (Package (MkPackage, name, version)) import CLC.Stackage.Parser qualified as Parser -import CLC.Stackage.Parser.Data.Response (PackageResponse (name, version)) +import CLC.Stackage.Parser.API (PackageResponse (name, version)) import CLC.Stackage.Runner.Args - ( ColorLogs + ( Args (snapshotPath), + ColorLogs ( ColorLogsDetect, ColorLogsOff, ColorLogsOn @@ -89,10 +89,19 @@ data RunnerEnv = MkRunnerEnv -- | Creates an environment based on cli args and cache data. The parameter -- modifies the package set returned by stackage. setup :: Logging.Handle -> ([Package] -> [Package]) -> IO RunnerEnv -setup hLogger modifyPackages = do - startTime <- hLogger.getLocalTime +setup hLoggerRaw modifyPackages = do + startTime <- hLoggerRaw.getLocalTime cliArgs <- Args.getArgs + colorLogs <- + case cliArgs.colorLogs of + ColorLogsOff -> pure False + ColorLogsOn -> pure True + ColorLogsDetect -> supportsPretty + + -- Update logger with CLI color param. + let hLogger = hLoggerRaw {Logging.color = colorLogs} + -- Set up build args for cabal, filling in missing defaults let buildArgs = "build" @@ -114,22 +123,16 @@ setup hLogger modifyPackages = do successesRef <- newIORef Set.empty failuresRef <- newIORef Set.empty - colorLogs <- - case cliArgs.colorLogs of - ColorLogsOff -> pure False - ColorLogsOn -> pure True - ColorLogsDetect -> supportsPretty - cache <- if cliArgs.noCache then pure Nothing - else Report.readCache hLogger colorLogs + else Report.readCache hLogger -- (entire set, packages to build) (completePackageSet, pkgsList) <- case cache of Nothing -> do -- if no cache exists, query stackage - pkgsResponses <- Parser.getPackageList + pkgsResponses <- Parser.getPackageList hLogger cliArgs.snapshotPath let completePackageSet = responseToPkgs <$> pkgsResponses pkgs = modifyPackages completePackageSet pure (completePackageSet, pkgs) @@ -154,7 +157,7 @@ setup hLogger modifyPackages = do packagesToBuild <- case pkgsList of (p : ps) -> pure (p :| ps) [] -> do - Logging.putTimeInfoStr hLogger colorLogs "Cache exists but has no packages to test." + Logging.putTimeInfoStr hLogger "Cache exists but has no packages to test." throwIO ExitSuccess let progress = @@ -168,7 +171,6 @@ setup hLogger modifyPackages = do { batch = cliArgs.batch, buildArgs, cabalPath, - colorLogs, groupFailFast = cliArgs.groupFailFast, hLogger, packagesToBuild, @@ -216,16 +218,17 @@ teardown env = do Report.saveReport report + let colorLogs = env.buildEnv.hLogger.color env.buildEnv.hLogger.logStrLn $ T.unlines [ "", "", - Logging.colorGreen env.buildEnv.colorLogs $ "- Successes: " <> successStr report, - Logging.colorRed env.buildEnv.colorLogs $ "- Failures: " <> failureStr report, - Logging.colorMagenta env.buildEnv.colorLogs $ "- Untested: " <> untestedStr report, + Logging.colorGreen colorLogs $ "- Successes: " <> successStr report, + Logging.colorRed colorLogs $ "- Failures: " <> failureStr report, + Logging.colorMagenta colorLogs $ "- Untested: " <> untestedStr report, "", - Logging.colorBlue env.buildEnv.colorLogs $ "- Start: " <> report.startTime, - Logging.colorBlue env.buildEnv.colorLogs $ "- End: " <> report.endTime + Logging.colorBlue colorLogs $ "- Start: " <> report.startTime, + Logging.colorBlue colorLogs $ "- End: " <> report.endTime ] where successStr r = fmtPercent r.stats.numSuccesses r.stats.successRate diff --git a/src/runner/CLC/Stackage/Runner/Report.hs b/src/runner/CLC/Stackage/Runner/Report.hs index 9d7aaed..d1b46d5 100644 --- a/src/runner/CLC/Stackage/Runner/Report.hs +++ b/src/runner/CLC/Stackage/Runner/Report.hs @@ -104,19 +104,19 @@ mkReport results startTime endTime = dv n = floor $ 100 * (fromIntegral n / numAllTested) -- | Reads results data, if the cache exists. -readCache :: Logging.Handle -> Bool -> IO (Maybe Results) -readCache handle colorLogs = do +readCache :: Logging.Handle -> IO (Maybe Results) +readCache handle = do catchPathStr <- T.pack <$> OsPath.decodeUtf Paths.cachePath Dir.doesFileExist Paths.cachePath >>= \case False -> do - Logging.putTimeInfoStr handle colorLogs $ "Cached results do not exist: " <> catchPathStr + Logging.putTimeInfoStr handle $ "Cached results do not exist: " <> catchPathStr pure Nothing True -> do contents <- IO.readBinaryFile Paths.cachePath case JSON.decode contents of Left err -> throwIO $ AesonException err Right r -> do - Logging.putTimeInfoStr handle colorLogs $ "Using cached results: " <> catchPathStr + Logging.putTimeInfoStr handle $ "Using cached results: " <> catchPathStr pure $ Just r -- | Saves the current progress data as the next prior run. diff --git a/src/utils/CLC/Stackage/Utils/Logging.hs b/src/utils/CLC/Stackage/Utils/Logging.hs index 1b8a42f..217d93a 100644 --- a/src/utils/CLC/Stackage/Utils/Logging.hs +++ b/src/utils/CLC/Stackage/Utils/Logging.hs @@ -1,10 +1,12 @@ module CLC.Stackage.Utils.Logging ( -- * Logging Handler Handle (..), + mkDefaultLogger, -- * Printing with timestamps putTimeInfoStr, putTimeSuccessStr, + putTimeWarnStr, putTimeErrStr, -- ** ANSI Colors @@ -22,13 +24,17 @@ import Data.Text (Text) import Data.Text qualified as T import Data.Time.Format qualified as Format import Data.Time.LocalTime (LocalTime) +import Data.Time.LocalTime qualified as Local import Data.Word (Word16) -import System.Console.Pretty (Color (Blue, Green, Magenta, Red)) +import System.Console.Pretty (Color (Blue, Green, Magenta, Red, Yellow)) import System.Console.Pretty qualified as Pretty +import System.IO (hPutStrLn, stderr) -- | Simple handle for logging, for testing output. data Handle = MkHandle - { -- | Retrieve local time. + { -- | If true, colors the logs. + color :: Bool, + -- | Retrieve local time. getLocalTime :: IO LocalTime, -- | Log stderr. logStrErrLn :: Text -> IO (), @@ -39,26 +45,37 @@ data Handle = MkHandle } -- | 'putStrLn' with a timestamp and info prefix. -putTimeInfoStr :: Handle -> Bool -> Text -> IO () -putTimeInfoStr hLogger b s = do +putTimeInfoStr :: Handle -> Text -> IO () +putTimeInfoStr hLogger s = do timeStr <- getLocalTimeString hLogger - hLogger.logStrLn $ colorBlue b $ "[" <> timeStr <> "][Info] " <> s' + hLogger.logStrLn $ colorBlue hLogger.color $ "[" <> timeStr <> "][Info] " <> s' where s' = truncateIfNeeded hLogger.terminalWidth s -- | 'putStrLn' with a timestamp and info prefix. -putTimeSuccessStr :: Handle -> Bool -> Text -> IO () -putTimeSuccessStr hLogger b s = do +putTimeSuccessStr :: Handle -> Text -> IO () +putTimeSuccessStr hLogger s = do timeStr <- getLocalTimeString hLogger - hLogger.logStrLn $ colorGreen b $ "[" <> timeStr <> "][Success] " <> s' + hLogger.logStrLn $ colorGreen hLogger.color $ "[" <> timeStr <> "][Success] " <> s' where s' = truncateIfNeeded hLogger.terminalWidth s +-- | 'putStrLn' with a timestamp and warn prefix. +putTimeWarnStr :: Handle -> Text -> IO () +putTimeWarnStr hLogger s = do + timeStr <- getLocalTimeString hLogger + hLogger.logStrErrLn $ colorYellow hLogger.color $ "[" <> timeStr <> "][Warn] " <> s' + where + -- Allow this to be longer than the terminal width, since this is + -- generally used as a one-off message, and the warning may include useful + -- error info. + s' = truncateIfNeeded 200 s + -- | 'putStrErrLn' with a timestamp and error prefix. -putTimeErrStr :: Handle -> Bool -> Text -> IO () -putTimeErrStr hLogger b s = do +putTimeErrStr :: Handle -> Text -> IO () +putTimeErrStr hLogger s = do timeStr <- getLocalTimeString hLogger - hLogger.logStrErrLn $ colorRed b $ "[" <> timeStr <> "][Error] " <> s' + hLogger.logStrErrLn $ colorRed hLogger.color $ "[" <> timeStr <> "][Error] " <> s' where s' = truncateIfNeeded hLogger.terminalWidth s @@ -82,6 +99,9 @@ colorGreen b = colorIf b Green colorRed :: Bool -> Text -> Text colorRed b = colorIf b Red +colorYellow :: Bool -> Text -> Text +colorYellow b = colorIf b Yellow + colorIf :: Bool -> Color -> Text -> Text colorIf True = Pretty.color colorIf False = const id @@ -119,3 +139,13 @@ constLen = 11 -- e.g. [2024-10-14 15:14:00] timeStrLen :: Word16 timeStrLen = 21 + +mkDefaultLogger :: Handle +mkDefaultLogger = + MkHandle + { color = False, + getLocalTime = Local.zonedTimeToLocalTime <$> Local.getZonedTime, + logStrErrLn = hPutStrLn stderr . T.unpack, + logStrLn = putStrLn . T.unpack, + terminalWidth = 80 + } diff --git a/test/functional/Main.hs b/test/functional/Main.hs index b530e3d..14ed1e0 100644 --- a/test/functional/Main.hs +++ b/test/functional/Main.hs @@ -36,7 +36,8 @@ main = testGroup "Functional" [ testSmall getNoCleanup, - testSmallBatch getNoCleanup + testSmallBatch getNoCleanup, + testSmallSnapshotPath getNoCleanup ] testSmall :: IO Bool -> TestTree @@ -61,6 +62,22 @@ testSmallBatch getNoCleanup = runGolden getNoCleanup params testName = [osp|testSmallBatch|] } +testSmallSnapshotPath :: IO Bool -> TestTree +testSmallSnapshotPath getNoCleanup = runGolden getNoCleanup params + where + params = + MkGoldenParams + { args = ["--snapshot-path", snapshotPath], + runner = runSmall, + testDesc, + testName = [osp|testSmallSnapshotPath|] + } + testDesc = "Finishes clc-stackage with small package list and --snapshot-path" + + snapshotPath = + Paths.unsafeDecodeUtf $ + [osp|test|] [osp|functional|] [osp|snapshot.txt|] + -- | Tests building only a few packages runSmall :: IO [ByteString] runSmall = do @@ -113,7 +130,8 @@ mkHLogger = do let hLogger = Logging.MkHandle - { Logging.getLocalTime = pure mkLocalTime, + { Logging.color = False, + Logging.getLocalTime = pure mkLocalTime, Logging.logStrLn = \s -> modifyIORef' logsRef (s :), Logging.logStrErrLn = \s -> modifyIORef' logsRef (s :), Logging.terminalWidth = 80 diff --git a/test/functional/goldens/testSmallSnapshotPath_posix.golden b/test/functional/goldens/testSmallSnapshotPath_posix.golden new file mode 100644 index 0000000..c73b3e4 --- /dev/null +++ b/test/functional/goldens/testSmallSnapshotPath_posix.golden @@ -0,0 +1,12 @@ +[2020-05-31 12:00:00][Info] Cached results do not exist: output/cache.json +[2020-05-31 12:00:00][Warn] Only found 7 packages. Is that right? +[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, optics-co... + + +- Successes: 4 (100%) +- Failures: 0 (0%) +- Untested: 0 (0%) + +- Start: 2020-05-31 12:00:00 +- End: 2020-05-31 12:00:00 + diff --git a/test/functional/goldens/testSmallSnapshotPath_windows.golden b/test/functional/goldens/testSmallSnapshotPath_windows.golden new file mode 100644 index 0000000..3da6d58 --- /dev/null +++ b/test/functional/goldens/testSmallSnapshotPath_windows.golden @@ -0,0 +1,12 @@ +[2020-05-31 12:00:00][Info] Cached results do not exist: output\cache.json +[2020-05-31 12:00:00][Warn] Only found 7 packages. Is that right? +[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, optics-co... + + +- Successes: 4 (100%) +- Failures: 0 (0%) +- Untested: 0 (0%) + +- Start: 2020-05-31 12:00:00 +- End: 2020-05-31 12:00:00 + diff --git a/test/functional/snapshot.txt b/test/functional/snapshot.txt new file mode 100644 index 0000000..325a117 --- /dev/null +++ b/test/functional/snapshot.txt @@ -0,0 +1,14 @@ +# This is the list of packages from the functional tests' modifyPackages +# (chosen for consistency) with a few others thrown in, to test that filtering +# works the same for both querying stackage and reading this file. This list +# is intentionally kept small to make maintenance easier. +aeson ==2.2.3.0 +cborg ==0.2.10.0 +clock ==0.8.4 +kan-extensions ==5.2.6 +# Note that mtl doesn't actually get parsed, due to the different format. +# This line merely tests that such lines do not cause an error. +mtl installed +optics-core ==0.4.1.1 +profunctors ==5.6.2 +servant ==0.20.2 diff --git a/test/unit/Main.hs b/test/unit/Main.hs index 5851630..5ff9292 100644 --- a/test/unit/Main.hs +++ b/test/unit/Main.hs @@ -2,8 +2,9 @@ module Main (main) where import Test.Tasty (defaultMain, localOption, testGroup) import Test.Tasty.Golden (DeleteOutputFile (OnPass)) -import Unit.CLC.Stackage.Runner.Env qualified as Env -import Unit.CLC.Stackage.Runner.Report qualified as Report +import Unit.CLC.Stackage.Parser.API qualified as Parser.API +import Unit.CLC.Stackage.Runner.Env qualified as Runner.Env +import Unit.CLC.Stackage.Runner.Report qualified as Runner.Report main :: IO () main = @@ -11,6 +12,7 @@ main = localOption OnPass $ testGroup "Unit" - [ Env.tests, - Report.tests + [ Parser.API.tests, + Runner.Env.tests, + Runner.Report.tests ] diff --git a/test/unit/Unit/CLC/Stackage/Parser/API.hs b/test/unit/Unit/CLC/Stackage/Parser/API.hs new file mode 100644 index 0000000..2f84921 --- /dev/null +++ b/test/unit/Unit/CLC/Stackage/Parser/API.hs @@ -0,0 +1,40 @@ +{-# OPTIONS_GHC -Wno-missing-import-lists #-} + +module Unit.CLC.Stackage.Parser.API (tests) where + +import CLC.Stackage.Parser.API qualified as API +import CLC.Stackage.Parser.API.CabalConfig qualified as CabalConfig +import CLC.Stackage.Parser.API.JSON qualified as JSON +import CLC.Stackage.Utils.Exception qualified as Ex +import Control.DeepSeq (NFData, rnf) +import Control.Exception (displayException, evaluate) +import Network.HTTP.Client.TLS qualified as TLS +import Unit.Prelude + +tests :: TestTree +tests = + testGroup + "CLC.Stackage.Parser.API" + [ testCabalConfigEndpoint, + testJsonEndpoint + ] + +testCabalConfigEndpoint :: TestTree +testCabalConfigEndpoint = testCase desc $ do + manager <- TLS.newTlsManager + eval =<< CabalConfig.getStackage manager API.stackageSnapshot + where + desc = "Queries stackage cabal.config endpoint" + +testJsonEndpoint :: TestTree +testJsonEndpoint = testCase desc $ do + manager <- TLS.newTlsManager + eval =<< JSON.getStackage manager API.stackageSnapshot + where + desc = "Queries stackage json endpoint" + +eval :: (NFData a) => a -> IO () +eval x = + Ex.tryAny (evaluate $ rnf x) >>= \case + Right _ -> pure () + Left ex -> assertFailure $ "Exception during eval: " ++ displayException ex diff --git a/test/unit/Unit/CLC/Stackage/Runner/Env.hs b/test/unit/Unit/CLC/Stackage/Runner/Env.hs index 9667e68..22deef9 100644 --- a/test/unit/Unit/CLC/Stackage/Runner/Env.hs +++ b/test/unit/Unit/CLC/Stackage/Runner/Env.hs @@ -13,7 +13,7 @@ import Unit.Prelude tests :: TestTree tests = testGroup - "Sequential.Env" + "CLC.Stackage.Runner.Env" [ testResults, newCacheTests ] diff --git a/test/unit/Unit/CLC/Stackage/Runner/Report.hs b/test/unit/Unit/CLC/Stackage/Runner/Report.hs index ed9a128..f4b1d35 100644 --- a/test/unit/Unit/CLC/Stackage/Runner/Report.hs +++ b/test/unit/Unit/CLC/Stackage/Runner/Report.hs @@ -27,7 +27,7 @@ import Unit.Prelude tests :: TestTree tests = testGroup - "Sequential.Report" + "CLC.Stackage.Runner.Report" [ testMkReport, testResultJsonEncode, testReportJsonEncode diff --git a/test/unit/Unit/Prelude.hs b/test/unit/Unit/Prelude.hs index f168366..a360cb9 100644 --- a/test/unit/Unit/Prelude.hs +++ b/test/unit/Unit/Prelude.hs @@ -11,7 +11,6 @@ import CLC.Stackage.Builder.Env batch, buildArgs, cabalPath, - colorLogs, groupFailFast, hLogger, packagesToBuild, @@ -67,11 +66,11 @@ mkBuildEnv = do { batch = Nothing, buildArgs = [], cabalPath = "cabal", - colorLogs = True, groupFailFast = False, hLogger = Logging.MkHandle - { getLocalTime = pure mkLocalTime, + { color = False, + getLocalTime = pure mkLocalTime, logStrErrLn = const (pure ()), logStrLn = const (pure ()), terminalWidth = 80 From 1be6d7130c120263e9bd4f5dd3ce598929095e61 Mon Sep 17 00:00:00 2001 From: Tommy Bidne Date: Wed, 18 Jun 2025 01:10:43 +1200 Subject: [PATCH 3/6] Parse 'installed' constraints Cabal constraints, like those returned from stackage's cabal.config endpoint, can specify packages in a number of ways, including e.g. 'mtl installed'. Previously we were ignoring such constraints. We now parse these too. --- cabal.project | 1 - clc-stackage.cabal | 27 ++- src/builder/CLC/Stackage/Builder/Batch.hs | 2 +- src/builder/CLC/Stackage/Builder/Env.hs | 2 +- src/builder/CLC/Stackage/Builder/Package.hs | 65 ------- src/builder/CLC/Stackage/Builder/Process.hs | 4 +- src/builder/CLC/Stackage/Builder/Writer.hs | 8 +- src/parser/CLC/Stackage/Parser.hs | 26 ++- src/parser/CLC/Stackage/Parser/API.hs | 2 - .../CLC/Stackage/Parser/API/CabalConfig.hs | 59 ++---- src/parser/CLC/Stackage/Parser/API/Common.hs | 13 +- src/parser/CLC/Stackage/Parser/API/JSON.hs | 7 +- src/runner/CLC/Stackage/Runner.hs | 2 +- src/runner/CLC/Stackage/Runner/Env.hs | 3 +- src/runner/CLC/Stackage/Runner/Report.hs | 2 +- src/utils/CLC/Stackage/Utils/Package.hs | 175 ++++++++++++++++++ test/functional/Main.hs | 2 +- .../testSmallSnapshotPath_posix.golden | 6 +- .../testSmallSnapshotPath_windows.golden | 6 +- test/functional/snapshot.txt | 2 - test/unit/Main.hs | 4 +- test/unit/Unit/CLC/Stackage/Utils/Package.hs | 58 ++++++ 22 files changed, 304 insertions(+), 172 deletions(-) delete mode 100644 src/builder/CLC/Stackage/Builder/Package.hs create mode 100644 src/utils/CLC/Stackage/Utils/Package.hs create mode 100644 test/unit/Unit/CLC/Stackage/Utils/Package.hs diff --git a/cabal.project b/cabal.project index 45adba7..08f105a 100644 --- a/cabal.project +++ b/cabal.project @@ -17,7 +17,6 @@ program-options -Wprepositive-qualified-module -Wredundant-constraints -Wunused-binds - -Wunused-packages -Wunused-type-patterns -Wno-unticked-promoted-constructors diff --git a/clc-stackage.cabal b/clc-stackage.cabal index 6130f2f..9423fad 100644 --- a/clc-stackage.cabal +++ b/clc-stackage.cabal @@ -24,7 +24,6 @@ common common-lang if os(windows) cpp-options: -DWINDOWS - build-depends: base >=4.16.0.0 && <4.22 default-language: GHC2021 library utils @@ -35,15 +34,19 @@ library utils CLC.Stackage.Utils.JSON CLC.Stackage.Utils.Logging CLC.Stackage.Utils.OS + CLC.Stackage.Utils.Package CLC.Stackage.Utils.Paths build-depends: , aeson >=2.0 && <2.3 , aeson-pretty ^>=0.8.9 + , base >=4.16.0.0 && <4.22 , bytestring >=0.10.12.0 && <0.13 + , deepseq >=1.4.6.0 && <1.6 , directory ^>=1.3.5.0 , file-io ^>=0.1.0.0 , filepath >=1.4.100.0 && <1.6 + , megaparsec >=9.5.0 && <9.8 , pretty-terminal ^>=0.1.0.0 , text >=1.2.3.2 && <2.2 , time >=1.9.3 && <1.15 @@ -61,17 +64,20 @@ library parser build-depends: , aeson + , base , bytestring , containers >=0.6.3.1 && <0.9 - , deepseq >=1.4.6.0 && <1.6 + , deepseq , filepath , http-client >=0.5.9 && <0.8 , http-client-tls ^>=0.3 , http-types ^>=0.12.3 + , megaparsec , text , utils hs-source-dirs: src/parser + ghc-options: -Wunused-packages library builder import: common-lang @@ -79,12 +85,11 @@ library builder CLC.Stackage.Builder CLC.Stackage.Builder.Batch CLC.Stackage.Builder.Env - CLC.Stackage.Builder.Package CLC.Stackage.Builder.Process CLC.Stackage.Builder.Writer build-depends: - , aeson + , base , containers , directory , filepath @@ -93,6 +98,7 @@ library builder , utils hs-source-dirs: src/builder + ghc-options: -Wunused-packages library runner import: common-lang @@ -104,6 +110,7 @@ library runner build-depends: , aeson + , base , builder , containers , directory @@ -116,17 +123,19 @@ library runner , utils hs-source-dirs: src/runner + ghc-options: -Wunused-packages executable clc-stackage import: common-lang main-is: Main.hs build-depends: + , base , runner , terminal-size ^>=0.3.4 , utils hs-source-dirs: ./app - ghc-options: -threaded -with-rtsopts=-N + ghc-options: -threaded -with-rtsopts=-N -Wunused-packages library test-utils import: common-lang @@ -137,6 +146,7 @@ library test-utils , tasty-golden ^>=2.3.1.1 hs-source-dirs: test/utils + ghc-options: -Wunused-packages test-suite unit import: common-lang @@ -146,6 +156,7 @@ test-suite unit Unit.CLC.Stackage.Parser.API Unit.CLC.Stackage.Runner.Env Unit.CLC.Stackage.Runner.Report + Unit.CLC.Stackage.Utils.Package Unit.Prelude build-depends: @@ -165,7 +176,7 @@ test-suite unit , utils hs-source-dirs: test/unit - ghc-options: -threaded -with-rtsopts=-N + ghc-options: -threaded -with-rtsopts=-N -Wunused-packages test-suite functional import: common-lang @@ -187,3 +198,7 @@ test-suite functional , utils hs-source-dirs: test/functional + +-- For some reason -Wunused-packages is complaining about clc-stackage +-- being an unnecessary dep for the functional test suite...hence it is +-- removed from cabal.project and added manually to other targets. diff --git a/src/builder/CLC/Stackage/Builder/Batch.hs b/src/builder/CLC/Stackage/Builder/Batch.hs index 8d5fe13..ac3dfe3 100644 --- a/src/builder/CLC/Stackage/Builder/Batch.hs +++ b/src/builder/CLC/Stackage/Builder/Batch.hs @@ -10,7 +10,7 @@ import CLC.Stackage.Builder.Env packagesToBuild ), ) -import CLC.Stackage.Builder.Package (Package) +import CLC.Stackage.Utils.Package (Package) import Data.Bifunctor (Bifunctor (first)) import Data.List qualified as L import Data.List.NonEmpty (NonEmpty ((:|)), (<|)) diff --git a/src/builder/CLC/Stackage/Builder/Env.hs b/src/builder/CLC/Stackage/Builder/Env.hs index 48ba823..a6c565e 100644 --- a/src/builder/CLC/Stackage/Builder/Env.hs +++ b/src/builder/CLC/Stackage/Builder/Env.hs @@ -6,8 +6,8 @@ module CLC.Stackage.Builder.Env ) where -import CLC.Stackage.Builder.Package (Package) import CLC.Stackage.Utils.Logging qualified as Logging +import CLC.Stackage.Utils.Package (Package) import Data.IORef (IORef) import Data.List.NonEmpty (NonEmpty) import Data.Set (Set) diff --git a/src/builder/CLC/Stackage/Builder/Package.hs b/src/builder/CLC/Stackage/Builder/Package.hs deleted file mode 100644 index 9c2c558..0000000 --- a/src/builder/CLC/Stackage/Builder/Package.hs +++ /dev/null @@ -1,65 +0,0 @@ -{-# LANGUAGE ViewPatterns #-} - --- | Provides the type representing a package with version. -module CLC.Stackage.Builder.Package - ( Package (..), - fromText, - toText, - toDepText, - toDirName, - ) -where - -import CLC.Stackage.Utils.Paths qualified as Paths -import Data.Aeson (FromJSON, ToJSON) -import Data.String (IsString (fromString)) -import Data.Text (Text) -import Data.Text qualified as T -import GHC.Generics (Generic) -import System.OsPath (OsPath) - --- | Package data. -data Package = MkPackage - { name :: Text, - version :: Text - } - deriving stock (Eq, Generic, Ord, Show) - deriving anyclass (FromJSON, ToJSON) - -instance IsString Package where - fromString s = case fromText (T.pack s) of - Nothing -> - error $ - mconcat - [ "String '", - s, - "' did no match expected package format: ==" - ] - Just p -> p - -fromText :: Text -> Maybe Package -fromText txt = case T.breakOn delim txt of - (xs, T.stripPrefix delim -> Just ys) - -- point exists but version is empty - | T.null ys -> Nothing - -- correct - | otherwise -> Just $ MkPackage xs ys - -- point does not exist - _ -> Nothing - --- | Text representation of the package e.g. 'foo ==1.2.3'. -toText :: Package -> Text -toText p = p.name <> delim <> p.version - -delim :: Text -delim = " ==" - --- | Text representation suitable for cabal file build-depends. -toDepText :: Package -> Text -toDepText = (", " <>) . toText - --- | Returns an OsPath name based on this package i.e. the OsPath --- representation of 'toText'. Used when naming an error directory for a --- package that fails. -toDirName :: Package -> IO OsPath -toDirName = Paths.encodeUtf . T.unpack . toText diff --git a/src/builder/CLC/Stackage/Builder/Process.hs b/src/builder/CLC/Stackage/Builder/Process.hs index 9eb7d1f..754951b 100644 --- a/src/builder/CLC/Stackage/Builder/Process.hs +++ b/src/builder/CLC/Stackage/Builder/Process.hs @@ -18,10 +18,10 @@ import CLC.Stackage.Builder.Env WriteLogs (WriteLogsCurrent, WriteLogsNone, WriteLogsSaveFailures), ) import CLC.Stackage.Builder.Env qualified as Env -import CLC.Stackage.Builder.Package qualified as Package import CLC.Stackage.Builder.Writer qualified as Writer import CLC.Stackage.Utils.IO qualified as IO import CLC.Stackage.Utils.Logging qualified as Logging +import CLC.Stackage.Utils.Package qualified as Package import CLC.Stackage.Utils.Paths qualified as Paths import Control.Exception (throwIO) import Control.Monad (when) @@ -115,7 +115,7 @@ buildProject env idx pkgs = do mconcat [ T.pack $ show idx, ": ", - T.intercalate ", " (Package.toText <$> pkgsList) + T.intercalate ", " (Package.toTextInstalled <$> pkgsList) ] pkgsList = NE.toList pkgs.unPackageGroup pkgsSet = Set.fromList pkgsList diff --git a/src/builder/CLC/Stackage/Builder/Writer.hs b/src/builder/CLC/Stackage/Builder/Writer.hs index 4bb2f24..48787ea 100644 --- a/src/builder/CLC/Stackage/Builder/Writer.hs +++ b/src/builder/CLC/Stackage/Builder/Writer.hs @@ -5,9 +5,9 @@ module CLC.Stackage.Builder.Writer where import CLC.Stackage.Builder.Batch (PackageGroup (unPackageGroup)) -import CLC.Stackage.Builder.Package (Package) -import CLC.Stackage.Builder.Package qualified as Package import CLC.Stackage.Utils.IO qualified as IO +import CLC.Stackage.Utils.Package (Package) +import CLC.Stackage.Utils.Package qualified as Package import CLC.Stackage.Utils.Paths qualified as Paths import Data.List.NonEmpty qualified as NE import Data.Text (Text) @@ -33,7 +33,7 @@ writeCabalProjectLocal pkgs = IO.writeBinaryFile path constraintsSrc path = Paths.generatedCabalProjectLocalPath constraintsSrc = TEnc.encodeUtf8 constraintsTxt constraintsTxt = T.unlines $ "constraints:" : constraints - constraints = (\p -> " " <> Package.toText p <> ",") <$> pkgs + constraints = (\p -> " " <> Package.toCabalConstraintsText p) <$> pkgs -- | Writes the package set to a cabal file for building. This will be called -- for each group we want to build. @@ -60,7 +60,7 @@ mkCabalFile pkgs = " default-language: Haskell2010" ] where - pkgsTxt = (\p -> pkgsIndent <> Package.toDepText p) <$> NE.toList pkgs.unPackageGroup + pkgsTxt = (\p -> pkgsIndent <> Package.toCabalDepText p) <$> NE.toList pkgs.unPackageGroup -- build-depends is indented 4, then 2 for the package itself. pkgsIndent :: Text diff --git a/src/parser/CLC/Stackage/Parser.hs b/src/parser/CLC/Stackage/Parser.hs index 241b02a..d7d0720 100644 --- a/src/parser/CLC/Stackage/Parser.hs +++ b/src/parser/CLC/Stackage/Parser.hs @@ -11,8 +11,7 @@ module CLC.Stackage.Parser where import CLC.Stackage.Parser.API - ( PackageResponse (name, version), - StackageResponse (packages), + ( StackageResponse (packages), ) import CLC.Stackage.Parser.API qualified as API import CLC.Stackage.Parser.API.CabalConfig qualified as CabalConfig @@ -21,6 +20,8 @@ import CLC.Stackage.Utils.JSON qualified as JSON import CLC.Stackage.Utils.Logging qualified as Logging import CLC.Stackage.Utils.OS (Os (Linux, Osx, Windows)) import CLC.Stackage.Utils.OS qualified as OS +import CLC.Stackage.Utils.Package (Package) +import CLC.Stackage.Utils.Package qualified as Package import Control.Monad (when) import Data.Aeson (FromJSON, ToJSON) import Data.Foldable (for_) @@ -33,13 +34,13 @@ import System.OsPath (OsPath, osp) -- | Retrieves the list of packages, based on -- 'CLC.Stackage.Parser.API.stackageUrl'. -getPackageList :: Logging.Handle -> Maybe OsPath -> IO [PackageResponse] +getPackageList :: Logging.Handle -> Maybe OsPath -> IO [Package] getPackageList hLogger msnapshotPath = getPackageListByOs hLogger msnapshotPath OS.currentOs -- | Prints the package list to a file. -printPackageList :: Bool -> Maybe Os -> IO () -printPackageList incVers mOs = do +printPackageList :: Maybe Os -> IO () +printPackageList mOs = do case mOs of Just os -> printOsList os Nothing -> for_ [minBound .. maxBound] printOsList @@ -49,23 +50,18 @@ printPackageList incVers mOs = do file Windows = [osp|pkgs_windows.txt|] printOsList os = do - pkgs <- getPackageListByOsFmt incVers os + pkgs <- getPackageListByOsFmt os let txt = T.unlines pkgs IO.writeFileUtf8 (file os) txt -- | Retrieves the package list formatted to text. -getPackageListByOsFmt :: Bool -> Os -> IO [Text] -getPackageListByOsFmt incVers = - (fmap . fmap) toText +getPackageListByOsFmt :: Os -> IO [Text] +getPackageListByOsFmt = + (fmap . fmap) Package.toTextInstalled . getPackageListByOs Logging.mkDefaultLogger Nothing - where - toText r = - if incVers - then r.name <> "-" <> r.version - else r.name -- | Helper in case we want to see what the package set for a given OS is. -getPackageListByOs :: Logging.Handle -> Maybe OsPath -> Os -> IO [PackageResponse] +getPackageListByOs :: Logging.Handle -> Maybe OsPath -> Os -> IO [Package] getPackageListByOs hLogger msnapshotPath os = do excludedPkgs <- getExcludedPkgs os let filterExcluded = flip Set.notMember excludedPkgs . (.name) diff --git a/src/parser/CLC/Stackage/Parser/API.hs b/src/parser/CLC/Stackage/Parser/API.hs index cba4125..6b19704 100644 --- a/src/parser/CLC/Stackage/Parser/API.hs +++ b/src/parser/CLC/Stackage/Parser/API.hs @@ -2,7 +2,6 @@ module CLC.Stackage.Parser.API ( -- * Querying stackage StackageResponse (..), - PackageResponse (..), getStackage, -- ** Exceptions @@ -22,7 +21,6 @@ import CLC.Stackage.Parser.API.Common ReasonReadBody, ReasonStatus ), - PackageResponse (name, version), StackageException (MkStackageException), StackageResponse (MkStackageResponse, packages), ) diff --git a/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs b/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs index 5d82b67..bfd485c 100644 --- a/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs +++ b/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs @@ -13,25 +13,22 @@ import CLC.Stackage.Parser.API.Common ReasonReadBody, ReasonStatus ), - PackageResponse - ( MkPackageResponse, - name, - version - ), StackageException (MkStackageException), StackageResponse (MkStackageResponse), getStatusCode, ) import CLC.Stackage.Utils.Exception qualified as Ex +import CLC.Stackage.Utils.Package qualified as Package import Control.Exception (throwIO) import Control.Monad (when) -import Data.List qualified as L import Data.Maybe (catMaybes) import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding qualified as TEnc import Network.HTTP.Client (BodyReader, Manager, Request, Response) import Network.HTTP.Client qualified as HttpClient +import Text.Megaparsec qualified as MP +import Text.Megaparsec.Char qualified as MPC -- | Given http manager and snapshot string, queries the cabal config -- endpoint. This is intended as a backup, for when the primary endpoint fails. @@ -80,45 +77,13 @@ parseCabalConfig = . fmap parseCabalConfigLine . T.lines --- | Parses a line like ' =='. This does not currently handle --- "installed" packages e.g. 'mtl installed'. This probably isn't a big deal, --- since all such libs will be built transitively anyway. That said, if --- we wanted to fix it, we would probably want to change PackageResponse's --- --- version :: Text --- --- field to --- --- version :: Maybe Text --- --- and parse "installed" to Nothing. Then, when we go to write the generated --- cabal file, Nothing will correspond to writing no version number. --- (CLC.Stackage.Builder.Package.toText). -parseCabalConfigLine :: Text -> Maybe PackageResponse --- splitOn rather than breakOn since the former drops the delim, which is --- convenient. -parseCabalConfigLine txt = case T.splitOn delim txt of - [nameRaw, versRaw] -> do - (v, c) <- T.unsnoc versRaw - -- Strip trailing comma if it exists. Otherwise take everything. - let version = if c == ',' then v else T.snoc v c - - -- This line handles prefixes e.g. whitespace or a stanza e.g. - -- - -- constraints: abstract-deque ==0.3, - -- abstract-deque-tests ==0.3, - -- ... - -- - -- We split pre-delim on whitespace, and take the last word. - (_, name) <- L.unsnoc $ T.words nameRaw - - -- T.strip as trailing characters can cause problems e.g. windows can - -- pick up \r. - Just $ - MkPackageResponse - { name = T.strip name, - version = T.strip version - } - _ -> Nothing +-- | Parses a line like ' =='. +parseCabalConfigLine :: Text -> Maybe Package.Package +parseCabalConfigLine txt = case MP.parse (MPC.space *> p) "package" txt of + Right x -> Just x + Left _ -> Nothing where - delim = " ==" + -- Optional case for leading constraints section. + p = do + _ <- MP.optional (MPC.string "constraints:" *> MPC.space) + Package.packageParser diff --git a/src/parser/CLC/Stackage/Parser/API/Common.hs b/src/parser/CLC/Stackage/Parser/API/Common.hs index d6ef42e..fe2b5c9 100644 --- a/src/parser/CLC/Stackage/Parser/API/Common.hs +++ b/src/parser/CLC/Stackage/Parser/API/Common.hs @@ -2,7 +2,6 @@ module CLC.Stackage.Parser.API.Common ( -- * Types StackageResponse (..), - PackageResponse (..), -- * Exception StackageException (..), @@ -13,13 +12,13 @@ module CLC.Stackage.Parser.API.Common ) where +import CLC.Stackage.Utils.Package (Package) import Control.DeepSeq (NFData) import Control.Exception ( Exception (displayException), SomeException, ) import Data.ByteString (ByteString) -import Data.Text (Text) import Data.Text.Encoding.Error (UnicodeException) import GHC.Generics (Generic) import Network.HTTP.Client (Response) @@ -29,15 +28,7 @@ import Network.HTTP.Types.Status qualified as Status -- | Stackage response. This type unifies different stackage responses. newtype StackageResponse = MkStackageResponse - { packages :: [PackageResponse] - } - deriving stock (Eq, Generic, Show) - deriving anyclass (NFData) - --- | Package in a stackage snapshot. -data PackageResponse = MkPackageResponse - { name :: Text, - version :: Text + { packages :: [Package] } deriving stock (Eq, Generic, Show) deriving anyclass (NFData) diff --git a/src/parser/CLC/Stackage/Parser/API/JSON.hs b/src/parser/CLC/Stackage/Parser/API/JSON.hs index dd03658..9ee4a2d 100644 --- a/src/parser/CLC/Stackage/Parser/API/JSON.hs +++ b/src/parser/CLC/Stackage/Parser/API/JSON.hs @@ -12,6 +12,7 @@ import CLC.Stackage.Parser.API.Common import CLC.Stackage.Parser.API.Common qualified as Common import CLC.Stackage.Utils.Exception qualified as Ex import CLC.Stackage.Utils.JSON qualified as JSON +import CLC.Stackage.Utils.Package qualified as Package import Control.Exception (throwIO) import Control.Monad (when) import Data.Aeson (FromJSON, ToJSON) @@ -68,11 +69,11 @@ toSnapshotCommon (MkStackageResponse _ pkgs) = { packages = toPackageCommon <$> pkgs } -toPackageCommon :: PackageResponse -> Common.PackageResponse +toPackageCommon :: PackageResponse -> Package.Package toPackageCommon pr = - Common.MkPackageResponse + Package.MkPackage { name = pr.name, - version = pr.version + version = Package.PackageVersionText pr.version } -- | Response returned by primary stackage endpoint e.g. diff --git a/src/runner/CLC/Stackage/Runner.hs b/src/runner/CLC/Stackage/Runner.hs index c7d1eb4..c2bcfb2 100644 --- a/src/runner/CLC/Stackage/Runner.hs +++ b/src/runner/CLC/Stackage/Runner.hs @@ -11,11 +11,11 @@ import CLC.Stackage.Builder.Env ( BuildEnv (progress), Progress (failuresRef), ) -import CLC.Stackage.Builder.Package (Package) import CLC.Stackage.Builder.Writer qualified as Writer import CLC.Stackage.Runner.Env (RunnerEnv (completePackageSet)) import CLC.Stackage.Runner.Env qualified as Env import CLC.Stackage.Utils.Logging qualified as Logging +import CLC.Stackage.Utils.Package (Package) import Control.Exception (bracket, throwIO) import Control.Monad (when) import Data.Foldable (for_) diff --git a/src/runner/CLC/Stackage/Runner/Env.hs b/src/runner/CLC/Stackage/Runner/Env.hs index d4055f4..eafdd81 100644 --- a/src/runner/CLC/Stackage/Runner/Env.hs +++ b/src/runner/CLC/Stackage/Runner/Env.hs @@ -28,9 +28,7 @@ import CLC.Stackage.Builder.Env ), ) import CLC.Stackage.Builder.Env qualified as Builder.Env -import CLC.Stackage.Builder.Package (Package (MkPackage, name, version)) import CLC.Stackage.Parser qualified as Parser -import CLC.Stackage.Parser.API (PackageResponse (name, version)) import CLC.Stackage.Runner.Args ( Args (snapshotPath), ColorLogs @@ -45,6 +43,7 @@ import CLC.Stackage.Runner.Report qualified as Report import CLC.Stackage.Utils.Exception qualified as Ex import CLC.Stackage.Utils.IO qualified as IO import CLC.Stackage.Utils.Logging qualified as Logging +import CLC.Stackage.Utils.Package (Package (MkPackage, name, version)) import CLC.Stackage.Utils.Paths qualified as Paths import Control.Exception (throwIO) import Control.Monad (unless) diff --git a/src/runner/CLC/Stackage/Runner/Report.hs b/src/runner/CLC/Stackage/Runner/Report.hs index d1b46d5..39f00bc 100644 --- a/src/runner/CLC/Stackage/Runner/Report.hs +++ b/src/runner/CLC/Stackage/Runner/Report.hs @@ -16,10 +16,10 @@ module CLC.Stackage.Runner.Report ) where -import CLC.Stackage.Builder.Package (Package) import CLC.Stackage.Utils.IO qualified as IO import CLC.Stackage.Utils.JSON qualified as JSON import CLC.Stackage.Utils.Logging qualified as Logging +import CLC.Stackage.Utils.Package (Package) import CLC.Stackage.Utils.Paths qualified as Paths import Control.Exception (throwIO) import Data.Aeson (AesonException (AesonException), FromJSON, ToJSON) diff --git a/src/utils/CLC/Stackage/Utils/Package.hs b/src/utils/CLC/Stackage/Utils/Package.hs new file mode 100644 index 0000000..401df36 --- /dev/null +++ b/src/utils/CLC/Stackage/Utils/Package.hs @@ -0,0 +1,175 @@ +{-# LANGUAGE ViewPatterns #-} + +-- | Provides the type representing a package with version. +module CLC.Stackage.Utils.Package + ( -- * Package + Package (..), + + -- ** Creation + fromCabalConstraintsText, + + -- ** Elimination + toCabalDepText, + toCabalConstraintsText, + toDirName, + toTextInstalled, + + -- * Version + PackageVersion (..), + + -- * Parsers + packageParser, + ) +where + +import CLC.Stackage.Utils.Paths qualified as Paths +import Control.Applicative (Alternative ((<|>))) +import Control.DeepSeq (NFData) +import Control.Monad (void) +import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) +import Data.Aeson qualified as Asn +import Data.Char qualified as Ch +import Data.String (IsString (fromString)) +import Data.Text (Text) +import Data.Text qualified as T +import Data.Void (Void) +import GHC.Generics (Generic) +import System.OsPath (OsPath) +import Text.Megaparsec qualified as MP +import Text.Megaparsec.Char qualified as MPC +import Text.Megaparsec.Char.Lexer qualified as MPCL + +-- | Wrapper for package version. +data PackageVersion + = -- | Basic version text e.g. "2.3". + PackageVersionText Text + | -- | Represents an installed lib e.g. "foo installed" from cabal + -- constraints. This is included in the from/toJSON instances, for the + -- writing/reading the report. + -- + -- Generally speaking, this is only used when clc-stackage is falls back + -- to the cabal.config endpoint, or is used with an explicit + -- --snapshot-path argument. + PackageVersionInstalled + deriving stock (Eq, Generic, Ord, Show) + deriving anyclass (NFData) + +instance FromJSON PackageVersion where + parseJSON = Asn.withText "PackageVersion" $ \case + "installed" -> pure PackageVersionInstalled + other -> pure $ PackageVersionText other + +instance ToJSON PackageVersion where + toJSON (PackageVersionText t) = toJSON t + toJSON PackageVersionInstalled = "installed" + +-- | Package data. +data Package = MkPackage + { name :: Text, + version :: PackageVersion + } + deriving stock (Eq, Generic, Ord, Show) + deriving anyclass (FromJSON, NFData, ToJSON) + +instance IsString Package where + fromString s = case fromCabalConstraintsText (T.pack s) of + Nothing -> + error $ + mconcat + [ "String '", + s, + "' did no match expected package format: ==" + ] + Just p -> p + +-- | Text representation suitable for cabal file build-depends. +toCabalDepText :: Package -> Text +toCabalDepText = (", " <>) . toTextNoInstalled + +-- | Text representation suitable for cabal file constraints. +toCabalConstraintsText :: Package -> Text +toCabalConstraintsText = (<> ",") . toTextInstalled + +-- | Returns an OsPath name based on this package i.e. the OsPath +-- representation of 'toText'. Used when naming an error directory for a +-- package that fails. +toDirName :: Package -> IO OsPath +toDirName = Paths.encodeUtf . T.unpack . toTextInstalled + +-- | Text representation of the package respecting "installed" versions e.g. +-- +-- - "aeson ==1.2.3" +-- - "mtl installed" +-- +-- This output is suitable for cabal constraints, _not_ a cabal file. +toTextInstalled :: Package -> Text +toTextInstalled p = p.name <> versToTextInstalled p.version + +-- | Text representation of the package, dropping "installed" versions e.g. +-- +-- - "aeson ==1.2.3" +-- - "mtl" +-- +-- This output is suitable for cabal file, _not_ constraints. +toTextNoInstalled :: Package -> Text +toTextNoInstalled p = p.name <> versToTextNoInstalled p.version + +-- | Attempts to parse the text to the package. Some flexibility e.g. +-- supports: +-- +-- - "aeson ==2.0.0," +-- - "mtl installed" +fromCabalConstraintsText :: Text -> Maybe Package +fromCabalConstraintsText txt = + case MP.parse (MPC.space *> packageParser) "package" txt of + Right x -> Just x + Left _ -> Nothing + +type Parser = MP.Parsec Void Text + +-- | Parses packages e.g. +-- +-- - "aeson ==2.0.0," +-- - "mtl installed" +packageParser :: Parser Package +packageParser = do + name <- nameParser + vers <- versionTextParser <|> versionInstalledParser + pure $ MkPackage name vers + +nameParser :: Parser Text +nameParser = lexeme $ MP.takeWhile1P (Just "name") isNameChar + where + isNameChar c = c /= ' ' && c /= '=' + +versionInstalledParser :: Parser PackageVersion +versionInstalledParser = do + MPC.string "installed" + mcommaParser + pure PackageVersionInstalled + +versionTextParser :: Parser PackageVersion +versionTextParser = do + lexeme $ MPC.string delim + vers <- lexeme $ MP.takeWhile1P (Just "version") isVersChar + mcommaParser + pure $ PackageVersionText vers + where + isVersChar c = Ch.isDigit c || c == '.' + +mcommaParser :: Parser () +mcommaParser = lexeme $ void $ MP.optional $ MPC.char ',' + +lexeme :: Parser a -> Parser a +lexeme = MPCL.lexeme MPC.space + +versToTextInstalled :: PackageVersion -> Text +versToTextInstalled (PackageVersionText t) = " " <> delim <> t +versToTextInstalled PackageVersionInstalled = " installed" + +versToTextNoInstalled :: PackageVersion -> Text +versToTextNoInstalled (PackageVersionText t) = " " <> delim <> t +versToTextNoInstalled PackageVersionInstalled = "" + +delim :: Text +delim = "==" diff --git a/test/functional/Main.hs b/test/functional/Main.hs index 14ed1e0..bf90e24 100644 --- a/test/functional/Main.hs +++ b/test/functional/Main.hs @@ -2,11 +2,11 @@ module Main (main) where -import CLC.Stackage.Builder.Package (Package (name)) import CLC.Stackage.Runner qualified as Runner import CLC.Stackage.Utils.IO qualified as IO import CLC.Stackage.Utils.Logging qualified as Logging import CLC.Stackage.Utils.OS (Os (Windows), currentOs) +import CLC.Stackage.Utils.Package (Package (name)) import CLC.Stackage.Utils.Paths qualified as Paths import Data.ByteString (ByteString) import Data.ByteString.Char8 qualified as C8 diff --git a/test/functional/goldens/testSmallSnapshotPath_posix.golden b/test/functional/goldens/testSmallSnapshotPath_posix.golden index c73b3e4..a98f220 100644 --- a/test/functional/goldens/testSmallSnapshotPath_posix.golden +++ b/test/functional/goldens/testSmallSnapshotPath_posix.golden @@ -1,9 +1,9 @@ [2020-05-31 12:00:00][Info] Cached results do not exist: output/cache.json -[2020-05-31 12:00:00][Warn] Only found 7 packages. Is that right? -[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, optics-co... +[2020-05-31 12:00:00][Warn] Only found 8 packages. Is that right? +[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, mtl insta... -- Successes: 4 (100%) +- Successes: 5 (100%) - Failures: 0 (0%) - Untested: 0 (0%) diff --git a/test/functional/goldens/testSmallSnapshotPath_windows.golden b/test/functional/goldens/testSmallSnapshotPath_windows.golden index 3da6d58..aaac901 100644 --- a/test/functional/goldens/testSmallSnapshotPath_windows.golden +++ b/test/functional/goldens/testSmallSnapshotPath_windows.golden @@ -1,9 +1,9 @@ [2020-05-31 12:00:00][Info] Cached results do not exist: output\cache.json -[2020-05-31 12:00:00][Warn] Only found 7 packages. Is that right? -[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, optics-co... +[2020-05-31 12:00:00][Warn] Only found 8 packages. Is that right? +[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, mtl insta... -- Successes: 4 (100%) +- Successes: 5 (100%) - Failures: 0 (0%) - Untested: 0 (0%) diff --git a/test/functional/snapshot.txt b/test/functional/snapshot.txt index 325a117..f1f48cb 100644 --- a/test/functional/snapshot.txt +++ b/test/functional/snapshot.txt @@ -6,8 +6,6 @@ aeson ==2.2.3.0 cborg ==0.2.10.0 clock ==0.8.4 kan-extensions ==5.2.6 -# Note that mtl doesn't actually get parsed, due to the different format. -# This line merely tests that such lines do not cause an error. mtl installed optics-core ==0.4.1.1 profunctors ==5.6.2 diff --git a/test/unit/Main.hs b/test/unit/Main.hs index 5ff9292..f2188bc 100644 --- a/test/unit/Main.hs +++ b/test/unit/Main.hs @@ -5,6 +5,7 @@ import Test.Tasty.Golden (DeleteOutputFile (OnPass)) import Unit.CLC.Stackage.Parser.API qualified as Parser.API import Unit.CLC.Stackage.Runner.Env qualified as Runner.Env import Unit.CLC.Stackage.Runner.Report qualified as Runner.Report +import Unit.CLC.Stackage.Utils.Package qualified as Utils.Package main :: IO () main = @@ -14,5 +15,6 @@ main = "Unit" [ Parser.API.tests, Runner.Env.tests, - Runner.Report.tests + Runner.Report.tests, + Utils.Package.tests ] diff --git a/test/unit/Unit/CLC/Stackage/Utils/Package.hs b/test/unit/Unit/CLC/Stackage/Utils/Package.hs new file mode 100644 index 0000000..53a0d9e --- /dev/null +++ b/test/unit/Unit/CLC/Stackage/Utils/Package.hs @@ -0,0 +1,58 @@ +{-# OPTIONS_GHC -Wno-missing-import-lists #-} + +module Unit.CLC.Stackage.Utils.Package (tests) where + +import CLC.Stackage.Utils.Package + ( Package (MkPackage, name, version), + PackageVersion (PackageVersionInstalled, PackageVersionText), + ) +import CLC.Stackage.Utils.Package qualified as Package +import Unit.Prelude + +tests :: TestTree +tests = + testGroup + "CLC.Stackage.Utils.Package" + [ testFromCabalConstraintsTextSuccesses, + testToCabalDepText, + testToCabalConstraintsText + ] + +testFromCabalConstraintsTextSuccesses :: TestTree +testFromCabalConstraintsTextSuccesses = testCase desc $ do + Just e1 @=? Package.fromCabalConstraintsText "aeson ==2.0.1" + Just e1 @=? Package.fromCabalConstraintsText "aeson ==2.0.1," + Just e1 @=? Package.fromCabalConstraintsText " aeson == 2.0.1 , " + Just e2 @=? Package.fromCabalConstraintsText "mtl installed" + Just e2 @=? Package.fromCabalConstraintsText "mtl installed," + Just e2 @=? Package.fromCabalConstraintsText " mtl installed , " + where + desc = "fromCabalConstraintsText successes" + +testToCabalDepText :: TestTree +testToCabalDepText = testCase desc $ do + ", aeson ==2.0.1" @=? Package.toCabalDepText e1 + ", mtl" @=? Package.toCabalDepText e2 + where + desc = "toCabalDepText" + +testToCabalConstraintsText :: TestTree +testToCabalConstraintsText = testCase desc $ do + "aeson ==2.0.1," @=? Package.toCabalConstraintsText e1 + "mtl installed," @=? Package.toCabalConstraintsText e2 + where + desc = "toCabalConstraintsText" + +e1 :: Package +e1 = + MkPackage + { name = "aeson", + version = PackageVersionText "2.0.1" + } + +e2 :: Package +e2 = + MkPackage + { name = "mtl", + version = PackageVersionInstalled + } From ff436aaf5356f7ac817b8067f991c2579de0f070 Mon Sep 17 00:00:00 2001 From: Tommy Bidne Date: Wed, 18 Jun 2025 11:19:14 +1200 Subject: [PATCH 4/6] Swap megaparsec for text parsing Using megaparsec was probably overkill, as the constraint parsing is simple enough that we can do it with text without too much trouble. --- clc-stackage.cabal | 2 - .../CLC/Stackage/Parser/API/CabalConfig.hs | 16 ++-- src/utils/CLC/Stackage/Utils/Package.hs | 80 +++++++++---------- 3 files changed, 45 insertions(+), 53 deletions(-) diff --git a/clc-stackage.cabal b/clc-stackage.cabal index 9423fad..5c72666 100644 --- a/clc-stackage.cabal +++ b/clc-stackage.cabal @@ -46,7 +46,6 @@ library utils , directory ^>=1.3.5.0 , file-io ^>=0.1.0.0 , filepath >=1.4.100.0 && <1.6 - , megaparsec >=9.5.0 && <9.8 , pretty-terminal ^>=0.1.0.0 , text >=1.2.3.2 && <2.2 , time >=1.9.3 && <1.15 @@ -72,7 +71,6 @@ library parser , http-client >=0.5.9 && <0.8 , http-client-tls ^>=0.3 , http-types ^>=0.12.3 - , megaparsec , text , utils diff --git a/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs b/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs index bfd485c..74fdfff 100644 --- a/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs +++ b/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs @@ -27,8 +27,6 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as TEnc import Network.HTTP.Client (BodyReader, Manager, Request, Response) import Network.HTTP.Client qualified as HttpClient -import Text.Megaparsec qualified as MP -import Text.Megaparsec.Char qualified as MPC -- | Given http manager and snapshot string, queries the cabal config -- endpoint. This is intended as a backup, for when the primary endpoint fails. @@ -79,11 +77,11 @@ parseCabalConfig = -- | Parses a line like ' =='. parseCabalConfigLine :: Text -> Maybe Package.Package -parseCabalConfigLine txt = case MP.parse (MPC.space *> p) "package" txt of - Right x -> Just x - Left _ -> Nothing +parseCabalConfigLine txt = do + -- Strip leading 'constraints:' keyword, if it exists. + let s = case T.stripPrefix "constraints:" txt' of + Nothing -> txt' + Just rest -> T.stripStart rest + Package.packageParser s where - -- Optional case for leading constraints section. - p = do - _ <- MP.optional (MPC.string "constraints:" *> MPC.space) - Package.packageParser + txt' = T.stripStart txt diff --git a/src/utils/CLC/Stackage/Utils/Package.hs b/src/utils/CLC/Stackage/Utils/Package.hs index 401df36..63a211a 100644 --- a/src/utils/CLC/Stackage/Utils/Package.hs +++ b/src/utils/CLC/Stackage/Utils/Package.hs @@ -25,29 +25,24 @@ where import CLC.Stackage.Utils.Paths qualified as Paths import Control.Applicative (Alternative ((<|>))) import Control.DeepSeq (NFData) -import Control.Monad (void) import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) import Data.Aeson qualified as Asn import Data.Char qualified as Ch import Data.String (IsString (fromString)) import Data.Text (Text) import Data.Text qualified as T -import Data.Void (Void) import GHC.Generics (Generic) import System.OsPath (OsPath) -import Text.Megaparsec qualified as MP -import Text.Megaparsec.Char qualified as MPC -import Text.Megaparsec.Char.Lexer qualified as MPCL -- | Wrapper for package version. data PackageVersion = -- | Basic version text e.g. "2.3". PackageVersionText Text | -- | Represents an installed lib e.g. "foo installed" from cabal - -- constraints. This is included in the from/toJSON instances, for the + -- constraints. This is included in the from/toJSON instances, for -- writing/reading the report. -- - -- Generally speaking, this is only used when clc-stackage is falls back + -- Generally speaking, this is only used when clc-stackage falls back -- to the cabal.config endpoint, or is used with an explicit -- --snapshot-path argument. PackageVersionInstalled @@ -120,49 +115,50 @@ toTextNoInstalled p = p.name <> versToTextNoInstalled p.version -- - "aeson ==2.0.0," -- - "mtl installed" fromCabalConstraintsText :: Text -> Maybe Package -fromCabalConstraintsText txt = - case MP.parse (MPC.space *> packageParser) "package" txt of - Right x -> Just x - Left _ -> Nothing +fromCabalConstraintsText = packageParser . T.stripStart -type Parser = MP.Parsec Void Text - --- | Parses packages e.g. +-- NOTE: [*Parsers] -- --- - "aeson ==2.0.0," --- - "mtl installed" -packageParser :: Parser Package -packageParser = do - name <- nameParser - vers <- versionTextParser <|> versionInstalledParser +-- DIY parser, where each function parses only as much as it needs, then +-- returns the rest to be fed into the next parser. Following megaparsec's +-- lead, each parser assumes that it is at the start of relevant text +-- (i.e. no leading whitespace), and consumes trailing whitespace. +-- +-- Hence the "rest" that is returned must have its leading whitespace stripped, +-- so that the next parser can make the same assumption. + +packageParser :: Text -> Maybe Package +packageParser txt = do + (name, r1) <- nameParser txt + (vers, _) <- versionTextParser r1 <|> versionInstalledParser r1 pure $ MkPackage name vers -nameParser :: Parser Text -nameParser = lexeme $ MP.takeWhile1P (Just "name") isNameChar +-- Split on whitepspace or equals e.g. "mtl installed", "aeson ==1.2.3". +nameParser :: Text -> Maybe (Text, Text) +nameParser txt + | T.null name = Nothing + | otherwise = Just (name, T.stripStart rest) where - isNameChar c = c /= ' ' && c /= '=' - -versionInstalledParser :: Parser PackageVersion -versionInstalledParser = do - MPC.string "installed" - mcommaParser - pure PackageVersionInstalled - -versionTextParser :: Parser PackageVersion -versionTextParser = do - lexeme $ MPC.string delim - vers <- lexeme $ MP.takeWhile1P (Just "version") isVersChar - mcommaParser - pure $ PackageVersionText vers + (name, rest) = T.break isNameChar txt + isNameChar c = c == ' ' || c == '=' + +-- Parse "installed". +versionInstalledParser :: Text -> Maybe (PackageVersion, Text) +versionInstalledParser txt = do + rest <- T.stripPrefix "installed" txt + pure (PackageVersionInstalled, T.stripStart rest) + +-- Parse e.g. "==1.2.3". +versionTextParser :: Text -> Maybe (PackageVersion, Text) +versionTextParser txt = do + r1 <- T.stripPrefix delim txt + let (vers, r2) = T.span isVersChar (T.stripStart r1) + if not (T.null vers) + then Just (PackageVersionText vers, T.stripStart r2) + else Nothing where isVersChar c = Ch.isDigit c || c == '.' -mcommaParser :: Parser () -mcommaParser = lexeme $ void $ MP.optional $ MPC.char ',' - -lexeme :: Parser a -> Parser a -lexeme = MPCL.lexeme MPC.space - versToTextInstalled :: PackageVersion -> Text versToTextInstalled (PackageVersionText t) = " " <> delim <> t versToTextInstalled PackageVersionInstalled = " installed" From 4852b97b509dd7ba4f53c2499d926f0ad918fe0b Mon Sep 17 00:00:00 2001 From: Tommy Bidne Date: Wed, 18 Jun 2025 11:43:48 +1200 Subject: [PATCH 5/6] Minor package display improvements Displayed package names (stdout, log directory names) now use a hyphen e.g. aeson-1.2.3, rather than reusing the default delimiter == e.g. aeson ==1.2.3. --- src/builder/CLC/Stackage/Builder/Process.hs | 2 +- src/parser/CLC/Stackage/Parser.hs | 2 +- .../CLC/Stackage/Parser/API/CabalConfig.hs | 2 +- src/utils/CLC/Stackage/Utils/Package.hs | 16 +++++++++++----- .../goldens/testSmallBatch_posix.golden | 6 +++--- .../goldens/testSmallBatch_windows.golden | 6 +++--- .../goldens/testSmallSnapshotPath_posix.golden | 2 +- .../goldens/testSmallSnapshotPath_windows.golden | 2 +- test/functional/goldens/testSmall_posix.golden | 2 +- test/functional/goldens/testSmall_windows.golden | 2 +- test/unit/Unit/CLC/Stackage/Utils/Package.hs | 10 +++++++++- 11 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/builder/CLC/Stackage/Builder/Process.hs b/src/builder/CLC/Stackage/Builder/Process.hs index 754951b..55342f5 100644 --- a/src/builder/CLC/Stackage/Builder/Process.hs +++ b/src/builder/CLC/Stackage/Builder/Process.hs @@ -115,7 +115,7 @@ buildProject env idx pkgs = do mconcat [ T.pack $ show idx, ": ", - T.intercalate ", " (Package.toTextInstalled <$> pkgsList) + T.intercalate ", " (Package.toDisplayName <$> pkgsList) ] pkgsList = NE.toList pkgs.unPackageGroup pkgsSet = Set.fromList pkgsList diff --git a/src/parser/CLC/Stackage/Parser.hs b/src/parser/CLC/Stackage/Parser.hs index d7d0720..b92bf86 100644 --- a/src/parser/CLC/Stackage/Parser.hs +++ b/src/parser/CLC/Stackage/Parser.hs @@ -57,7 +57,7 @@ printPackageList mOs = do -- | Retrieves the package list formatted to text. getPackageListByOsFmt :: Os -> IO [Text] getPackageListByOsFmt = - (fmap . fmap) Package.toTextInstalled + (fmap . fmap) Package.toDisplayName . getPackageListByOs Logging.mkDefaultLogger Nothing -- | Helper in case we want to see what the package set for a given OS is. diff --git a/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs b/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs index 74fdfff..de7a323 100644 --- a/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs +++ b/src/parser/CLC/Stackage/Parser/API/CabalConfig.hs @@ -82,6 +82,6 @@ parseCabalConfigLine txt = do let s = case T.stripPrefix "constraints:" txt' of Nothing -> txt' Just rest -> T.stripStart rest - Package.packageParser s + Package.fromCabalConstraintsText s where txt' = T.stripStart txt diff --git a/src/utils/CLC/Stackage/Utils/Package.hs b/src/utils/CLC/Stackage/Utils/Package.hs index 63a211a..7e91e08 100644 --- a/src/utils/CLC/Stackage/Utils/Package.hs +++ b/src/utils/CLC/Stackage/Utils/Package.hs @@ -12,13 +12,10 @@ module CLC.Stackage.Utils.Package toCabalDepText, toCabalConstraintsText, toDirName, - toTextInstalled, + toDisplayName, -- * Version PackageVersion (..), - - -- * Parsers - packageParser, ) where @@ -89,7 +86,16 @@ toCabalConstraintsText = (<> ",") . toTextInstalled -- representation of 'toText'. Used when naming an error directory for a -- package that fails. toDirName :: Package -> IO OsPath -toDirName = Paths.encodeUtf . T.unpack . toTextInstalled +toDirName = Paths.encodeUtf . T.unpack . toDisplayName + +-- | Slightly nicer display name e.g. "mtl-installed", "aeson-1.2.3". +toDisplayName :: Package -> Text +toDisplayName (MkPackage name vers) = txt + where + txt = name <> "-" <> v + v = case vers of + PackageVersionText t -> t + PackageVersionInstalled -> "installed" -- | Text representation of the package respecting "installed" versions e.g. -- diff --git a/test/functional/goldens/testSmallBatch_posix.golden b/test/functional/goldens/testSmallBatch_posix.golden index e088b47..e8ce0c3 100644 --- a/test/functional/goldens/testSmallBatch_posix.golden +++ b/test/functional/goldens/testSmallBatch_posix.golden @@ -1,7 +1,7 @@ [2020-05-31 12:00:00][Info] Cached results do not exist: output/cache.json -[2020-05-31 12:00:00][Success] 3: cborg ==0.2.10.0, clock ==0.8.4 -[2020-05-31 12:00:00][Success] 2: mtl ==2.3.1, optics-core ==0.4.1.1 -[2020-05-31 12:00:00][Success] 1: profunctors ==5.6.2 +[2020-05-31 12:00:00][Success] 3: cborg-0.2.10.0, clock-0.8.4 +[2020-05-31 12:00:00][Success] 2: mtl-2.3.1, optics-core-0.4.1.1 +[2020-05-31 12:00:00][Success] 1: profunctors-5.6.2 - Successes: 5 (100%) diff --git a/test/functional/goldens/testSmallBatch_windows.golden b/test/functional/goldens/testSmallBatch_windows.golden index e867167..5569fee 100644 --- a/test/functional/goldens/testSmallBatch_windows.golden +++ b/test/functional/goldens/testSmallBatch_windows.golden @@ -1,7 +1,7 @@ [2020-05-31 12:00:00][Info] Cached results do not exist: output\cache.json -[2020-05-31 12:00:00][Success] 3: cborg ==0.2.10.0, clock ==0.8.4 -[2020-05-31 12:00:00][Success] 2: mtl ==2.3.1, optics-core ==0.4.1.1 -[2020-05-31 12:00:00][Success] 1: profunctors ==5.6.2 +[2020-05-31 12:00:00][Success] 3: cborg-0.2.10.0, clock-0.8.4 +[2020-05-31 12:00:00][Success] 2: mtl-2.3.1, optics-core-0.4.1.1 +[2020-05-31 12:00:00][Success] 1: profunctors-5.6.2 - Successes: 5 (100%) diff --git a/test/functional/goldens/testSmallSnapshotPath_posix.golden b/test/functional/goldens/testSmallSnapshotPath_posix.golden index a98f220..731f562 100644 --- a/test/functional/goldens/testSmallSnapshotPath_posix.golden +++ b/test/functional/goldens/testSmallSnapshotPath_posix.golden @@ -1,6 +1,6 @@ [2020-05-31 12:00:00][Info] Cached results do not exist: output/cache.json [2020-05-31 12:00:00][Warn] Only found 8 packages. Is that right? -[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, mtl insta... +[2020-05-31 12:00:00][Success] 1: cborg-0.2.10.0, clock-0.8.4, mtl-installed... - Successes: 5 (100%) diff --git a/test/functional/goldens/testSmallSnapshotPath_windows.golden b/test/functional/goldens/testSmallSnapshotPath_windows.golden index aaac901..6ba7a5a 100644 --- a/test/functional/goldens/testSmallSnapshotPath_windows.golden +++ b/test/functional/goldens/testSmallSnapshotPath_windows.golden @@ -1,6 +1,6 @@ [2020-05-31 12:00:00][Info] Cached results do not exist: output\cache.json [2020-05-31 12:00:00][Warn] Only found 8 packages. Is that right? -[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, mtl insta... +[2020-05-31 12:00:00][Success] 1: cborg-0.2.10.0, clock-0.8.4, mtl-installed... - Successes: 5 (100%) diff --git a/test/functional/goldens/testSmall_posix.golden b/test/functional/goldens/testSmall_posix.golden index 1da2cea..9e6ac70 100644 --- a/test/functional/goldens/testSmall_posix.golden +++ b/test/functional/goldens/testSmall_posix.golden @@ -1,5 +1,5 @@ [2020-05-31 12:00:00][Info] Cached results do not exist: output/cache.json -[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, mtl ==2.3... +[2020-05-31 12:00:00][Success] 1: cborg-0.2.10.0, clock-0.8.4, mtl-2.3.1, op... - Successes: 5 (100%) diff --git a/test/functional/goldens/testSmall_windows.golden b/test/functional/goldens/testSmall_windows.golden index 6c55ac1..519cd25 100644 --- a/test/functional/goldens/testSmall_windows.golden +++ b/test/functional/goldens/testSmall_windows.golden @@ -1,5 +1,5 @@ [2020-05-31 12:00:00][Info] Cached results do not exist: output\cache.json -[2020-05-31 12:00:00][Success] 1: cborg ==0.2.10.0, clock ==0.8.4, mtl ==2.3... +[2020-05-31 12:00:00][Success] 1: cborg-0.2.10.0, clock-0.8.4, mtl-2.3.1, op... - Successes: 5 (100%) diff --git a/test/unit/Unit/CLC/Stackage/Utils/Package.hs b/test/unit/Unit/CLC/Stackage/Utils/Package.hs index 53a0d9e..6f121dd 100644 --- a/test/unit/Unit/CLC/Stackage/Utils/Package.hs +++ b/test/unit/Unit/CLC/Stackage/Utils/Package.hs @@ -15,7 +15,8 @@ tests = "CLC.Stackage.Utils.Package" [ testFromCabalConstraintsTextSuccesses, testToCabalDepText, - testToCabalConstraintsText + testToCabalConstraintsText, + testToDisplayName ] testFromCabalConstraintsTextSuccesses :: TestTree @@ -43,6 +44,13 @@ testToCabalConstraintsText = testCase desc $ do where desc = "toCabalConstraintsText" +testToDisplayName :: TestTree +testToDisplayName = testCase desc $ do + "aeson-2.0.1" @=? Package.toDisplayName e1 + "mtl-installed" @=? Package.toDisplayName e2 + where + desc = "toDisplayName" + e1 :: Package e1 = MkPackage From 094005ca06bd0d92f85899ff31861cedcaf0160f Mon Sep 17 00:00:00 2001 From: Tommy Bidne Date: Thu, 19 Jun 2025 11:52:06 +1200 Subject: [PATCH 6/6] Add --cabal-global-options --- clc-stackage.cabal | 3 ++- src/runner/CLC/Stackage/Runner/Args.hs | 26 ++++++++++++++++++++++++-- src/runner/CLC/Stackage/Runner/Env.hs | 20 +++++++++++++------- src/utils/CLC/Stackage/Utils/Paths.hs | 18 ++++++++++++++++++ 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/clc-stackage.cabal b/clc-stackage.cabal index 5c72666..52662a7 100644 --- a/clc-stackage.cabal +++ b/clc-stackage.cabal @@ -45,7 +45,8 @@ library utils , deepseq >=1.4.6.0 && <1.6 , directory ^>=1.3.5.0 , file-io ^>=0.1.0.0 - , filepath >=1.4.100.0 && <1.6 + , filepath >=1.5.0.0 && <1.6 + , os-string ^>=2.0.0 , pretty-terminal ^>=0.1.0.0 , text >=1.2.3.2 && <2.2 , time >=1.9.3 && <1.15 diff --git a/src/runner/CLC/Stackage/Runner/Args.hs b/src/runner/CLC/Stackage/Runner/Args.hs index b8449ce..9652b04 100644 --- a/src/runner/CLC/Stackage/Runner/Args.hs +++ b/src/runner/CLC/Stackage/Runner/Args.hs @@ -45,6 +45,8 @@ data Args = MkArgs { -- | If given, batches packages together so we build more than one. -- Defaults to batching everything together in the same group. batch :: Maybe Int, + -- | Global options to pass to cabal e.g. --store-dir. + cabalGlobalOpts :: [String], -- | Options to pass to cabal e.g. --semaphore. cabalOpts :: [String], -- | Optional path to cabal executable. @@ -123,7 +125,7 @@ getArgs = OA.execParser parserInfoArgs ], mkExample [ "# Run with custom cabal", - "$ clc-stackage --cabal-path=path/to/cabal --cabal-options='--store-dir=path/to/store'" + "$ clc-stackage --cabal-path=path/to/cabal --cabal-global-options='--store-dir=path/to/store'" ], mkExample [ "# Run with custom snapshot", @@ -139,6 +141,7 @@ parseCliArgs :: Parser Args parseCliArgs = ( do batch <- parseBatch + cabalGlobalOpts <- parseCabalGlobalOpts cabalOpts <- parseCabalOpts cabalPath <- parseCabalPath colorLogs <- parseColorLogs @@ -153,6 +156,7 @@ parseCliArgs = pure $ MkArgs { batch, + cabalGlobalOpts, cabalOpts, cabalPath, colorLogs, @@ -186,6 +190,24 @@ parseBatch = ] ) +parseCabalGlobalOpts :: Parser [String] +parseCabalGlobalOpts = + OA.option + readOpts + ( mconcat + [ OA.long "cabal-global-options", + OA.metavar "ARGS...", + OA.value [], + mkHelp $ + mconcat + [ "Global arguments to pass to cabal e.g. '--store-dir=path/to/store'. ", + "These precede the build command." + ] + ] + ) + where + readOpts = Str.words <$> OA.str + parseCabalOpts :: Parser [String] parseCabalOpts = OA.option @@ -194,7 +216,7 @@ parseCabalOpts = [ OA.long "cabal-options", OA.metavar "ARGS...", OA.value [], - mkHelp "Quoted arguments to pass to cabal e.g. '--semaphore --verbose=1'" + mkHelp "Quoted arguments to pass to cabal e.g. '--semaphore --verbose=1'." ] ) where diff --git a/src/runner/CLC/Stackage/Runner/Env.hs b/src/runner/CLC/Stackage/Runner/Env.hs index eafdd81..2bcd26b 100644 --- a/src/runner/CLC/Stackage/Runner/Env.hs +++ b/src/runner/CLC/Stackage/Runner/Env.hs @@ -46,7 +46,7 @@ import CLC.Stackage.Utils.Logging qualified as Logging import CLC.Stackage.Utils.Package (Package (MkPackage, name, version)) import CLC.Stackage.Utils.Paths qualified as Paths import Control.Exception (throwIO) -import Control.Monad (unless) +import Control.Monad (join, unless) import Data.Bool (Bool (False, True), not) import Data.Foldable (Foldable (foldl')) import Data.IORef (newIORef, readIORef) @@ -61,7 +61,7 @@ import System.Directory.OsPath qualified as Dir import System.Exit (ExitCode (ExitSuccess)) import System.OsPath (osp) import System.OsPath qualified as OsP -import Prelude (IO, Monad ((>>=)), mconcat, pure, show, ($), (++), (.), (<$>), (<>)) +import Prelude (IO, Monad ((>>=)), mconcat, pure, show, ($), (.), (<$>), (<>)) -- | Args used for building all packages. data RunnerEnv = MkRunnerEnv @@ -103,21 +103,27 @@ setup hLoggerRaw modifyPackages = do -- Set up build args for cabal, filling in missing defaults let buildArgs = - "build" - : keepGoingArg - ++ cliArgs.cabalOpts + join + [ cliArgs.cabalGlobalOpts, + ["build"], + keepGoingArg, + cliArgs.cabalOpts + ] -- when packageFailFast is false, add keep-going so that we build as many -- packages in the group. keepGoingArg = ["--keep-going" | not cliArgs.packageFailFast] - let cabalPathRaw = fromMaybe [osp|cabal|] cliArgs.cabalPath + cabalPathRaw <- case cliArgs.cabalPath of + Nothing -> pure [osp|cabal|] + Just p -> Paths.canonicalizePath p + cabalPath <- Dir.findExecutable cabalPathRaw >>= \case -- TODO: It would be nice to avoid the decode here and keep everything -- in OsPath, though that is blocked until process support OsPath. Just p -> OsP.decodeUtf p - Nothing -> Ex.throwText "Cabal not found" + Nothing -> Ex.throwText $ "Cabal not found: " <> T.pack (show cabalPathRaw) successesRef <- newIORef Set.empty failuresRef <- newIORef Set.empty diff --git a/src/utils/CLC/Stackage/Utils/Paths.hs b/src/utils/CLC/Stackage/Utils/Paths.hs index 1623164..a8101b1 100644 --- a/src/utils/CLC/Stackage/Utils/Paths.hs +++ b/src/utils/CLC/Stackage/Utils/Paths.hs @@ -11,6 +11,7 @@ module CLC.Stackage.Utils.Paths generatedCabalProjectLocalPath, -- * Utils + canonicalizePath, OsPath.encodeUtf, decodeUtfLenient, unsafeDecodeUtf, @@ -20,8 +21,10 @@ where import GHC.IO.Encoding.Failure (CodingFailureMode (TransliterateCodingFailure)) import GHC.IO.Encoding.UTF16 qualified as UTF16 import GHC.IO.Encoding.UTF8 qualified as UTF8 +import System.Directory.OsPath qualified as Dir import System.OsPath (OsPath, osp, ()) import System.OsPath qualified as OsPath +import System.OsString qualified as OsStr -- | Leniently decodes OsPath to String. decodeUtfLenient :: OsPath -> String @@ -37,6 +40,21 @@ unsafeDecodeUtf p = case OsPath.decodeUtf p of Just fp -> fp Nothing -> error $ "Error decoding ospath: " <> show p +-- | Calls canonicalizePath, after manually expanding tilde (~) to the home +-- directory. The latter usually shouldn't be needed, as the shell normally +-- performs such expansions before the string makes it to the program. +-- But when it is part of an argument e.g. +-- +-- --cabal-path=~/... +-- +-- it is not expanded. +canonicalizePath :: OsPath -> IO OsPath +canonicalizePath p = case OsStr.stripPrefix [osp|~/|] p of + Nothing -> Dir.canonicalizePath p + Just rest -> do + home <- Dir.getHomeDirectory + Dir.canonicalizePath $ home rest + -- | Output directory. outputDir :: OsPath outputDir = [osp|output|]