Skip to content

Commit 6535692

Browse files
authored
feat: Add CLI option learn-ocaml build --build-dir=[./_learn-ocaml-build] (#585)
Motivation: - in current master, the exercises were always precompiled in-place; - this conflicted with the documented workflow (option "ro"): - https://ocaml-sf.org/learn-ocaml/howto-deploy-a-learn-ocaml-instance.html - https://ocaml-sf.org/learn-ocaml/howto-setup-exercise-development-environment.html - this new option makes it possible to copy and precompile exercises apart by default (in the ./_learn-ocaml-build/exercises subdirectory, which is erased before the build). Examples: - `learn-ocaml build --repo=demo-repository` - `learn-ocaml build --repo=demo-repository serve` - `learn-ocaml build --repo=demo-repository serve --replace` # or - `learn-ocaml build --repo=demo-repository --build-dir=demo-repository` # to retrieve the previous behavior of learn-ocaml master. Note: externalizing the build-dir in a volume looks unneeded: if the build-dir is shared, but the container is deleted, the www is deleted as well, and the exercices are rebuilt anyway and an extra volume would add more noise (mandating `-v args` tweaks)
1 parent fe2a806 commit 6535692

File tree

1 file changed

+73
-12
lines changed

1 file changed

+73
-12
lines changed

src/main/learnocaml_main.ml

+73-12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ let readlink f =
2424
try Sys.chdir cwd; f
2525
with Sys_error _ -> Sys.chdir (Filename.get_temp_dir_name ()); f
2626

27+
let absolute_filename path =
28+
(* Note: symlinks are not taken into account *)
29+
if Filename.is_relative path
30+
then Filename.concat (Sys.getcwd ()) path
31+
else path
32+
33+
let dflt_build_dir = "_learn-ocaml-build"
34+
2735
module Args = struct
2836
open Arg
2937

@@ -44,6 +52,16 @@ module Args = struct
4452
"The path to the repository containing the exercises, lessons and \
4553
tutorials."
4654

55+
let build_dir =
56+
value & opt dir ("./" ^ dflt_build_dir) & info ["build-dir"] ~docs ~docv:"DIR" ~doc:
57+
(Printf.sprintf
58+
"Directory where the repo exercises are copied and precompiled. \
59+
When $(docv) takes its default value (e.g. when it is omitted in CLI), \
60+
'$(b,learn-ocaml build)' first erases the '$(docv)/exercises' subfolder. \
61+
Note that the default value for $(docv), './%s', is generally a sensible choice. \
62+
But passing the same argument as the one for $(i,--repo) is also a valid value for $(docv)."
63+
dflt_build_dir)
64+
4765
let app_dir =
4866
value & opt string "./www" & info ["app-dir"; "o"] ~docs ~docv:"DIR" ~doc:
4967
"Directory where the app should be generated for the $(i,build) command, \
@@ -215,9 +233,10 @@ module Args = struct
215233
Term.(const apply $contents_dir $try_ocaml $lessons $exercises $playground $toplevel $base_url)
216234

217235
let repo_conf =
218-
let apply repo_dir exercises_filtered jobs =
236+
let apply repo_dir build_dir exercises_filtered jobs =
219237
Learnocaml_process_exercise_repository.exercises_dir :=
220-
repo_dir/"exercises";
238+
(* not repo_dir/"exercises" here - since we need write permissions *)
239+
build_dir/"exercises";
221240
Learnocaml_process_exercise_repository.exercises_filtered :=
222241
Learnocaml_data.SSet.of_list (List.flatten exercises_filtered);
223242
Learnocaml_process_tutorial_repository.tutorials_dir :=
@@ -227,7 +246,7 @@ module Args = struct
227246
Learnocaml_process_exercise_repository.n_processes := jobs;
228247
()
229248
in
230-
Term.(const apply $repo_dir $exercises_filtered $jobs)
249+
Term.(const apply $repo_dir $build_dir $exercises_filtered $jobs)
231250

232251
let term =
233252
let apply conf () = conf in
@@ -243,16 +262,17 @@ module Args = struct
243262
commands: command list;
244263
app_dir: string;
245264
repo_dir: string;
265+
build_dir: string;
246266
grader: Grader.t;
247267
builder: Builder.t;
248268
server: Server.t;
249269
}
250270

251271
let term =
252-
let apply commands app_dir repo_dir grader builder server =
253-
{ commands; app_dir; repo_dir; grader; builder; server }
272+
let apply commands app_dir repo_dir build_dir grader builder server =
273+
{ commands; app_dir; repo_dir; build_dir; grader; builder; server }
254274
in
255-
Term.(const apply $commands $app_dir $repo_dir
275+
Term.(const apply $commands $app_dir $repo_dir $build_dir
256276
$Grader.term $Builder.term $Server.term app_dir base_url)
257277
end
258278

@@ -328,6 +348,49 @@ let main o =
328348
>|= fun i -> Some i)
329349
else Lwt.return_none
330350
in
351+
let copy_build_exercises o =
352+
(* NOTE: if `--build` = `--repo`, then no copy is needed.
353+
Before checking path equality, we need to get canonical paths *)
354+
let repo_exos_dir = readlink o.repo_dir / "exercises" in
355+
let build_exos_dir = readlink o.build_dir / "exercises" in
356+
if repo_exos_dir <> build_exos_dir then begin
357+
(* NOTE: if the CLI arg is "./_learn-ocaml-build" or "_learn-ocaml-build"
358+
then the exercises subdirectory is erased beforehand *)
359+
begin
360+
if (o.build_dir = dflt_build_dir || o.build_dir = "./" ^ dflt_build_dir)
361+
&& Sys.file_exists build_exos_dir then
362+
Lwt.catch (fun () ->
363+
Lwt_process.exec ("rm",[|"rm";"-rf"; build_exos_dir|]) >>= fun r ->
364+
if r <> Unix.WEXITED 0 then
365+
Lwt.fail_with "Remove command failed"
366+
else Lwt.return_unit)
367+
(fun ex ->
368+
Printf.eprintf
369+
"Error: while removing previous build-dir \
370+
%s:\n %s\n%!"
371+
build_exos_dir (Printexc.to_string ex);
372+
exit 1)
373+
else
374+
Lwt.return_unit
375+
end >>= fun () ->
376+
Printf.printf "Building %s\n%!" (o.build_dir / "exercises");
377+
(* NOTE: we choose to reuse Lwt_utils.copy_tree,
378+
even if we could use "rsync" (upside: "--delete-delay",
379+
but downside: would require the availability of rsync). *)
380+
Lwt.catch
381+
(fun () -> Lwt_utils.copy_tree repo_exos_dir build_exos_dir)
382+
(function
383+
| Failure _ ->
384+
Lwt.fail_with @@ Printf.sprintf
385+
"Failed to copy repo exercises to %s"
386+
(build_exos_dir)
387+
| e -> Lwt.fail e)
388+
(* NOTE: no chown is needed,
389+
but we may want to run "chmod -R u+w exercises"
390+
if the source repository has bad permissions... *)
391+
end
392+
else Lwt.return_unit
393+
in
331394
let generate o =
332395
if List.mem Build o.commands then
333396
(let get_app_dir o =
@@ -412,7 +475,9 @@ let main o =
412475
(fun _ -> Learnocaml_process_playground_repository.main (o.app_dir))
413476
>>= fun playground_ret ->
414477
if_enabled o.builder.Builder.exercises (o.repo_dir/"exercises")
415-
(fun _ -> Learnocaml_process_exercise_repository.main (o.app_dir))
478+
(fun _ ->
479+
copy_build_exercises o >>= fun () ->
480+
Learnocaml_process_exercise_repository.main (o.app_dir))
416481
>>= fun exercises_ret ->
417482
Lwt_io.with_file ~mode:Lwt_io.Output (o.app_dir/"js"/"learnocaml-config.js")
418483
(fun oc ->
@@ -442,11 +507,7 @@ let main o =
442507
let running = Learnocaml_server.check_running () in
443508
Option.iter Learnocaml_server.kill_running running;
444509
let temp = temp_app_dir o in
445-
let app_dir =
446-
if Filename.is_relative o.app_dir
447-
then Filename.concat (Sys.getcwd ()) o.app_dir
448-
else o.app_dir
449-
in
510+
let app_dir = absolute_filename o.app_dir in
450511
let bak =
451512
let f =
452513
Filename.temp_file

0 commit comments

Comments
 (0)