From aa3d09c3fff12e379fd189ceaf55644574ff5c43 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Sun, 30 Jun 2019 01:00:01 +0700 Subject: Initial import of libraries from Vyconf and old libvyosconfig. --- .gitignore | 4 + LICENSE | 505 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 + dune-project | 3 + src/config_tree.ml | 214 +++++++++++++++++++++ src/config_tree.mli | 38 ++++ src/dune | 10 + src/util.ml | 4 + src/util.mli | 1 + src/vylist.ml | 46 +++++ src/vylist.mli | 7 + src/vyos1x_lexer.mll | 138 ++++++++++++++ src/vyos1x_parser.mly | 115 ++++++++++++ src/vytree.ml | 192 +++++++++++++++++++ src/vytree.mli | 50 +++++ vyos1x-config.opam | 25 +++ 16 files changed, 1356 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dune-project create mode 100644 src/config_tree.ml create mode 100644 src/config_tree.mli create mode 100644 src/dune create mode 100644 src/util.ml create mode 100644 src/util.mli create mode 100644 src/vylist.ml create mode 100644 src/vylist.mli create mode 100644 src/vyos1x_lexer.mll create mode 100644 src/vyos1x_parser.mly create mode 100644 src/vytree.ml create mode 100644 src/vytree.mli create mode 100644 vyos1x-config.opam diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf22eed --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +_build/* + +*.merlin +*.install diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..40c8ae6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,505 @@ +GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +(This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.) + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + {signature of Ty Coon}, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3de28f --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +vyos1-config +============ + +A library for parsing, manipulating, and exporting VyOS 1.x and EdgeOS config files. diff --git a/dune-project b/dune-project new file mode 100644 index 0000000..26da116 --- /dev/null +++ b/dune-project @@ -0,0 +1,3 @@ +(lang dune 1.9) +(using menhir 2.0) +(name vyos1x-config) diff --git a/src/config_tree.ml b/src/config_tree.ml new file mode 100644 index 0000000..463eb4b --- /dev/null +++ b/src/config_tree.ml @@ -0,0 +1,214 @@ +type value_behaviour = AddValue | ReplaceValue + +exception Duplicate_value +exception Node_has_no_value +exception No_such_value +exception Useless_set + +type config_node_data = { + values: string list; + comment: string option; + tag: bool; +} [@@deriving yojson] + +type t = config_node_data Vytree.t [@@deriving yojson] + +let default_data = { + values = []; + comment = None; + tag = false; +} + +let make name = Vytree.make default_data name + +let replace_value node path value = + let data = {default_data with values=[value]} in + Vytree.update node path data + +let add_value node path value = + let node' = Vytree.get node path in + let data = Vytree.data_of_node node' in + let values = data.values in + match (Vylist.find (fun x -> x = value) values) with + | Some _ -> raise Duplicate_value + | None -> + let values = values @ [value] in + Vytree.update node path ({data with values=values}) + +let delete_value node path value = + let data = Vytree.data_of_node @@ Vytree.get node path in + let values = Vylist.remove (fun x -> x = value) data.values in + Vytree.update node path {data with values=values} + +let set_value node path value behaviour = + match behaviour with + | AddValue -> add_value node path value + | ReplaceValue -> replace_value node path value + +let set node path value behaviour = + if (Vytree.exists node path) then + (match value with + | None -> raise Useless_set + | Some v -> set_value node path v behaviour) + else + let path_existing = Vytree.get_existent_path node path in + let path_remaining = Vylist.complement path path_existing in + let values = match value with None -> [] | Some v -> [v] in + Vytree.insert_multi_level default_data node path_existing path_remaining {default_data with values=values} + +let get_values node path = + let node' = Vytree.get node path in + let data = Vytree.data_of_node node' in + data.values + +let get_value node path = + let values = get_values node path in + match values with + | [] -> raise Node_has_no_value + | x :: _ -> x + +let delete node path value = + match value with + | Some v -> + (let values = get_values node path in + if Vylist.in_list values v then + (match values with + | [_] -> Vytree.delete node path + | _ -> delete_value node path v) + else raise No_such_value) + | None -> + Vytree.delete node path + +let set_comment node path comment = + let data = Vytree.get_data node path in + Vytree.update node path {data with comment=comment} + +let get_comment node path = + let data = Vytree.get_data node path in + data.comment + +let set_tag node path tag = + let data = Vytree.get_data node path in + Vytree.update node path {data with tag=tag} + +let is_tag node path = + let data = Vytree.get_data node path in + data.tag + +module Renderer = +struct + (* Rendering configs as set commands *) + let render_set_path path value = + let v = Printf.sprintf "\'%s\'" value in + List.append path [v] |> String.concat " " |> Printf.sprintf "set %s" + + let rec render_commands path ct = + let new_path = List.append path [Vytree.name_of_node ct] in + let new_path_str = String.concat " " new_path in + let data = Vytree.data_of_node ct in + (* Get the node comment, if any *) + let comment = Util.default "" data.comment in + let comment_cmd = (if comment = "" then "" else Printf.sprintf "comment %s \'%s\'" new_path_str comment) in + let child_names = Vytree.list_children ct in + (* Now handle the different cases for nodes with and without children *) + match child_names with + | [] -> + (* This is a leaf node *) + let values = List.map String.escaped data.values in + let cmds = + begin + match values with + | [] -> + (* Valueless leaf node *) + String.concat " " new_path |> Printf.sprintf "set %s" + | [v] -> + (* Single value, just one command *) + render_set_path new_path v + | vs -> + (* A leaf node with multiple values *) + List.map (render_set_path new_path) vs |> String.concat "\n" + end + in + if comment_cmd = "" then cmds else Printf.sprintf "%s\n%s" cmds comment_cmd + | _ :: _ -> + (* A node with children *) + let children = List.map (fun n -> Vytree.get ct [n]) child_names in + let rendered_children = List.map (render_commands new_path) children in + let cmds = String.concat "\n" rendered_children in + if comment_cmd = "" then cmds else Printf.sprintf "%s\n%s" cmds comment_cmd + + (* Rendering config as a VyOS/EdgeOS config file *) + let make_indent indent level = String.make (level * indent) ' ' + + let render_values indent_str name values = + match values with + | [] -> Printf.sprintf "%s%s { }\n" indent_str name + | [v] -> Printf.sprintf "%s%s \"%s\"\n" indent_str name (String.escaped v) + | _ -> + let rendered = List.map (fun s -> Printf.sprintf "%s%s \"%s\"" indent_str name (String.escaped s)) values in + let rendered = String.concat "\n" rendered in + Printf.sprintf "%s\n" rendered + + let render_comment indent c = + match c with + | None -> "" + | Some c -> Printf.sprintf "%s/* %s */\n" indent c + + let rec render_node indent level node = + let indent_str = make_indent indent level in + let name = Vytree.name_of_node node in + let data = Vytree.data_of_node node in + let is_tag = data.tag in + let comment = render_comment indent_str data.comment in + let values = render_values indent_str name data.values in + let children = Vytree.children_of_node node in + match children with + | [] -> Printf.sprintf "%s%s" comment values + | _ :: _ -> + if is_tag then + begin + let inner = List.map (render_tag_node_child indent level name) children in + String.concat "" inner + end + else + begin + let inner = List.map (render_node indent (level + 1)) children in + let inner = String.concat "" inner in + Printf.sprintf "%s%s%s {\n%s%s}\n" comment indent_str name inner indent_str + end + and render_tag_node_child indent level parent node = + let indent_str = make_indent indent level in + let name = Vytree.name_of_node node in + let data = Vytree.data_of_node node in + let comment = render_comment indent_str data.comment in + let values = render_values indent_str name data.values in + let children = Vytree.children_of_node node in + match children with + (* This produces too much whitespace due to indent_str from values, + but the issue is cosmetic *) + | [] -> Printf.sprintf "%s%s%s %s" comment indent_str parent values + | _ -> + (* Exploiting the fact that immediate children of tag nodes are + never themselves tag nodes *) + let inner = List.map (render_node indent (level + 1)) children in + let inner = String.concat "" inner in + Printf.sprintf "%s%s%s %s {\n%s%s}\n" comment indent_str parent name inner indent_str + + let render_config node = + let children = Vytree.children_of_node node in + let child_configs = List.map (render_node 4 0) children in + String.concat "" child_configs + +end (* Renderer *) + +let render_commands node path = + let node = + match path with + | [] -> node + | _ -> Vytree.get node path + in + let children = Vytree.children_of_node node in + let commands = List.map (Renderer.render_commands path) children in + String.concat "\n" commands + +let render_config = Renderer.render_config diff --git a/src/config_tree.mli b/src/config_tree.mli new file mode 100644 index 0000000..c01e299 --- /dev/null +++ b/src/config_tree.mli @@ -0,0 +1,38 @@ +type value_behaviour = AddValue | ReplaceValue + +exception Duplicate_value +exception Node_has_no_value +exception No_such_value +exception Useless_set + +type config_node_data = { + values : string list; + comment : string option; + tag : bool; +} [@@deriving yojson] + +type t = config_node_data Vytree.t [@@deriving yojson] + +val default_data : config_node_data + +val make : string -> t + +val set : t -> string list -> string option -> value_behaviour -> t + +val delete : t -> string list -> string option -> t + +val get_values : t -> string list -> string list + +val get_value : t -> string list -> string + +val set_comment : t -> string list -> string option -> t + +val get_comment : t -> string list -> string option + +val set_tag : t -> string list -> bool -> t + +val is_tag : t -> string list -> bool + +val render_commands : t -> string list -> string + +val render_config : t -> string diff --git a/src/dune b/src/dune new file mode 100644 index 0000000..7b39748 --- /dev/null +++ b/src/dune @@ -0,0 +1,10 @@ +(ocamllex vyos1x_lexer) +(menhir + (flags --table) + (modules vyos1x_parser)) + +(library + (name vyos1x) + (public_name vyos1x-config) + (libraries yojson menhirLib) + (preprocess (pps ppx_deriving_yojson))) diff --git a/src/util.ml b/src/util.ml new file mode 100644 index 0000000..c2ec4c4 --- /dev/null +++ b/src/util.ml @@ -0,0 +1,4 @@ +let default default_value opt = + match opt with + | None -> default_value + | Some value -> value diff --git a/src/util.mli b/src/util.mli new file mode 100644 index 0000000..5316e90 --- /dev/null +++ b/src/util.mli @@ -0,0 +1 @@ +val default : 'a -> 'a option -> 'a diff --git a/src/vylist.ml b/src/vylist.ml new file mode 100644 index 0000000..cd4a32e --- /dev/null +++ b/src/vylist.ml @@ -0,0 +1,46 @@ +let rec find p xs = + match xs with + | [] -> None + | y :: ys -> if (p y) then (Some y) + else find p ys + +let rec remove p xs = + match xs with + | [] -> [] + | y :: ys -> if (p y) then ys + else y :: (remove p ys) + +let rec replace p x xs = + match xs with + | [] -> raise Not_found + | y :: ys -> if (p y) then x :: ys + else y :: (replace p x ys) + +let rec insert_before p x xs = + match xs with + | [] -> raise Not_found + | y :: ys -> if (p y) then x :: y :: ys + else y :: (insert_before p x ys) + +let rec insert_after p x xs = + match xs with + | [] -> raise Not_found + | y :: ys -> if (p y) then y :: x :: ys + else y :: (insert_after p x ys) + +let complement xs ys = + let rec aux xs ys = + match xs, ys with + | [], _ -> ys + | _, [] -> assert false (* Can't happen *) + | p :: ps, q :: qs -> if p = q then aux ps qs + else [] + in + if List.length xs < List.length ys then aux xs ys + else aux ys xs + +let in_list xs x = + let x' = find ((=) x) xs in + match x' with + | None -> false + | Some _ -> true diff --git a/src/vylist.mli b/src/vylist.mli new file mode 100644 index 0000000..9135bf6 --- /dev/null +++ b/src/vylist.mli @@ -0,0 +1,7 @@ +val find : ('a -> bool) -> 'a list -> 'a option +val remove : ('a -> bool) -> 'a list -> 'a list +val replace : ('a -> bool) -> 'a -> 'a list -> 'a list +val insert_before : ('a -> bool) -> 'a -> 'a list -> 'a list +val insert_after : ('a -> bool) -> 'a -> 'a list -> 'a list +val complement : 'a list -> 'a list -> 'a list +val in_list : 'a list -> 'a -> bool diff --git a/src/vyos1x_lexer.mll b/src/vyos1x_lexer.mll new file mode 100644 index 0000000..a996642 --- /dev/null +++ b/src/vyos1x_lexer.mll @@ -0,0 +1,138 @@ +{ + +open Vyos1x_parser + +exception Error of string + +(* + +The language of the VyOS 1.x config file has multiple ambiguities that +make it not even context free. + +The main issue is lack of explicit statement separators, so if we ignore whitespace, +a parser is left to guess if, for example + +address dhcp # leaf node with a value +disable # valueless leaf node + +is three valueless nodes, a valueless node followed by a node with a value, +or a node with a value followed by a valueless node. + +The only cue is the newline, which means that newlines are sometimes significant, +and sometimes they aren't. + +interfaces { # doesn't matter + ethernet 'eth0' { # doesn't matter + address '192.0.2.1/24' # significant! + disable # significant! + + # empty line -- doesn't matter + hw-id 00:aa:bb:cc:dd:ee # significant! + } # doesn't matter +} + +If there were explicit terminators (like we do in VyConf, or like JunOS does), +the language would be context free. Enter the lexer hack: let's emit newlines only +when they are significant, so that the parser can use them as terminators. + +The informal idea is that a newline is only significant if it follows a leaf node. +So we need rules for finding out if we are inside a leaf node or not. + +These are the formal rules. A newline is significant if and only if +1. Preceding token is an identifier +2. Preceding token is a quoted string + +We set the vy_inside_node flag to true when we enter a leaf node and reset it when +we reach the end of it. + +*) + +let vy_inside_node = ref false + +} + +rule token = parse +| [' ' '\t' '\r'] + { token lexbuf } +| '\n' + { Lexing.new_line lexbuf; if !vy_inside_node then (vy_inside_node := false; NEWLINE) else token lexbuf } +| '"' + { vy_inside_node := true; read_string (Buffer.create 16) lexbuf } +| ''' + { vy_inside_node := true; read_single_quoted_string (Buffer.create 16) lexbuf } +| "/*" + { vy_inside_node := false; read_comment (Buffer.create 16) lexbuf } +| '{' + { vy_inside_node := false; LEFT_BRACE } +| '}' + { vy_inside_node := false; RIGHT_BRACE } +| "//" [^ '\n']* + { token lexbuf } +| [^ ' ' '\t' '\n' '\r' '{' '}' '"' ''' ]+ as s + { vy_inside_node := true; IDENTIFIER s} +| eof + { EOF } +| _ +{ raise (Error (Printf.sprintf "At offset %d: unexpected character.\n" (Lexing.lexeme_start lexbuf))) } + +and read_string buf = + parse + | '"' { STRING (Buffer.contents buf) } + | '\\' '/' { Buffer.add_char buf '/'; read_string buf lexbuf } + | '\\' '\\' { Buffer.add_char buf '\\'; read_string buf lexbuf } + | '\\' 'b' { Buffer.add_char buf '\b'; read_string buf lexbuf } + | '\\' 'f' { Buffer.add_char buf '\012'; read_string buf lexbuf } + | '\\' 'n' { Buffer.add_char buf '\n'; read_string buf lexbuf } + | '\\' 'r' { Buffer.add_char buf '\r'; read_string buf lexbuf } + | '\\' 't' { Buffer.add_char buf '\t'; read_string buf lexbuf } + | '\\' '\'' { Buffer.add_char buf '\''; read_string buf lexbuf } + | '\\' '"' { Buffer.add_char buf '"'; read_string buf lexbuf } + | '\n' { Lexing.new_line lexbuf; Buffer.add_char buf '\n'; read_string buf lexbuf } + | [^ '"' '\\']+ + { Buffer.add_string buf (Lexing.lexeme lexbuf); + read_string buf lexbuf + } + | _ { raise (Error (Printf.sprintf "Illegal string character: %s" (Lexing.lexeme lexbuf))) } + | eof { raise (Error ("String is not terminated")) } + +and read_single_quoted_string buf = + parse + | ''' { STRING (Buffer.contents buf) } + | '\\' '/' { Buffer.add_char buf '/'; read_string buf lexbuf } + | '\\' '\\' { Buffer.add_char buf '\\'; read_string buf lexbuf } + | '\\' 'b' { Buffer.add_char buf '\b'; read_string buf lexbuf } + | '\\' 'f' { Buffer.add_char buf '\012'; read_string buf lexbuf } + | '\\' 'n' { Buffer.add_char buf '\n'; read_string buf lexbuf } + | '\\' 'r' { Buffer.add_char buf '\r'; read_string buf lexbuf } + | '\\' 't' { Buffer.add_char buf '\t'; read_string buf lexbuf } + | '\\' '\'' { Buffer.add_char buf '\''; read_string buf lexbuf } + | '\\' '"' { Buffer.add_char buf '"'; read_string buf lexbuf } + | '\n' { Lexing.new_line lexbuf; Buffer.add_char buf '\n'; read_string buf lexbuf } + | [^ ''' '\\']+ + { Buffer.add_string buf (Lexing.lexeme lexbuf); + read_single_quoted_string buf lexbuf + } + | _ { raise (Error (Printf.sprintf "Illegal string character: %s" (Lexing.lexeme lexbuf))) } + | eof { raise (Error ("String is not terminated")) } + +and read_comment buf = + parse + | "*/" + { COMMENT (Buffer.contents buf) } + | _ + { Buffer.add_string buf (Lexing.lexeme lexbuf); + read_comment buf lexbuf + } + +(* + +If you are curious how the original parsers handled the issue: they did not. +The CStore parser cheated by reading data from command definitions to resolve +the ambiguities, which made it impossible to use in standalone config +manipulation programs like migration scripts. + +The XorpConfigParser could not tell tag nodes' name and tag from +a leaf node with a value, which made it impossible to manipulate +tag nodes or change values properly. + +*) diff --git a/src/vyos1x_parser.mly b/src/vyos1x_parser.mly new file mode 100644 index 0000000..c31552f --- /dev/null +++ b/src/vyos1x_parser.mly @@ -0,0 +1,115 @@ +%{ + open Config_tree + + exception Duplicate_child of (string * string) + + (* Used for checking if after merging immediate children, + any of them have duplicate children inside, + e.g. "interfaces { ethernet eth0 {...} ethernet eth0 {...} }" *) + let find_duplicate_children n = + let rec aux xs = + let xs = List.sort compare xs in + match xs with + | [] | [_] -> () + | x :: x' :: xs -> + if x = x' then raise (Duplicate_child (Vytree.name_of_node n, x)) + else aux (x' :: xs) + in + aux @@ Vytree.list_children n + + (* When merging nodes with values, append values of subsequent nodes to the + first one *) + let merge_data l r = {l with values=(List.append l.values r.values)} +%} + +%token IDENTIFIER +%token STRING +%token COMMENT +%token LEFT_BRACE +%token RIGHT_BRACE +%token NEWLINE +%token EOF + +%start config +%% + +(* If there are multiple comments before a node, consider the last one its real comment *) +comments: + cs = list(COMMENT) { match cs with [] -> None | _ -> Some (List.rev cs |> List.hd |> String.trim) } + +value: + | v = STRING + { v } + | v = IDENTIFIER + { v } +; + + +leaf_node_body: + | comment = comments; + name = IDENTIFIER; value = value; + { Vytree.make_full {default_data with values=[value]; comment=comment} name []} + | comment = comments; + name = IDENTIFIER; (* valueless node *) + { Vytree.make_full {default_data with comment=comment} name [] } +; + +leaf_node: + | n = leaf_node_body; NEWLINE; + { n } + | n = leaf_node_body; EOF; + { n } + +node: + | comment = comments; + name = IDENTIFIER; LEFT_BRACE; children = list(node_content); RIGHT_BRACE; + { + let node = + Vytree.make_full {default_data with comment=comment} name [] in + let node = List.fold_left Vytree.adopt node (List.rev children) |> Vytree.merge_children merge_data in + try + List.iter find_duplicate_children (Vytree.children_of_node node); + node + with + | Duplicate_child (child, dup) -> + failwith (Printf.sprintf "Node \"%s %s\" has two children named \"%s\"" name child dup) + } +; + +(* XXX: for the config to be loadable with the old CStore backend, what was formatted as a tag node + in the original config, must remain formatted that way. + This is why the tag field is set. + *) +tag_node: + | comment = comments; + name = IDENTIFIER; tag = value; LEFT_BRACE; children = list(node_content); RIGHT_BRACE + { + let outer_node = Vytree.make_full {default_data with tag=true} name [] in + let inner_node = + Vytree.make_full {default_data with comment=comment} tag [] in + let inner_node = List.fold_left Vytree.adopt inner_node (List.rev children) |> Vytree.merge_children merge_data in + let node = Vytree.adopt outer_node inner_node in + try + List.iter find_duplicate_children (Vytree.children_of_node inner_node); + node + with + | Duplicate_child (child, dup) -> + failwith (Printf.sprintf "Node \"%s %s %s\" has two children named \"%s\"" name tag child dup) + } + +node_content: n = node { n } | n = leaf_node { n } | n = tag_node { n }; + + +%public config: + | ns = list(node_content); EOF + { + let root = make "root" in + let root = List.fold_left Vytree.adopt root (List.rev ns) |> Vytree.merge_children merge_data in + try + List.iter find_duplicate_children (Vytree.children_of_node root); + root + with + | Duplicate_child (child, dup) -> + failwith (Printf.sprintf "Node \"%s\" has two children named \"%s\"" child dup) + } +; diff --git a/src/vytree.ml b/src/vytree.ml new file mode 100644 index 0000000..a3e2750 --- /dev/null +++ b/src/vytree.ml @@ -0,0 +1,192 @@ +type 'a t = { + name: string; + data: 'a; + children: 'a t list +} [@@deriving yojson] + +type position = Before of string | After of string | End | Default + +exception Empty_path +exception Duplicate_child +exception Nonexistent_path +exception Insert_error of string + +let make data name = { name = name; data = data; children = [] } + +let make_full data name children = { name = name; data = data; children = children } + +let name_of_node node = node.name +let data_of_node node = node.data +let children_of_node node = node.children + +let insert_immediate ?(position=Default) node name data children = + let new_node = make_full data name children in + let children' = + match position with + | Default -> new_node :: node.children + | End -> node.children @ [new_node] + | Before s -> Vylist.insert_before (fun x -> x.name = s) new_node node.children + | After s -> Vylist.insert_after (fun x -> x.name = s) new_node node.children + in { node with children = children' } + +let delete_immediate node name = + let children' = Vylist.remove (fun x -> x.name = name) node.children in + { node with children = children' } + +let adopt node child = + { node with children = child :: node.children } + +let replace node child = + let children = node.children in + let name = child.name in + let children' = Vylist.replace (fun x -> x.name = name) child children in + { node with children = children' } + +let replace_full node child name = + let children = node.children in + let children' = Vylist.replace (fun x -> x.name = name) child children in + { node with children = children' } + +let find node name = + Vylist.find (fun x -> x.name = name) node.children + +let find_or_fail node name = + let child = find node name in + match child with + | None -> raise Nonexistent_path + | Some child' -> child' + +let list_children node = + List.map (fun x -> x.name) node.children + +let rec do_with_child fn node path = + match path with + | [] -> raise Empty_path + | [name] -> fn node name + | name :: names -> + let next_child = find_or_fail node name in + let new_node = do_with_child fn next_child names in + replace node new_node + +let rec insert ?(position=Default) ?(children=[]) node path data = + match path with + | [] -> raise Empty_path + | [name] -> + (let last_child = find node name in + match last_child with + | None -> insert_immediate ~position:position node name data children + | (Some _) -> raise Duplicate_child) + | name :: names -> + let next_child = find node name in + match next_child with + | Some next_child' -> + let new_node = insert ~position:position ~children:children next_child' names data in + replace node new_node + | None -> + raise (Insert_error "Path does not exist") + +(** Given a node N check if it has children with duplicate names, + and merge subsequent children's children into the first child by + that name. + + While all insert functions maintain the "every child has unique name" + invariant, for nodes constructed manually with make/make_full and adopt + it may not hold, and constructing nodes this way is a sensible approach + for config parsing. Depending on the config format, duplicate node names + may be normal and even expected, such as "ethernet eth0" and "ethernet eth1" + in the "curly" format. + *) +let merge_children merge_data node = + (* Given a node N and a list of nodes NS, find all nodes in NS that + have the same name as N and merge their children into N *) + let rec merge_into n ns = + match ns with + | [] -> n + | n' :: ns' -> + if n.name = n'.name then + let children = List.append n.children n'.children in + let data = merge_data n.data n'.data in + let n = {n with children=children; data=data} in + merge_into n ns' + else merge_into n ns' + in + (* Given a list of nodes, for every node, find subsequent children with + the same name and merge them into the first node, then delete remaining + nodes from the list *) + let rec aux ns = + match ns with + | [] -> [] + | n :: ns -> + let n = merge_into n ns in + let ns = List.filter (fun x -> x.name <> n.name) ns in + n :: (aux ns) + in {node with children=(aux node.children)} + +(* When inserting at a path that, entirely or partially, + does not exist yet, create missing nodes on the way with default data *) +let rec insert_multi_level default_data node path_done path_remaining data = + match path_remaining with + | [] | [_] -> insert node (path_done @ path_remaining) data + | name :: names -> + let path_done = path_done @ [name] in + let node = insert node path_done default_data in + insert_multi_level default_data node path_done names data + +let delete node path = + do_with_child delete_immediate node path + +let rename node path newname = + let rename_immediate newname' node' name' = + let child = find_or_fail node' name' in + let child = { child with name=newname' } in + replace_full node' child name' + in do_with_child (rename_immediate newname) node path + +let update node path data = + let update_data data' node' name = + let child = find_or_fail node' name in + let child = { child with data=data' } in + replace node' child + in do_with_child (update_data data) node path + +let rec get node path = + match path with + | [] -> raise Empty_path + | [name] -> find_or_fail node name + | name :: names -> get (find_or_fail node name) names + +let get_data node path = data_of_node @@ get node path + +let exists node path = + try ignore (get node path); true + with Nonexistent_path -> false + +let get_existent_path node path = + let rec aux node path acc = + match path with + | [] -> acc + | name :: names -> + let child = find node name in + match child with + | None -> acc + | Some c -> aux c names (name :: acc) + in List.rev (aux node path []) + +let children_of_path node path = + let node' = get node path in + list_children node' + +let sorted_children_of_node cmp node = + let names = list_children node in + let names = List.sort cmp names in + List.map (find_or_fail node) names + +let copy node old_path new_path = + if exists node new_path then raise Duplicate_child else + let child = get node old_path in + insert ~position:End ~children:child.children node new_path child.data + +let move node path position = + let child = get node path in + let node = delete node path in + insert ~position:position ~children:child.children node path child.data diff --git a/src/vytree.mli b/src/vytree.mli new file mode 100644 index 0000000..451e130 --- /dev/null +++ b/src/vytree.mli @@ -0,0 +1,50 @@ +type 'a t [@@deriving yojson] + +exception Empty_path +exception Duplicate_child +exception Nonexistent_path +exception Insert_error of string + +type position = Before of string | After of string | End | Default + +val make : 'a -> string -> 'a t +val make_full : 'a -> string -> ('a t) list -> 'a t + +val name_of_node : 'a t -> string +val data_of_node : 'a t -> 'a +val children_of_node : 'a t -> 'a t list + +val find : 'a t -> string -> 'a t option +val find_or_fail : 'a t -> string -> 'a t + +val adopt : 'a t -> 'a t -> 'a t + +val insert : ?position:position -> ?children:('a t list) -> 'a t -> string list -> 'a -> 'a t + +val insert_multi_level : 'a -> 'a t -> string list -> string list -> 'a -> 'a t + +val merge_children : ('a -> 'a -> 'a) -> 'a t -> 'a t + +val delete : 'a t -> string list -> 'a t + +val update : 'a t -> string list -> 'a -> 'a t + +val rename : 'a t -> string list -> string -> 'a t + +val list_children : 'a t -> string list + +val get : 'a t -> string list -> 'a t + +val get_existent_path : 'a t -> string list -> string list + +val get_data : 'a t -> string list -> 'a + +val exists : 'a t -> string list -> bool + +val children_of_path : 'a t -> string list -> string list + +val sorted_children_of_node : (string -> string -> int) -> 'a t -> ('a t) list + +val copy : 'a t -> string list -> string list -> 'a t + +val move : 'a t -> string list -> position -> 'a t diff --git a/vyos1x-config.opam b/vyos1x-config.opam new file mode 100644 index 0000000..0d67b74 --- /dev/null +++ b/vyos1x-config.opam @@ -0,0 +1,25 @@ +opam-version: "2.0" +name: "vyos1x-config" +version: "0.1" +synopsis: "VyOS 1.x and EdgeOS config file manipulation library" +description: """ +A library for parsing, manipulating, and exporting VyOS 1.x and EdgeOS config files. +""" +maintainer: "Daniil Baturin " +authors: "VyOS maintainers and contributors " +license: "MIT" +homepage: "https://github.com/vyos/vyos1x-config" +bug-reports: "https://phabricator.vyos.net" +dev-repo: "git+https://github.com/vyos/vyos1x-config/" +build: [ + ["dune" "subst"] {pinned} + ["dune" "build" "-p" name] +] +depends: [ + "ocamlfind" {build} + "menhir" {build} + "dune" {build & >= "1.4.0"} + "ppx_deriving_yojson" + "batteries" + "yojson" +] -- cgit v1.2.3