diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6c929d4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.golden -text diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..48bc656 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,59 @@ +name: ci +on: + push: + branches: + - master + + pull_request: + branches: + - master + + workflow_dispatch: +jobs: + cabal: + strategy: + fail-fast: false + matrix: + os: + - "macos-latest" + - "ubuntu-latest" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: haskell-actions/setup@v2 + with: + ghc-version: "9.8.2" + - name: Configure + run: | + cabal configure --enable-tests --ghc-options -Werror + + - name: Build executable + run: cabal build clc-stackage + + - name: Unit Tests + id: unit + run: cabal test unit + + - name: Print unit failures + if: ${{ failure() && steps.unit.conclusion == 'failure' }} + run: | + cd test/unit/goldens + + for f in $(ls); do + echo "$f" + cat "$f" + done + + - name: Functional Tests + id: functional + run: cabal test functional + + - name: Print functional failures + if: ${{ failure() && steps.functional.conclusion == 'failure' }} + run: | + cd test/functional/goldens + + for f in $(ls); do + echo "$f" + cat "$f" + done diff --git a/.gitignore b/.gitignore index 82fdd79..e913ac9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -/dist-newstyle \ No newline at end of file +/bin +/dist-newstyle +/generated/cabal.project.local +/generated/dist-newstyle +/generated/generated.cabal +/output diff --git a/README.md b/README.md index cadd1fe..3824d38 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## How to? -This is a meta-package to facilitate impact assessment for [CLC proposals](/~https://github.com/haskell/core-libraries-committee). The package `clc-stackage.cabal` lists almost entire Stackage as `build-depends`, so that `cabal build` transitively compiles them all. +This is a meta-package to facilitate impact assessment for [CLC proposals](/~https://github.com/haskell/core-libraries-committee). An impact assessment is due when @@ -13,21 +13,85 @@ An impact assessment is due when The procedure is as follows: 1. Rebase changes, mandated by your proposal, atop of `ghc-9.8` branch. + 2. Compile a patched GHC, say, `~/ghc/_build/stage1/bin/ghc`. -3. `git clone /~https://github.com/Bodigrim/clc-stackage`, then `cd clc-stackage`. -4. Run `cabal build -w ~/ghc/_build/stage1/bin/ghc --keep-going` and wait for a long time. - * On a recent Macbook Air it takes around 12 hours, YMMV. - * You can interrupt `cabal` at any time and rerun again later. - * Consider setting `--jobs` to retain free CPU cores for other tasks. - * Full build requires roughly 7 Gb of free disk space. -5. If any packages fail to compile: - * copy them locally using `cabal unpack`, - * patch to confirm with your proposal, - * link them from `packages` section of `cabal.project`, - * return to Step 4. -6. When everything finally builds, get back to CLC with a list of packages affected and patches required. + +3. `git clone /~https://github.com/haskell/clc-stackage`, then `cd clc-stackage`. + +4. Build the exe: `cabal install clc-stackage --installdir=./bin`. + + > :warning: **Warning:** Use a normal downloaded GHC for this step, **not** your custom built one. Why? Using the custom GHC can force a build of many dependencies you'd otherwise get for free e.g. `vector`. + +5. Uncomment and modify the `with-compiler` line in [generated/cabal.project](generated/cabal.project) e.g. + + ``` + with-compiler: /home/ghc/_build/stage1/bin/ghc + ``` + +6. Run `./bin/clc-stackage` and wait for a long time. See [below](#the-clc-stackage-exe) for more details. + + * On a recent Macbook Air it takes around 12 hours, YMMV. + * You can interrupt `cabal` at any time and rerun again later. + * Consider setting `--jobs` to retain free CPU cores for other tasks. + * Full build requires roughly 7 Gb of free disk space. + + To get an idea of the current progress, we can run the following commands + on the log file: + + ```sh + # prints completed / total packages in this group + $ grep -Eo 'Completed|^ -' output/logs/current-build/stdout.log | sort -r | uniq -c | awk '{print $1}' + 110 + 182 + + # combine with watch + $ watch -n 10 "grep -Eo 'Completed|^ -' output/logs/current-build/stdout.log | sort -r | uniq -c | awk '{print \$1}'" + ``` + +7. If any packages fail to compile: + + * copy them locally using `cabal unpack`, + * patch to confirm with your proposal, + * link them from `packages` section of `cabal.project`, + * return to Step 6. + +8. When everything finally builds, get back to CLC with a list of packages affected and patches required. + +### 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. + +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): + +![demo](example_output.png) + +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: + +``` +$ ./bin/clc-stackage --batch 100 +``` + +This will split the entire downloaded package set into groups of size 100. Each time a group finishes (success or failure), stdout/err will be updated, and then the next group will start. If the group failed to build and we have `--write-logs save-failures` (the default), then the logs and error output will be in `./output/logs//`, where `` is the name of the first package in the group. + +See `./bin/clc-stackage --help` for more info. + +#### 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. + +In general, the smaller `N` is, the worse the performance. There are several reasons for this: + +- The smaller `N` is, the more `cabal build` processes, which adds overhead. +- More packages increase the chances for concurrency gains. + +Thus for optimal performance, you want to take the largest group possible, with the upper limit being no `--batch` argument at all, as that puts all packages into the same group. + +> [!TIP] +> +> Additionally, the `./output/cache.json` file can be manipulated directly. For example, if you want to try building only `foo`, ensure `foo` is the only entry in the json file's `untested` field. ## Getting dependencies via `nix` + For Linux based systems, there's a provided `flake.nix` and `shell.nix` to get a nix shell with an approximation of the required dependencies (cabal itself, C libs) to build `clc-stackage`. @@ -35,10 +99,10 @@ Note that it is not actively maintained, so it may require some tweaking to get ## Misc -* Your custom GHC will need to be on the PATH to build the `stack` library i.e. +* Your custom GHC will need to be on the PATH to build the `stack` library e.g. ``` - export PATH=/path/to/custom/ghc/stage1/bin/:$PATH + export PATH=/home/ghc/_build/stage1/bin/:$PATH ``` - Nix users can uncomment (and modify) this line in the `flake.nix`. \ No newline at end of file + Nix users can uncomment (and modify) this line in the `flake.nix`. diff --git a/app/Main.hs b/app/Main.hs new file mode 100644 index 0000000..d4f375b --- /dev/null +++ b/app/Main.hs @@ -0,0 +1,27 @@ +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 + + case mWidth of + Just w -> Runner.run $ mkLogger w + Nothing -> do + let hLogger = mkLogger 80 + Logging.putTimeInfoStr hLogger False "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/cabal.project b/cabal.project index 82c3cd5..a288f6f 100644 --- a/cabal.project +++ b/cabal.project @@ -1,44 +1,24 @@ -index-state: 2024-03-27T00:32:46Z +index-state: 2024-10-11T23:26:13Z packages: . -constraints: - al < 0, - alsa-pcm < 0, - alsa-seq < 0, - ALUT < 0, - btrfs < 0, - fft < 0, - flac < 0, - glpk-headers < 0, - hmatrix-gsl < 0, - hopenssl < 0, - hpqtypes < 0, - hsdns < 0, - hsndfile < 0, - HsOpenSSL < 0, - hw-kafka-client < 0, - jack < 0, - lame < 0, - lapack-ffi < 0, - lmdb < 0, - magic < 0, - mysql < 0, - nfc < 0, - pcre-light < 0, - postgresql-libpq < 0, - primecount < 0, - pthread < 0, - pulse-simple < 0, - rdtsc < 0, - regex-pcre < 0, - re2 < 0, - text-icu < 0, +program-options + ghc-options: + -Wall -Wcompat + -Widentities + -Wincomplete-record-updates + -Wincomplete-uni-patterns + -Wmissing-deriving-strategies + -Wmissing-export-lists + -Wmissing-exported-signatures + -Wmissing-home-modules + -Wmissing-import-lists + -Wpartial-fields + -Wprepositive-qualified-module + -Wredundant-constraints + -Wunused-binds + -Wunused-packages + -Wunused-type-patterns + -Wno-unticked-promoted-constructors -allow-newer: - aura:bytestring, - aura:time - -constraints: hlint +ghc-lib -constraints: ghc-lib-parser-ex -auto -constraints: stylish-haskell +ghc-lib +optimization: 2 diff --git a/clc-stackage.cabal b/clc-stackage.cabal index fd4ef0e..7c808f3 100644 --- a/clc-stackage.cabal +++ b/clc-stackage.cabal @@ -1,2673 +1,173 @@ -cabal-version: 2.4 -name: clc-stackage -version: 0.1.0.0 -author: Andrew Lelechenko -maintainer: andrew.lelechenko@gmail.com +cabal-version: 2.4 +name: clc-stackage +version: 0.1.0.0 +author: Andrew Lelechenko +maintainer: andrew.lelechenko@gmail.com --- Build with cabal build -w ghc-9.8.2 --keep-going --- --- Derived from https://www.stackage.org/nightly-2024-03-26/cabal.config --- minus packages which do not contain libraries, --- minus packages with system dependencies +common common-lang + default-extensions: + ApplicativeDo + DeriveAnyClass + DerivingVia + DuplicateRecordFields + ExplicitNamespaces + ImportQualifiedPost + LambdaCase + NamedFieldPuns + OverloadedRecordDot + OverloadedStrings + StrictData -library - exposed-modules: Lib - hs-source-dirs: src - default-language: Haskell2010 - if os(darwin) - build-depends: - hfsevents ==0.1.6, - build-depends: - abstract-deque ==0.3, - abstract-deque-tests ==0.3, - abstract-par ==0.3.3, - AC-Angle ==1.0, - acc ==0.2.0.3, - ace ==0.6, - acid-state ==0.16.1.3, - action-permutations ==0.0.0.1, - active ==0.2.1, - ad ==4.5.5, - ad-delcont ==0.5.0.0, - adjunctions ==4.4.2, - adler32 ==0.1.2.0, - advent-of-code-api ==0.2.9.1, - aern2-mp ==0.2.15.1, - aern2-real ==0.2.15, - aeson ==2.2.1.0, - aeson-attoparsec ==0.0.0, - aeson-casing ==0.2.0.0, - aeson-combinators ==0.1.2.1, - aeson-diff ==1.1.0.13, - aeson-extra ==0.5.1.3, - aeson-generic-compat ==0.0.2.0, - aeson-pretty ==0.8.10, - aeson-qq ==0.8.4, - aeson-schemas ==1.4.2.1, - aeson-typescript ==0.6.3.0, - aeson-unqualified-ast ==1.0.0.3, - aeson-value-parser ==0.19.7.2, - aeson-warning-parser ==0.1.1, - aeson-yak ==0.1.1.3, - aeson-yaml ==1.1.0.1, - Agda ==2.6.4.3, - agreeing ==0.2.2.0, - alarmclock ==0.7.0.6, - alex-meta ==0.3.0.13, - algebra ==4.3.1, - algebraic-graphs ==0.7, - almost-fix ==0.0.2, - alternative-vector ==0.0.0, - amazonka-apigatewaymanagementapi ==2.0, - amazonka-appconfigdata ==2.0, - amazonka-backupstorage ==2.0, - amazonka-cloudhsm ==2.0, - amazonka-controltower ==2.0, - amazonka-core ==2.0, - amazonka-ec2-instance-connect ==2.0, - amazonka-finspace ==2.0, - amazonka-forecastquery ==2.0, - amazonka-importexport ==2.0, - amazonka-iot-dataplane ==2.0, - amazonka-iotfleethub ==2.0, - amazonka-iotthingsgraph ==2.0, - amazonka-kinesis-video-signaling ==2.0, - amazonka-kinesis-video-webrtc-storage ==2.0, - amazonka-marketplace-analytics ==2.0, - amazonka-marketplace-metering ==2.0, - amazonka-migrationhub-config ==2.0, - amazonka-personalize-runtime ==2.0, - amazonka-rbin ==2.0, - amazonka-s3outposts ==2.0, - amazonka-sagemaker-a2i-runtime ==2.0, - amazonka-sagemaker-metrics ==2.0, - amazonka-sso-oidc ==2.0, - amazonka-test ==2.0, - amazonka-worklink ==2.0, - amazonka-workmailmessageflow ==2.0, - amqp ==0.22.2, - annotated-exception ==0.2.0.5, - annotated-wl-pprint ==0.7.0, - ansi-terminal ==1.0.2, - ansi-terminal-game ==1.9.3.0, - ansi-terminal-types ==0.11.5, - ansi-wl-pprint ==1.0.2, - ANum ==0.2.0.2, - aos-signature ==0.1.1, - apecs ==0.9.6, - apecs-gloss ==0.2.4, - apecs-physics ==0.4.6, - api-field-json-th ==0.1.0.2, - ap-normalize ==0.1.0.1, - appar ==0.1.8, - appendful ==0.1.0.0, - appendful-persistent ==0.1.0.1, - appendmap ==0.1.5, - apply-refact ==0.14.0.0, - apportionment ==0.0.0.4, - approximate ==0.3.5, - approximate-equality ==1.1.0.2, - arbor-lru-cache ==0.1.1.1, - arithmoi ==0.13.0.0, - array ==0.5.6.0, - array-memoize ==0.6.0, - arrow-extras ==0.1.0.1, - arrows ==0.4.4.2, - ascii-char ==1.0.1.0, - ascii-group ==1.0.0.17, - ascii-progress ==0.3.3.0, - asn1-encoding ==0.9.6, - asn1-parse ==0.9.5, - asn1-types ==0.3.4, - assert-failure ==0.1.3.0, - assoc ==1.1, - astro ==0.4.3.0, - async ==2.2.5, - async-extra ==0.2.0.0, - async-pool ==0.9.2, - async-refresh ==0.3.0.0, - async-refresh-tokens ==0.4.0.0, - atom-basic ==0.2.5, - atomic-counter ==0.1.2.1, - atomic-primops ==0.8.5, - atomic-write ==0.2.0.7, - attoparsec ==0.14.4, - attoparsec-aeson ==2.2.0.1, - attoparsec-base64 ==0.0.0, - attoparsec-binary ==0.2, - attoparsec-data ==1.0.5.4, - attoparsec-expr ==0.1.1.2, - attoparsec-framer ==0.1.0.3, - attoparsec-iso8601 ==1.1.0.1, - attoparsec-path ==0.0.0.1, - attoparsec-time ==1.0.3.1, - attoparsec-uri ==0.0.9, - audacity ==0.0.2.2, - authenticate ==1.3.5.2, - authenticate-oauth ==1.7, - autodocodec ==0.2.2.0, - autodocodec-openapi3 ==0.2.1.1, - autodocodec-schema ==0.1.0.3, - autodocodec-yaml ==0.2.0.3, - autoexporter ==2.0.0.9, - auto-update ==0.1.6, - aws ==0.24.2, - aws-lambda-haskell-runtime ==4.3.2, - aws-lambda-haskell-runtime-wai ==2.0.2, - aws-sns-verify ==0.0.0.3, - aws-xray-client ==0.1.0.2, - aws-xray-client-persistent ==0.1.0.5, - aws-xray-client-wai ==0.1.0.2, - backprop ==0.2.6.5, - backtracking ==0.1.0, - bank-holiday-germany ==1.2.0.0, - bank-holidays-england ==0.2.0.9, - barbies ==2.1.1.0, - base ==4.19.1.0, - base16 ==1.0, - base16-bytestring ==1.0.2.0, - base32string ==0.9.1, - base58-bytestring ==0.1.0, - base58string ==0.10.0, - base64 ==1.0, - base64-bytestring ==1.2.1.0, - base64-bytestring-type ==1.0.1, - base64-string ==0.2, - base-compat ==0.13.1, - base-compat-batteries ==0.13.1, - basement ==0.0.16, - base-orphans ==0.9.1, - base-prelude ==1.6.1.1, - base-unicode-symbols ==0.2.4.2, - basic-prelude ==0.7.0, - battleship-combinatorics ==0.0.1, - bazel-runfiles ==0.12, - bbdb ==0.8, - bcrypt ==0.0.11, - bech32 ==1.1.5, - bech32-th ==1.1.5, - benchpress ==0.2.2.23, - bench-show ==0.3.2, - bencode ==0.6.1.1, - bencoding ==0.4.5.5, - benri-hspec ==0.1.0.2, - between ==0.11.0.0, - bibtex ==0.1.0.7, - bifunctor-classes-compat ==0.1, - bifunctors ==5.6.2, - bimap ==0.5.0, - bimaps ==0.1.0.2, - bin ==0.1.3, - binance-exports ==0.1.2.0, - binary ==0.8.9.1, - binary-conduit ==1.3.1, - binaryen ==0.0.6.0, - binary-generic-combinators ==0.4.4.0, - binary-ieee754 ==0.1.0.0, - binary-instances ==1.0.4, - binary-list ==1.1.1.2, - binary-orphans ==1.0.4.1, - binary-parser ==0.5.7.6, - binary-search ==2.0.0, - binary-shared ==0.8.3, - binary-tagged ==0.3.1, - bindings-DSL ==1.0.25, - bindings-uname ==0.1, - BiobaseNewick ==0.0.0.2, - bitarray ==0.0.1.1, - bits ==0.6, - bitset-word8 ==0.1.1.2, - bits-extra ==0.0.2.3, - bitvec ==1.1.5.0, - bitwise-enum ==1.0.1.2, - Blammo ==1.1.2.1, - blank-canvas ==0.7.4, - blas-hs ==0.1.1.0, - blaze-bootstrap ==0.1.0.1, - blaze-builder ==0.4.2.3, - blaze-html ==0.9.2.0, - blaze-markup ==0.8.3.0, - blaze-svg ==0.3.7, - blaze-textual ==0.2.3.1, - bloodhound ==0.21.0.0, - bloomfilter ==2.0.1.2, - bm ==0.2.0.0, - bmp ==1.2.6.3, - bnb-staking-csvs ==0.2.2.0, - BNFC ==2.9.5, - BNFC-meta ==0.6.1, - bodhi ==0.1.0, - boltzmann-samplers ==0.1.1.0, - Boolean ==0.2.4, - boolsimplifier ==0.1.8, - bordacount ==0.1.0.0, - boring ==0.2.1, - bound ==2.0.7, - BoundedChan ==1.0.3.0, - bounded-qsem ==0.1.0.2, - bounded-queue ==1.0.0, - boundingboxes ==0.2.3, - box ==0.9.3.1, - boxes ==0.1.5, - breakpoint ==0.1.3.1, - brick ==2.3.1, - brotli ==0.0.0.1, - brotli-streams ==0.0.0.0, - bsb-http-chunked ==0.0.0.4, - bson ==0.4.0.1, - bson-lens ==0.1.1, - bugsnag-hs ==0.2.0.12, - bugzilla-redhat ==1.0.1.1, - burrito ==2.0.1.8, - bv ==0.5, - bv-little ==1.3.2, - byteable ==0.1.1, - bytebuild ==0.3.16.2, - byte-count-reader ==0.10.1.11, - bytedump ==1.0, - byte-order ==0.1.3.1, - byteorder ==1.0.4, - bytes ==0.17.3, - byteset ==0.1.1.1, - byteslice ==0.2.13.2, - bytesmith ==0.3.11.1, - bytestring ==0.12.1.0, - bytestring-builder ==0.10.8.2.0, - bytestring-conversion ==0.3.2, - bytestring-lexing ==0.5.0.11, - bytestring-strict-builder ==0.4.5.7, - bytestring-to-vector ==0.3.0.1, - bytestring-tree-builder ==0.2.7.12, - bz2 ==1.0.1.1, - bzip2-clib ==1.0.8, - bzlib ==0.5.2.0, - bzlib-conduit ==0.3.0.3, - c14n ==0.1.0.3, - Cabal ==3.10.2.0, - cabal2spec ==2.7.0, - cabal-appimage ==0.4.0.2, - cabal-debian ==5.2.3, - cabal-doctest ==1.0.9, - cabal-file ==0.1.1, - cabal-gild ==1.3.0.1, - Cabal-syntax ==3.10.2.0, - cache ==0.1.3.0, - cached-json-file ==0.1.1, - cacophony ==0.10.1, - cairo ==0.13.10.0, - cairo-image ==0.1.0.3, - call-alloy ==0.4.1.1, - calligraphy ==0.1.6, - call-plantuml ==0.0.1.3, - call-stack ==0.4.0, - can-i-haz ==0.3.1.1, - capability ==0.5.0.1, - ca-province-codes ==1.0.0.0, - cardano-coin-selection ==1.0.1, - carray ==0.1.6.8, - casa-client ==0.0.2, - casa-types ==0.0.2, - cased ==0.1.0.0, - case-insensitive ==1.2.1.0, - cases ==0.1.4.3, - casing ==0.1.4.1, - cassava ==0.5.3.0, - cassava-conduit ==0.6.6, - cassava-megaparsec ==2.1.1, - cast ==0.1.0.2, - cayley-client ==0.4.19.3, - cborg ==0.2.10.0, - cborg-json ==0.2.6.0, - cdar-mBound ==0.1.0.4, - c-enum ==0.1.1.3, - cereal ==0.5.8.3, - cereal-conduit ==0.8.0, - cereal-text ==0.1.0.2, - cereal-unordered-containers ==0.1.0.1, - cereal-vector ==0.2.0.1, - cfenv ==0.1.0.0, - chan ==0.0.4.1, - ChannelT ==0.0.0.7, - character-cases ==0.1.0.6, - charset ==0.3.10, - Chart ==1.9.5, - Chart-cairo ==1.9.4.1, - Chart-diagrams ==1.9.5.1, - ChasingBottoms ==1.3.1.13, - check-email ==1.0.2, - checkers ==0.6.0, - checksum ==0.0.0.1, - chimera ==0.4.0.0, - choice ==0.2.3, - chronologique ==0.3.1.3, - chronos ==1.1.6.1, - chronos-bench ==0.2.0.2, - chunked-data ==0.3.1, - cipher-aes ==0.2.11, - cipher-camellia ==0.0.2, - cipher-rc4 ==0.1.4, - circle-packing ==0.1.0.6, - circular ==0.4.0.3, - citeproc ==0.8.1, - clash-ghc ==1.8.1, - clash-lib ==1.8.1, - clash-prelude ==1.8.1, - clash-prelude-hedgehog ==1.8.1, - classy-prelude ==1.5.0.3, - classy-prelude-conduit ==1.5.0, - classy-prelude-yesod ==1.5.0, - clay ==0.15.0, - clientsession ==0.9.2.0, - clock ==0.8.4, - closed ==0.2.0.2, - clumpiness ==0.17.0.2, - ClustalParser ==1.3.0, - cmark ==0.6.1, - cmark-gfm ==0.2.6, - cmark-lucid ==0.1.0.0, - cmdargs ==0.10.22, - codec-beam ==0.2.0, - code-conjure ==0.5.14, - code-page ==0.2.1, - cointracking-imports ==0.1.0.2, - collect-errors ==0.1.5.0, - co-log ==0.6.1.0, - co-log-core ==0.3.2.1, - co-log-polysemy ==0.0.1.4, - Color ==0.3.3, - colorful-monoids ==0.2.1.3, - colorize-haskell ==1.0.1, - colour ==2.3.6, - colourista ==0.1.0.2, - columnar ==1.0.0.0, - combinatorial ==0.1.1, - comfort-array ==0.5.4.2, - comfort-array-shape ==0.0, - comfort-graph ==0.0.4, - commonmark ==0.2.6, - commonmark-extensions ==0.2.5.4, - commonmark-pandoc ==0.2.2.1, - commutative ==0.0.2, - commutative-semigroups ==0.1.0.2, - comonad ==5.0.8, - compact ==0.2.0.0, - compactmap ==0.1.4.3, - companion ==0.1.0, - compdata ==0.13.1, - compensated ==0.8.3, - compiler-warnings ==0.1.0, - componentm ==0.0.0.2, - componentm-devel ==0.0.0.2, - composable-associations ==0.1.0.0, - composition ==1.0.2.2, - composition-extra ==2.1.0, - composition-prelude ==3.0.0.2, - concise ==0.1.0.1, - concurrency ==1.11.0.3, - concurrent-extra ==0.7.0.12, - concurrent-output ==1.10.20, - concurrent-split ==0.0.1.1, - concurrent-supply ==0.1.8, - cond ==0.5.1, - conduino ==0.2.4.0, - conduit ==1.3.5, - conduit-algorithms ==0.0.14.0, - conduit-combinators ==1.3.0, - conduit-concurrent-map ==0.1.3, - conduit-extra ==1.3.6, - conduit-parse ==0.2.1.1, - conduit-zstd ==0.0.2.0, - conferer ==1.1.0.0, - conferer-aeson ==1.1.0.2, - config-ini ==0.2.7.0, - configuration-tools ==0.7.0, - configurator ==0.3.0.0, - configurator-export ==0.1.0.1, - configurator-pg ==0.2.10, - constraints ==0.14, - constraints-extras ==0.4.0.0, - constraint-tuples ==0.1.2, - construct ==0.3.1.2, - containers ==0.6.8, - context ==0.2.1.0, - context-http-client ==0.2.0.2, - context-resource ==0.2.0.2, - context-wai-middleware ==0.2.0.2, - contiguous ==0.6.4.2, - contravariant ==1.5.5, - contravariant-extras ==0.3.5.4, - control-bool ==0.2.1, - control-dsl ==0.2.1.3, - control-monad-free ==0.6.2, - control-monad-omega ==0.3.2, - convertible ==1.1.1.1, - cookie ==0.4.6, - copr-api ==0.2.0, - core-data ==0.3.9.1, - core-program ==0.7.0.0, - core-text ==0.3.8.1, - countable ==1.2, - covariance ==0.2.0.1, - cpphs ==1.20.9.1, - cpu ==0.1.2, - cql ==4.0.4, - credential-store ==0.1.2, - criterion ==1.6.3.0, - criterion-measurement ==0.2.1.0, - cron ==0.7.1, - crypto-api ==0.13.3, - crypto-api-tests ==0.3, - crypto-cipher-tests ==0.0.11, - crypto-cipher-types ==0.0.9, - cryptocompare ==0.1.2, - cryptohash ==0.11.9, - cryptohash-cryptoapi ==0.1.4, - cryptohash-md5 ==0.11.101.0, - cryptohash-sha1 ==0.11.101.0, - cryptohash-sha256 ==0.11.102.1, - cryptohash-sha512 ==0.11.102.0, - crypton ==0.34, - crypton-conduit ==0.2.3, - crypton-connection ==0.3.2, - cryptonite ==0.30, - cryptonite-conduit ==0.2.2, - crypton-x509 ==1.7.6, - crypton-x509-store ==1.6.9, - crypton-x509-system ==1.6.7, - crypton-x509-validation ==1.6.12, - crypto-pubkey-types ==0.4.3, - crypto-random-api ==0.2.0, - cryptostore ==0.3.0.1, - crypto-token ==0.1.1, - crypt-sha512 ==0, - csp ==1.4.0, - css-text ==0.1.3.0, - c-struct ==0.1.3.0, - csv ==0.1.2, - ctrie ==0.2, - cubicspline ==0.1.2, - cue-sheet ==2.0.2, - curl ==1.3.8, - currency ==0.2.0.0, - currycarbon ==0.3.0.1, - cursor ==0.3.2.0, - cursor-brick ==0.1.0.1, - cursor-fuzzy-time ==0.1.0.0, - cursor-gen ==0.4.0.0, - cyclotomic ==1.1.2, - data-accessor ==0.2.3.1, - data-accessor-mtl ==0.2.0.5, - data-accessor-transformers ==0.2.1.8, - data-array-byte ==0.1.0.1, - data-binary-ieee754 ==0.4.4, - data-bword ==0.1.0.2, - data-checked ==0.3, - data-clist ==0.2, - data-default ==0.7.1.1, - data-default-class ==0.1.2.0, - data-default-instances-base ==0.1.0.1, - data-default-instances-bytestring ==0.0.1, - data-default-instances-case-insensitive ==0.0.1, - data-default-instances-containers ==0.0.1, - data-default-instances-dlist ==0.0.1, - data-default-instances-old-locale ==0.0.1, - data-default-instances-unordered-containers ==0.0.1, - data-default-instances-vector ==0.0.1, - data-diverse ==4.7.1.0, - data-dword ==0.3.2.1, - data-endian ==0.1.1, - data-fix ==0.3.2, - data-functor-logistic ==0.0, - data-has ==0.4.0.0, - data-hash ==0.2.0.1, - data-interval ==2.1.2, - data-inttrie ==0.1.4, - data-lens-light ==0.1.2.4, - data-memocombinators ==0.5.1, - data-msgpack ==0.0.13, - data-msgpack-types ==0.0.3, - data-or ==1.0.0.7, - data-ordlist ==0.4.7.0, - data-ref ==0.1, - data-reify ==0.6.3, - data-serializer ==0.3.5, - data-sketches ==0.3.1.0, - data-sketches-core ==0.1.0.0, - data-textual ==0.3.0.3, - dataurl ==0.1.0.0, - DAV ==1.3.4, - DBFunctor ==0.1.2.1, - dbus ==1.3.2, - dbus-hslogger ==0.1.0.1, - debian ==4.0.5, - debian-build ==0.10.2.1, - debug-trace-var ==0.2.0, - dec ==0.0.5, - Decimal ==0.5.2, - declarative ==0.5.4, - deepseq ==1.5.0.0, - deepseq-generics ==0.2.0.0, - deferred-folds ==0.9.18.6, - dejafu ==2.4.0.5, - dense-linear-algebra ==0.1.0.0, - dependent-map ==0.4.0.0, - dependent-sum ==0.7.2.0, - depq ==0.4.2, - deque ==0.4.4.1, - deriveJsonNoPrefix ==0.1.0.1, - derive-storable ==0.3.1.0, - derive-topdown ==0.0.3.0, - deriving-aeson ==0.2.9, - deriving-compat ==0.6.6, - deriving-trans ==0.9.1.0, - detour-via-sci ==1.0.0, - df1 ==0.4.2, - di ==1.3, - diagrams-builder ==0.8.0.6, - diagrams-cairo ==1.4.2.1, - diagrams-canvas ==1.4.1.2, - diagrams-core ==1.5.1.1, - diagrams-lib ==1.4.6.1, - diagrams-postscript ==1.5.1.1, - diagrams-rasterific ==1.4.2.3, - diagrams-solve ==0.1.3, - dice ==0.1.1, - di-core ==1.0.4, - dictionary-sharing ==0.1.0.0, - di-df1 ==1.2.1, - Diff ==0.5, - diff-loc ==0.1.0.0, - digest ==0.0.2.1, - digits ==0.3.1, - di-handle ==1.0.1, - dimensional ==1.5, - di-monad ==1.3.5, - directory ==1.3.8.1, - directory-ospath-streaming ==0.1.0.2, - directory-tree ==0.12.1, - direct-sqlite ==2.3.29, - dirichlet ==0.1.0.7, - discover-instances ==0.1.0.0, - discrimination ==0.5, - disk-free-space ==0.1.0.1, - distributed-closure ==0.5.0.0, - distribution-nixpkgs ==1.7.0.1, - distribution-opensuse ==1.1.4, - distributive ==0.6.2.1, - djinn-lib ==0.0.1.4, - djot ==0.1.1.3, - dlist ==1.0, - dlist-instances ==0.1.1.1, - dlist-nonempty ==0.1.3, - dns ==4.2.0, - dockerfile ==0.2.0, - doclayout ==0.4.0.1, - docopt ==0.7.0.8, - doctemplates ==0.11, - doctest ==0.22.2, - doctest-discover ==0.2.0.0, - doctest-driver-gen ==0.3.0.8, - doctest-exitcode-stdio ==0.0, - doctest-lib ==0.1.1, - doctest-parallel ==0.3.1, - doldol ==0.4.1.2, - do-list ==1.0.1, - domain ==0.1.1.5, - domain-aeson ==0.1.1.2, - domain-cereal ==0.1.0.1, - domain-core ==0.1.0.4, - domain-optics ==0.1.0.4, - do-notation ==0.1.0.2, - dot ==0.3, - dotenv ==0.12.0.0, - dotgen ==0.4.3, - dotnet-timespan ==0.0.1.0, - double-conversion ==2.0.5.0, - download ==0.3.2.7, - download-curl ==0.1.4, - DPutils ==0.1.1.0, - drawille ==0.1.3.0, - drifter ==0.3.0, - drifter-sqlite ==0.1.0.0, - dsp ==0.2.5.2, - dual-tree ==0.2.3.1, - dublincore-xml-conduit ==0.1.0.3, - duration ==0.2.0.0, - dvorak ==0.1.0.0, - dynamic-state ==0.3.1, - dyre ==0.9.2, - eap ==0.9.0.2, - Earley ==0.13.0.1, - easy-file ==0.2.5, - easy-logger ==0.1.0.7, - echo ==0.1.4, - ecstasy ==0.2.1.0, - ed25519 ==0.0.5.0, - edit-distance ==0.2.2.1, - edit-distance-vector ==1.0.0.4, - editor-open ==0.6.0.0, - effectful ==2.3.0.0, - effectful-core ==2.3.0.1, - effectful-plugin ==1.1.0.2, - effectful-th ==1.0.0.1, - egison-pattern-src ==0.2.1.2, - either ==5.0.2, - either-unwrap ==1.1, - ekg-core ==0.1.1.7, - elerea ==2.9.0, - elf ==0.31, - eliminators ==0.9.4, - elm2nix ==0.3.1, - elm-core-sources ==1.0.0, - elm-export ==0.6.0.1, - elynx-markov ==0.7.2.2, - elynx-nexus ==0.7.2.2, - elynx-seq ==0.7.2.2, - elynx-tools ==0.7.2.2, - elynx-tree ==0.7.2.2, - emacs-module ==0.2.1, - email-validate ==2.3.2.20, - emojis ==0.1.3, - enclosed-exceptions ==1.0.3, - ENIG ==0.0.1.0, - entropy ==0.4.1.10, - enummapset ==0.7.2.0, - enumset ==0.1, - enum-subset-generate ==0.1.0.3, - enum-text ==0.5.3.0, - envelope ==0.2.2.0, - envparse ==0.5.0, - epub-metadata ==5.4, - eq ==4.3, - equational-reasoning ==0.7.0.2, - equivalence ==0.4.1, - erf ==2.0.0.0, - errata ==0.4.0.2, - error ==1.0.0.0, - errorcall-eq-instance ==0.3.0, - error-or ==0.3.0, - error-or-utils ==0.2.0, - errors ==2.3.0, - errors-ext ==0.4.2, - ersatz ==0.5, - esqueleto ==3.5.11.2, - event-list ==0.1.3, - every ==0.0.1, - evm-opcodes ==0.1.2, - exact-combinatorics ==0.2.0.11, - exact-pi ==0.5.0.2, - exception-hierarchy ==0.1.0.11, - exception-mtl ==0.4.0.2, - exceptions ==0.10.7, - exception-transformers ==0.4.0.12, - executable-hash ==0.2.0.4, - executable-path ==0.0.3.1, - exinst ==0.9, - exit-codes ==1.0.0, - exomizer ==1.0.0, - exon ==1.6.1.1, - expiring-cache-map ==0.0.6.1, - explainable-predicates ==0.1.2.4, - explicit-exception ==0.2, - exp-pairs ==0.2.1.0, - express ==1.0.16, - extended-reals ==0.2.4.0, - extensible ==0.9, - extensible-effects ==5.0.0.1, - extensible-exceptions ==0.1.1.4, - extra ==1.7.14, - extractable-singleton ==0.0.1, - extra-data-yj ==0.1.0.0, - extrapolate ==0.4.6, - fail ==4.9.0.0, - FailT ==0.1.2.0, - fakedata-parser ==0.1.0.0, - fakefs ==0.3.0.2, - fakepull ==0.3.0.2, - faktory ==1.1.2.6, - fast-digits ==0.3.2.0, - fast-logger ==3.2.2, - fast-math ==1.0.2, - fast-myers-diff ==0.0.0, - fcf-family ==0.2.0.1, - fdo-notify ==0.3.1, - feature-flags ==0.1.0.1, - fedora-dists ==2.1.1, - feed ==1.3.2.1, - FenwickTree ==0.1.2.1, - fgl ==5.8.2.0, - fgl-arbitrary ==0.2.0.6, - fields-json ==0.4.0.0, - filecache ==0.5.0, - file-embed ==0.0.16.0, - file-embed-lzma ==0.0.1, - file-io ==0.1.1, - filelock ==0.1.1.7, - filemanip ==0.3.6.3, - filepath ==1.4.200.1, - file-path-th ==0.1.0.0, - filepattern ==0.1.3, - fileplow ==0.1.0.0, - filter-logger ==0.6.0.0, - filtrable ==0.1.6.0, - fin ==0.3, - FindBin ==0.0.5, - fingertree ==0.1.5.0, - finite-typelits ==0.1.6.0, - first-class-families ==0.8.0.1, - fits-parse ==0.3.6, - fitspec ==0.4.10, - fixed ==0.3, - fixed-length ==0.2.3.1, - fixed-vector ==1.2.3.0, - fixed-vector-hetero ==0.6.1.1, - flags-applicative ==0.1.0.3, - flat ==0.6, - flatparse ==0.5.1.0, - flay ==0.4, - flexible-defaults ==0.0.3, - FloatingHex ==0.5, - floatshow ==0.2.4, - flow ==2.0.0.4, - flush-queue ==1.0.0, - fmlist ==0.9.4, - fmt ==0.6.3.0, - fn ==0.3.0.2, - focus ==1.0.3.2, - focuslist ==0.1.1.0, - foldable1-classes-compat ==0.1, - fold-debounce ==0.2.0.11, - foldl ==1.4.16, - folds ==0.7.8, - FontyFruity ==0.5.3.5, - force-layout ==0.4.0.6, - foreign-store ==0.2.1, - ForestStructures ==0.0.1.1, - forkable-monad ==0.2.0.3, - forma ==1.2.0, - formatn ==0.3.0.1, - format-numbers ==0.1.0.1, - formatting ==7.2.0, - foundation ==0.0.30, - Frames ==0.7.4.2, - free ==5.2, - free-categories ==0.2.0.2, - freer-par-monad ==0.1.0.0, - freetype2 ==0.2.0, - free-vl ==0.1.4, - friday ==0.2.3.2, - friday-juicypixels ==0.1.2.4, - friendly-time ==0.4.1, - frisby ==0.2.5, - from-sum ==0.2.3.0, - frontmatter ==0.1.0.2, - funcmp ==1.9, - function-builder ==0.3.0.1, - functor-classes-compat ==2.0.0.2, - functor-combinators ==0.4.1.3, - fusion-plugin ==0.2.7, - fusion-plugin-types ==0.1.0, - fuzzcheck ==0.1.1, - fuzzy ==0.1.1.0, - fuzzy-dates ==0.1.1.2, - fuzzyset ==0.3.2, - fuzzy-time ==0.3.0.0, - gdp ==0.0.3.0, - gemini-exports ==0.1.0.1, - general-games ==1.1.1, - generically ==0.1.1, - generic-arbitrary ==1.0.1, - generic-constraints ==1.1.1.1, - generic-data ==1.1.0.0, - generic-data-surgery ==0.3.0.0, - generic-deriving ==1.14.5, - generic-functor ==1.1.0.0, - generic-lens ==2.2.2.0, - generic-lens-core ==2.2.1.0, - generic-monoid ==0.1.0.1, - generic-optics ==2.2.1.0, - GenericPretty ==1.2.2, - generic-random ==1.5.0.1, - generics-eot ==0.4.0.1, - generics-sop ==0.5.1.4, - generics-sop-lens ==0.2.0.1, - geniplate-mirror ==0.7.10, - genvalidity ==1.1.0.0, - genvalidity-aeson ==1.0.0.1, - genvalidity-appendful ==0.1.0.0, - genvalidity-bytestring ==1.0.0.1, - genvalidity-case-insensitive ==0.0.0.1, - genvalidity-containers ==1.0.0.1, - genvalidity-criterion ==1.1.0.0, - genvalidity-hspec ==1.0.0.3, - genvalidity-hspec-aeson ==1.0.0.0, - genvalidity-hspec-binary ==1.0.0.0, - genvalidity-hspec-cereal ==1.0.0.0, - genvalidity-hspec-hashable ==1.0.0.1, - genvalidity-hspec-optics ==1.0.0.0, - genvalidity-hspec-persistent ==1.0.0.0, - genvalidity-mergeful ==0.3.0.1, - genvalidity-mergeless ==0.3.0.0, - genvalidity-network-uri ==0.0.0.0, - genvalidity-path ==1.0.0.1, - genvalidity-persistent ==1.0.0.2, - genvalidity-property ==1.0.0.0, - genvalidity-scientific ==1.0.0.0, - genvalidity-sydtest ==1.0.0.0, - genvalidity-sydtest-aeson ==1.0.0.0, - genvalidity-sydtest-hashable ==1.0.0.1, - genvalidity-sydtest-lens ==1.0.0.0, - genvalidity-sydtest-persistent ==1.0.0.0, - genvalidity-text ==1.0.0.1, - genvalidity-time ==1.0.0.1, - genvalidity-typed-uuid ==0.1.0.1, - genvalidity-unordered-containers ==1.0.0.1, - genvalidity-uuid ==1.0.0.1, - genvalidity-vector ==1.0.0.0, - geodetics ==0.1.2, - getopt-generics ==0.13.1.0, - ghc-bignum ==1.3, - ghc-bignum-orphans ==0.1.1, - ghc-byteorder ==4.11.0.0.10, - ghc-check ==0.5.0.8, - ghc-compact ==0.1.0.0, - ghc-events ==0.19.0.1, - ghc-exactprint ==1.8.0.0, - ghc-hs-meta ==0.1.3.0, - ghcid ==0.8.9, - ghci-hexcalc ==0.1.1.0, - ghcjs-codemirror ==0.0.0.2, - ghcjs-perch ==0.3.3.3, - ghc-lib ==9.8.2.20240223, - ghc-lib-parser ==9.8.2.20240223, - ghc-lib-parser-ex ==9.8.0.2, - ghc-parser ==0.2.6.0, - ghc-paths ==0.1.0.12, - ghc-prim ==0.11.0, - ghc-tcplugins-extra ==0.4.5, - ghc-trace-events ==0.1.2.8, - ghc-typelits-extra ==0.4.6, - ghc-typelits-knownnat ==0.7.10, - ghc-typelits-natnormalise ==0.7.9, - ghc-typelits-presburger ==0.7.3.0, - ghost-buster ==0.1.1.0, - githash ==0.1.7.0, - github ==0.29, - github-release ==2.0.0.10, - github-rest ==1.1.4, - github-types ==0.2.1, - github-webhooks ==0.17.0, - gitlab-haskell ==1.0.0.5, - git-lfs ==1.2.2, - gitlib ==3.1.3, - gitrev ==1.3.1, - glabrous ==2.0.6.3, - glasso ==0.1.0, - GLFW-b ==3.3.9.0, - glib ==0.13.10.0, - glib-stopgap ==0.1.0.0, - Glob ==0.10.2, - glob-posix ==0.2.0.1, - gloss ==1.13.2.2, - gloss-algorithms ==1.13.0.3, - gloss-rendering ==1.13.1.2, - GLURaw ==2.0.0.5, - GLUT ==2.7.0.16, - gmail-simple ==0.1.0.6, - gnuplot ==0.5.7, - google-isbn ==1.0.3, - gpolyline ==0.1.0.1, - graph-core ==0.3.0.0, - graphite ==0.10.0.1, - graphql ==1.2.0.3, - graphql-client ==1.2.4, - graphs ==0.7.2, - graphula ==2.1.0.0, - graphviz ==2999.20.2.0, - graph-wrapper ==0.2.6.0, - gravatar ==0.8.1, - gridtables ==0.1.0.0, - grisette ==0.4.1.0, - groom ==0.1.2.1, - groups ==0.5.3, - guarded-allocation ==0.0.1, - hackage-db ==2.1.3, - hackage-security ==0.6.2.4, - haddock-library ==1.11.0, - haha ==0.3.1.1, - hakyll ==4.16.2.0, - hakyll-convert ==0.3.0.4, - hal ==1.1, - half ==0.3.1, - hall-symbols ==0.1.0.6, - hamlet ==1.2.0, - HandsomeSoup ==0.4.2, - handwriting ==0.1.0.3, - happstack-jmacro ==7.0.12.6, - happstack-server ==7.9.0, - happy-meta ==0.2.1.0, - HasBigDecimal ==0.2.0.0, - hashable ==1.4.3.0, - hashids ==1.1.1.0, - hashmap ==1.3.3, - hashtables ==1.3.1, - haskeline ==0.8.2.1, - haskell-lexer ==1.1.1, - haskell-src ==1.0.4, - haskell-src-exts ==1.23.1, - haskell-src-exts-simple ==1.23.0.0, - haskell-src-exts-util ==0.2.5, - haskell-src-meta ==0.8.13, - haskintex ==0.8.0.2, - hasktags ==0.73.0, - has-transformers ==0.1.0.4, - hasty-hamiltonian ==1.3.4, - HaTeX ==3.22.4.1, - HaXml ==1.25.13, - HCodecs ==0.5.2, - hdaemonize ==0.5.7, - HDBC ==2.4.0.4, - HDBC-session ==0.1.2.1, - headed-megaparsec ==0.2.1.3, - heap ==1.0.4, - heaps ==0.4, - heatshrink ==0.1.0.0, - hebrew-time ==0.1.2, - hedgehog ==1.4, - hedgehog-classes ==0.2.5.4, - hedgehog-corpus ==0.2.0, - hedgehog-fn ==1.0, - hedgehog-quickcheck ==0.1.1, - hedis ==0.15.2, - hedn ==0.3.0.4, - hegg ==0.5.0.0, - heist ==1.1.1.2, - here ==1.2.14, - heredoc ==0.2.0.0, - heterocephalus ==1.0.5.7, - hex ==0.2.0, - hexml ==0.3.4, - hexml-lens ==0.2.2, - hexpat ==0.20.13, - hex-text ==0.1.0.9, - hformat ==0.3.3.1, - hgal ==2.0.0.3, - hidapi ==0.1.8, - hie-bios ==0.13.1, - hi-file-parser ==0.1.6.0, - hindent ==6.1.1, - hinfo ==0.0.3.0, - hint ==0.9.0.8, - histogram-fill ==0.9.1.0, - hjsmin ==0.2.1, - hkd-default ==1.1.0.0, - hlibcpuid ==0.2.0, - hlibsass ==0.1.10.1, - hlint ==3.8, - hmatrix ==0.20.2, - hmatrix-backprop ==0.1.3.0, - hmatrix-morpheus ==0.1.1.2, - hmatrix-vector-sized ==0.1.3.0, - HMock ==0.5.1.2, - hoauth2 ==2.13.1, - hoogle ==5.0.18.4, - horizontal-rule ==0.6.0.0, - hosc ==0.20, - hostname ==1.0, - hostname-validate ==1.0.0, - hourglass ==0.2.12, - hourglass-orphans ==0.1.0.0, - hpack ==0.36.0, - hpc ==0.7.0.0, - hpc-codecov ==0.5.0.0, - hpc-lcov ==1.1.2, - HPDF ==1.7, - hpp ==0.6.5, - hquantlib-time ==0.1.0, - hreader ==1.1.1, - hreader-lens ==0.1.3.0, - hsass ==0.8.0, - hs-bibutils ==6.10.0.0, - hscolour ==1.25, - hse-cpp ==0.2, - hsemail ==2.2.2, - hset ==2.2.0, - HSet ==0.0.2, - hsini ==0.5.2.2, - HSlippyMap ==3.0.1, - hslogger ==1.3.1.0, - hslua ==2.3.1, - hslua-aeson ==2.3.1, - hslua-classes ==2.3.1, - hslua-cli ==1.4.2, - hslua-core ==2.3.2, - hslua-list ==1.1.1, - hslua-marshalling ==2.3.1, - hslua-module-doclayout ==1.1.1, - hslua-module-path ==1.1.1, - hslua-module-system ==1.1.1, - hslua-module-text ==1.1.1, - hslua-module-version ==1.1.1, - hslua-module-zip ==1.1.1, - hslua-objectorientation ==2.3.1, - hslua-packaging ==2.3.1, - hslua-repl ==0.1.2, - hslua-typing ==0.1.1, - hspec ==2.11.7, - hspec-api ==2.11.7, - hspec-attoparsec ==0.1.0.2, - hspec-checkers ==0.1.0.2, - hspec-contrib ==0.5.2, - hspec-core ==2.11.7, - hspec-discover ==2.11.7, - hspec-expectations ==0.8.4, - hspec-expectations-json ==1.0.2.1, - hspec-expectations-lifted ==0.10.0, - hspec-expectations-pretty-diff ==0.7.2.6, - hspec-golden ==0.2.1.0, - hspec-golden-aeson ==0.9.0.0, - hspec-hedgehog ==0.1.1.0, - hspec-junit-formatter ==1.1.0.2, - hspec-leancheck ==0.0.6, - hspec-megaparsec ==2.2.1, - hspec-meta ==2.11.7, - hspec-parsec ==0, - hspec-smallcheck ==0.5.3, - hspec-tmp-proc ==0.6.0.0, - hspec-wai ==0.11.1, - hspec-wai-json ==0.11.0, - hspec-webdriver ==1.2.2, - hs-php-session ==0.0.9.3, - HStringTemplate ==0.8.8, - HSvm ==1.0.3.32, - HsYAML ==0.2.1.3, - HsYAML-aeson ==0.2.0.1, - hsyslog ==5.0.2, - HTF ==0.15.0.1, - html ==1.0.1.2, - html-conduit ==1.3.2.2, - html-email-validate ==0.2.0.0, - html-entities ==1.1.4.7, - html-entity-map ==0.1.0.0, - HTTP ==4000.4.1, - http2 ==5.0.1, - http-api-data ==0.6, - http-api-data-qq ==0.1.0.0, - http-client ==0.7.17, - http-client-overrides ==0.1.1.0, - http-client-restricted ==0.1.0, - http-client-tls ==0.3.6.3, - http-common ==0.8.3.4, - http-conduit ==2.3.8.3, - http-date ==0.0.11, - http-directory ==0.1.10, - http-download ==0.2.1.0, - httpd-shed ==0.4.1.2, - http-link-header ==1.2.1, - http-media ==0.8.1.1, - http-query ==0.1.3, - http-reverse-proxy ==0.6.0.2, - http-types ==0.12.4, - human-readable-duration ==0.2.1.4, - HUnit ==1.6.2.0, - HUnit-approx ==1.1.1.1, - hunit-dejafu ==2.0.0.6, - hvect ==0.4.0.1, - hvega ==0.12.0.7, - hw-bits ==0.7.2.2, - hw-conduit-merges ==0.2.1.0, - hw-diagnostics ==0.0.1.0, - hweblib ==0.6.3, - hw-excess ==0.2.3.0, - hw-hedgehog ==0.1.1.1, - hw-hspec-hedgehog ==0.1.1.1, - hw-int ==0.0.2.0, - hw-parser ==0.1.1.0, - hw-prim ==0.6.3.2, - hw-rankselect-base ==0.3.4.1, - hw-string-parse ==0.0.0.5, - hxt ==9.3.1.22, - hxt-charproperties ==9.5.0.0, - hxt-css ==0.1.0.3, - hxt-curl ==9.1.1.1, - hxt-expat ==9.1.1, - hxt-http ==9.1.5.2, - hxt-regex-xmlschema ==9.2.0.7, - hxt-tagsoup ==9.1.4, - hxt-unicode ==9.0.2.4, - hybrid-vectors ==0.2.4, - hyper ==0.2.1.1, - hyperloglog ==0.4.6, - hyphenation ==0.8.2, - iconv ==0.4.1.3, - identicon ==0.2.3, - ieee754 ==0.8.0, - if ==0.1.0.0, - IfElse ==0.85, - iff ==0.0.6.1, - ilist ==0.4.0.1, - imagesize-conduit ==1.1, - immortal ==0.3, - immortal-queue ==0.1.0.1, - inbox ==0.2.0, - incipit-base ==0.6.0.0, - incipit-core ==0.6.0.0, - include-file ==0.1.0.4, - incremental ==0.3.1, - incremental-parser ==0.5.1, - indents ==0.5.0.1, - indexed ==0.1.3, - indexed-containers ==0.1.0.2, - indexed-list-literals ==0.2.1.3, - indexed-profunctors ==0.1.1.1, - indexed-transformers ==0.1.0.4, - indexed-traversable ==0.1.3, - indexed-traversable-instances ==0.1.1.2, - inf-backprop ==0.1.0.2, - infer-license ==0.2.0, - infinite-list ==0.1.1, - influxdb ==1.9.3.1, - ini ==0.4.2, - inj ==1.0, - inline-c ==0.9.1.10, - inline-c-cpp ==0.5.0.2, - input-parsers ==0.3.0.2, - insert-ordered-containers ==0.2.5.3, - inspection-testing ==0.5.0.3, - integer-conversion ==0.1.0.1, - integer-gmp ==1.1, - integer-logarithms ==1.0.3.1, - integer-roots ==1.0.2.0, - integration ==0.2.1, - intern ==0.9.5, - interpolate ==0.2.1, - interpolatedstring-perl6 ==1.0.2, - interpolation ==0.1.1.2, - Interpolation ==0.3.0, - IntervalMap ==0.6.2.1, - intervals ==0.9.2, - intset-imperative ==0.1.0.0, - int-supply ==1.0.0, - invariant ==0.6.3, - invertible-grammar ==0.1.3.5, - io-machine ==0.2.0.0, - io-manager ==0.1.0.4, - io-memoize ==1.1.1.0, - io-region ==0.1.1, - io-storage ==0.3, - io-streams ==1.5.2.2, - io-streams-haproxy ==1.0.1.0, - ip ==1.7.8, - iproute ==1.7.12, - IPv6Addr ==2.0.6, - ipynb ==0.2, - irc ==0.6.1.1, - irc-ctcp ==0.1.3.1, - isbn ==1.1.0.5, - islink ==0.1.0.0, - iso3166-country-codes ==0.20140203.8, - iso639 ==0.1.0.3, - iso8601-time ==0.1.5, - isocline ==1.0.9, - isomorphism-class ==0.1.0.12, - ix-shapable ==0.1.0, - jalaali ==1.0.0.0, - jira-wiki-markup ==1.5.1, - jmacro ==0.6.18, - jose ==0.11, - jose-jwt ==0.10.0, - journalctl-stream ==0.6.0.6, - js-chart ==2.9.4.1, - js-dgtable ==0.5.2, - js-flot ==0.8.3, - js-jquery ==3.3.1, - json ==0.11, - json-feed ==2.0.0.11, - jsonifier ==0.2.1.3, - jsonpath ==0.3.0.0, - json-rpc ==1.1.1, - json-stream ==0.4.5.3, - JuicyCairo ==0.1.0.0, - JuicyPixels ==3.3.8, - JuicyPixels-extra ==0.6.0, - junit-xml ==0.1.0.3, - justified-containers ==0.3.0.0, - jwt ==0.11.0, - kan-extensions ==5.2.5, - kansas-comet ==0.4.2, - katip ==0.8.8.0, - katip-logstash ==0.1.0.2, - katip-wai ==0.1.2.3, - kazura-queue ==0.1.0.4, - kdt ==0.2.5, - keep-alive ==0.2.1.0, - keter ==2.1.5, - keycode ==0.2.2, - keyed-vals ==0.2.3.1, - keyed-vals-hspec-tests ==0.2.3.1, - keyed-vals-mem ==0.2.3.1, - keyed-vals-redis ==0.2.3.1, - keys ==3.12.3, - ki ==1.0.1.1, - kind-apply ==0.4.0.0, - kind-generics ==0.5.0.0, - ki-unlifted ==1.0.0.2, - kmeans ==0.1.3, - knob ==0.2.2, - labels ==0.3.3, - lackey ==2.0.0.7, - language-c ==0.9.3, - language-c-quote ==0.13.0.1, - language-dot ==0.1.2, - language-glsl ==0.3.0, - language-java ==0.2.9, - language-javascript ==0.7.1.0, - language-nix ==2.2.0, - language-protobuf ==1.0.1, - largeword ==1.2.5, - latex ==0.1.0.4, - lattices ==2.2, - lawful ==0.1.0.0, - lazy-csv ==0.5.1, - lazyio ==0.1.0.4, - lazysmallcheck ==0.6, - lca ==0.4, - leancheck ==1.0.2, - leancheck-instances ==0.0.5, - leapseconds-announced ==2017.1.0.1, - leb128-cereal ==1.2, - lens ==5.2.3, - lens-action ==0.2.6, - lens-aeson ==1.2.3, - lens-csv ==0.1.1.0, - lens-family ==2.1.3, - lens-family-core ==2.1.3, - lens-family-th ==0.5.3.1, - lens-misc ==0.0.2.0, - lens-properties ==4.11.1, - lens-regex ==0.1.3, - LetsBeRational ==1.0.0.0, - lexer-applicative ==2.1.0.2, - libBF ==0.6.7, - libffi ==0.2.1, - libyaml ==0.1.4, - libyaml-clib ==0.2.5, - lifted-async ==0.10.2.5, - lifted-base ==0.2.3.12, - lift-generics ==0.2.1, - lift-type ==0.1.1.1, - linear ==1.22, - linear-base ==0.4.0, - linear-generics ==0.2.3, - linear-programming ==0.0.1, - linebreak ==1.1.0.4, - linux-capabilities ==0.1.1.0, - List ==0.6.2, - ListLike ==4.7.8.2, - list-predicate ==0.1.0.1, - listsafe ==0.1.0.1, - list-shuffle ==1.0.0, - list-t ==1.0.5.7, - list-transformer ==1.1.0, - ListTree ==0.2.3, - ListZipper ==1.2.0.2, - literatex ==0.3.0.0, - load-env ==0.2.1.0, - locators ==0.3.0.3, - loch-th ==0.2.2, - lockfree-queue ==0.2.4, - log-base ==0.12.0.1, - log-domain ==0.13.2, - logfloat ==0.14.0, - logger-thread ==0.1.0.2, - logging ==3.0.5, - logging-facade ==0.3.1, - logging-facade-syslog ==1, - logict ==0.8.1.0, - logstash ==0.1.0.4, - loop ==0.3.0, - lpeg ==1.1.0, - LPFP ==1.1.1, - LPFP-core ==1.1.1, - lrucache ==1.2.0.1, - lrucaching ==0.3.4, - lsp ==2.4.0.0, - lsp-test ==0.17.0.0, - lsp-types ==2.1.1.0, - lua ==2.3.2, - lua-arbitrary ==1.0.1.1, - lucid ==2.11.20230408, - lucid2 ==0.0.20230706, - lucid-cdn ==0.2.2.0, - lucid-extras ==0.2.2, - lukko ==0.1.1.3, - lz4 ==0.2.3.1, - lzma ==0.0.1.0, - machines ==0.7.3, - mainland-pretty ==0.7.1, - main-tester ==0.2.0.1, - managed ==1.0.10, - mappings ==0.3.0.0, - map-syntax ==0.3, - markdown ==0.1.17.5, - markdown-unlit ==0.6.0, - markov-chain ==0.0.3.4, - markov-chain-usage-model ==0.0.0, - markup-parse ==0.1.1, - mason ==0.2.6, - massiv ==1.0.4.0, - massiv-io ==1.0.0.1, - massiv-serialise ==1.0.0.2, - massiv-test ==1.0.0.0, - matchable ==0.1.2.1, - mathexpr ==0.3.1.0, - math-extras ==0.1.1.0, - math-functions ==0.3.4.3, - mathlist ==0.2.0.0, - matplotlib ==0.7.7, - matrices ==0.5.0, - matrix ==0.3.6.3, - matrix-as-xyz ==0.1.2.2, - matrix-market-attoparsec ==0.1.1.3, - matrix-static ==0.3, - maximal-cliques ==0.1.1, - mcmc ==0.8.2.0, - mcmc-types ==1.0.3, - median-stream ==0.7.0.0, - med-module ==0.1.3, - megaparsec ==9.6.1, - megaparsec-tests ==9.6.1, - membership ==0.0.1, - memcache ==0.3.0.1, - mem-info ==0.3.0.0, - memory ==0.18.0, - mergeful ==0.3.0.0, - mergeful-persistent ==0.3.0.1, - mergeless ==0.4.0.0, - mergeless-persistent ==0.1.0.1, - merkle-tree ==0.1.1, - mersenne-random ==1.0.0.1, - mersenne-random-pure64 ==0.2.2.0, - messagepack ==0.5.5, - metrics ==0.4.1.1, - mfsolve ==0.3.2.2, - microaeson ==0.1.0.1, - microlens ==0.4.13.1, - microlens-aeson ==2.5.2, - microlens-contra ==0.1.0.3, - microlens-ghc ==0.4.14.2, - microlens-mtl ==0.2.0.3, - microlens-platform ==0.4.3.5, - microlens-th ==0.4.3.14, - microspec ==0.2.1.3, - microstache ==1.0.2.3, - midair ==0.2.0.1, - midi ==0.2.2.4, - mighty-metropolis ==2.0.0, - mime-mail ==0.5.1, - mime-mail-ses ==0.4.3, - mime-types ==0.1.2.0, - minimal-configuration ==0.1.4, - minimorph ==0.3.0.1, - minisat-solver ==0.1, - miniterion ==0.1.1.0, - miniutter ==0.5.1.2, - min-max-pqueue ==0.1.0.2, - mintty ==0.1.4, - missing-foreign ==0.1.1, - MissingH ==1.6.0.1, - mixed-types-num ==0.5.12, - mmap ==0.5.9, - mmark ==0.0.7.6, - mmark-ext ==0.2.1.5, - mmorph ==1.2.0, - mnist-idx ==0.1.3.2, - mnist-idx-conduit ==0.4.0.0, - mockery ==0.3.5, - mod ==0.2.0.1, - modern-uri ==0.3.6.1, - modular ==0.1.0.8, - moffy ==0.1.1.0, - moffy-samples ==0.1.0.3, - moffy-samples-events ==0.2.2.5, - monad-chronicle ==1.0.1, - monad-control ==1.0.3.1, - monad-control-aligned ==0.0.2.1, - monad-control-identity ==0.2.0.0, - monad-coroutine ==0.9.2, - monad-extras ==0.6.0, - monad-interleave ==0.2.0.1, - monadlist ==0.0.2, - monad-logger ==0.3.40, - monad-logger-aeson ==0.4.1.3, - monad-logger-json ==0.1.0.0, - monad-logger-logstash ==0.2.0.2, - monad-loops ==0.4.3, - monad-memo ==0.5.4, - monad-metrics ==0.2.2.1, - monadoid ==0.0.3, - monadology ==0.3, - monad-par ==0.3.6, - monad-parallel ==0.8, - monad-par-extras ==0.3.3, - monad-peel ==0.3, - MonadPrompt ==1.0.0.5, - MonadRandom ==0.6, - monad-resumption ==0.1.4.0, - monad-schedule ==0.1.2.2, - monad-st ==0.2.4.1, - monad-time ==0.4.0.0, - mongoDB ==2.7.1.4, - monoidal-containers ==0.6.4.0, - monoidal-functors ==0.2.3.0, - monoid-extras ==0.6.2, - monoid-subclasses ==1.2.4.1, - monoid-transformer ==0.0.4, - mono-traversable ==1.0.17.0, - mono-traversable-instances ==0.1.1.0, - mono-traversable-keys ==0.3.0, - more-containers ==0.2.2.2, - moss ==0.2.0.1, - mountpoints ==1.0.2, - msgpack ==1.0.1.0, - mtl ==2.3.1, - mtl-compat ==0.2.2, - mtl-prelude ==2.0.3.2, - multiarg ==0.30.0.10, - multi-containers ==0.2, - multimap ==1.2.1, - MultipletCombiner ==0.0.7, - multiset ==0.3.4.3, - multistate ==0.8.0.4, - murmur3 ==1.0.5, - murmur-hash ==0.1.0.10, - MusicBrainz ==0.4.1, - mustache ==2.4.2, - mutable-containers ==0.3.4.1, - mwc-probability ==2.3.1, - mwc-random ==0.15.0.2, - mx-state-codes ==1.0.0.0, - myers-diff ==0.3.0.0, - mysql-haskell ==1.1.4, - mysql-haskell-nem ==0.1.0.0, - n2o ==0.11.1, - n2o-nitro ==0.11.2, - nagios-check ==0.3.2, - named ==0.3.0.1, - names-th ==0.3.0.1, - nano-erl ==0.1.0.1, - nanospec ==0.2.2, - nats ==1.1.2, - natural-arithmetic ==0.2.1.0, - natural-induction ==0.2.0.0, - natural-sort ==0.1.2, - natural-transformation ==0.4, - neat-interpolation ==0.5.1.4, - netlib-carray ==0.1, - netlib-comfort-array ==0.0.0.2, - netlib-ffi ==0.1.1, - netpbm ==1.0.4, - netrc ==0.2.0.0, - nettle ==0.3.1.1, - netwire ==5.0.3, - netwire-input ==0.0.7, - netwire-input-glfw ==0.0.12, - network ==3.1.4.0, - network-bsd ==2.8.1.0, - network-byte-order ==0.1.7, - network-conduit-tls ==1.4.0, - network-control ==0.0.2, - network-info ==0.2.1, - network-ip ==0.3.0.3, - network-messagepack-rpc ==0.1.2.0, - network-multicast ==0.3.2, - network-run ==0.2.7, - network-simple ==0.4.5, - network-simple-tls ==0.4.2, - network-transport ==0.5.7, - network-uri ==2.6.4.2, - network-wait ==0.2.0.0, - newtype ==0.2.2.0, - nicify-lib ==1.0.1, - nix-derivation ==1.1.3, - NoHoed ==0.1.1, - nonce ==1.0.7, - nondeterminism ==1.5, - non-empty ==0.3.5, - nonempty-containers ==0.3.4.5, - non-empty-sequence ==0.2.0.4, - non-empty-text ==0.2.1, - nonempty-vector ==0.2.3, - nonempty-zipper ==1.0.0.4, - non-negative ==0.1.2, - normaldistribution ==1.1.0.3, - nothunks ==0.2.1.0, - no-value ==1.0.0.0, - nowdoc ==0.1.1.0, - nqe ==0.6.5, - nsis ==0.3.3, - n-tuple ==0.0.3, - numbers ==3000.2.0.2, - numeric-extras ==0.1, - numeric-limits ==0.1.0.0, - numeric-prelude ==0.4.4, - numeric-quest ==0.2.0.2, - numhask ==0.11.1.0, - numhask-array ==0.11.0.1, - numhask-space ==0.11.1.0, - NumInstances ==1.4, - numtype-dk ==0.5.0.3, - nuxeo ==0.3.2, - nvim-hs ==2.3.2.3, - nvim-hs-contrib ==2.0.0.2, - nvim-hs-ghcid ==2.0.1.0, - ObjectName ==1.1.0.2, - oblivious-transfer ==0.1.0, - o-clock ==1.4.0, - ofx ==0.4.4.0, - old-locale ==1.0.0.7, - old-time ==1.1.0.4, - om-elm ==2.0.0.6, - once ==0.4, - one-liner ==2.1, - one-liner-instances ==0.1.3.0, - OneTuple ==0.4.1.1, - Only ==0.1, - oo-prototypes ==0.1.0.0, - oops ==0.2.0.1, - OpenAL ==1.7.0.5, - openapi3 ==3.2.4, - open-browser ==0.2.1.0, - openexr-write ==0.1.0.2, - OpenGL ==3.0.3.0, - OpenGLRaw ==3.3.4.1, - openpgp-asciiarmor ==0.1.2, - opensource ==0.1.1.0, - opentelemetry ==0.8.0, - opentelemetry-extra ==0.8.0, - opentelemetry-lightstep ==0.8.0, - opentelemetry-wai ==0.8.0, - open-witness ==0.6, - operational ==0.2.4.2, - optics ==0.4.2.1, - optics-core ==0.4.1.1, - optics-extra ==0.4.2.1, - optics-operators ==0.1.0.1, - optics-th ==0.4.1, - optics-vl ==0.2.1, - optima ==0.4.0.5, - optional-args ==1.0.2, - optparse-applicative ==0.18.1.0, - optparse-enum ==1.0.0.0, - optparse-generic ==1.5.2, - optparse-simple ==0.1.1.4, - optparse-text ==0.1.1.0, - ordered-containers ==0.2.3, - os-string ==2.0.2, - overhang ==1.0.0, - packcheck ==0.7.0, - pager ==0.1.1.0, - pagination ==0.2.2, - pagure ==0.1.1, - palette ==0.3.0.3, - pandoc ==3.1.12.3, - pandoc-lua-engine ==0.2.1.3, - pandoc-lua-marshal ==0.2.5, - pandoc-plot ==1.8.0, - pandoc-server ==0.1.0.5, - pandoc-throw ==0.1.0.0, - pandoc-types ==1.23.1, - pango ==0.13.10.0, - pantry ==0.9.3.2, - parallel ==3.2.2.0, - parallel-io ==0.3.5, - parameterized ==0.5.0.0, - park-bench ==0.1.1.0, - parseargs ==0.2.0.9, - parsec ==3.1.17.0, - parsec-class ==1.0.1.0, - parsec-numbers ==0.1.0, - parsec-numeric ==0.1.0.0, - ParsecTools ==0.0.2.0, - parser-combinators ==1.3.0, - parsers ==0.12.11, - partial-handler ==1.0.3, - partialord ==0.0.2, - partial-order ==0.2.0.0, - password ==3.0.4.0, - password-instances ==3.0.0.0, - password-types ==1.0.0.0, - path ==0.9.5, - path-binary-instance ==0.1.0.1, - path-extensions ==0.1.1.0, - path-extra ==0.3.1, - path-io ==1.8.1, - path-like ==0.2.0.2, - path-pieces ==0.2.1, - pathtype ==0.8.1.3, - path-utils ==0.1.1.0, - pathwalk ==0.3.1.2, - patrol ==1.0.0.7, - pava ==0.1.1.4, - pcg-random ==0.1.4.0, - pcre2 ==2.2.1, - pcre-utils ==0.1.9, - pdc ==0.1.1, - peano ==0.1.0.2, - pedersen-commitment ==0.2.0, - pem ==0.2.4, - percent-format ==0.0.4, - perfect-hash-generator ==1.0.0, - persistable-record ==0.6.0.6, - persistable-types-HDBC-pg ==0.0.3.5, - persistent ==2.14.6.1, - persistent-discover ==0.1.0.7, - persistent-lens ==1.0.0, - persistent-mongoDB ==2.13.0.1, - persistent-mtl ==0.5.1, - persistent-pagination ==0.1.1.2, - persistent-qq ==2.12.0.6, - persistent-sqlite ==2.13.3.0, - persistent-template ==2.12.0.0, - persistent-test ==2.13.1.3, - persistent-typed-db ==0.1.0.7, - pg-harness-client ==0.6.0, - phantom-state ==0.2.1.4, - phatsort ==0.6.0.0, - pid1 ==0.1.3.1, - pinch ==0.5.1.0, - pipes ==4.3.16, - pipes-attoparsec ==0.6.0, - pipes-binary ==0.4.4, - pipes-bytestring ==2.1.7, - pipes-concurrency ==2.0.14, - pipes-csv ==1.4.3, - pipes-extras ==1.0.15, - pipes-fastx ==0.3.0.0, - pipes-fluid ==0.6.0.1, - pipes-group ==1.0.12, - pipes-mongodb ==0.1.0.0, - pipes-ordered-zip ==1.2.1, - pipes-parse ==3.0.9, - pipes-random ==1.0.0.5, - pipes-safe ==2.3.5, - pipes-wai ==3.2.0, - placeholders ==0.1, - plaid ==0.1.0.4, - plotlyhs ==0.2.3, - Plural ==0.0.2, - pointed ==5.0.4, - pointedlist ==0.6.1, - pointless-fun ==1.1.0.8, - poll ==0.0.0.2, - poly ==0.5.1.0, - poly-arity ==0.1.0, - polynomials-bernstein ==1.1.2, - polyparse ==1.13, - polysemy ==1.9.1.3, - polysemy-fs ==0.1.0.0, - polysemy-plugin ==0.4.5.2, - polysemy-webserver ==0.2.1.2, - pooled-io ==0.0.2.3, - portable-lines ==0.1, - port-utils ==0.2.1.0, - posix-paths ==0.3.0.0, - posix-pty ==0.2.2, - possibly ==1.0.0.0, - postgres-options ==0.2.1.0, - postgresql-binary ==0.13.1.3, - postgresql-syntax ==0.4.1.1, - postgresql-typed ==0.6.2.5, - post-mess-age ==0.2.1.0, - pptable ==0.3.0.0, - pqueue ==1.5.0.0, - prairie ==0.0.3.0, - pred-set ==0.0.1, - prefix-units ==0.3.0.1, - prelude-compat ==0.0.0.2, - prelude-safeenum ==0.1.1.3, - pretty ==1.1.3.6, - pretty-class ==1.0.1.1, - prettyclass ==1.0.0.0, - pretty-hex ==1.1, - prettyprinter ==1.7.1, - prettyprinter-ansi-terminal ==1.1.3, - prettyprinter-combinators ==0.1.2, - prettyprinter-compat-annotated-wl-pprint ==1.1, - prettyprinter-compat-ansi-wl-pprint ==1.0.2, - prettyprinter-compat-wl-pprint ==1.0.1, - prettyprinter-interp ==0.2.0.0, - pretty-relative-time ==0.3.0.0, - pretty-show ==1.10, - pretty-simple ==4.1.2.0, - pretty-sop ==0.2.0.3, - pretty-terminal ==0.1.0.0, - primes ==0.2.1.0, - primitive ==0.8.0.0, - primitive-addr ==0.1.0.3, - primitive-extras ==0.10.1.10, - primitive-offset ==0.2.0.1, - primitive-serial ==0.1, - primitive-unaligned ==0.1.1.2, - primitive-unlifted ==2.1.0.0, - prim-uniq ==0.2, - print-console-colors ==0.1.0.0, - probability ==0.2.8, - process ==1.6.18.0, - process-extras ==0.7.4, - product-isomorphic ==0.0.3.4, - product-profunctors ==0.11.1.1, - profunctors ==5.6.2, - projectroot ==0.2.0.1, - project-template ==0.2.1.0, - prometheus ==2.2.4, - prometheus-client ==1.1.1, - prometheus-metrics-ghc ==1.0.1.2, - promises ==0.3, - prospect ==0.1.0.0, - protobuf ==0.2.1.3, - protobuf-simple ==0.1.1.1, - protocol-radius ==0.0.1.1, - protocol-radius-test ==0.1.0.1, - protolude ==0.3.4, - proxied ==0.3.1, - PSQueue ==1.2.0, - psqueues ==0.2.8.0, - ptr ==0.16.8.6, - ptr-poker ==0.1.2.14, - pureMD5 ==2.1.4, - purescript-bridge ==0.15.0.0, - pusher-http-haskell ==2.1.0.17, - pvar ==1.0.0.0, - pwstore-fast ==2.4.4, - PyF ==0.11.2.1, - qchas ==1.1.0.1, - quaalude ==0.0.0.1, - quadratic-irrational ==0.1.1, - QuasiText ==0.1.2.6, - quickbench ==1.0.1, - QuickCheck ==2.14.3, - quickcheck-arbitrary-adt ==0.3.1.0, - quickcheck-assertions ==0.3.0, - quickcheck-classes ==0.6.5.0, - quickcheck-classes-base ==0.6.2.0, - quickcheck-groups ==0.0.1.1, - quickcheck-higherorder ==0.1.0.1, - quickcheck-instances ==0.3.30, - quickcheck-io ==0.2.0, - quickcheck-monoid-subclasses ==0.3.0.1, - quickcheck-simple ==0.1.1.1, - quickcheck-text ==0.1.2.1, - quickcheck-transformer ==0.3.1.2, - quickcheck-unicode ==1.0.1.0, - quicklz ==1.5.0.11, - quiet ==0.2, - quote-quot ==0.2.1.0, - radius ==0.7.1.0, - rainbow ==0.34.2.2, - rainbox ==0.26.0.0, - ral ==0.2.1, - rampart ==2.0.0.7, - ramus ==0.1.2, - rando ==0.0.0.4, - random ==1.2.1.2, - random-bytestring ==0.1.4, - random-fu ==0.3.0.1, - random-shuffle ==0.0.4, - random-tree ==0.6.0.5, - range ==0.3.0.2, - ranged-list ==0.1.2.1, - Ranged-sets ==0.4.0, - ranges ==0.2.4, - range-set-list ==0.1.3.1, - rank1dynamic ==0.4.1, - rank2classes ==1.5.3, - Rasterific ==0.7.5.4, - rasterific-svg ==0.3.3.2, - ratel ==2.0.0.11, - rate-limit ==1.4.3, - ratel-wai ==2.0.0.6, - ratio-int ==0.1.2, - rattle ==0.2, - rattletrap ==12.1.3, - Rattus ==0.5.1.1, - rawstring-qm ==0.2.3.0, - raw-strings-qq ==1.1, - rcu ==0.2.7, - rdf ==0.1.0.8, - reactive-banana ==1.3.2.0, - reactive-banana-bunch ==1.0.0.1, - reactive-midyim ==0.4.1.1, - readable ==0.3.1, - read-editor ==0.1.0.2, - read-env-var ==1.0.0.0, - rebase ==1.20.2, - rec-def ==0.2.2, - record-dot-preprocessor ==0.2.17, - record-hasfield ==1.0.1, - recursion-schemes ==5.2.2.5, - recv ==0.1.0, - redact ==0.5.0.0, - reddit-scrape ==0.0.1, - redis-glob ==0.1.0.8, - redis-resp ==1.0.0, - reducers ==3.12.4, - refact ==0.3.0.2, - ref-fd ==0.5.0.1, - reflection ==2.1.7, - RefSerialize ==0.4.0, - ref-tf ==0.5.0.1, - regex ==1.1.0.2, - regex-applicative ==0.3.4, - regex-base ==0.94.0.2, - regex-compat ==0.95.2.1, - regex-pcre-builtin ==0.95.2.3.8.44, - regex-posix ==0.96.0.1, - regex-posix-clib ==2.7, - regex-tdfa ==1.3.2.2, - regex-with-pcre ==1.1.0.2, - regression-simple ==0.2.1, - reinterpret-cast ==0.1.0, - relapse ==1.0.0.1, - relational-query ==0.12.3.1, - relational-query-HDBC ==0.7.2.1, - relational-record ==0.2.2.0, - relational-schemas ==0.1.8.1, - reliable-io ==0.0.2, - relude ==1.2.1.0, - renderable ==0.2.0.1, - reorder-expression ==0.1.0.1, - replace-attoparsec ==1.5.0.0, - replace-megaparsec ==1.5.0.1, - repline ==0.4.2.0, - req ==3.13.2, - req-conduit ==1.0.2, - rerebase ==1.20.2, - reroute ==0.7.0.0, - resolv ==0.2.0.2, - resource-pool ==0.4.0.0, - resourcet ==1.3.0, - rest-rewrite ==0.4.3, - result ==0.2.6.0, - retry ==0.9.3.1, - rev-state ==0.2.0.1, - rfc1751 ==0.1.3, - rfc5051 ==0.2, - rg ==1.4.0.0, - richenv ==0.1.0.1, - rio ==0.1.22.0, - rio-orphans ==0.1.2.0, - rio-prettyprint ==0.1.8.0, - rng-utils ==0.3.1, - roc-id ==0.2.0.1, - roles ==0.2.1.0, - rollbar ==1.1.3, - rope-utf16-splay ==0.4.0.0, - rosezipper ==0.2, - rot13 ==0.2.0.1, - row-types ==1.0.1.2, - rpmbuild-order ==0.4.11, - rpm-nvr ==0.1.2, - rp-tree ==0.7.1, - rrb-vector ==0.2.1.0, - RSA ==2.4.1, - rss ==3000.2.0.8, - run-haskell-module ==0.0.2, - runmemo ==1.0.0.1, - run-st ==0.1.3.3, - rvar ==0.3.0.2, - rzk ==0.7.3, - s3-signer ==0.5.0.0, - safe ==0.3.21, - safe-coloured-text ==0.2.0.1, - safe-coloured-text-gen ==0.0.0.2, - safe-coloured-text-layout ==0.0.0.0, - safe-coloured-text-layout-gen ==0.0.0.0, - safe-coloured-text-terminfo ==0.1.0.0, - safecopy ==0.10.4.2, - safe-decimal ==0.2.1.0, - safe-exceptions ==0.1.7.4, - safe-foldable ==0.1.0.0, - safe-gen ==1.0.1, - safeio ==0.0.6.0, - safe-json ==1.2.0.1, - SafeSemaphore ==0.10.1, - salve ==2.0.0.4, - sample-frame ==0.0.4, - sample-frame-np ==0.0.5, - sampling ==0.3.5, - sandi ==0.5, - sandwich ==0.2.2.0, - sandwich-hedgehog ==0.1.3.0, - sandwich-quickcheck ==0.1.0.7, - sandwich-slack ==0.1.2.0, - sandwich-webdriver ==0.2.3.1, - saturn ==1.0.0.3, - say ==0.1.0.1, - sbp ==5.0.7, - sbv ==10.7, - scalpel ==0.6.2.2, - scalpel-core ==0.6.2.2, - scanf ==0.1.0.0, - scanner ==0.3.1, - scheduler ==2.0.0.1, - SciBaseTypes ==0.1.1.0, - scientific ==0.3.7.0, - scientist ==0.0.0.0, - scotty ==0.21, - search-algorithms ==0.3.2, - securemem ==0.1.10, - selections ==0.3.0.0, - selective ==0.7, - semaphore-compat ==1.0.0, - semialign ==1.3, - semigroupoids ==6.0.0.1, - semigroups ==0.20, - semirings ==0.6, - semiring-simple ==1.0.0.1, - semver ==0.4.0.1, - sendfile ==0.7.11.5, - sendgrid-v3 ==1.0.0.1, - seqid ==0.6.3, - seqid-streams ==0.7.2, - sequence-formats ==1.8.0.1, - sequenceTools ==1.5.3.1, - serialise ==0.2.6.1, - servant ==0.20.1, - servant-auth ==0.4.1.0, - servant-auth-client ==0.4.1.1, - servant-auth-docs ==0.2.10.1, - servant-auth-server ==0.4.8.0, - servant-auth-swagger ==0.2.10.2, - servant-blaze ==0.9.1, - servant-checked-exceptions ==2.2.0.1, - servant-checked-exceptions-core ==2.2.0.1, - servant-cli ==0.1.1.0, - servant-client ==0.20, - servant-client-core ==0.20, - servant-conduit ==0.16, - servant-docs ==0.13, - servant-exceptions ==0.2.1, - servant-exceptions-server ==0.2.1, - servant-foreign ==0.16, - servant-JuicyPixels ==0.3.1.1, - servant-lucid ==0.9.0.6, - servant-machines ==0.16, - servant-multipart ==0.12.1, - servant-multipart-api ==0.12.1, - servant-multipart-client ==0.12.2, - servant-openapi3 ==2.0.1.6, - servant-pipes ==0.16, - servant-rate-limit ==0.2.0.0, - servant-rawm ==1.0.0.0, - servant-server ==0.20, - servant-static-th ==1.0.0.0, - servant-swagger ==1.2, - servant-swagger-ui ==0.3.5.5.0.0, - servant-swagger-ui-core ==0.3.5, - servant-websockets ==2.0.0, - servant-xml ==1.0.3, - serversession ==1.0.3, - serversession-backend-redis ==1.0.5, - serversession-frontend-wai ==1.0.1, - serversession-frontend-yesod ==1.0.1, - set-cover ==0.1.1, - setenv ==0.1.1.3, - setlocale ==1.0.0.10, - set-monad ==0.3.0.0, - sexp-grammar ==2.3.4.2, - SHA ==1.6.4.4, - shake ==0.19.8, - shake-plus ==0.3.4.0, - shakespeare ==2.1.0.1, - shakespeare-text ==1.1.0, - shared-memory ==0.2.0.1, - ShellCheck ==0.10.0, - shell-escape ==0.2.0, - shellify ==0.11.0.1, - shell-utility ==0.1, - shellwords ==0.1.3.1, - shelly ==1.12.1, - should-not-typecheck ==2.1.0, - show-combinators ==0.2.0.0, - siggy-chardust ==1.0.0, - signal ==0.1.0.4, - silently ==1.2.5.3, - simple ==2.0.0, - simple-affine-space ==0.2.1, - simple-cabal ==0.1.3.1, - simple-cairo ==0.1.0.6, - simple-cmd ==0.2.7, - simple-cmd-args ==0.1.8, - simple-expr ==0.1.1.0, - simple-media-timestamp ==0.2.1.0, - simple-media-timestamp-attoparsec ==0.1.0.0, - simple-pango ==0.1.0.1, - simple-prompt ==0.2.2, - simple-reflect ==0.3.3, - simple-sendfile ==0.2.32, - simple-session ==2.0.0, - simple-templates ==2.0.0, - simple-vec3 ==0.6.0.1, - since ==0.0.0, - singleton-bool ==0.1.7, - singleton-nats ==0.4.7, - singletons ==3.0.2, - singletons-base ==3.3, - singletons-presburger ==0.7.3.0, - singletons-th ==3.3, - Sit ==0.2023.8.3, - sitemap-gen ==0.1.0.0, - size-based ==0.1.3.2, - sized ==1.1.0.1, - skein ==1.0.9.4, - skews ==0.1.0.3, - skip-var ==0.1.1.0, - skylighting ==0.14.1.1, - skylighting-core ==0.14.1.1, - skylighting-format-ansi ==0.1, - skylighting-format-blaze-html ==0.1.1.2, - skylighting-format-context ==0.1.0.2, - skylighting-format-latex ==0.1, - slave-thread ==1.1.0.3, - slick ==1.2.1.0, - slist ==0.2.1.0, - slynx ==0.7.2.2, - smallcheck ==1.2.1.1, - snap ==1.1.3.3, - snap-blaze ==0.2.1.5, - snap-core ==1.0.5.1, - snap-server ==1.1.2.1, - snowflake ==0.1.1.1, - socks ==0.6.1, - solana-staking-csvs ==0.1.3.0, - some ==1.0.6, - some-dict-of ==0.1.0.2, - sop-core ==0.5.0.2, - sort ==1.0.0.0, - sorted-list ==0.2.2.0, - sourcemap ==0.1.7, - sox ==0.2.3.2, - SpatialMath ==0.2.7.1, - speculate ==0.4.20, - specup ==0.2.0.1, - speedy-slice ==0.3.2, - splice ==0.6.1.1, - split ==0.2.5, - splitmix ==0.1.0.5, - splitmix-distributions ==1.0.0, - Spock-api ==0.14.0.0, - spoon ==0.3.1, - spreadsheet ==0.1.3.10, - sqids ==0.2.2.0, - sql-words ==0.1.6.5, - squeather ==0.8.0.0, - srcloc ==0.6.0.1, - srtree ==1.0.0.5, - stache ==2.3.4, - stack ==2.13.1, - stamina ==0.1.0.3, - state-codes ==0.1.3, - stateref ==0.3, - statestack ==0.3.1.1, - StateVar ==1.2.2, - stateWriter ==0.4.0, - static-bytes ==0.1.0, - static-text ==0.2.0.7, - statistics ==0.16.2.1, - statistics-linreg ==0.3, - status-notifier-item ==0.3.1.0, - step-function ==0.2.0.1, - stitch ==0.6.0.0, - stm ==2.5.2.1, - stm-chans ==3.0.0.9, - stm-conduit ==4.0.1, - stm-containers ==1.2.0.3, - stm-delay ==0.1.1.1, - stm-extras ==0.1.0.3, - stm-hamt ==1.2.0.14, - STMonadTrans ==0.4.8, - stm-split ==0.0.2.1, - stm-supply ==0.2.0.0, - storable-complex ==0.2.3.0, - storable-endian ==0.2.6.1, - storable-record ==0.0.7, - storable-tuple ==0.1, - storablevector ==0.2.13.2, - store ==0.7.18, - store-core ==0.4.4.7, - store-streaming ==0.2.0.5, - stratosphere ==0.60.0, - Stream ==0.4.7.2, - streaming ==0.2.4.0, - streaming-attoparsec ==1.0.0.1, - streaming-bytestring ==0.3.2, - streaming-commons ==0.2.2.6, - streaming-wai ==0.1.1, - streamly ==0.10.1, - streamly-core ==0.2.2, - streamly-process ==0.3.1, - streams ==3.3.2, - strict ==0.5, - strict-base-types ==0.8, - strict-concurrency ==0.2.4.3, - strict-lens ==0.4.0.3, - strict-list ==0.1.7.4, - strict-tuple ==0.1.5.3, - strict-wrapper ==0.0.0.0, - stringable ==0.1.3, - stringbuilder ==0.5.1, - string-combinators ==0.6.0.5, - string-conv ==0.2.0, - string-conversions ==0.4.0.1, - string-interpolate ==0.3.3.0, - string-qq ==0.0.6, - stringsearch ==0.3.6.6, - string-transform ==1.1.1, - strive ==6.0.0.11, - structs ==0.1.9, - structured ==0.1.1, - stylish-haskell ==0.14.6.0, - subcategories ==0.2.1.0, - sundown ==0.6, - svg-builder ==0.1.1, - svg-tree ==0.6.2.4, - swagger2 ==2.8.8, - swish ==0.10.7.0, - syb ==0.7.2.4, - sydtest ==0.15.1.1, - sydtest-aeson ==0.1.0.0, - sydtest-amqp ==0.1.0.0, - sydtest-autodocodec ==0.0.0.0, - sydtest-discover ==0.0.0.4, - sydtest-hedgehog ==0.4.0.0, - sydtest-hedis ==0.0.0.0, - sydtest-hspec ==0.4.0.2, - sydtest-mongo ==0.0.0.0, - sydtest-persistent ==0.0.0.2, - sydtest-persistent-sqlite ==0.2.0.3, - sydtest-process ==0.0.0.0, - sydtest-rabbitmq ==0.1.0.0, - sydtest-servant ==0.2.0.2, - sydtest-typed-process ==0.0.0.0, - sydtest-wai ==0.2.0.1, - sydtest-webdriver ==0.0.0.1, - sydtest-webdriver-screenshot ==0.0.0.2, - sydtest-webdriver-yesod ==0.0.0.1, - sydtest-yesod ==0.3.0.2, - symbol ==0.2.4, - symengine ==0.1.2.0, - symmetry-operations-symbols ==0.0.2.1, - synthesizer-core ==0.8.3, - synthesizer-dimensional ==0.8.1.1, - synthesizer-midi ==0.6.1.2, - sysinfo ==0.1.1, - system-argv0 ==0.1.1, - systemd ==2.3.0, - system-fileio ==0.3.16.4, - system-filepath ==0.4.14, - system-info ==0.5.2, - system-linux-proc ==0.1.1.1, - tabular ==0.2.2.8, - tagchup ==0.4.1.2, - tagged ==0.8.8, - tagged-binary ==0.2.0.1, - tagged-identity ==0.1.4, - tagged-transformer ==0.8.2, - tagsoup ==0.14.8, - tagstream-conduit ==0.5.6, - tao ==1.0.0, - tao-example ==1.0.0, - tar ==0.5.1.1, - tar-conduit ==0.4.1, - tardis ==0.5.0, - tasty ==1.5, - tasty-ant-xml ==1.1.9, - tasty-autocollect ==0.4.2, - tasty-bench ==0.3.5, - tasty-bench-fit ==0.1, - tasty-dejafu ==2.1.0.1, - tasty-discover ==5.0.0, - tasty-expected-failure ==0.12.3, - tasty-fail-fast ==0.0.3, - tasty-focus ==1.0.1, - tasty-golden ==2.3.5, - tasty-hedgehog ==1.4.0.2, - tasty-hslua ==1.1.1, - tasty-hspec ==1.2.0.4, - tasty-html ==0.4.2.2, - tasty-hunit ==0.10.1, - tasty-inspection-testing ==0.2.1, - tasty-kat ==0.0.3, - tasty-leancheck ==0.0.2, - tasty-lua ==1.1.1, - tasty-program ==1.1.0, - tasty-quickcheck ==0.10.3, - tasty-rerun ==1.1.19, - tasty-silver ==3.3.1.3, - tasty-smallcheck ==0.8.2, - tasty-tap ==0.1.0, - tasty-th ==0.1.7, - tasty-wai ==0.1.2.0, - tce-conf ==1.3, - tdigest ==0.3, - teardown ==0.5.0.1, - telegram-bot-api ==7.0, - telegram-bot-simple ==0.13, - tempgres-client ==1.0.0, - template ==0.2.0.10, - template-haskell ==2.21.0.0, - template-haskell-compat-v0208 ==0.1.9.3, - temporary ==1.3, - temporary-rc ==1.2.0.3, - temporary-resourcet ==0.1.0.1, - tensorflow-test ==0.1.0.0, - termbox ==2.0.0.1, - termbox-banana ==2.0.0, - termbox-bindings-c ==0.1.0.1, - termbox-bindings-hs ==1.0.0, - termbox-tea ==1.0.0, - terminal-progress-bar ==0.4.2, - terminal-size ==0.3.4, - terminfo ==0.4.1.6, - test-framework ==0.8.2.0, - test-framework-hunit ==0.3.0.2, - test-framework-leancheck ==0.0.4, - test-framework-quickcheck2 ==0.3.0.5, - test-framework-smallcheck ==0.2, - test-fun ==0.1.0.0, - testing-feat ==1.1.1.1, - testing-type-modifiers ==0.1.0.1, - texmath ==0.12.8.7, - text ==2.1.1, - text-ansi ==0.3.0.1, - text-binary ==0.2.1.1, - text-builder ==0.6.7.2, - text-builder-dev ==0.3.4.2, - text-builder-linear ==0.1.2, - text-conversions ==0.3.1.1, - text-iso8601 ==0.1, - text-latin1 ==0.3.1, - text-ldap ==0.1.1.14, - textlocal ==0.1.0.5, - text-manipulate ==0.3.1.0, - text-metrics ==0.3.2, - text-postgresql ==0.0.3.1, - text-printer ==0.5.0.2, - text-rope ==0.2, - text-short ==0.1.5, - text-show ==3.10.4, - text-show-instances ==3.9.7, - text-zipper ==0.13, - tfp ==1.0.2, - tf-random ==0.5, - th-abstraction ==0.6.0.0, - th-bang-compat ==0.0.1.0, - th-compat ==0.1.5, - th-constraint-compat ==0.0.1.0, - th-data-compat ==0.1.3.1, - th-desugar ==1.16, - th-env ==0.1.1, - these ==1.2, - these-lens ==1.0.1.3, - these-optics ==1.0.1.2, - th-expand-syns ==0.4.11.0, - th-extras ==0.0.0.7, - th-lego ==0.3.0.3, - th-lift ==0.8.4, - th-lift-instances ==0.1.20, - th-nowq ==0.1.0.5, - th-orphans ==0.13.14, - th-printf ==0.8, - thread-hierarchy ==0.3.0.2, - thread-local-storage ==0.2, - threads ==0.5.1.8, - threads-extras ==0.1.0.3, - thread-supervisor ==0.2.0.0, - threepenny-gui ==0.9.4.1, - th-reify-compat ==0.0.1.5, - th-reify-many ==0.1.10, - th-strict-compat ==0.1.0.1, - th-test-utils ==1.2.1, - th-utilities ==0.2.5.0, - tidal ==1.9.4, - tidal-link ==1.0.2, - tile ==0.3.0.0, - time ==1.12.2, - time-compat ==1.9.6.1, - time-domain ==0.1.0.3, - timeit ==2.0, - time-lens ==0.4.0.2, - timelens ==0.2.0.2, - time-locale-compat ==0.1.1.5, - time-locale-vietnamese ==1.0.0.0, - time-manager ==0.0.1, - timerep ==2.1.0.0, - timers-tick ==0.5.0.4, - timer-wheel ==1.0.0, - timespan ==0.4.0.0, - time-units ==1.0.0, - time-units-types ==0.2.0.1, - timezone-olson ==0.2.1, - timezone-olson-th ==0.1.0.11, - timezone-series ==0.1.13, - titlecase ==1.0.1, - tldr ==0.9.2, - tls ==2.0.1, - tls-session-manager ==0.0.5, - tlynx ==0.7.2.2, - tmapchan ==0.0.3, - tmapmvar ==0.0.4, - tmp-proc ==0.6.1.0, - tmp-proc-rabbitmq ==0.6.0.1, - tmp-proc-redis ==0.6.0.1, - token-bucket ==0.1.0.1, - toml-parser ==2.0.0.0, - toml-reader ==0.2.1.0, - toml-reader-parse ==0.1.1.1, - tophat ==1.0.7.0, - topograph ==1.0.0.2, - torrent ==10000.1.3, - torsor ==0.1.0.1, - tracing ==0.0.7.4, - transaction ==0.1.1.4, - transformers ==0.6.1.0, - transformers-base ==0.4.6, - transformers-compat ==0.7.2, - transformers-either ==0.1.4, - traverse-with-class ==1.0.1.1, - tree-diff ==0.3.0.1, - tree-fun ==0.8.1.0, - tree-view ==0.5.1, - trie-simple ==0.4.2, - trifecta ==2.1.4, - trimdent ==0.1.0.0, - trivial-constraint ==0.7.0.0, - tsv2csv ==0.1.0.2, - ttc ==1.4.0.0, - ttrie ==0.1.2.2, - tuple ==0.3.0.2, - tuples ==0.1.0.0, - tuples-homogenous-h98 ==0.1.1.0, - tuple-sop ==0.3.1.0, - tuple-th ==0.2.5, - turtle ==1.6.2, - twitter-types ==0.11.0, - twitter-types-lens ==0.11.0, - typecheck-plugin-nat-simple ==0.1.0.9, - typed-process ==0.2.11.1, - typed-uuid ==0.2.0.0, - type-equality ==1, - type-errors ==0.2.0.2, - type-flip ==0.1.0.0, - type-fun ==0.1.3, - type-hint ==0.1, - type-level-integers ==0.0.1, - type-level-kv-list ==2.0.2.0, - type-level-natural-number ==2.0, - type-level-numbers ==0.1.1.2, - typelits-witnesses ==0.4.0.1, - type-map ==0.1.7.0, - type-natural ==1.3.0.1, - typenums ==0.1.4, - type-of-html ==1.6.2.0, - type-of-html-static ==0.1.0.2, - type-rig ==0.1, - type-set ==0.1.0.0, - type-spec ==0.4.0.0, - typography-geometry ==1.0.1.0, - typst ==0.5.0.2, - typst-symbols ==0.1.5, - tz ==0.1.3.6, - tzdata ==0.2.20240201.0, - tztime ==0.1.1.0, - uglymemo ==0.1.0.1, - ulid ==0.3.2.0, - unagi-chan ==0.4.1.4, - unbounded-delays ==0.1.1.1, - unboxed-ref ==0.4.0.0, - unboxing-vector ==0.2.0.0, - uncaught-exception ==0.1.0, - unconstrained ==0.1.0.2, - unexceptionalio ==0.5.1, - unexceptionalio-trans ==0.5.2, - unicode ==0.0.1.1, - unicode-collation ==0.1.3.6, - unicode-data ==0.4.0.1, - unicode-show ==0.1.1.1, - unicode-transforms ==0.4.0.1, - unidecode ==0.1.0.4, - union-angle ==0.1.0.1, - union-color ==0.1.2.1, - unipatterns ==0.0.0.0, - uniplate ==1.6.13, - unique ==0.0.1, - unique-logic ==0.4.0.1, - unique-logic-tf ==0.5.1, - unit-constraint ==0.0.0, - units-parser ==0.1.1.5, - universe ==1.2.2, - universe-base ==1.1.3.1, - universe-dependent-sum ==1.3, - universe-instances-extended ==1.1.3, - universe-reverse-instances ==1.1.1, - universe-some ==1.2.1, - universum ==1.8.2.1, - unix ==2.8.4.0, - unix-bytestring ==0.4.0.1, - unix-compat ==0.7.1, - unix-time ==0.4.12, - unjson ==0.15.4, - unlifted ==0.2.2.0, - unliftio ==0.2.25.0, - unliftio-core ==0.2.1.0, - unliftio-path ==0.0.2.0, - unliftio-pool ==0.4.3.0, - unliftio-streams ==0.2.0.0, - unlit ==0.4.0.0, - unordered-containers ==0.2.20, - unsafe ==0.0, - uri-bytestring ==0.3.3.1, - uri-bytestring-aeson ==0.1.0.8, - uri-encode ==1.5.0.7, - url ==2.1.3, - urlpath ==11.0.2, - users ==0.5.0.0, - users-test ==0.5.0.1, - utf8-light ==0.4.4.0, - utf8-string ==1.0.2, - utility-ht ==0.0.17.1, - uuid ==1.3.15, - uuid-types ==1.0.5.1, - valida ==1.1.0, - valida-base ==0.2.0, - validity ==0.12.0.2, - validity-aeson ==0.2.0.5, - validity-bytestring ==0.4.1.1, - validity-case-insensitive ==0.0.0.0, - validity-containers ==0.5.0.4, - validity-network-uri ==0.0.0.1, - validity-path ==0.4.0.1, - validity-persistent ==0.0.0.0, - validity-primitive ==0.0.0.1, - validity-scientific ==0.2.0.3, - validity-text ==0.3.1.3, - validity-time ==0.5.0.0, - validity-unordered-containers ==0.2.0.3, - validity-uuid ==0.1.0.3, - validity-vector ==0.2.0.3, - valor ==1.0.0.0, - varying ==0.8.1.0, - vault ==0.3.1.5, - vcs-ignore ==0.0.2.0, - vec ==0.5, - vector ==0.13.1.0, - vector-algorithms ==0.9.0.1, - vector-binary-instances ==0.2.5.2, - vector-buffer ==0.4.1, - vector-builder ==0.3.8.5, - vector-bytes-instances ==0.1.1, - vector-extras ==0.2.8.1, - vector-hashtables ==0.1.1.4, - vector-instances ==3.4.2, - vector-mmap ==0.0.3, - vector-rotcev ==0.1.0.2, - vector-sized ==1.6.1, - vector-split ==1.0.0.3, - vector-stream ==0.1.0.1, - vector-th-unbox ==0.2.2, - verset ==0.0.1.9, - versions ==6.0.6, - vformat ==0.14.1.0, - vformat-time ==0.1.0.0, - ViennaRNAParser ==1.3.3, - vinyl ==0.14.3, - vinyl-loeb ==0.0.1.0, - Vis ==0.7.7.0, - vivid-osc ==0.5.0.0, - vivid-supercollider ==0.4.1.2, - void ==0.7.3, - vty ==6.2, - vty-crossplatform ==0.4.0.0, - vty-unix ==0.2.0.0, - wai ==3.2.4, - wai-app-static ==3.1.9, - wai-conduit ==3.0.0.4, - wai-cors ==0.2.7, - wai-enforce-https ==1.0.0.0, - wai-eventsource ==3.0.0, - wai-extra ==3.1.14, - wai-feature-flags ==0.1.0.8, - wai-handler-launch ==3.0.3.1, - wai-logger ==2.4.0, - wai-middleware-bearer ==1.0.3, - wai-middleware-caching ==0.1.0.2, - wai-middleware-caching-lru ==0.1.0.0, - wai-middleware-caching-redis ==0.2.0.0, - wai-middleware-clacks ==0.1.0.1, - wai-middleware-delegate ==0.1.4.1, - wai-middleware-metrics ==0.2.4, - wai-middleware-prometheus ==1.0.0.1, - wai-middleware-static ==0.9.2, - wai-middleware-throttle ==0.3.0.1, - wai-rate-limit ==0.3.0.0, - wai-rate-limit-redis ==0.2.0.1, - wai-saml2 ==0.5, - wai-session ==0.3.3, - wai-slack-middleware ==0.2.0, - wai-transformers ==0.1.0, - wai-websockets ==3.0.1.2, - wakame ==0.1.0.0, - warp ==3.3.31, - warp-tls ==3.4.4, - wave ==0.2.1, - wcwidth ==0.0.2, - webdriver ==0.12.0.0, - webex-teams-api ==0.2.0.1, - webex-teams-conduit ==0.2.0.1, - webgear-core ==1.2.0, - webgear-openapi ==1.2.0, - webgear-server ==1.2.0, - webgear-swagger ==1.2.0, - webgear-swagger-ui ==1.2.0, - webpage ==0.0.5.1, - webrtc-vad ==0.1.0.3, - websockets ==0.13.0.0, - websockets-simple ==0.2.0, - websockets-snap ==0.10.3.1, - weigh ==0.0.17, - welford-online-mean-variance ==0.2.0.0, - wherefrom-compat ==0.1.1.0, - wide-word ==0.1.6.0, - witch ==1.2.1.0, - withdependencies ==0.3.0, - witherable ==0.4.2, - within ==0.2.0.1, - with-location ==0.1.0, - with-utf8 ==1.1.0.0, - witness ==0.6.2, - wizards ==1.0.3, - wl-pprint ==1.2.1, - wl-pprint-annotated ==0.1.0.1, - wl-pprint-text ==1.2.0.2, - word8 ==0.1.3, - word-compat ==0.0.6, - word-trie ==0.3.0, - word-wrap ==0.5, - world-peace ==1.0.2.0, - wrap ==0.0.0, - wraxml ==0.5, - wreq ==0.5.4.3, - wreq-stringless ==0.5.9.1, - writer-cps-transformers ==0.5.6.1, - ws ==0.0.6, - wuss ==2.0.1.7, - x509 ==1.7.7, - x509-store ==1.6.9, - x509-system ==1.6.7, - x509-validation ==1.6.12, - Xauth ==0.1, - xdg-basedir ==0.2.2, - xdg-userdirs ==0.1.0.2, - xeno ==0.6, - xhtml ==3000.2.2.1, - xlsx ==1.1.2.1, - xml ==1.3.14, - xml-basic ==0.1.3.2, - xmlbf ==0.7, - xmlbf-xeno ==0.2.2, - xmlbf-xmlhtml ==0.2.2, - xml-conduit ==1.9.1.3, - xml-conduit-writer ==0.1.1.5, - xmlgen ==0.6.2.2, - xml-hamlet ==0.5.0.2, - xml-helpers ==1.0.0, - xmlhtml ==0.2.5.4, - xml-html-qq ==0.1.0.1, - xml-indexed-cursor ==0.1.1.0, - xml-picklers ==0.3.6, - xml-to-json-fast ==2.0.0, - xml-types ==0.3.8, - xor ==0.0.1.2, - xss-sanitize ==0.3.7.2, - xxhash-ffi ==0.2.0.0, - yaml ==0.11.11.2, - yaml-unscrambler ==0.1.0.19, - Yampa ==0.14.7, - yarn-lock ==0.6.5, - yeshql-core ==4.2.0.0, - yesod ==1.6.2.1, - yesod-auth ==1.6.11.2, - yesod-auth-basic ==0.1.0.3, - yesod-auth-hashdb ==1.7.1.7, - yesod-auth-oauth2 ==0.7.2.0, - yesod-core ==1.6.25.1, - yesod-eventsource ==1.6.0.1, - yesod-form ==1.7.6, - yesod-form-bootstrap4 ==3.0.1.1, - yesod-gitrepo ==0.3.0, - yesod-gitrev ==0.2.2, - yesod-markdown ==0.12.6.13, - yesod-newsfeed ==1.7.0.0, - yesod-page-cursor ==2.0.1.0, - yesod-paginator ==1.1.2.2, - yesod-persistent ==1.6.0.8, - yesod-recaptcha2 ==1.0.2.1, - yesod-routes-flow ==3.0.0.2, - yesod-sitemap ==1.6.0, - yesod-static ==1.6.1.0, - yesod-test ==1.6.16, - yesod-websockets ==0.3.0.3, - yes-precure5-command ==5.5.3, - yi-rope ==0.11, - yjsvg ==0.2.0.1, - yjtools ==0.9.18, - zigzag ==0.1.0.0, - zim-parser ==0.2.1.0, - zip ==2.0.0, - zip-archive ==0.4.3.1, - zippers ==0.3.2, - zip-stream ==0.2.2.0, - zlib ==0.6.3.0, - zlib-bindings ==0.1.1.5, - zstd ==0.1.3.0, + if os(osx) + cpp-options: -DOSX + + if os(windows) + cpp-options: -DWINDOWS + + build-depends: base >=4.16.0.0 && <4.21 + default-language: GHC2021 + +library utils + import: common-lang + exposed-modules: + CLC.Stackage.Utils.Exception + CLC.Stackage.Utils.IO + CLC.Stackage.Utils.JSON + CLC.Stackage.Utils.Logging + CLC.Stackage.Utils.OS + CLC.Stackage.Utils.Paths + + build-depends: + , aeson >=2.0 && <2.3 + , aeson-pretty ^>=0.8.9 + , 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 + , pretty-terminal ^>=0.1.0.0 + , text >=1.2.3.2 && <2.2 + , time >=1.9.3 && <1.15 + + hs-source-dirs: src/utils + +library parser + import: common-lang + exposed-modules: + CLC.Stackage.Parser + CLC.Stackage.Parser.API + CLC.Stackage.Parser.Data.Response + CLC.Stackage.Parser.Query + + build-depends: + , aeson + , bytestring + , containers >=0.6.3.1 && <0.8 + , filepath + , http-client >=0.5.9 && <0.8 + , http-client-tls ^>=0.3 + , http-types ^>=0.12.3 + , text + , utils + + hs-source-dirs: src/parser + +library builder + import: common-lang + exposed-modules: + 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 + , containers + , directory + , filepath + , process ^>=1.6.9.0 + , text + , utils + + hs-source-dirs: src/builder + +library runner + import: common-lang + exposed-modules: + CLC.Stackage.Runner + CLC.Stackage.Runner.Args + CLC.Stackage.Runner.Env + CLC.Stackage.Runner.Report + + build-depends: + , aeson + , builder + , containers + , directory + , filepath + , optparse-applicative >=0.16.1.0 && <0.19 + , parser + , pretty-terminal + , text + , time + , utils + + hs-source-dirs: src/runner + +executable clc-stackage + import: common-lang + main-is: Main.hs + build-depends: + , runner + , terminal-size ^>=0.3.4 + , text + , time + , utils + + hs-source-dirs: ./app + ghc-options: -threaded -with-rtsopts=-N + +test-suite unit + import: common-lang + type: exitcode-stdio-1.0 + main-is: Main.hs + other-modules: + Unit.CLC.Stackage.Runner.Env + Unit.CLC.Stackage.Runner.Report + Unit.Prelude + + build-depends: + , base + , builder + , containers + , filepath + , runner + , tasty >=1.1.0.3 && <1.6 + , tasty-golden ^>=2.3.1.1 + , tasty-hunit >=0.9 && <0.11 + , time + , utils + + hs-source-dirs: test/unit + ghc-options: -threaded -with-rtsopts=-N + +test-suite functional + import: common-lang + type: exitcode-stdio-1.0 + main-is: Main.hs + build-depends: + , base + , builder + , bytestring + , containers + , env-guard ^>=0.2 + , filepath + , runner + , tasty + , tasty-golden + , text + , time + , utils + + hs-source-dirs: test/functional diff --git a/dev.md b/dev.md new file mode 100644 index 0000000..feec12d --- /dev/null +++ b/dev.md @@ -0,0 +1,119 @@ +# Development + +## Introduction + +This project is organized into several libraries and a single executable. Roughly, the idea is: + +1. Download a snapshot `s` from stackage.org. +2. Prune `s` based on packages we know we do not want (e.g. system deps). +3. Generate a custom `generated.cabal` file for the given package set, and try to build it. + +Futhermore, we allow for building subsets of the entire stackage package set with the `--batch` feature. This will split the package set into disjoint groups, and build each group sequentially. The process can be interrupted at any time (e.g. `CTRL-C`), and progress will be saved in a "cache" (json file), so we can pick up where we left off. + +## Components + +### utils + +`utils` is a library containing common utilities e.g. logging and hardcoded file paths. + +### parser + +`parser` contains the parsing functionality. In particular, `parser` is responsible for querying stackage's REST endpoint and retrieving the package set. That package set is then filtered according to [excluded_pkgs.json](excluded_pkgs.json). The primary function is: + +```haskell +-- CLC.Stackage.Parser +getPackageList :: IO [PackageResponse] +``` + +If you want to get the list of the packages to be built (i.e. stackage_snapshot - excluded_packages), load the parser into the repl with `cabal repl parser`, and run the following: + +```haskell +-- CLC.Stackage.Parser +-- printPackageList :: Bool -> Maybe Os -> IO () +λ. printPackageList True Nothing +``` + +This will write the package list used for each OS to `pkgs_.txt`. + +### builder + +`builder` is responsible for building a given package set. The primary functions are: + +```haskell +-- CLC.Stackage.Builder +writeCabalProjectLocal :: NonEmpty Package -> IO () + +batchPackages :: BuildEnv -> NonEmpty (PackageGroup, Int) + +buildProject :: BuildEnv -> Int -> PackageGroup -> IO () +``` + +That is: + +1. `writeCabalProjectLocal` will be called **once** at start-up, and write the entire package set to a `cabal.project.local`'s `constraints` section. This ensures the same transitive deps are used every time. + +2. `batchPackages` splits the entire package set into separate groups, based on the `--batch` value. If `--batch` is not given, then we will have a single group containing every package. + +3. `buildProject` will write the given package group to the generated cabal file, and attempt to build it. + +### runner + +`runner` orchestrates everything. The primary function is: + +```haskell +run :: Logging.Handle -> IO () +``` + +Which takes in a logging handler (we use the handler pattern for testing), and then: + +1. Sets up the environment based on CLI args and previous cache data. +2. Runs the parser and builder until it either finishes or is interrupted. +3. Saves the current progress to the cache. +4. Prints out a summary. + +The reason this logic is a library function and not the executable itself is for testing. + +### clc-stackage + +The executable that actually runs. This is a very thin wrapper over `runner`, which merely sets up the logging handler. + +## Updating to a new shapshot + +1. Update to the desired snapshot: + + ```haskell + -- CLC.Stackage.Parser.API + stackageSnapshot :: String + stackageSnapshot = "nightly-2024-03-26" + ``` + +2. Update the `index-state` in [cabal.project](cabal.project) and [generated/cabal.project](generated/cabal.project). + +3. Modify [excluded_pkgs.json](excluded_pkgs.json) as needed. That is, updating the snapshot will probably bring in some new packages that we do not want. The update process is essentially trial-and-error i.e. run `clc-stackage` as normal, and later add any failing packages that should be excluded. + +4. Update references to the current ghc e.g. + + 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`). + +6. Optional: Update nix inputs (`nix flake update`). + +## Testing + +There are two test suites, `unit` and `functional`. The latter actually runs all of the logic, though it uses the generalized runner: + +```haskell +runModifyPackages :: Logging.Handle -> ([Package] -> [Package]) -> IO () +``` + +This allows us to limit the number of packages to something reasonable to build on CI. + +If the functional tests fail, it can be difficult to see what the actual error is, given that the error message is in the logs, which will be deleted by default. To keep the logs and generated files, run with: + +```sh +$ NO_CLEANUP=1 cabal test functional +``` + +Note that this only saves files from the _last_ test, so if you want to examine test output for a particular test, you need to run only that test. diff --git a/example_output.png b/example_output.png new file mode 100644 index 0000000..9b6f8fe Binary files /dev/null and b/example_output.png differ diff --git a/excluded_pkgs.json b/excluded_pkgs.json new file mode 100644 index 0000000..a7cb212 --- /dev/null +++ b/excluded_pkgs.json @@ -0,0 +1,327 @@ +{ + "all": [ + "agda2lagda", + "al", + "alex", + "align-audio", + "Allure", + "alsa-core", + "alsa-mixer", + "alsa-pcm", + "alsa-seq", + "ALUT", + "amqp-utils", + "arbtt", + "beam-postgres", + "bench", + "bindings-libzip", + "blas-carray", + "blas-comfort-array", + "blas-ffi", + "boomwhacker", + "btrfs", + "buffer-pipe", + "bugsnag", + "bugsnag-wai", + "bugsnag-yesod", + "c2hs", + "cabal-clean", + "cabal-install", + "cabal-install-solver", + "cabal-rpm", + "cabal-sort", + "cabal2nix", + "calendar-recycling", + "Clipboard", + "coinor-clp", + "comfort-blas", + "comfort-fftw", + "comfort-glpk", + "core-telemetry", + "cql-io", + "crackNum", + "cryptonite-openssl", + "cuda", + "cutter", + "dbcleaner", + "diagrams-svg", + "discount", + "dl-fedora", + "doctest-extract", + "drifter-postgresql", + "Ebnf2ps", + "elynx", + "emd", + "equal-files", + "essence-of-live-coding-pulse", + "experimenter", + "fedora-haskell-tools", + "fft", + "fftw-ffi", + "file-modules", + "fix-whitespace", + "flac", + "flac-picture", + "follow-file", + "freckle-app", + "freenect", + "fsnotify", + "fsnotify-conduit", + "gauge", + "gd", + "ghc", + "ghc-core", + "ghc-syntax-highlighter", + "ghostscript-parallel", + "gi-atk", + "gi-cairo", + "gi-cairo-connector", + "gi-cairo-render", + "gi-dbusmenu", + "gi-dbusmenugtk3", + "gi-freetype2", + "gi-gdk", + "gi-gdkpixbuf", + "gi-gdkx11", + "gi-gio", + "gi-glib", + "gi-gmodule", + "gi-gobject", + "gi-graphene", + "gi-gtk", + "gi-gtk-hs", + "gi-gtksource", + "gi-harfbuzz", + "gi-javascriptcore", + "gi-pango", + "gi-soup", + "gi-vte", + "gi-webkit2", + "gi-xlib", + "git-annex", + "git-mediate", + "gl", + "gloss-examples", + "glpk-headers", + "goldplate", + "google-oauth2-jwt", + "gopher-proxy", + "group-by-date", + "gtk", + "gtk-sni-tray", + "gtk-strut", + "gtk2hs-buildtools", + "gtk3", + "H", + "hackage-cli", + "hamtsolo", + "happstack-server-tls", + "happy", + "haskell-gi", + "haskell-gi-base", + "haskell-gi-overloading", + "haskoin-core", + "haskoin-node", + "haskoin-store-data", + "hasql", + "hasql-dynamic-statements", + "hasql-implicits", + "hasql-interpolate", + "hasql-listen-notify", + "hasql-migration", + "hasql-notifications", + "hasql-optparse-applicative", + "hasql-pool", + "hasql-queue", + "hasql-th", + "hasql-transaction", + "haxr", + "hinotify", + "hkgr", + "hledger-interest", + "hledger-ui", + "hlibgit2", + "hmatrix-gsl", + "hmatrix-gsl-stats", + "hmatrix-special", + "hmm-lapack", + "hmpfr", + "hopenssl", + "hp2pretty", + "hpqtypes", + "hpqtypes-extras", + "hruby", + "hs-GeoIP", + "hsc2hs", + "hsdns", + "hsignal", + "hsndfile", + "hsndfile-vector", + "HsOpenSSL", + "HsOpenSSL-x509-system", + "hsshellscript", + "hstatistics", + "htaglib", + "http-client-openssl", + "http-io-streams", + "http-streams", + "hw-json-simd", + "hw-kafka-client", + "hwk", + "ihaskell", + "ihaskell-hvega", + "ihs", + "Imlib", + "inline-r", + "ip6addr", + "ipython-kernel", + "jack", + "jailbreak-cabal", + "java-adt", + "JuicyPixels-scale-dct", + "koji", + "koji-tool", + "krank", + "LambdaHack", + "lame", + "lapack", + "lapack-carray", + "lapack-comfort-array", + "lapack-ffi", + "lapack-ffi-tools", + "lapack-hmatrix", + "lens-regex-pcre", + "lentil", + "leveldb-haskell", + "liboath-hs", + "linear-circuit", + "linux-file-extents", + "linux-namespaces", + "lmdb", + "lzma-clib", + "magic", + "magico", + "mbox-utility", + "mega-sdist", + "midi-alsa", + "midi-music-box", + "misfortune", + "mmark-cli", + "moffy-samples-gtk3", + "moffy-samples-gtk3-run", + "mpi-hs", + "mpi-hs-binary", + "mpi-hs-cereal", + "mstate", + "mysql", + "mysql-json-table", + "mysql-simple", + "nanovg", + "netcode-io", + "nfc", + "NineP", + "nix-paths", + "nvvm", + "ods2csv", + "opaleye", + "openssl-streams", + "OrderedBits", + "pagure-cli", + "pandoc-cli", + "parser-combinators-tests", + "pcre-heavy", + "pcre-light", + "peregrin", + "perf", + "persistent-mysql", + "persistent-postgresql", + "pg-transact", + "pkgtreediff", + "place-cursor-at", + "postgresql-libpq-notify", + "postgresql-migration", + "postgresql-schema", + "postgresql-simple", + "postgresql-simple-url", + "primecount", + "profiterole", + "psql-helpers", + "pthread", + "pulse-simple", + "rawfilepath", + "rdtsc", + "re2", + "reactive-balsa", + "reactive-jack", + "reanimate-svg", + "regex-pcre", + "rel8", + "resistor-cube", + "rex", + "rhbzquery", + "rocksdb-haskell", + "rocksdb-haskell-jprupp", + "rocksdb-query", + "scrypt", + "sdl2", + "sdl2-gfx", + "sdl2-image", + "sdl2-mixer", + "sdl2-ttf", + "secp256k1-haskell", + "seqalign", + "servant-http-streams", + "servius", + "ses-html", + "shelltestrunner", + "sound-collage", + "soxlib", + "split-record", + "sqlcli", + "sqlcli-odbc", + "sqlite-simple", + "stack-all", + "stack-clean-old", + "stack-templatizer", + "stringprep", + "SVGFonts", + "sydtest-persistent-postgresql", + "synthesizer-alsa", + "termonad", + "test-certs", + "text-icu", + "text-regex-replace", + "tls-debug", + "tmp-postgres", + "tmp-proc-postgres", + "ua-parser", + "uniq-deep", + "users-postgresql-simple", + "validate-input", + "wai-session-postgresql", + "Win32", + "Win32-notify", + "windns", + "X11", + "X11-xft", + "x11-xim", + "xmonad", + "xmonad-contrib", + "xmonad-extras", + "yesod-bin", + "yoga", + "youtube", + "zeromq4-haskell", + "zeromq4-patterns", + "zot", + "ztail" + ], + "linux": [ + "hfsevents" + ], + "osx": [], + "windows": [ + "hfsevents", + "postgresql-libpq" + ] +} diff --git a/flake.nix b/flake.nix index e691f97..e2f1807 100644 --- a/flake.nix +++ b/flake.nix @@ -24,8 +24,14 @@ }; nixpkgs.follows = "ghc_nix/nixpkgs"; - # Temporarily include a newer version of nixpkgs so that we get the newer - # cabal-install 3.12 and can use jsem. + # Temporarily include a newer version of nixpkgs so that we can use GHC + # 9.8.2. This newer nixpkgs also contains cabal-install 3.12, which allows + # us to use the new --semaphore option, but unfortunately breaks Agda: + # + # /~https://github.com/agda/agda/pull/7471 + # + # Once we have the Agda fix (version 2.7.0.1), we can use cabal-install + # 3.12. nixpkgs_new.url = "github:nixos/nixpkgs/nixos-unstable"; }; @@ -71,7 +77,7 @@ # The comments indicate haskell packages that require the given # dependency. This is not exhaustive. deps = with pkgs; [ - pkgs_new.cabal-install + cabal-install curl # curl fribidi # simple-pango libdatrie # simple-pango @@ -85,6 +91,7 @@ pango # simple-pango pcre2 # simple-cairo pkg-config + postgresql_16 # postgresql-libpq systemdMinimal # hidapi requires udev util-linux # simple-pango requires mount xorg.libXdmcp # simple-cairo @@ -117,6 +124,16 @@ ''; }; + dev = pkgs.mkShell { + buildInputs = [ + compiler.ghc + compiler.haskell-language-server + compiler.ormolu + pkgs.cabal-install + pkgs.zlib + ]; + }; + # This shell is useless wrt the intended purpose of this repo, # as the whole point is building stackage w/ a custom GHC. It exists # primarily to test that there is nothing wrong with the package set diff --git a/generated/cabal.project b/generated/cabal.project new file mode 100644 index 0000000..e3d42dc --- /dev/null +++ b/generated/cabal.project @@ -0,0 +1,42 @@ +index-state: 2024-10-11T23:26:13Z + +-- should be an absolute path to your custom compiler +--with-compiler: /home/ghc/_build/stage1/bin/ghc + +packages: . + +optimization: False + +allow-newer: + amazonka-apigatewaymanagementapi:base, + amazonka-appconfigdata:base, + amazonka-backupstorage:base, + amazonka-cloudhsm:base, + amazonka-controltower:base, + amazonka-core:base, + amazonka-ec2-instance-connect:base, + amazonka-finspace:base, + amazonka-forecastquery:base, + amazonka-importexport:base, + amazonka-iot-dataplane:base, + amazonka-iotfleethub:base, + amazonka-iotthingsgraph:base, + amazonka-kinesis-video-signaling:base, + amazonka-kinesis-video-webrtc-storage:base, + amazonka-marketplace-analytics:base, + amazonka-marketplace-metering:base, + amazonka-migrationhub-config:base, + amazonka-personalize-runtime:base, + amazonka-rbin:base, + amazonka-s3outposts:base, + amazonka-sagemaker-a2i-runtime:base, + amazonka-sagemaker-metrics:base, + amazonka-sso-oidc:base, + amazonka-worklink:base, + amazonka-workmailmessageflow:base, + aura:bytestring, + aura:time + +constraints: hlint +ghc-lib +constraints: ghc-lib-parser-ex -auto +constraints: stylish-haskell +ghc-lib diff --git a/src/Lib.hs b/generated/src/Lib.hs similarity index 100% rename from src/Lib.hs rename to generated/src/Lib.hs diff --git a/src/builder/CLC/Stackage/Builder.hs b/src/builder/CLC/Stackage/Builder.hs new file mode 100644 index 0000000..6e79c55 --- /dev/null +++ b/src/builder/CLC/Stackage/Builder.hs @@ -0,0 +1,13 @@ +module CLC.Stackage.Builder + ( -- * Primary + Process.buildProject, + Writer.writeCabalProjectLocal, + + -- * Misc + Batch.batchPackages, + ) +where + +import CLC.Stackage.Builder.Batch qualified as Batch +import CLC.Stackage.Builder.Process qualified as Process +import CLC.Stackage.Builder.Writer qualified as Writer diff --git a/src/builder/CLC/Stackage/Builder/Batch.hs b/src/builder/CLC/Stackage/Builder/Batch.hs new file mode 100644 index 0000000..8d5fe13 --- /dev/null +++ b/src/builder/CLC/Stackage/Builder/Batch.hs @@ -0,0 +1,61 @@ +module CLC.Stackage.Builder.Batch + ( PackageGroup (..), + batchPackages, + ) +where + +import CLC.Stackage.Builder.Env + ( BuildEnv + ( batch, + packagesToBuild + ), + ) +import CLC.Stackage.Builder.Package (Package) +import Data.Bifunctor (Bifunctor (first)) +import Data.List qualified as L +import Data.List.NonEmpty (NonEmpty ((:|)), (<|)) +import Data.List.NonEmpty qualified as NE + +-- | A (non-proper) subset of the original stackage package set. +newtype PackageGroup = MkPackageGroup {unPackageGroup :: NonEmpty Package} + deriving stock (Eq, Show) + +-- | Split entire package set from the BuildEnv into package groups. +batchPackages :: BuildEnv -> NonEmpty (PackageGroup, Int) +batchPackages env = first MkPackageGroup <$> pkgGroupsIdx + where + pkgGroups = case env.batch of + Nothing -> NE.singleton env.packagesToBuild + Just n -> chunksOf n env.packagesToBuild + + -- NOTE: NE.fromList is obviously unsafe in general, but here it is fine + -- as 'n := length pkgGroups > 0' and [1 .. n] is non-empty such n. + indexes = NE.fromList $ reverse [1 .. length pkgGroups] + pkgGroupsIdx = NE.zip pkgGroups indexes + +chunksOf :: Int -> NonEmpty a -> NonEmpty (NonEmpty a) +chunksOf n = go + where + go xs = case splitAtNE n xs of + This ys -> NE.singleton ys + That _ -> e + These ys zs -> ys <| go zs + e = error "chunksOf: A non-empty can only be broken up into positively-sized chunks." +{-# INLINEABLE chunksOf #-} + +splitAtNE :: Int -> NonEmpty a -> These (NonEmpty a) (NonEmpty a) +splitAtNE n xs0@(x :| xs) + | n <= 0 = That xs0 + | otherwise = case (NE.nonEmpty ys, NE.nonEmpty zs) of + (Nothing, Nothing) -> This (NE.singleton x) + (Just _, Nothing) -> This xs0 + (Nothing, Just zs') -> These (NE.singleton x) zs' + (Just ys', Just zs') -> These (x <| ys') zs' + where + (ys, zs) = L.splitAt (n - 1) xs +{-# INLINEABLE splitAtNE #-} + +data These a b + = This a + | That b + | These a b diff --git a/src/builder/CLC/Stackage/Builder/Env.hs b/src/builder/CLC/Stackage/Builder/Env.hs new file mode 100644 index 0000000..3fb5475 --- /dev/null +++ b/src/builder/CLC/Stackage/Builder/Env.hs @@ -0,0 +1,91 @@ +-- | Provides the environment for building. +module CLC.Stackage.Builder.Env + ( BuildEnv (..), + CabalVerbosity (..), + cabalVerbosityToArg, + Jobs (..), + jobsToArg, + Progress (..), + WriteLogs (..), + ) +where + +import CLC.Stackage.Builder.Package (Package) +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. + successesRef :: IORef (Set Package), + -- | Dependencies that failed to build. + failuresRef :: IORef (Set Package) + } + +-- | Determines what cabal output to write +data WriteLogs + = -- | No logs written. + WriteLogsNone + | -- | Current logs written (overwritten). + WriteLogsCurrent + | -- | Current logs written with failures saved. + WriteLogsSaveFailures + deriving stock (Eq, Show) + +-- | Environment for the builder. +data BuildEnv = MkBuildEnv + { -- | If we have @Just n@, 'packagesToBuild' will be split into groups of at most + -- size @n@. If @Nothing@, the entire set will be built in one go. + batch :: Maybe Int, + -- | Build arguments for cabal. + buildArgs :: [String], + -- | If true, colors logs. + colorLogs :: Bool, + -- | If true, the first group that fails to completely build stops + -- clc-stackage. Defaults to false. + groupFailFast :: Bool, + -- | Logging handler + hLogger :: Logging.Handle, + -- | All packages that are to be built during the entire run. This may + -- be split into groups, if 'batch' exists. + packagesToBuild :: NonEmpty Package, + -- | Status for this run. + progress :: Progress, + -- | Determines logging behavior. + writeLogs :: Maybe WriteLogs + } diff --git a/src/builder/CLC/Stackage/Builder/Package.hs b/src/builder/CLC/Stackage/Builder/Package.hs new file mode 100644 index 0000000..9c2c558 --- /dev/null +++ b/src/builder/CLC/Stackage/Builder/Package.hs @@ -0,0 +1,65 @@ +{-# 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 new file mode 100644 index 0000000..9bf88c3 --- /dev/null +++ b/src/builder/CLC/Stackage/Builder/Process.hs @@ -0,0 +1,138 @@ +{-# LANGUAGE QuasiQuotes #-} + +module CLC.Stackage.Builder.Process + ( buildProject, + ) +where + +import CLC.Stackage.Builder.Batch (PackageGroup (unPackageGroup)) +import CLC.Stackage.Builder.Env + ( BuildEnv + ( colorLogs, + groupFailFast, + hLogger, + progress, + writeLogs + ), + Progress (failuresRef, successesRef), + 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.Paths qualified as Paths +import Control.Exception (throwIO) +import Control.Monad (when) +import Data.IORef (modifyIORef') +import Data.List.NonEmpty qualified as NE +import Data.Set qualified as Set +import Data.Text qualified as T +import System.Directory.OsPath qualified as Dir +import System.Directory.OsPath qualified as OsPath +import System.Exit (ExitCode (ExitFailure, ExitSuccess)) +import System.OsPath (OsPath, osp, ()) +import System.Process qualified as P + +-- | Given the build environment, index, and package group, writes the package +-- group to the cabal file and attempts to build it. The index is for logging. +buildProject :: BuildEnv -> Int -> PackageGroup -> IO () +buildProject env idx pkgs = do + -- write the package group to the cabal file + Writer.writeCabal pkgs + + let buildNoLogs :: IO ExitCode + buildNoLogs = + withGeneratedDir $ + (\(ec, _, _) -> ec) <$> P.readProcessWithExitCode "cabal" env.buildArgs "" + + buildLogs :: Bool -> IO ExitCode + buildLogs saveFailures = do + (dirPath, stdoutPath, stderrPath) <- createCurrentLogsDir + + IO.withBinaryFileWriteMode stdoutPath $ \stdoutHandle -> + IO.withBinaryFileWriteMode stderrPath $ \stderrHandle -> do + let createProc = P.proc "cabal" env.buildArgs + createProc' = + createProc + { P.std_out = P.UseHandle stdoutHandle, + P.std_err = P.UseHandle stderrHandle, + P.close_fds = False + } + + ec <- + withGeneratedDir $ + P.withCreateProcess createProc' (\_ _ _ ph -> P.waitForProcess ph) + case ec of + -- Build dir will be overwritten next run anyway, so no need + -- to do anything + ExitSuccess -> pure () + -- Rename build dir to pkg specific name + ExitFailure _ -> when saveFailures $ do + pkgsDirPath <- getPkgGroupLogsDirPath pkgs + + -- Unnecessary with "normal" usage, as the only time + -- pkgsDirPath exists is if we are retrying a previous failure, + -- in which case the entire logs dir would have been deleted + -- on startup (see NOTE: [Remove old logs]). + -- + -- That said, it is occasionally useful to manually manipulate + -- the cache e.g. we want to retry a single package, so we + -- manually move the cache entry from 'failed' to 'untested'. + -- + -- In this case, the directory will not have been deleted, + -- hence we do it here. + IO.removeDirectoryRecursiveIfExists pkgsDirPath + + Dir.renamePath dirPath pkgsDirPath + + pure ec + + exitCode <- + case env.writeLogs of + Just WriteLogsNone -> buildNoLogs + Just WriteLogsCurrent -> buildLogs False + Just WriteLogsSaveFailures -> buildLogs True + Nothing -> buildLogs True + + case exitCode of + ExitSuccess -> do + -- save results + modifyIORef' env.progress.successesRef addPackages + Logging.putTimeSuccessStr env.hLogger env.colorLogs msg + ExitFailure _ -> do + -- save results + modifyIORef' env.progress.failuresRef addPackages + Logging.putTimeErrStr env.hLogger env.colorLogs msg + + -- throw error if fail fast + when env.groupFailFast $ throwIO exitCode + where + withGeneratedDir = OsPath.withCurrentDirectory Paths.generatedDir + + msg = + mconcat + [ T.pack $ show idx, + ": ", + T.intercalate ", " (Package.toText <$> pkgsList) + ] + pkgsList = NE.toList pkgs.unPackageGroup + pkgsSet = Set.fromList pkgsList + + addPackages = Set.union pkgsSet + +createCurrentLogsDir :: IO (OsPath, OsPath, OsPath) +createCurrentLogsDir = do + let dirPath = Paths.logsDir [osp|current-build|] + stdoutPath = dirPath [osp|stdout.log|] + stderrPath = dirPath [osp|stderr.log|] + + Dir.createDirectoryIfMissing True dirPath + pure (dirPath, stdoutPath, stderrPath) + +-- Name the dir after the first package in the group +getPkgGroupLogsDirPath :: PackageGroup -> IO OsPath +getPkgGroupLogsDirPath pkgs = do + dirName <- Package.toDirName . NE.head $ pkgs.unPackageGroup + pure $ Paths.logsDir dirName diff --git a/src/builder/CLC/Stackage/Builder/Writer.hs b/src/builder/CLC/Stackage/Builder/Writer.hs new file mode 100644 index 0000000..4bb2f24 --- /dev/null +++ b/src/builder/CLC/Stackage/Builder/Writer.hs @@ -0,0 +1,67 @@ +module CLC.Stackage.Builder.Writer + ( writeCabal, + writeCabalProjectLocal, + ) +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.Paths qualified as Paths +import Data.List.NonEmpty qualified as NE +import Data.Text (Text) +import Data.Text qualified as T +import Data.Text.Encoding qualified as TEnc + +-- | Writes a cabal.project.local for the _entire_ package set. This should +-- only be called once, regardless of the number of builds. The purpose of +-- this function is it to ensure we always use the same transitive dependencies +-- +-- For example, suppose our snapshot contains aeson-2.2.3.0. When aeson is +-- in the group, there are no problems, since we will write the exact version +-- in the cabal file in 'writeCabal'. But if aeson is _not_ in the group but +-- a __transitive dependency__, then we are at the mercy of cabal's constraint +-- solver. +-- +-- By writing the entire (exact) dependency set into the cabal.project.local's +-- constraints section, we ensure the same version of aeson is used every time +-- it is a (transitive) dependency. +writeCabalProjectLocal :: [Package] -> IO () +writeCabalProjectLocal pkgs = IO.writeBinaryFile path constraintsSrc + where + path = Paths.generatedCabalProjectLocalPath + constraintsSrc = TEnc.encodeUtf8 constraintsTxt + constraintsTxt = T.unlines $ "constraints:" : constraints + constraints = (\p -> " " <> Package.toText p <> ",") <$> pkgs + +-- | Writes the package set to a cabal file for building. This will be called +-- for each group we want to build. +writeCabal :: PackageGroup -> IO () +writeCabal pkgs = IO.writeBinaryFile Paths.generatedCabalPath cabalFileSrc + where + cabalFileSrc = TEnc.encodeUtf8 cabalFileTxt + cabalFileTxt = mkCabalFile pkgs + +mkCabalFile :: PackageGroup -> Text +mkCabalFile pkgs = + T.unlines $ + [ "cabal-version: 3.0", + "name: generated", + "version: 0.1.0.0", + "build-type: Simple", + "", + "library", + " exposed-modules: Lib", + " build-depends:" + ] + <> pkgsTxt + <> [ " hs-source-dirs: src", + " default-language: Haskell2010" + ] + where + pkgsTxt = (\p -> pkgsIndent <> Package.toDepText p) <$> NE.toList pkgs.unPackageGroup + +-- build-depends is indented 4, then 2 for the package itself. +pkgsIndent :: Text +pkgsIndent = T.replicate 6 " " diff --git a/src/parser/CLC/Stackage/Parser.hs b/src/parser/CLC/Stackage/Parser.hs new file mode 100644 index 0000000..4c021c7 --- /dev/null +++ b/src/parser/CLC/Stackage/Parser.hs @@ -0,0 +1,99 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE QuasiQuotes #-} + +module CLC.Stackage.Parser + ( -- * Retrieving packages + getPackageList, + + -- * Misc helpers + printPackageList, + getPackageListByOsFmt, + ) +where + +import CLC.Stackage.Parser.Data.Response + ( PackageResponse (name, version), + StackageResponse (packages), + ) +import CLC.Stackage.Parser.Query qualified as Query +import CLC.Stackage.Utils.IO qualified as IO +import CLC.Stackage.Utils.JSON qualified as JSON +import CLC.Stackage.Utils.OS (Os (Linux, Osx, Windows)) +import CLC.Stackage.Utils.OS qualified as OS +import Data.Aeson (FromJSON, ToJSON) +import Data.Foldable (for_) +import Data.Set (Set) +import Data.Set qualified as Set +import Data.Text (Text) +import Data.Text qualified as T +import GHC.Generics (Generic) +import System.OsPath (osp) + +-- | Retrieves the list of packages, based on +-- 'CLC.Stackage.Parser.API.stackageUrl'. +getPackageList :: IO [PackageResponse] +getPackageList = getPackageListByOs OS.currentOs + +-- | Prints the package list to a file. +printPackageList :: Bool -> Maybe Os -> IO () +printPackageList incVers mOs = do + case mOs of + Just os -> printOsList os + Nothing -> for_ [minBound .. maxBound] printOsList + where + file Linux = [osp|pkgs_linux.txt|] + file Osx = [osp|pkgs_osx.txt|] + file Windows = [osp|pkgs_windows.txt|] + + printOsList os = do + pkgs <- getPackageListByOsFmt incVers 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 . getPackageListByOs + 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 :: Os -> IO [PackageResponse] +getPackageListByOs os = do + excludedPkgs <- getExcludedPkgs os + let filterExcluded = flip Set.notMember excludedPkgs . (.name) + + response <- Query.getStackage + + let packages = filter filterExcluded response.packages + + pure packages + +getExcludedPkgs :: Os -> IO (Set Text) +getExcludedPkgs os = do + contents <- IO.readBinaryFile path + + excluded <- case JSON.decode contents of + Left err -> fail err + Right x -> pure x + + pure $ Set.fromList (excluded.all ++ osSel excluded) + where + path = [osp|excluded_pkgs.json|] + + osSel :: Excluded -> [Text] + osSel = case os of + Linux -> (.linux) + Osx -> (.osx) + Windows -> (.windows) + +data Excluded = MkExcluded + { all :: [Text], + linux :: [Text], + osx :: [Text], + windows :: [Text] + } + deriving stock (Eq, Generic, Show) + deriving anyclass (FromJSON, ToJSON) diff --git a/src/parser/CLC/Stackage/Parser/API.hs b/src/parser/CLC/Stackage/Parser/API.hs new file mode 100644 index 0000000..fb793ae --- /dev/null +++ b/src/parser/CLC/Stackage/Parser/API.hs @@ -0,0 +1,36 @@ +-- | REST API for stackage.org. +module CLC.Stackage.Parser.API + ( withResponse, + stackageSnapshot, + ) +where + +import Network.HTTP.Client (BodyReader, Request, Response) +import Network.HTTP.Client qualified as HttpClient +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 + manager <- TLS.newTlsManager + req <- getRequest + HttpClient.withResponse req manager onResponse + +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 + +-- | Stackage snapshot. +stackageSnapshot :: String +stackageSnapshot = "nightly-2024-03-26" diff --git a/src/parser/CLC/Stackage/Parser/Data/Response.hs b/src/parser/CLC/Stackage/Parser/Data/Response.hs new file mode 100644 index 0000000..f270e44 --- /dev/null +++ b/src/parser/CLC/Stackage/Parser/Data/Response.hs @@ -0,0 +1,40 @@ +-- | 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/parser/CLC/Stackage/Parser/Query.hs b/src/parser/CLC/Stackage/Parser/Query.hs new file mode 100644 index 0000000..7343caa --- /dev/null +++ b/src/parser/CLC/Stackage/Parser/Query.hs @@ -0,0 +1,118 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE QuasiQuotes #-} + +module CLC.Stackage.Parser.Query + ( -- * Querying stackage + getStackage, + + -- ** Exceptions + StackageException (..), + ExceptionReason (..), + ) +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.Exception + ( Exception (displayException), + SomeException, + throwIO, + ) +import Control.Monad (when) +import Data.ByteString (ByteString) +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) + + Ex.mapThrowLeft + (mkEx . ReasonDecodeJson bodyBs) + (JSON.decode bodyBs) + +-- | Exception reason. +data ExceptionReason + = -- | Received non-200. + ReasonStatus Status + | -- | Exception when reading the body. + ReasonReadBody SomeException + | -- | Exception decoding JSON. The first string is the json we attempted + -- to decode. The second is the error message. + ReasonDecodeJson ByteString String + deriving stock (Show) + +-- | General network exception. +data StackageException = MkStackageException + { snapshot :: String, + reason :: ExceptionReason + } + deriving stock (Show) + +instance Exception StackageException where + displayException ex = + case ex.reason of + ReasonStatus status -> + if is404 status + then + mconcat + [ "Received 404 for snapshot '", + snapshot, + "'. Is the snapshot correct? ", + statusMessage status + ] + else + mconcat + [ "Received ", + show $ Status.statusCode status, + " for snapshot '", + snapshot, + "'. ", + statusMessage status + ] + ReasonReadBody readBodyEx -> + mconcat + [ "Exception reading body for snapshot '", + snapshot, + "':\n\n", + displayException readBodyEx + ] + ReasonDecodeJson jsonBs err -> + mconcat + [ "Could not decode JSON:\n\n", + show jsonBs, + "\n\nError: ", + err + ] + where + snapshot = ex.snapshot + is404 x = Status.statusCode x == 404 + + statusMessage s = + mconcat + [ "Status message: ", + show $ Status.statusMessage s + ] + +getStatusCode :: Response body -> Int +getStatusCode = Status.statusCode . HttpClient.responseStatus diff --git a/src/runner/CLC/Stackage/Runner.hs b/src/runner/CLC/Stackage/Runner.hs new file mode 100644 index 0000000..c7d1eb4 --- /dev/null +++ b/src/runner/CLC/Stackage/Runner.hs @@ -0,0 +1,53 @@ +-- | Entry-point for the project. Provides libraries functions over a mere +-- executable for testing. +module CLC.Stackage.Runner + ( run, + runModifyPackages, + ) +where + +import CLC.Stackage.Builder qualified as Builder +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 Control.Exception (bracket, throwIO) +import Control.Monad (when) +import Data.Foldable (for_) +import Data.IORef (readIORef) +import System.Exit (ExitCode (ExitFailure)) + +-- | Entry-point for testing clc-stackage. In particular: +-- +-- 1. Sets up environment based on CLI args and possible cache data from a +-- previous run. +-- +-- 2. For each group of packages, write a cabal file for the group and attempt +-- a build. +-- +-- 3. Once all groups are finished (or the first failure, if +-- 'CLC.Stackage.Builder.Env.failFast' is active), print a summary and +-- update the cache if 'CLC.Stackage.Runner.Env.noCache' is /inactive/. +run :: Logging.Handle -> IO () +run hLogger = runModifyPackages hLogger id + +-- | Like 'run', except takes a package modifier. This is used for testing, so +-- that we can whittle down the (very large) package set. +runModifyPackages :: Logging.Handle -> ([Package] -> [Package]) -> IO () +runModifyPackages hLogger modifyPackages = do + bracket (Env.setup hLogger modifyPackages) Env.teardown $ \env -> do + let buildEnv = env.buildEnv + pkgGroupsIdx = Builder.batchPackages buildEnv + + -- write the entire package set to the cabal.project.local's constraints + Writer.writeCabalProjectLocal env.completePackageSet + + for_ pkgGroupsIdx $ \(pkgGroup, idx) -> Builder.buildProject buildEnv idx pkgGroup + + numErrors <- length <$> readIORef buildEnv.progress.failuresRef + when (numErrors > 0) $ throwIO $ ExitFailure 1 diff --git a/src/runner/CLC/Stackage/Runner/Args.hs b/src/runner/CLC/Stackage/Runner/Args.hs new file mode 100644 index 0000000..4c0cf81 --- /dev/null +++ b/src/runner/CLC/Stackage/Runner/Args.hs @@ -0,0 +1,348 @@ +module CLC.Stackage.Runner.Args + ( Args (..), + ColorLogs (..), + getArgs, + ) +where + +import CLC.Stackage.Builder.Env + ( CabalVerbosity + ( CabalVerbosity0, + CabalVerbosity1, + CabalVerbosity2, + CabalVerbosity3 + ), + Jobs (JobsN, JobsNCpus, JobsSemaphore), + WriteLogs (WriteLogsCurrent, WriteLogsNone, WriteLogsSaveFailures), + ) +import Options.Applicative + ( Mod, + Parser, + ParserInfo + ( ParserInfo, + infoFailureCode, + infoFooter, + infoFullDesc, + infoHeader, + infoParser, + infoPolicy, + infoProgDesc + ), + (<**>), + ) +import Options.Applicative qualified as OA +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 + +-- | Log coloring option. +data ColorLogs + = ColorLogsOff + | ColorLogsOn + | ColorLogsDetect + deriving stock (Eq, Show) + +-- | CLI args. +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, + -- | 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, + -- | If true, leaves the last generated cabal files. + noCleanup :: Bool, + -- | If true, the first package that fails _within_ a package group will + -- cause the entire group to fail. + packageFailFast :: Bool, + -- | Whether to retry packages that failed. + retryFailures :: Bool, + -- | Determines what logs to write. + writeLogs :: Maybe WriteLogs + } + deriving stock (Eq, Show) + +-- | Returns CLI args. +getArgs :: IO Args +getArgs = OA.execParser parserInfoArgs + where + parserInfoArgs = + ParserInfo + { infoParser = parseCliArgs, + infoFullDesc = True, + infoProgDesc = desc, + infoHeader = Chunk headerTxt, + infoFooter = Chunk Nothing, + infoFailureCode = 1, + infoPolicy = Intersperse + } + headerTxt = Just "clc-stackage: Builds all packages in a stackage snapshot." + desc = + Chunk.vsepChunks + [ Chunk.paragraph $ + mconcat + [ "clc-stackage is an executable that downloads a stackage ", + "snapshot and attempts to build all packages in it. Build ", + "logs are saved in ./output, where they can be examined for ", + "determining which packages failed." + ], + Chunk.paragraph $ + mconcat + [ "The '--batch N' arg will divide the package set into groups ", + "of size N, then build each group sequentially. This process ", + "can be interrupted at any time, in which case the progress ", + "will be saved to a cache, so we can pick up where we left ", + "off." + ], + Chunk.paragraph "Alternatively, to build everything in one go, run:", + Pretty.indent 2 <$> Chunk.paragraph "clc-stackage", + Chunk.paragraph $ + mconcat + [ "This will build everything in one package group, and pass ", + "--keep-going to cabal." + ] + ] + +parseCliArgs :: Parser Args +parseCliArgs = + ( do + batch <- parseBatch + cabalVerbosity <- parseCabalVerbosity + colorLogs <- parseColorLogs + groupFailFast <- parseGroupFailFast + jobs <- parseJobs + noCache <- parseNoCache + noCleanup <- parseNoCleanup + packageFailFast <- parsePackageFailFast + retryFailures <- parseRetryFailures + writeLogs <- parseWriteLogs + + pure $ + MkArgs + { batch, + cabalVerbosity, + colorLogs, + groupFailFast, + jobs, + noCache, + noCleanup, + packageFailFast, + retryFailures, + writeLogs + } + ) + <**> OA.helper + +parseBatch :: Parser (Maybe Int) +parseBatch = + OA.optional $ + OA.option + OA.auto + ( mconcat + [ OA.long "batch", + OA.metavar "NAT", + mkHelp $ + mconcat + [ "If given N, divides the package set into groups of at ", + "most size N. This can be useful when building everything ", + "in one build is infeasible, or taking advantage of the ", + "better status reporting. No option means we batch ", + "everything in the same group." + ] + ] + ) + +parseCabalVerbosity :: Parser (Maybe CabalVerbosity) +parseCabalVerbosity = + OA.optional $ + OA.option + readCabalVerbosity + ( mconcat + [ OA.long "cabal-verbosity", + OA.metavar "(0 | 1 | 2 | 3)", + mkHelp + "Cabal's --verbose flag. Uses cabal's default if not given (1)." + ] + ) + 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 + +parseColorLogs :: Parser ColorLogs +parseColorLogs = + OA.option + readColorLogs + ( mconcat + [ OA.long "color-logs", + OA.metavar "(off | on | detect)", + OA.value ColorLogsDetect, + mkHelp "Determines whether we color logs. Defaults to detect." + ] + ) + where + readColorLogs = + OA.str >>= \case + "off" -> pure ColorLogsOff + "on" -> pure ColorLogsOn + "detect" -> pure ColorLogsDetect + bad -> fail $ "Expected one of (off | on | detect), received: " <> bad + +parseGroupFailFast :: Parser Bool +parseGroupFailFast = + OA.switch + ( mconcat + [ OA.long "group-fail-fast", + mkHelp helpTxt + ] + ) + where + helpTxt = + mconcat + [ "If true, the first group that fails to completely build stops ", + "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 + ( mconcat + [ OA.long "no-cache", + mkHelp $ + mconcat + [ "Disables the cache. Normally, the outcome of a run is saved ", + "to a json cache. This is useful for resuming a run that was ", + "interrupted (e.g. CTRL-C). The next run will fetch the ", + "packages to build from the cache." + ] + ] + ) + +parseNoCleanup :: Parser Bool +parseNoCleanup = + OA.switch + ( mconcat + [ OA.long "no-cleanup", + mkHelp "Will not remove the generated cabal files after exiting." + ] + ) + +parsePackageFailFast :: Parser Bool +parsePackageFailFast = + OA.switch + ( mconcat + [ OA.long "package-fail-fast", + mkHelp helpTxt + ] + ) + where + helpTxt = + mconcat + [ "If true, the first package that fails _within_ a package group ", + "will cause the entire group to fail. We then move to the next ", + "group, as normal. The default (off) behavior is equivalent to ", + "cabal's --keep-going)." + ] + +parseRetryFailures :: Parser Bool +parseRetryFailures = + OA.switch + ( mconcat + [ OA.long "retry-failures", + mkHelp "Retries failures from the cache. Incompatible with --no-cache. " + ] + ) + +parseWriteLogs :: Parser (Maybe WriteLogs) +parseWriteLogs = + OA.optional $ + OA.option + readWriteLogs + ( mconcat + [ OA.long "write-logs", + OA.metavar "(none | current | save-failures)", + mkHelp $ + mconcat + [ "Determines what cabal logs to write to the output/ ", + "directory. 'None' writes nothing. 'Current' writes stdout ", + "and stderr for the currently building project. ", + "'Save-failures' is the same as 'current' except the files ", + "are not deleted if the build failed. Defaults to ", + "save-failures." + ] + ] + ) + where + readWriteLogs = + OA.str >>= \case + "none" -> pure WriteLogsNone + "current" -> pure WriteLogsCurrent + "save-failures" -> pure WriteLogsSaveFailures + other -> + fail $ + mconcat + [ "Expected one of (none | current | save-failures), received: ", + other + ] + +mkHelp :: String -> Mod f a +mkHelp = + OA.helpDoc + . fmap (<> Pretty.hardline) + . Chunk.unChunk + . Chunk.paragraph diff --git a/src/runner/CLC/Stackage/Runner/Env.hs b/src/runner/CLC/Stackage/Runner/Env.hs new file mode 100644 index 0000000..28c8084 --- /dev/null +++ b/src/runner/CLC/Stackage/Runner/Env.hs @@ -0,0 +1,278 @@ +module CLC.Stackage.Runner.Env + ( RunnerEnv (..), + setup, + teardown, + + -- * Misc + getResults, + resultsToNewCache, + ) +where + +import CLC.Stackage.Builder.Env + ( BuildEnv + ( MkBuildEnv, + batch, + colorLogs, + groupFailFast, + hLogger, + packagesToBuild, + progress, + writeLogs + ), + Progress + ( MkProgress, + failuresRef, + successesRef + ), + ) +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.Runner.Args + ( ColorLogs + ( ColorLogsDetect, + ColorLogsOff, + ColorLogsOn + ), + ) +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.IO qualified as IO +import CLC.Stackage.Utils.Logging qualified as Logging +import CLC.Stackage.Utils.Paths qualified as Paths +import Control.Exception (throwIO) +import Control.Monad (unless) +import Data.Foldable (Foldable (foldl')) +import Data.IORef (newIORef, readIORef) +import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.Maybe (fromMaybe) +import Data.Set (Set) +import Data.Set qualified as Set +import Data.Text qualified as T +import Data.Time (LocalTime) +import System.Console.Pretty (supportsPretty) +import System.Directory.OsPath qualified as Dir +import System.Exit (ExitCode (ExitSuccess)) + +-- | Args used for building all packages. +data RunnerEnv = MkRunnerEnv + { -- | Environment used in building. + buildEnv :: BuildEnv, + -- | Status from previous run. + cache :: Maybe Results, + -- | The complete package set from stackage. This is used to write the + -- cabal.project.local's constraint section, to ensure we always use the + -- same transitive dependencies. + completePackageSet :: [Package], + -- | 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, + -- | If we do not revert the cabal file at the end (i.e. we leave the + -- last attempted build). + noCleanup :: Bool, + -- | Whether to retry packages that failed. + retryFailures :: Bool, + -- | Start time. + startTime :: LocalTime + } + +-- | 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 + cliArgs <- Args.getArgs + + -- 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 + + -- when packageFailFast is false, add keep-going so that we build as many + -- packages in the group. + keepGoingArg = ["--keep-going" | not cliArgs.packageFailFast] + + 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 + + -- (entire set, packages to build) + (completePackageSet, pkgsList) <- case cache of + Nothing -> do + -- if no cache exists, query stackage + pkgsResponses <- Parser.getPackageList + let completePackageSet = responseToPkgs <$> pkgsResponses + pkgs = modifyPackages completePackageSet + pure (completePackageSet, pkgs) + Just oldResults -> do + -- cache exists, use it rather than stackage + oldFailures <- + if cliArgs.retryFailures + then do + -- NOTE: [Remove old logs] + -- + -- Remove previous errors if we are retrying. + IO.removeDirectoryRecursiveIfExists Paths.logsDir + pure oldResults.failures + else pure Set.empty + + let completePackageSet = Report.allPackages oldResults + untested = oldResults.untested + toTest = Set.union untested oldFailures + + pure (Set.toList completePackageSet, Set.toList toTest) + + packagesToBuild <- case pkgsList of + (p : ps) -> pure (p :| ps) + [] -> do + Logging.putTimeInfoStr hLogger colorLogs "Cache exists but has no packages to test." + throwIO ExitSuccess + + let progress = + MkProgress + { successesRef, + failuresRef + } + + buildEnv = + MkBuildEnv + { batch = cliArgs.batch, + buildArgs, + colorLogs, + groupFailFast = cliArgs.groupFailFast, + hLogger, + packagesToBuild, + progress, + writeLogs = cliArgs.writeLogs + } + + -- delete this if they were leftover from a previous run + IO.removeFileIfExists Paths.generatedCabalPath + IO.removeFileIfExists Paths.generatedCabalProjectLocalPath + + pure $ + MkRunnerEnv + { buildEnv, + cache, + completePackageSet, + noCache = cliArgs.noCache, + noCleanup = cliArgs.noCleanup, + retryFailures = cliArgs.retryFailures, + startTime + } + where + responseToPkgs p = + MkPackage + { name = p.name, + 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 + endTime <- env.buildEnv.hLogger.getLocalTime + unless env.noCleanup $ do + Dir.removeFile Paths.generatedCabalPath + Dir.removeFile Paths.generatedCabalProjectLocalPath + + results <- getResults env.buildEnv + let report = + Report.mkReport + results + (Logging.formatLocalTime env.startTime) + (Logging.formatLocalTime endTime) + + unless env.noCache (updateCache env results) + + Report.saveReport report + + 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.colorBlue env.buildEnv.colorLogs $ "- Start: " <> report.startTime, + Logging.colorBlue env.buildEnv.colorLogs $ "- End: " <> report.endTime + ] + where + successStr r = fmtPercent r.stats.numSuccesses r.stats.successRate + failureStr r = fmtPercent r.stats.numFailures r.stats.failureRate + untestedStr r = fmtPercent r.stats.numUntested r.stats.untestedRate + + fmtPercent n p = + mconcat + [ T.pack $ show n, + " (", + T.pack $ show p, + "%)" + ] + +getResults :: BuildEnv -> IO Results +getResults env = do + currSuccesses :: Set Package <- readIORef env.progress.successesRef + currFailures :: Set Package <- readIORef env.progress.failuresRef + + let currAllTested = Set.union currSuccesses currFailures + + currUntested = foldl' addUntested Set.empty env.packagesToBuild + + addUntested acc d = + if Set.member d currAllTested + then acc + else Set.insert d acc + + pure $ + MkResults + { successes = currSuccesses, + failures = currFailures, + untested = currUntested + } + +updateCache :: RunnerEnv -> Results -> IO () +updateCache env = Report.saveCache . resultsToNewCache env + +resultsToNewCache :: RunnerEnv -> Results -> Results +resultsToNewCache env newResults = newCache + where + oldCache = fromMaybe Report.emptyResults env.cache + newCache = + MkResults + { -- Successes is append-only. + successes = Set.union oldCache.successes newResults.successes, + -- Untested is always the latest as each cached run always adds the + -- previous untested to the pkgsToBuild. + untested = newResults.untested, + failures = + if env.retryFailures + then -- Case 1: Retrying previous failures: Then the cache's + -- results are out-of-date i.e. old failures might + -- have passed or become untested. Only save the new + -- results. + newResults.failures + else -- Case 2: No retry: total failures are previous + new. + Set.union oldCache.failures newResults.failures + } diff --git a/src/runner/CLC/Stackage/Runner/Report.hs b/src/runner/CLC/Stackage/Runner/Report.hs new file mode 100644 index 0000000..9d7aaed --- /dev/null +++ b/src/runner/CLC/Stackage/Runner/Report.hs @@ -0,0 +1,132 @@ +module CLC.Stackage.Runner.Report + ( -- * Results + Results (..), + emptyResults, + allPackages, + + -- ** Cache + readCache, + saveCache, + + -- * Report + Report (..), + Stats (..), + mkReport, + saveReport, + ) +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.Paths qualified as Paths +import Control.Exception (throwIO) +import Data.Aeson (AesonException (AesonException), FromJSON, ToJSON) +import Data.Set (Set) +import Data.Set qualified as Set +import Data.Text (Text) +import Data.Text qualified as T +import GHC.Generics (Generic) +import System.Directory.OsPath qualified as Dir +import System.OsPath qualified as OsPath + +-- | Results of a run. This represents the current run __only__ i.e. it is not +-- the sum of the current run + cache. +data Results = MkResults + { successes :: Set Package, + failures :: Set Package, + untested :: Set Package + } + deriving stock (Eq, Generic, Show) + deriving anyclass (FromJSON, ToJSON) + +-- | Empty results. +emptyResults :: Results +emptyResults = MkResults Set.empty Set.empty Set.empty + +-- | Unions all packages in the results. +allPackages :: Results -> Set Package +allPackages r = Set.unions [r.successes, r.failures, r.untested] + +data Stats = MkStats + { numSuccesses :: Int, + successRate :: Int, + numFailures :: Int, + failureRate :: Int, + numUntested :: Int, + untestedRate :: Int + } + deriving stock (Eq, Generic, Show) + deriving anyclass (FromJSON, ToJSON) + +-- | Full report. +data Report = MkReport + { results :: Results, + stats :: Stats, + startTime :: Text, + endTime :: Text + } + deriving stock (Eq, Generic, Show) + deriving anyclass (FromJSON, ToJSON) + +-- | Derives a report from results. +mkReport :: Results -> Text -> Text -> Report +mkReport results startTime endTime = + MkReport + { results, + stats = + MkStats + { numSuccesses, + successRate, + numFailures, + failureRate, + numUntested, + untestedRate + }, + startTime, + endTime + } + where + numSuccesses = length results.successes + successRate = dv numSuccesses + + numFailures = length results.failures + failureRate = dv numFailures + + numUntested = length results.untested + untestedRate = dv numUntested + + numAllTested :: Double + numAllTested = fromIntegral $ numSuccesses + numFailures + numUntested + + dv :: Int -> Int + 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 + catchPathStr <- T.pack <$> OsPath.decodeUtf Paths.cachePath + Dir.doesFileExist Paths.cachePath >>= \case + False -> do + Logging.putTimeInfoStr handle colorLogs $ "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 + pure $ Just r + +-- | Saves the current progress data as the next prior run. +saveCache :: Results -> IO () +saveCache results = do + Dir.createDirectoryIfMissing False Paths.outputDir + JSON.writeJson Paths.cachePath results + +-- | Saves the report +saveReport :: Report -> IO () +saveReport report = do + Dir.createDirectoryIfMissing False Paths.outputDir + JSON.writeJson Paths.reportPath report diff --git a/src/utils/CLC/Stackage/Utils/Exception.hs b/src/utils/CLC/Stackage/Utils/Exception.hs new file mode 100644 index 0000000..2db6c4f --- /dev/null +++ b/src/utils/CLC/Stackage/Utils/Exception.hs @@ -0,0 +1,44 @@ +-- | Provides utils for exceptions. +module CLC.Stackage.Utils.Exception + ( try, + tryAny, + throwLeft, + mapThrowLeft, + ) +where + +import Control.Exception + ( Exception (fromException, toException), + SomeAsyncException (SomeAsyncException), + SomeException, + throwIO, + ) +import Control.Exception qualified as Ex +import Data.Bifunctor (first) + +mapThrowLeft :: (Exception e2) => (e1 -> e2) -> Either e1 a -> IO a +mapThrowLeft f = throwLeft . first f + +-- | Throws left. +throwLeft :: (Exception e) => Either e a -> IO a +throwLeft (Right x) = pure x +throwLeft (Left e) = throwIO e + +tryAny :: IO a -> IO (Either SomeException a) +tryAny = try @SomeException + +-- | Try but it does not catch async exceptions. This allows us to catch +-- 'SomeException' more safely. +try :: (Exception e) => IO a -> IO (Either e a) +try io = + Ex.try io >>= \case + Left ex + | isSyncException ex -> pure $ Left ex + | otherwise -> throwIO ex + Right x -> pure $ Right x + +isSyncException :: (Exception e) => e -> Bool +isSyncException e = + case fromException (toException e) of + Just (SomeAsyncException _) -> False + Nothing -> True diff --git a/src/utils/CLC/Stackage/Utils/IO.hs b/src/utils/CLC/Stackage/Utils/IO.hs new file mode 100644 index 0000000..4f05490 --- /dev/null +++ b/src/utils/CLC/Stackage/Utils/IO.hs @@ -0,0 +1,58 @@ +module CLC.Stackage.Utils.IO + ( -- * Files + readBinaryFile, + writeBinaryFile, + withBinaryFileWriteMode, + + -- ** UTF8 + readFileUtf8, + writeFileUtf8, + + -- * Directories + removeDirectoryRecursiveIfExists, + removeFileIfExists, + ) +where + +import Control.Exception (throwIO) +import Control.Monad (when, (>=>)) +import Data.ByteString (ByteString) +import Data.Text (Text) +import Data.Text.Encoding qualified as TEnc +import System.Directory.OsPath qualified as Dir +import System.File.OsPath qualified as FileIO +import System.IO (Handle, IOMode (WriteMode)) +import System.OsPath (OsPath) + +-- | Reads a UTF-8 file into 'Text', throw an exception if decoding fails. +readFileUtf8 :: OsPath -> IO Text +readFileUtf8 = readBinaryFile >=> either throwIO pure . TEnc.decodeUtf8' + +-- | Reads a file. +readBinaryFile :: OsPath -> IO ByteString +readBinaryFile = FileIO.readFile' + +-- | Writes a file. +writeBinaryFile :: OsPath -> ByteString -> IO () +writeBinaryFile = FileIO.writeFile' + +-- | Writes a Text file. +writeFileUtf8 :: OsPath -> Text -> IO () +writeFileUtf8 p = writeBinaryFile p . TEnc.encodeUtf8 + +-- | With a file in write mode. +withBinaryFileWriteMode :: OsPath -> (Handle -> IO r) -> IO r +withBinaryFileWriteMode p = withBinaryFile p WriteMode + +-- | With a file. +withBinaryFile :: OsPath -> IOMode -> (Handle -> IO r) -> IO r +withBinaryFile = FileIO.withBinaryFile + +-- | Removes the directory if it exists. +removeDirectoryRecursiveIfExists :: OsPath -> IO () +removeDirectoryRecursiveIfExists p = + Dir.doesDirectoryExist p >>= (`when` Dir.removeDirectoryRecursive p) + +-- | Removes the directory if it exists. +removeFileIfExists :: OsPath -> IO () +removeFileIfExists p = Dir.doesFileExist p >>= (`when` Dir.removeFile p) diff --git a/src/utils/CLC/Stackage/Utils/JSON.hs b/src/utils/CLC/Stackage/Utils/JSON.hs new file mode 100644 index 0000000..3454577 --- /dev/null +++ b/src/utils/CLC/Stackage/Utils/JSON.hs @@ -0,0 +1,53 @@ +module CLC.Stackage.Utils.JSON + ( writeJson, + decode, + encodePretty, + ) +where + +import CLC.Stackage.Utils.IO qualified as IO +import Data.Aeson (FromJSON, ToJSON) +import Data.Aeson qualified as Asn +import Data.Aeson.Encode.Pretty qualified as AsnPretty +import Data.ByteString (ByteString) +import Data.ByteString.Lazy qualified as BSL +import Data.Text (Text) +import System.OsPath (OsPath) + +-- | Decodes JSON. +decode :: (FromJSON a) => ByteString -> Either String a +decode = Asn.eitherDecodeStrict + +-- | Write to the file. +writeJson :: (ToJSON a) => OsPath -> a -> IO () +writeJson p = IO.writeBinaryFile p . encodePretty + +-- | Encodes JSON +encodePretty :: (ToJSON a) => a -> ByteString +encodePretty = + BSL.toStrict + . AsnPretty.encodePretty' + ( AsnPretty.defConfig + { AsnPretty.confCompare = orderJsonKeys + } + ) + where + orderJsonKeys :: Text -> Text -> Ordering + orderJsonKeys l r = case liftA2 (,) (topKeyInt l) (topKeyInt r) of + Just (lInt, rInt) -> compare lInt rInt + Nothing -> case liftA2 (,) (resultsKeyInt l) (resultsKeyInt r) of + Just (lInt, rInt) -> compare lInt rInt + Nothing -> EQ + + topKeyInt :: Text -> Maybe Int + topKeyInt "startTime" = Just 0 + topKeyInt "endTime" = Just 1 + topKeyInt "stats" = Just 2 + topKeyInt "results" = Just 3 + topKeyInt _ = Nothing + + resultsKeyInt :: Text -> Maybe Int + resultsKeyInt "failures" = Just 0 + resultsKeyInt "untested" = Just 1 + resultsKeyInt "successes" = Just 2 + resultsKeyInt _ = Nothing diff --git a/src/utils/CLC/Stackage/Utils/Logging.hs b/src/utils/CLC/Stackage/Utils/Logging.hs new file mode 100644 index 0000000..1b8a42f --- /dev/null +++ b/src/utils/CLC/Stackage/Utils/Logging.hs @@ -0,0 +1,121 @@ +module CLC.Stackage.Utils.Logging + ( -- * Logging Handler + Handle (..), + + -- * Printing with timestamps + putTimeInfoStr, + putTimeSuccessStr, + putTimeErrStr, + + -- ** ANSI Colors + colorBlue, + colorGreen, + colorRed, + colorMagenta, + + -- * Misc + formatLocalTime, + ) +where + +import Data.Text (Text) +import Data.Text qualified as T +import Data.Time.Format qualified as Format +import Data.Time.LocalTime (LocalTime) +import Data.Word (Word16) +import System.Console.Pretty (Color (Blue, Green, Magenta, Red)) +import System.Console.Pretty qualified as Pretty + +-- | Simple handle for logging, for testing output. +data Handle = MkHandle + { -- | Retrieve local time. + getLocalTime :: IO LocalTime, + -- | Log stderr. + logStrErrLn :: Text -> IO (), + -- | Log stdout. + logStrLn :: Text -> IO (), + -- | Max terminal width. + terminalWidth :: Word16 + } + +-- | 'putStrLn' with a timestamp and info prefix. +putTimeInfoStr :: Handle -> Bool -> Text -> IO () +putTimeInfoStr hLogger b s = do + timeStr <- getLocalTimeString hLogger + hLogger.logStrLn $ colorBlue b $ "[" <> 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 + timeStr <- getLocalTimeString hLogger + hLogger.logStrLn $ colorGreen b $ "[" <> timeStr <> "][Success] " <> s' + where + s' = truncateIfNeeded hLogger.terminalWidth s + +-- | 'putStrErrLn' with a timestamp and error prefix. +putTimeErrStr :: Handle -> Bool -> Text -> IO () +putTimeErrStr hLogger b s = do + timeStr <- getLocalTimeString hLogger + hLogger.logStrErrLn $ colorRed b $ "[" <> timeStr <> "][Error] " <> s' + where + s' = truncateIfNeeded hLogger.terminalWidth s + +getLocalTimeString :: Handle -> IO Text +getLocalTimeString hLogger = formatLocalTime <$> hLogger.getLocalTime + +formatLocalTime :: LocalTime -> Text +formatLocalTime = T.pack . Format.formatTime Format.defaultTimeLocale fmt + where + fmt = "%0Y-%m-%d %H:%M:%S" + +colorBlue :: Bool -> Text -> Text +colorBlue b = colorIf b Blue + +colorMagenta :: Bool -> Text -> Text +colorMagenta b = colorIf b Magenta + +colorGreen :: Bool -> Text -> Text +colorGreen b = colorIf b Green + +colorRed :: Bool -> Text -> Text +colorRed b = colorIf b Red + +colorIf :: Bool -> Color -> Text -> Text +colorIf True = Pretty.color +colorIf False = const id + +-- | Given maxLen, constant extra and Text t, returns t' s.t. +-- len t' <= max (0, maxLen - extra). An ellipse is added if t' was truncated. +truncateIfNeeded :: Word16 -> Text -> Text +truncateIfNeeded maxLen txt + | T.length txt <= maxAllowed = txt + | otherwise = txt' + where + maxAllowed = fromIntegral $ maxLen `monus` extra + txt' = T.take (maxAllowed - 3) txt <> "..." + + extra = timeStrLen + constLen + +-- subtraction clamped to zero. +monus :: Word16 -> Word16 -> Word16 +monus x y + | y >= x = 0 + | otherwise = x - y + +-- | This is the total space after the timestamp before the message. Constant +-- for every message because whitespace makes up the difference i.e. +-- +-- '[Info] ' +-- '[Success] ' +-- '[Error] ' +-- +-- We add one more so that long messages leave at least one space before the +-- terminal edge. +constLen :: Word16 +constLen = 11 + +-- e.g. [2024-10-14 15:14:00] +timeStrLen :: Word16 +timeStrLen = 21 diff --git a/src/utils/CLC/Stackage/Utils/OS.hs b/src/utils/CLC/Stackage/Utils/OS.hs new file mode 100644 index 0000000..1e27763 --- /dev/null +++ b/src/utils/CLC/Stackage/Utils/OS.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE CPP #-} + +module CLC.Stackage.Utils.OS + ( Os (..), + currentOs, + ) +where + +data Os + = Linux + | Osx + | Windows + deriving stock (Bounded, Enum, Eq, Show) + +{- ORMOLU_DISABLE -} + +currentOs :: Os +currentOs = +#if OSX + Osx +#elif WINDOWS + Windows +#else + Linux +#endif + +{- ORMOLU_ENABLE -} diff --git a/src/utils/CLC/Stackage/Utils/Paths.hs b/src/utils/CLC/Stackage/Utils/Paths.hs new file mode 100644 index 0000000..1623164 --- /dev/null +++ b/src/utils/CLC/Stackage/Utils/Paths.hs @@ -0,0 +1,64 @@ +{-# LANGUAGE QuasiQuotes #-} + +module CLC.Stackage.Utils.Paths + ( -- * CLC-Stackage paths + outputDir, + cachePath, + reportPath, + logsDir, + generatedDir, + generatedCabalPath, + generatedCabalProjectLocalPath, + + -- * Utils + OsPath.encodeUtf, + decodeUtfLenient, + unsafeDecodeUtf, + ) +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.OsPath (OsPath, osp, ()) +import System.OsPath qualified as OsPath + +-- | Leniently decodes OsPath to String. +decodeUtfLenient :: OsPath -> String +decodeUtfLenient = + either (error . show) id + . OsPath.decodeWith uft8Encoding utf16Encoding + where + uft8Encoding = UTF8.mkUTF8 TransliterateCodingFailure + utf16Encoding = UTF16.mkUTF16le TransliterateCodingFailure + +unsafeDecodeUtf :: OsPath -> FilePath +unsafeDecodeUtf p = case OsPath.decodeUtf p of + Just fp -> fp + Nothing -> error $ "Error decoding ospath: " <> show p + +-- | Output directory. +outputDir :: OsPath +outputDir = [osp|output|] + +-- | Cache path. +cachePath :: OsPath +cachePath = outputDir [osp|cache.json|] + +-- | Report path. +reportPath :: OsPath +reportPath = outputDir [osp|report.json|] + +-- | Path to generated project. +generatedDir :: OsPath +generatedDir = [osp|generated|] + +generatedCabalPath :: OsPath +generatedCabalPath = generatedDir [OsPath.osp|generated.cabal|] + +generatedCabalProjectLocalPath :: OsPath +generatedCabalProjectLocalPath = generatedDir [OsPath.osp|cabal.project.local|] + +-- | Logs dir. +logsDir :: OsPath +logsDir = outputDir [osp|logs|] diff --git a/test/functional/Main.hs b/test/functional/Main.hs new file mode 100644 index 0000000..43c1ffb --- /dev/null +++ b/test/functional/Main.hs @@ -0,0 +1,163 @@ +{-# LANGUAGE QuasiQuotes #-} + +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.Paths qualified as Paths +import Data.ByteString (ByteString) +import Data.ByteString.Char8 qualified as C8 +import Data.IORef (IORef, modifyIORef', newIORef, readIORef) +import Data.List qualified as L +import Data.Maybe (isJust) +import Data.Set qualified as Set +import Data.Text (Text) +import Data.Text.Encoding qualified as TEnc +import Data.Time.LocalTime (LocalTime (LocalTime), midday) +import System.Environment (lookupEnv, withArgs) +import System.Environment.Guard (guardOrElse') +import System.Environment.Guard.Lifted (ExpectEnv (ExpectEnvSet)) +import System.OsPath (OsPath, osp, ()) +import Test.Tasty (TestName, TestTree, testGroup) +import Test.Tasty qualified as Tasty +import Test.Tasty.Golden (DeleteOutputFile (OnPass), goldenVsFile) + +main :: IO () +main = + Tasty.defaultMain $ + Tasty.localOption OnPass $ + Tasty.withResource setup (const teardown) specs + where + specs getNoCleanup = + testGroup + "Functional" + [ testSmall getNoCleanup, + testSmallBatch getNoCleanup + ] + +testSmall :: IO Bool -> TestTree +testSmall getNoCleanup = runGolden getNoCleanup params + where + params = + MkGoldenParams + { args = [], + runner = runSmall, + testDesc = "Finishes clc-stackage with small package list", + testName = [osp|testSmall|] + } + +testSmallBatch :: IO Bool -> TestTree +testSmallBatch getNoCleanup = runGolden getNoCleanup params + where + params = + MkGoldenParams + { args = ["--batch", "2"], + runner = runSmall, + testDesc = "Finishes clc-stackage with small package list and --batch", + testName = [osp|testSmallBatch|] + } + +-- | Tests building only a few packages +runSmall :: IO [ByteString] +runSmall = do + (hLogger, logsRef) <- mkHLogger + + Runner.runModifyPackages hLogger modifyPackages + + readLogs logsRef + where + readLogs = fmap (fmap toBS . L.reverse) . readIORef + toBS = TEnc.encodeUtf8 + +modifyPackages :: [Package] -> [Package] +modifyPackages = filter (\p -> Set.member p.name pkgs) + where + pkgs = + -- chosen at semi-random due to small dep footprint + no system deps + Set.fromList + [ "cborg", + "clock", + "mtl", + "optics-core", + "profunctors" + ] + +setup :: IO Bool +setup = do + IO.removeFileIfExists Paths.generatedCabalPath + IO.removeFileIfExists Paths.generatedCabalProjectLocalPath + IO.removeDirectoryRecursiveIfExists Paths.outputDir + + isJust <$> lookupEnv "NO_CLEANUP" + +-- NOTE: [Skipping cleanup] +-- +-- guardOrElse' will run doNothing over cleanup if NO_CLEANUP is set. +teardown :: IO () +teardown = guardOrElse' "NO_CLEANUP" ExpectEnvSet doNothing cleanup + where + cleanup = do + IO.removeFileIfExists Paths.generatedCabalPath + IO.removeFileIfExists Paths.generatedCabalProjectLocalPath + IO.removeDirectoryRecursiveIfExists Paths.outputDir + + doNothing = putStrLn "*** Not cleaning up output or generated dir" + +mkHLogger :: IO (Logging.Handle, IORef [Text]) +mkHLogger = do + logsRef <- newIORef [] + + let hLogger = + Logging.MkHandle + { Logging.getLocalTime = pure mkLocalTime, + Logging.logStrLn = \s -> modifyIORef' logsRef (s :), + Logging.logStrErrLn = \s -> modifyIORef' logsRef (s :), + Logging.terminalWidth = 80 + } + + pure (hLogger, logsRef) + +mkLocalTime :: LocalTime +mkLocalTime = LocalTime (toEnum 59_000) midday + +data GoldenParams = MkGoldenParams + { args :: [String], + runner :: IO [ByteString], + testDesc :: TestName, + testName :: OsPath + } + +runGolden :: IO Bool -> GoldenParams -> TestTree +runGolden getNoCleanup params = goldenVsFile params.testDesc goldenFilePath actualFilePath $ do + -- we always need to cleanup the cache prior to a run so that the generated cache from + -- a previous run does not interfere + IO.removeFileIfExists Paths.cachePath + + -- While NOTE: [Skipping cleanup] will prevent the test cleanup from running, + -- the clc-stackage also performs a cleanup. Thus if no cleanup is desired + -- (NO_CLEANUP is set), we also need to pass the --no-cleanup arg to the + -- exe. + noCleanup <- getNoCleanup + let noCleanupArgs = ["--no-cleanup" | noCleanup] + finalArgs = args' ++ noCleanupArgs + + logs <- withArgs finalArgs params.runner + + writeActualFile $ toBS logs + where + -- test w/ color off since CI can't handle it, apparently + args' = "--color-logs" : "off" : params.args + + actualOsPath = goldensDir params.testName <> [osp|.actual|] + actualFilePath = Paths.unsafeDecodeUtf actualOsPath + goldenFilePath = Paths.unsafeDecodeUtf $ goldensDir params.testName <> [osp|.golden|] + + toBS = C8.unlines + + writeActualFile :: ByteString -> IO () + writeActualFile = IO.writeBinaryFile actualOsPath + +goldensDir :: OsPath +goldensDir = [osp|test|] [osp|functional|] [osp|goldens|] diff --git a/test/functional/goldens/testSmall.golden b/test/functional/goldens/testSmall.golden new file mode 100644 index 0000000..1da2cea --- /dev/null +++ b/test/functional/goldens/testSmall.golden @@ -0,0 +1,11 @@ +[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... + + +- Successes: 5 (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/testSmallBatch.golden b/test/functional/goldens/testSmallBatch.golden new file mode 100644 index 0000000..e088b47 --- /dev/null +++ b/test/functional/goldens/testSmallBatch.golden @@ -0,0 +1,13 @@ +[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 + + +- Successes: 5 (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/unit/Main.hs b/test/unit/Main.hs new file mode 100644 index 0000000..5851630 --- /dev/null +++ b/test/unit/Main.hs @@ -0,0 +1,16 @@ +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 + +main :: IO () +main = + defaultMain $ + localOption OnPass $ + testGroup + "Unit" + [ Env.tests, + Report.tests + ] diff --git a/test/unit/Unit/CLC/Stackage/Runner/Env.hs b/test/unit/Unit/CLC/Stackage/Runner/Env.hs new file mode 100644 index 0000000..9667e68 --- /dev/null +++ b/test/unit/Unit/CLC/Stackage/Runner/Env.hs @@ -0,0 +1,113 @@ +{-# OPTIONS_GHC -Wno-missing-import-lists #-} + +module Unit.CLC.Stackage.Runner.Env (tests) where + +import CLC.Stackage.Runner.Env (RunnerEnv (cache, retryFailures)) +import CLC.Stackage.Runner.Env qualified as Env +import CLC.Stackage.Runner.Report + ( Results (MkResults, failures, successes, untested), + ) +import Data.Set qualified as Set +import Unit.Prelude + +tests :: TestTree +tests = + testGroup + "Sequential.Env" + [ testResults, + newCacheTests + ] + +testResults :: TestTree +testResults = testCase "Retrieves expected results" $ do + buildEnv <- mkBuildEnv + results <- Env.getResults buildEnv + + expected @=? results + where + expected = + MkResults + { successes = Set.fromList ["p1 ==1", "p2 ==1"], + failures = Set.fromList ["p3 ==1", "p4 ==1"], + untested = Set.fromList ["p5 ==1", "p6 ==1"] + } + +newCacheTests :: TestTree +newCacheTests = + testGroup + "resultsToNewCache" + [ testEmptyCacheUpdate, + testCacheUpdate, + testCacheUpdateRetryFailures + ] + +testEmptyCacheUpdate :: TestTree +testEmptyCacheUpdate = testCase "Empty cache updated to new results" $ do + runnerEnv <- mkRunnerEnv + let newResults = + MkResults + { successes = Set.fromList ["p1 ==1", "p2 ==1"], + failures = Set.fromList ["p3 ==1", "p4 ==1"], + untested = Set.fromList ["p5 ==1", "p6 ==1"] + } + newCache = Env.resultsToNewCache runnerEnv newResults + + newResults @=? newCache + +testCacheUpdate :: TestTree +testCacheUpdate = testCase "Cache updated to new results" $ do + runnerEnv <- mkRunnerEnv + + let oldCache = + MkResults + { successes = Set.fromList ["p1 ==1"], + failures = Set.fromList ["p3 ==1"], + untested = Set.fromList ["p2 ==1", "p4 ==1", "p5 ==1", "p6 ==1"] + } + runnerEnv' = runnerEnv {cache = Just oldCache} + + let newResults = + MkResults + { successes = Set.fromList ["p2 ==1"], + failures = Set.fromList ["p4 ==1"], + untested = Set.fromList ["p5 ==1", "p6 ==1"] + } + newCache = Env.resultsToNewCache runnerEnv' newResults + + expected @=? newCache + where + expected = + MkResults + { successes = Set.fromList ["p1 ==1", "p2 ==1"], + failures = Set.fromList ["p3 ==1", "p4 ==1"], + untested = Set.fromList ["p5 ==1", "p6 ==1"] + } + +testCacheUpdateRetryFailures :: TestTree +testCacheUpdateRetryFailures = testCase "Cache updated to new results with retryFailures" $ do + runnerEnv <- mkRunnerEnv + + let oldCache = + MkResults + { successes = Set.fromList ["p1 ==1"], + failures = Set.fromList ["p3 ==1"], + untested = Set.fromList ["p2 ==1", "p4 ==1", "p5 ==1", "p6 ==1"] + } + runnerEnv' = runnerEnv {cache = Just oldCache, retryFailures = True} + + let newResults = + MkResults + { successes = Set.fromList ["p2 ==1"], + failures = Set.fromList ["p4 ==1"], + untested = Set.fromList ["p5 ==1", "p6 ==1"] + } + newCache = Env.resultsToNewCache runnerEnv' newResults + + expected @=? newCache + where + expected = + MkResults + { successes = Set.fromList ["p1 ==1", "p2 ==1"], + failures = Set.fromList ["p4 ==1"], + untested = Set.fromList ["p5 ==1", "p6 ==1"] + } diff --git a/test/unit/Unit/CLC/Stackage/Runner/Report.hs b/test/unit/Unit/CLC/Stackage/Runner/Report.hs new file mode 100644 index 0000000..edf900e --- /dev/null +++ b/test/unit/Unit/CLC/Stackage/Runner/Report.hs @@ -0,0 +1,120 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# OPTIONS_GHC -Wno-missing-import-lists #-} + +module Unit.CLC.Stackage.Runner.Report (tests) where + +import CLC.Stackage.Runner.Report + ( Report (MkReport, endTime, results, startTime, stats), + Results (MkResults, failures, successes, untested), + Stats + ( MkStats, + failureRate, + numFailures, + numSuccesses, + numUntested, + successRate, + untestedRate + ), + ) +import CLC.Stackage.Runner.Report qualified as Report +import CLC.Stackage.Utils.IO qualified as IO +import CLC.Stackage.Utils.JSON qualified as JSON +import CLC.Stackage.Utils.Paths qualified as Paths +import Data.Set qualified as Set +import System.OsPath (OsPath, osp, ()) +import Unit.Prelude + +tests :: TestTree +tests = + testGroup + "Sequential.Report" + [ testMkReport, + testResultJsonEncode, + testReportJsonEncode + ] + +testMkReport :: TestTree +testMkReport = testCase "Creates a report" $ do + let report = Report.mkReport results "start" "end" + + expected @=? report + where + results = + MkResults + { successes = Set.fromList ["p1 ==1", "p2 ==1", "p3 ==1", "p4 ==1", "p5 ==1"], + failures = Set.fromList ["p4 ==1", "p5 ==1"], + untested = Set.fromList ["p6 ==1", "p7 ==1", "p8 ==1"] + } + + expected = + MkReport + { results = results, + stats = + MkStats + { numSuccesses = 5, + successRate = 50, + numFailures = 2, + failureRate = 20, + numUntested = 3, + untestedRate = 30 + }, + startTime = "start", + endTime = "end" + } + +testResultJsonEncode :: TestTree +testResultJsonEncode = goldenVsFile desc goldenFilePath actualFilePath $ do + let json = JSON.encodePretty results <> "\n" + + IO.writeBinaryFile actualOsPath json + where + desc = "Encodes Result to JSON" + + goldenFilePath = Paths.unsafeDecodeUtf $ goldenDir [osp|result.golden|] + actualOsPath = goldenDir [osp|result.actual|] + actualFilePath = Paths.unsafeDecodeUtf actualOsPath + + results = + MkResults + { successes = Set.fromList ["p1 ==1", "p2 ==1", "p3 ==1", "p4 ==1", "p5 ==1"], + failures = Set.fromList ["p4 ==1", "p5 ==1"], + untested = Set.fromList ["p6 ==1", "p7 ==1", "p8 ==1"] + } + +testReportJsonEncode :: TestTree +testReportJsonEncode = goldenVsFile desc goldenFilePath actualFilePath $ do + let json = JSON.encodePretty report <> "\n" + + IO.writeBinaryFile actualOsPath json + where + desc = "Encodes Report to JSON" + + goldenFilePath = Paths.unsafeDecodeUtf $ goldenDir [osp|report.golden|] + actualOsPath = goldenDir [osp|report.actual|] + actualFilePath = Paths.unsafeDecodeUtf actualOsPath + + results = + MkResults + { successes = Set.fromList ["p1 ==1", "p2 ==1", "p3 ==1", "p4 ==1", "p5 ==1"], + failures = Set.fromList ["p4 ==1", "p5 ==1"], + untested = Set.fromList ["p6 ==1", "p7 ==1", "p8 ==1"] + } + + report = + MkReport + { results = results, + stats = + MkStats + { numSuccesses = 5, + successRate = 50, + numFailures = 2, + failureRate = 20, + numUntested = 3, + untestedRate = 30 + }, + startTime = "start", + endTime = "end" + } + +goldenDir :: OsPath +goldenDir = [osp|test/|] [osp|unit|] [osp|goldens|] diff --git a/test/unit/Unit/Prelude.hs b/test/unit/Unit/Prelude.hs new file mode 100644 index 0000000..d6f9045 --- /dev/null +++ b/test/unit/Unit/Prelude.hs @@ -0,0 +1,87 @@ +module Unit.Prelude + ( module X, + mkRunnerEnv, + mkBuildEnv, + ) +where + +import CLC.Stackage.Builder.Env + ( BuildEnv + ( MkBuildEnv, + batch, + buildArgs, + colorLogs, + groupFailFast, + hLogger, + packagesToBuild, + progress, + writeLogs + ), + Progress (MkProgress, failuresRef, successesRef), + ) +import CLC.Stackage.Runner.Env + ( RunnerEnv + ( MkRunnerEnv, + buildEnv, + cache, + completePackageSet, + noCache, + noCleanup, + retryFailures, + startTime + ), + ) +import CLC.Stackage.Utils.Logging qualified as Logging +import Data.IORef (newIORef) +import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.List.NonEmpty qualified as NE +import Data.Set qualified as Set +import Data.Time.LocalTime (LocalTime (LocalTime), midday) +import Test.Tasty as X (TestTree, testGroup) +import Test.Tasty.Golden as X (goldenVsFile) +import Test.Tasty.HUnit as X (assertFailure, testCase, (@=?)) + +mkRunnerEnv :: IO RunnerEnv +mkRunnerEnv = do + buildEnv <- mkBuildEnv + + pure $ + MkRunnerEnv + { buildEnv, + cache = Nothing, + completePackageSet = NE.toList buildEnv.packagesToBuild, + noCache = False, + noCleanup = False, + retryFailures = False, + startTime = mkLocalTime + } + +mkBuildEnv :: IO BuildEnv +mkBuildEnv = do + successesRef <- newIORef (Set.fromList ["p1 ==1", "p2 ==1"]) + failuresRef <- newIORef (Set.fromList ["p3 ==1", "p4 ==1"]) + + pure $ + MkBuildEnv + { batch = Nothing, + buildArgs = [], + colorLogs = True, + groupFailFast = False, + hLogger = + Logging.MkHandle + { getLocalTime = pure mkLocalTime, + logStrErrLn = const (pure ()), + logStrLn = const (pure ()), + terminalWidth = 80 + }, + packagesToBuild = "p1 ==1" :| ["p2 ==1", "p3 ==1", "p4 ==1", "p5 ==1", "p6 ==1"], + progress = + MkProgress + { failuresRef, + successesRef + }, + writeLogs = Nothing + } + +mkLocalTime :: LocalTime +mkLocalTime = LocalTime (toEnum 59_000) midday diff --git a/test/unit/goldens/report.golden b/test/unit/goldens/report.golden new file mode 100644 index 0000000..5305d32 --- /dev/null +++ b/test/unit/goldens/report.golden @@ -0,0 +1,60 @@ +{ + "startTime": "start", + "endTime": "end", + "stats": { + "failureRate": 20, + "numFailures": 2, + "numSuccesses": 5, + "numUntested": 3, + "successRate": 50, + "untestedRate": 30 + }, + "results": { + "failures": [ + { + "name": "p4", + "version": "1" + }, + { + "name": "p5", + "version": "1" + } + ], + "untested": [ + { + "name": "p6", + "version": "1" + }, + { + "name": "p7", + "version": "1" + }, + { + "name": "p8", + "version": "1" + } + ], + "successes": [ + { + "name": "p1", + "version": "1" + }, + { + "name": "p2", + "version": "1" + }, + { + "name": "p3", + "version": "1" + }, + { + "name": "p4", + "version": "1" + }, + { + "name": "p5", + "version": "1" + } + ] + } +} diff --git a/test/unit/goldens/result.golden b/test/unit/goldens/result.golden new file mode 100644 index 0000000..dce7191 --- /dev/null +++ b/test/unit/goldens/result.golden @@ -0,0 +1,48 @@ +{ + "failures": [ + { + "name": "p4", + "version": "1" + }, + { + "name": "p5", + "version": "1" + } + ], + "untested": [ + { + "name": "p6", + "version": "1" + }, + { + "name": "p7", + "version": "1" + }, + { + "name": "p8", + "version": "1" + } + ], + "successes": [ + { + "name": "p1", + "version": "1" + }, + { + "name": "p2", + "version": "1" + }, + { + "name": "p3", + "version": "1" + }, + { + "name": "p4", + "version": "1" + }, + { + "name": "p5", + "version": "1" + } + ] +}