1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528(**************************************************************************)(* *)(* Copyright 2012-2020 OCamlPro *)(* Copyright 2012 INRIA *)(* *)(* All rights reserved. This file is distributed under the terms of the *)(* GNU Lesser General Public License version 2.1, with the special *)(* exception on linking described in the file LICENSE. *)(* *)(**************************************************************************)openOpamCompattypeinstall_warning=[`Add_exe|`Install_dll|`Install_script|`Install_unknown|`Cygwin|`Cygwin_libraries]typeinstall_warning_fn=string->install_warning->unitexceptionProcess_errorofOpamProcess.resultexceptionInternal_errorofstringexceptionCommand_not_foundofstringexceptionFile_not_foundofstringexceptionPermission_deniedofstringletlog?levelfmt=OpamConsole.log"SYSTEM"?levelfmtletslog=OpamConsole.slogletinternal_errorfmt=Printf.ksprintf(funstr->log"error: %s"str;raise(Internal_errorstr))fmtletprocess_errorr=ifr.OpamProcess.r_signal=SomeSys.sigintthenraiseSys.Breakelseraise(Process_errorr)letraise_on_process_errorr=ifOpamProcess.is_failurerthenraise(Process_errorr)letcommand_not_foundcmd=raise(Command_not_foundcmd)letpermission_deniedcmd=raise(Permission_deniedcmd)moduleSys2=struct(* same as [Sys.is_directory] except for symlinks, which returns always [false]. *)letis_directoryfile=tryUnix.((lstatfile).st_kind=S_DIR)withUnix.Unix_error_ase->raise(Sys_error(Printexc.to_stringe))endletfile_or_symlink_existsf=tryignore(Unix.lstatf);truewithUnix.Unix_error(Unix.ENOENT,_,_)->falselet(/)=Filename.concatlettemp_basenameprefix=Printf.sprintf"%s-%d-%06x"prefix(OpamStubs.getpid())(Random.int0xFFFFFF)letrecmk_temp_dir?(prefix="opam")()=lets=Filename.get_temp_dir_name()/temp_basenameprefixinifSys.file_existssthenmk_temp_dir()elsesletsafe_mkdirdir=trylog"mkdir %s"dir;Unix.mkdirdir0o755withUnix.Unix_error(Unix.EEXIST,_,_)->()letmkdirdir=letrecauxdir=ifnot(Sys.file_existsdir)thenbeginaux(Filename.dirnamedir);safe_mkdirdir;endinauxdirletrm_command=ifSys.win32then"cmd /d /v:off /c rd /s /q"else"rm -rf"letremove_dirdir=log"rmdir %s"dir;ifSys.file_existsdirthen(leterr=Sys.command(Printf.sprintf"%s %s"rm_commanddir)iniferr<>0theninternal_error"Cannot remove %s (error %d)."direrr)lettemp_files=Hashtbl.create1024letlogs_cleaner=letto_clean=refOpamStd.String.Set.emptyinOpamStd.Sys.at_exit(fun()->OpamStd.String.Set.iter(funf->tryUnix.unlinkf;(* Only log the item if unlink succeeded *)log"logs_cleaner: rm: %s"fwithUnix.Unix_error_->())!to_clean;ifOpamCoreConfig.(!r.log_dir=default.log_dir)thentryUnix.rmdirOpamCoreConfig.(default.log_dir)withUnix.Unix_error_->());funtmp_dir->ifOpamCoreConfig.(!r.keep_log_dir)thento_clean:=OpamStd.String.Set.removetmp_dir!to_cleanelseto_clean:=OpamStd.String.Set.addtmp_dir!to_cleanletrectemp_file?(auto_clean=true)?dirprefix=lettemp_dir=matchdirwith|None->OpamCoreConfig.(!r.log_dir)|Somed->dinmkdirtemp_dir;letfile=temp_dir/temp_basenameprefixinifHashtbl.memtemp_filesfilethentemp_file~auto_clean?dirprefixelse(Hashtbl.addtemp_filesfiletrue;ifauto_cleanthenlogs_cleanerfile;file)letremove_filefile=iftryignore(Unix.lstatfile);truewithUnix.Unix_error_->falsethen(trylog"rm %s"file;Unix.unlinkfilewithUnix.Unix_error_ase->internal_error"Cannot remove %s (%s)."file(Printexc.to_stringe))letstring_of_channelic=letn=32768inlets=Bytes.createninletb=Buffer.create1024inletrecitericbs=letnread=tryinputics0nwithEnd_of_file->0inifnread>0then(Buffer.add_subbytesbs0nread;itericbs)initericbs;Buffer.contentsbletreadfile=letic=tryopen_in_binfilewithSys_error_->raise(File_not_foundfile)inUnix.lockf(Unix.descr_of_in_channelic)Unix.F_RLOCK0;lets=string_of_channelicinclose_inic;sletwritefilecontents=mkdir(Filename.dirnamefile);letoc=tryopen_out_binfilewithSys_error_->raise(File_not_foundfile)inUnix.lockf(Unix.descr_of_out_channeloc)Unix.F_LOCK0;output_stringoccontents;close_outocletsetup_copy?(chmod=funx->x)~src~dst()=letic=open_in_binsrcinletoc=tryletperm=(Unix.fstat(Unix.descr_of_in_channelic)).st_perm|>chmodinopen_out_gen[Open_wronly;Open_creat;Open_trunc;Open_binary]permdstwithexn->OpamStd.Exn.finaliseexn(fun()->close_inic)in(ic,oc)letcopy_channels=letbuf_len=4096inletbuf=Bytes.createbuf_leninletrecloopicoc=matchinputicbuf0buf_lenwith|0->()|n->outputocbuf0n;loopicocinloopletcopy_file_aux?chmod~src~dst()=letclose_channelsicoc=OpamStd.Exn.finally(fun()->close_inic)(fun()->close_outoc)intryletic,oc=setup_copy?chmod~src~dst()inOpamStd.Exn.finally(fun()->close_channelsicoc)(fun()->copy_channelsicoc);withUnix.Unix_error_ase->(* Remove the partial destination file, if any. *)(tryUnix.unlinkdstwithUnix.Unix_error_->());internal_error"Cannot copy %s to %s (%s)."srcdst(Printexc.to_stringe)letchdirdir=tryUnix.chdirdirwithUnix.Unix_error_->raise(File_not_founddir)letin_dirdirfn=letreset_cwd=letcwd=trySome(Sys.getcwd())withSys_error_->Noneinfun()->matchcwdwith|None->()|Somecwd->trychdircwdwithFile_not_found_->()inchdirdir;tryletr=fn()inreset_cwd();rwithe->OpamStd.Exn.finaliseereset_cwdletlistkinddir=tryin_dirdir(fun()->letd=Sys.readdir(Sys.getcwd())inletd=Array.to_listdinletl=List.filterkinddinList.map(Filename.concatdir)(List.sortcomparel))withFile_not_found_->[]letlsdir=list(fun_->true)dirletfiles_with_links=list(funf->trynot(Sys.is_directoryf)withSys_error_->false)letfiles_all_not_dir=list(funf->trynot(Sys2.is_directoryf)withSys_error_->false)letdirectories_strict=list(funf->trySys2.is_directoryfwithSys_error_->false)letdirectories_with_links=list(funf->trySys.is_directoryfwithSys_error_->false)letrec_filesdir=letrecauxaccudir=letd=directories_with_linksdirinletf=files_with_linksdirinList.fold_leftaux(f@accu)dinaux[]dirletfilesdir=files_with_linksdirletrec_dirsdir=letrecauxaccudir=letd=directories_with_linksdirinList.fold_leftaux(d@accu)dinaux[]dirletdirsdir=directories_with_linksdirletdir_is_emptydir=tryin_dirdir(fun()->Sys.readdir(Sys.getcwd())=[||])withFile_not_found_->falseletwith_tmp_dirfn=letdir=mk_temp_dir()intrymkdirdir;lete=fndirinremove_dirdir;ewithe->OpamStd.Exn.finalisee@@fun()->remove_dirdirletwith_tmp_dir_jobfjob=letdir=mk_temp_dir()inmkdirdir;OpamProcess.Job.finally(fun()->remove_dirdir)(fun()->fjobdir)letremovefile=if(trySys2.is_directoryfilewithSys_error_->false)thenremove_dirfileelseremove_filefile(* Sets path to s and returns the old path *)letgetchdirs=letp=trySys.getcwd()withSys_error_->letp=OpamCoreConfig.(!r.log_dir)inmkdirp;pinchdirs;pletnormalizes=trygetchdir(getchdirs)withFile_not_found_->sletreal_pathp=(* if Filename.is_relative p then *)match(trySome(Sys.is_directoryp)withSys_error_->None)with|None->letrecresolvedir=ifSys.file_existsdirthennormalizedirelseletparent=Filename.dirnamedirinifdir=parentthendirelseFilename.concat(resolveparent)(Filename.basenamedir)inletp=ifFilename.is_relativepthenFilename.concat(Sys.getcwd())pelsepinresolvep|Sometrue->normalizep|Somefalse->letdir=normalize(Filename.dirnamep)inmatchFilename.basenamepwith|"."->dir|base->dir/base(* else p *)typecommand=stringlistletdefault_env=Unix.environment()letenv_varenvvar=letlen=Array.lengthenvinletf=ifSys.win32thenString.uppercase_asciielsefunx->xinletprefix=fvar^"="inletpfxlen=String.lengthprefixinletrecauxi=ifi>=lenthen""elselets=env.(i)inifOpamStd.String.starts_with~prefix(fs)thenString.subspfxlen(String.lengths-pfxlen)elseaux(i+1)inaux0letforward_to_back=ifSys.win32thenString.map(function'/'->'\\'|c->c)elsefunx->xletback_to_forward=ifSys.win32thenString.map(function'\\'->'/'|c->c)elsefunx->x(* OCaml 4.05.0 no longer follows the updated PATH to resolve commands. This
makes unqualified commands absolute as a workaround. *)lett_resolve_command=letis_external_cmdname=letname=forward_to_backnameinOpamStd.String.contains_charnameFilename.dir_sep.[0]inletcheck_perms=ifSys.win32thenfunf->try(Unix.statf).Unix.st_kind=Unix.S_REGwithe->OpamStd.Exn.fatale;falseelsefunf->tryletopenUnixinletuid=geteuid()inletgroups=OpamStd.IntSet.of_list(getegid()::Array.to_list(getgroups()))inlet{st_uid;st_gid;st_perm;_}=statfinletmask=ifuid=st_uidthen0o100elseifOpamStd.IntSet.memst_gidgroupsthen0o010else0o001inif(st_permlandmask)<>0thentrueelsematchOpamACL.get_acl_executable_infofst_uidwith|None->false|Some[]->true|Somegids->OpamStd.IntSet.(not(is_empty(inter(of_listgids)groups)))withe->OpamStd.Exn.fatale;falseinletresolve?direnvname=ifnot(Filename.is_relativename)thenbegin(* absolute path *)ifnot(Sys.file_existsname)then`Not_foundelseifnot(check_permsname)then`Deniedelse`Cmdnameendelseifis_external_cmdnamethenbegin(* relative path *)letcmd=matchdirwith|None->name|Somed->Filename.concatdnameinifnot(Sys.file_existscmd)then`Not_foundelseifnot(check_permscmd)then`Deniedelse`Cmdcmdendelse(* bare command, lookup in PATH *)(* Following the shell sematics for looking up PATH, programs with the
expected name but not the right permissions are skipped silently.
Therefore, only two outcomes are possible in that case, [`Cmd ..] or
[`Not_found]. *)letpath=OpamStd.Sys.split_path_variable(env_varenv"PATH")inletname=ifSys.win32&¬(Filename.check_suffixname".exe")thenname^".exe"elsenameinletpossibles=OpamStd.List.filter_map(funpath->letcandidate=Filename.concatpathnameinifSys.file_existscandidatethenSomecandidateelseNone)pathinmatchList.findcheck_permspossibleswith|cmdname->`Cmdcmdname|exceptionNot_found->ifpossibles=[]then`Not_foundelse`Deniedinfun?(env=default_env)?dirname->resolveenv?dirnameletresolve_command?env?dirname=matcht_resolve_command?env?dirnamewith|`Cmdcmd->Somecmd|`Denied|`Not_found->Noneletapply_cygpathname=letr=OpamProcess.run(OpamProcess.command~name:(temp_file"command")~verbose:false"cygpath"[name])inOpamProcess.cleanup~force:truer;ifOpamProcess.is_successrthenList.hdr.OpamProcess.r_stdoutelseOpamConsole.error_and_exit`Internal_error"Could not apply cygpath to %s"nameletget_cygpath_function=ifSys.win32thenfun~command->lazy(ifOpamStd.(Option.map_defaultSys.is_cygwin_variant`Native(resolve_commandcommand))=`Cygwinthenapply_cygpathelsefunx->x)elseletf=Lazy.from_val(funx->x)infun~command:_->fletruns=ref[]letprint_stats()=match!runswith|[]->()|l->OpamConsole.msg"%d external processes called:\n%s"(List.lengthl)(OpamStd.Format.itemize~bullet:" "(String.concat" ")l)letlog_file?dirname=temp_file?dir(OpamStd.Option.default"log"name)letmake_command?verbose?(env=default_env)?name?text?metadata?allow_stdin?stdout?dir?(resolve_path=true)cmdargs=letname=log_filenameinletverbose=OpamStd.Option.defaultOpamCoreConfig.(!r.verbose_level>=2)verbosein(* Check that the command doesn't contain whitespaces *)ifNone<>trySome(String.indexcmd' ')withNot_found->NonethenOpamConsole.warning"Command %S contains space characters"cmd;letfull_cmd=ifresolve_paththent_resolve_command~env?dircmdelse`Cmdcmdinmatchfull_cmdwith|`Cmdcmd->OpamProcess.command~env~name?text~verbose?metadata?allow_stdin?stdout?dircmdargs|`Not_found->command_not_foundcmd|`Denied->permission_deniedcmdletrun_process?verbose?(env=default_env)~name?metadata?stdout?allow_stdincommand=letchrono=OpamConsole.timer()inruns:=command::!runs;matchcommandwith|[]->invalid_arg"run_process"|cmd::args->ifOpamStd.String.contains_charcmd' 'thenOpamConsole.warning"Command %S contains space characters"cmd;matcht_resolve_command~envcmdwith|`Cmdfull_cmd->letverbose=matchverbosewith|None->OpamCoreConfig.(!r.verbose_level)>=2|Someb->binletr=OpamProcess.run(OpamProcess.command~env~name~verbose?metadata?allow_stdin?stdoutfull_cmdargs)inletstr=String.concat" "(cmd::args)inlog~level:2"[%a] (in %.3fs) %s"(OpamConsole.slogFilename.basename)name(chrono())str;r|`Not_found->command_not_foundcmd|`Denied->permission_deniedcmdletcommand?verbose?env?name?metadata?allow_stdincmd=letname=log_filenameinletr=run_process?verbose?env~name?metadata?allow_stdincmdinOpamProcess.cleanupr;raise_on_process_errorrletcommands?verbose?env?name?metadata?(keep_going=false)commands=letname=log_filenameinletrun=run_process?verbose?env~name?metadatainletcommandr0c=matchr0,keep_goingwith|(`Error_|`Exception_),false->r0|_->letr1=tryletr=runcinifOpamProcess.is_successrthen`Successfulrelse`ErrorrwithCommand_not_found_ase->`Exceptioneinmatchr0with`Start|`Successful_->r1|_->r0inmatchList.fold_leftcommand`Startcommandswith|`Start->()|`Successfulr->OpamProcess.cleanupr|`Errore->process_errore|`Exceptione->raiseeletread_command_output?verbose?env?metadata?allow_stdincmd=letname=log_fileNoneinletr=run_process?verbose?env~name?metadata?allow_stdin~stdout:(name^".out")cmdinOpamProcess.cleanupr;raise_on_process_errorr;r.OpamProcess.r_stdoutletverbose_for_base_commands()=OpamCoreConfig.(!r.verbose_level)>=3letcygifyf=ifSys.win32thenList.map(Lazy.forcef)elsefunx->xletcopy_filesrcdst=if(trySys.is_directorysrcwithSys_error_->raise(File_not_foundsrc))theninternal_error"Cannot copy %s: it is a directory."src;if(trySys.is_directorydstwithSys_error_->false)theninternal_error"Cannot copy to %s: it is a directory."dst;iffile_or_symlink_existsdstthenremove_filedst;mkdir(Filename.dirnamedst);log"copy %s -> %s"srcdst;copy_file_aux~src~dst()letcopy_dirsrcdst=ifSys.file_existsdstthenifSys.is_directorydstthenmatchlssrcwith|[]->()|srcfiles->command~verbose:(verbose_for_base_commands())(["cp";"-PRp"]@srcfiles@[dst])elseinternal_error"Can not copy dir %s to %s, which is not a directory"srcdstelse(mkdir(Filename.dirnamedst);command~verbose:(verbose_for_base_commands())["cp";"-PRp";src;dst])letmv_auxfsrcdst=iffile_or_symlink_existsdstthenremove_filedst;mkdir(Filename.dirnamedst);command~verbose:(verbose_for_base_commands())("mv"::(cygifyf[src;dst]))letmv=mv_aux(get_cygpath_function~command:"mv")letis_execfile=letstat=Unix.statfileinstat.Unix.st_kind=Unix.S_REG&&stat.Unix.st_permland0o111<>0letfile_is_emptyf=Unix.((statf).st_size=0)letclassify_executablefile=letc=open_infilein(* On a 32-bit system, this could fail for a PE image with a 2GB+ DOS header =-o *)letinput_int_littlec=letb1=input_bytecinletb2=input_bytecinletb3=input_bytecinletb4=input_bytecinb1lor(b2lsl8)lor(b3lsl16)lor(b4lsl24)inletinput_short_littlec=letb1=input_bytecinletb2=input_bytecinb1lor(b2lsl8)inset_binary_mode_inctrue;trymatchreally_input_stringc2with"#!"->close_inc;`Script|"MZ"->letis_pe=try(* Offset to PE header at 0x3c (but we've already read two bytes) *)ignore(really_input_stringc0x3a);ignore(really_input_stringc(input_int_littlec-0x40));letmagic=really_input_stringc4inmagic="PE\000\000"withEnd_of_file->close_inc;falseinifis_pethentryletarch=(* NB It's not necessary to determine PE/PE+ headers for x64/x86 determination *)matchinput_short_littlecwith0x8664->`x86_64|0x14c->`x86|_->raiseEnd_of_fileinignore(really_input_stringc14);letsize_of_opt_header=input_short_littlecinletcharacteristics=input_short_littlecin(* Executable images must have a PE "optional" header and be marked executable *)(* Could also validate IMAGE_FILE_32BIT_MACHINE (0x100) for x86 and IMAGE_FILE_LARGE_ADDRESS_AWARE (0x20) for x64 *)ifsize_of_opt_header<=0||characteristicsland0x2=0thenraiseEnd_of_file;close_inc;ifcharacteristicsland0x2000<>0then`Dllarchelse`ExearchwithEnd_of_file->close_inc;`Unknownelse`Exe`i386|_->close_inc;`UnknownwithEnd_of_file->close_inc;`Unknownletdefault_install_warningdst=function|`Add_exe->OpamConsole.warning"Automatically adding .exe to %s"dst|`Install_dll->(* TODO Installation of .dll to bin is unfortunate, but not sure if it should be a warning *)()|`Install_script->(* TODO Generate a .cmd wrapper (and warn about it - they're not perfect) *)OpamConsole.warning"%s is a script; the command won't be available"dst;|`Install_unknown->(* TODO Installation of a non-executable file is unexpected, but not sure if it should be a warning/error *)()|`Cygwin->OpamConsole.warning"%s is a Cygwin-linked executable"dst|`Cygwin_libraries->OpamConsole.warning"%s links with a Cygwin-compiled DLL (almost certainly a packaging or environment error)"dstletinstall?(warning=default_install_warning)?execsrcdst=ifSys.is_directorysrctheninternal_error"Cannot install %s: it is a directory."src;if(trySys.is_directorydstwithSys_error_->false)theninternal_error"Cannot install to %s: it is a directory."dst;mkdir(Filename.dirnamedst);letexec=matchexecwith|Somee->e|None->is_execsrcinbeginifSys.win32thenifexecthenbeginlet(dst,cygcheck)=matchclassify_executablesrcwith`Exe_->ifnot(Filename.check_suffixdst".exe")&¬(Filename.check_suffixdst".dll")thenbeginwarningdst`Add_exe;(dst^".exe",true)endelse(dst,true)|`Dll_->warningdst`Install_dll;(dst,true)|`Script->warningdst`Install_script;(dst,false)|`Unknown->warningdst`Install_unknown;(dst,false)incopy_file_aux~src~dst();ifcygcheckthenmatchOpamStd.Sys.is_cygwin_variantdstwith`Native->()|`Cygwin->warningdst`Cygwin|`CygLinked->warningdst`Cygwin_librariesendelsecopy_file_aux~src~dst()else(letperm=ifexecthen0o755else0o644inlog"install %s -> %s (%o)"srcdstperm;copy_file_aux~chmod:(fun_->perm)~src~dst())endletcpu_count()=tryletans=letopenOpamStdinmatchSys.os()with|Sys.Win32->[Env.get"NUMBER_OF_PROCESSORS"]|Sys.FreeBSD->read_command_output~verbose:(verbose_for_base_commands())["sysctl";"-n";"hw.ncpu"]|_->read_command_output~verbose:(verbose_for_base_commands())["getconf";"_NPROCESSORS_ONLN"]inint_of_string(List.hdans)withNot_found|Process_error_|Failure_->1openOpamProcess.Job.OpmoduleTar=structtypeextract=|Bzip2|Gzip|Lzma|Xzletextract_command=function|Bzip2->"bzip2"|Gzip->"gzip"|Lzma->"lzma"|Xz->"xz"letextract_option=function|Bzip2->'j'|Gzip->'z'|Lzma->'Y'|Xz->'J'letextensions=[["tar.gz";"tgz"],Gzip;["tar.bz2";"tbz"],Bzip2;["tar.xz";"txz"],Xz;["tar.lzma";"tlz"],Lzma]letguess_typef=tryletic=open_infinletc1=input_charicinletc2=input_charicinclose_inic;matchc1,c2with|'\031','\139'->SomeGzip|'B','Z'->SomeBzip2|'\xfd','\x37'->SomeXz|'\x5d','\x00'->SomeLzma|_->NonewithSys_error_->Noneletmatch_extfileext=List.exists(Filename.check_suffixfile)extletget_typefile=letext=List.fold_left(funacc(ext,t)->matchaccwith|Somet->Somet|None->ifmatch_extfileextthenSometelseNone)Noneextensionsinmatchextwith|Somet->Somet|None->matchguess_typefilewith|Somet->Somet|_->Noneletis_archivefile=get_typefile<>Noneletcheck_extractfile=OpamStd.Option.Op.(get_typefile>>=funtyp->letcmd=extract_commandtypinletres=resolve_commandcmd<>NoneinifnotresthenSome(Printf.sprintf"Tar needs %s to extract the archive"cmd)elseNone)lettar_cmd=lazy(matchOpamStd.Sys.os()with|OpamStd.Sys.OpenBSD->"gtar"|_->"tar")letcygpath_tar=lazy(Lazy.force(get_cygpath_function~command:(Lazy.forcetar_cmd)))letextract_command=funfile->OpamStd.Option.Op.(get_typefile>>|funtyp->letf=Lazy.forcecygpath_tarinlettar_cmd=Lazy.forcetar_cmdinletcommandcdir=make_commandtar_cmd[Printf.sprintf"xf%c"c;ffile;"-C";fdir]incommand(extract_optiontyp))letcompress_command=funfiledir->letf=Lazy.forcecygpath_tarinlettar_cmd=Lazy.forcetar_cmdinmake_commandtar_cmd["cfz";ffile;"-C";f(Filename.dirnamedir);f(Filename.basenamedir)]endmoduleZip=structletis_archivef=tryletic=open_infinletc1=input_charicinletc2=input_charicinletc3=input_charicinletc4=input_charicinclose_inic;matchc1,c2,c3,c4with|'\x50','\x4b','\x03','\x04'->true|_->falsewithSys_error_|End_of_file->falseletextract_commandfile=Some(fundir->make_command"unzip"[file;"-d";dir])endletis_archivefile=Tar.is_archivefile||Zip.is_archivefileletextract_commandfile=ifZip.is_archivefilethenZip.extract_commandfileelseTar.extract_commandfileletmake_tar_gz_job~dirfile=lettmpfile=file^".tmp"inremove_filetmpfile;Tar.compress_commandtmpfiledir@@>funr->OpamProcess.cleanupr;ifOpamProcess.is_successrthen(mvtmpfilefile;DoneNone)else(remove_filetmpfile;Done(Some(Process_errorr)))letextract_job~dirfile=ifnot(Sys.file_existsfile)thenDone(Some(File_not_foundfile))elsewith_tmp_dir_job@@funtmp_dir->matchextract_commandfilewith|None->Done(Some(Failure("Unknown archive type: "^file)))|Somecmd->cmdtmp_dir@@>funr->ifnot(OpamProcess.is_successr)thenifZip.is_archivefilethenDone(Some(Process_errorr))elsematchTar.check_extractfilewith|None->Done(Some(Process_errorr))|Somes->Done(Some(Failures))elseiftrynot(Sys.is_directorydir)withSys_error_->falsetheninternal_error"Extracting the archive would overwrite %s."direlseletflist=OpamStd.Op.(files_all_not_dirtmp_dir|>List.filter(not@*OpamStd.String.contains~sub:"pax_global_header"))inmatchflistwith|[]->beginmatchdirectories_stricttmp_dirwith|[x]->(trymkdir(Filename.dirnamedir);copy_dirxdir;DoneNonewithe->OpamStd.Exn.fatale;Done(Somee))|_->internal_error"The archive %S contains multiple root directories."fileend|_->mkdir(Filename.dirnamedir);trycopy_dirtmp_dirdir;DoneNonewithe->OpamStd.Exn.fatale;Done(Somee)letextract~dirfile=matchOpamProcess.Job.run(extract_job~dirfile)with|Somee->raisee|None->()letextract_in_job~dirfile=OpamProcess.Job.catch(fune->Done(Somee))@@fun()->mkdirdir;matchextract_commandfilewith|None->internal_error"%s is not a valid tar or zip archive."file|Somecmd->cmddir@@>funr->ifnot(OpamProcess.is_successr)thenifZip.is_archivefilethenDone(Some(Process_errorr))elsematchTar.check_extractfilewith|None->Done(Some(Failure(Printf.sprintf"Failed to extract archive %s: %s"file(OpamProcess.result_summaryr))))|Somes->Done(Some(Failures))elseDoneNoneletextract_in~dirfile=matchOpamProcess.Job.run(extract_in_job~dirfile)with|Somee->raisee|None->()letlinksrcdst=mkdir(Filename.dirnamedst);iffile_or_symlink_existsdstthenremove_filedst;trylog"ln -s %s %s"srcdst;Unix.symlinksrcdstwithUnix.Unix_error(Unix.EXDEV,_,_)->(* Fall back to copy if symlinks are not supported *)letsrc=ifFilename.is_relativesrcthenFilename.dirnamedst/srcelsesrcinifSys.is_directorysrcthencopy_dirsrcdstelsecopy_filesrcdsttypeactual_lock_flag=[`Lock_read|`Lock_write]typelock_flag=[`Lock_none|actual_lock_flag]typelock={mutablefd:Unix.file_descroption;file:string;mutablekind:lock_flag;}exceptionLockedletunix_lock_op~dontblock=function|`Lock_read->ifdontblockthenUnix.F_TRLOCKelseUnix.F_RLOCK|`Lock_write->ifOpamCoreConfig.(!r.safe_mode)thenOpamConsole.error_and_exit`Locked"Write lock attempt in safe mode"elseifdontblockthenUnix.F_TLOCKelseUnix.F_LOCKletstring_of_lock_kind=function|`Lock_none->"none"|`Lock_read->"read"|`Lock_write->"write"letlocks=Hashtbl.create16letrelease_all_locks()=Hashtbl.iter(funfd_->Unix.closefd)locks;Hashtbl.clearlocksletrecflock_update:'a.([<lock_flag]as'a)->?dontblock:bool->lock->unit=funflag?(dontblock=OpamCoreConfig.(!r.safe_mode))lock->log"LOCK %s (%a => %a)"~level:2lock.file(slogstring_of_lock_kind)(lock.kind)(slogstring_of_lock_kind)flag;iflock.kind=(flag:>lock_flag)then()elsematchflag,lockwith|`Lock_none,{fd=Somefd;kind=(`Lock_read|`Lock_write);_}->Hashtbl.removelocksfd;Unix.closefd;(* implies Unix.lockf fd Unix.F_ULOCK 0 *)lock.kind<-(flag:>lock_flag);lock.fd<-None|(`Lock_read|`Lock_write),{fd=None;kind=`Lock_none;file}->letnew_lock=flockflag~dontblockfileinlock.kind<-(flag:>lock_flag);lock.fd<-new_lock.fd|`Lock_write,{fd=Somefd;file;kind=`Lock_read}->Unix.closefd;(* fd needs read-write reopen *)letnew_lock=flockflag~dontblockfileinlock.kind<-(flag:>lock_flag);lock.fd<-new_lock.fd|(`Lock_read|`Lock_write)asflag,{fd=Somefd;file;kind}->(* Write locks are not recursive on Windows, so only call lockf if necessary *)ifkind<>flagthen(try(* Locks can't be promoted (or demoted) on Windows - see PR#7264 *)ifSys.win32&&kind<>`Lock_nonethenUnix.(lockffdF_ULOCK0);Unix.lockffd(unix_lock_op~dontblock:trueflag)0withUnix.Unix_error(Unix.EAGAIN,_,_)->ifdontblockthenOpamConsole.error_and_exit`Locked"Another process has locked %s and non blocking mode enabled"file;OpamConsole.formatted_msg"Another process has locked %s, waiting (%s to abort)... "file(ifSys.win32then"CTRL+C"else"C-c");letreclock_w_ignore_sig()=tryUnix.lockffd(unix_lock_op~dontblock:falseflag)0;withSys.Breakase->(OpamConsole.msg"\n";raisee)|Unix.Unix_error(Unix.EINTR,_,_)->lock_w_ignore_sig()inlock_w_ignore_sig();OpamConsole.msg"lock acquired.\n");lock.kind<-(flag:>lock_flag)|_->assertfalseandflock:'a.([<lock_flag]as'a)->?dontblock:bool->string->lock=funflag?dontblockfile->matchflagwith|`Lock_none->{fd=None;file;kind=`Lock_none}|`Lock_writewhenOpamCoreConfig.(!r.safe_mode)->OpamConsole.error_and_exit`Locked"Write lock attempt in safe mode";|flag->mkdir(Filename.dirnamefile);letrdflag=if(flag:>lock_flag)=`Lock_writethenUnix.O_RDWRelseUnix.O_RDONLYinletfd=Unix.openfilefileUnix.([O_CREAT;O_CLOEXEC;rdflag])0o666inHashtbl.addlocksfd();letlock={fd=Somefd;file;kind=`Lock_none}inflock_updateflag?dontblocklock;lockletfunlocklock=flock_update`Lock_nonelockletget_lock_flaglock=lock.kindletget_lock_fdlock=matchlock.fdwithSomefd->fd|None->raiseNot_foundletlock_maxflag1flag2=matchflag1,flag2with|`Lock_write,_|_,`Lock_write->`Lock_write|`Lock_read,_|_,`Lock_read->`Lock_read|`Lock_none,`Lock_none->`Lock_noneletlock_none={fd=None;file="";kind=`Lock_none;}letlock_isatleastflaglock=lock_maxflaglock.kind=lock.kindletget_eol_encodingfile=letch=tryopen_in_binfilewithSys_error_->raise(File_not_foundfile)inlethas_crline=letlength=String.lengthlineinlength>0&&line.[length-1]='\r'inletlast_charch=seek_inch(in_channel_lengthch-1);input_charchinletrecread_linescrline=lethas_cr=has_crlineinmatchinput_linechwith|line->ifhas_cr=crthenread_linescrlineelsebeginclose_inch;Noneend|exceptionEnd_of_file->letresult=ifcr=has_crthenSomecrelseifcr&&last_charch<>'\n'thenSometrueelseNoneinclose_inch;resultinmatchinput_linechwith|line_one->lethas_cr=has_crline_oneinbeginmatchinput_linechwith|line_two->read_lineshas_crline_two|exceptionEnd_of_file->letresult=iflast_charch='\n'thenSomehas_crelseNoneinclose_inch;resultend|exceptionEnd_of_file->close_inch;Nonelettranslate_patch~dirorigcorrected=(* It's unnecessarily complicated to infer whether the entire file is CRLF
encoded and also the status of individual files, so accept scanning the
file three times instead of two. *)letlog?levelfmt=OpamConsole.log"PATCH"?levelfmtinletstrip_cr=get_eol_encodingorig=Sometrueinletch=tryopen_in_binorigwithSys_error_->raise(File_not_foundorig)in(* CRLF detection with patching can be more complicated than that used here,
especially in the presence of files with mixed LF/CRLF endings. The
processing done here aims to allow patching to succeed on files which are
wholly encoded CRLF or LF against patches which may have been translated to
be the opposite.
The resulting patch will *always* have LF line endings for the patch
metadata (headers, chunk locations, etc.) but uses either CRLF or LF
depending on the target file. Endings in the patch are always preserved for
new files. The benefit of always using LF endings for the metadata is that
patch's "Stripping trailing CRs from patch" behaviour won't be triggered.
There are various patch formats, though only the Unified and Context
formats allow multiple files to be patched. I tired of trying to get
sufficient documented detail of Context diffs to be able to parse them
without resorting to reverse-engineering code. It is unusual to see them
these days, so for now opam just emits a warning if a Context diff file is
encountered and does no processing to it.
There are various semantic aspects of Unified diffs which are not handled
(at least at present) by this function which are documented in the code
with the marker "Weakness". *)letprocess_chunk_headerresultline=matchOpamStd.String.splitline' 'with|"@@"::a::b::"@@"::_->(* Weakness: for a new file [a] should always be -0,0 (not checked) *)letl_a=String.lengthainletl_b=String.lengthbinifl_a>1&&l_b>1&&a.[0]='-'&&b.[0]='+'thentryletf(_,v)=int_of_stringvinletneg=OpamStd.String.cut_at(String.suba1(l_a-1))','|>OpamStd.Option.map_defaultf1inletpos=OpamStd.String.cut_at(String.subb1(l_b-1))','|>OpamStd.Option.map_defaultf1inresultnegposwithe->OpamStd.Exn.fatale;(* TODO Should display some kind of re-sync warning *)`Headerelse(* TODO Should display some kind of re-sync warning *)`Header|_->(* TODO Should display some kind of warning that there were no chunks *)`Headerinletprocess_state_transitionnext_statestatetransforms=match(state,next_state)with|(`Processing_,`Processing_)->transforms|(`Processing(_,target,crlf,patch_crlf,chunks,_),_)->letcompute_transformpatch_crlf=(* Emit the patch *)lettransform=match(crlf,patch_crlf)with|(None,_)|(_,None)->log~level:3"CRLF adaptation skipped for %s"target;None|(Somecrlf,Somepatch_crlf)->ifcrlf=patch_crlfthenbeginlog~level:3"No CRLF adaptation necessary for %s"target;Noneendelseifcrlfthenbeginlog~level:3"Adding \\r to patch chunks for %s"target;Sometrueendelsebeginlog~level:3"Stripping \\r to patch chunks for %s"target;Somefalseendinletrecord_transformtransform=letaugment_record(first_line,last_line)=(first_line,last_line,transform)inList.rev_append(List.rev_mapaugment_recordchunks)transformsinOpamStd.Option.map_defaultrecord_transformtransformstransforminOpamStd.Option.map_defaultcompute_transformtransformspatch_crlf|_->transformsinletrecfold_linesstatentransforms=matchinput_linechwith|line->letline=ifstrip_crthenString.subline0(String.lengthline-1)elselineinletlength=String.lengthlineinletnext_state=matchstatewith|`Header->beginmatch(iflength>4thenString.subline04else"")with|"--- "->(* Start of a unified diff header. *)letfile=letfile=String.subline4(length-4)inletopenOpamStdinOption.map_defaultfstfile(String.cut_atfile'\t')in(* Weakness: new files are also marked with a time-stamp at
the start of the epoch, however it's localised,
making it a bit tricky to identify! New files are
also identified by their absence on disk, so this
weakness isn't particularly critical. *)iffile="/dev/null"then`NewHeaderelselettarget=OpamStd.String.cut_at(back_to_forwardfile)'/'|>OpamStd.Option.map_defaultsndfile|>Filename.concatdirinifSys.file_existstargetthenletcrlf=get_eol_encodingtargetin`Patching(file,crlf)else`NewHeader|"*** "->OpamConsole.warning"File %s uses context diffs which are \
less portable; consider using unified \
diffs"orig;`SkipFile|_->(* Headers will contain other lines, which are ignored (e.g.
the diff command which generated the diff, or Git commit
messages) *)`Headerend|`NewHeader->if(iflength>4thenString.subline04else"")="+++ "then`Newelse(* TODO Should display some kind of re-sync warning *)`Header|`New->process_chunk_header(funnegpos->`NewChunk(neg,pos))line|`NewChunk(neg,pos)->(* Weakness: new files should only have + lines *)letneg=ifline=""||line.[0]=' '||line.[0]='-'thenneg-1elseneginletpos=ifline=""||line.[0]=' '||line.[0]='+'thenpos-1elseposinifneg=0&&pos=0then`Newelse(* Weakness: there should only be one chunk for a new file *)`NewChunk(neg,pos)|`Patching(orig,crlf)->if(iflength>4thenString.subline04else"")="+++ "thenletfile=letfile=String.subline4(length-4)inletopenOpamStdinOption.map_defaultfstfile(String.cut_atfile'\t')in`Processing(orig,file,crlf,None,[],`Head)else`Header|`Processing(orig,target,crlf,patch_crlf,chunks,`Head)->ifline="\\ No newline at end of file"then(* If the no eol-at-eof indicator is found, never add \r to
final chunk line *)letchunks=matchchunkswith|(a,b)::chunks->(a,b-1)::chunks|_->chunksin`Processing(orig,target,crlf,patch_crlf,chunks,`Head)elseprocess_chunk_header(funnegpos->`Processing(orig,target,crlf,patch_crlf,chunks,`Chunk(succn,neg,pos)))line|`Processing(orig,target,crlf,patch_crlf,chunks,`Chunk(first_line,neg,pos))->letneg=ifline=""||line.[0]=' '||line.[0]='-'thenneg-1elseneginletpos=ifline=""||line.[0]=' '||line.[0]='+'thenpos-1elseposinletpatch_crlf=lethas_cr=(length>0&&line.[length-1]='\r')inmatchpatch_crlfwith|None->Some(Somehas_cr)|Some(Somethink_cr)whenthink_cr<>has_cr->log~level:2"Patch adaptation disabled for %s: \
mixed endings or binary file"target;SomeNone|_->patch_crlfinifneg=0&&pos=0thenletchunks=(first_line,n)::chunksin`Processing(orig,target,crlf,patch_crlf,chunks,`Head)else`Processing(orig,target,crlf,patch_crlf,chunks,`Chunk(first_line,neg,pos))|`SkipFile->`SkipFileinifnext_state=`SkipFilethen[]elseprocess_state_transitionnext_statestatetransforms|>fold_linesnext_state(succn)|exceptionEnd_of_file->process_state_transition`Headerstatetransforms|>List.revinlettransforms=fold_lines`Header1[]iniftransforms=[]thencopy_fileorigcorrectedelsebeginseek_inch0;letch_out=tryopen_out_bincorrectedwithSys_error_->close_inch;raise(File_not_foundcorrected)inlet(normal,add_cr,strip_cr)=letstripns=String.subs0(String.lengths-n)inletidx=xinifstrip_crthen(strip1,id,strip2)else(id,(funs->s^"\r"),strip1)inifOpamConsole.debug()thenletlog_transform(first_line,last_line,add_cr)=letindicator=ifadd_crthen'+'else'-'inlog~level:3"Transform %d-%d %c\\r"first_linelast_lineindicatorinList.iterlog_transformtransforms;letrecfold_linesntransforms=matchinput_linechwith|line->let(f,transforms)=matchtransformswith|(first_line,last_line,add_cr_to_chunks)::next_transforms->lettransforms=ifn=last_linethennext_transformselsetransformsinletf=ifn>=first_linethenifadd_cr_to_chunksthenadd_crelsestrip_crelsenormalin(f,transforms)|[]->(normal,[])inoutput_stringch_out(fline);output_charch_out'\n';fold_lines(succn)transforms|exceptionEnd_of_file->close_outch_outinfold_lines1transformsend;close_inchletpatch?(preprocess=true)~dirp=ifnot(Sys.file_existsp)then(OpamConsole.error"Patch file %S not found."p;raiseNot_found);letp'=ifpreprocessthenletp'=temp_file~auto_clean:false"processed-patch"intranslate_patch~dirpp';p'elsepinletpatch_cmd=matchOpamStd.Sys.os()with|OpamStd.Sys.OpenBSD|OpamStd.Sys.FreeBSD->"gpatch"|_->"patch"inmake_command~name:"patch"~dirpatch_cmd["-p1";"-i";p']@@>funr->ifnot(OpamConsole.debug())thenSys.removep';ifOpamProcess.is_successrthenDoneNoneelseDone(Some(Process_errorr))letregister_printer()=Printexc.register_printer(function|Process_errorr->Some(OpamProcess.result_summaryr)|Internal_errorm->Somem|Command_not_foundc->Some(Printf.sprintf"%S: command not found."c)|Permission_deniedc->Some(Printf.sprintf"%S: permission denied."c)|Sys.Break->Some"User interruption"|Unix.Unix_error(e,fn,msg)->letmsg=ifmsg=""then""else" on "^msginleterror=Printf.sprintf"%s: %S failed%s: %s"Sys.executable_namefnmsg(Unix.error_messagee)inSomeerror|_->None)letinit()=register_printer();Sys.catch_breaktrue;trySys.set_signalSys.sigpipe(Sys.Signal_handle(fun_->()))withInvalid_argument_->()let()=OpamProcess.set_resolve_commandresolve_command