package b0

  1. Overview
  2. Docs
Software construction and deployment kit

Install

dune-project
 Dependency

Authors

Maintainers

Sources

b0-0.0.6.tbz
sha512=e9aa779e66c08fc763019f16d4706f465d16c05d6400b58fbd0313317ef33ddea51952e2b058db28e65f7ddb7012f328c8bf02d8f1da17bb543348541a2587f0

doc/src/b0.kit/b0_show_url.ml.html

Source file b0_show_url.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
(*---------------------------------------------------------------------------
   Copyright (c) 2023 The b0 programmers. All rights reserved.
   SPDX-License-Identifier: ISC
  ---------------------------------------------------------------------------*)

let () = B0_scope.open_lib ~module':__MODULE__ "show-url"

open B0_std
open Result.Syntax

let url_of_path _env path =
  let* exists = Os.Path.exists path in (* FIXME remote build the build env *)
  if not exists
  then Fmt.error "%a: Not such path" Fpath.pp path
  else Ok (Fmt.str "file://%s" (Fpath.to_string path))

(* URLs *)

type url =
[ `Url of B0_url.t
| `In of B0_env.dir * Fpath.t
| `Fun of string * (B0_env.t -> B0_unit.t -> (B0_url.t, string) result) ]

let pp_url ppf = function
| `Url u -> Fmt.pf ppf "URL %s" u
| `In (dir, p) -> Fmt.pf ppf "%a in %a" Fpath.pp p B0_env.pp_dir dir
| `Fun (doc, _) -> Fmt.pf ppf "<fun> %s" doc

let url : url B0_meta.key =
  let doc = "The default URL to show." in
  let default = `In (`Unit_dir, Fpath.v ".") in
  B0_meta.Key.make "url" ~default ~doc ~pp_value:pp_url

let get_url env unit = match B0_unit.find_or_default_meta url unit with
| `Url url -> Ok url
| `In (`Unit_dir, p) ->
    let dir = B0_env.unit_dir env unit in
    let p = if Fpath.is_current_dir p then dir else Fpath.(dir // p) in
    url_of_path env (B0_env.in_unit_dir env unit p)
| `In (dir, p) -> url_of_path env (B0_env.in_dir env dir p)
| `Fun (_, f) -> f env unit

(* Server mode *)

let listen_args =
  let doc = "The command line arguments used to listen on an authority." in
  let default = fun ~authority -> Cmd.(arg "--listen" % authority) in
  let pp_value ppf _ = Fmt.pf ppf "<fun>, default is --listen AUTHORITY" in
  B0_meta.Key.make "listen-args" ~doc ~default ~pp_value:pp_value

let timeout_s =
  let doc =
    "Maximal number of seconds to wait for the server to be connectable \
     before showing the URL."
  in
  B0_meta.Key.make "timeout-s" ~doc ~default:1 ~pp_value:Fmt.int

let make_server_cmd cmd args ~listen_args =
  (* Inserts ~listen_args at the end of args or before -- *)
  let prev, rest =
    let rec loop prev = function
    | "--" :: rest as l -> prev, l
    | [] -> prev, []
    | arg :: args -> loop (arg :: prev) args
    in
    loop [] args
  in
  let args = List.rev_append prev (Cmd.to_list listen_args) @ rest in
  Cmd.(cmd %% of_list Fun.id args)

let endpoint_of_url_and_authority url authority = match B0_url.scheme url with
| None -> Fmt.error "URL %s: no scheme found" url
| Some "http" -> Os.Socket.Endpoint.of_string ~default_port:80 authority
| Some "https" -> Os.Socket.Endpoint.of_string ~default_port:443 authority
| Some scheme ->
    (Log.warn @@ fun m ->
     m "Unknown scheme %a using 80 as the default port" Fmt.code scheme);
    Os.Socket.Endpoint.of_string ~default_port:80 authority

let find_server_mode_unit = function
| [] -> Ok None
| entity :: args ->
    let keep = B0_unit.tool_is_user_accessible in
    match B0_unit.get_or_suggest_tool ~keep entity with
    | Ok us ->
        (* FIXME let u = check_tool_ambiguities name us in *)
        Ok (Some (List.hd us, args))
    | Error tool_suggs ->
        let u = B0_unit.get_or_suggest entity in
        match u with
        | Ok u -> Ok (Some (u, args))
        | Error us ->
            let tname u = Option.get (B0_unit.find_meta B0_unit.tool_name u) in
            let ts = List.rev_map tname tool_suggs in
            let us = List.rev_map B0_unit.name us in
            let set = String.Set.of_list (List.concat [ts; us]) in
            let suggs = String.Set.elements set in
            let hint = Fmt.did_you_mean in
            let nothing_to ppf v =
              Fmt.pf ppf "Nothing to execute for %a." Fmt.code v
            in
            let pp ppf (v, hints) = match hints with
            | [] -> nothing_to ppf v
            | hints -> Fmt.pf ppf "%a@ %a" nothing_to v (hint Fmt.code) hints
            in
            Fmt.error "@[%a@]" pp (entity, suggs)

let server_mode env timeout_cli no_exec ~url args =
  match B0_url.authority url with
  | None -> Fmt.error "Could not extract authority from %s" url
  | Some authority ->
      let* endpoint = endpoint_of_url_and_authority url authority in
      let* unit = find_server_mode_unit args in
      match unit with
      | None ->
          if not no_exec then
            (Log.warn @@ fun m ->
             m "@[<v>No tool specified but trying to connect to %s@,\
                Use option %a to suppress this warning.@]"
               authority Fmt.code "--no-exec");
          let timeout = match timeout_cli with
          | Some timeout -> timeout
          | None -> B0_meta.Key.get_default timeout_s
          in
          Ok (`Show_url_no_exec (endpoint, timeout, url))
      | Some (unit, args) ->
          (* XXX at somepoint we should be able to invoke the same
             logic as [B0_cmd_build.find_store_and_execution]. *)
          (* XXX We might actually want to look into actions aswell.
             See the todo.mld for more details.
             FIXME This is quite messy we need to clarify executable
             units ideally we should be using B0_unit.find_exec *)
          (* FIXME this should execute according to the build unit execution
             protocol. *)
          let* cmd = Result.map Cmd.path (B0_env.unit_exe_file env unit) in
          let timeout = match timeout_cli with
          | Some timeout -> timeout
          | None -> B0_unit.find_or_default_meta timeout_s unit
          in
          let listen_args =
            (B0_unit.find_or_default_meta listen_args unit) ~authority
          in
          let cmd = make_server_cmd cmd args ~listen_args in
          let* cwd = B0_unit.Action.get_cwd env unit in
          let* env' = B0_unit.Action.get_env env unit in
          let env' = Os.Env.to_assignments env' in
          Ok (`Show_url_server (endpoint, timeout, url, cmd, cwd, env'))

(* Unit .show-url.url mode *)

let parse_unit_specs args =
  let parse_arg arg = match String.split_first ~sep:":" arg with
  | None -> arg, (arg, None)
  | Some (uname, path) -> uname, (arg, Some path)
  in
  List.map parse_arg args

let unit_mode env args =
  let specs = parse_unit_specs args in
  let unit_names, paths = List.split specs in
  let* units = B0_unit.get_list_or_hint ~all_if_empty:false unit_names in
  let specs = List.combine units paths in
  let make_url (unit, (arg, p)) =
    Result.error_to_failure @@
    Result.map_error (fun e -> Fmt.str "%a: %s" Fmt.code arg e) @@
    let unit_dir = B0_build.unit_dir (B0_env.build env) unit in
    match p with
    | Some p -> url_of_path env Fpath.(unit_dir // v p)
    | None -> get_url env unit
  in
  try Ok (`Show_unit_urls (List.map make_url specs)) with Failure e -> Error e

(* Action *)

let first_is_url args =
  if args = [] then false else String.includes ~affix:"://" (List.hd args)

let find_mode env timeout noexec args =
  if first_is_url args
  then server_mode env timeout noexec ~url:(List.hd args) (List.tl args)
  else unit_mode env args

let dyn_units ~args =
  (* XXX This is all very hackish we need a better mecanism.
     Actions should be able to requests units after they parsed their cli. *)
  let args = Cmd.to_list args in
  let is_opt = String.starts_with ~prefix:"-" in
  let args = List.filter (Fun.negate is_opt) args in
  Log.if_error ~use:[] @@
  let unit_mode_units args =
    let units = List.map fst (parse_unit_specs args) in
    B0_unit.get_list_or_hint ~all_if_empty:false units
  in
  let server_mode_unit args =
    let* unit = find_server_mode_unit (List.tl args) in
    match unit with
    | None -> Ok [] | Some (u, _) -> Ok [u]
  in
  if first_is_url args
  then server_mode_unit args
  else unit_mode_units args

let show_url
    ~env ~browser ~background ~prefix ~timeout ~dry_run ~no_exec ~args
  =
  let secs timeout = Mtime.Span.(timeout * s) in
  Log.if_error ~use:Os.Exit.some_error @@
  let search = B0_env.get_cmd env in
  let* browser = B0_web_browser.find ~search ?cmd:browser () in
  let show url = B0_web_browser.show ~background ~prefix browser url in
  let* mode = find_mode env timeout no_exec args in
  match mode with
  | `Show_url_no_exec (endpoint, timeout, url) ->
      if dry_run
      then (Log.stdout (fun m -> m "%s" url); Ok Os.Exit.ok) else
      let timeout = secs timeout in
      let* () = Os.Socket.Endpoint.wait_connectable' ~timeout endpoint in
      let* () = show url in
      Ok Os.Exit.ok
  | `Show_url_server (endpoint, timeout, url, cmd, cwd, env) ->
      if dry_run
      then (Log.stdout (fun m -> m "%a" Cmd.pp cmd); Ok Os.Exit.ok) else
      let* server = Os.Cmd.spawn ~cwd ~env cmd in
      let* () =
        let timeout = secs timeout in
        Os.Socket.Endpoint.wait_connectable' ~timeout endpoint
      in
      let* () = show url in
      let* st = Os.Cmd.spawn_wait_status server in
      let code = match st with `Exited c -> c | `Signaled c -> 128 + c in
      Ok (Os.Exit.code code)
  | `Show_unit_urls urls ->
      let* () =
        if dry_run
        then (Log.stdout (fun m -> m "@[<v>%a@]" Fmt.(list string) urls); Ok ())
        else List.iter_stop_on_error show urls
      in
      Ok Os.Exit.ok

(* .show-url unit  *)

let unit =
  let doc = "Open and reload file or server URLs in browsers" in
  B0_unit.of_cmdliner_cmd "" ~dyn_units ~doc @@ fun env unit ->
  let open Cmdliner in
  let open Cmdliner.Term.Syntax in
  let man =
    [ `S Manpage.s_synopsis;
      `P "$(cmd) [$(i,OPTION)]… $(i,UNIT[:PATH])…";
      `Noblank;
      `P "$(cmd) [$(i,OPTION)]… $(i,URL) $(b,--) $(i,ENTITY) [$(i,ARG)]…";
      `S Manpage.s_description;
      `P "$(cmd) opens or reloads URLs of files in units or requested from \
          built servers.";
      `P "In the first mode of operation for each of the given \
          $(i,UNIT[:PATH]) argument, the $(i,UNIT) is built, $(i,PATH) \
          is made absolute in $(i,UNIT)'s build directory and turned \
          into a $(b,file://) URL. If $(i,PATH) is unspecified the value \
          specified in the unit's $(b,B0_show_url.url) key is used; \
          defaults to the unit's build directory. \
          Consult $(b,b0 get .show-url.url) $(i,UNIT) for \
          the concrete value.";
      `P "In the second mode of operation the given $(i,ENTITY) is built,
          this can be a tool name or an executable UNIT name (TODO eventually
          we should be able to simply have ACTION here, like for build)
          A hostname and port is derived from the authority of $(i,URL) and \
          transformed into command line arguments via the function specified \
          in $(i,ENTITY)'s $(b,B0_show_url.listen_args) key. These argument \
          are added after $(i,ARG)… or before a $(b,--) present in them. \
          The tool is then executed with the arguments, and the environment
          $(b,B0_unit.exec_env) and $(cmd) \
          waits for the port to become connectable \
          (this may result in churn in your tool logs) before finally \
          showing the given $(i,URL) in your browser.";
      `Blocks B0_web_browser.man_best_effort_reload;
      `S Manpage.s_see_also;
      `P "Consult $(b,odig doc b0) for the $(b,B0_show_url) \
          module documentation."]
  in
  Cmd.make (Cmd.info (B0_unit.name unit) ~doc ~man) @@
  let+ args =
    let doc =
      "This is either a list of $(i,UNIT[:PATH]) arguments or an $(i,URL) \
       followed by a tool invocation. The mode of operation is \
       discriminated by looking up for $(b,://) \
       in the first argument to catch an URL."
    in
    Arg.(non_empty & pos_all string [] & info [] ~doc ~docv:"ARGS")
  and+ timeout =
    let doc = "Maximal number of seconds to wait for the port to \
               become connectable. Default is defined by \
               $(b,b0 get .show-url.timeout) $(i,TOOL)."
    in
    let docv = "SECS" in
    Arg.(value & opt (some int) None & info ["t"; "timeout"] ~doc ~docv)
  and+ dry_run =
    let doc =
      "Build but do not show the URLs. Print them or print the tool \
       invocation."
    in
    Arg.(value & flag & info ["dry-run"] ~doc)
  and+ no_exec =
    let doc = "Do not invoke a tool, output the URL on $(b,stdout)." in
    Arg.(value & flag & info ["n"; "no-exec"] ~doc)
  and+ browser = B0_web_browser.browser ()
  and+ background = B0_web_browser.background ()
  and+ prefix = B0_web_browser.prefix ~default:true () in
  show_url ~env ~browser ~background ~prefix ~timeout ~dry_run ~no_exec ~args

(* Unit action *)

let action =
  B0_unit.Action.func ~doc:"show-url" @@ fun env unit ~args ->
  let* url = get_url env unit in
  let args = if Cmd.is_empty args then Cmd.arg "--prefix" else args in
  Ok (Os.Exit.execv Cmd.(tool "show-url" % url %% args))

let () = B0_scope.close ()