Skip to content

Commit e67bb14

Browse files
developer manual (#3596)
Hacking manual Signed-off-by: Jeremie Dimino <[email protected]> Signed-off-by: Rudi Grinberg <[email protected]> Co-authored-by: Rudi Grinberg <[email protected]>
1 parent 4c0e6b8 commit e67bb14

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed

doc/hacking.rst

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
****************************
2+
Working on the Dune codebase
3+
****************************
4+
5+
This section gives guidelines for working on Dune itself. Many of these are
6+
general guidelines that are specific to Dune. However, given that Dune is a
7+
large project developed by many different people, it is important to follow
8+
these guidelines when working on Dune in order to keep the project in a good
9+
state and pleasant to work on for everybody.
10+
11+
Writing tests
12+
=============
13+
14+
Most of our tests are written as expectation style tests. While writing such
15+
tests, the developer write some code and then let the system insert the output
16+
produced during the execution of this code right next to the code in the source
17+
file.
18+
19+
Once a test is written and committed, the system will check that the captured
20+
output is still the one produced by a fresh execution of the code. When the two
21+
don't match, the test is considered as failing and the system displays a diff
22+
between what was expected and what the code produced.
23+
24+
Both our unit tests and integration tests are written this way. For unit tests,
25+
we use the ppx_expect_ framework where tests are introduced via
26+
``let%expect_test`` and expectation are capture in ``[%expect ...]`` nodes:
27+
28+
.. code:: ocaml
29+
30+
let%expect_test "<test name>" =
31+
print_string "Hello, world!";
32+
[%expect {|
33+
Hello, world!
34+
|}]
35+
36+
For integration tests, we use a system similar to `cram tests
37+
<https://bitheap.org/cram/>`_ for testing shell commands and their behavior:
38+
39+
.. code:: bash
40+
41+
$ echo 'Hello, world!'
42+
Hello, world!
43+
44+
$ false
45+
[1]
46+
47+
$ cat <<EOF
48+
> multi
49+
> line
50+
> EOF
51+
multi
52+
line
53+
54+
.. _ppx_expect: https://github.com/janestreet/ppx_expect
55+
56+
Guidelines
57+
----------
58+
59+
As with any long running software project, code written by one person will
60+
always eventually end up being maintained by another. Just like normal code, it
61+
is important to document tests. Especially since test suites are most often
62+
composed of many individual tests that must be understood on their own.
63+
64+
A well written test case should be easy to understand. A reader should be able
65+
to quickly understand what property the test is checking, how it is doing it and
66+
how to convince one-self that the test outcome is the right one. A well written
67+
test will make it easy for future maintainers to understand the test and react
68+
when the test breaks. Most often, the code will need to be adapted to preserve
69+
the existing behavior, however in some rare cases the test expectation will need
70+
to be updated.
71+
72+
It is crucial that each test cases makes it purpose and logic crystal clear so
73+
that future maintainers know how to deal with it.
74+
75+
When writing a test, we generally have a good idea of what we want to test.
76+
Sometimes, we want to test that a new feature we developed is behaving as we
77+
expect. Sometimes, we want to add a reproduction case for a bug reported by a
78+
user to make sure future changes won't re-introduce the faulty behaviour. Just
79+
like when programming, we turn such an idea into code, which is a formal
80+
language that a computer can understand. While another person reading this code
81+
might be able to follow and understand what the code is doing step by step, it
82+
is not clear that they will be able to reconstruct the original idea the
83+
developer had in their mind when they originally wrote the code. What is worse,
84+
they might understand the code in a completely different way which would lead
85+
them to update it the wrong way.
86+
87+
Adding Stanzas
88+
==============
89+
90+
Adding new stanzas is the most natural way to extend dune with new features.
91+
Therefore we try to make this as easy as possible. The minimal amount of steps
92+
to add a new stanza is:
93+
94+
- Extend ``Stanza.t`` with a new constructor to represent the new stanza
95+
- Modify ``Dune_file`` to parse the dune language into this constructor
96+
- Modify the rules to interpret this stanza into rules. This is usually done in
97+
``Gen_rules```
98+
99+
Versioning
100+
----------
101+
102+
Dune is incredibly strict with versioning of new features, modifications that
103+
are visible to the user, and changes to existing rules. This means that any
104+
added stanza must be guarded behind the version of the dune language in which it
105+
was introduced. For example:
106+
107+
.. code:: ocaml
108+
109+
; ( "cram"
110+
, let+ () = Dune_lang.Syntax.since Stanza.syntax (2, 7)
111+
and+ t = Cram_stanza.decode in
112+
[ Cram t ] )
113+
114+
Here the cram stanza was introduced in dune 2.7, so the user must enable ``(lang
115+
dune 2.7)`` in their dune-project file to use it.
116+
117+
``since`` isn't the only primitive for making sure that versions are respected.
118+
See ``Dune_lang.Syntax`` for other commonly used functions.
119+
120+
Experimental & Independent Extensions
121+
-------------------------------------
122+
123+
Sometimes, dune's versioning policy is too strict. For example, it does not work
124+
in the following situations:
125+
126+
- Mostly independent extensions of dune that only exist inside dune for
127+
development convenience. For example, build rules for coq. Such extensions
128+
would like to impose their own versioning policy.
129+
130+
- Experimental features that cannot yet guarantee dune's strict backwards
131+
compatibility. Such features may dropped or modified at any time.
132+
133+
To handle both of these use cases, dune allows to define new languages (with the
134+
same syntax). These languages have their own versioning scheme and their own
135+
stanzas (or fields). In dune itself, such languages are represented with
136+
``Syntax.t`` Here's an example of how the coq syntax is defined:
137+
138+
.. code:: ocaml
139+
140+
let coq_syntax =
141+
Dune_lang.Syntax.create ~name:"coq" ~desc:"the coq extension (experimental)"
142+
[ ((0, 1), `Since (1, 9)); ((0, 2), `Since (2, 5)) ]
143+
144+
The list provides which versions of the syntax are provided, and in which
145+
version of dune they were introduced.
146+
147+
Such languages must be enabled in the dune-project separately:
148+
149+
.. code:: scheme
150+
151+
(lang dune 2.8)
152+
(using coq 0.2)
153+
154+
If such extensions are experimental, it's recommended that they pass
155+
``~experimental:true``, and that their versions are below 1.0.
156+
157+
It's also recommended that such extensions introduce stanzas or fields of the
158+
form ``ext_name.stanza_name`` or ``ext_name.field_name`` to make it clear to the
159+
user which extensions is providing a certain feature.
160+
161+
Dune Rules
162+
==========
163+
164+
Creating Rules
165+
--------------
166+
167+
A dune rule consists of 3 components:
168+
169+
- Dependencies that the rule may read when executed (files, aliases, ..)
170+
This is described by ``'a Build.t`` values
171+
172+
- Targets the rule produces (files)
173+
Targets, in addition to dependencies is described by ``'a Build.With_targets.t'``
174+
175+
- Action that dune must execute (external programs, redirects, etc.)
176+
Actions are represented by ``Action.t``
177+
178+
Combined, one needs to produce a ``Action.t Build.With_targets.t`` value to
179+
create a rule. The rule may then be added by ``Super_context.add_rule``, or a
180+
related function.
181+
182+
To make this maximally convenient, there's a ``Command`` module to make it
183+
easier to create actions that run external commands and describe their targets &
184+
dependencies simultaneously.
185+
186+
Loading Rules
187+
-------------
188+
189+
Dune rules are loaded lazily to improve performance. Here's a sketch of the
190+
algorithm that tries to load the rule that generates some target file `t`.
191+
192+
- Get the directory that of `t`. Call it `d`.
193+
194+
- Load all rules in `d` into a map from targets in that directory to rules that
195+
produce it.
196+
197+
- Look up the rule for `t` in this map.
198+
199+
To adhere to this loading scheme, our rules must therefore be generated as part
200+
of the callback that generates targets in that directory. See the ``Gen_rules``
201+
module for how this callback is constructed.

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ Welcome to dune's documentation!
3333
caching
3434
toplevel-integration
3535
goals
36+
hacking

0 commit comments

Comments
 (0)