package opam-repository

  1. Overview
  2. Docs

Source file opamLocal.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
(**************************************************************************)
(*                                                                        *)
(*    Copyright 2012-2019 OCamlPro                                        *)
(*    Copyright 2012 INRIA                                                *)
(*                                                                        *)
(*  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 OpamTypes
open OpamProcess.Job.Op

let log fmt = OpamConsole.log "RSYNC" fmt

(* Rsync args recap:
  - recurse into directories (r)
  - skip based on checksum, not mod-time & size (c)
  - preserve permissions (p), times (t), group (g), owner (o),
    device & special files (D)
  - transform symlink into referent file/dir (L)
  *)
let rsync_arg = "-rLptgoDvc"

(* if rsync -arv return 4 lines, this means that no files have changed *)
let rsync_trim = function
  | [] -> []
  | _ :: t ->
    match List.rev t with
    | _ :: _ :: _ :: l -> List.filter ((<>) "./") l
    | _ -> []

let convert_path =
  OpamSystem.get_cygpath_function ~command:"rsync"

let call_rsync check args =
  OpamSystem.make_command "rsync" args
  @@> fun r ->
  match r.OpamProcess.r_code with
  | 0 -> Done (Some (rsync_trim r.OpamProcess.r_stdout))
  | 3 | 5 | 10 | 11 | 12 -> (* protocol or file errors *)
    Done None
  | 20 -> (* signal *)
    raise Sys.Break
  | 23 | 24 ->
    (* partial, mostly mode, link or perm errors. But may also be a
       complete error so we do an additional check *)
    if check () then
      (OpamConsole.warning "Rsync partially failed:\n%s"
         (OpamStd.Format.itemize ~bullet:"" (fun x -> x) r.OpamProcess.r_stderr);
       Done (Some (rsync_trim r.OpamProcess.r_stdout)))
    else Done None
  | 30 | 35 -> (* timeouts *)
    Done None
  | _ -> OpamSystem.process_error r

let rsync ?(args=[]) ?(exclude_vcdirs=true) src dst =
  log "rsync: src=%s dst=%s" src dst;
  let remote = String.contains src ':' in
  let overlap src dst =
    let norm d = Filename.concat d "" in
    OpamStd.String.starts_with ~prefix:(norm src) (norm dst) &&
    not (OpamStd.String.contains ~sub:OpamSwitch.external_dirname (norm dst)) ||
    OpamStd.String.starts_with ~prefix:(norm dst) (norm src)
  in
  (* See also [OpamVCS.sync_dirty] *)
  let exclude_args =
    (if not exclude_vcdirs then [] else
       [ "--exclude"; ".git";
         "--exclude"; "_darcs";
         "--exclude"; ".hg";
       ])
    @ [
      "--exclude"; ".#*";
      "--exclude"; OpamSwitch.external_dirname ^ "*";
      "--exclude"; "_build";
    ]
  in
  if not(remote || Sys.file_exists src) then
    Done (Not_available (None, src))
  else if src = dst then
    Done (Up_to_date [])
  else if overlap src dst then
    (OpamConsole.error "Cannot sync %s into %s: they overlap" src dst;
     Done (Not_available (None, src)))
  else (
    OpamSystem.mkdir dst;
    let convert_path = Lazy.force convert_path in
    call_rsync (fun () -> not (OpamSystem.dir_is_empty dst))
      ( rsync_arg :: args @ exclude_args @
        [ "--delete"; "--delete-excluded"; convert_path src; convert_path dst; ])
    @@| function
    | None -> Not_available (None, src)
    | Some [] -> Up_to_date []
    | Some lines -> Result lines
  )

let is_remote url = url.OpamUrl.transport <> "file"

let rsync_dirs ?args ?exclude_vcdirs url dst =
  let src_s = OpamUrl.(Op.(url / "").path) in (* Ensure trailing '/' *)
  let dst_s = OpamFilename.Dir.to_string dst in
  if not (is_remote url) &&
     not (OpamFilename.exists_dir (OpamFilename.Dir.of_string src_s))
  then
    Done (Not_available (None, Printf.sprintf "Directory %s does not exist" src_s))
  else
  rsync ?args ?exclude_vcdirs src_s dst_s @@| function
  | Not_available _ as na -> na
  | Result _ ->
    if OpamFilename.exists_dir dst then Result dst
    else Not_available (None, dst_s)
  | Up_to_date _ -> Up_to_date dst

let rsync_file ?(args=[]) url dst =
  let src_s = url.OpamUrl.path in
  let dst_s = OpamFilename.to_string dst in
  log "rsync_file src=%s dst=%s" src_s dst_s;
  if not (is_remote url || OpamFilename.(exists (of_string src_s))) then
    Done (Not_available (None, src_s))
  else if src_s = dst_s then
    Done (Up_to_date dst)
  else
    (OpamFilename.mkdir (OpamFilename.dirname dst);
     let convert_path = Lazy.force convert_path in
     call_rsync (fun () -> Sys.file_exists dst_s)
       ( rsync_arg :: args @ [ convert_path src_s; convert_path dst_s ])
     @@| function
     | None -> Not_available (None, src_s)
     | Some [] -> Up_to_date dst
     | Some [_] ->
       if OpamFilename.exists dst then Result dst
       else Not_available (None, src_s)
     | Some l ->
       OpamSystem.internal_error
         "unknown rsync output: {%s}"
         (String.concat ", " l))

module B = struct

  let name = `rsync

  let pull_dir_quiet local_dirname url =
    rsync_dirs url local_dirname

  let fetch_repo_update repo_name ?cache_dir:_ repo_root url =
    log "pull-repo-update";
    let quarantine =
      OpamFilename.Dir.(of_string (to_string repo_root ^ ".new"))
    in
    let finalise () = OpamFilename.rmdir quarantine in
    OpamProcess.Job.catch (fun e ->
        finalise ();
        Done (OpamRepositoryBackend.Update_err e))
    @@ fun () ->
    OpamRepositoryBackend.job_text repo_name "sync"
      (match OpamUrl.local_dir url with
       | Some dir ->
         OpamFilename.copy_dir ~src:dir ~dst:quarantine;
         (* fixme: Would be best to symlink, but at the moment our filename api
            isn't able to cope properly with the symlinks afterwards
            OpamFilename.link_dir ~target:dir ~link:quarantine; *)
         Done (Result quarantine)
       | None ->
         if OpamFilename.exists_dir repo_root then
           OpamFilename.copy_dir ~src:repo_root ~dst:quarantine
         else
           OpamFilename.mkdir quarantine;
         pull_dir_quiet quarantine url) @@+ function
    | Not_available (_, msg) ->
      finalise ();
      Done (OpamRepositoryBackend.Update_err (Failure ("rsync error: " ^ msg)))
    | Up_to_date _ ->
      finalise (); Done OpamRepositoryBackend.Update_empty
    | Result _ ->
      if not (OpamFilename.exists_dir repo_root) ||
         OpamFilename.dir_is_empty repo_root then
        Done (OpamRepositoryBackend.Update_full quarantine)
      else
        OpamProcess.Job.finally finalise @@ fun () ->
        OpamRepositoryBackend.job_text repo_name "diff" @@
        OpamRepositoryBackend.get_diff
          (OpamFilename.dirname_dir repo_root)
          (OpamFilename.basename_dir repo_root)
          (OpamFilename.basename_dir quarantine)
        @@| function
        | None -> OpamRepositoryBackend.Update_empty
        | Some p -> OpamRepositoryBackend.Update_patch p

  let repo_update_complete _ _ = Done ()

  let pull_url ?full_fetch:_ ?cache_dir:_ ?subpath local_dirname _checksum remote_url =
    let local_dirname = OpamFilename.SubPath.(local_dirname /? subpath) in
    OpamFilename.mkdir local_dirname;
    let dir = OpamFilename.Dir.to_string local_dirname in
    let remote_url =
      OpamStd.Option.map_default (fun x -> OpamUrl.Op.(remote_url / OpamFilename.SubPath.to_string x))
        remote_url subpath
    in
    let remote_url =
      match OpamUrl.local_dir remote_url with
      | Some _ ->
        (* ensure that rsync doesn't recreate a subdir: add trailing '/' *)
        OpamUrl.Op.(remote_url / "")
      | None -> remote_url
    in
    rsync remote_url.OpamUrl.path dir
    @@| function
    | Not_available _ as na -> na
    | (Result _ | Up_to_date _) as r ->
      let res x = match r with
        | Result _ -> Result x
        | Up_to_date _ -> Up_to_date x
        | _ -> assert false
      in
      if OpamUrl.has_trailing_slash remote_url then
        res None
      else
      let filename =
        OpamFilename.Op.(local_dirname // OpamUrl.basename remote_url)
      in
      if OpamFilename.exists filename then res (Some filename)
      else
        Not_available
          (None, Printf.sprintf
             "Could not find target file %s after rsync with %s. \
              Perhaps you meant %s/ ?"
             (OpamUrl.basename remote_url)
             (OpamUrl.to_string remote_url)
             (OpamUrl.to_string remote_url))

  let revision _ =
    Done None

  let sync_dirty ?subpath dir url = pull_url ?subpath dir None url

  let get_remote_url ?hash:_ _ =
    Done None

end
OCaml

Innovation. Community. Security.