123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542(*---------------------------------------------------------------------------
Copyright (c) 2018 The odig programmers. All rights reserved.
SPDX-License-Identifier: ISC
---------------------------------------------------------------------------*)openB0_stdopenResult.SyntaxmoduleDigest=structincludeDigestletppppfd=Format.pp_print_stringppf(to_hexd)letpp_optppf=function|None->Fmt.stringppf"--------------------------------"|Somed->ppppfdmoduleSet=Set.Make(Digest)moduleMap=Map.Make(Digest)endmodulePkg=structtypename=stringtypet=name*Fpath.tletname=fstletpath=sndletppppf(n,p)=Fmt.pfppf"%s %a"n(Fmt.st'[`Faint]Fpath.pp_quoted)pletpp_nameppf(n,p)=Fmt.stringppfnletpp_versionppfv=letv=ifv=""then"?"elsevinFmt.pfppf"%a"(Fmt.st[`Fg`Green])vletequal=(=)letcompare=compareletcompare_by_caseless_namepp'=letnp=String.Ascii.lowercase(namep)inString.compare(np)(np')moduleT=structtypenonrect=tletcompare=compareendmoduleSet=Set.Make(T)moduleMap=Map.Make(T)letof_dirdir=Log.time(fun_m->m"package list of %a"Fpath.pp_quoteddir)@@fun()->letocaml_pkg()=letocaml_where=Cmd.(arg"ocamlc"%"-where")inletp=Os.Cmd.run_out~trim:trueocaml_where|>Result.error_to_failurein"ocaml",Fpath.of_stringp|>Result.error_to_failureintryletadd_pkg_namediracc=ifname<>"ocaml"then(name,dir)::accelseaccinletpkgs=Os.Dir.fold_dirs~recurse:falseadd_pkgdir[]inletpkgs=pkgs|>Result.error_to_failureinList.sortcompare_by_caseless_name(ocaml_pkg()::pkgs)withFailuree->Log.err(funm->m"package list: %s"e);[]letby_names?(init=String.Map.empty)pkgs=letadd_pkgacc(n,_aspkg)=String.Map.addnpkgaccinList.fold_leftadd_pkginitpkgsendmoduleDoc_cobj=structtypekind=Cmi|Cmti|Cmttypet={path:Fpath.t;kind:kind;modname:string;hidden:bool;pkg:Pkg.t;}letpathcobj=cobj.pathletkindcobj=cobj.kindletmodnamecobj=cobj.modnameletpkgcobj=cobj.pkglethiddencobj=cobj.hiddenletdon't_listcobj=hiddencobj||String.includes~affix:"__"(modnamecobj)letadd_cobjpkg__pathacc=tryletmulti=truein(* implies e.g .p.ext objects are not considered *)letbase,kind=matchFpath.cut_ext~multipathwith|base,".cmi"->base,Cmi|base,".cmti"->base,Cmti|base,".cmt"->base,Cmt|base,_->raise_notraceExitinletmodname=String.Ascii.capitalize(Fpath.basenamebase)inletcobj=matchFpath.Map.findbaseaccwith|exceptionNot_found->lethidden=not(kind=Cmi)in{path;kind;modname;hidden;pkg;}|cobj'->matchcobj'.kind,kindwith|Cmi,(Cmti|Cmt)->{path;kind;modname;hidden=false;pkg;}|(Cmti|Cmt),Cmi->{cobj'withhidden=false}|Cmt,Cmti->{path;kind;modname;hidden=cobj'.hidden;pkg}|Cmti,Cmt|_->cobj'inFpath.Map.addbasecobjaccwithExit->accletof_pkgpkg=letdir=Pkg.pathpkginletrecurse=trueinletcobjs=Os.Dir.fold_files~recurse(add_cobjpkg)dirFpath.Map.emptyinletcobjs=Log.if_error~use:Fpath.Map.emptycobjsinFpath.Map.fold(fun_cacc->c::acc)cobjs[]letby_modname?(init=String.Map.empty)cobjs=letaddacccobj=matchString.Map.findcobj.modnameaccwith|exceptionNot_found->String.Map.addcobj.modname[cobj]acc|cobjs->String.Map.addcobj.modname(cobj::cobjs)accinList.fold_leftaddinitcobjsendmoduleOpam=struct(* opam metadata *)typet=(string*string)listletunescapes=s(* TODO *)letparse_string=function|""->("","")|s->matchString.indexs'"'with|exceptionNot_found->(s,"")|i->letstart=i+1inletrecfind_endi=matchString.index_fromsi'"'with|exceptionNot_found->(s,"")(* unreported error ... *)|jwhens.[j-1]='\\'->find_end(j+1)|j->letstop=j-1inletstr=String.subsstart(stop-start+1)inletrest=String.subs(j+1)(String.lengths-(j+1))in(unescapestr,rest)infind_endstartletparse_lists=ifs=""then[]elseletrecloopaccs=lets,rest=parse_stringsinletrest=String.trimrestinifrest=""||rest="]"thenList.rev(s::acc)elseloop(s::acc)restinloop[]sletstring_fieldffields=matchList.assocffieldswith|exceptionNot_found->""|v->fst@@parse_stringvletlist_field?(sort=true)ffields=matchList.assocffieldswith|exceptionNot_found->[]|vwhensort->List.sortcompare(parse_listv)|v->parse_listvletauthors=list_field"authors"letbug_reports=list_field"bug-reports"letdependsfs=matchList.assoc"depends"fswith|exceptionNot_found->[]|v->letdelete_constraintss=letrecloops=matchString.indexs'{'with|exceptionNot_found->s|i->matchString.indexs'}'with|exceptionNot_found->s|j->loop(String.subs0i)^loop(String.subs(j+1)(String.lengths-(j+1)))inloopsinList.sortcompare@@parse_list(delete_constraintsv)letdev_repo=list_field"dev-repo"letdoc=list_field"doc"lethomepage=list_field"homepage"letlicense=list_field"license"letmaintainer=list_field"maintainer"letsynopsis=string_field"synopsis"lettagsfs=List.rev_mapString.Ascii.lowercase@@list_field"tags"fsletversion=string_field"version"(* Queries *)letfilepkg=letopam=Fpath.(Pkg.pathpkg/"opam")inmatchOs.File.existsopam|>Log.if_error~use:falsewith|true->Someopam|false->Noneletbin=lazybeginletopenResult.Syntaxinlet*opam=Os.Cmd.get(Cmd.arg"opam")inlet*v=Os.Cmd.run_out~trim:trueCmd.(opam%"--version")inmatchString.split_first~sep:"."(String.trimv)with|Some(maj,_)whenmaj<>""&&Char.codemaj.[0]-0x30>=2->Okopam|Some_|None->Fmt.error"%a: unsupported version %s"Cmd.ppopamvendletfields=["name:";"authors:";"bug-reports:";"depends:";"dev-repo:";"doc:";"homepage:";"license:";"maintainer:";"synopsis:";"tags:";"version:"]letfield_count=List.lengthfieldsletfield_arg=Fmt.str"--field=%s"(String.concat","fields)letrectake_fieldsnacclines=matchnwith|0->acc,lines|n->matchlineswith|[]->[],[](* unreported error... *)|l::ls->matchString.split_first~sep:":"lwith|None->[],[](* unreported error... *)|Some(f,v)->take_fields(n-1)((f,String.trimv)::acc)lsletrecparse_linesacc=function|[]->acc|name::lines->leterrl=Log.err(funm->m"%S: opam metadata expected name: field line"l)inmatchString.split_first~sep:":"namewith|Some("name",n)->letn,_=parse_stringninletfields,lines=take_fields(field_count-1)[]linesinparse_lines(String.Map.addnfieldsacc)lines|None|Some_->errname;accletqueryqpkgs=(* opam show (at least until v2.0.3) returns results in package
name order which is too easy to get confused about (we need to
precisely know how opam orders and apparently we do not). So we
also query for the name: field first and rebind the data to packages
after parsing. *)letpkgs=Pkg.Set.of_listqpkgsinletadd_opampacc=matchfilepwithNone->acc|Somef->f::accinletopams=Pkg.Set.foldadd_opampkgs[]inletno_datapkgs=List.map(funp->(p,[]))pkgsinmatchLazy.forcebinwith|Errore->Log.err(funm->m"%s"e);no_dataqpkgs|Okopam->ifopams=[]thenno_dataqpkgselseletshow=Cmd.(opam%"show"%"--normalise"%"--no-lint")inletshow=Cmd.(show%field_arg%%pathsopams)inmatchLog.time(fun_m->m"opam show")@@fun()->letstderr=`Stdo(Os.Cmd.out_null)inOs.Cmd.run_out~stderr~trim:trueshowwith|Errore->Log.err(funm->m"%s"e);no_dataqpkgs|Okout->letlines=String.split_all~sep:"\n"outinletinfos=parse_linesString.Map.emptylinesinletfind_infoisp=matchString.Map.find(Pkg.namep)iswith|exceptionNot_found->p,[]|i->p,iintryList.map(find_infoinfos)qpkgswith|Not_found->assertfalseendmoduleDoc_dir=struct(* Doc dir info *)typefiles={changes_files:Fpath.tlist;license_files:Fpath.tlist;readme_files:Fpath.tlist;}typet={dir:Fpath.toption;files:filesLazy.t;odoc_pages:Fpath.tlistLazy.t;odoc_assets_dir:Fpath.toptionLazy.t;odoc_assets:Fpath.tlistLazy.t;}letdoc_dir_filespkg_doc_dir=letcs,ls,rs=matchpkg_doc_dirwith|None->[],[],[]|Somedoc_dir->letadd_file_basefile(cs,ls,rsasacc)=letbase=String.uppercase_asciibaseinletis_preprefix=String.starts_with~prefixbaseinifis_pre"CHANGE"||is_pre"HISTORY"||is_pre"NEWS"then(file::cs),ls,rselseifis_pre"LICENSE"thencs,(file::ls),rselseifis_pre"README"thencs,ls,(file::rs)elseaccinOs.Dir.fold_files~recurse:falseadd_filedoc_dir([],[],[])|>Log.if_error~use:([],[],[])inletchanges_files=List.sortFpath.comparecsinletlicense_files=List.sortFpath.comparelsinletreadme_files=List.sortFpath.comparersin{changes_files;license_files;readme_files}letdoc_dir_subdir_filespkg_doc_dirsub~sat=matchpkg_doc_dirwith|None->[]|Somepkg_doc_dir->letdir=Fpath.(pkg_doc_dir/sub)inmatchOs.Dir.existsdirwith|Okfalse|Error_->[]|Oktrue->letadd_file=matchsatwith|None->fun__fileacc->file::acc|Somesat->fun__fileacc->ifsatfilethenfile::accelseaccinOs.Dir.fold_files~recurse:trueadd_filedir[]|>Log.if_error~use:[]letdoc_dir_odoc_pagespkg_doc_dir=letis_mld=Some(Fpath.has_ext".mld")indoc_dir_subdir_filespkg_doc_dir"odoc-pages"~sat:is_mldletdoc_dir_odoc_assetspkg_doc_dir=doc_dir_subdir_filespkg_doc_dir"odoc-assets"~sat:Noneletdoc_dir_odoc_assets_dirpkg_doc_dir=matchpkg_doc_dirwith|None->None|Somepkg_doc_dir->letdir=Fpath.(pkg_doc_dir/"odoc-assets")inmatchOs.Dir.existsdir|>Log.if_error~use:falsewith|false->None|true->Somedirletvpkg_doc_dir=letfiles=lazy(doc_dir_filespkg_doc_dir)inletodoc_pages=lazy(doc_dir_odoc_pagespkg_doc_dir)inletodoc_assets_dir=lazy(doc_dir_odoc_assets_dirpkg_doc_dir)inletodoc_assets=lazy(doc_dir_odoc_assetspkg_doc_dir)in{dir=pkg_doc_dir;files;odoc_pages;odoc_assets_dir;odoc_assets}letdiri=i.dirletchanges_filesi=(Lazy.forcei.files).changes_filesletlicense_filesi=(Lazy.forcei.files).license_filesletodoc_pagesi=Lazy.forcei.odoc_pagesletodoc_assets_diri=Lazy.forcei.odoc_assets_dirletodoc_assetsi=Lazy.forcei.odoc_assetsletreadme_filesi=(Lazy.forcei.files).readme_filesletof_pkg~doc_dirpkg=letdoc_dir=Fpath.(doc_dir/Pkg.namepkg)inmatchOs.Dir.existsdoc_dir|>Log.if_error~use:falsewith|true->v(Somedoc_dir)|false->vNoneendmodulePkg_info=structtypet={doc_cobjs:Doc_cobj.tlistLazy.t;opam:Opam.t;doc_dir:Doc_dir.tLazy.t}letdoc_cobjsi=Lazy.forcei.doc_cobjsletopami=i.opamletdoc_diri=Lazy.forcei.doc_dirtypefield=[`Authors|`Changes_files|`Doc_cobjs|`Depends|`Homepage|`Issues|`License|`License_files|`Maintainers|`Odoc_assets|`Odoc_pages|`Online_doc|`Readme_files|`Repo|`Synopsis|`Tags|`Version]letfield_names=["authors",`Authors;"changes-files",`Changes_files;"depends",`Depends;"doc-cobjs",`Doc_cobjs;"homepage",`Homepage;"issues",`Issues;"license",`License;"license-files",`License_files;"maintainers",`Maintainers;"odoc-assets",`Odoc_assets;"odoc-pages",`Odoc_pages;"online-doc",`Online_doc;"readme-files",`Readme_files;"repo",`Repo;"synopsis",`Synopsis;"tags",`Tags;"version",`Version;]letgetfieldi=letpathsps=List.mapFpath.to_stringpsinmatchfieldwith|`Authors->Opam.authors(opami)|`Changes_files->paths@@Doc_dir.changes_files(doc_diri)|`Depends->Opam.depends(opami)|`Doc_cobjs->paths@@List.mapDoc_cobj.path(doc_cobjsi)|`Homepage->Opam.homepage(opami)|`Issues->Opam.bug_reports(opami)|`License->Opam.license(opami)|`License_files->paths@@Doc_dir.license_files(doc_diri)|`Maintainers->Opam.maintainer(opami)|`Odoc_assets->paths@@Doc_dir.odoc_assets(doc_diri)|`Odoc_pages->paths@@Doc_dir.odoc_pages(doc_diri)|`Online_doc->Opam.doc(opami)|`Readme_files->paths@@Doc_dir.readme_files(doc_diri)|`Repo->Opam.dev_repo(opami)|`Synopsis->(matchOpam.synopsis(opami)with""->[]|s->[s])|`Tags->Opam.tags(opami)|`Version->(matchOpam.version(opami)with""->[]|s->[s])letppppfi=letpp_value=Fmt.(hvbox@@list~sep:spstring)inletpp_fieldppf(n,f)=Fmt.fieldn(getf)pp_valueppfiinletpp_fieldppfspec=Fmt.pfppf"| %a"pp_fieldspecinFmt.pfppf"@[<v>%a@]"(Fmt.listpp_field)field_names(* Queries *)letquery~doc_dirpkgs=letrecloopacc=function|[]->List.revacc|(p,opam)::ps->letdoc_cobjs=lazy(Doc_cobj.of_pkgp)inletdoc_dir=lazy(Doc_dir.of_pkg~doc_dirp)inloop((p,{doc_cobjs;opam;doc_dir})::acc)psinloop[](Opam.querypkgs)endmoduleEnv=structletb0_cache_dir="ODIG_B0_CACHE_DIR"letb0_log_file="ODIG_B0_LOG_FILE"letcache_dir="ODIG_CACHE_DIR"letdoc_dir="ODIG_DOC_DIR"letlib_dir="ODIG_LIB_DIR"letodoc_theme="ODIG_ODOC_THEME"letshare_dir="ODIG_SHARE_DIR"endmoduleConf=structtypet={b0_cache_dir:Fpath.t;b0_log_file:Fpath.t;cache_dir:Fpath.t;cwd:Fpath.t;doc_dir:Fpath.t;html_dir:Fpath.t;jobs:int;lib_dir:Fpath.t;memo:(B0_memo.t,string)resultLazy.t;odoc_theme:string;pkg_infos:Pkg_info.tPkg.Map.tLazy.t;pkgs:Pkg.tlistLazy.t;share_dir:Fpath.t;}letmemo~cwd~cache_dir(* b0 not odig *)~trash_dir~jobs=letfeedback=letop_howtoppfo=Fmt.pfppf"odig log --id %d"(B0_zero.Op.ido)inletoutput_op_level=Log.Debugandoutput_ui_level=Log.Infoinletlevel=Log.level()inB0_memo_cli.pp_leveled_feedback~op_howto~output_op_level~output_ui_level~levelFmt.stderrinB0_memo.make~cwd~cache_dir~trash_dir~jobs~feedback()letmake~b0_cache_dir~b0_log_file~cache_dir~cwd~doc_dir~html_dir~jobs~lib_dir~odoc_theme~share_dir()=lettrash_dir=Fpath.(b0_cache_dir/B0_memo_cli.trash_dirname)inletmemo=lazy(memo~cwd:cache_dir~cache_dir:b0_cache_dir~trash_dir~jobs)inletpkgs=lazy(Pkg.of_dirlib_dir)inletpkg_infos=Lazy.from_fun@@fun()->letaddacc(p,i)=Pkg.Map.addpiaccinletpkg_infos=Pkg_info.query~doc_dir(Lazy.forcepkgs)inList.fold_leftaddPkg.Map.emptypkg_infosin{b0_cache_dir;b0_log_file;cache_dir;cwd;doc_dir;html_dir;jobs;lib_dir;memo;odoc_theme;pkg_infos;pkgs;share_dir}letb0_cache_dirc=c.b0_cache_dirletb0_log_filec=c.b0_log_fileletcache_dirc=c.cache_dirletcwdc=c.cwdletdoc_dirc=c.doc_dirlethtml_dirc=c.html_dirletjobsc=c.jobsletlib_dirc=c.lib_dirletmemoc=Lazy.forcec.memoletodoc_themec=c.odoc_themeletpkg_infosc=Lazy.forcec.pkg_infosletpkgsc=Lazy.forcec.pkgsletshare_dirc=c.share_dirletpp=Fmt.record@@[Fmt.field"b0-cache-dir"b0_cache_dirFpath.pp_quoted;Fmt.field"b0-log-file"b0_log_fileFpath.pp_quoted;Fmt.field"cache-dir"cache_dirFpath.pp_quoted;Fmt.field"doc-dir"doc_dirFpath.pp_quoted;Fmt.field"lib-dir"lib_dirFpath.pp_quoted;Fmt.field"jobs"jobsFmt.int;Fmt.field"odoc-theme"odoc_themeFmt.string;Fmt.field"share-dir"share_dirFpath.pp_quoted;](* Setup *)letget_dir~cwd~execdefault_dir=function|Somedir->Fpath.(cwd//dir)|None->(* relocation hack find directory relative to executable path *)Fpath.((parent@@parent@@exec)//default_dir)letget_odoc_theme=function|Somev->Okv|None->let*n=B0_odoc.Theme.get_user_preference()inOk(Option.value~default:B0_odoc.Theme.odig_defaultn)letsetup_with_cli~b0_cache_dir~b0_log_file~cache_dir~doc_dir~jobs~lib_dir~odoc_theme~share_dir()=Result.map_error(Fmt.str"conf: %s")@@let*cwd=Os.Dir.cwd()inlet*exec=Fpath.of_stringSys.executable_nameinletcache_dir=get_dir~cwd~exec(Fpath.v"var/cache/odig")cache_dirinletb0_cache_dir=matchb0_cache_dirwith|None->Fpath.(cache_dir/B0_memo_cli.File_cache.dirname)|Somedir->Fpath.(cwd//dir)inletb0_log_file=matchb0_log_filewith|None->Fpath.(cache_dir/B0_memo_cli.Log.filename)|Somefile->Fpath.(cwd//file)inlethtml_dir=Fpath.(cache_dir/"html")inletlib_dir=get_dir~cwd~exec(Fpath.v"lib")lib_dirinletdoc_dir=get_dir~cwd~exec(Fpath.v"doc")doc_dirinletshare_dir=get_dir~cwd~exec(Fpath.v"share")share_dirinlet*odoc_theme=get_odoc_themeodoc_themeinletjobs=B0_memo_cli.get_jobs~jobsinOk(make~b0_cache_dir~b0_log_file~cache_dir~cwd~doc_dir~html_dir~jobs~lib_dir~odoc_theme~share_dir())end