C build tool of the 21st century
1open Types
2open Flags
3module Build = Build
4module Util = Util
5
6module Compiler_config = struct
7 type t = {
8 name : string;
9 ext : string list; [@default []]
10 command : string list option; [@default None]
11 link_type : string; [@key "link-type"] [@default "exe"]
12 has_runtime : bool; [@key "has-runtime"] [@default false]
13 parallel : bool; [@default true]
14 compile_flag_prefix : string option;
15 [@key "compile-flag-prefix"] [@default None]
16 link_flag_prefix : string option; [@key "link-flag-prefix"] [@default None]
17 }
18 [@@deriving yaml]
19
20 let named name =
21 {
22 name;
23 ext = [];
24 command = None;
25 link_type = "exe";
26 has_runtime = false;
27 parallel = true;
28 compile_flag_prefix = None;
29 link_flag_prefix = None;
30 }
31
32 let wrap_c_flags t flags =
33 if Option.is_none t.compile_flag_prefix && Option.is_none t.link_flag_prefix
34 then flags
35 else
36 let compile =
37 Option.fold ~none:flags.Flags.compile
38 ~some:(fun prefix ->
39 List.concat_map (fun x -> [ prefix; x ]) flags.compile)
40 t.compile_flag_prefix
41 in
42 let link =
43 Option.fold ~none:flags.link
44 ~some:(fun prefix ->
45 List.concat_map (fun x -> [ prefix; x ]) flags.link)
46 t.link_flag_prefix
47 in
48 Flags.v ~compile ~link ()
49
50 let compiler ?compilers t =
51 match t.command with
52 | Some cmd ->
53 Compiler.
54 {
55 name = t.name;
56 ext = String_set.of_list t.ext;
57 command =
58 (fun ~flags ~objects:_ ~output ->
59 List.concat_map
60 (fun x ->
61 if String.equal x "#output" then
62 [ Eio.Path.native_exn output.path ]
63 else if String.equal x "#flags" then flags.compile
64 else [ x ])
65 cmd);
66 transform_output = Fun.id;
67 parallel = t.parallel;
68 wrap_c_flags = wrap_c_flags t;
69 }
70 | None -> (
71 match Compiler.find_by_name ?compilers t.name with
72 | None -> invalid_arg ("unknown compiler: " ^ t.name)
73 | Some x -> x)
74
75 let linker ?linkers t =
76 match t.command with
77 | Some cmd ->
78 Linker.
79 {
80 name = t.name;
81 link_type = Linker.link_type_of_string t.link_type;
82 exts = String_set.of_list t.ext;
83 has_runtime = t.has_runtime;
84 wrap_c_flags = wrap_c_flags t;
85 command =
86 (fun ~flags ~objs ~output ->
87 List.concat_map
88 (fun x ->
89 if String.equal x "#objs" then
90 let objs =
91 List.map
92 (fun obj -> Eio.Path.native_exn obj.Object_file.path)
93 objs
94 in
95 objs
96 else if String.equal x "#output" then
97 [ Eio.Path.native_exn output ]
98 else if String.equal x "#flags" then flags.link
99 else [ x ])
100 cmd);
101 }
102 | None -> (
103 match
104 Linker.find_by_name (Option.value ~default:!Linker.all linkers) t.name
105 with
106 | None -> invalid_arg ("unknown linker: " ^ t.name)
107 | Some x -> x)
108end
109
110module Build_config = struct
111 module Lang_flags = struct
112 type t = {
113 lang : string;
114 compile : string list; [@default []]
115 link : string list; [@default []]
116 all : string list; [@default []]
117 }
118 [@@deriving yaml]
119 end
120
121 type t = {
122 name : string option; [@default None]
123 root : string option;
124 target : string option; [@default None]
125 compilers : string list; [@default []]
126 linker : string option; [@default None]
127 files : string list; [@default []]
128 headers : string list; [@default []]
129 ignore : string list; [@default []]
130 flags : Lang_flags.t list; [@default []]
131 script : string option; [@default None]
132 after : string option; [@default None]
133 depends_on : string list; [@default []] [@key "depends-on"]
134 disable_cache : bool; [@default false]
135 only_if : string option; [@default None] [@key "if"]
136 pkgconf : string list; [@default []] [@key "pkg"]
137 hidden : bool; [@default false]
138 parallel : bool; [@default true]
139 }
140 [@@deriving yaml]
141
142 let default =
143 {
144 name = None;
145 target = Some "a.out";
146 root = Some ".";
147 ignore = [];
148 compilers = [];
149 linker = None;
150 files = [];
151 headers = [];
152 script = None;
153 after = None;
154 depends_on = [];
155 flags = [];
156 disable_cache = false;
157 only_if = None;
158 pkgconf = [];
159 hidden = false;
160 parallel = true;
161 }
162end
163
164let default_compilers =
165 List.map (fun c -> Compiler_config.named c.Compiler.name) Compiler.default
166
167let default_linkers =
168 List.map (fun c -> Compiler_config.named c.Linker.name) Linker.default
169
170module Tools = struct
171 type t = {
172 compilers : Compiler_config.t list; [@default default_compilers]
173 linkers : Compiler_config.t list; [@default default_linkers]
174 }
175 [@@deriving yaml]
176
177 let default = { compilers = default_compilers; linkers = default_linkers }
178 let empty = { compilers = []; linkers = [] }
179end
180
181type t = {
182 build : Build_config.t list;
183 flags : Build_config.Lang_flags.t list; [@default []]
184 tools : Tools.t; [@default Tools.default]
185 files : string list; [@default []]
186 ignore : string list; [@default []]
187 pkgconf : string list; [@default []] [@key "pkg"]
188}
189[@@deriving yaml]
190
191let empty =
192 {
193 build = [];
194 flags = [];
195 tools = Tools.empty;
196 files = [];
197 ignore = [];
198 pkgconf = [];
199 }
200
201let read_file path =
202 try
203 let s = Eio.Path.load path in
204 let y = Yaml.of_string_exn s in
205 let st = Eio.Path.stat ~follow:true path in
206 Result.map (fun y -> (y, st.Eio.File.Stat.mtime)) @@ of_yaml y
207 with exn -> Error (`Msg (Printexc.to_string exn))
208
209(* Search for config file in current directory and parent directories *)
210let rec find_config_in_parents path =
211 let yml_path = Eio.Path.(path / "zenon.yml") in
212 let yaml_path = Eio.Path.(path / "zenon.yaml") in
213
214 if Eio.Path.is_file yml_path then Some yml_path
215 else if Eio.Path.is_file yaml_path then Some yaml_path
216 else
217 let native = Eio.Path.native_exn path in
218 (* Stop if we're at the root *)
219 if
220 String.equal native "/" || String.equal native "."
221 || String.equal native ""
222 then None
223 else
224 match Eio.Path.split path with
225 | None -> None
226 | Some (parent_path, _) ->
227 let parent_native = Eio.Path.native_exn parent_path in
228 if String.equal native parent_native then None
229 else find_config_in_parents parent_path
230
231let read_file_or_default path =
232 if Eio.Path.is_file path then read_file path
233 else if Eio.Path.is_directory path then
234 match find_config_in_parents path with
235 | Some config_path -> read_file config_path
236 | None -> Ok (empty, Unix.gettimeofday ())
237 else Ok (empty, Unix.gettimeofday ())
238
239let init ?mtime ~env ~log_level path t =
240 let () =
241 List.iter
242 (fun c -> Compiler.register @@ Compiler_config.compiler ~compilers:[] c)
243 t.tools.compilers
244 in
245 let () =
246 List.iter
247 (fun l -> Linker.register @@ Compiler_config.linker ~linkers:[] l)
248 t.tools.linkers
249 in
250
251 let gitignore_patterns =
252 Util.parse_gitignore Eio.Path.(path / ".gitignore")
253 in
254
255 let config_ignore_patterns = List.map Util.glob t.ignore in
256
257 List.filter_map
258 (fun config ->
259 let ok =
260 match config.Build_config.only_if with
261 | Some script -> (
262 try
263 Eio.Process.run env#process_mgr [ "sh"; "-c"; script ];
264 true
265 with _ -> false)
266 | None -> true
267 in
268 if not ok then
269 let () =
270 Util.log "! SKIP %s"
271 (Option.value
272 ~default:(Option.value ~default:"default" config.root)
273 config.name)
274 in
275 None
276 else
277 let compilers =
278 if List.is_empty config.compilers then !Compiler.all
279 else
280 List.filter_map
281 (fun name -> Compiler.find_by_name name)
282 config.compilers
283 in
284 let linker_name =
285 match config.Build_config.linker with
286 | Some linker -> Some linker (* User-specified linker *)
287 | None -> (
288 match config.target with
289 | Some target when Util.is_static_lib (Filename.basename target)
290 ->
291 Some "ar"
292 | Some target when Util.is_shared_lib (Filename.basename target)
293 ->
294 let has_cxx =
295 List.exists
296 (fun name -> name = "clang++" || name = "g++")
297 config.compilers
298 in
299 let linker_name =
300 if has_cxx then Some "clang++-shared"
301 else Some "clang-shared"
302 in
303 linker_name
304 | _ -> None)
305 in
306 let linker =
307 Option.map
308 (fun linker_name ->
309 Compiler_config.linker @@ Compiler_config.named linker_name)
310 linker_name
311 in
312 let compiler_flags =
313 let tbl = Hashtbl.create 8 in
314 let add_flags flags =
315 List.iter
316 (fun (f : Build_config.Lang_flags.t) ->
317 let lang = f.lang in
318 let compile = f.compile @ f.all in
319 let link = f.link @ f.all in
320 let existing =
321 Option.value (Hashtbl.find_opt tbl lang) ~default:(Flags.v ())
322 in
323 let new_flags =
324 Flags.v
325 ~compile:(existing.compile @ compile)
326 ~link:(existing.link @ link) ()
327 in
328 Hashtbl.replace tbl lang new_flags)
329 flags
330 in
331 add_flags t.flags;
332 add_flags config.flags;
333 List.of_seq (Hashtbl.to_seq tbl)
334 in
335 let source =
336 match config.root with None -> path | Some p -> Eio.Path.(path / p)
337 in
338 let output =
339 Option.map
340 (fun output ->
341 let normalized = Util.normalize_shared_lib_ext output in
342 Eio.Path.(env#fs / normalized))
343 config.target
344 in
345 let name =
346 match config.name with
347 | Some name -> name
348 | None -> (
349 match config.target with
350 | Some p -> Filename.basename p
351 | None -> "default")
352 in
353 let build_ignore_patterns = List.map Util.glob config.ignore in
354 let all_ignore =
355 gitignore_patterns @ config_ignore_patterns @ build_ignore_patterns
356 in
357 let build =
358 Build.v ~parallel:config.parallel ?script:config.script
359 ~pkgconf:(t.pkgconf @ config.pkgconf)
360 ?after:config.after ~depends_on:config.depends_on ?linker ~compilers
361 ~compiler_flags ?output ~source ~files:(t.files @ config.files)
362 ~headers:config.headers ~name ~ignore:all_ignore
363 ~hidden:config.hidden ~log_level ?mtime env
364 in
365 Some build)
366 t.build
367
368let load ~env ~log_level path =
369 let project_root =
370 if Eio.Path.is_directory path then path
371 else
372 match Eio.Path.split path with None -> assert false | Some (p, _) -> p
373 in
374 match read_file_or_default path with
375 | Ok (config, mtime) -> Ok (init ~mtime ~env ~log_level project_root config)
376 | Error e -> Error e