-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.ml
More file actions
executable file
·221 lines (200 loc) · 7.68 KB
/
Copy pathinstall.ml
File metadata and controls
executable file
·221 lines (200 loc) · 7.68 KB
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
#!/usr/bin/env -S ocaml -I +unix -I +str unix.cma str.cma
open Unix
(* file we store dest places in *)
let filename = "dest_places.txt"
(* data types *)
type dot_file =
{source: string; destination: string; mode: int option; symlink: bool}
type action = Install | Uninstall | Configure
let source_top_dir source =
match String.split_on_char '/' source with dir :: _ -> dir | [] -> source
(* parse command line input *)
let get_chosen_dotfiles args =
let len = Array.length args in
if len > 2 then Some (Array.to_list (Array.sub args 2 (len - 2))) else None
let get_chosen_action args =
let len = Array.length args in
if len >= 2 then
match Array.get args 1 with
| "i" -> Install
| "u" -> Uninstall
| "c" -> Configure
| a -> raise (Failure a)
else Install
(* Determine whether the dotfile should be symlinked or hardlinked. If the
value is "hardlink", we use hardlinks (or mirrored directories with
hardlinked files). For any other value, we default to symlinking. *)
let should_symlink opt_syml =
not (String.equal (Option.value opt_syml ~default:"symlink") "hardlink")
let parse_mode maybe_mode =
match maybe_mode with
| Some mode -> (
try Some (int_of_string ("0o" ^ mode)) with Failure _ -> None )
| None -> None
(* parse the lines in dest file *)
let line_to_dot_file s =
let home = Unix.getenv "HOME" in
match String.split_on_char ':' s with
| source :: dest :: rest ->
let destination = home ^ "/" ^ dest in
let symlink = should_symlink (List.nth_opt rest 0) in
let mode = parse_mode (List.nth_opt rest 1) in
{source; destination; mode; symlink}
| _ -> failwith (Printf.sprintf "malformed line in dest_places.txt: %s" s)
(* skip comments lines *)
let shouldnt_skip_line str =
not
(String.starts_with ~prefix:"#" str || String.equal (String.trim str) "")
(* parse the dest file *)
let places =
In_channel.with_open_bin filename In_channel.input_all
|> String.split_on_char '\n'
|> List.filter shouldnt_skip_line
|> List.map line_to_dot_file
(* Match the whole source or its top directory, never a substring: "haskell"
selects every haskell/* entry, but a stray "a" selects nothing. *)
let dotfile_is_chosen chosen place =
let matches name =
String.equal place.source name
|| String.equal (source_top_dir place.source) name
in
List.exists matches chosen
(* logic for doing installation of dotfiles *)
(* Destinations nest deeper than their parents exist on a fresh machine,
.local/bin being the usual one. *)
let rec create_dir dir =
if not (Sys.file_exists dir) then (
let parent = Filename.dirname dir in
if not (String.equal parent dir) then create_dir parent ;
Sys.mkdir dir 0o700 )
let set_permissions source current_mode maybe_mode =
match maybe_mode with
| Some mode ->
if not (current_mode == mode) then (
Printf.printf "setting permissions to 0%o for %s\n" mode source ;
chmod source mode )
| None -> ()
let rec link_dotfile please_symlink maybe_mode source destination =
let file_stat = stat source in
if file_stat.st_kind == S_DIR && please_symlink then (
(* symlink the whole directory in one shot *)
if not (Sys.file_exists destination) then (
let dest_dir = Filename.dirname destination in
if not (Sys.file_exists dest_dir) then create_dir dest_dir ;
Printf.printf "symlinking dir %s %s\n" source destination ;
symlink source destination ) )
else if file_stat.st_kind == S_DIR then
(* hardlink mode: recurse and hardlink individual files *)
let files = Sys.readdir source in
for i = 0 to Array.length files - 1 do
let single_file = files.(i) in
(* Editor droppings and vendored checkouts are skipped, but ordinary
dotfiles such as .dir-locals.el are the point of the repo. *)
if
not
( String.starts_with single_file ~prefix:"#"
|| String.starts_with single_file ~prefix:".#"
|| String.ends_with single_file ~suffix:"~"
|| String.equal single_file ".git" )
then
link_dotfile please_symlink maybe_mode
(source ^ "/" ^ single_file)
(destination ^ "/" ^ single_file)
done
else (
set_permissions source file_stat.st_perm maybe_mode ;
if not (Sys.file_exists destination) then (
let dest_dir = Filename.dirname destination in
if not (Sys.file_exists dest_dir) then create_dir dest_dir ;
if please_symlink then (
Printf.printf "symlinking %s %s\n" source destination ;
symlink source destination )
else (
Printf.printf "hardlinking %s %s\n" source destination ;
link source destination ) ) )
let install cwd places =
let dotfile_basepath = cwd ^ "/" in
let do_install place =
let source = dotfile_basepath ^ place.source in
let destination = place.destination in
let please_symlink = place.symlink in
let maybe_mode = place.mode in
link_dotfile please_symlink maybe_mode source destination
in
List.iter do_install places
(* logic for uninstallation of dotfiles *)
let confirm question =
Printf.printf "%s [y/N] %!" question ;
try
match String.lowercase_ascii (String.trim (input_line Stdlib.stdin)) with
| "y" | "yes" -> true
| _ -> false
with End_of_file -> false
(* A symlink is ours alone, so it goes without asking. A real directory is
shared with whatever the program writes there itself, such as elpa/ and
eln-cache/ under .config/emacs, so that one needs a yes. *)
let uninstall places =
let rec rmrf path =
match (Unix.lstat path).st_kind with
| Unix.S_DIR ->
Sys.readdir path
|> Array.iter (fun name -> rmrf (Filename.concat path name)) ;
Unix.rmdir path
| _ -> Sys.remove path
in
let remove_dotfile path =
try
match (Unix.lstat path).st_kind with
| Unix.S_DIR ->
if
confirm
(Printf.sprintf "delete %s and everything inside it?" path)
then (
Printf.printf "uninstalling %s\n" path ;
rmrf path )
else Printf.printf "skipping %s\n" path
| _ ->
Printf.printf "uninstalling %s\n" path ;
Unix.unlink path
with Unix.Unix_error (Unix.ENOENT, _, _) -> ()
in
List.iter (fun dotfile -> remove_dotfile dotfile.destination) places
(* entry *)
let perform_action action maybe_chosen_sources cwd places =
let chosen_places =
match maybe_chosen_sources with
| Some chosen -> List.filter (dotfile_is_chosen chosen) places
| None -> places
in
match action with
| Install -> install cwd chosen_places
| Uninstall -> uninstall chosen_places
| Configure ->
let seen = Hashtbl.create 8 in
List.iter
(fun place ->
let dir = source_top_dir place.source in
if not (Hashtbl.mem seen dir) then (
Hashtbl.add seen dir true ;
let configure_ml = cwd ^ "/" ^ dir ^ "/configure.ml" in
if Sys.file_exists configure_ml then (
let cmd =
Printf.sprintf
"ocaml -I +unix -I +str unix.cma str.cma %s"
configure_ml
in
Printf.printf "configuring %s\n%!" dir ;
match Unix.system cmd with
| Unix.WEXITED 0 -> ()
| Unix.WEXITED code ->
Printf.eprintf "configuring %s failed with exit code %d\n%!"
dir code
| Unix.WSIGNALED _ | Unix.WSTOPPED _ ->
Printf.eprintf "configuring %s was killed\n%!" dir ) ) )
chosen_places
let () =
let cwd = Sys.getcwd () in
let all_places = places in
let chosen_dotfiles = get_chosen_dotfiles Sys.argv in
let action = get_chosen_action Sys.argv in
perform_action action chosen_dotfiles cwd all_places