├── .gitignore ├── .merlin ├── .ocp-indent ├── B0.ml ├── BRZO ├── CHANGES.md ├── DEVEL.md ├── LICENSE.md ├── README.md ├── _tags ├── doc ├── index-topkg-care.mld ├── index-topkg.mld └── index.mld ├── pkg ├── META └── pkg.ml ├── src-bin ├── bistro.ml ├── bistro.mli ├── browse.ml ├── browse.mli ├── build.ml ├── build.mli ├── clean.ml ├── clean.mli ├── cli.ml ├── cli.mli ├── distrib.ml ├── distrib.mli ├── doc.ml ├── doc.mli ├── help.ml ├── help.mli ├── ipc.ml ├── ipc.mli ├── issue.ml ├── issue.mli ├── lint.ml ├── lint.mli ├── log.ml ├── log.mli ├── opam.ml ├── opam.mli ├── publish.ml ├── publish.mli ├── run.ml ├── run.mli ├── status.ml ├── status.mli ├── tag.ml ├── tag.mli ├── test.ml ├── test.mli ├── topkg_bin.ml └── toy_github_delegate.ml ├── src-care ├── topkg_care.ml ├── topkg_care.mli ├── topkg_care.mllib ├── topkg_care_archive.ml ├── topkg_care_archive.mli ├── topkg_care_delegate.ml ├── topkg_care_delegate.mli ├── topkg_care_ipc.ml ├── topkg_care_ipc.mli ├── topkg_care_ocamlbuild.ml ├── topkg_care_ocamlbuild.mli ├── topkg_care_ocamlfind.ml ├── topkg_care_ocamlfind.mli ├── topkg_care_opam.ml ├── topkg_care_opam.mli ├── topkg_care_pkg.ml ├── topkg_care_pkg.mli ├── topkg_care_text.ml └── topkg_care_text.mli ├── src ├── topkg.ml ├── topkg.mli ├── topkg.mllib ├── topkg_build.ml ├── topkg_build.mli ├── topkg_cmd.ml ├── topkg_cmd.mli ├── topkg_codec.ml ├── topkg_codec.mli ├── topkg_conf.ml ├── topkg_conf.mli ├── topkg_distrib.ml ├── topkg_distrib.mli ├── topkg_fexts.ml ├── topkg_fexts.mli ├── topkg_fpath.ml ├── topkg_fpath.mli ├── topkg_install.ml ├── topkg_install.mli ├── topkg_ipc.ml ├── topkg_ipc.mli ├── topkg_log.ml ├── topkg_log.mli ├── topkg_main.ml ├── topkg_main.mli ├── topkg_opam.ml ├── topkg_opam.mli ├── topkg_os.ml ├── topkg_os.mli ├── topkg_pkg.ml ├── topkg_pkg.mli ├── topkg_publish.ml ├── topkg_publish.mli ├── topkg_result.ml ├── topkg_result.mli ├── topkg_string.ml ├── topkg_string.mli ├── topkg_test.ml ├── topkg_test.mli ├── topkg_vcs.ml └── topkg_vcs.mli ├── test ├── echo-delegate ├── test.ml └── unsupportive-delegate ├── topkg-care.opam └── topkg.opam /.gitignore: -------------------------------------------------------------------------------- 1 | _b0 2 | _build 3 | tmp 4 | *.install 5 | *.native 6 | *.byte -------------------------------------------------------------------------------- /.merlin: -------------------------------------------------------------------------------- 1 | PKG b0.kit rresult astring fmt fpath logs bos cmdliner 2 | PKG webbrowser webbrowser.cli opam-format 3 | S src 4 | S src-bin 5 | S src-care 6 | S test 7 | B _build/** 8 | -------------------------------------------------------------------------------- /.ocp-indent: -------------------------------------------------------------------------------- 1 | strict_with=always,match_clause=4,strict_else=never 2 | -------------------------------------------------------------------------------- /B0.ml: -------------------------------------------------------------------------------- 1 | open B0_kit.V000 2 | open Result.Syntax 3 | 4 | (* Note we have much more deps than in ocamlbuild here because we don't do 5 | transitive -I. *) 6 | 7 | let opam_core = B0_ocaml.libname "opam-core" 8 | let opam_format = B0_ocaml.libname "opam-format" 9 | let cmdliner = B0_ocaml.libname "cmdliner" 10 | let rresult = B0_ocaml.libname "rresult" 11 | let astring = B0_ocaml.libname "astring" 12 | let fpath = B0_ocaml.libname "fpath" 13 | let fmt = B0_ocaml.libname "fmt" 14 | let fmt_tty = B0_ocaml.libname "fmt.tty" 15 | let fmt_cli = B0_ocaml.libname "fmt.cli" 16 | let logs = B0_ocaml.libname "logs" 17 | let logs_fmt = B0_ocaml.libname "logs.fmt" 18 | let logs_cli = B0_ocaml.libname "logs.cli" 19 | let bos_setup = B0_ocaml.libname "bos.setup" 20 | let webbrowser = B0_ocaml.libname "webbrowser" 21 | let webbrowser_cli = B0_ocaml.libname "webbrowser.cli" 22 | 23 | let topkg = B0_ocaml.libname "topkg" 24 | let topkg_care = B0_ocaml.libname "topkg.care" 25 | 26 | (* Libraries *) 27 | 28 | let topkg_lib = 29 | B0_ocaml.lib topkg ~name:"topkg-lib" ~srcs:[`Dir ~/"src"] 30 | 31 | let care_requires requires = 32 | opam_core :: opam_format:: cmdliner:: rresult :: astring :: fpath :: fmt :: 33 | fmt_tty :: fmt_cli :: logs :: logs_fmt :: logs_cli:: bos_setup :: 34 | webbrowser :: webbrowser_cli :: topkg :: requires 35 | 36 | let topkg_care_lib = 37 | let srcs = [`Dir ~/"src-care"] in 38 | B0_ocaml.lib topkg_care ~srcs ~requires:(care_requires []) 39 | 40 | (* Tools *) 41 | 42 | let topkg_tool = 43 | let srcs = [`Dir ~/"src-bin"; `X ~/"src-bin/toy_github_delegate.ml" ] in 44 | B0_ocaml.exe ~public:true "topkg" ~srcs ~requires:(care_requires [topkg_care]) 45 | 46 | let toy_github_delegate = 47 | let srcs = [`File ~/"src-bin/toy_github_delegate.ml"] in 48 | let requires = [cmdliner; fpath; rresult; fmt_tty; astring; bos_setup; topkg; 49 | topkg_care] 50 | in 51 | B0_ocaml.exe ~public:true "toy-github-topkg-delegate" ~srcs ~requires 52 | 53 | 54 | (* Packs *) 55 | 56 | let base_metadata = 57 | B0_meta.empty 58 | |> ~~ B0_meta.authors ["The topkg programmers"] 59 | |> ~~ B0_meta.maintainers ["Daniel Bünzli "] 60 | |> ~~ B0_meta.homepage "https://erratique.ch/software/topkg" 61 | |> ~~ B0_meta.online_doc "https://erratique.ch/software/topkg/doc" 62 | |> ~~ B0_meta.licenses ["ISC"] 63 | |> ~~ B0_meta.repo "git+https://erratique.ch/repos/topkg.git" 64 | |> ~~ B0_meta.issues "https://github.com/dbuenzli/topkg/issues" 65 | |> ~~ B0_meta.description_tags ["packaging"; "ocamlbuild"; "org:erratique"] 66 | |> B0_meta.tag B0_opam.tag 67 | |> ~~ B0_opam.build 68 | {|[["ocaml" "pkg/pkg.ml" "build" "--pkg-name" name 69 | "--dev-pkg" "%{dev}%"]]|} 70 | let topkg = 71 | let meta = 72 | base_metadata 73 | |> ~~ B0_opam.depends 74 | [ "ocaml", {|>= "4.08.0"|}; 75 | "ocamlfind", {|build & >= "1.6.1"|}; 76 | "ocamlbuild", ""; ] 77 | in 78 | B0_pack.make "topkg" ~doc:"topkg package" ~meta ~locked:true [] 79 | 80 | let topkg_care = 81 | let meta = 82 | base_metadata 83 | |> ~~ B0_release.src_archive_name (B0_pack.basename topkg) 84 | |> ~~ B0_opam.depends 85 | [ "ocaml", {|>= "4.08.0"|}; 86 | "ocamlfind", {|build & >= "1.6.1"|}; 87 | "ocamlbuild", ""; 88 | "topkg", {|= version|}; 89 | "fmt", {|>= "0.9.0"|}; 90 | "logs", ""; 91 | "bos", {|>= "0.2.1"|}; 92 | "cmdliner", {|>= "1.3.0"|}; 93 | "webbrowser", ""; 94 | "opam-format", {|>= "2.0.0"|}; 95 | ] 96 | in 97 | B0_pack.make "topkg-care" ~doc:"topkg-care package" ~meta ~locked:true [] 98 | -------------------------------------------------------------------------------- /BRZO: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbuenzli/topkg/5118b194ad08df97298b206648dd2f521b65d9e3/BRZO -------------------------------------------------------------------------------- /DEVEL.md: -------------------------------------------------------------------------------- 1 | Package structure 2 | ----------------- 3 | 4 | This repo is the base of two opam packages: 5 | 6 | - `topkg`, with opam file [topkg.opam](topkg.opam) 7 | - `topkg-care` with opam file [topkg-care.opam](topkg-care.opam) 8 | 9 | Both share the same [pkg/pkg.ml](pkg/pkg.ml) file. `topkg` simply 10 | builds the `Topkg` library and `topkg-care` builds the `Topkg_care` 11 | library and the `topkg` command line tool. The distinction is made in 12 | the `pkg.ml` file based on the package name passed to the build 13 | instructions. 14 | 15 | The reason for this structure is that while both `Topkg` and `Topkg_care` 16 | could be distributed together and `Topkg_care` be simply build 17 | depending on optional dependencies being present, this would have a 18 | fatal flaw: `topkg` cannot be used for any of `Topkg_care`'s 19 | dependencies. Since we want to use `topkg` for these dependencies 20 | aswell, this structure allows to cut the dependency cycle that would 21 | occur. 22 | 23 | So if you want to develop `topkg` you should do a: 24 | 25 | ``` 26 | opam pin add -kgit topkg topkg#master 27 | opam pin add -kgit topkg-care topkg#master 28 | ``` 29 | 30 | Changing the Topkg API 31 | ---------------------- 32 | 33 | Is a *very* delicate thing to do as it could break packages. Here's an 34 | invocation that can be used to reinstall packages that build depend on 35 | `topkg`: 36 | 37 | ``` 38 | opam reinstall $(opam list -s --installed --depends-on topkg) 39 | ``` 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Daniel C. Bünzli 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Topkg — The transitory OCaml software packager 2 | ============================================== 3 | 4 | **Warning** Topkg is in maintenance mode and should not longer be used. 5 | 6 | Topkg is a packager for distributing OCaml software. It provides an 7 | API to describe the files a package installs in a given build 8 | configuration and to specify information about the package's 9 | distribution, creation and publication procedures. 10 | 11 | The optional topkg-care package provides the `topkg` command line tool 12 | which helps with various aspects of a package's life cycle: creating 13 | and linting a distribution, releasing it on the WWW, publish its 14 | documentation, add it to the OCaml opam repository, etc. 15 | 16 | Topkg is distributed under the ISC license and has **no** 17 | dependencies. This is what your packages will need as a *build* 18 | dependency. 19 | 20 | Topkg-care is distributed under the ISC license it depends on 21 | [fmt][fmt], [logs][logs], [bos][bos], [cmdliner][cmdliner], 22 | [webbrowser][webbrowser] and `opam-format`. 23 | 24 | [fmt]: http://erratique.ch/software/fmt 25 | [logs]: http://erratique.ch/software/logs 26 | [bos]: http://erratique.ch/software/bos 27 | [cmdliner]: http://erratique.ch/software/cmdliner 28 | [webbrowser]: http://erratique.ch/software/webbrowser 29 | 30 | Home page: 31 | 32 | ## Installation 33 | 34 | Topkg and topkg-care can be installed with `opam`: 35 | 36 | opam install topkg # All you need for your packages 37 | opam install topkg-care # topkg binary, takes care of your packages 38 | 39 | If you don't use `opam` consult the [`topkg.opam`](topkg.opam) and 40 | [`topkg-care.opam`](topkg-care.opam) files for build instructions. 41 | 42 | ## Documentation 43 | 44 | A basic introduction and API reference is automatically generated by 45 | `ocamldoc` from the interfaces. It can be consulted [online][doc]. 46 | 47 | The `topkg` command line tool is extensively documented in man pages 48 | available through it's help system. Type: 49 | 50 | ``` 51 | topkg help release # for help about releasing your package 52 | topkg help # for more help 53 | ``` 54 | 55 | [doc]: http://erratique.ch/software/topkg/doc 56 | -------------------------------------------------------------------------------- /_tags: -------------------------------------------------------------------------------- 1 | true : bin_annot, safe_string 2 | 3 | <_b0> : -traverse 4 | : include 5 | 6 | : include 7 | : package(opam-format cmdliner fmt.cli logs.cli bos.setup), \ 8 | package(webbrowser webbrowser.cli) 9 | 10 | : include 11 | : include -------------------------------------------------------------------------------- /doc/index-topkg-care.mld: -------------------------------------------------------------------------------- 1 | {0 Topkg {%html: %%VERSION%%%}} 2 | 3 | {b Warning.} Topkg is in maintenance mode and should not longer be 4 | used. 5 | 6 | {1:library_topkg_care Library [topkg.care]} 7 | 8 | {!modules: Topkg_care } 9 | -------------------------------------------------------------------------------- /doc/index-topkg.mld: -------------------------------------------------------------------------------- 1 | {0 Topkg {%html: %%VERSION%%%}} 2 | 3 | {b Warning.} Topkg is in maintenance mode and should not longer be 4 | used. 5 | 6 | {1:library_topkg Library [topkg]} 7 | 8 | {!modules: Topkg} 9 | -------------------------------------------------------------------------------- /doc/index.mld: -------------------------------------------------------------------------------- 1 | {0 Topkg {%html: %%VERSION%%%}} 2 | 3 | {b Warning.} Topkg is in maintenance mode and should not longer be 4 | used. 5 | 6 | {1:library_topkg Library [topkg]} 7 | 8 | {!modules: Topkg} 9 | 10 | {1:library_topkg_care Library [topkg.care]} 11 | 12 | {!modules: Topkg_care } 13 | -------------------------------------------------------------------------------- /pkg/META: -------------------------------------------------------------------------------- 1 | description = "The transitory OCaml software packager" 2 | version = "%%VERSION_NUM%%" 3 | requires = "" 4 | archive(byte) = "topkg.cma" 5 | archive(native) = "topkg.cmxa" 6 | plugin(byte) = "topkg.cma" 7 | plugin(native) = "topkg.cmxs" 8 | 9 | package "care" ( 10 | directory = "../topkg-care" 11 | description = "Topkg package care tools" 12 | version = "%%VERSION_NUM%%" 13 | requires = "topkg opam-format cmdliner bos.setup" 14 | archive(byte) = "topkg_care.cma" 15 | archive(native) = "topkg_care.cmxa" 16 | plugin(byte) = "topkg_care.cma" 17 | plugin(native) = "topkg_care.cmxs" 18 | exists_if = "topkg_care.cma" 19 | ) 20 | -------------------------------------------------------------------------------- /pkg/pkg.ml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ocaml 2 | #use "topfind" 3 | 4 | (* Bootstrap from source, note #mod_use is 4.01 *) 5 | #directory "src" 6 | #mod_use "topkg_result.ml" 7 | #mod_use "topkg_string.ml" 8 | #mod_use "topkg_log.ml" 9 | #mod_use "topkg_fpath.ml" 10 | #mod_use "topkg_cmd.ml" 11 | #mod_use "topkg_os.ml" 12 | #mod_use "topkg_vcs.ml" 13 | #mod_use "topkg_codec.ml" 14 | #mod_use "topkg_conf.ml" 15 | #mod_use "topkg_fexts.ml" 16 | #mod_use "topkg_opam.ml" 17 | #mod_use "topkg_test.ml" 18 | #mod_use "topkg_install.ml" 19 | #mod_use "topkg_build.ml" 20 | #mod_use "topkg_distrib.ml" 21 | #mod_use "topkg_publish.ml" 22 | #mod_use "topkg_pkg.ml" 23 | #mod_use "topkg_ipc.ml" 24 | #mod_use "topkg_main.ml" 25 | #mod_use "topkg.ml" 26 | 27 | open Topkg 28 | 29 | let () = 30 | let metas = [ Pkg.meta_file ~install:false "pkg/META" ] in 31 | let opams = 32 | let install = false in 33 | let not_topkg_deps = 34 | Some ["fmt"; "logs"; "bos"; "cmdliner"; "webbrowser"; "opam-format"] 35 | in 36 | [ Pkg.opam_file ~install "topkg.opam" ~lint_deps_excluding:not_topkg_deps; 37 | Pkg.opam_file ~install "topkg-care.opam" ] 38 | in 39 | Pkg.describe ~metas ~opams "topkg" @@ fun c -> 40 | match (* bootstrap, Conf doesn't work, eqs *) Topkg_conf.pkg_name c with 41 | | "topkg" -> 42 | Ok [ Pkg.lib "pkg/META"; 43 | Pkg.lib "topkg.opam" ~dst:"opam"; 44 | Pkg.mllib ~api:["Topkg"] "src/topkg.mllib"; 45 | Pkg.doc "doc/index-topkg.mld" ~dst:"odoc-pages/index.mld"; 46 | Pkg.test "test/test"; ] 47 | | "topkg-care" -> 48 | Ok [ Pkg.lib "topkg-care.opam" ~dst:"opam"; 49 | Pkg.mllib ~api:["Topkg_care"] "src-care/topkg_care.mllib"; 50 | Pkg.bin "src-bin/topkg_bin" ~dst:"topkg"; 51 | Pkg.bin "src-bin/toy_github_delegate" 52 | ~dst:"toy-github-topkg-delegate"; 53 | Pkg.doc "doc/index-topkg-care.mld" ~dst:"odoc-pages/index.mld"; 54 | Pkg.doc "test/unsupportive-delegate"; 55 | Pkg.doc "test/echo-delegate" ] 56 | | other -> 57 | R.error_msgf "unknown package name: %s" other 58 | -------------------------------------------------------------------------------- /src-bin/bistro.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let bistro () = 9 | begin 10 | let verb = Cmd.(v "--verbosity" % Logs.(level_to_string (level ()))) in 11 | let topkg = Cmd.(v "topkg") in 12 | OS.Cmd.run Cmd.(topkg % "distrib" %% verb) 13 | >>= fun () -> OS.Cmd.run Cmd.(topkg % "publish" %% verb) 14 | >>= fun () -> OS.Cmd.run Cmd.(topkg % "opam" %% verb % "pkg") 15 | >>= fun () -> OS.Cmd.run Cmd.(topkg % "opam" %% verb % "submit") 16 | >>= fun () -> Ok 0 17 | end 18 | |> Cli.handle_error 19 | 20 | (* Command line interface *) 21 | 22 | open Cmdliner 23 | 24 | let doc = "For when you are in a hurry or need to go for a drink" 25 | let sdocs = Manpage.s_common_options 26 | let exits = Cli.exits 27 | let man_xrefs = [ `Main; `Cmd "distrib"; `Cmd "publish"; `Cmd "opam" ] 28 | let man = 29 | [ `S Manpage.s_description; 30 | `P "The $(tname) command (quick in Russian) is equivalent to invoke:"; 31 | `Pre "\ 32 | topkg distrib # Create the distribution archive 33 | topkg publish # Publish it on the WWW with its documentation 34 | topkg opam pkg # Create an opam package 35 | topkg opam submit # Submit it to OCaml's opam repository"; 36 | `P "See topkg-release(7) for more information."; ] 37 | 38 | let cmd = 39 | Cmd.v (Cmd.info "bistro" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 40 | Term.(const bistro $ Cli.setup) 41 | -------------------------------------------------------------------------------- /src-bin/bistro.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [bistro] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/browse.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | (* Targets *) 9 | 10 | let opam_doc_field = 11 | "doc", `Opam "doc", "doc opam file field" 12 | 13 | let opam_homepage_field = 14 | "homepage", `Opam "homepage", "homepage opam file field" 15 | 16 | let opam_issues_field = 17 | "issues", `Opam "bug-reports", "bug-reports opam file field" 18 | 19 | let opam_repo_field = 20 | "repo", `Opam "dev-repo", "dev-repo opam file field" 21 | 22 | let topkg_api = 23 | "topkg-api", `Uri "%%PKG_DOC%%", "topkg's API docs" 24 | 25 | let ocaml_man = 26 | "ocaml-man", `Uri "http://caml.inria.fr/pub/docs/manual-ocaml/", 27 | "OCaml manual" 28 | 29 | let ocaml_issues = 30 | "ocaml-issues", `Uri "http://caml.inria.fr/mantis/", "OCaml issue tracker" 31 | 32 | let ocamlbuild_man = 33 | "ocamlbuild-man", 34 | `Uri "https://github.com/ocaml/ocamlbuild/blob/master/manual/manual.adoc", 35 | "OCamlbuild manual" 36 | 37 | let opam_man = 38 | "opam-man", `Uri "http://opam.ocaml.org/doc/Manual.html", "opam manual" 39 | 40 | let packages = 41 | "packages", `Uri "http://opam.ocaml.org/packages/", "OCaml opam repository" 42 | 43 | let planet = 44 | "planet", `Uri "https://ocaml.org/community/planet/", "OCaml Planet" 45 | 46 | let temptation = 47 | "temptation", 48 | `Uri "https://www.\x2568\x2561\x2573\x256B\x2565\x256C\x256C.org", "" 49 | 50 | let caml_list = 51 | "caml-list", `Uri "http://news.gmane.org/gmane.comp.lang.caml.inria", 52 | "Main OCaml mailing list" 53 | 54 | let weekly_news = 55 | "weekly-news", `Uri "http://alan.petitepomme.net/cwn/", "OCaml Weekly News" 56 | 57 | let targets = 58 | [ opam_doc_field; opam_homepage_field; opam_issues_field; opam_repo_field; 59 | topkg_api; ocaml_man; ocaml_issues; ocamlbuild_man; opam_man; packages; 60 | planet; temptation; caml_list; weekly_news; ] 61 | 62 | let parse_target, max_target_len = 63 | let add (acc, len) (t, v, _) = (t, v) :: acc, max len (String.length t) in 64 | let index, max = List.fold_left add ([], 0) targets in 65 | (* This gives us trie lookup *) 66 | Cmdliner.Arg.conv_parser (Cmdliner.Arg.enum index), max 67 | 68 | (* opam field uris *) 69 | 70 | let opam_field_uri opam field = 71 | Topkg_care.Opam.File.fields opam 72 | >>= fun fields -> match String.Map.find field fields with 73 | | Some (uri :: _) -> Ok uri 74 | | Some [] -> R.error_msgf "%a: field %s is empty" Fpath.pp opam field 75 | | None -> R.error_msgf "%a: field %s is undefined" Fpath.pp opam field 76 | 77 | (* Browse command *) 78 | 79 | let browse () pkg_file opam browser prefix background target = 80 | begin 81 | let uri = match parse_target target with 82 | | Ok (`Uri uri) -> Ok uri 83 | | Ok (`Opam field) -> 84 | let pkg = Topkg_care.Pkg.v ?opam pkg_file in 85 | Topkg_care.Pkg.opam pkg >>= fun opam -> opam_field_uri opam field 86 | | Error msg -> 87 | let uri_prefixes = ["http://"; "https://"; "file://"] in 88 | if List.exists (fun p -> String.is_prefix ~affix:p target) uri_prefixes 89 | then Ok target 90 | else Error msg 91 | in 92 | uri 93 | >>= fun uri -> Webbrowser.reload ~background ~prefix ?browser uri 94 | >>= fun () -> Ok 0 95 | end 96 | |> Cli.handle_error 97 | 98 | (* Command line interface *) 99 | 100 | open Cmdliner 101 | 102 | let target = 103 | let doc = "Target to browse, see above for the list of targets." in 104 | Arg.(value & pos 0 string "homepage" & info [] ~doc ~docv:"TARGET or URI") 105 | 106 | let doc = "Browse the package's WWW links" 107 | let sdocs = Manpage.s_common_options 108 | let exits = Cli.exits 109 | let man_xrefs = [ `Main ] 110 | let man = 111 | let target acc (t, _, doc) = 112 | if doc = "" then acc else 113 | let pad = String.v ~len:(max_target_len - String.length t) (fun _ -> ' ') in 114 | `Pre (strf "%s$(b,%s) %s" pad t doc) :: `Noblank :: acc 115 | in 116 | [ `S Manpage.s_description; 117 | `P "The $(tname) command opens or reloads URIs mentioned in the 118 | opam file in a WWW browser. A few other useful logical target are 119 | provided and arbitrary file, http or https schemed URIs can also 120 | be specified as the target."; 121 | `Blocks (List.(tl @@ rev @@ fold_left target [] targets)); ] 122 | 123 | let cmd = 124 | Cmd.v (Cmd.info "browse" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 125 | Term.(const browse $ Cli.setup $ Cli.pkg_file $ Cli.opam $ 126 | Webbrowser_cli.browser $ Webbrowser_cli.prefix $ 127 | Webbrowser_cli.background $ target) 128 | -------------------------------------------------------------------------------- /src-bin/browse.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [browse] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/build.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let build_args pkg_name build_dir dry_run raws tests debug args = 9 | let on_some_use_opt opt to_arg = function 10 | | None -> Cmd.empty 11 | | Some value -> Cmd.(v opt % to_arg value) 12 | in 13 | let verb = Cli.propagate_verbosity_to_pkg_file () in 14 | let pkg_name = on_some_use_opt "--pkg-name" (fun x -> x) pkg_name in 15 | let build_dir = on_some_use_opt "--build-dir" Cmd.p build_dir in 16 | let dry_run = if dry_run then Cmd.(v "--dry-run") else Cmd.empty in 17 | let raws = Cmd.of_list ~slip:"--raw" raws in 18 | let tests = on_some_use_opt "--tests" String.of_bool tests in 19 | let debug = on_some_use_opt "--debug" String.of_bool debug in 20 | Cmd.(verb %% dry_run %% raws %% pkg_name %% build_dir %% tests %% debug %% 21 | Cmd.of_list args) 22 | 23 | let build () pkg_file pkg_name build_dir dry_run raws tests debug args = 24 | let pkg = Topkg_care.Pkg.v pkg_file in 25 | let args = build_args pkg_name build_dir dry_run raws tests debug args in 26 | let out = OS.Cmd.out_stdout in 27 | begin 28 | OS.Dir.current () 29 | >>= fun dir -> Topkg_care.Pkg.build pkg ~dir ~args ~out 30 | >>| (function ((), (_, `Exited 0)) -> 0 | _ -> 1) 31 | end 32 | |> Cli.handle_error 33 | 34 | (* Command line interface *) 35 | 36 | open Cmdliner 37 | 38 | let args = 39 | let doc = "Build configuration. Needs to be specified after a -- token 40 | so that the command line options do not get interpreted by 41 | $(b,topkg build) itself." 42 | in 43 | Arg.(value & pos_all string [] & info [] ~doc ~docv:"BUILD_CONF") 44 | 45 | let pkg_name = 46 | let doc = "The name $(docv) of the package (and of the opam install file). 47 | This is equivalent to specify the same option after the -- token. 48 | If absent provided by the package description." 49 | in 50 | let docv = "PKG_NAME" in 51 | Arg.(value & opt (some string) None & info ["n"; "pkg-name"] ~doc ~docv) 52 | 53 | let build_dir = 54 | let doc = "Specifies the build directory $(docv). This is equivalent to 55 | specify the same option after the -- token. If absent, provided 56 | by the package description." 57 | in 58 | let docv = "BUILD_DIR" in 59 | Arg.(value & opt (some Cli.path_arg) None & info ["build-dir"] ~doc ~docv) 60 | 61 | let dry_run = 62 | let doc = "Do not run build instructions, only determine and write the opam 63 | install file. This is equivalent to specify the same option after 64 | the -- token." 65 | in 66 | Arg.(value & flag & info ["d"; "dry-run"] ~doc) 67 | 68 | let raws = 69 | let doc = "Do not run build instructions or write the opam install file, only 70 | invoke the build system with the given $(docv) argument." 71 | in 72 | Arg.(value & opt_all string [] & info ["r"; "raw"] ~doc ~docv:"ARG") 73 | 74 | let tests = 75 | let doc = "Specifies whether tests should be built. If absent depends on the 76 | build context, true for development and false otherwise. This is 77 | equivalent to specify the same option after the -- token." 78 | in 79 | Arg.(value & opt (some bool) None & info ["tests"] ~doc ~docv:"BOOL") 80 | 81 | let debug = 82 | let doc = "Debug build. Specifies if debugging information should be 83 | saved in build artefacts. This is equivalent to specify the 84 | same option after the -- token." 85 | in 86 | let env = Cmd.Env.info "TOPKG_CONF_DEBUG" in 87 | Arg.(value & opt (some bool) None & info ["debug"] ~env ~doc ~docv:"BOOL") 88 | 89 | let doc = "Build the package" 90 | let sdocs = Manpage.s_common_options 91 | let exits = Cmd.Exit.info 1 ~doc:"on build failure." :: Cli.exits 92 | let man_xrefs = [ `Main ] 93 | let man = 94 | [ `S Manpage.s_synopsis; 95 | `P "$(mname) $(tname) [$(i,OPTION)]... [-- $(i,BUILD_CONF)...]"; 96 | `S Manpage.s_description; 97 | `P "The $(tname) command builds the package. This is equivalent to 98 | invoke:"; 99 | `Pre "ocaml ./pkg/pkg.ml build $(i,BUILD_CONF)..."; ] 100 | 101 | let cmd = 102 | Cmd.v (Cmd.info "build" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 103 | Term.(const build $ Cli.setup $ Cli.pkg_file $ pkg_name $ build_dir $ 104 | dry_run $ raws $ tests $ debug $ args) 105 | -------------------------------------------------------------------------------- /src-bin/build.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [build] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/clean.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | (* Command *) 9 | 10 | let clean_args name build_dir = 11 | let on_some_use_opt opt to_arg = function 12 | | None -> Cmd.empty 13 | | Some value -> Cmd.(v opt % to_arg value) 14 | in 15 | let verb = Cli.propagate_verbosity_to_pkg_file () in 16 | let build_dir = on_some_use_opt "--build-dir" Cmd.p build_dir in 17 | let name = on_some_use_opt "--pkg-name" (fun n -> n) name in 18 | Cmd.(verb %% name %% build_dir) 19 | 20 | let clean () pkg_file name build_dir = 21 | let pkg = Topkg_care.Pkg.v ?build_dir ?name pkg_file in 22 | let args = clean_args name build_dir in 23 | let out = OS.Cmd.out_stdout in 24 | begin 25 | OS.Dir.current () 26 | >>= fun dir -> Topkg_care.Pkg.clean pkg ~dir ~args ~out 27 | >>| (function ((), (_, `Exited 0)) -> 0 | _ -> 1) 28 | end 29 | |> Cli.handle_error 30 | 31 | (* Command line interface *) 32 | 33 | open Cmdliner 34 | 35 | let doc = "Clean the package's build" 36 | let sdocs = Manpage.s_common_options 37 | let exits = Cmd.Exit.info 1 ~doc:"on clean failure." :: Cli.exits 38 | let man_xrefs = [`Main; `Cmd "build"] 39 | let man = 40 | [ `S Manpage.s_synopsis; 41 | `P "$(mname) $(tname) [$(i,OPTION)]..."; 42 | `S Manpage.s_description; 43 | `P "The $(tname) command deletes the package's build and its opam 44 | install file. This is equivalent to invoke:"; 45 | `Pre "ocaml ./pkg/pkg.ml clean";] 46 | 47 | let cmd = 48 | Cmd.v (Cmd.info "clean" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 49 | Term.(const clean $ Cli.setup $ Cli.pkg_file $ Cli.pkg_name $ Cli.build_dir) 50 | -------------------------------------------------------------------------------- /src-bin/clean.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [clean] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/cli.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | open Cmdliner 8 | 9 | (* Converters and arguments *) 10 | 11 | let path_arg = Arg.conv Fpath.(of_string, pp) 12 | 13 | let pkg_file = 14 | let doc = "Use $(docv) as the package description file." in 15 | let docv = "FILE" in 16 | Arg.(value & opt path_arg (Fpath.v "pkg/pkg.ml") & 17 | info ["pkg-file"] ~docs:Manpage.s_common_options ~doc ~docv) 18 | 19 | let pkg_name = 20 | let doc = "The name $(docv) of the opam package. If absent provided 21 | by the package description." 22 | in 23 | let docv = "PKG_NAME" in 24 | Arg.(value & opt (some string) None & info ["n"; "pkg-name"] ~doc ~docv) 25 | 26 | let opam = 27 | let doc = "The opam file to use. If absent uses the default opam file 28 | mentioned in the package description." 29 | in 30 | let docv = "FILE" in 31 | Arg.(value & opt (some path_arg) None & info ["opam"] ~doc ~docv) 32 | 33 | let dist_name = 34 | let doc = "The name $(docv) of the package to use for the package 35 | distribution. If absent, provided by the package description." 36 | in 37 | let docv = "NAME" in 38 | Arg.(value & opt (some string) None & info ["dist-name"] ~doc ~docv) 39 | 40 | let dist_version = 41 | let doc = "The version string to use for the package distribution. 42 | If absent, provided by the VCS tag description of the 43 | HEAD commit." 44 | in 45 | let docv = "VERSION" in 46 | Arg.(value & opt (some string) None & info ["dist-version"] ~doc ~docv) 47 | 48 | let dist_file = 49 | let doc = "The package distribution archive. If absent the file 50 | $(i,BUILD_DIR)/$(i,NAME)-$(i,VERSION).tbz (see options 51 | $(b,--build-dir), $(b,--dist-name) and $(b,--dist-version))." 52 | in 53 | let docv = "FILE" in 54 | Arg.(value & opt (some path_arg) None & info ["dist-file"] ~doc ~docv) 55 | 56 | let dist_opam = 57 | let doc = "opam file to use for the distribution. If absent uses the opam 58 | file mentioned in the package description that corresponds to 59 | the distribution package name $(i,NAME) (see option 60 | $(b,--dist-name))." 61 | in 62 | let docv = "FILE" in 63 | Arg.(value & opt (some path_arg) None & info ["dist-opam"] ~doc ~docv) 64 | 65 | let dist_uri = 66 | let doc = "The distribution archive URI on the WWW. If absent, provided by the 67 | package description." 68 | in 69 | let docv = "URI" in 70 | Arg.(value & opt (some string) None & info ["dist-uri"] ~doc ~docv) 71 | 72 | let readme = 73 | let doc = "The readme to use. If absent, provided by the package 74 | description." 75 | in 76 | let docv = "FILE" in 77 | Arg.(value & opt (some path_arg) None & info ["readme"] ~doc ~docv) 78 | 79 | let change_log = 80 | let doc = "The change log to use. If absent, provided by the package 81 | description." 82 | in 83 | let docv = "FILE" in 84 | Arg.(value & opt (some path_arg) None & info ["change-log"] ~doc ~docv) 85 | 86 | let delegate = 87 | let doc = "The delegate tool $(docv) to use. If absent, see topkg-delegate(7) 88 | for the lookup procedure." 89 | in 90 | let docv = "TOOL" in 91 | let to_cmd = function None -> None | Some s -> Some (Bos.Cmd.v s) in 92 | Term.(const to_cmd $ 93 | Arg.(value & opt (some string) None & info ["delegate"] ~doc ~docv)) 94 | 95 | let build_dir = 96 | let doc = "Specifies the build directory $(docv). If absent, provided by the 97 | package description." 98 | in 99 | let docv = "BUILD_DIR" in 100 | Arg.(value & opt (some path_arg) None & info ["build-dir"] ~doc ~docv) 101 | 102 | let publish_msg = 103 | let doc = "The publication message $(docv). Defaults to the change 104 | log of the last version (see $(b,topkg log -l))." 105 | in 106 | let docv = "MSG" in 107 | Arg.(value & opt (some string) None & info ["m"; "message"] ~doc ~docv) 108 | 109 | (* Terms *) 110 | 111 | let logs_to_topkg_log_level = function 112 | | None -> None 113 | | Some Logs.App -> Some (Topkg.Log.App) 114 | | Some Logs.Error -> Some (Topkg.Log.Error) 115 | | Some Logs.Warning -> Some (Topkg.Log.Warning) 116 | | Some Logs.Info -> Some (Topkg.Log.Info) 117 | | Some Logs.Debug -> Some (Topkg.Log.Debug) 118 | 119 | let setup style_renderer log_level cwd = 120 | Fmt_tty.setup_std_outputs ?style_renderer (); 121 | Topkg.Log.set_level (logs_to_topkg_log_level log_level); 122 | Logs.set_level log_level; 123 | Logs.set_reporter (Logs_fmt.reporter ~app:Fmt.stdout ()); 124 | Logs.info (fun m -> m "topkg %%VERSION%% running"); 125 | match cwd with 126 | | None -> `Ok () 127 | | Some dir -> 128 | match OS.Dir.set_current dir with 129 | | Ok () -> `Ok () 130 | | Error (`Msg m) -> `Error (false, m) (* use cmdliner evaluation error *) 131 | 132 | let setup = 133 | let style_renderer = 134 | let env = Cmd.Env.info "TOPKG_COLOR" in 135 | Fmt_cli.style_renderer ~docs:Manpage.s_common_options ~env () 136 | in 137 | let log_level = 138 | let env = Cmd.Env.info "TOPKG_VERBOSITY" in 139 | Logs_cli.level ~docs:Manpage.s_common_options ~env () 140 | in 141 | let cwd = 142 | let doc = "Change to directory $(docv) before doing anything." in 143 | let docv = "DIR" in 144 | Arg.(value & opt (some path_arg) None & info ["C"; "pkg-dir"] 145 | ~docs:Manpage.s_common_options ~doc ~docv) 146 | in 147 | Term.(ret (const setup $ style_renderer $ log_level $ cwd)) 148 | 149 | (* Verbosity propagation. *) 150 | 151 | let propagate_verbosity_to_pkg_file () = match Logs.level () with 152 | | None -> Bos.Cmd.(v "-q") 153 | | Some Logs.Info -> Bos.Cmd.(v "-v") 154 | | Some Logs.Debug -> Bos.Cmd.(v "-v" % "-v") 155 | | Some _ -> Bos.Cmd.empty 156 | 157 | (* Error handling *) 158 | 159 | let warn_if_vcs_dirty msg = 160 | Topkg.Vcs.get () 161 | >>= fun repo -> Topkg.Vcs.is_dirty repo 162 | >>= function 163 | | false -> Ok () 164 | | true -> 165 | Logs.warn 166 | (fun m -> m "The repo is %a. %a" Topkg_care.Pp.dirty () Fmt.text msg); 167 | Ok () 168 | 169 | let handle_error = function 170 | | Ok 0 -> if Logs.err_count () > 0 then 3 else 0 171 | | Ok n -> n 172 | | Error _ as r -> Logs.on_error_msg ~use:(fun _ -> 3) r 173 | 174 | let exits = 175 | Cmd.Exit.info 3 ~doc:"on indiscriminate errors reported on stderr." :: 176 | Cmd.Exit.defaults 177 | -------------------------------------------------------------------------------- /src-bin/cli.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** {!Cmdliner} and common definitions for commands. *) 7 | 8 | open Cmdliner 9 | open Rresult 10 | 11 | (** {1 Converters and options} *) 12 | 13 | val path_arg : Fpath.t Arg.conv 14 | (** [path_arg] is a path argument converter. *) 15 | 16 | val pkg_file : Fpath.t Term.t 17 | (** A [--pkg-file] option to specify the package description file to use. *) 18 | 19 | val pkg_name : string option Term.t 20 | (** A [--pkg-name] option to specify the opam package name. *) 21 | 22 | val opam : Fpath.t option Term.t 23 | (** An [--opam] option for defining an opam file. *) 24 | 25 | val dist_name : string option Term.t 26 | (** A [--dist-name] option to define the package name of the distribution. *) 27 | 28 | val dist_version : string option Term.t 29 | (** A [--dist-version] option to define the package version. *) 30 | 31 | val dist_file : Fpath.t option Term.t 32 | (** A [--dist-file] option to define the distribution archive file. *) 33 | 34 | val dist_uri : string option Term.t 35 | (** A [--dist-uri] option to define the distribution archive URI on the WWW. *) 36 | 37 | val dist_opam : Fpath.t option Term.t 38 | (** An [--dist-opam] option to define the opam file. *) 39 | 40 | val readme : Fpath.t option Term.t 41 | (** A [--readme] option to define the readme. *) 42 | 43 | val change_log : Fpath.t option Term.t 44 | (** A [--change-log] option to define the change log. *) 45 | 46 | val opam : Fpath.t option Term.t 47 | (** An [--opam] option to define an opam file. *) 48 | 49 | val delegate : Bos.Cmd.t option Term.t 50 | (** A [--delegate] option to define the delegate. *) 51 | 52 | val build_dir : Fpath.t option Term.t 53 | (** A [--build-dir] option to define the build directory. *) 54 | 55 | val publish_msg : string option Term.t 56 | (** A [--msg] option to define a publication message. *) 57 | 58 | (** {1 Terms} *) 59 | 60 | val setup : unit Term.t 61 | (** [setup env] defines a basic setup common to all commands. The 62 | setup does, by side effect, set {!Logs} log verbosity, adjusts 63 | colored output and sets the current working directory. *) 64 | 65 | (** {1 Verbosity propagation} *) 66 | 67 | val propagate_verbosity_to_pkg_file : unit -> Bos.Cmd.t 68 | (** [propagate_verbosity_to_pkg_file ()] is 69 | a command line fragment that has the option to propagate 70 | the current log verbosity to an invocation of the package 71 | description. *) 72 | 73 | (** {1 Warnings and errors} *) 74 | 75 | val warn_if_vcs_dirty : string -> (unit, R.msg) result 76 | (** [warn_if_vcs_dirty msg] warns with [msg] if the VCS is dirty. *) 77 | 78 | val handle_error : (int, R.msg) result -> int 79 | (** [handle_error r] is [r]'s result or logs [r]'s error and returns [3]. *) 80 | 81 | val exits : Cmd.Exit.info list 82 | (** [exits] is are the exit codes common to all commands. *) 83 | -------------------------------------------------------------------------------- /src-bin/distrib.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let lint_distrib pkg ~dir = 9 | Logs.app (fun m -> m "@.Linting distrib in %a" Fpath.pp dir); 10 | Topkg_care.Pkg.lint pkg ~dir Topkg_care.Pkg.lint_all 11 | 12 | let build_distrib pkg ~dir skip_tests = 13 | Logs.app (fun m -> m "@.Building package in %a" Fpath.pp dir); 14 | let tests = if skip_tests then Cmd.empty else Cmd.(v "--tests" % "true") in 15 | let args = Cmd.(v "--dev-pkg" % "false" % "--vcs" % "false" %% tests) in 16 | let out = OS.Cmd.out_string in 17 | Topkg_care.Pkg.build pkg ~dir ~args ~out >>= function 18 | | (_, (_, `Exited 0)) -> 19 | Logs.app (fun m -> m "%a package builds" Topkg_care.Pp.status `Ok); Ok 0 20 | | (stdout, _) -> 21 | Logs.app (fun m -> m "%s@\n%a package builds" 22 | stdout Topkg_care.Pp.status `Fail); Ok 1 23 | 24 | let test_distrib pkg ~dir = 25 | Logs.app (fun m -> m "@.Running package tests in %a" Fpath.pp dir); 26 | let out = OS.Cmd.out_string in 27 | Topkg_care.Pkg.test pkg ~dir ~args:Cmd.empty ~out >>= function 28 | | (_, (_, `Exited 0)) -> 29 | Logs.app (fun m -> m "%a package tests" 30 | Topkg_care.Pp.status `Ok); Ok 0 31 | | (stdout, _) -> 32 | Logs.app (fun m -> m "%s@\n%a package tests" 33 | stdout Topkg_care.Pp.status `Fail); Ok 1 34 | 35 | let check_archive pkg ar ~skip_lint ~skip_build ~skip_tests = 36 | Topkg_care.Archive.untbz ~clean:true ar 37 | >>= fun dir -> (if skip_lint then Ok 0 else lint_distrib pkg ~dir) 38 | >>= fun c0 -> (if skip_build then Ok 0 else build_distrib pkg ~dir skip_tests) 39 | >>= fun c1 -> (if skip_tests || skip_build then Ok 0 else 40 | test_distrib pkg ~dir) 41 | >>= fun c2 -> match c0 + c1 + c2 with 42 | | 0 -> OS.Dir.delete ~recurse:true dir >>= fun () -> Ok 0 43 | | n -> Ok 1 44 | 45 | let warn_if_vcs_dirty ()= 46 | Cli.warn_if_vcs_dirty "The distribution archive may be inconsistent." 47 | 48 | let log_footprint pkg archive = 49 | Topkg_care.Pkg.name pkg 50 | >>= fun name -> Topkg_care.Pkg.version pkg 51 | >>= fun version -> Topkg.Vcs.get () 52 | >>= fun repo -> Topkg.Vcs.commit_id repo ~dirty:false ~commit_ish:"HEAD" 53 | >>= fun commit_ish -> 54 | Logs.app 55 | (fun m -> m "@.@[@[Distribution for %a@ %a@]@,@[Commit %a@]@,\ 56 | @[Archive %a@]@]" 57 | Topkg_care.Pp.name name Topkg_care.Pp.version version 58 | Topkg_care.Pp.commit commit_ish Topkg_care.Pp.path archive); 59 | Ok () 60 | 61 | let log_wrote_archive ar = 62 | Logs.app (fun m -> m "Wrote archive %a" Topkg_care.Pp.path ar); Ok () 63 | 64 | let distrib 65 | () pkg_file opam build_dir name version keep_dir skip_lint skip_build 66 | skip_tests 67 | = 68 | begin 69 | let pkg = Topkg_care.Pkg.v ?name ?version ?build_dir ?opam pkg_file in 70 | Topkg_care.Pkg.distrib_archive pkg ~keep_dir 71 | >>= fun ar -> log_wrote_archive ar 72 | >>= fun () -> check_archive pkg ar ~skip_lint ~skip_build ~skip_tests 73 | >>= fun errs -> log_footprint pkg ar 74 | >>= fun () -> warn_if_vcs_dirty () 75 | >>= fun () -> Ok errs 76 | end 77 | |> Cli.handle_error 78 | 79 | (* Command line interface *) 80 | 81 | open Cmdliner 82 | 83 | let keep_build_dir = 84 | let doc = "Keep the distribution build directory after successful archival." 85 | in 86 | Arg.(value & flag & info ["keep-build-dir"] ~doc) 87 | 88 | let skip_lint = 89 | let doc = "Do not lint the archive distribution." in 90 | Arg.(value & flag & info ["skip-lint"] ~doc) 91 | 92 | let skip_build = 93 | let doc = "Do not try to build the package from the archive." in 94 | Arg.(value & flag & info ["skip-build"] ~doc) 95 | 96 | let skip_tests = 97 | let doc = "Do not try to build and run the package tests from the archive. 98 | Implied by $(b,--skip-build)." 99 | in 100 | Arg.(value & flag & info ["skip-tests"] ~doc) 101 | 102 | let doc = "Create a package distribution archive" 103 | let sdocs = Manpage.s_common_options 104 | let exits = Cli.exits 105 | let envs = 106 | [ Cmd.Env.info "TOPKG_BZIP2" ~doc:"The $(b,bzip2) tool to use to compress the 107 | archive. Gets the archive on stdin and must output the result on 108 | standard out."; 109 | Cmd.Env.info "TOPKG_TAR" ~doc:"The $(b,tar) tool to use to unarchive a tbz 110 | archive (archive creation itself is handled by topkg)."; ] 111 | 112 | let man_xrefs = [ `Main ] 113 | let man = 114 | [ `S Manpage.s_description; 115 | `P "The $(tname) command creates a package distribution 116 | archive in the build directory of the package. The generated 117 | archive should be bit-wise reproducible. There are however a few 118 | caveats, see the section about this further down."; 119 | `P "More detailed information about the archive creation process and its 120 | customization can be found in topkg's API documentation."; 121 | `P "Once the archive is created it is unpacked in the build directory, 122 | linted and the package is built using the package description 123 | contained in the archive. The build will use the default package 124 | configuration so it may fail in the current environment 125 | without this necessarily implying an actual problem with the 126 | distribution; one should still worry about it though. 127 | These checks can be prevented by using the $(b,--skip-lint) and 128 | $(b,--skip-build) options."; 129 | `S "REPRODUCIBLE DISTRIBUTION ARCHIVES"; 130 | `P "Given the package name, the HEAD commit identifier 131 | and the version string, the $(tname) command should always 132 | generate the same archive."; 133 | `P "More precisely, files are added to the archive using a well 134 | defined order on path names. Their file permissions are either 135 | 0o775 for directories and files that are executable for the user 136 | in the HEAD repository checkout or 0o664 for those that are not. 137 | Their modification times are set to the commit date (note that if 138 | git is used, git-log(1) shows the author date which may not 139 | coincide). No other file metadata is recorded."; 140 | `P "This should ensure that the resulting archive is bit-wise 141 | identical regardless of the context in which it is 142 | created. However this may fail for one or more of the 143 | following reasons:"; 144 | `I ("Non-reproducible distribution massage", "The package 145 | distribution massaging hook relies on external factors 146 | that are not captured by the source repository checkout. 147 | For example external data files, environment variables, etc."); 148 | `I ("File paths with non US-ASCII characters", 149 | "If these paths are encoded in UTF-8, different file systems 150 | may return the paths with different Unicode normalization 151 | forms which could yield different byte serializations in the 152 | archive (note that this could be lifted at the cost of a 153 | dependency on Uunf)."); 154 | `I ("The bzip2 utility", "The archive is compressed using the bzip2 utility. 155 | Reproducibility relies on bzip2 to be a reproducible function 156 | across platforms."); 157 | `I ("Topkg changes", "Topkg could change its distribution procedure in 158 | the future, for example to correct bugs."); ] 159 | 160 | let cmd = 161 | Cmd.v (Cmd.info "distrib" ~doc ~sdocs ~exits ~envs ~man ~man_xrefs) @@ 162 | Term.(const distrib $ Cli.setup $ Cli.pkg_file $ Cli.dist_opam $ 163 | Cli.build_dir $ Cli.dist_name $ Cli.dist_version $ keep_build_dir $ 164 | skip_lint $ skip_build $ skip_tests) 165 | -------------------------------------------------------------------------------- /src-bin/distrib.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [distrib] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/doc.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let unixy_path p = 9 | (* ocamlbuild doesn't like Windows paths it seems. Try to do our best here. *) 10 | let volume, p = Fpath.split_volume p in 11 | volume ^ (String.concat ~sep:"/" (Fpath.segs p)) 12 | 13 | let copy_assets src_dir dst_dir = 14 | let copy_asset dst_dir file = match Fpath.get_ext file with 15 | | ".css" | ".svg" | ".svgz" | ".png" | ".jpeg" | ".gif" | ".woff" | ".ttf" 16 | | ".otf" | ".eot" -> 17 | begin OS.File.exists file >>= function 18 | | false -> Ok () 19 | | true -> 20 | OS.File.read file 21 | >>= fun cont -> OS.File.write Fpath.(dst_dir / filename file) cont 22 | end 23 | |> Logs.on_error_msg ~use:(fun () -> ()) 24 | | _ -> () 25 | in 26 | OS.Dir.exists src_dir >>= function 27 | | false -> Ok () 28 | | true -> 29 | OS.Dir.contents src_dir 30 | >>= fun files -> List.iter (copy_asset dst_dir) files; Ok () 31 | 32 | let copy_odig_css doc_dir dst_dir = 33 | OS.File.exists Fpath.(doc_dir / "style.css") >>= function 34 | | true -> Ok () 35 | | false -> 36 | let get_odig_etc = Cmd.(v "opam" % "var" % "odig:etc") in 37 | OS.Cmd.(run_out get_odig_etc |> to_string) >>= function 38 | | "#undefined" (* no comment *) -> Ok () 39 | | etcdir -> 40 | Fpath.of_string etcdir >>= fun etcdir -> 41 | OS.File.read Fpath.(etcdir / "ocamldoc.css") 42 | >>= fun cont -> OS.File.write Fpath.(dst_dir / "style.css") cont 43 | 44 | let doc_build_args pkg_name build_dir dev target = 45 | let verb = Cli.propagate_verbosity_to_pkg_file () in 46 | let pkg_name = Cmd.(v "--pkg-name" % pkg_name) in 47 | let build_dir = Cmd.(v "--build-dir" % Cmd.p build_dir) in 48 | let target = unixy_path target in 49 | let doc_flags = ["-docflags"; "-colorize-code,-charset,utf-8"; target ] in 50 | let raws = Cmd.of_list ~slip:"--raw" doc_flags in 51 | Cmd.(verb %% pkg_name %% build_dir %% raws) 52 | 53 | let build_doc pkg pkg_name build_dir dev = 54 | let out = OS.Cmd.to_stdout in 55 | let doc_dir = Fpath.v "doc" in 56 | let odocl = Fpath.(doc_dir / (if dev then "dev.odocl" else "api.odocl")) in 57 | Ok Fpath.(set_ext ".docdir" odocl / "index.html") 58 | >>= fun target -> Ok (doc_build_args pkg_name build_dir dev target) 59 | >>= fun args -> OS.Dir.current () 60 | >>= fun dir -> Topkg_care.Pkg.build pkg ~dir ~args ~out 61 | >>= fun () -> Ok Fpath.(build_dir // parent target) 62 | >>= fun dst_dir -> copy_assets doc_dir dst_dir 63 | >>= fun () -> copy_odig_css doc_dir dst_dir 64 | >>= fun () -> Ok dst_dir 65 | 66 | let browser_reload reload ~background ~browser dir = 67 | OS.Dir.current () 68 | >>= fun cwd -> Ok Fpath.(cwd // dir) 69 | >>= fun abs_dir -> match not (reload || background) with 70 | | true -> Ok abs_dir 71 | | false -> 72 | let uri = strf "file://%s" Fpath.(to_string abs_dir) in 73 | Webbrowser.reload ~background ~prefix:true ?browser uri 74 | >>= fun () -> Ok abs_dir 75 | 76 | let doc_cmd () pkg_file name build_dir dev reload background browser = 77 | begin 78 | let pkg = Topkg_care.Pkg.v ?build_dir ?name pkg_file in 79 | Topkg_care.Pkg.name pkg 80 | >>= fun pkg_name -> Topkg_care.Pkg.build_dir pkg 81 | >>= fun build_dir -> build_doc pkg pkg_name build_dir dev 82 | >>= fun docdir -> browser_reload reload ~background ~browser docdir 83 | >>= fun abs_docdir -> 84 | Logs.app (fun m -> 85 | m "Generated %s doc in %a" 86 | (if dev then "dev" else "API") Topkg_care.Pp.path abs_docdir); 87 | Ok 0 88 | end 89 | |> Cli.handle_error 90 | 91 | (* Command line interface *) 92 | 93 | open Cmdliner 94 | 95 | let reload_browser = 96 | let doc = "Open an URI of the documentation directory or reload an 97 | existing browser tab that holds a sub-page of the documentation." 98 | in 99 | Arg.(value & flag & info ["r"; "reload-browser"] ~doc) 100 | 101 | let dev = 102 | let doc = "Build the development documentation." in 103 | Arg.(value & flag & info ["d"; "dev"] ~doc) 104 | 105 | let doc = "Build the package's API documentation" 106 | let sdocs = Manpage.s_common_options 107 | let exits = Cli.exits 108 | let man_xrefs = [ `Main ] 109 | let man = 110 | [ `S Manpage.s_description; 111 | `P "The $(tname) command builds the package's API documentation. Use 112 | the option $(b,-r) to open or refresh the documentation in 113 | a WWW browser (see $(b,--browser) for details)."; 114 | `P "$(b,WARNING.) The way this command works is at the 115 | moment very ad-hoc and ocamlbuild specific. It will 116 | change in the future."; 117 | `P "Current support relies on having a doc/ directory at the root of the 118 | distribution. The ocamlbuild file doc/api.odocl defines the API 119 | documentation and the doc/dev.odocl the development documentation. 120 | The directory can also hold CSS, PNG, JPEG, GIF, SVG, WOFF, TTF, OTF 121 | files that are copied over to the generated documentation directory."; 122 | `P "The package's build system is invoked via `--raw` with the 123 | ocamlbuild documentation targets."; 124 | `P "If the doc/ directory has no doc/style.css file but odig(1) is 125 | installed, its ocamldoc stylesheet is used." ] 126 | 127 | let cmd = 128 | Cmd.v (Cmd.info "doc" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 129 | Term.(const doc_cmd $ Cli.setup $ Cli.pkg_file $ Cli.pkg_name $ Cli.build_dir 130 | $ dev $ reload_browser $ Webbrowser_cli.background $ 131 | Webbrowser_cli.browser) 132 | -------------------------------------------------------------------------------- /src-bin/doc.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [doc] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/help.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [help] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/ipc.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let opam_fields file = 9 | begin 10 | Logs.info begin fun m -> 11 | m ~header:"IPC" "opam fields of %s with cwd %a" file 12 | (R.pp ~ok:Fpath.pp ~error:R.pp_msg) (OS.Dir.current ()) 13 | end; 14 | Fpath.of_string file 15 | >>= fun file -> Topkg_care.Opam.File.fields file 16 | >>= fun fs -> Ok (String.Map.bindings fs) 17 | >>= fun fs -> Ok (Topkg.Private.(Codec.enc Opam.File.codec fs)) 18 | >>= fun enc -> OS.File.(write dash enc) 19 | end 20 | |> R.reword_error_msg ~replace:true 21 | (fun msg -> R.msgf "ipc opam-fields: %s" msg) 22 | 23 | let ipc_answer = function 24 | | ["opam-fields"; file] -> opam_fields file 25 | | args -> R.error_msgf "ipc: unknown arguments %a" Cmd.dump (Cmd.of_list args) 26 | 27 | let ipc () args = match ipc_answer args with 28 | | Ok () -> `Ok 0 29 | | Error (`Msg msg) -> `Error (false, msg) 30 | 31 | (* Command line interface *) 32 | 33 | open Cmdliner 34 | 35 | let args = 36 | let doc = "IPC call arguments" in 37 | Arg.(value (pos_all string [] & info [] ~doc ~docv:"ARG")) 38 | 39 | let doc = "Interprocess communication with package description files" 40 | let sdocs = Manpage.s_common_options 41 | let exits = Cli.exits 42 | let man_xrefs = [`Main] 43 | let man = 44 | [ `S Manpage.s_description; 45 | `P "The $(tname) command is used by package description files. It is 46 | undocumented." ] 47 | 48 | let cmd = 49 | Cmd.v (Cmd.info "ipc" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 50 | Term.(ret (const ipc $ Cli.setup $ args)) 51 | -------------------------------------------------------------------------------- /src-bin/ipc.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [ipc] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/issue.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let get_id = function 9 | | Some id -> Ok id 10 | | None -> R.error_msgf "No issue ID specified" 11 | 12 | let get_issue_msg ~info = function 13 | | Some "" -> Ok None 14 | | Some msg -> Ok (Some (String.cuts ~sep:"\n" msg)) 15 | | None -> 16 | let is_msg s = not (String.is_prefix ~affix:"#" s) in 17 | let rec rem_white_prefix = function 18 | | l :: ls when String.for_all Char.Ascii.is_white l -> rem_white_prefix ls 19 | | ls -> ls 20 | in 21 | OS.File.tmp "topkg-issue-msg-%s" 22 | >>= fun f -> OS.File.write f info 23 | >>= fun () -> Topkg_care.Text.edit_file f 24 | >>= function 25 | | 0 -> 26 | OS.File.read f >>= fun m -> 27 | let msg = List.filter is_msg (String.cuts ~sep:"\n" m) in 28 | begin match rem_white_prefix msg with 29 | | [] -> Ok None 30 | | lines -> Ok (Some lines) 31 | end 32 | | n -> 33 | Logs.err (fun m -> m "Editor exited with non-zero error code."); 34 | Ok None 35 | 36 | (* Actions *) 37 | 38 | let issue_show pkg ~id = 39 | get_id id >>= fun id -> Topkg_care.Delegate.issue_show pkg ~id 40 | 41 | let issue_open pkg msg = 42 | let open_info = 43 | "\n\ 44 | # Please enter an issue description. The first non-blank line will be\n\ 45 | # the issue title and the rest the issue description. Lines starting\n\ 46 | # with '#' will be ignored. An empty description aborts the action." 47 | in 48 | get_issue_msg ~info:open_info msg >>= function 49 | | None -> 50 | Logs.app (fun m -> m "Open issue aborted due to empty issue message."); 51 | Ok (); 52 | | Some lines -> 53 | let title, body = match lines with 54 | | title :: body -> title, String.(trim @@ concat ~sep:"\n" body) 55 | | [] -> assert false 56 | in 57 | Topkg_care.Delegate.issue_open pkg ~title ~body 58 | 59 | let issue_close pkg ~id msg = 60 | let close_info = 61 | "\n\ 62 | # Please enter a closing message. Lines starting with '#' will\n\ 63 | # be ignored. An empty message aborts the action." 64 | in 65 | get_id id 66 | >>= fun id -> get_issue_msg ~info:close_info msg 67 | >>= function 68 | | None -> 69 | Logs.app 70 | (fun m -> m "Close issue %s aborted due to empty issue message." id); 71 | Ok () 72 | | Some lines -> 73 | let msg = String.(trim @@ concat ~sep:"\n" lines) in 74 | Topkg_care.Delegate.issue_close pkg ~id ~msg 75 | 76 | (* Command *) 77 | 78 | let issue () pkg_file opam delegate action id msg = 79 | begin 80 | let pkg = Topkg_care.Pkg.v ?opam ?delegate pkg_file in 81 | let ret = match action with 82 | | `List -> Topkg_care.Delegate.issue_list pkg 83 | | `Show -> issue_show pkg ~id 84 | | `Open -> issue_open pkg msg 85 | | `Close -> issue_close pkg ~id msg 86 | in 87 | ret >>= fun () -> Ok 0 88 | end 89 | |> Cli.handle_error 90 | 91 | (* Command line interface *) 92 | 93 | open Cmdliner 94 | 95 | let action = 96 | let action = ["list",`List; "show",`Show; "open",`Open; "close",`Close;] in 97 | let doc = strf "The action to perform. $(docv) must be one of %s." 98 | (Arg.doc_alts_enum action) 99 | in 100 | let cmd = Arg.enum action in 101 | Arg.(value & pos 0 cmd `List & info [] ~doc ~docv:"ACTION") 102 | 103 | let id = 104 | let doc = "An issue ID of the package's issue tracker." in 105 | Arg.(value & pos 1 (some string) None & info [] ~doc ~docv:"ID") 106 | 107 | let msg = 108 | let doc = "For $(b,open) and $(b,close), $(docv) is the issue message. 109 | Prevents the interactive prompt for the message." 110 | in 111 | let docv = "MSG" in 112 | Arg.(value & opt (some string) None & info ["m"; "message"] ~doc ~docv) 113 | 114 | let doc = "Interact with the package's issue tracker" 115 | let sdocs = Manpage.s_common_options 116 | let exits = Cli.exits 117 | let envs = 118 | [ Cmd.Env.info "EDITOR" ~doc:"The editor used to edit issue messages."; 119 | Cmd.Env.info "TOPKG_DELEGATE" ~doc:"The package delegate to use, see 120 | topkg-delegate(7)." ] 121 | 122 | let man_xrefs = [ `Main ] 123 | let man = 124 | [ `S Manpage.s_synopsis; 125 | `P "$(mname) $(tname) [$(i,OPTION)]... [$(i,ACTION)]..."; 126 | `S Manpage.s_description; 127 | `P "The $(tname) command interacts with the package's issue 128 | tracker via the package delegate. See topkg-delegate(7) for more 129 | details."; 130 | `P "To consult the issues in a WWW browser invoke 131 | $(b,topkg browse issues), no delegate is needed for this."; 132 | `S "ACTIONS"; 133 | `I ("$(b,list) (default)", 134 | "List open issues."); 135 | `I ("$(b,show) $(i,ID)", 136 | "Show information about issue $(i,ID)."); 137 | `I ("$(b,open)", 138 | "Open a new issue."); 139 | `I ("$(b,close) $(i,ID)", 140 | "Close issue $(i,ID).") ] 141 | 142 | let cmd = 143 | Cmd.v (Cmd.info "issue" ~doc ~sdocs ~exits ~envs ~man ~man_xrefs) @@ 144 | Term.(const issue $ Cli.setup $ Cli.pkg_file $ Cli.opam $ Cli.delegate $ 145 | action $ id $ msg) 146 | -------------------------------------------------------------------------------- /src-bin/issue.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [issue] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/lint.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let lint () pkg_file ignore_pkg lints = 9 | begin 10 | let pkg = Topkg_care.Pkg.v pkg_file in 11 | OS.Dir.current () 12 | >>= fun dir -> Topkg_care.Pkg.lint ~ignore_pkg pkg ~dir lints 13 | end 14 | |> Cli.handle_error 15 | 16 | (* Command line interface *) 17 | 18 | open Cmdliner 19 | 20 | let lints = 21 | let test = [ "custom", `Custom; 22 | "std-files", `Std_files; 23 | "meta", `Meta; 24 | "opam", `Opam; 25 | "deps", `Deps; ] 26 | in 27 | let doc = strf "Test to perform. $(docv) must be one of %s. If unspecified 28 | all tests are performed." (Arg.doc_alts_enum test) 29 | in 30 | let test = Arg.enum test in 31 | let docv = "TEST" in 32 | Arg.(value & pos_all test Topkg_care.Pkg.lint_all & info [] ~doc ~docv) 33 | 34 | let ignore_pkg = 35 | let doc = "Ignore package description file." in 36 | Arg.(value & flag & info ["i"; "ignore-pkg"] ~doc) 37 | 38 | let doc = "Check package distribution consistency and conventions" 39 | let sdocs = Manpage.s_common_options 40 | let exits = Cmd.Exit.info 1 ~doc:"on lint failure" :: Cli.exits 41 | let man_xrefs = [`Main; `Cmd "distrib"] 42 | let man = 43 | [ `S Manpage.s_description; 44 | `P "The $(tname) command makes tests on a package distribution or 45 | source repository. It checks that standard files exist, that 46 | ocamlfind META files pass the ocamlfind lint test, that opam package 47 | files pass the opam lint test and that the opam dependencies are 48 | consistent with those of the build system."; 49 | `P "Linting is automatically performed on distribution generation, see 50 | topkg-distrib(1) for more details." ] 51 | 52 | let cmd = 53 | Cmd.v (Cmd.info "lint" ~doc ~sdocs ~exits ~man ~man_xrefs) 54 | Term.(const lint $ Cli.setup $ Cli.pkg_file $ ignore_pkg $ lints) 55 | -------------------------------------------------------------------------------- /src-bin/lint.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [lint] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/log.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | (* Actions *) 9 | 10 | let show change_log last last_version no_pager = 11 | let text = 12 | if not (last || last_version) then OS.File.read change_log else 13 | (Topkg_care.Text.change_log_file_last_entry change_log 14 | >>= fun (v, (h, t)) -> Ok (if last_version then v else strf "%s\n%s" h t)) 15 | in 16 | text 17 | >>= fun text -> Topkg_care.Text.find_pager ~don't:(no_pager || last_version) 18 | >>= function 19 | | None -> Logs.app (fun m -> m "%s" text); Ok () 20 | | Some pager -> OS.Cmd.(in_string text |> run_in pager) 21 | 22 | let commit change_log = 23 | let change_log = Fpath.to_string change_log in 24 | Topkg.Vcs.get () 25 | >>= fun repo -> Topkg.Vcs.file_is_dirty repo change_log 26 | >>= function 27 | | true -> Topkg.Vcs.commit_files repo ~msg:"Update change log." [change_log] 28 | | false -> 29 | Logs.app (fun m -> m "No changes to commit in %s" change_log); Ok () 30 | 31 | (* Command *) 32 | 33 | let log () pkg_file change_log action last last_version no_pager = 34 | begin 35 | let pkg = Topkg_care.Pkg.v ?change_log pkg_file in 36 | Topkg_care.Pkg.change_log pkg 37 | >>= fun change_log -> match action with 38 | | `Show -> show change_log last last_version no_pager >>= fun () -> Ok 0 39 | | `Edit -> Topkg_care.Text.edit_file change_log 40 | | `Commit -> commit change_log >>= fun () -> Ok 0 41 | end 42 | |> Cli.handle_error 43 | 44 | (* Command line interface *) 45 | 46 | open Cmdliner 47 | 48 | let action = 49 | let action = [ "show", `Show; "edit", `Edit; "commit", `Commit] in 50 | let doc = strf "The action to perform. $(docv) must be one of %s." 51 | (Arg.doc_alts_enum action) 52 | in 53 | let cmd = Arg.enum action in 54 | Arg.(value & pos 0 cmd `Show & info [] ~doc ~docv:"ACTION") 55 | 56 | let no_pager = 57 | let doc = "Do not pipe the output into a pager. This automatically 58 | happens if the TERM environment variable is 'dumb' or undefined." 59 | in 60 | Arg.(value & flag & info ["no-pager"] ~doc) 61 | 62 | let last = 63 | let doc = "Show only the change log of the last version. Extracted as the 64 | first marked up section of the change log." 65 | in 66 | Arg.(value & flag & info ["l"; "last"] ~doc) 67 | 68 | let last_version = 69 | let doc = "Show only the version string of the last version. Extracted as 70 | the first token of the title of the first marked up section of 71 | the change log. Implies $(b,--no-pager)."; 72 | in 73 | Arg.(value & flag & info ["t"; "last-version"] ~doc) 74 | 75 | let doc = "Show and edit the package's change log" 76 | let sdocs = Manpage.s_common_options 77 | let exits = Cli.exits 78 | let envs = 79 | [ Cmd.Env.info "EDITOR" ~doc:"The editor used to edit the change log."; 80 | Cmd.Env.info "PAGER" ~doc:"The pager used to consult the change log."; 81 | Cmd.Env.info "TERM" ~doc:"See option $(b,--no-pager)." ] 82 | 83 | let man_xrefs = [ `Main; `Cmd "publish"; `Cmd "tag"; `Cmd "opam"; ] 84 | let man = 85 | [ `S Manpage.s_description; 86 | `P "The $(tname) command shows, edits and commits 87 | the package's change log."; 88 | `S "CHANGE LOG FORMAT"; 89 | `P "To be able to extract the version and changes of the last distribution, 90 | a well defined change log format is assumed. Not abiding to the 91 | format is not catastrophic but may hinder or derail some facilities 92 | provided by topkg."; 93 | `P "The format assumes that the change log is written either in Markdown 94 | (default or .md extension) or Asciidoc (.asciidoc or .adoc extension). 95 | A change log is a list of marked up sections. A section is 96 | a header of any level until the next header at the same level or 97 | the end of file. For example here are two Markdown sections:"; 98 | `Pre "\ 99 | v2.0.0 100 | ------ 101 | ### New features 102 | etc. 103 | ### Breaking changes 104 | etc. 105 | 106 | v1.6.0 1995-09-12 107 | ----------------- 108 | etc."; 109 | `P "The first marked up section in the file is taken as being the 110 | change log for the last distribution; use $(b,topkg log -l) 111 | to check that it is parsed correctly. This is used by topkg-publish(1) 112 | and topkg-opam(1) to enrich distribution publication."; 113 | `P "The first token of the first section header title is taken as being the 114 | version string of the distribution; use $(b,topkg log -t) to check 115 | that it is parsed correctly. It is used by topkg-tag(1) to tag the 116 | source repository."; 117 | `S "ACTIONS"; 118 | `I ("$(b,show) (default)", "shows the package's change log."); 119 | `I ("$(b,edit)", "edit the package's change log."); 120 | `I ("$(b,commit)", "commit changes made to the package's change log to the 121 | VCS.") ] 122 | 123 | let cmd = 124 | Cmd.v (Cmd.info "log" ~doc ~sdocs ~exits ~envs ~man) @@ 125 | Term.(const log $ Cli.setup $ Cli.pkg_file $ Cli.change_log $ action $ 126 | last $ last_version $ no_pager) 127 | -------------------------------------------------------------------------------- /src-bin/log.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [log] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/opam.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let get_opam_publish_file p opam_publish_file = match opam_publish_file with 9 | | Some file -> Ok file 10 | | None -> 11 | Topkg_care.Pkg.build_dir p 12 | >>= fun bdir -> Topkg_care.Pkg.distrib_filename ~opam:true p 13 | >>= fun fname -> Ok Fpath.(bdir // fname + ".opam") 14 | 15 | let descr pkg = 16 | Topkg_care.Pkg.opam_descr pkg >>= fun (descr, _) -> 17 | Logs.app (fun m -> m "%s" (Topkg_care.Opam.Descr.to_string descr)); 18 | Ok 0 19 | 20 | let pkg pkg dist_pkg opam_publish_file = 21 | let log_pkg dst = 22 | Logs.app (fun m -> m "Wrote opam package %a" Topkg_care.Pp.path dst) 23 | in 24 | let warn_if_vcs_dirty () = 25 | Cli.warn_if_vcs_dirty "The opam package may be inconsistent with the \ 26 | distribution." 27 | in 28 | let opam_file_content opam (descr, from_opam) url = 29 | let descr = 30 | if from_opam then "" else 31 | (Topkg_care.Opam.Descr.to_opam_fields descr ^ "\n") 32 | in 33 | strf "%s\n%s%s" opam descr (Topkg_care.Opam.Url.to_opam_section url) 34 | in 35 | get_opam_publish_file pkg opam_publish_file 36 | >>= fun dst -> Topkg_care.Pkg.opam pkg 37 | >>= fun opam -> OS.File.read opam 38 | >>= fun opam -> Topkg_care.Pkg.opam_descr pkg 39 | >>= fun descr -> Topkg_care.Pkg.distrib_file dist_pkg 40 | >>= fun distrib_file -> Topkg_care.Pkg.distrib_uri dist_pkg 41 | >>= fun uri -> Topkg_care.Opam.Url.with_distrib_file ~uri distrib_file 42 | >>= fun url -> OS.File.write dst (opam_file_content opam descr url) 43 | >>= fun () -> log_pkg dst; warn_if_vcs_dirty () 44 | >>= fun () -> 45 | Ok 0 46 | 47 | let submit pkg opam_pkg_dst = 48 | Topkg_care.Opam.ensure_publish () 49 | >>= fun () -> get_opam_publish_file pkg opam_pkg_dst 50 | >>= fun opam_file -> OS.File.exists opam_file 51 | >>= function 52 | | false -> 53 | Logs.err (fun m -> m "Package@ file %a@ does@ not@ exist. Did@ you@ \ 54 | forget@ to@ invoke 'topkg opam pkg' ?" 55 | Fpath.pp opam_file); 56 | Ok 1 57 | | true -> 58 | Logs.app (fun m -> m "Publishing %a" Topkg_care.Pp.path opam_file); 59 | Topkg_care.Pkg.publish_msg pkg 60 | >>= fun msg -> Topkg_care.Opam.submit ~opam_file ~msg () 61 | >>= fun () -> Ok 0 62 | 63 | let field pkg field = match field with 64 | | None -> Logs.err (fun m -> m "Missing FIELD positional argument"); Ok 1 65 | | Some field -> 66 | Topkg_care.Pkg.opam_field pkg field 67 | >>= function 68 | | Some v -> Logs.app (fun m -> m "%s" (String.concat ~sep:" " v)); Ok 0 69 | | None -> 70 | Topkg_care.Pkg.opam pkg >>= fun opam -> 71 | Logs.err (fun m -> m "%a: field %s is undefined" Fpath.pp opam field); 72 | Ok 1 73 | 74 | (* Command *) 75 | 76 | let opam () pkg_file build_dir 77 | dist_name dist_version dist_opam dist_uri dist_file 78 | opam_publish_file pkg_name pkg_version pkg_opam pkg_descr 79 | readme change_log publish_msg action field_name 80 | = 81 | let p = 82 | Topkg_care.Pkg.v 83 | ?build_dir ?name:pkg_name ?version:pkg_version ?opam:pkg_opam 84 | ?opam_descr:pkg_descr ?readme ?change_log ?publish_msg pkg_file 85 | in 86 | begin match action with 87 | | `Descr -> descr p 88 | | `Pkg -> 89 | let dist_p = 90 | Topkg_care.Pkg.v 91 | ?build_dir ?name:dist_name ?version:dist_version ?opam:dist_opam 92 | ?distrib_uri:dist_uri ?distrib_file:dist_file ?readme ?change_log 93 | ?publish_msg pkg_file 94 | in 95 | pkg p dist_p opam_publish_file 96 | | `Submit -> submit p opam_publish_file 97 | | `Field -> field p field_name 98 | end 99 | |> Cli.handle_error 100 | 101 | (* Command line interface *) 102 | 103 | open Cmdliner 104 | 105 | let action = 106 | let action = [ "descr", `Descr; "pkg", `Pkg; "submit", `Submit; 107 | "publish", `Submit; "field", `Field ] 108 | in 109 | let doc = strf "The action to perform. $(docv) must be one of %s." 110 | (Arg.doc_alts_enum action) 111 | in 112 | let action = Arg.enum action in 113 | Arg.(required & pos 0 (some action) None & info [] ~doc ~docv:"ACTION") 114 | 115 | let field = 116 | let doc = "the field to output ($(b,field) action)" in 117 | Arg.(value & pos 1 (some string) None & info [] ~doc ~docv:"FIELD") 118 | 119 | let opam_publish_file = 120 | let doc = "The file to use to publish the opam package. If absent the 121 | file $(i,BUILD_DIR)/$(i,PKG_NAME).$(i,PKG_VERSION).opam in the 122 | build directory is used (see options $(b,--build-dir), 123 | $(b,--pkg-name) and $(b,--pkg-version))" 124 | in 125 | let docv = "FILE" in 126 | Arg.(value & opt (some Cli.path_arg) None & info ["opam-publish-file"] 127 | ~doc ~docv) 128 | 129 | let pkg_version = 130 | let doc = "The version string $(docv) of the opam package. If absent provided 131 | provided by the VCS tag description of the HEAD commit." 132 | in 133 | let docv = "PKG_NAME" in 134 | Arg.(value & opt (some string) None & info ["pkg-version"] ~doc ~docv) 135 | 136 | let pkg_opam = 137 | let doc = "The opam file to use for the opam package. If absent uses the 138 | opam file mentioned in the package description that corresponds 139 | to the opam package name $(i,PKG_NAME) (see option 140 | $(b,--pkg-name))" 141 | in 142 | let docv = "FILE" in 143 | Arg.(value & opt (some Cli.path_arg) None & info ["pkg-opam"] ~doc ~docv) 144 | 145 | let pkg_descr = 146 | let doc = "The opam descr file to use for the opam package. If absent 147 | and the opam file has synopsis and description fields this 148 | is used for the description. It absent and there are no such 149 | fields in the opam file and the opam file name 150 | (see $(b,--pkg-opam)) has a `.opam` 151 | extension, uses an existing file with the same path but a `.descr` 152 | extension. If the opam file name is `opam` uses a `descr` 153 | file in the same directory. If these files are not found 154 | a description is extracted from the the readme (see 155 | option $(b,--readme)) as follow: the first marked up 156 | section of the readme is extracted, its title is parsed 157 | according to the pattern '\\$(NAME) \\$(SEP) \\$(SYNOPSIS)', 158 | the body of the section is the long description. A few 159 | lines are filtered out: lines that start with either 160 | 'Home page:', 'Contact:' or '%%VERSION'." 161 | in 162 | let docv = "FILE" in 163 | Arg.(value & opt (some Cli.path_arg) None & info ["pkg-descr"] ~doc ~docv) 164 | 165 | let doc = "Interaction with opam and the OCaml opam repository" 166 | let sdocs = Manpage.s_common_options 167 | let envs = 168 | [ Cmd.Env.info "TOPKG_OPAM_PUBLISH" ~doc:"The $(b,opam-publish) tool to use 169 | to submit packages." ] 170 | 171 | let man_xrefs = [`Main; `Cmd "distrib" ] 172 | let man = 173 | [ `S Manpage.s_synopsis; 174 | `P "$(mname) $(tname) [$(i,OPTION)]... $(i,ACTION)"; 175 | `S Manpage.s_description; 176 | `P "The $(tname) command provides a few actions to interact with 177 | opam and the OCaml opam repository."; 178 | `S "ACTIONS"; 179 | `I ("$(b,descr)", 180 | "extract and print an opam descr file. This is used by the 181 | $(b,pkg) action. See the $(b,--pkg-descr) option for details."); 182 | `I ("$(b,pkg)", 183 | "create an opam package description for a distribution. 184 | The action needs a distribution archive to operate, see 185 | topkg-distrib(1) or the $(b,--dist-file) option."); 186 | `I ("$(b,submit) or $(b,publish)", 187 | "submits a package created with the action $(b,pkg) the OCaml 188 | opam repository. Requires the $(b,opam-publish) tool to be 189 | installed."); 190 | `I ("$(b,field) $(i,FIELD)", 191 | "outputs the field $(i,FIELD) of the package's opam file."); ] 192 | 193 | let cmd = 194 | Cmd.v (Cmd.info "opam" ~doc ~sdocs ~envs ~man ~man_xrefs) @@ 195 | Term.(const opam $ Cli.setup $ Cli.pkg_file $ Cli.build_dir $ 196 | Cli.dist_name $ Cli.dist_version $ Cli.dist_opam $ 197 | Cli.dist_uri $ Cli.dist_file $ 198 | opam_publish_file $ Cli.pkg_name $ pkg_version $ pkg_opam $ 199 | pkg_descr $ Cli.readme $ Cli.change_log $ Cli.publish_msg $ 200 | action $ field) 201 | -------------------------------------------------------------------------------- /src-bin/opam.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [opam] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/publish.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let absolute path = OS.Dir.current () >>| fun cwd -> Fpath.(cwd // path) 9 | 10 | let gen_doc dir = 11 | let do_doc () = 12 | OS.Cmd.run Cmd.(v "topkg" % "doc") 13 | >>| fun () -> Fpath.(dir / "_build" / "doc" / "api.docdir") 14 | in 15 | R.join @@ OS.Dir.with_current dir do_doc () 16 | 17 | let publish_doc pkg = 18 | Topkg_care.Pkg.distrib_file pkg 19 | >>= fun distrib_file -> Topkg_care.Pkg.publish_msg pkg 20 | >>= fun msg -> Topkg_care.Archive.untbz ~clean:true distrib_file 21 | >>= fun dir -> gen_doc dir 22 | >>= fun docdir -> absolute docdir 23 | >>= fun docdir -> Topkg_care.Delegate.publish_doc pkg ~msg ~docdir 24 | 25 | let publish_distrib pkg = 26 | Topkg_care.Pkg.distrib_file pkg 27 | >>= fun distrib_file -> Topkg_care.Pkg.publish_msg pkg 28 | >>= fun msg -> absolute distrib_file 29 | >>= fun archive -> Topkg_care.Delegate.publish_distrib pkg ~msg ~archive 30 | 31 | let publish_alt pkg kind = 32 | Topkg_care.Pkg.distrib_file pkg 33 | >>= fun distrib_file -> Topkg_care.Pkg.publish_msg pkg 34 | >>= fun msg -> absolute distrib_file 35 | >>= fun archive -> Topkg_care.Delegate.publish_alt pkg ~kind ~msg ~archive 36 | 37 | let publish () 38 | pkg_file build_dir name version opam delegate change_log distrib_uri 39 | distrib_file publish_msg publish_artefacts 40 | = 41 | begin 42 | let publish_artefacts = match publish_artefacts with 43 | | [] -> None 44 | | v -> Some v 45 | in 46 | let pkg = Topkg_care.Pkg.v ?name ?version ?build_dir ?opam ?delegate 47 | ?change_log ?distrib_uri ?distrib_file ?publish_msg 48 | ?publish_artefacts pkg_file 49 | in 50 | let publish_artefact acc artefact = 51 | acc >>= fun acc -> match artefact with 52 | | `Doc -> publish_doc pkg 53 | | `Distrib -> publish_distrib pkg 54 | | `Alt kind -> publish_alt pkg kind 55 | in 56 | Topkg_care.Pkg.publish_artefacts pkg 57 | >>= fun todo -> List.fold_left publish_artefact (Ok ()) todo 58 | >>= fun () -> Ok 0 59 | end 60 | |> Cli.handle_error 61 | 62 | (* Command line interface *) 63 | 64 | open Cmdliner 65 | 66 | let artefacts = 67 | let alt_prefix = "alt-" in 68 | let parser = function 69 | | "do" | "doc" -> Ok `Doc 70 | | "di" | "dis" | "dist" | "distr" | "distri" | "distrib" -> Ok `Distrib 71 | | s when String.is_prefix ~affix:alt_prefix s -> 72 | begin match String.(with_range ~first:(length alt_prefix) s) with 73 | | "" -> Error ("`alt-' alternative artefact kind is missing") 74 | | kind -> Ok (`Alt kind) 75 | end 76 | | s -> Error (strf "`%s' unknown publication artefact" s) 77 | in 78 | let printer ppf = function 79 | | `Doc -> Fmt.string ppf "doc" 80 | | `Distrib -> Fmt.string ppf "distrib" 81 | | `Alt a -> Fmt.pf ppf "alt-%s" a 82 | in 83 | let artefact = Arg.conv' (parser, printer) in 84 | let doc = strf "The artefact to publish. $(docv) must be one of `doc`, 85 | `distrib` or `alt-$(i,KIND)`. If absent, the set of 86 | default publication artefacts is determined by the 87 | package description." 88 | in 89 | Arg.(value & pos_all artefact [] & info [] ~doc ~docv:"ARTEFACT") 90 | 91 | let doc = "Publish package distribution archives and derived artefacts" 92 | let sdocs = Manpage.s_common_options 93 | let exits = Cli.exits 94 | let envs = 95 | [ Cmd.Env.info "TOPKG_DELEGATE" ~doc:"The package delegate to use, see 96 | topkg-delegate(7)."; ] 97 | 98 | let man_xrefs = [`Main; `Cmd "distrib" ] 99 | let man = 100 | [ `S Manpage.s_synopsis; 101 | `P "$(mname) $(tname) [$(i,OPTION)]... [$(i,ARTEFACT)]..."; 102 | `S Manpage.s_description; 103 | `P "The $(tname) command publishes package distribution archives 104 | and other artefacts via the package delegate. See topkg-delegate(7) for 105 | more details."; 106 | `P "Artefact publication always relies on a distribution archive having 107 | been generated before with topkg-distrib(1)."; 108 | `S "ARTEFACTS"; 109 | `I ("$(b,distrib)", 110 | "Publishes a distribution archive on the WWW."); 111 | `I ("$(b,doc)", 112 | "Publishes the documentation of a distribution archive on the WWW."); 113 | `I ("$(b,alt)-$(i,KIND)", 114 | "Publishes the alternative artefact of kind $(i,KIND) of 115 | a distribution archive. The semantics of alternative artefacts 116 | is left to the delegate, it could be anything, an email, 117 | a pointless tweet, a feed entry etc. See topkg-delegate(7) for 118 | more details."); ] 119 | 120 | let cmd = 121 | Cmd.v (Cmd.info "publish" ~doc ~sdocs ~exits ~envs ~man ~man_xrefs) @@ 122 | Term.(const publish $ Cli.setup $ Cli.pkg_file $ Cli.build_dir $ 123 | Cli.dist_name $ Cli.dist_version $ Cli.dist_opam $ 124 | Cli.delegate $ Cli.change_log $ Cli.dist_uri $ Cli.dist_file $ 125 | Cli.publish_msg $ artefacts) 126 | -------------------------------------------------------------------------------- /src-bin/publish.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [publish] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/run.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let pp_exec = Fmt.(quote string) 9 | 10 | let blacklist = [ ".so"; ".cmxs" ] (* Don't try to run these kind of files *) 11 | 12 | let exec_match exec p = 13 | OS.File.exists p >>= function 14 | | false -> Ok false 15 | | true -> 16 | OS.Path.Mode.get p >>= fun mode -> 17 | if mode land 0o111 = 0 then Ok false else 18 | let p_base, ext = Fpath.split_ext p in 19 | let p_base = Fpath.to_string p_base in 20 | let p = Fpath.to_string p in 21 | Ok (not (List.mem ext blacklist) && 22 | (String.is_suffix ~affix:exec p_base || 23 | String.is_suffix ~affix:exec p)) 24 | 25 | let find_exec exec dir = 26 | let ambiguous l = 27 | R.error_msgf "Ambiguous matches for %a, could match any of %a" 28 | pp_exec exec Fmt.(list ~sep:(any ", ") Fpath.pp) l 29 | in 30 | OS.Dir.exists dir >>= function 31 | | false -> R.error_msgf "Build directory %a does not exist" Fpath.pp dir 32 | | true -> 33 | let elements = `Sat (exec_match exec) in 34 | OS.Dir.fold_contents ~elements (fun p ps -> p :: ps) [] dir 35 | >>= function 36 | | [p0] -> Ok p0 37 | | [p0; p1] as l -> 38 | let p0, ext0 = Fpath.split_ext p0 in 39 | let p1, ext1 = Fpath.split_ext p1 in 40 | if Fpath.equal p0 p1 && ext0 = ".native" || ext1 = ".native" 41 | then Ok (Fpath.add_ext ".native" p0) 42 | else ambiguous l 43 | | [] -> 44 | R.error_msgf "No matches for %a in build directory %a" 45 | Fmt.(quote string) exec Fpath.pp dir 46 | | l -> 47 | ambiguous l 48 | 49 | let run () pkg_file build_dir exec args = 50 | let pkg = Topkg_care.Pkg.v pkg_file ?build_dir in 51 | begin 52 | Topkg_care.Pkg.build_dir pkg 53 | >>= fun build_dir -> find_exec exec build_dir 54 | >>= fun exec -> Ok Cmd.(v (p exec) %% of_list args) 55 | >>= fun cmd -> OS.Cmd.run_status cmd 56 | >>= function 57 | | `Exited 0 -> Ok 0 58 | | status -> 59 | Logs.err (fun m -> m "run %a %a" 60 | Cmd.dump cmd OS.Cmd.pp_status status); 61 | Ok 1 62 | end 63 | |> Cli.handle_error 64 | 65 | (* Command line interface *) 66 | 67 | open Cmdliner 68 | 69 | let args = 70 | let doc = "Arguments given to the executable. If options are being 71 | passed, needs to be specified after a -- token so that the 72 | command line options do not get interpreted by the $(tname) 73 | command itself." 74 | in 75 | Arg.(value & pos_right 0 string [] & info [] ~doc ~docv:"ARG") 76 | 77 | let exec = 78 | let doc = "Executable name or path suffix, with or without its 79 | extension. If multiple executable match the specification 80 | the command errors except if two paths match and differ 81 | only by their .byte and .native file extension. In the latter 82 | case the .native path is used." 83 | in 84 | let docv = "EXEC" in 85 | Arg.(required & pos 0 (some string) None & info [] ~doc ~docv) 86 | 87 | let doc = "Run built executables" 88 | let sdocs = Manpage.s_common_options 89 | let exits = Cmd.Exit.info 1 ~doc:"on run non-zero status exit." :: Cli.exits 90 | let man_xrefs = [ `Main ] 91 | let man = 92 | [ `S Manpage.s_synopsis; 93 | `P "$(mname) $(tname) [$(i,OPTION)]... $(i,EXEC) \ 94 | [-- [$(i,ARG)]...]]"; 95 | `S Manpage.s_description; 96 | `P "The $(tname) command runs executable files found 97 | in the build directory."; 98 | `P "$(b,WARNING) The way this command works is subject to change 99 | in the future." ] 100 | 101 | let cmd = 102 | Cmd.v (Cmd.info "run" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 103 | Term.(const run $ Cli.setup $ Cli.pkg_file $ Cli.build_dir $ exec $ args) 104 | -------------------------------------------------------------------------------- /src-bin/run.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [run] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/status.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let pp_since ppf = function 9 | | "" -> () 10 | | v -> Fmt.pf ppf " since %a" Topkg_care.Pp.version v 11 | 12 | let pp_dirty ppf = function 13 | | false -> () 14 | | true -> Fmt.pf ppf "The repository is %a.@," Topkg_care.Pp.dirty () 15 | 16 | let pp_commit ppf (id, log) = 17 | Fmt.pf ppf "%a %s" Topkg_care.Pp.commit id log 18 | 19 | let pp_status ppf (dirty, version, changes) = match changes with 20 | | [] when not dirty -> Fmt.pf ppf "@[No changes%a@]" pp_since version 21 | | changes -> 22 | Fmt.pf ppf "@[Changes%a:@,%a%a@]" 23 | pp_since version pp_dirty dirty (Fmt.list pp_commit) changes 24 | 25 | let find_latest_version_tag repo = 26 | let rev_compare v v' = -1 * compare v v' in 27 | let parse_tag acc t = match Topkg.String.parse_version t with 28 | | None -> acc 29 | | Some v -> (v, t) :: acc 30 | in 31 | Topkg.Vcs.tags repo >>| fun tags -> 32 | match List.(sort rev_compare (fold_left parse_tag [] tags)) with 33 | | (_, latest) :: _ -> Some latest 34 | | [] -> None 35 | 36 | let find_after repo = function 37 | | Some after -> Ok after 38 | | None -> 39 | find_latest_version_tag repo >>| function 40 | | None -> 41 | Logs.info (fun m -> m "No VCS version tag found."); "" 42 | | Some tag -> 43 | Logs.info (fun m -> m "Latest VCS version tag found: %s" tag); tag 44 | 45 | let status () _ after until = 46 | begin 47 | Topkg.Vcs.get () 48 | >>= fun repo -> Topkg.Vcs.is_dirty repo 49 | >>= fun dirty -> find_after repo after 50 | >>= fun after -> Topkg.Vcs.changes repo ~after ~until 51 | >>= fun changes -> 52 | Logs.app (fun m -> m "%a" pp_status (dirty, after, changes)); 53 | Ok (if dirty || changes <> [] then 0 else 1) 54 | end 55 | |> Cli.handle_error 56 | 57 | (* Command line interface *) 58 | 59 | open Cmdliner 60 | 61 | let after = 62 | let doc = "Commit-ish $(docv) after which commits are considered. 63 | Default is the latest VCS version tag of the form [v]X.Y.Z[+info]." 64 | in 65 | Arg.(value & opt (some string) None & info ["after"] ~doc ~docv:"COMMIT-ISH") 66 | 67 | let until = 68 | let doc = "Commit-ish $(docv) until which commits are considered." in 69 | let docv = "COMMIT-ISH" in 70 | Arg.(value & opt string "HEAD" & info ["until"] ~doc ~docv) 71 | 72 | let doc = "List commits to publish in the next distribution" 73 | let sdocs = Manpage.s_common_options 74 | let exits = 75 | (Cmd.Exit.info 0 ~doc:"changes have been detected.") :: 76 | (Cmd.Exit.info 1 ~doc:"no changes have been detected.") :: 77 | Cmd.Exit.defaults 78 | 79 | let man_xrefs = [ `Main ] 80 | let man = 81 | [ `S Manpage.s_description; 82 | `P "The $(tname) command consults the package's VCS and outputs the 83 | list of commits that define the changes for the next distribution." ] 84 | 85 | let cmd = 86 | Cmd.v (Cmd.info "status" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 87 | Term.(const status $ Cli.setup $ Cli.pkg_file $ after $ until) 88 | -------------------------------------------------------------------------------- /src-bin/status.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [status] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/tag.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let extract_version change_log = 9 | Topkg_care.Text.change_log_file_last_entry change_log 10 | >>= fun (version, _) -> Ok version 11 | 12 | let vcs_tag tag ~commit_ish ~force ~sign ~delete ~msg = 13 | let msg = match msg with None -> strf "Distribution %s" tag | Some m -> m in 14 | Topkg.Vcs.get () 15 | >>= fun repo -> match delete with 16 | | true -> Topkg.Vcs.delete_tag repo tag 17 | | false -> 18 | Topkg.Vcs.tag repo ~force ~sign ~msg ~commit_ish tag >>| fun () -> 19 | Logs.app (fun m -> m "Tagged version %a" Topkg_care.Pp.version tag) 20 | 21 | let tag () pkg_file change_log tag commit_ish force sign delete msg = 22 | begin 23 | let pkg = Topkg_care.Pkg.v ?change_log pkg_file in 24 | let tag = match tag with 25 | | Some t -> Ok t 26 | | None -> Topkg_care.Pkg.change_log pkg >>= fun cl -> extract_version cl 27 | in 28 | tag 29 | >>= fun tag -> vcs_tag tag ~commit_ish ~force ~sign ~delete ~msg 30 | >>= fun () -> Ok 0 31 | end 32 | |> Cli.handle_error 33 | 34 | (* Command line interface *) 35 | 36 | open Cmdliner 37 | 38 | let version = 39 | let doc = "The version tag to use. If absent, automatically extracted 40 | from the package's change log; see topkg-log(1) for details." 41 | in 42 | Arg.(value & pos 0 (some string) None & info [] ~doc ~docv:"VERSION") 43 | 44 | let commit = 45 | let doc = "Commit-ish $(docv) to tag." in 46 | Arg.(value & opt string "HEAD" & info ["commit"] ~doc ~docv:"COMMIT-ISH") 47 | 48 | let msg = 49 | let doc = "Commit message for the tag. If absent, the message 50 | 'Distribution $(i,VERSION)' is used." 51 | in 52 | Arg.(value & opt (some string) None & info ["m"; "message"] ~doc ~docv:"MSG") 53 | 54 | let sign = 55 | let doc = "Sign the tag using the VCS's default signing key." in 56 | Arg.(value & flag & info ["s"; "sign"] ~doc) 57 | 58 | let force = 59 | let doc = "If the tag exists, replace it rather than fail." in 60 | Arg.(value & flag & info ["f"; "force"] ~doc) 61 | 62 | let delete = 63 | let doc = "Delete the specified tag rather than create it." in 64 | Arg.(value & flag & info ["d"; "delete"] ~doc) 65 | 66 | let doc = "Tag the package's source repository with a version" 67 | let sdocs = Manpage.s_common_options 68 | let exits = Cli.exits 69 | let man_xrefs = [ `Main; `Cmd "log" ] 70 | let man = 71 | [ `S Manpage.s_description; 72 | `P "The $(tname) command tags the package's VCS HEAD commit with a 73 | version. If the version is not specified on the command line it is 74 | automatically extracted from the package's change log; use 75 | $(b,topkg log -t) to check the extracted value." ] 76 | 77 | let cmd = 78 | Cmd.v (Cmd.info "tag" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 79 | Term.(const tag $ Cli.setup $ Cli.pkg_file $ Cli.change_log $ 80 | version $ commit $ force $ sign $ delete $ msg) 81 | -------------------------------------------------------------------------------- /src-bin/tag.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [tag] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/test.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let test_args name build_dir list args = 9 | let on_some_use_opt opt to_arg = function 10 | | None -> Cmd.empty 11 | | Some value -> Cmd.(v opt % to_arg value) 12 | in 13 | let verb = Cli.propagate_verbosity_to_pkg_file () in 14 | let build_dir = on_some_use_opt "--build-dir" Cmd.p build_dir in 15 | let list = if list then Cmd.(v "--list") else Cmd.empty in 16 | let name = on_some_use_opt "--pkg-name" (fun n -> n) name in 17 | Cmd.(verb %% name %% list %% build_dir %% Cmd.of_list args) 18 | 19 | let test () pkg_file pkg_name build_dir list args = 20 | let pkg = Topkg_care.Pkg.v pkg_file in 21 | let args = test_args pkg_name build_dir list args in 22 | let out = OS.Cmd.out_stdout in 23 | begin 24 | OS.Dir.current () 25 | >>= fun dir -> Topkg_care.Pkg.test pkg ~dir ~args ~out 26 | >>| (function ((), (_, `Exited 0)) -> 0 | _ -> 1) 27 | end 28 | |> Cli.handle_error 29 | 30 | (* Command line interface *) 31 | 32 | open Cmdliner 33 | 34 | let args = 35 | let doc = "Tests and arguments to the tests. If options are being 36 | passed, needs to be specified after a -- token so that the 37 | command line options do not get interpreted by $(b,topkg test) 38 | itself. If arguments need to be specified for the test itself a 39 | second -- token is needed." 40 | in 41 | Arg.(value & pos_all string [] & info [] ~doc ~docv:"[TEST]... [-- ARG...]") 42 | 43 | let build_dir = 44 | let doc = "Specifies the build directory $(docv). If absent, provided 45 | by the package description. This is equivalent to specify 46 | the same option after the first -- token" 47 | in 48 | let docv = "BUILD_DIR" in 49 | Arg.(value & opt (some Cli.path_arg) None & info ["build-dir"] ~doc ~docv) 50 | 51 | let list = 52 | let doc = "Do not run the tests, list them. This is equivalent to 53 | specify the same option after the first -- token." 54 | in 55 | Arg.(value & flag & info ["l"; "list"] ~doc) 56 | 57 | let doc = "Run built package tests" 58 | let sdocs = Manpage.s_common_options 59 | let exits = Cmd.Exit.info 1 ~doc:"on test failure." :: Cli.exits 60 | let man_xrefs = [ `Main ] 61 | let man = 62 | [ `S Manpage.s_synopsis; 63 | `P "$(mname) $(tname) [$(i,OPTION)]... [-- [$(i,TEST)]... \ 64 | [-- [$(i,ARG)]...]]"; 65 | `S Manpage.s_description; 66 | `P "The $(tname) command runs the tests that were built by 67 | topkg-build(1). This is equivalent to invoke:"; 68 | `Pre "ocaml ./pkg/pkg.ml test [$(i,TEST)]... [-- [$(i,ARG)]...]"; 69 | `P "The value for $(i,TEST) can be a full path to the test executable 70 | or simply the basename of the test executable with or without the file 71 | extension. The option $(b,--list) lists the tests that were built."; 72 | `P "Note that if you want to pass command line arguments to a test you 73 | need to specify the token -- twice. For example to pass 'arg' to a 74 | test 'mytest' use one of the following invocation:"; 75 | `Pre "topkg test -- mytest -- arg"; `Noblank; 76 | `Pre "topkg test mytest -- -- arg" ] 77 | 78 | let cmd = 79 | Cmd.v (Cmd.info "test" ~doc ~sdocs ~exits ~man ~man_xrefs) @@ 80 | Term.(const test $ Cli.setup $ Cli.pkg_file $ Cli.pkg_name $ build_dir $ 81 | list $ args) 82 | -------------------------------------------------------------------------------- /src-bin/test.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** The [test] command. *) 7 | 8 | val cmd : int Cmdliner.Cmd.t 9 | -------------------------------------------------------------------------------- /src-bin/topkg_bin.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Cmdliner 7 | 8 | let cmds = 9 | [ Bistro.cmd; Browse.cmd; Build.cmd; Clean.cmd; Distrib.cmd; Doc.cmd; 10 | Help.cmd; Ipc.cmd; Issue.cmd; Lint.cmd; Log.cmd; Opam.cmd; 11 | Publish.cmd; Run.cmd; Status.cmd; Tag.cmd; Test.cmd; ] 12 | 13 | let main () = `Help (`Pager, None) 14 | 15 | (* Command line interface *) 16 | 17 | let doc = "Topkg package care" 18 | let sdocs = Manpage.s_common_options 19 | let exits = Cli.exits 20 | let man = 21 | [ `S Manpage.s_description; 22 | `P "$(mname) takes care of topkg packages."; 23 | `P "Use '$(mname) help release' for help to release a package."; 24 | `Noblank; 25 | `P "Use '$(mname) help delegate' for help about the topkg delegate."; 26 | `Noblank; 27 | `P "Use '$(mname) help troubleshoot' for a few troubleshooting tips."; 28 | `Noblank; 29 | `P "Use '$(mname) help $(i,COMMAND)' for help about $(i,COMMAND)."; 30 | `S Manpage.s_bugs; 31 | `P "Report them, see $(i,%%PKG_HOMEPAGE%%) for contact information."; 32 | `S Manpage.s_authors; 33 | `P "Daniel C. Buenzli, $(i,http://erratique.ch)"; ] 34 | 35 | let main = 36 | let default = Term.(ret (const main $ Cli.setup)) in 37 | let info = Cmd.info "topkg" ~version:"%%VERSION%%" ~doc ~sdocs ~exits ~man in 38 | Cmd.group ~default info cmds 39 | 40 | let main () = 41 | Topkg.Private.disable_main (); 42 | Cmd.eval' main 43 | 44 | let () = if !Sys.interactive then () else exit (main ()) 45 | -------------------------------------------------------------------------------- /src-care/topkg_care.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | module Text = Topkg_care_text 7 | module Pp = Topkg_care_text.Pp 8 | module Opam = Topkg_care_opam 9 | module OCamlbuild = Topkg_care_ocamlbuild 10 | module OCamlfind = Topkg_care_ocamlfind 11 | module Archive = Topkg_care_archive 12 | module Pkg = Topkg_care_pkg 13 | module Delegate = Topkg_care_delegate 14 | -------------------------------------------------------------------------------- /src-care/topkg_care.mllib: -------------------------------------------------------------------------------- 1 | Topkg_care 2 | Topkg_care_archive 3 | Topkg_care_delegate 4 | Topkg_care_ipc 5 | Topkg_care_ocamlbuild 6 | Topkg_care_ocamlfind 7 | Topkg_care_opam 8 | Topkg_care_pkg 9 | Topkg_care_text 10 | -------------------------------------------------------------------------------- /src-care/topkg_care_archive.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | (* Ustar archives *) 9 | 10 | module Tar = struct 11 | 12 | type ptime = int 13 | type t = string list 14 | 15 | let empty = [] 16 | 17 | (* Header. 18 | 19 | See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/\ 20 | pax.html#tag_20_92_13_06 *) 21 | 22 | let to_unix_path_string = 23 | if Fpath.dir_sep = "/" then Fpath.to_string else 24 | fun f -> String.concat ~sep:"/" (Fpath.segs f) 25 | 26 | let set_filename h f = 27 | let s = to_unix_path_string f in 28 | match String.length s with 29 | | n when n <= 100 -> Bytes.blit_string s 0 h 0 (String.length s) 30 | | n -> 31 | try match String.cut ~rev:true ~sep:"/" s with 32 | | None -> raise Exit 33 | | Some (p, n) -> 34 | (* This could be made more clever by trying to find 35 | the slash nearest to the half string position. *) 36 | if String.length p > 155 || String.length n > 100 then raise Exit; 37 | Bytes.blit_string n 0 h 0 (String.length n); 38 | Bytes.blit_string p 0 h 345 (String.length p); 39 | with 40 | | Exit -> failwith (strf "%a: file name too long" Fpath.pp f) 41 | 42 | let set_string off h s = Bytes.blit_string s 0 h off (String.length s) 43 | let set_octal field off len (* terminating NULL included *) h n = 44 | let octal = Printf.sprintf "%0*o" (len - 1) n in 45 | if String.length octal < len 46 | then Bytes.blit_string octal 0 h off (String.length octal) else 47 | failwith (strf "field %s: can't encode %d in %d-digit octal number" 48 | field (len - 1) n) 49 | 50 | let header_checksum h = 51 | let len = Bytes.length h in 52 | let rec loop acc i = 53 | if i > len then acc else 54 | loop (acc + (Char.to_int @@ Bytes.unsafe_get h i)) (i + 1) 55 | in 56 | loop 0 0 57 | 58 | let header fname mode mtime size typeflag = 59 | try 60 | let h = Bytes.make 512 '\x00' in 61 | set_filename h fname; 62 | set_octal "mode" 100 8 h mode; 63 | set_octal "owner" 108 8 h 0; 64 | set_octal "group" 116 8 h 0; 65 | set_octal "size" 124 12 h size; 66 | set_octal "mtime" 136 12 h mtime; 67 | set_string 148 h " "; (* Checksum *) 68 | set_string 156 h typeflag; 69 | set_string 257 h "ustar"; 70 | set_string 263 h "00"; 71 | set_octal "devmajor" 329 8 h 0; 72 | set_octal "devminor" 329 8 h 0; 73 | let c = header_checksum h in 74 | set_octal "checksum" 148 9 (* not NULL terminated *) h c; 75 | Ok (Bytes.unsafe_to_string h) 76 | with Failure msg -> R.error_msg msg 77 | 78 | (* Files *) 79 | 80 | let padding content = match String.length content mod 512 with 81 | | 0 -> "" 82 | | n -> Bytes.unsafe_to_string (Bytes.make (512 - n) '\x00') 83 | 84 | let add t fname ~mode ~mtime kind = 85 | let typeflag, size, data = match kind with 86 | | `Dir -> "5", 0, [] 87 | | `File cont -> "0", String.length cont, [cont; padding cont] 88 | in 89 | header fname mode mtime size typeflag 90 | >>| fun header -> List.rev_append data (header :: t) 91 | 92 | (* Encode *) 93 | 94 | let to_string t = 95 | let end_of_file = Bytes.unsafe_to_string (Bytes.make 1024 '\x00') in 96 | String.concat (List.rev (end_of_file :: t)) 97 | end 98 | 99 | let path_set_of_dir dir ~exclude_paths = 100 | let add_prefix p acc = Fpath.(Set.add (dir // p) acc) in 101 | let exclude_paths = Fpath.Set.(fold add_prefix exclude_paths empty) in 102 | let not_excluded p = Ok (not (Fpath.Set.mem p exclude_paths)) in 103 | let traverse = `Sat not_excluded in 104 | let elements = `Sat not_excluded in 105 | let err _ e = e in 106 | OS.Dir.fold_contents ~dotfiles:true ~err ~elements ~traverse 107 | Fpath.Set.add Fpath.Set.empty dir 108 | 109 | let tar dir ~exclude_paths ~root ~mtime = 110 | let tar_add file tar = 111 | let fname = match Fpath.rem_prefix dir file with 112 | | None -> assert false 113 | | Some file -> Fpath.(root // file) 114 | in 115 | Logs.info (fun m -> m "Archiving %a" Fpath.pp fname); 116 | tar 117 | >>= fun tar -> OS.Dir.exists file 118 | >>= function 119 | | true -> Tar.add tar fname ~mode:0o775 ~mtime `Dir 120 | | false -> 121 | OS.Path.Mode.get file 122 | >>= fun mode -> OS.File.read file 123 | >>= fun contents -> 124 | let mode = if 0o100 land mode > 0 then 0o775 else 0o664 in 125 | Tar.add tar fname ~mode ~mtime (`File contents) 126 | in 127 | path_set_of_dir dir ~exclude_paths 128 | >>= fun fset -> Fpath.Set.fold tar_add fset (Ok Tar.empty) 129 | >>| fun tar -> Tar.to_string tar 130 | 131 | (* Bzip2 compression and unarchiving *) 132 | 133 | let bzip2_cmd = OS.Env.(value "TOPKG_BZIP2" cmd ~absent:(Cmd.v "bzip2")) 134 | let ensure_bzip2 () = OS.Cmd.must_exist bzip2_cmd >>| fun _ -> () 135 | let bzip2 s ~dst = OS.Cmd.(in_string s |> run_io bzip2_cmd |> to_file dst) 136 | 137 | let tar_cmd = OS.Env.(value "TOPKG_TAR" cmd ~absent:(Cmd.v "tar")) 138 | let ensure_tar () = OS.Cmd.must_exist tar_cmd >>| fun _ -> () 139 | let untbz ?(clean = false) ar = 140 | let clean_dir dir = OS.Dir.exists dir >>= function 141 | | true when clean -> OS.Dir.delete ~recurse:true dir 142 | | _ -> Ok () 143 | in 144 | let archive_dir, ar = Fpath.split_base ar in 145 | let unarchive ar = 146 | let dir = Fpath.rem_ext ar in 147 | OS.Cmd.must_exist tar_cmd 148 | >>= fun cmd -> clean_dir dir 149 | >>= fun () -> OS.Cmd.run Cmd.(tar_cmd % "-xjf" % p ar) 150 | >>= fun () -> Ok Fpath.(archive_dir // dir) 151 | in 152 | R.join @@ OS.Dir.with_current archive_dir unarchive ar 153 | -------------------------------------------------------------------------------- /src-care/topkg_care_archive.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Archive creation. 7 | 8 | See {!Topkg_care.Archive}. *) 9 | 10 | open Bos_setup 11 | 12 | (** {1 Ustar archives} *) 13 | 14 | (** ustar encoder. 15 | 16 | {b References}. 17 | {{:http://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06}ustar Interchange Format} in POSIX 1003.1, 2013. *) 18 | module Tar : sig 19 | 20 | (** {1 Ustar encoder} *) 21 | 22 | type ptime = int 23 | (** The type for POSIX times in seconds since the epoch. *) 24 | 25 | type t 26 | (** The type for ustar archives. *) 27 | 28 | val empty : t 29 | (** [empty] is the empty ustar archive. *) 30 | 31 | val add : 32 | t -> Fpath.t -> mode:int -> mtime:ptime -> [`Dir | `File of string ] -> 33 | (t, R.msg) result 34 | (** [add a f mode mtime kind] adds to archive [a] an element of 35 | type [kind] with file path [f], permission mode [mode] and modificaton 36 | time [mtime]. *) 37 | 38 | val to_string : t -> string 39 | (** [to_string a] is the byte serialization of the archive [a]. *) 40 | end 41 | 42 | val tar : 43 | Fpath.t -> exclude_paths:Fpath.set -> root:Fpath.t -> mtime:int -> 44 | (string, R.msg) result 45 | 46 | (** {1 Bzip2 compression and unarchiving} *) 47 | 48 | val ensure_bzip2 : unit -> (unit, R.msg) result 49 | val bzip2 : string -> dst:Fpath.t -> (unit, R.msg) result 50 | val ensure_tar : unit -> (unit, R.msg) result 51 | val untbz : ?clean:bool -> Fpath.t -> (Fpath.t, R.msg) result 52 | -------------------------------------------------------------------------------- /src-care/topkg_care_delegate.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | (* Running the delegate *) 9 | 10 | let run_delegate pkg args = 11 | let verbosity = Logs.level_to_string (Logs.level ()) in 12 | Topkg_care_pkg.delegate pkg 13 | >>= fun del -> Ok Cmd.(del % "ipc" % verbosity %% args) 14 | >>= fun cmd -> OS.Cmd.run_status cmd 15 | >>= function 16 | | `Exited 0 -> Ok () 17 | | `Exited 1 -> 18 | R.error_msgf "Action unsupported by delegate %a" Cmd.pp del 19 | | (`Exited n | `Signaled n) -> 20 | R.error_msgf "Delegate %a errored with %d" Cmd.pp del n 21 | 22 | (* Publish request *) 23 | 24 | let publish_distrib p ~msg ~archive = 25 | Topkg_care_pkg.name p 26 | >>= fun name -> Topkg_care_pkg.version p 27 | >>= fun version -> Topkg_care_pkg.distrib_uri p 28 | >>= fun distrib_uri -> 29 | run_delegate p Cmd.(v "publish" % "distrib" % distrib_uri % 30 | name % version % msg % p archive) 31 | 32 | let publish_doc p ~msg ~docdir = 33 | let doc_uri p = Topkg_care_pkg.opam_field_hd p "doc" >>= function 34 | | None -> Ok "" 35 | | Some uri -> Ok uri 36 | in 37 | Topkg_care_pkg.name p 38 | >>= fun name -> Topkg_care_pkg.version p 39 | >>= fun version -> doc_uri p 40 | >>= fun doc_uri -> 41 | run_delegate p Cmd.(v "publish" % "doc" % doc_uri % name % version % msg % 42 | p docdir) 43 | 44 | let publish_alt p ~kind ~msg ~archive = 45 | Topkg_care_pkg.name p 46 | >>= fun name -> Topkg_care_pkg.version p 47 | >>= fun version -> Topkg_care_pkg.distrib_uri p 48 | >>= fun distrib_uri -> 49 | run_delegate p Cmd.(v "publish" % "alt" % distrib_uri % kind % 50 | name % version % msg % p archive) 51 | 52 | let publish_in_git_branch ~remote ~branch ~name ~version ~docdir ~dir = 53 | let pp_distrib ppf (name, version) = 54 | Fmt.pf ppf "%a %a" 55 | Topkg_care_text.Pp.name name Topkg_care_text.Pp.version version 56 | in 57 | let log_publish_result msg distrib dir = 58 | Logs.app (fun m -> m "%s %a@ in@ directory@ %a@ of@ gh-pages@ branch" 59 | msg pp_distrib distrib Fpath.pp dir) 60 | in 61 | let cp src dst = 62 | let dst_is_root = Fpath.is_current_dir dst in 63 | let src = 64 | if dst_is_root then Fpath.to_dir_path src else Fpath.rem_empty_seg src 65 | in 66 | (* FIXME we lost Windows friends here, fix bos #30 *) 67 | OS.Cmd.run Cmd.(v "cp" % "-R" % p src % p dst) 68 | in 69 | let delete dir = 70 | if not (Fpath.is_current_dir dir) then OS.Dir.delete ~recurse:true dir else 71 | let delete acc p = acc >>= fun () -> OS.Path.delete ~recurse:true p in 72 | let gitdir = Fpath.v ".git" in 73 | let not_git p = not (Fpath.equal p gitdir) in 74 | OS.Dir.contents dir 75 | >>= fun files -> List.fold_left delete (Ok ()) (List.filter not_git files) 76 | in 77 | let git_for_repo r = Cmd.of_list (Topkg.Cmd.to_list @@ Topkg.Vcs.cmd r) in 78 | let replace_dir_and_push docdir dir = 79 | let msg = strf "Update %s doc to %s." name version in 80 | Topkg.Vcs.get () 81 | >>= fun repo -> Ok (git_for_repo repo) 82 | >>= fun git -> OS.Cmd.run Cmd.(git % "checkout" % branch) 83 | >>= fun () -> delete dir 84 | >>= fun () -> cp docdir dir 85 | >>= fun () -> Topkg.Vcs.is_dirty repo 86 | >>= function 87 | | false -> Ok false 88 | | true -> 89 | OS.Cmd.run Cmd.(git % "add" % p dir) 90 | >>= fun () -> OS.Cmd.run Cmd.(git % "commit" % "-m" % msg) 91 | >>= fun () -> OS.Cmd.run Cmd.(git % "push") 92 | >>= fun () -> Ok true 93 | in 94 | if not (Fpath.is_rooted ~root:Fpath.(v ".") dir) 95 | then 96 | R.error_msgf "%a directory is not rooted in the repository or not relative" 97 | Fpath.pp dir 98 | else 99 | let clonedir = Fpath.(parent docdir / strf "%s-%s.pubdoc" name version) in 100 | OS.Dir.delete ~recurse:true clonedir 101 | >>= fun () -> Topkg.Vcs.get () 102 | >>= fun repo -> Topkg.Vcs.clone repo ~dir:(Fpath.to_string clonedir) 103 | >>= fun () -> OS.Dir.with_current clonedir (replace_dir_and_push docdir) dir 104 | >>= fun res -> res 105 | >>= function 106 | | false (* no changes *) -> 107 | log_publish_result "No documentation changes for" (name, version) dir; 108 | Ok () 109 | | true -> 110 | let push_spec = strf "%s:%s" branch branch in 111 | Ok (git_for_repo repo) >>= fun git -> 112 | OS.Cmd.run Cmd.(git % "push" % remote % push_spec) 113 | >>= fun () -> OS.Dir.delete ~recurse:true clonedir 114 | >>= fun () -> 115 | log_publish_result "Published documentation for" (name, version) dir; 116 | Ok () 117 | 118 | (* Issue requests *) 119 | 120 | let issues_uri p = Topkg_care_pkg.opam_field p "dev-repo" >>| function 121 | | None | Some [] -> "" 122 | | Some (u :: _) -> u 123 | 124 | let issue_list p = 125 | issues_uri p >>= fun issues_uri -> 126 | run_delegate p Cmd.(v "issue" % "list" % issues_uri) 127 | 128 | let issue_show p ~id = 129 | issues_uri p >>= fun issues_uri -> 130 | run_delegate p Cmd.(v "issue" % "show" % issues_uri % "id") 131 | 132 | let issue_open p ~title ~body = 133 | issues_uri p >>= fun issues_uri -> 134 | match run_delegate p Cmd.(v "issue" % "open" % issues_uri % title % body) with 135 | | Ok _ as v -> v 136 | | Error _ as e -> 137 | let pp_body ppf = function "" -> () | body -> Fmt.pf ppf "@,@,%s" body in 138 | Logs.app (fun m -> m "@[Your open issue message was:@,---@,%s%a@]" 139 | title pp_body body); 140 | e 141 | 142 | let issue_close p ~id ~msg = 143 | issues_uri p >>= fun issues_uri -> 144 | match run_delegate p Cmd.(v "issue" % "close" % issues_uri % id % msg) with 145 | | Ok _ as v -> v 146 | | Error _ as e -> 147 | Logs.app (fun m -> m "@[Your closing message was:@,---@,%s@]" msg); 148 | e 149 | -------------------------------------------------------------------------------- /src-care/topkg_care_delegate.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Package delegate. 7 | 8 | See {!Topkg_care.Delegate} for documentation. *) 9 | 10 | open Bos_setup 11 | 12 | (** {1 Publish} *) 13 | 14 | val publish_distrib : 15 | Topkg_care_pkg.t -> msg:string -> archive:Fpath.t -> 16 | (unit, R.msg) result 17 | 18 | val publish_doc : 19 | Topkg_care_pkg.t -> msg:string -> docdir:Fpath.t -> 20 | (unit, R.msg) result 21 | 22 | val publish_alt : 23 | Topkg_care_pkg.t -> kind:string -> msg:string -> archive:Fpath.t -> 24 | (unit, R.msg) result 25 | 26 | val publish_in_git_branch : 27 | remote:string -> branch:string -> 28 | name:string -> version:string -> docdir:Fpath.t -> 29 | dir:Fpath.t -> (unit, R.msg) result 30 | 31 | (** {1 Delegate} *) 32 | 33 | val issue_list : Topkg_care_pkg.t -> (unit, R.msg) result 34 | val issue_show : Topkg_care_pkg.t -> id:string -> (unit, R.msg) result 35 | 36 | val issue_open : 37 | Topkg_care_pkg.t -> title:string -> body:string -> (unit, R.msg) result 38 | 39 | val issue_close : 40 | Topkg_care_pkg.t -> id:string -> msg:string -> (unit, R.msg) result 41 | -------------------------------------------------------------------------------- /src-care/topkg_care_ipc.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let ocaml = 9 | Cmd.of_list @@ Topkg.Cmd.to_list @@ Topkg.Conf.tool "ocaml" `Build_os 10 | 11 | let pkg_must_exist pkg_file = match OS.File.must_exist pkg_file with 12 | | Ok _ -> Ok pkg_file 13 | | Error _ -> 14 | let p = match OS.Dir.current () with 15 | | Error _ -> (* ignore *) pkg_file 16 | | Ok dir -> Fpath.(dir // pkg_file) 17 | in 18 | R.error_msgf "Not a package: no file %a" Fpath.pp p 19 | 20 | let ask ~pkg_file ipc = 21 | let codec = Topkg.Private.Ipc.codec ipc in 22 | let verbosity = Logs.(level_to_string (level ())) in 23 | let ipc_cmd = Cmd.of_list @@ Topkg.Cmd.to_list @@ Topkg.Private.Ipc.cmd ipc in 24 | let cmd = Cmd.(ocaml % p pkg_file % "ipc" % verbosity %% ipc_cmd) in 25 | pkg_must_exist pkg_file >>= fun pkg_file -> 26 | begin 27 | OS.Cmd.run cmd 28 | >>= fun () -> Fpath.of_string (Topkg.Private.Ipc.answer ipc) 29 | >>= fun answer -> OS.File.read answer 30 | >>= fun data -> Topkg.Private.Codec.dec_result codec data 31 | end 32 | |> R.reword_error_msg 33 | (fun _ -> R.msgf "Failed to load package description %a" Fpath.pp pkg_file) 34 | -------------------------------------------------------------------------------- /src-care/topkg_care_ipc.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** IPC with package description files *) 7 | 8 | (** {1 Asking packages} *) 9 | 10 | open Bos_setup 11 | 12 | val ocaml : Cmd.t 13 | (** [ocaml] is a command for [ocaml] looked up using 14 | {!Topkg.Conf.tool}[ "ocaml" `Build_os]. *) 15 | 16 | val ask : pkg_file:Fpath.t -> 'a Topkg.Private.Ipc.t -> ('a, R.msg) result 17 | (** [ask pkg_file ipc] performs the IPC [ipc] with the package description 18 | file [pkg_file] using the interpreter {!ocaml}. *) 19 | -------------------------------------------------------------------------------- /src-care/topkg_care_ocamlbuild.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let cmd = 9 | Cmd.of_list @@ Topkg.Cmd.to_list @@ Topkg.Conf.tool "ocamlbuild" `Host_os 10 | 11 | let find_packages ~roots s = 12 | let package = String.sub "package(" in 13 | let not_rpar c = not (Char.equal ')' c) in 14 | let not_dot c = not (Char.equal '.' c) in 15 | let is_comma c = Char.equal ',' c in 16 | let is_sep c = Char.Ascii.is_white c || is_comma c in 17 | let rec loop acc s = match String.Sub.find_sub ~sub:package s with 18 | | None -> acc 19 | | Some s -> 20 | let rest = String.Sub.(extend (stop s)) in 21 | let ids, rest = String.Sub.span ~sat:not_rpar rest in 22 | let ids = String.Sub.fields ~empty:false ~is_sep ids in 23 | let add_id acc id = 24 | let id = if roots then String.Sub.take ~sat:not_dot id else id in 25 | String.Set.add (String.Sub.to_string id) acc 26 | in 27 | let acc = List.fold_left add_id acc ids in 28 | loop acc (String.Sub.tail rest) 29 | in 30 | loop String.Set.empty (String.sub s) 31 | 32 | let package_tags ?(roots = false) file = 33 | OS.File.read file >>= fun contents -> Ok (find_packages ~roots contents) 34 | -------------------------------------------------------------------------------- /src-care/topkg_care_ocamlbuild.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** [ocamlbuild] helpers. 7 | 8 | See {!Topkg_care.OCamlbuild}. *) 9 | 10 | (** {1 OCamlbuild} *) 11 | 12 | open Bos_setup 13 | 14 | val cmd : Cmd.t 15 | val package_tags : ?roots:bool -> Fpath.t -> (String.set, R.msg) result 16 | -------------------------------------------------------------------------------- /src-care/topkg_care_ocamlfind.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | let cmd = 9 | Cmd.of_list @@ Topkg.Cmd.to_list @@ Topkg.Conf.tool "ocamlfind" `Host_os 10 | 11 | let base_packages = String.Set.of_list 12 | [ "bigarray"; "bytes"; "compiler-libs"; "dynlink"; "graphics"; "num"; 13 | "ocamldoc"; "stdlib"; "str"; "threads"; "unix" ] 14 | -------------------------------------------------------------------------------- /src-care/topkg_care_ocamlfind.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** [ocamlfind] helpers. 7 | 8 | See {!Topkg_care.OCamlfind}. *) 9 | 10 | (** {1 OCamlfind} *) 11 | 12 | open Bos_setup 13 | 14 | val cmd : Cmd.t 15 | val base_packages : String.set 16 | -------------------------------------------------------------------------------- /src-care/topkg_care_opam.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | (* Command *) 9 | 10 | let cmd = 11 | Cmd.of_list @@ Topkg.Cmd.to_list @@ Topkg.Conf.tool "opam" `Host_os 12 | 13 | (* Publish *) 14 | 15 | let publish = 16 | let absent = Cmd.(v "opam-publish") in 17 | OS.Env.(value "TOPKG_OPAM_PUBLISH" cmd ~absent) 18 | 19 | let ensure_publish () = 20 | OS.Cmd.must_exist publish >>= fun cmd -> 21 | OS.Cmd.run_out Cmd.(publish % "--version") |> OS.Cmd.out_string 22 | >>= fun (version, _) -> match Topkg.String.parse_version version with 23 | | None -> R.error_msgf "Could not determine the version of opam-publish" 24 | | Some (m, _, _, _) when m < 2 -> 25 | R.error_msgf "topkg needs at least opam-publish 2.0.0" 26 | | Some _ -> Ok () 27 | 28 | let submit ?msg ~opam_file () = 29 | let msg = match msg with 30 | | None -> Ok (Cmd.empty) 31 | | Some msg -> 32 | OS.File.tmp "topkg-opam-submit-msg-%s" 33 | >>= fun m -> OS.File.write m msg 34 | >>= fun () -> Ok Cmd.(v "--msg-file" % p m) 35 | in 36 | msg >>= fun msg -> OS.Cmd.run Cmd.(publish %% msg % p opam_file) 37 | 38 | (* Packages *) 39 | 40 | let ocaml_base_packages = String.Set.of_list 41 | [ "base-bigarray"; "base-bytes"; "base-threads"; "base-unix"; ] 42 | 43 | (* Files *) 44 | 45 | module File = struct 46 | 47 | (* Try to compose with the OpamFile.OPAM API *) 48 | 49 | let id x = x 50 | let list f = fun v -> [f v] 51 | let field name field conv = 52 | name, fun acc o -> String.Map.add name (conv (field o)) acc 53 | 54 | let opt_field name field conv = 55 | name, fun acc o -> match field o with 56 | | None -> acc 57 | | Some v -> String.Map.add name (conv v) acc 58 | 59 | let deps_conv d = 60 | let add_pkg acc (n, _) = OpamPackage.Name.to_string n :: acc in 61 | OpamFormula.fold_left add_pkg [] d 62 | 63 | let fields = [ 64 | opt_field "name" OpamFile.OPAM.name_opt (list OpamPackage.Name.to_string); 65 | opt_field "version" OpamFile.OPAM.version_opt 66 | (list OpamPackage.Version.to_string); 67 | field "opam-version" OpamFile.OPAM.opam_version 68 | (list OpamVersion.to_string); 69 | field "available" OpamFile.OPAM.available (list OpamFilter.to_string); 70 | field "maintainer" OpamFile.OPAM.maintainer id; 71 | field "homepage" OpamFile.OPAM.homepage id; 72 | field "authors" OpamFile.OPAM.author id; 73 | field "license" OpamFile.OPAM.license id; 74 | field "doc" OpamFile.OPAM.doc id; 75 | field "tags" OpamFile.OPAM.tags id; 76 | field "bug-reports" OpamFile.OPAM.bug_reports id; 77 | opt_field "dev-repo" OpamFile.OPAM.dev_repo (list OpamUrl.to_string); 78 | field "depends" OpamFile.OPAM.depends deps_conv; 79 | field "depopts" OpamFile.OPAM.depopts deps_conv; 80 | opt_field "synopsis" OpamFile.OPAM.synopsis (list id); 81 | opt_field "description" OpamFile.OPAM.descr_body (list id); 82 | ] 83 | 84 | let field_names = 85 | let add acc (name, field) = String.Set.add name acc in 86 | List.fold_left add String.Set.empty fields 87 | 88 | let fields file = 89 | let parse file = 90 | let file = OpamFilename.of_string (Fpath.to_string file) in 91 | let opam = OpamFile.OPAM.read (OpamFile.make file) in 92 | let known_fields = 93 | let add_field acc (_, field) = field acc opam in 94 | List.fold_left add_field String.Map.empty fields 95 | in 96 | (* FIXME add OpamFile.OPAM.extensions when supported *) 97 | known_fields 98 | in 99 | Logs.info (fun m -> m "Parsing opam file %a" Fpath.pp file); 100 | try Ok (parse file) with 101 | | exn -> 102 | (* Apparently in at least opam-lib 1.2.2, the error will be logged 103 | on stdout. *) 104 | R.error_msgf "%a: could not parse opam file" Fpath.pp file 105 | 106 | let deps ?(opts = true) fields = 107 | let deps = match String.Map.find "depends" fields with 108 | | None -> [] | Some deps -> deps 109 | in 110 | let dep_opts = 111 | if not opts then [] else 112 | match String.Map.find "depopts" fields with 113 | | None -> [] | Some deps -> deps 114 | in 115 | String.Set.of_list (List.rev_append dep_opts deps) 116 | end 117 | 118 | module Descr = struct 119 | type t = string * string 120 | 121 | let of_string s = match String.cuts ~sep:"\n" s with 122 | | [] -> R.error_msgf "Cannot extract opam descr." 123 | | synopsis :: descr -> Ok (synopsis, String.concat ~sep:"\n" descr) 124 | 125 | let to_string (synopsis, descr) = strf "%s\n%s" synopsis descr 126 | let to_opam_fields (synopsis, descr) = 127 | strf "synopsis: \"\"\"%s\"\"\"\ndescription: \"\"\"\\\n%s\"\"\"" 128 | synopsis descr 129 | 130 | let of_readme ?flavour r = 131 | let parse_synopsis l = 132 | let error l = R.error_msgf "%S: can't extract opam synopsis" l in 133 | let ok s = Ok String.(Ascii.capitalize @@ String.Sub.to_string s) in 134 | let not_white c = not (Char.Ascii.is_white c) in 135 | let skip_non_white l = String.Sub.drop ~sat:not_white l in 136 | let skip_white l = String.Sub.drop ~sat:Char.Ascii.is_white l in 137 | let start = 138 | String.sub l |> skip_white |> skip_non_white |> skip_white 139 | in 140 | match String.Sub.head start with 141 | | None -> error l 142 | | Some c when Char.Ascii.is_letter c -> ok start 143 | | Some c -> (* Try to skip a separator. *) 144 | let start = start |> skip_non_white |> skip_white in 145 | match String.Sub.head start with 146 | | None -> error l 147 | | Some _ -> ok start 148 | in 149 | let drop_line l = 150 | String.is_prefix ~affix:"Home page:" l || 151 | String.is_prefix ~affix:"Homepage:" l || 152 | String.is_prefix ~affix:"Contact:" l || 153 | String.is_prefix ~affix:"%%VERSION" l 154 | in 155 | let keep_line l = not (drop_line l) in 156 | match Topkg_care_text.head ?flavour r with 157 | | None -> R.error_msgf "Could not extract opam description." 158 | | Some (title, text) -> 159 | let sep = "\n" in 160 | let title = Topkg_care_text.header_title ?flavour title in 161 | parse_synopsis title 162 | >>= fun synopsis -> Ok (String.cuts ~sep text) 163 | >>= fun text -> Ok (List.filter keep_line text) 164 | >>= fun text -> Ok (synopsis, String.concat ~sep text) 165 | 166 | let of_readme_file file = 167 | let flavour = Topkg_care_text.flavour_of_fpath file in 168 | (OS.File.read file 169 | >>= fun text -> of_readme ?flavour text) 170 | |> R.reword_error_msg ~replace:true 171 | (fun m -> R.msgf "%a: %s" Fpath.pp file m) 172 | end 173 | 174 | module Url = struct 175 | type t = string 176 | let v ~uri ~checksum = strf "archive: \"%s\"\nchecksum: \"%s\"" uri checksum 177 | let with_distrib_file ~uri distrib_file = 178 | try 179 | let checksum = Digest.(to_hex @@ file (Fpath.to_string distrib_file)) in 180 | Ok (v ~uri ~checksum) 181 | with Failure msg | Sys_error msg -> R.error_msg msg 182 | 183 | let to_opam_section u = strf "url {\n%s\n}" u 184 | end 185 | -------------------------------------------------------------------------------- /src-care/topkg_care_opam.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** opam interaction. 7 | 8 | See {!Topkg_care.Opam}. *) 9 | 10 | (** {1 opam} *) 11 | 12 | open Bos_setup 13 | 14 | val cmd : Cmd.t 15 | val ensure_publish : unit -> (unit, R.msg) result 16 | val submit : ?msg:string -> opam_file:Fpath.t -> unit -> (unit, R.msg) result 17 | val ocaml_base_packages : String.set 18 | 19 | module File : sig 20 | val field_names : String.set 21 | val fields : Fpath.t -> ((string list) String.map , R.msg) result 22 | val deps : ?opts:bool -> (string list) String.map -> String.set 23 | end 24 | 25 | module Descr : sig 26 | type t = string * string 27 | val of_string : string -> (t, R.msg) result 28 | val to_string : t -> string 29 | val to_opam_fields : t -> string 30 | val of_readme : 31 | ?flavour:Topkg_care_text.flavour -> string -> (t, R.msg) result 32 | val of_readme_file : Fpath.t -> (t, R.msg) result 33 | end 34 | 35 | module Url : sig 36 | type t = string 37 | val v : uri:string -> checksum:string -> t 38 | val with_distrib_file : uri:string -> Fpath.t -> (t, R.msg) result 39 | val to_opam_section : t -> string 40 | end 41 | -------------------------------------------------------------------------------- /src-care/topkg_care_pkg.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Package descriptions. 7 | 8 | See {!Topkg_care.Pkg} *) 9 | 10 | open Bos_setup 11 | 12 | (** {1 Package} *) 13 | 14 | type t 15 | 16 | val v : 17 | ?name:string -> 18 | ?version:string -> 19 | ?delegate:Cmd.t -> 20 | ?build_dir:Fpath.t -> 21 | ?opam:Fpath.t -> 22 | ?opam_descr:Fpath.t -> 23 | ?readme:Fpath.t -> 24 | ?change_log:Fpath.t -> 25 | ?license:Fpath.t -> 26 | ?distrib_uri:string -> 27 | ?distrib_file:Fpath.t -> 28 | ?publish_msg:string -> 29 | ?publish_artefacts:[ `Distrib | `Doc | `Alt of string] list -> 30 | Fpath.t -> t 31 | 32 | val pkg_file : t -> Fpath.t 33 | val name : t -> (string, R.msg) result 34 | val version : t -> (string, R.msg) result 35 | val delegate : t -> (Cmd.t, R.msg) result 36 | val build_dir : t -> (Fpath.t, R.msg) result 37 | val opam : t -> (Fpath.t, R.msg) result 38 | val opam_field : t -> string -> (string list option, R.msg) result 39 | val opam_field_hd : t -> string -> (string option, R.msg) result 40 | val opam_fields : t -> (string list String.map, R.msg) result 41 | val opam_descr : t -> (Topkg_care_opam.Descr.t * bool, R.msg) result 42 | val readmes : t -> (Fpath.t list, R.msg) result 43 | val readme : t -> (Fpath.t, R.msg) result 44 | val change_logs : t -> (Fpath.t list, R.msg) result 45 | val change_log : t -> (Fpath.t, R.msg) result 46 | val licenses : t -> (Fpath.t list, R.msg) result 47 | val distrib_uri : ?raw:bool -> t -> (string, R.msg) result 48 | val distrib_file : t -> (Fpath.t, R.msg) result 49 | val publish_msg : t -> (string, R.msg) result 50 | val publish_artefacts : t -> 51 | ([ `Distrib | `Doc | `Alt of string] list, R.msg) result 52 | 53 | (** {1 Test} *) 54 | 55 | val test : 56 | t -> dir:Fpath.t -> args:Cmd.t -> 57 | out:(OS.Cmd.run_out -> ('a, R.msg) result) -> ('a, R.msg) result 58 | 59 | (** {1 Build} *) 60 | 61 | val build : 62 | t -> dir:Fpath.t -> args:Cmd.t -> 63 | out:(OS.Cmd.run_out -> ('a, R.msg) result) -> ('a, R.msg) result 64 | 65 | (** {1 Clean} *) 66 | 67 | val clean : 68 | t -> dir:Fpath.t -> args:Cmd.t -> 69 | out:(OS.Cmd.run_out -> ('a, R.msg) result) -> ('a, R.msg) result 70 | 71 | (** {1 Distrib} *) 72 | 73 | val distrib_filename : ?opam:bool -> t -> (Fpath.t, R.msg) result 74 | val distrib_archive : t -> keep_dir:bool -> (Fpath.t, R.msg) result 75 | 76 | (** {1 Lint} *) 77 | 78 | type lint = [ `Custom | `Std_files | `Meta | `Opam | `Deps ] 79 | val lint_all : lint list 80 | val lint : 81 | ?ignore_pkg:bool -> t -> dir:Fpath.t -> lint list -> (int, R.msg) result 82 | -------------------------------------------------------------------------------- /src-care/topkg_care_text.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Bos_setup 7 | 8 | type flavour = [ `Markdown | `Asciidoc ] 9 | 10 | let flavour_of_fpath f = match String.Ascii.lowercase (Fpath.get_ext f) with 11 | | ".md" -> Some `Markdown 12 | | ".asciidoc" | ".adoc" -> Some `Asciidoc 13 | | _ -> None 14 | 15 | let rec drop_blanks = function "" :: ls -> drop_blanks ls | ls -> ls 16 | let last_line = function [] -> None | l :: rev_ls -> Some l 17 | 18 | (* Detecting headers *) 19 | 20 | let simple_header hchar l before rest = 21 | match String.(length @@ take ~sat:(Char.equal hchar) l) with 22 | | 0 -> None 23 | | n -> Some (n, l, before, rest) 24 | 25 | let underline_header n uchar l before rest = 26 | let is_underline_header uchar l = 27 | String.(length @@ take ~sat:(Char.equal uchar) l) >= 2 28 | in 29 | if not (is_underline_header uchar l) then None else 30 | match last_line before with 31 | | None -> None 32 | | Some t -> Some (n, strf "%s\n%s" t l, List.tl before, rest) 33 | 34 | let rec find_markdown_header before = function 35 | | [] -> None 36 | | l :: ls -> 37 | match simple_header '#' l before ls with 38 | | Some _ as h -> h 39 | | None -> 40 | match underline_header 1 '=' l before ls with 41 | | Some _ as h -> h 42 | | None -> 43 | match underline_header 2 '-' l before ls with 44 | | Some _ as h -> h 45 | | None -> find_markdown_header (l :: before) ls 46 | 47 | let rec find_asciidoc_header before = function 48 | | [] -> None 49 | | l :: ls -> 50 | match simple_header '=' l before ls with 51 | | Some _ as h -> h 52 | | None -> 53 | match underline_header 1 '-' l before ls with 54 | | Some _ as h -> h 55 | | None -> 56 | match underline_header 2 '~' l before ls with 57 | | Some _ as h -> h 58 | | None -> 59 | match underline_header 3 '^' l before ls with 60 | | Some _ as h -> h 61 | | None -> 62 | match underline_header 4 '+' l before ls with 63 | | Some _ as h -> h 64 | | None -> find_asciidoc_header (l :: before) ls 65 | 66 | let head find_header text = 67 | let lines = String.cuts ~sep:"\n" text in 68 | let ret h acc = 69 | let contents = String.concat ~sep:"\n" (List.rev @@ drop_blanks acc) in 70 | Some (h, contents) 71 | in 72 | match find_header [] lines with 73 | | None -> None 74 | | Some (n, first, _ (* discard *), rest) -> 75 | let rec loop acc rest = match find_header acc rest with 76 | | None -> ret first (List.rev_append rest acc) 77 | | Some (n', h, before, rest) -> 78 | if n' > n then loop (h :: before) rest else 79 | ret first before 80 | in 81 | loop [] rest 82 | 83 | let head ?(flavour = `Markdown) text = match flavour with 84 | | `Markdown -> head find_markdown_header text 85 | | `Asciidoc -> head find_asciidoc_header text 86 | 87 | let header_title ?(flavour = `Markdown) h = match String.cuts ~sep:"\n" h with 88 | | [h] -> 89 | begin match flavour with 90 | | `Markdown -> String.(trim @@ drop ~sat:(Char.equal '#') h) 91 | | `Asciidoc -> String.(trim @@ drop ~sat:(Char.equal '=') h) 92 | end 93 | | h :: _ -> h (* underline headers *) 94 | | [] -> assert false 95 | 96 | (* Toy change log parsing *) 97 | 98 | let change_log_last_entry ?flavour text = match head ?flavour text with 99 | | None -> None 100 | | Some (h, changes) -> 101 | let title = header_title ?flavour h in 102 | match String.take ~sat:Char.Ascii.is_graphic title with 103 | | "" -> Logs.app (fun m -> m "%S %S" h changes); None 104 | | version -> Some (version, (h, changes)) 105 | 106 | let change_log_file_last_entry file = 107 | let flavour = flavour_of_fpath file in 108 | OS.File.read file 109 | >>= fun text -> match change_log_last_entry ?flavour text with 110 | | None -> R.error_msgf "%a: Could not parse change log." Fpath.pp file 111 | | Some (version, (header, changes)) -> Ok (version, (header, changes)) 112 | 113 | (* Toy URI parsing *) 114 | 115 | let split_uri ?(rel = false) uri = match String.(cut ~sep:"//" (trim uri)) with 116 | | None -> None 117 | | Some (scheme, rest) -> 118 | match String.cut ~sep:"/" rest with 119 | | None -> Some (scheme, rest, "") 120 | | Some (host, path) -> 121 | let path = if rel then path else "/" ^ path in 122 | Some (scheme, host, path) 123 | 124 | (* Edit and page text *) 125 | 126 | let find_pager ~don't = 127 | if don't then Ok None else 128 | match OS.Env.var "TERM" with 129 | | Some "dumb" | None -> Ok None 130 | | _ -> 131 | let add_env v cmds = match OS.Env.(value v (some cmd) ~absent:None) with 132 | | None -> cmds 133 | | Some cmd -> cmd :: cmds 134 | in 135 | let cmds = [Cmd.v "less"; Cmd.v "more" ] in 136 | let cmds = add_env "PAGER" cmds in 137 | let rec loop = function 138 | | [] -> Ok None 139 | | cmd :: cmds -> 140 | OS.Cmd.exists cmd >>= function 141 | | true -> Ok (Some cmd) 142 | | false -> loop cmds 143 | in 144 | loop cmds 145 | 146 | let edit_file f = match OS.Env.(value "EDITOR" (some cmd) ~absent:None) with 147 | | None -> R.error_msg "EDITOR environment variable undefined." 148 | | Some editor -> 149 | OS.Cmd.exists editor >>= function 150 | | false -> R.error_msgf "Editor %a not in search path" Cmd.pp editor 151 | | true -> 152 | OS.Cmd.(run_status Cmd.(editor % p f)) >>= function 153 | | `Exited n | `Signaled n -> Ok n 154 | 155 | (* Pretty-printers. *) 156 | 157 | module Pp = struct 158 | let name = Fmt.(styled `Bold string) 159 | let version = Fmt.(styled `Cyan string) 160 | let commit = Fmt.(styled `Yellow string) 161 | let dirty = Fmt.(styled `Red (any "dirty")) 162 | let path = Fmt.(styled `Bold Fpath.pp) 163 | let status ppf = function 164 | | `Ok -> Fmt.(brackets @@ styled `Green (any " OK ")) ppf () 165 | | `Fail -> Fmt.(brackets @@ styled `Red (any "FAIL")) ppf () 166 | end 167 | -------------------------------------------------------------------------------- /src-care/topkg_care_text.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Text processing helpers. 7 | 8 | See {!Topkg_care.Text}. *) 9 | 10 | (** {1 Text} *) 11 | 12 | open Bos_setup 13 | 14 | type flavour = [ `Markdown | `Asciidoc ] 15 | 16 | val flavour_of_fpath : Fpath.t -> flavour option 17 | val head : ?flavour:flavour -> string -> (string * string) option 18 | val header_title : ?flavour:flavour -> string -> string 19 | 20 | val change_log_last_entry : 21 | ?flavour:flavour -> string -> (string * (string * string)) option 22 | 23 | val change_log_file_last_entry : 24 | Fpath.t -> ((string * (string * string)), R.msg) result 25 | 26 | val split_uri : ?rel:bool -> string -> (string * string * string) option 27 | 28 | val find_pager : don't:bool -> (Cmd.t option, R.msg) result 29 | val edit_file : Fpath.t -> (int, R.msg) result 30 | 31 | module Pp : sig 32 | val name : string Fmt.t 33 | val version : string Fmt.t 34 | val commit : string Fmt.t 35 | val dirty : unit Fmt.t 36 | val path : Fpath.t Fmt.t 37 | val status : [`Ok | `Fail] Fmt.t 38 | end 39 | -------------------------------------------------------------------------------- /src/topkg.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (* Preliminaries *) 7 | 8 | include Topkg_result 9 | 10 | let strf = Topkg_string.strf 11 | module String = Topkg_string 12 | 13 | type fpath = string 14 | module Fpath = Topkg_fpath 15 | 16 | module Cmd = Topkg_cmd 17 | module Log = Topkg_log 18 | module OS = Topkg_os 19 | module Vcs = Topkg_vcs 20 | 21 | (* Package description *) 22 | 23 | module Conf = Topkg_conf 24 | module Exts = Topkg_fexts 25 | module Pkg = struct 26 | 27 | (* Install *) 28 | 29 | type install = Topkg_install.t 30 | type field = Topkg_install.field 31 | type exec_field = ?auto:bool -> field 32 | 33 | let nothing = Topkg_install.nothing 34 | let flatten = Topkg_install.flatten 35 | let bin = Topkg_install.bin 36 | let doc = Topkg_install.doc 37 | let etc = Topkg_install.etc 38 | let lib = Topkg_install.lib 39 | let lib_root = Topkg_install.lib_root 40 | let libexec = Topkg_install.libexec 41 | let libexec_root = Topkg_install.libexec_root 42 | let man = Topkg_install.man 43 | let misc = Topkg_install.misc 44 | let sbin = Topkg_install.sbin 45 | let share = Topkg_install.share 46 | let share_root = Topkg_install.share_root 47 | let stublibs = Topkg_install.stublibs 48 | let toplevel = Topkg_install.toplevel 49 | let unknown = Topkg_install.unknown 50 | let test = Topkg_install.test 51 | 52 | let mllib = Topkg_install.mllib 53 | let clib = Topkg_install.clib 54 | 55 | (* Distrib *) 56 | 57 | type watermark = Topkg_distrib.watermark 58 | type distrib = Topkg_distrib.t 59 | 60 | let distrib = Topkg_distrib.v 61 | let watermarks = Topkg_distrib.default_watermarks 62 | let files_to_watermark = Topkg_distrib.default_files_to_watermark 63 | let massage = Topkg_distrib.default_massage 64 | let exclude_paths = Topkg_distrib.default_exclude_paths 65 | 66 | (* Publish *) 67 | 68 | type publish = Topkg_publish.t 69 | let publish = Topkg_publish.v 70 | 71 | (* Build *) 72 | 73 | type build = Topkg_build.t 74 | let build = Topkg_build.v 75 | let build_cmd = Topkg_build.build_cmd 76 | let clean_cmd = Topkg_build.clean_cmd 77 | let ocb_tag = Topkg_build.ocb_tag 78 | let ocb_bool_tag = Topkg_build.ocb_bool_tag 79 | let ocb_bool_tags = Topkg_build.ocb_bool_tags 80 | 81 | (* Package *) 82 | 83 | type std_file = Topkg_pkg.std_file 84 | let std_file = Topkg_pkg.std_file 85 | 86 | type meta_file = Topkg_pkg.meta_file 87 | let meta_file = Topkg_pkg.meta_file 88 | 89 | type opam_file = Topkg_pkg.opam_file 90 | let opam_file = Topkg_pkg.opam_file 91 | 92 | (* Describe *) 93 | 94 | let describe = Topkg_main.describe 95 | end 96 | 97 | module Private = struct 98 | let disable_main = Topkg_main.disable 99 | module Codec = Topkg_codec 100 | module Pkg = Topkg_pkg 101 | module Ipc = Topkg_ipc 102 | module Opam = Topkg_opam 103 | end 104 | -------------------------------------------------------------------------------- /src/topkg.mllib: -------------------------------------------------------------------------------- 1 | Topkg 2 | Topkg_build 3 | Topkg_cmd 4 | Topkg_codec 5 | Topkg_conf 6 | Topkg_distrib 7 | Topkg_fexts 8 | Topkg_fpath 9 | Topkg_install 10 | Topkg_ipc 11 | Topkg_log 12 | Topkg_main 13 | Topkg_opam 14 | Topkg_os 15 | Topkg_pkg 16 | Topkg_publish 17 | Topkg_result 18 | Topkg_string 19 | Topkg_test 20 | Topkg_vcs -------------------------------------------------------------------------------- /src/topkg_build.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Topkg_result 7 | 8 | let ocamlbuild_flags = 9 | Topkg_cmd.(empty % "-use-ocamlfind" % "-classic-display") 10 | 11 | let build_cmd c os = 12 | let ocamlbuild = Topkg_conf.tool "ocamlbuild" os in 13 | let build_dir = Topkg_conf.build_dir c in 14 | let toolchain = 15 | match Topkg_conf.toolchain c with 16 | | Some toolchain -> Topkg_cmd.(v "-toolchain" % toolchain) 17 | | _ -> Topkg_cmd.empty 18 | in 19 | let debug = Topkg_cmd.(on (Topkg_conf.debug c) (v "-tag" % "debug")) in 20 | let profile = Topkg_cmd.(on (Topkg_conf.profile c) (v "-tag" % "profile")) in 21 | let jobs = 22 | let n = Topkg_conf.jobs c in 23 | Topkg_log.info (fun m -> m "using %d jobs" n); 24 | Topkg_cmd.(on (n != 1) (v "-j" % string_of_int n)) in 25 | Topkg_cmd.(ocamlbuild %% ocamlbuild_flags %% toolchain %% jobs %% 26 | debug %% profile % "-build-dir" % build_dir) 27 | 28 | let clean_cmd os ~build_dir = 29 | let ocamlbuild = Topkg_conf.tool "ocamlbuild" os in 30 | Topkg_cmd.(ocamlbuild %% ocamlbuild_flags % 31 | "-build-dir" % build_dir % "-clean") 32 | 33 | type t = 34 | { prepare_on_pin : bool; 35 | dir : Topkg_fpath.t; 36 | pre : Topkg_conf.t -> unit result; 37 | cmd : Topkg_conf.t -> Topkg_conf.os -> Topkg_fpath.t list -> unit result; 38 | post : Topkg_conf.t -> unit result; 39 | clean : Topkg_conf.os -> build_dir:Topkg_fpath.t -> unit result; } 40 | 41 | let with_dir b dir = { b with dir } 42 | 43 | let nop = fun _ -> Ok () 44 | 45 | let cmd c os files = 46 | Topkg_os.Cmd.run @@ Topkg_cmd.(build_cmd c os %% of_list files) 47 | 48 | let clean os ~build_dir = 49 | Topkg_os.Cmd.run @@ clean_cmd os ~build_dir 50 | 51 | let v 52 | ?(prepare_on_pin = true) ?(dir = "_build") ?(pre = nop) ?(cmd = cmd) 53 | ?(post = nop) ?(clean = clean) () = 54 | { prepare_on_pin; dir; pre; cmd; post; clean; } 55 | 56 | let prepare_on_pin b = b.prepare_on_pin 57 | let dir b = b.dir 58 | let pre b = b.pre 59 | let cmd b = b.cmd 60 | let post b = b.post 61 | let clean b = b.clean 62 | let codec = 63 | let prepare_on_pin = Topkg_codec.(with_kind "prepare_on_pin" @@ bool) in 64 | let dir = Topkg_codec.(with_kind "dir" @@ string) in 65 | let fields = 66 | let stub _ = invalid_arg "not executable outside package definition" in 67 | (fun b -> b.prepare_on_pin, b.dir), 68 | (fun (prepare_on_pin, dir) -> 69 | { prepare_on_pin; dir; pre = stub; cmd = stub; post = stub; 70 | clean = stub }) 71 | in 72 | Topkg_codec.version 0 @@ 73 | Topkg_codec.(view ~kind:"build" fields (pair prepare_on_pin dir)) 74 | 75 | let ocb_tag c key tag = 76 | let tag = Topkg_string.strf "%s(%a)" tag (Topkg_conf.pp_value c) key in 77 | Topkg_cmd.(v "-tag" % tag) 78 | 79 | let ocb_bool_tag c key tag = 80 | Topkg_cmd.(on (Topkg_conf.value c key) @@ v "-tag" % tag) 81 | 82 | let ocb_bool_tags c tags = 83 | let f (key, tag) = Topkg_cmd.(%%) (ocb_bool_tag c key tag) in 84 | List.fold_right f tags Topkg_cmd.empty 85 | -------------------------------------------------------------------------------- /src/topkg_build.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Package build description. *) 7 | 8 | open Topkg_result 9 | 10 | (** {1 Build} *) 11 | 12 | type t 13 | 14 | val build_cmd : Topkg_conf.t -> Topkg_conf.os -> Topkg_cmd.t 15 | val clean_cmd : Topkg_conf.os -> build_dir:Topkg_fpath.t -> Topkg_cmd.t 16 | 17 | val v : 18 | ?prepare_on_pin:bool -> 19 | ?dir:Topkg_fpath.t -> 20 | ?pre:(Topkg_conf.t -> unit result) -> 21 | ?cmd:(Topkg_conf.t -> Topkg_conf.os -> Topkg_fpath.t list -> unit result) -> 22 | ?post:(Topkg_conf.t -> unit result) -> 23 | ?clean:(Topkg_conf.os -> build_dir:Topkg_fpath.t -> unit result) -> 24 | unit -> t 25 | 26 | val with_dir : t -> Topkg_fpath.t -> t 27 | 28 | val prepare_on_pin : t -> bool 29 | val dir : t -> Topkg_fpath.t 30 | val pre : t -> (Topkg_conf.t -> unit result) 31 | val cmd : 32 | t -> (Topkg_conf.t -> Topkg_conf.os -> Topkg_fpath.t list -> unit result) 33 | 34 | val post : t -> (Topkg_conf.t -> unit result) 35 | val clean : t -> (Topkg_conf.os -> build_dir:Topkg_fpath.t -> unit result) 36 | 37 | val codec : t Topkg_codec.t 38 | 39 | val ocb_tag : Topkg_conf.t -> 'a Topkg_conf.key -> string -> Topkg_cmd.t 40 | val ocb_bool_tag : Topkg_conf.t -> bool Topkg_conf.key -> string -> Topkg_cmd.t 41 | val ocb_bool_tags : 42 | Topkg_conf.t -> (bool Topkg_conf.key * string) list -> Topkg_cmd.t 43 | -------------------------------------------------------------------------------- /src/topkg_cmd.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (* Command line fragments *) 7 | 8 | type t = string list 9 | 10 | let empty = [] 11 | let is_empty = function [] -> true | _ -> false 12 | let v a = [a] 13 | let ( % ) l a = a :: l 14 | let ( %% ) l0 l1 = List.rev_append (List.rev l1) l0 15 | let add_arg l a = l % a 16 | let add_args l a = l %% a 17 | let on bool l = if bool then l else [] 18 | let p f = f 19 | 20 | (* Predicates and comparison *) 21 | 22 | let equal l l' = l = l' 23 | let compare l l' = compare l l' 24 | 25 | (* Conversions and pretty printing *) 26 | 27 | let to_rev_list line = line 28 | let to_list line = List.rev line 29 | let of_list ?slip line = match slip with 30 | | None -> List.rev line 31 | | Some slip -> List.fold_left (fun acc v -> v :: slip :: acc) [] line 32 | 33 | let dump ppf cmd = 34 | let pp_elt ppf s = Format.fprintf ppf "%s" (Filename.quote s) in 35 | let rec loop = function 36 | | [] -> () 37 | | v :: vs -> 38 | if vs = [] then pp_elt ppf v else 39 | (Format.fprintf ppf "%a@ " pp_elt v; loop vs) 40 | in 41 | Format.fprintf ppf "@[<1>["; loop (List.rev cmd); Format.fprintf ppf "]@]" 42 | -------------------------------------------------------------------------------- /src/topkg_cmd.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Command lines 7 | 8 | See {!Topkg.Cmd} for documentation. *) 9 | 10 | (** {1 Command lines} *) 11 | 12 | type t 13 | 14 | val v : string -> t 15 | val empty : t 16 | val is_empty : t -> bool 17 | val ( % ) : t -> string -> t 18 | val ( %% ) : t -> t -> t 19 | val add_arg : t -> string -> t 20 | val add_args : t -> t -> t 21 | val on : bool -> t -> t 22 | val p : Topkg_fpath.t -> string 23 | 24 | val equal : t -> t -> bool 25 | val compare : t -> t -> int 26 | 27 | val to_rev_list : t -> string list 28 | val to_list : t -> string list 29 | val of_list : ?slip:string -> string list -> t 30 | val dump : Format.formatter -> t -> unit 31 | -------------------------------------------------------------------------------- /src/topkg_codec.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Topkg_result 7 | 8 | (* Decode errors *) 9 | 10 | type error = Corrupted of (string * string) | Version of int * int 11 | 12 | let pp_error ppf = function 13 | | Corrupted (kind, v) -> 14 | Format.fprintf ppf "corrupted %s in %S" kind v 15 | | Version (exp, fnd) -> 16 | Format.fprintf ppf "version mismatch, expected %d found %d" exp fnd 17 | 18 | exception Error of error 19 | 20 | let err ~kind v = raise (Error (Corrupted (kind, v))) 21 | let err_version ~exp ~fnd = raise (Error (Version (exp, fnd))) 22 | 23 | (* Codecs *) 24 | 25 | type 'a t = 26 | { kind : string; 27 | enc : 'a -> string; 28 | dec : string -> 'a; } 29 | 30 | let v ~kind ~enc ~dec = { kind; enc; dec } 31 | let kind c = c.kind 32 | let enc c = c.enc 33 | let dec c = c.dec 34 | let with_kind kind c = { c with kind } 35 | 36 | let dec_result c s = try Ok (dec c s) with 37 | | Error err -> 38 | R.error_msgf "Decode %s: %a Input data: %S" (kind c) pp_error err s 39 | 40 | let write file c v = 41 | Topkg_os.File.write file (enc c v) 42 | |> R.reword_error_msg ~replace:true 43 | (fun err -> R.msgf "Encode %s to %s: %s" (kind c) file err) 44 | 45 | let read file c = 46 | Topkg_os.File.read file >>= fun s -> 47 | try Ok (dec c s) with 48 | | Error e -> R.error_msgf "Decode %s from %s: %a" (kind c) file pp_error e 49 | 50 | (* Base type codecs *) 51 | 52 | let tail s = Topkg_string.with_index_range ~first:1 s 53 | 54 | let unit = 55 | let kind = "unit" in 56 | let enc = function () -> "\x00" in 57 | let dec = function "\x00" -> () | s -> err ~kind s in 58 | v ~kind ~enc ~dec 59 | 60 | let const c = 61 | let kind = "const" in 62 | let enc = function _ -> "" in 63 | let dec = function "" -> c | s -> err ~kind s in 64 | v ~kind ~enc ~dec 65 | 66 | let bool = 67 | let kind = "bool" in 68 | let enc = function false -> "\x00" | true -> "\x01" in 69 | let dec = function "\x00" -> false | "\x01" -> true | s -> err ~kind s in 70 | v ~kind ~enc ~dec 71 | 72 | let int = 73 | let kind = "int" in 74 | let enc = string_of_int (* will do for now *) in 75 | let dec s = try int_of_string s with Failure _ -> err ~kind s in 76 | v ~kind ~enc ~dec 77 | 78 | let string = 79 | let kind = "string" in 80 | let enc s = s in 81 | let dec s = s in 82 | v ~kind ~enc ~dec 83 | 84 | let option some = 85 | let kind = Printf.sprintf "(%s) option" (kind some) in 86 | let enc = function None -> "\x00" | Some v -> "\x01" ^ (enc some v) in 87 | let dec s = match Topkg_string.head s with 88 | | Some '\x00' -> None 89 | | Some '\x01' -> Some (dec some (tail s)) 90 | | _ -> err ~kind s 91 | in 92 | v ~kind ~enc ~dec 93 | 94 | let result ~ok ~error = 95 | let kind = Printf.sprintf "(%s, %s) result" (kind ok) (kind error) in 96 | let enc = function 97 | | Ok v -> "\x00" ^ (enc ok v) 98 | | Error e -> "\x01" ^ (enc error e) 99 | in 100 | let dec s = match Topkg_string.head s with 101 | | Some '\x00' -> Ok (dec ok (tail s)) 102 | | Some '\x01' -> Error (dec error (tail s)) 103 | | _ -> err ~kind s 104 | in 105 | v ~kind ~enc ~dec 106 | 107 | let list el = 108 | let kind = Printf.sprintf "(%s) list" (kind el) in 109 | let enc vs = 110 | let b = Buffer.create 255 in 111 | let rec loop = function 112 | | [] -> Buffer.add_char b '\x00' 113 | | v :: vs -> 114 | let venc = (enc el) v in 115 | let venc_len = String.length venc in 116 | Buffer.add_char b '\x01'; 117 | Buffer.add_string b (string_of_int venc_len) (* will do for now *); 118 | Buffer.add_char b '\x01'; 119 | Buffer.add_string b venc; 120 | loop vs 121 | in 122 | loop vs; Buffer.contents b 123 | in 124 | let dec s = 125 | let rec loop acc s = match Topkg_string.head s with 126 | | Some '\x00' -> acc 127 | | Some '\x01' -> 128 | begin match Topkg_string.find_byte ~start:1 '\x01' s with 129 | | None -> err ~kind s 130 | | Some one -> 131 | try 132 | let last = one - 1 in 133 | let len = Topkg_string.with_index_range ~first:1 ~last s in 134 | let len = int_of_string len in 135 | let first = one + 1 in 136 | let last = first + len - 1 in 137 | let venc = Topkg_string.with_index_range ~first ~last s in 138 | let rest = Topkg_string.with_index_range ~first:(last + 1) s in 139 | loop ((dec el venc) :: acc) rest 140 | with Failure _ (* of int_of_string *) -> err ~kind s 141 | end 142 | | _ -> err ~kind s 143 | in 144 | List.rev (loop [] s) 145 | in 146 | v ~kind ~enc ~dec 147 | 148 | let seq = list string 149 | 150 | let pair c0 c1 = 151 | let kind = Printf.sprintf "%s * %s" (kind c0) (kind c1) in 152 | let enc (v0, v1) = enc seq [enc c0 v0; enc c1 v1] in 153 | let dec s = match dec seq s with 154 | | [lenc; renc] -> (dec c0 lenc), (dec c1 renc) 155 | | _ -> err ~kind s 156 | in 157 | v ~kind ~enc ~dec 158 | 159 | let t2 = pair 160 | 161 | let t3 c0 c1 c2 = 162 | let kind = Printf.sprintf "%s * %s * %s" (kind c0) (kind c1) (kind c2) in 163 | let seq = list string in 164 | let enc (v0, v1, v2) = enc seq [enc c0 v0; enc c1 v1; enc c2 v2] in 165 | let dec s = match (dec seq) s with 166 | | [v0; v1; v2] -> (dec c0 v0), (dec c1 v1), (dec c2 v2) 167 | | _ -> err ~kind s 168 | in 169 | v ~kind ~enc ~dec 170 | 171 | let t4 c0 c1 c2 c3 = 172 | let kind = 173 | Printf.sprintf "%s * %s * %s * %s" (kind c0) (kind c1) (kind c2) (kind c3) 174 | in 175 | let seq = list string in 176 | let enc (v0, v1, v2, v3) = 177 | enc seq [enc c0 v0; enc c1 v1; enc c2 v2; enc c3 v3] 178 | in 179 | let dec s = match (dec seq) s with 180 | | [v0; v1; v2; v3] -> (dec c0 v0), (dec c1 v1), (dec c2 v2), (dec c3 v3) 181 | | _ -> err ~kind s 182 | in 183 | v ~kind ~enc ~dec 184 | 185 | let t5 c0 c1 c2 c3 c4 = 186 | let kind = 187 | Printf.sprintf "%s * %s * %s * %s * %s" 188 | (kind c0) (kind c1) (kind c2) (kind c3) (kind c4) 189 | in 190 | let seq = list string in 191 | let enc (v0, v1, v2, v3, v4) = 192 | enc seq [enc c0 v0; enc c1 v1; enc c2 v2; enc c3 v3; enc c4 v4] 193 | in 194 | let dec s = match (dec seq) s with 195 | | [v0; v1; v2; v3; v4] -> 196 | (dec c0 v0), (dec c1 v1), (dec c2 v2), (dec c3 v3), (dec c4 v4) 197 | | _ -> err ~kind s 198 | in 199 | v ~kind ~enc ~dec 200 | 201 | let alt ~kind tag cs = 202 | let l = Array.length cs in 203 | if l > 256 then invalid_arg @@ Topkg_string.strf "too many codecs (%d)" l; 204 | let enc v = 205 | let tag = tag v in 206 | Printf.sprintf "%c%s" (Char.chr tag) (enc cs.(tag) v) 207 | in 208 | let dec s = match Topkg_string.head s with 209 | | None -> err ~kind s 210 | | Some tag -> 211 | let tag = Char.code tag in 212 | if tag < Array.length cs then dec cs.(tag) (tail s) else 213 | err ~kind s 214 | in 215 | v ~kind ~enc ~dec 216 | 217 | let version version = 218 | let enc_version = string_of_int version in 219 | fun c -> 220 | let kind = Printf.sprintf "(%s) v%s" (kind c) enc_version in 221 | let enc v = String.concat "\x00" [enc_version; enc c v] in 222 | let dec s = match Topkg_string.cut ~sep:'\x00' s with 223 | | None -> err ~kind s 224 | | Some (fnd_version, s) -> 225 | try 226 | let fnd = int_of_string fnd_version in 227 | if fnd <> version then err_version ~exp:version ~fnd else 228 | dec c s 229 | with 230 | | Failure _ (* of int_of_string *) -> err ~kind s 231 | in 232 | v ~kind ~enc ~dec 233 | 234 | let view ?kind:k (inj, proj) c = 235 | let kind = match k with None -> kind c | Some k -> k in 236 | let enc v = enc c (inj v) in 237 | let dec s = proj (dec c s) in 238 | v ~kind ~enc ~dec 239 | 240 | let msg = 241 | let msg = (fun (`Msg m) -> m), (fun m -> `Msg m) in 242 | view msg string 243 | 244 | let result_error_msg ok = result ~ok ~error:msg 245 | let fpath = string 246 | let cmd = 247 | let cmd = 248 | (fun cmd -> Topkg_cmd.to_list cmd), 249 | (fun l -> Topkg_cmd.of_list l) 250 | in 251 | view cmd (list string) 252 | -------------------------------------------------------------------------------- /src/topkg_codec.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Topkg interprocess communication codec. 7 | 8 | See {!Topkg.Private.Codec} for documentation. *) 9 | 10 | (** {1 Codec} *) 11 | 12 | open Topkg_result 13 | 14 | type error = Corrupted of (string * string) | Version of int * int 15 | val pp_error : Format.formatter -> error -> unit 16 | exception Error of error 17 | val err : kind:string -> string -> 'a 18 | 19 | type 'a t 20 | 21 | val v : kind:string -> enc:('a -> string) -> dec:(string -> 'a) -> 'a t 22 | val kind : 'a t -> string 23 | val enc : 'a t -> 'a -> string 24 | val dec : 'a t -> string -> 'a 25 | val dec_result : 'a t -> string -> 'a result 26 | val with_kind : string -> 'a t -> 'a t 27 | val write : Topkg_fpath.t -> 'a t -> 'a -> unit result 28 | val read : Topkg_fpath.t -> 'a t -> 'a result 29 | 30 | val unit : unit t 31 | val const : 'a -> 'a t 32 | val bool : bool t 33 | val int : int t 34 | val string : string t 35 | val option : 'a t -> 'a option t 36 | val result : ok:'a t -> error:'b t -> ('a, 'b) r t 37 | val list : 'a t -> 'a list t 38 | val pair : 'a t -> 'b t -> ('a * 'b) t 39 | val t2 : 'a t -> 'b t -> ('a * 'b) t 40 | val t3 : 'a t -> 'b t -> 'c t -> ('a * 'b * 'c) t 41 | val t4 : 'a t -> 'b t -> 'c t -> 'd t -> ('a * 'b * 'c * 'd) t 42 | val t5 : 'a t -> 'b t -> 'c t -> 'd t -> 'e t -> ('a * 'b * 'c * 'd * 'e) t 43 | val alt : kind:string -> ('a -> int) -> 'a t array -> 'a t 44 | val version : int -> 'a t -> 'a t 45 | val view : ?kind:string -> ('a -> 'b) * ('b -> 'a) -> 'b t -> 'a t 46 | 47 | val msg : [`Msg of string ] t 48 | val result_error_msg : 'a t -> 'a result t 49 | 50 | val fpath : Topkg_fpath.t t 51 | val cmd : Topkg_cmd.t t 52 | -------------------------------------------------------------------------------- /src/topkg_conf.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Build configuration 7 | 8 | See {!Topkg.Conf}. *) 9 | 10 | open Topkg_result 11 | 12 | (** {1 Configuration key value converters} *) 13 | 14 | type 'a conv 15 | 16 | val conv : 17 | ?docv:string -> (string -> 'a result) -> (Format.formatter -> 'a -> unit) -> 18 | 'a conv 19 | 20 | val conv_with_docv : 'a conv -> docv:string -> 'a conv 21 | val conv_parser : 'a conv -> (string -> 'a result) 22 | val conv_printer : 'a conv -> (Format.formatter -> 'a -> unit) 23 | val conv_docv : 'a conv -> string 24 | 25 | val bool : bool conv 26 | val int : int conv 27 | val string : string conv 28 | val fpath : Topkg_fpath.t conv 29 | val some : ?none:string -> 'a conv -> 'a option conv 30 | 31 | (** {1 Configuration keys} *) 32 | 33 | type 'a key 34 | 35 | val key : 36 | ?docv:string -> ?doc:string -> ?env:string -> string -> 'a conv -> 37 | absent:'a -> 'a key 38 | 39 | val discovered_key : 40 | ?docv:string -> ?doc:string -> ?env:string -> string -> 'a conv -> 41 | absent:(unit -> 'a result) -> 'a key 42 | 43 | val with_pkg : ?default:bool -> string -> bool key 44 | 45 | val pp_keys_cli_opts : Format.formatter -> unit -> unit 46 | 47 | (** {1 Build configuration} *) 48 | 49 | type t 50 | val empty : t 51 | val value : t -> 'a key -> 'a 52 | val pp_value : t -> Format.formatter -> 'a key -> unit 53 | val dump : Format.formatter -> t -> unit 54 | val of_cli_args : 55 | pkg_name:string -> build_dir:Topkg_fpath.t -> string list -> t result 56 | 57 | val pkg_name : t -> string 58 | val build_dir : t -> Topkg_fpath.t 59 | val vcs : t -> bool 60 | val pinned : t -> bool 61 | val dev_pkg : t -> bool 62 | val jobs : t -> int 63 | 64 | type build_context = [`Dev | `Distrib | `Pin ] 65 | val build_context : t -> [`Dev | `Distrib | `Pin ] 66 | val build_tests : t -> bool 67 | 68 | val debug : t -> bool 69 | val debugger_support : t -> bool 70 | val profile : t -> bool 71 | val toolchain : t -> string option 72 | 73 | (** {1 Tool lookup} *) 74 | 75 | type os = [ `Build_os | `Host_os ] 76 | val tool : ?conf:t -> string -> os -> Topkg_cmd.t 77 | 78 | (** {1 OCaml configuration} *) 79 | 80 | module OCaml : sig 81 | type conf = t 82 | type t 83 | val v : conf -> os -> t 84 | val find : string -> t -> string option 85 | val version : t -> int * int * int * string option 86 | val ext_obj : t -> string 87 | val ext_asm : t -> string 88 | val ext_lib : t -> string 89 | val ext_dll : t -> string 90 | val ext_exe : t -> string 91 | val native : t -> bool 92 | val native_dynlink : t -> bool 93 | val supports_shared_libraries : t -> bool 94 | val word_size : t -> int 95 | val dump : Format.formatter -> t -> unit 96 | end 97 | -------------------------------------------------------------------------------- /src/topkg_distrib.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Topkg_result 7 | 8 | (* Watermarks *) 9 | 10 | type watermark = 11 | string * 12 | [ `String of string 13 | | `Name 14 | | `Version 15 | | `Version_num 16 | | `Vcs of [ `Commit_id ] 17 | | `Opam of Topkg_fpath.t option * string * string ] 18 | 19 | let opam_fields file = 20 | (Topkg_opam.File.fields file) 21 | |> R.reword_error_msg ~replace:true (fun msg -> R.msgf "Watermarks: %s" msg) 22 | |> Topkg_log.on_error_msg ~level:Topkg_log.Warning ~use:(fun () -> []) 23 | 24 | let opam_field = 25 | let find k m = try Some (List.assoc k m) with Not_found -> None in 26 | let opam_memo = ref [] in (* memoizes the opam files *) 27 | let rec opam_field file field = match find file !opam_memo with 28 | | None -> 29 | opam_memo := (file, (opam_fields file)) :: !opam_memo; 30 | opam_field file field 31 | | Some fields -> 32 | match find field fields with 33 | | Some vs -> vs 34 | | None -> 35 | Topkg_log.warn 36 | (fun m -> m "file %s: opam field %S undefined or unsupported" 37 | file field); 38 | ["UNDEFINED"] 39 | in 40 | opam_field 41 | 42 | let vcs_commit_id () = 43 | (Topkg_vcs.get () >>= fun repo -> Topkg_vcs.head ~dirty:true repo) 44 | |> R.reword_error_msg ~replace:true 45 | (fun msg -> R.msgf "Watermarks: VCS commit id determination: %s" msg) 46 | |> Topkg_log.on_error_msg ~level:Topkg_log.Warning 47 | ~use:(fun () -> "UNDEFINED") 48 | 49 | let define_watermarks ~name ~version ~opam watermarks = 50 | let define (id, v) = 51 | let (id, v as def) = match v with 52 | | `String s -> (id, s) 53 | | `Version -> (id, version) 54 | | `Version_num -> (id, Topkg_string.drop_initial_v version) 55 | | `Name -> (id, name) 56 | | `Vcs `Commit_id -> (id, vcs_commit_id ()) 57 | | `Opam (file, field, sep) -> 58 | let file = match file with None -> opam | Some file -> file in 59 | (id, String.concat sep (opam_field file field)) 60 | in 61 | Topkg_log.info (fun m -> m "Watermark %s = %S" id v); 62 | def 63 | in 64 | List.map define watermarks 65 | 66 | let watermark_file ws file = 67 | Topkg_os.File.read file >>= fun content -> 68 | Topkg_os.File.write_subst file ws content >>= fun () -> 69 | Topkg_log.info (fun m -> m "Watermarked %s" file); Ok () 70 | 71 | let rec watermark_files ws = function 72 | | [] -> Ok () 73 | | f :: fs -> watermark_file ws f >>= fun () -> watermark_files ws fs 74 | 75 | (* Defaults *) 76 | 77 | let default_watermarks = 78 | let space = " " in 79 | let comma = ", " in 80 | [ "NAME", `Name; 81 | "VERSION", `Version; 82 | "VERSION_NUM", `Version_num; 83 | "VCS_COMMIT_ID", `Vcs `Commit_id; 84 | "PKG_MAINTAINER", `Opam (None, "maintainer", comma); 85 | "PKG_AUTHORS", `Opam (None, "authors", comma); 86 | "PKG_HOMEPAGE", `Opam (None, "homepage", comma); 87 | "PKG_ISSUES", `Opam (None, "bug-reports", space); 88 | "PKG_DOC", `Opam (None, "doc", space); 89 | "PKG_LICENSE", `Opam (None, "license", comma); 90 | "PKG_REPO", `Opam (None, "dev-repo", space); ] 91 | 92 | let default_files_to_watermark = 93 | let is_file f = 94 | Topkg_os.File.exists f |> Topkg_log.on_error_msg ~use:(fun _ -> false) 95 | in 96 | let is_binary_ext ext = 97 | let module Set = Set.Make (String) in 98 | let exts = 99 | Set.(empty |> 100 | add ".eps" |> add ".flv" |> add ".gif" |> add ".ico" |> 101 | add ".jpeg" |> add ".jpg" |> add ".mov" |> add ".mp3" |> 102 | add ".mp4" |> add ".otf" |> add ".pdf" |> add ".png" |> 103 | add ".ps" |> add ".ttf" |> add ".woff") 104 | in 105 | Set.mem ext exts 106 | in 107 | let keep f = not (is_binary_ext @@ Topkg_fpath.get_ext f) && is_file f in 108 | fun () -> 109 | Topkg_vcs.get () 110 | >>= fun repo -> Topkg_vcs.tracked_files repo 111 | >>= fun files -> Ok (List.filter keep files) 112 | 113 | let default_massage () = Ok () 114 | 115 | let default_exclude_paths () = 116 | Ok [".git"; ".gitignore"; ".gitattributes"; ".hg"; ".hgignore"; "build"; 117 | "Makefile"; "_build"] 118 | 119 | (* Distribution *) 120 | 121 | type t = 122 | { watermarks : watermark list; 123 | files_to_watermark : unit -> Topkg_fpath.t list result; 124 | massage : unit -> unit result; 125 | exclude_paths : unit -> Topkg_fpath.t list result; 126 | uri : string option; } 127 | 128 | let v 129 | ?(watermarks = default_watermarks) 130 | ?(files_to_watermark = default_files_to_watermark) 131 | ?(massage = fun () -> Ok ()) 132 | ?(exclude_paths = default_exclude_paths) 133 | ?uri () = 134 | { watermarks; files_to_watermark; massage; exclude_paths; uri } 135 | 136 | let watermarks d = d.watermarks 137 | let files_to_watermark d = d.files_to_watermark 138 | let massage d = d.massage 139 | let exclude_paths d = d.exclude_paths 140 | let uri d = d.uri 141 | let codec = 142 | let uri = Topkg_codec.(with_kind "uri" @@ option string) in 143 | let fields = 144 | let stub () = invalid_arg "not executable outside package definition" in 145 | (fun d -> d.uri), 146 | (fun uri -> 147 | { watermarks = [] (* bad *); files_to_watermark = stub; 148 | massage = stub; exclude_paths = stub; uri }) 149 | in 150 | Topkg_codec.version 0 @@ 151 | Topkg_codec.(view ~kind:"distrib" fields uri) 152 | -------------------------------------------------------------------------------- /src/topkg_distrib.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** {1 Distribution description} 7 | 8 | See {!section:Topkg.Pkg.distrib}. *) 9 | 10 | (** {1 Distribution} *) 11 | 12 | open Topkg_result 13 | 14 | (* Watermarks *) 15 | 16 | type watermark = 17 | string * 18 | [ `String of string | `Name | `Version | `Version_num | `Vcs of [ `Commit_id ] 19 | | `Opam of Topkg_fpath.t option * string * string ] 20 | 21 | val define_watermarks : 22 | name:string -> version:string -> opam:Topkg_fpath.t -> 23 | watermark list -> (string * string) list 24 | 25 | val watermark_file : (string * string) list -> Topkg_fpath.t -> unit result 26 | val watermark_files : 27 | (string * string) list -> Topkg_fpath.t list -> unit result 28 | 29 | (* Distribution *) 30 | 31 | type t 32 | 33 | val v : 34 | ?watermarks:watermark list -> 35 | ?files_to_watermark:(unit -> Topkg_fpath.t list result) -> 36 | ?massage:(unit -> unit result) -> 37 | ?exclude_paths:(unit -> Topkg_fpath.t list result) -> 38 | ?uri:string -> 39 | unit -> t 40 | 41 | val watermarks : t -> watermark list 42 | val files_to_watermark : t -> (unit -> Topkg_fpath.t list result) 43 | val massage : t -> (unit -> unit result) 44 | val exclude_paths : t -> (unit -> Topkg_fpath.t list result) 45 | val uri : t -> string option 46 | val codec : t Topkg_codec.t 47 | 48 | (* Defaults *) 49 | 50 | val default_watermarks : watermark list 51 | val default_files_to_watermark : unit -> Topkg_fpath.t list result 52 | val default_massage : unit -> unit result 53 | val default_exclude_paths : unit -> Topkg_fpath.t list result 54 | -------------------------------------------------------------------------------- /src/topkg_fexts.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | type ext = [ `Ext of string | `Obj | `Real_clib | `Lib | `Dll | `Exe ] 7 | type t = ext list 8 | 9 | let interface = [ `Ext ".mli"; `Ext ".cmi"; `Ext ".cmti"; ] 10 | let cmx = [ `Ext ".cmx" ] 11 | let api = interface @ cmx 12 | let real_c_library = [ `Real_clib ] 13 | let c_library = [ `Lib ] 14 | let c_dll_library = [ `Dll ] 15 | let library = [` Ext ".cma"; `Ext ".cmxa"; `Ext ".cmxs" ] @ c_library 16 | let module_library = (api @ library) 17 | let exe = [ `Exe ] 18 | let ext e = [ `Ext e ] 19 | let exts es = List.map (fun e -> `Ext e) es 20 | 21 | let ext_to_string c = 22 | let ext_obj = Topkg_conf.OCaml.ext_obj c in 23 | let ext_lib = Topkg_conf.OCaml.ext_lib c in 24 | let ext_dll = Topkg_conf.OCaml.ext_dll c in 25 | let ext_exe = Topkg_conf.OCaml.ext_exe c in 26 | function 27 | | `Ext s -> s 28 | | `Obj -> ext_obj 29 | | `Lib | `Real_clib -> ext_lib 30 | | `Dll -> ext_dll 31 | | `Exe -> ext_exe 32 | -------------------------------------------------------------------------------- /src/topkg_fexts.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** File extensions. 7 | 8 | See {!Topkg.Exts} for documentation. *) 9 | 10 | (** {1 File extensions} *) 11 | 12 | type ext = [`Ext of string | `Obj | `Real_clib | `Lib | `Dll | `Exe] 13 | 14 | type t = ext list 15 | 16 | val interface : ext list 17 | val api : ext list 18 | val cmx : ext list 19 | val real_c_library : ext list 20 | val c_library : ext list 21 | val c_dll_library : ext list 22 | val library : ext list 23 | val module_library : ext list 24 | val exe : ext list 25 | val exts : string list -> ext list 26 | val ext : string -> ext list 27 | val ext_to_string : Topkg_conf.OCaml.t -> ext -> string 28 | -------------------------------------------------------------------------------- /src/topkg_fpath.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | type t = string 7 | 8 | let dir_sep_prefix s = 9 | Topkg_string.is_prefix ~affix:Filename.dir_sep s || 10 | (String.length s > 0 && s.[0] = '/') 11 | 12 | let dir_sep_suffix s = 13 | Topkg_string.is_suffix ~affix:Filename.dir_sep s || 14 | (String.length s > 0 && s.[String.length s - 1] = '/') 15 | 16 | let append = 17 | fun p q -> match p with 18 | | "" -> q 19 | | p -> 20 | match q with 21 | | "" -> p 22 | | q -> 23 | if dir_sep_prefix q then q else 24 | if dir_sep_suffix p then (p ^ q) else 25 | (p ^ "/" ^ q) 26 | 27 | let ( // ) = append 28 | 29 | let is_dir_path p = match p with 30 | | "." | ".." -> true 31 | | _ -> 32 | let is_suffix affix = Topkg_string.is_suffix ~affix p in 33 | List.exists is_suffix ["/"; "/.."; "/."] 34 | 35 | let is_file_path p = not (is_dir_path p) 36 | 37 | let basename s = Filename.basename s 38 | let dirname s = Filename.dirname s 39 | 40 | let last_dot_index s = try Some (String.rindex s '.') with Not_found -> None 41 | let get_ext s = match last_dot_index s with 42 | | None -> "" 43 | | Some i -> Topkg_string.with_index_range ~first:i s 44 | 45 | let has_ext e p = Topkg_string.is_suffix ~affix:e p 46 | 47 | let rem_ext s = match last_dot_index s with 48 | | None -> s 49 | | Some i -> Topkg_string.with_index_range ~last:(i - 1) s 50 | -------------------------------------------------------------------------------- /src/topkg_fpath.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** File system paths. 7 | 8 | See {!Topkg.Fpath}. *) 9 | 10 | (** {1 File system paths} *) 11 | 12 | type t = string 13 | val append : t -> t -> t 14 | val ( // ) : t -> t -> t 15 | 16 | val is_dir_path : t -> bool 17 | val is_file_path : t -> bool 18 | 19 | val basename : t -> string 20 | val dirname : t -> string 21 | 22 | val get_ext : t -> string 23 | val has_ext : string -> t -> bool 24 | val rem_ext : t -> t 25 | -------------------------------------------------------------------------------- /src/topkg_install.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Package install. *) 7 | 8 | (** {1 Install} *) 9 | 10 | type t 11 | 12 | val nothing : t 13 | val flatten : t list -> t 14 | val to_build : 15 | ?header:string -> 16 | Topkg_conf.t -> 17 | Topkg_conf.os -> t list -> 18 | (Topkg_fpath.t list * Topkg_opam.Install.t * Topkg_test.t list option) 19 | 20 | type field = 21 | ?force:bool -> ?built:bool -> ?cond:bool -> ?exts:Topkg_fexts.t -> 22 | ?dst:string -> string -> t 23 | 24 | val bin : ?auto:bool -> field 25 | val doc : field 26 | val etc : field 27 | val lib : field 28 | val lib_root : field 29 | val libexec : ?auto:bool -> field 30 | val libexec_root : ?auto:bool -> field 31 | val man : field 32 | val misc : field 33 | val sbin : ?auto:bool -> field 34 | val share : field 35 | val share_root : field 36 | val stublibs : field 37 | val toplevel : field 38 | val unknown : string -> field 39 | 40 | val test : 41 | ?run:bool -> ?dir:Topkg_fpath.t -> ?args:Topkg_cmd.t -> ?auto:bool -> field 42 | 43 | val mllib : 44 | ?field:field -> ?cond:bool -> ?cma:bool -> ?cmxa:bool -> ?cmxs:bool -> 45 | ?api:string list -> ?dst_dir:Topkg_fpath.t -> Topkg_fpath.t -> t 46 | 47 | val clib : 48 | ?dllfield:field -> 49 | ?libfield:field -> 50 | ?cond:bool -> ?lib_dst_dir:Topkg_fpath.t -> Topkg_fpath.t -> t 51 | 52 | val codec : t Topkg_codec.t 53 | -------------------------------------------------------------------------------- /src/topkg_ipc.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Topkg_result 7 | 8 | type 'a t = 9 | { cmd : Topkg_cmd.t; 10 | codec : 'a Topkg_codec.t; 11 | answer : Topkg_fpath.t; } 12 | 13 | let v ?answer cmd codec = 14 | let answer = match answer with 15 | | Some a -> a 16 | | None -> 17 | (Topkg_os.File.tmp ()) 18 | |> R.reword_error_msg ~replace:true 19 | (fun m -> R.msgf "Could not create IPC answer file: %s, using stdout" m) 20 | |> Topkg_log.on_error_msg ~use:(fun () -> Topkg_os.File.dash) 21 | in 22 | let cmd = Topkg_cmd.(v answer %% cmd) in 23 | { cmd; codec; answer } 24 | 25 | let cmd ipc = ipc.cmd 26 | let codec ipc = ipc.codec 27 | let answer ipc = ipc.answer 28 | 29 | let error_args args = 30 | R.error_msgf "IPC: %a, unknown arguments" 31 | Topkg_cmd.dump (Topkg_cmd.of_list args) 32 | 33 | (* Package description IPC. Description functions raise Invalid_argument 34 | at the other end. *) 35 | 36 | let pkg () = v Topkg_cmd.(v "pkg") Topkg_pkg.codec 37 | let answer_pkg answer p = Topkg_codec.write answer Topkg_pkg.codec p 38 | 39 | (* Run custom lint IPC *) 40 | 41 | let lint_custom_codec = Topkg_codec.(option @@ list @@ result_error_msg @@ msg) 42 | let lint_custom () = 43 | let cmd = Topkg_cmd.(v "lint" % "custom") in 44 | v cmd lint_custom_codec 45 | 46 | let answer_lint_custom answer p = 47 | let custom_run = match (Topkg_pkg.lint_custom p) with 48 | | None -> None 49 | | Some custom -> Some (custom ()) 50 | in 51 | Topkg_codec.write answer lint_custom_codec custom_run 52 | 53 | (* Distrib prepare IPC *) 54 | 55 | let distrib_prepared_codec = 56 | Topkg_codec.version 0 @@ 57 | Topkg_codec.(with_kind "prepared" @@ result_error_msg (list fpath)) 58 | 59 | let distrib_prepare ~dist_build_dir ~name ~version ~opam ~opam_adds = 60 | let cmd = 61 | Topkg_cmd.(v "distrib" % "prepare" % 62 | "dist-build-dir" % dist_build_dir % "name" % name % 63 | "version" % version % "opam" % opam % "opam-adds" % opam_adds) 64 | in 65 | v cmd distrib_prepared_codec 66 | 67 | let answer_distrib_prepare 68 | answer p ~dist_build_dir ~name ~version ~opam ~opam_adds 69 | = 70 | Topkg_codec.write answer distrib_prepared_codec @@ 71 | Topkg_pkg.distrib_prepare p ~dist_build_dir ~name ~version ~opam ~opam_adds 72 | 73 | (* IPC answer *) 74 | 75 | let write_answer cmd p = match Topkg_cmd.to_list cmd with 76 | | answer :: "pkg" :: [] -> 77 | answer_pkg answer p 78 | | answer :: "lint" :: "custom" :: [] -> 79 | answer_lint_custom answer p 80 | | answer :: "distrib" :: "prepare" :: 81 | "dist-build-dir" :: dist_build_dir :: "name" :: name :: 82 | "version" :: version :: "opam" :: opam :: "opam-adds" :: opam_adds :: [] -> 83 | answer_distrib_prepare 84 | answer p ~dist_build_dir ~name ~version ~opam ~opam_adds 85 | 86 | | args -> 87 | error_args args 88 | -------------------------------------------------------------------------------- /src/topkg_ipc.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Topkg interprocess communication. 7 | 8 | See {!Topkg.Private.Ipc} for documentation. *) 9 | 10 | open Topkg_result 11 | 12 | (** {1 Interprocess communication} *) 13 | 14 | type 'a t 15 | 16 | val v : ?answer:Topkg_fpath.t -> Topkg_cmd.t -> 'a Topkg_codec.t -> 'a t 17 | val cmd : 'a t -> Topkg_cmd.t 18 | val codec : 'a t -> 'a Topkg_codec.t 19 | val answer : 'a t -> Topkg_fpath.t 20 | 21 | val pkg : unit -> Topkg_pkg.t t 22 | val lint_custom : unit -> Topkg_result.R.msg Topkg_result.result list option t 23 | val distrib_prepare : 24 | dist_build_dir:string -> name:string -> version:string -> opam:string -> 25 | opam_adds:string -> Topkg_fpath.t list result t 26 | 27 | val write_answer : Topkg_cmd.t -> Topkg_pkg.t -> unit Topkg_result.result 28 | -------------------------------------------------------------------------------- /src/topkg_log.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Topkg_result 7 | 8 | type level = App | Error | Warning | Info | Debug 9 | 10 | let exec = match Array.length Sys.argv with 11 | | 0 -> Filename.basename Sys.executable_name 12 | | n -> Filename.basename Sys.argv.(0) 13 | 14 | let _level = 15 | let default = Some Warning in 16 | let init = 17 | try match Sys.getenv "TOPKG_VERBOSITY" with 18 | | l when Topkg_string.is_prefix ~affix:"quiet" l -> None 19 | | l when Topkg_string.is_prefix ~affix:"error" l -> Some Error 20 | | l when Topkg_string.is_prefix ~affix:"warning" l -> Some Warning 21 | | l when Topkg_string.is_prefix ~affix:"info" l -> Some Info 22 | | l when Topkg_string.is_prefix ~affix:"debug" l -> Some Debug 23 | | l -> 24 | Format.eprintf 25 | "%s: @[TOPKG_VERBOSITY env var unknown value: %S@]@." exec l; 26 | default 27 | with Not_found | Sys_error _ -> default 28 | in 29 | ref init 30 | 31 | let level () = !_level 32 | let set_level l = _level := l 33 | 34 | let level_to_string = function 35 | | None -> "quiet" | Some App -> "app" | Some Error -> "error" 36 | | Some Warning -> "warning" | Some Info -> "info" | Some Debug -> "debug" 37 | 38 | let level_of_string = function 39 | | "quiet" -> Ok None 40 | | "app" -> Ok (Some App) 41 | | "error" -> Ok (Some Error) 42 | | "warning" -> Ok (Some Warning) 43 | | "info" -> Ok (Some Info) 44 | | "debug" -> Ok (Some Debug) 45 | | l -> R.error_msgf "%S: unknown log level" l 46 | 47 | type 'a msgf = 48 | (?header:string -> ('a, Format.formatter, unit) format -> 'a) -> unit 49 | 50 | let _err_count = ref 0 51 | let err_count () = !_err_count 52 | 53 | let _warn_count = ref 0 54 | let warn_count () = !_warn_count 55 | 56 | let pp_level_header ppf (h,l) = match h with 57 | | Some h -> Format.fprintf ppf "[%s] " h 58 | | None -> 59 | Format.pp_print_string ppf begin match l with 60 | | App -> "" 61 | | Error -> "[ERROR] " 62 | | Warning -> "[WARNING] " 63 | | Info -> "[INFO] " 64 | | Debug -> "[DEBUG] " 65 | end 66 | 67 | let msg level msgf = match !_level with 68 | | None -> () 69 | | Some level' when level > level' -> 70 | if level = Error then incr _err_count else 71 | if level = Warning then incr _warn_count else () 72 | | Some _ -> 73 | (if level = Error then incr _err_count else 74 | if level = Warning then incr _warn_count else ()); 75 | let pr = if level = App then Format.printf else Format.eprintf in 76 | msgf @@ 77 | (fun ?header fmt -> 78 | pr ("%s: %a@[" ^^ fmt ^^ "@]@.") exec pp_level_header (header, level)) 79 | 80 | let app msgf = msg App msgf 81 | let err msgf = msg Error msgf 82 | let warn msgf = msg Warning msgf 83 | let info msgf = msg Info msgf 84 | let debug msgf = msg Debug msgf 85 | 86 | let on_error_msg ?(level = Error) ~use = function 87 | | Ok v -> v 88 | | Error (`Msg e) -> msg level (fun m -> m "%s" e); use () 89 | -------------------------------------------------------------------------------- /src/topkg_log.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Log 7 | 8 | Abridged [logs]. See {!Topkg.Log} for documentation. *) 9 | 10 | (** {1 Log} *) 11 | 12 | open Topkg_result 13 | 14 | type level = App | Error | Warning | Info | Debug 15 | 16 | val level : unit -> level option 17 | val set_level : level option -> unit 18 | val level_to_string : level option -> string 19 | val level_of_string : string -> (level option, [`Msg of string]) r 20 | 21 | type 'a msgf = 22 | (?header:string -> ('a, Format.formatter, unit) format -> 'a) -> unit 23 | 24 | val msg : level -> 'a msgf -> unit 25 | val app : 'a msgf -> unit 26 | val err : 'a msgf -> unit 27 | val warn : 'a msgf -> unit 28 | val info : 'a msgf -> unit 29 | val debug : 'a msgf -> unit 30 | 31 | val on_error_msg : ?level:level -> use:(unit -> 'a) -> 'a result -> 'a 32 | val err_count : unit -> int 33 | val warn_count : unit -> int 34 | -------------------------------------------------------------------------------- /src/topkg_main.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Entry point for [pkg.ml] files. *) 7 | 8 | (** {1 Main} *) 9 | 10 | open Topkg_result 11 | 12 | val describe : 13 | ?delegate:Topkg_cmd.t -> 14 | ?readmes:Topkg_pkg.std_file list -> 15 | ?licenses:Topkg_pkg.std_file list -> 16 | ?change_logs:Topkg_pkg.std_file list -> 17 | ?metas:Topkg_pkg.meta_file list -> 18 | ?opams:Topkg_pkg.opam_file list -> 19 | ?lint_files:Topkg_fpath.t list option -> 20 | ?lint_custom:(unit -> R.msg result list) -> 21 | ?distrib:Topkg_distrib.t -> 22 | ?publish:Topkg_publish.t -> 23 | ?build:Topkg_build.t -> 24 | string -> (Topkg_conf.t -> Topkg_install.t list result) -> unit 25 | 26 | val disable : unit -> unit 27 | -------------------------------------------------------------------------------- /src/topkg_opam.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Topkg_result 7 | 8 | (* opam File *) 9 | 10 | module File = struct 11 | type t = (string * string list) list 12 | 13 | let codec = 14 | Topkg_codec.version 0 @@ 15 | Topkg_codec.with_kind "opam fields" @@ 16 | Topkg_codec.(list (pair string (list string))) 17 | 18 | let topkg_cmd = Topkg_cmd.v "topkg" 19 | let topkg_cmd_available () = 20 | Topkg_os.Cmd.must_exist topkg_cmd 21 | |> R.reword_error_msg ~replace:true 22 | (fun m -> R.msgf "%s. Did you install topkg-care ?" m) 23 | 24 | let ipc_cmd file = 25 | (* Propagate the log level to the IPC call *) 26 | let level = Topkg_log.(level_to_string (level ())) in 27 | let verbosity = Topkg_string.strf "--verbosity=%s" level in 28 | Topkg_cmd.(v "ipc" % verbosity % "opam-fields" % file) 29 | 30 | let fields file = 31 | begin 32 | let cmd = Topkg_cmd.(topkg_cmd %% ipc_cmd file) in 33 | topkg_cmd_available () 34 | >>= fun _ -> Topkg_os.File.must_exist file 35 | >>= fun _ -> Topkg_os.Cmd.(run_out cmd |> to_string) 36 | >>= fun s -> (Topkg_codec.dec_result codec s) 37 | end 38 | |> R.reword_error_msg ~replace:true 39 | (fun msg -> R.msgf "opam fields of %s: %s" file msg) 40 | end 41 | 42 | (* opam install file *) 43 | 44 | module Install = struct 45 | 46 | type field = 47 | [ `Bin 48 | | `Doc 49 | | `Etc 50 | | `Lib 51 | | `Lib_root 52 | | `Libexec 53 | | `Libexec_root 54 | | `Man 55 | | `Misc 56 | | `Sbin 57 | | `Share 58 | | `Share_root 59 | | `Stublibs 60 | | `Toplevel 61 | | `Unknown of string ] 62 | 63 | let field_to_string = function 64 | | `Bin -> "bin" 65 | | `Doc -> "doc" 66 | | `Etc -> "etc" 67 | | `Lib -> "lib" 68 | | `Lib_root -> "lib_root" 69 | | `Libexec -> "libexec" 70 | | `Libexec_root -> "libexec_root" 71 | | `Man -> "man" 72 | | `Misc -> "misc" 73 | | `Sbin -> "sbin" 74 | | `Share -> "share" 75 | | `Share_root -> "share_root" 76 | | `Stublibs -> "stublibs" 77 | | `Toplevel -> "toplevel" 78 | | `Unknown name -> name 79 | 80 | type move = { src : string; dst : string option; maybe : bool; } 81 | 82 | let move ?(maybe = false) ?dst src = { src; dst; maybe } 83 | 84 | type t = [ `Header of string option ] * (field * move) list 85 | 86 | let to_string (`Header h, mvs) = 87 | let b = Buffer.create 1024 in 88 | let pr b fmt = Printf.bprintf b fmt in 89 | let pr_header b = function None -> () | Some h -> pr b "# %s\n\n" h in 90 | let pr_src b src maybe = 91 | pr b " \"%s%s\"" (if maybe then "?" else "") src 92 | in 93 | let pr_dst b dst = match dst with 94 | | None -> () 95 | | Some dst -> pr b " {\"%s\"}" dst 96 | in 97 | let pr_field_end b last = if last <> "" (* not start *) then pr b " ]\n" in 98 | let pr_field b last field = 99 | if last = field then pr b "\n" else 100 | (pr_field_end b last; pr b "%s: [\n" field) 101 | in 102 | let pr_move b last (field, { src; dst; maybe }) = 103 | pr_field b last field; 104 | pr_src b src maybe; 105 | pr_dst b dst; 106 | field 107 | in 108 | let sortable (field, mv) = (field_to_string field, mv) in 109 | let moves = List.sort compare (List.rev_map sortable mvs) in 110 | pr_header b h; 111 | let last = List.fold_left (pr_move b) "" moves in 112 | pr_field_end b last; 113 | Buffer.contents b 114 | end 115 | -------------------------------------------------------------------------------- /src/topkg_opam.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** opam helpers. 7 | 8 | See also {!Topkg.Private.Opam}. *) 9 | 10 | open Topkg_result 11 | 12 | module File : sig 13 | type t = (string * string list) list 14 | val codec : t Topkg_codec.t 15 | val fields : Topkg_fpath.t -> t result 16 | end 17 | 18 | (** opam install file. 19 | 20 | A module to generate opam install files. 21 | 22 | {b Reference}. 23 | {{:http://opam.ocaml.org/doc/manual/dev-manual.html#sec25} 24 | Syntax and semantics} of opam install files. *) 25 | module Install : sig 26 | 27 | (** {1 opam install files} *) 28 | 29 | type field = 30 | [ `Bin 31 | | `Doc 32 | | `Etc 33 | | `Lib 34 | | `Lib_root 35 | | `Libexec 36 | | `Libexec_root 37 | | `Man 38 | | `Misc 39 | | `Sbin 40 | | `Share 41 | | `Share_root 42 | | `Stublibs 43 | | `Toplevel 44 | | `Unknown of string ] 45 | (** The type for opam install file fields. *) 46 | 47 | type move 48 | (** The type for file moves. *) 49 | 50 | val move : ?maybe:bool -> ?dst:Topkg_fpath.t -> Topkg_fpath.t -> move 51 | (** [move ~maybe ~dst src] moves [src] to [dst], where [dst] is a 52 | path relative to the directory corresponding to the 53 | {{!field}field}. If [maybe] is [true] (defaults to [false]), 54 | then [src] may not exist, otherwise an install error will occur 55 | if the file doesn't exist. *) 56 | 57 | type t = [ `Header of string option ] * (field * move) list 58 | (** The type for opam install files. An optional starting header 59 | comment and a list of field moves. *) 60 | 61 | val to_string : t -> string 62 | (** [to_string t] is [t] as syntactically valid opam install file. *) 63 | end 64 | -------------------------------------------------------------------------------- /src/topkg_os.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** OS interaction. 7 | 8 | Abridged [bos]. See {!Topkg.OS} for documentation. *) 9 | 10 | (** {1 OS} *) 11 | 12 | open Topkg_result 13 | 14 | module Env : sig 15 | val var : string -> string option 16 | val opt_var : string -> absent:string -> string 17 | end 18 | 19 | module File : sig 20 | val null : Topkg_fpath.t 21 | val dash : Topkg_fpath.t 22 | 23 | val exists : Topkg_fpath.t -> bool result 24 | val must_exist : Topkg_fpath.t -> Topkg_fpath.t result 25 | val delete : ?must_exist:bool -> Topkg_fpath.t -> unit result 26 | 27 | val fold : 28 | ?skip:(Topkg_fpath.t -> bool) -> (Topkg_fpath.t -> 'a -> 'a) -> 29 | 'a -> Topkg_fpath.t list -> 'a result 30 | 31 | val read : Topkg_fpath.t -> string result 32 | val write : Topkg_fpath.t -> string -> unit result 33 | val write_subst : 34 | Topkg_fpath.t -> (string * string) list -> string -> unit result 35 | 36 | val tmp : unit -> Topkg_fpath.t result 37 | end 38 | 39 | module Dir : sig 40 | val exists : Topkg_fpath.t -> bool result 41 | val must_exist : Topkg_fpath.t -> Topkg_fpath.t result 42 | 43 | val current : unit -> Topkg_fpath.t result 44 | val set_current : Topkg_fpath.t -> unit result 45 | val with_current : Topkg_fpath.t -> ('a -> 'b) -> 'a -> 'b result 46 | 47 | val contents : 48 | ?dotfiles:bool -> ?rel:bool -> Topkg_fpath.t -> Topkg_fpath.t list result 49 | end 50 | 51 | module Cmd : sig 52 | val exists : Topkg_cmd.t -> bool result 53 | val must_exist : Topkg_cmd.t -> Topkg_cmd.t result 54 | 55 | val run : ?err:Topkg_fpath.t -> Topkg_cmd.t -> unit result 56 | val run_status : ?err:Topkg_fpath.t -> Topkg_cmd.t -> [`Exited of int] result 57 | 58 | 59 | type run_status = Topkg_cmd.t * [`Exited of int ] 60 | val success : ('a * run_status) result -> 'a result 61 | 62 | type run_out 63 | 64 | val out_string : ?trim:bool -> run_out -> (string * run_status) result 65 | val out_lines : ?trim:bool -> run_out -> (string list * run_status) result 66 | val out_file : Topkg_fpath.t -> run_out -> (unit * run_status) result 67 | val out_stdout : run_out -> (unit * run_status) result 68 | 69 | val to_string : ?trim:bool -> run_out -> string result 70 | val to_lines : ?trim:bool -> run_out -> string list result 71 | val to_file : Topkg_fpath.t -> run_out -> unit result 72 | val run_out : ?err:Topkg_fpath.t -> Topkg_cmd.t -> run_out 73 | end 74 | -------------------------------------------------------------------------------- /src/topkg_pkg.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Package descriptions. *) 7 | 8 | (** {1 Package} *) 9 | 10 | open Topkg_result 11 | 12 | type std_file 13 | val std_file : ?install:bool -> Topkg_fpath.t -> std_file 14 | 15 | type meta_file 16 | val meta_file : ?lint:bool -> ?install:bool -> Topkg_fpath.t -> meta_file 17 | 18 | type opam_file 19 | val opam_file : 20 | ?lint:bool -> ?lint_deps_excluding:string list option -> ?install:bool -> 21 | Topkg_fpath.t -> opam_file 22 | 23 | type t 24 | 25 | val empty : t 26 | val with_name_and_build_dir : 27 | ?name:string -> ?build_dir:Topkg_fpath.t -> t -> t 28 | 29 | val v : 30 | ?delegate:Topkg_cmd.t -> 31 | ?readmes:std_file list -> 32 | ?licenses:std_file list -> 33 | ?change_logs:std_file list -> 34 | ?metas:meta_file list -> 35 | ?opams:opam_file list -> 36 | ?lint_files:Topkg_fpath.t list option -> 37 | ?lint_custom:(unit -> R.msg result list) -> 38 | ?distrib:Topkg_distrib.t -> 39 | ?publish:Topkg_publish.t -> 40 | ?build:Topkg_build.t -> string -> 41 | (Topkg_conf.t -> Topkg_install.t list result) -> t 42 | 43 | val name : t -> string 44 | val delegate : t -> Topkg_cmd.t option 45 | val readmes : t -> Topkg_fpath.t list 46 | val change_logs : t -> Topkg_fpath.t list 47 | val licenses : t -> Topkg_fpath.t list 48 | val distrib : t -> Topkg_distrib.t 49 | val build : t -> Topkg_build.t 50 | val install : t -> Topkg_conf.t -> Topkg_install.t list result 51 | val codec : t Topkg_codec.t 52 | 53 | (* Derived accessors *) 54 | 55 | val build_dir : t -> Topkg_fpath.t 56 | val opam : name:string -> t -> Topkg_fpath.t 57 | 58 | (* Distrib *) 59 | 60 | val distrib_uri : t -> string option 61 | val distrib_prepare : 62 | t -> dist_build_dir:Topkg_fpath.t -> name:string -> version:string -> 63 | opam:Topkg_fpath.t -> opam_adds:string -> Topkg_fpath.t list result 64 | 65 | (* Publish *) 66 | 67 | val publish_artefacts : t -> [ `Distrib | `Doc | `Alt of string ] list 68 | 69 | (* Test *) 70 | 71 | val test : 72 | t -> list:bool -> tests:string list -> args:Topkg_cmd.t option -> int result 73 | 74 | (* Build *) 75 | 76 | val build : 77 | t -> kind:[`Build | `Dry_run | `Raw of string list ] -> 78 | Topkg_conf.t -> Topkg_conf.os -> int result 79 | 80 | (* Clean *) 81 | 82 | val clean : t -> Topkg_conf.os -> int result 83 | 84 | (* Lint *) 85 | 86 | val lint_custom : t -> (unit -> R.msg result list) option 87 | val lint_files : t -> Topkg_fpath.t list option 88 | val lint_metas : t -> (Topkg_fpath.t * bool) list 89 | val lint_opams : t -> (Topkg_fpath.t * bool * string list option) list 90 | -------------------------------------------------------------------------------- /src/topkg_publish.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | open Topkg_result 7 | 8 | type artefact = [`Distrib | `Doc | `Alt of string] 9 | type t = { artefacts : artefact list } 10 | 11 | let v ?(artefacts = [`Doc; `Distrib]) () = { artefacts } 12 | let artefacts p = p.artefacts 13 | let codec_artefact = 14 | let tag = function `Distrib -> 0 | `Doc -> 1 | `Alt _ -> 2 in 15 | let codecs = 16 | let alt_case = 17 | ((function `Alt s -> s | _ -> assert false), 18 | (function s -> `Alt s)) 19 | in 20 | Topkg_codec.([| const `Distrib; const `Doc; view alt_case string |]) 21 | in 22 | Topkg_codec.alt ~kind:"artefact" tag codecs 23 | 24 | let codec = 25 | let artefacts = Topkg_codec.(list codec_artefact) in 26 | let fields = 27 | (fun p -> p.artefacts), 28 | (fun artefacts -> { artefacts }) 29 | in 30 | Topkg_codec.version 0 @@ 31 | Topkg_codec.(view ~kind:"publish" fields artefacts) 32 | -------------------------------------------------------------------------------- /src/topkg_publish.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** {1 Distribution publication description} 7 | 8 | See {!section:Topkg.Pkg.publish}. *) 9 | 10 | (** {1 Distribution} *) 11 | 12 | open Topkg_result 13 | 14 | (* Publication *) 15 | 16 | type artefact = [`Distrib | `Doc | `Alt of string] 17 | type t 18 | 19 | val v : ?artefacts:artefact list -> unit -> t 20 | val artefacts : t -> artefact list 21 | val codec : t Topkg_codec.t 22 | -------------------------------------------------------------------------------- /src/topkg_result.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | let ( >>= ) v f = match v with Ok v -> f v | Error _ as e -> e 7 | let ( >>| ) v f = match v with Ok v -> Ok (f v) | Error _ as e -> e 8 | 9 | type ('a, 'b) r = ('a, 'b) result = Ok of 'a | Error of 'b 10 | type 'a result = ('a, [`Msg of string]) r 11 | 12 | module R = struct 13 | type msg = [`Msg of string ] 14 | 15 | let msgf fmt = 16 | let kmsg _ = `Msg (Format.flush_str_formatter ()) in 17 | Format.kfprintf kmsg Format.str_formatter fmt 18 | 19 | let reword_error reword = function 20 | | Ok _ as r -> r 21 | | Error e -> Error (reword e) 22 | 23 | let error_msg m = Error (`Msg m) 24 | let error_msgf fmt = 25 | let kerr _ = Error (`Msg (Format.flush_str_formatter ())) in 26 | Format.kfprintf kerr Format.str_formatter fmt 27 | 28 | let reword_error_msg ?(replace = false) reword = function 29 | | Ok _ as r -> r 30 | | Error (`Msg e) -> 31 | let (`Msg e' as v) = reword e in 32 | if replace then Error v else error_msgf "%s\n%s" e e' 33 | end 34 | -------------------------------------------------------------------------------- /src/topkg_result.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Results 7 | 8 | Abbridged [rresult]. See {!section:Topkg.prels} for documention. *) 9 | 10 | val ( >>= ) : ('a, 'b) result -> ('a -> ('c, 'b) result) -> ('c, 'b) result 11 | val ( >>| ) : ('a, 'b) result -> ('a -> 'c) -> ('c, 'b) result 12 | 13 | type ('a, 'b) r = ('a, 'b) result = Ok of 'a | Error of 'b 14 | type 'a result = ('a, [ `Msg of string]) r 15 | 16 | module R : sig 17 | val reword_error : ('b -> 'c) -> ('a, 'b) r -> ('a, 'c) r 18 | 19 | type msg = [ `Msg of string ] 20 | 21 | val msgf : ('a, Format.formatter, unit, [> msg]) format4 -> 'a 22 | 23 | val error_msg : string -> ('b, [> msg]) r 24 | val error_msgf : 25 | ('a, Format.formatter, unit, ('b, [> msg]) r) format4 -> 'a 26 | 27 | val reword_error_msg : 28 | ?replace:bool -> (string -> msg) -> ('a, msg) r -> ('a, [> msg]) r 29 | end 30 | -------------------------------------------------------------------------------- /src/topkg_string.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | let strf = Format.asprintf 7 | 8 | include String 9 | 10 | let head s = if s = "" then None else Some s.[0] 11 | 12 | (* Predicates *) 13 | 14 | let is_prefix ~affix s = 15 | let len_a = length affix in 16 | let len_s = length s in 17 | if len_a > len_s then false else 18 | let max_idx_a = len_a - 1 in 19 | let rec loop i = 20 | if i > max_idx_a then true else 21 | if unsafe_get affix i <> unsafe_get s i then false else loop (i + 1) 22 | in 23 | loop 0 24 | 25 | let is_suffix ~affix s = 26 | let max_idx_a = length affix - 1 in 27 | let max_idx_s = length s - 1 in 28 | if max_idx_a > max_idx_s then false else 29 | let rec loop i = 30 | if i > max_idx_a then true else 31 | if unsafe_get affix (max_idx_a - i) <> unsafe_get s (max_idx_s - i) 32 | then false 33 | else loop (i + 1) 34 | in 35 | loop 0 36 | 37 | let for_all sat s = 38 | let max_idx = length s - 1 in 39 | let rec loop i = 40 | if i > max_idx then true else 41 | if sat (unsafe_get s i) then loop (i + 1) else false 42 | in 43 | loop 0 44 | 45 | let exists sat s = 46 | let max_idx = length s - 1 in 47 | let rec loop i = 48 | if i > max_idx then false else 49 | if sat (unsafe_get s i) then true else loop (i + 1) 50 | in 51 | loop 0 52 | 53 | (* Traversing *) 54 | 55 | let find_byte ?(start = 0) c s = 56 | let max = String.length s - 1 in 57 | if start > max then None else 58 | try Some (String.index_from s start c) with Not_found -> None 59 | 60 | (* Extracting substrings *) 61 | 62 | let with_index_range ?(first = 0) ?last s = 63 | let max = String.length s - 1 in 64 | let last = match last with 65 | | None -> max 66 | | Some l when l > max -> max 67 | | Some l -> l 68 | in 69 | let first = if first < 0 then 0 else first in 70 | if first > last then "" else 71 | String.sub s first (last - first + 1) 72 | 73 | let cut ?(rev = false) ~sep s = 74 | let find_index = if rev then String.rindex else String.index in 75 | match try Some (find_index s sep) with Not_found -> None with 76 | | None -> None 77 | | Some i -> 78 | Some (String.sub s 0 i, String.sub s (i+1) (String.length s - i - 1)) 79 | 80 | let cuts ?(empty = true) ~sep s = 81 | let no_empty = not empty in 82 | let rec loop acc s = match cut ~sep s with 83 | | Some (v, vs) -> loop (if no_empty && v = "" then acc else (v :: acc)) vs 84 | | None -> List.rev (if no_empty && s = "" then acc else (s :: acc)) 85 | in 86 | loop [] s 87 | 88 | (* Version strings *) 89 | 90 | let parse_version v = 91 | let version = 92 | if is_prefix ~affix:"v" v then with_index_range ~first:1 v else v 93 | in 94 | let cut_left_plus_or_tilde s = 95 | let cut = match String.index_opt s '+', String.index_opt s '~' with 96 | | None, None -> None 97 | | (Some _ as i), None | None, (Some _ as i) -> i 98 | | Some i, Some i' -> Some (if i < i' then i else i') 99 | in 100 | match cut with 101 | | None -> None 102 | | Some i -> 103 | Some (with_index_range ~last:(i - 1) s, with_index_range ~first:i s) 104 | in 105 | try match cut ~sep:'.' version with 106 | | None -> None 107 | | Some (maj, rest) -> 108 | let maj = int_of_string maj in 109 | match cut ~sep:'.' rest with 110 | | None -> 111 | begin match cut_left_plus_or_tilde rest with 112 | | None -> Some (maj, int_of_string rest, 0, None) 113 | | Some (min, i) -> Some (maj, int_of_string min, 0, Some i) 114 | end 115 | | Some (min, rest) -> 116 | let min = int_of_string min in 117 | begin match cut_left_plus_or_tilde rest with 118 | | None -> Some (maj, min, int_of_string rest, None) 119 | | Some (p, i) -> Some (maj, min, int_of_string p, Some i) 120 | end 121 | with 122 | | Failure _ -> None 123 | 124 | let drop_initial_v version = match head version with 125 | | Some ('v' | 'V') -> with_index_range ~first:1 version 126 | | None | Some _ -> version 127 | 128 | (* Formatters *) 129 | 130 | let pp_text ppf s = (* was c&p from Fmt, pp_print_text is 4.02 *) 131 | let is_nl_or_sp c = c = '\n' || c = ' ' in 132 | let rec stop_at sat ~start ~max s = 133 | if start > max then start else 134 | if sat s.[start] then start else 135 | stop_at sat ~start:(start + 1) ~max s 136 | in 137 | let sub s start stop ~max = 138 | if start = stop then "" else 139 | if start = 0 && stop > max then s else 140 | String.sub s start (stop - start) 141 | in 142 | let max = String.length s - 1 in 143 | let rec loop start s = match stop_at is_nl_or_sp ~start ~max s with 144 | | stop when stop > max -> Format.pp_print_string ppf (sub s start stop ~max) 145 | | stop -> 146 | Format.pp_print_string ppf (sub s start stop ~max); 147 | begin match s.[stop] with 148 | | ' ' -> Format.pp_print_space ppf () 149 | | '\n' -> Format.pp_force_newline ppf () 150 | | _ -> assert false 151 | end; 152 | loop (stop + 1) s 153 | in 154 | loop 0 s 155 | -------------------------------------------------------------------------------- /src/topkg_string.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Strings. 7 | 8 | See {!Topkg.String} for documentation. *) 9 | 10 | val strf : ('a, Format.formatter, unit, string) format4 -> 'a 11 | 12 | include module type of String 13 | 14 | val head : string -> char option 15 | 16 | val is_prefix : affix:string -> string -> bool 17 | val is_suffix : affix:string -> string -> bool 18 | val for_all : (char -> bool) -> string -> bool 19 | val exists : (char -> bool) -> string -> bool 20 | 21 | val find_byte : ?start:int -> char -> string -> int option 22 | 23 | val trim : string -> string 24 | val cut : ?rev:bool -> sep:char -> string -> (string * string) option 25 | val cuts : ?empty:bool -> sep:char -> string -> string list 26 | 27 | val with_index_range : ?first:int -> ?last:int -> string -> string 28 | 29 | val uppercase_ascii : string -> string 30 | 31 | val parse_version : string -> (int * int * int * string option) option 32 | val drop_initial_v : string -> string 33 | 34 | val pp_text : Format.formatter -> string -> unit 35 | -------------------------------------------------------------------------------- /src/topkg_test.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | type t = 7 | { exec : Topkg_fpath.t; 8 | args : Topkg_cmd.t; 9 | run : bool; 10 | dir : Topkg_fpath.t option; } 11 | 12 | let v exec ~args ~run ~dir = { exec; args; run; dir } 13 | let exec t = t.exec 14 | let args t = t.args 15 | let run t = t.run 16 | let dir t = t.dir 17 | let codec = 18 | let fields = 19 | (fun t -> (t.exec, t.args, t.run, t.dir)), 20 | (fun (exec, args, run, dir) -> { exec; args; run; dir}) 21 | in 22 | Topkg_codec.(view ~kind:"test" fields (t4 fpath cmd bool (option fpath))) 23 | -------------------------------------------------------------------------------- /src/topkg_test.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** Topkg test description. *) 7 | 8 | type t 9 | 10 | val v : 11 | Topkg_fpath.t -> args:Topkg_cmd.t -> run:bool -> 12 | dir:Topkg_fpath.t option -> t 13 | 14 | val exec : t -> Topkg_fpath.t 15 | val args : t -> Topkg_cmd.t 16 | val run : t -> bool 17 | val dir : t -> Topkg_fpath.t option 18 | val codec : t Topkg_codec.t 19 | -------------------------------------------------------------------------------- /src/topkg_vcs.mli: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | (** VCS repositories. 7 | 8 | See {!Topkg.Vcs} for documentation. *) 9 | 10 | (** {1 VCS} *) 11 | 12 | open Topkg_result 13 | 14 | type kind = [ `Git | `Hg ] 15 | val pp_kind : Format.formatter -> kind -> unit 16 | 17 | type commit_ish = string 18 | 19 | type t 20 | 21 | val kind : t -> kind 22 | val dir : t -> Topkg_fpath.t 23 | val find : ?dir:Topkg_fpath.t -> unit -> t option result 24 | val get : ?dir:Topkg_fpath.t -> unit -> t result 25 | val cmd : t -> Topkg_cmd.t 26 | val pp : Format.formatter -> t -> unit 27 | 28 | val is_dirty : t -> bool result 29 | val not_dirty : t -> unit result 30 | val file_is_dirty : t -> Topkg_fpath.t -> bool result 31 | val head : ?dirty:bool -> t -> string result 32 | val commit_id : ?dirty:bool -> ?commit_ish:string -> t -> string result 33 | val commit_ptime_s : ?commit_ish:commit_ish -> t -> int result 34 | val describe : ?dirty:bool -> ?commit_ish:string -> t -> string result 35 | val tags : t -> string list result 36 | val changes : 37 | ?until:string -> t -> after:string -> (string * string) list result 38 | 39 | val tracked_files : ?tree_ish:string -> t -> Topkg_fpath.t list result 40 | 41 | val clone : t -> dir:Topkg_fpath.t -> unit result 42 | val checkout : ?branch:string -> t -> commit_ish:string -> unit result 43 | val commit_files : ?msg:string -> t -> Topkg_fpath.t list -> unit result 44 | 45 | val delete_tag : t -> string -> unit result 46 | val tag : 47 | ?force:bool -> ?sign:bool -> ?msg:string -> ?commit_ish:string -> t -> 48 | string -> unit result 49 | -------------------------------------------------------------------------------- /test/echo-delegate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ocaml 2 | #use "topfind" 3 | #require "bos.setup" 4 | #require "fmt" 5 | open Bos_setup 6 | 7 | let ok = Ok 0 8 | let unsupported = Ok 1 9 | 10 | let publish = function 11 | | "distrib" :: uri :: name :: version :: msg :: archive :: _ -> 12 | Fmt.pr "publish distrib %S %S %S %S %S@." uri name version msg archive; ok 13 | | "doc" :: uri :: name :: version :: msg :: docdir :: _ -> 14 | Fmt.pr "publish doc %S %S %S %S %S@." uri name version msg docdir; ok 15 | | "alt" :: k :: uri :: name :: version :: msg :: archive :: _ -> 16 | Fmt.pr "publish alt %S %S %S %S %S %S@." k uri name version msg archive; ok 17 | | args -> 18 | unsupported 19 | 20 | let issue = function 21 | | "list" :: uri :: _ -> Fmt.pr "issue list %S@." uri; ok 22 | | "show" :: uri :: id :: _ -> Fmt.pr "issue show %S %S@." uri id; ok 23 | | "open" :: uri :: t :: d :: _ -> Fmt.pr "issue open %S %S %S@." uri t d; ok 24 | | "close" :: uri :: id :: m :: _ -> Fmt.pr "issue close %S %S %S@." uri id m; ok 25 | | args -> unsupported 26 | 27 | let request = function 28 | | "publish" :: args -> publish args 29 | | "issue" :: args -> issue args 30 | | args -> unsupported 31 | 32 | let main () = 33 | let doc = "the unsupportive delegate" in 34 | begin match OS.Arg.(parse ~doc ~pos:string ()) with 35 | | "ipc" :: verbosity :: req -> 36 | Logs.level_of_string verbosity 37 | >>= fun level -> Logs.set_level level; request req 38 | | "ipc" :: [] -> 39 | R.error_msg "malformed delegate request, verbosity is missing" 40 | | args -> 41 | R.error_msgf "unknown arguments: %s" (String.concat ~sep:" " args) 42 | end 43 | |> Logs.on_error_msg ~use:(fun () -> 2) 44 | 45 | let () = exit (main ()) 46 | -------------------------------------------------------------------------------- /test/test.ml: -------------------------------------------------------------------------------- 1 | (*--------------------------------------------------------------------------- 2 | Copyright (c) 2016 Daniel C. Bünzli. All rights reserved. 3 | SPDX-License-Identifier: ISC 4 | ---------------------------------------------------------------------------*) 5 | 6 | let () = 7 | let args = String.concat " " (List.tl (Array.to_list Sys.argv)) in 8 | Printf.printf "The test is ok, the arguments are: %s\n" args 9 | -------------------------------------------------------------------------------- /test/unsupportive-delegate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ocaml 2 | #use "topfind" 3 | #require "bos.setup" 4 | open Bos_setup 5 | 6 | let unsupported = Ok 1 7 | 8 | let publish = function 9 | | "distrib" :: uri :: name :: version :: msg :: archive :: _ -> 10 | unsupported 11 | | "doc" :: uri :: name :: version :: msg :: docdir :: _ -> 12 | unsupported 13 | | "alt" :: kind :: uri :: name :: version :: msg :: archive :: _ -> 14 | unsupported 15 | | args -> 16 | unsupported 17 | 18 | let issue = function 19 | | "list" :: uri :: _ -> unsupported 20 | | "show" :: uri :: id :: _ -> unsupported 21 | | "open" :: uri :: title :: descr :: _ -> unsupported 22 | | "close" :: uri :: id :: msg :: _ -> unsupported 23 | | args -> unsupported 24 | 25 | let request = function 26 | | "publish" :: args -> publish args 27 | | "issue" :: args -> issue args 28 | | args -> unsupported 29 | 30 | let main () = 31 | let doc = "the unsupportive delegate" in 32 | begin match OS.Arg.(parse ~doc ~pos:string ()) with 33 | | "ipc" :: verbosity :: req -> 34 | Logs.level_of_string verbosity 35 | >>= fun level -> Logs.set_level level; request req 36 | | "ipc" :: [] -> 37 | R.error_msg "malformed delegate request, verbosity is missing" 38 | | args -> 39 | R.error_msgf "unknown arguments: %s" (String.concat ~sep:" " args) 40 | end 41 | |> Logs.on_error_msg ~use:(fun () -> 2) 42 | 43 | let () = exit (main ()) 44 | -------------------------------------------------------------------------------- /topkg-care.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | name: "topkg-care" 3 | synopsis: "The transitory OCaml software packager" 4 | description: """\ 5 | **Warning** Topkg is in maintenance mode and should not longer be used. 6 | 7 | Topkg is a packager for distributing OCaml software. It provides an 8 | API to describe the files a package installs in a given build 9 | configuration and to specify information about the package's 10 | distribution, creation and publication procedures. 11 | 12 | The optional topkg-care package provides the `topkg` command line tool 13 | which helps with various aspects of a package's life cycle: creating 14 | and linting a distribution, releasing it on the WWW, publish its 15 | documentation, add it to the OCaml opam repository, etc. 16 | 17 | Topkg is distributed under the ISC license and has **no** 18 | dependencies. This is what your packages will need as a *build* 19 | dependency. 20 | 21 | Topkg-care is distributed under the ISC license it depends on 22 | [fmt][fmt], [logs][logs], [bos][bos], [cmdliner][cmdliner], 23 | [webbrowser][webbrowser] and `opam-format`. 24 | 25 | [fmt]: http://erratique.ch/software/fmt 26 | [logs]: http://erratique.ch/software/logs 27 | [bos]: http://erratique.ch/software/bos 28 | [cmdliner]: http://erratique.ch/software/cmdliner 29 | [webbrowser]: http://erratique.ch/software/webbrowser 30 | 31 | Home page: """ 32 | maintainer: "Daniel Bünzli " 33 | authors: "The topkg programmers" 34 | license: "ISC" 35 | tags: ["packaging" "ocamlbuild" "org:erratique"] 36 | homepage: "https://erratique.ch/software/topkg" 37 | doc: "https://erratique.ch/software/topkg/doc" 38 | bug-reports: "https://github.com/dbuenzli/topkg/issues" 39 | depends: [ 40 | "ocaml" {>= "4.08.0"} 41 | "ocamlfind" {build & >= "1.6.1"} 42 | "ocamlbuild" 43 | "topkg" {= version} 44 | "fmt" {>= "0.9.0"} 45 | "logs" 46 | "bos" {>= "0.2.1"} 47 | "cmdliner" {>= "1.3.0"} 48 | "webbrowser" 49 | "opam-format" {>= "2.0.0"} 50 | ] 51 | build: ["ocaml" "pkg/pkg.ml" "build" "--pkg-name" name "--dev-pkg" "%{dev}%"] 52 | dev-repo: "git+https://erratique.ch/repos/topkg.git" 53 | x-maintenance-intent: ["(latest)"] 54 | -------------------------------------------------------------------------------- /topkg.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | name: "topkg" 3 | synopsis: "The transitory OCaml software packager" 4 | description: """\ 5 | **Warning** Topkg is in maintenance mode and should not longer be used. 6 | 7 | Topkg is a packager for distributing OCaml software. It provides an 8 | API to describe the files a package installs in a given build 9 | configuration and to specify information about the package's 10 | distribution, creation and publication procedures. 11 | 12 | The optional topkg-care package provides the `topkg` command line tool 13 | which helps with various aspects of a package's life cycle: creating 14 | and linting a distribution, releasing it on the WWW, publish its 15 | documentation, add it to the OCaml opam repository, etc. 16 | 17 | Topkg is distributed under the ISC license and has **no** 18 | dependencies. This is what your packages will need as a *build* 19 | dependency. 20 | 21 | Topkg-care is distributed under the ISC license it depends on 22 | [fmt][fmt], [logs][logs], [bos][bos], [cmdliner][cmdliner], 23 | [webbrowser][webbrowser] and `opam-format`. 24 | 25 | [fmt]: http://erratique.ch/software/fmt 26 | [logs]: http://erratique.ch/software/logs 27 | [bos]: http://erratique.ch/software/bos 28 | [cmdliner]: http://erratique.ch/software/cmdliner 29 | [webbrowser]: http://erratique.ch/software/webbrowser 30 | 31 | Home page: """ 32 | maintainer: "Daniel Bünzli " 33 | authors: "The topkg programmers" 34 | license: "ISC" 35 | tags: ["packaging" "ocamlbuild" "org:erratique"] 36 | homepage: "https://erratique.ch/software/topkg" 37 | doc: "https://erratique.ch/software/topkg/doc" 38 | bug-reports: "https://github.com/dbuenzli/topkg/issues" 39 | depends: [ 40 | "ocaml" {>= "4.08.0"} 41 | "ocamlfind" {build & >= "1.6.1"} 42 | "ocamlbuild" 43 | ] 44 | build: ["ocaml" "pkg/pkg.ml" "build" "--pkg-name" name "--dev-pkg" "%{dev}%"] 45 | dev-repo: "git+https://erratique.ch/repos/topkg.git" 46 | x-maintenance-intent: ["(latest)"] 47 | --------------------------------------------------------------------------------