123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652(*
* 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>
* Copyright (c) 2019-2020 Etienne Millon <etienne@tarides.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.
*)letsrc=Logs.Src.create"functoria.action"~doc:"functoria library"moduleLog=(valLogs.src_logsrc:Logs.LOG)openAstringtype'aor_err=('a,Rresult.R.msg)resulttypetmp_name_pat=Bos.OS.File.tmp_name_pattype'awith_output={mode:intoption;path:Fpath.t;purpose:string;contents:Format.formatter->'a;append:bool;}typechannel=[`Null|`FmtofFormat.formatter]typecmd={cmd:Bos.Cmd.t;err:channel;out:channel;trim:bool}typels={root:Fpath.t;filter:Fpath.t->bool}type_command=|Rmdir:Fpath.t->unitcommand|Mkdir:Fpath.t->boolcommand|Ls:ls->Fpath.tlistcommand|Rm:Fpath.t->unitcommand|Is_file:Fpath.t->boolcommand|Is_dir:Fpath.t->boolcommand|Size_of:Fpath.t->intoptioncommand|Run_cmd:cmd->unitcommand|Run_cmd_out:cmd->stringcommand|Run_cmd_cli:Bos.Cmd.t->unitcommand|Get_var:string->stringoptioncommand|Set_var:string*stringoption->unitcommand|With_dir:Fpath.t*(unit->'at)->'acommand|Pwd:Fpath.tcommand|Tmp_file:intoption*tmp_name_pat->Fpath.tcommand|Write_file:Fpath.t*string->unitcommand|Read_file:Fpath.t->stringcommand|With_output:'awith_output->'acommandand_t=|Done:'a->'at|Fail:string->'at|Run:'rcommand*('r->'at)->'atletokx=Donexleterrore=Faileleterrorffmt=Fmt.kstrerrorfmtletrecbind~f=function|Doner->fr|Fails->Fails|Run(c,k)->letk2r=bind~f(kr)inRun(c,k2)letmap~fx=bindx~f:(funy->ok(fy))letrecseq=function[]->ok()|h::t->bind~f:(fun()->seqt)hletwrapx=Run(x,ok)let(!)=Fpath.normalizeletrmpath=wrap@@Rm!pathletrmdirpath=wrap@@Rmdir!pathletmkdirpath=wrap@@Mkdir!pathletlspathfilter=wrap@@Ls{root=!path;filter}letwith_dirpathf=wrap@@With_dir(!path,f)letpwd()=wrap@@Pwdletis_filepath=wrap@@Is_file!pathletis_dirpath=wrap@@Is_dir!pathletsize_ofpath=wrap@@Size_of!pathletset_varcv=wrap@@Set_var(c,v)letget_varc=wrap@@Get_varcletrun_cmd?(err=`FmtFmt.stderr)?(out=`FmtFmt.stdout)cmd=wrap@@Run_cmd{cmd;out;err;trim=false}letrun_cmd_out?(err=`FmtFmt.stderr)cmd=wrap@@Run_cmd_out{cmd;out=`Null;err;trim=true}letrun_cmd_clicmd=wrap@@Run_cmd_clicmdletwrite_filepathcontents=wrap@@Write_file(!path,contents)letread_filepath=wrap@@Read_file!pathlettmp_file?modepat=wrap@@Tmp_file(mode,pat)letwith_output?mode?(append=false)~path~purposecontents=wrap@@With_output{append;mode;path;purpose;contents}letpfoppfs=matchppfwith`Null->()|`Fmtppf->Fmt.pfppf"%s%!"sletinterpret_cmd{cmd;err;out;trim}=Log.debug(funl->l"RUN: %a"Bos.Cmd.ppcmd);letopenRresultinleterr=matcherrwith|`Null->Ok(Bos.OS.Cmd.err_null,fun()->Ok())|`Fmtppf->Bos.OS.File.tmp"cmd-err-%s">>|funpath->letflush()=Bos.OS.File.readpath>>|funs->Fmt.pfppf"%s%!"sin(Bos.OS.Cmd.err_filepath,flush)inerr>>=fun(err,flush_err)->letres=Bos.OS.Cmd.run_out~errcmdinletres=Bos.OS.Cmd.out_string~trimresinres>>=fun(str_out,_)->pfooutstr_out;flush_err()>>=fun()->Bos.OS.Cmd.successresletinterpret_cmd_clicmd=Log.debug(funl->l"RUN-CLI: %a"Bos.Cmd.ppcmd);letres=Bos.OS.Cmd.run_outcmdinmatchBos.OS.Cmd.out_stdoutreswith|Ok((),(_,`Exited0))->Ok()|Ok((),(_,`Exited_))->Error(`Msg"")|failure->Bos.OS.Cmd.successfailureletrecinterpret_command:typer.rcommand->ror_err=function|Rmdirpath->Log.debug(funl->l"rmdir %a"Fpath.pppath);Bos.OS.Dir.delete~recurse:truepath|Mkdirpath->Log.debug(funl->l"mkdir %a"Fpath.pppath);Bos.OS.Dir.create~path:truepath|Ls{root;filter}->letopenRresultinLog.debug(funl->l"ls %a"Fpath.pproot);Bos.OS.Path.matches~dotfiles:trueFpath.(root/"$(file)")>>|funfiles->List.filterfilterfiles|Rmpath->Log.debug(funl->l"rm %a"Fpath.pppath);Bos.OS.File.delete~must_exist:falsepath|Is_filepath->Log.debug(funl->l"is-file %a"Fpath.pppath);Bos.OS.File.existspath|Is_dirpath->Log.debug(funl->l"is-dir %a"Fpath.pppath);Bos.OS.Dir.existspath|Size_ofpath->(Log.debug(funl->l"size-of %a"Fpath.pppath);matchBos.OS.Path.statpathwith|Oks->Ok(Somes.Unix.st_size)|_->OkNone)|Run_cmdcmd->Rresult.(interpret_cmdcmd>>|fun_->())|Run_cmd_outcmd->interpret_cmdcmd|Run_cmd_clicmd->interpret_cmd_clicmd|Set_var(c,v)->Log.debug(funl->l"set_var %s %a"cFmt.(option~none:(any"<unset>")string)v);Bos.OS.Env.set_varcv|Get_varc->Log.debug(funl->l"get_var %s"c);Ok(Bos.OS.Env.varc)|With_dir(dir,f)->letf()=run(f())inletopenRresultinBos.OS.Dir.current()>>=funold->Log.debug(funl->l"entering %a"Fpath.ppdir);Rresult.R.join@@Bos.OS.Dir.with_currentdirf()>>|funr->Log.debug(funl->l"entering %a"Fpath.ppold);r|Pwd->Log.debug(funl->l"pwd");Bos.OS.Dir.current()|Write_file(path,contents)->Log.debug(funl->l"write %a"Fpath.pppath);Bos.OS.File.writepathcontents|Read_filepath->Log.debug(funl->l"read-file %a"Fpath.pppath);Bos.OS.File.readpath|Tmp_file(mode,pat)->Log.debug(funl->l"tmp-file %s"Fmt.(strpat"*"));Bos.OS.File.tmp?modepat|With_output{mode;path;purpose;contents;append}->(tryletoc=letpath=Fpath.to_stringpathinletmode=matchmodewithNone->0o666|Somem->minifappendthenopen_out_gen[Open_wronly;Open_append;Open_text]modepathelseopen_outpathinletppf=Format.formatter_of_out_channelocinletr=contentsppfinFmt.pfppf"%!";flushoc;close_outoc;Okrwithe->Rresult.R.error_msgf"couldn't open output channel for %s: %a"purposeFmt.exne)andrun:typer.rt->ror_err=function|Doner->Okr|Failf->Error(`Msgf)|Run(cmd,k)->Rresult.R.bind(interpret_commandcmd)(funx->run@@kx)typefiles=[`PasstroughofFpath.t|`Filesof(Fpath.t*string)list]letdefault_execcmd=letcmd=Fmt.str"$(%a)\n"Fmt.(list~sep:(any" ")string)(Bos.Cmd.to_listcmd)inSome(cmd,"")(* (simple) virtual environment *)moduleEnv:sigtypetvaleq:t->t->boolvalpp:tFmt.tvaldiff_files:old:t->t->Fpath.Set.tvalpwd:t->Fpath.tvalchdir:t->Fpath.t->tvalls:t->Fpath.t->Fpath.tlistoptionvalv:?exec:(Bos.Cmd.t->(string*string)option)->?env:(string*string)list->?pwd:Fpath.t->?files:files->unit->tvalexec:t->Bos.Cmd.t->(string*string)optionvalis_file:t->Fpath.t->boolvalis_dir:t->Fpath.t->boolvalmkdir:t->Fpath.t->(t*bool)optionvalrm:t->Fpath.t->(t*bool)optionvalrmdir:t->Fpath.t->tvalsize_of:t->Fpath.t->intoptionvalwrite:t->Fpath.t->string->tvalread:t->Fpath.t->stringoptionvaltmp_file:t->tmp_name_pat->Fpath.tvalset_var:t->string->stringoption->tvalget_var:t->string->stringoptionend=structtypet={files:stringFpath.Map.t;pwd:Fpath.t;env:stringString.Map.t;exec:Bos.Cmd.t->(string*string)option;}letdiff_files~oldt=letto_sett=Fpath.Map.fold(funf_acc->matchFpath.rem_prefixt.pwdfwith|None->acc|Somef->Fpath.Set.addfacc)t.filesFpath.Set.emptyinFpath.Set.diff(to_sett)(to_setold)letscandir=(letopenRresultinBos.OS.Path.fold~dotfiles:true~elements:`Files~traverse:`Any(funfilefiles->files>>=funfiles->Bos.OS.File.readfile>>|func->(file,c)::files)(Ok[])[dir])|>Rresult.R.join|>Rresult.R.error_msg_to_invalid_argletv?(exec=default_exec)?env?pwd?(files=`Files[])()=letenv=matchenvwithSomee->String.Map.of_liste|None->String.Map.emptyinletpwd=matchpwdwithNone->Fpath.v"/"|Somep->pinletfiles=letfiles=matchfileswith`Passtroughdir->scandir|`Filesfiles->filesinletfiles=List.map(fun(f,c)->matchFpath.is_relfwith|false->(f,c)|true->(Fpath.(pwd//f),c))filesinList.map(fun(f,c)->(Fpath.normalizef,c))filesin{files=Fpath.Map.of_listfiles;pwd;env;exec}leteqxy=Fpath.Map.equal(=)x.filesy.files&&Fpath.equalx.pwdy.pwd&&String.Map.equal(=)x.envy.envletpp=letopenFmt.Dumpinrecord[field"files"(funt->t.files)(Fpath.Map.dumpstring);field"pwd"(funt->t.pwd)Fpath.dump;field"env"(funt->t.env)(String.Map.dumpstring);]letpwdt=t.pwdletexectcmd=t.execcmdletmk_pathtpath=match(Fpath.to_stringt.pwd,Fpath.is_relpath)with|_,true->Fpath.(normalize@@(t.pwd//path))|_,false->Fpath.normalizepathletchdirtpath=letpwd=mk_pathtpathin{twithpwd}letis_rootpath=Fpath.to_stringpath="/"letmkdirtpath=letpath=mk_pathtpathinifis_rootpaththenSome(t,false)elsematchFpath.Map.findpatht.fileswith|Somefwhenf<>"<DIR>"->None|r->lett={twithfiles=Fpath.Map.addpath"<DIR>"t.files}inSome(t,r=None)letrmdirtpath=letpath=mk_pathtpathinletfiles=Fpath.Map.filter(funf_->letf=mk_pathtfinletb=not(Fpath.is_prefixpathf)inb)t.filesin{twithfiles}letlstpath=letroot=mk_pathtpathinmatchFpath.Map.findroott.fileswith|Some"<DIR>"->Some[]|Some_->Some[path]|None->(Fpath.Map.fold(funfile_acc->letfile=mk_pathtfileinmatchFpath.relativize~rootfilewith|None->acc|Somef->f::acc)t.files[]|>function|[]->None|x->Some(List.revx))letwritetpathf=letpath=mk_pathtpathin{twithfiles=Fpath.Map.addpathft.files}letreadtpath=letpath=mk_pathtpathinFpath.Map.findpatht.fileslettmp_filetpat=letrecauxn=letdir=Fpath.v"/tmp"inletfile=Fpath.(dir/Fmt.strpat(string_of_intn))inifFpath.Map.memfilet.filesthenaux(n+1)elsefileinaux0letis_dirtpath=letpath=mk_pathtpathinmatchFpath.Map.findpatht.fileswith|Some"<DIR>"->true|Some_->false|None->Fpath.Map.exists(funf_->letf=mk_pathtfinFpath.is_prefixpathf)t.filesletis_filetpath=letpath=mk_pathtpathinmatchFpath.Map.findpatht.fileswith|Some"<DIR>"|None->false|Some_->trueletrmtpath=letpath=mk_pathtpathinmatchFpath.Map.findpatht.fileswith|Some"<DIR>"->None|Some_->Some({twithfiles=Fpath.Map.removepatht.files},true)|None->ifis_dirtpaththenNoneelseSome(t,false)letsize_oftpath=letpath=mk_pathtpathinmatchFpath.Map.findpatht.fileswith|None->None|Some"<DIR>"->Some0|Somef->Some(String.lengthf)letset_vartc=function|None->{twithenv=String.Map.removect.env}|Somev->{twithenv=String.Map.addcvt.env}letget_vartc=String.Map.findct.envendleterror_msg=Rresult.R.error_msgftypeenv=Env.tletenv=Env.vtype'adomain={result:'aor_err;env:Env.t;logs:stringlist}letpp_or_errpp_a=Rresult.R.pp~error:Rresult.R.pp_msg~ok:pp_aleteq_or_erreq_a=Rresult.R.equal~error:(=)~ok:eq_aletpp_domainpp_a=letopenFmt.Dumpinrecord[field"result"(funt->t.result)(pp_or_errpp_a);field"env"(funt->t.env)Env.pp;field"logs"(funt->t.logs)Fmt.Dump.(liststring);]leteq_domaineqab=eq_or_erreqa.resultb.result&&Env.eqa.envb.env&&a.logs=b.logsletdomresultenvlogs={result;env;logs}letinterpret_dry_cmdenv{cmd;err;out;_}:stringdomain=Log.debug(funl->l"Run_cmd '%a'"Bos.Cmd.ppcmd);letlogx=Fmt.str"Run_cmd '%a' (%s)"Bos.Cmd.ppcmdxinmatchEnv.execenvcmdwith|None->dom(error_msg"'%a' not found"Bos.Cmd.ppcmd)env[log"error"]|Some(o,e)->pfoouto;pfoerre;dom(Oko)env[log"ok"]letinterpret_dry_cmd_clienvcmd:unitdomain=Log.debug(funl->l"Run_cmd_cli '%a'"Bos.Cmd.ppcmd);letlogx=Fmt.str"Run_cmd_cli '%a' (%s)"Bos.Cmd.ppcmdxinmatchEnv.execenvcmdwith|None->dom(error_msg"'%a' not found"Bos.Cmd.ppcmd)env[log"error"]|Some_->dom(Ok())env[log"ok"]letrecinterpret_dry:typer.env:Env.t->rcommand->rdomain=fun~env->function|Mkdirpath->(Log.debug(funl->l"Mkdir %a"Fpath.pppath);letlogs=Fmt.str"Mkdir %a (%s)"Fpath.pppathsinmatchEnv.mkdirenvpathwith|Some(env,true)->dom(Oktrue)env[log"created"]|Some(env,false)->dom(Okfalse)env[log"already exists"]|None->dom(error_msg"a file named '%a' already exists"Fpath.pppath)env[log"error"])|Rmdirpath->Log.debug(funl->l"Rmdir %a"Fpath.pppath);letlogs=Fmt.str"Rmdir %a (%s)"Fpath.pppathsinifEnv.is_direnvpath||Env.is_fileenvpaththendom(Ok())(Env.rmdirenvpath)[log"removed"]elsedom(Ok())env[log"no-op"]|Ls{root;filter}->(Log.debug(funl->l"Ls %a"Fpath.pproot);letlogsfmt=Fmt.kstr(Fmt.str"Ls %a (%s)"Fpath.pproot)fmtinmatchEnv.lsenvrootwith|None->dom(error_msg"%a: no such file or directory"Fpath.pproot)env[logs"error"]|Somees->(matchList.filterfiltereswith|([]|[_])ase->dom(Oke)env[logs"%d entry"(List.lengthe)]|es->dom(Okes)env[logs"%d entries"(List.lengthes)]))|Rmpath->(Log.debug(funl->l"Rm %a"Fpath.pppath);letlogs=Fmt.str"Rm %a (%s)"Fpath.pppathsinmatchEnv.rmenvpathwith|Some(env,b)->dom(Ok())env[log(ifbthen"removed"else"no-op")]|None->dom(error_msg"%a is a directory"Fpath.pppath)env[log"error"])|Is_filepath->Log.debug(funl->l"Is_file %a"Fpath.pppath);letr=Env.is_fileenvpathindom(Okr)env[Fmt.str"Is_file? %a -> %b"Fpath.pppathr]|Is_dirpath->Log.debug(funl->l"Is_dir %a"Fpath.pppath);letr=Env.is_direnvpathindom(Okr)env[Fmt.str"Is_dir? %a -> %b"Fpath.pppathr]|Size_ofpath->Log.debug(funl->l"Size_of %a"Fpath.pppath);letr=Env.size_ofenvpathindom(Okr)env[Fmt.str"Size_of %a -> %a"Fpath.pppathFmt.(option~none:(any"error")int)r;]|Run_cmdcmd->(letdomain=interpret_dry_cmdenvcmdinmatchdomain.resultwith|Ok_->{domainwithresult=Ok()}|Error_asr->{domainwithresult=r})|Run_cmd_outcmd->interpret_dry_cmdenvcmd|Run_cmd_clicmd->interpret_dry_cmd_clienvcmd|Write_file(path,s)->Log.debug(funl->l"Write_file %a"Fpath.pppath);dom(Ok())(Env.writeenvpaths)[Fmt.str"Write to %a (%d bytes)"Fpath.pppath(String.lengths)]|Read_filepath->(Log.debug(funl->l"Read_file %a"Fpath.pppath);matchEnv.readenvpathwith|None->letlog=Fmt.str"Read: %a"Fpath.pppathindom(error_msg"read_file: file does not exist")env[log]|Somer->letlog=Fmt.str"Read %a (%d bytes)"Fpath.pppath(String.lengthr)indom(Okr)env[log])|Tmp_file(_,pat)->Log.debug(funl->l"Tmp_file %s"Fmt.(strpat"*"));letr=Env.tmp_fileenvpatindom(Okr)env[Fmt.str"Tmp_file -> %a"Fpath.ppr]|Set_var(c,v)->Log.debug(funl->l"Set_var %s %a"cFmt.(option~none:(any"<none>")string)v);letenv=Env.set_varenvcvinletlog=Fmt.str"Set_var %s %a"cFmt.(option~none:(any"<unset>")string)vindom(Ok())env[log]|Get_varc->Log.debug(funl->l"Get_var %s"c);letv=Env.get_varenvcinletlog=Fmt.str"Get_var %s -> %a"cFmt.(option~none:(any"<not set>")string)vindom(Okv)env[log]|With_dir(dir,f)->Log.debug(funl->l"With_dir %a"Fpath.ppdir);letold=Env.pwdenvinletenv=Env.chdirenvdirinletdomain=dry_run~env(f())inletenv=Env.chdirdomain.envoldinletlog=Fmt.str"With_dir %a [%a]"Fpath.ppdirFmt.(vbox~indent:2(list~sep:(any"@,")string))domain.logsin{domainwithenv;logs=[log]}|Pwd->Log.debug(funl->l"Pwd");letr=Env.pwdenvindom(Okr)env[Fmt.str"Pwd -> %a"Fpath.ppr]|With_output{mode;path;purpose;contents;append}->letpp_appendppf()=ifappendthenFmt.stringppf"[append]"else()inLog.debug(funl->l"With_output%a %a (%s)"pp_append()Fpath.pppathpurpose);letbuf=Buffer.create0inletfmt=Format.formatter_of_bufferbufinletpp_modefmt=function|None->Format.fprintffmt"default"|Somen->Format.fprintffmt"%#o"ninletr=contentsfmtinFmt.pffmt"%!";letf=Buffer.contentsbufinletlog=Fmt.str"Write to %a (mode: %a, purpose: %s)"Fpath.pppathpp_modemodepurposeindom(Okr)(Env.writeenvpathf)[log]anddry_run:typer.env:Env.t->rt->rdomain=fun~envt->letrecgot~envlog=matchtwith|Doner->dom(Okr)envlog|Faile->dom(Error(`Msge))envlog|Run(cmd,k)->(letdomain=interpret_dry~envcmdinletnew_log=List.revdomain.logs@loginmatchdomain.resultwith|Okx->go(kx)~env:domain.envnew_log|Error_ase->domedomain.envnew_log)inletdomain=got~env[]in{domainwithlogs=List.revdomain.logs}letdry_run?(env=env())t=dry_run~envtletdry_run_trace?envt=letdomain=dry_run?envtinList.iterprint_endlinedomain.logsletgenerated_files?(env=env~exec:(fun_->None)())t=letdomain=dry_run~envtinEnv.diff_files~old:envdomain.envmoduleInfix=structlet(>>=)xf=bind~fxlet(>|=)xf=map~fxendmoduleSyntax=structopenInfixlet(let*)=(>>=)let(let+)=(>|=)endmoduleList=structopenInfixletiter~fl=List.fold_left(funacce->acc>>=fun()->fe)(ok())lletmap~fl=List.fold_left(funacce->acc>>=funacc->fe>|=fune->e::acc)(ok[])lend