(* * Copyright (c) 2016 - present Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. *) open! IStd module F = Format module L = Logging module Target = struct type t = {name: string; flavors: string list} let of_string target = match String.split target ~on:'#' with | [name; flavors_string] -> let flavors = String.split flavors_string ~on:',' in {name; flavors} | [name] -> {name; flavors= []} | _ -> L.(die ExternalError) "cannot parse target %s" target let to_string {name; flavors} = F.asprintf "%s#%a" name (Pp.comma_seq Pp.string) flavors let add_flavor_internal target flavor = if List.mem ~equal:String.equal target.flavors flavor then (* there's already an infer flavor associated to the target, do nothing *) target else {target with flavors= flavor :: target.flavors} let add_flavor ~extra_flavors target = let target = List.fold_left ~f:add_flavor_internal ~init:target extra_flavors in match (Config.buck_compilation_database, Config.analyzer) with | Some _, _ -> add_flavor_internal target "compilation-database" | None, CompileOnly -> target | None, (BiAbduction | CaptureOnly | Checkers | Linters) -> add_flavor_internal target "infer-capture-all" | None, Crashcontext -> L.(die UserError) "Analyzer %s is Java-only; not supported with Buck flavors" (Config.string_of_analyzer Config.analyzer) end let parse_target_string = let alias_target_regexp = Str.regexp "^[^/:]+\\(#.*\\)?$" in let pattern_target_regexp = Str.regexp "^[^/]*//\\(\\.\\.\\.\\|.*\\(:\\|/\\.\\.\\.\\)\\)$" in let normal_target_regexp = Str.regexp "^[^/]*//[^/].*:.+$" in let noname_target_regexp = Str.regexp "^[^/]*//.*$" in let parse_with_retry s ~retry = (* do not consider --buck-options as targets *) if String.equal s "" || Char.equal s.[0] '-' || Char.equal s.[0] '@' then `NotATarget s else if Str.string_match alias_target_regexp s 0 then `AliasTarget s else if Str.string_match pattern_target_regexp s 0 then `PatternTarget s else if Str.string_match normal_target_regexp s 0 then `NormalTarget s else if Str.string_match noname_target_regexp s 0 then let name = String.split s ~on:'/' |> List.last_exn in `NormalTarget (F.sprintf "%s:%s" s name) else retry s in fun s -> parse_with_retry s ~retry:(fun s -> parse_with_retry ("//" ^ s) ~retry:(fun s -> L.(die InternalError) "Do not know how to parse buck command line argument '%s'" s ) ) module Query = struct type expr = | Deps of {depth: int option; expr: expr} | Kind of {pattern: string; expr: expr} | Set of string list | Target of string | Union of expr list exception NotATarget let quote_if_needed = let no_quote_needed_regexp = Str.regexp "^[a-zA-Z0-9/:_*][a-zA-Z0-9/:.-_*]*$" in fun s -> if Str.string_match no_quote_needed_regexp s 0 then s else s |> Escape.escape_double_quotes |> F.sprintf "\"%s\"" let target string = Target (quote_if_needed string) let kind ~pattern expr = Kind {pattern= quote_if_needed pattern; expr} let deps ?depth expr = Deps {depth; expr} let set exprs = match List.rev_map exprs ~f:(function Target t -> t | _ -> raise NotATarget) with | targets -> Set targets | exception NotATarget -> Union exprs let rec pp fmt = function | Target s -> Pp.string fmt s | Kind {pattern; expr} -> F.fprintf fmt "kind(%s, %a)" pattern pp expr | Deps {depth= None; expr} -> F.fprintf fmt "deps(%a)" pp expr (* full depth *) | Deps {depth= Some depth; expr} -> F.fprintf fmt "deps(%a, %d)" pp expr depth | Set sl -> F.fprintf fmt "set(%a)" (Pp.seq Pp.string) sl | Union exprs -> Pp.seq ~sep:" + " pp fmt exprs let exec expr = let query = F.asprintf "%a" pp expr in let cmd = ["buck"; "query"; query] in let tmp_prefix = "buck_query_" in let debug = L.(debug Capture Medium) in Utils.with_process_lines ~debug ~cmd ~tmp_prefix ~f:Fn.id end let accepted_buck_commands = ["build"] let parameters_with_argument = ["--build-report"; "--config"; "-j"; "--num-threads"; "--out"; "-v"; "--verbose"] let accepted_buck_kinds_pattern = "(apple|cxx)_(binary|library|test)" let max_command_line_length = 50 let die_if_empty f = function [] -> f L.(die UserError) | l -> l let resolve_pattern_targets ~filter_kind ~dep_depth targets = targets |> List.rev_map ~f:Query.target |> Query.set |> (match dep_depth with None -> Fn.id | Some depth -> Query.deps ?depth) |> (if filter_kind then Query.kind ~pattern:accepted_buck_kinds_pattern else Fn.id) |> Query.exec |> die_if_empty (fun die -> die "*** buck query returned no targets.") let resolve_alias_targets aliases = let debug = L.(debug Capture Medium) in (* we could use buck query to resolve aliases but buck targets --resolve-alias is faster *) let cmd = "buck" :: "targets" :: "--resolve-alias" :: aliases in let tmp_prefix = "buck_targets_" in let on_result_lines = die_if_empty (fun die -> die "*** No alias found for: '%a'." (Pp.seq ~sep:"', '" Pp.string) aliases ) in Utils.with_process_lines ~debug ~cmd ~tmp_prefix ~f:on_result_lines type parsed_args = { rev_not_targets': string list ; normal_targets: string list ; alias_targets: string list ; pattern_targets: string list } let empty_parsed_args = {rev_not_targets'= []; normal_targets= []; alias_targets= []; pattern_targets= []} let split_buck_command buck_cmd = match buck_cmd with | command :: args when List.mem ~equal:String.equal accepted_buck_commands command -> (command, args) | _ -> L.(die UserError) "ERROR: cannot parse buck command `%a`. Expected %a." (Pp.seq Pp.string) buck_cmd (Pp.seq ~sep:" or " Pp.string) accepted_buck_commands (** Given a list of arguments return the extended list of arguments where the args in a file have been extracted *) let inline_argument_files buck_args = let expand_buck_arg buck_arg = if String.is_prefix ~prefix:"@" buck_arg then let file_name = String.chop_prefix_exn ~prefix:"@" buck_arg in if Sys.file_exists file_name <> `Yes then [buck_arg] (* Arguments that start with @ could mean something different than an arguments file in buck. *) else let expanded_args = try Utils.with_file_in file_name ~f:In_channel.input_lines with exn -> Logging.die UserError "Could not read from file '%s': %a@." file_name Exn.pp exn in expanded_args else [buck_arg] in List.concat_map ~f:expand_buck_arg buck_args type flavored_arguments = {command: string; rev_not_targets: string list; targets: string list} let add_flavors_to_buck_arguments ~filter_kind ~dep_depth ~extra_flavors original_buck_args = let expanded_buck_args = inline_argument_files original_buck_args in let command, args = split_buck_command expanded_buck_args in let rec parse_cmd_args parsed_args = function | [] -> parsed_args | param :: arg :: args when List.mem ~equal:String.equal parameters_with_argument param -> parse_cmd_args {parsed_args with rev_not_targets'= arg :: param :: parsed_args.rev_not_targets'} args | target :: args -> let parsed_args = match parse_target_string target with | `NotATarget s -> {parsed_args with rev_not_targets'= s :: parsed_args.rev_not_targets'} | `NormalTarget t -> {parsed_args with normal_targets= t :: parsed_args.normal_targets} | `AliasTarget a -> {parsed_args with alias_targets= a :: parsed_args.alias_targets} | `PatternTarget p -> {parsed_args with pattern_targets= p :: parsed_args.pattern_targets} in parse_cmd_args parsed_args args in let parsed_args = parse_cmd_args empty_parsed_args args in let targets = match (filter_kind, dep_depth, parsed_args) with | false, None, {pattern_targets= []; alias_targets= []; normal_targets} -> normal_targets | false, None, {pattern_targets= []; alias_targets; normal_targets} -> alias_targets |> resolve_alias_targets |> List.rev_append normal_targets | _, _, {pattern_targets; alias_targets; normal_targets} -> pattern_targets |> List.rev_append alias_targets |> List.rev_append normal_targets |> resolve_pattern_targets ~filter_kind ~dep_depth in match targets with | [] -> L.(die UserError) "ERROR: no targets found in Buck command `%a`." (Pp.seq Pp.string) original_buck_args | _ -> let rev_not_targets = parsed_args.rev_not_targets' in let targets = List.rev_map targets ~f:(fun t -> Target.(t |> of_string |> add_flavor ~extra_flavors |> to_string) ) in {command; rev_not_targets; targets} let rec exceed_length ~max = function | _ when max < 0 -> true | [] -> false | h :: t -> exceed_length ~max:(max - String.length h) t let store_args_in_file args = if exceed_length ~max:max_command_line_length args then let file = Filename.temp_file "buck_targets_" ".txt" in let write_args outc = Out_channel.output_string outc (String.concat ~sep:"\n" args) in let () = Utils.with_file_out file ~f:write_args in L.(debug Capture Quiet) "Buck targets options stored in file '%s'@\n" file ; [Printf.sprintf "@%s" file] else args let filter_compatible subcommand args = match subcommand with | `Targets -> let blacklist = "--keep-going" in List.filter args ~f:(fun arg -> not (String.equal blacklist arg)) | _ -> args