File: lib.ml

package info (click to toggle)
opam 2.5.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 7,500 kB
  • sloc: ml: 61,414; sh: 2,963; ansic: 1,147; makefile: 479; sed: 6; csh: 1
file content (263 lines) | stat: -rw-r--r-- 8,991 bytes parent folder | download
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
(**************************************************************************)
(*                                                                        *)
(*    Copyright 2021 David Allsopp Ltd.                                   *)
(*                                                                        *)
(*  All rights reserved. This file is distributed under the terms of the  *)
(*  GNU Lesser General Public License version 2.1, with the special       *)
(*  exception on linking described in the file LICENSE.                   *)
(*                                                                        *)
(**************************************************************************)

module Option = struct
  include Option

  let map_default f dft = function
  | Some x -> f x
  | None -> dft
end

module List = struct
  include List

  let rec drop_while f = function
  | [] -> []
  | (h::tl) as l -> if f h then drop_while f tl else l

  let rec take_until f = function
  | [] -> []
  | h::tl -> if f h then h::take_until f tl else []
end

let fprintf = Printf.fprintf

type _ platform = MacOS : os_only platform
                | Linux : os_only platform
                | Windows : os_only platform
                | Specific : os_only platform * string -> with_runner_version platform
and os_only = Os_only
and with_runner_version = With_runner_version

let rec name_of_platform (type a) (platform : a platform) =
  match platform with
  | MacOS -> "macOS"
  | Linux -> "Linux"
  | Windows -> "Windows"
  | Specific (platform, _) -> name_of_platform platform

let os_of_platform (type a) (platform : a platform) =
  match platform with
  | MacOS
  | Linux
  | Windows as platform -> platform
  | Specific (platform, _) -> platform

let rec os_name_of_platform (type a) (platform : a platform) =
  match platform with
  | MacOS -> "macos"
  | Linux -> "ubuntu"
  | Windows -> "windows"
  | Specific (platform, _) -> os_name_of_platform platform

let emit_map ~oc ?(indent=4) name map =
  let indent = String.make indent ' ' in
  fprintf oc "%s%s:\n" indent name;
  let emit_binding (key, value) =
    let value = if value = "" then "" else " " ^ value in
    fprintf oc "%s  %s:%s\n" indent key value
  in
  List.iter emit_binding map

(* Prints an `env:` block *)
let emit_env ?indent = emit_map ?indent "env"

(* Prints an `outputs:` block *)
let emit_outputs ?indent = emit_map ?indent "outputs"

(* Prints a string list field (not printed if value = []) *)
let emit_yaml_list ?(indent=4) ?(force_list=false) ~oc name value =
  if value <> [] then
    let indent = String.make indent ' ' in
    match value with
    | [elt] when not force_list ->
        fprintf oc "%s%s: %s\n" indent name elt
    | elts ->
        fprintf oc "%s%s: [ %s ]\n" indent name (String.concat ", " elts)

let print_include ~oc values =
  fprintf oc "          - %s\n" (String.concat "\n            " (List.map (fun (key, value) -> Printf.sprintf "%s: %s" key value) values))

(* Prints a strategy block for a job *)
let emit_strategy ~oc (fail_fast, matrix, includes) =
  output_string oc
{|    strategy:
      matrix:
|};
  let print_matrix (key, elts) = emit_yaml_list ~oc ~indent:8 ~force_list:true key elts in
  List.iter print_matrix matrix;
  if includes <> [] then begin
    output_string oc "        include:\n";
    List.iter (print_include ~oc) includes
  end;
  fprintf oc "      fail-fast: %b\n" fail_fast

type 'a runs_on = Runner of 'a platform list | Matrix of string

type job = ..
let jobs = Hashtbl.create 15

let find_need need = Hashtbl.find jobs need

let emit_runs_on ~oc runs_on =
  let runner_of_platform (type a) (platform : a platform) =
    match platform with
    | Windows -> "windows-2022"
    | MacOS
    | Linux as platform -> os_name_of_platform platform ^ "-latest"
    | Specific (platform, version) -> os_name_of_platform platform ^ "-" ^ version
  in
  let value =
    match runs_on with
    | Runner [platform] ->
        runner_of_platform platform
    | Runner platforms ->
        Printf.sprintf "[%s]" (String.concat ", " (List.map runner_of_platform platforms))
    | Matrix entry -> entry
  in
  fprintf oc "    runs-on: %s\n" value

(* Continuation for a job. Steps are specified as continuations as for
   the jobs within the workflow, terminated with {!end_job}. *)
let job ~oc ~workflow ?shell ?section ?(needs = []) ?matrix ?env ?outputs ~runs_on name f =
  let module M = struct type job += Key end in
  Hashtbl.add jobs M.Key name;
  output_char oc '\n';
  let emit_section =
    fprintf oc
{|####
# %s
####
|} in
  Option.iter emit_section section;
  fprintf oc "  %s:\n" name;
  emit_runs_on ~oc runs_on;
  emit_yaml_list ~oc "needs" (List.map find_need needs);
  Option.iter (emit_strategy ~oc) matrix;
  Option.iter (emit_env ~oc) env;
  Option.iter (emit_outputs ~oc) outputs;
  Option.iter (fprintf oc "    defaults:\n      run:\n        shell: %s\n") shell;
  output_string oc "    steps:\n";
  f ~oc ~workflow ~job:M.Key

let end_job ~oc ~workflow ~job f = f job ~oc ~workflow

(* Left-associative version of (@@) which allows combining jobs and steps
   without parentheses. *)
let (++) = (@@)

type condition =
| And of condition list
| Or of condition list
| Predicate of bool * variable
and variable =
| Runner of os_only platform
| CacheMiss of string
| EndsWith of string * string
| Contains of string * string
| Compare of string * string

let all_predicates =
  List.for_all (function Predicate(_, _) -> true | _ -> false)

let emit_condition ~oc ~indent =
  let indent = String.make indent ' ' in
  let rec to_yaml condition =
    match condition with
    | And predicates when all_predicates predicates ->
        String.concat " && " (List.map recurse predicates)
    | Or predicates when all_predicates predicates ->
        String.concat " || " (List.map recurse predicates)
    | cond -> recurse cond
  and recurse = function
    | And tests ->
        String.concat " && " (List.map recurse tests)
        |> Printf.sprintf "(%s)"
    | Or tests ->
        String.concat " || " (List.map recurse tests)
        |> Printf.sprintf "(%s)"
    | Predicate (op, EndsWith(variable, constant)) ->
        let op = if op then "" else " == false" in
        Printf.sprintf "endsWith(%s, '%s')%s" variable constant op
    | Predicate (op, Contains(variable, constant)) ->
        let op = if op then "" else " == false" in
        Printf.sprintf "contains(%s, '%s')%s" variable constant op
    | Predicate (op, Compare(variable, constant)) ->
        let op = if op then '=' else '!' in
        Printf.sprintf "%s %c= '%s'" variable op constant
    | Predicate (op, Runner platform) ->
        let op = if op then '=' else '!' in
        Printf.sprintf "runner.os %c= '%s'" op (name_of_platform platform)
    | Predicate (op, CacheMiss id) ->
        let op = if op then '!' else '=' in
        Printf.sprintf "steps.%s.outputs.cache-hit %c= 'true'" id op
  in
  let convert cond = fprintf oc "%sif: %s\n" indent (to_yaml cond) in
  Option.iter convert

let run name ?id ?cond ?shell ?env run ~oc ~workflow ~job f =
  fprintf oc "    - name: %s\n" name;
  Option.iter (emit_env ~indent:6 ~oc) env;
  Option.iter (fprintf oc "      id: %s\n") id;
  emit_condition ~oc ~indent:6 cond;
  Option.iter (fprintf oc "      shell: %s\n") shell;
  begin match run with
  | [command] ->
      fprintf oc "      run: %s\n" command
  | commands ->
      fprintf oc "      run: |\n        %s\n" (String.concat "\n        " commands)
  end;
  f ~oc ~workflow ~job

type with_entry =
| Literal of string list
| Expression of string

let yaml_of_with_entry = function
| Literal [entry] -> entry
| Literal entries -> "|\n          " ^ String.concat "\n          " entries
| Expression expr -> Printf.sprintf "${{ %s }}" expr

let uses name ?id ?cond ?(continue_on_error=false) ?(withs=[]) action ~oc ~workflow ~job f =
  fprintf oc "    - name: %s\n" name;
  Option.iter (fprintf oc "      id: %s\n") id;
  emit_condition ~oc ~indent:6 cond;
  fprintf oc "      uses: %s\n" action;
  if continue_on_error then
    output_string oc "      continue-on-error: true\n";
  if withs <> [] then begin
    fprintf oc "      with:\n";
    List.iter (fun (key, value) -> fprintf oc "        %s: %s\n" key (yaml_of_with_entry value)) withs
  end;
  f ~oc ~workflow ~job

let checkout ?cond () =
  uses "Checkout tree" ?id:None ?cond "actions/checkout@v5"

let skip_step ~oc ~workflow ~job f = f ~oc ~workflow ~job

(* Appends `|| exit /b 1` to a series of cmd commands *)
let run_or_fail = List.map (Fun.flip (^) " || exit /b 1")

let rec host_of_platform (type a) (platform : a platform) =
  match platform with
  | Windows -> "${{ matrix.host }}"
  | Linux -> "x86_64-pc-linux-gnu"
  | MacOS -> "x86_64-apple-darwin"
  | Specific (platform, _) -> host_of_platform platform

let gen_on op platform target step =
  if op target platform then
    step
  else
    skip_step
let only_on platform = gen_on (=) platform
let not_on platform = gen_on (<>) platform