C build tool of the 21st century
at main 376 lines 12 kB view raw
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