C build tool of the 21st century
1open Zenon_build
2open Common
3
4let build ?output ?(ignore = []) ~arg ~cflags ~ldflags ~path ~builds ~file ~run
5 ~pkg ~(linker : string option) ~log_level ~watch () =
6 let path = Unix.realpath path in
7 Eio_posix.run @@ fun env ->
8 let get_config_path () =
9 let eio_path = Eio.Path.(env#fs / Unix.realpath path) in
10 let dir_path =
11 if Eio.Path.is_directory eio_path then eio_path
12 else
13 match Eio.Path.split eio_path with Some (d, _) -> d | None -> eio_path
14 in
15 Config.find_config_in_parents dir_path
16 in
17
18 let get_file_mtimes build_configs =
19 let all_files = ref String_map.empty in
20 (* Add config file mtime *)
21 (match get_config_path () with
22 | Some config_path ->
23 let path_str = Eio.Path.native_exn config_path in
24 let mtime = (Eio.Path.stat ~follow:true config_path).mtime in
25 all_files := String_map.add path_str mtime !all_files
26 | None -> ());
27 List.iter
28 (fun (build : Build.t) ->
29 let sources = Build.locate_source_files build |> List.of_seq in
30 List.iter
31 (fun (sf : Source_file.t) ->
32 let path = Eio.Path.native_exn sf.path in
33 try
34 let mtime = (Eio.Path.stat ~follow:true sf.path).mtime in
35 all_files := String_map.add path mtime !all_files
36 with _ ->
37 (* File might have been deleted during the build process,
38 which is fine. *)
39 ())
40 sources)
41 build_configs;
42 !all_files
43 in
44
45 let do_build build_configs =
46 let ignore_patterns = List.map Util.glob ignore in
47 let targets, current_build_configs =
48 match build_configs with
49 | [] ->
50 ( [ "default" ],
51 [
52 Build.v env ~ignore:ignore_patterns ~pkgconf:pkg
53 ~flags:(Flags.v ())
54 ~source:Eio.Path.(env#fs / path)
55 ~files:file ~name:"default"
56 ?output:(Option.map (fun x -> Eio.Path.(env#cwd / x)) output)
57 ~log_level
58 ?linker:
59 (Option.map
60 (fun name -> Config.Compiler_config.(linker (named name)))
61 linker);
62 ] )
63 | configs -> (builds, configs)
64 in
65 let active_targets = filter_builds current_build_configs targets in
66 let build_map = make_build_map current_build_configs in
67 let builds_with_deps_set = builds_with_deps build_map active_targets in
68 let plan = Plan.v () in
69 (* Separate generic and language-specific flags *)
70 let generic_cflags, lang_cflags =
71 List.partition (fun (lang, _) -> lang = "all") cflags
72 in
73 let generic_ldflags, lang_ldflags =
74 List.partition (fun (lang, _) -> lang = "all") ldflags
75 in
76 let generic_cflags = List.map snd generic_cflags in
77 let generic_ldflags = List.map snd generic_ldflags in
78 let cflags_by_lang = Hashtbl.create 8 in
79 List.iter
80 (fun (lang, flag) ->
81 let flags =
82 try Hashtbl.find cflags_by_lang lang with Not_found -> []
83 in
84 Hashtbl.replace cflags_by_lang lang (flag :: flags))
85 lang_cflags;
86 let ldflags_by_lang = Hashtbl.create 8 in
87 List.iter
88 (fun (lang, flag) ->
89 let flags =
90 try Hashtbl.find ldflags_by_lang lang with Not_found -> []
91 in
92 Hashtbl.replace ldflags_by_lang lang (flag :: flags))
93 lang_ldflags;
94 let reset = not (List.is_empty file) in
95 List.iter
96 (fun build ->
97 if
98 String_set.is_empty builds_with_deps_set
99 || String_set.mem build.Build.name builds_with_deps_set
100 then (
101 let output =
102 match output with
103 | None -> build.Build.output
104 | Some output -> Some Eio.Path.(env#cwd / output)
105 in
106 (* Add generic flags to build.flags *)
107 Flags.add_compile_flags build.Build.flags generic_cflags;
108 Flags.add_link_flags build.Build.flags generic_ldflags;
109 (* Add language-specific flags to build.compiler_flags *)
110 Hashtbl.iter
111 (fun lang flags ->
112 let existing_flags =
113 try Hashtbl.find build.Build.compiler_flags lang
114 with Not_found ->
115 let new_flags = Flags.v () in
116 Hashtbl.add build.Build.compiler_flags lang new_flags;
117 new_flags
118 in
119 Flags.add_compile_flags existing_flags (List.rev flags))
120 cflags_by_lang;
121 Hashtbl.iter
122 (fun lang flags ->
123 let existing_flags =
124 try Hashtbl.find build.Build.compiler_flags lang
125 with Not_found ->
126 let new_flags = Flags.v () in
127 Hashtbl.add build.Build.compiler_flags lang new_flags;
128 new_flags
129 in
130 Flags.add_link_flags existing_flags (List.rev flags))
131 ldflags_by_lang;
132 let () = Build.add_source_files ~reset build file in
133 Plan.build plan
134 {
135 build with
136 log_level;
137 pkgconf = build.Build.pkgconf @ pkg;
138 ignore = build.Build.ignore @ ignore_patterns;
139 output;
140 linker =
141 (match linker with
142 | Some l -> Some Config.Compiler_config.(linker @@ named l)
143 | None -> build.Build.linker);
144 }))
145 current_build_configs;
146 Plan.run_all ~execute:run ~args:arg ~log_level plan
147 (List.filter
148 (fun b -> String_set.mem b.Build.name builds_with_deps_set)
149 current_build_configs);
150 get_file_mtimes current_build_configs
151 in
152
153 let initial_build_configs = load_config ~log_level ~builds env path in
154
155 (* If the user specified targets, check that they are all valid *)
156 if not (List.is_empty builds) then (
157 let found_build_names =
158 List.map (fun (b : Build.t) -> b.name) initial_build_configs
159 |> String_set.of_list
160 in
161 let requested_build_names = String_set.of_list builds in
162 let missing = String_set.diff requested_build_names found_build_names in
163 if not (String_set.is_empty missing) then
164 Fmt.failwith "Unknown build targets: %s"
165 (String.concat ", " (String_set.to_list missing)));
166
167 let initial_mtimes = do_build initial_build_configs in
168
169 if watch then
170 let rec loop mtimes =
171 Eio.Time.sleep env#clock 2.0;
172 let new_build_configs = load_config ~log_level ~builds env path in
173 let new_mtimes = get_file_mtimes new_build_configs in
174 if not (String_map.equal Float.equal mtimes new_mtimes) then (
175 Util.log
176 ~verbose:(Util.is_verbose log_level)
177 "+ Change detected, rebuilding...";
178 let updated_mtimes = do_build new_build_configs in
179 loop updated_mtimes)
180 else loop mtimes
181 in
182 loop initial_mtimes