├── .gitignore ├── _tags ├── opam-bundle.install ├── opam ├── _oasis ├── configure ├── Makefile ├── README ├── setup.ml └── src └── main.ml /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | *.native 3 | *.byte 4 | *.docdir 5 | setup.data 6 | setup.log 7 | -------------------------------------------------------------------------------- /_tags: -------------------------------------------------------------------------------- 1 | #OASIS_START 2 | #OASIS_STOP 3 | true: debug, annot, bin_annot, warn(+a-4-40-42-44-45) 4 | -------------------------------------------------------------------------------- /opam-bundle.install: -------------------------------------------------------------------------------- 1 | bin: [ 2 | "_build/src/main.native" {"opam-bundle"} 3 | ] 4 | #man: [ 5 | # "?opam-bundle.1" 6 | #] 7 | -------------------------------------------------------------------------------- /opam: -------------------------------------------------------------------------------- 1 | opam-version: "1.1" 2 | name: "opam-bundle" 3 | version: "git" 4 | license: "MIT" 5 | maintainer: "ygrek@autistici.org" 6 | homepage: "https://github.com/ygrek/opam-bundle" 7 | build: [ 8 | ["./configure"] 9 | [make] 10 | ] 11 | depends: ["ocamlfind" "opam-lib" "jsonm"] 12 | -------------------------------------------------------------------------------- /_oasis: -------------------------------------------------------------------------------- 1 | OASISFormat: 0.4 2 | Name: opam-bundle 3 | Version: 0.1 4 | Authors: ygrek 5 | License: MIT 6 | Plugins: DevFiles (0.4) 7 | BuildTools: ocamlbuild 8 | 9 | Synopsis: OPAM bundler 10 | Description: Generate self-contained archives of opam packages with all transitive dependencies 11 | 12 | Executable "opam-bundle" 13 | Path: src/ 14 | MainIs: main.ml 15 | CompiledObject: best 16 | BuildDepends: opam-lib.client, jsonm 17 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # OASIS_START 4 | # DO NOT EDIT (digest: dc86c2ad450f91ca10c931b6045d0499) 5 | set -e 6 | 7 | FST=true 8 | for i in "$@"; do 9 | if $FST; then 10 | set -- 11 | FST=false 12 | fi 13 | 14 | case $i in 15 | --*=*) 16 | ARG=${i%%=*} 17 | VAL=${i##*=} 18 | set -- "$@" "$ARG" "$VAL" 19 | ;; 20 | *) 21 | set -- "$@" "$i" 22 | ;; 23 | esac 24 | done 25 | 26 | ocaml setup.ml -configure "$@" 27 | # OASIS_STOP 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # OASIS_START 2 | # DO NOT EDIT (digest: a3c674b4239234cbbe53afe090018954) 3 | 4 | SETUP = ocaml setup.ml 5 | 6 | build: setup.data 7 | $(SETUP) -build $(BUILDFLAGS) 8 | 9 | doc: setup.data build 10 | $(SETUP) -doc $(DOCFLAGS) 11 | 12 | test: setup.data build 13 | $(SETUP) -test $(TESTFLAGS) 14 | 15 | all: 16 | $(SETUP) -all $(ALLFLAGS) 17 | 18 | install: setup.data 19 | $(SETUP) -install $(INSTALLFLAGS) 20 | 21 | uninstall: setup.data 22 | $(SETUP) -uninstall $(UNINSTALLFLAGS) 23 | 24 | reinstall: setup.data 25 | $(SETUP) -reinstall $(REINSTALLFLAGS) 26 | 27 | clean: 28 | $(SETUP) -clean $(CLEANFLAGS) 29 | 30 | distclean: 31 | $(SETUP) -distclean $(DISTCLEANFLAGS) 32 | 33 | setup.data: 34 | $(SETUP) -configure $(CONFIGUREFLAGS) 35 | 36 | configure: 37 | $(SETUP) -configure $(CONFIGUREFLAGS) 38 | 39 | .PHONY: build doc test all install uninstall reinstall clean distclean configure 40 | 41 | # OASIS_STOP 42 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | OPAM bundler 2 | ============ 3 | 4 | opam-bundle takes an OPAM package name (target) and creates an archive with the 5 | sources of the package and all its transitive dependencies along with the shell script 6 | to build the target in the standalone setting. 7 | 8 | After that the bundle can be distributed to other hosts (without OPAM or any 9 | OCaml libraries installed) and used to build the target software without 10 | requestiing the user to install OPAM and dependecies manually. Dependencies get 11 | built and installed into the local directory independently from the host 12 | environment by configuring the environment variables for OCaml tools 13 | appropriately. 14 | 15 | opam-bundle will use the information from the current OPAM switch when creating 16 | the bundle (.e.g. the list of installed packages, optional dependencies, compiler 17 | version) which may (will) influence the dependency resolution and the contents of the 18 | bundle. 19 | 20 | Build 21 | ----- 22 | 23 | Currently requires patched opam-lib 24 | 25 | opam pin add opam-lib git://github.com/ygrek/opam#expose 26 | 27 | After that usual: 28 | 29 | ./configure && make 30 | 31 | TODO 32 | ---- 33 | 34 | * handle depexts 35 | -------------------------------------------------------------------------------- /setup.ml: -------------------------------------------------------------------------------- 1 | (* setup.ml generated for the first time by OASIS v0.4.4 *) 2 | 3 | (* OASIS_START *) 4 | (* DO NOT EDIT (digest: 9852805d5c19ca1cb6abefde2dcea323) *) 5 | (******************************************************************************) 6 | (* OASIS: architecture for building OCaml libraries and applications *) 7 | (* *) 8 | (* Copyright (C) 2011-2013, Sylvain Le Gall *) 9 | (* Copyright (C) 2008-2011, OCamlCore SARL *) 10 | (* *) 11 | (* This library is free software; you can redistribute it and/or modify it *) 12 | (* under the terms of the GNU Lesser General Public License as published by *) 13 | (* the Free Software Foundation; either version 2.1 of the License, or (at *) 14 | (* your option) any later version, with the OCaml static compilation *) 15 | (* exception. *) 16 | (* *) 17 | (* This library is distributed in the hope that it will be useful, but *) 18 | (* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY *) 19 | (* or FITNESS FOR A PARTICULAR PURPOSE. See the file COPYING for more *) 20 | (* details. *) 21 | (* *) 22 | (* You should have received a copy of the GNU Lesser General Public License *) 23 | (* along with this library; if not, write to the Free Software Foundation, *) 24 | (* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA *) 25 | (******************************************************************************) 26 | 27 | let () = 28 | try 29 | Topdirs.dir_directory (Sys.getenv "OCAML_TOPLEVEL_PATH") 30 | with Not_found -> () 31 | ;; 32 | #use "topfind";; 33 | #require "oasis.dynrun";; 34 | open OASISDynRun;; 35 | 36 | (* OASIS_STOP *) 37 | let () = setup ();; 38 | -------------------------------------------------------------------------------- /src/main.ml: -------------------------------------------------------------------------------- 1 | 2 | open Printf 3 | open OpamTypes 4 | 5 | let match_package_atom nv (name,cstr) = 6 | OpamPackage.Name.compare (OpamPackage.name nv) name = 0 && 7 | match cstr with 8 | | None -> true 9 | | Some (relop,version) -> OpamFormula.eval_relop relop (OpamPackage.version nv) version 10 | 11 | let resolve_deps t atoms = 12 | let atoms = OpamSolution.sanitize_atom_list t ~permissive:true atoms in 13 | let action = Install (OpamPackage.Name.Set.of_list (List.map fst atoms)) in 14 | let universe = { (OpamState.universe t action) with u_installed = OpamPackage.Set.empty; } in 15 | let request = { wish_install = atoms; wish_remove = []; wish_upgrade = []; criteria = `Default; extra_attributes = []; } in 16 | match OpamSolver.resolve ~verbose:true universe ~orphans:OpamPackage.Set.empty request with 17 | | Success solution -> 18 | (* OpamSolver.new_packages, but return in order *) 19 | let l = OpamSolver.ActionGraph.Topological.fold (fun act acc -> match act with 20 | | `Install p | `Change (_,_,p) -> p :: acc 21 | | `Reinstall _ | `Remove _ | `Build _ -> acc) 22 | (OpamSolver.get_atomic_action_graph solution) [] 23 | in 24 | List.rev l 25 | | Conflicts cs -> 26 | OpamConsole.error_and_exit "Conflicts: %s" 27 | (OpamCudf.string_of_conflict (OpamState.unavailable_reason t) cs) 28 | 29 | (** see OpamState.install_compiler *) 30 | (* 31 | let bundle_compiler ~reuse ~dryrun (t:OpamState.state) archive = 32 | let open OpamFilename in 33 | let repo_name = 34 | Option.map_default 35 | (fun (r,_) -> OpamRepositoryName.to_string r.repo_name) 36 | "unknown repo" 37 | (OpamState.repository_and_prefix_of_compiler t t.compiler) 38 | in 39 | OpamConsole.msg "Bundling compiler %s [%s]\n" (OpamCompiler.to_string t.compiler) repo_name; 40 | let comp_f = OpamPath.compiler_comp t.root t.compiler in 41 | match exists comp_f with 42 | | false -> OpamConsole.error_and_exit "Cannot bundle compiler %s: no such file : %s" 43 | (OpamCompiler.to_string t.compiler) 44 | (to_string comp_f); 45 | | true -> 46 | let comp = OpamFile.Comp.read comp_f in 47 | match OpamFile.Comp.preinstalled comp with 48 | | true -> OpamConsole.error_and_exit "Cannot bundle preinstalled compiler %s" (OpamCompiler.to_string t.compiler) 49 | | false -> 50 | match OpamFile.Comp.configure comp @ OpamFile.Comp.make comp <> [] with 51 | | true -> assert false (* old style comp file - not supported *) 52 | | false -> 53 | (* Install the compiler *) 54 | match OpamFile.Comp.src comp with 55 | | None -> OpamConsole.error_and_exit "No source for compiler %s" (OpamCompiler.to_string t.compiler) 56 | | Some comp_src -> 57 | match dryrun with 58 | | true -> None 59 | | false -> 60 | let build_dir = OpamPath.Switch.build_ocaml t.root t.switch in 61 | let return = Some (basename_dir build_dir, comp) in 62 | match reuse && exists archive with 63 | | true -> OpamConsole.msg "Reusing %s\n" (to_string archive); return 64 | | false -> 65 | let open OpamProcess.Job.Op in 66 | let kind = OpamFile.Comp.kind comp in 67 | if kind = `local 68 | && Sys.file_exists (fst comp_src) 69 | && Sys.is_directory (fst comp_src) then 70 | OpamFilename.link_dir 71 | ~src:(OpamFilename.Dir.of_string (fst comp_src)) ~dst:build_dir 72 | else 73 | OpamProcess.Job.run @@ 74 | OpamFilename.with_tmp_dir_job begin fun download_dir -> 75 | OpamRepository.pull_url kind (OpamPackage.of_string "compiler.get") download_dir None [comp_src] 76 | @@| function 77 | | Not_available u -> OpamConsole.error_and_exit "%s is not available." u 78 | | Up_to_date r 79 | | Result r -> OpamFilename.extract_generic_file r build_dir 80 | end; 81 | let patches = OpamFile.Comp.patches comp in 82 | let patches = List.map (fun f -> 83 | OpamFilename.download ~overwrite:true f build_dir 84 | ) patches in 85 | List.iter (fun f -> OpamFilename.patch f build_dir) patches; 86 | (exec (dirname_dir build_dir) [ 87 | [ "tar"; "czf"; (to_string archive); Base.to_string (basename_dir build_dir) ] 88 | ]); 89 | return 90 | *) 91 | 92 | let bundle ~reuse ~dryrun ~deps_only ~with_compiler bundle atoms = 93 | let open OpamFilename in 94 | (* OpamSystem.real_path is pure evil *) 95 | let bundle = 96 | let open OpamFilename in 97 | if Filename.is_relative @@ Dir.to_string bundle then 98 | Op.(cwd () / Dir.to_string bundle) 99 | else 100 | bundle 101 | in 102 | let archives = OpamFilename.Op.(bundle / "archives") in 103 | if not dryrun && not reuse && exists_dir bundle then 104 | OpamConsole.error_and_exit "Directory %s already exists" (Dir.to_string bundle); 105 | let root = OpamStateConfig.opamroot () in 106 | OpamFormatConfig.init (); 107 | match OpamStateConfig.load_defaults root with 108 | | false -> OpamConsole.error_and_exit "Failed init" 109 | | true -> 110 | OpamStateConfig.init ~root_dir:root (); 111 | OpamRepositoryConfig.init (); 112 | OpamSolverConfig.init (); 113 | OpamStd.Config.init (); 114 | let t = OpamState.load_state "bundle" OpamStateConfig.(!r.current_switch) in 115 | (* let atoms = (OpamPackage.Name.of_string "opam", None) :: atoms in *) 116 | let atoms = OpamSolution.sanitize_atom_list ~permissive:true t atoms in 117 | let packages = resolve_deps t atoms in 118 | let packages = match deps_only with 119 | | false -> packages 120 | | true -> List.filter (fun nv -> not (List.exists (match_package_atom nv) atoms)) packages 121 | in 122 | (* sync: variables and OpamPath.Switch directories *) 123 | let root_dirs = [ "lib"; "bin"; "sbin"; "man"; "doc"; "share"; "etc" ] in 124 | let variables = 125 | let vars1 = List.map (fun k -> OpamVariable.(of_string k, S (Filename.concat "$BUNDLE_PREFIX" k))) root_dirs in 126 | let vars2 = OpamVariable.([ 127 | of_string "prefix", S "$BUNDLE_PREFIX"; 128 | of_string "stublibs", S "$BUNDLE_PREFIX/lib/stublibs"; 129 | of_string "preinstalled", B (not with_compiler); 130 | of_string "make", S "$MAKE"; 131 | ]) in 132 | OpamVariable.Map.of_list @@ List.map (fun (k,v) -> k, Some v) (vars1 @ vars2) 133 | in 134 | let b = Buffer.create 10 in 135 | (* expecting quoted commands *) 136 | let shellout_build ?dir ~archive ~env commands = 137 | let archive_name = Filename.chop_suffix archive ".tar.gz" in 138 | let dir = Option.default archive_name dir in 139 | let pr fmt = ksprintf (fun s -> Buffer.add_string b (s ^ "\n")) fmt in 140 | pr "("; 141 | pr "echo BUILD %s" archive_name; 142 | pr "cd build"; 143 | pr "rm -rf %s" dir; 144 | pr "tar -xzf ../archives/%s" archive; 145 | pr "cd %s" dir; 146 | List.iter (fun (k,v) -> pr "export %s=%s" k v) env; 147 | List.iter (fun args -> pr "%s" (String.concat " " args)) commands; 148 | pr ")" 149 | in 150 | List.iter (fun s -> Buffer.add_string b (s^"\n")) [ 151 | "#! /bin/sh"; 152 | "set -eu"; 153 | ": ${BUNDLE_PREFIX=$(pwd)/local}"; 154 | ": ${MAKE=make}"; 155 | "mkdir -p build $BUNDLE_PREFIX " ^ 156 | (String.concat " " (List.map (Filename.concat "$BUNDLE_PREFIX") root_dirs)) ^ 157 | " $BUNDLE_PREFIX/lib/stublibs"; 158 | "export PATH=$BUNDLE_PREFIX/bin:$PATH"; 159 | "export CAML_LD_LIBRARY_PATH=$BUNDLE_PREFIX/lib/stublibs:${CAML_LD_LIBRARY_PATH:-}"; 160 | ]; 161 | if not dryrun then 162 | List.iter mkdir [bundle; archives]; 163 | if with_compiler then 164 | begin 165 | OpamConsole.msg "compiler bundling - not implemented"; 166 | (* 167 | let archive = "ocaml." ^ (OpamCompiler.to_string t.compiler) ^ ".tar.gz" in 168 | let archive_path = OP.(archives // archive) in 169 | match bundle_compiler ~reuse ~dryrun t archive_path with 170 | | None -> () 171 | | Some (dir,comp) -> 172 | let env = OpamState.add_to_env t [] (OpamFile.Comp.env comp) variables in 173 | let commands = OpamState.filter_commands t variables (OpamFile.Comp.build comp) in 174 | let commands = List.map (fun l -> l @ [">>build.log 2>>build.log || (echo FAILED; tail build.log; exit 1)"]) commands in 175 | shellout_build ~dir:(Base.to_string dir) ~archive ~env commands 176 | *) 177 | end; 178 | List.iter begin fun nv -> 179 | try 180 | let repo_name = 181 | Option.map_default 182 | (fun r -> OpamRepositoryName.to_string r.OpamTypes.repo_name) 183 | "unknown repo" 184 | (OpamState.repository_of_package t nv) 185 | in 186 | OpamConsole.msg "Bundling %s [%s]\n" (OpamPackage.to_string nv) repo_name; 187 | match dryrun with 188 | | true -> () 189 | | false -> 190 | let archive = OpamFilename.Op.(archives // (OpamPackage.to_string nv ^ ".tar.gz")) in 191 | if reuse && exists archive then 192 | begin 193 | OpamConsole.msg "Reusing %s\n" (to_string archive) 194 | end 195 | else 196 | begin 197 | (* gets the source (from url, local path, git, etc) and applies patches and substitutions *) 198 | match OpamProcess.Job.run (OpamAction.download_package t nv) with 199 | | `Error s -> OpamConsole.error_and_exit "Download failed : %s" s 200 | | `Successful s -> 201 | OpamAction.extract_package t s nv; 202 | let p_build = OpamPath.Switch.build t.root t.switch nv in 203 | (* 204 | OpamConsole.msg "p_build: %s\n" (OpamFilename.Dir.to_string p_build); 205 | OpamConsole.msg "archives: %s\n" (OpamFilename.Dir.to_string archives); 206 | OpamConsole.msg "archive: %s\n" (OpamFilename.to_string archive); 207 | *) 208 | exec (dirname_dir p_build) [ 209 | [ "tar"; "czf"; to_string archive; Base.to_string (basename_dir p_build) ] 210 | ]; 211 | end; 212 | let archive = Base.to_string (basename archive) in 213 | let opam = OpamState.opam t nv in (* dev? *) 214 | let env = OpamState.add_to_env t ~opam [] (OpamFile.OPAM.build_env opam) ~variables in 215 | let commands = OpamFile.OPAM.build opam @ OpamFile.OPAM.install opam in 216 | let commands = OpamFilter.commands (OpamState.filter_env t ~opam ~local_variables:variables) commands in 217 | let commands = List.map (fun l -> l @ [">>build.log 2>>build.log || (echo FAILED; tail build.log; exit 1)"]) commands in 218 | let install_commands = 219 | let name = OpamPackage.(Name.to_string @@ name nv) in 220 | [ 221 | [ sprintf "if [ -f \"%s.install\" ] ; then opam-installer --prefix \"$BUNDLE_PREFIX\" \"%s.install\" ; fi" name name ] 222 | ] 223 | in 224 | shellout_build ~archive ~env (commands @ install_commands); 225 | with e -> 226 | OpamStd.Exn.fatal e; 227 | OpamConsole.error_and_exit "%s" (Printexc.to_string e); 228 | end packages; 229 | match dryrun with 230 | | true -> () 231 | | false -> 232 | let install_sh = OpamFilename.Op.(bundle // "install.sh") in 233 | write install_sh (Buffer.contents b); 234 | chmod install_sh 0o755; 235 | () 236 | 237 | let cmd = 238 | let open Cmdliner in 239 | (* / various bits from OpamArg *) 240 | let dirname = 241 | let parse str = `Ok (OpamFilename.Dir.of_string str) in 242 | let print ppf dir = Format.pp_print_string ppf (OpamFilename.prettify_dir dir) in 243 | parse, print 244 | in 245 | let nonempty_arg_list name doc conv = 246 | let doc = Arg.info ~docv:name ~doc [] in 247 | Arg.(non_empty & pos_all conv [] & doc) 248 | in 249 | (* name * version constraint *) 250 | let atom = 251 | let parse str = 252 | let re = Re_str.regexp "\\([^>=<.!]+\\)\\(>=?\\|<=?\\|=\\|\\.\\|!=\\)\\(.*\\)" in 253 | try 254 | if not (Re_str.string_match re str 0) then failwith "no_version"; 255 | let sname = Re_str.matched_group 1 str in 256 | let sop = Re_str.matched_group 2 str in 257 | let sversion = Re_str.matched_group 3 str in 258 | let name = OpamPackage.Name.of_string sname in 259 | let sop = if sop = "." then "=" else sop in 260 | let op = OpamFormula.relop_of_string sop in (* may raise Invalid_argument *) 261 | let version = OpamPackage.Version.of_string sversion in 262 | `Ok (name, Some (op, version)) 263 | with Failure _ | Invalid_argument _ -> 264 | try `Ok (OpamPackage.Name.of_string str, None) 265 | with Failure msg -> `Error msg 266 | in 267 | let print ppf atom = 268 | Format.pp_print_string ppf (OpamFormula.short_string_of_atom atom) in 269 | parse, print 270 | in 271 | let nonempty_atom_list = 272 | nonempty_arg_list "PACKAGES" 273 | "List of package names, with an optional version or constraint, \ 274 | e.g `pkg', `pkg.1.0' or `pkg>=0.5'." 275 | atom 276 | in 277 | (* / *) 278 | let doc = "Generate a self-contained bundle of given packages with all dependencies." in 279 | let man = [ 280 | `S "DESCRIPTION"; 281 | `P "This command calculates the transitive dependencies of the given packages \ 282 | and collects all the corresponding archives into a specified directory, \ 283 | along with the shell script to unpack, build and install those packages \ 284 | on any remote machine (even without OPAM installed)."; 285 | ] in 286 | let outdir = 287 | let doc = Arg.info ["o";"outdir"] ~docv:"DIR" ~doc:"Write bundle to the directory $(docv)." in 288 | Arg.(value & opt dirname (OpamFilename.Dir.of_string "bundle") & doc) 289 | in 290 | let deps_only = 291 | Arg.(value & flag & info ["deps-only"] ~doc:"Bundle only the dependencies, excluding the specified packages.") 292 | in 293 | let dryrun = 294 | Arg.(value & flag & info ["dry-run"] ~doc:"Do not actually create bundle, only show actions to be done.") 295 | in 296 | let with_compiler = 297 | Arg.(value & flag & info ["with-compiler"] ~doc:"Bundle the compiler too") 298 | in 299 | let reuse = 300 | let doc = "Allow reusing archives already bundled in the output directory (no checking performed, can be useful to \ 301 | skip redownloading compiler sources)" 302 | in 303 | Arg.(value & flag & info ["reuse"] ~doc) 304 | in 305 | let bundle deps_only dryrun with_compiler reuse outdir names = 306 | bundle ~dryrun ~deps_only ~with_compiler ~reuse outdir names 307 | in 308 | Term.(pure bundle $deps_only $dryrun $with_compiler $reuse $outdir $nonempty_atom_list), 309 | Term.info "opam-bundle" ~doc ~man 310 | 311 | let () = match Cmdliner.Term.eval cmd with `Error _ -> exit 1 | _ -> exit 0 312 | --------------------------------------------------------------------------------