diff --git a/.envrc b/.envrc index b459b5e..abfcc90 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,3 @@ -use nix -source_env_if_exists .envrc.private # allow custom extensions +PATH_add $HOME/.ghcup/bin +PATH_add $(pwd)/bin/cabal +PATH_add $(pwd)/bin/ghc/bin diff --git a/.github/workflows/haskell.yml b/.github/workflows/haskell.yml new file mode 100644 index 0000000..b6bd460 --- /dev/null +++ b/.github/workflows/haskell.yml @@ -0,0 +1,68 @@ +# Stripped down version of https://github.com/haskell-actions/setup#model-cabal-workflow-with-caching + +name: Haskell build +on: + push: + branches: [main] + pull_request: + +jobs: + build: + name: GHC ${{ matrix.ghc-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ./slides + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + ghc-version: ['9.4.7'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up GHC ${{ matrix.ghc-version }} + uses: haskell-actions/setup@v2 + id: setup + with: + ghc-version: ${{ matrix.ghc-version }} + # Defaults, added for clarity: + cabal-version: '3.6.2.0' + cabal-update: true + + - name: Configure the build + run: | + cabal configure --enable-tests --enable-benchmarks --disable-documentation + cabal build --dry-run + # The last step generates dist-newstyle/cache/plan.json for the cache key. + + - name: Restore cached dependencies + uses: actions/cache/restore@v3 + id: cache + env: + key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }} + with: + path: ${{ steps.setup.outputs.cabal-store }} + key: ${{ env.key }}-plan-${{ hashFiles('**/plan.json') }} + restore-keys: ${{ env.key }}- + + - name: Install dependencies + # If we had an exact cache hit, the dependencies will be up to date. + if: steps.cache.outputs.cache-hit != 'true' + run: cabal build all --only-dependencies + + # Cache dependencies already here, so that we do not have to rebuild them should the subsequent steps fail. + - name: Save cached dependencies + uses: actions/cache/save@v3 + # If we had an exact cache hit, trying to save the cache would error because of key clash. + if: steps.cache.outputs.cache-hit != 'true' + with: + path: ${{ steps.setup.outputs.cabal-store }} + key: ${{ steps.cache.outputs.cache-primary-key }} + + - name: Build + run: cabal build all + + - name: Run tests + run: cabal test all diff --git a/.gitignore b/.gitignore index bdeaf3b..6901457 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ .direnv .venv -ghc -hls -tps/dist-newstyle +bin +dist-newstyle slides/*.java -slides/app/Course*.hs -slides/dist-newstyle +slides/Course*.hs diff --git a/README.md b/README.md index 32d928e..e2d9973 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,24 @@ But I've never done it. ### Development instruction -- GHC and hls are installed in an isolated manner at the top-level of the repo +- The Haskell compiler (GHC) is installed in an isolated manner, in `bin/ghc` (see below) - This requires [ghcup](https://www.haskell.org/ghcup) in `PATH` and that's all +- When started from this directory, the [vscode Haskell extension](https://github.com/haskell/vscode-haskell) + will install the required [language server](https://github.com/haskell/haskell-language-server) on its own, + so nothing to do here. ``` -# Populate GHC, this matches PATH_ADD $(pwd)/ghc/bin in .envrc -ghcup install ghc 8.10.7 --isolate $(pwd)/ghc -# Populate hls, this matches PATH_ADD $(pwd)/hls/bin in .envrc -ghcup install hls --isolate $(pwd)/hls +mkdir -p bin/ghc +# Instal cabal, this matches PATH_ADD $(pwd)/bin/ghc/bin in .envrc +ghcup install cabal --isolate $(pwd)/bin/cabal +# Populate GHC, this matches PATH_ADD $(pwd)/bin/ghc/bin in .envrc +# Note that GHC's version number is also in .github/workflows/haskell.yml +ghcup install ghc 9.4.7 --isolate $(pwd)/bin/ghc ``` +Because there is a [cabal.project](./cabal.project) file that pins the set of packages to a specific +timestamp, this project is highly reproducible. + --- This course is funded by my employer: [Tweag](https://www.tweag.io/) diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..abfb84a --- /dev/null +++ b/cabal.project @@ -0,0 +1,2 @@ +packages: . +index-state: 2023-08-22T10:00:00Z diff --git a/nix/sources.json b/nix/sources.json deleted file mode 100755 index 70ec039..0000000 --- a/nix/sources.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "exdown": { - "branch": "master", - "description": "Extract code blocks from markdown files", - "homepage": "", - "owner": "smelc", - "repo": "exdown", - "rev": "912b454f9823f466708dd98dbdd8be837d67af65", - "sha256": "084hd63bxwamprmrkhcmprr3va1wkic6x48j959zfq88scwxn2l4", - "type": "tarball", - "url": "https://github.com/smelc/exdown/archive/912b454f9823f466708dd98dbdd8be837d67af65.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "nixpkgs": { - "branch": "nixpkgs-unstable", - "description": "Nix Packages collection", - "homepage": "https://github.com/NixOS/nixpkgs", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "abfd31179174133ab8131139d650297bf4da63b7", - "sha256": "1jmkz6l7sj876wzyn5niyfaxshbmw9fp3g8r41k1wbjvmm5xrnsn", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/abfd31179174133ab8131139d650297bf4da63b7.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - } -} diff --git a/nix/sources.nix b/nix/sources.nix deleted file mode 100755 index 1938409..0000000 --- a/nix/sources.nix +++ /dev/null @@ -1,174 +0,0 @@ -# This file has been generated by Niv. - -let - - # - # The fetchers. fetch_ fetches specs of type . - # - - fetch_file = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchurl { inherit (spec) url sha256; name = name'; } - else - pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; - - fetch_tarball = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchTarball { name = name'; inherit (spec) url sha256; } - else - pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; - - fetch_git = name: spec: - let - ref = - if spec ? ref then spec.ref else - if spec ? branch then "refs/heads/${spec.branch}" else - if spec ? tag then "refs/tags/${spec.tag}" else - abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"; - in - builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; }; - - fetch_local = spec: spec.path; - - fetch_builtin-tarball = name: throw - ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=tarball -a builtin=true''; - - fetch_builtin-url = name: throw - ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=file -a builtin=true''; - - # - # Various helpers - # - - # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 - sanitizeName = name: - ( - concatMapStrings (s: if builtins.isList s then "-" else s) - ( - builtins.split "[^[:alnum:]+._?=-]+" - ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) - ) - ); - - # The set of packages used when specs are fetched using non-builtins. - mkPkgs = sources: system: - let - sourcesNixpkgs = - import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; - hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; - hasThisAsNixpkgsPath = == ./.; - in - if builtins.hasAttr "nixpkgs" sources - then sourcesNixpkgs - else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then - import {} - else - abort - '' - Please specify either (through -I or NIX_PATH=nixpkgs=...) or - add a package called "nixpkgs" to your sources.json. - ''; - - # The actual fetching function. - fetch = pkgs: name: spec: - - if ! builtins.hasAttr "type" spec then - abort "ERROR: niv spec ${name} does not have a 'type' attribute" - else if spec.type == "file" then fetch_file pkgs name spec - else if spec.type == "tarball" then fetch_tarball pkgs name spec - else if spec.type == "git" then fetch_git name spec - else if spec.type == "local" then fetch_local spec - else if spec.type == "builtin-tarball" then fetch_builtin-tarball name - else if spec.type == "builtin-url" then fetch_builtin-url name - else - abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; - - # If the environment variable NIV_OVERRIDE_${name} is set, then use - # the path directly as opposed to the fetched source. - replace = name: drv: - let - saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; - ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; - in - if ersatz == "" then drv else - # this turns the string into an actual Nix path (for both absolute and - # relative paths) - if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; - - # Ports of functions for older nix versions - - # a Nix version of mapAttrs if the built-in doesn't exist - mapAttrs = builtins.mapAttrs or ( - f: set: with builtins; - listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) - ); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 - range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 - stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 - stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); - concatMapStrings = f: list: concatStrings (map f list); - concatStrings = builtins.concatStringsSep ""; - - # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 - optionalAttrs = cond: as: if cond then as else {}; - - # fetchTarball version that is compatible between all the versions of Nix - builtins_fetchTarball = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchTarball; - in - if lessThan nixVersion "1.12" then - fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) - else - fetchTarball attrs; - - # fetchurl version that is compatible between all the versions of Nix - builtins_fetchurl = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchurl; - in - if lessThan nixVersion "1.12" then - fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) - else - fetchurl attrs; - - # Create the final "sources" from the config - mkSources = config: - mapAttrs ( - name: spec: - if builtins.hasAttr "outPath" spec - then abort - "The values in sources.json should not have an 'outPath' attribute" - else - spec // { outPath = replace name (fetch config.pkgs name spec); } - ) config.sources; - - # The "config" used by the fetchers - mkConfig = - { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null - , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile) - , system ? builtins.currentSystem - , pkgs ? mkPkgs sources system - }: rec { - # The sources, i.e. the attribute set of spec name to spec - inherit sources; - - # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers - inherit pkgs; - }; - -in -mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/shell.nix b/shell.nix deleted file mode 100755 index aa70353..0000000 --- a/shell.nix +++ /dev/null @@ -1,29 +0,0 @@ -{ system ? builtins.currentSystem, sources ? import ./nix/sources.nix, ghcVersion ? "8107" }: - -let - overlay = _: pkgs: { - exdown = pkgs.writeShellApplication { - name = "exdown.py"; - text = '' - python3 ${sources.exdown}/exdown.py "$@" - ''; - runtimeInputs = [ pkgs.python38 ]; - }; - }; - pkgs = import sources.nixpkgs { inherit system; overlays = [ overlay ]; }; -in with pkgs; - -mkShell { - # Set UTF-8 local so that run-tests can parse GHC's unicode output. - LANG="C.UTF-8"; - NIX_PATH = "nixpkgs=${pkgs.path}"; - - buildInputs = [ - exdown - haskell.compiler."ghc${ghcVersion}" - cabal-install - nix - bash - jdk - ]; -} diff --git a/slides/app/Main.hs b/slides/app/Main.hs deleted file mode 100644 index 65ae4a0..0000000 --- a/slides/app/Main.hs +++ /dev/null @@ -1,4 +0,0 @@ -module Main where - -main :: IO () -main = putStrLn "Hello, Haskell!" diff --git a/slides/build_hs.sh b/slides/extract_hs.sh similarity index 77% rename from slides/build_hs.sh rename to slides/extract_hs.sh index 0548794..0f53eb2 100755 --- a/slides/build_hs.sh +++ b/slides/extract_hs.sh @@ -1,16 +1,15 @@ #!/usr/bin/env bash -set -e # -# Generate the .hs files in app, taking the snippets +# Generate the .hs files, taking the snippets # from the various *.md files as input +set -e + for f in $(ls *.md | grep course) do hs_module_name=$(echo $f | tr -d '-' | sed 's/\.md$//') hs_module_name=${hs_module_name^} # Put first character uppercase - f_hs="app/${hs_module_name}.hs" + f_hs="${hs_module_name}.hs" rm -Rf "$f_hs" exdown.py -f hs $f > "$f_hs" || exit 1 # exdown is https://github.com/smelc/exdown done - -cabal build diff --git a/slides/slides.cabal b/slides/slides.cabal deleted file mode 100644 index a6c6880..0000000 --- a/slides/slides.cabal +++ /dev/null @@ -1,30 +0,0 @@ -cabal-version: 2.4 -name: slides -version: 0.1.0.0 -synopsis: Boilerplate to check Haskell snippets of the slides -author: smelc -maintainer: smelc@users.noreply.github.com -extra-source-files: - -executable slides - main-is: Main.hs - - -- Modules included in this executable, other than Main. - other-modules: - Course01 - Course02 - Course03 - Course04 - - -- LANGUAGE extensions used by modules in this package. - -- other-extensions: - build-depends: base ^>=4.14.1.0 - hs-source-dirs: app - default-language: Haskell2010 - ghc-options: - -Wall - -Wno-name-shadowing - -Werror - -fno-code - -fprof-auto - -rtsopts diff --git a/tn-fp-course.cabal b/tn-fp-course.cabal new file mode 100644 index 0000000..c89df09 --- /dev/null +++ b/tn-fp-course.cabal @@ -0,0 +1,85 @@ +cabal-version: 2.4 +name: tn-fp-course +version: 2023 + +synopsis: Lab exercises for Telecom Nancy Functional Programming course +bug-reports: https://github.com/smelc/tn-fp-haskell-course +license: Unlicense +author: Clément Hurlin +maintainer: smelc@users.noreply.github.com + +common common-all + default-language: Haskell2010 + build-depends: base ^>=4.17.0.0 + , aeson + , bytestring + , containers + , extra + , generic-random + , http-conduit + , ilist + , megaparsec + , mtl + , random + , random-shuffle + , req + , process + , QuickCheck + , scotty + , text + , vector + ghc-options: -Wall + -Wunticked-promoted-constructors + -Wno-name-shadowing + -Wno-unused-imports + -Werror + -Wwarn=missing-home-modules + default-extensions: DataKinds + DeriveGeneric + DuplicateRecordFields + NamedFieldPuns + LambdaCase + OverloadedRecordDot + OverloadedStrings + ScopedTypeVariables + TypeApplications + +library slides + import: common-all + hs-source-dirs: slides + + -- Modules included in this executable, other than Main. + other-modules: Course01 + Course02 + Course03 + Course04 + +executable TP1.hs + import: common-all + main-is: TP1.hs + hs-source-dirs: tps + +executable TP2.hs + import: common-all + main-is: TP2.hs + hs-source-dirs: tps + +executable TP3.hs + import: common-all + main-is: TP3.hs + hs-source-dirs: tps + +executable TP4.hs + import: common-all + main-is: TP4.hs + hs-source-dirs: tps + +executable TP5.hs + import: common-all + main-is: TP5.hs + hs-source-dirs: tps + +library + import: common-all + hs-source-dirs: tps + exposed-modules: Scratch diff --git a/tps/tps.cabal b/tps/tps.cabal deleted file mode 100644 index 22d9087..0000000 --- a/tps/tps.cabal +++ /dev/null @@ -1,77 +0,0 @@ -cabal-version: 2.4 -name: tps -version: 0.2.0.0 - -synopsis: Lab exercises for Telecom Nancy Functional Programming course -bug-reports: https://github.com/smelc/tn-fp-haskell-course -license: Unlicense -author: Clément Hurlin -maintainer: smelc@users.noreply.github.com - -common common-all - -- LANGUAGE extensions used by modules in this package. - default-extensions: - DeriveFoldable - DeriveFunctor - DeriveGeneric - DuplicateRecordFields - GeneralizedNewtypeDeriving - LambdaCase - NamedFieldPuns - OverloadedStrings - RecordWildCards - ScopedTypeVariables - - build-depends: base ^>=4.14.1.0, - aeson, - bytestring, - containers, - extra, - generic-random, - ilist, - mtl, - random, - random-shuffle, - req, - process, - QuickCheck, - scotty, - text, - vector - hs-source-dirs: . - default-language: Haskell2010 - ghc-options: - -haddock - -Wall - -Wno-name-shadowing - -Werror - -Wwarn=missing-home-modules - -Wno-unused-imports - -rtsopts - -executable TP1.hs - import: common-all - main-is: TP1.hs - - -- Modules included in this executable, other than Main. - -- other-modules: - -executable TP2.hs - import: common-all - main-is: TP2.hs - -executable TP3.hs - import: common-all - main-is: TP3.hs - -executable TP4.hs - import: common-all - main-is: TP4.hs - -executable TP5.hs - import: common-all - main-is: TP5.hs - -library - import: common-all - exposed-modules: Scratch