├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── _tags ├── manual.md ├── src ├── META ├── descr ├── generators.ml ├── generators.mli ├── modules.ml ├── modules.mli ├── namespaces.ml ├── namespaces.mli ├── opam ├── rules.ml ├── rules.mli └── summary.png └── test ├── .gitignore ├── Makefile ├── _tags ├── test_basic.ml ├── test_helpers.ml ├── test_helpers.mli ├── test_library.ml ├── test_main.ml └── test_oasis.ml /.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | scratch/ 3 | *.docdir 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - PPA=avsm/ocaml42+opam12 3 | 4 | before_script: 5 | - sudo add-apt-repository -y ppa:$PPA 6 | - sudo apt-get update 7 | - sudo apt-get install ocaml-nox opam 8 | - ocaml -version 9 | - opam init -y 10 | - eval `opam config env` 11 | 12 | script: 13 | - opam install -y ocamlfind 14 | - make build 15 | - opam install -y ounit process oasis 16 | - make test 17 | - make clean 18 | - opam remove -y ocamlfind ounit process oasis 19 | - ln -s src/opam 20 | - opam pin add -y . 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE := namespaces 2 | TARGETS := src/namespaces.cma src/namespaces.cmxa src/namespaces.cmi 3 | 4 | INSTALL := \ 5 | $(foreach target,$(TARGETS),_build/$(target)) src/namespaces.mli \ 6 | _build/src/namespaces.cmt _build/src/namespaces.cmti 7 | 8 | CFLAGS := -bin-annot 9 | OCAMLBUILD := ocamlbuild -use-ocamlfind -cflags $(CFLAGS) 10 | 11 | .PHONY : build 12 | build : 13 | $(OCAMLBUILD) src/namespaces.cma src/namespaces.cmxa 14 | 15 | .PHONY : install 16 | install : uninstall build 17 | ocamlfind install $(PACKAGE) src/META $(INSTALL) _build/src/namespaces.a 18 | 19 | .PHONY : uninstall 20 | uninstall : 21 | ocamlfind remove $(PACKAGE) 22 | 23 | .PHONY : test 24 | test : 25 | make -C test 26 | 27 | .PHONY : clean 28 | clean : 29 | ocamlbuild -clean 30 | make -C test clean 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *This project is **not supported** for the time being. Similar behaviour may be 2 | possible using [codept][codept] [v0.10][codept-0.10]'s `-nested` feature.* 3 | 4 | [codept]: https://github.com/octachron/codept 5 | [codept-0.10]: https://github.com/Octachron/codept/blob/master/Changelog.md#version-010 6 | 7 | # Namespaces   [![version 0.5.1][version]][releases] [![BSD license][license-img]][license] [![Travis status][travis-img]][travis] 8 | 9 | [version]: https://img.shields.io/badge/version-0.5.1-blue.svg 10 | [releases]: https://github.com/aantron/namespaces/releases 11 | [license-img]: https://img.shields.io/badge/license-BSD-blue.svg 12 | [travis]: https://travis-ci.org/aantron/namespaces/branches 13 | [travis-img]: https://img.shields.io/travis/aantron/namespaces/master.svg 14 | 15 | Ever wish `src/server/foo.ml` was module `Server.Foo` and not just `Foo`? 16 | 17 | Who likes to name it `src/server/server_foo.ml`, just to avoid naming conflicts? 18 | 19 | *Namespaces* is an Ocamlbuild plugin that turns directories in your source tree 20 | into scoped OCaml modules. You get a nice, logical, and predictable structure, 21 | and you don't have to worry about duplicate filenames anymore. It's the sane 22 | file naming convention that most other languages have always had. 23 | 24 | If you have the directory structure on the left, you will get the OCaml modules 25 | on the right: 26 | 27 | ![Effect summary][summary] 28 | 29 | [summary]: https://github.com/aantron/namespaces/blob/master/src/summary.png 30 | 31 | The module structure works as if you had written: 32 | 33 | ```ocaml 34 | module Server = 35 | struct 36 | module Foo = (* server/foo.ml *) 37 | module Bar = (* server/bar.ml *) 38 | end 39 | 40 | module Client = 41 | struct 42 | module Foo = (* client/foo.ml *) 43 | module Bar = (* client/bar.ml *) 44 | 45 | module Ui = 46 | struct 47 | module Reactive = (* client/ui/reactive.ml *) 48 | end 49 | 50 | include (* client/client.ml *) 51 | end 52 | ``` 53 | 54 | There is no conflict between `server/foo.ml` and `client/foo.ml`, because there 55 | is no globally-visible module `Foo`. 56 | 57 | Within a module, you don't have to (indeed, must not) qualify sibling module 58 | names: from `Server.Foo`, you access `Server.Bar` as just `Bar`. You can access 59 | children and nephews, but cannot access parents. 60 | 61 | The usual dependency restrictions between OCaml modules apply: there must not be 62 | any dependency cycles. Siblings, if they depend on each other, must depend on 63 | each other in some order, and each parent module depends on all of its child 64 | modules. 65 | 66 | The modules are composed with `-no-alias-deps`, so depending on a directory 67 | module does not automatically pull in all of its descendants, unless they are 68 | actually used. This also means that Namespaces requires OCaml 4.02 or higher. 69 | 70 | 71 | 72 |
73 | 74 | ## Instructions 75 | 76 | 1. Install Namespaces: 77 | 78 | opam install namespaces 79 | 80 | 2. Add it to your build system. 81 | 82 | - If using Ocamlbuild, create `myocamlbuild.ml`, and make it look like this: 83 | 84 | let () = Ocamlbuild_plugin.dispatch Namespaces.handler 85 | 86 | Invoke Ocamlbuild with: 87 | 88 | ocamlbuild -use-ocamlfind -plugin-tag "package(namespaces)" 89 | 90 | If you already have `myocamlbuild.ml`, call `Namespaces.handler` before or 91 | after your hook that you pass to `dispatch`. 92 | 93 | - If using OASIS, find the bottom of your `myocamlbuild.ml` file, and replace 94 | the call to `dispatch` with this: 95 | 96 | let () = 97 | dispatch 98 | (MyOCamlbuildBase.dispatch_combine 99 | [MyOCamlbuildBase.dispatch_default conf package_default; 100 | Namespaces.handler]) 101 | 102 | Then, add this to the top of your OASIS file: 103 | 104 | OCamlVersion: >= 4.02 105 | AlphaFeatures: ocamlbuild_more_args 106 | XOCamlbuildPluginTags: package(namespaces) 107 | 108 | and re-run `oasis setup`. 109 | 110 | 3. Tag the directories that you want to become namespaces with the `namespace` 111 | tag. For example, to make all directories in `src/` into namespaces, you 112 | can add this to your `_tags` file: 113 | 114 | : namespace 115 | : include 116 | 117 | 4. If you are building a library, see additional instructions in 118 | [`manual.md`][manual]. See also that file if you have generated files in your 119 | project. 120 | 121 | 5. Enjoy! 122 | 123 | [manual]: https://github.com/aantron/namespaces/blob/master/manual.md 124 | 125 | 126 | 127 |
128 | 129 | ## The Future 130 | 131 | An Ocamlbuild plugin is by no means the best way, ideally, to deal with the 132 | problem of structuring modules in OCaml. I am releasing this, in part, to 133 | advance the discussion. My hope is that after some time, we will have a more 134 | conclusive solution integrated into the OCaml tool chain. 135 | 136 | 137 | 138 |
139 | 140 | ## License 141 | 142 | Namespaces is distributed under the terms of the 2-clause 143 | [BSD license][license]. 144 | 145 | [license]: https://opensource.org/licenses/BSD-2-Clause 146 | -------------------------------------------------------------------------------- /_tags: -------------------------------------------------------------------------------- 1 | : safe_string, package(ocamlbuild) 2 | "test" or "scratch": -traverse 3 | -------------------------------------------------------------------------------- /manual.md: -------------------------------------------------------------------------------- 1 | # Namespaces 2 | 3 | - [Building libraries](#Libraries) 4 | - [With Ocamlbuild](#LibrariesOcamlbuild) 5 | - [With OASIS](#LibrariesOASIS) 6 | - [Generated files](#Generated) 7 | - [Effect on separate compilation](#SeparateCompilation) 8 | 9 | 10 | 11 |
12 | 13 | 14 | ## Building libraries 15 | 16 | These instructions are in addition to the main instructions in 17 | [README.md][readme-instructions]. Follow those first, then the ones in this 18 | file. 19 | 20 | [readme-instructions]: https://github.com/aantron/namespaces#instructions 21 | 22 | 23 | #### With Ocamlbuild 24 | 25 | To build one or more libraries with Namespaces and Ocamlbuild: 26 | 27 | 1. Tag your library directories with the additional tag `namespace_lib`, like 28 | this: 29 | 30 | "src/lib_1" or "src/lib_2": namespace_lib 31 | 32 | That gives the tag structure: 33 | 34 | src/ 35 | |-- lib_1/ namespace, namespace_lib 36 | | |-- namespace/ namespace 37 | | | |-- foo.ml 38 | | | +-- bar.ml 39 | | | 40 | | +-- other_namespace/ namespace 41 | | +-- foo.ml 42 | | 43 | +-- lib_2/ namespace, namespace_lib 44 | +-- namespace/ namespace 45 | |-- foo.ml 46 | +-- bar.ml 47 | 48 | These tags tell Namespaces to generate the requisite `.mllib` files for 49 | Ocamlbuild. 50 | 51 | 2. You can now build the library targets `lib_1.cma`, `lib_1.cmxa`, `lib_2.cma`, 52 | `lib_2.cmxa`. 53 | 54 | 3. To install the libraries with Findlib, describe the libraries as normal in a 55 | `META` file. Then, install it, together with the library files. For example, 56 | in `Makefile` syntax: 57 | 58 | TO_INSTALL := \ 59 | META \ 60 | _build/src/lib_1.cma \ 61 | _build/src/lib_1.cmxa \ 62 | _build/src/lib_1.a \ 63 | _build/src/lib_2.cma \ 64 | _build/src/lib_2.cmxa \ 65 | _build/src/lib_2.a \ 66 | $(shell find _build -name *.cmi) \ 67 | $(shell find _build -name *.cmt) \ 68 | $(shell find _build -name *.cmti) 69 | 70 | install : 71 | ocamlfind install $(PACKAGE) $(TO_INSTALL) 72 | 73 | See the [library test][libtest] for a small example. 74 | 75 | [libtest]: https://github.com/aantron/namespaces/tree/master/test/3-library 76 | 77 | 78 | #### With OASIS 79 | 80 | To build libraries with Namespaces and OASIS: 81 | 82 | 1. Tag your library directories with `namespace_lib`, as described in step (1) 83 | above for Ocamlbuild. 84 | 85 | 2. Namespaces will now generate its own `.mllib` files, but these will conflict 86 | with the ones that OASIS generates from each `Library` section in `_oasis`. 87 | To fix that, the OASIS `.mllib` files need to be deleted. Make sure your 88 | project doesn't have any `.mllib` files that you want to keep, and then add 89 | the following to the bottom of your `myocamlbuild.ml`: 90 | 91 | let () = Namespaces.delete_mllib_files () 92 | 93 | This is an unpleasant hack, and I hope to remove it in a future version of 94 | Namespaces. 95 | 96 | 3. When using Namespaces with OASIS, the `_oasis` file is essentially only for 97 | assembling the `META` file – at least with respect to these instructions. The 98 | `_oasis` file will look something like this: 99 | 100 | OASISFormat: 0.4 101 | Name: my_lib 102 | Version: 0.1 103 | Synopsis: Description 104 | Authors: Me 105 | License: BSD-2-clause 106 | BuildTools: ocamlbuild 107 | Plugins: META (0.3) 108 | 109 | OCamlVersion: >= 4.02 110 | AlphaFeatures: ocamlbuild_more_args 111 | XOCamlbuildPluginTags: package(namespaces) 112 | 113 | Library lib_1 114 | FindlibName: my_lib 115 | Path: src 116 | Modules: Lib_1 117 | XMETADescription: lib_1 118 | 119 | Library lib_2 120 | FindlibParent: lib_1 121 | Path: src 122 | Modules: Lib_2 123 | XMETADescription: lib_2 124 | 125 | 4. Do not use `ocaml setup.ml -install` to install your library. Instead, 126 | install as in step (3) of the Ocamlbuild instructions above, but change the 127 | path `META` to `src/META`, `lib/META`, or whatever subdirectory your 128 | libraries are located in. 129 | 130 | See the [OASIS library test][oasis-libtest] for an example. 131 | 132 | [oasis-libtest]: https://github.com/aantron/namespaces/tree/master/test/4-oasis-library 133 | 134 | 135 | 136 |
137 | 138 | 139 | ## Generated files 140 | 141 | Namespaces has trouble with generated files, such as `.ml` files generated from 142 | `.mll` files by `ocamllex`. If you have such files in your project, you need to 143 | tell Namespaces about the generator. For example, if you have a program that 144 | generates `.ml` and `.mli` files from `.rpc` files, 145 | 146 | let () = 147 | Ocamlbuild_plugin.dispatch 148 | (Namespaces.handler ~generators:["%.rpc", ["%.ml"; "%.mli"]]) 149 | 150 | The default value of `~generators` is `Namespaces.builtin_generators`, which has 151 | rules for `ocamllex` and `ocamlyacc`. See [`namespaces.mli`][mli] for details. 152 | 153 | [mli]: https://github.com/aantron/namespaces/blob/master/src/namespaces.mli#L37 154 | 155 | 156 | 157 |
158 | 159 | 160 | ## Effect on separate compilation 161 | 162 | As currently implemented, grouping modules in a namespace causes recompilation 163 | of everything that depends on that namespace, each time one of the namespace 164 | child modules changes. However, only the children that are actually referenced 165 | by depending code are linked with that code. To illustrate, suppose there are 166 | modules 167 | 168 | ``` 169 | Namespace.Foo 170 | Namespace.Bar 171 | Main 172 | ``` 173 | 174 | and `Main` refers only to `Namespace.Foo`. `Main` could be in the same project, 175 | or `Namespace` might be packaged as a library, with `Main` in a different 176 | project. 177 | 178 | When an executable with `Main` is linked, only `Namespace.Foo` is included. 179 | However, if `Main` is in the same project as `Namespace`, then when *either* 180 | `Namespace.Foo` or `Namespace.Bar` is changed, `Main` is recompiled – even 181 | though that is not strictly necessary in the case `Namespace.Bar` is changed. 182 | This is a result of an imprecision in the current dependency analysis. 183 | -------------------------------------------------------------------------------- /src/META: -------------------------------------------------------------------------------- 1 | version = "0.5.1" 2 | description = "Directory-based module hierarchy" 3 | requires = "str ocamlbuild" 4 | archive(byte) = "namespaces.cma" 5 | archive(native) = "namespaces.cmxa" 6 | -------------------------------------------------------------------------------- /src/descr: -------------------------------------------------------------------------------- 1 | Turn directories into OCaml modules 2 | 3 | An Ocamlbuild plugin that turns directories tagged with "namespace" into a 4 | nested module hierarchy. Each directory becomes a module. Filenames become 5 | scoped, so you can have the same filename in multiple directories. So, if you 6 | have 7 | 8 | server 9 | |-- foo.ml 10 | +-- bar.ml 11 | client 12 | |-- foo.ml 13 | |-- bar.ml 14 | |-- ui 15 | | +-- reactive.ml 16 | +-- client.ml 17 | 18 | It is as if you had written 19 | 20 | module Server = 21 | struct 22 | module Foo = (* server/foo.ml *) 23 | module Bar = (* server/bar.ml *) 24 | end 25 | 26 | module Client = 27 | struct 28 | module Foo = (* client/foo.ml *) 29 | module Bar = (* client/bar.ml *) 30 | module Ui = 31 | struct 32 | module Reactive = (* client/ui/reactive.ml *) 33 | end 34 | 35 | include (* client/client.ml *) 36 | end 37 | -------------------------------------------------------------------------------- /src/generators.ml: -------------------------------------------------------------------------------- 1 | (* This file is part of Namespaces, distributed under the terms of the 2-clause 2 | BSD license. See https://github.com/aantron/namespaces. *) 3 | 4 | type t = string * string list 5 | 6 | let identity = "%.ml", ["%.ml"] 7 | let signature = "%.mli", ["%.mli"] 8 | let ocamllex = "%.mll", ["%.ml"] 9 | let ocamlyacc = "%.mly", ["%.ml"] 10 | let builtin = [ocamllex; ocamlyacc] 11 | 12 | type parsed = string -> string list 13 | 14 | let parse_single = 15 | let wildcard = Str.regexp_string "%" in 16 | fun (dep, prods) -> 17 | let dep_prefix, dep_suffix = 18 | match Str.split_delim wildcard dep with 19 | | [p; s] -> p, s 20 | | _ -> 21 | failwith "Generator dependency must contain exactly one wildcard" in 22 | let dep_regexp = 23 | "^" ^ (Str.quote dep_prefix) ^ "\\(.+\\)" ^ 24 | (Str.quote dep_suffix) ^ "$" 25 | |> Str.regexp in 26 | 27 | let substitute prod = 28 | let prod_fragments = Str.split_delim wildcard prod in 29 | fun stem -> String.concat stem prod_fragments in 30 | let substitute_all = List.map substitute prods in 31 | 32 | let parsed_generator file = 33 | try 34 | Str.search_forward dep_regexp file 0 |> ignore; 35 | let stem = Str.matched_group 1 file in 36 | List.map (fun f -> f stem) substitute_all 37 | with Not_found -> [] in 38 | 39 | parsed_generator 40 | 41 | let parse generators = 42 | let parsed_generators = 43 | List.map parse_single (identity::signature::generators) in 44 | fun file -> 45 | List.map (fun g -> g file) parsed_generators |> List.concat 46 | -------------------------------------------------------------------------------- /src/generators.mli: -------------------------------------------------------------------------------- 1 | (* This file is part of Namespaces, distributed under the terms of the 2-clause 2 | BSD license. See https://github.com/aantron/namespaces. *) 3 | 4 | (** Generator specification parser. *) 5 | 6 | type t = string * string list 7 | 8 | val ocamllex : t 9 | val ocamlyacc : t 10 | val builtin : t list 11 | 12 | type parsed = string -> string list 13 | val parse : t list -> parsed 14 | -------------------------------------------------------------------------------- /src/modules.ml: -------------------------------------------------------------------------------- 1 | (* This file is part of Namespaces, distributed under the terms of the 2-clause 2 | BSD license. See https://github.com/aantron/namespaces. *) 3 | 4 | open Ocamlbuild_plugin 5 | 6 | let sprintf = Printf.sprintf 7 | let bprintf = Printf.bprintf 8 | 9 | (* User tags. *) 10 | let namespace_tag = "namespace" 11 | let namespace_with_name_tag = "namespace_with_name" 12 | let namespace_level_tag = "namespace_level" 13 | let namespace_library_tag = "namespace_lib" 14 | 15 | let namespace_tag_regexp = 16 | Str.regexp 17 | (sprintf "%s$\\|%s\\((\\([^)]*\\))\\)$" 18 | namespace_tag namespace_with_name_tag) 19 | 20 | let library_tag_regexp = 21 | Str.regexp (sprintf "%s\\((\\([^)]*\\))\\)?$" namespace_library_tag) 22 | 23 | (* Internal use tags. *) 24 | let alias_file_tag = "namespace_alias_file" 25 | let ordered_open_tag = "namespace_open" 26 | let namespace_library_dependency_tag lib = 27 | sprintf "namespace_library_dependency_%s" lib 28 | let dummy_tag = "namespaces_dummy" 29 | 30 | let namespace_library_title = "_namespaces" 31 | 32 | type file = 33 | {original_name : string; 34 | renamed_name : string; 35 | prefixed_name : string; 36 | directory : string list; 37 | namespace : string list} 38 | 39 | type namespace_members = 40 | {modules : file list; 41 | interfaces : file list; 42 | namespaces : namespace list} 43 | and namespace = file * namespace_members 44 | 45 | let empty_members = {modules = []; interfaces = []; namespaces = []} 46 | 47 | let tree : namespace_members ref = ref empty_members 48 | 49 | let path_to_string (path : string list) = 50 | match path with 51 | [] -> Filename.current_dir_name 52 | | _ -> String.concat Filename.dir_sep path 53 | 54 | let original_module {original_name; _} = 55 | module_name_of_pathname original_name 56 | 57 | let original_path {original_name; directory; _} = 58 | directory @ [original_name] |> path_to_string 59 | 60 | let final_path {prefixed_name; directory; _} = 61 | directory @ [prefixed_name] |> path_to_string 62 | 63 | let module_path {renamed_name; namespace; _} = 64 | namespace @ [module_name_of_pathname renamed_name] 65 | 66 | let member_module_files {modules; namespaces; _} = 67 | (List.map fst namespaces) @ modules 68 | 69 | let digest_file base_name = 70 | sprintf "%s.digest" base_name 71 | 72 | let alias_container_module base_name = 73 | sprintf "%s__aliases_" (module_name_of_pathname base_name) 74 | 75 | let alias_group_module base_name for_module = 76 | sprintf "%s__aliases__for_%s_" 77 | (module_name_of_pathname base_name) (module_name_of_pathname for_module) 78 | 79 | let alias_export_module base_name = 80 | sprintf "%s__aliases__export_" (module_name_of_pathname base_name) 81 | 82 | let alias_container_file base_name = 83 | sprintf "%s__aliases_.ml" base_name 84 | 85 | let namespace_file base_name = 86 | sprintf "%s.ml" base_name 87 | 88 | let library_list_file base_name = 89 | sprintf "%s.mllib" base_name 90 | 91 | (* Recursively traverses the source tree. Keeps track of the current filesystem 92 | path as a list of strings. Keeps track of the current namespace path in the 93 | same way. The namespace path is extended when a directory is encountered that 94 | has the "namespace" tag set. 95 | 96 | Notes all [.ml] and [.mli] files and "namespace" directories found by 97 | creating records of type [file] for them. Organizes these into a forest of 98 | records of type [namespace], represented by a single top-level record of type 99 | [namespace_members]. This top-level record represents the top-level modules 100 | and namespaces in the project. 101 | 102 | Generated [.ml] and [.mli] files are part of the project, but cannot be found 103 | in the source tree. These are discovered through the rules passed in the 104 | [generators] argument to [scan_tree]. *) 105 | let scan_tree : 106 | Generators.parsed -> (string -> string option) -> 107 | namespace_members * (string, string list) Hashtbl.t = 108 | 109 | (* If the given directory is tagged with "namespace", evaluates to Some None. 110 | If it is tagged with "namespace(foo)", evaluates to Some (Some "foo"). 111 | Otherwise, evaluates to None. *) 112 | let is_namespace directory_path = 113 | let rec scan_for_namespace_tag namespace_tag_found = function 114 | | [] -> if namespace_tag_found then Some None else None 115 | | tag::more_tags -> 116 | if not @@ Str.string_match namespace_tag_regexp tag 0 then 117 | scan_for_namespace_tag namespace_tag_found more_tags 118 | else 119 | try Some (Some (Str.matched_group 2 tag)) 120 | with Not_found -> scan_for_namespace_tag true more_tags in 121 | 122 | directory_path 123 | |> tags_of_pathname 124 | |> Tags.elements 125 | |> scan_for_namespace_tag false in 126 | 127 | (* If the given path is tagged with "namespace_lib(foo)", evaluates to 128 | Some "foo". Otherwise, evaluates to None. *) 129 | let is_for_library path = 130 | let rec scan_for_library_tag = function 131 | | [] -> None 132 | | tag::more_tags -> 133 | if not @@ Str.string_match library_tag_regexp tag 0 then 134 | scan_for_library_tag more_tags 135 | else 136 | try Some (Str.matched_group 2 tag) 137 | with Not_found -> 138 | sprintf 139 | "The %s tag requires an argument (library name)" 140 | namespace_library_tag 141 | |> failwith in 142 | 143 | path 144 | |> tags_of_pathname 145 | |> Tags.elements 146 | |> scan_for_library_tag in 147 | 148 | (* Constructs a [file] record when given the current filesystem and module 149 | paths, and the basename of a file or directory. *) 150 | let make_file_record directory namespace original_name maybe_renamed_name = 151 | let renamed_name = 152 | match maybe_renamed_name with 153 | | None -> original_name 154 | | Some other_name -> other_name in 155 | let prefixed_name = String.concat "__" (namespace @ [renamed_name]) in 156 | {original_name; 157 | renamed_name; 158 | prefixed_name; 159 | directory; 160 | namespace} in 161 | 162 | (* Given a [namespace_members], eliminates duplicates in its [modules] and 163 | [interfaces] fields. *) 164 | let unique {modules; interfaces; namespaces} = 165 | let compare_files {original_name = name; _} {original_name = name'; _} = 166 | String.compare name name' in 167 | {modules = List.sort_uniq compare_files modules; 168 | interfaces = List.sort_uniq compare_files interfaces; 169 | namespaces} in 170 | 171 | fun generators filter -> 172 | let libraries = Hashtbl.create 32 in 173 | let add_to_library library_name module_file = 174 | let modules = 175 | try Hashtbl.find libraries library_name 176 | with Not_found -> [] in 177 | Hashtbl.replace libraries library_name (module_file::modules) in 178 | 179 | let rec traverse library directory namespace members_acc = 180 | path_to_string directory 181 | |> Sys.readdir 182 | |> Array.fold_left 183 | (fun members_acc entry -> 184 | let entry_path = directory @ [entry] in 185 | let entry_path_string = path_to_string entry_path in 186 | 187 | let library = 188 | match is_for_library entry_path_string with 189 | | None -> library 190 | | Some library_name -> Some library_name in 191 | 192 | (* Directories. *) 193 | if Sys.is_directory entry_path_string then 194 | let absolute_path = 195 | Filename.concat 196 | (Sys.getcwd ()) (String.concat Filename.dir_sep directory) in 197 | if absolute_path = !Options.build_dir then members_acc 198 | else 199 | match is_namespace entry_path_string with 200 | | None -> traverse library entry_path namespace members_acc 201 | | Some rename -> 202 | let new_namespace = 203 | create_namespace 204 | library directory namespace entry entry_path rename in 205 | {members_acc 206 | with namespaces = new_namespace::members_acc.namespaces} 207 | 208 | (* Files. *) 209 | else 210 | create_modules library directory namespace entry members_acc) 211 | members_acc 212 | 213 | and create_namespace library directory namespace entry entry_path rename = 214 | let file = make_file_record directory namespace entry rename in 215 | 216 | let library_name = 217 | match library with 218 | | None -> namespace_library_title 219 | | Some name -> name in 220 | 221 | add_to_library library_name (final_path file); 222 | add_to_library library_name (alias_container_file (final_path file)); 223 | 224 | let nested_namespace = 225 | namespace @ [module_name_of_pathname file.renamed_name] in 226 | let members = 227 | traverse 228 | (Some library_name) entry_path nested_namespace empty_members in 229 | (file, unique members) 230 | 231 | and create_modules library directory namespace entry members_acc = 232 | generators entry 233 | |> List.map filter 234 | |> List.fold_left 235 | (fun accumulator -> function 236 | | None -> accumulator 237 | | Some name -> name::accumulator) 238 | [] 239 | |> List.fold_left 240 | (fun members_acc generated_file -> 241 | let file = make_file_record directory namespace generated_file None in 242 | if Filename.check_suffix generated_file ".ml" then 243 | ((match library with 244 | | None -> () 245 | | Some library_name -> 246 | add_to_library library_name (final_path file)); 247 | {members_acc with modules = file::members_acc.modules}) 248 | else if Filename.check_suffix generated_file ".mli" then 249 | {members_acc with interfaces = file::members_acc.interfaces} 250 | else members_acc) 251 | members_acc in 252 | 253 | let top_level_members = traverse None [] [] empty_members in 254 | 255 | unique top_level_members, libraries 256 | 257 | let iter f = 258 | let rec traverse members = 259 | List.iter (fun file -> f (`Interface, file)) members.interfaces; 260 | List.iter (fun file -> f (`Module, file)) members.modules; 261 | List.iter 262 | (fun (file, members') -> f (`Namespace, file); traverse members') 263 | members.namespaces in 264 | traverse !tree 265 | 266 | (* Maps namespaced file paths to [file] records. For example, for 267 | [server/foo.ml], the key is ["server/server__foo.ml"]. *) 268 | let renamed_files : (string, file) Hashtbl.t = 269 | Hashtbl.create 512 270 | 271 | (* Maps namespaced namespace directories to [namespace] records. For example, 272 | for [server/api], the key is ["server/server__api"]. *) 273 | let namespace_directory_map : (string, namespace) Hashtbl.t = 274 | Hashtbl.create 32 275 | 276 | (** Maps namespace directory namespace paths to [namespace] records. For 277 | example, for [server/api], the key is [["Server"; "Api"]]. *) 278 | let namespace_module_map : (string list, namespace) Hashtbl.t = 279 | Hashtbl.create 32 280 | 281 | let namespace_libraries : (string, string list) Hashtbl.t ref = 282 | ref (Hashtbl.create 1) 283 | 284 | (* For each [.ml] or [.mli] file found during the source tree scan, if its 285 | namespaced name differs from its original name, adds it to [renamed_files]. 286 | The condition holds for files inside namespaces, and doesn't hold for files 287 | representing top-level modules that have not been renamed with 288 | [namespace_with_name]. *) 289 | let index_renamed_files () = 290 | iter 291 | (function 292 | | `Namespace, _ -> () 293 | | (`Interface | `Module), file -> 294 | if file.prefixed_name <> file.original_name then 295 | Hashtbl.add renamed_files (final_path file) file) 296 | 297 | let file_by_renamed_path s = 298 | try Some (Hashtbl.find renamed_files s) 299 | with Not_found -> None 300 | 301 | (* Populates [namespace_directory_map] and [namespace_module_map]. *) 302 | let index_namespaces () = 303 | let rec traverse members = 304 | members.namespaces |> List.iter 305 | (fun ((file, members') as namespace) -> 306 | Hashtbl.add namespace_directory_map (final_path file) namespace; 307 | Hashtbl.add namespace_module_map (module_path file) namespace; 308 | traverse members') in 309 | traverse !tree; 310 | 311 | (* Silence warnings about unused tags. *) 312 | pflag [dummy_tag] namespace_with_name_tag (fun _ -> N); 313 | pflag [dummy_tag] namespace_library_tag (fun _ -> N); 314 | mark_tag_used namespace_tag; 315 | mark_tag_used namespace_level_tag 316 | 317 | let namespace_by_final_directory s = 318 | try Some (Hashtbl.find namespace_directory_map s) 319 | with Not_found -> None 320 | 321 | let extension s = 322 | try 323 | let index = (String.rindex s '.') + 1 in 324 | let remainder_length = (String.length s) - index in 325 | String.sub s index remainder_length 326 | with Not_found -> "" 327 | 328 | let build_native_and_or_bytecode () = 329 | !Options.targets 330 | |> List.fold_left 331 | (fun (native, bytecode) target -> 332 | match extension target with 333 | | "native" | "cmx" | "cmxa" -> true, bytecode 334 | | "byte" | "cmo" | "cma" -> native, true 335 | | _ -> native, bytecode) 336 | (false, false) 337 | 338 | let digest_dependencies (_, members) = 339 | let build_native, build_bytecode = build_native_and_or_bytecode () in 340 | 341 | let module_files = List.map final_path members.modules in 342 | let namespace_files = 343 | members.namespaces 344 | |> List.map (fun (file, _) -> final_path file |> namespace_file) in 345 | let ml_files = module_files @ namespace_files in 346 | 347 | let native_objects = 348 | if build_native then 349 | ml_files |> List.map (fun name -> (Filename.chop_extension name) ^ ".cmx") 350 | else [] in 351 | let bytecode_objects = 352 | if build_bytecode then 353 | ml_files |> List.map (fun name -> (Filename.chop_extension name) ^ ".cmo") 354 | else [] in 355 | 356 | native_objects @ bytecode_objects 357 | |> List.sort_uniq String.compare 358 | 359 | let alias_file_contents (namespace_file, members) = 360 | let buffer = Buffer.create 4096 in 361 | let member_modules = member_module_files members in 362 | 363 | member_modules |> List.iter 364 | (fun member -> 365 | bprintf buffer "module %s =\nstruct\n" 366 | (alias_group_module namespace_file.prefixed_name member.renamed_name); 367 | member_modules |> List.iter 368 | (fun member' -> 369 | if member' <> member then 370 | bprintf buffer " module %s = %s\n" 371 | (module_name_of_pathname member'.renamed_name) 372 | (module_name_of_pathname member'.prefixed_name)); 373 | bprintf buffer "end\n\n"); 374 | 375 | bprintf buffer "module %s =\nstruct\n" 376 | (alias_export_module namespace_file.prefixed_name); 377 | member_modules |> List.iter 378 | (fun member -> 379 | bprintf buffer " module %s = %s\n" 380 | (module_name_of_pathname member.renamed_name) 381 | (module_name_of_pathname member.prefixed_name)); 382 | bprintf buffer "end\n"; 383 | 384 | Buffer.contents buffer 385 | 386 | let namespace_file_contents (namespace_file, members) digest = 387 | let buffer = Buffer.create 4096 in 388 | let alias_module = alias_container_module namespace_file.prefixed_name in 389 | let export_module = alias_export_module namespace_file.prefixed_name in 390 | let included_members = 391 | member_module_files members 392 | |> List.fold_left 393 | (fun names member -> 394 | let tags = tags_of_pathname (original_path member) in 395 | if Tags.does_match tags (Tags.of_list [namespace_level_tag]) then 396 | (module_name_of_pathname member.prefixed_name)::names 397 | else names) 398 | [] in 399 | 400 | bprintf buffer "open %s\n" alias_module; 401 | bprintf buffer "include %s\n" export_module; 402 | included_members |> List.iter (bprintf buffer "include %s\n"); 403 | bprintf buffer "\nlet _digest_%s = ()\n" digest; 404 | 405 | Buffer.contents buffer 406 | 407 | let library_contents_by_final_base_path path = 408 | try Some (Hashtbl.find !namespace_libraries path) 409 | with Not_found -> None 410 | 411 | let resolve (referrer : file) (referent : string) : string = 412 | let rec loop self = function 413 | | [] -> referent 414 | | (_::rest) as reversed_namespace -> 415 | let namespace_file, members = 416 | List.rev reversed_namespace |> Hashtbl.find namespace_module_map in 417 | let member_modules = member_module_files members in 418 | try 419 | let matching_module = 420 | member_modules |> List.find 421 | (fun {renamed_name; _} -> 422 | referent = (module_name_of_pathname renamed_name)) 423 | |> fun {prefixed_name; _} -> module_name_of_pathname prefixed_name in 424 | if matching_module = self then raise_notrace Not_found 425 | else matching_module 426 | with Not_found -> 427 | loop (module_name_of_pathname namespace_file.prefixed_name) rest in 428 | 429 | loop 430 | (module_name_of_pathname referrer.prefixed_name) 431 | (List.rev referrer.namespace) 432 | 433 | let tag_namespace_files () = 434 | let for_namespace base_path (_, members) = 435 | let alias_file_path = alias_container_file base_path in 436 | 437 | tag_file alias_file_path [alias_file_tag; "no_alias_deps"]; 438 | 439 | member_module_files members 440 | |> List.iter 441 | (fun {prefixed_name; _} -> 442 | non_dependency 443 | alias_file_path (module_name_of_pathname prefixed_name)) in 444 | 445 | Hashtbl.iter for_namespace namespace_directory_map; 446 | 447 | flag ["ocaml"; "compile"; alias_file_tag] (S [A "-w"; A "-49"]) 448 | 449 | let add_open_tags () = 450 | let tag_file final_path file = 451 | let rec list_modules_to_open accumulator enclosing_namespace_path = function 452 | | [] -> failwith "impossible" 453 | | [_] -> List.rev accumulator 454 | | c::c'::rest -> 455 | let enclosing_namespace_path = enclosing_namespace_path @ [c] in 456 | let namespace_file, _ = 457 | Hashtbl.find namespace_module_map enclosing_namespace_path in 458 | 459 | let alias_container = 460 | alias_container_module namespace_file.prefixed_name in 461 | let alias_group = 462 | alias_group_module namespace_file.prefixed_name c' in 463 | 464 | list_modules_to_open 465 | (alias_group::alias_container::accumulator) 466 | enclosing_namespace_path 467 | (c'::rest) in 468 | 469 | let tag = 470 | list_modules_to_open [] [] (module_path file) 471 | |> String.concat "," 472 | |> sprintf "%s(%s)" ordered_open_tag in 473 | 474 | tag_file final_path [tag]; 475 | tag_file (final_path ^ ".depends") [tag] in 476 | 477 | Hashtbl.iter tag_file renamed_files; 478 | 479 | let open_tag_to_flags modules_string = 480 | modules_string 481 | |> Str.split (Str.regexp ",") 482 | |> List.map (fun m -> S [A "-open"; A m]) 483 | |> fun options -> S options in 484 | 485 | pflag ["ocaml"; "compile"] ordered_open_tag open_tag_to_flags; 486 | pflag ["ocamldep"] ordered_open_tag open_tag_to_flags 487 | 488 | (* Makes each executable target depend on all the libraries in the current 489 | project. *) 490 | let create_library_tags () = 491 | Hashtbl.iter 492 | (fun base_path _ -> 493 | let tag_name = namespace_library_dependency_tag base_path in 494 | ocaml_lib ~tag_name base_path) 495 | !namespace_libraries 496 | 497 | let tag_executable_with_libraries filename = 498 | Hashtbl.iter 499 | (fun base_path _ -> 500 | let tag_name = namespace_library_dependency_tag base_path in 501 | tag_file filename [tag_name]) 502 | !namespace_libraries 503 | 504 | (* For each module [Foo] that is not a top-level module, hides the module from 505 | Ocamlbuild by extending [Options.ignore_list]. *) 506 | let hide_original_nested_modules () = 507 | let top_level_names = 508 | member_module_files !tree 509 | |> List.map (fun {renamed_name; _} -> 510 | module_name_of_pathname renamed_name) in 511 | 512 | let hide_if_nested file = 513 | match file.namespace with 514 | | [] -> () 515 | | _ -> 516 | let renamed_module_name = module_name_of_pathname file.renamed_name in 517 | if not (List.mem renamed_module_name top_level_names) then 518 | Options.ignore_list := renamed_module_name::!Options.ignore_list in 519 | 520 | Hashtbl.iter (fun _ file -> hide_if_nested file) renamed_files; 521 | Hashtbl.iter (fun _ (file, _) -> hide_if_nested file) namespace_directory_map 522 | 523 | let scan generators filter = 524 | let namespace_trees, libraries = scan_tree generators filter in 525 | tree := namespace_trees; 526 | namespace_libraries := libraries; 527 | 528 | index_renamed_files (); 529 | index_namespaces (); 530 | tag_namespace_files (); 531 | add_open_tags (); 532 | hide_original_nested_modules (); 533 | create_library_tags () 534 | -------------------------------------------------------------------------------- /src/modules.mli: -------------------------------------------------------------------------------- 1 | (* This file is part of Namespaces, distributed under the terms of the 2-clause 2 | BSD license. See https://github.com/aantron/namespaces. *) 3 | 4 | (** Module scanning and index. *) 5 | 6 | type file = 7 | {original_name : string; 8 | (** The original basename of the file or directory, including extension, if 9 | any. For example, [server/foo.ml] has this set to ["foo.ml"]. *) 10 | renamed_name : string; 11 | (** For namespaces tagged with [namespace_with_name(n)], this is [n]. 12 | Otherwise, the same as [original_name]. *) 13 | prefixed_name : string; 14 | (** The prefixed name of the file or directory. For example, [server/foo.ml] 15 | has this field set to [server__foo.ml]. *) 16 | directory : string list; 17 | (** The path, relative to the project root, in which this file or directory 18 | was found. For example, [server/foo.ml] has this set to [["server"]]. *) 19 | namespace : string list 20 | (** The module path in which this file's or directory's module is aliased. 21 | For example, [server/foo] is has this set to ["Server"]. *)} 22 | 23 | type namespace 24 | 25 | val scan : Generators.parsed -> (string -> string option) -> unit 26 | (** Scans the source tree for directories to become namespaces and files to 27 | become namespace members, and notes the results as values of type [file] in 28 | a state variable inside this module. *) 29 | 30 | val iter : ([ `Module | `Interface | `Namespace ] * file -> unit) -> unit 31 | 32 | val namespace_level_tag : string 33 | 34 | (** File by its final path. Used for symlink rules and [ocamldep] output 35 | rewriting. *) 36 | val file_by_renamed_path : string -> file option 37 | 38 | val original_path : file -> string 39 | 40 | (** Namespace by its final directory. *) 41 | val namespace_by_final_directory : string -> namespace option 42 | 43 | (** Digest file by namespace final directory. Simply appends [.digest]. *) 44 | val digest_file : string -> string 45 | 46 | (** List of [.cmi] files, one for each member of the given namespace. *) 47 | val digest_dependencies : namespace -> string list 48 | 49 | (** Alias file by namespace final directory. Appends [__aliases_.ml]. *) 50 | val alias_container_file : string -> string 51 | 52 | (** Contents of the alias file. For each module [M] of the namespace ([N]), 53 | creates a module (in the alias file) called [N__aliases__for_M_], which 54 | contains aliases for all the members of [N] besides [M]. These are the 55 | siblings of [M]. The module is opened while compiling [M] to make the 56 | siblings accessible by their short paths. 57 | 58 | Also creates a module in the alias file called [N__aliases__export_], which 59 | contains aliases for all the members of [N]. This module is included in the 60 | namespace file, which makes the members of [N] accessible from modules 61 | outside the namespace. *) 62 | val alias_file_contents : namespace -> string 63 | 64 | (** Contents of the namespace file. This file includes the export module from 65 | the alias file, and any namespace members that are tagged with 66 | [namespace_level]. The string argument gives the digest, which is a dummy 67 | value used to force recompilation of any file depending on the namespace 68 | file when the interfaces inside the namespace change. *) 69 | val namespace_file_contents : namespace -> string -> string 70 | 71 | (** Library module list file by namespace final directory. Appends [.mllib]. *) 72 | val library_list_file : string -> string 73 | 74 | (** Lists the module file names in the given namespace library. *) 75 | val library_contents_by_final_base_path : string -> string list option 76 | 77 | (** Makes the given executable depend on all namespace libraries. *) 78 | val tag_executable_with_libraries : string -> unit 79 | 80 | (** Given a file (the referrer) and the short name of a module it refers to, 81 | tries to resolve the module name to a namespaced module. *) 82 | val resolve : file -> string -> string 83 | -------------------------------------------------------------------------------- /src/namespaces.ml: -------------------------------------------------------------------------------- 1 | (* This file is part of Namespaces, distributed under the terms of the 2-clause 2 | BSD license. See https://github.com/aantron/namespaces. *) 3 | 4 | open Ocamlbuild_plugin 5 | 6 | type generator = Generators.t 7 | 8 | let ocamllex = Generators.ocamllex 9 | let ocamlyacc = Generators.ocamlyacc 10 | let builtin_generators = Generators.builtin 11 | 12 | let plugin_filter = function 13 | | "myocamlbuild.ml" -> None 14 | | "setup.ml" -> None 15 | | s -> Some s 16 | 17 | type file = Modules.file = 18 | {original_name : string; 19 | renamed_name : string; 20 | prefixed_name : string; 21 | directory : string list; 22 | namespace : string list} 23 | 24 | let iter = Modules.iter 25 | 26 | let rec innermost_namespace = function 27 | | [] -> None 28 | | [n] -> Some n 29 | | _::namespaces -> innermost_namespace namespaces 30 | 31 | let include_files_sharing_namespace_title () = 32 | iter 33 | (function 34 | | `Module, file -> 35 | (match innermost_namespace file.namespace with 36 | | Some n when n = (module_name_of_pathname file.original_name) -> 37 | tag_file (Modules.original_path file) [Modules.namespace_level_tag] 38 | | _ -> ()) 39 | | _ -> ()) 40 | 41 | let handler ?(generators = Generators.builtin) ?(filter = plugin_filter) = 42 | function 43 | | After_rules -> 44 | Modules.scan (Generators.parse generators) filter; 45 | Rules.add_all (); 46 | include_files_sharing_namespace_title () 47 | | _ -> () 48 | 49 | let delete_mllib_files () = 50 | let rec traverse path = 51 | path 52 | |> Sys.readdir 53 | |> Array.iter (fun entry -> 54 | let path = Filename.concat path entry in 55 | if Sys.is_directory path then 56 | traverse path 57 | else 58 | if Filename.check_suffix entry ".mllib" then 59 | Sys.remove path) 60 | in 61 | traverse Filename.current_dir_name 62 | -------------------------------------------------------------------------------- /src/namespaces.mli: -------------------------------------------------------------------------------- 1 | (* This file is part of Namespaces, distributed under the terms of the 2-clause 2 | BSD license. See https://github.com/aantron/namespaces. *) 3 | 4 | (** [ocamlbuild] namespaces plugin. This plugin turns directories in the source 5 | tree into namespace modules. For example: 6 | 7 | The directory tree 8 | {v 9 | main.ml 10 | foo/ 11 | bar/ 12 | some.ml v} 13 | 14 | results in modules [Main], [Foo], [Foo.Bar], and [Foo.Bar.Some], provided 15 | the directories [foo/] and [bar/] are tagged with [namespace]. The easiest 16 | way to do this is to add [<**/*>: namespace] to your [_tags] file. 17 | 18 | To include this plugin in your build process, call [Namespaces.handler] from 19 | your [myocamlbuild.ml] file, and invoke ocamlbuild using 20 | [ocamlbuild -use-ocamlfind -plugin-tag "package(namespaces)" [your_target]]. 21 | A minimal [myocamlbuild.ml] file using this plugin looks like this: 22 | 23 | {v 24 | open Ocamlbuild_plugin 25 | let () = dispatch Namespaces.handler v} 26 | 27 | For more information on [ocamlbuild], [_tags], and [myocamlbuild.ml], see 28 | the 29 | {{:http://caml.inria.fr/pub/docs/manual-ocaml/ocamlbuild.html} 30 | ocamlbuild manual}. 31 | *) 32 | 33 | open Ocamlbuild_plugin 34 | 35 | 36 | 37 | (** {1 Generated files} *) 38 | 39 | (** The plugin does not automatically detect generated [.ml] and [.mli] files in 40 | namespaces. If some of your files are generated, you must describe the 41 | generator to the plugin. The syntax is the same as for [ocamlbuild] rule 42 | dependencies and products. For example, the description of [ocamllex] is 43 | ["%.mll", ["%.ml"]]. Note that the outer brackets are part of `ocamldoc` 44 | code style syntax, not OCaml list syntax! 45 | *) 46 | type generator = string * string list 47 | 48 | (** ["%.mll", ["%.ml"]]. *) 49 | val ocamllex : generator 50 | 51 | (** ["%.mly", ["%.ml"]]. *) 52 | val ocamlyacc : generator 53 | 54 | (** The list [[ocamllex; ocamlyacc]]. *) 55 | val builtin_generators : generator list 56 | 57 | 58 | 59 | (** {1 Plugin} *) 60 | 61 | (** Scans the source tree and creates namespaces, as described above. If the 62 | [generators] parameter is not specified, it is equal to 63 | [builtin_generators]. The [filter] parameter allows transformation of the 64 | detected filenames or the omission of the files. *) 65 | val handler : 66 | ?generators:generator list -> 67 | ?filter:(string -> string option) -> hook -> unit 68 | 69 | (** Deletes all [.mllib] files in the source tree. This function is a workaround 70 | for building libraries with OASIS. OASIS generates its own [.mllib] files. 71 | To prevent them from being used, they should be deleted on each build by 72 | calling this function in `myocamlbuild.ml`, e.g.: 73 | 74 | {v 75 | let () = Namespaces.delete_mllib_files () v} *) 76 | val delete_mllib_files : unit -> unit 77 | 78 | 79 | 80 | (** {1 Debugging} *) 81 | 82 | (** Type of a file that has been indexed by the plugin during its scan of the 83 | source tree. [file] can represent a module, interface, or namespace. In the 84 | first two cases, [original_name] and [prefixed_name] end with [.ml] or 85 | [.mli], respectively. If the file is a namespace, [original_name] and 86 | [prefixed_name] are directory names without suffix. *) 87 | type file = 88 | {original_name : string; 89 | (** File or directory name as it appears in the source tree. *) 90 | renamed_name : string; 91 | (** If the file is a namespace tagged with [namespace_with_name(n)], this is 92 | [n]. Otherwise, it is equal to [original_name]. *) 93 | prefixed_name : string; 94 | (** File or directory name after prefixing with its namespace path. *) 95 | directory : string list; 96 | (** List of path components giving the directory containing the file. *) 97 | namespace : string list 98 | (** Module path of the namespace containing the module resulting from the 99 | file. *)} 100 | 101 | (** Calls the given function for each [.ml] file, [.mli] file, and namespace in 102 | the source tree. Must be called in the [After_rules] hook. *) 103 | val iter : ([ `Module | `Interface | `Namespace ] * file -> unit) -> unit 104 | -------------------------------------------------------------------------------- /src/opam: -------------------------------------------------------------------------------- 1 | opam-version: "1.2" 2 | name: "namespaces" 3 | version: "0.5.1" 4 | maintainer: "Anton Bachin " 5 | authors: "Anton Bachin " 6 | homepage: "https://github.com/aantron/namespaces" 7 | bug-reports: "https://github.com/aantron/namespaces/issues" 8 | dev-repo: "https://github.com/aantron/namespaces.git" 9 | license: "BSD" 10 | build: [make "build"] 11 | install: [make "install"] 12 | remove: ["ocamlfind" "remove" "namespaces"] 13 | depends: [ 14 | "ocamlfind" {build} 15 | "ocamlbuild" {build} 16 | ] 17 | available: [ 18 | ocaml-version >= "4.02" 19 | ] 20 | -------------------------------------------------------------------------------- /src/rules.ml: -------------------------------------------------------------------------------- 1 | (* This file is part of Namespaces, distributed under the terms of the 2-clause 2 | BSD license. See https://github.com/aantron/namespaces. *) 3 | 4 | open Ocamlbuild_plugin 5 | 6 | let sprintf = Printf.sprintf 7 | 8 | let fail = 9 | let exceptions : (string, exn) Hashtbl.t = Hashtbl.create 512 in 10 | let space = Str.regexp " " in 11 | fun file message -> 12 | let transformed = 13 | sprintf "___%s___%s___" file (Str.global_replace space "_" message) in 14 | fun build -> 15 | try Hashtbl.find exceptions transformed |> raise 16 | with Not_found -> 17 | match build [[transformed]] with 18 | | [Outcome.Bad e] -> Hashtbl.add exceptions transformed e; raise e 19 | | _ -> 20 | failwith "Couldn't capture solver failure exception" 21 | 22 | let make_directory for_file = 23 | Command.execute (Cmd (S [A "mkdir"; A "-p"; A (Filename.dirname for_file)])) 24 | 25 | let get_namespace env build = 26 | match Modules.namespace_by_final_directory (env "%") with 27 | | Some n -> n 28 | | None -> fail (env "%") "not a namespace" build 29 | 30 | let digest_file_rule () = 31 | let name = sprintf "namespace: directory -> %s" (Modules.digest_file "") in 32 | let prod = Modules.digest_file "%" in 33 | rule name ~stamp:prod 34 | begin 35 | fun env build -> 36 | get_namespace env build 37 | |> Modules.digest_dependencies 38 | |> List.map (fun f -> [f]) 39 | |> build 40 | |> List.map Outcome.ignore_good |> ignore; 41 | Nop 42 | end 43 | 44 | let alias_file_generation_rule () = 45 | let name = 46 | sprintf "namespace: directory -> %s" (Modules.alias_container_file "") in 47 | let prod = Modules.alias_container_file "%" in 48 | rule name ~prod 49 | begin 50 | fun env build -> 51 | let namespace = get_namespace env build in 52 | make_directory (env prod); 53 | Echo ([Modules.alias_file_contents namespace], env prod) 54 | end 55 | 56 | let namespace_file_generation_rule () = 57 | let prod = "%.ml" in 58 | let dep = "%.digest" in 59 | rule "namespace: directory -> ml" ~dep ~prod 60 | begin 61 | fun env build -> 62 | let namespace = get_namespace env build in 63 | let digest = Pathname.read (env dep) in 64 | make_directory (env prod); 65 | Echo ([Modules.namespace_file_contents namespace digest], env prod) 66 | end 67 | 68 | let long_name_rules () = 69 | let for_extension extension = 70 | let name = 71 | sprintf "namespace: %s -> %s (long name)" extension extension in 72 | let prod = "%." ^ extension in 73 | rule name ~prod 74 | begin 75 | fun env build -> 76 | let file = 77 | match Modules.file_by_renamed_path (env prod) with 78 | | Some f -> f 79 | | None -> fail (env prod) "not a namespaced file" build in 80 | 81 | build [[Modules.original_path file]] 82 | |> List.map Outcome.ignore_good |> ignore; 83 | 84 | (* Transfer tags from the target to the link. *) 85 | Modules.original_path file 86 | |> tags_of_pathname 87 | |> Tags.elements 88 | |> tag_file (env prod); 89 | 90 | ln_s file.Modules.original_name (env prod) 91 | end in 92 | 93 | for_extension "ml"; 94 | for_extension "mli" 95 | 96 | let dependency_filter_rules () = 97 | let whitespace = Str.regexp "[ \n\t\r]+" in 98 | let for_extension extension = 99 | let name = sprintf "namespace dependencies %s" extension in 100 | let prod = sprintf "%%.%s.depends" extension in 101 | let dep = sprintf "%%.%s" extension in 102 | rule name ~prod ~dep ~insert:`top 103 | begin 104 | fun env build -> 105 | let file = 106 | match Modules.file_by_renamed_path (env dep) with 107 | | Some f -> f 108 | | None -> fail (env prod) "not a namespaced file" build in 109 | 110 | let tags = 111 | (tags_of_pathname (env prod)) ++ "ocamldep" ++ "pp:dep" in 112 | 113 | let command = 114 | S [A "ocamlfind"; A "ocamldep"; T tags; A "-modules"; 115 | A (env dep)] in 116 | 117 | let dependency_string = 118 | Command.string_of_command_spec command |> run_and_read in 119 | 120 | let colon_index = String.index dependency_string ':' in 121 | let remainder = 122 | String.length dependency_string - (colon_index + 1) in 123 | let dependencies = 124 | String.sub dependency_string (colon_index + 1) remainder 125 | |> Str.split whitespace 126 | |> List.filter (fun s -> String.length s > 0) in 127 | 128 | let resolved_dependencies = 129 | dependencies |> List.map (Modules.resolve file) in 130 | 131 | let text = 132 | ((env dep) ^ ":")::resolved_dependencies 133 | |> String.concat " " 134 | |> fun s -> s ^ "\n" in 135 | 136 | Echo ([text], env prod) 137 | end in 138 | 139 | for_extension "ml"; 140 | for_extension "mli" 141 | 142 | let dependencies_from_log = 143 | let newline = Str.regexp_string "\n" in 144 | let compiled = 145 | Str.regexp "ocamlfind ocaml\\(c\\|opt\\).* \\([^ ]+\\.ml\\)$" in 146 | 147 | fun () -> 148 | if not @@ Sys.file_exists "_log" then 149 | failwith "_log not present; are you running with -quiet?"; 150 | 151 | "cat _log" 152 | |> run_and_read 153 | |> Str.split newline 154 | |> List.fold_left (fun acc line -> 155 | if not @@ Str.string_match compiled line 0 then acc 156 | else (Str.matched_group 2 line)::acc) [] 157 | |> List.rev_map module_name_of_pathname 158 | 159 | let library_rule () = 160 | let name = "namespace: * -> mllib" in 161 | let prod = Modules.library_list_file "%" in 162 | rule name ~prod 163 | begin 164 | fun env build -> 165 | let module_files = 166 | match Modules.library_contents_by_final_base_path (env "%") with 167 | | Some s -> s 168 | | None -> fail (env prod) "not a namespace library" build in 169 | 170 | (* Build the library contents to get a topological sort according to the 171 | internal dependency relation. *) 172 | module_files 173 | |> List.map (fun file -> 174 | let file = 175 | if Filename.check_suffix file ".ml" then file 176 | else file ^ ".ml" in 177 | [file ^ ".depends"]) 178 | |> build 179 | |> List.map Outcome.ignore_good 180 | |> ignore; 181 | 182 | let order_snapshot = dependencies_from_log () in 183 | 184 | let order_index module_name = 185 | let rec scan n = function 186 | | [] -> -1 187 | | name::rest -> 188 | if module_name = name then n else scan (n + 1) rest in 189 | scan 0 order_snapshot in 190 | 191 | let text = 192 | module_files 193 | |> List.map (fun file -> 194 | let name = module_name_of_pathname file in 195 | (name, order_index name)) 196 | |> List.sort (fun (_, index) (_, index') -> compare index index') 197 | |> List.map fst 198 | |> String.concat "\n" in 199 | 200 | make_directory (env prod); 201 | Echo ([text], env prod) 202 | end 203 | 204 | (* A side-effecting rule for each executable, that makes each executable depend 205 | on all the libraries in the same project. It is not possible to use 206 | [Options.targets] for this purpose, because that contains the literal names 207 | of each target, as given on the command line. If the user gives a target such 208 | as [main.byte], when the "real" target is [src/main.byte], Ocamlbuild will 209 | build [src/main.byte], [Options.targets] will contain [main.byte], and 210 | Ocamlbuild will ignore tags of [main.byte]. *) 211 | let executable_rules () = 212 | let for_extension extension = 213 | let name = sprintf "executable dependencies %s" extension in 214 | let prod = sprintf "%%.%s" extension in 215 | rule name ~prod ~insert:`top 216 | begin 217 | fun env build -> 218 | Modules.tag_executable_with_libraries (env prod); 219 | fail (env prod) "__dummy_target__" build 220 | end in 221 | 222 | for_extension "byte"; 223 | for_extension "native" 224 | 225 | 226 | 227 | let add_all () = 228 | digest_file_rule (); 229 | alias_file_generation_rule (); 230 | namespace_file_generation_rule (); 231 | long_name_rules (); 232 | dependency_filter_rules (); 233 | library_rule (); 234 | executable_rules () 235 | -------------------------------------------------------------------------------- /src/rules.mli: -------------------------------------------------------------------------------- 1 | (* This file is part of Namespaces, distributed under the terms of the 2-clause 2 | BSD license. See https://github.com/aantron/namespaces. *) 3 | 4 | (** [ocamlbuild] rules. *) 5 | 6 | val add_all : unit -> unit 7 | -------------------------------------------------------------------------------- /src/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aantron/namespaces/6974b5b8b93e4da3d0a9cc7b4d663518da7184eb/src/summary.png -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | _*/ 2 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | OCAMLBUILD := ocamlbuild -use-ocamlfind -no-links 2 | 3 | .PHONY : unit 4 | unit : rebuild 5 | $(OCAMLBUILD) test_main.native -- -runner sequential 6 | 7 | .PHONY : one 8 | one : rebuild 9 | $(OCAMLBUILD) test_main.native -- -runner sequential -only-test $(NAME) 10 | 11 | .PHONY : clean 12 | clean : 13 | ocamlbuild -clean 14 | rm -rf _plugin_findlib _project _project_findlib _depending 15 | 16 | .PHONY : rebuild 17 | rebuild : 18 | make -C .. 19 | -------------------------------------------------------------------------------- /test/_tags: -------------------------------------------------------------------------------- 1 | <**/*>: package(oUnit) 2 | "test_helpers.ml": package(process), package(str) 3 | <*.native> or <*.byte>: package(process), package(str) 4 | <_*>: -traverse 5 | -------------------------------------------------------------------------------- /test/test_basic.ml: -------------------------------------------------------------------------------- 1 | open OUnit2 2 | open Test_helpers 3 | 4 | let basic_build_system () = 5 | myocamlbuild_ml (); 6 | tags [": namespace"] 7 | 8 | let build_and_test ?(target = "main") expect = 9 | basic_build_system (); 10 | 11 | let byte_target = target ^ ".byte" in 12 | let native_target = target ^ ".native" in 13 | ocamlbuild [byte_target; native_target]; 14 | 15 | let expect = Printf.sprintf "%i\n" expect in 16 | let byte_product = 17 | Filename.concat Filename.current_dir_name (Filename.basename byte_target) in 18 | let native_product = 19 | Filename.concat 20 | Filename.current_dir_name (Filename.basename native_target) in 21 | 22 | assert_equal (run byte_product []) expect; 23 | assert_equal (run native_product []) expect 24 | 25 | let tests = "basic" >::: [ 26 | test "namespacing" begin fun () -> 27 | project begin fun () -> 28 | file "src/namespace/foo.ml" ["let v = 1"]; 29 | file "src/main.ml" ["Namespace.Foo.v |> string_of_int |> print_endline"]; 30 | 31 | build_and_test ~target:"src/main" 1 32 | end 33 | end; 34 | 35 | test "inferred_target" begin fun () -> 36 | project begin fun () -> 37 | file "src/namespace/foo.ml" ["let v = 1"]; 38 | file "src/main.ml" ["Namespace.Foo.v |> string_of_int |> print_endline"]; 39 | 40 | build_and_test 1 41 | end 42 | end; 43 | 44 | test "sibling_not_linked" begin fun () -> 45 | project begin fun () -> 46 | file "src/namespace/foo.ml" ["let v = 1"]; 47 | file "src/namespace/bar.ml" ["let () = exit 1"]; 48 | file "src/main.ml" ["Namespace.Foo.v |> string_of_int |> print_endline"]; 49 | 50 | build_and_test 1 51 | end; 52 | 53 | project begin fun () -> 54 | file "src/namespace/foo.ml" ["let v = 1"]; 55 | file "src/namespace/bar.ml" ["let v = 2"]; 56 | file "src/main.ml" 57 | ["(Namespace.Foo.v + Namespace.Bar.v)"; 58 | "|> string_of_int |> print_endline"]; 59 | 60 | build_and_test 3 61 | end 62 | end; 63 | 64 | test "nested_namespace" begin fun () -> 65 | project begin fun () -> 66 | file "src/namespace/a/foo.ml" ["let v = 1"]; 67 | file "src/main.ml" 68 | ["Namespace.A.Foo.v |> string_of_int |> print_endline"]; 69 | 70 | build_and_test 1 71 | end 72 | end; 73 | 74 | test "submodules_hidden" begin fun () -> 75 | project begin fun () -> 76 | file "src/namespace/foo.ml" ["let v = 1"]; 77 | file "src/main.ml" ["Foo.v |> string_of_int |> print_endline"]; 78 | 79 | basic_build_system (); 80 | 81 | ocamlbuild ~fails_with:"Unbound module Foo" ["main.native"] 82 | end 83 | end; 84 | 85 | test "sibling_reference" begin fun () -> 86 | project begin fun () -> 87 | file "src/namespace/foo.ml" ["let v = 1"]; 88 | file "src/namespace/bar.ml" ["let v = Foo.v + 1"]; 89 | file "src/main.ml" ["Namespace.Bar.v |> string_of_int |> print_endline"]; 90 | 91 | build_and_test 2 92 | end 93 | end; 94 | 95 | test "cousin_reference" begin fun () -> 96 | project begin fun () -> 97 | file "src/namespace_1/foo.ml" ["let v = 1"]; 98 | file "src/namespace_2/foo.ml" ["let v = Namespace_1.Foo.v + 1"]; 99 | file "src/main.ml" 100 | ["Namespace_2.Foo.v |> string_of_int |> print_endline"]; 101 | 102 | build_and_test 2 103 | end 104 | end; 105 | 106 | test "cousin_reference_reversed" begin fun () -> 107 | project begin fun () -> 108 | file "src/namespace_1/foo.ml" ["let v = Namespace_2.Foo.v + 1"]; 109 | file "src/namespace_2/foo.ml" ["let v = 1"]; 110 | file "src/main.ml" 111 | ["Namespace_1.Foo.v |> string_of_int |> print_endline"]; 112 | 113 | build_and_test 2 114 | end 115 | end 116 | ] 117 | -------------------------------------------------------------------------------- /test/test_helpers.ml: -------------------------------------------------------------------------------- 1 | open OUnit2 2 | 3 | let _plugin_install_directory = "_plugin_findlib" 4 | let _plugin_install_directory_absolute = 5 | Filename.concat (Sys.getcwd ()) _plugin_install_directory 6 | 7 | let _project_install_directory = "_project_findlib" 8 | let _project_install_directory_absolute = 9 | Filename.concat (Sys.getcwd ()) _project_install_directory 10 | 11 | let _project_directory = "_project" 12 | let _depending_directory = "_depending" 13 | 14 | let _package_name = "test-library" 15 | 16 | let run command arguments = 17 | let open Process in 18 | let result = run command (Array.of_list arguments) in 19 | if result.Output.exit_status <> Exit.Exit 0 then 20 | let report = 21 | Printf.sprintf 22 | "Command failed (%s):\n%s %s\n\nSTDERR:\n\n%s\n\nSTDOUT:\n\n%s" 23 | (Exit.to_string result.Output.exit_status) 24 | command (String.concat " " arguments) 25 | (String.concat "\n" result.Output.stderr) 26 | (String.concat "\n" result.Output.stdout) 27 | in 28 | assert_failure report 29 | else 30 | String.concat "\n" result.Output.stdout 31 | 32 | let _run command arguments = run command arguments |> ignore 33 | 34 | let _fresh_dir name = 35 | _run "rm" ["-rf"; name]; 36 | _run "mkdir" ["-p"; name] 37 | 38 | let install_plugin () = 39 | _fresh_dir _plugin_install_directory; 40 | 41 | Unix.putenv "OCAMLFIND_DESTDIR" _plugin_install_directory_absolute; 42 | _run "make" ["-C"; ".."; "install"]; 43 | Unix.putenv "OCAMLFIND_DESTDIR" _project_install_directory_absolute; 44 | 45 | Printf.sprintf "%s:%s" 46 | _plugin_install_directory_absolute _project_install_directory_absolute 47 | |> Unix.putenv "OCAMLPATH" 48 | 49 | let _project directory f = 50 | _fresh_dir directory; 51 | 52 | let cwd = Sys.getcwd () in 53 | let project_directory = Filename.concat cwd directory in 54 | Sys.chdir project_directory; 55 | 56 | try 57 | f (); 58 | Sys.chdir cwd 59 | 60 | with e -> 61 | Sys.chdir cwd; 62 | raise e 63 | 64 | let project = _project _project_directory 65 | 66 | let depending = _project _depending_directory 67 | 68 | let file name lines = 69 | _run "mkdir" ["-p"; (Filename.dirname name)]; 70 | 71 | let channel = open_out name in 72 | try 73 | lines |> List.iter (fun s -> 74 | output_string channel s; output_char channel '\n'); 75 | close_out_noerr channel 76 | 77 | with e -> 78 | close_out_noerr channel; 79 | raise e 80 | 81 | let myocamlbuild_ml () = 82 | file "myocamlbuild.ml" 83 | ["let () = Ocamlbuild_plugin.dispatch Namespaces.handler"] 84 | 85 | let tags lines = 86 | file "_tags" ("<**/*>: include"::lines) 87 | 88 | let oasis_myocamlbuild_ml () = 89 | file "myocamlbuild.ml" 90 | ["(* OASIS_START *)"; 91 | "(* OASIS_STOP *)"; 92 | ""; 93 | "let () ="; 94 | " dispatch"; 95 | " (MyOCamlbuildBase.dispatch_combine"; 96 | " [MyOCamlbuildBase.dispatch_default conf package_default;"; 97 | " Namespaces.handler])"] 98 | 99 | let oasis_tags lines = 100 | file "_tags" 101 | (["# OASIS_START"; 102 | "# OASIS_STOP"; 103 | ""; 104 | "<**/*>: include"] @ lines) 105 | 106 | let oasis_file lines = 107 | file "_oasis" 108 | (["OASISFormat: 0.4"; 109 | "Name: namespaces-oasis-test"; 110 | "Version: 0.1"; 111 | "Synopsis: OASIS test"; 112 | "Authors: Anton Bachin"; 113 | "License: BSD-2-clause"; 114 | "OCamlVersion: >= 4.02"; 115 | "AlphaFeatures: ocamlbuild_more_args"; 116 | "XOCamlbuildPluginTags: package(namespaces)"; 117 | ""] @ lines) 118 | 119 | let ocamlbuild ?fails_with targets = 120 | let arguments = 121 | ["-use-ocamlfind"; "-plugin-tag"; "package(namespaces)"; "-cflags"; 122 | "-bin-annot"] @ targets 123 | in 124 | 125 | match fails_with with 126 | | None -> _run "ocamlbuild" arguments 127 | 128 | | Some message -> 129 | let open Process in 130 | let result = run "ocamlbuild" (Array.of_list arguments) in 131 | if result.Output.exit_status = Exit.Exit 0 then 132 | let report = 133 | Printf.sprintf "Command did not fail:\n%s %s" 134 | "ocamlbuild" (String.concat " " arguments) 135 | in 136 | assert_failure report 137 | else 138 | let stdout = String.concat "\n" result.Output.stdout in 139 | let regexp = Str.regexp_string message in 140 | try Str.search_forward regexp stdout 0 |> ignore 141 | with Not_found -> 142 | let report = Printf.sprintf 143 | "Command did not fail with expected message:\n%s %s\n\nSTDOUT:\n\n%s" 144 | "ocamlbuild" (String.concat " " arguments) 145 | stdout 146 | in 147 | assert_failure report 148 | 149 | let oasis () = 150 | _run "oasis" ["setup"]; 151 | _run "ocaml" ["setup.ml"; "-configure"]; 152 | _run "ocaml" ["setup.ml"; "-build"] 153 | 154 | let test name f = name >:: fun context -> f () 155 | 156 | let install_project = 157 | let whitespace = Str.regexp "[ \t\r\n]+" in 158 | 159 | fun () -> 160 | _fresh_dir _project_install_directory_absolute; 161 | 162 | let find_files extension = 163 | run "find" ["_build"; "-name"; "*." ^ extension] 164 | |> Str.split whitespace 165 | in 166 | 167 | let files = 168 | ["cma"; "cmxa"; "a"; "cmi"; "cmt"; "cmti"] 169 | |> List.map find_files 170 | |> List.flatten 171 | |> List.filter ((<>) "_build/myocamlbuild.cmi") 172 | in 173 | 174 | _run "ocamlfind" (["install"; _package_name; "META"] @ files) 175 | -------------------------------------------------------------------------------- /test/test_helpers.mli: -------------------------------------------------------------------------------- 1 | val install_plugin : unit -> unit 2 | 3 | val project : (unit -> unit) -> unit 4 | val depending : (unit -> unit) -> unit 5 | 6 | val file : string -> string list -> unit 7 | 8 | val myocamlbuild_ml : unit -> unit 9 | val tags : string list -> unit 10 | 11 | val oasis_myocamlbuild_ml : unit -> unit 12 | val oasis_tags : string list -> unit 13 | val oasis_file : string list -> unit 14 | 15 | val run : string -> string list -> string 16 | val ocamlbuild : ?fails_with:string -> string list -> unit 17 | val oasis : unit -> unit 18 | 19 | val install_project : unit -> unit 20 | 21 | val test : string -> (unit -> unit) -> OUnit2.test 22 | -------------------------------------------------------------------------------- /test/test_library.ml: -------------------------------------------------------------------------------- 1 | open OUnit2 2 | open Test_helpers 3 | 4 | let prepare_library () = 5 | project begin fun () -> 6 | file "src/foo.ml" ["let v = 1"]; 7 | file "src/optional/bar.ml" ["let v = Foo.v + 1"]; 8 | file "src/optional_bar.ml" ["include Optional.Bar"]; 9 | 10 | file "src/baz.ml" ["exit 1"]; 11 | file "src/optional/quux.ml" ["exit 1"]; 12 | 13 | tags 14 | [": namespace"; 15 | "\"src\": namespace_with_name(Test_library), namespace_lib(library)"; 16 | "\"src/optional\": namespace_lib(optional)"; 17 | "\"src/optional_bar.ml\": namespace_lib(optional)"]; 18 | myocamlbuild_ml (); 19 | 20 | ocamlbuild 21 | ["library.cma"; "library.cmxa"; "optional.cma"; "optional.cmxa"]; 22 | 23 | file "META" 24 | ["archive(byte) = \"library.cma\""; 25 | "archive(native) = \"library.cmxa\""; 26 | ""; 27 | "package \"optional\" ("; 28 | " requires = \"test-library\""; 29 | " archive(byte) = \"optional.cma\""; 30 | " archive(native) = \"optional.cmxa\""; 31 | ")"]; 32 | 33 | install_project () 34 | end 35 | 36 | let depend ?(optional = false) code expect = 37 | depending begin fun () -> 38 | let package = 39 | if optional then "test-library.optional" else "test-library" in 40 | 41 | file "src/main.ml" code; 42 | tags [": package(" ^ package ^ ")"]; 43 | ocamlbuild ["main.byte"; "main.native"]; 44 | 45 | let expect = Printf.sprintf "%i\n" expect in 46 | assert_equal (run "./main.byte" []) expect; 47 | assert_equal (run "./main.native" []) expect 48 | end 49 | 50 | let tests = "library" >::: [ 51 | test "basic" begin fun () -> 52 | prepare_library (); 53 | depend ["Test_library.Foo.v |> string_of_int |> print_endline"] 1 54 | end; 55 | 56 | test "optional" begin fun () -> 57 | prepare_library (); 58 | depend ~optional:true 59 | ["Test_library.Optional.Bar.v |> string_of_int |> print_endline"] 2 60 | end; 61 | 62 | test "optional_legacy" begin fun () -> 63 | prepare_library (); 64 | depend ~optional:true 65 | ["Test_library.Optional_bar.v |> string_of_int |> print_endline"] 2 66 | end 67 | ] 68 | -------------------------------------------------------------------------------- /test/test_main.ml: -------------------------------------------------------------------------------- 1 | open OUnit2 2 | 3 | let tests = "namespaces" >::: [ 4 | Test_basic.tests; 5 | Test_oasis.tests; 6 | Test_library.tests 7 | ] 8 | 9 | let () = 10 | Test_helpers.install_plugin (); 11 | run_test_tt_main tests 12 | -------------------------------------------------------------------------------- /test/test_oasis.ml: -------------------------------------------------------------------------------- 1 | open OUnit2 2 | open Test_helpers 3 | 4 | let tests = "oasis" >::: [ 5 | test "binary" begin fun () -> 6 | project begin fun () -> 7 | file "src/namespace/foo.ml" ["let v = 1"]; 8 | file "src/main.ml" ["Namespace.Foo.v |> string_of_int |> print_endline"]; 9 | 10 | oasis_myocamlbuild_ml (); 11 | oasis_tags [": namespace"]; 12 | 13 | oasis_file 14 | ["Executable main"; 15 | " Path: src"; 16 | " BuildTools: ocamlbuild"; 17 | " MainIs: main.ml"]; 18 | 19 | oasis (); 20 | 21 | assert_equal (run "./main.byte" []) "1\n" 22 | end 23 | end 24 | ] 25 | --------------------------------------------------------------------------------