├── .gitignore ├── .merlin ├── Makefile ├── README.md ├── _tags ├── build.ml ├── build.mli └── opam /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | sources/ 3 | build/ 4 | _build/ 5 | -------------------------------------------------------------------------------- /.merlin: -------------------------------------------------------------------------------- 1 | PKG fmt astring 2 | S . 3 | B _build/ -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | ocamlbuild -pkgs fmt.tty,unix,astring build.native -no-links 3 | @if [ ! -e opam-build ]; then ln -s _build/build.native opam-build; fi 4 | 5 | clean: 6 | ocamlbuild -clean 7 | rm -rf _build/ opam-build 8 | rm -rf build/ sources/ .opamconfig 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## opam-build 2 | 3 | A sandboxed build environment for opam projects. 4 | 5 | ### Usage 6 | 7 | Running `opam build` at the root of an opam project (i.e. a repo having an 8 | `opam` file) will create: 9 | 10 | - an `.opamconfig` file with project settings 11 | - a `sources/` repo with everyting needed to compile the package 12 | - a `build/` directory, which is valid (local) opam root, which 13 | uses only code packages in `sources/` 14 | 15 | ### Status 16 | 17 | This is still very experimental, feedback and patches are very welcome. -------------------------------------------------------------------------------- /_tags: -------------------------------------------------------------------------------- 1 | true: debug, bin_annot 2 | true: warn_error(+1..49), warn(A-4-41-44) 3 | 4 | "sources": -traverse 5 | "build": -traverse 6 | -------------------------------------------------------------------------------- /build.ml: -------------------------------------------------------------------------------- 1 | (* 2 | * Copyright (c) 2016 Thomas Gazagnaire 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | *) 16 | 17 | open Astring 18 | 19 | let () = Fmt_tty.setup_std_outputs () 20 | 21 | (* PRELUDE *) 22 | (* FIXME: use Bos *) 23 | 24 | let debug = ref true 25 | 26 | let date () = 27 | let now = Unix.gettimeofday () in 28 | (* Feb 26 07:07:50 *) 29 | let local = Unix.localtime now in 30 | let month = match local.Unix.tm_mon with 31 | | 0 -> "Jan" | 1 -> "Feb" | 2 -> "Mar" | 3 -> "Apr" | 4 -> "May" | 5 -> "Jun" 32 | | 6 -> "Jul" | 7 -> "Aug" | 8 -> "Sep" | 9 -> "Oct" | 10 -> "Nov" | 11 -> "Dec" 33 | | _ -> assert false in 34 | Printf.sprintf "%s %d %02d:%02d:%02d" month local.Unix.tm_mday 35 | local.Unix.tm_hour local.Unix.tm_min local.Unix.tm_sec 36 | 37 | let err fmt = Printf.ksprintf (fun e -> failwith (date () ^ " " ^ e)) fmt 38 | let fail cmd fmt = Printf.ksprintf (fun e -> err "%s %s: %s" (date ()) cmd e) fmt 39 | let failv cmd fmt = 40 | Printf.ksprintf (fun e -> `Error (Printf.sprintf "%s %s: %s" (date ()) cmd e)) fmt 41 | 42 | let show fmt = 43 | Fmt.kstrf (fun str -> 44 | Fmt.(pf stdout) "%a\n%!" Fmt.(styled `Cyan string) str 45 | ) fmt 46 | 47 | let debug fmt = 48 | Fmt.kstrf (fun cmd -> 49 | if !debug then 50 | Fmt.(pf stdout) "%s %a %s\n%!" 51 | (date ()) Fmt.(styled `Yellow string) "=>" cmd 52 | ) fmt 53 | 54 | let check_exit_status cmd (out, err) status = 55 | match status with 56 | | Unix.WEXITED 0 -> 57 | debug "command successful"; 58 | `Ok (out, err) 59 | | Unix.WEXITED i -> 60 | debug "command failed with code %d" i; 61 | failv cmd "exit %d" i 62 | | Unix.WSIGNALED i -> 63 | debug "command caught signal %d" i; 64 | if i = Sys.sigkill then fail cmd "timeout" else fail cmd "signal %d" i 65 | | Unix.WSTOPPED i -> 66 | debug "command stopped %d" i; 67 | fail cmd "stopped %d" i 68 | 69 | let read_lines ?(prefix="") oc = 70 | let rec aux acc = 71 | let line = 72 | try 73 | let line = input_line oc in 74 | debug "%s %s" prefix line; 75 | Some line 76 | with End_of_file -> None 77 | in 78 | match line with 79 | | Some l -> aux (l :: acc) 80 | | None -> List.rev acc 81 | in 82 | aux [] 83 | 84 | let syscall ?env cmd = 85 | let env = match env with None -> Unix.environment () | Some e -> e in 86 | let oc, ic, ec = Unix.open_process_full cmd env in 87 | let out = ref [] in 88 | let err = ref [] in 89 | (* FIXME: deadlock *) 90 | out := read_lines ~prefix:"stdout:" oc; 91 | err := read_lines ~prefix:"stderr:" ec; 92 | let exit_status = Unix.close_process_full (oc, ic, ec) in 93 | check_exit_status cmd (!out, !err) exit_status 94 | 95 | let read_outputs ?env fmt = Printf.ksprintf (syscall ?env) fmt 96 | 97 | let exec ?env fmt = 98 | Printf.ksprintf (fun cmd -> 99 | debug "%s" cmd; 100 | match read_outputs ?env "%s" cmd with 101 | | `Ok _ -> () 102 | | `Error e -> err "exec: %s" e 103 | ) fmt 104 | 105 | let read_stdout ?env fmt = 106 | Printf.ksprintf (fun cmd -> 107 | debug "%s" cmd; 108 | match read_outputs ?env "%s" cmd with 109 | | `Ok (out, _) -> out 110 | | `Error e -> err "exec: %s" e 111 | ) fmt 112 | 113 | let read_line ?env fmt = 114 | Printf.ksprintf (fun str -> 115 | match read_stdout ?env "%s" str with 116 | | [] -> "" 117 | | h::_ -> h 118 | ) fmt 119 | 120 | let in_dir dir f = 121 | let pwd = Sys.getcwd () in 122 | let reset () = if pwd <> dir then Sys.chdir pwd in 123 | if pwd <> dir then Sys.chdir dir; 124 | try let r = f () in reset (); r 125 | with e -> reset (); raise e 126 | 127 | let rmdir path = exec "rm -rf %s" path 128 | let mkdir path = exec "mkdir -p %s" path 129 | let (/) = Filename.concat 130 | 131 | let some x = Some x 132 | 133 | (* END OF PRELUDE *) 134 | 135 | let opam ?env ?root fmt = 136 | let root = match root with 137 | | None -> "" 138 | | Some s -> " --root=" ^ s 139 | in 140 | Printf.ksprintf (fun cmd -> 141 | exec ?env "OPAMYES=1 opam %s%s" cmd root 142 | ) fmt 143 | 144 | let opam_readl ?env ?root fmt = 145 | let root = match root with 146 | | None -> "" 147 | | Some s -> " --root=" ^ s 148 | in 149 | Printf.ksprintf (fun cmd -> 150 | read_line ?env "OPAMYES=1 opam %s%s" cmd root 151 | ) fmt 152 | 153 | (* let opam_info ?root ~pkg field = opam_readl ?root "info %s -f %s" pkg field *) 154 | 155 | type pin = string * [`Local of string | `Dev | `Other of string] 156 | 157 | let string_of_pin (k, v) = match v with 158 | | `Other path 159 | | `Local path -> k ^ ":" ^ path 160 | | `Dev -> k 161 | 162 | let string_of_pins pins = String.concat ~sep:" " @@ List.map string_of_pin pins 163 | 164 | let pin_of_string str = 165 | match String.cut ~sep:":" str with 166 | | None -> str, `Dev 167 | | Some (name, path) -> 168 | if Sys.file_exists path && Sys.is_directory path then name, `Local path 169 | else name, `Other path 170 | 171 | let pins_of_string str = 172 | String.cuts ~sep:" " ~empty:false str 173 | |> List.map pin_of_string 174 | 175 | type config = { 176 | name: string; 177 | ocaml_version: string; 178 | compiler: string; 179 | preinstalled: bool; 180 | os: string; 181 | pins: pin list; 182 | } 183 | 184 | let pp_config ppf config = 185 | let pp k v = Fmt.(pf ppf "%s %a\n" k (styled `Yellow @@ string) v) in 186 | pp "name: " config.name; 187 | pp "ocaml-version:" config.ocaml_version; 188 | pp "compiler: " config.compiler; 189 | pp "preinstalled: " (string_of_bool config.preinstalled); 190 | pp "os: " config.os; 191 | pp "pins: " (string_of_pins config.pins) 192 | 193 | let default_config () = 194 | let ocaml_version = opam_readl "config var ocaml-version" in 195 | let compiler = opam_readl "config var compiler" in 196 | let preinstalled = opam_readl "config var preinstalled" |> bool_of_string in 197 | let os = opam_readl "config var os" in 198 | let name = "local-pkg" in 199 | let pins = [] in 200 | { name; ocaml_version; compiler; preinstalled; os; pins } 201 | 202 | (* FIXME: this is horrible, I'm so sorry... *) 203 | let config_of_string str = 204 | let read ~d k = 205 | match read_line "echo '%s' | jq '.%s' | xargs" str k with 206 | | "null" -> d 207 | | x -> x 208 | in 209 | let name = read ~d:"local-pkg" "name" in 210 | let default_config = default_config () in 211 | let ocaml_version = read ~d:default_config.ocaml_version "ocaml" in 212 | let compiler = read ~d:default_config.compiler "compiler" in 213 | let preinstalled = 214 | read ~d:(string_of_bool default_config.preinstalled) "preinstalled" 215 | |> bool_of_string 216 | in 217 | let os = read ~d:default_config.os "os" in 218 | let pins = read ~d:"" "pins" |> pins_of_string in 219 | { name; ocaml_version; compiler; preinstalled; os; pins } 220 | 221 | let string_of_config t = 222 | let one (k, v) = Printf.sprintf "%S: %S" k v in 223 | "{ " ^ 224 | String.concat ~sep:", " (List.map one [ 225 | "name" , t.name; 226 | "ocaml" , t.ocaml_version; 227 | "preinstalled" , string_of_bool t.preinstalled; 228 | "os" , t.os; 229 | "pins" , string_of_pins t.pins; 230 | ]) 231 | ^ " }" 232 | 233 | let read_config opamconfig = 234 | let ic = open_in opamconfig in 235 | let contents = input_line ic in 236 | close_in ic; 237 | config_of_string contents 238 | 239 | let write_config t opamconfig = 240 | let contents = string_of_config t in 241 | let oc = open_out opamconfig in 242 | output_string oc contents; 243 | close_out oc 244 | 245 | let add_to_env extra_env = 246 | Array.of_list extra_env |> 247 | Array.map (fun (k,v) -> k^"="^v) |> 248 | Array.append (Unix.environment ()) 249 | 250 | let env config = 251 | let extra_env = [ 252 | "OPAMVAR_ocaml_version" , config.ocaml_version; 253 | "OPAMVAR_compiler" , config.compiler; 254 | "OPAMVAR_preinstalled" , string_of_bool config.preinstalled; 255 | "OPAMVAR_os" , config.os; 256 | "OPAMYES" , "1"; 257 | ] in 258 | add_to_env extra_env 259 | 260 | let name s = match String.cut ~sep:"." s with Some (n, _) -> n | None -> s 261 | 262 | let finally f g = 263 | try let r = f () in g (); r 264 | with e -> g (); raise e 265 | 266 | let packages_to_install t ~root_pkg = 267 | (* 1. we create a temporary switch to have a clean universe where 268 | we can call the solver *) 269 | let json = Filename.get_temp_dir_name () / "pkg-" ^ t.name ^ ".json" in 270 | let tmp_switch = "tmpswitch-" ^ string_of_int @@ Random.int 1024 in 271 | let current_switch = opam_readl "switch show" in 272 | finally (fun () -> 273 | (* FIXME: seems that --no-switch is broken for opam 1.2.2 *) 274 | opam "switch %s -A system --no-switch" tmp_switch; 275 | opam "pin add %s . -n" root_pkg; 276 | show "Calling the solver..."; 277 | let env = env t in 278 | opam ~env "install %s --dry-run --json=\"%s\" --switch=%s" t.name json tmp_switch; 279 | ) (fun () -> 280 | opam "switch %s" current_switch; 281 | opam "switch remove %s" tmp_switch 282 | ); 283 | (* 2. parse the JSON file that the solver returned. I'm sorry. *) 284 | let all_packages = 285 | read_line 286 | "jq '.[] | map(select(.install)) | \ 287 | map( [.install.name, .install.version] | join(\".\")) | \ 288 | join(\" \")' \"%s\"" json 289 | in 290 | let all_packages = String.trim ~drop:((=)'"') all_packages in 291 | let all_packages = String.cuts ~sep:" " ~empty:false all_packages in 292 | all_packages 293 | 294 | let opam_source t ~sources_dir ~root_pkg pkgs = 295 | (* 3. gather all the metada in the global opam state, and populate 296 | sources/ *) 297 | in_dir sources_dir (fun () -> 298 | List.iter (fun pkg -> 299 | if pkg <> root_pkg then ( 300 | let pkg_name = name pkg in 301 | let pin = 302 | try List.find (fun (p, _) -> p = pkg_name) t.pins |> snd |> some 303 | with Not_found -> None 304 | in 305 | match pin with 306 | | None -> 307 | if Sys.file_exists pkg then 308 | show "pkg: %s, already there\n%!" pkg 309 | else ( 310 | show "pkg: %s, getting the sources" pkg; 311 | opam "source %s" pkg; 312 | ) 313 | | Some (`Local path) -> 314 | show "pkg: %s, linking from %s" pkg path; 315 | exec "ln -s %s %s" path pkg; 316 | | _ -> err "TODO" 317 | ) 318 | ) pkgs 319 | ); 320 | show "The sources are all in %s" sources_dir 321 | 322 | let local_pin t ~root_pkg ~sources_dir ~build_dir pkgs = 323 | let root = build_dir in 324 | (* 4. we create an opam root in the repo and pin all the local 325 | packages in there. *) 326 | if not (Sys.file_exists root) then ( 327 | (* FIXME: we should be able to init opam with an empty repo! *) 328 | exec "opam init -n --root=%s --compiler=%s" root t.compiler; 329 | exec "opam remote remove default --root=%s" root; 330 | ); 331 | List.iter (fun pkg -> 332 | if pkg = root_pkg then 333 | (* FIXME: cannot pin if opam root is a subdir *) 334 | (* opam ~root "pin add %s . -n" (name pkg) *) 335 | () 336 | else 337 | opam ~root "pin add %s %s -n" pkg (sources_dir / pkg) 338 | ) pkgs; 339 | show "All the packages are pinned to the local sources" 340 | 341 | let setup t ~sources_dir = 342 | let root_pkg = t.name ^ ".root" in 343 | let all_packages = packages_to_install t ~root_pkg in 344 | 345 | if not (Sys.file_exists sources_dir) || not (Sys.is_directory sources_dir) then 346 | mkdir "sources"; 347 | opam_source t ~sources_dir ~root_pkg all_packages; 348 | local_pin t ~sources_dir ~root_pkg all_packages 349 | 350 | let build ~sources_dir ~build_dir = 351 | let root = build_dir in 352 | let t0 = Unix.time () in 353 | (* FIXME: this doesn't seem to work with opam 1.2.2 *) 354 | let packages = Array.to_list @@ Sys.readdir sources_dir in 355 | opam ~root "install %s" (String.concat ~sep:" " packages); 356 | (* FIXME: run the right commands *) 357 | exec "opam config exec --root=%s -- make" root; 358 | show "Project built in %.2f" (Unix.time () -. t0) 359 | 360 | let () = 361 | let cwd = Sys.getcwd () in 362 | let opamconfig = cwd / ".opamconfig" in 363 | let sources_dir = cwd / "sources" in 364 | let build_dir = cwd / "build" in 365 | let init () = 366 | rmdir sources_dir; 367 | rmdir build_dir; 368 | let config = default_config () in 369 | write_config config opamconfig; 370 | config 371 | in 372 | let config = 373 | if not (Sys.file_exists opamconfig) then init () 374 | else ( 375 | show "%s exists, picking an existing configuration" opamconfig; 376 | try read_config opamconfig 377 | with Failure _ -> init () 378 | ) 379 | in 380 | Fmt.(pf stdout) "%a" pp_config config; 381 | setup config ~sources_dir ~build_dir; 382 | build ~sources_dir ~build_dir 383 | -------------------------------------------------------------------------------- /build.mli: -------------------------------------------------------------------------------- 1 | (* 2 | * Copyright (c) 2016 Thomas Gazagnaire 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | *) 16 | -------------------------------------------------------------------------------- /opam: -------------------------------------------------------------------------------- 1 | opam-version: "1.2" 2 | maintainer: "Thomas Gazagnaire " 3 | authors: ["Thomas Gazagnaire"] 4 | homepage: "http://github.com/samoht/opam-build" 5 | dev-repo: "http://github.com/samoht/opam-build.git" 6 | bug-reports: "http://github.com/samoht/opam-build/issues" 7 | license: "BSD-3-Clause" 8 | 9 | depends: [ 10 | "ocamlfind" {build} 11 | "ocamlbuild" {build} 12 | "base-unix" 13 | "fmt" 14 | "astring" 15 | ] 16 | build: [make] 17 | --------------------------------------------------------------------------------