Skip to content

Fix & Document static deployment CLI support #368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/howto-deploy-learn-ocaml-statically.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
How to deploy learn-ocaml statically
====================================

This section explains how to deploy a static version of learn-ocaml on
an HTTP server.

## Using pre-built docker images

You will just need to:

- install Docker Engine (<https://docs.docker.com/get-docker/>)
- build the `www` folder by using the commands below
- use an HTTP server to serve learn-ocaml statically.

Assuming your exercise repository is in directory `$REPOSITORY` and
you want to generate the website contents in directory `$TARGET/www`
to serve it at `$URL` (the base URL **without trailing slash**), then
you can run:

```bash
# Remove old version
cd "$TARGET"
rm -fr www
# Get the last (dev) version of learn-ocaml
sudo docker pull ocamlsf/learn-ocaml:master
# Build the site within a Docker container
sudo docker run --rm -i --entrypoint="" \
-v "$REPOSITORY:/repository" -v "$TARGET:/home/learn-ocaml/target" \
ocamlsf/learn-ocaml:master \
sh -c "learn-ocaml build --repo=/repository --base-url $URL && mv www target/"
```

Regarding the `--base-url` option, if you plan to deploy the `www`
directory with **GitHub Pages**, assuming the underlying GitHub repo
(either public or private) is `https://github.com/user-name/repo-name`
then you should first run:

```bash
export URL=https://user-name.github.io/repo-name
```

For a comprehensive example of one such deployment, you may take a
look at the following repository:
- <https://github.com/pfitaxel/pfitaxel-demo>
- deployed to <https://pfitaxel.github.io/pfitaxel-demo>
- thanks to this [`deploy` script](https://github.com/pfitaxel/pfitaxel-demo/blob/master/deploy).

## Manual compilation

Note: you need a working `opam` environment (version 2.0+) as well as
an `opam switch` and a compiled version of `learn-ocaml` (see
[How to deploy a learn-ocaml instance](howto-deploy-a-learn-ocaml-instance.md#manual-compilation)
for details).

Assuming your exercise repository is in directory `$REPOSITORY` and
you want to generate the website contents in directory `$TARGET/www`
to serve it at `$URL` (the base URL **without trailing slash**), then
you can run:

```bash
rm -fr "$TARGET/www"
cd .../learn-ocaml # go to the learn-ocaml git repo
learn-ocaml build --repo=$REPOSITORY --base-url $URL
mv www "$TARGET/www"
```
4 changes: 2 additions & 2 deletions src/app/learnocaml_config.ml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class type learnocaml_config = object
method enablePlayground: bool Js.optdef_prop
method txtLoginWelcome: Js.js_string Js.t Js.optdef_prop
method txtNickname: Js.js_string Js.t Js.optdef_prop
method root: Js.js_string Js.t Js.optdef_prop
method baseUrl: Js.js_string Js.t Js.optdef_prop
end

let config : learnocaml_config Js.t = Js.Unsafe.js_expr "learnocaml_config"
let api_server = Js.(to_string (Optdef.get config##.root (fun () -> string "")))
let api_server = Js.(to_string (Optdef.get config##.baseUrl (fun () -> string "")))
2 changes: 1 addition & 1 deletion src/app/learnocaml_config.mli
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class type learnocaml_config = object
method enablePlayground: bool Js.optdef_prop
method txtLoginWelcome: Js.js_string Js.t Js.optdef_prop
method txtNickname: Js.js_string Js.t Js.optdef_prop
method root: Js.js_string Js.t Js.optdef_prop
method baseUrl: Js.js_string Js.t Js.optdef_prop
end

val config : learnocaml_config Js.t
Expand Down
48 changes: 29 additions & 19 deletions src/main/learnocaml_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ module Args = struct
"Directory where the app should be generated for the $(i,build) command, \
and from where it is served by the $(i,serve) command."

let base_url =
value & opt string "" &
info ["base-url"] ~docv:"BASE_URL" ~env:(Arg.env_var "LEARNOCAML_BASE_URL") ~doc:
"Set the base URL of the website. \
Should not end with a trailing slash. \
Currently, this has no effect on the backend - '$(b,learn-ocaml serve)'. \
Mandatory for '$(b,learn-ocaml build)' if the site is not hosted in path '/', \
which typically occurs for static deployment."

module Grader = struct
let info = info ~docs:"GRADER OPTIONS"

Expand Down Expand Up @@ -180,27 +189,22 @@ module Args = struct
value & opt int 1 & info ["jobs";"j"] ~docv:"INT" ~doc:
"Number of building jobs to run in parallel"

let root =
value & opt string "" & info ["root"] ~docv:"ROOT" ~doc:
"Set the root of all documents. Use only for static deployment.\
Should not end with a trailing slash."

type t = {
contents_dir: string;
try_ocaml: bool option;
lessons: bool option;
exercises: bool option;
playground: bool option;
toplevel: bool option;
root: string
base_url: string
}

let builder_conf =
let apply
contents_dir try_ocaml lessons exercises playground toplevel root
= { contents_dir; try_ocaml; lessons; exercises; playground; toplevel; root }
contents_dir try_ocaml lessons exercises playground toplevel base_url
= { contents_dir; try_ocaml; lessons; exercises; playground; toplevel; base_url }
in
Term.(const apply $contents_dir $try_ocaml $lessons $exercises $playground $toplevel $root)
Term.(const apply $contents_dir $try_ocaml $lessons $exercises $playground $toplevel $base_url)

let repo_conf =
let apply repo_dir exercises_filtered jobs =
Expand Down Expand Up @@ -241,16 +245,16 @@ module Args = struct
{ commands; app_dir; repo_dir; grader; builder; server }
in
Term.(const apply $commands $app_dir $repo_dir
$Grader.term $Builder.term $Server.term app_dir)
$Grader.term $Builder.term $Server.term app_dir base_url)
end

open Args

let process_html_file orig_file dest_file root =
let process_html_file orig_file dest_file base_url =
let transform_tag e tag attrs attr =
let attr_pair = ("", attr) in
match List.assoc_opt attr_pair attrs with
| Some url -> `Start_element ((e, tag), (attr_pair, root ^ url) :: (List.remove_assoc attr_pair attrs))
| Some url -> `Start_element ((e, tag), (attr_pair, base_url ^ url) :: (List.remove_assoc attr_pair attrs))
| None -> `Start_element ((e, tag), attrs) in
Lwt_io.open_file ~mode:Lwt_io.Input orig_file >>= fun ofile ->
Lwt_io.open_file ~mode:Lwt_io.Output dest_file >>= fun wfile ->
Expand Down Expand Up @@ -322,11 +326,13 @@ let main o =
let json_config = ServerData.build_config preconfig in
Learnocaml_store.write_to_file ServerData.config_enc json_config www_server_config
>>= fun () ->
if o.builder.Builder.base_url <> "" then
Printf.printf "Base URL: %s\n%!" o.builder.Builder.base_url;
Lwt_unix.files_of_directory o.builder.Builder.contents_dir
|> Lwt_stream.iter_s (fun file ->
if Filename.extension file = ".html" then
process_html_file (o.builder.Builder.contents_dir/file)
(o.app_dir/file) o.builder.Builder.root
(o.app_dir/file) o.builder.Builder.base_url
else
Lwt.return_unit) >>= fun () ->
let if_enabled opt dir f = (match opt with
Expand Down Expand Up @@ -363,14 +369,14 @@ let main o =
\ enableLessons: %b,\n\
\ enableExercises: %b,\n\
\ enableToplevel: %b,\n\
\ root: \"%s\"\n\
\ baseUrl: \"%s\"\n\
}\n"
(tutorials_ret <> None)
(playground_ret <> None)
(lessons_ret <> None)
(exercises_ret <> None)
(o.builder.Builder.toplevel <> Some false)
o.builder.Builder.root >>= fun () ->
o.builder.Builder.base_url >>= fun () ->
Lwt.return (tutorials_ret <> Some false && exercises_ret <> Some false)))
else
Lwt.return true
Expand All @@ -383,14 +389,18 @@ let main o =
let open Server in
("--app-dir="^o.app_dir) ::
("--sync-dir="^o.server.sync_dir) ::
("--base-url="^o.builder.Builder.base_url) ::
("--port="^string_of_int o.server.port) ::
(match o.server.cert with None -> [] | Some c -> ["--cert="^c])
in
Unix.execv native_server (Array.of_list (native_server::server_args))
else
Printf.printf "Starting server on port %d\n%!"
!Learnocaml_server.port;
Learnocaml_server.launch ()
else begin
Printf.printf "Starting server on port %d\n%!"
!Learnocaml_server.port;
if o.builder.Builder.base_url <> "" then
Printf.printf "Base URL: %s\n%!" o.builder.Builder.base_url;
Learnocaml_server.launch ()
end
else
Lwt.return true
in
Expand Down
12 changes: 7 additions & 5 deletions src/main/learnocaml_server_args.ml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ let port =

type t = {
sync_dir: string;
cert: string option;
base_url: string;
port: int;
cert: string option;
}

let term app_dir =
let apply app_dir sync_dir port cert =
let term app_dir base_url =
let apply app_dir sync_dir base_url port cert =
Learnocaml_store.static_dir := app_dir;
Learnocaml_store.sync_dir := sync_dir;
let port = match port, cert with
Expand All @@ -52,8 +53,9 @@ let term app_dir =
| Some base -> Some (base ^ ".pem", base ^ ".key");
| None -> None);
Learnocaml_server.port := port;
{ sync_dir; port; cert }
Learnocaml_server.base_url := base_url;
{ sync_dir; base_url; port; cert }
in
(* warning: if you add any options here, remember to pass them through when
calling the native server from learn-ocaml main *)
Term.(const apply $app_dir $sync_dir $port $cert)
Term.(const apply $ app_dir $ sync_dir $ base_url $ port $ cert)
5 changes: 3 additions & 2 deletions src/main/learnocaml_server_args.mli
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

type t = {
sync_dir: string;
cert: string option;
base_url: string;
port: int;
cert: string option;
}

val term: string Cmdliner.Term.t -> t Cmdliner.Term.t
val term: string Cmdliner.Term.t -> string Cmdliner.Term.t -> t Cmdliner.Term.t
13 changes: 12 additions & 1 deletion src/main/learnocaml_server_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ let signal_waiter =
let main o =
Printf.printf "Learnocaml server v.%s starting on port %d\n%!"
Learnocaml_api.version o.port;
if o.base_url <> "" then
Printf.printf "Base URL: %s\n%!" o.base_url;
let rec run () =
let minimum_duration = 15. in
let t0 = Unix.time () in
Expand Down Expand Up @@ -68,9 +70,18 @@ let app_dir =
"Directory where the app has been generated by the $(b,learn-ocaml build) \
command, and from where it will be served."

let base_url =
let open Cmdliner.Arg in
value & opt string "" &
info ["base-url"] ~docv:"BASE_URL" ~env:(env_var "LEARNOCAML_BASE_URL") ~doc:
"Set the base URL of the website. \
Should not end with a trailing slash. \
Currently, this has no effect on the backend - '$(b,learn-ocaml serve)'. \
Mandatory for '$(b,learn-ocaml build)' if the site is not hosted in path '/', \
which typically occurs for static deployment."

let main_cmd =
Cmdliner.Term.(const main $ Learnocaml_server_args.term app_dir),
Cmdliner.Term.(const main $ Learnocaml_server_args.term app_dir base_url),
Cmdliner.Term.info
~man
~doc:"Learn-ocaml web-app manager"
Expand Down
8 changes: 8 additions & 0 deletions src/server/learnocaml_server.ml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@ let cert_key_files = ref None

let log_channel = ref (Some stdout)

let base_url = ref ""

let args = Arg.align @@
[ "-static-dir", Arg.Set_string static_dir,
"PATH where static files should be found (./www)" ;
"-sync-dir", Arg.Set_string sync_dir,
"PATH where sync tokens are stored (./sync)" ;
"-base-url", Arg.Set_string base_url,
"BASE_URL of the website. \
Should not end with a trailing slash. \
Currently, this has no effect on the native backend. \
Mandatory for 'learn-ocaml build' if the site is not hosted in path '/', \
which typically occurs for static deployment." ;
"-port", Arg.Set_int port,
"PORT the TCP port (8080)" ]

Expand Down
1 change: 1 addition & 0 deletions src/server/learnocaml_server.mli
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

val port: int ref
val cert_key_files: (string * string) option ref
val base_url: string ref

val args: (Arg.key * Arg.spec * Arg.doc) list

Expand Down
2 changes: 1 addition & 1 deletion static/exercise.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</div>
<script language="JavaScript">
var n = Math.floor (Math.random () * 8.99) + 1;
document.getElementById('chamo-img').src = learnocaml_config.root + '/icons/tryocaml_loading_' + n + '.gif';
document.getElementById('chamo-img').src = learnocaml_config.baseUrl + '/icons/tryocaml_loading_' + n + '.gif';
</script>
<!-- Anything below could be recreated dynamically, but IDs must be kept. -->
<div id="learnocaml-exo-toolbar">
Expand Down
2 changes: 1 addition & 1 deletion static/partition-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</div>
<script language="JavaScript">
var n = Math.floor (Math.random () * 8.99) + 1;
document.getElementById('chamo-img').src = '/icons/tryocaml_loading_' + n + '.gif';
document.getElementById('chamo-img').src = learnocaml_config.baseUrl + '/icons/tryocaml_loading_' + n + '.gif';
</script>
<!-- Anything below could be recreated dynamically, but IDs must be kept. -->
<div id="learnocaml-exo-toolbar">
Expand Down
2 changes: 1 addition & 1 deletion static/playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
</div>
<script language="JavaScript">
var n = Math.floor (Math.random () * 8.99) + 1;
document.getElementById('chamo-img').src = learnocaml_config.root + '/icons/tryocaml_loading_' + n + '.gif';
document.getElementById('chamo-img').src = learnocaml_config.baseUrl + '/icons/tryocaml_loading_' + n + '.gif';
</script>
<!-- Anything below could be recreated dynamically, but IDs must be kept. -->
<div id="learnocaml-exo-toolbar">
Expand Down
2 changes: 1 addition & 1 deletion static/student-view.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</div>
<script language="JavaScript">
var n = Math.floor (Math.random () * 8.99) + 1;
document.getElementById('chamo-img').src = '/icons/tryocaml_loading_' + n + '.gif';
document.getElementById('chamo-img').src = learnocaml_config.baseUrl + '/icons/tryocaml_loading_' + n + '.gif';
</script>
<!-- Anything below could be recreated dynamically, but IDs must be kept. -->
<div id="learnocaml-exo-toolbar">
Expand Down