├── .github └── workflows │ ├── gh-pages.yml │ └── pr.yml ├── .gitignore ├── .vscode ├── configurationCache.log ├── dryrun.log ├── settings.json └── targets.log ├── Makefile ├── README.md ├── book ├── chapters.json └── chapters │ ├── basics │ ├── hello-world │ │ ├── chapter.json │ │ ├── console │ │ ├── flake.lock │ │ └── flake.nix │ └── setup │ │ ├── chapter.json │ │ └── console │ ├── flakes │ ├── devshells │ │ ├── chapter.json │ │ ├── console │ │ └── flake.nix │ ├── flake-utils │ │ ├── chapter.json │ │ └── flake.nix │ ├── overlays │ │ ├── chapter.json │ │ └── flake.nix │ └── packaging │ │ ├── chapter.json │ │ ├── console │ │ ├── flake.nix │ │ └── hello.c │ └── language │ ├── functions │ ├── chapter.json │ └── repl │ ├── let-in │ ├── chapter.json │ └── repl │ └── repl │ ├── chapter.json │ ├── console │ └── repl ├── flake.lock ├── flake.nix ├── templates ├── chapter.html └── index.html └── tools ├── .ocamlformat ├── README.md ├── bin ├── dune └── main.ml ├── byexample.opam ├── dune-project └── lib ├── byexample.ml ├── dune └── watch.ml /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - name: get code 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup ocaml 16 | uses: avsm/setup-ocaml@v1 17 | with: 18 | ocaml-version: 4.12.0 19 | # ^^^^^^ 20 | 21 | - name: Build 22 | run: | 23 | eval $(opam env) 24 | make deps 25 | make 26 | 27 | - name: Deploy 28 | uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./dist 32 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Check pull requests 2 | 3 | on: 4 | pull_request 5 | 6 | jobs: 7 | check_pr: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: 12 | - macos-latest 13 | - ubuntu-latest 14 | ocaml-version: 15 | - 4.12.0 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Get code 21 | uses: actions/checkout@v2 22 | 23 | - name: Use OCaml ${{ matrix.ocaml-version }} 24 | uses: avsm/setup-ocaml@v1 25 | with: 26 | ocaml-version: ${{ matrix.ocaml-version }} 27 | 28 | - name: Build 29 | run: | 30 | eval $(opam env) 31 | make deps 32 | make 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | dist 3 | .direnv 4 | -------------------------------------------------------------------------------- /.vscode/configurationCache.log: -------------------------------------------------------------------------------- 1 | {"buildTargets":["all","deps","watch"],"launchTargets":[],"customConfigurationProvider":{"workspaceBrowse":{"browsePath":[],"compilerArgs":[]},"fileIndex":[]}} -------------------------------------------------------------------------------- /.vscode/dryrun.log: -------------------------------------------------------------------------------- 1 | make --dry-run --always-make --keep-going --print-directory 2 | make: Entering directory `/Users/davidwong/Perso/nixbyexample' 3 | dune exec bin/main.exe --root ./tools 4 | make: Leaving directory `/Users/davidwong/Perso/nixbyexample' 5 | 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "makefile.extensionOutputFolder": "./.vscode" 3 | } -------------------------------------------------------------------------------- /.vscode/targets.log: -------------------------------------------------------------------------------- 1 | make all --print-data-base --no-builtin-variables --no-builtin-rules --question 2 | # GNU Make 3.81 3 | # Copyright (C) 2006 Free Software Foundation, Inc. 4 | # This is free software; see the source for copying conditions. 5 | # There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A 6 | # PARTICULAR PURPOSE. 7 | 8 | # This program built for i386-apple-darwin11.3.0 9 | 10 | 11 | # Make data base, printed on Tue Aug 9 00:54:40 2022 12 | 13 | # Variables 14 | 15 | # automatic 16 | /nix/store/zrm3agz6hqncqbbpmn8v3w70qzzd2wdd-hello-2.12.1 20 | 21 | $ /nix/store/zrm3agz6hqncqbbpmn8v3w70qzzd2wdd-hello-2.12.1/bin/hello 22 | Hello, world! 23 | 24 | $ nix flake show 25 | path:/some/path/hello?lastModified=1659875514&narHash=sha256-mse3RPTu2OYYM%2fteTK54p6xgO5QpyYRiVdjZgj80%2frQ= 26 | ├───defaultPackage 27 | │ ├───x86_64-darwin: package 'hello-2.12.1' 28 | │ └───x86_64-linux: package 'hello-2.12.1' 29 | └───packages 30 | ├───x86_64-darwin 31 | │ └───hello: package 'hello-2.12.1' 32 | └───x86_64-linux 33 | └───hello: package 'hello-2.12.1' -------------------------------------------------------------------------------- /book/chapters/basics/hello-world/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1659782844, 6 | "narHash": "sha256-tM/qhHFE61puBxh9ebP3BIG1fkRAT4rHqD3jCM0HXGY=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "c85e56bb060291eac3fb3c75d4e0e64f6836fcfe", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "nixpkgs", 14 | "type": "indirect" 15 | } 16 | }, 17 | "root": { 18 | "inputs": { 19 | "nixpkgs": "nixpkgs" 20 | } 21 | } 22 | }, 23 | "root": "root", 24 | "version": 7 25 | } -------------------------------------------------------------------------------- /book/chapters/basics/hello-world/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A very basic flake"; 3 | 4 | outputs = { self, nixpkgs }: { 5 | 6 | packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello; 7 | 8 | defaultPackage.x86_64-linux = self.packages.x86_64-linux.hello; 9 | 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /book/chapters/basics/setup/chapter.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "setup", 3 | "sections": [ 4 | { 5 | "file": "console", 6 | "lang": "console", 7 | "explanations": [ 8 | { 9 | "line": 1, 10 | "text": "You can install nix by following the instruction on their [github](https://github.com/NixOS/nix) or executing the following command in your terminal." 11 | }, 12 | { 13 | "line": 3, 14 | "text": "Nix's manpages are really good and you should use them as first documentation." 15 | } 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /book/chapters/basics/setup/console: -------------------------------------------------------------------------------- 1 | $ curl -L https://nixos.org/nix/install | sh 2 | 3 | # nix --help -------------------------------------------------------------------------------- /book/chapters/flakes/devshells/chapter.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "devshells", 3 | "sections": [ 4 | { 5 | "file": "flake.nix", 6 | "lang": "nix", 7 | "explanations": [ 8 | { 9 | "line": 1, 10 | "text": "you can create devshells manually, but they won't use the derivation you created and the packages there. So not sure how useful it is." 11 | } 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /book/chapters/flakes/devshells/console: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimoo/nixbyexample/a654ddb3b17ca70fe30754bf85698de01b2ea12d/book/chapters/flakes/devshells/console -------------------------------------------------------------------------------- /book/chapters/flakes/devshells/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "My own hello world"; 3 | 4 | outputs = { self, nixpkgs }: ( 5 | let 6 | build_for = system: 7 | let 8 | pkgs = import nixpkgs { inherit system; }; 9 | in 10 | pkgs.stdenv.mkDerivation { 11 | name = "hello"; 12 | src = self; 13 | buildInputs = [ pkgs.gcc ]; 14 | buildPhase = "gcc -o hello ./hello.c"; 15 | installPhase = "mkdir -p $out/bin; install -t $out/bin hello"; 16 | }; 17 | shell_for = system: 18 | let 19 | pkgs = import nixpkgs { inherit system; }; 20 | in 21 | pkgs.mkShell { 22 | packages = [ 23 | pkgs.gcc 24 | pkgs.python 25 | ]; 26 | }; 27 | in 28 | { 29 | packages.x86_64-darwin.default = build_for "x86_64-darwin"; 30 | packages.x86_64-linux.default = build_for "x86_64-linux"; 31 | 32 | devShells.x86_64-darwin.default = shell_for "x86_64-darwin"; 33 | devShells.x86_64-linux.default = shell_for "x86_64-linux"; 34 | }); 35 | } -------------------------------------------------------------------------------- /book/chapters/flakes/flake-utils/chapter.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "flake-utils", 3 | "sections": [ 4 | { 5 | "file": "flake.nix", 6 | "lang": "nix", 7 | "explanations": [ 8 | { 9 | "line": 1, 10 | "text": "Writing the same build instructions for each systems is a bit tedious, fortunately you can use [flake-utils](https://github.com/numtide/flake-utils) to automate all of that." 11 | } 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /book/chapters/flakes/flake-utils/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "My own hello world"; 3 | 4 | outputs = { self, nixpkgs, flake-utils }: 5 | flake-utils.lib.eachDefaultSystem (system: 6 | let 7 | pkgs = import nixpkgs { inherit system; }; 8 | in 9 | rec { 10 | 11 | packages.hello = pkgs.stdenv.mkDerivation { 12 | name = "hello"; 13 | src = self; 14 | buildInputs = [ pkgs.gcc ]; 15 | buildPhase = "gcc -o hello ./hello.c"; 16 | installPhase = "mkdir -p $out/bin; install -t $out/bin hello"; 17 | }; 18 | 19 | defaultPackage = packages.hello; 20 | 21 | } 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /book/chapters/flakes/overlays/chapter.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "overlays", 3 | "sections": [ 4 | { 5 | "file": "flake.nix", 6 | "lang": "nix", 7 | "explanations": [ 8 | { 9 | "line": 1, 10 | "text": "TODO" 11 | } 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /book/chapters/flakes/overlays/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "overlay example"; 3 | 4 | outputs = { self, nixpkgs }: 5 | let 6 | system = "x86_64-linux"; 7 | pkgs = import nixpkgs { 8 | inherit system; 9 | overlays = [ 10 | self.overlays.default 11 | ]; 12 | config = { allowUnfree = true; }; 13 | }; 14 | in { 15 | overlays.default = (final: prev: rec { 16 | 17 | }); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /book/chapters/flakes/packaging/chapter.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "packaging 101", 3 | "sections": [ 4 | { 5 | "file": "hello.c", 6 | "lang": "c", 7 | "explanations": [ 8 | { 9 | "line": 1, 10 | "text": "Let's imagine that we want to package a C binary, so that one can easily install it or develop on it." 11 | } 12 | ] 13 | }, 14 | { 15 | "file": "flake.nix", 16 | "lang": "nix", 17 | "explanations": [ 18 | { 19 | "line": 1, 20 | "text": "The first thing to do is to create a `flake.nix` file." 21 | }, 22 | { 23 | "line": 4, 24 | "text": "a flake must contain an `outputs` function that takes an attribute set of at least `self` and an optional number of packages to use (here just `nixpkgs`)." 25 | }, 26 | { 27 | "line": 5, 28 | "text": "The first thing we do here, is to create a function that we will reuse to build our package for different systems (mac and linux). This function takes one argument: `system`." 29 | }, 30 | { 31 | "line": 7, 32 | "text": "We import `nixpkgs` with the `system` that we passed as argument (same as writing `system = system;` in the second argument of `import`." 33 | }, 34 | { 35 | "line": 10, 36 | "text": "`stdenv.mkDerivation` allows us to create a **derivation**, which is nix's term for a package." 37 | }, 38 | { 39 | "line": 18, 40 | "text": "the `output` function must return an attribute set containing default packages for one or more platforms." 41 | } 42 | ] 43 | }, 44 | { 45 | "file": "console", 46 | "lang": "console", 47 | "explanations": [ 48 | { 49 | "line": 1, 50 | "text": "You can use `nix flake check` to make sure that the flake is correctly written." 51 | }, 52 | { 53 | "line": 3, 54 | "text": "While you can build your derivation using `nix build`, you can also directly run it using `nix run` (which will try to run `/bin/`)." 55 | }, 56 | { 57 | "line": 6, 58 | "text": "Using `nix develop -i` you can open a shell that comprises the same dependencies as your build environment. (Note that `which` is accessible because we added it to the `buildInputs` of the derivation." 59 | } 60 | ] 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /book/chapters/flakes/packaging/console: -------------------------------------------------------------------------------- 1 | $ nix flake check 2 | 3 | $ nix run 4 | hello world 5 | 6 | $ nix develop -i 7 | $ which gcc 8 | /nix/store/h8v9jm7zsjzccp08c9jb5vh65yba02lb-gcc-wrapper-11.2.0/bin/gcc -------------------------------------------------------------------------------- /book/chapters/flakes/packaging/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "My own hello world"; 3 | 4 | outputs = { self, nixpkgs }: 5 | let 6 | build_for = system: 7 | let 8 | pkgs = import nixpkgs { inherit system; }; 9 | in 10 | pkgs.stdenv.mkDerivation { 11 | name = "hello"; 12 | src = self; 13 | buildInputs = [ pkgs.gcc pkgs.which ]; 14 | buildPhase = "gcc -o hello ./hello.c"; 15 | installPhase = "mkdir -p $out/bin; install -t $out/bin hello"; 16 | }; 17 | in 18 | { 19 | packages.x86_64-darwin.default = build_for "x86_64-darwin"; 20 | packages.x86_64-linux.default = build_for "x86_64-linux"; 21 | }; 22 | } -------------------------------------------------------------------------------- /book/chapters/flakes/packaging/hello.c: -------------------------------------------------------------------------------- 1 | int main() { 2 | printf("hello world"); 3 | return 0; 4 | } 5 | -------------------------------------------------------------------------------- /book/chapters/language/functions/chapter.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "functions", 3 | "sections": [ 4 | { 5 | "file": "repl", 6 | "lang": "console", 7 | "explanations": [ 8 | { 9 | "line": 1, 10 | "text": "..." 11 | } 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /book/chapters/language/functions/repl: -------------------------------------------------------------------------------- 1 | nix-repl> let 2 | add = a: b: a + b; 3 | in 4 | add 1 2 5 | 3 -------------------------------------------------------------------------------- /book/chapters/language/let-in/chapter.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "let ... in ...", 3 | "sections": [ 4 | { 5 | "file": "repl", 6 | "lang": "console", 7 | "explanations": [ 8 | { 9 | "line": 1, 10 | "text": "..." 11 | } 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /book/chapters/language/let-in/repl: -------------------------------------------------------------------------------- 1 | nix-repl> let 2 | a = 5; 3 | b = 7; 4 | in 5 | a + b 6 | 12 -------------------------------------------------------------------------------- /book/chapters/language/repl/chapter.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "REPL", 3 | "sections": [ 4 | { 5 | "file": "console", 6 | "lang": "console", 7 | "explanations": [ 8 | { 9 | "line": 1, 10 | "text": "The `nix repl` command opens a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) which can be handy to debug or play with the language. As with most REPLs, `CTRL-c` allows you to cancel a command, and `CTRL-d` allows you to quit the REPL." 11 | }, 12 | { 13 | "line": 3, 14 | "text": "The nix language is a function language. If you've done some OCaml before, it should not be too hard to ramp up. I recommend to read the Nix page on [learnXinYminutes](https://learnxinyminutes.com/docs/nix/), although it would be nice to have explanations on this page." 15 | } 16 | ] 17 | }, 18 | { 19 | "file": "repl", 20 | "lang": "console", 21 | "explanations": [ 22 | { 23 | "line": 1, 24 | "text": "The nix language is a function language. If you've done some OCaml before, it should not be too hard to ramp up. I recommend to read the Nix page on [learnXinYminutes](https://learnxinyminutes.com/docs/nix/), although it would be nice to have explanations on this page." 25 | } 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /book/chapters/language/repl/console: -------------------------------------------------------------------------------- 1 | $ nix repl 2 | 3 | Welcome to Nix 2.9.1. Type :? for help. 4 | 5 | nix-repl> -------------------------------------------------------------------------------- /book/chapters/language/repl/repl: -------------------------------------------------------------------------------- 1 | nix-repl> 1 + 1 2 | 2 3 | 4 | nix-repl> (import ./flake.nix) 5 | { description = "A very basic flake"; outputs = «lambda @ /some/path/hello/flake.nix:4:13»; } -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1627913399, 7 | "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "locked": { 21 | "lastModified": 1659877975, 22 | "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", 23 | "owner": "numtide", 24 | "repo": "flake-utils", 25 | "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "numtide", 30 | "repo": "flake-utils", 31 | "type": "github" 32 | } 33 | }, 34 | "flake-utils_2": { 35 | "locked": { 36 | "lastModified": 1638122382, 37 | "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", 38 | "owner": "numtide", 39 | "repo": "flake-utils", 40 | "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", 41 | "type": "github" 42 | }, 43 | "original": { 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "type": "github" 47 | } 48 | }, 49 | "nix-filter": { 50 | "locked": { 51 | "lastModified": 1659352118, 52 | "narHash": "sha256-X/Tdlj/PYxcQg/1hcHXxdnDr5zLO22LohIudX+oT968=", 53 | "owner": "numtide", 54 | "repo": "nix-filter", 55 | "rev": "3e1fff9ec0112fe5ec61ea7cc6d37c1720d865f8", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "numtide", 60 | "repo": "nix-filter", 61 | "type": "github" 62 | } 63 | }, 64 | "nixpkgs": { 65 | "locked": { 66 | "lastModified": 1659987637, 67 | "narHash": "sha256-8l+5QiCkackVPu/F3vX7RCKHyYKxEsq/TKMuaG6UX5k=", 68 | "owner": "NixOS", 69 | "repo": "nixpkgs", 70 | "rev": "a47896bf817e7324471e687fc2bb2312fff682ce", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "id": "nixpkgs", 75 | "type": "indirect" 76 | } 77 | }, 78 | "nixpkgs_2": { 79 | "locked": { 80 | "lastModified": 1657802959, 81 | "narHash": "sha256-9+JWARSdlL8KiH3ymnKDXltE1vM+/WEJ78F5B1kjXys=", 82 | "owner": "nixos", 83 | "repo": "nixpkgs", 84 | "rev": "4a01ca36d6bfc133bc617e661916a81327c9bbc8", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "nixos", 89 | "ref": "nixos-unstable", 90 | "repo": "nixpkgs", 91 | "type": "github" 92 | } 93 | }, 94 | "nixpkgs_3": { 95 | "locked": { 96 | "lastModified": 1640418986, 97 | "narHash": "sha256-a8GGtxn2iL3WAkY5H+4E0s3Q7XJt6bTOvos9qqxT5OQ=", 98 | "owner": "NixOS", 99 | "repo": "nixpkgs", 100 | "rev": "5c37ad87222cfc1ec36d6cd1364514a9efc2f7f2", 101 | "type": "github" 102 | }, 103 | "original": { 104 | "owner": "nixos", 105 | "ref": "nixos-unstable", 106 | "repo": "nixpkgs", 107 | "type": "github" 108 | } 109 | }, 110 | "opam-nix": { 111 | "inputs": { 112 | "flake-compat": "flake-compat", 113 | "flake-utils": "flake-utils_2", 114 | "nixpkgs": "nixpkgs_2", 115 | "opam-repository": "opam-repository", 116 | "opam2json": "opam2json" 117 | }, 118 | "locked": { 119 | "lastModified": 1658239448, 120 | "narHash": "sha256-inyixa1kWMwEL/LCGJkHSL/FJyDZFC3y62QqhXTSKu8=", 121 | "owner": "tweag", 122 | "repo": "opam-nix", 123 | "rev": "2fc0fcf144a66d0f94c5c6314c1af06511639bd5", 124 | "type": "github" 125 | }, 126 | "original": { 127 | "owner": "tweag", 128 | "repo": "opam-nix", 129 | "type": "github" 130 | } 131 | }, 132 | "opam-repository": { 133 | "flake": false, 134 | "locked": { 135 | "lastModified": 1657825363, 136 | "narHash": "sha256-snzLtiePirVuba+K1jVCtKGKanOabsmmZJIe/aXbSOM=", 137 | "owner": "ocaml", 138 | "repo": "opam-repository", 139 | "rev": "476a0ed6d0ab487b5fcad2be574803a617dc9745", 140 | "type": "github" 141 | }, 142 | "original": { 143 | "owner": "ocaml", 144 | "repo": "opam-repository", 145 | "type": "github" 146 | } 147 | }, 148 | "opam2json": { 149 | "inputs": { 150 | "nixpkgs": "nixpkgs_3" 151 | }, 152 | "locked": { 153 | "lastModified": 1651529032, 154 | "narHash": "sha256-fe8bm/V/4r2iNxgbitT2sXBqDHQ0GBSnSUSBg/1aXoI=", 155 | "owner": "tweag", 156 | "repo": "opam2json", 157 | "rev": "e8e9f2fa86ef124b9f7b8db41d3d19471c1d8901", 158 | "type": "github" 159 | }, 160 | "original": { 161 | "owner": "tweag", 162 | "repo": "opam2json", 163 | "type": "github" 164 | } 165 | }, 166 | "root": { 167 | "inputs": { 168 | "flake-utils": "flake-utils", 169 | "nix-filter": "nix-filter", 170 | "nixpkgs": "nixpkgs", 171 | "opam-nix": "opam-nix" 172 | } 173 | } 174 | }, 175 | "root": "root", 176 | "version": 7 177 | } 178 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | # flake taken from github.com/brendanzab/ocaml-flake-example 2 | { 3 | description = "The nixbyexample flake"; 4 | 5 | inputs = { 6 | # Convenience functions for writing flakes 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | 9 | # Precisely filter files copied to the nix store 10 | nix-filter.url = "github:numtide/nix-filter"; 11 | }; 12 | 13 | outputs = { self, nixpkgs, flake-utils, nix-filter }: 14 | flake-utils.lib.eachDefaultSystem (system: 15 | let 16 | # Legacy packages that have not been converted to flakes 17 | legacyPackages = nixpkgs.legacyPackages.${system}; 18 | # OCaml packages available on nixpkgs 19 | ocamlPackages = legacyPackages.ocamlPackages; 20 | # Library functions from nixpkgs 21 | lib = legacyPackages.lib; 22 | 23 | # OCaml source files 24 | ocaml-src = nix-filter.lib.filter { 25 | root = ./tools; 26 | include = [ 27 | ".ocamlformat" 28 | "dune-project" 29 | "byexample.opam" 30 | (nix-filter.lib.matchExt "opam") 31 | (nix-filter.lib.inDirectory "bin") 32 | (nix-filter.lib.inDirectory "lib") 33 | ]; 34 | }; 35 | 36 | # Nix source files 37 | nix-src = nix-filter.lib.filter { 38 | root = ./.; 39 | include = [ 40 | (nix-filter.lib.matchExt "nix") 41 | ]; 42 | }; 43 | in 44 | { 45 | # Executed by `nix build .#` 46 | packages = { 47 | # Executed by `nix build .#byexample` 48 | byexample = ocamlPackages.buildDunePackage { 49 | pname = "byexample"; 50 | version = "0.1.0"; 51 | # Would be nice to use dune 3.x, but the odoc package needs to be 52 | # updated first. 53 | duneVersion = "3"; 54 | 55 | src = ocaml-src; 56 | 57 | outputs = [ "doc" "out" ]; 58 | 59 | nativeBuildInputs = [ 60 | ocamlPackages.odoc 61 | ocamlPackages.yojson 62 | ocamlPackages.jingoo 63 | ocamlPackages.ppx_deri 64 | ]; 65 | 66 | strictDeps = true; 67 | 68 | preBuild = '' 69 | dune build ./bin/main.exe 70 | ''; 71 | 72 | postBuild = '' 73 | echo "building docs" 74 | dune build @doc -p byexample 75 | ''; 76 | 77 | postInstall = '' 78 | echo "Installing $doc/share/doc/byexample/html" 79 | mkdir -p $doc/share/doc/byexample/html 80 | cp -r _build/default/_doc/_html/* $doc/share/doc/byexample/html 81 | ''; 82 | }; 83 | 84 | # Executed by `nix build` 85 | default = self.packages.${system}.byexample; 86 | }; 87 | 88 | # Executed by `nix run .# ` 89 | apps = { 90 | # Executed by `nix run .#byexample` 91 | byexample = { 92 | type = "app"; 93 | program = "${self.packages.${system}.byexample}/bin/byexample"; 94 | }; 95 | 96 | # Executed by `nix run` 97 | default = self.apps.${system}.byexample; 98 | }; 99 | 100 | 101 | # Used by `nix develop` 102 | devShells = { 103 | default = legacyPackages.mkShell { 104 | # Development tools 105 | packages = [ 106 | # Source file formatting 107 | legacyPackages.nixpkgs-fmt 108 | legacyPackages.ocamlformat 109 | # For `dune build --watch ...` 110 | legacyPackages.fswatch 111 | # OCaml editor support 112 | ocamlPackages.ocaml-lsp 113 | # Nicely formatted types on hover 114 | ocamlPackages.ocamlformat-rpc-lib 115 | # Fancy REPL thing 116 | ocamlPackages.utop 117 | ]; 118 | 119 | # Tools from packages 120 | inputsFrom = [ 121 | self.packages.${system}.byexample 122 | ]; 123 | }; 124 | }; 125 | }); 126 | } 127 | -------------------------------------------------------------------------------- /templates/chapter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Nix By Example | {{ title }} 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 121 | 122 | 123 | 124 |
125 |
126 |
127 |
128 | 129 |
130 |

Nix By Examples

131 |
132 |
133 | 134 | 135 |
136 |
137 | 138 |
139 | {% set ns = namespace (found=false, folder="", title="") %} 140 | {% for part in parts %} 141 |

{{ part.title }}

142 |
    143 | {% for c in part.chapters %} 144 | 145 | {% if ns.found %} 146 | {% set ns.folder = c.folder %} 147 | {% set ns.title = c.title %} 148 | {% set ns.found = false %} 149 | {% endif %} 150 | 151 | {% if c.folder == chapter.folder %} 152 | {% set ns.found = true -%} 153 | {% endif %} 154 | 155 | {% if c.folder == chapter.folder || c.folder == "" %} 156 | 157 |
  • {{ c.title}}
  • 158 | 159 | {% else %} 160 | 161 |
  • {{ c.title}}
  • 162 | 163 | {% endif %} 164 | 165 | {% endfor %} 166 |
167 | {% endfor %} 168 |
169 | 170 |
171 | 172 |
173 | 174 |
175 | 176 |

{{ chapter.title }}

177 | 178 | {% for section in chapter.sections %} 179 | 180 | 181 | 182 | 185 | 186 | {% for explanation in section.explanations %} 187 | 188 | 191 | 194 | 195 | {% endfor %} 196 |
183 |

{{ section.file }}

184 |
189 | {{ explanation.explanation }} 190 | 192 |
{{ explanation.code }}
193 |
197 | {% endfor %} 198 | 199 |
200 | {% if ns.folder != "" %} 201 | next: {{ns.title}} 202 | {% elseif ns.title != "" %} 203 | next: {{ns.title}} 204 | {% else %} 205 | you've reached the end! 206 | {% endif %} 207 |
208 | 209 |
210 |
211 |
212 |
213 | 214 |
215 |
216 | This is a work-in-progress. Help the project by contributing. 218 |
219 |
220 | 221 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Nix By Example 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 |
77 |
78 | 79 |
80 |

Nix By Examples

81 |
82 |
83 | 84 | 85 |
86 |
87 | 88 |
89 | {% for part in parts %} 90 |

{{ part.title }}

91 |
    92 | {% for c in part.chapters %} 93 | 94 | {% if c.folder == chapter.folder || c.folder == "" %} 95 | 96 |
  • {{ c.title}}
  • 97 | 98 | {% else %} 99 | 100 |
  • {{ c.title}}
  • 101 | 102 | {% endif %} 103 | 104 | {% endfor %} 105 |
106 | {% endfor %} 107 |
108 | 109 |
110 | 111 |
112 | 113 |
114 | Nix is a package manager that allows you to setup a dev environment, as well as a build environment, 115 | for reproducible builds.
116 | Nix != NixOS. 117 |
118 | 119 | 120 |
121 |
122 | 123 |
124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /tools/.ocamlformat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimoo/nixbyexample/a654ddb3b17ca70fe30754bf85698de01b2ea12d/tools/.ocamlformat -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | ``` 4 | tools/ 5 | ├── bin/ 6 | │ └── main.ml # code to build the page 7 | └── lib/ 8 | └── byexample.ml # logic to create examples 9 | ├── watch.ml # logic to watch a folder 10 | ``` 11 | -------------------------------------------------------------------------------- /tools/bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main) 3 | (libraries core yojson jingoo byexample) 4 | (preprocess 5 | (pps ppx_deriving.show))) 6 | -------------------------------------------------------------------------------- /tools/bin/main.ml: -------------------------------------------------------------------------------- 1 | let () = Byexample.main () 2 | -------------------------------------------------------------------------------- /tools/byexample.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | name: "byexample" 3 | version: "~dev" 4 | synopsis: "by Example" 5 | maintainer: "David Wong " 6 | authors: "David Wong " 7 | license: "The MIT License (MIT)" 8 | homepage: "https://github.com/o1-labs/ocamlbyexample" 9 | bug-reports: "David Wong " 10 | depends: [ 11 | "dune" {>= "2.8.5"} 12 | "core" {>= "0.14.1"} 13 | "core_unix" {>= "0.15.0"} 14 | "yojson" {>= "1.7.0"} 15 | "jingoo" {>= "1.4.3"} 16 | "ppx_deriving" {>= "5.2.1"} 17 | ] 18 | build: [ 19 | [make] 20 | ] 21 | install: [make "install"] 22 | -------------------------------------------------------------------------------- /tools/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.8) 2 | 3 | (name byexample) 4 | -------------------------------------------------------------------------------- /tools/lib/byexample.ml: -------------------------------------------------------------------------------- 1 | open Core 2 | 3 | (* constants *) 4 | 5 | let book_dir = "book" 6 | let chapters_list = book_dir ^ "/chapters.json" 7 | let chapters_dir = book_dir ^ "/chapters" 8 | let template_dir = "templates" 9 | let chapter_template = template_dir ^ "/chapter.html" 10 | let index_template = template_dir ^ "/index.html" 11 | let output_dir = "dist/" 12 | 13 | (* data structures *) 14 | 15 | type explanation = 16 | (* 1:1 matching with what's in the JSON file *) 17 | | Unparsed of { line : int; text : string } 18 | (* a parsed explanation includes the code related to a block of text *) 19 | | Parsed of { code : string list; explanation : string } 20 | [@@deriving show] 21 | 22 | type section = { 23 | (* path to the file *) 24 | file : string; 25 | (* this must be a class recognized by https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md *) 26 | lang : string; 27 | (* a file has several "blocks" of text explaining the code *) 28 | explanations : explanation list; 29 | } 30 | [@@deriving show] 31 | 32 | type chapter = { 33 | title : string; 34 | folder : string; 35 | (* a chapter has several files *) 36 | sections : section list; 37 | } 38 | [@@deriving show] 39 | 40 | type part = { 41 | title : string; 42 | (* each part has several chapters *) 43 | chapters : chapter list; 44 | } 45 | [@@deriving show] 46 | 47 | (* read JSON files *) 48 | 49 | let get_chapter folder = 50 | let chapter_file = chapters_dir ^ "/" ^ folder ^ "/" ^ "chapter.json" in 51 | match Sys_unix.file_exists chapter_file with 52 | | `Unknown | `No -> { title = folder; folder = ""; sections = [] } 53 | | `Yes -> 54 | (); 55 | let json = Yojson.Basic.from_file chapter_file in 56 | let open Yojson.Basic.Util in 57 | let title = json |> member "title" |> to_string in 58 | let to_explanation = function 59 | | `Assoc [ ("line", line); ("text", text) ] -> 60 | Unparsed { line = to_int line; text = to_string text } 61 | | _ -> 62 | failwith 63 | "misformated chapter.json: explanations must contain blocks of { \ 64 | line, text }" 65 | in 66 | let to_section = function 67 | | `Assoc 68 | [ ("file", file); ("lang", lang); ("explanations", explanations) ] 69 | -> 70 | let file = to_string file in 71 | let lang = to_string lang in 72 | let explanations = explanations |> convert_each to_explanation in 73 | { file; lang; explanations } 74 | | _ -> 75 | failwith 76 | "misformated chapter.json: sections must contain blocks of { \ 77 | file, explanations }" 78 | in 79 | let sections = json |> member "sections" |> convert_each to_section in 80 | { title; folder; sections } 81 | 82 | let get_part part = 83 | let open Yojson.Basic.Util in 84 | let title = part |> member "title" |> to_string in 85 | let chapters = part |> member "chapters" |> to_list |> filter_string in 86 | let chapters = List.map chapters ~f:get_chapter in 87 | { title; chapters } 88 | 89 | let get_parts _ = 90 | let json = Yojson.Basic.from_file chapters_list in 91 | let open Yojson.Basic.Util in 92 | let parts = json |> member "parts" |> to_list in 93 | let parts = List.map parts ~f:get_part in 94 | parts 95 | 96 | let get_code folder file_name = 97 | let file_name = chapters_dir ^ "/" ^ folder ^ "/" ^ file_name in 98 | In_channel.read_lines file_name 99 | 100 | (* transform *) 101 | 102 | let produce_explanation (code : string list) (ln : int) (e : explanation) 103 | (rest_expls : explanation list) = 104 | match e with 105 | | Parsed _ -> 106 | failwith "this shouldn't happen, explanation passed is already parsed" 107 | | Unparsed expl -> 108 | if ln < expl.line then 109 | (* explanation starts later *) 110 | let limit = expl.line - ln in 111 | let code, remaining_code = List.split_n code limit in 112 | let explanation = Parsed { code; explanation = "" } in 113 | let ln = expl.line in 114 | (explanation, ln, remaining_code, e :: rest_expls) 115 | else if ln = expl.line then 116 | (* explanation starts now *) 117 | match rest_expls with 118 | | [] -> 119 | (* and goes to the end *) 120 | let explanation = Parsed { code; explanation = expl.text } in 121 | let ln = ln + List.length code in 122 | let remaining_code = [] in 123 | (explanation, ln, remaining_code, []) 124 | | Unparsed next :: rest_expls -> 125 | (* and is upper bounded *) 126 | let limit = next.line - ln in 127 | let code, remaining_code = List.split_n code limit in 128 | let explanation = Parsed { code; explanation = expl.text } in 129 | let ln = next.line in 130 | let rest_expls = Unparsed next :: rest_expls in 131 | (explanation, ln, remaining_code, rest_expls) 132 | | _ -> 133 | failwith 134 | "this shouldn't happen, parsed explanation should be unparsed" 135 | else 136 | (* explanation starts before code *) 137 | let explanation = Parsed { code = []; explanation = expl.text } in 138 | (explanation, ln, code, rest_expls) 139 | 140 | let rec produce_explanations (result : explanation list) ln (code : string list) 141 | (explanations : explanation list) = 142 | match (code, explanations) with 143 | | [], [] -> result 144 | | code, [] -> result @ [ Parsed { code; explanation = "" } ] 145 | | [], [ Unparsed expl ] -> 146 | result @ [ Parsed { code = []; explanation = expl.text } ] 147 | | [], Unparsed expl :: _ -> 148 | eprintf "explanation dangling: %s" expl.text; 149 | failwith 150 | "misformated chapter.json: there can only be one trailing explanation \ 151 | (with no associated code)" 152 | | [], _ -> 153 | failwith 154 | "this shouldn't happen, trailing explanation has already been parsed" 155 | | code, expl :: rest_expls -> 156 | let explanation, ln, remaining_code, rest_expls = 157 | produce_explanation code ln expl rest_expls 158 | in 159 | let new_result = result @ [ explanation ] in 160 | produce_explanations new_result ln remaining_code rest_expls 161 | 162 | let parse_parts parts = 163 | let parse_section folder ({ file; explanations; _ } as section) = 164 | let code = get_code folder file in 165 | let explanations = produce_explanations [] 1 code explanations in 166 | { section with explanations } 167 | in 168 | let parse_chapter ({ folder; sections; _ } as chapter) = 169 | let sections = List.map sections ~f:(parse_section folder) in 170 | let folder = String.substr_replace_all folder ~pattern:"/" ~with_:"-" in 171 | { chapter with sections; folder } 172 | in 173 | let parse_part ({ chapters; _ } as part) = 174 | let chapters = List.map chapters ~f:parse_chapter in 175 | { part with chapters } 176 | in 177 | List.map parts ~f:parse_part 178 | 179 | (* ocaml -> models for HTML *) 180 | 181 | let chapter_to_model { title; folder; sections } = 182 | let explanation_to_model = function 183 | | Unparsed _ -> 184 | failwith "this shouldn't happen, explanations must be parsed first" 185 | | Parsed { code; explanation } -> 186 | let code = String.concat code ~sep:"\n" in 187 | let open Jingoo.Jg_types in 188 | Tobj [ ("code", Tstr code); ("explanation", Tstr explanation) ] 189 | in 190 | let section_to_model { file; lang; explanations } = 191 | let explanations = List.map explanations ~f:explanation_to_model in 192 | let open Jingoo.Jg_types in 193 | Tobj 194 | [ 195 | ("file", Tstr file); 196 | ("lang", Tstr lang); 197 | ("explanations", Tlist explanations); 198 | ] 199 | in 200 | let sections = List.map sections ~f:section_to_model in 201 | let open Jingoo in 202 | Jg_types.Tobj 203 | [ 204 | ("title", Jg_types.Tstr title); 205 | ("folder", Jg_types.Tstr folder); 206 | ("sections", Jg_types.Tlist sections); 207 | ] 208 | 209 | let chapters_to_model chapters = 210 | let chapters = List.map chapters ~f:chapter_to_model in 211 | Jingoo.Jg_types.Tlist chapters 212 | 213 | let part_to_model { title; chapters } = 214 | let chapters = chapters_to_model chapters in 215 | let open Jingoo in 216 | Jg_types.Tobj [ ("title", Jg_types.Tstr title); ("chapters", chapters) ] 217 | 218 | let parts_to_model parts = 219 | let parts = List.map parts ~f:part_to_model in 220 | Jingoo.Jg_types.Tlist parts 221 | 222 | (* models -> HTML *) 223 | 224 | let chapter_to_html parts chapter = 225 | let folder = chapter.folder in 226 | let chapter = chapter_to_model chapter in 227 | let models = [ ("parts", parts); ("chapter", chapter) ] in 228 | let result = Jingoo.Jg_template.from_file chapter_template ~models in 229 | (folder, result) 230 | 231 | let parts_to_index parts = 232 | let parts = parts_to_model parts in 233 | let models = [ ("parts", parts) ] in 234 | let result = Jingoo.Jg_template.from_file index_template ~models in 235 | result 236 | 237 | (* HTML -> disk *) 238 | 239 | let html_to_disk (name, data) = 240 | let name = String.substr_replace_all name ~pattern:"/" ~with_:"-" in 241 | let output_file = output_dir ^ name ^ ".html" in 242 | Out_channel.write_all output_file ~data 243 | 244 | let chapters_to_html parts chapters = 245 | let parts_mod = parts_to_model parts in 246 | let chapters = 247 | List.filter chapters ~f:(fun { folder; _ } -> not (String.is_empty folder)) 248 | in 249 | let chapters_html = List.map chapters ~f:(chapter_to_html parts_mod) in 250 | List.iter chapters_html ~f:html_to_disk 251 | 252 | let parts_to_html parts = 253 | List.iter parts ~f:(fun { chapters; _ } -> chapters_to_html parts chapters) 254 | 255 | let index_to_html parts = 256 | let index_html = parts_to_index parts in 257 | html_to_disk ("index", index_html) 258 | 259 | (* helpers *) 260 | 261 | let print_explanation = function 262 | | Unparsed { line; text } -> printf " + %d: %s\n" line text 263 | | Parsed { code; explanation } -> 264 | let code = String.concat ~sep:"\n" code in 265 | printf " + code: %s\n + %s\n" code explanation 266 | 267 | let print_section { file; lang; explanations } = 268 | printf " - %s (%s)\n" file lang; 269 | List.iter explanations ~f:print_explanation 270 | 271 | let print_chapter (idx : int) { title; sections; _ } = 272 | printf "%d - %s\n" (idx + 1) title; 273 | List.iter sections ~f:print_section 274 | 275 | let array_get array idx = 276 | let len = Array.length array in 277 | if idx > len - 1 then None else Some array.(idx) 278 | 279 | (* main *) 280 | 281 | let build _ = 282 | Core_unix.mkdir_p output_dir; 283 | let parts = parse_parts @@ get_parts @@ () in 284 | parts_to_html parts; 285 | index_to_html parts; 286 | printf "done generating HTML files in dist/\n" 287 | 288 | let main _ = 289 | let argv = Sys.get_argv () in 290 | let arg = array_get argv 1 in 291 | match arg with 292 | | None | Some "build" -> build () 293 | | Some "watch" -> 294 | print_endline "building once first"; 295 | build (); 296 | print_endline ("now watching " ^ book_dir); 297 | Watch.main [ book_dir; template_dir ] ~f:build 298 | | _ -> print_endline "usage: byexample " 299 | -------------------------------------------------------------------------------- /tools/lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name byexample) 3 | (libraries core core_unix core_unix.sys_unix yojson jingoo) 4 | (preprocess 5 | (pps ppx_deriving.show))) 6 | -------------------------------------------------------------------------------- /tools/lib/watch.ml: -------------------------------------------------------------------------------- 1 | open Core 2 | 3 | (* finds the highest modification_time from all the files in a folder (defaults to 0 if folders is the empty list) *) 4 | let rec highest_timestamp ?(highest = 0.) folders = 5 | match folders with 6 | | [] -> highest 7 | | file :: rest -> ( 8 | let is_dir = Sys_unix.is_directory file in 9 | match is_dir with 10 | | `Yes -> 11 | let inside_dirs = Sys_unix.ls_dir file in 12 | let inside_dirs = List.map inside_dirs ~f:(fun x -> file ^ "/" ^ x) in 13 | highest_timestamp ~highest (inside_dirs @ rest) 14 | | _ -> 15 | let stat = Core_unix.stat file in 16 | let modif_time = stat.st_mtime in 17 | let highest = 18 | if Float.(modif_time > highest) then modif_time else highest 19 | in 20 | highest_timestamp ~highest rest) 21 | 22 | (* returns a higher timestamp than [last_change] if a more recent change is detected, otherwise returns [None] *) 23 | let rec has_changed (last_change : float) (folders : string list) : float option 24 | = 25 | match folders with 26 | | [] -> None 27 | | file :: rest -> ( 28 | let is_dir = Sys_unix.is_directory file in 29 | match is_dir with 30 | | `Yes -> 31 | let inside_dirs = Sys_unix.ls_dir file in 32 | let inside_dirs = List.map inside_dirs ~f:(fun x -> file ^ "/" ^ x) in 33 | has_changed last_change (inside_dirs @ rest) 34 | | _ -> 35 | let stat = Core_unix.stat file in 36 | let modif_time = stat.st_mtime in 37 | if Float.(modif_time > last_change) then Some modif_time 38 | else has_changed last_change rest) 39 | 40 | (* watches a folder and apply [f] if any change is detected *) 41 | let rec watch last_timestamp path ~f = 42 | Core_unix.sleep 1; 43 | match has_changed last_timestamp path with 44 | | None -> watch last_timestamp path ~f 45 | | Some new_timestamp -> 46 | printf "some changes were observed (timestamp %f). Rebuilding...\n%!" 47 | new_timestamp; 48 | (try f () 49 | with some_exception -> 50 | printf "there's an error in your files: %s\n%!" 51 | (Exn.to_string some_exception)); 52 | watch new_timestamp path ~f 53 | 54 | (* watches a folder and apply [f] if any change is detected *) 55 | let main path ~f = 56 | let last_timestamp = highest_timestamp path in 57 | watch last_timestamp path ~f 58 | --------------------------------------------------------------------------------