/*------------------------------------------------------------------------------- zippkg.ado: Stata module to create ZIP archives of community-contributed content for offline distribution Copyright (C) 2019-2022 Daniel Bela (daniel.bela@lifbi.de) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -------------------------------------------------------------------------------*/ *! zippkg.ado: Stata module to create ZIP archives of community-contributed content for offline distribution *! Daniel Bela (daniel.bela@lifbi.de), Leibniz Institute for Educational Trajectories (LIfBi), Germany *! version 1.1 24 February 2022 - bugfix in handling relative ZIP file paths, so we don't create the archive inside of the temporary (and later: erased) directory *! version 1.0 26 March 2019 - initialized development version release program define zippkg , nclass // version requirements (and string functions dependent on Stata version) version 12 if (_caller()<14) { local substr_fcn substr local strpos_fcn strpos local word_fcn word local strlower_fcn strlower local strtrim_fcn strtrim } else { local substr_fcn usubstr local strpos_fcn ustrpos local word_fcn ustrword local strlower_fcn ustrlower local strtrim_fcn ustrtrim } // convert input syntax from ||-separator notation to ()-binding notation local pipepos=`strpos_fcn'(`"`macval(0)'"',`"||"') if (`pipepos'!=0) { local zero : copy local 0 local 0 while (`pipepos'!=0) { local before=`substr_fcn'(`"`macval(zero)'"',1,`pipepos'-1) local zero=`substr_fcn'(`"`macval(zero)'"',`pipepos'+2,.) local pipepos=`strpos_fcn'(`"`macval(zero)'"',`"||"') local nextpart=cond(`substr_fcn'(`word_fcn'(`"`macval(before)'"',1),1,1)==`","',`"`macval(before)'"',`"(`macval(before)')"') local 0 `macval(0)' `macval(nextpart)' if (`pipepos'==0) { local nextpart=cond(`substr_fcn'(`word_fcn'(`"`macval(zero)'"',1),1,1)==`","',`"`macval(zero)'"',`"(`macval(zero)')"') local 0 `macval(0)' `macval(nextpart)' } } } // parse syntax syntax anything(everything equalok id="package specification" name=pkgspecs) /// [ , FLat single SAVing(string asis) replace Verbose noTRACKfile From(string) all CHECKSUMs ] // list of possible global options local global_opts flat saving replace verbose trackfile from all checksums // list of possible pkgspec options local pkgspec_opts flat saving replace trackfile from all // list of possible pkgspec options that have to be applied per saved archive file local targetarchive_opts flat replace trackfile if (`"`verbose'"'==`"verbose"') display as result in smcl `"{text}parsing global options..."' // parse global options, set defaults foreach global_opt of local global_opts { if (!missing(`"``global_opt''"')) { local glob_`global_opt' ``global_opt'' if (`"`verbose'"'==`"verbose"') { if (inlist(`"`global_opt'"',`"saving"',`"from"')) display as result in smcl `"{tab}{text}...detected global option {result:`global_opt'(``global_opt'')}"' else display as result in smcl `"{tab}{text}...detected global option {result:`global_opt'}"' } } } if (missing(`"`glob_saving'"')) local glob_saving `"zippkg.zip"' if (!missing(`"`single'"')) local glob_saving `"zippkg\`speccounter'.zip"' if (missing(`"`glob_from'"')) local glob_from `"ssc"' // parse pkgspecs local speccounter 0 local targetarchivecounter 0 if (`strpos_fcn'(`"`macval(pkgspecs)'"',`"("')==0) local pkgspecs (`macval(pkgspecs)') while (!missing(`"`macval(pkgspecs)'"')) { gettoken pkgspec`++speccounter' pkgspecs : pkgspecs , match(parens) bind quotes if (`"`verbose'"'==`"verbose"') display as result in smcl `"{text}parsing {it:pkgspec} {result:`speccounter'}..."' local 0 `pkgspec`speccounter'' // concatenate options syntax namelist(min=1 name=pkgspec`speccounter'_pkglist id="package list") [ , FLat SAVing(string asis) replace noTRACKfile From(string) all ] if (`"`verbose'"'==`"verbose"') display as result in smcl `"{tab}{text}...list of packages to download is {result:`pkgspec`speccounter'_pkglist'}"' local pkgspec`speccounter'_installopts foreach pkgspec_opt of local pkgspec_opts { if (missing(`"``pkgspec_opt''"')) local pkgspec`speccounter'_`pkgspec_opt' : copy local glob_`pkgspec_opt' else { local pkgspec`speccounter'_`pkgspec_opt' : copy local `pkgspec_opt' if (`"`verbose'"'==`"verbose"') { if (inlist(`"`pkgspec_opt'"',`"saving"',`"from"')) display as result in smcl `"{tab}{text}...detected {it:pkgspec} option {result:`pkgspec_opt'(``pkgspec_opt'')}"' else display as result in smcl `"{tab}{text}...detected {it:pkgspec} option {result:`pkgspec_opt'}"' } } } if (`strlower_fcn'(`"`macval(pkgspec`speccounter'_from)'"')==`"ssc"') local pkgspec`speccounter'_installcmd ssc install else { local pkgspec`speccounter'_installcmd net install local pkgspec`speccounter'_installopts `macval(pkgspec`speccounter'_installopts)' from(`macval(pkgspec`speccounter'_from)') } if (!missing(`"`macval(pkgspec`speccounter'_all)'"')) local pkgspec`speccounter'_installopts `macval(pkgspec`speccounter'_installopts)' `macval(pkgspec`speccounter'_all)' if (!missing(`strtrim_fcn'(`"`macval(pkgspec`speccounter'_installopts)'"'))) local pkgspec`speccounter'_installopts `", `macval(pkgspec`speccounter'_installopts)'"' // determine target ZIP archive name if (`: list pkgspec`speccounter'_saving in targetarchives'==1) { local targetnum: list posof `"`pkgspec`speccounter'_saving'"' in targetarchives local targetarchive`targetnum'_pkgspecs : list targetarchive`targetnum'_pkgspecs | speccounter } else { local targetarchives : list targetarchives | pkgspec`speccounter'_saving local targetarchive`++targetarchivecounter'_pkgspecs `speccounter' foreach targetarchive_opt of local targetarchive_opts { if (!missing(`"`macval(targetarchive`targetarchivecounter'_`targetarchive_opt')'"')) { if (`"`macval(targetarchive`targetarchivecounter'_`targetarchive_opt')'"'!=`"`macval(pkgspec`speccounter'_`targetarchive_opt')'"') display as error `"option clash for archiving {input:`pkgspec`speccounter'_saving'}; will use {input:`pkgspec`speccounter'_`targetarchive_opt''}"' } else local targetarchive`targetarchivecounter'_zipopts `macval(targetarchive`targetarchivecounter'_zipopts)' `macval(pkgspec`speccounter'_`targetarchive_opt')' } } } // create archives if (`"`checksums'"'==`"checksums"') { local oldchecksumval `c(checksum)' if (`"`oldchecksumval'"'!=`"on"') set checksum on } forvalues num=1/`targetarchivecounter' { // target name local savename : word `num' of `targetarchives' if (`"`verbose'"'==`"verbose"') display as result in smcl `"{text}downloading material to be included in {result:`savename'}..."' // create temporary directory _create_tempsubdir `"`c(tmpdir)'"' local tmppath `s(tempdirfullpath)' if (`"`verbose'"'==`"verbose"') display as result in smcl `"{tab}{text}...created temporary directory {result:`tmppath'}"' // download stuff foreach specnum of local targetarchive`num'_pkgspecs { if (`"`verbose'"'==`"verbose"') display as result in smcl `"{tab}{text}...downloading package`=cond(`: word count `pkgspec`specnum'_pkglist''>1,`"s"',`""')' {result:`pkgspec`specnum'_pkglist'} (via {input:`pkgspec`specnum'_installcmd'} {it:pkg}{input:`pkgname'`pkgspec`specnum'_installopts'})"' foreach pkgname of local pkgspec`specnum'_pkglist { net set ado `"`tmppath'"' if (`"`pkgspec`specnum'_all'"'==`"all"') net set other `"`tmppath'"' capture : `pkgspec`specnum'_installcmd' `pkgname'`pkgspec`specnum'_installopts' net set ado PLUS if (`"`pkgspec`specnum'_all'"'==`"all"') net set other if (_rc!=0) { display as error in smcl `"package download failed, package `pkgname' will not be included in ZIP archive {it:`savename'}!"' if (`"`verbose'"'==`"verbose"') display as error in smcl `"{tab}used syntax was: {input}`pkgspec`specnum'_installcmd' `pkgname'`pkgspec`specnum'_installopts'"' } } } // create archive local oldpwd `"`c(pwd)'"' _make_absolute_path `"`savename'"' local savename_path `s(absolute_path)' capture : noisily _zipdir `"`tmppath'"' , saving(`"`savename_path'"') `targetarchive`targetarchivecounter'_zipopts' `verbose' if (_rc!=0) { quietly : cd `"`oldpwd'"' display as error in smcl `"creating ZIP archive file {input:`savename_path'} failed!"' if (`"`verbose'"'==`"verbose"') display as error in smcl `"{tab}used syntax was: {input}_zipdir `"`tmppath'"' , saving(`"`savename_path'"') `targetarchive`targetarchivecounter'_zipopts' `verbose'"' } // clean up if (`"`verbose'"'==`"verbose"') display as result in smcl `"{tab}{text}...erasing temporary directory {result:`tmppath'}"' _rm_dircontents `"`tmppath'"' rmdir `"`tmppath'"' } if (`"`checksums'"'==`"checksums"' & `"`oldchecksumval'"'!=`"on"') set checksum off // exit exit 0 end /* subroutine: create ZIP archive from a complete directory, descending by one level */ program define _zipdir , nclass syntax anything(everything name=parentdir id="directory name") , /// saving(string) [ flat replace noTRACKFILE verbose ] local parentdir `parentdir' if (`"`flat'"'==`"flat"') local archivetargets flattargets else { local archivebase `"`parentdir'"' local archivetargets allfiles } local allfiles : dir "`parentdir'" files "*" , respectcase if (`"`trackfile'"'==`"notrackfile"') { local allfiles : subinstr local allfiles `""stata.trk""' "" , all word } local subdirs : dir "`parentdir'" dirs "*" , respectcase foreach subdir of local subdirs { local subdirfiles : dir "`parentdir'/`subdir'" files "*" , respectcase foreach file of local subdirfiles { local newfile `""`subdir'/`file'""' local allfiles : list allfiles | newfile } } if (`"`flat'"'==`"flat"') { _create_tempsubdir `"`c(tmpdir)'"' local archivebase `s(tempdirfullpath)' foreach file of local allfiles { mata : st_local("filename",pathbasename(`"`file'"')) quietly : copy `"`parentdir'/`file'"' `"`archivebase'/`filename'"' local flattargets `"`flattargets' `"`filename'"'"' } } local oldpwd `"`c(pwd)'"' quietly : cd `"`archivebase'"' if (`"`verbose'"'==`"verbose"') { display as result in smcl `"{text}creating archive {result}`saving'{text}"' local q noisily } else local q quietly local mode create if (!missing(`"`replace'"')) { capture : confirm file `"`saving'"' if (_rc==0) { quietly : rm `"`saving'"' local mode replace } } `q' zipfile ``archivetargets'' , saving(`"`saving'"') quietly : cd `"`oldpwd'"' display as result in smcl `"{text}archive {result}`saving'{text} `mode'd"' if (`"`flat'"'==`"flat"') { _rm_dircontents `"`archivebase'"' rmdir `"`archivebase'"' } exit 0 end /* subroutine: remove all files from a directory, descending by one level */ program define _rm_dircontents , nclass syntax anything(everything name=dirname id="directory name") local dirname `dirname' local subdirs : dir "`dirname'" dirs "*" , respectcase foreach subdir of local subdirs { local subdirfiles : dir "`dirname'/`subdir'" files "*" , respectcase foreach file of local subdirfiles { rm `"`dirname'/`subdir'/`file'"' } rmdir `"`dirname'/`subdir'"' } local dirfiles : dir "`dirname'" files "*" , respectcase foreach file of local dirfiles { rm `"`dirname'/`file'"' } exit 0 end /* subroutine: create a temporary subdirectory not yet existent */ program define _create_tempsubdir , sclass syntax anything(everything name=parent id="parent directory") local parent `parent' local counter 0 tempname subdirname capture : mkdir `"`parent'/`subdirname'"' while (_rc!=0) { if (`++counter'>1000) { display as error in smcl `"Error: could not create random subdirectory inside of {it:`parent'} in 1000 tries. Is the parent directory corretly specified?"' exit 693 } tempname subdirname capture : mkdir `"`parent'/`subdirname'"' } sreturn local tempdirname `"`subdirname'"' sreturn local tempdirfullpath `"`parent'/`subdirname'"' exit 0 end /* subroutine: convert a given, possibly relative, filepath to an absolute filepath */ program define _make_absolute_path , sclass syntax anything(everything name=dirname id="directory name") local dirname `dirname' mata: st_local("absolute_path",pathresolve(`"`c(pwd)'"',`"`dirname'"')) sreturn local absolute_path `"`absolute_path'"' end // EOF