diff --git a/CHANGES.md b/CHANGES.md index 94b161a..5585a1a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## Added +- Added an `hello` command to generate a tutorial project - Added a `parse_binaries` stanza that can be `true` to force Spin to parse binary files - Added a `raw_files` stanza that takes a list of file or glob expressions to instruct Spin to copy files instead of parsing them - Added a new `c-bindings` template for C bindings using `ctypes` diff --git a/bin/commands/cmd_hello.ml b/bin/commands/cmd_hello.ml new file mode 100644 index 0000000..47eea54 --- /dev/null +++ b/bin/commands/cmd_hello.ml @@ -0,0 +1,67 @@ +open Spin + +let run ~path = + let open Result.Syntax in + let path = Option.value path ~default:Filename.current_dir_name in + let* () = + try + match Sys.readdir path with + | [||] -> + Ok () + | _ -> + Error + (Spin_error.failed_to_generate "The output directory is not empty.") + with + | Sys_error _ -> + Sys.mkdir_p path; + Ok () + in + try + let* template = Template.read (Template.Official Spin_template.hello) in + Template.generate ~path template + with + | Sys.Break | Failure _ -> + exit 1 + | e -> + raise e + +(* Command line interface *) + +open Cmdliner + +let doc = "Generate a tutorial project in the given directory" + +let sdocs = Manpage.s_common_options + +let exits = Common.exits + +let envs = Common.envs + +let man_xrefs = [ `Main; `Cmd "new" ] + +let man = + [ `S Manpage.s_description + ; `P + "$(tname) generates a tutorial project. This is useful if this is your \ + first project with OCaml and you want to learn by example." + ; `P + "If you are already familiar with the typical OCaml development \ + environment, use spin-new(1) instead." + ] + +let info = Term.info "hello" ~doc ~sdocs ~exits ~envs ~man ~man_xrefs + +let term = + let open Common.Syntax in + let+ _term = Common.term + and+ path = + let doc = + "The path where the project will be generated. If absent, the project \ + will be generated in the current working directory." + in + let docv = "PATH" in + Arg.(value & pos 0 (some string) None & info [] ~doc ~docv) + in + run ~path |> Common.handle_errors + +let cmd = term, info diff --git a/bin/commands/cmd_new.ml b/bin/commands/cmd_new.ml index d37aaf1..69f293e 100644 --- a/bin/commands/cmd_new.ml +++ b/bin/commands/cmd_new.ml @@ -37,7 +37,6 @@ let run ~ignore_config ~use_defaults ~template ~path = in match Template.source_of_string template with | Some source -> - let open Result.Syntax in (try let* template = Template.read ?context ~use_defaults source in Template.generate ~path template diff --git a/bin/main.ml b/bin/main.ml index 054cf57..19c3869 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -2,7 +2,7 @@ open Cmdliner let () = Printexc.record_backtrace true -let cmds = [ Cmd_config.cmd; Cmd_ls.cmd; Cmd_new.cmd ] +let cmds = [ Cmd_config.cmd; Cmd_ls.cmd; Cmd_new.cmd; Cmd_hello.cmd ] let run () = let message = @@ -16,6 +16,7 @@ Available Commands: config Update the current user's configuration ls List the official templates new Generate a new project from a template + hello Generate the tutorial project Useful options: --help Show manual page diff --git a/script/demo-record.sh b/script/demo-record.sh index 58e8d84..72916ed 100755 --- a/script/demo-record.sh +++ b/script/demo-record.sh @@ -13,4 +13,4 @@ cd "$TEMP_DIR" asciinema rec -c "$DIR/demo-emulate.sh" -# cat /var/folders/1k/w8wtfpk909s_mvn_72q6d2p40000gn/T/tmpl3vpup57-ascii.cast | svg-term --out "doc/demo.svg" --window --no-cursor --width 80 --height 24 +# cat /var/folders/1k/w8wtfpk909s_mvn_72q6d2p40000gn/T/tmp8dhvupt4-ascii.cast | svg-term --out "doc/demo.svg" --window --no-cursor --width 80 --height 24 diff --git a/template/dune b/template/dune index 3ed9281..64009be 100644 --- a/template/dune +++ b/template/dune @@ -51,3 +51,11 @@ (source_tree js))) (action (run %{bin:ocaml-crunch} -m plain js -o %{targets}))) + +(rule + (targets hello.ml) + (deps + (:data + (source_tree hello))) + (action + (run %{bin:ocaml-crunch} -m plain hello -o %{targets}))) diff --git a/template/hello/README.md b/template/hello/README.md new file mode 100644 index 0000000..39d34bb --- /dev/null +++ b/template/hello/README.md @@ -0,0 +1,7 @@ +# bin + +Native project containing a binary. + +```bash +spin new bin +``` diff --git a/template/hello/spin b/template/hello/spin new file mode 100644 index 0000000..c0bd984 --- /dev/null +++ b/template/hello/spin @@ -0,0 +1,15 @@ +(name hello) + +(description "Tutorial template without any configuration") + +(post_gen + (actions + (run make deps) + (run make build)) + (message "🎁 Installing packages globally. This might take a couple minutes.")) + +(example_commands + (commands + ("make deps" "Download runtime and development dependencies.") + ("make build" "Build the dependencies and the project.") + ("make test" "Starts the test runner."))) diff --git a/template/hello/template/.gitignore b/template/hello/template/.gitignore new file mode 100644 index 0000000..62731f9 --- /dev/null +++ b/template/hello/template/.gitignore @@ -0,0 +1,9 @@ +# Dune generated files +_build/ +*.install + +# Merlin configuring file for Vim and Emacs +.merlin + +# Local OPAM switch +_opam/ diff --git a/template/hello/template/.ocamlformat b/template/hello/template/.ocamlformat new file mode 100644 index 0000000..970b054 --- /dev/null +++ b/template/hello/template/.ocamlformat @@ -0,0 +1,9 @@ +# Pin the version of ocamlformat. The formatting will fail if a version other than 0.18.0 is used. +# This ensures that the formatting does not depend on the specific version of ocamlformat developers use. +version = 0.18.0 +# The conventional profile is fairly standard, but you can also try `sparse` or `janestreet`. +profile = conventional +# Format the code source found in docstrings +parse-docstrings = true +# Insert break lines for docstring that are longer than 80 characters. +wrap-comments = true diff --git a/template/hello/template/LICENSE b/template/hello/template/LICENSE new file mode 100644 index 0000000..ae2263f --- /dev/null +++ b/template/hello/template/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2021 Your name + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/template/hello/template/Makefile b/template/hello/template/Makefile new file mode 100644 index 0000000..b0adb82 --- /dev/null +++ b/template/hello/template/Makefile @@ -0,0 +1,77 @@ +# This Makefile offers simpler frontend to the different tools for common development tasks. +# You'll see that all of the commands are prepended by `opam exec -- ...` to run use the +# tools installed in your Opam switch without having to run `eval $(opam env)` +.DEFAULT_GOAL := all + +ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) +$(eval $(ARGS):;@:) + +.PHONY: all +all: + opam exec -- dune build --root . @install + +.PHONY: deps +deps: ## Install development dependencies + opam install -y dune-release ocamlformat utop ocaml-lsp-server + opam install --deps-only --with-test --with-doc -y . + +.PHONY: create_switch +create_switch: ## Create an opam switch without any dependency + opam switch create . --no-install + +.PHONY: switch +switch: ## Create an opam switch and install development dependencies + opam install . --deps-only --with-doc --with-test + opam install -y dune-release ocamlformat utop ocaml-lsp-server + +.PHONY: lock +lock: ## Generate a lock file + opam lock -y . + +.PHONY: build +build: ## Build the project, including non installable libraries and executables + opam exec -- dune build --root . + +.PHONY: install +install: all ## Install the packages on the system + opam exec -- dune install --root . + +.PHONY: start +start: all ## Run the produced executable + opam exec -- dune exec --root . bin/main.exe $(ARGS) + +.PHONY: test +test: ## Run the unit tests + opam exec -- dune runtest --root . + +.PHONY: clean +clean: ## Clean build artifacts and other generated files + opam exec -- dune clean --root . + +.PHONY: doc +doc: ## Generate odoc documentation + opam exec -- dune build --root . @doc + +.PHONY: servedoc +servedoc: doc ## Open odoc documentation with default web browser + open _build/default/_doc/_html/index.html + +.PHONY: fmt +fmt: ## Format the codebase with ocamlformat + opam exec -- dune build --root . --auto-promote @fmt + +.PHONY: watch +watch: ## Watch for the filesystem and rebuild on every change + opam exec -- dune build --root . --watch + +.PHONY: utop +utop: ## Run a REPL and link with the project's libraries + opam exec -- dune utop --root . lib -- -implicit-bindings + +.PHONY: release +release: all ## Run the release script + opam exec -- dune-release tag + opam exec -- dune-release distrib + opam exec -- dune-release publish distrib -y + opam exec -- dune-release opam pkg + opam exec -- dune-release opam submit --no-auto-open -y diff --git a/template/hello/template/README.md b/template/hello/template/README.md new file mode 100644 index 0000000..9158fbf --- /dev/null +++ b/template/hello/template/README.md @@ -0,0 +1,89 @@ +# Hello + +This project serves as a tutorial for your first OCaml project. + +It will guide you through the typical project structure and OCaml development workflow. +Although the organisation and tools used by the community vary, the ones used here are fairly standard and are the recommended way to work with OCaml with the [OCaml Platform](https://ocaml.org/platform/). + +If you are already familiar with the setup and are looking to start a new project, you should probably use one of Spin's official template (list them with `spin ls`). + +## Setup your development environment + +You need Opam, you can install it by following [Opam's documentation](https://opam.ocaml.org/doc/Install.html). + +With Opam installed, you can install the dependencies in a new local switch with: + +```bash +make switch +``` + +Or globally, with: + +```bash +make deps +``` + +Both of these commands will install the tools you typically need for you IDE (e.g. `ocaml-lsp-server`, `ocamlformat`). +Once the installation is complete, you can open the project in your IDE with the `vscode-ocaml-platform` extension. + +Finally, build the project with: + +```bash +make build +``` + +### Running Binary + +After building the project, you can run the main binary that is produced. + +```bash +make start +``` + +### Running Tests + +You can run the test compiled executable: + +```bash +make test +``` + +### Building documentation + +Documentation for the libraries in the project can be generated with: + +```bash +make doc +make servedoc +``` + +### Project Structure + +The following snippet describes the project structure. + +```text +. +├── bin/ +| Source for the executable. This links to the library defined in `lib/`. +│ +├── lib/ +| Source for library of the project. +│ +├── test/ +| Unit tests and integration tests. +│ +├── dune-project +| Dune file used to mark the root of the project and define project-wide parameters. +| For the documentation of the syntax, see https://dune.readthedocs.io/en/stable/dune-files.html#dune-project +│ +├── LICENSE +│ +├── Makefile +| Make file containing common development command. +│ +├── README.md +│ +└── hello.opam + Opam package definition. + To know more about creating and publishing opam packages, see https://opam.ocaml.org/doc/Packaging.html. +``` diff --git a/template/hello/template/bin/dune b/template/hello/template/bin/dune new file mode 100644 index 0000000..2a0a8de --- /dev/null +++ b/template/hello/template/bin/dune @@ -0,0 +1,10 @@ +(executable + ; The name of th executable. + ; This is only the development name, but if user install the package, it will + ; be accessible with the name defined in `public_name` + (name main) + (public_name hello) + ; The executable depends on the library defined in `lib/`. + ; This make the module `Hello` and its exposed functions available in + ; all of the modules in this directory. + (libraries hello)) diff --git a/template/hello/template/bin/main.ml b/template/hello/template/bin/main.ml new file mode 100644 index 0000000..367462a --- /dev/null +++ b/template/hello/template/bin/main.ml @@ -0,0 +1,3 @@ +let () = + let greeting = Hello.greet "world" in + print_endline greeting diff --git a/template/hello/template/bin/main.mli b/template/hello/template/bin/main.mli new file mode 100644 index 0000000..dff1e41 --- /dev/null +++ b/template/hello/template/bin/main.mli @@ -0,0 +1,4 @@ +(** Main entry point for our application. + + This is left empty intentionnally, because executables should not have any + interfaces. *) diff --git a/template/hello/template/dune b/template/hello/template/dune new file mode 100644 index 0000000..c06ca5b --- /dev/null +++ b/template/hello/template/dune @@ -0,0 +1,11 @@ +; This defines some global environment configuration for Dune. +; Here, we add some compiler flags in development to add warnings +; on compilation, and fail the compilation when there are warnings. +; If you want to deactivate some warning or error, you can add them +; here, for instance `-w +A-48-42-44-4` will deactivate warning 4 in +; addition to the ones already listed. + +(env + (dev + (flags + (:standard -w +A-48-42-44 -warn-error +A-3)))) diff --git a/template/hello/template/dune-project b/template/hello/template/dune-project new file mode 100644 index 0000000..9e2ef96 --- /dev/null +++ b/template/hello/template/dune-project @@ -0,0 +1,50 @@ +(lang dune 2.0) +; The line above is the version of the syntax, not the actual version of the Dune binary +; used. All of the Dune version after 2.0 will be retro-compatible. + +; The name of the Dune project. Not to be confused with the name of the package, +; which is defined below: they can be different. +(name hello) + +; The URL where the documentation is deployed. +; If you release the package with dune-release (see `release` in the Makefile), +; it will ask you to deploy the documentation at this address. +(documentation "https://username.github.io/hello/") + +; The remote repository where the sources are hosted. This is used by opam, for instance +; to add a link to the repository in the package page at `opam.ocaml.org`. +(source + (github username/hello)) + +; The license of the project. +(license ISC) + +; The names of the authors of the project, typically, all of the main contributors. +(authors "Your name") + +; The names of the current maintainers of the project. +(maintainers "Your name") + +; This tells dune to generate the `.opam` files for each package listed below. +; If you prefer to manage your opam files yourself, you can delete this line and +; edit the opam files directly. +; If you need to add a field in the opam file that is not handled by dune, you can also +; create a `hello.opam.template` file with the additionnal fields you'd like to generate. +(generate_opam_files true) + +; This is used to generate the `hello.opam` file. +(package + (name hello) + (synopsis "A short description of the project") + (description "A short description of the project") + (depends + (ocaml + (>= 4.08.0)) + dune + ; You can tag dependencies with `:with-test`, `:with-doc`, or `:build`. + ; This will be used by the different tools and users that read the opam metadata. + ; For instance, users that want to install your package without the test will do + ; do so without the `--with-test` in the `opam install` command. But the CI, which + ; needs to run the unit tests, will run `opam install . --with-test`. + (alcotest :with-test) + (odoc :with-doc))) diff --git a/template/hello/template/hello.opam b/template/hello/template/hello.opam new file mode 100644 index 0000000..ab7d0eb --- /dev/null +++ b/template/hello/template/hello.opam @@ -0,0 +1,31 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "A short description of the project" +description: "A short description of the project" +maintainer: ["Your name"] +authors: ["Your name"] +license: "ISC" +homepage: "https://github.com/username/hello" +doc: "https://username.github.io/hello/" +bug-reports: "https://github.com/username/hello/issues" +depends: [ + "ocaml" {>= "4.08.0"} + "dune" + "alcotest" {with-test} + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {pinned} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: "git+https://github.com/username/hello.git" diff --git a/template/hello/template/lib/dune b/template/hello/template/lib/dune new file mode 100644 index 0000000..a168ca5 --- /dev/null +++ b/template/hello/template/lib/dune @@ -0,0 +1,12 @@ +; Define a library named `hello` for our project. +; The library does not have any dependency, so only the standard library +; will be available in the modules of the `lib/` directory. +; +; Note that when a module with the same name as the library is defined, +; it will be used as the top-level of our library. If it's not the case, +; all of the modules in the directory will be exposed in a generated top-level. + +(library + (name hello) + (public_name hello) + (libraries)) diff --git a/template/hello/template/lib/hello.ml b/template/hello/template/lib/hello.ml new file mode 100644 index 0000000..010a770 --- /dev/null +++ b/template/hello/template/lib/hello.ml @@ -0,0 +1,14 @@ +(* This file is the implementation of the Hello module. + + The interface (aka exposed API, or signature) of the module is the hello.mli + counterpart. + + When adding or changing the signature of a function here, the changes must be + reflected in the module interface as well. *) + +(* This defines a function named "greet" that takes an argument "name". + + The infix operator "^" concatenates two strings, so the function will return + a new string "Hello {name}!" where "{name}" is the value of the name + argument. *) +let greet name = "Hello " ^ name ^ "!" diff --git a/template/hello/template/lib/hello.mli b/template/hello/template/lib/hello.mli new file mode 100644 index 0000000..40d4029 --- /dev/null +++ b/template/hello/template/lib/hello.mli @@ -0,0 +1,27 @@ +(* This file is the interface (aka exposed API, or signature) for the [Hello] + module. It only serves as documenting the module and selecting the functions + to expose. The actual implementation of the functions are located in the + module implementation [hello.ml]. *) + +(** This is a docstring, as it starts with "(**", as opposed to normal comments + that start with "(*". + + The top-most docstring of the module should contain a description of the + module, what it does, how to use it, etc. + + The function-specific documentation located below the function signatures. *) + +val greet : string -> string +(** This is the docstring for the [greet] function. + + You can read more about the ocamldoc syntax used to write docstrings here: + + https://ocamlverse.github.io/content/documentation_guidelines.html + + A typical documentation for this function would be: + + Returns a greeting message. + + {4 Examples} + + {[ print_endline @@ greet "Jane" ]} *) diff --git a/template/hello/template/test/dune b/template/hello/template/test/dune new file mode 100644 index 0000000..514af67 --- /dev/null +++ b/template/hello/template/test/dune @@ -0,0 +1,13 @@ +; This defines a new test. +; The tests defined with `test` will be executed by Dune when running `dune test`. +; The test runner is also available with the `make test` command for convenience. +(test + ; The name of the test must match the module name. + ; Here for instance, the module `Hello_test` will be used. + (name hello_test) + ; The test executable links to the `hello` library defined in `lib/` + ; and the `alcotest` library, installed through Opam. + ; Alcotest is a well known test framework. There are many test frameworks + ; in OCaml, but Alcotest is fairly standard and should be a good fit for + ; most of your needs. + (libraries alcotest hello)) diff --git a/template/hello/template/test/hello_test.ml b/template/hello/template/test/hello_test.ml new file mode 100644 index 0000000..910d597 --- /dev/null +++ b/template/hello/template/test/hello_test.ml @@ -0,0 +1,28 @@ +(* We open (or import) the Alcotest module. This makes all of the functions of + the Alcotest module available. *) +open Alcotest + +(* The Alcotest tests have the signature [unit -> unit], which means they take a + unit [()] (the ML equivalent of void, i.e. nothing) as the only argument, and + return a unit. *) +let test_greet () = + (* We call the [greet] function defined in our [hello] library with the + argument "Tom" and store the result in a [greeting] variable. *) + let greeting = Hello.greet "Tom" in + let expected = "Hello Tom!" in + (* The [check] and [string] come from Alcotest. + + This line will make sure that [expected] and [greeting] have the same + value, or fail. *) + check string "same string" expected greeting + +(* This defined a new test suite. + + A test suite in Alcotest is a list of (name, flag, test_function)*) +let suite = [ ("can greet", `Quick, test_greet) ] + +(* This line runs the test runner. + + We define the name of the test runner, here "hello", and the different test + suites to run. Here, there's only one test suite called "Hello". *) +let () = Alcotest.run "hello" [ ("Hello", suite) ] diff --git a/template/spin_template.ml b/template/spin_template.ml index e438b39..e509bf4 100644 --- a/template/spin_template.ml +++ b/template/spin_template.ml @@ -42,6 +42,14 @@ module Js : Template = struct let name = "js" end +module Hello : Template = struct + include Hello + + let name = "hello" +end + +let hello : (module Template) = (module Hello) + let all : (module Template) list = [ (module Cli) ; (module Lib) diff --git a/template/spin_template.mli b/template/spin_template.mli index 8883710..5ebe8ae 100644 --- a/template/spin_template.mli +++ b/template/spin_template.mli @@ -6,5 +6,8 @@ module type Template = sig val read : string -> string option end +val hello : (module Template) +(** The hello tutorial template *) + val all : (module Template) list (** List of all of the official spin templates *)