Source file skeleton.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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
(**************************************************************************)
(*                                                                        *)
(*    Copyright 2020 OCamlPro & Origin Labs                               *)
(*                                                                        *)
(*  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.                   *)
(*                                                                        *)
(**************************************************************************)

open EzCompat
open Types
open EzFile.OP

let default_flags =
    { flag_file = "";
      flag_create = false;
      flag_record = true;
      flag_skips = [];
      flag_skip = false ;
      flag_skipper = ref [];
      flag_subst = true ;
      flag_perm = 0;
    }

let bracket flags eval_cond =
  let bracket flags ( (), p ) s =
    match EzString.split s ':' with
    (* set the name of the file *)
    | [ "file"; v ] ->
        flags.flag_file <- v;
        ""
    (* create only once *)
    | [ "create" ] ->
        flags.flag_create <- true;
        ""
    (* skip with this tag *)
    | [ "skip"; v ] ->
        flags.flag_skips <- v :: flags.flag_skips;
        ""
    (* skip always *)
    | [ "skip" ] ->
        flags.flag_skip <- true;
        ""
    (* do not record in .git *)
    | [ "no-record" ] ->
        flags.flag_record <- false;
        ""
    | [ "perm" ; v ] ->
        flags.flag_perm <- int_of_string ( "0o" ^ v );
        ""
    | "if" :: cond ->
        flags.flag_skipper := ( not ( eval_cond p cond ) ) :: (!)
                                flags.flag_skipper;
        ""
    | [ "else" ] ->
        flags.flag_skipper := ( match (!) flags.flag_skipper with
            | cond :: tail -> ( not cond ) :: tail
            | [] -> failwith "else without if");
        ""
    | "elif" :: cond ->
        flags.flag_skipper := ( match (!) flags.flag_skipper with
            | _cond :: tail -> ( not (eval_cond p cond) ) :: tail
            | [] -> failwith "elif without if");
        ""
    | [ "fi" | "endif" ] ->
        flags.flag_skipper := ( match (!) flags.flag_skipper with
            | _ :: tail -> tail
            | [] -> failwith "fi without if");
        ""
    | _ ->
        Printf.eprintf "Warning: unknown flag %S\n%!" s;
        ""
  in
  bracket flags

let flags_encoding =
  EzToml.encoding
    ~to_toml:(fun _ -> assert false)
    ~of_toml:(fun ~key v ->
        let table = EzToml.expect_table ~key ~name:"flags" v in
        let flags = { default_flags with flag_file = "" } in
        EzToml.iter
          (fun k v ->
             let key = key @ [ k ] in
             match k with
             | "file" ->
                 flags.flag_file <- EzToml.expect_string ~key v
             | "create" ->
                 flags.flag_create <- EzToml.expect_bool ~key v
             | "record" ->
                 flags.flag_record <- EzToml.expect_bool ~key v
             | "skips" ->
                 flags.flag_skips <- EzToml.expect_string_list ~key v
             | "skip" ->
                 flags.flag_skip <- EzToml.expect_bool ~key v
             | "subst" ->
                 flags.flag_subst <- EzToml.expect_bool ~key v
             | "perm" ->
                 flags.flag_perm <-
                   int_of_string ( "0o" ^ EzToml.expect_string ~key v)
             | _ ->
                 Printf.eprintf "Warning: discarding flags field %S\n%!"
                   (EzToml.key2str key)
          )
          table;
        flags
      )

let load_skeleton ~drom ~dir ~toml ~kind =
  let table =
    match EzToml.from_file toml with
    | `Ok table -> table
    | `Error _ -> Error.raise "Could not parse skeleton file %S" toml
  in

  let name = try
      EzToml.get_string table [ "skeleton"; "name" ]
    with Not_found ->
      failwith "load_skeleton: wrong or missing key skeleton.name"
  in
  let skeleton_inherits =
    EzToml.get_string_option table [ "skeleton"; "inherits" ]
  in
  let skeleton_toml =
    let basename =
      if kind = "project" then "drom.toml" else
      if kind = "package" then "package.toml" else
        assert false
    in
    let file = dir // basename in
    if Sys.file_exists file then
      [ EzFile.read_file file ]
    else begin
      Printf.eprintf "Warning: file %s does not exist\n%!" file;
      []
    end
  in
  let skeleton_files =
    let files = ref [] in
    EzFile.make_select EzFile.iter_dir ~deep:true dir ~kinds:[ S_REG; S_LNK ]
      ~f:(fun path ->
          match String.lowercase ( Filename.basename path ) with
          | "drom.toml"
          | "package.toml"
          | "skeleton.toml" -> ()
          | _ ->
              if not ( Filename.check_suffix path "~" ) then
                let filename = dir // path in
                let content = EzFile.read_file filename in
                let st = Unix.lstat filename in
                let perm = st.Unix.st_perm in
                files := (path, content, perm) :: !files);
    !files
  in
  let skeleton_flags = EzToml.get_encoding_default
      (EzToml.ENCODING.stringMap flags_encoding) table [ "file" ]
      StringMap.empty  in
  begin
    match EzToml.get table [ "files" ] with
    | exception Not_found -> ()
    | _ ->
        Printf.eprintf
          "Warning: %s skeleton %S has an entry [files], probably instead of [file]\n%!"
          kind name
  end;
  (*  Printf.eprintf "Loaded %s skeleton %s\n%!" kind name; *)
  (name, { skeleton_toml;
           skeleton_inherits;
           skeleton_files ;
           skeleton_flags ;
           skeleton_drom = drom;
           skeleton_name = name;
         })

let load_dir_skeletons ~drom map kind dir =
  if Sys.file_exists dir then begin
    let map = ref map in
    EzFile.iter_dir dir ~f:(fun file ->
        let dir = dir // file in
        let toml = dir // "skeleton.toml" in
        if Sys.file_exists toml then
          try
            let name, skeleton = load_skeleton ~drom ~dir ~toml ~kind in
            if !Globals.verbosity > 0 &&  StringMap.mem name !map then
              Printf.eprintf "Warning: %s skeleton %S overwritten in %s\n%!"
                kind name dir;
            map := StringMap.add name skeleton !map
          with exn ->
            Printf.eprintf "Warning: could not load %s skeleton from %S, exception:\n%S\n%!" kind dir (Printexc.to_string exn)
      );
    !map
  end else
    map

let kind_dir ~kind = "skeletons" // kind ^ "s"

let load_system_skeletons map kind =
    match Config.share_dir () with
    | Some dir ->
        let global_skeletons_dir = dir // kind_dir ~kind in
        load_dir_skeletons ~drom:true map kind global_skeletons_dir
    | None ->
        Printf.eprintf "Warning: could not load skeletons from share/%s/skeletons/%s\n%!" Globals.command kind;
        map

let load_user_skeletons map kind =
  let user_skeletons_dir = Globals.config_dir // kind_dir ~kind in
  load_dir_skeletons ~drom:false map kind user_skeletons_dir

let load_skeletons kind =
  let map = load_system_skeletons StringMap.empty kind in
  load_user_skeletons map kind

let project_skeletons = lazy (load_skeletons "project")

let package_skeletons = lazy (load_skeletons "package")

let rec inherit_files self_files super_files =
  match (self_files, super_files) with
  | _, [] -> self_files
  | [], _ -> super_files
  | ( (self_file, self_content, self_mode) :: self_files_tail,
      (super_file, super_content, super_mode) :: super_files_tail ) ->
    if self_file = super_file then
      (self_file, self_content, self_mode)
      :: inherit_files self_files_tail super_files_tail
    else if self_file < super_file then
      (self_file, self_content, self_mode) ::
      inherit_files self_files_tail super_files
    else
      (super_file, super_content, super_mode)
      :: inherit_files self_files super_files_tail

let lookup_skeleton ?(project=false) skeletons name =
  let skeletons = Lazy.force skeletons in
  let rec iter name =
    match StringMap.find name skeletons with
    | exception Not_found -> download_skeleton name
    | self ->
        match self.skeleton_inherits with
        | None ->
            self
        | Some super ->
            let super = iter super in
            let skeleton_toml = self.skeleton_toml @ super.skeleton_toml in
            let skeleton_files =
              inherit_files self.skeleton_files super.skeleton_files
            in
            let skeleton_flags =
              StringMap.union (fun _ x _ -> Some x)
                self.skeleton_flags super.skeleton_flags
            in
            { skeleton_name = name;
              skeleton_inherits = None;
              skeleton_toml; skeleton_files ;
              skeleton_drom = false;
              skeleton_flags ;
            }

  and download_skeleton name =
    if project then
      match EzString.chop_prefix name ~prefix:"gh:" with
      | None ->
          Error.raise "Missing skeleton %S" name
      | Some github_project ->
          let url = Printf.sprintf "https://github.com/%s/tarball/master"
              github_project in
          let output = Filename.temp_file "archive" ".tgz" in
          Misc.wget ~url ~output;
          let basedir = Printf.sprintf "gh-%s"
              ( Digest.string name |> Digest.to_hex ) in
          let dir = Globals.config_dir // "skeletons"
                    // "projects" // basedir in
          EzFile.make_dir ~p:true dir;
          Misc.call [| "tar" ; "zxf"; output ;
                       "--strip-components=1"; "-C"; dir |];
          let toml = dir // "skeleton.toml" in
          if not ( Sys.file_exists toml ) then
            EzFile.write_file toml
              ( Printf.sprintf {|[skeleton]
name = "%s"
|} name );
          let (skel_name, skeleton) =
            load_skeleton ~drom:false ~dir ~toml ~kind:"project"
          in
          if skel_name = name then
            skeleton
          else
            Error.raise "Wrong remote skeleton %S instead of %S in %s\n"
              skel_name name dir
    else
      Error.raise "Missing skeleton %S" name
  in
  iter name

let backup_skeleton file content ~perm =
  let skeleton_dir = Globals.drom_dir // "skeleton" in
  let drom_file = skeleton_dir // file in
  EzFile.make_dir ~p:true (Filename.dirname drom_file);
  EzFile.write_file drom_file content;
  Unix.chmod drom_file perm

let lookup_project skeleton =
  lookup_skeleton ~project:true
    project_skeletons (Misc.project_skeleton skeleton)

let lookup_package skeleton = lookup_skeleton package_skeletons skeleton

let rec eval_project_cond p cond =
  match cond with
  | [ "skeleton" ; "is" ; skeleton ] ->
      Misc.project_skeleton p.skeleton = skeleton
  | [ "skip" ;  skip ] -> List.mem skip p.skip
  | [ "gen" ;  skip ] -> not ( List.mem skip p.skip )
  | "not" :: cond -> not ( eval_project_cond p cond )
  | [ "true" ] -> true
  | [ "false" ] -> false
  | [ "ci" ; system ] -> List.mem system p.ci_systems
  | [ "github-organization"] -> p.github_organization <> None
  | [ "homepage"] -> Misc.homepage p <> None
  | [ "copyright"] -> p.copyright <> None
  | [ "bug-reports"] -> Misc.bug_reports p <> None
  | [ "dev-repo"] -> Misc.dev_repo p <> None
  | [ "doc-gen"] -> Misc.doc_gen p <> None
  | [ "doc-api"] -> Misc.doc_api p <> None
  | [ "sphinx-target"] -> p.sphinx_target <> None
  | [ "profile"] -> p.profile <> None
  | [ "min-edition" ] -> p.min_edition <> p.edition
  | [ "field" ; name ] -> StringMap.mem name p.fields
  | "field" :: name :: v ->
      let v = String.concat ":" v in
      begin
        match StringMap.find name p.fields with
        | exception Not_found -> false
        | x -> x = v
      end

  | _ ->
      Printf.kprintf failwith "eval_project_cond: unknown condition %S\n%!"
        ( String.concat ":" cond )

let rec eval_package_cond p cond =
  match cond with
  | [ "skeleton" ; "is" ; skeleton ] ->
      Misc.package_skeleton p = skeleton
  | [ "kind" ; "is" ; kind ] -> kind = Misc.string_of_kind p.kind
  | [ "pack" ] -> Misc.p_pack_modules p
  | [ "skip" ;  skip ] -> List.mem skip p.project.skip
  | [ "gen" ;  skip ] -> not ( List.mem skip p.project.skip )
  | "not" :: cond -> not ( eval_package_cond p cond )
  | [ "true" ] -> true
  | [ "false" ] -> false
  | "project" :: cond -> eval_project_cond p.project cond

  | [ "field" ; name ] -> StringMap.mem name p.p_fields
  | "field" :: name :: v ->
      let v = String.concat ":" v in
      begin
        match StringMap.find name p.p_fields with
        | exception Not_found -> false
        | x -> x = v
      end

  | _ ->
      Printf.kprintf failwith "eval_package_cond: unknown condition %S\n%!"
        ( String.concat ":" cond )

let default_flags flag_file =
  { default_flags with flag_file ; flag_skipper = ref [] }

let skeleton_flags skeleton file =
  try
    let flags =
      try
        StringMap.find file skeleton.skeleton_flags
      with
      (* This is absurd: toml.5.0.0 does not treat quoted keys and
         unquoted keys internally in the same way... *)
        Not_found ->
          try
            StringMap.find ( Printf.sprintf "\"%s\"" file )
              skeleton.skeleton_flags
          with
            Not_found ->
              if Misc.verbose 2 then
                Printf.eprintf "skeleton %S has no flags for file %S\n%!"
                  skeleton.skeleton_name file;
              raise Not_found
    in
    if flags.flag_file = "" then
      { flags with
        flag_file = file ;
        flag_skipper = ref [] ;
      }
    else
      { flags with flag_skipper = ref [] }
  with Not_found ->
    default_flags file

let skeleton_flags skeleton file perm =
  let flags = skeleton_flags skeleton file in
  if flags.flag_perm = 0 then flags.flag_perm <- perm;
  if Filename.check_suffix file ".sh" then
    flags.flag_perm <- flags.flag_perm lor 0o111;
  flags

let write_project_files write_file p =
  let skeleton = lookup_project p.skeleton in
  List.iter
    (fun (file, content, perm) ->
       (* Printf.eprintf "File %s perm %o\n%!" file perm; *)
      backup_skeleton file content ~perm;
      let flags = skeleton_flags skeleton file perm in
      let bracket = bracket flags eval_project_cond in
      let content =
        if flags.flag_subst then
          try Subst.project () ~bracket ~skipper:flags.flag_skipper p content
          with Not_found ->
            Printf.kprintf failwith "Exception Not_found in %S\n%!" file;
        else content
      in
      let { flag_file;
            flag_create = create;
            flag_skips = skips;
            flag_record = record;
            flag_skip = skip;
            flag_perm = perm;
            flag_skipper= _ ;
            flag_subst = _ ; } = flags in
      let flag_file = Subst.project () p flag_file in
      write_file flag_file ~create ~skips ~content ~record ~skip ~perm;
    )
    skeleton.skeleton_files;
  ()

let subst_package_file flags content package =
  let bracket = bracket flags eval_package_cond in
  let content =
    if flags.flag_subst then
      try
        Subst.package () ~bracket
          ~skipper:flags.flag_skipper package content
      with Not_found ->
        Printf.kprintf failwith "Exception Not_found in %S\n%!"
          flags.flag_file
    else
      content
  in
  content

let write_package_files write_file package =
  let skeleton = lookup_package (Misc.package_skeleton package) in

  List.iter
    (fun (file, content, perm) ->
       (* Printf.eprintf "File %s perm %o\n%!" file perm; *)
       backup_skeleton file content ~perm;
       let flags = skeleton_flags skeleton file perm in
       let content = subst_package_file flags content package in
       let { flag_file;
             flag_create = create;
             flag_skips = skips;
             flag_record = record;
             flag_skip = skip;
             flag_perm = perm;
             flag_skipper= _ ;
             flag_subst = _ ; } = flags in
       let flag_file = Subst.package () package flag_file in
       let file = package.dir // flag_file in
       write_file file ~create ~skips ~content ~record ~skip ~perm)
    skeleton.skeleton_files

let write_files write_file p =
  write_project_files write_file p;
  List.iter (fun package -> write_package_files write_file package) p.packages

let project_skeletons () =
  Lazy.force project_skeletons |> StringMap.to_list |> List.map snd

let package_skeletons () =
  Lazy.force package_skeletons |> StringMap.to_list |> List.map snd

let known_skeletons () =
  Printf.sprintf "project skeletons: %s\npackage skeletons: %s\n"
    (project_skeletons ()
     |> List.map (fun s -> s.skeleton_name)
     |> String.concat " " )
    (package_skeletons ()
     |> List.map (fun s -> s.skeleton_name)
     |> String.concat " " )