123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359(*
* Copyright (c) 2013-2020 Thomas Gazagnaire <thomas@gazagnaire.org>
* Copyright (c) 2013-2020 Anil Madhavapeddy <anil@recoil.org>
* Copyright (c) 2015-2020 Gabriel Radanne <drupyog@zoho.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*)openAction.SyntaxopenDSLletsrc=Logs.Src.create"functoria.tool"~doc:"functoria library"moduleLog=(valLogs.src_logsrc:Logs.LOG)moduletypeS=sigvalname:stringvalversion:stringvalpackages:packagelistvalcreate:jobimpllist->jobimplendletcheck_version~name~versiondata=let(let*)=Result.bindinletextract_versionv=tryOk(Scanf.sscanfv"%u.%u.%u"(funmamipa->(ma,mi,pa)))with|Scanf.Scan_failure_|End_of_file->(tryOk(Scanf.sscanfv"%u.%u"(funmami->(ma,mi,0)))with|Scanf.Scan_failure_|End_of_file->(tryOk(Scanf.sscanfv"%u"(funma->(ma,0,0)))withScanf.Scan_failure_|Failure_|End_of_file->Error("couldn't extract version (%u) from "^v))|Failuref->Error("couldn't extract version (%u.%u) from "^v^": "^f))|Failuref->Error("couldn't extract version (%u.%u.%u) from "^v^": "^f)inifString.equalversion("%%"^"VERSION%%")then(Log.info(funm->m"Skipping version check, since our_version is not watermarked");Ok())elselet*version'=extract_versionversioninletfirst_str="(* "^name^" "inletfl=String.lengthfirst_striniffl<String.lengthdata&&String.equal(String.subdata0fl)first_strthenlet*lower_version,upper_version=letvs=String.split_on_char' '(String.subdatafl(String.lengthdata-fl))inletrecgolowerupper=function|"&"::tl->goloweruppertl|">="::v::tl->iflower=Nonethengo(Somev)uppertlelseError"Bad comment, multiple >= constraints"|"<"::v::tl->ifupper=Nonethengolower(Somev)tlelseError"Bad comment, multiple < constraints"|"*)"::_->Ok(lower,upper)|""::tl->goloweruppertl|_->Error(Fmt.str"Unknown first line, must be (* %s [>= a.b.c] [&] [< d.e.f] \
*)"name)ingoNoneNonevsinletcmp~eq(ma,mi,pa)(ma',mi',pa')=ma>ma'||(ma=ma'&&mi>mi')||(ma=ma'&&mi=mi'&&pa>pa')||(ma=ma'&&mi=mi'&&pa=pa'&&eq)inlet*()=matchlower_versionwith|None->Ok()|Somev->let*v'=extract_versionvinifcmp~eq:trueversion'v'thenOk()elseError(Fmt.str"Version mismatch: required is %s >= %s, but %s is \
installed. Please upgrade your installation (opam update; \
opam install '%s>=%s')"namevversionnamev)inmatchupper_versionwith|None->Ok()|Somev->let*v'=extract_versionvinifcmp~eq:falsev'version'thenOk()elseError(Fmt.str"Version mismatch: required is %s < %s, but %s is installed. \
Please downgrade your installation (opam update; opam \
install '%s<%s')"namevversionnamev)elseOk()moduleMake(P:S)=structmoduleFilegen=Filegen.Make(P)letbuild_dirt=Fpath.parentt.Cli.config_fileletcontext_filet=Context_cache.file~name:P.nametletadd_context_filetargv=matcht.Cli.context_filewith|Some_->Action.okargv|None->letfile=context_filetinlet+is_file=Action.is_filefileinifis_filethenArray.appendargv[|"--context";Fpath.to_stringfile|]else(* should only happen when doing configure --help *)argvletrun_cmd?ppf?err_ppfcommand=leterr=matcherr_ppfwithNone->None|Somef->Some(`Fmtf)inletout=matchppfwithNone->None|Somef->Some(`Fmtf)inAction.run_cmd?err?outcommandletre_exec_clitargv=let*argv=add_context_filetargvinletargs=Bos.Cmd.of_list(List.tl(Array.to_listargv))inletconfig_exe=Fpath.(v"_build"/"default"//build_dirt/"config.exe")inletcommand=Bos.Cmd.(v(pconfig_exe)%%args)inAction.run_cmd_clicommand(* Generate the base dune and dune-project files *)letgenerate_base_dunet=letdune_config_path=Fpath.(build_dirt/"dune.config")inLog.info(funm->m"Generating: %a (base)"Fpath.ppdune_config_path);letdune_config=Dune.base~config_ml_file:t.Cli.config_file~packages:P.packagesinletdune_config=Fmt.str"%a\n%!"Dune.ppdune_configinlet*()=Filegen.writedune_config_pathdune_configinletdune_path=Fpath.(build_dirt/"dune")inletdune=Fmt.str"(include dune.config)"inFilegen.writedune_pathduneletdune_workspace_patht=Fpath.(build_dirt/P.name/"dune-workspace.config")letgenerate_base_dune_workspacet=letdune_workspace_path=dune_workspace_pathtinLog.info(funm->m"Generating: %a (base)"Fpath.ppdune_workspace_path);letdune=Dune.base_workspaceinletdune=Fmt.str"%a\n%!"Dune.ppduneinFilegen.writedune_workspace_pathduneletgenerate_base_dune_project()=letdune_project_path=Fpath.(v"dune-project")inLog.info(funm->m"Generating: %a (base)"Fpath.ppdune_project_path);letdune=Dune.vDune.base_projectinletdune=Fmt.str"%a\n%!"Dune.ppduneinFilegen.writedune_project_pathduneletbuild_config_exet?ppf?err_ppf()=letdune_workspace_path=dune_workspace_pathtinletcommand=Bos.Cmd.(v"dune"%"build"%pFpath.(build_dirt/"config.exe")%"--root"%"."%"--workspace"%pdune_workspace_path)inrun_cmd?ppf?err_ppfcommandletwrite_contexttargv=Context_cache.write(context_filet)argvletremove_contextt=Action.rm(context_filet)(* Generated a project skeleton and try to compile config.exe. *)letgenerate_project_skeleton~save_argst?ppf?err_ppfargv=let*_=Action.mkdirFpath.(build_dirt/P.name)inlet*()=generate_base_dune_workspacetinlet*()=generate_base_dune_project()inlet*()=generate_base_dunetinlet*()=ifsave_argsthenwrite_contexttargvelseAction.ok()in(* try to compile config.exe to detect early compilation errors. *)build_config_exet?ppf?err_ppf()letexit_errt=function|Okv->v|Error(`Msgm)->flush_all();ifm<>""thenFmt.epr"%a\n%!"Fmt.(styled(`Fg`Red)string)m;ifnott.Cli.dry_runthenexit1elseFmt.epr"(exit 1)\n%!"lethandle_parse_args_no_config?help_ppf?err_ppf(`Msgerror)argv=letcontext=(* Extract all the keys directly. Useful to pre-resolve the keys
provided by the specialized DSL. *)letbase_keys=Engine.keys@@Impl.abstract@@P.create[]inCmdliner.Term.(const(fun_->Action.ok())$Key.contextbase_keys)inletresult=Cli.eval?help_ppf?err_ppf~name:P.name~version:P.version~configure:context~query:context~describe:context~clean:context~help:context~mname:P.nameargvinletok=Action.ok()inleterror=Action.errorerrorinmatchresultwith`Version|`Help|`Ok(Cli.Help_)->ok|_->errorletwith_project_skeleton~save_argst?ppf?err_ppfargvf=letfile=t.Cli.config_fileinlet*is_file=Action.is_filefileinifnotis_filethenletmsg=Fmt.str"configuration file %a missing"Fpath.ppfileinhandle_parse_args_no_config?help_ppf:ppf?err_ppf(`Msgmsg)argvelselet*()=generate_project_skeleton~save_argst?ppf?err_ppfargvinf()letaction_runta=ifnott.Cli.dry_runthenAction.runaelseletenv=Action.env~files:(`Passtrough(Fpath.v"."))()inletdom=Action.dry_run~envainList.iter(funline->Fmt.epr"%a %s\n%!"Fmt.(styled(`Fg`Cyan)string)"*"line)dom.logs;dom.resultletclean_files?ppf?err_ppfargs=letdune_clean()=let*var=Action.get_var"INSIDE_FUNCTORIA_TESTS"inmatchvarwith|Some"1"|Some""->Action.rmFpath.(build_dirargs/".merlin")|_->run_cmd?ppf?err_ppfBos.Cmd.(v"dune"%"clean")inletrm_gen_files()=let*files=Action.ls(Fpath.v".")(fun_->true)inletfiles=List.sortFpath.comparefilesinletfiles=List.filter_map(funfile->ifFpath.parentfile<>Fpath.v"./"thenNoneelseletbase,ext=Fpath.split_extfileinletbase=Fpath.basenamebaseinmatch(base,ext)with|("Makefile"|"dune-project"|"dune-workspace"),""->Somefile|_->Log.info(funf->f"Skipped %a"Fpath.ppfile);None)filesinlet*()=Action.List.iter~f:Filegen.rmfilesinlet*()=remove_contextargsinlet*()=Filegen.rmFpath.(build_dirargs/"dune")inlet*()=Filegen.rmFpath.(build_dirargs/"dune.build")inFilegen.rmFpath.(build_dirargs/"dune.config")inlet*()=dune_clean()inrm_gen_files()(* App builder configuration *)letconfigure({args;_}:_Cli.configure_args)?ppf?err_ppfargv=letfile=args.Cli.config_fileinlet*()=let*is_file=Action.is_filefileinifnotis_filethenAction.errorf"configuration file %a missing"Fpath.ppfileelseAction.ok()inlet*()=let*data=letcmd=Bos.Cmd.(v"head"%"-1"%pfile)inAction.run_cmd_out~err:`Nullcmdinletversion=letv=P.versioninifString.lengthv>0&&String.getv0='v'thenString.subv1(String.lengthv-1)elsevinResult.fold~ok:(fun()->Action.ok())~error:(funmsg->Action.errormsg)(check_version~name:P.name~versiondata)in(* Files to build config.ml *)with_project_skeleton~save_args:trueargs?ppf?err_ppfargv@@fun()->Log.info(funf->f"Set-up config skeleton.");(* Launch config.exe: additional generated files for the application. *)re_exec_cliargsargvlettry_to_re_execargs?ppf?err_ppfargv=with_project_skeleton~save_args:falseargs?ppf?err_ppfargv@@fun()->re_exec_cliargsargvleterrort=try_to_re_exectletquery(t:'aCli.query_args)=try_to_re_exect.argsletdescribe(t:'aCli.describe_args)=try_to_re_exect.argslethelp(t:'aCli.help_args)=try_to_re_exectletcleanargs?ppf?err_ppfargv=letconfig=args.Cli.config_fileinlet*()=let*is_file=Action.is_fileconfiginifis_filethentry_to_re_execargs?ppf?err_ppfargvelseAction.ok()inclean_filesargsletrunargsaction=action|>action_runargs|>exit_errargsletpp_unit__=()letrun_with_argv?help_ppf?err_ppfargv=lett=Cli.peek~with_setup:true~mname:P.nameargvinmatchtwith|`Version->Log.info(funl->l"version");Fmt.pr"%s\n%!"P.version|`Error(Somet,_)->Log.info(funl->l"error: %a"(Cli.pp_argspp_unit)t);runt@@errort?ppf:help_ppf?err_ppfargv|`Error(None,_)->letaction=handle_parse_args_no_config?help_ppf?err_ppf(`Msg"")argvinletargs=Cli.default_argsinaction_runargsaction|>exit_errargs|`Okt->(Log.info(funl->l"run: %a"(Cli.pp_actionpp_unit)t);letrun=run(Cli.argst)inletppf=help_ppfinmatchtwith|Configuret->run@@configuret?ppf?err_ppfargv|Cleant->run@@cleant?ppf?err_ppfargv|Queryt->run@@queryt?ppf?err_ppfargv|Describet->run@@describet?ppf?err_ppfargv|Helpt->run@@helpt?ppf?err_ppfargv)letrun()=run_with_argvSys.argvend